Compare commits

..

No commits in common. "master" and "v1.0" have entirely different histories.
master ... v1.0

303 changed files with 10302 additions and 9104 deletions

View File

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

3
.gitignore vendored
View File

@ -1,6 +1,3 @@
# Project exclude paths # Project exclude paths
/venv/ /venv/
/.idea/ /.idea/
/.coverage
/htmlcov/
/.env

View File

@ -13,7 +13,6 @@ from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID
from konova.forms import BaseForm from konova.forms import BaseForm
from konova.utils import validators
class TimespanReportForm(BaseForm): class TimespanReportForm(BaseForm):
@ -23,7 +22,6 @@ class TimespanReportForm(BaseForm):
date_from = forms.DateField( date_from = forms.DateField(
label_suffix="", label_suffix="",
label=_("From"), label=_("From"),
validators=[validators.reasonable_date],
help_text=_("Entries created from..."), help_text=_("Entries created from..."),
widget=forms.DateInput( widget=forms.DateInput(
attrs={ attrs={
@ -37,7 +35,6 @@ class TimespanReportForm(BaseForm):
date_to = forms.DateField( date_to = forms.DateField(
label_suffix="", label_suffix="",
label=_("To"), label=_("To"),
validators=[validators.reasonable_date],
help_text=_("Entries created until..."), help_text=_("Entries created until..."),
widget=forms.DateInput( widget=forms.DateInput(
attrs={ attrs={

View File

@ -9,8 +9,4 @@ Created on: 19.10.21
# Defines the date of the legal publishing of the LKompVzVo # Defines the date of the legal publishing of the LKompVzVo
from django.utils import timezone from django.utils import timezone
LKOMPVZVO_PUBLISH_DATE = timezone.make_aware( LKOMPVZVO_PUBLISH_DATE = timezone.make_aware(timezone.datetime.fromisoformat("2018-06-16"))
timezone.datetime.fromisoformat(
"2018-06-16"
)
).date()

View File

@ -31,6 +31,6 @@
{% include 'analysis/reports/includes/intervention/card_intervention.html' %} {% include 'analysis/reports/includes/intervention/card_intervention.html' %}
{% include 'analysis/reports/includes/compensation/card_compensation.html' %} {% include 'analysis/reports/includes/compensation/card_compensation.html' %}
{% include 'analysis/reports/includes/eco_account/card_eco_account.html' %} {% include 'analysis/reports/includes/eco_account/card_eco_account.html' %}
{% include 'analysis/reports/includes/old_data/card_old_data.html' %} {% include 'analysis/reports/includes/old_data/card_old_interventions.html' %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -10,7 +10,6 @@
{% fa5_icon 'leaf' %} {% fa5_icon 'leaf' %}
{% trans 'Compensations' %} {% trans 'Compensations' %}
</h5> </h5>
<span>{% trans 'Binding date after' %} 16.06.2018</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -10,7 +10,6 @@
{% fa5_icon 'tree' %} {% fa5_icon 'tree' %}
{% trans 'Eco-Accounts' %} {% trans 'Eco-Accounts' %}
</h5> </h5>
<span>{% trans 'Binding date after' %} 16.06.2018</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -9,7 +9,6 @@
{% fa5_icon 'pencil-ruler' %} {% fa5_icon 'pencil-ruler' %}
{% trans 'Interventions' %} {% trans 'Interventions' %}
</h5> </h5>
<span>{% trans 'Binding date after' %} 16.06.2018</span>
</div> </div>
</div> </div>
</div> </div>

3
analysis/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,7 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 15.08.23
"""

View File

@ -1,7 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 15.08.23
"""

View File

@ -1,47 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 15.08.23
"""
from datetime import timedelta
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from analysis.forms import TimespanReportForm
from konova.tests.test_views import BaseTestCase
class TimeSpanReportFormTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
eiv = self.create_dummy_intervention()
def test_init(self):
form = TimespanReportForm()
self.assertEqual(form.form_title, str(_("Generate report")))
self.assertEqual(form.form_caption, str(_("Select a timespan and the desired conservation office") ))
self.assertEqual(form.action_url, reverse("analysis:reports"))
self.assertFalse(form.show_cancel_btn)
self.assertEqual(form.action_btn_label, str(_("Continue")))
def test_save(self):
date_from = now().date() - timedelta(days=365)
date_to = now().date()
office = self.get_conservation_office_code()
data = {
"date_from": date_from,
"date_to": date_to,
"conservation_office": office,
}
form = TimespanReportForm(data)
self.assertTrue(form.is_valid(), msg=f"{form.errors}")
detail_report_url = form.save()
self.assertEqual(
detail_report_url,
reverse("analysis:report-detail", args=(office.id,)) + f"?df={date_from}&dt={date_to}"
)

View File

@ -1,98 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 17.08.23
"""
from datetime import timedelta
from django.utils.timezone import now
from analysis.settings import LKOMPVZVO_PUBLISH_DATE
from analysis.utils.report import TimespanReport
from konova.sub_settings.django_settings import DEFAULT_DATE_FORMAT
from konova.tests.test_views import BaseTestCase
class TimeSpanReportTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
today = now().date()
old_date = LKOMPVZVO_PUBLISH_DATE - timedelta(days=1)
self.conservation_office = self.get_conservation_office_code()
self.eiv_old = self.create_dummy_intervention()
self.kom_old = self.create_dummy_compensation(interv=self.eiv_old)
self.assertNotEqual(self.compensation.intervention, self.kom_old.intervention)
self.eiv = self.compensation.intervention
self.oek_old = self.create_dummy_eco_account()
self.eiv_old.responsible.conservation_office = self.conservation_office
self.eiv_old.legal.binding_date = old_date
self.eiv_old.legal.registration_date = old_date
self.eiv.responsible.conservation_office = self.conservation_office
self.eiv.legal.binding_date = today
self.eiv.legal.registration_date = today
self.eco_account.responsible.conservation_office = self.conservation_office
self.eco_account.legal.registration_date = today
self.eco_account.legal.binding_date = today
self.oek_old.responsible.conservation_office = self.conservation_office
self.oek_old.legal.registration_date = old_date
self.oek_old.legal.binding_date = old_date
self.eiv.legal.save()
self.eiv.responsible.save()
self.eiv_old.legal.save()
self.eiv_old.responsible.save()
self.eco_account.legal.save()
self.eco_account.responsible.save()
self.oek_old.legal.save()
self.oek_old.responsible.save()
self.deduction.account = self.eco_account
self.deduction.intervention = self.eiv
self.deduction.save()
def test_init(self):
date_from = now().date() - timedelta(days=365)
date_to = now().date()
report = TimespanReport(self.conservation_office.id, date_from, date_to)
self.assertEqual(report.office_id, self.conservation_office.id)
self.assertEqual(report.date_from, date_from)
self.assertEqual(report.date_to, date_to)
self.assertIsNotNone(report.intervention_report)
self.assertIsNotNone(report.compensation_report)
self.assertIsNotNone(report.eco_account_report)
self.assertIsNotNone(report.old_data_report)
self.assertEqual(report.excel_map["date_from"], date_from.strftime(DEFAULT_DATE_FORMAT))
self.assertEqual(report.excel_map["date_to"], date_to.strftime(DEFAULT_DATE_FORMAT))
self.assertEqual(report.old_data_report.queryset_intervention_count, 1)
self.assertEqual(report.old_data_report.queryset_intervention_recorded_count, 0)
self.assertEqual(report.old_data_report.queryset_comps_count, 1)
self.assertEqual(report.old_data_report.queryset_acc_count, 1)
self.assertEqual(report.old_data_report.queryset_acc_recorded_count, 0)
self.assertEqual(report.intervention_report.queryset_count, 1)
self.assertEqual(report.intervention_report.queryset_checked_count, 0)
self.assertEqual(report.intervention_report.queryset_recorded_count, 0)
self.assertEqual(report.compensation_report.queryset_count, 1)
self.assertEqual(report.compensation_report.queryset_checked_count, 0)
self.assertEqual(report.compensation_report.queryset_recorded_count, 0)
self.assertEqual(report.eco_account_report.queryset_count, 1)
self.assertEqual(report.eco_account_report.queryset_recorded_count, 0)
self.assertEqual(report.eco_account_report.queryset_deductions_count, 1)
self.assertEqual(report.eco_account_report.queryset_deductions_recorded_count, 0)

View File

@ -17,7 +17,6 @@ from compensation.models import Compensation, Payment, EcoAccountDeduction, EcoA
from intervention.models import Intervention from intervention.models import Intervention
from konova.models import Geometry from konova.models import Geometry
from konova.sub_settings.django_settings import BASE_DIR, DEFAULT_DATE_FORMAT from konova.sub_settings.django_settings import BASE_DIR, DEFAULT_DATE_FORMAT
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
class TimespanReport: class TimespanReport:
@ -104,9 +103,9 @@ class TimespanReport:
"iterable": self.evaluated_laws, "iterable": self.evaluated_laws,
"attrs": [ "attrs": [
"short_name", "short_name",
"num",
"num_checked", "num_checked",
"num_recorded", "num_recorded",
"num",
] ]
}, },
"i_laws_checked": self.law_sum_checked, "i_laws_checked": self.law_sum_checked,
@ -334,7 +333,7 @@ class TimespanReport:
return Geometry.objects.filter( return Geometry.objects.filter(
id__in=ids id__in=ids
).annotate( ).annotate(
geom_cast=Cast("geom", MultiPolygonField(srid=DEFAULT_SRID_RLP)) geom_cast=Cast("geom", MultiPolygonField())
).annotate( ).annotate(
num=NumGeometries("geom_cast") num=NumGeometries("geom_cast")
).aggregate( ).aggregate(
@ -413,7 +412,6 @@ class TimespanReport:
def __init__(self, id: str, date_from: str, date_to: str): def __init__(self, id: str, date_from: str, date_to: str):
# First fetch all eco account for this office # First fetch all eco account for this office
self.queryset = EcoAccount.objects.filter( self.queryset = EcoAccount.objects.filter(
legal__registration_date__gt=LKOMPVZVO_PUBLISH_DATE,
responsible__conservation_office__id=id, responsible__conservation_office__id=id,
deleted=None, deleted=None,
created__timestamp__date__gte=date_from, created__timestamp__date__gte=date_from,
@ -517,8 +515,8 @@ class TimespanReport:
legal__registration_date__lte=LKOMPVZVO_PUBLISH_DATE, legal__registration_date__lte=LKOMPVZVO_PUBLISH_DATE,
responsible__conservation_office__id=id, responsible__conservation_office__id=id,
deleted=None, deleted=None,
created__timestamp__date__gte=date_from, created__timestamp__gte=date_from,
created__timestamp__date__lte=date_to, created__timestamp__lte=date_to,
) )
self.queryset_acc_recorded = self.queryset_acc.filter( self.queryset_acc_recorded = self.queryset_acc.filter(
recorded__isnull=False, recorded__isnull=False,

View File

@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from api.models.token import APIUserToken, OAuthToken from api.models.token import APIUserToken
class APITokenAdmin(admin.ModelAdmin): 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(APIUserToken, APITokenAdmin)
admin.site.register(OAuthToken, OAuthTokenAdmin)

View File

@ -1,18 +0,0 @@
# Generated by Django 4.2.6 on 2023-11-30 11:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='apiusertoken',
name='valid_until',
field=models.DateField(blank=True, help_text='Token is only valid until this date. Forever if null/blank.', null=True),
),
]

View File

@ -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,
},
),
]

View File

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

View File

@ -1,7 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 17.08.23
"""

View File

@ -1,71 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 17.08.23
"""
from datetime import timedelta
from django.utils.timezone import now
from api.models import APIUserToken
from konova.tests.test_views import BaseTestCase
class APIUserTokenTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.token = APIUserToken.objects.create()
self.superuser.api_token = self.token
self.superuser.save()
def test_str(self):
self.assertEqual(str(self.token), self.token.token)
def test_get_user_from_token(self):
a_day = timedelta(days=1)
today = now().date()
self.assertFalse(self.token.is_active)
self.assertIsNone(self.token.valid_until)
try:
#Token not existing --> fail
token_user = APIUserToken.get_user_from_token(self.token.token[::-1])
self.fail("There should not have been any token")
except PermissionError:
pass
try:
# Token not active --> fail
token_user = APIUserToken.get_user_from_token(self.token.token)
self.fail("Token is unverified but token user has been fetchable.")
except PermissionError:
pass
self.token.is_active = True
self.token.valid_until = today - a_day
self.token.save()
try:
# Token valid until yesterday --> fail
token_user = APIUserToken.get_user_from_token(self.token.token)
self.fail("Token reached end of lifetime but token user has been fetchable.")
except PermissionError:
pass
# Token valid until tomorrow --> success
self.token.valid_until = today + a_day
self.token.save()
token_user = APIUserToken.get_user_from_token(self.token.token)
self.assertEqual(token_user, self.superuser)
del token_user
# Token valid forever --> success
self.token.valid_until = None
self.token.save()
token_user = APIUserToken.get_user_from_token(self.token.token)
self.assertEqual(token_user, self.superuser)

View File

@ -1,5 +1,5 @@
{ {
"eco_account": "CHANGE_BEFORE_RUN!!!", "eco_account": "CHANGE_BEFORE_RUN!!!",
"surface": 500.50, "surface": 500.0,
"intervention": "CHANGE_BEFORE_RUN!!!" "intervention": "CHANGE_BEFORE_RUN!!!"
} }

View File

@ -4,6 +4,7 @@ from django.urls import reverse
from konova.settings import DEFAULT_GROUP from konova.settings import DEFAULT_GROUP
from konova.tests.test_views import BaseTestCase from konova.tests.test_views import BaseTestCase
from konova.utils.user_checks import is_default_group_only
class BaseAPIV1TestCase(BaseTestCase): class BaseAPIV1TestCase(BaseTestCase):
@ -137,7 +138,7 @@ class APIV1SharingTestCase(BaseAPIV1TestCase):
# Give the user only default group rights # Give the user only default group rights
default_group = self.groups.get(name=DEFAULT_GROUP) default_group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([default_group]) self.superuser.groups.set([default_group])
self.assertTrue(self.superuser.is_default_group_only()) self.assertTrue(is_default_group_only(self.superuser))
# Add only him as shared_users an object # Add only him as shared_users an object
self.intervention.users.set([self.superuser]) self.intervention.users.set([self.superuser])

View File

@ -1,5 +1,5 @@
{ {
"eco_account": "CHANGE_BEFORE_RUN!!!", "eco_account": "CHANGE_BEFORE_RUN!!!",
"surface": 523400.50, "surface": 523400.0,
"intervention": "CHANGE_BEFORE_RUN!!!" "intervention": "CHANGE_BEFORE_RUN!!!"
} }

View File

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

View File

@ -6,13 +6,12 @@ Created on: 24.01.22
""" """
from django.db import transaction from django.db import transaction
from django.db.models import Q
from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin
from compensation.models import Compensation from compensation.models import Compensation
from intervention.models import Intervention from intervention.models import Intervention
from konova.models import Geometry from konova.models import Geometry
from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts from konova.tasks import celery_update_parcels
from konova.utils.message_templates import DATA_UNSHARED from konova.utils.message_templates import DATA_UNSHARED
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@ -22,10 +21,8 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
def prepare_lookup(self, id, user): def prepare_lookup(self, id, user):
super().prepare_lookup(id, user) super().prepare_lookup(id, user)
self.shared_lookup = Q( del self.lookup["users__in"]
Q(intervention__users__in=[user]) | self.lookup["intervention__users__in"] = [user]
Q(intervention__teams__in=user.shared_teams)
)
def intervention_to_json(self, entry): def intervention_to_json(self, entry):
return { return {
@ -67,7 +64,6 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
obj = Compensation() obj = Compensation()
created = create_action created = create_action
obj.created = created obj.created = created
obj.modified = created
obj.geometry = geometry obj.geometry = geometry
return obj return obj
@ -132,7 +128,6 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
obj.log.add(obj.created) obj.log.add(obj.created)
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)
celery_check_for_geometry_conflicts.delay(obj.geometry.id)
return obj.id return obj.id

View File

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

View File

@ -13,7 +13,7 @@ from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_
from compensation.models import EcoAccount from compensation.models import EcoAccount
from intervention.models import Legal, Responsibility, Handler from intervention.models import Legal, Responsibility, Handler
from konova.models import Geometry from konova.models import Geometry
from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts from konova.tasks import celery_update_parcels
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@ -103,7 +103,6 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
obj.legal = Legal() obj.legal = Legal()
created = create_action created = create_action
obj.created = created obj.created = created
obj.modified = created
obj.geometry = geometry obj.geometry = geometry
return obj return obj
@ -151,7 +150,6 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
obj.users.add(user) obj.users.add(user)
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)
celery_check_for_geometry_conflicts.delay(obj.geometry.id)
return obj.id return obj.id

View File

@ -13,7 +13,7 @@ from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_
from ema.models import Ema from ema.models import Ema
from intervention.models import Responsibility, Handler from intervention.models import Responsibility, Handler
from konova.models import Geometry from konova.models import Geometry
from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts from konova.tasks import celery_update_parcels
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@ -85,7 +85,6 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
) )
created = create_action created = create_action
obj.created = created obj.created = created
obj.modified = created
obj.geometry = geometry obj.geometry = geometry
return obj return obj
@ -123,7 +122,6 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
obj.users.add(user) obj.users.add(user)
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)
celery_check_for_geometry_conflicts.delay(obj.geometry.id)
return obj.id return obj.id

View File

@ -13,7 +13,7 @@ from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, \
from compensation.models import Payment from compensation.models import Payment
from intervention.models import Intervention, Responsibility, Legal, Handler from intervention.models import Intervention, Responsibility, Legal, Handler
from konova.models import Geometry from konova.models import Geometry
from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts from konova.tasks import celery_update_parcels
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@ -76,7 +76,6 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
created = create_action created = create_action
obj.legal = legal obj.legal = legal
obj.created = created obj.created = created
obj.modified = created
obj.geometry = geometry obj.geometry = geometry
obj.responsible = resp obj.responsible = resp
return obj return obj
@ -133,6 +132,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
id__in=payments id__in=payments
) )
obj.payments.set(payments) obj.payments.set(payments)
obj.send_data_to_egon()
return obj return obj
def create_model_from_json(self, json_model, user): def create_model_from_json(self, json_model, user):
@ -165,7 +165,6 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
obj.log.add(obj.created) obj.log.add(obj.created)
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)
celery_check_for_geometry_conflicts.delay(obj.geometry.id)
return obj.id return obj.id
@ -200,7 +199,6 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
obj.save() obj.save()
obj.mark_as_edited(user, edit_comment="API update") obj.mark_as_edited(user, edit_comment="API update")
obj.send_data_to_egon()
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)

View File

@ -9,15 +9,13 @@ Created on: 24.01.22
import json import json
from django.contrib.gis.geos import MultiPolygon from django.contrib.gis.geos import MultiPolygon
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import QuerySet from django.db.models import QuerySet
from api.utils.serializer.serializer import AbstractModelAPISerializer from api.utils.serializer.serializer import AbstractModelAPISerializer
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_PROCESS_TYPE_ID, \ from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_PROCESS_TYPE_ID, \
CODELIST_LAW_ID, CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, \ CODELIST_LAW_ID, CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, \
CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_HANDLER_ID, \ CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, CODELIST_HANDLER_ID
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
from compensation.models import CompensationAction, UnitChoices, CompensationState from compensation.models import CompensationAction, UnitChoices, CompensationState
from intervention.models import Responsibility, Legal, Handler from intervention.models import Responsibility, Legal, Handler
from konova.models import Deadline, DeadlineType from konova.models import Deadline, DeadlineType
@ -62,7 +60,7 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer):
if konova_code is None: if konova_code is None:
return None return None
return { return {
"id": konova_code.id, "atom_id": konova_code.atom_id,
"long_name": konova_code.long_name, "long_name": konova_code.long_name,
"short_name": konova_code.short_name, "short_name": konova_code.short_name,
} }
@ -71,7 +69,7 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer):
""" Returns a konova code instance """ Returns a konova code instance
Args: Args:
json_str (str): The value for the code (id) json_str (str): The value for the code (atom id)
code_list_identifier (str): From which konova code list this code is supposed to be from code_list_identifier (str): From which konova code list this code is supposed to be from
Returns: Returns:
@ -82,14 +80,10 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer):
json_str = str(json_str) json_str = str(json_str)
if len(json_str) == 0: if len(json_str) == 0:
return None return None
try:
code = KonovaCode.objects.get( code = KonovaCode.objects.get(
id=json_str, atom_id=json_str,
code_lists__in=[code_list_identifier] code_lists__in=[code_list_identifier]
) )
except ObjectDoesNotExist as e:
msg = f"{e.args[0]} ({json_str} not found in official list {code_list_identifier})"
raise ObjectDoesNotExist(msg)
return code return code
def _created_on_to_json(self, entry): def _created_on_to_json(self, entry):
@ -298,12 +292,9 @@ class AbstractCompensationAPISerializerV1Mixin:
""" """
deadlines = [] deadlines = []
for entry in deadline_data: for entry in deadline_data:
try:
deadline_type = entry["type"] deadline_type = entry["type"]
date = entry["date"] date = entry["date"]
comment = entry["comment"] comment = entry["comment"]
except KeyError:
raise ValueError(f"Invalid deadline content. Content was {entry} but should follow the specification")
# Check on validity # Check on validity
if deadline_type not in DeadlineType: if deadline_type not in DeadlineType:
@ -345,14 +336,11 @@ class AbstractCompensationAPISerializerV1Mixin:
""" """
states = [] states = []
for entry in states_data: for entry in states_data:
try:
biotope_type = entry["biotope"] biotope_type = entry["biotope"]
biotope_details = [ 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"]) surface = float(entry["surface"])
except KeyError:
raise ValueError(f"Invalid biotope content. Content was {entry} but should follow the specification ")
# Check on validity # Check on validity
if surface <= 0: if surface <= 0:
@ -361,7 +349,7 @@ class AbstractCompensationAPISerializerV1Mixin:
# If this exact data is already existing, we do not create it new. Instead put it's id in the list of # If this exact data is already existing, we do not create it new. Instead put it's id in the list of
# entries, we will use to set the new actions # entries, we will use to set the new actions
state = states_manager.filter( state = states_manager.filter(
biotope_type__id=biotope_type, biotope_type__atom_id=biotope_type,
surface=surface, surface=surface,
).exclude( ).exclude(
id__in=states id__in=states
@ -392,7 +380,6 @@ class AbstractCompensationAPISerializerV1Mixin:
""" """
actions = [] actions = []
for entry in actions_data: for entry in actions_data:
try:
action_types = [ action_types = [
self._konova_code_from_json(e, CODELIST_COMPENSATION_ACTION_ID) for e in entry["action_types"] self._konova_code_from_json(e, CODELIST_COMPENSATION_ACTION_ID) for e in entry["action_types"]
] ]
@ -400,11 +387,8 @@ class AbstractCompensationAPISerializerV1Mixin:
self._konova_code_from_json(e, CODELIST_COMPENSATION_ACTION_DETAIL_ID) for e in entry["action_details"] self._konova_code_from_json(e, CODELIST_COMPENSATION_ACTION_DETAIL_ID) for e in entry["action_details"]
] ]
amount = float(entry["amount"]) amount = float(entry["amount"])
# Mapping of old "qm" into "m²" unit = entry["unit"]
unit = UnitChoices.m2.value if entry["unit"] == "qm" else entry["unit"]
comment = entry["comment"] comment = entry["comment"]
except KeyError:
raise ValueError(f"Invalid action content. Content was {entry} but should follow specification")
# Check on validity # Check on validity
if amount <= 0: if amount <= 0:

View File

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

View File

@ -18,6 +18,7 @@ from compensation.models import EcoAccount
from ema.models import Ema from ema.models import Ema
from intervention.models import Intervention from intervention.models import Intervention
from konova.utils.message_templates import DATA_UNSHARED from konova.utils.message_templates import DATA_UNSHARED
from konova.utils.user_checks import is_default_group_only
from user.models import User, Team from user.models import User, Team
@ -50,20 +51,9 @@ class AbstractAPIView(View):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try: try:
# Fetch the proper user from the given request header token # Fetch the proper user from the given request header token
token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None) ksp_token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None) ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None)
self.user = APIUserToken.get_user_from_token(ksp_token, ksp_user)
if not token and not ksp_user:
bearer_token = request.headers.get("authorization", None)
if not bearer_token:
raise PermissionError("No token provided")
token = bearer_token.split(" ")[1]
token_user = APIUserToken.get_user_from_token(token)
if ksp_user and ksp_user != token_user.username:
raise PermissionError(f"Invalid token for {ksp_user}")
self.user = token_user
request.user = self.user request.user = self.user
if not self.user.is_default_user(): if not self.user.is_default_user():
raise PermissionError("Default permissions required") raise PermissionError("Default permissions required")
@ -325,7 +315,7 @@ class AbstractModelShareAPIView(AbstractAPIView):
for team_name in new_teams: for team_name in new_teams:
new_teams_objs.append(Team.objects.get(name=team_name)) new_teams_objs.append(Team.objects.get(name=team_name))
if self.user.is_default_group_only(): if is_default_group_only(self.user):
# Default only users are not allowed to remove other users from having access. They can only add new ones! # Default only users are not allowed to remove other users from having access. They can only add new ones!
new_users_to_be_added = User.objects.filter( new_users_to_be_added = User.objects.filter(
username__in=new_users username__in=new_users

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -81,15 +81,13 @@ class EcoAccountAdmin(AbstractCompensationAdmin):
] ]
filter_horizontal = [ filter_horizontal = [
"users", "users"
"teams",
] ]
def get_fields(self, request, obj=None): def get_fields(self, request, obj=None):
return super().get_fields(request, obj) + [ return super().get_fields(request, obj) + [
"deductable_surface", "deductable_surface",
"users", "users"
"teams",
] ]
@ -148,7 +146,7 @@ class CompensationActionAdmin(admin.ModelAdmin):
admin.site.register(Compensation, CompensationAdmin) admin.site.register(Compensation, CompensationAdmin)
admin.site.register(EcoAccount, EcoAccountAdmin) admin.site.register(EcoAccount, EcoAccountAdmin)
#admin.site.register(EcoAccountDeduction, EcoAccountDeductionAdmin) admin.site.register(EcoAccountDeduction, EcoAccountDeductionAdmin)
# For a more cleaner admin interface these rarely used admin views are not important for deployment # For a more cleaner admin interface these rarely used admin views are not important for deployment
#admin.site.register(Payment, PaymentAdmin) #admin.site.register(Payment, PaymentAdmin)

View File

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

View File

@ -55,12 +55,10 @@ class CheckboxCompensationTableFilter(CheckboxTableFilter):
""" """
if not value: if not value:
user_teams = self.user.shared_teams return queryset.filter(
result = queryset.filter(
Q(intervention__users__in=[self.user]) | # requesting user has access Q(intervention__users__in=[self.user]) | # requesting user has access
Q(intervention__teams__in=user_teams) Q(intervention__teams__in=self.user.shared_teams)
).distinct() ).distinct()
return result
else: else:
return queryset return queryset

View File

@ -30,12 +30,11 @@ class AbstractCompensationForm(BaseForm):
label=_("Identifier"), label=_("Identifier"),
label_suffix="", label_suffix="",
max_length=255, max_length=255,
help_text=_("Generated automatically - not editable"), help_text=_("Generated automatically"),
widget=GenerateInput( widget=GenerateInput(
attrs={ attrs={
"class": "form-control", "class": "form-control",
"url": None, # Needs to be set in inheriting constructors "url": None, # Needs to be set in inheriting constructors
"readonly": True,
} }
) )
) )
@ -130,11 +129,12 @@ class NewCompensationForm(AbstractCompensationForm,
self.initialize_form_field("identifier", identifier) self.initialize_form_field("identifier", identifier)
self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:new-id") self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:new-id")
def __create_comp(self, user): def __create_comp(self, user, geom_form) -> Compensation:
""" Creates the compensation from form data """ Creates the compensation from form data
Args: Args:
user (User): The performing user user (User): The performing user
geom_form (SimpleGeomForm): The geometry form
Returns: Returns:
comp (Compensation): The compensation object comp (Compensation): The compensation object
@ -150,6 +150,8 @@ class NewCompensationForm(AbstractCompensationForm,
# Create log entry # Create log entry
action = UserActionLogEntry.get_created_action(user) action = UserActionLogEntry.get_created_action(user)
# Process the geometry form
geometry = geom_form.save(action)
# Finally create main object # Finally create main object
comp = Compensation.objects.create( comp = Compensation.objects.create(
@ -157,27 +159,21 @@ class NewCompensationForm(AbstractCompensationForm,
title=title, title=title,
intervention=intervention, intervention=intervention,
created=action, created=action,
modified=action,
is_cef=is_cef, is_cef=is_cef,
is_coherence_keeping=is_coherence_keeping, is_coherence_keeping=is_coherence_keeping,
is_pik=is_pik, is_pik=is_pik,
geometry=geometry,
comment=comment, comment=comment,
) )
# Add the log entry to the main objects log list # Add the log entry to the main objects log list
comp.log.add(action) comp.log.add(action)
return comp, action return comp
def save(self, user: User, geom_form: SimpleGeomForm): def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic(): with transaction.atomic():
comp, action = self.__create_comp(user) comp = self.__create_comp(user, geom_form)
comp.intervention.mark_as_edited(user, edit_comment=COMPENSATION_ADDED_TEMPLATE.format(comp.identifier)) comp.intervention.mark_as_edited(user, edit_comment=COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
# Process the geometry form
geometry = geom_form.save(action)
comp.geometry = geometry
comp.save()
return comp return comp
@ -209,10 +205,8 @@ class EditCompensationForm(NewCompensationForm):
def save(self, user: User, geom_form: SimpleGeomForm): def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic(): with transaction.atomic():
# Create log entry
action = UserActionLogEntry.get_edited_action(user)
# Fetch data from cleaned POST values # Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None) title = self.cleaned_data.get("title", None)
intervention = self.cleaned_data.get("intervention", None) intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None) is_cef = self.cleaned_data.get("is_cef", None)
@ -220,8 +214,17 @@ class EditCompensationForm(NewCompensationForm):
is_pik = self.cleaned_data.get("is_pik", None) is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None) comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.get_edited_action(user)
# Process the geometry form
geometry = geom_form.save(action)
# Finally create main object
self.instance.identifier = identifier
self.instance.title = title self.instance.title = title
self.instance.intervention = intervention self.instance.intervention = intervention
self.instance.geometry = geometry
self.instance.is_cef = is_cef self.instance.is_cef = is_cef
self.instance.is_coherence_keeping = is_coherence_keeping self.instance.is_coherence_keeping = is_coherence_keeping
self.instance.comment = comment self.instance.comment = comment
@ -230,11 +233,6 @@ class EditCompensationForm(NewCompensationForm):
self.instance.save() self.instance.save()
self.instance.log.add(action) self.instance.log.add(action)
intervention.mark_as_edited(user, self.request, EDITED_GENERAL_DATA) intervention.mark_as_edited(user, self.request, EDITED_GENERAL_DATA)
# Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
geometry = geom_form.save(action)
self.instance.geometry = geometry
self.instance.save()
return self.instance return self.instance

View File

@ -14,8 +14,6 @@ from compensation.forms.mixins import CompensationResponsibleFormMixin, PikCompe
from compensation.models import EcoAccount from compensation.models import EcoAccount
from intervention.models import Handler, Responsibility, Legal from intervention.models import Handler, Responsibility, Legal
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm
from konova.utils import validators
from user.models import User, UserActionLogEntry from user.models import User, UserActionLogEntry
@ -44,7 +42,6 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
label_suffix="", label_suffix="",
help_text=_("When did the parties agree on this?"), help_text=_("When did the parties agree on this?"),
required=False, required=False,
validators=[validators.reasonable_date],
widget=forms.DateInput( widget=forms.DateInput(
attrs={ attrs={
"type": "date", "type": "date",
@ -96,6 +93,8 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
# Create log entry # Create log entry
action = UserActionLogEntry.get_created_action(user) action = UserActionLogEntry.get_created_action(user)
# Process the geometry form
geometry = geom_form.save(action)
handler = Handler.objects.create( handler = Handler.objects.create(
type=handler_type, type=handler_type,
@ -119,7 +118,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
responsible=responsible, responsible=responsible,
deductable_surface=surface, deductable_surface=surface,
created=action, created=action,
modified=action, geometry=geometry,
comment=comment, comment=comment,
is_pik=is_pik, is_pik=is_pik,
legal=legal legal=legal
@ -128,12 +127,6 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
# Add the log entry to the main objects log list # Add the log entry to the main objects log list
acc.log.add(action) acc.log.add(action)
# Process the geometry form
geometry = geom_form.save(action)
acc.geometry = geometry
acc.save()
acc.update_deductable_rest()
return acc return acc
@ -172,26 +165,10 @@ class EditEcoAccountForm(NewEcoAccountForm):
disabled_fields disabled_fields
) )
def is_valid(self):
valid = super().is_valid()
deductable_surface = self.cleaned_data.get("surface") or 0.0
deduction_surface_sum = self.instance.get_deductions_surface()
if deductable_surface < deduction_surface_sum:
self.add_error(
"surface",
_("{}m² have been deducted from this eco account so far. The given value of {} would be too low.").format(
deduction_surface_sum,
deductable_surface
)
)
valid &= False
return valid
def save(self, user: User, geom_form: SimpleGeomForm): def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic(): with transaction.atomic():
# Fetch data from cleaned POST values # Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None) title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None) registration_date = self.cleaned_data.get("registration_date", None)
handler_type = self.cleaned_data.get("handler_type", None) handler_type = self.cleaned_data.get("handler_type", None)
@ -205,6 +182,9 @@ class EditEcoAccountForm(NewEcoAccountForm):
# Create log entry # Create log entry
action = UserActionLogEntry.get_edited_action(user) action = UserActionLogEntry.get_edited_action(user)
# Process the geometry form
geometry = geom_form.save(action)
# Update responsible data # Update responsible data
self.instance.responsible.handler.type = handler_type self.instance.responsible.handler.type = handler_type
self.instance.responsible.handler.detail = handler_detail self.instance.responsible.handler.detail = handler_detail
@ -218,8 +198,10 @@ class EditEcoAccountForm(NewEcoAccountForm):
self.instance.legal.save() self.instance.legal.save()
# Update main oject data # Update main oject data
self.instance.identifier = identifier
self.instance.title = title self.instance.title = title
self.instance.deductable_surface = surface self.instance.deductable_surface = surface
self.instance.geometry = geometry
self.instance.comment = comment self.instance.comment = comment
self.instance.is_pik = is_pik self.instance.is_pik = is_pik
self.instance.modified = action self.instance.modified = action
@ -227,23 +209,4 @@ class EditEcoAccountForm(NewEcoAccountForm):
# Add the log entry to the main objects log list # Add the log entry to the main objects log list
self.instance.log.add(action) self.instance.log.add(action)
# Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
geometry = geom_form.save(action)
self.instance.geometry = geometry
self.instance.save()
self.instance.update_deductable_rest()
return self.instance return self.instance
class RemoveEcoAccountModalForm(RemoveModalForm):
def is_valid(self):
super_valid = super().is_valid()
has_deductions = self.instance.deductions.exists()
if has_deductions:
self.add_error(
"confirm",
_("The account can not be removed, since there are still deductions.")
)
return super_valid and not has_deductions

View File

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

View File

@ -10,7 +10,6 @@ from django.utils.translation import gettext_lazy as _
from konova.forms.modals import BaseModalForm from konova.forms.modals import BaseModalForm
from konova.models import DeadlineType from konova.models import DeadlineType
from konova.utils import validators
from konova.utils.message_templates import DEADLINE_EDITED from konova.utils.message_templates import DEADLINE_EDITED
@ -35,7 +34,6 @@ class NewDeadlineModalForm(BaseModalForm):
label_suffix="", label_suffix="",
required=True, required=True,
help_text=_("Select date"), help_text=_("Select date"),
validators=[validators.reasonable_date],
widget=forms.DateInput( widget=forms.DateInput(
attrs={ attrs={
"type": "date", "type": "date",
@ -65,22 +63,6 @@ class NewDeadlineModalForm(BaseModalForm):
self.form_title = _("New deadline") self.form_title = _("New deadline")
self.form_caption = _("Insert data for the new deadline") self.form_caption = _("Insert data for the new deadline")
def is_valid(self):
valid = super().is_valid()
deadline_type = self.cleaned_data.get("type")
comment = self.cleaned_data.get("comment") or None
other_deadline_without_comment = deadline_type == DeadlineType.OTHER and comment is None
if other_deadline_without_comment:
self.add_error(
"comment",
_("Please explain this 'other' type of deadline.")
)
valid &= False
return valid
def save(self): def save(self):
deadline = self.instance.add_deadline(self) deadline = self.instance.add_deadline(self)
return deadline return deadline

View File

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

View File

@ -5,7 +5,7 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22 Created on: 18.08.22
""" """
from bootstrap_modal_forms.mixins import is_ajax from bootstrap_modal_forms.utils import is_ajax
from dal import autocomplete from dal import autocomplete
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
@ -14,8 +14,7 @@ from django.shortcuts import render
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_BIOTOPES_ID, \ from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
from intervention.inputs import CompensationStateTreeRadioSelect from intervention.inputs import CompensationStateTreeRadioSelect
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.forms.modals import RemoveModalForm, BaseModalForm from konova.forms.modals import RemoveModalForm, BaseModalForm
@ -44,7 +43,7 @@ class NewCompensationStateModalForm(BaseModalForm):
queryset=KonovaCode.objects.filter( queryset=KonovaCode.objects.filter(
is_archived=False, is_archived=False,
is_leaf=True, is_leaf=True,
code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID], code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_ID],
), ),
widget=autocomplete.ModelSelect2Multiple( widget=autocomplete.ModelSelect2Multiple(
url="codelist:biotope-extra-type-autocomplete", url="codelist:biotope-extra-type-autocomplete",

View File

@ -1,36 +0,0 @@
# Generated by Django 3.1.3 on 2022-10-11 11:39
from django.db import migrations, models
from django.db.models import Sum
def fill_deductable_rest(apps, schema_editor):
EcoAccount = apps.get_model("compensation", "EcoAccount")
accs = EcoAccount.objects.all()
for acc in accs:
deductions = acc.deductions.filter(
intervention__deleted=None,
)
deductions_surfaces = deductions.aggregate(Sum("surface"))["surface__sum"] or 0
available_surfaces = acc.deductable_surface or deductions_surfaces
rest = available_surfaces - deductions_surfaces
acc.deductable_rest = rest
acc.save()
class Migration(migrations.Migration):
dependencies = [
('compensation', '0010_auto_20220815_1030'),
]
operations = [
migrations.AddField(
model_name='ecoaccount',
name='deductable_rest',
field=models.FloatField(blank=True, default=0, help_text='Amount of deductable rest', null=True),
),
migrations.RunPython(fill_deductable_rest)
]

View File

@ -1,26 +0,0 @@
# Generated by Django 3.1.3 on 2022-11-16 12:22
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0006_auto_20220815_0759'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('compensation', '0011_ecoaccount_deductable_rest'),
]
operations = [
migrations.AlterField(
model_name='ecoaccount',
name='teams',
field=models.ManyToManyField(blank=True, help_text='Teams having access (data shared with)', to='user.Team'),
),
migrations.AlterField(
model_name='ecoaccount',
name='users',
field=models.ManyToManyField(blank=True, help_text='Users having access (data shared with)', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,35 +0,0 @@
# Generated by Django 3.1.3 on 2022-11-17 07:19
from django.db import migrations
from compensation.models import UnitChoices
def harmonize_action_units(apps, schema_editor):
"""
CompensationAction units (based on UnitChoices) can be mixed up at this point where
* qm represents and
* m2 represents
We drop qm in support of m2
"""
CompensationAction = apps.get_model("compensation", "CompensationAction")
actions = CompensationAction.objects.filter(
unit="qm"
)
for action in actions:
action.unit = UnitChoices.m2
action.save()
class Migration(migrations.Migration):
dependencies = [
('compensation', '0012_auto_20221116_1322'),
]
operations = [
migrations.RunPython(harmonize_action_units),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.1.3 on 2022-11-18 15:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('compensation', '0013_auto_20221117_0819'),
]
operations = [
migrations.AlterField(
model_name='compensationaction',
name='unit',
field=models.CharField(blank=True, choices=[('cm', 'cm'), ('m', 'm'), ('m2', ''), ('m3', ''), ('km', 'km'), ('ha', 'ha'), ('pcs', 'Pieces')], max_length=100, null=True),
),
]

View File

@ -1,70 +0,0 @@
# Generated by Django 4.2.6 on 2023-11-30 11:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('codelist', '0001_initial'),
('konova', '0014_resubmission'),
('compensation', '0014_auto_20221118_1620'),
]
operations = [
migrations.AlterField(
model_name='compensation',
name='after_states',
field=models.ManyToManyField(blank=True, help_text="Refers to 'Zielzustand Biotop'", related_name='+', to='compensation.compensationstate'),
),
migrations.AlterField(
model_name='compensation',
name='before_states',
field=models.ManyToManyField(blank=True, help_text="Refers to 'Ausgangszustand Biotop'", related_name='+', to='compensation.compensationstate'),
),
migrations.AlterField(
model_name='compensation',
name='deadlines',
field=models.ManyToManyField(blank=True, related_name='+', to='konova.deadline'),
),
migrations.AlterField(
model_name='compensation',
name='resubmissions',
field=models.ManyToManyField(blank=True, related_name='+', to='konova.resubmission'),
),
migrations.AlterField(
model_name='compensationaction',
name='action_type',
field=models.ManyToManyField(blank=True, limit_choices_to={'code_lists__in': [1026], 'is_archived': False, 'is_selectable': True}, related_name='+', to='codelist.konovacode'),
),
migrations.AlterField(
model_name='compensationaction',
name='action_type_details',
field=models.ManyToManyField(blank=True, limit_choices_to={'code_lists__in': [1035], 'is_archived': False, 'is_selectable': True}, related_name='+', to='codelist.konovacode'),
),
migrations.AlterField(
model_name='compensationstate',
name='biotope_type_details',
field=models.ManyToManyField(blank=True, limit_choices_to={'code_lists__in': [975], 'is_archived': False, 'is_selectable': True}, related_name='+', to='codelist.konovacode'),
),
migrations.AlterField(
model_name='ecoaccount',
name='after_states',
field=models.ManyToManyField(blank=True, help_text="Refers to 'Zielzustand Biotop'", related_name='+', to='compensation.compensationstate'),
),
migrations.AlterField(
model_name='ecoaccount',
name='before_states',
field=models.ManyToManyField(blank=True, help_text="Refers to 'Ausgangszustand Biotop'", related_name='+', to='compensation.compensationstate'),
),
migrations.AlterField(
model_name='ecoaccount',
name='deadlines',
field=models.ManyToManyField(blank=True, related_name='+', to='konova.deadline'),
),
migrations.AlterField(
model_name='ecoaccount',
name='resubmissions',
field=models.ManyToManyField(blank=True, related_name='+', to='konova.resubmission'),
),
]

View File

@ -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'),
),
]

View File

@ -19,9 +19,8 @@ class UnitChoices(models.TextChoices):
""" """
cm = "cm", _("cm") cm = "cm", _("cm")
m = "m", _("m") m = "m", _("m")
m2 = "m2", _("")
m3 = "m3", _("")
km = "km", _("km") km = "km", _("km")
qm = "qm", _("")
ha = "ha", _("ha") ha = "ha", _("ha")
st = "pcs", _("Pieces") # pieces st = "pcs", _("Pieces") # pieces

View File

@ -8,13 +8,8 @@ Created on: 16.11.21
import shutil import shutil
from django.contrib import messages from django.contrib import messages
from django.urls import reverse
from analysis.settings import LKOMPVZVO_PUBLISH_DATE
from codelist.models import KonovaCode from codelist.models import KonovaCode
from compensation.settings import COMPENSATION_IDENTIFIER_TEMPLATE, COMPENSATION_IDENTIFIER_LENGTH, \
COMPENSATION_LANIS_LAYER_NAME_RECORDED, COMPENSATION_LANIS_LAYER_NAME_UNRECORDED, COMPENSATION_LANIS_LAYER_NAME_UNRECORDED_OLD_ENTRY
from konova.sub_settings.django_settings import BASE_URL
from user.models import User, Team from user.models import User, Team
from django.db import models, transaction from django.db import models, transaction
from django.db.models import QuerySet, Sum from django.db.models import QuerySet, Sum
@ -26,7 +21,7 @@ from compensation.utils.quality import CompensationQualityChecker
from konova.models import BaseObject, AbstractDocument, Deadline, generate_document_file_upload_path, \ from konova.models import BaseObject, AbstractDocument, Deadline, generate_document_file_upload_path, \
GeoReferencedMixin, DeadlineType, ResubmitableObjectMixin GeoReferencedMixin, DeadlineType, ResubmitableObjectMixin
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, COMPENSATION_REMOVED_TEMPLATE, \ from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, COMPENSATION_REMOVED_TEMPLATE, \
DOCUMENT_REMOVED_TEMPLATE, DEADLINE_REMOVED, DEADLINE_ADDED, \ DOCUMENT_REMOVED_TEMPLATE, DEADLINE_REMOVED, ADDED_DEADLINE, \
COMPENSATION_ACTION_REMOVED, COMPENSATION_STATE_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE COMPENSATION_ACTION_REMOVED, COMPENSATION_STATE_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@ -80,7 +75,7 @@ class AbstractCompensation(BaseObject,
self.save() self.save()
self.deadlines.add(deadline) self.deadlines.add(deadline)
self.mark_as_edited(user, edit_comment=DEADLINE_ADDED) self.mark_as_edited(user, edit_comment=ADDED_DEADLINE)
return deadline return deadline
def remove_deadline(self, form): def remove_deadline(self, form):
@ -204,9 +199,7 @@ class AbstractCompensation(BaseObject,
Returns: Returns:
""" """
val = qs.aggregate(Sum("surface"))["surface__sum"] or 0 return qs.aggregate(Sum("surface"))["surface__sum"] or 0
val = float('{:0.2f}'.format(val))
return val
def quality_check(self) -> CompensationQualityChecker: def quality_check(self) -> CompensationQualityChecker:
""" Performs data quality check """ Performs data quality check
@ -303,18 +296,9 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
objects = CompensationManager() objects = CompensationManager()
identifier_length = COMPENSATION_IDENTIFIER_LENGTH
identifier_template = COMPENSATION_IDENTIFIER_TEMPLATE
def __str__(self): def __str__(self):
return "{}".format(self.identifier) return "{}".format(self.identifier)
def get_detail_url(self):
return reverse("compensation:detail", args=(self.id,))
def get_detail_url_absolute(self):
return BASE_URL + self.get_detail_url()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.identifier is None or len(self.identifier) == 0: if self.identifier is None or len(self.identifier) == 0:
# Create new identifier is none was given # Create new identifier is none was given
@ -344,20 +328,6 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
# Compensations inherit their shared state from the interventions # Compensations inherit their shared state from the interventions
return self.intervention.is_shared_with(user) return self.intervention.is_shared_with(user)
def is_only_shared_with(self, user: User):
""" Share check
Checks whether a given user is the only one having shared access to this entry
Args:
user (User): The user to be checked
Returns:
"""
# Compensations inherit their shared state from the interventions
return self.intervention.is_only_shared_with(user)
def share_with_user(self, user: User): def share_with_user(self, user: User):
""" Adds user to list of shared access users """ Adds user to list of shared access users
@ -409,7 +379,7 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
Returns: Returns:
users (QuerySet) users (QuerySet)
""" """
return self.intervention.shared_users return self.intervention.users.all()
@property @property
def shared_teams(self) -> QuerySet: def shared_teams(self) -> QuerySet:
@ -418,7 +388,7 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
Returns: Returns:
users (QuerySet) users (QuerySet)
""" """
return self.intervention.shared_teams return self.intervention.teams.all()
def get_documents(self) -> QuerySet: def get_documents(self) -> QuerySet:
""" Getter for all documents of a compensation """ Getter for all documents of a compensation
@ -431,18 +401,19 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
) )
return docs return docs
def mark_as_edited(self, user: User, request: HttpRequest = None, edit_comment: str = None): def mark_as_edited(self, user: User, request: HttpRequest = None, edit_comment: str = None, reset_recorded: bool = True):
""" Performs internal logic for setting the checked state of the related intervention """ Performs internal logic for setting the recordedd/checked state of the related intervention
Args: Args:
user (User): The performing user user (User): The performing user
request (HttpRequest): The performing request request (HttpRequest): The performing request
edit_comment (str): Additional comment for the log entry edit_comment (str): Additional comment for the log entry
reset_recorded (bool): Whether the record-state of the object should be reset
Returns: Returns:
""" """
self.intervention.set_unchecked() self.intervention.unrecord(user, request)
action = super().mark_as_edited(user, edit_comment=edit_comment) action = super().mark_as_edited(user, edit_comment=edit_comment)
return action return action
@ -488,28 +459,6 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
""" """
return self.intervention.is_recorded return self.intervention.is_recorded
def get_lanis_layer_name(self):
""" Getter for specific LANIS/WFS object layer
Returns:
"""
retval = None
if self.is_recorded:
retval = COMPENSATION_LANIS_LAYER_NAME_RECORDED
else:
try:
is_old_entry = self.intervention.legal.binding_date < LKOMPVZVO_PUBLISH_DATE
except TypeError:
is_old_entry = False
if is_old_entry:
retval = COMPENSATION_LANIS_LAYER_NAME_UNRECORDED_OLD_ENTRY
else:
retval = COMPENSATION_LANIS_LAYER_NAME_UNRECORDED
return retval
class CompensationDocument(AbstractDocument): class CompensationDocument(AbstractDocument):
""" """
@ -544,11 +493,8 @@ class CompensationDocument(AbstractDocument):
# The only file left for this compensation is the one which is currently processed and will be deleted # The only file left for this compensation is the one which is currently processed and will be deleted
# Make sure that the compensation folder itself is deleted as well, not only the file # Make sure that the compensation folder itself is deleted as well, not only the file
# Therefore take the folder path from the file path # Therefore take the folder path from the file path
try:
folder_path = self.file.path.split("/")[:-1] folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path) folder_path = "/".join(folder_path)
except ValueError:
folder_path = None
if user: if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title)) self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))

View File

@ -9,13 +9,12 @@ import shutil
from django.urls import reverse from django.urls import reverse
from compensation.settings import ECO_ACCOUNT_IDENTIFIER_TEMPLATE, ECO_ACCOUNT_IDENTIFIER_LENGTH, \
ECO_ACCOUNT_LANIS_LAYER_NAME_RECORDED, ECO_ACCOUNT_LANIS_LAYER_NAME_UNRECORDED
from konova.sub_settings.django_settings import BASE_URL
from konova.utils.message_templates import DEDUCTION_REMOVED, DOCUMENT_REMOVED_TEMPLATE from konova.utils.message_templates import DEDUCTION_REMOVED, DOCUMENT_REMOVED_TEMPLATE
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.db import models from django.db import models
from django.db.models import Sum, QuerySet from django.db.models import Sum, QuerySet
from django.utils.translation import gettext_lazy as _
from compensation.managers import EcoAccountManager, EcoAccountDeductionManager from compensation.managers import EcoAccountManager, EcoAccountDeductionManager
from compensation.models.compensation import AbstractCompensation, PikMixin from compensation.models.compensation import AbstractCompensation, PikMixin
@ -36,12 +35,6 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
help_text="Amount of deductable surface - can be lower than the total surface due to deduction limitations", help_text="Amount of deductable surface - can be lower than the total surface due to deduction limitations",
default=0, default=0,
) )
deductable_rest = models.FloatField(
blank=True,
null=True,
help_text="Amount of deductable rest",
default=0,
)
legal = models.OneToOneField( legal = models.OneToOneField(
"intervention.Legal", "intervention.Legal",
@ -53,17 +46,22 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
objects = EcoAccountManager() objects = EcoAccountManager()
identifier_length = ECO_ACCOUNT_IDENTIFIER_LENGTH
identifier_template = ECO_ACCOUNT_IDENTIFIER_TEMPLATE
def __str__(self): def __str__(self):
return f"{self.identifier} ({self.title})" return f"{self.identifier} ({self.title})"
def get_detail_url(self): def clean(self):
return reverse("compensation:acc:detail", args=(self.id,)) # Deductable surface can not be larger than added states after surface
after_state_sum = self.get_state_after_surface_sum()
if self.deductable_surface > after_state_sum:
raise ValidationError(_("Deductable surface can not be larger than existing surfaces in after states"))
def get_detail_url_absolute(self): # Deductable surface can not be lower than amount of already deducted surfaces
return BASE_URL + self.get_detail_url() # User needs to contact deducting user in case of further problems
deducted_sum = self.get_deductions_surface()
if self.deductable_surface < deducted_sum:
raise ValidationError(
_("Deductable surface can not be smaller than the sum of already existing deductions. Please contact the responsible users for the deductions!")
)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.identifier is None or len(self.identifier) == 0: if self.identifier is None or len(self.identifier) == 0:
@ -92,30 +90,38 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
Returns: Returns:
sum_surface (float) sum_surface (float)
""" """
val = self.deductions.all().aggregate(Sum("surface"))["surface__sum"] or 0 return self.deductions.all().aggregate(Sum("surface"))["surface__sum"] or 0
val = float('{:0.2f}'.format(val))
return val
def __calculate_deductable_rest(self): def get_state_after_surface_sum(self) -> float:
""" Calculates the account's after state surface sum
Returns:
sum_surface (float)
"""
return self.after_states.all().aggregate(Sum("surface"))["surface__sum"] or 0
def get_available_rest(self) -> (float, float):
""" Calculates available rest surface of the eco account """ Calculates available rest surface of the eco account
Args: Args:
Returns: Returns:
ret_val_total (float): Total amount ret_val_total (float): Total amount
ret_val_relative (float): Amount as percentage (0-100)
""" """
deductions_surfaces = self.get_deductions_surface() deductions = self.deductions.filter(
intervention__deleted=None,
)
deductions_surfaces = deductions.aggregate(Sum("surface"))["surface__sum"] or 0
available_surfaces = self.deductable_surface or deductions_surfaces ## no division by zero
ret_val_total = available_surfaces - deductions_surfaces
available_surface = self.deductable_surface if available_surfaces > 0:
if available_surface is None: ret_val_relative = int((ret_val_total / available_surfaces) * 100)
# Fallback!
available_surface = deductions_surfaces
else: else:
available_surface = float(available_surface) ret_val_relative = 0
ret_val = available_surface - deductions_surfaces return ret_val_total, ret_val_relative
return ret_val
def quality_check(self) -> EcoAccountQualityChecker: def quality_check(self) -> EcoAccountQualityChecker:
""" Quality check """ Quality check
@ -168,49 +174,13 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
# Send mail # Send mail
shared_users = self.shared_users.values_list("id", flat=True) shared_users = self.shared_users.values_list("id", flat=True)
for user_id in shared_users: for user_id in shared_users:
celery_send_mail_deduction_changed.delay(self.id, self.get_app_object_tuple(), user_id, data_change) celery_send_mail_deduction_changed.delay(self.identifier, self.title, user_id, data_change)
# Send mail # Send mail
shared_teams = self.shared_teams.values_list("id", flat=True) shared_teams = self.shared_teams.values_list("id", flat=True)
for team_id in shared_teams: for team_id in shared_teams:
celery_send_mail_deduction_changed_team.delay(self.id, self.get_app_object_tuple(), team_id, data_change) celery_send_mail_deduction_changed_team.delay(self.identifier, self.title, team_id, data_change)
def update_deductable_rest(self):
"""
Updates deductable_rest, which holds the amount of rest surface for this account.
Returns:
"""
self.deductable_rest = self.__calculate_deductable_rest()
self.save()
def get_deductable_rest_relative(self):
"""
Returns deductable_rest relative to deductable_surface mapped to [0,100]
Returns:
"""
try:
ret_val = int((self.deductable_rest / (self.deductable_surface or 0)) * 100)
except ZeroDivisionError:
ret_val = 0
return ret_val
def get_lanis_layer_name(self):
""" Getter for specific LANIS/WFS object layer
Returns:
"""
retval = None
if self.is_recorded:
retval = ECO_ACCOUNT_LANIS_LAYER_NAME_RECORDED
else:
retval = ECO_ACCOUNT_LANIS_LAYER_NAME_UNRECORDED
return retval
class EcoAccountDocument(AbstractDocument): class EcoAccountDocument(AbstractDocument):
""" """
@ -245,11 +215,8 @@ class EcoAccountDocument(AbstractDocument):
# The only file left for this eco account is the one which is currently processed and will be deleted # The only file left for this eco account is the one which is currently processed and will be deleted
# Make sure that the compensation folder itself is deleted as well, not only the file # Make sure that the compensation folder itself is deleted as well, not only the file
# Therefore take the folder path from the file path # Therefore take the folder path from the file path
try:
folder_path = self.file.path.split("/")[:-1] folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path) folder_path = "/".join(folder_path)
except ValueError:
folder_path = None
if user: if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title)) self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))
@ -305,8 +272,3 @@ class EcoAccountDeduction(BaseResource):
self.intervention.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED) self.intervention.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED)
self.account.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED) self.account.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED)
super().delete(*args, **kwargs) super().delete(*args, **kwargs)
self.account.update_deductable_rest()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.account.update_deductable_rest()

View File

@ -10,6 +10,8 @@ from django.db import models
from intervention.models import Intervention from intervention.models import Intervention
from konova.models import BaseResource from konova.models import BaseResource
from konova.utils.message_templates import PAYMENT_REMOVED
from user.models import UserActionLogEntry
class Payment(BaseResource): class Payment(BaseResource):

View File

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

View File

@ -7,11 +7,6 @@ Created on: 18.12.20
""" """
COMPENSATION_IDENTIFIER_LENGTH = 6 COMPENSATION_IDENTIFIER_LENGTH = 6
COMPENSATION_IDENTIFIER_TEMPLATE = "KOM-{}" COMPENSATION_IDENTIFIER_TEMPLATE = "KOM-{}"
COMPENSATION_LANIS_LAYER_NAME_RECORDED = "kom_recorded"
COMPENSATION_LANIS_LAYER_NAME_UNRECORDED = "kom_unrecorded"
COMPENSATION_LANIS_LAYER_NAME_UNRECORDED_OLD_ENTRY = "kom_unrecorded_old_entries"
ECO_ACCOUNT_IDENTIFIER_LENGTH = 6 ECO_ACCOUNT_IDENTIFIER_LENGTH = 6
ECO_ACCOUNT_IDENTIFIER_TEMPLATE = "OEK-{}" ECO_ACCOUNT_IDENTIFIER_TEMPLATE = "OEK-{}"
ECO_ACCOUNT_LANIS_LAYER_NAME_RECORDED = "oek_recorded"
ECO_ACCOUNT_LANIS_LAYER_NAME_UNRECORDED = "oek_unrecorded"

View File

@ -14,11 +14,11 @@ from django.utils.translation import gettext_lazy as _
from compensation.filters.compensation import CompensationTableFilter from compensation.filters.compensation import CompensationTableFilter
from compensation.models import Compensation from compensation.models import Compensation
from konova.utils.message_templates import DATA_IS_UNCHECKED, DATA_CHECKED_ON_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE from konova.utils.message_templates import DATA_IS_UNCHECKED, DATA_CHECKED_ON_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE
from konova.utils.tables import BaseTable, TableRenderMixin, TableOrderMixin from konova.utils.tables import BaseTable, TableRenderMixin
import django_tables2 as tables import django_tables2 as tables
class CompensationTable(BaseTable, TableRenderMixin, TableOrderMixin): class CompensationTable(BaseTable, TableRenderMixin):
id = tables.Column( id = tables.Column(
verbose_name=_("Identifier"), verbose_name=_("Identifier"),
orderable=True, orderable=True,
@ -31,7 +31,7 @@ class CompensationTable(BaseTable, TableRenderMixin, TableOrderMixin):
) )
d = tables.Column( d = tables.Column(
verbose_name=_("Parcel gmrkng"), verbose_name=_("Parcel gmrkng"),
orderable=False, orderable=True,
accessor="geometry", accessor="geometry",
) )
c = tables.Column( c = tables.Column(
@ -126,6 +126,28 @@ class CompensationTable(BaseTable, TableRenderMixin, TableOrderMixin):
) )
return format_html(html) return format_html(html)
def render_d(self, value, record: Compensation):
""" Renders the parcel district column for a compensation
Args:
value (str): The geometry
record (Compensation): The compensation record
Returns:
"""
parcels = value.get_underlying_parcels().values_list(
"parcel_group__name",
flat=True
).distinct()
html = render_to_string(
"table/gmrkng_col.html",
{
"entries": parcels
}
)
return html
def render_r(self, value, record: Compensation): def render_r(self, value, record: Compensation):
""" Renders the registered column for a compensation """ Renders the registered column for a compensation
@ -148,3 +170,20 @@ class CompensationTable(BaseTable, TableRenderMixin, TableOrderMixin):
) )
return format_html(html) return format_html(html)
def render_e(self, value, record: Compensation):
""" Renders the editable column for a compensation
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
has_access = record.is_shared_with(self.user)
html = self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),
icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit",
)
return format_html(html)

View File

@ -8,18 +8,17 @@ Created on: 18.08.22
from django.http import HttpRequest from django.http import HttpRequest
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
from django.utils.formats import number_format
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from compensation.filters.eco_account import EcoAccountTableFilter from compensation.filters.eco_account import EcoAccountTableFilter
from compensation.models import EcoAccount from compensation.models import EcoAccount
from konova.utils.tables import TableRenderMixin, BaseTable, TableOrderMixin from konova.utils.tables import TableRenderMixin, BaseTable
import django_tables2 as tables import django_tables2 as tables
class EcoAccountTable(BaseTable, TableRenderMixin, TableOrderMixin): class EcoAccountTable(BaseTable, TableRenderMixin):
id = tables.Column( id = tables.Column(
verbose_name=_("Identifier"), verbose_name=_("Identifier"),
orderable=True, orderable=True,
@ -32,13 +31,13 @@ class EcoAccountTable(BaseTable, TableRenderMixin, TableOrderMixin):
) )
d = tables.Column( d = tables.Column(
verbose_name=_("Parcel gmrkng"), verbose_name=_("Parcel gmrkng"),
orderable=False, orderable=True,
accessor="geometry", accessor="geometry",
) )
av = tables.Column( av = tables.Column(
verbose_name=_("Available"), verbose_name=_("Available"),
orderable=True, orderable=True,
accessor="deductable_rest", empty_values=[],
attrs={ attrs={
"th": { "th": {
"class": "w-20", "class": "w-20",
@ -101,20 +100,38 @@ class EcoAccountTable(BaseTable, TableRenderMixin, TableOrderMixin):
""" Renders the available column for an eco account """ Renders the available column for an eco account
Args: Args:
value (float): The deductable_rest value (str): The identifier value
record (EcoAccount): The eco account record record (EcoAccount): The eco account record
Returns: Returns:
""" """
try: value_total, value_relative = record.get_available_rest()
value_relative = record.get_deductable_rest_relative()
except ZeroDivisionError:
value_relative = 0
html = render_to_string("konova/widgets/progressbar.html", {"value": value_relative}) html = render_to_string("konova/widgets/progressbar.html", {"value": value_relative})
html += f"{number_format(record.deductable_rest, decimal_pos=2)}"
return format_html(html) return format_html(html)
def render_d(self, value, record):
""" Renders the parcel district column for a compensation
Args:
value (str): The geometry
record (Compensation): The compensation record
Returns:
"""
parcels = value.get_underlying_parcels().values_list(
"parcel_group__name",
flat=True
).distinct()
html = render_to_string(
"table/gmrkng_col.html",
{
"entries": parcels
}
)
return html
def render_r(self, value, record: EcoAccount): def render_r(self, value, record: EcoAccount):
""" Renders the recorded column for an eco account """ Renders the recorded column for an eco account
@ -136,3 +153,23 @@ class EcoAccountTable(BaseTable, TableRenderMixin, TableOrderMixin):
icn_filled=checked, icn_filled=checked,
) )
return format_html(html) return format_html(html)
def render_e(self, value, record: EcoAccount):
""" Renders the editable column for an eco account
Args:
value (str): The identifier value
record (EcoAccount): The eco account record
Returns:
"""
html = ""
# Do not use value in here, since value does use unprefetched 'users' manager, where record has already
# prefetched users data
has_access = record.is_shared_with(self.user)
html += self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),
icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit",
)
return format_html(html)

View File

@ -11,7 +11,7 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
{% if is_default_member and 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' %}"> <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-action' obj.id %}" title="{% trans 'Add new action' %}">
{% fa5_icon 'plus' %} {% fa5_icon 'plus' %}
{% fa5_icon 'seedling' %} {% fa5_icon 'seedling' %}
@ -21,7 +21,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card-body {% if tables_scrollable %}scroll-300{% endif %} p-2"> <div class="card-body scroll-300 p-2">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@ -34,7 +34,7 @@
<th scope="col"> <th scope="col">
{% trans 'Comment' %} {% trans 'Comment' %}
</th> </th>
{% if is_default_member and is_entry_shared %} {% if is_default_member and has_access %}
<th class="w-10" scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
@ -52,7 +52,7 @@
<hr> <hr>
{% endfor %} {% endfor %}
{% for detail in action.action_type_details.all %} {% for detail in action.action_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{ detail.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 %} {% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span> <span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
{% endfor %} {% endfor %}
@ -64,7 +64,7 @@
</div> </div>
</td> </td>
<td class="align-middle float-right"> <td class="align-middle float-right">
{% if is_default_member and 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' %}"> <button data-form-url="{% url 'compensation:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>
@ -74,10 +74,6 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %}
<div class="alert alert-danger mb-0">
{% trans 'Missing' %}
</div>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

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

View File

@ -10,7 +10,7 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
{% if is_default_member and 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' %}"> <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
{% fa5_icon 'plus' %} {% fa5_icon 'plus' %}
{% fa5_icon 'calendar-check' %} {% fa5_icon 'calendar-check' %}
@ -25,7 +25,7 @@
{% trans 'Missing finished deadline ' %} {% trans 'Missing finished deadline ' %}
</div> </div>
{% endif %} {% endif %}
<div class="card-body {% if tables_scrollable %}scroll-300{% endif %} p-2"> <div class="card-body scroll-300 p-2">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@ -38,7 +38,7 @@
<th scope="col"> <th scope="col">
{% trans 'Comment' %} {% trans 'Comment' %}
</th> </th>
{% if is_default_member and is_entry_shared %} {% if is_default_member and has_access %}
<th class="w-10" scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
@ -60,7 +60,7 @@
</div> </div>
</td> </td>
<td class="align-middle float-right"> <td class="align-middle float-right">
{% if is_default_member and 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' %}"> <button data-form-url="{% url 'compensation:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>

View File

@ -10,7 +10,7 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
{% if is_default_member and 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' %}"> <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
{% fa5_icon 'plus' %} {% fa5_icon 'plus' %}
{% fa5_icon 'file' %} {% fa5_icon 'file' %}
@ -20,7 +20,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card-body {% if tables_scrollable %}scroll-300{% endif %} p-2"> <div class="card-body scroll-300 p-2">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@ -33,7 +33,7 @@
<th scope="col"> <th scope="col">
{% trans 'Comment' %} {% trans 'Comment' %}
</th> </th>
{% if is_default_member and is_entry_shared %} {% if is_default_member and has_access %}
<th class="w-10" scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
@ -59,7 +59,7 @@
</div> </div>
</td> </td>
<td class="align-middle float-right"> <td class="align-middle float-right">
{% if is_default_member and 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' %}"> <button data-form-url="{% url 'compensation:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>

View File

@ -10,7 +10,7 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
{% if is_default_member and 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' %}"> <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
{% fa5_icon 'plus' %} {% fa5_icon 'plus' %}
{% fa5_icon 'layer-group' %} {% fa5_icon 'layer-group' %}
@ -25,7 +25,7 @@
{% trans 'Missing surfaces according to states before: ' %}{{ diff_states|floatformat:2 }} m² {% trans 'Missing surfaces according to states before: ' %}{{ diff_states|floatformat:2 }} m²
</div> </div>
{% endif %} {% endif %}
<div class="card-body {% if tables_scrollable %}scroll-300{% endif %} p-2"> <div class="card-body scroll-300 p-2">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@ -35,7 +35,7 @@
<th scope="col"> <th scope="col">
{% trans 'Surface' %} {% trans 'Surface' %}
</th> </th>
{% if is_default_member and is_entry_shared %} {% if is_default_member and has_access %}
<th class="w-10" scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
@ -51,14 +51,14 @@
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span> <span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
<br> <br>
{% for detail in state.biotope_type_details.all %} {% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{ detail.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 %} {% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span> <span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
{% endfor %} {% endfor %}
</td> </td>
<td>{{ state.surface|floatformat:2 }} m²</td> <td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right"> <td class="align-middle float-right">
{% if is_default_member and 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' %}"> <button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>
@ -68,10 +68,6 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %}
<div class="alert alert-danger mb-0">
{% trans 'Missing' %}
</div>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@ -10,7 +10,7 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
{% if is_default_member and 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' %}"> <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
{% fa5_icon 'plus' %} {% fa5_icon 'plus' %}
{% fa5_icon 'layer-group' %} {% fa5_icon 'layer-group' %}
@ -25,7 +25,7 @@
{% trans 'Missing surfaces according to states after: ' %}{{ diff_states|floatformat:2 }} m² {% trans 'Missing surfaces according to states after: ' %}{{ diff_states|floatformat:2 }} m²
</div> </div>
{% endif %} {% endif %}
<div class="card-body {% if tables_scrollable %}scroll-300{% endif %} p-2"> <div class="card-body scroll-300 p-2">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@ -35,7 +35,7 @@
<th scope="col"> <th scope="col">
{% trans 'Surface' %} {% trans 'Surface' %}
</th> </th>
{% if is_default_member and is_entry_shared %} {% if is_default_member and has_access %}
<th class="w-10" scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
@ -51,14 +51,14 @@
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span> <span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
<br> <br>
{% for detail in state.biotope_type_details.all %} {% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{ detail.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 %} {% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span> <span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
{% endfor %} {% endfor %}
</td> </td>
<td>{{ state.surface|floatformat:2 }} m²</td> <td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right"> <td class="align-middle float-right">
{% if is_default_member and 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' %}"> <button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>
@ -68,10 +68,6 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %}
<div class="alert alert-danger mb-0">
{% trans 'Missing' %}
</div>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@ -123,14 +123,14 @@
{% include 'user/includes/team_data_modal_button.html' %} {% include 'user/includes/team_data_modal_button.html' %}
{% endfor %} {% endfor %}
<hr> <hr>
{% if is_entry_shared %} {% if has_access %}
{% for user in obj.intervention.shared_users %} {% for user in obj.users.all %}
{% include 'user/includes/contact_modal_button.html' %} {% include 'user/includes/contact_modal_button.html' %}
{% endfor %} {% endfor %}
{% else %} {% else %}
<span title="{% trans 'The data must be shared with you, if you want to see which other users have shared access as well.' %}"> <span title="{% trans 'The data must be shared with you, if you want to see which other users have shared access as well.' %}">
{% fa5_icon 'eye-slash' %} {% fa5_icon 'eye-slash' %}
{{obj.intervention.users.count}} {% trans 'other users' %} {{obj.users.count}} {% trans 'other users' %}
</span> </span>
{% endif %} {% endif %}
</td> </td>

View File

@ -10,7 +10,7 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
{% if is_default_member and 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' %}"> <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-action' obj.id %}" title="{% trans 'Add new action' %}">
{% fa5_icon 'plus' %} {% fa5_icon 'plus' %}
{% fa5_icon 'seedling' %} {% fa5_icon 'seedling' %}
@ -20,7 +20,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card-body {% if tables_scrollable %}scroll-300{% endif %} p-2"> <div class="card-body scroll-300 p-2">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@ -33,7 +33,7 @@
<th scope="col"> <th scope="col">
{% trans 'Comment' %} {% trans 'Comment' %}
</th> </th>
{% if is_default_member and is_entry_shared %} {% if is_default_member and has_access %}
<th class="w-10" scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
@ -51,7 +51,7 @@
<hr> <hr>
{% endfor %} {% endfor %}
{% for detail in action.action_type_details.all %} {% for detail in action.action_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{ detail.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 %} {% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span> <span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
{% endfor %} {% endfor %}
@ -63,7 +63,7 @@
</div> </div>
</td> </td>
<td class="align-middle float-right"> <td class="align-middle float-right">
{% if is_default_member and 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' %}"> <button data-form-url="{% url 'compensation:acc:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>
@ -73,10 +73,6 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %}
<div class="alert alert-danger mb-0">
{% trans 'Missing' %}
</div>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

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

View File

@ -10,7 +10,7 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
{% if is_default_member and 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' %}"> <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
{% fa5_icon 'plus' %} {% fa5_icon 'plus' %}
{% fa5_icon 'calendar-check' %} {% fa5_icon 'calendar-check' %}
@ -25,7 +25,7 @@
{% trans 'Missing finished deadline ' %} {% trans 'Missing finished deadline ' %}
</div> </div>
{% endif %} {% endif %}
<div class="card-body {% if tables_scrollable %}scroll-300{% endif %} p-2"> <div class="card-body scroll-300 p-2">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@ -58,7 +58,7 @@
</div> </div>
</td> </td>
<td class="align-middle float-right"> <td class="align-middle float-right">
{% if is_default_member and 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' %}"> <button data-form-url="{% url 'compensation:acc:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>

View File

@ -20,7 +20,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card-body {% if tables_scrollable %}scroll-300{% endif %} p-2"> <div class="card-body scroll-300 p-2">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@ -61,7 +61,7 @@
<td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td> <td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td>
<td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td> <td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td>
<td class="align-middle float-right"> <td class="align-middle float-right">
{% if is_default_member and 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' %}"> <button data-form-url="{% url 'compensation:acc:edit-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>

View File

@ -10,7 +10,7 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
{% if is_default_member and 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' %}"> <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
{% fa5_icon 'plus' %} {% fa5_icon 'plus' %}
{% fa5_icon 'file' %} {% fa5_icon 'file' %}
@ -20,7 +20,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card-body {% if tables_scrollable %}scroll-300{% endif %} p-2"> <div class="card-body scroll-300 p-2">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@ -57,7 +57,7 @@
</div> </div>
</td> </td>
<td class="align-middle float-right"> <td class="align-middle float-right">
{% if is_default_member and 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' %}"> <button data-form-url="{% url 'compensation:acc:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>

View File

@ -10,7 +10,7 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
{% if is_default_member and 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' %}"> <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
{% fa5_icon 'plus' %} {% fa5_icon 'plus' %}
{% fa5_icon 'layer-group' %} {% fa5_icon 'layer-group' %}
@ -25,7 +25,7 @@
{% trans 'Missing surfaces according to states before: ' %}{{ diff_states|floatformat:2 }} m² {% trans 'Missing surfaces according to states before: ' %}{{ diff_states|floatformat:2 }} m²
</div> </div>
{% endif %} {% endif %}
<div class="card-body {% if tables_scrollable %}scroll-300{% endif %} p-2"> <div class="card-body scroll-300 p-2">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@ -35,7 +35,7 @@
<th scope="col"> <th scope="col">
{% trans 'Surface' %} {% trans 'Surface' %}
</th> </th>
{% if is_default_member and is_entry_shared %} {% if is_default_member and has_access %}
<th class="w-10" scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
@ -51,14 +51,14 @@
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span> <span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
<br> <br>
{% for detail in state.biotope_type_details.all %} {% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{ detail.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 %} {% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span> <span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
{% endfor %} {% endfor %}
</td> </td>
<td>{{ state.surface|floatformat:2 }} m²</td> <td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right"> <td class="align-middle float-right">
{% if is_default_member and 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' %}"> <button data-form-url="{% url 'compensation:acc:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>
@ -68,10 +68,6 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %}
<div class="alert alert-danger mb-0">
{% trans 'Missing' %}
</div>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@ -10,7 +10,7 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
{% if is_default_member and 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' %}"> <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
{% fa5_icon 'plus' %} {% fa5_icon 'plus' %}
{% fa5_icon 'layer-group' %} {% fa5_icon 'layer-group' %}
@ -25,7 +25,7 @@
{% trans 'Missing surfaces according to states after: ' %}{{ diff_states|floatformat:2 }} m² {% trans 'Missing surfaces according to states after: ' %}{{ diff_states|floatformat:2 }} m²
</div> </div>
{% endif %} {% endif %}
<div class="card-body {% if tables_scrollable %}scroll-300{% endif %} p-2"> <div class="card-body scroll-300 p-2">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@ -35,7 +35,7 @@
<th scope="col"> <th scope="col">
{% trans 'Surface' %} {% trans 'Surface' %}
</th> </th>
{% if is_default_member and is_entry_shared %} {% if is_default_member and has_access %}
<th class="w-10" scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
@ -51,14 +51,14 @@
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span> <span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
<br> <br>
{% for detail in state.biotope_type_details.all %} {% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{ detail.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 %} {% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span> <span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
{% endfor %} {% endfor %}
</td> </td>
<td>{{ state.surface|floatformat:2 }} m²</td> <td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right"> <td class="align-middle float-right">
{% if is_default_member and 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' %}"> <button data-form-url="{% url 'compensation:acc:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>
@ -68,10 +68,6 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %}
<div class="alert alert-danger mb-0">
{% trans 'Missing' %}
</div>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

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

View File

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

View File

@ -1,7 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 21.08.23
"""

View File

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

View File

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

View File

@ -47,7 +47,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
"identifier": test_id, "identifier": test_id,
"title": test_title, "title": test_title,
"geom": geom_json, "geom": geom_json,
"surface": test_deductable_surface, "deductable_surface": test_deductable_surface,
"conservation_office": test_conservation_office.id "conservation_office": test_conservation_office.id
} }
self.client_user.post(new_url, post_data) self.client_user.post(new_url, post_data)
@ -61,11 +61,8 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.assertEqual(acc.identifier, test_id) self.assertEqual(acc.identifier, test_id)
self.assertEqual(acc.title, test_title) self.assertEqual(acc.title, test_title)
self.assertEqual(acc.deductable_surface, test_deductable_surface)
self.assertEqual(acc.deductable_rest, test_deductable_surface)
self.assert_equal_geometries(acc.geometry.geom, test_geom) self.assert_equal_geometries(acc.geometry.geom, test_geom)
self.assertEqual(acc.log.count(), 1) self.assertEqual(acc.log.count(), 1)
self.assertEqual(acc.created, acc.modified)
# Expect logs to be set # Expect logs to be set
self.assertEqual(acc.log.count(), 1) self.assertEqual(acc.log.count(), 1)
@ -82,13 +79,12 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
url = reverse("compensation:acc:edit", args=(self.eco_account.id,)) url = reverse("compensation:acc:edit", args=(self.eco_account.id,))
pre_edit_log_count = self.eco_account.log.count() pre_edit_log_count = self.eco_account.log.count()
old_identifier = self.eco_account.identifier
new_title = self.create_dummy_string() new_title = self.create_dummy_string()
new_identifier = self.create_dummy_string() new_identifier = self.create_dummy_string()
new_comment = self.create_dummy_string() new_comment = self.create_dummy_string()
new_geometry = MultiPolygon(srid=4326) # Create an empty geometry new_geometry = MultiPolygon(srid=4326) # Create an empty geometry
test_conservation_office = self.get_conservation_office_code() test_conservation_office = self.get_conservation_office_code()
test_deductable_surface = self.eco_account.deductable_surface + 100 test_deductable_surface = 10005
check_on_elements = { check_on_elements = {
self.eco_account.title: new_title, self.eco_account.title: new_title,
@ -107,23 +103,19 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
"surface": test_deductable_surface, "surface": test_deductable_surface,
"conservation_office": test_conservation_office.id "conservation_office": test_conservation_office.id
} }
response = self.client_user.post(url, post_data) self.client_user.post(url, post_data)
self.assertEqual(response.status_code, 302, msg=f"{response.content.decode('utf-8')}")
self.eco_account.refresh_from_db() self.eco_account.refresh_from_db()
deductions_surface = self.eco_account.get_deductions_surface()
check_on_elements = { check_on_elements = {
self.eco_account.title: new_title, self.eco_account.title: new_title,
self.eco_account.identifier: new_identifier,
self.eco_account.deductable_surface: test_deductable_surface, self.eco_account.deductable_surface: test_deductable_surface,
self.eco_account.deductable_rest: test_deductable_surface - deductions_surface,
self.eco_account.comment: new_comment, self.eco_account.comment: new_comment,
} }
for k, v in check_on_elements.items(): for k, v in check_on_elements.items():
self.assertEqual(k, v) self.assertEqual(k, v)
self.assertEqual(self.eco_account.identifier, old_identifier)
self.assert_equal_geometries(self.eco_account.geometry.geom, new_geometry) self.assert_equal_geometries(self.eco_account.geometry.geom, new_geometry)
# Expect logs to be set # Expect logs to be set
@ -193,7 +185,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
# Prepare data for deduction creation # Prepare data for deduction creation
deduct_url = reverse("compensation:acc:new-deduction", args=(self.eco_account.id,)) deduct_url = reverse("compensation:acc:new-deduction", args=(self.eco_account.id,))
test_surface = 10.50 test_surface = 10.00
post_data = { post_data = {
"surface": test_surface, "surface": test_surface,
"account": self.eco_account.id, "account": self.eco_account.id,
@ -202,17 +194,17 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
# Perform request --> expect to fail # Perform request --> expect to fail
self.client_user.post(deduct_url, post_data) self.client_user.post(deduct_url, post_data)
# Expect that no deduction has been created since the eco account is not recorded, yet # Expect that no deduction has been created
self.assertEqual(0, self.eco_account.deductions.count()) self.assertEqual(0, self.eco_account.deductions.count())
self.assertEqual(0, self.intervention.deductions.count()) self.assertEqual(0, self.intervention.deductions.count())
self.assertEqual(pre_deduction_acc_log_count, 0) self.assertEqual(pre_deduction_acc_log_count, 0)
self.assertEqual(pre_deduction_int_log_count, 0) self.assertEqual(pre_deduction_int_log_count, 0)
# Now mock the eco account as it would be recorded (with invalid data) # Now mock the eco account as it would be recorded (with invalid data)
# Make sure the deductible surface is valid for the request # Make sure the deductible surface is high enough for the request
self.eco_account.set_recorded(self.superuser) self.eco_account.set_recorded(self.superuser)
self.eco_account.refresh_from_db() self.eco_account.refresh_from_db()
self.eco_account.deductable_surface = test_surface + 1.0 self.eco_account.deductable_surface = test_surface + 1.00
self.eco_account.save() self.eco_account.save()
self.assertIsNotNone(self.eco_account.recorded) self.assertIsNotNone(self.eco_account.recorded)
self.assertGreater(self.eco_account.deductable_surface, test_surface) self.assertGreater(self.eco_account.deductable_surface, test_surface)
@ -224,14 +216,10 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.client_user.post(deduct_url, post_data) self.client_user.post(deduct_url, post_data)
# Expect that the deduction has been created # Expect that the deduction has been created
self.eco_account.refresh_from_db()
self.assertEqual(1, self.eco_account.deductions.count()) self.assertEqual(1, self.eco_account.deductions.count())
self.assertEqual(1, self.intervention.deductions.count()) self.assertEqual(1, self.intervention.deductions.count())
deduction = self.eco_account.deductions.get( deduction = self.eco_account.deductions.first()
surface=test_surface
)
self.assertEqual(deduction.surface, test_surface) self.assertEqual(deduction.surface, test_surface)
self.assertEqual(self.eco_account.deductable_rest, self.eco_account.deductable_surface - deduction.surface)
self.assertEqual(deduction.account, self.eco_account) self.assertEqual(deduction.account, self.eco_account)
self.assertEqual(deduction.intervention, self.intervention) self.assertEqual(deduction.intervention, self.intervention)
@ -242,7 +230,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.assertTrue(self.intervention.log.first().action == UserAction.EDITED) self.assertTrue(self.intervention.log.first().action == UserAction.EDITED)
def test_edit_deduction(self): def test_edit_deduction(self):
test_surface = self.eco_account.deductable_rest test_surface = self.eco_account.get_available_rest()[0]
self.eco_account.set_recorded(self.superuser) self.eco_account.set_recorded(self.superuser)
self.intervention.share_with_user(self.superuser) self.intervention.share_with_user(self.superuser)
self.eco_account.refresh_from_db() self.eco_account.refresh_from_db()
@ -251,7 +239,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
deduction = EcoAccountDeduction.objects.create( deduction = EcoAccountDeduction.objects.create(
intervention=self.intervention, intervention=self.intervention,
account=self.eco_account, account=self.eco_account,
surface=1.10 surface=0
) )
self.assertEqual(1, self.intervention.deductions.count()) self.assertEqual(1, self.intervention.deductions.count())
self.assertEqual(1, self.eco_account.deductions.count()) self.assertEqual(1, self.eco_account.deductions.count())
@ -274,7 +262,6 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.eco_account.refresh_from_db() self.eco_account.refresh_from_db()
deduction.refresh_from_db() deduction.refresh_from_db()
self.assertEqual(self.eco_account.deductable_rest, self.eco_account.deductable_surface - deduction.surface)
self.assertEqual(num_deductions_intervention, self.intervention.deductions.count()) self.assertEqual(num_deductions_intervention, self.intervention.deductions.count())
self.assertEqual(num_deductions_account, self.eco_account.deductions.count()) self.assertEqual(num_deductions_account, self.eco_account.deductions.count())
self.assertEqual(deduction.surface, test_surface) self.assertEqual(deduction.surface, test_surface)
@ -288,7 +275,6 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
def test_remove_deduction(self): def test_remove_deduction(self):
intervention = self.deduction.intervention intervention = self.deduction.intervention
account = self.deduction.account account = self.deduction.account
deducted_surface = self.deduction.surface
# Prepare url and form data to be posted # Prepare url and form data to be posted
new_url = reverse("compensation:acc:remove-deduction", args=(account.id, self.deduction.id)) new_url = reverse("compensation:acc:remove-deduction", args=(account.id, self.deduction.id))
@ -301,7 +287,6 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
pre_edit_intervention_log_count = intervention.log.count() pre_edit_intervention_log_count = intervention.log.count()
pre_edit_account_log_count = account.log.count() pre_edit_account_log_count = account.log.count()
pre_edit_account_rest = account.deductable_rest
num_deductions_intervention = intervention.deductions.count() num_deductions_intervention = intervention.deductions.count()
num_deductions_account = account.deductions.count() num_deductions_account = account.deductions.count()
@ -312,7 +297,6 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.assertEqual(num_deductions_intervention - 1, intervention.deductions.count()) self.assertEqual(num_deductions_intervention - 1, intervention.deductions.count())
self.assertEqual(num_deductions_account - 1, account.deductions.count()) self.assertEqual(num_deductions_account - 1, account.deductions.count())
self.assertEqual(account.deductable_rest, pre_edit_account_rest + deducted_surface)
# Expect logs to be set # Expect logs to be set
self.assertEqual(pre_edit_intervention_log_count + 1, intervention.log.count()) self.assertEqual(pre_edit_intervention_log_count + 1, intervention.log.count())

View File

@ -1,7 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 30.08.23
"""

View File

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

View File

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

View File

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

View File

@ -67,13 +67,12 @@ def report_view(request: HttpRequest, id: str):
"img": qrcode_img_lanis, "img": qrcode_img_lanis,
"url": qrcode_lanis_url, "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, "before_states": before_states,
"after_states": after_states, "after_states": after_states,
"geom_form": geom_form, "geom_form": geom_form,
"parcels": parcels, "parcels": parcels,
"actions": actions, "actions": actions,
"tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title, TAB_TITLE_IDENTIFIER: tab_title,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context

View File

@ -13,17 +13,18 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm, RemoveEcoAccountModalForm from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm
from compensation.models import EcoAccount from compensation.models import EcoAccount
from compensation.tables.eco_account import EcoAccountTable from compensation.tables.eco_account import EcoAccountTable
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal, \ from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal
uuid_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm
from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \ from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \
IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED IDENTIFIER_REPLACED
from konova.utils.user_checks import in_group
@login_required @login_required
@ -41,8 +42,6 @@ def index_view(request: HttpRequest):
template = "generic_index.html" template = "generic_index.html"
eco_accounts = EcoAccount.objects.filter( eco_accounts = EcoAccount.objects.filter(
deleted=None, deleted=None,
).order_by(
"-modified__timestamp"
) )
table = EcoAccountTable( table = EcoAccountTable(
request=request, request=request,
@ -84,11 +83,6 @@ def new_view(request: HttpRequest):
) )
) )
messages.success(request, _("Eco-Account {} added").format(acc.identifier)) messages.success(request, _("Eco-Account {} added").format(acc.identifier))
if geom_form.geometry_simplified:
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
return redirect("compensation:acc:detail", id=acc.id) return redirect("compensation:acc:detail", id=acc.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger",) messages.error(request, FORM_INVALID, extra_tags="danger",)
@ -150,17 +144,10 @@ def edit_view(request: HttpRequest, id: str):
data_form = EditEcoAccountForm(request.POST or None, instance=acc) data_form = EditEcoAccountForm(request.POST or None, instance=acc)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc) geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc)
if request.method == "POST": if request.method == "POST":
data_form_valid = data_form.is_valid() if data_form.is_valid() and geom_form.is_valid():
geom_form_valid = geom_form.is_valid()
if data_form_valid and geom_form_valid:
# The data form takes the geom form for processing, as well as the performing user # The data form takes the geom form for processing, as well as the performing user
acc = data_form.save(request.user, geom_form) acc = data_form.save(request.user, geom_form)
messages.success(request, _("Eco-Account {} edited").format(acc.identifier)) messages.success(request, _("Eco-Account {} edited").format(acc.identifier))
if geom_form.geometry_simplified:
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
return redirect("compensation:acc:detail", id=acc.id) return redirect("compensation:acc:detail", id=acc.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger",) messages.error(request, FORM_INVALID, extra_tags="danger",)
@ -178,7 +165,6 @@ def edit_view(request: HttpRequest, id: str):
@login_required @login_required
@any_group_check @any_group_check
@uuid_required
def detail_view(request: HttpRequest, id: str): def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for a compensation """ Renders a detail view for a compensation
@ -197,8 +183,7 @@ def detail_view(request: HttpRequest, id: str):
'geometry', 'geometry',
'responsible', 'responsible',
), ),
id=id, id=id
deleted=None,
) )
geom_form = SimpleGeomForm(instance=acc) geom_form = SimpleGeomForm(instance=acc)
parcels = acc.get_underlying_parcels() parcels = acc.get_underlying_parcels()
@ -211,12 +196,11 @@ def detail_view(request: HttpRequest, id: str):
# Precalculate logical errors between before- and after-states # Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling # Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = acc.get_surface_before_states() sum_before_states = before_states.aggregate(Sum("surface"))["surface__sum"] or 0
sum_after_states = acc.get_surface_after_states() sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0
diff_states = abs(sum_before_states - sum_after_states) diff_states = abs(sum_before_states - sum_after_states)
# Calculate rest of available surface for deductions # Calculate rest of available surface for deductions
available_total = acc.deductable_rest available_total, available_relative = acc.get_available_rest()
available_relative = acc.get_deductable_rest_relative()
# Prefetch related data to decrease the amount of db connections # Prefetch related data to decrease the amount of db connections
deductions = acc.deductions.filter( deductions = acc.deductions.filter(
@ -226,18 +210,11 @@ def detail_view(request: HttpRequest, id: str):
request = acc.set_status_messages(request) request = acc.set_status_messages(request)
requesting_user_is_only_shared_user = acc.is_only_shared_with(_user)
if requesting_user_is_only_shared_user:
messages.info(
request,
DO_NOT_FORGET_TO_SHARE
)
context = { context = {
"obj": acc, "obj": acc,
"geom_form": geom_form, "geom_form": geom_form,
"parcels": parcels, "parcels": parcels,
"is_entry_shared": is_data_shared, "has_access": is_data_shared,
"before_states": before_states, "before_states": before_states,
"after_states": after_states, "after_states": after_states,
"sum_before_states": sum_before_states, "sum_before_states": sum_before_states,
@ -245,9 +222,9 @@ def detail_view(request: HttpRequest, id: str):
"diff_states": diff_states, "diff_states": diff_states,
"available": available_relative, "available": available_relative,
"available_total": available_total, "available_total": available_total,
"is_default_member": _user.in_group(DEFAULT_GROUP), "is_default_member": in_group(_user, DEFAULT_GROUP),
"is_zb_member": _user.in_group(ZB_GROUP), "is_zb_member": in_group(_user, ZB_GROUP),
"is_ets_member": _user.in_group(ETS_GROUP), "is_ets_member": in_group(_user, ETS_GROUP),
"LANIS_LINK": acc.get_LANIS_link(), "LANIS_LINK": acc.get_LANIS_link(),
"deductions": deductions, "deductions": deductions,
"actions": actions, "actions": actions,
@ -278,11 +255,11 @@ def remove_view(request: HttpRequest, id: str):
# default group user # default group user
if acc.recorded is not None or acc.deductions.exists(): if acc.recorded is not None or acc.deductions.exists():
user = request.user user = request.user
if not user.in_group(ETS_GROUP): if not in_group(user, ETS_GROUP):
messages.info(request, CANCEL_ACC_RECORDED_OR_DEDUCTED) messages.info(request, CANCEL_ACC_RECORDED_OR_DEDUCTED)
return redirect("compensation:acc:detail", id=id) return redirect("compensation:acc:detail", id=id)
form = RemoveEcoAccountModalForm(request.POST or None, instance=acc, request=request) form = RemoveModalForm(request.POST or None, instance=acc, request=request)
return form.process_request( return form.process_request(
request=request, request=request,
msg_success=_("Eco-account removed"), msg_success=_("Eco-account removed"),

View File

@ -73,14 +73,13 @@ def report_view(request: HttpRequest, id: str):
"img": qrcode_img_lanis, "img": qrcode_img_lanis,
"url": qrcode_lanis_url, "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, "before_states": before_states,
"after_states": after_states, "after_states": after_states,
"geom_form": geom_form, "geom_form": geom_form,
"parcels": parcels, "parcels": parcels,
"actions": actions, "actions": actions,
"deductions": deductions, "deductions": deductions,
"tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title, TAB_TITLE_IDENTIFIER: tab_title,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context

View File

@ -6,15 +6,12 @@ from ema.models import Ema
class EmaAdmin(AbstractCompensationAdmin): class EmaAdmin(AbstractCompensationAdmin):
filter_horizontal = [ filter_horizontal = [
"users", "users"
"teams",
] ]
def get_fields(self, request, obj=None): def get_fields(self, request, obj=None):
return super().get_fields(request, obj) + [ return super().get_fields(request, obj) + [
"users", "users"
"teams",
] ]
admin.site.register(Ema, EmaAdmin) admin.site.register(Ema, EmaAdmin)

View File

@ -64,6 +64,8 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin, Pik
# Create log entry # Create log entry
action = UserActionLogEntry.get_created_action(user) action = UserActionLogEntry.get_created_action(user)
# Process the geometry form
geometry = geom_form.save(action)
handler = Handler.objects.create( handler = Handler.objects.create(
type=handler_type, type=handler_type,
@ -76,27 +78,22 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin, Pik
) )
# Finally create main object # Finally create main object
ema = Ema.objects.create( acc = Ema.objects.create(
identifier=identifier, identifier=identifier,
title=title, title=title,
responsible=responsible, responsible=responsible,
created=action, created=action,
modified=action, geometry=geometry,
comment=comment, comment=comment,
is_pik=is_pik, is_pik=is_pik,
) )
# Add the creating user to the list of shared users # Add the creating user to the list of shared users
ema.share_with_user(user) acc.share_with_user(user)
# Add the log entry to the main objects log list # Add the log entry to the main objects log list
ema.log.add(action) acc.log.add(action)
return acc
# Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
geometry = geom_form.save(action)
ema.geometry = geometry
ema.save()
return ema
class EditEmaForm(NewEmaForm): class EditEmaForm(NewEmaForm):
@ -133,6 +130,7 @@ class EditEmaForm(NewEmaForm):
def save(self, user: User, geom_form: SimpleGeomForm): def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic(): with transaction.atomic():
# Fetch data from cleaned POST values # Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None) title = self.cleaned_data.get("title", None)
handler_type = self.cleaned_data.get("handler_type", None) handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None) handler_detail = self.cleaned_data.get("handler_detail", None)
@ -143,6 +141,8 @@ class EditEmaForm(NewEmaForm):
# Create log entry # Create log entry
action = UserActionLogEntry.get_edited_action(user) action = UserActionLogEntry.get_edited_action(user)
# Process the geometry form
geometry = geom_form.save(action)
# Update responsible data # Update responsible data
self.instance.responsible.handler.type = handler_type self.instance.responsible.handler.type = handler_type
@ -153,7 +153,9 @@ class EditEmaForm(NewEmaForm):
self.instance.responsible.save() self.instance.responsible.save()
# Update main oject data # Update main oject data
self.instance.identifier = identifier
self.instance.title = title self.instance.title = title
self.instance.geometry = geometry
self.instance.comment = comment self.instance.comment = comment
self.instance.is_pik = is_pik self.instance.is_pik = is_pik
self.instance.modified = action self.instance.modified = action
@ -161,11 +163,6 @@ class EditEmaForm(NewEmaForm):
# Add the log entry to the main objects log list # Add the log entry to the main objects log list
self.instance.log.add(action) self.instance.log.add(action)
# Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
geometry = geom_form.save(action)
self.instance.geometry = geometry
self.instance.save()
return self.instance return self.instance

View File

@ -1,26 +0,0 @@
# Generated by Django 3.1.3 on 2022-11-16 12:22
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0006_auto_20220815_0759'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('ema', '0007_auto_20220815_1030'),
]
operations = [
migrations.AlterField(
model_name='ema',
name='teams',
field=models.ManyToManyField(blank=True, help_text='Teams having access (data shared with)', to='user.Team'),
),
migrations.AlterField(
model_name='ema',
name='users',
field=models.ManyToManyField(blank=True, help_text='Users having access (data shared with)', to=settings.AUTH_USER_MODEL),
),
]

View File

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

View File

@ -15,11 +15,8 @@ from django.urls import reverse
from compensation.models import AbstractCompensation, PikMixin from compensation.models import AbstractCompensation, PikMixin
from ema.managers import EmaManager from ema.managers import EmaManager
from ema.settings import EMA_IDENTIFIER_LENGTH, EMA_IDENTIFIER_TEMPLATE, EMA_LANIS_LAYER_NAME_RECORDED, \
EMA_LANIS_LAYER_NAME_UNRECORDED
from ema.utils.quality import EmaQualityChecker from ema.utils.quality import EmaQualityChecker
from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObjectMixin, ShareableObjectMixin from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObjectMixin, ShareableObjectMixin
from konova.sub_settings.django_settings import BASE_URL
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE
@ -41,18 +38,9 @@ class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, Pik
""" """
objects = EmaManager() objects = EmaManager()
identifier_length = EMA_IDENTIFIER_LENGTH
identifier_template = EMA_IDENTIFIER_TEMPLATE
def __str__(self): def __str__(self):
return "{}".format(self.identifier) return "{}".format(self.identifier)
def get_detail_url(self):
return reverse("ema:detail", args=(self.id,))
def get_detail_url_absolute(self):
return BASE_URL + self.get_detail_url()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.identifier is None or len(self.identifier) == 0: if self.identifier is None or len(self.identifier) == 0:
# Create new identifier # Create new identifier
@ -117,20 +105,6 @@ class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, Pik
""" """
return reverse("ema:share-token", args=(self.id, self.access_token)) return reverse("ema:share-token", args=(self.id, self.access_token))
def get_lanis_layer_name(self):
""" Getter for specific LANIS/WFS object layer
Returns:
"""
retval = None
if self.is_recorded:
retval = EMA_LANIS_LAYER_NAME_RECORDED
else:
retval = EMA_LANIS_LAYER_NAME_UNRECORDED
return retval
class EmaDocument(AbstractDocument): class EmaDocument(AbstractDocument):
""" """
@ -148,7 +122,7 @@ class EmaDocument(AbstractDocument):
def delete(self, user=None, *args, **kwargs): def delete(self, user=None, *args, **kwargs):
""" """
Custom delete functionality for EmaDocuments. Custom delete functionality for EcoAccountDocuments.
Removes the folder from the file system if there are no further documents for this entry. Removes the folder from the file system if there are no further documents for this entry.
Args: Args:
@ -165,11 +139,8 @@ class EmaDocument(AbstractDocument):
# The only file left for this EMA is the one which is currently processed and will be deleted # The only file left for this EMA is the one which is currently processed and will be deleted
# Make sure that the compensation folder itself is deleted as well, not only the file # Make sure that the compensation folder itself is deleted as well, not only the file
# Therefore take the folder path from the file path # Therefore take the folder path from the file path
try:
folder_path = self.file.path.split("/")[:-1] folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path) folder_path = "/".join(folder_path)
except ValueError:
folder_path = None
if user: if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title)) self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))

View File

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

View File

@ -6,18 +6,21 @@ Created on: 19.08.21
""" """
from django.http import HttpRequest from django.http import HttpRequest
from django.template.loader import render_to_string
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.timezone import localtime
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.urls import reverse from django.urls import reverse
import django_tables2 as tables import django_tables2 as tables
from konova.utils.tables import BaseTable, TableRenderMixin, TableOrderMixin from konova.sub_settings.django_settings import DEFAULT_DATE_TIME_FORMAT
from konova.utils.tables import BaseTable, TableRenderMixin
from ema.filters import EmaTableFilter from ema.filters import EmaTableFilter
from ema.models import Ema from ema.models import Ema
class EmaTable(BaseTable, TableRenderMixin, TableOrderMixin): class EmaTable(BaseTable, TableRenderMixin):
""" """
Since EMA and compensation are basically the same, we can reuse CompensationTableFilter and extend the EMA filter Since EMA and compensation are basically the same, we can reuse CompensationTableFilter and extend the EMA filter
in the future by inheriting. in the future by inheriting.
@ -34,7 +37,7 @@ class EmaTable(BaseTable, TableRenderMixin, TableOrderMixin):
) )
d = tables.Column( d = tables.Column(
verbose_name=_("Parcel gmrkng"), verbose_name=_("Parcel gmrkng"),
orderable=False, orderable=True,
accessor="geometry", accessor="geometry",
) )
r = tables.Column( r = tables.Column(
@ -90,6 +93,28 @@ class EmaTable(BaseTable, TableRenderMixin, TableOrderMixin):
) )
return format_html(html) return format_html(html)
def render_d(self, value, record: Ema):
""" Renders the parcel district column for a ema
Args:
value (str): The geometry
record (Ema): The ema record
Returns:
"""
parcels = value.get_underlying_parcels().values_list(
"parcel_group__name",
flat=True
).distinct()
html = render_to_string(
"table/gmrkng_col.html",
{
"entries": parcels
}
)
return html
def render_r(self, value, record: Ema): def render_r(self, value, record: Ema):
""" Renders the registered column for a EMA """ Renders the registered column for a EMA
@ -111,3 +136,22 @@ class EmaTable(BaseTable, TableRenderMixin, TableOrderMixin):
icn_filled=recorded, icn_filled=recorded,
) )
return format_html(html) return format_html(html)
def render_e(self, value, record: Ema):
""" Renders the editable column for a EMA
Args:
value (str): The identifier value
record (Ema): The EMA record
Returns:
"""
html = ""
has_access = record.is_shared_with(self.user)
html += self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),
icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit",
)
return format_html(html)

View File

@ -10,7 +10,7 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
{% if is_default_member and 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' %}"> <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-action' obj.id %}" title="{% trans 'Add new action' %}">
{% fa5_icon 'plus' %} {% fa5_icon 'plus' %}
{% fa5_icon 'seedling' %} {% fa5_icon 'seedling' %}
@ -20,7 +20,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card-body {% if tables_scrollable %}scroll-300{% endif %} p-2"> <div class="card-body scroll-300 p-2">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@ -49,7 +49,7 @@
<hr> <hr>
{% endfor %} {% endfor %}
{% for detail in action.action_type_details.all %} {% for detail in action.action_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{ detail.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 %} {% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span> <span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
{% endfor %} {% endfor %}
@ -61,7 +61,7 @@
</div> </div>
</td> </td>
<td class="align-middle float-right"> <td class="align-middle float-right">
{% if is_default_member and 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' %}"> <button data-form-url="{% url 'ema:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>
@ -71,10 +71,6 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %}
<div class="alert alert-danger mb-0">
{% trans 'Missing' %}
</div>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

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

View File

@ -10,7 +10,7 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
{% if is_default_member and 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' %}"> <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
{% fa5_icon 'plus' %} {% fa5_icon 'plus' %}
{% fa5_icon 'calendar-check' %} {% fa5_icon 'calendar-check' %}
@ -25,7 +25,7 @@
{% trans 'Missing finished deadline ' %} {% trans 'Missing finished deadline ' %}
</div> </div>
{% endif %} {% endif %}
<div class="card-body {% if tables_scrollable %}scroll-300{% endif %} p-2"> <div class="card-body scroll-300 p-2">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@ -58,7 +58,7 @@
</div> </div>
</td> </td>
<td class="align-middle float-right"> <td class="align-middle float-right">
{% if is_default_member and 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' %}"> <button data-form-url="{% url 'ema:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>

View File

@ -10,7 +10,7 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
{% if is_default_member and 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' %}"> <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
{% fa5_icon 'plus' %} {% fa5_icon 'plus' %}
{% fa5_icon 'file' %} {% fa5_icon 'file' %}
@ -20,7 +20,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card-body {% if tables_scrollable %}scroll-300{% endif %} p-2"> <div class="card-body scroll-300 p-2">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@ -57,7 +57,7 @@
</div> </div>
</td> </td>
<td class="align-middle float-right"> <td class="align-middle float-right">
{% if is_default_member and 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' %}"> <button data-form-url="{% url 'ema:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>

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