Compare commits

..

No commits in common. "master" and "v0.5" have entirely different histories.
master ... v0.5

409 changed files with 11636 additions and 18903 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,8 +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..."),
widget=forms.DateInput( widget=forms.DateInput(
attrs={ attrs={
"type": "date", "type": "date",
@ -37,8 +34,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..."),
widget=forms.DateInput( widget=forms.DateInput(
attrs={ attrs={
"type": "date", "type": "date",
@ -58,7 +53,7 @@ class TimespanReportForm(BaseForm):
code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID], code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID],
), ),
widget=autocomplete.ModelSelect2( widget=autocomplete.ModelSelect2(
url="codelist:conservation-office-autocomplete", url="codes-conservation-office-autocomplete",
attrs={ attrs={
"data-placeholder": _("Click for selection") "data-placeholder": _("Click for selection")
} }

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

@ -15,40 +15,40 @@
<thead> <thead>
<tr> <tr>
<th scope="col">{% trans 'Area of responsibility' %}</th> <th scope="col">{% trans 'Area of responsibility' %}</th>
<th scope="col">{% trans 'Total' %}</th>
<th scope="col">{% trans 'Number single areas' %}</th>
<th scope="col">{% fa5_icon 'star' %} {% trans 'Checked' %}</th> <th scope="col">{% fa5_icon 'star' %} {% trans 'Checked' %}</th>
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th> <th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
<th scope="col">{% trans 'Number single areas' %}</th>
<th scope="col">{% trans 'Total' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{% trans 'Conservation office by law' %}</td> <td>{% trans 'Conservation office by law' %}</td>
<td>{{report.compensation_report.queryset_registration_office_unb_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.num_single_surfaces_total_unb|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_unb_checked_count|default_if_zero:"-"}}</td> <td>{{report.compensation_report.queryset_registration_office_unb_checked_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_unb_recorded_count|default_if_zero:"-"}}</td> <td>{{report.compensation_report.queryset_registration_office_unb_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.num_single_surfaces_total_unb|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_unb_count|default_if_zero:"-"}}</td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Land-use planning' %}</td> <td>{% trans 'Land-use planning' %}</td>
<td>{{report.compensation_report.queryset_registration_office_tbp_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.num_single_surfaces_total_tbp|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_tbp_checked_count|default_if_zero:"-"}}</td> <td>{{report.compensation_report.queryset_registration_office_tbp_checked_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_tbp_recorded_count|default_if_zero:"-"}}</td> <td>{{report.compensation_report.queryset_registration_office_tbp_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.num_single_surfaces_total_tbp|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_tbp_count|default_if_zero:"-"}}</td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Other registration office' %}</td> <td>{% trans 'Other registration office' %}</td>
<td>{{report.compensation_report.queryset_registration_office_other_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.num_single_surfaces_total_other|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_other_checked_count|default_if_zero:"-"}}</td> <td>{{report.compensation_report.queryset_registration_office_other_checked_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_other_recorded_count|default_if_zero:"-"}}</td> <td>{{report.compensation_report.queryset_registration_office_other_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.num_single_surfaces_total_other|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_other_count|default_if_zero:"-"}}</td>
</tr> </tr>
<tr> <tr>
<td><strong>{% trans 'Total' %}</strong></td> <td><strong>{% trans 'Total' %}</strong></td>
<td><strong>{{report.compensation_report.queryset_count|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.compensation_report.num_single_surfaces_total|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.compensation_report.queryset_checked_count|default_if_zero:"-"}}</strong></td> <td><strong>{{report.compensation_report.queryset_checked_count|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.compensation_report.queryset_recorded_count|default_if_zero:"-"}}</strong></td> <td><strong>{{report.compensation_report.queryset_recorded_count|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.compensation_report.num_single_surfaces_total|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.compensation_report.queryset_count|default_if_zero:"-"}}</strong></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

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,14 +10,14 @@
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th scope="col">{% trans 'Total' %}</th>
<th scope="col" class="w-25">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th> <th scope="col" class="w-25">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
<th scope="col">{% trans 'Total' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{{report.eco_account_report.queryset_count|default_if_zero:"-"}}</td>
<td>{{report.eco_account_report.queryset_recorded_count|default_if_zero:"-"}}</td> <td>{{report.eco_account_report.queryset_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.eco_account_report.queryset_count|default_if_zero:"-"}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

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

@ -1,37 +1,22 @@
{% load i18n fontawesome_5 ksp_filters %} {% load i18n fontawesome_5 ksp_filters %}
<h3>{% trans 'Deductions' %}</h3> <h3>{% trans 'Deductions' %}</h3>
<strong>
{% blocktrans %}
Recorded = Counts the deductions whose interventions have been recorded
{% endblocktrans %}
</strong>
<div class="table-container"> <div class="table-container">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th scope="col" class="w-25">{% trans 'Total' %}</th>
<th scope="col" class="w-25">{% trans 'Total' %} {% trans 'Surface' %}</th>
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th> <th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %} {% trans 'Surface' %}</th> <th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %} {% trans 'Surface' %}</th>
<th scope="col" class="w-25">{% trans 'Total' %}</th>
<th scope="col" class="w-25">{% trans 'Total' %} {% trans 'Surface' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{{report.eco_account_report.queryset_deductions_count|default_if_zero:"-"}}</td>
<td>
{{report.eco_account_report.deductions_sq_m|default_if_zero:"-"}}
{% if report.eco_account_report.deductions_sq_m > 0 %}
{% endif %}
</td>
<td>{{report.eco_account_report.queryset_deductions_recorded_count|default_if_zero:"-"}}</td> <td>{{report.eco_account_report.queryset_deductions_recorded_count|default_if_zero:"-"}}</td>
<td> <td>{{report.eco_account_report.recorded_deductions_sq_m|default_if_zero:"-"}}m²</td>
{{report.eco_account_report.recorded_deductions_sq_m|default_if_zero:"-"}} <td>{{report.eco_account_report.queryset_deductions_count|default_if_zero:"-"}}</td>
{% if report.eco_account_report.recorded_deductions_sq_m > 0 %} <td>{{report.eco_account_report.deductions_sq_m|default_if_zero:"-"}}m²</td>
{% endif %}
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -14,16 +14,16 @@
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th scope="col" class="w-25">{% trans 'Total' %}</th>
<th scope="col">{% fa5_icon 'star' %} {% trans 'Checked' %}</th> <th scope="col">{% fa5_icon 'star' %} {% trans 'Checked' %}</th>
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th> <th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
<th scope="col" class="w-25">{% trans 'Total' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{{report.intervention_report.queryset_count|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.queryset_checked_count|default_if_zero:"-"}}</td> <td>{{report.intervention_report.queryset_checked_count|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.queryset_recorded_count|default_if_zero:"-"}}</td> <td>{{report.intervention_report.queryset_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.queryset_count|default_if_zero:"-"}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

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>

View File

@ -5,29 +5,29 @@
<thead> <thead>
<tr> <tr>
<th class="w-25" scope="col">{% trans 'Compensation type' %}</th> <th class="w-25" scope="col">{% trans 'Compensation type' %}</th>
<th class="w-25" scope="col">{% trans 'Total' %}</th>
<th class="w-25" scope="col">{% fa5_icon 'star' %} {% trans 'Checked' %}</th> <th class="w-25" scope="col">{% fa5_icon 'star' %} {% trans 'Checked' %}</th>
<th class="w-25" scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th> <th class="w-25" scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
<th class="w-25" scope="col">{% trans 'Total' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<th>{% trans 'Compensation' %}</th> <th>{% trans 'Compensation' %}</th>
<td>{{report.intervention_report.compensation_sum|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.compensation_sum_checked|default_if_zero:"-"}}</td> <td>{{report.intervention_report.compensation_sum_checked|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.compensation_sum_recorded|default_if_zero:"-"}}</td> <td>{{report.intervention_report.compensation_sum_recorded|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.compensation_sum|default_if_zero:"-"}}</td>
</tr> </tr>
<tr> <tr>
<th>{% trans 'Payment' %}</th> <th>{% trans 'Payment' %}</th>
<td>{{report.intervention_report.payment_sum|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.payment_sum_checked|default_if_zero:"-"}}</td> <td>{{report.intervention_report.payment_sum_checked|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.payment_sum_recorded|default_if_zero:"-"}}</td> <td>{{report.intervention_report.payment_sum_recorded|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.payment_sum|default_if_zero:"-"}}</td>
</tr> </tr>
<tr> <tr>
<th>{% trans 'Deductions' %}</th> <th>{% trans 'Deductions' %}</th>
<td>{{report.intervention_report.deduction_sum|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.deduction_sum_checked|default_if_zero:"-"}}</td> <td>{{report.intervention_report.deduction_sum_checked|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.deduction_sum_recorded|default_if_zero:"-"}}</td> <td>{{report.intervention_report.deduction_sum_recorded|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.deduction_sum|default_if_zero:"-"}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -13,15 +13,15 @@
<th class="w-25" scope="col"> <th class="w-25" scope="col">
{% trans 'Law' %} {% trans 'Law' %}
</th> </th>
<th scope="col">
{% trans 'Total' %}
</th>
<th scope="col"> <th scope="col">
{% fa5_icon 'star' %} {% trans 'Checked' %} {% fa5_icon 'star' %} {% trans 'Checked' %}
</th> </th>
<th scope="col"> <th scope="col">
{% fa5_icon 'bookmark' %} {% trans 'Recorded' %} {% fa5_icon 'bookmark' %} {% trans 'Recorded' %}
</th> </th>
<th scope="col">
{% trans 'Total' %}
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -34,16 +34,16 @@
{{law.long_name}} {{law.long_name}}
</small> </small>
</td> </td>
<td>{{law.num|default_if_zero:"-"}}</td>
<td>{{law.num_checked|default_if_zero:"-"}}</td> <td>{{law.num_checked|default_if_zero:"-"}}</td>
<td>{{law.num_recorded|default_if_zero:"-"}}</td> <td>{{law.num_recorded|default_if_zero:"-"}}</td>
<td>{{law.num|default_if_zero:"-"}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
<tr> <tr>
<td><strong>{% trans 'Total' %}</strong></td> <td><strong>{% trans 'Total' %}</strong></td>
<td><strong>{{report.intervention_report.law_sum|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.intervention_report.law_sum_checked|default_if_zero:"-"}}</strong></td> <td><strong>{{report.intervention_report.law_sum_checked|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.intervention_report.law_sum_recorded|default_if_zero:"-"}}</strong></td> <td><strong>{{report.intervention_report.law_sum_recorded|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.intervention_report.law_sum|default_if_zero:"-"}}</strong></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -14,26 +14,26 @@
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th scope="col" class="w-25">{% trans 'Type' %}</th> <th scope="col" class="w-25">{% fa5_icon 'star' %} {% trans 'Type' %}</th>
<th scope="col">{% trans 'Total' %}</th>
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th> <th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
<th scope="col">{% trans 'Total' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{% trans 'Intervention' %}</td> <td>{% trans 'Intervention' %}</td>
<td>{{report.old_data_report.queryset_intervention_count|default_if_zero:"-"}}</td>
<td>{{report.old_data_report.queryset_intervention_recorded_count|default_if_zero:"-"}}</td> <td>{{report.old_data_report.queryset_intervention_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.old_data_report.queryset_intervention_count|default_if_zero:"-"}}</td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Compensation' %}</td> <td>{% trans 'Compensation' %}</td>
<td>{{report.old_data_report.queryset_comps_count|default_if_zero:"-"}}</td>
<td>{{report.old_data_report.queryset_comps_recorded_count|default_if_zero:"-"}}</td> <td>{{report.old_data_report.queryset_comps_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.old_data_report.queryset_comps_count|default_if_zero:"-"}}</td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Eco-account' %}</td> <td>{% trans 'Eco-account' %}</td>
<td>{{report.old_data_report.queryset_acc_count|default_if_zero:"-"}}</td>
<td>{{report.old_data_report.queryset_acc_recorded_count|default_if_zero:"-"}}</td> <td>{{report.old_data_report.queryset_acc_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.old_data_report.queryset_acc_count|default_if_zero:"-"}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -10,7 +10,7 @@
{% fa5_icon 'pencil-ruler' %} {% fa5_icon 'pencil-ruler' %}
{% trans 'Old interventions' %} {% trans 'Old interventions' %}
</h5> </h5>
<span>{% trans 'Binding date before' %} 16.06.2018</span> <span>{% trans 'Before' %} 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

@ -108,7 +108,7 @@ class TempExcelFile:
for _iter_entry in _iter_obj: for _iter_entry in _iter_obj:
j = 0 j = 0
for _iter_attr in _attrs: for _iter_attr in _attrs:
_new_cell = ws.cell(start_cell.row + i, start_cell.column + j, _iter_entry.get(_iter_attr, "MISSING")) _new_cell = ws.cell(start_cell.row + i, start_cell.column + j, getattr(_iter_entry, _iter_attr))
_new_cell.border = border_style _new_cell.border = border_style
j += 1 j += 1
i += 1 i += 1

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,
@ -138,36 +137,22 @@ class TimespanReport:
).order_by( ).order_by(
"long_name" "long_name"
) )
# Fetch all law ids which are used by any .legal object of an intervention object
evaluated_laws = [] intervention_laws_total = self.queryset.values_list("legal__laws__id")
sum_num_checked = 0 intervention_laws_checked = self.queryset.filter(checked__isnull=False).values_list("legal__laws__id")
sum_num_recorded = 0 intervention_laws_recorded = self.queryset.filter(recorded__isnull=False).values_list(
sum_num = 0 "legal__laws__id")
for law in laws: # Count how often which law id appears in the above list, return only the long_name of the law and the resulting
num = self.queryset.filter( # count (here 'num'). This is for keeping the db fetch as small as possible
legal__laws__atom_id=law.atom_id # Compute the sum for total, checked and recorded
).count() self.evaluated_laws = laws.annotate(
num_checked = self.queryset_checked.filter( num=Count("id", filter=Q(id__in=intervention_laws_total)),
legal__laws__atom_id=law.atom_id num_checked=Count("id", filter=Q(id__in=intervention_laws_checked)),
).count() num_recorded=Count("id", filter=Q(id__in=intervention_laws_recorded)),
num_recorded = self.queryset_recorded.filter( ).values_list("short_name", "long_name", "num_checked", "num_recorded", "num", named=True)
legal__laws__atom_id=law.atom_id self.law_sum = self.evaluated_laws.aggregate(sum_num=Sum("num"))["sum_num"]
).count() self.law_sum_checked = self.evaluated_laws.aggregate(sum_num_checked=Sum("num_checked"))["sum_num_checked"]
evaluated_laws.append({ self.law_sum_recorded = self.evaluated_laws.aggregate(sum_num_recorded=Sum("num_recorded"))["sum_num_recorded"]
"short_name": law.short_name,
"long_name": law.long_name,
"num": num,
"num_checked": num_checked,
"num_recorded": num_recorded,
})
sum_num += num
sum_num_checked += num_checked
sum_num_recorded += num_recorded
self.evaluated_laws = evaluated_laws
self.law_sum = sum_num
self.law_sum_checked = sum_num_checked
self.law_sum_recorded = sum_num_recorded
def _evaluate_compensations(self): def _evaluate_compensations(self):
""" Analyzes the types of compensation distribution """ Analyzes the types of compensation distribution
@ -334,7 +319,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 +398,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 +501,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

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

View File

@ -11,9 +11,7 @@ from abc import abstractmethod
from django.contrib.gis import geos from django.contrib.gis import geos
from django.contrib.gis.geos import GEOSGeometry from django.contrib.gis.geos import GEOSGeometry
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models import Q
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.utils.message_templates import DATA_UNSHARED from konova.utils.message_templates import DATA_UNSHARED
@ -33,8 +31,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 +75,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 +85,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)
@ -145,8 +133,8 @@ class AbstractModelAPISerializer:
if isinstance(geojson, dict): if isinstance(geojson, dict):
geojson = json.dumps(geojson) geojson = json.dumps(geojson)
geometry = geos.fromstr(geojson) geometry = geos.fromstr(geojson)
if geometry.srid != DEFAULT_SRID_RLP: if geometry.empty:
geometry.transform(DEFAULT_SRID_RLP) 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

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

View File

@ -1,74 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from dal_select2.views import Select2GroupQuerySetView
from django.db.models import Q
from codelist.models import KonovaCode
class KonovaCodeAutocomplete(Select2GroupQuerySetView):
"""
Provides simple autocomplete functionality for codes
Parameter support:
* q: Search for a word inside long_name of a code
* c: Search inside a special codelist
"""
paginate_by = 50
def order_by(self, qs):
""" Orders by a predefined value
Wrapped in a function to provide inheritance-based different orders
Args:
qs (QuerySet): The queryset to be ordered
Returns:
qs (QuerySet): The ordered queryset
"""
return qs.order_by(
"long_name"
)
def get_queryset(self):
if self.request.user.is_anonymous:
return KonovaCode.objects.none()
qs = KonovaCode.objects.filter(
is_archived=False,
is_selectable=True,
is_leaf=True,
)
qs = self.order_by(qs)
if self.c:
qs = qs.filter(
code_lists__in=[self.c]
)
if self.q:
# Remove whitespaces from self.q and split input in all keywords (if multiple given)
q = dict.fromkeys(self.q.strip().split(" "))
# Create one filter looking up for all keys where all keywords can be found in the same result
_filter = Q()
for keyword in q:
q_or = Q()
q_or |= Q(long_name__icontains=keyword)
q_or |= Q(short_name__icontains=keyword)
q_or |= Q(parent__long_name__icontains=keyword)
q_or |= Q(parent__short_name__icontains=keyword)
q_or |= Q(parent__parent__long_name__icontains=keyword)
q_or |= Q(parent__parent__short_name__icontains=keyword)
_filter.add(q_or, Q.AND)
qs = qs.filter(_filter).distinct()
return qs
def get_result_label(self, result):
return f"{result.long_name}"
def get_selected_result_label(self, result):
return f"{result.__str__()}"

View File

@ -1,114 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
import collections
from django.core.exceptions import ImproperlyConfigured
from codelist.settings import CODELIST_BIOTOPES_ID, \
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
from codelist.autocomplete.base import KonovaCodeAutocomplete
from konova.utils.message_templates import UNGROUPED
class BiotopeCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_BIOTOPES_ID
super().__init__(*args, **kwargs)
def order_by(self, qs):
""" Orders by a predefined value
Wrapped in a function to provide inheritance-based different orders
Args:
qs (QuerySet): The queryset to be ordered
Returns:
qs (QuerySet): The ordered queryset
"""
return qs.order_by(
"short_name",
)
def get_result_label(self, result):
return f"{result.long_name} ({result.short_name})"
def get_results(self, context):
"""Return the options grouped by a common related model.
Raises ImproperlyConfigured if self.group_by_name is not configured
"""
if not self.group_by_related:
raise ImproperlyConfigured("Missing group_by_related.")
super_groups = collections.OrderedDict()
object_list = context['object_list']
for result in object_list:
group = result.parent if result.parent else None
group_name = f"{group.long_name} ({group.short_name})" if group else UNGROUPED
super_group = result.parent.parent if result.parent else None
super_group_name = f"{super_group.long_name} ({super_group.short_name})" if super_group else UNGROUPED
super_groups.setdefault(super_group_name, {})
super_groups[super_group_name].setdefault(group_name, [])
super_groups[super_group_name][group_name].append(result)
return [{
'id': None,
'text': super_group,
'children': [{
"id": None,
"text": group,
"children": [{
'id': self.get_result_value(result),
'text': self.get_result_label(result),
'selected_text': self.get_selected_result_label(result),
} for result in results]
} for group, results in groups.items()]
} for super_group, groups in super_groups.items()]
class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "short_name"
paginate_by = 200
def __init__(self, *args, **kwargs):
self.c = CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
super().__init__(*args, **kwargs)
def order_by(self, qs):
""" Orders by a predefined value
Wrapped in a function to provide inheritance-based different orders
Args:
qs (QuerySet): The queryset to be ordered
Returns:
qs (QuerySet): The ordered queryset
"""
return qs.order_by(
"short_name",
)
def get_result_label(self, result):
return f"{result.long_name} ({result.short_name})"
def get_selected_result_label(self, result):
return f"{result.parent.short_name} > {result.long_name} ({result.short_name})"

View File

@ -1,45 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID
from codelist.autocomplete.base import KonovaCodeAutocomplete
class CompensationActionCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_COMPENSATION_ACTION_ID
super().__init__(*args, **kwargs)
def order_by(self, qs):
return qs.order_by(
"parent__long_name"
)
class CompensationActionDetailCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
paginate_by = 200
def __init__(self, *args, **kwargs):
self.c = CODELIST_COMPENSATION_ACTION_DETAIL_ID
super().__init__(*args, **kwargs)
def order_by(self, qs):
return qs.order_by(
"long_name"
)

View File

@ -1,24 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from codelist.settings import CODELIST_HANDLER_ID
from codelist.autocomplete.base import KonovaCodeAutocomplete
class HandlerCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_HANDLER_ID
super().__init__(*args, **kwargs)
def get_result_label(self, result):
return result.long_name

View File

@ -1,24 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from codelist.settings import CODELIST_LAW_ID
from codelist.autocomplete.base import KonovaCodeAutocomplete
class LawCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_LAW_ID
super().__init__(*args, **kwargs)
def get_result_label(self, result):
return f"{result.long_name} ({result.short_name})"

View File

@ -1,41 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_REGISTRATION_OFFICE_ID
from codelist.autocomplete.base import KonovaCodeAutocomplete
class RegistrationOfficeCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_REGISTRATION_OFFICE_ID
super().__init__(*args, **kwargs)
def order_by(self, qs):
return qs.order_by(
"parent__long_name"
)
class ConservationOfficeCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_CONSERVATION_OFFICE_ID
super().__init__(*args, **kwargs)
def get_result_label(self, result):
return f"{result.long_name} ({result.short_name})"

View File

@ -1,21 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from codelist.autocomplete.base import KonovaCodeAutocomplete
from codelist.settings import CODELIST_PROCESS_TYPE_ID
class ProcessTypeCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_PROCESS_TYPE_ID
super().__init__(*args, **kwargs)

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

@ -7,24 +7,8 @@ Created on: 23.08.21
""" """
from django.urls import path from django.urls import path
from codelist.autocomplete.biotope import BiotopeCodeAutocomplete, BiotopeExtraCodeAutocomplete
from codelist.autocomplete.compensation_action import CompensationActionDetailCodeAutocomplete, \
CompensationActionCodeAutocomplete
from codelist.autocomplete.handler import HandlerCodeAutocomplete
from codelist.autocomplete.law import LawCodeAutocomplete
from codelist.autocomplete.office import ConservationOfficeCodeAutocomplete, RegistrationOfficeCodeAutocomplete
from codelist.autocomplete.process_type import ProcessTypeCodeAutocomplete
app_name = "codelist" app_name = "codelist"
urlpatterns = [ urlpatterns = [
path("atcmplt/codes/biotope", BiotopeCodeAutocomplete.as_view(), name="biotope-autocomplete"),
path("atcmplt/codes/biotope/extra", BiotopeExtraCodeAutocomplete.as_view(),
name="biotope-extra-type-autocomplete"),
path("atcmplt/codes/law", LawCodeAutocomplete.as_view(), name="law-autocomplete"),
path("atcmplt/codes/reg-off", RegistrationOfficeCodeAutocomplete.as_view(), name="registration-office-autocomplete"),
path("atcmplt/codes/cons-off", ConservationOfficeCodeAutocomplete.as_view(), name="conservation-office-autocomplete"),
path("atcmplt/codes/handler", HandlerCodeAutocomplete.as_view(), name="handler-autocomplete"),
path("atcmplt/codes/comp/action", CompensationActionCodeAutocomplete.as_view(), name="compensation-action-autocomplete"),
path("atcmplt/codes/comp/action/detail", CompensationActionDetailCodeAutocomplete.as_view(), name="compensation-action-detail-autocomplete"),
path("atcmplt/codes/prc-type", ProcessTypeCodeAutocomplete.as_view(), name="process-type-autocomplete"),
] ]

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

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

View File

@ -1,40 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from dal_select2.views import Select2QuerySetView
from django.db.models import Q
from compensation.models import EcoAccount
class EcoAccountAutocomplete(Select2QuerySetView):
""" Autocomplete for ecoAccount entries
Only returns entries that are already recorded and not deleted
"""
def get_queryset(self):
if self.request.user.is_anonymous:
return EcoAccount.objects.none()
qs = EcoAccount.objects.filter(
deleted=None,
recorded__isnull=False,
).order_by(
"identifier"
)
if self.q:
qs = qs.filter(
Q(identifier__icontains=self.q) |
Q(title__icontains=self.q)
).distinct()
return qs
def get_result_label(self, result):
return str(result)
def get_selected_result_label(self, result):
return str(result)

View File

@ -1,14 +1,17 @@
""" """
Author: Michel Peltriaux Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 18.08.22 Created on: 29.07.21
""" """
import django_filters
from django.utils.translation import gettext_lazy as _
from django import forms
from django.db.models import QuerySet, Q from django.db.models import QuerySet, Q
from konova.filters.table_filters import AbstractTableFilter, CheckboxTableFilter, QueryTableFilter, \ from konova.filters.mixins import ConservationOfficeTableFilterMixin
SelectionTableFilter from konova.filters.table_filters import QueryTableFilter, CheckboxTableFilter, SelectionTableFilter, AbstractTableFilter
class SelectionCompensationTableFilter(SelectionTableFilter): class SelectionCompensationTableFilter(SelectionTableFilter):
@ -55,12 +58,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
@ -113,3 +114,71 @@ class CompensationTableFilter(AbstractTableFilter):
) )
# Overwrite final queryset as well # Overwrite final queryset as well
self.qs = self.checkbox_filter.qs self.qs = self.checkbox_filter.qs
class CheckboxEcoAccountTableFilter(CheckboxTableFilter):
sr = django_filters.BooleanFilter(
method='filter_only_show_unrecorded',
label=_("Show only unrecorded"),
label_suffix=_(""),
widget=forms.CheckboxInput(
attrs={
"class": "form-check-input",
}
)
)
def filter_only_show_unrecorded(self, queryset, name, value) -> QuerySet:
""" Filters queryset depending on value of 'show_recorded' setting
Args:
queryset ():
name ():
value ():
Returns:
"""
if value:
return queryset.filter(
recorded=None,
)
else:
return queryset
class SelectionEcoAccountTableFilter(ConservationOfficeTableFilterMixin):
""" Special selection table filter for eco accounts
EcoAccounts only need a selection filter for conservation office
"""
pass
class EcoAccountTableFilter(AbstractTableFilter):
""" TableFilter for eco accounts
"""
def __init__(self, user=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = user
qs = kwargs.get("queryset", None)
request_data = kwargs.get("data", None)
# Pipe the queryset through all needed filters
self.selection_filter = SelectionEcoAccountTableFilter(
data=request_data,
queryset=qs,
)
self.query_filter = QueryTableFilter(
data=request_data,
queryset=self.selection_filter.qs,
)
self.checkbox_filter = CheckboxEcoAccountTableFilter(
user=user,
data=request_data,
queryset=self.query_filter.qs,
)
# Overwrite the final queryset result
self.qs = self.checkbox_filter.qs

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: 18.08.22
"""

View File

@ -1,82 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from django import forms
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
import django_filters
from konova.filters.mixins.office import ConservationOfficeTableFilterMixin
from konova.filters.table_filters import AbstractTableFilter, CheckboxTableFilter, QueryTableFilter
class CheckboxEcoAccountTableFilter(CheckboxTableFilter):
sr = django_filters.BooleanFilter(
method='filter_only_show_unrecorded',
label=_("Show only unrecorded"),
label_suffix=_(""),
widget=forms.CheckboxInput(
attrs={
"class": "form-check-input",
}
)
)
def filter_only_show_unrecorded(self, queryset, name, value) -> QuerySet:
""" Filters queryset depending on value of 'show_recorded' setting
Args:
queryset ():
name ():
value ():
Returns:
"""
if value:
return queryset.filter(
recorded=None,
)
else:
return queryset
class SelectionEcoAccountTableFilter(ConservationOfficeTableFilterMixin):
""" Special selection table filter for eco accounts
EcoAccounts only need a selection filter for conservation office
"""
pass
class EcoAccountTableFilter(AbstractTableFilter):
""" TableFilter for eco accounts
"""
def __init__(self, user=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = user
qs = kwargs.get("queryset", None)
request_data = kwargs.get("data", None)
# Pipe the queryset through all needed filters
self.selection_filter = SelectionEcoAccountTableFilter(
data=request_data,
queryset=qs,
)
self.query_filter = QueryTableFilter(
data=request_data,
queryset=self.selection_filter.qs,
)
self.checkbox_filter = CheckboxEcoAccountTableFilter(
user=user,
data=request_data,
queryset=self.query_filter.qs,
)
# Overwrite the final queryset result
self.qs = self.checkbox_filter.qs

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: 18.08.22
"""

View File

@ -1,240 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from dal import autocomplete
from django import forms
from django.db import transaction
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from compensation.forms.mixins import CEFCompensationFormMixin, CoherenceCompensationFormMixin, PikCompensationFormMixin
from compensation.models import Compensation
from intervention.inputs import GenerateInput
from intervention.models import Intervention
from konova.forms import BaseForm, SimpleGeomForm
from konova.utils.message_templates import COMPENSATION_ADDED_TEMPLATE, EDITED_GENERAL_DATA
from user.models import UserActionLogEntry, User
class AbstractCompensationForm(BaseForm):
""" Abstract form for compensations
Holds all important form fields, which are used in compensation and eco account forms
"""
identifier = forms.CharField(
label=_("Identifier"),
label_suffix="",
max_length=255,
help_text=_("Generated automatically - not editable"),
widget=GenerateInput(
attrs={
"class": "form-control",
"url": None, # Needs to be set in inheriting constructors
"readonly": True,
}
)
)
title = forms.CharField(
label=_("Title"),
label_suffix="",
help_text=_("An explanatory name"),
max_length=255,
widget=forms.TextInput(
attrs={
"placeholder": _("Compensation XY; Location ABC"),
"class": "form-control",
}
)
)
comment = forms.CharField(
label_suffix="",
label=_("Comment"),
required=False,
help_text=_("Additional comment"),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control"
}
)
)
class Meta:
abstract = True
class NewCompensationForm(AbstractCompensationForm,
CEFCompensationFormMixin,
CoherenceCompensationFormMixin,
PikCompensationFormMixin):
""" Form for creating new compensations.
Can be initialized with an intervention id for preselecting the related intervention.
form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
...
The intervention id will not be resolved into the intervention ORM object but instead will be used to initialize
the related form field.
"""
intervention = forms.ModelChoiceField(
label=_("compensates intervention"),
label_suffix="",
help_text=_("Select the intervention for which this compensation compensates"),
queryset=Intervention.objects.filter(
deleted=None,
),
widget=autocomplete.ModelSelect2(
url="intervention:autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
"data-minimum-input-length": 3,
}
),
)
# Define a field order for a nicer layout instead of running with the inheritance result
field_order = [
"identifier",
"title",
"intervention",
"is_pik",
"is_cef",
"is_coherence_keeping",
"comment",
]
def __init__(self, *args, **kwargs):
intervention_id = kwargs.pop("intervention_id", None)
super().__init__(*args, **kwargs)
self.form_title = _("New compensation")
# If the compensation shall directly be initialized from an intervention, we need to fill in the intervention id
# and disable the form field.
# Furthermore the action_url needs to be set accordingly.
if intervention_id is not None:
self.initialize_form_field("intervention", intervention_id)
self.disable_form_field("intervention")
self.action_url = reverse("compensation:new", args=(intervention_id,))
self.cancel_redirect = reverse("intervention:detail", args=(intervention_id,))
else:
self.action_url = reverse("compensation:new")
self.cancel_redirect = reverse("compensation:index")
tmp = Compensation()
identifier = tmp.generate_new_identifier()
self.initialize_form_field("identifier", identifier)
self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:new-id")
def __create_comp(self, user):
""" Creates the compensation from form data
Args:
user (User): The performing user
Returns:
comp (Compensation): The compensation object
"""
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None)
is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None)
is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.get_created_action(user)
# Finally create main object
comp = Compensation.objects.create(
identifier=identifier,
title=title,
intervention=intervention,
created=action,
modified=action,
is_cef=is_cef,
is_coherence_keeping=is_coherence_keeping,
is_pik=is_pik,
comment=comment,
)
# Add the log entry to the main objects log list
comp.log.add(action)
return comp, action
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
comp, action = self.__create_comp(user)
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
class EditCompensationForm(NewCompensationForm):
""" Form for editing compensations
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Edit compensation")
self.action_url = reverse("compensation:edit", args=(self.instance.id,))
self.cancel_redirect = reverse("compensation:detail", args=(self.instance.id,))
# Initialize form data
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"intervention": self.instance.intervention,
"is_cef": self.instance.is_cef,
"is_coherence_keeping": self.instance.is_coherence_keeping,
"is_pik": self.instance.is_pik,
"comment": self.instance.comment,
}
disabled_fields = []
self.load_initial_data(
form_data,
disabled_fields
)
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Create log entry
action = UserActionLogEntry.get_edited_action(user)
# Fetch data from cleaned POST values
title = self.cleaned_data.get("title", None)
intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None)
is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None)
is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None)
self.instance.title = title
self.instance.intervention = intervention
self.instance.is_cef = is_cef
self.instance.is_coherence_keeping = is_coherence_keeping
self.instance.comment = comment
self.instance.is_pik = is_pik
self.instance.modified = action
self.instance.save()
self.instance.log.add(action)
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

View File

@ -1,249 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from django import forms
from django.db import transaction
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from compensation.forms.compensation import AbstractCompensationForm
from compensation.forms.mixins import CompensationResponsibleFormMixin, PikCompensationFormMixin
from compensation.models import EcoAccount
from intervention.models import Handler, Responsibility, Legal
from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm
from konova.utils import validators
from user.models import User, UserActionLogEntry
class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMixin, PikCompensationFormMixin):
""" Form for creating eco accounts
Inherits from basic AbstractCompensationForm and further form fields from CompensationResponsibleFormMixin
"""
surface = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_("Available Surface"),
label_suffix="",
required=False,
help_text=_("The amount that can be used for deductions"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00"
}
)
)
registration_date = forms.DateField(
label=_("Agreement date"),
label_suffix="",
help_text=_("When did the parties agree on this?"),
required=False,
validators=[validators.reasonable_date],
widget=forms.DateInput(
attrs={
"type": "date",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
field_order = [
"identifier",
"title",
"conservation_office",
"registration_date",
"surface",
"conservation_file_number",
"is_pik",
"handler_type",
"handler_detail",
"comment",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New Eco-Account")
self.action_url = reverse("compensation:acc:new")
self.cancel_redirect = reverse("compensation:acc:index")
tmp = EcoAccount()
identifier = tmp.generate_new_identifier()
self.initialize_form_field("identifier", identifier)
self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:acc:new-id")
self.fields["title"].widget.attrs["placeholder"] = _("Eco-Account XY; Location ABC")
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None)
handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None)
surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.get_created_action(user)
handler = Handler.objects.create(
type=handler_type,
detail=handler_detail,
)
responsible = Responsibility.objects.create(
handler=handler,
conservation_file_number=conservation_file_number,
conservation_office=conservation_office,
)
legal = Legal.objects.create(
registration_date=registration_date
)
# Finally create main object
acc = EcoAccount.objects.create(
identifier=identifier,
title=title,
responsible=responsible,
deductable_surface=surface,
created=action,
modified=action,
comment=comment,
is_pik=is_pik,
legal=legal
)
acc.share_with_user(user)
# Add the log entry to the main objects log list
acc.log.add(action)
# Process the geometry form
geometry = geom_form.save(action)
acc.geometry = geometry
acc.save()
acc.update_deductable_rest()
return acc
class EditEcoAccountForm(NewEcoAccountForm):
""" Form for editing eco accounts
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Edit Eco-Account")
self.action_url = reverse("compensation:acc:edit", args=(self.instance.id,))
self.cancel_redirect = reverse("compensation:acc:detail", args=(self.instance.id,))
# Initialize form data
reg_date = self.instance.legal.registration_date
if reg_date is not None:
reg_date = reg_date.isoformat()
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"surface": self.instance.deductable_surface,
"handler_type": self.instance.responsible.handler.type,
"handler_detail": self.instance.responsible.handler.detail,
"registration_date": reg_date,
"conservation_office": self.instance.responsible.conservation_office,
"conservation_file_number": self.instance.responsible.conservation_file_number,
"is_pik": self.instance.is_pik,
"comment": self.instance.comment,
}
disabled_fields = []
self.load_initial_data(
form_data,
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):
with transaction.atomic():
# Fetch data from cleaned POST values
title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None)
handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None)
surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
comment = self.cleaned_data.get("comment", None)
is_pik = self.cleaned_data.get("is_pik", None)
# Create log entry
action = UserActionLogEntry.get_edited_action(user)
# Update responsible data
self.instance.responsible.handler.type = handler_type
self.instance.responsible.handler.detail = handler_detail
self.instance.responsible.handler.save()
self.instance.responsible.conservation_office = conservation_office
self.instance.responsible.conservation_file_number = conservation_file_number
self.instance.responsible.save()
# Update legal data
self.instance.legal.registration_date = registration_date
self.instance.legal.save()
# Update main oject data
self.instance.title = title
self.instance.deductable_surface = surface
self.instance.comment = comment
self.instance.is_pik = is_pik
self.instance.modified = action
self.instance.save()
# Add the log entry to the main objects log list
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
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

539
compensation/forms/forms.py Normal file
View File

@ -0,0 +1,539 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.12.20
"""
from dal import autocomplete
from user.models import User
from django.db import transaction
from django.urls import reverse_lazy, reverse
from django.utils.translation import gettext_lazy as _
from django import forms
from codelist.models import KonovaCode
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID
from compensation.models import Compensation, EcoAccount
from intervention.inputs import GenerateInput
from intervention.models import Intervention, Responsibility, Legal, Handler
from konova.forms import BaseForm, SimpleGeomForm
from konova.utils.message_templates import EDITED_GENERAL_DATA, COMPENSATION_ADDED_TEMPLATE
from user.models import UserActionLogEntry
class AbstractCompensationForm(BaseForm):
""" Abstract form for compensations
Holds all important form fields, which are used in compensation and eco account forms
"""
identifier = forms.CharField(
label=_("Identifier"),
label_suffix="",
max_length=255,
help_text=_("Generated automatically"),
widget=GenerateInput(
attrs={
"class": "form-control",
"url": None, # Needs to be set in inheriting constructors
}
)
)
title = forms.CharField(
label=_("Title"),
label_suffix="",
help_text=_("An explanatory name"),
max_length=255,
widget=forms.TextInput(
attrs={
"placeholder": _("Compensation XY; Location ABC"),
"class": "form-control",
}
)
)
comment = forms.CharField(
label_suffix="",
label=_("Comment"),
required=False,
help_text=_("Additional comment"),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control"
}
)
)
class Meta:
abstract = True
class CompensationResponsibleFormMixin(forms.Form):
""" Encapsulates form fields used in different compensation related models like EcoAccount or EMA
"""
conservation_office = forms.ModelChoiceField(
label=_("Conservation office"),
label_suffix="",
help_text=_("Select the responsible office"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID],
),
widget=autocomplete.ModelSelect2(
url="codes-conservation-office-autocomplete",
attrs={
"data-placeholder": _("Click for selection")
}
),
)
conservation_file_number = forms.CharField(
label=_("Conservation office file number"),
label_suffix="",
max_length=255,
required=False,
widget=forms.TextInput(
attrs={
"placeholder": _("ETS-123/ABC.456"),
"class": "form-control",
}
)
)
handler_type = forms.ModelChoiceField(
label=_("Eco-Account handler type"),
label_suffix="",
help_text=_("What type of handler is responsible for the ecoaccount?"),
required=False,
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_HANDLER_ID],
),
widget=autocomplete.ModelSelect2(
url="codes-handler-autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
}
),
)
handler_detail = forms.CharField(
label=_("Eco-Account handler detail"),
label_suffix="",
max_length=255,
required=False,
help_text=_("Detail input on the handler"),
widget=forms.TextInput(
attrs={
"placeholder": _("Company Mustermann"),
"class": "form-control",
}
)
)
class CEFCompensationFormMixin(forms.Form):
""" A form mixin, providing CEF compensation field
"""
is_cef = forms.BooleanField(
label_suffix="",
label=_("Is CEF"),
help_text=_("Optionally: Whether this compensation is a CEF compensation?"),
required=False,
widget=forms.CheckboxInput()
)
class CoherenceCompensationFormMixin(forms.Form):
""" A form mixin, providing coherence compensation field
"""
is_coherence_keeping = forms.BooleanField(
label_suffix="",
label=_("Is coherence keeping"),
help_text=_("Optionally: Whether this compensation is a coherence keeping compensation?"),
required=False,
widget=forms.CheckboxInput()
)
class PikCompensationFormMixin(forms.Form):
""" A form mixin, providing PIK compensation field
"""
is_pik = forms.BooleanField(
label_suffix="",
label=_("Is PIK"),
help_text=_("Optionally: Whether this compensation is a compensation integrated in production?"),
required=False,
widget=forms.CheckboxInput()
)
class NewCompensationForm(AbstractCompensationForm,
CEFCompensationFormMixin,
CoherenceCompensationFormMixin,
PikCompensationFormMixin):
""" Form for creating new compensations.
Can be initialized with an intervention id for preselecting the related intervention.
form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
...
The intervention id will not be resolved into the intervention ORM object but instead will be used to initialize
the related form field.
"""
intervention = forms.ModelChoiceField(
label=_("compensates intervention"),
label_suffix="",
help_text=_("Select the intervention for which this compensation compensates"),
queryset=Intervention.objects.filter(
deleted=None,
),
widget=autocomplete.ModelSelect2(
url="interventions-autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
"data-minimum-input-length": 3,
}
),
)
# Define a field order for a nicer layout instead of running with the inheritance result
field_order = [
"identifier",
"title",
"intervention",
"is_pik",
"is_cef",
"is_coherence_keeping",
"comment",
]
def __init__(self, *args, **kwargs):
intervention_id = kwargs.pop("intervention_id", None)
super().__init__(*args, **kwargs)
self.form_title = _("New compensation")
# If the compensation shall directly be initialized from an intervention, we need to fill in the intervention id
# and disable the form field.
# Furthermore the action_url needs to be set accordingly.
if intervention_id is not None:
self.initialize_form_field("intervention", intervention_id)
self.disable_form_field("intervention")
self.action_url = reverse("compensation:new", args=(intervention_id,))
self.cancel_redirect = reverse("intervention:detail", args=(intervention_id,))
else:
self.action_url = reverse("compensation:new")
self.cancel_redirect = reverse("compensation:index")
tmp = Compensation()
identifier = tmp.generate_new_identifier()
self.initialize_form_field("identifier", identifier)
self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:new-id")
def __create_comp(self, user, geom_form) -> Compensation:
""" Creates the compensation from form data
Args:
user (User): The performing user
geom_form (SimpleGeomForm): The geometry form
Returns:
comp (Compensation): The compensation object
"""
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None)
is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None)
is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.get_created_action(user)
# Process the geometry form
geometry = geom_form.save(action)
# Finally create main object
comp = Compensation.objects.create(
identifier=identifier,
title=title,
intervention=intervention,
created=action,
is_cef=is_cef,
is_coherence_keeping=is_coherence_keeping,
is_pik=is_pik,
geometry=geometry,
comment=comment,
)
# Add the log entry to the main objects log list
comp.log.add(action)
return comp
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
comp = self.__create_comp(user, geom_form)
comp.intervention.mark_as_edited(user, edit_comment=COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
return comp
class EditCompensationForm(NewCompensationForm):
""" Form for editing compensations
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Edit compensation")
self.action_url = reverse("compensation:edit", args=(self.instance.id,))
self.cancel_redirect = reverse("compensation:detail", args=(self.instance.id,))
# Initialize form data
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"intervention": self.instance.intervention,
"is_cef": self.instance.is_cef,
"is_coherence_keeping": self.instance.is_coherence_keeping,
"is_pik": self.instance.is_pik,
"comment": self.instance.comment,
}
disabled_fields = []
self.load_initial_data(
form_data,
disabled_fields
)
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None)
is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None)
is_pik = self.cleaned_data.get("is_pik", 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.intervention = intervention
self.instance.geometry = geometry
self.instance.is_cef = is_cef
self.instance.is_coherence_keeping = is_coherence_keeping
self.instance.comment = comment
self.instance.is_pik = is_pik
self.instance.modified = action
self.instance.save()
self.instance.log.add(action)
intervention.mark_as_edited(user, self.request, EDITED_GENERAL_DATA)
return self.instance
class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMixin, PikCompensationFormMixin):
""" Form for creating eco accounts
Inherits from basic AbstractCompensationForm and further form fields from CompensationResponsibleFormMixin
"""
surface = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_("Available Surface"),
label_suffix="",
required=False,
help_text=_("The amount that can be used for deductions"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00"
}
)
)
registration_date = forms.DateField(
label=_("Agreement date"),
label_suffix="",
help_text=_("When did the parties agree on this?"),
required=False,
widget=forms.DateInput(
attrs={
"type": "date",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
field_order = [
"identifier",
"title",
"conservation_office",
"registration_date",
"surface",
"conservation_file_number",
"is_pik",
"handler_type",
"handler_detail",
"comment",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New Eco-Account")
self.action_url = reverse("compensation:acc:new")
self.cancel_redirect = reverse("compensation:acc:index")
tmp = EcoAccount()
identifier = tmp.generate_new_identifier()
self.initialize_form_field("identifier", identifier)
self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:acc:new-id")
self.fields["title"].widget.attrs["placeholder"] = _("Eco-Account XY; Location ABC")
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None)
handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None)
surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.get_created_action(user)
# Process the geometry form
geometry = geom_form.save(action)
handler = Handler.objects.create(
type=handler_type,
detail=handler_detail,
)
responsible = Responsibility.objects.create(
handler=handler,
conservation_file_number=conservation_file_number,
conservation_office=conservation_office,
)
legal = Legal.objects.create(
registration_date=registration_date
)
# Finally create main object
acc = EcoAccount.objects.create(
identifier=identifier,
title=title,
responsible=responsible,
deductable_surface=surface,
created=action,
geometry=geometry,
comment=comment,
is_pik=is_pik,
legal=legal
)
acc.share_with_user(user)
# Add the log entry to the main objects log list
acc.log.add(action)
return acc
class EditEcoAccountForm(NewEcoAccountForm):
""" Form for editing eco accounts
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Edit Eco-Account")
self.action_url = reverse("compensation:acc:edit", args=(self.instance.id,))
self.cancel_redirect = reverse("compensation:acc:detail", args=(self.instance.id,))
# Initialize form data
reg_date = self.instance.legal.registration_date
if reg_date is not None:
reg_date = reg_date.isoformat()
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"surface": self.instance.deductable_surface,
"handler_type": self.instance.responsible.handler.type,
"handler_detail": self.instance.responsible.handler.detail,
"registration_date": reg_date,
"conservation_office": self.instance.responsible.conservation_office,
"conservation_file_number": self.instance.responsible.conservation_file_number,
"is_pik": self.instance.is_pik,
"comment": self.instance.comment,
}
disabled_fields = []
self.load_initial_data(
form_data,
disabled_fields
)
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None)
handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None)
surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
comment = self.cleaned_data.get("comment", None)
is_pik = self.cleaned_data.get("is_pik", None)
# Create log entry
action = UserActionLogEntry.get_edited_action(user)
# Process the geometry form
geometry = geom_form.save(action)
# Update responsible data
self.instance.responsible.handler.type = handler_type
self.instance.responsible.handler.detail = handler_detail
self.instance.responsible.handler.save()
self.instance.responsible.conservation_office = conservation_office
self.instance.responsible.conservation_file_number = conservation_file_number
self.instance.responsible.save()
# Update legal data
self.instance.legal.registration_date = registration_date
self.instance.legal.save()
# Update main oject data
self.instance.identifier = identifier
self.instance.title = title
self.instance.deductable_surface = surface
self.instance.geometry = geometry
self.instance.comment = comment
self.instance.is_pik = is_pik
self.instance.modified = action
self.instance.save()
# Add the log entry to the main objects log list
self.instance.log.add(action)
return self.instance

View File

@ -1,117 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from dal import autocomplete
from django import forms
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID
class CompensationResponsibleFormMixin(forms.Form):
""" Encapsulates form fields used in different compensation related models like EcoAccount or EMA
"""
conservation_office = forms.ModelChoiceField(
label=_("Conservation office"),
label_suffix="",
help_text=_("Select the responsible office"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID],
),
widget=autocomplete.ModelSelect2(
url="codelist:conservation-office-autocomplete",
attrs={
"data-placeholder": _("Click for selection")
}
),
)
conservation_file_number = forms.CharField(
label=_("Conservation office file number"),
label_suffix="",
max_length=255,
required=False,
widget=forms.TextInput(
attrs={
"placeholder": _("ETS-123/ABC.456"),
"class": "form-control",
}
)
)
handler_type = forms.ModelChoiceField(
label=_("Eco-Account handler type"),
label_suffix="",
help_text=_("What type of handler is responsible for the ecoaccount?"),
required=False,
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_HANDLER_ID],
),
widget=autocomplete.ModelSelect2(
url="codelist:handler-autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
}
),
)
handler_detail = forms.CharField(
label=_("Eco-Account handler detail"),
label_suffix="",
max_length=255,
required=False,
help_text=_("Detail input on the handler"),
widget=forms.TextInput(
attrs={
"placeholder": _("Company Mustermann"),
"class": "form-control",
}
)
)
class CEFCompensationFormMixin(forms.Form):
""" A form mixin, providing CEF compensation field
"""
is_cef = forms.BooleanField(
label_suffix="",
label=_("Is CEF"),
help_text=_("Optionally: Whether this compensation is a CEF compensation?"),
required=False,
widget=forms.CheckboxInput()
)
class CoherenceCompensationFormMixin(forms.Form):
""" A form mixin, providing coherence compensation field
"""
is_coherence_keeping = forms.BooleanField(
label_suffix="",
label=_("Is coherence keeping"),
help_text=_("Optionally: Whether this compensation is a coherence keeping compensation?"),
required=False,
widget=forms.CheckboxInput()
)
class PikCompensationFormMixin(forms.Form):
""" A form mixin, providing PIK compensation field
"""
is_pik = forms.BooleanField(
label_suffix="",
label=_("Is PIK"),
help_text=_("Optionally: Whether this compensation is a compensation integrated in production?"),
required=False,
widget=forms.CheckboxInput()
)

View File

@ -0,0 +1,534 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.10.21
"""
from bootstrap_modal_forms.utils import is_ajax
from dal import autocomplete
from django import forms
from django.contrib import messages
from django.http import HttpRequest, HttpResponseRedirect
from django.shortcuts import render
from django.utils.translation import pgettext_lazy as _con, gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, \
CODELIST_COMPENSATION_ACTION_DETAIL_ID
from compensation.models import CompensationDocument, EcoAccountDocument
from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple, \
CompensationStateTreeRadioSelect
from konova.contexts import BaseContext
from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm
from konova.models import DeadlineType
from konova.utils.message_templates import FORM_INVALID, ADDED_COMPENSATION_STATE, \
ADDED_COMPENSATION_ACTION, PAYMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED
class NewPaymentForm(BaseModalForm):
""" Form handling payment related input
"""
amount = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_con("money", "Amount"), # contextual translation
label_suffix=_(""),
help_text=_("in Euro"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00",
}
)
)
due = forms.DateField(
label=_("Due on"),
label_suffix=_(""),
required=False,
help_text=_("Due on which date"),
widget=forms.DateInput(
attrs={
"type": "date",
"data-provide": "datepicker",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
comment = forms.CharField(
max_length=200,
required=False,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment, maximum {} letters").format(200),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control"
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.intervention = self.instance
self.form_title = _("Payment")
self.form_caption = _("Add a payment for intervention '{}'").format(self.intervention.title)
def is_valid(self):
"""
Checks on form validity.
For this form we need to make sure that a date or a comment is set.
If both are missing, the user needs to enter at least an explanation why
there is no date to be entered.
Returns:
is_valid (bool): True if valid, False otherwise
"""
super_valid = super().is_valid()
date = self.cleaned_data["due"]
comment = self.cleaned_data["comment"] or None
if not date and not comment:
# At least one needs to be set!
self.add_error(
"comment",
_("If there is no date you can enter, please explain why.")
)
return False
return super_valid
def save(self):
pay = self.instance.add_payment(self)
return pay
class EditPaymentModalForm(NewPaymentForm):
""" Form handling edit for Payment
"""
payment = None
def __init__(self, *args, **kwargs):
self.payment = kwargs.pop("payment", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit payment")
form_date = {
"amount": self.payment.amount,
"due": str(self.payment.due_on),
"comment": self.payment.comment,
}
self.load_initial_data(form_date, disabled_fields=[])
def save(self):
payment = self.payment
payment.amount = self.cleaned_data.get("amount", None)
payment.due_on = self.cleaned_data.get("due", None)
payment.comment = self.cleaned_data.get("comment", None)
payment.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=PAYMENT_EDITED)
self.instance.send_data_to_egon()
return payment
class RemovePaymentModalForm(RemoveModalForm):
""" Removing modal form for Payment
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
payment = None
def __init__(self, *args, **kwargs):
payment = kwargs.pop("payment", None)
self.payment = payment
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_payment(self)
class NewStateModalForm(BaseModalForm):
""" Form handling state related input
Compensation states refer to 'before' and 'after' states of a compensated surface. Basically it means:
What has been on this area before changes/compensations have been applied and what will be the result ('after')?
"""
biotope_type = forms.ChoiceField(
label=_("Biotope Type"),
label_suffix="",
required=True,
help_text=_("Select the biotope type"),
widget=CompensationStateTreeRadioSelect(),
)
biotope_extra = forms.ModelMultipleChoiceField(
label=_("Biotope additional type"),
label_suffix="",
required=False,
help_text=_("Select an additional biotope type"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_ID],
),
widget=autocomplete.ModelSelect2Multiple(
url="codes-biotope-extra-type-autocomplete",
attrs={
"data-placeholder": _("Biotope additional type"),
}
),
)
surface = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_("Surface"),
label_suffix="",
required=True,
help_text=_("in m²"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00"
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New state")
self.form_caption = _("Insert data for the new state")
choices = KonovaCode.objects.filter(
code_lists__in=[CODELIST_BIOTOPES_ID],
is_archived=False,
is_leaf=True,
).values_list("id", flat=True)
choices = [
(choice, choice)
for choice in choices
]
self.fields["biotope_type"].choices = choices
def save(self, is_before_state: bool = False):
state = self.instance.add_state(self, is_before_state)
self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_STATE)
return state
def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None):
""" Generic processing of request
Wraps the request processing logic, so we don't need the same code everywhere a RemoveModalForm is being used
+++
The generic method from super class can not be used, since we need to do some request parameter check in here.
+++
Args:
request (HttpRequest): The incoming request
msg_success (str): The message in case of successful removing
msg_error (str): The message in case of an error
Returns:
"""
redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home")
template = self.template
if request.method == "POST":
if self.is_valid():
# Modal forms send one POST for checking on data validity. This can be used to return possible errors
# on the form. A second POST (if no errors occured) is sent afterwards and needs to process the
# saving/commiting of the data to the database. is_ajax() performs this check. The first request is
# an ajax call, the second is a regular form POST.
if not is_ajax(request.META):
is_before_state = bool(request.GET.get("before", False))
self.save(is_before_state=is_before_state)
messages.success(
request,
msg_success
)
return HttpResponseRedirect(redirect_url)
else:
context = {
"form": self,
}
context = BaseContext(request, context).context
return render(request, template, context)
elif request.method == "GET":
context = {
"form": self,
}
context = BaseContext(request, context).context
return render(request, template, context)
else:
raise NotImplementedError
class EditCompensationStateModalForm(NewStateModalForm):
state = None
def __init__(self, *args, **kwargs):
self.state = kwargs.pop("state", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit state")
biotope_type_id = self.state.biotope_type.id if self.state.biotope_type else None
form_data = {
"biotope_type": biotope_type_id,
"biotope_extra": self.state.biotope_type_details.all(),
"surface": self.state.surface,
}
self.load_initial_data(form_data)
def save(self, is_before_state: bool = False):
state = self.state
biotope_type_id = self.cleaned_data.get("biotope_type", None)
state.biotope_type = KonovaCode.objects.get(id=biotope_type_id)
state.biotope_type_details.set(self.cleaned_data.get("biotope_extra", []))
state.surface = self.cleaned_data.get("surface", None)
state.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=COMPENSATION_STATE_EDITED)
return state
class RemoveCompensationStateModalForm(RemoveModalForm):
""" Removing modal form for CompensationState
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
state = None
def __init__(self, *args, **kwargs):
state = kwargs.pop("state", None)
self.state = state
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_state(self)
class RemoveCompensationActionModalForm(RemoveModalForm):
""" Removing modal form for CompensationAction
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
action = None
def __init__(self, *args, **kwargs):
action = kwargs.pop("action", None)
self.action = action
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_action(self)
class NewDeadlineModalForm(BaseModalForm):
""" Form handling deadline related input
"""
type = forms.ChoiceField(
label=_("Deadline Type"),
label_suffix="",
required=True,
help_text=_("Select the deadline type"),
choices=DeadlineType.choices,
widget=forms.Select(
attrs={
"class": "form-control"
}
)
)
date = forms.DateField(
label=_("Date"),
label_suffix="",
required=True,
help_text=_("Select date"),
widget=forms.DateInput(
attrs={
"type": "date",
"data-provide": "datepicker",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
comment = forms.CharField(
required=False,
max_length=200,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment, maximum {} letters").format(200),
widget=forms.Textarea(
attrs={
"cols": 30,
"rows": 5,
"class": "form-control",
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New deadline")
self.form_caption = _("Insert data for the new deadline")
def save(self):
deadline = self.instance.add_deadline(self)
return deadline
class EditDeadlineModalForm(NewDeadlineModalForm):
deadline = None
def __init__(self, *args, **kwargs):
self.deadline = kwargs.pop("deadline", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit deadline")
form_data = {
"type": self.deadline.type,
"date": str(self.deadline.date),
"comment": self.deadline.comment,
}
self.load_initial_data(form_data)
def save(self):
deadline = self.deadline
deadline.type = self.cleaned_data.get("type", None)
deadline.date = self.cleaned_data.get("date", None)
deadline.comment = self.cleaned_data.get("comment", None)
deadline.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=DEADLINE_EDITED)
return deadline
class NewActionModalForm(BaseModalForm):
""" Form handling action related input
Compensation actions are the actions performed on the area, which shall be compensated. Actions will change the
surface of the area, the biotopes, and have an environmental impact. With actions the before-after states can change
(not in the process logic in Konova, but in the real world).
"""
from compensation.models import UnitChoices
action_type = forms.MultipleChoiceField(
label=_("Action Type"),
label_suffix="",
required=True,
help_text=_("An action can consist of multiple different action types. All the selected action types are expected to be performed according to the amount and unit below on this form."),
choices=[],
widget=CompensationActionTreeCheckboxSelectMultiple(),
)
action_type_details = forms.ModelMultipleChoiceField(
label=_("Action Type detail"),
label_suffix="",
required=False,
help_text=_("Select the action type detail"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_COMPENSATION_ACTION_DETAIL_ID],
),
widget=autocomplete.ModelSelect2Multiple(
url="codes-compensation-action-detail-autocomplete",
attrs={
"data-placeholder": _("Action Type detail"),
}
),
)
unit = forms.ChoiceField(
label=_("Unit"),
label_suffix="",
required=True,
help_text=_("Select the unit"),
choices=UnitChoices.choices,
widget=forms.Select(
attrs={
"class": "form-control"
}
)
)
amount = forms.DecimalField(
label=_("Amount"),
label_suffix="",
required=True,
help_text=_("Insert the amount"),
decimal_places=2,
min_value=0.00,
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00",
}
)
)
comment = forms.CharField(
required=False,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment"),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control",
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New action")
self.form_caption = _("Insert data for the new action")
choices =KonovaCode.objects.filter(
code_lists__in=[CODELIST_COMPENSATION_ACTION_ID],
is_archived=False,
is_leaf=True,
).values_list("id", flat=True)
choices = [
(choice, choice)
for choice in choices
]
self.fields["action_type"].choices = choices
def save(self):
action = self.instance.add_action(self)
self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_ACTION)
return action
class EditCompensationActionModalForm(NewActionModalForm):
action = None
def __init__(self, *args, **kwargs):
self.action = kwargs.pop("action", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit action")
form_data = {
"action_type": list(self.action.action_type.values_list("id", flat=True)),
"action_type_details": self.action.action_type_details.all(),
"amount": self.action.amount,
"unit": self.action.unit,
"comment": self.action.comment,
}
self.load_initial_data(form_data)
def save(self):
action = self.action
action.action_type.set(self.cleaned_data.get("action_type", []))
action.action_type_details.set(self.cleaned_data.get("action_type_details", []))
action.amount = self.cleaned_data.get("amount", None)
action.unit = self.cleaned_data.get("unit", None)
action.comment = self.cleaned_data.get("comment", None)
action.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=COMPENSATION_ACTION_EDITED)
return action
class NewCompensationDocumentModalForm(NewDocumentModalForm):
document_model = CompensationDocument
class NewEcoAccountDocumentModalForm(NewDocumentModalForm):
document_model = EcoAccountDocument

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: 18.08.22
"""

View File

@ -1,155 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from dal import autocomplete
from django import forms
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID
from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple
from konova.forms.modals import BaseModalForm, RemoveModalForm
from konova.utils.message_templates import COMPENSATION_ACTION_EDITED, ADDED_COMPENSATION_ACTION
class NewCompensationActionModalForm(BaseModalForm):
""" Form handling action related input
Compensation actions are the actions performed on the area, which shall be compensated. Actions will change the
surface of the area, the biotopes, and have an environmental impact. With actions the before-after states can change
(not in the process logic in Konova, but in the real world).
"""
from compensation.models import UnitChoices
action_type = forms.MultipleChoiceField(
label=_("Action Type"),
label_suffix="",
required=True,
help_text=_("An action can consist of multiple different action types. All the selected action types are expected to be performed according to the amount and unit below on this form."),
choices=[],
widget=CompensationActionTreeCheckboxSelectMultiple(),
)
action_type_details = forms.ModelMultipleChoiceField(
label=_("Action Type detail"),
label_suffix="",
required=False,
help_text=_("Select the action type detail"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_COMPENSATION_ACTION_DETAIL_ID],
),
widget=autocomplete.ModelSelect2Multiple(
url="codelist:compensation-action-detail-autocomplete",
attrs={
"data-placeholder": _("Action Type detail"),
}
),
)
unit = forms.ChoiceField(
label=_("Unit"),
label_suffix="",
required=True,
help_text=_("Select the unit"),
choices=UnitChoices.choices,
widget=forms.Select(
attrs={
"class": "form-control"
}
)
)
amount = forms.DecimalField(
label=_("Amount"),
label_suffix="",
required=True,
help_text=_("Insert the amount"),
decimal_places=2,
min_value=0.00,
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00",
}
)
)
comment = forms.CharField(
required=False,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment"),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control",
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New action")
self.form_caption = _("Insert data for the new action")
choices = KonovaCode.objects.filter(
code_lists__in=[CODELIST_COMPENSATION_ACTION_ID],
is_archived=False,
is_leaf=True,
).values_list("id", flat=True)
choices = [
(choice, choice)
for choice in choices
]
self.fields["action_type"].choices = choices
def save(self):
action = self.instance.add_action(self)
self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_ACTION)
return action
class EditCompensationActionModalForm(NewCompensationActionModalForm):
action = None
def __init__(self, *args, **kwargs):
self.action = kwargs.pop("action", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit action")
form_data = {
"action_type": list(self.action.action_type.values_list("id", flat=True)),
"action_type_details": self.action.action_type_details.all(),
"amount": self.action.amount,
"unit": self.action.unit,
"comment": self.action.comment,
}
self.load_initial_data(form_data)
def save(self):
action = self.action
action.action_type.set(self.cleaned_data.get("action_type", []))
action.action_type_details.set(self.cleaned_data.get("action_type_details", []))
action.amount = self.cleaned_data.get("amount", None)
action.unit = self.cleaned_data.get("unit", None)
action.comment = self.cleaned_data.get("comment", None)
action.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=COMPENSATION_ACTION_EDITED)
return action
class RemoveCompensationActionModalForm(RemoveModalForm):
""" Removing modal form for CompensationAction
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
action = None
def __init__(self, *args, **kwargs):
action = kwargs.pop("action", None)
self.action = action
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_action(self)

View File

@ -1,110 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from django import forms
from django.utils.translation import gettext_lazy as _
from konova.forms.modals import BaseModalForm
from konova.models import DeadlineType
from konova.utils import validators
from konova.utils.message_templates import DEADLINE_EDITED
class NewDeadlineModalForm(BaseModalForm):
""" Form handling deadline related input
"""
type = forms.ChoiceField(
label=_("Deadline Type"),
label_suffix="",
required=True,
help_text=_("Select the deadline type"),
choices=DeadlineType.choices,
widget=forms.Select(
attrs={
"class": "form-control"
}
)
)
date = forms.DateField(
label=_("Date"),
label_suffix="",
required=True,
help_text=_("Select date"),
validators=[validators.reasonable_date],
widget=forms.DateInput(
attrs={
"type": "date",
"data-provide": "datepicker",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
comment = forms.CharField(
required=False,
max_length=200,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment, maximum {} letters").format(200),
widget=forms.Textarea(
attrs={
"cols": 30,
"rows": 5,
"class": "form-control",
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("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):
deadline = self.instance.add_deadline(self)
return deadline
class EditDeadlineModalForm(NewDeadlineModalForm):
deadline = None
def __init__(self, *args, **kwargs):
self.deadline = kwargs.pop("deadline", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit deadline")
form_data = {
"type": self.deadline.type,
"date": str(self.deadline.date),
"comment": self.deadline.comment,
}
self.load_initial_data(form_data)
def save(self):
deadline = self.deadline
deadline.type = self.cleaned_data.get("type", None)
deadline.date = self.cleaned_data.get("date", None)
deadline.comment = self.cleaned_data.get("comment", None)
deadline.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=DEADLINE_EDITED)
return deadline

View File

@ -1,17 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from compensation.models import CompensationDocument, EcoAccountDocument
from konova.forms.modals import NewDocumentModalForm
class NewCompensationDocumentModalForm(NewDocumentModalForm):
document_model = CompensationDocument
class NewEcoAccountDocumentModalForm(NewDocumentModalForm):
document_model = EcoAccountDocument

View File

@ -1,141 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from django import forms
from django.utils.translation import pgettext_lazy as _con, gettext_lazy as _
from konova.forms.modals import RemoveModalForm, BaseModalForm
from konova.utils import validators
from konova.utils.message_templates import PAYMENT_EDITED
class NewPaymentForm(BaseModalForm):
""" Form handling payment related input
"""
amount = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_con("money", "Amount"), # contextual translation
label_suffix=_(""),
help_text=_("in Euro"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00",
}
)
)
due = forms.DateField(
label=_("Due on"),
label_suffix=_(""),
required=False,
validators=[validators.reasonable_date],
help_text=_("Due on which date"),
widget=forms.DateInput(
attrs={
"type": "date",
"data-provide": "datepicker",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
comment = forms.CharField(
max_length=200,
required=False,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment, maximum {} letters").format(200),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control"
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.intervention = self.instance
self.form_title = _("Payment")
self.form_caption = _("Add a payment for intervention '{}'").format(self.intervention.title)
def is_valid(self):
"""
Checks on form validity.
For this form we need to make sure that a date or a comment is set.
If both are missing, the user needs to enter at least an explanation why
there is no date to be entered.
Returns:
is_valid (bool): True if valid, False otherwise
"""
super_valid = super().is_valid()
if not super_valid:
return super_valid
date = self.cleaned_data.get("due", None)
comment = self.cleaned_data.get("comment", None)
if not date and not comment:
# At least one needs to be set!
self.add_error(
"comment",
_("If there is no date you can enter, please explain why.")
)
return False
return super_valid
def save(self):
pay = self.instance.add_payment(self)
return pay
class EditPaymentModalForm(NewPaymentForm):
""" Form handling edit for Payment
"""
payment = None
def __init__(self, *args, **kwargs):
self.payment = kwargs.pop("payment", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit payment")
form_date = {
"amount": self.payment.amount,
"due": str(self.payment.due_on),
"comment": self.payment.comment,
}
self.load_initial_data(form_date, disabled_fields=[])
def save(self):
payment = self.payment
payment.amount = self.cleaned_data.get("amount", None)
payment.due_on = self.cleaned_data.get("due", None)
payment.comment = self.cleaned_data.get("comment", None)
payment.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=PAYMENT_EDITED)
self.instance.send_data_to_egon()
return payment
class RemovePaymentModalForm(RemoveModalForm):
""" Removing modal form for Payment
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
payment = None
def __init__(self, *args, **kwargs):
payment = kwargs.pop("payment", None)
self.payment = payment
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_payment(self)

View File

@ -1,180 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from bootstrap_modal_forms.mixins import is_ajax
from dal import autocomplete
from django import forms
from django.contrib import messages
from django.http import HttpResponseRedirect, HttpRequest
from django.shortcuts import render
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_BIOTOPES_ID, \
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
from intervention.inputs import CompensationStateTreeRadioSelect
from konova.contexts import BaseContext
from konova.forms.modals import RemoveModalForm, BaseModalForm
from konova.utils.message_templates import COMPENSATION_STATE_EDITED, FORM_INVALID, ADDED_COMPENSATION_STATE
class NewCompensationStateModalForm(BaseModalForm):
""" Form handling state related input
Compensation states refer to 'before' and 'after' states of a compensated surface. Basically it means:
What has been on this area before changes/compensations have been applied and what will be the result ('after')?
"""
biotope_type = forms.ChoiceField(
label=_("Biotope Type"),
label_suffix="",
required=True,
help_text=_("Select the biotope type"),
widget=CompensationStateTreeRadioSelect(),
)
biotope_extra = forms.ModelMultipleChoiceField(
label=_("Biotope additional type"),
label_suffix="",
required=False,
help_text=_("Select an additional biotope type"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID],
),
widget=autocomplete.ModelSelect2Multiple(
url="codelist:biotope-extra-type-autocomplete",
attrs={
"data-placeholder": _("Biotope additional type"),
}
),
)
surface = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_("Surface"),
label_suffix="",
required=True,
help_text=_("in m²"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00"
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New state")
self.form_caption = _("Insert data for the new state")
choices = KonovaCode.objects.filter(
code_lists__in=[CODELIST_BIOTOPES_ID],
is_archived=False,
is_leaf=True,
).values_list("id", flat=True)
choices = [
(choice, choice)
for choice in choices
]
self.fields["biotope_type"].choices = choices
def save(self, is_before_state: bool = False):
state = self.instance.add_state(self, is_before_state)
self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_STATE)
return state
def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None):
""" Generic processing of request
Wraps the request processing logic, so we don't need the same code everywhere a RemoveModalForm is being used
+++
The generic method from super class can not be used, since we need to do some request parameter check in here.
+++
Args:
request (HttpRequest): The incoming request
msg_success (str): The message in case of successful removing
msg_error (str): The message in case of an error
Returns:
"""
redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home")
template = self.template
if request.method == "POST":
if self.is_valid():
# Modal forms send one POST for checking on data validity. This can be used to return possible errors
# on the form. A second POST (if no errors occured) is sent afterwards and needs to process the
# saving/commiting of the data to the database. is_ajax() performs this check. The first request is
# an ajax call, the second is a regular form POST.
if not is_ajax(request.META):
is_before_state = bool(request.GET.get("before", False))
self.save(is_before_state=is_before_state)
messages.success(
request,
msg_success
)
return HttpResponseRedirect(redirect_url)
else:
context = {
"form": self,
}
context = BaseContext(request, context).context
return render(request, template, context)
elif request.method == "GET":
context = {
"form": self,
}
context = BaseContext(request, context).context
return render(request, template, context)
else:
raise NotImplementedError
class EditCompensationStateModalForm(NewCompensationStateModalForm):
state = None
def __init__(self, *args, **kwargs):
self.state = kwargs.pop("state", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit state")
biotope_type_id = self.state.biotope_type.id if self.state.biotope_type else None
form_data = {
"biotope_type": biotope_type_id,
"biotope_extra": self.state.biotope_type_details.all(),
"surface": self.state.surface,
}
self.load_initial_data(form_data)
def save(self, is_before_state: bool = False):
state = self.state
biotope_type_id = self.cleaned_data.get("biotope_type", None)
state.biotope_type = KonovaCode.objects.get(id=biotope_type_id)
state.biotope_type_details.set(self.cleaned_data.get("biotope_extra", []))
state.surface = self.cleaned_data.get("surface", None)
state.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=COMPENSATION_STATE_EDITED)
return state
class RemoveCompensationStateModalForm(RemoveModalForm):
""" Removing modal form for CompensationState
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
state = None
def __init__(self, *args, **kwargs):
state = kwargs.pop("state", None)
self.state = state
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_state(self)

View File

@ -1,24 +0,0 @@
# Generated by Django 3.1.3 on 2022-08-15 06:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('konova', '0014_resubmission'),
('compensation', '0007_auto_20220531_1245'),
]
operations = [
migrations.AddField(
model_name='compensation',
name='resubmission',
field=models.ManyToManyField(blank=True, null=True, related_name='_compensation_resubmission_+', to='konova.Resubmission'),
),
migrations.AddField(
model_name='ecoaccount',
name='resubmission',
field=models.ManyToManyField(blank=True, null=True, related_name='_ecoaccount_resubmission_+', to='konova.Resubmission'),
),
]

View File

@ -1,32 +0,0 @@
# Generated by Django 3.1.3 on 2022-08-15 06:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('konova', '0014_resubmission'),
('compensation', '0008_auto_20220815_0803'),
]
operations = [
migrations.RemoveField(
model_name='compensation',
name='resubmission',
),
migrations.RemoveField(
model_name='ecoaccount',
name='resubmission',
),
migrations.AddField(
model_name='compensation',
name='resubmissions',
field=models.ManyToManyField(blank=True, null=True, related_name='_compensation_resubmissions_+', to='konova.Resubmission'),
),
migrations.AddField(
model_name='ecoaccount',
name='resubmissions',
field=models.ManyToManyField(blank=True, null=True, related_name='_ecoaccount_resubmissions_+', to='konova.Resubmission'),
),
]

View File

@ -1,24 +0,0 @@
# Generated by Django 3.1.3 on 2022-08-15 08:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('konova', '0014_resubmission'),
('compensation', '0009_auto_20220815_0803'),
]
operations = [
migrations.AlterField(
model_name='compensation',
name='resubmissions',
field=models.ManyToManyField(blank=True, related_name='_compensation_resubmissions_+', to='konova.Resubmission'),
),
migrations.AlterField(
model_name='ecoaccount',
name='resubmissions',
field=models.ManyToManyField(blank=True, related_name='_ecoaccount_resubmissions_+', to='konova.Resubmission'),
),
]

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,33 +8,27 @@ 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
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _
from compensation.managers import CompensationManager from compensation.managers import CompensationManager
from compensation.models import CompensationState, CompensationAction from compensation.models import CompensationState, CompensationAction
from compensation.utils.quality import CompensationQualityChecker 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
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
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, COMPENSATION_EDITED_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
class AbstractCompensation(BaseObject, class AbstractCompensation(BaseObject, GeoReferencedMixin):
GeoReferencedMixin,
ResubmitableObjectMixin
):
""" """
Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation, Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation,
EMA or EcoAccount. EMA or EcoAccount.
@ -80,7 +74,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 +198,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
@ -234,15 +226,6 @@ class AbstractCompensation(BaseObject,
request = self.set_geometry_conflict_message(request) request = self.set_geometry_conflict_message(request)
return request return request
def get_finished_deadlines(self):
""" Getter for FINISHED-deadlines
Returns:
queryset (QuerySet): The finished deadlines
"""
return self.deadlines.filter(
type=DeadlineType.FINISHED
)
class CEFMixin(models.Model): class CEFMixin(models.Model):
""" Provides CEF flag as Mixin """ Provides CEF flag as Mixin
@ -303,18 +286,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 +318,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 +369,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 +378,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 +391,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 +449,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 +483,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,20 +9,18 @@ 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
from compensation.utils.quality import EcoAccountQualityChecker from compensation.utils.quality import EcoAccountQualityChecker
from konova.models import ShareableObjectMixin, RecordableObjectMixin, AbstractDocument, BaseResource, \ from konova.models import ShareableObjectMixin, RecordableObjectMixin, AbstractDocument, BaseResource, \
generate_document_file_upload_path generate_document_file_upload_path
from konova.tasks import celery_send_mail_deduction_changed, celery_send_mail_deduction_changed_team
class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, PikMixin): class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, PikMixin):
@ -36,12 +34,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 +45,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 +89,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
@ -154,63 +159,8 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
Returns: Returns:
""" """
return reverse("compensation:acc:share-token", args=(self.id, self.access_token)) return reverse("compensation:acc:share", args=(self.id, self.access_token))
def send_notification_mail_on_deduction_change(self, data_change: dict):
""" Sends notification mails for changes on the deduction
Args:
data_change ():
Returns:
"""
# Send mail
shared_users = self.shared_users.values_list("id", flat=True)
for user_id in shared_users:
celery_send_mail_deduction_changed.delay(self.id, self.get_app_object_tuple(), user_id, data_change)
# Send mail
shared_teams = self.shared_teams.values_list("id", flat=True)
for team_id in shared_teams:
celery_send_mail_deduction_changed_team.delay(self.id, self.get_app_object_tuple(), 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 +195,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 +252,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"

346
compensation/tables.py Normal file
View File

@ -0,0 +1,346 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 01.12.20
"""
from konova.utils.message_templates import DATA_IS_UNCHECKED, DATA_CHECKED_ON_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE
from django.http import HttpRequest
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from compensation.filters import CompensationTableFilter, EcoAccountTableFilter
from compensation.models import Compensation, EcoAccount
from konova.utils.tables import BaseTable, TableRenderMixin
import django_tables2 as tables
class CompensationTable(BaseTable, TableRenderMixin):
id = tables.Column(
verbose_name=_("Identifier"),
orderable=True,
accessor="identifier",
)
t = tables.Column(
verbose_name=_("Title"),
orderable=True,
accessor="title",
)
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=True,
accessor="geometry",
)
c = tables.Column(
verbose_name=_("Checked"),
orderable=True,
empty_values=[],
accessor="intervention__checked",
)
r = tables.Column(
verbose_name=_("Recorded"),
orderable=True,
empty_values=[],
accessor="intervention__recorded",
)
e = tables.Column(
verbose_name=_("Editable"),
orderable=True,
empty_values=[],
accessor="intervention__users",
)
lm = tables.Column(
verbose_name=_("Last edit"),
orderable=True,
accessor="modified__timestamp",
)
class Meta(BaseTable.Meta):
template_name = "django_tables2/bootstrap4.html"
def __init__(self, request: HttpRequest, *args, **kwargs):
self.title = _("Compensations")
self.add_new_url = reverse("compensation:new")
qs = kwargs.get("queryset", None)
self.filter = CompensationTableFilter(
user=request.user,
data=request.GET,
queryset=qs,
)
kwargs["queryset"] = self.filter.qs
super().__init__(request, *args, **kwargs)
def render_id(self, value, record: Compensation):
""" Renders the id column for a compensation
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
context = {
"tooltip": _("Open {}").format(_("Intervention")),
"content": value,
"url": reverse("compensation:detail", args=(record.id,)),
"has_revocations": record.intervention.legal.revocations.exists()
}
html = render_to_string(
"table/revocation_warning_col.html",
context
)
return html
def render_c(self, value, record: Compensation):
""" Renders the checked column for a compensation
checked is set by the main object Intervention
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
html = ""
checked = value is not None
tooltip = DATA_IS_UNCHECKED
previously_checked = record.intervention.get_last_checked_action()
if checked:
checked_on = value.get_timestamp_str_formatted()
tooltip = DATA_CHECKED_ON_TEMPLATE.format(checked_on, record.intervention.checked.user)
html += self.render_checked_star(
tooltip=tooltip,
icn_filled=checked,
)
if previously_checked and not checked:
checked_on = previously_checked.get_timestamp_str_formatted()
tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(checked_on, previously_checked.user)
html += self.render_previously_checked_star(
tooltip=tooltip,
)
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):
""" Renders the registered column for a compensation
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
html = ""
recorded = value is not None
tooltip = _("Not recorded yet")
if recorded:
on = value.get_timestamp_str_formatted()
tooltip = _("Recorded on {} by {}").format(on, record.intervention.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
icn_filled=recorded,
)
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)
class EcoAccountTable(BaseTable, TableRenderMixin):
id = tables.Column(
verbose_name=_("Identifier"),
orderable=True,
accessor="identifier",
)
t = tables.Column(
verbose_name=_("Title"),
orderable=True,
accessor="title",
)
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=True,
accessor="geometry",
)
av = tables.Column(
verbose_name=_("Available"),
orderable=True,
empty_values=[],
attrs={
"th": {
"class": "w-20",
}
}
)
r = tables.Column(
verbose_name=_("Recorded"),
orderable=True,
empty_values=[],
accessor="recorded",
)
e = tables.Column(
verbose_name=_("Editable"),
orderable=True,
empty_values=[],
accessor="users",
)
lm = tables.Column(
verbose_name=_("Last edit"),
orderable=True,
accessor="modified__timestamp",
)
class Meta(BaseTable.Meta):
template_name = "django_tables2/bootstrap4.html"
def __init__(self, request: HttpRequest, *args, **kwargs):
self.title = _("Eco Accounts")
self.add_new_url = reverse("compensation:acc:new")
qs = kwargs.get("queryset", None)
self.filter = EcoAccountTableFilter(
user=request.user,
data=request.GET,
queryset=qs,
)
kwargs["queryset"] = self.filter.qs
super().__init__(request, *args, **kwargs)
def render_id(self, value, record: EcoAccount):
""" Renders the id column for an eco account
Args:
value (str): The identifier value
record (EcoAccount): The eco account record
Returns:
"""
html = ""
html += self.render_link(
tooltip=_("Open {}").format(_("Eco-account")),
href=reverse("compensation:acc:detail", args=(record.id,)),
txt=value,
new_tab=False,
)
return format_html(html)
def render_av(self, value, record: EcoAccount):
""" Renders the available column for an eco account
Args:
value (str): The identifier value
record (EcoAccount): The eco account record
Returns:
"""
value_total, value_relative = record.get_available_rest()
html = render_to_string("konova/widgets/progressbar.html", {"value": value_relative})
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: EcoAccount):
""" Renders the recorded column for an eco account
Args:
value (str): The identifier value
record (EcoAccount): The eco account record
Returns:
"""
html = ""
checked = value is not None
tooltip = _("Not recorded yet. Can not be used for deductions, yet.")
if checked:
on = value.get_timestamp_str_formatted()
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
icn_filled=checked,
)
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

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

View File

@ -1,150 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from django.http import HttpRequest
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from compensation.filters.compensation import CompensationTableFilter
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.tables import BaseTable, TableRenderMixin, TableOrderMixin
import django_tables2 as tables
class CompensationTable(BaseTable, TableRenderMixin, TableOrderMixin):
id = tables.Column(
verbose_name=_("Identifier"),
orderable=True,
accessor="identifier",
)
t = tables.Column(
verbose_name=_("Title"),
orderable=True,
accessor="title",
)
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=False,
accessor="geometry",
)
c = tables.Column(
verbose_name=_("Checked"),
orderable=True,
empty_values=[],
accessor="intervention__checked",
)
r = tables.Column(
verbose_name=_("Recorded"),
orderable=True,
empty_values=[],
accessor="intervention__recorded",
)
e = tables.Column(
verbose_name=_("Editable"),
orderable=True,
empty_values=[],
accessor="intervention__users",
)
lm = tables.Column(
verbose_name=_("Last edit"),
orderable=True,
accessor="modified__timestamp",
)
class Meta(BaseTable.Meta):
template_name = "django_tables2/bootstrap4.html"
def __init__(self, request: HttpRequest, *args, **kwargs):
self.title = _("Compensations")
self.add_new_url = reverse("compensation:new")
qs = kwargs.get("queryset", None)
self.filter = CompensationTableFilter(
user=request.user,
data=request.GET,
queryset=qs,
)
kwargs["queryset"] = self.filter.qs
super().__init__(request, *args, **kwargs)
def render_id(self, value, record: Compensation):
""" Renders the id column for a compensation
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
context = {
"tooltip": _("Open {}").format(_("Intervention")),
"content": value,
"url": reverse("compensation:detail", args=(record.id,)),
"has_revocations": record.intervention.legal.revocations.exists()
}
html = render_to_string(
"table/revocation_warning_col.html",
context
)
return html
def render_c(self, value, record: Compensation):
""" Renders the checked column for a compensation
checked is set by the main object Intervention
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
html = ""
checked = value is not None
tooltip = DATA_IS_UNCHECKED
previously_checked = record.intervention.get_last_checked_action()
if checked:
checked_on = value.get_timestamp_str_formatted()
tooltip = DATA_CHECKED_ON_TEMPLATE.format(checked_on, record.intervention.checked.user)
html += self.render_checked_star(
tooltip=tooltip,
icn_filled=checked,
)
if previously_checked and not checked:
checked_on = previously_checked.get_timestamp_str_formatted()
tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(checked_on, previously_checked.user)
html += self.render_previously_checked_star(
tooltip=tooltip,
)
return format_html(html)
def render_r(self, value, record: Compensation):
""" Renders the registered column for a compensation
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
html = ""
recorded = value is not None
tooltip = _("Not recorded yet")
if recorded:
on = value.get_timestamp_str_formatted()
tooltip = _("Recorded on {} by {}").format(on, record.intervention.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
icn_filled=recorded,
)
return format_html(html)

View File

@ -1,138 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from django.http import HttpRequest
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.formats import number_format
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from compensation.filters.eco_account import EcoAccountTableFilter
from compensation.models import EcoAccount
from konova.utils.tables import TableRenderMixin, BaseTable, TableOrderMixin
import django_tables2 as tables
class EcoAccountTable(BaseTable, TableRenderMixin, TableOrderMixin):
id = tables.Column(
verbose_name=_("Identifier"),
orderable=True,
accessor="identifier",
)
t = tables.Column(
verbose_name=_("Title"),
orderable=True,
accessor="title",
)
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=False,
accessor="geometry",
)
av = tables.Column(
verbose_name=_("Available"),
orderable=True,
accessor="deductable_rest",
attrs={
"th": {
"class": "w-20",
}
}
)
r = tables.Column(
verbose_name=_("Recorded"),
orderable=True,
empty_values=[],
accessor="recorded",
)
e = tables.Column(
verbose_name=_("Editable"),
orderable=True,
empty_values=[],
accessor="users",
)
lm = tables.Column(
verbose_name=_("Last edit"),
orderable=True,
accessor="modified__timestamp",
)
class Meta(BaseTable.Meta):
template_name = "django_tables2/bootstrap4.html"
def __init__(self, request: HttpRequest, *args, **kwargs):
self.title = _("Eco Accounts")
self.add_new_url = reverse("compensation:acc:new")
qs = kwargs.get("queryset", None)
self.filter = EcoAccountTableFilter(
user=request.user,
data=request.GET,
queryset=qs,
)
kwargs["queryset"] = self.filter.qs
super().__init__(request, *args, **kwargs)
def render_id(self, value, record: EcoAccount):
""" Renders the id column for an eco account
Args:
value (str): The identifier value
record (EcoAccount): The eco account record
Returns:
"""
html = ""
html += self.render_link(
tooltip=_("Open {}").format(_("Eco-account")),
href=reverse("compensation:acc:detail", args=(record.id,)),
txt=value,
new_tab=False,
)
return format_html(html)
def render_av(self, value, record: EcoAccount):
""" Renders the available column for an eco account
Args:
value (float): The deductable_rest
record (EcoAccount): The eco account record
Returns:
"""
try:
value_relative = record.get_deductable_rest_relative()
except ZeroDivisionError:
value_relative = 0
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)
def render_r(self, value, record: EcoAccount):
""" Renders the recorded column for an eco account
Args:
value (str): The identifier value
record (EcoAccount): The eco account record
Returns:
"""
html = ""
checked = value is not None
tooltip = _("Not recorded yet. Can not be used for deductions, yet.")
if checked:
on = value.get_timestamp_str_formatted()
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
icn_filled=checked,
)
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,10 +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 %}">
{% fa5_icon 'bell' %}
</button>
{% if is_default_member %} {% if is_default_member %}
<a href="{% url 'compensation:edit' obj.id %}" class="mr-2"> <a href="{% url 'compensation:edit' obj.id %}" class="mr-2">
<button class="btn btn-default" title="{% trans 'Edit' %}"> <button class="btn btn-default" title="{% trans 'Edit' %}">

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' %}
@ -20,12 +20,7 @@
</div> </div>
</div> </div>
</div> </div>
{% if not has_finished_deadlines %} <div class="card-body scroll-300 p-2">
<div class="alert alert-danger mb-0">
{% trans 'Missing finished deadline ' %}
</div>
{% endif %}
<div class="card-body {% if tables_scrollable %}scroll-300{% endif %} p-2">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@ -38,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' %}
@ -60,7 +55,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>

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