Compare commits
	
		
			2 Commits
		
	
	
		
			master
			...
			test-actio
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					e14f0700cf | ||
| 
						 | 
					814b2bb15f | 
							
								
								
									
										48
									
								
								.env.sample
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								.env.sample
									
									
									
									
									
								
							@ -1,48 +0,0 @@
 | 
			
		||||
# 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
 | 
			
		||||
							
								
								
									
										19
									
								
								.gitea/workflows/demo.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								.gitea/workflows/demo.yaml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
name: Gitea Actions Demo
 | 
			
		||||
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
 | 
			
		||||
on: [push]
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  Explore-Gitea-Actions:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - run: echo "The job was automatically triggered by a ${{ gitea.event_name }} event."
 | 
			
		||||
      - run: echo "This job is now running on a ${{ runner.os }} server hosted by Gitea!"
 | 
			
		||||
      - run: echo "The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
 | 
			
		||||
      - name: Check out repository code
 | 
			
		||||
        uses: https://github.com/actions/checkout@v3
 | 
			
		||||
      - run: echo "The ${{ gitea.repository }} repository has been cloned to the runner."
 | 
			
		||||
      - run: echo "The workflow is now ready to test your code on the runner."
 | 
			
		||||
      - name: List files in the repository
 | 
			
		||||
        run: |
 | 
			
		||||
          ls ${{ gitea.workspace }}
 | 
			
		||||
      - run: echo "This job's status is ${{ job.status }}."
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -3,4 +3,3 @@
 | 
			
		||||
/.idea/
 | 
			
		||||
/.coverage
 | 
			
		||||
/htmlcov/
 | 
			
		||||
/.env
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										15
									
								
								api/admin.py
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								api/admin.py
									
									
									
									
									
								
							@ -1,6 +1,6 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
from api.models.token import APIUserToken, OAuthToken
 | 
			
		||||
from api.models.token import APIUserToken
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class APITokenAdmin(admin.ModelAdmin):
 | 
			
		||||
@ -17,17 +17,4 @@ 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(OAuthToken, OAuthTokenAdmin)
 | 
			
		||||
 | 
			
		||||
@ -1,26 +0,0 @@
 | 
			
		||||
# 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,
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -1,14 +1,7 @@
 | 
			
		||||
import json
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
from django.core.exceptions import ObjectDoesNotExist
 | 
			
		||||
from django.db import models
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -51,129 +44,5 @@ class APIUserToken(models.Model):
 | 
			
		||||
            if token_obj.valid_until is not None and token_obj.valid_until < _today:
 | 
			
		||||
                raise PermissionError("Token validity expired")
 | 
			
		||||
        except ObjectDoesNotExist:
 | 
			
		||||
            raise PermissionError("Token unknown")
 | 
			
		||||
            raise PermissionError("Credentials invalid")
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@ from django.contrib.gis import geos
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
 | 
			
		||||
from api.tests.v1.share.test_api_sharing import BaseAPIV1TestCase
 | 
			
		||||
from konova.models import Geometry
 | 
			
		||||
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class APIV1UpdateTestCase(BaseAPIV1TestCase):
 | 
			
		||||
@ -64,7 +64,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
 | 
			
		||||
 | 
			
		||||
        put_props = put_body["properties"]
 | 
			
		||||
        put_geom = geos.fromstr(json.dumps(put_body))
 | 
			
		||||
        put_geom = Geometry.cast_to_rlp_srid(put_geom)
 | 
			
		||||
        put_geom.transform(DEFAULT_SRID_RLP)
 | 
			
		||||
        self.assertEqual(put_geom, self.intervention.geometry.geom)
 | 
			
		||||
        self.assertEqual(put_props["title"], self.intervention.title)
 | 
			
		||||
        self.assertNotEqual(modified_on, self.intervention.modified)
 | 
			
		||||
@ -94,7 +94,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
 | 
			
		||||
 | 
			
		||||
        put_props = put_body["properties"]
 | 
			
		||||
        put_geom = geos.fromstr(json.dumps(put_body))
 | 
			
		||||
        put_geom = Geometry.cast_to_rlp_srid(put_geom)
 | 
			
		||||
        put_geom.transform(DEFAULT_SRID_RLP)
 | 
			
		||||
        self.assertEqual(put_geom, self.compensation.geometry.geom)
 | 
			
		||||
        self.assertEqual(put_props["title"], self.compensation.title)
 | 
			
		||||
        self.assertNotEqual(modified_on, self.compensation.modified)
 | 
			
		||||
@ -124,7 +124,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
 | 
			
		||||
 | 
			
		||||
        put_props = put_body["properties"]
 | 
			
		||||
        put_geom = geos.fromstr(json.dumps(put_body))
 | 
			
		||||
        put_geom = Geometry.cast_to_rlp_srid(put_geom)
 | 
			
		||||
        put_geom.transform(DEFAULT_SRID_RLP)
 | 
			
		||||
        self.assertEqual(put_geom, self.eco_account.geometry.geom)
 | 
			
		||||
        self.assertEqual(put_props["title"], self.eco_account.title)
 | 
			
		||||
        self.assertNotEqual(modified_on, self.eco_account.modified)
 | 
			
		||||
@ -156,7 +156,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
 | 
			
		||||
 | 
			
		||||
        put_props = put_body["properties"]
 | 
			
		||||
        put_geom = geos.fromstr(json.dumps(put_body))
 | 
			
		||||
        put_geom = Geometry.cast_to_rlp_srid(put_geom)
 | 
			
		||||
        put_geom.transform(DEFAULT_SRID_RLP)
 | 
			
		||||
        self.assertEqual(put_geom, self.ema.geometry.geom)
 | 
			
		||||
        self.assertEqual(put_props["title"], self.ema.title)
 | 
			
		||||
        self.assertNotEqual(modified_on, self.ema.modified)
 | 
			
		||||
 | 
			
		||||
@ -11,9 +11,8 @@ from abc import abstractmethod
 | 
			
		||||
from django.contrib.gis import geos
 | 
			
		||||
from django.contrib.gis.geos import GEOSGeometry
 | 
			
		||||
from django.core.paginator import Paginator
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
 | 
			
		||||
from konova.models import Geometry
 | 
			
		||||
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
 | 
			
		||||
from konova.utils.message_templates import DATA_UNSHARED
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -33,8 +32,8 @@ class AbstractModelAPISerializer:
 | 
			
		||||
        self.lookup = {
 | 
			
		||||
            "id": None,  # must be set
 | 
			
		||||
            "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)
 | 
			
		||||
 | 
			
		||||
    @abstractmethod
 | 
			
		||||
@ -77,11 +76,7 @@ class AbstractModelAPISerializer:
 | 
			
		||||
        else:
 | 
			
		||||
            # Return certain object
 | 
			
		||||
            self.lookup["id"] = _id
 | 
			
		||||
 | 
			
		||||
        self.shared_lookup = Q(
 | 
			
		||||
            Q(users__in=[user]) |
 | 
			
		||||
            Q(teams__in=list(user.shared_teams))
 | 
			
		||||
        )
 | 
			
		||||
        self.lookup["users__in"] = [user]
 | 
			
		||||
 | 
			
		||||
    def fetch_and_serialize(self):
 | 
			
		||||
        """ Serializes the model entry according to the given lookup data
 | 
			
		||||
@ -91,13 +86,7 @@ class AbstractModelAPISerializer:
 | 
			
		||||
        Returns:
 | 
			
		||||
            serialized_data (dict)
 | 
			
		||||
        """
 | 
			
		||||
        entries = self.model.objects.filter(
 | 
			
		||||
            **self.lookup
 | 
			
		||||
        ).filter(
 | 
			
		||||
            self.shared_lookup
 | 
			
		||||
        ).order_by(
 | 
			
		||||
            "id"
 | 
			
		||||
        ).distinct()
 | 
			
		||||
        entries = self.model.objects.filter(**self.lookup).order_by("id")
 | 
			
		||||
        self.paginator = Paginator(entries, self.rpp)
 | 
			
		||||
        requested_entries = self.paginator.page(self.page_number)
 | 
			
		||||
 | 
			
		||||
@ -145,8 +134,8 @@ class AbstractModelAPISerializer:
 | 
			
		||||
        if isinstance(geojson, dict):
 | 
			
		||||
            geojson = json.dumps(geojson)
 | 
			
		||||
        geometry = geos.fromstr(geojson)
 | 
			
		||||
        geometry = Geometry.cast_to_rlp_srid(geometry)
 | 
			
		||||
        geometry = Geometry.cast_to_multipolygon(geometry)
 | 
			
		||||
        if geometry.srid != DEFAULT_SRID_RLP:
 | 
			
		||||
            geometry.transform(DEFAULT_SRID_RLP)
 | 
			
		||||
        return geometry
 | 
			
		||||
 | 
			
		||||
    def _get_obj_from_db(self, id, user):
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,6 @@ Created on: 24.01.22
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from django.db import transaction
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
 | 
			
		||||
from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin
 | 
			
		||||
from compensation.models import Compensation
 | 
			
		||||
@ -22,10 +21,8 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
 | 
			
		||||
 | 
			
		||||
    def prepare_lookup(self, id, user):
 | 
			
		||||
        super().prepare_lookup(id, user)
 | 
			
		||||
        self.shared_lookup = Q(
 | 
			
		||||
            Q(intervention__users__in=[user]) |
 | 
			
		||||
            Q(intervention__teams__in=user.shared_teams)
 | 
			
		||||
        )
 | 
			
		||||
        del self.lookup["users__in"]
 | 
			
		||||
        self.lookup["intervention__users__in"] = [user]
 | 
			
		||||
 | 
			
		||||
    def intervention_to_json(self, entry):
 | 
			
		||||
        return {
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,6 @@ Created on: 28.01.22
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from django.core.exceptions import ObjectDoesNotExist
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
 | 
			
		||||
from api.utils.serializer.v1.serializer import DeductableAPISerializerV1Mixin, AbstractModelAPISerializerV1
 | 
			
		||||
from compensation.models import EcoAccountDeduction, EcoAccount
 | 
			
		||||
@ -29,11 +28,9 @@ class DeductionAPISerializerV1(AbstractModelAPISerializerV1,
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        super().prepare_lookup(_id, user)
 | 
			
		||||
        del self.lookup["users__in"]
 | 
			
		||||
        del self.lookup["deleted__isnull"]
 | 
			
		||||
        self.shared_lookup = Q(
 | 
			
		||||
            Q(intervention__users__in=[user]) |
 | 
			
		||||
            Q(intervention__teams__in=user.shared_teams)
 | 
			
		||||
        )
 | 
			
		||||
        self.lookup["intervention__users__in"] = [user]
 | 
			
		||||
 | 
			
		||||
    def _model_to_geo_json(self, entry):
 | 
			
		||||
        """ Adds the basic data
 | 
			
		||||
 | 
			
		||||
@ -16,8 +16,7 @@ from api.utils.serializer.serializer import AbstractModelAPISerializer
 | 
			
		||||
from codelist.models import KonovaCode
 | 
			
		||||
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_PROCESS_TYPE_ID, \
 | 
			
		||||
    CODELIST_LAW_ID, CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, \
 | 
			
		||||
    CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_HANDLER_ID, \
 | 
			
		||||
    CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
 | 
			
		||||
    CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, CODELIST_HANDLER_ID
 | 
			
		||||
from compensation.models import CompensationAction, UnitChoices, CompensationState
 | 
			
		||||
from intervention.models import Responsibility, Legal, Handler
 | 
			
		||||
from konova.models import Deadline, DeadlineType
 | 
			
		||||
@ -348,7 +347,7 @@ class AbstractCompensationAPISerializerV1Mixin:
 | 
			
		||||
            try:
 | 
			
		||||
                biotope_type = entry["biotope"]
 | 
			
		||||
                biotope_details = [
 | 
			
		||||
                    self._konova_code_from_json(e, CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID) for e in entry["biotope_details"]
 | 
			
		||||
                    self._konova_code_from_json(e, CODELIST_BIOTOPES_EXTRA_CODES_ID) for e in entry["biotope_details"]
 | 
			
		||||
                ]
 | 
			
		||||
                surface = float(entry["surface"])
 | 
			
		||||
            except KeyError:
 | 
			
		||||
 | 
			
		||||
@ -23,6 +23,11 @@ class AbstractAPIViewV1(AbstractAPIView):
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
        self.serializer = self.serializer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -50,19 +50,14 @@ class AbstractAPIView(View):
 | 
			
		||||
    def dispatch(self, request, *args, **kwargs):
 | 
			
		||||
        try:
 | 
			
		||||
            # Fetch the proper user from the given request header token
 | 
			
		||||
            token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
 | 
			
		||||
            ksp_token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
 | 
			
		||||
            ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None)
 | 
			
		||||
            token_user = APIUserToken.get_user_from_token(ksp_token)
 | 
			
		||||
 | 
			
		||||
            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:
 | 
			
		||||
            if ksp_user != token_user.username:
 | 
			
		||||
                raise PermissionError(f"Invalid token for {ksp_user}")
 | 
			
		||||
            self.user = token_user
 | 
			
		||||
            else:
 | 
			
		||||
                self.user = token_user
 | 
			
		||||
 | 
			
		||||
            request.user = self.user
 | 
			
		||||
            if not self.user.is_default_user():
 | 
			
		||||
 | 
			
		||||
@ -9,8 +9,7 @@ import collections
 | 
			
		||||
 | 
			
		||||
from django.core.exceptions import ImproperlyConfigured
 | 
			
		||||
 | 
			
		||||
from codelist.settings import CODELIST_BIOTOPES_ID, \
 | 
			
		||||
    CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
 | 
			
		||||
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
 | 
			
		||||
from codelist.autocomplete.base import KonovaCodeAutocomplete
 | 
			
		||||
from konova.utils.message_templates import UNGROUPED
 | 
			
		||||
 | 
			
		||||
@ -85,11 +84,11 @@ class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete):
 | 
			
		||||
    Due to limitations of the django dal package, we need to subclass for each code list
 | 
			
		||||
    """
 | 
			
		||||
    group_by_related = "parent"
 | 
			
		||||
    related_field_name = "short_name"
 | 
			
		||||
    related_field_name = "long_name"
 | 
			
		||||
    paginate_by = 200
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        self.c = CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
 | 
			
		||||
        self.c = CODELIST_BIOTOPES_EXTRA_CODES_ID
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def order_by(self, qs):
 | 
			
		||||
@ -104,11 +103,8 @@ class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete):
 | 
			
		||||
            qs (QuerySet): The ordered queryset
 | 
			
		||||
        """
 | 
			
		||||
        return qs.order_by(
 | 
			
		||||
            "short_name",
 | 
			
		||||
            "long_name",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def get_result_label(self, result):
 | 
			
		||||
        return f"{result.long_name} ({result.short_name})"
 | 
			
		||||
 | 
			
		||||
    def get_selected_result_label(self, result):
 | 
			
		||||
        return f"{result.parent.short_name} > {result.long_name} ({result.short_name})"
 | 
			
		||||
@ -13,7 +13,7 @@ from codelist.settings import CODELIST_INTERVENTION_HANDLER_ID, CODELIST_CONSERV
 | 
			
		||||
    CODELIST_REGISTRATION_OFFICE_ID, CODELIST_BIOTOPES_ID, CODELIST_LAW_ID, CODELIST_HANDLER_ID, \
 | 
			
		||||
    CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_CLASS_ID, CODELIST_COMPENSATION_ADDITIONAL_TYPE_ID, \
 | 
			
		||||
    CODELIST_BASE_URL, CODELIST_PROCESS_TYPE_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, \
 | 
			
		||||
    CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
 | 
			
		||||
    CODELIST_COMPENSATION_ACTION_DETAIL_ID
 | 
			
		||||
from konova.management.commands.setup import BaseKonovaCommand
 | 
			
		||||
from konova.settings import PROXIES
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,6 @@ class Command(BaseKonovaCommand):
 | 
			
		||||
                CODELIST_REGISTRATION_OFFICE_ID,
 | 
			
		||||
                CODELIST_BIOTOPES_ID,
 | 
			
		||||
                CODELIST_BIOTOPES_EXTRA_CODES_ID,
 | 
			
		||||
                CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID,
 | 
			
		||||
                CODELIST_LAW_ID,
 | 
			
		||||
                CODELIST_HANDLER_ID,
 | 
			
		||||
                CODELIST_COMPENSATION_ACTION_ID,
 | 
			
		||||
@ -56,7 +55,7 @@ class Command(BaseKonovaCommand):
 | 
			
		||||
                content = result.content.decode("utf-8")
 | 
			
		||||
                root = etree.fromstring(content)
 | 
			
		||||
                items = root.findall("item")
 | 
			
		||||
                self._write_warning("   Found {} codes. Process now...".format(len(items)))
 | 
			
		||||
                self._write_warning("Found {} codes. Process now...".format(len(items)))
 | 
			
		||||
 | 
			
		||||
                code_list = KonovaCodeList.objects.get_or_create(
 | 
			
		||||
                    id=list_id,
 | 
			
		||||
@ -75,15 +74,15 @@ class Command(BaseKonovaCommand):
 | 
			
		||||
        if items is None:
 | 
			
		||||
            return
 | 
			
		||||
        else:
 | 
			
		||||
            self._write_warning("       --- Found {} subcodes. Process now...".format(len(items)))
 | 
			
		||||
            self._write_warning(" --- Found {} subcodes. Process now...".format(len(items)))
 | 
			
		||||
            for element in items:
 | 
			
		||||
                children = element.find("items")
 | 
			
		||||
                _id = element.find("id").text
 | 
			
		||||
                atom_id = element.find("atomid").text
 | 
			
		||||
                selectable = element.find("selectable").text.lower()
 | 
			
		||||
                selectable = bool_map.get(selectable, False)
 | 
			
		||||
                short_name = element.find("shortname").text or ""
 | 
			
		||||
                long_name = element.find("longname").text or ""
 | 
			
		||||
                short_name = element.find("shortname").text
 | 
			
		||||
                long_name = element.find("longname").text
 | 
			
		||||
                is_archived = bool_map.get((element.find("archive").text.lower()), False)
 | 
			
		||||
 | 
			
		||||
                code = KonovaCode.objects.get_or_create(
 | 
			
		||||
 | 
			
		||||
@ -1,60 +0,0 @@
 | 
			
		||||
# 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))
 | 
			
		||||
 | 
			
		||||
@ -1,25 +0,0 @@
 | 
			
		||||
# 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,
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -25,11 +25,13 @@ class KonovaCode(models.Model):
 | 
			
		||||
    )
 | 
			
		||||
    short_name = models.CharField(
 | 
			
		||||
        max_length=500,
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        help_text="Short version of long name",
 | 
			
		||||
    )
 | 
			
		||||
    long_name = models.CharField(
 | 
			
		||||
        max_length=1000,
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        help_text="",
 | 
			
		||||
    )
 | 
			
		||||
@ -48,28 +50,12 @@ class KonovaCode(models.Model):
 | 
			
		||||
 | 
			
		||||
    def __str__(self, with_parent: bool = True):
 | 
			
		||||
        ret_val = ""
 | 
			
		||||
 | 
			
		||||
        long_name = self.long_name
 | 
			
		||||
        short_name = self.short_name
 | 
			
		||||
 | 
			
		||||
        both_names_exist = long_name is not None and short_name is not None
 | 
			
		||||
 | 
			
		||||
        if both_names_exist:
 | 
			
		||||
            if with_parent and self.parent:
 | 
			
		||||
                parent_short_name_exists = self.parent.short_name is not None
 | 
			
		||||
                parent_long_name_exists = self.parent.long_name is not None
 | 
			
		||||
                if parent_long_name_exists:
 | 
			
		||||
                    ret_val += self.parent.long_name + " > "
 | 
			
		||||
                elif parent_short_name_exists:
 | 
			
		||||
                    ret_val += self.parent.short_name + " > "
 | 
			
		||||
 | 
			
		||||
            ret_val += long_name
 | 
			
		||||
 | 
			
		||||
            if short_name and short_name != long_name:
 | 
			
		||||
                ret_val += f" ({short_name})"
 | 
			
		||||
        else:
 | 
			
		||||
            ret_val += str(long_name or short_name)
 | 
			
		||||
 | 
			
		||||
        if self.parent and self.parent.long_name and with_parent:
 | 
			
		||||
            ret_val += self.parent.long_name + " > "
 | 
			
		||||
        ret_val += self.long_name
 | 
			
		||||
        if self.short_name and self.short_name != self.long_name:
 | 
			
		||||
            # Only add short name, if we won't have stupid repition like 'thing a (thing a)' due to misused long-short names
 | 
			
		||||
            ret_val += f" ({self.short_name})"
 | 
			
		||||
        return ret_val
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
@ -89,8 +75,7 @@ class KonovaCode(models.Model):
 | 
			
		||||
            return self
 | 
			
		||||
 | 
			
		||||
        children = KonovaCode.objects.filter(
 | 
			
		||||
            parent=self,
 | 
			
		||||
            is_archived=False,
 | 
			
		||||
            parent=self
 | 
			
		||||
        ).order_by(
 | 
			
		||||
            order_by
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -15,8 +15,7 @@ CODELIST_CONSERVATION_OFFICE_ID = 907  # CLNaturschutzbehörden
 | 
			
		||||
CODELIST_REGISTRATION_OFFICE_ID = 1053  # CLZulassungsbehörden
 | 
			
		||||
CODELIST_BIOTOPES_ID = 654  # CL_Biotoptypen
 | 
			
		||||
CODELIST_AFTER_STATE_BIOTOPES_ID = 974  # CL-KSP_ZielBiotoptypen - USAGE HAS BEEN DROPPED IN 2022 IN FAVOR OF 654
 | 
			
		||||
CODELIST_BIOTOPES_EXTRA_CODES_ID = 975  # CLZusatzbezeichnung - Subset of 288. Migration usage 975->288 in 08/2024
 | 
			
		||||
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID = 288  # CLBiotoptypZusatzcode
 | 
			
		||||
CODELIST_BIOTOPES_EXTRA_CODES_ID = 975  # CLZusatzbezeichnung
 | 
			
		||||
CODELIST_LAW_ID = 1048  # CLVerfahrensrecht
 | 
			
		||||
CODELIST_PROCESS_TYPE_ID = 44382  # CLVerfahrenstyp
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -14,8 +14,7 @@ from django.shortcuts import render
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from codelist.models import KonovaCode
 | 
			
		||||
from codelist.settings import CODELIST_BIOTOPES_ID, \
 | 
			
		||||
    CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
 | 
			
		||||
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
 | 
			
		||||
from intervention.inputs import CompensationStateTreeRadioSelect
 | 
			
		||||
from konova.contexts import BaseContext
 | 
			
		||||
from konova.forms.modals import RemoveModalForm, BaseModalForm
 | 
			
		||||
@ -44,7 +43,7 @@ class NewCompensationStateModalForm(BaseModalForm):
 | 
			
		||||
        queryset=KonovaCode.objects.filter(
 | 
			
		||||
            is_archived=False,
 | 
			
		||||
            is_leaf=True,
 | 
			
		||||
            code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID],
 | 
			
		||||
            code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_ID],
 | 
			
		||||
        ),
 | 
			
		||||
        widget=autocomplete.ModelSelect2Multiple(
 | 
			
		||||
            url="codelist:biotope-extra-type-autocomplete",
 | 
			
		||||
 | 
			
		||||
@ -1,19 +0,0 @@
 | 
			
		||||
# 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'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -6,10 +6,10 @@ Created on: 16.11.21
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
 | 
			
		||||
from codelist.models import KonovaCode
 | 
			
		||||
from codelist.settings import CODELIST_BIOTOPES_ID, \
 | 
			
		||||
    CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
 | 
			
		||||
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
 | 
			
		||||
from compensation.managers import CompensationStateManager
 | 
			
		||||
from konova.models import UuidModel
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,7 @@ class CompensationState(UuidModel):
 | 
			
		||||
        KonovaCode,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        limit_choices_to={
 | 
			
		||||
            "code_lists__in": [CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID],
 | 
			
		||||
            "code_lists__in": [CODELIST_BIOTOPES_EXTRA_CODES_ID],
 | 
			
		||||
            "is_selectable": True,
 | 
			
		||||
            "is_archived": False,
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-sm-6">
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-action' obj.id %}" title="{% trans 'Add new action' %}">
 | 
			
		||||
                        {% fa5_icon 'plus' %}
 | 
			
		||||
                        {% fa5_icon 'seedling' %}
 | 
			
		||||
@ -34,7 +34,7 @@
 | 
			
		||||
                <th scope="col">
 | 
			
		||||
                    {% trans 'Comment' %}
 | 
			
		||||
                </th>
 | 
			
		||||
                {% if is_default_member and is_entry_shared %}
 | 
			
		||||
                {% if is_default_member and has_access %}
 | 
			
		||||
                <th class="w-10" scope="col">
 | 
			
		||||
                    <span class="float-right">
 | 
			
		||||
                        {% trans 'Action' %}
 | 
			
		||||
@ -52,7 +52,7 @@
 | 
			
		||||
                    <hr>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                    {% for detail in action.action_type_details.all %}
 | 
			
		||||
                        <span class="badge badge-pill rlp-r" title="{{ detail.parent.long_name }} > {{detail}}">{{ detail.parent.long_name }} > {{detail.long_name}}</span>
 | 
			
		||||
                        <span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
 | 
			
		||||
                    {% empty %}
 | 
			
		||||
                        <span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
@ -64,7 +64,7 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="align-middle float-right">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button data-form-url="{% url 'compensation:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
 | 
			
		||||
                        {% fa5_icon 'edit' %}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@
 | 
			
		||||
            {% fa5_icon 'file-alt' %}
 | 
			
		||||
        </button>
 | 
			
		||||
    </a>
 | 
			
		||||
    {% if is_entry_shared %}
 | 
			
		||||
    {% if has_access %}
 | 
			
		||||
        <button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'compensation:resubmission-create' obj.id %}">
 | 
			
		||||
            {% fa5_icon 'bell' %}
 | 
			
		||||
        </button>
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-sm-6">
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
 | 
			
		||||
                        {% fa5_icon 'plus' %}
 | 
			
		||||
                        {% fa5_icon 'calendar-check' %}
 | 
			
		||||
@ -38,7 +38,7 @@
 | 
			
		||||
                <th scope="col">
 | 
			
		||||
                    {% trans 'Comment' %}
 | 
			
		||||
                </th>
 | 
			
		||||
                {% if is_default_member and is_entry_shared %}
 | 
			
		||||
                {% if is_default_member and has_access %}
 | 
			
		||||
                    <th class="w-10" scope="col">
 | 
			
		||||
                        <span class="float-right">
 | 
			
		||||
                            {% trans 'Action' %}
 | 
			
		||||
@ -60,7 +60,7 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="align-middle float-right">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button data-form-url="{% url 'compensation:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
 | 
			
		||||
                        {% fa5_icon 'edit' %}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-sm-6">
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
 | 
			
		||||
                        {% fa5_icon 'plus' %}
 | 
			
		||||
                        {% fa5_icon 'file' %}
 | 
			
		||||
@ -33,7 +33,7 @@
 | 
			
		||||
                <th scope="col">
 | 
			
		||||
                    {% trans 'Comment' %}
 | 
			
		||||
                </th>
 | 
			
		||||
                {% if is_default_member and is_entry_shared %}
 | 
			
		||||
                {% if is_default_member and has_access %}
 | 
			
		||||
                    <th class="w-10" scope="col">
 | 
			
		||||
                        <span class="float-right">
 | 
			
		||||
                            {% trans 'Action' %}
 | 
			
		||||
@ -59,7 +59,7 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="align-middle float-right">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button data-form-url="{% url 'compensation:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
 | 
			
		||||
                        {% fa5_icon 'edit' %}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-sm-6">
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
 | 
			
		||||
                        {% fa5_icon 'plus' %}
 | 
			
		||||
                        {% fa5_icon 'layer-group' %}
 | 
			
		||||
@ -35,7 +35,7 @@
 | 
			
		||||
                <th scope="col">
 | 
			
		||||
                    {% trans 'Surface' %}
 | 
			
		||||
                </th>
 | 
			
		||||
                {% if is_default_member and is_entry_shared %}
 | 
			
		||||
                {% if is_default_member and has_access %}
 | 
			
		||||
                <th class="w-10" scope="col">
 | 
			
		||||
                    <span class="float-right">
 | 
			
		||||
                        {% trans 'Action' %}
 | 
			
		||||
@ -51,14 +51,14 @@
 | 
			
		||||
                    <span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
 | 
			
		||||
                    <br>
 | 
			
		||||
                    {% for detail in state.biotope_type_details.all %}
 | 
			
		||||
                        <span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
 | 
			
		||||
                        <span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
 | 
			
		||||
                    {% empty %}
 | 
			
		||||
                        <span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>{{ state.surface|floatformat:2 }} m²</td>
 | 
			
		||||
                <td class="align-middle float-right">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
 | 
			
		||||
                        {% fa5_icon 'edit' %}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-sm-6">
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
 | 
			
		||||
                        {% fa5_icon 'plus' %}
 | 
			
		||||
                        {% fa5_icon 'layer-group' %}
 | 
			
		||||
@ -35,7 +35,7 @@
 | 
			
		||||
                <th scope="col">
 | 
			
		||||
                    {% trans 'Surface' %}
 | 
			
		||||
                </th>
 | 
			
		||||
                {% if is_default_member and is_entry_shared %}
 | 
			
		||||
                {% if is_default_member and has_access %}
 | 
			
		||||
                <th class="w-10" scope="col">
 | 
			
		||||
                    <span class="float-right">
 | 
			
		||||
                        {% trans 'Action' %}
 | 
			
		||||
@ -51,14 +51,14 @@
 | 
			
		||||
                    <span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
 | 
			
		||||
                    <br>
 | 
			
		||||
                    {% for detail in state.biotope_type_details.all %}
 | 
			
		||||
                        <span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
 | 
			
		||||
                        <span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
 | 
			
		||||
                    {% empty %}
 | 
			
		||||
                        <span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>{{ state.surface|floatformat:2 }} m²</td>
 | 
			
		||||
                <td class="align-middle float-right">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
 | 
			
		||||
                        {% fa5_icon 'edit' %}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
@ -123,7 +123,7 @@
 | 
			
		||||
                                {% include 'user/includes/team_data_modal_button.html' %}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                            <hr>
 | 
			
		||||
                            {% if is_entry_shared %}
 | 
			
		||||
                            {% if has_access %}
 | 
			
		||||
                                {% for user in obj.intervention.shared_users %}
 | 
			
		||||
                                    {% include 'user/includes/contact_modal_button.html' %}
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-sm-6">
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-action' obj.id %}" title="{% trans 'Add new action' %}">
 | 
			
		||||
                        {% fa5_icon 'plus' %}
 | 
			
		||||
                        {% fa5_icon 'seedling' %}
 | 
			
		||||
@ -33,7 +33,7 @@
 | 
			
		||||
                <th scope="col">
 | 
			
		||||
                    {% trans 'Comment' %}
 | 
			
		||||
                </th>
 | 
			
		||||
                {% if is_default_member and is_entry_shared %}
 | 
			
		||||
                {% if is_default_member and has_access %}
 | 
			
		||||
                    <th class="w-10" scope="col">
 | 
			
		||||
                        <span class="float-right">
 | 
			
		||||
                            {% trans 'Action' %}
 | 
			
		||||
@ -51,7 +51,7 @@
 | 
			
		||||
                    <hr>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                    {% for detail in action.action_type_details.all %}
 | 
			
		||||
                        <span class="badge badge-pill rlp-r" title="{{ detail.parent.long_name }} > {{detail}}">{{ detail.parent.long_name }} > {{detail.long_name}}</span>
 | 
			
		||||
                        <span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
 | 
			
		||||
                    {% empty %}
 | 
			
		||||
                        <span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
@ -63,7 +63,7 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="align-middle float-right">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button data-form-url="{% url 'compensation:acc:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
 | 
			
		||||
                        {% fa5_icon 'edit' %}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@
 | 
			
		||||
            {% fa5_icon 'file-alt' %}
 | 
			
		||||
        </button>
 | 
			
		||||
    </a>
 | 
			
		||||
    {% if is_entry_shared %}
 | 
			
		||||
    {% if has_access %}
 | 
			
		||||
        <button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'compensation:acc:resubmission-create' obj.id %}">
 | 
			
		||||
            {% fa5_icon 'bell' %}
 | 
			
		||||
        </button>
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-sm-6">
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
 | 
			
		||||
                        {% fa5_icon 'plus' %}
 | 
			
		||||
                        {% fa5_icon 'calendar-check' %}
 | 
			
		||||
@ -58,7 +58,7 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="align-middle float-right">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button data-form-url="{% url 'compensation:acc:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
 | 
			
		||||
                        {% fa5_icon 'edit' %}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
@ -61,7 +61,7 @@
 | 
			
		||||
                <td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td>
 | 
			
		||||
                <td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td>
 | 
			
		||||
                <td class="align-middle float-right">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared or is_default_member and user in deduction.intervention.shared_users  %}
 | 
			
		||||
                    {% if is_default_member and has_access or is_default_member and user in deduction.intervention.shared_users  %}
 | 
			
		||||
                    <button data-form-url="{% url 'compensation:acc:edit-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
 | 
			
		||||
                        {% fa5_icon 'edit' %}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-sm-6">
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
 | 
			
		||||
                        {% fa5_icon 'plus' %}
 | 
			
		||||
                        {% fa5_icon 'file' %}
 | 
			
		||||
@ -57,7 +57,7 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="align-middle float-right">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button data-form-url="{% url 'compensation:acc:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
 | 
			
		||||
                        {% fa5_icon 'edit' %}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-sm-6">
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
 | 
			
		||||
                        {% fa5_icon 'plus' %}
 | 
			
		||||
                        {% fa5_icon 'layer-group' %}
 | 
			
		||||
@ -35,7 +35,7 @@
 | 
			
		||||
                <th scope="col">
 | 
			
		||||
                    {% trans 'Surface' %}
 | 
			
		||||
                </th>
 | 
			
		||||
                {% if is_default_member and is_entry_shared %}
 | 
			
		||||
                {% if is_default_member and has_access %}
 | 
			
		||||
                <th class="w-10" scope="col">
 | 
			
		||||
                    <span class="float-right">
 | 
			
		||||
                        {% trans 'Action' %}
 | 
			
		||||
@ -51,14 +51,14 @@
 | 
			
		||||
                    <span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
 | 
			
		||||
                    <br>
 | 
			
		||||
                    {% for detail in state.biotope_type_details.all %}
 | 
			
		||||
                        <span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
 | 
			
		||||
                        <span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
 | 
			
		||||
                    {% empty %}
 | 
			
		||||
                        <span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>{{ state.surface|floatformat:2 }} m²</td>
 | 
			
		||||
                <td class="align-middle float-right">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button data-form-url="{% url 'compensation:acc:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
 | 
			
		||||
                        {% fa5_icon 'edit' %}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-sm-6">
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
 | 
			
		||||
                        {% fa5_icon 'plus' %}
 | 
			
		||||
                        {% fa5_icon 'layer-group' %}
 | 
			
		||||
@ -35,7 +35,7 @@
 | 
			
		||||
                <th scope="col">
 | 
			
		||||
                    {% trans 'Surface' %}
 | 
			
		||||
                </th>
 | 
			
		||||
                {% if is_default_member and is_entry_shared %}
 | 
			
		||||
                {% if is_default_member and has_access %}
 | 
			
		||||
                <th class="w-10" scope="col">
 | 
			
		||||
                    <span class="float-right">
 | 
			
		||||
                        {% trans 'Action' %}
 | 
			
		||||
@ -51,14 +51,14 @@
 | 
			
		||||
                    <span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
 | 
			
		||||
                    <br>
 | 
			
		||||
                    {% for detail in state.biotope_type_details.all %}
 | 
			
		||||
                        <span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
 | 
			
		||||
                        <span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
 | 
			
		||||
                    {% empty %}
 | 
			
		||||
                        <span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>{{ state.surface|floatformat:2 }} m²</td>
 | 
			
		||||
                <td class="align-middle float-right">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button data-form-url="{% url 'compensation:acc:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
 | 
			
		||||
                        {% fa5_icon 'edit' %}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
@ -101,7 +101,7 @@
 | 
			
		||||
                                {% include 'user/includes/team_data_modal_button.html' %}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                            <hr>
 | 
			
		||||
                            {% if is_entry_shared %}
 | 
			
		||||
                            {% if has_access %}
 | 
			
		||||
                                {% for user in obj.users.all %}
 | 
			
		||||
                                    {% include 'user/includes/contact_modal_button.html' %}
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
 | 
			
		||||
@ -54,7 +54,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
 | 
			
		||||
        post_data = {
 | 
			
		||||
            "identifier": test_id,
 | 
			
		||||
            "title": test_title,
 | 
			
		||||
            "output": geom_json,
 | 
			
		||||
            "geom": geom_json,
 | 
			
		||||
            "intervention": self.intervention.id,
 | 
			
		||||
        }
 | 
			
		||||
        pre_creation_intervention_log_count = self.intervention.log.count()
 | 
			
		||||
@ -94,7 +94,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
 | 
			
		||||
        post_data = {
 | 
			
		||||
            "identifier": test_id,
 | 
			
		||||
            "title": test_title,
 | 
			
		||||
            "output": geom_json,
 | 
			
		||||
            "geom": geom_json,
 | 
			
		||||
        }
 | 
			
		||||
        pre_creation_intervention_log_count = self.intervention.log.count()
 | 
			
		||||
 | 
			
		||||
@ -150,7 +150,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
 | 
			
		||||
            "title": new_title,
 | 
			
		||||
            "intervention": self.intervention.id,  # just keep the intervention as it is
 | 
			
		||||
            "comment": new_comment,
 | 
			
		||||
            "output": geojson,
 | 
			
		||||
            "geom": geojson,
 | 
			
		||||
        }
 | 
			
		||||
        self.client_user.post(url, post_data)
 | 
			
		||||
        self.compensation.refresh_from_db()
 | 
			
		||||
 | 
			
		||||
@ -46,7 +46,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
 | 
			
		||||
        post_data = {
 | 
			
		||||
            "identifier": test_id,
 | 
			
		||||
            "title": test_title,
 | 
			
		||||
            "output": geom_json,
 | 
			
		||||
            "geom": geom_json,
 | 
			
		||||
            "surface": test_deductable_surface,
 | 
			
		||||
            "conservation_office": test_conservation_office.id
 | 
			
		||||
        }
 | 
			
		||||
@ -86,7 +86,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
 | 
			
		||||
        new_title = self.create_dummy_string()
 | 
			
		||||
        new_identifier = self.create_dummy_string()
 | 
			
		||||
        new_comment = self.create_dummy_string()
 | 
			
		||||
        new_geometry = self.create_dummy_geometry()
 | 
			
		||||
        new_geometry = MultiPolygon(srid=4326)  # Create an empty geometry
 | 
			
		||||
        test_conservation_office = self.get_conservation_office_code()
 | 
			
		||||
        test_deductable_surface = self.eco_account.deductable_surface + 100
 | 
			
		||||
 | 
			
		||||
@ -103,7 +103,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
 | 
			
		||||
            "identifier": new_identifier,
 | 
			
		||||
            "title": new_title,
 | 
			
		||||
            "comment": new_comment,
 | 
			
		||||
            "output": self.create_geojson(new_geometry),
 | 
			
		||||
            "geom": new_geometry.geojson,
 | 
			
		||||
            "surface": test_deductable_surface,
 | 
			
		||||
            "conservation_office": test_conservation_office.id
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,7 @@ from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
 | 
			
		||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
 | 
			
		||||
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \
 | 
			
		||||
    RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \
 | 
			
		||||
    COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
 | 
			
		||||
    COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
@ -103,19 +103,11 @@ def new_view(request: HttpRequest, intervention_id: str = None):
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
            messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
 | 
			
		||||
            if geom_form.has_geometry_simplified():
 | 
			
		||||
            if geom_form.geometry_simplified:
 | 
			
		||||
                messages.info(
 | 
			
		||||
                    request,
 | 
			
		||||
                    GEOMETRY_SIMPLIFIED
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            num_ignored_geometries = geom_form.get_num_geometries_ignored()
 | 
			
		||||
            if num_ignored_geometries > 0:
 | 
			
		||||
                messages.info(
 | 
			
		||||
                    request,
 | 
			
		||||
                    GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            return redirect("compensation:detail", id=comp.id)
 | 
			
		||||
        else:
 | 
			
		||||
            messages.error(request, FORM_INVALID, extra_tags="danger",)
 | 
			
		||||
@ -187,19 +179,11 @@ def edit_view(request: HttpRequest, id: str):
 | 
			
		||||
            if intervention_is_checked:
 | 
			
		||||
                messages.info(request, CHECK_STATE_RESET)
 | 
			
		||||
            messages.success(request, _("Compensation {} edited").format(comp.identifier))
 | 
			
		||||
            if geom_form.has_geometry_simplified():
 | 
			
		||||
            if geom_form.geometry_simplified:
 | 
			
		||||
                messages.info(
 | 
			
		||||
                    request,
 | 
			
		||||
                    GEOMETRY_SIMPLIFIED
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            num_ignored_geometries = geom_form.get_num_geometries_ignored()
 | 
			
		||||
            if num_ignored_geometries > 0:
 | 
			
		||||
                messages.info(
 | 
			
		||||
                    request,
 | 
			
		||||
                    GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            return redirect("compensation:detail", id=comp.id)
 | 
			
		||||
        else:
 | 
			
		||||
            messages.error(request, FORM_INVALID, extra_tags="danger",)
 | 
			
		||||
@ -275,7 +259,7 @@ def detail_view(request: HttpRequest, id: str):
 | 
			
		||||
        "last_checked_tooltip": last_checked_tooltip,
 | 
			
		||||
        "geom_form": geom_form,
 | 
			
		||||
        "parcels": parcels,
 | 
			
		||||
        "is_entry_shared": is_data_shared,
 | 
			
		||||
        "has_access": is_data_shared,
 | 
			
		||||
        "actions": actions,
 | 
			
		||||
        "before_states": before_states,
 | 
			
		||||
        "after_states": after_states,
 | 
			
		||||
 | 
			
		||||
@ -12,12 +12,11 @@ from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from compensation.models import Compensation
 | 
			
		||||
from konova.contexts import BaseContext
 | 
			
		||||
from konova.decorators import uuid_required
 | 
			
		||||
from konova.forms import SimpleGeomForm
 | 
			
		||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
 | 
			
		||||
from konova.utils.generators import generate_qr_code
 | 
			
		||||
 | 
			
		||||
@uuid_required
 | 
			
		||||
 | 
			
		||||
def report_view(request: HttpRequest, id: str):
 | 
			
		||||
    """ Renders the public report view
 | 
			
		||||
 | 
			
		||||
@ -68,7 +67,7 @@ def report_view(request: HttpRequest, id: str):
 | 
			
		||||
            "img": qrcode_img_lanis,
 | 
			
		||||
            "url": qrcode_lanis_url,
 | 
			
		||||
        },
 | 
			
		||||
        "is_entry_shared": False,  # disables action buttons during rendering
 | 
			
		||||
        "has_access": False,  # disables action buttons during rendering
 | 
			
		||||
        "before_states": before_states,
 | 
			
		||||
        "after_states": after_states,
 | 
			
		||||
        "geom_form": geom_form,
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,7 @@ from konova.forms import SimpleGeomForm
 | 
			
		||||
from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP
 | 
			
		||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
 | 
			
		||||
from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \
 | 
			
		||||
    IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
 | 
			
		||||
    IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
@ -84,19 +84,11 @@ def new_view(request: HttpRequest):
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
            messages.success(request, _("Eco-Account {} added").format(acc.identifier))
 | 
			
		||||
            if geom_form.has_geometry_simplified():
 | 
			
		||||
            if geom_form.geometry_simplified:
 | 
			
		||||
                messages.info(
 | 
			
		||||
                    request,
 | 
			
		||||
                    GEOMETRY_SIMPLIFIED
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            num_ignored_geometries = geom_form.get_num_geometries_ignored()
 | 
			
		||||
            if num_ignored_geometries > 0:
 | 
			
		||||
                messages.info(
 | 
			
		||||
                    request,
 | 
			
		||||
                    GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            return redirect("compensation:acc:detail", id=acc.id)
 | 
			
		||||
        else:
 | 
			
		||||
            messages.error(request, FORM_INVALID, extra_tags="danger",)
 | 
			
		||||
@ -164,19 +156,11 @@ def edit_view(request: HttpRequest, id: str):
 | 
			
		||||
            # The data form takes the geom form for processing, as well as the performing user
 | 
			
		||||
            acc = data_form.save(request.user, geom_form)
 | 
			
		||||
            messages.success(request, _("Eco-Account {} edited").format(acc.identifier))
 | 
			
		||||
            if geom_form.has_geometry_simplified():
 | 
			
		||||
            if geom_form.geometry_simplified:
 | 
			
		||||
                messages.info(
 | 
			
		||||
                    request,
 | 
			
		||||
                    GEOMETRY_SIMPLIFIED
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            num_ignored_geometries = geom_form.get_num_geometries_ignored()
 | 
			
		||||
            if num_ignored_geometries > 0:
 | 
			
		||||
                messages.info(
 | 
			
		||||
                    request,
 | 
			
		||||
                    GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            return redirect("compensation:acc:detail", id=acc.id)
 | 
			
		||||
        else:
 | 
			
		||||
            messages.error(request, FORM_INVALID, extra_tags="danger",)
 | 
			
		||||
@ -253,7 +237,7 @@ def detail_view(request: HttpRequest, id: str):
 | 
			
		||||
        "obj": acc,
 | 
			
		||||
        "geom_form": geom_form,
 | 
			
		||||
        "parcels": parcels,
 | 
			
		||||
        "is_entry_shared": is_data_shared,
 | 
			
		||||
        "has_access": is_data_shared,
 | 
			
		||||
        "before_states": before_states,
 | 
			
		||||
        "after_states": after_states,
 | 
			
		||||
        "sum_before_states": sum_before_states,
 | 
			
		||||
 | 
			
		||||
@ -12,13 +12,11 @@ from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from compensation.models import EcoAccount
 | 
			
		||||
from konova.contexts import BaseContext
 | 
			
		||||
from konova.decorators import uuid_required
 | 
			
		||||
from konova.forms import SimpleGeomForm
 | 
			
		||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
 | 
			
		||||
from konova.utils.generators import generate_qr_code
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@uuid_required
 | 
			
		||||
def report_view(request: HttpRequest, id: str):
 | 
			
		||||
    """ Renders the public report view
 | 
			
		||||
 | 
			
		||||
@ -75,7 +73,7 @@ def report_view(request: HttpRequest, id: str):
 | 
			
		||||
            "img": qrcode_img_lanis,
 | 
			
		||||
            "url": qrcode_lanis_url,
 | 
			
		||||
        },
 | 
			
		||||
        "is_entry_shared": False,  # disables action buttons during rendering
 | 
			
		||||
        "has_access": False,  # disables action buttons during rendering
 | 
			
		||||
        "before_states": before_states,
 | 
			
		||||
        "after_states": after_states,
 | 
			
		||||
        "geom_form": geom_form,
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-sm-6">
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-action' obj.id %}" title="{% trans 'Add new action' %}">
 | 
			
		||||
                        {% fa5_icon 'plus' %}
 | 
			
		||||
                        {% fa5_icon 'seedling' %}
 | 
			
		||||
@ -49,7 +49,7 @@
 | 
			
		||||
                    <hr>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                    {% for detail in action.action_type_details.all %}
 | 
			
		||||
                        <span class="badge badge-pill rlp-r" title="{{ detail.parent.long_name }} > {{detail}}">{{ detail.parent.long_name }} > {{detail.long_name}}</span>
 | 
			
		||||
                        <span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
 | 
			
		||||
                    {% empty %}
 | 
			
		||||
                        <span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
@ -61,7 +61,7 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="align-middle float-right">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button data-form-url="{% url 'ema:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
 | 
			
		||||
                        {% fa5_icon 'edit' %}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@
 | 
			
		||||
            {% fa5_icon 'file-alt' %}
 | 
			
		||||
        </button>
 | 
			
		||||
    </a>
 | 
			
		||||
    {% if is_entry_shared %}
 | 
			
		||||
    {% if has_access %}
 | 
			
		||||
        <button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'ema:resubmission-create' obj.id %}">
 | 
			
		||||
            {% fa5_icon 'bell' %}
 | 
			
		||||
        </button>
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-sm-6">
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
 | 
			
		||||
                        {% fa5_icon 'plus' %}
 | 
			
		||||
                        {% fa5_icon 'calendar-check' %}
 | 
			
		||||
@ -58,7 +58,7 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="align-middle float-right">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button data-form-url="{% url 'ema:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
 | 
			
		||||
                        {% fa5_icon 'edit' %}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-sm-6">
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
 | 
			
		||||
                        {% fa5_icon 'plus' %}
 | 
			
		||||
                        {% fa5_icon 'file' %}
 | 
			
		||||
@ -57,7 +57,7 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="align-middle float-right">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button data-form-url="{% url 'ema:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
 | 
			
		||||
                        {% fa5_icon 'edit' %}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-sm-6">
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
 | 
			
		||||
                        {% fa5_icon 'plus' %}
 | 
			
		||||
                        {% fa5_icon 'layer-group' %}
 | 
			
		||||
@ -49,14 +49,14 @@
 | 
			
		||||
                    <span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
 | 
			
		||||
                    <br>
 | 
			
		||||
                    {% for detail in state.biotope_type_details.all %}
 | 
			
		||||
                        <span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
 | 
			
		||||
                        <span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
 | 
			
		||||
                    {% empty %}
 | 
			
		||||
                        <span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>{{ state.surface|floatformat:2 }} m²</td>
 | 
			
		||||
                <td class="align-middle float-right">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button data-form-url="{% url 'ema:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
 | 
			
		||||
                        {% fa5_icon 'edit' %}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-sm-6">
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
 | 
			
		||||
                        {% fa5_icon 'plus' %}
 | 
			
		||||
                        {% fa5_icon 'layer-group' %}
 | 
			
		||||
@ -49,14 +49,14 @@
 | 
			
		||||
                    <span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
 | 
			
		||||
                    <br>
 | 
			
		||||
                    {% for detail in state.biotope_type_details.all %}
 | 
			
		||||
                        <span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
 | 
			
		||||
                        <span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
 | 
			
		||||
                    {% empty %}
 | 
			
		||||
                        <span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>{{ state.surface|floatformat:2 }} m²</td>
 | 
			
		||||
                <td class="align-middle float-right">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button data-form-url="{% url 'ema:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
 | 
			
		||||
                        {% fa5_icon 'edit' %}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
@ -87,7 +87,7 @@
 | 
			
		||||
                                {% include 'user/includes/team_data_modal_button.html' %}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                            <hr>
 | 
			
		||||
                            {% if is_entry_shared %}
 | 
			
		||||
                            {% if has_access %}
 | 
			
		||||
                                {% for user in obj.users.all %}
 | 
			
		||||
                                    {% include 'user/includes/contact_modal_button.html' %}
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
 | 
			
		||||
@ -46,7 +46,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
 | 
			
		||||
        post_data = {
 | 
			
		||||
            "identifier": test_id,
 | 
			
		||||
            "title": test_title,
 | 
			
		||||
            "output": geom_json,
 | 
			
		||||
            "geom": geom_json,
 | 
			
		||||
            "conservation_office": test_conservation_office.id
 | 
			
		||||
        }
 | 
			
		||||
        self.client_user.post(new_url, post_data)
 | 
			
		||||
@ -84,7 +84,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
 | 
			
		||||
        new_title = self.create_dummy_string()
 | 
			
		||||
        new_identifier = self.create_dummy_string()
 | 
			
		||||
        new_comment = self.create_dummy_string()
 | 
			
		||||
        new_geometry = self.create_dummy_geometry()  # Create an empty geometry
 | 
			
		||||
        new_geometry = MultiPolygon(srid=4326)  # Create an empty geometry
 | 
			
		||||
        test_conservation_office = self.get_conservation_office_code()
 | 
			
		||||
 | 
			
		||||
        check_on_elements = {
 | 
			
		||||
@ -99,7 +99,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
 | 
			
		||||
            "identifier": new_identifier,
 | 
			
		||||
            "title": new_title,
 | 
			
		||||
            "comment": new_comment,
 | 
			
		||||
            "output": self.create_geojson(new_geometry),
 | 
			
		||||
            "geom": new_geometry.geojson,
 | 
			
		||||
            "conservation_office": test_conservation_office.id
 | 
			
		||||
        }
 | 
			
		||||
        self.client_user.post(url, post_data)
 | 
			
		||||
 | 
			
		||||
@ -48,7 +48,7 @@ class NewEmaFormTestCase(BaseTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        geom_form_data = json.loads(geom_form_data)
 | 
			
		||||
        geom_form_data = {
 | 
			
		||||
            "output": json.dumps(geom_form_data)
 | 
			
		||||
            "geom": json.dumps(geom_form_data)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        geom_form = SimpleGeomForm(geom_form_data)
 | 
			
		||||
@ -116,7 +116,7 @@ class EditEmaFormTestCase(BaseTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        geom_form_data = json.loads(geom_form_data)
 | 
			
		||||
        geom_form_data = {
 | 
			
		||||
            "output": json.dumps(geom_form_data)
 | 
			
		||||
            "geom": json.dumps(geom_form_data)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        geom_form = SimpleGeomForm(geom_form_data)
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,7 @@ from konova.forms.modals import RemoveModalForm
 | 
			
		||||
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
 | 
			
		||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
 | 
			
		||||
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \
 | 
			
		||||
    DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
 | 
			
		||||
    DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
@ -84,17 +84,11 @@ def new_view(request: HttpRequest):
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
            messages.success(request, _("EMA {} added").format(ema.identifier))
 | 
			
		||||
            if geom_form.has_geometry_simplified():
 | 
			
		||||
            if geom_form.geometry_simplified:
 | 
			
		||||
                messages.info(
 | 
			
		||||
                    request,
 | 
			
		||||
                    GEOMETRY_SIMPLIFIED
 | 
			
		||||
                )
 | 
			
		||||
            num_ignored_geometries = geom_form.get_num_geometries_ignored()
 | 
			
		||||
            if num_ignored_geometries > 0:
 | 
			
		||||
                messages.info(
 | 
			
		||||
                    request,
 | 
			
		||||
                    GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            return redirect("ema:detail", id=ema.id)
 | 
			
		||||
        else:
 | 
			
		||||
@ -148,7 +142,7 @@ def detail_view(request: HttpRequest, id: str):
 | 
			
		||||
    geom_form = SimpleGeomForm(instance=ema)
 | 
			
		||||
    parcels = ema.get_underlying_parcels()
 | 
			
		||||
    _user = request.user
 | 
			
		||||
    is_entry_shared = ema.is_shared_with(_user)
 | 
			
		||||
    is_data_shared = ema.is_shared_with(_user)
 | 
			
		||||
 | 
			
		||||
    # Order states according to surface
 | 
			
		||||
    before_states = ema.before_states.all().order_by("-surface")
 | 
			
		||||
@ -173,7 +167,7 @@ def detail_view(request: HttpRequest, id: str):
 | 
			
		||||
        "obj": ema,
 | 
			
		||||
        "geom_form": geom_form,
 | 
			
		||||
        "parcels": parcels,
 | 
			
		||||
        "is_entry_shared": is_entry_shared,
 | 
			
		||||
        "has_access": is_data_shared,
 | 
			
		||||
        "before_states": before_states,
 | 
			
		||||
        "after_states": after_states,
 | 
			
		||||
        "sum_before_states": sum_before_states,
 | 
			
		||||
@ -221,19 +215,11 @@ def edit_view(request: HttpRequest, id: str):
 | 
			
		||||
            # The data form takes the geom form for processing, as well as the performing user
 | 
			
		||||
            ema = data_form.save(request.user, geom_form)
 | 
			
		||||
            messages.success(request, _("EMA {} edited").format(ema.identifier))
 | 
			
		||||
            if geom_form.has_geometry_simplified():
 | 
			
		||||
            if geom_form.geometry_simplified:
 | 
			
		||||
                messages.info(
 | 
			
		||||
                    request,
 | 
			
		||||
                    GEOMETRY_SIMPLIFIED
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            num_ignored_geometries = geom_form.get_num_geometries_ignored()
 | 
			
		||||
            if num_ignored_geometries > 0:
 | 
			
		||||
                messages.info(
 | 
			
		||||
                    request,
 | 
			
		||||
                    GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            return redirect("ema:detail", id=ema.id)
 | 
			
		||||
        else:
 | 
			
		||||
            messages.error(request, FORM_INVALID, extra_tags="danger",)
 | 
			
		||||
 | 
			
		||||
@ -12,12 +12,11 @@ from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from ema.models import Ema
 | 
			
		||||
from konova.contexts import BaseContext
 | 
			
		||||
from konova.decorators import uuid_required
 | 
			
		||||
from konova.forms import SimpleGeomForm
 | 
			
		||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
 | 
			
		||||
from konova.utils.generators import generate_qr_code
 | 
			
		||||
 | 
			
		||||
@uuid_required
 | 
			
		||||
 | 
			
		||||
def report_view(request:HttpRequest, id: str):
 | 
			
		||||
    """ Renders the public report view
 | 
			
		||||
 | 
			
		||||
@ -68,7 +67,7 @@ def report_view(request:HttpRequest, id: str):
 | 
			
		||||
            "img": qrcode_img_lanis,
 | 
			
		||||
            "url": qrcode_lanis_url
 | 
			
		||||
        },
 | 
			
		||||
        "is_entry_shared": False,  # disables action buttons during rendering
 | 
			
		||||
        "has_access": False,  # disables action buttons during rendering
 | 
			
		||||
        "before_states": before_states,
 | 
			
		||||
        "after_states": after_states,
 | 
			
		||||
        "geom_form": geom_form,
 | 
			
		||||
 | 
			
		||||
@ -5,8 +5,6 @@ Contact: michel.peltriaux@sgdnord.rlp.de
 | 
			
		||||
Created on: 30.11.20
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from konova.sub_settings.django_settings import env
 | 
			
		||||
 | 
			
		||||
INTERVENTION_IDENTIFIER_LENGTH = 6
 | 
			
		||||
INTERVENTION_IDENTIFIER_TEMPLATE = "EIV-{}"
 | 
			
		||||
 | 
			
		||||
@ -16,7 +14,7 @@ INTERVENTION_LANIS_LAYER_NAME_UNRECORDED_OLD_ENTRY = "eiv_unrecorded_old_entries
 | 
			
		||||
 | 
			
		||||
# EGON connection settings via rabbitmq
 | 
			
		||||
# NEEDED FOR BACKWARDS COMPATIBILITY
 | 
			
		||||
EGON_RABBITMQ_HOST = env("EGON_RABBITMQ_HOST")
 | 
			
		||||
EGON_RABBITMQ_PORT = env("EGON_RABBITMQ_PORT")
 | 
			
		||||
EGON_RABBITMQ_USER = env("EGON_RABBITMQ_USER")
 | 
			
		||||
EGON_RABBITMQ_PW = env("EGON_RABBITMQ_PW")
 | 
			
		||||
EGON_RABBITMQ_HOST = "CHANGE_ME"
 | 
			
		||||
EGON_RABBITMQ_PORT = "CHANGE_ME"
 | 
			
		||||
EGON_RABBITMQ_USER = "CHANGE_ME"
 | 
			
		||||
EGON_RABBITMQ_PW = "CHANGE_ME"
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-sm-6">
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <a href="{% url 'compensation:new' obj.id %}" title="{% trans 'Add new compensation' %}">
 | 
			
		||||
                        <button class="btn btn-outline-default">
 | 
			
		||||
                            {% fa5_icon 'plus' %}
 | 
			
		||||
@ -32,7 +32,7 @@
 | 
			
		||||
                <th scope="col">
 | 
			
		||||
                    {% trans 'Title' %}
 | 
			
		||||
                </th>
 | 
			
		||||
                {% if is_default_member and is_entry_shared %}
 | 
			
		||||
                {% if is_default_member and has_access %}
 | 
			
		||||
                    <th class="w-10" scope="col">
 | 
			
		||||
                        <span class="float-right">
 | 
			
		||||
                            {% trans 'Action' %}
 | 
			
		||||
@ -51,7 +51,7 @@
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="align-middle">{{ comp.title }}</td>
 | 
			
		||||
                <td>
 | 
			
		||||
                {% if is_default_member and is_entry_shared %}
 | 
			
		||||
                {% if is_default_member and has_access %}
 | 
			
		||||
                    <button data-form-url="{% url 'intervention:remove-compensation' obj.id comp.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove compensation' %}">
 | 
			
		||||
                        {% fa5_icon 'trash' %}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@
 | 
			
		||||
            {% fa5_icon 'file-alt' %}
 | 
			
		||||
        </button>
 | 
			
		||||
    </a>
 | 
			
		||||
    {% if is_entry_shared %}
 | 
			
		||||
    {% if has_access %}
 | 
			
		||||
        <button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'intervention:resubmission-create' obj.id %}">
 | 
			
		||||
            {% fa5_icon 'bell' %}
 | 
			
		||||
        </button>
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-sm-6">
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'intervention:new-deduction' obj.id %}" title="{% trans 'Add new deduction' %}">
 | 
			
		||||
                        {% fa5_icon 'plus' %}
 | 
			
		||||
                        {% fa5_icon 'tree' %}
 | 
			
		||||
@ -33,7 +33,7 @@
 | 
			
		||||
                <th scope="col">
 | 
			
		||||
                    {% trans 'Created' %}
 | 
			
		||||
                </th>
 | 
			
		||||
                {% if is_default_member and is_entry_shared %}
 | 
			
		||||
                {% if is_default_member and has_access %}
 | 
			
		||||
                    <th class="w-10" scope="col">
 | 
			
		||||
                        <span class="float-right">
 | 
			
		||||
                            {% trans 'Action' %}
 | 
			
		||||
@ -56,7 +56,7 @@
 | 
			
		||||
                <td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td>
 | 
			
		||||
                <td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td>
 | 
			
		||||
                <td class="align-middle float-right">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button data-form-url="{% url 'intervention:edit-deduction' obj.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
 | 
			
		||||
                        {% fa5_icon 'edit' %}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-sm-6">
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'intervention:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
 | 
			
		||||
                        {% fa5_icon 'plus' %}
 | 
			
		||||
                        {% fa5_icon 'file' %}
 | 
			
		||||
@ -38,7 +38,7 @@
 | 
			
		||||
                <th scope="col">
 | 
			
		||||
                    {% trans 'Comment' %}
 | 
			
		||||
                </th>
 | 
			
		||||
                {% if is_default_member and is_entry_shared %}
 | 
			
		||||
                {% if is_default_member and has_access %}
 | 
			
		||||
                    <th class="w-10" scope="col">
 | 
			
		||||
                        <span class="float-right">
 | 
			
		||||
                            {% trans 'Action' %}
 | 
			
		||||
@ -66,7 +66,7 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="align-middle float-right">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button data-form-url="{% url 'intervention:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
 | 
			
		||||
                        {% fa5_icon 'edit' %}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="col-sm-6">
 | 
			
		||||
                <div class="d-flex justify-content-end">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:pay:new' obj.id %}" title="{% trans 'Add new payment' %}">
 | 
			
		||||
                        {% fa5_icon 'plus' %}
 | 
			
		||||
                        {% fa5_icon 'money-bill-wave' %}
 | 
			
		||||
@ -33,7 +33,7 @@
 | 
			
		||||
                <th class="w-50" scope="col">
 | 
			
		||||
                    {% trans 'Comment' %}
 | 
			
		||||
                </th>
 | 
			
		||||
                {% if is_default_member and is_entry_shared %}
 | 
			
		||||
                {% if is_default_member and has_access %}
 | 
			
		||||
                    <th class="w-10" scope="col">
 | 
			
		||||
                        <span class="float-right">
 | 
			
		||||
                            {% trans 'Action' %}
 | 
			
		||||
@ -46,24 +46,16 @@
 | 
			
		||||
            {% for pay in obj.payments.all %}
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td class="align-middle">
 | 
			
		||||
                    {% if is_entry_shared %}
 | 
			
		||||
                        {{ pay.amount|floatformat:2 }} €
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                        ***
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    {{ pay.amount|floatformat:2 }} €
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="align-middle">{{ pay.due_on|default_if_none:"---" }}</td>
 | 
			
		||||
                <td class="align-middle">
 | 
			
		||||
                    <div class="scroll-150">
 | 
			
		||||
                        {% if is_entry_shared %}
 | 
			
		||||
                            {{ pay.comment }}
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                            {% trans 'This data is not shared with you' %}
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                        {{ pay.comment }}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="align-middle float-right">
 | 
			
		||||
                    {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                    {% if is_default_member and has_access  %}
 | 
			
		||||
                    <button data-form-url="{% url 'compensation:pay:edit' obj.id pay.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit payment' %}">
 | 
			
		||||
                        {% fa5_icon 'edit' %}
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@
 | 
			
		||||
                    {% comment %}
 | 
			
		||||
                        Only show add-button if no revocation exists, yet.
 | 
			
		||||
                    {% endcomment %}
 | 
			
		||||
                    {% if is_default_member and is_entry_shared and not obj.legal.revocation %}
 | 
			
		||||
                    {% if is_default_member and has_access and not obj.legal.revocation %}
 | 
			
		||||
                    <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'intervention:new-revocation' obj.id %}" title="{% trans 'Add revocation' %}">
 | 
			
		||||
                        {% fa5_icon 'plus' %}
 | 
			
		||||
                        {% fa5_icon 'ban' %}
 | 
			
		||||
@ -36,7 +36,7 @@
 | 
			
		||||
                <th scope="col">
 | 
			
		||||
                    {% trans 'Comment' %}
 | 
			
		||||
                </th>
 | 
			
		||||
                {% if is_default_member and is_entry_shared %}
 | 
			
		||||
                {% if is_default_member and has_access %}
 | 
			
		||||
                    <th class="w-10" scope="col">
 | 
			
		||||
                        <span class="float-right">
 | 
			
		||||
                            {% trans 'Action' %}
 | 
			
		||||
@ -64,7 +64,7 @@
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td class="align-middle float-right">
 | 
			
		||||
                        {% if is_default_member and is_entry_shared  %}
 | 
			
		||||
                        {% if is_default_member and has_access  %}
 | 
			
		||||
                        <button data-form-url="{% url 'intervention:edit-revocation' obj.id rev.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit revocation' %}">
 | 
			
		||||
                            {% fa5_icon 'edit' %}
 | 
			
		||||
                        </button>
 | 
			
		||||
 | 
			
		||||
@ -129,7 +129,7 @@
 | 
			
		||||
                                {% include 'user/includes/team_data_modal_button.html' %}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                            <hr>
 | 
			
		||||
                            {% if is_entry_shared %}
 | 
			
		||||
                            {% if has_access %}
 | 
			
		||||
                                {% for user in obj.users.all %}
 | 
			
		||||
                                    {% include 'user/includes/contact_modal_button.html' %}
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
 | 
			
		||||
@ -60,7 +60,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
 | 
			
		||||
        post_data = {
 | 
			
		||||
            "identifier": test_id,
 | 
			
		||||
            "title": test_title,
 | 
			
		||||
            "output": geom_json,
 | 
			
		||||
            "geom": geom_json,
 | 
			
		||||
        }
 | 
			
		||||
        response = self.client_user.post(
 | 
			
		||||
            new_url,
 | 
			
		||||
 | 
			
		||||
@ -62,7 +62,7 @@ class NewInterventionFormTestCase(BaseTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        geom_form_data = json.loads(geom_form_data)
 | 
			
		||||
        geom_form_data = {
 | 
			
		||||
            "output": json.dumps(geom_form_data)
 | 
			
		||||
            "geom": json.dumps(geom_form_data)
 | 
			
		||||
        }
 | 
			
		||||
        geom_form = SimpleGeomForm(geom_form_data)
 | 
			
		||||
 | 
			
		||||
@ -104,7 +104,7 @@ class EditInterventionFormTestCase(NewInterventionFormTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        geom_form_data = json.loads(geom_form_data)
 | 
			
		||||
        geom_form_data = {
 | 
			
		||||
            "output": json.dumps(geom_form_data)
 | 
			
		||||
            "geom": json.dumps(geom_form_data)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        geom_form = SimpleGeomForm(geom_form_data)
 | 
			
		||||
@ -124,7 +124,7 @@ class EditInterventionFormTestCase(NewInterventionFormTestCase):
 | 
			
		||||
        self.assertIsNotNone(obj.responsible.handler)
 | 
			
		||||
        self.assertEqual(obj.title, data["title"])
 | 
			
		||||
        self.assertEqual(obj.comment, data["comment"])
 | 
			
		||||
        self.assert_equal_geometries(test_geom, obj.geometry.geom)
 | 
			
		||||
        self.assertTrue(test_geom.equals_exact(obj.geometry.geom, 0.000001))
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(obj.legal.binding_date, today)
 | 
			
		||||
        self.assertEqual(obj.legal.registration_date, today)
 | 
			
		||||
 | 
			
		||||
@ -23,8 +23,7 @@ from konova.forms.modals import RemoveModalForm
 | 
			
		||||
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
 | 
			
		||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
 | 
			
		||||
from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \
 | 
			
		||||
    CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, \
 | 
			
		||||
    GEOMETRIES_IGNORED_TEMPLATE
 | 
			
		||||
    CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@login_required
 | 
			
		||||
@ -89,19 +88,11 @@ def new_view(request: HttpRequest):
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
            messages.success(request, _("Intervention {} added").format(intervention.identifier))
 | 
			
		||||
            if geom_form.has_geometry_simplified():
 | 
			
		||||
            if geom_form.geometry_simplified:
 | 
			
		||||
                messages.info(
 | 
			
		||||
                    request,
 | 
			
		||||
                    GEOMETRY_SIMPLIFIED
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            num_ignored_geometries = geom_form.get_num_geometries_ignored()
 | 
			
		||||
            if num_ignored_geometries > 0:
 | 
			
		||||
                messages.info(
 | 
			
		||||
                    request,
 | 
			
		||||
                    GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            return redirect("intervention:detail", id=intervention.id)
 | 
			
		||||
        else:
 | 
			
		||||
            messages.error(request, FORM_INVALID, extra_tags="danger",)
 | 
			
		||||
@ -194,7 +185,7 @@ def detail_view(request: HttpRequest, id: str):
 | 
			
		||||
        "last_checked": last_checked,
 | 
			
		||||
        "last_checked_tooltip": last_checked_tooltip,
 | 
			
		||||
        "compensations": compensations,
 | 
			
		||||
        "is_entry_shared": is_data_shared,
 | 
			
		||||
        "has_access": is_data_shared,
 | 
			
		||||
        "geom_form": geom_form,
 | 
			
		||||
        "is_default_member": _user.in_group(DEFAULT_GROUP),
 | 
			
		||||
        "is_zb_member": _user.in_group(ZB_GROUP),
 | 
			
		||||
@ -245,19 +236,11 @@ def edit_view(request: HttpRequest, id: str):
 | 
			
		||||
            messages.success(request, _("Intervention {} edited").format(intervention.identifier))
 | 
			
		||||
            if intervention_is_checked:
 | 
			
		||||
                messages.info(request, CHECK_STATE_RESET)
 | 
			
		||||
            if geom_form.has_geometry_simplified():
 | 
			
		||||
            if geom_form.geometry_simplified:
 | 
			
		||||
                messages.info(
 | 
			
		||||
                    request,
 | 
			
		||||
                    GEOMETRY_SIMPLIFIED
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            num_ignored_geometries = geom_form.get_num_geometries_ignored()
 | 
			
		||||
            if num_ignored_geometries > 0:
 | 
			
		||||
                messages.info(
 | 
			
		||||
                    request,
 | 
			
		||||
                    GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            return redirect("intervention:detail", id=intervention.id)
 | 
			
		||||
        else:
 | 
			
		||||
            messages.error(request, FORM_INVALID, extra_tags="danger",)
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from celery import Celery
 | 
			
		||||
from konova.sub_settings.django_settings import env
 | 
			
		||||
 | 
			
		||||
# Set the default Django settings module for the 'celery' program.
 | 
			
		||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'konova.settings')
 | 
			
		||||
@ -18,7 +17,7 @@ app.config_from_object('django.conf:settings', namespace='CELERY')
 | 
			
		||||
app.autodiscover_tasks()
 | 
			
		||||
 | 
			
		||||
# Declare redis as broker
 | 
			
		||||
app.conf.broker_url = f'redis://{env("REDIS_HOST")}:{env.int("REDIS_PORT")}/0'
 | 
			
		||||
app.conf.broker_url = 'redis://localhost:6379/0'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.task(bind=True)
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@ from uuid import UUID
 | 
			
		||||
 | 
			
		||||
from bootstrap_modal_forms.mixins import is_ajax
 | 
			
		||||
from django.contrib import messages
 | 
			
		||||
from django.core.exceptions import BadRequest
 | 
			
		||||
from django.http import Http404
 | 
			
		||||
from django.shortcuts import redirect, get_object_or_404, render
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
@ -185,7 +185,7 @@ def uuid_required(function):
 | 
			
		||||
        try:
 | 
			
		||||
            uuid = UUID(uuid)
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            raise BadRequest(
 | 
			
		||||
            raise Http404(
 | 
			
		||||
                "Invalid UUID"
 | 
			
		||||
            )
 | 
			
		||||
        return function(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
@ -1,55 +0,0 @@
 | 
			
		||||
"""
 | 
			
		||||
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
 | 
			
		||||
@ -14,7 +14,6 @@ from konova.filters.mixins.office import ConservationOfficeTableFilterMixin, Reg
 | 
			
		||||
from konova.filters.mixins.record import RecordableTableFilterMixin
 | 
			
		||||
from konova.filters.mixins.self_created import SelfCreatedTableFilterMixin
 | 
			
		||||
from konova.filters.mixins.share import ShareableTableFilterMixin
 | 
			
		||||
from konova.filters.mixins.user_log import UserLoggedTableFilterMixin
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AbstractTableFilter(django_filters.FilterSet):
 | 
			
		||||
@ -41,8 +40,7 @@ class SelectionTableFilter(RegistrationOfficeTableFilterMixin,
 | 
			
		||||
 | 
			
		||||
class QueryTableFilter(KeywordTableFilterMixin,
 | 
			
		||||
                       FileNumberTableFilterMixin,
 | 
			
		||||
                       GeoReferencedTableFilterMixin,
 | 
			
		||||
                       UserLoggedTableFilterMixin):
 | 
			
		||||
                       GeoReferencedTableFilterMixin):
 | 
			
		||||
    """ TableFilter holding different filter options for query related filtering
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
@ -25,16 +25,15 @@ class SimpleGeomForm(BaseForm):
 | 
			
		||||
    """ A geometry form for rendering geometry read-only using a widget
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    read_only: bool = True
 | 
			
		||||
    _geometry_simplified: bool = False
 | 
			
		||||
    output = JSONField(
 | 
			
		||||
    read_only = True
 | 
			
		||||
    geometry_simplified = False
 | 
			
		||||
    geom = JSONField(
 | 
			
		||||
        label=_("Geometry"),
 | 
			
		||||
        help_text=_(""),
 | 
			
		||||
        label_suffix="",
 | 
			
		||||
        required=False,
 | 
			
		||||
        disabled=False,
 | 
			
		||||
    )
 | 
			
		||||
    _num_geometries_ignored: int = 0
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        self.read_only = kwargs.pop("read_only", True)
 | 
			
		||||
@ -49,33 +48,33 @@ class SimpleGeomForm(BaseForm):
 | 
			
		||||
                raise AttributeError
 | 
			
		||||
 | 
			
		||||
            geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP)
 | 
			
		||||
            self._set_geojson_properties(geojson, title=self.instance.identifier or None)
 | 
			
		||||
            geom = json.dumps(geojson)
 | 
			
		||||
        except AttributeError:
 | 
			
		||||
            # If no geometry exists for this form, we simply set the value to None and zoom to the maximum level
 | 
			
		||||
            geom = ""
 | 
			
		||||
            self.empty = True
 | 
			
		||||
 | 
			
		||||
        self.initialize_form_field("output", geom)
 | 
			
		||||
        self.initialize_form_field("geom", geom)
 | 
			
		||||
 | 
			
		||||
    def is_valid(self):
 | 
			
		||||
        super().is_valid()
 | 
			
		||||
        is_valid = True
 | 
			
		||||
 | 
			
		||||
        # Get geojson from form
 | 
			
		||||
        geom = self.data.get("output", None)
 | 
			
		||||
        geom = self.data["geom"]
 | 
			
		||||
        if geom is None or len(geom) == 0:
 | 
			
		||||
            # empty geometry is a valid geometry
 | 
			
		||||
            self.cleaned_data["output"] = MultiPolygon(srid=DEFAULT_SRID_RLP).ewkt
 | 
			
		||||
            self.cleaned_data["geom"] = MultiPolygon(srid=DEFAULT_SRID_RLP).ewkt
 | 
			
		||||
            return is_valid
 | 
			
		||||
        geom = json.loads(geom)
 | 
			
		||||
 | 
			
		||||
        # Write submitted data back into form field to make sure invalid geometry
 | 
			
		||||
        # will be rendered again on failed submit
 | 
			
		||||
        self.initialize_form_field("output", self.data["output"])
 | 
			
		||||
        self.initialize_form_field("geom", self.data["geom"])
 | 
			
		||||
 | 
			
		||||
        # Initialize features list with empty MultiPolygon, so that an empty input will result in a
 | 
			
		||||
        # proper empty MultiPolygon object
 | 
			
		||||
        # Read geojson into gdal geometry
 | 
			
		||||
        # HINT: This can be simplified if the geojson format holds data in epsg:4326 (GDAL provides direct creation for
 | 
			
		||||
        # this case)
 | 
			
		||||
        features = []
 | 
			
		||||
        features_json = geom.get("features", [])
 | 
			
		||||
        accepted_ogr_types = [
 | 
			
		||||
@ -98,41 +97,33 @@ class SimpleGeomForm(BaseForm):
 | 
			
		||||
                g = self.__flatten_geom_to_2D(g)
 | 
			
		||||
 | 
			
		||||
            if g.geom_type not in accepted_ogr_types:
 | 
			
		||||
                self.add_error("output", _("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
 | 
			
		||||
                return is_valid
 | 
			
		||||
 | 
			
		||||
            is_area_valid = self.__is_area_valid(g)
 | 
			
		||||
            if not is_area_valid:
 | 
			
		||||
                # Geometries with an invalid size will not be saved to the db
 | 
			
		||||
                # We assume these are malicious snippets which are not supposed to be in the geometry in the first place
 | 
			
		||||
                self._num_geometries_ignored += 1
 | 
			
		||||
                continue
 | 
			
		||||
            is_valid &= self.__is_area_valid(g)
 | 
			
		||||
 | 
			
		||||
            g = Polygon.from_ewkt(g.ewkt)
 | 
			
		||||
            is_valid &= g.valid
 | 
			
		||||
            if not g.valid:
 | 
			
		||||
                self.add_error("output", g.valid_reason)
 | 
			
		||||
            polygon = Polygon.from_ewkt(g.ewkt)
 | 
			
		||||
            is_valid &= polygon.valid
 | 
			
		||||
            if not polygon.valid:
 | 
			
		||||
                self.add_error("geom", polygon.valid_reason)
 | 
			
		||||
                return is_valid
 | 
			
		||||
 | 
			
		||||
            if isinstance(g, Polygon):
 | 
			
		||||
                features.append(g)
 | 
			
		||||
            elif isinstance(g, MultiPolygon):
 | 
			
		||||
                features.extend(list(g))
 | 
			
		||||
            features.append(polygon)
 | 
			
		||||
 | 
			
		||||
        # Unionize all geometry features into one new MultiPolygon
 | 
			
		||||
        if features:
 | 
			
		||||
            form_geom = MultiPolygon(*features, srid=DEFAULT_SRID_RLP).unary_union
 | 
			
		||||
        else:
 | 
			
		||||
            form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
 | 
			
		||||
        form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
 | 
			
		||||
        for feature in features:
 | 
			
		||||
            form_geom = form_geom.union(feature)
 | 
			
		||||
 | 
			
		||||
        # Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
 | 
			
		||||
        form_geom = Geometry.cast_to_multipolygon(form_geom)
 | 
			
		||||
        if form_geom.geom_type != "MultiPolygon":
 | 
			
		||||
            form_geom = MultiPolygon(form_geom, srid=DEFAULT_SRID_RLP)
 | 
			
		||||
 | 
			
		||||
        # Write unioned Multipolygon into cleaned data
 | 
			
		||||
        if self.cleaned_data is None:
 | 
			
		||||
            self.cleaned_data = {}
 | 
			
		||||
        self.cleaned_data["output"] = form_geom.ewkt
 | 
			
		||||
        self.cleaned_data["geom"] = form_geom.ewkt
 | 
			
		||||
 | 
			
		||||
        return is_valid
 | 
			
		||||
 | 
			
		||||
@ -142,7 +133,7 @@ class SimpleGeomForm(BaseForm):
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        geom = self.cleaned_data.get("output")
 | 
			
		||||
        geom = self.cleaned_data.get("geom")
 | 
			
		||||
        g = gdal.OGRGeometry(geom, srs=DEFAULT_SRID_RLP)
 | 
			
		||||
        num_vertices = g.num_coords
 | 
			
		||||
 | 
			
		||||
@ -155,6 +146,15 @@ class SimpleGeomForm(BaseForm):
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        is_area_valid = geom.area > 1  # > 1m² (SRID:25832)
 | 
			
		||||
 | 
			
		||||
        if not is_area_valid:
 | 
			
		||||
            self.add_error(
 | 
			
		||||
                "geom",
 | 
			
		||||
                _("Geometry must be greater than 1m². Currently is {}m²").format(
 | 
			
		||||
                    float(geom.area)
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return is_area_valid
 | 
			
		||||
 | 
			
		||||
    def __simplify_geometry(self, geom, max_vert: int):
 | 
			
		||||
@ -192,14 +192,14 @@ class SimpleGeomForm(BaseForm):
 | 
			
		||||
            if self.instance is None or self.instance.geometry is None:
 | 
			
		||||
                raise LookupError
 | 
			
		||||
            geometry = self.instance.geometry
 | 
			
		||||
            geometry.geom = self.cleaned_data.get("output", MultiPolygon(srid=DEFAULT_SRID_RLP))
 | 
			
		||||
            geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP))
 | 
			
		||||
            geometry.modified = action
 | 
			
		||||
 | 
			
		||||
            geometry.save()
 | 
			
		||||
        except LookupError:
 | 
			
		||||
            # No geometry or linked instance holding a geometry exist --> create a new one!
 | 
			
		||||
            geometry = Geometry.objects.create(
 | 
			
		||||
                geom=self.cleaned_data.get("output", MultiPolygon(srid=DEFAULT_SRID_RLP)),
 | 
			
		||||
                geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)),
 | 
			
		||||
                created=action,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
@ -207,29 +207,13 @@ class SimpleGeomForm(BaseForm):
 | 
			
		||||
        if not is_vertices_num_valid:
 | 
			
		||||
            geometry.geom = self.__simplify_geometry(geometry.geom, max_vert=GEOM_MAX_VERTICES)
 | 
			
		||||
            geometry.save()
 | 
			
		||||
            self._geometry_simplified = True
 | 
			
		||||
            self.geometry_simplified = True
 | 
			
		||||
 | 
			
		||||
        # Start parcel update and geometry conflict checking procedure in a background process
 | 
			
		||||
        celery_update_parcels.delay(geometry.id)
 | 
			
		||||
        celery_check_for_geometry_conflicts.delay(geometry.id)
 | 
			
		||||
        return geometry
 | 
			
		||||
 | 
			
		||||
    def get_num_geometries_ignored(self):
 | 
			
		||||
        """ Returns the number of geometries which had to be ignored for various reasons
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        return self._num_geometries_ignored
 | 
			
		||||
 | 
			
		||||
    def has_geometry_simplified(self):
 | 
			
		||||
        """ Returns whether the geometry has been simplified or not.
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        return self._geometry_simplified
 | 
			
		||||
 | 
			
		||||
    def __flatten_geom_to_2D(self, geom):
 | 
			
		||||
        """
 | 
			
		||||
        Enforces a given OGRGeometry from higher dimensions into 2D
 | 
			
		||||
@ -239,20 +223,3 @@ class SimpleGeomForm(BaseForm):
 | 
			
		||||
        g_wkt = wkt_w.write(geom.geos).decode("utf-8")
 | 
			
		||||
        geom = gdal.OGRGeometry(g_wkt)
 | 
			
		||||
        return geom
 | 
			
		||||
 | 
			
		||||
    def _set_geojson_properties(self, geojson: dict, title: str = None):
 | 
			
		||||
        """ Toggles the editable property of the geojson for proper handling in map client
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            geojson (dict): The GeoJson
 | 
			
		||||
            title (str): An alternative title for the geometry
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            geojson (dict): The altered GeoJson
 | 
			
		||||
        """
 | 
			
		||||
        features = geojson.get("features", [])
 | 
			
		||||
        for feature in features:
 | 
			
		||||
            feature["properties"]["editable"] = not self.read_only
 | 
			
		||||
            if title:
 | 
			
		||||
                feature["properties"]["title"] = title
 | 
			
		||||
        return geojson
 | 
			
		||||
 | 
			
		||||
@ -6,11 +6,11 @@ Created on: 26.10.22
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
import zipfile
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from io import BytesIO
 | 
			
		||||
 | 
			
		||||
from django.core.mail import EmailMessage
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from django.utils.datetime_safe import datetime
 | 
			
		||||
 | 
			
		||||
from analysis.utils.excel.excel import TempExcelFile
 | 
			
		||||
from analysis.utils.report import TimespanReport
 | 
			
		||||
 | 
			
		||||
@ -1,88 +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, 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()
 | 
			
		||||
@ -1,54 +0,0 @@
 | 
			
		||||
"""
 | 
			
		||||
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)")
 | 
			
		||||
							
								
								
									
										51
									
								
								konova/management/commands/test_identifier_generating.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								konova/management/commands/test_identifier_generating.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
			
		||||
"""
 | 
			
		||||
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")
 | 
			
		||||
							
								
								
									
										54
									
								
								konova/management/commands/update_all_parcels.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								konova/management/commands/update_all_parcels.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,54 @@
 | 
			
		||||
"""
 | 
			
		||||
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()
 | 
			
		||||
@ -8,10 +8,9 @@ Created on: 15.11.21
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
from django.contrib.gis.db.models import MultiPolygonField
 | 
			
		||||
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
 | 
			
		||||
from django.core.exceptions import ObjectDoesNotExist
 | 
			
		||||
from django.db import models, transaction
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from django.contrib.gis.geos import MultiPolygon
 | 
			
		||||
 | 
			
		||||
from konova.models import BaseResource, UuidModel
 | 
			
		||||
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
 | 
			
		||||
@ -224,17 +223,6 @@ class Geometry(BaseResource):
 | 
			
		||||
                )
 | 
			
		||||
                parcel_obj.updated_on = _now
 | 
			
		||||
                parcels_to_update.append(parcel_obj)
 | 
			
		||||
            except MultipleObjectsReturned:
 | 
			
		||||
                parcel_obj = Parcel.make_unique(
 | 
			
		||||
                    district=district,
 | 
			
		||||
                    municipal=municipal,
 | 
			
		||||
                    parcel_group=parcel_group,
 | 
			
		||||
                    flr=flr_val,
 | 
			
		||||
                    flrstck_nnr=flrstck_nnr,
 | 
			
		||||
                    flrstck_zhlr=flrstck_zhlr,
 | 
			
		||||
                )
 | 
			
		||||
                parcel_obj.updated_on = _now
 | 
			
		||||
                parcels_to_update.append(parcel_obj)
 | 
			
		||||
            except ObjectDoesNotExist:
 | 
			
		||||
                # If not existing, create object but do not commit, yet
 | 
			
		||||
                parcel_obj = Parcel(
 | 
			
		||||
@ -343,7 +331,6 @@ class Geometry(BaseResource):
 | 
			
		||||
                {
 | 
			
		||||
                    "type": "Feature",
 | 
			
		||||
                    "geometry": json.loads(p.json),
 | 
			
		||||
                    "properties": {},
 | 
			
		||||
                 }
 | 
			
		||||
                for p in polygons
 | 
			
		||||
            ]
 | 
			
		||||
@ -379,42 +366,13 @@ class Geometry(BaseResource):
 | 
			
		||||
        diff = geom_envelope - self.geom
 | 
			
		||||
 | 
			
		||||
        if diff.area == 0:
 | 
			
		||||
            complexity_factor = 1
 | 
			
		||||
            ratio = 1
 | 
			
		||||
        else:
 | 
			
		||||
            complexity_factor = self.geom.area / diff.area
 | 
			
		||||
            ratio = self.geom.area / diff.area
 | 
			
		||||
 | 
			
		||||
        complexity_factor = 1 - ratio
 | 
			
		||||
        return complexity_factor
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def cast_to_multipolygon(input_geom):
 | 
			
		||||
        """ If input_geom is not a MultiPolygon, cast to MultiPolygon
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            input_geom ():
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            output_geom
 | 
			
		||||
        """
 | 
			
		||||
        output_geom = input_geom
 | 
			
		||||
        if not isinstance(input_geom, MultiPolygon):
 | 
			
		||||
            output_geom = MultiPolygon(input_geom, srid=DEFAULT_SRID_RLP)
 | 
			
		||||
        return output_geom
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def cast_to_rlp_srid(input_geom):
 | 
			
		||||
        """ If input_geom is not of RLP SRID (25832), cast to RLP SRID
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            input_geom ():
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            output_geom
 | 
			
		||||
        """
 | 
			
		||||
        output_geom = input_geom
 | 
			
		||||
        if output_geom.srid != DEFAULT_SRID_RLP:
 | 
			
		||||
            output_geom.transform(DEFAULT_SRID_RLP)
 | 
			
		||||
        return output_geom
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GeometryConflict(UuidModel):
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ Contact: michel.peltriaux@sgdnord.rlp.de
 | 
			
		||||
Created on: 16.12.21
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from django.db import models, transaction
 | 
			
		||||
from django.db import models
 | 
			
		||||
 | 
			
		||||
from konova.models import UuidModel
 | 
			
		||||
 | 
			
		||||
@ -158,46 +158,6 @@ class Parcel(UuidModel):
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.parcel_group} | {self.flr} | {self.flrstck_zhlr} | {self.flrstck_nnr}"
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def make_unique(cls, **kwargs):
 | 
			
		||||
        """ Checks for duplicates of a Parcel, choose a (now) unique one,
 | 
			
		||||
        repairs relations for ParcelIntersection and removes duplicates.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            **kwargs ():
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            unique_true (Parcel): The new unique 'true one'
 | 
			
		||||
        """
 | 
			
		||||
        parcel_objs = Parcel.objects.filter(**kwargs)
 | 
			
		||||
 | 
			
		||||
        if not parcel_objs.exists():
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        # Get one of the found parcels and use it as new 'true one'
 | 
			
		||||
        unique_parcel = parcel_objs.first()
 | 
			
		||||
        # separate it from the rest
 | 
			
		||||
        parcel_objs = parcel_objs.exclude(id=unique_parcel.id)
 | 
			
		||||
 | 
			
		||||
        if not parcel_objs.exists():
 | 
			
		||||
            # There are no duplicates - all good, just return
 | 
			
		||||
            return unique_parcel
 | 
			
		||||
 | 
			
		||||
        # Fetch existing intersections, which still point on the duplicated parcels
 | 
			
		||||
        intersection_objs = ParcelIntersection.objects.filter(
 | 
			
		||||
            parcel__in=parcel_objs
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Change each intersection, so they point on the 'true one' parcel from now on
 | 
			
		||||
        for intersection in intersection_objs:
 | 
			
		||||
            intersection.parcel = unique_parcel
 | 
			
		||||
            intersection.save()
 | 
			
		||||
 | 
			
		||||
        # Remove the duplicated parcels
 | 
			
		||||
        parcel_objs.delete()
 | 
			
		||||
 | 
			
		||||
        return unique_parcel
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ParcelIntersection(UuidModel):
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@ from konova.sub_settings.proxy_settings import *
 | 
			
		||||
from konova.sub_settings.sso_settings import *
 | 
			
		||||
from konova.sub_settings.table_settings import *
 | 
			
		||||
from konova.sub_settings.lanis_settings import *
 | 
			
		||||
from konova.sub_settings.wfs_parcel_settings import *
 | 
			
		||||
from konova.sub_settings.logging_settings import *
 | 
			
		||||
 | 
			
		||||
# Max upload size for POST forms
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										78
									
								
								konova/sso/sso.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								konova/sso/sso.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,78 @@
 | 
			
		||||
"""
 | 
			
		||||
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
 | 
			
		||||
@ -278,18 +278,4 @@ Similar to bootstraps 'shadow-lg'
 | 
			
		||||
}
 | 
			
		||||
.alert{
 | 
			
		||||
    margin-bottom: 0 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Overwrites netgis.css attributes
 | 
			
		||||
 */
 | 
			
		||||
.netgis-gradient-a{
 | 
			
		||||
    /*
 | 
			
		||||
    Overwrites gradient used on default css of netgis map client
 | 
			
		||||
     */
 | 
			
		||||
    background: var(--rlp-red) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.netgis-menu{
 | 
			
		||||
    z-index: 1 !important;
 | 
			
		||||
}
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 1.6 MiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 1.7 MiB  | 
@ -10,8 +10,6 @@ For the full list of settings and their values, see
 | 
			
		||||
https://docs.djangoproject.com/en/3.1/ref/settings/
 | 
			
		||||
"""
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
import environ
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from django.conf.locale.de import formats as de_formats
 | 
			
		||||
 | 
			
		||||
@ -26,28 +24,32 @@ BASE_DIR = os.path.dirname(
 | 
			
		||||
    )
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
env = environ.Env()
 | 
			
		||||
# Take environment variables from .env.dev file
 | 
			
		||||
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
 | 
			
		||||
# Quick-start development settings - unsuitable for production
 | 
			
		||||
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
 | 
			
		||||
 | 
			
		||||
# SECURITY WARNING: keep the secret key used in production secret!
 | 
			
		||||
SECRET_KEY = env("SECRET_KEY")
 | 
			
		||||
SECRET_KEY = '5=9-)2)h$u9=!zrhia9=lj-2#cpcb8=#$7y+)l$5tto$3q(n_+'
 | 
			
		||||
 | 
			
		||||
# SECURITY WARNING: don't run with debug turned on in production!
 | 
			
		||||
DEBUG = env.bool("DEBUG", default=False)
 | 
			
		||||
DEBUG = True
 | 
			
		||||
 | 
			
		||||
ADMINS = [x.split(':') for x in env.list('ADMINS')]
 | 
			
		||||
ADMINS = [
 | 
			
		||||
    ('KSP-Servicestelle', 'ksp-servicestelle@sgdnord.rlp.de'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS")
 | 
			
		||||
BASE_URL = "http://localhost:8001"
 | 
			
		||||
 | 
			
		||||
BASE_URL = env("BASE_URL")
 | 
			
		||||
ALLOWED_HOSTS = [
 | 
			
		||||
    "127.0.0.1",
 | 
			
		||||
    "localhost",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
CSRF_TRUSTED_ORIGINS = [
 | 
			
		||||
    BASE_URL
 | 
			
		||||
    "http://localhost",  # not only host but schema (http/s) as well!
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
# Authentication settings
 | 
			
		||||
LOGIN_URL = "/oauth/login/"
 | 
			
		||||
LOGIN_URL = "/login/"
 | 
			
		||||
 | 
			
		||||
# Session settings
 | 
			
		||||
SESSION_COOKIE_AGE = 60 * 60  # 60 minutes
 | 
			
		||||
@ -66,6 +68,7 @@ INSTALLED_APPS = [
 | 
			
		||||
    'django.contrib.staticfiles',
 | 
			
		||||
    'django.contrib.gis',
 | 
			
		||||
    'django.contrib.humanize',
 | 
			
		||||
    'simple_sso.sso_server',
 | 
			
		||||
    'django_tables2',
 | 
			
		||||
    'bootstrap_modal_forms',
 | 
			
		||||
    'fontawesome_5',
 | 
			
		||||
@ -80,6 +83,10 @@ INSTALLED_APPS = [
 | 
			
		||||
    'analysis',
 | 
			
		||||
    'api',
 | 
			
		||||
]
 | 
			
		||||
if DEBUG:
 | 
			
		||||
    INSTALLED_APPS += [
 | 
			
		||||
        'debug_toolbar',
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
MIDDLEWARE = [
 | 
			
		||||
    'django.middleware.security.SecurityMiddleware',
 | 
			
		||||
@ -91,6 +98,10 @@ MIDDLEWARE = [
 | 
			
		||||
    'django.contrib.messages.middleware.MessageMiddleware',
 | 
			
		||||
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
 | 
			
		||||
]
 | 
			
		||||
if DEBUG:
 | 
			
		||||
    MIDDLEWARE += [
 | 
			
		||||
        "debug_toolbar.middleware.DebugToolbarMiddleware",
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
ROOT_URLCONF = 'konova.urls'
 | 
			
		||||
 | 
			
		||||
@ -120,11 +131,10 @@ WSGI_APPLICATION = 'konova.wsgi.application'
 | 
			
		||||
DATABASES = {
 | 
			
		||||
    'default': {
 | 
			
		||||
        'ENGINE': 'django.contrib.gis.db.backends.postgis',
 | 
			
		||||
        'NAME': env("DB_NAME"),
 | 
			
		||||
        'USER': env("DB_USER"),
 | 
			
		||||
        'PASSWORD': env("DB_PASSWORD"),
 | 
			
		||||
        'HOST': env("DB_HOST"),
 | 
			
		||||
        'PORT': env("DB_PORT"),
 | 
			
		||||
        'NAME': 'konova',
 | 
			
		||||
        'USER': 'postgres',
 | 
			
		||||
        'HOST': '127.0.0.1',
 | 
			
		||||
        'PORT': '5432',
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
 | 
			
		||||
@ -188,8 +198,31 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static")
 | 
			
		||||
STATICFILES_DIRS = [
 | 
			
		||||
    os.path.join(BASE_DIR, 'konova/static'),
 | 
			
		||||
    os.path.join(BASE_DIR, 'templates/map/client'),         # NETGIS map client files
 | 
			
		||||
    os.path.join(BASE_DIR, 'templates/map/client/libs'),    # NETGIS map client files
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
# 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/)
 | 
			
		||||
 | 
			
		||||
# CHANGE_ME !!! ONLY FOR DEVELOPMENT !!!
 | 
			
		||||
@ -197,10 +230,13 @@ if DEBUG:
 | 
			
		||||
    EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
 | 
			
		||||
    EMAIL_FILE_PATH = '/tmp/app-messages' # change this to a proper location
 | 
			
		||||
 | 
			
		||||
DEFAULT_FROM_EMAIL = env("DEFAULT_FROM_EMAIL")  # The default email address for the 'from' element
 | 
			
		||||
DEFAULT_FROM_EMAIL = "service@ksp.de"  # 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
 | 
			
		||||
EMAIL_HOST = env("SMTP_HOST")
 | 
			
		||||
EMAIL_REPLY_TO = env("REPLY_TO_ADDR")
 | 
			
		||||
EMAIL_PORT = env("SMTP_PORT")
 | 
			
		||||
EMAIL_HOST = "localhost"
 | 
			
		||||
EMAIL_REPLY_TO = "ksp-servicestelle@sgdnord.rlp.de"
 | 
			
		||||
SUPPORT_MAIL_RECIPIENT = EMAIL_REPLY_TO
 | 
			
		||||
EMAIL_PORT = "25"
 | 
			
		||||
#EMAIL_HOST_USER = ""
 | 
			
		||||
#EMAIL_HOST_PASSWORD = ""
 | 
			
		||||
EMAIL_USE_TLS = False
 | 
			
		||||
EMAIL_USE_SSL = False
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,6 @@ Contact: michel.peltriaux@sgdnord.rlp.de
 | 
			
		||||
Created on: 31.01.22
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from konova.sub_settings.django_settings import env
 | 
			
		||||
 | 
			
		||||
# MAPS
 | 
			
		||||
DEFAULT_LAT = 50.00
 | 
			
		||||
@ -29,6 +28,3 @@ LANIS_ZOOM_LUT = {
 | 
			
		||||
    1000: 30,
 | 
			
		||||
    500: 31,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
MAP_PROXY_HOST_WHITELIST = env.list("MAP_PROXY_HOST_WHITELIST")
 | 
			
		||||
i = 0
 | 
			
		||||
@ -5,13 +5,12 @@ Contact: michel.peltriaux@sgdnord.rlp.de
 | 
			
		||||
Created on: 31.01.22
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from konova.sub_settings.django_settings import env
 | 
			
		||||
 | 
			
		||||
proxy = env("PROXY")
 | 
			
		||||
proxy = ""
 | 
			
		||||
PROXIES = {
 | 
			
		||||
    "http": proxy,
 | 
			
		||||
    "https": proxy,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
GEOPORTAL_RLP_USER = env("GEOPORTAL_RLP_USER")
 | 
			
		||||
GEOPORTAL_RLP_PASSWORD = env("GEOPORTAL_RLP_PASSWORD")
 | 
			
		||||
CLIENT_PROXY_AUTH_USER = "CHANGE_ME"
 | 
			
		||||
CLIENT_PROXY_AUTH_PASSWORD = "CHANGE_ME"
 | 
			
		||||
@ -5,8 +5,7 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
 | 
			
		||||
Created on: 14.12.22
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from konova.sub_settings.django_settings import env
 | 
			
		||||
 | 
			
		||||
base_url = env("SCHNEIDER_BASE_URL")
 | 
			
		||||
auth_header = env("SCHNEIDER_AUTH_HEADER")
 | 
			
		||||
auth_header_token = env("SCHNEIDER_AUTH_TOKEN")
 | 
			
		||||
base_url = "http://127.0.0.1:8002"
 | 
			
		||||
auth_header = "auth"
 | 
			
		||||
auth_header_token = "CHANGE_ME"
 | 
			
		||||
 | 
			
		||||
@ -5,16 +5,9 @@ Contact: michel.peltriaux@sgdnord.rlp.de
 | 
			
		||||
Created on: 31.01.22
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from konova.sub_settings.django_settings import env
 | 
			
		||||
 | 
			
		||||
# SSO settings
 | 
			
		||||
SSO_SERVER_BASE = env("SSO_SERVER_BASE_URL")
 | 
			
		||||
SSO_SERVER_BASE = "http://127.0.0.1:8000/"
 | 
			
		||||
SSO_SERVER = f"{SSO_SERVER_BASE}sso/"
 | 
			
		||||
 | 
			
		||||
# 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")
 | 
			
		||||
SSO_PRIVATE_KEY = "CHANGE_ME"
 | 
			
		||||
SSO_PUBLIC_KEY = "CHANGE_ME"
 | 
			
		||||
							
								
								
									
										12
									
								
								konova/sub_settings/wfs_parcel_settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								konova/sub_settings/wfs_parcel_settings.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
"""
 | 
			
		||||
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"
 | 
			
		||||
@ -7,7 +7,7 @@ from django.core.exceptions import ObjectDoesNotExist
 | 
			
		||||
 | 
			
		||||
@shared_task
 | 
			
		||||
def celery_update_parcels(geometry_id: str, recheck: bool = True):
 | 
			
		||||
    from konova.models import Geometry
 | 
			
		||||
    from konova.models import Geometry, ParcelIntersection
 | 
			
		||||
    try:
 | 
			
		||||
        geom = Geometry.objects.get(id=geometry_id)
 | 
			
		||||
        geom.parcels.clear()
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
            <div class="scroll-150 font-italic">
 | 
			
		||||
                {{obj.comment|linebreaks}}
 | 
			
		||||
                {{obj.comment}}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -124,7 +124,6 @@ class GeometryTestCase(BaseTestCase):
 | 
			
		||||
                {
 | 
			
		||||
                    "type": "Feature",
 | 
			
		||||
                    "geometry": json.loads(p.json),
 | 
			
		||||
                    "properties": {}
 | 
			
		||||
                }
 | 
			
		||||
                for p in polygons
 | 
			
		||||
            ]
 | 
			
		||||
 | 
			
		||||
@ -469,7 +469,7 @@ class BaseTestCase(TestCase):
 | 
			
		||||
        eco_account.save()
 | 
			
		||||
        return eco_account
 | 
			
		||||
 | 
			
		||||
    def assert_equal_geometries(self, geom1: MultiPolygon, geom2: MultiPolygon, tolerance=0.001):
 | 
			
		||||
    def assert_equal_geometries(self, geom1: MultiPolygon, geom2: MultiPolygon, tolerance = 0.001):
 | 
			
		||||
        """ Assert for geometries to be equal
 | 
			
		||||
 | 
			
		||||
        Transforms the geometries to matching srids before checking
 | 
			
		||||
@ -491,10 +491,7 @@ class BaseTestCase(TestCase):
 | 
			
		||||
            # transformation from one coordinate system into the other, which is valid
 | 
			
		||||
            geom1.transform(geom2.srid)
 | 
			
		||||
            geom2.transform(geom1.srid)
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            geom1.equals_exact(geom2, tolerance=tolerance),
 | 
			
		||||
            msg=f"Difference is {abs(geom1.area - geom2.area)} with {geom1.area} and {geom2.area} in a tolerance of {tolerance}"
 | 
			
		||||
        )
 | 
			
		||||
        self.assertTrue(geom1.equals_exact(geom2, tolerance) or geom2.equals_exact(geom1, tolerance))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseViewTestCase(BaseTestCase):
 | 
			
		||||
@ -512,7 +509,7 @@ class BaseViewTestCase(BaseTestCase):
 | 
			
		||||
        
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        super().setUp()
 | 
			
		||||
        self.login_url = reverse("oauth-login")
 | 
			
		||||
        self.login_url = reverse("simple-sso-login")
 | 
			
		||||
 | 
			
		||||
    def assert_url_success(self, client: Client, urls: list):
 | 
			
		||||
        """ Assert for all given urls a direct 200 response
 | 
			
		||||
 | 
			
		||||
@ -13,19 +13,21 @@ Including another URLconf
 | 
			
		||||
    1. Import the include() function: from django.urls import include, path
 | 
			
		||||
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
 | 
			
		||||
"""
 | 
			
		||||
import debug_toolbar
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
from django.urls import path, include
 | 
			
		||||
 | 
			
		||||
from konova.settings import SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY, DEBUG
 | 
			
		||||
from konova.sso.sso import KonovaSSOClient
 | 
			
		||||
from konova.views.logout import LogoutView
 | 
			
		||||
from konova.views.geometry import GeomParcelsView, GeomParcelsContentView
 | 
			
		||||
from konova.views.home import HomeView
 | 
			
		||||
from konova.views.map_proxy import ClientProxyParcelSearch, ClientProxyParcelWFS
 | 
			
		||||
from konova.views.oauth import OAuthLoginView, OAuthCallbackView
 | 
			
		||||
 | 
			
		||||
sso_client = KonovaSSOClient(SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY)
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path('admin/', admin.site.urls),
 | 
			
		||||
    path('oauth/callback/', OAuthCallbackView.as_view(), name="oauth-callback"),
 | 
			
		||||
    path('oauth/login/', OAuthLoginView.as_view(), name="oauth-login"),
 | 
			
		||||
    path('login/', include(sso_client.get_urls())),
 | 
			
		||||
    path('logout/', LogoutView.as_view(), name="logout"),
 | 
			
		||||
    path('', HomeView.as_view(), name="home"),
 | 
			
		||||
    path('intervention/', include("intervention.urls")),
 | 
			
		||||
@ -42,6 +44,10 @@ urlpatterns = [
 | 
			
		||||
    path('client/proxy/wfs', ClientProxyParcelWFS.as_view(), name="client-proxy-wfs"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
handler400 = "konova.views.error.get_400_view"
 | 
			
		||||
if DEBUG:
 | 
			
		||||
    urlpatterns += [
 | 
			
		||||
        path('__debug__/', include(debug_toolbar.urls)),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
handler404 = "konova.views.error.get_404_view"
 | 
			
		||||
handler500 = "konova.views.error.get_500_view"
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ from django.core.mail import send_mail
 | 
			
		||||
from django.template.loader import render_to_string
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from konova.sub_settings.django_settings import DEFAULT_FROM_EMAIL, EMAIL_REPLY_TO
 | 
			
		||||
from konova.sub_settings.django_settings import DEFAULT_FROM_EMAIL, EMAIL_REPLY_TO, SUPPORT_MAIL_RECIPIENT
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Mailer:
 | 
			
		||||
@ -416,7 +416,7 @@ class Mailer:
 | 
			
		||||
            "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
 | 
			
		||||
        }
 | 
			
		||||
        msg = render_to_string("email/api/verify_token.html", context)
 | 
			
		||||
        user_mail_address = [EMAIL_REPLY_TO]
 | 
			
		||||
        user_mail_address = [SUPPORT_MAIL_RECIPIENT]
 | 
			
		||||
        self.send(
 | 
			
		||||
            user_mail_address,
 | 
			
		||||
            _("Request for new API token"),
 | 
			
		||||
 | 
			
		||||
@ -83,7 +83,6 @@ EDITED_GENERAL_DATA = _("Edited general data")
 | 
			
		||||
# Geometry
 | 
			
		||||
GEOMETRY_CONFLICT_WITH_TEMPLATE = _("Geometry conflict detected with {}")
 | 
			
		||||
GEOMETRY_SIMPLIFIED = _("The geometry contained more than {} vertices. It had to be simplified to match the allowed limit of {} vertices.").format(GEOM_MAX_VERTICES, GEOM_MAX_VERTICES)
 | 
			
		||||
GEOMETRIES_IGNORED_TEMPLATE = _("The geometry contained {} parts which have been detected as invalid (e.g. too small to be valid). These parts have been removed. Please check the stored geometry.")
 | 
			
		||||
 | 
			
		||||
# INTERVENTION
 | 
			
		||||
INTERVENTION_HAS_REVOCATIONS_TEMPLATE = _("This intervention has {} revocations")
 | 
			
		||||
@ -92,6 +91,3 @@ INTERVENTION_HAS_REVOCATIONS_TEMPLATE = _("This intervention has {} revocations"
 | 
			
		||||
DATA_CHECKED_ON_TEMPLATE = _("Checked on {} by {}")
 | 
			
		||||
DATA_CHECKED_PREVIOUSLY_TEMPLATE = _("Data has changed since last check on {} by {}")
 | 
			
		||||
DATA_IS_UNCHECKED = _("Current data not checked yet")
 | 
			
		||||
 | 
			
		||||
# API TOKEN SETTINGS
 | 
			
		||||
NEW_API_TOKEN_GENERATED = _("New token generated. Administrators need to validate.")
 | 
			
		||||
							
								
								
									
										78
									
								
								konova/utils/messenger.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								konova/utils/messenger.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,78 @@
 | 
			
		||||
"""
 | 
			
		||||
Author: Michel Peltriaux
 | 
			
		||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
 | 
			
		||||
Contact: michel.peltriaux@sgdnord.rlp.de
 | 
			
		||||
Created on: 17.08.21
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from collections import Iterable
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
from user.models import User
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from konova.settings import SSO_SERVER_BASE, SSO_PUBLIC_KEY, PROXIES
 | 
			
		||||
from konova.sub_settings.context_settings import BASE_TITLE_SHORT
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Messenger:
 | 
			
		||||
    """ Used to send messages to the SSO server.
 | 
			
		||||
 | 
			
		||||
    Messages can be seen by the user the next time they login on their SSO dashboard.
 | 
			
		||||
    Documentation for SSO Server-Client communication can be found here:
 | 
			
		||||
    https://git.naturschutz.rlp.de/SGD-Nord/arnova/wiki/Messages
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    server_url = "{}communication/message/".format(SSO_SERVER_BASE)
 | 
			
		||||
 | 
			
		||||
    def __init__(self, users: Iterable, subject: str = None, body: str = None, type: str = None):
 | 
			
		||||
        self.users = users
 | 
			
		||||
        self.msg_subject = subject
 | 
			
		||||
        self.msg_body = body
 | 
			
		||||
        self.msg_type = type
 | 
			
		||||
 | 
			
		||||
    def send(self):
 | 
			
		||||
        """ Sends a message
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        if self.msg_body is None or len(self.msg_body) == 0:
 | 
			
		||||
            raise AttributeError("No message body set")
 | 
			
		||||
 | 
			
		||||
        headers = {
 | 
			
		||||
            "x-services-public-key": SSO_PUBLIC_KEY
 | 
			
		||||
        }
 | 
			
		||||
        for user in self.users:
 | 
			
		||||
            data = {
 | 
			
		||||
                "type": self.msg_type,
 | 
			
		||||
                "sender": BASE_TITLE_SHORT,
 | 
			
		||||
                "receiver": user.username,
 | 
			
		||||
                "subject": self.msg_subject,
 | 
			
		||||
                "body": self.msg_body,
 | 
			
		||||
            }
 | 
			
		||||
            requests.post(
 | 
			
		||||
                self.server_url,
 | 
			
		||||
                data=data,
 | 
			
		||||
                headers=headers,
 | 
			
		||||
                proxies=PROXIES
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def send_object_checked(self, obj_identifier: str, performing_user: User, detail_view_url: str = ""):
 | 
			
		||||
        """ Wraps sending of a message related to the checking of an object, like an intervention
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            obj_identifier (str): The object's identifier (e.g. 'EIV-123'
 | 
			
		||||
            performing_user (User): The user who performed the checking
 | 
			
		||||
            detail_view_url (str): If a direct link to the object shall be added to the message, it can be provided here
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        self.msg_subject = _("{} checked").format(obj_identifier)
 | 
			
		||||
        if len(detail_view_url) > 0:
 | 
			
		||||
            detail_view_url = _('<a href="{}">Check it out</a>').format(detail_view_url)
 | 
			
		||||
        self.msg_body = _("{} has been checked successfully by user {}! {}").format(
 | 
			
		||||
            obj_identifier,
 | 
			
		||||
            performing_user.username,
 | 
			
		||||
            detail_view_url
 | 
			
		||||
        )
 | 
			
		||||
        self.send()
 | 
			
		||||
@ -11,8 +11,6 @@ from json import JSONDecodeError
 | 
			
		||||
import requests
 | 
			
		||||
 | 
			
		||||
from konova.sub_settings import schneider_settings
 | 
			
		||||
from konova.sub_settings.lanis_settings import DEFAULT_SRID
 | 
			
		||||
from konova.sub_settings.proxy_settings import PROXIES
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ParcelFetcher:
 | 
			
		||||
@ -35,7 +33,6 @@ class ParcelFetcher:
 | 
			
		||||
        if geom.area < buffer_threshold:
 | 
			
		||||
            # Fallback for malicious geometries which are way too small and would disappear on negative buffering
 | 
			
		||||
            geom = geometry.geom
 | 
			
		||||
        geom.transform(DEFAULT_SRID)
 | 
			
		||||
        self.geojson = geom.ewkt
 | 
			
		||||
        self.results = []
 | 
			
		||||
 | 
			
		||||
@ -46,7 +43,6 @@ class ParcelFetcher:
 | 
			
		||||
 | 
			
		||||
        response = requests.post(
 | 
			
		||||
            url=post_url,
 | 
			
		||||
            proxies=PROXIES,
 | 
			
		||||
            data=self.geojson,
 | 
			
		||||
            headers={
 | 
			
		||||
                self.auth_header: self.auth_header_token
 | 
			
		||||
@ -57,11 +53,11 @@ class ParcelFetcher:
 | 
			
		||||
            content = json.loads(response.content.decode("utf-8"))
 | 
			
		||||
        except JSONDecodeError:
 | 
			
		||||
            content = {}
 | 
			
		||||
        _next = content.get("next", None)
 | 
			
		||||
        next = content.get("next", None)
 | 
			
		||||
        fetched_parcels = content.get("results", [])
 | 
			
		||||
        self.results += fetched_parcels
 | 
			
		||||
 | 
			
		||||
        if _next:
 | 
			
		||||
            self.get_parcels(_next)
 | 
			
		||||
        if next:
 | 
			
		||||
            self.get_parcels(next)
 | 
			
		||||
 | 
			
		||||
        return self.results
 | 
			
		||||
@ -216,11 +216,11 @@ class TableRenderMixin:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        html = ""
 | 
			
		||||
        is_entry_shared = record.is_shared_with(self.user)
 | 
			
		||||
        has_access = record.is_shared_with(self.user)
 | 
			
		||||
 | 
			
		||||
        html += self.render_icn(
 | 
			
		||||
            tooltip=_("Full access granted") if is_entry_shared else _("Access not granted"),
 | 
			
		||||
            icn_class="fas fa-edit rlp-r-inv" if is_entry_shared else "far fa-edit",
 | 
			
		||||
            tooltip=_("Full access granted") if has_access else _("Access not granted"),
 | 
			
		||||
            icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit",
 | 
			
		||||
        )
 | 
			
		||||
        return format_html(html)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -25,20 +25,6 @@ def get_404_view(request: HttpRequest, exception=None):
 | 
			
		||||
    return render(request, "404.html", context, status=404)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_400_view(request: HttpRequest, exception=None):
 | 
			
		||||
    """ Returns a 400 handling view
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        request ():
 | 
			
		||||
        exception ():
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    context = BaseContext.context
 | 
			
		||||
    return render(request, "400.html", context, status=400)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_500_view(request: HttpRequest):
 | 
			
		||||
    """ Returns a 404 handling view
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
 | 
			
		||||
Created on: 19.08.22
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from django.contrib.auth.mixins import LoginRequiredMixin
 | 
			
		||||
from django.contrib.gis.geos import MultiPolygon
 | 
			
		||||
from django.http import HttpResponse, HttpRequest
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user