Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5550b8aa67 | |||
| a6f7e605e6 | |||
| 8bce8b8e75 | |||
| 4f02e8ee1b | |||
| 117a4437fe | |||
| f9c23a8f29 | |||
| a5f0d7f8c6 | |||
| 982d9f1930 | |||
| 9840c8fa8f | |||
| 890911c2dc | |||
| 3d10e84852 | |||
| 05145673e1 | |||
| fd4c9b0e5e | |||
| 23f0567aef | |||
| 70555ee5a3 | |||
| 1c38acea25 | |||
| cf282c937f | |||
| ee9834c0da | |||
| dacfbd0504 | |||
| 0e7859e538 | |||
| 16107f93f6 | |||
| d669adf54f | |||
| 589a7aec60 | |||
| 3f3ae4e31b | |||
| 1bbede0d12 | |||
| 0801b3f6ab | |||
| 44ad156595 | |||
| 5c95bc7d85 | |||
| 3b045fea8e | |||
| 6e5237ab88 | |||
| f19ad5f639 | |||
| 766e3c8d37 | |||
| f6dcb6c6db | |||
| e90625c8b5 | |||
| 7bacbecdec | |||
| 58ce00a5a6 | |||
| be885306c5 | |||
| 8b67df7617 | |||
| ab9af7ae2f | |||
| f085caac5d | |||
| 7f8d900c10 | |||
| e7031d0bc2 | |||
| 8aa3fbd97a | |||
| eb3b9eb5c1 | |||
| 1e86a1ce5e | |||
| fbab67f897 | |||
| 59c5072619 | |||
| 51525d79f5 | |||
| 4f482595c6 | |||
| 57aa39a670 | |||
| 2f4301d09f | |||
| fec43e1bed | |||
| 9f32c2fdd9 | |||
| b310349c1a | |||
| 946f3af77c | |||
| e73b7633a3 | |||
| d1dc61cbd7 | |||
| 3ba3785ef1 | |||
| 71afdd8b36 | |||
| a334fff54d | |||
| b65dae5b95 | |||
| bb399571b1 | |||
| 90e76bc8f5 | |||
| 5e84dfc4ae | |||
| 615eb65534 | |||
| bcde400096 | |||
| aa02dbab96 | |||
| db884baa09 | |||
| 253b509122 | |||
| c60afb0391 | |||
| 49c14a67b6 | |||
| d13c3e8094 | |||
| c6e784e6d4 | |||
| 8b7c4a82aa | |||
| f7b074ab23 | |||
| ac4dacefe0 | |||
| b668c562dd | |||
| 460011a5e8 | |||
| 76e018e084 | |||
| e00b050c8b | |||
| ab9023aad0 | |||
| 0dd1ac70d1 | |||
| 2fcc41bf4a | |||
| b4e75fa2cd |
@@ -22,6 +22,7 @@ class TimespanReportForm(BaseForm):
|
||||
date_from = forms.DateField(
|
||||
label_suffix="",
|
||||
label=_("From"),
|
||||
help_text=_("Entries created from..."),
|
||||
widget=forms.DateInput(
|
||||
attrs={
|
||||
"type": "date",
|
||||
@@ -34,6 +35,7 @@ class TimespanReportForm(BaseForm):
|
||||
date_to = forms.DateField(
|
||||
label_suffix="",
|
||||
label=_("To"),
|
||||
help_text=_("Entries created until..."),
|
||||
widget=forms.DateInput(
|
||||
attrs={
|
||||
"type": "date",
|
||||
|
||||
@@ -15,40 +15,40 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<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 'bookmark' %} {% trans 'Recorded' %}</th>
|
||||
<th scope="col">{% trans 'Number single areas' %}</th>
|
||||
<th scope="col">{% trans 'Total' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<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_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>
|
||||
<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_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>
|
||||
<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_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>
|
||||
<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_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>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="w-25">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
|
||||
<th scope="col">{% trans 'Total' %}</th>
|
||||
<th scope="col" class="w-25">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{report.eco_account_report.queryset_recorded_count|default_if_zero:"-"}}</td>
|
||||
<td>{{report.eco_account_report.queryset_count|default_if_zero:"-"}}</td>
|
||||
<td>{{report.eco_account_report.queryset_recorded_count|default_if_zero:"-"}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
{% load i18n fontawesome_5 ksp_filters %}
|
||||
|
||||
<h3>{% trans 'Deductions' %}</h3>
|
||||
<strong>
|
||||
{% blocktrans %}
|
||||
Recorded = Counts the deductions whose interventions have been recorded
|
||||
{% endblocktrans %}
|
||||
</strong>
|
||||
<div class="table-container">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</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>
|
||||
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
|
||||
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %} {% trans 'Surface' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{report.eco_account_report.queryset_deductions_recorded_count|default_if_zero:"-"}}</td>
|
||||
<td>{{report.eco_account_report.recorded_deductions_sq_m|default_if_zero:"-"}}m²</td>
|
||||
<td>{{report.eco_account_report.queryset_deductions_count|default_if_zero:"-"}}</td>
|
||||
<td>{{report.eco_account_report.deductions_sq_m|default_if_zero:"-"}}m²</td>
|
||||
<td>
|
||||
{{report.eco_account_report.deductions_sq_m|default_if_zero:"-"}}
|
||||
{% if report.eco_account_report.deductions_sq_m > 0 %}
|
||||
m²
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{report.eco_account_report.queryset_deductions_recorded_count|default_if_zero:"-"}}</td>
|
||||
<td>
|
||||
{{report.eco_account_report.recorded_deductions_sq_m|default_if_zero:"-"}}
|
||||
{% if report.eco_account_report.recorded_deductions_sq_m > 0 %}
|
||||
m²
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -14,16 +14,16 @@
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="w-25">{% trans 'Total' %}</th>
|
||||
<th scope="col">{% fa5_icon 'star' %} {% trans 'Checked' %}</th>
|
||||
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
|
||||
<th scope="col" class="w-25">{% trans 'Total' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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_recorded_count|default_if_zero:"-"}}</td>
|
||||
<td>{{report.intervention_report.queryset_count|default_if_zero:"-"}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -5,29 +5,29 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<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 'bookmark' %} {% trans 'Recorded' %}</th>
|
||||
<th class="w-25" scope="col">{% trans 'Total' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<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_recorded|default_if_zero:"-"}}</td>
|
||||
<td>{{report.intervention_report.compensation_sum|default_if_zero:"-"}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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_recorded|default_if_zero:"-"}}</td>
|
||||
<td>{{report.intervention_report.payment_sum|default_if_zero:"-"}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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_recorded|default_if_zero:"-"}}</td>
|
||||
<td>{{report.intervention_report.deduction_sum|default_if_zero:"-"}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -13,15 +13,15 @@
|
||||
<th class="w-25" scope="col">
|
||||
{% trans 'Law' %}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{% trans 'Total' %}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{% fa5_icon 'star' %} {% trans 'Checked' %}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{% trans 'Total' %}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -34,16 +34,16 @@
|
||||
{{law.long_name}}
|
||||
</small>
|
||||
</td>
|
||||
<td>{{law.num|default_if_zero:"-"}}</td>
|
||||
<td>{{law.num_checked|default_if_zero:"-"}}</td>
|
||||
<td>{{law.num_recorded|default_if_zero:"-"}}</td>
|
||||
<td>{{law.num|default_if_zero:"-"}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<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_recorded|default_if_zero:"-"}}</strong></td>
|
||||
<td><strong>{{report.intervention_report.law_sum|default_if_zero:"-"}}</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -14,26 +14,26 @@
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="w-25">{% fa5_icon 'star' %} {% trans 'Type' %}</th>
|
||||
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
|
||||
<th scope="col" class="w-25">{% trans 'Type' %}</th>
|
||||
<th scope="col">{% trans 'Total' %}</th>
|
||||
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{% trans 'Intervention' %}</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>
|
||||
<td>{{report.old_data_report.queryset_intervention_recorded_count|default_if_zero:"-"}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Compensation' %}</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>
|
||||
<td>{{report.old_data_report.queryset_comps_recorded_count|default_if_zero:"-"}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans 'Eco-account' %}</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>
|
||||
<td>{{report.old_data_report.queryset_acc_recorded_count|default_if_zero:"-"}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
{% fa5_icon 'pencil-ruler' %}
|
||||
{% trans 'Old interventions' %}
|
||||
</h5>
|
||||
<span>{% trans 'Before' %} 16.06.2018</span>
|
||||
<span>{% trans 'Binding date before' %} 16.06.2018</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -108,7 +108,7 @@ class TempExcelFile:
|
||||
for _iter_entry in _iter_obj:
|
||||
j = 0
|
||||
for _iter_attr in _attrs:
|
||||
_new_cell = ws.cell(start_cell.row + i, start_cell.column + j, getattr(_iter_entry, _iter_attr))
|
||||
_new_cell = ws.cell(start_cell.row + i, start_cell.column + j, _iter_entry.get(_iter_attr, "MISSING"))
|
||||
_new_cell.border = border_style
|
||||
j += 1
|
||||
i += 1
|
||||
|
||||
Binary file not shown.
@@ -137,22 +137,36 @@ class TimespanReport:
|
||||
).order_by(
|
||||
"long_name"
|
||||
)
|
||||
# Fetch all law ids which are used by any .legal object of an intervention object
|
||||
intervention_laws_total = self.queryset.values_list("legal__laws__id")
|
||||
intervention_laws_checked = self.queryset.filter(checked__isnull=False).values_list("legal__laws__id")
|
||||
intervention_laws_recorded = self.queryset.filter(recorded__isnull=False).values_list(
|
||||
"legal__laws__id")
|
||||
# Count how often which law id appears in the above list, return only the long_name of the law and the resulting
|
||||
# count (here 'num'). This is for keeping the db fetch as small as possible
|
||||
# Compute the sum for total, checked and recorded
|
||||
self.evaluated_laws = laws.annotate(
|
||||
num=Count("id", filter=Q(id__in=intervention_laws_total)),
|
||||
num_checked=Count("id", filter=Q(id__in=intervention_laws_checked)),
|
||||
num_recorded=Count("id", filter=Q(id__in=intervention_laws_recorded)),
|
||||
).values_list("short_name", "long_name", "num_checked", "num_recorded", "num", named=True)
|
||||
self.law_sum = self.evaluated_laws.aggregate(sum_num=Sum("num"))["sum_num"]
|
||||
self.law_sum_checked = self.evaluated_laws.aggregate(sum_num_checked=Sum("num_checked"))["sum_num_checked"]
|
||||
self.law_sum_recorded = self.evaluated_laws.aggregate(sum_num_recorded=Sum("num_recorded"))["sum_num_recorded"]
|
||||
|
||||
evaluated_laws = []
|
||||
sum_num_checked = 0
|
||||
sum_num_recorded = 0
|
||||
sum_num = 0
|
||||
for law in laws:
|
||||
num = self.queryset.filter(
|
||||
legal__laws__atom_id=law.atom_id
|
||||
).count()
|
||||
num_checked = self.queryset_checked.filter(
|
||||
legal__laws__atom_id=law.atom_id
|
||||
).count()
|
||||
num_recorded = self.queryset_recorded.filter(
|
||||
legal__laws__atom_id=law.atom_id
|
||||
).count()
|
||||
evaluated_laws.append({
|
||||
"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):
|
||||
""" Analyzes the types of compensation distribution
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"title": "Test_compensation",
|
||||
"is_cef": false,
|
||||
"is_coherence_keeping": false,
|
||||
"is_pik": false,
|
||||
"intervention": "MUST_BE_SET_IN_TEST",
|
||||
"before_states": [
|
||||
],
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"properties": {
|
||||
"title": "Test_ecoaccount",
|
||||
"deductable_surface": 10000.0,
|
||||
"is_pik": false,
|
||||
"responsible": {
|
||||
"conservation_office": null,
|
||||
"conservation_file_number": null,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
],
|
||||
"properties": {
|
||||
"title": "Test_ema",
|
||||
"is_pik": false,
|
||||
"responsible": {
|
||||
"conservation_office": null,
|
||||
"conservation_file_number": null,
|
||||
|
||||
@@ -122,6 +122,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
|
||||
props = geojson["properties"]
|
||||
props["is_cef"]
|
||||
props["is_coherence_keeping"]
|
||||
props["is_pik"]
|
||||
props["intervention"]
|
||||
props["intervention"]["id"]
|
||||
props["intervention"]["identifier"]
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"title": "TEST_compensation_CHANGED",
|
||||
"is_cef": true,
|
||||
"is_coherence_keeping": true,
|
||||
"is_pik": true,
|
||||
"intervention": "CHANGE_BEFORE_RUN!!!",
|
||||
"before_states": [],
|
||||
"after_states": [],
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"properties": {
|
||||
"title": "TEST_account_CHANGED",
|
||||
"deductable_surface": "100000.0",
|
||||
"is_pik": true,
|
||||
"responsible": {
|
||||
"conservation_office": null,
|
||||
"conservation_file_number": "123-TEST",
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"detail": "TEST_HANDLER_CHANGED"
|
||||
}
|
||||
},
|
||||
"is_pik": true,
|
||||
"before_states": [],
|
||||
"after_states": [],
|
||||
"actions": [],
|
||||
|
||||
@@ -12,6 +12,7 @@ from django.contrib.gis import geos
|
||||
from django.urls import reverse
|
||||
|
||||
from api.tests.v1.share.test_api_sharing import BaseAPIV1TestCase
|
||||
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
|
||||
|
||||
|
||||
class APIV1UpdateTestCase(BaseAPIV1TestCase):
|
||||
@@ -63,6 +64,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
|
||||
|
||||
put_props = put_body["properties"]
|
||||
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_props["title"], self.intervention.title)
|
||||
self.assertNotEqual(modified_on, self.intervention.modified)
|
||||
@@ -92,11 +94,13 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
|
||||
|
||||
put_props = put_body["properties"]
|
||||
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_props["title"], self.compensation.title)
|
||||
self.assertNotEqual(modified_on, self.compensation.modified)
|
||||
self.assertEqual(put_props["is_cef"], self.compensation.is_cef)
|
||||
self.assertEqual(put_props["is_coherence_keeping"], self.compensation.is_coherence_keeping)
|
||||
self.assertEqual(put_props["is_pik"], self.compensation.is_pik)
|
||||
self.assertEqual(len(put_props["actions"]), self.compensation.actions.count())
|
||||
self.assertEqual(len(put_props["before_states"]), self.compensation.before_states.count())
|
||||
self.assertEqual(len(put_props["after_states"]), self.compensation.after_states.count())
|
||||
@@ -120,6 +124,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
|
||||
|
||||
put_props = put_body["properties"]
|
||||
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_props["title"], self.eco_account.title)
|
||||
self.assertNotEqual(modified_on, self.eco_account.modified)
|
||||
@@ -151,6 +156,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
|
||||
|
||||
put_props = put_body["properties"]
|
||||
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_props["title"], self.ema.title)
|
||||
self.assertNotEqual(modified_on, self.ema.modified)
|
||||
|
||||
@@ -12,6 +12,7 @@ from django.contrib.gis import geos
|
||||
from django.contrib.gis.geos import GEOSGeometry
|
||||
from django.core.paginator import Paginator
|
||||
|
||||
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
|
||||
from konova.utils.message_templates import DATA_UNSHARED
|
||||
|
||||
|
||||
@@ -133,6 +134,8 @@ class AbstractModelAPISerializer:
|
||||
if isinstance(geojson, dict):
|
||||
geojson = json.dumps(geojson)
|
||||
geometry = geos.fromstr(geojson)
|
||||
if geometry.srid != DEFAULT_SRID_RLP:
|
||||
geometry.transform(DEFAULT_SRID_RLP)
|
||||
if geometry.empty:
|
||||
geometry = None
|
||||
return geometry
|
||||
|
||||
@@ -34,6 +34,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
|
||||
def _extend_properties_data(self, entry):
|
||||
self.properties_data["is_cef"] = entry.is_cef
|
||||
self.properties_data["is_coherence_keeping"] = entry.is_coherence_keeping
|
||||
self.properties_data["is_pik"] = entry.is_pik
|
||||
self.properties_data["intervention"] = self.intervention_to_json(entry.intervention)
|
||||
self.properties_data["before_states"] = self._compensation_state_to_json(entry.before_states.all())
|
||||
self.properties_data["after_states"] = self._compensation_state_to_json(entry.after_states.all())
|
||||
@@ -113,6 +114,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
|
||||
obj.title = properties["title"]
|
||||
obj.is_cef = properties["is_cef"]
|
||||
obj.is_coherence_keeping = properties["is_coherence_keeping"]
|
||||
obj.is_pik = properties.get("is_pik", False)
|
||||
obj = self.set_intervention(obj, properties["intervention"], user)
|
||||
|
||||
obj.geometry.save()
|
||||
@@ -149,6 +151,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
|
||||
obj.title = properties["title"]
|
||||
obj.is_cef = properties["is_cef"]
|
||||
obj.is_coherence_keeping = properties["is_coherence_keeping"]
|
||||
obj.is_pik = properties.get("is_pik", False)
|
||||
obj.modified = update_action
|
||||
obj.geometry.geom = self._create_geometry_from_json(json_model)
|
||||
obj.geometry.modified = update_action
|
||||
|
||||
@@ -25,6 +25,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
|
||||
model = EcoAccount
|
||||
|
||||
def _extend_properties_data(self, entry):
|
||||
self.properties_data["is_pik"] = entry.is_pik
|
||||
self.properties_data["deductable_surface"] = entry.deductable_surface
|
||||
self.properties_data["deductable_surface_available"] = entry.deductable_surface - entry.get_deductions_surface()
|
||||
self.properties_data["responsible"] = self._responsible_to_json(entry.responsible)
|
||||
@@ -122,6 +123,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
|
||||
properties = json_model["properties"]
|
||||
obj.identifier = obj.generate_new_identifier()
|
||||
obj.title = properties["title"]
|
||||
obj.is_pik = properties.get("is_pik", False)
|
||||
|
||||
try:
|
||||
obj.deductable_surface = float(properties["deductable_surface"])
|
||||
@@ -169,6 +171,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
|
||||
# Fill in data to objects
|
||||
properties = json_model["properties"]
|
||||
obj.title = properties["title"]
|
||||
obj.is_pik = properties.get("is_pik", False)
|
||||
obj.deductable_surface = float(properties["deductable_surface"])
|
||||
obj.modified = update_action
|
||||
obj.geometry.geom = self._create_geometry_from_json(json_model)
|
||||
|
||||
@@ -21,6 +21,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
|
||||
model = Ema
|
||||
|
||||
def _extend_properties_data(self, entry):
|
||||
self.properties_data["is_pik"] = entry.is_pik
|
||||
self.properties_data["responsible"] = self._responsible_to_json(entry.responsible)
|
||||
self.properties_data["before_states"] = self._compensation_state_to_json(entry.before_states.all())
|
||||
self.properties_data["after_states"] = self._compensation_state_to_json(entry.after_states.all())
|
||||
@@ -104,6 +105,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
|
||||
properties = json_model["properties"]
|
||||
obj.identifier = obj.generate_new_identifier()
|
||||
obj.title = properties["title"]
|
||||
obj.is_pik = properties.get("is_pik", False)
|
||||
obj = self._set_responsibility(obj, properties["responsible"])
|
||||
|
||||
obj.geometry.save()
|
||||
@@ -141,6 +143,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
|
||||
# Fill in data to objects
|
||||
properties = json_model["properties"]
|
||||
obj.title = properties["title"]
|
||||
obj.is_pik = properties.get("is_pik", False)
|
||||
obj.modified = update_action
|
||||
obj.geometry.geom = self._create_geometry_from_json(json_model)
|
||||
obj.geometry.modified = update_action
|
||||
|
||||
165
codelist/management/commands/sync_codelist.py
Normal file
165
codelist/management/commands/sync_codelist.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 31.05.22
|
||||
|
||||
"""
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
from codelist.models import KonovaCode
|
||||
from compensation.models import CompensationAction, CompensationState
|
||||
from intervention.models import Legal, Handler, Responsibility
|
||||
from konova.management.commands.setup import BaseKonovaCommand
|
||||
|
||||
|
||||
class Command(BaseKonovaCommand):
|
||||
help = "Updates internal codelist by external API"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
try:
|
||||
with transaction.atomic():
|
||||
self.sync_codelist()
|
||||
except KeyboardInterrupt:
|
||||
self._break_line()
|
||||
exit(-1)
|
||||
|
||||
def __get_newest_code(self, code):
|
||||
code = KonovaCode.objects.filter(
|
||||
atom_id=code.atom_id,
|
||||
parent=code.parent,
|
||||
code_lists__in=code.code_lists.all(),
|
||||
).order_by(
|
||||
"-id"
|
||||
).first()
|
||||
return code
|
||||
|
||||
def __migrate_compensation_action_codes(self):
|
||||
all_actions = CompensationAction.objects.all()
|
||||
used_codes = []
|
||||
for action in all_actions:
|
||||
stored_codes = action.action_type.all()
|
||||
codes = []
|
||||
for code in stored_codes:
|
||||
codes.append(self.__get_newest_code(code))
|
||||
action.action_type.set(codes)
|
||||
used_codes += codes
|
||||
|
||||
stored_codes = action.action_type_details.all()
|
||||
codes = []
|
||||
for code in stored_codes:
|
||||
codes.append(self.__get_newest_code(code))
|
||||
action.action_type_details.set(codes)
|
||||
used_codes += codes
|
||||
|
||||
action.save()
|
||||
return used_codes
|
||||
|
||||
def __migrate_compensation_state_codes(self):
|
||||
all_states = CompensationState.objects.all()
|
||||
used_codes = []
|
||||
for state in all_states:
|
||||
code = state.biotope_type
|
||||
if code is not None:
|
||||
new_code = self.__get_newest_code(code)
|
||||
state.biotope_type = new_code
|
||||
used_codes.append(new_code)
|
||||
|
||||
stored_codes = state.biotope_type_details.all()
|
||||
codes = []
|
||||
for code in stored_codes:
|
||||
codes.append(self.__get_newest_code(code))
|
||||
state.biotope_type_details.set(codes)
|
||||
|
||||
used_codes += codes
|
||||
state.save()
|
||||
return used_codes
|
||||
|
||||
def __migrate_legal_codes(self):
|
||||
all_legal = Legal.objects.all()
|
||||
used_codes = []
|
||||
for legal in all_legal:
|
||||
code = legal.process_type
|
||||
if code is not None:
|
||||
new_code = self.__get_newest_code(code)
|
||||
legal.process_type = new_code
|
||||
used_codes.append(new_code)
|
||||
|
||||
stored_codes = legal.laws.all()
|
||||
codes = []
|
||||
for code in stored_codes:
|
||||
codes.append(self.__get_newest_code(code))
|
||||
legal.laws.set(codes)
|
||||
|
||||
used_codes += codes
|
||||
legal.save()
|
||||
return used_codes
|
||||
|
||||
def __migrate_handler_codes(apps):
|
||||
all_handlers = Handler.objects.all()
|
||||
used_codes = []
|
||||
for handler in all_handlers:
|
||||
code = handler.type
|
||||
if code is None:
|
||||
continue
|
||||
new_code = apps.__get_newest_code(code)
|
||||
handler.type = new_code
|
||||
used_codes.append(new_code)
|
||||
handler.save()
|
||||
return used_codes
|
||||
|
||||
def __migrate_responsibility_codes(apps):
|
||||
all_resps = Responsibility.objects.all()
|
||||
used_codes = []
|
||||
for responsibility in all_resps:
|
||||
code = responsibility.registration_office
|
||||
if code is not None:
|
||||
new_code = apps.__get_newest_code(code)
|
||||
responsibility.registration_office = new_code
|
||||
used_codes.append(new_code)
|
||||
|
||||
code = responsibility.conservation_office
|
||||
if code is not None:
|
||||
new_code = apps.__get_newest_code(code)
|
||||
responsibility.conservation_office = new_code
|
||||
used_codes.append(new_code)
|
||||
|
||||
responsibility.save()
|
||||
return used_codes
|
||||
|
||||
def sync_codelist(self):
|
||||
""" Due to issues on the external codelist app there can be multiple entries of the same code
|
||||
(atom_id, parent, list) but with different identifiers.
|
||||
|
||||
These issues have been resolved but already
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self._write_warning("Sync codes in usage and replace by newest entries...")
|
||||
used_codes = []
|
||||
used_codes += self.__migrate_compensation_action_codes()
|
||||
used_codes += self.__migrate_compensation_state_codes()
|
||||
used_codes += self.__migrate_legal_codes()
|
||||
used_codes += self.__migrate_handler_codes()
|
||||
used_codes += self.__migrate_responsibility_codes()
|
||||
self._write_success(f"Synced {len(used_codes)} code usages!")
|
||||
|
||||
all_codes = KonovaCode.objects.all()
|
||||
newest_code_ids = []
|
||||
for code in all_codes:
|
||||
newest_code = self.__get_newest_code(code)
|
||||
newest_code_ids.append(newest_code.id)
|
||||
|
||||
code_ids_to_keep = set(newest_code_ids)
|
||||
self._write_warning(f"Of {all_codes.count()} KonovaCodes there are {len(code_ids_to_keep)} to keep as newest versions...")
|
||||
|
||||
deletable_codes = KonovaCode.objects.all().exclude(
|
||||
id__in=code_ids_to_keep
|
||||
)
|
||||
deletable_codes_count = deletable_codes.count()
|
||||
self._write_warning(f"{deletable_codes_count} found which are obsolet...")
|
||||
if deletable_codes_count > 0:
|
||||
deletable_codes.delete()
|
||||
self._write_success("Obsolete codes deleted!")
|
||||
@@ -6,7 +6,6 @@ Created on: 23.08.21
|
||||
|
||||
"""
|
||||
import requests
|
||||
from django.core.management import BaseCommand
|
||||
from xml.etree import ElementTree as etree
|
||||
|
||||
from codelist.models import KonovaCode, KonovaCodeList
|
||||
|
||||
@@ -65,24 +65,23 @@ class KonovaCode(models.Model):
|
||||
ret_val += ", " + self.parent.long_name
|
||||
return ret_val
|
||||
|
||||
def add_children(self):
|
||||
def add_children(self, order_by: str = "long_name"):
|
||||
""" Adds all children (resurcively until leaf) as .children to the KonovaCode
|
||||
|
||||
Returns:
|
||||
code (KonovaCode): The manipulated KonovaCode instance
|
||||
"""
|
||||
if self.is_leaf:
|
||||
return None
|
||||
return self
|
||||
|
||||
children = KonovaCode.objects.filter(
|
||||
code_lists__in=self.code_lists.all(),
|
||||
parent=self
|
||||
).order_by(
|
||||
"long_name"
|
||||
order_by
|
||||
)
|
||||
self.children = children
|
||||
for child in children:
|
||||
child.add_children()
|
||||
child.add_children(order_by)
|
||||
return self
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ CODELIST_INTERVENTION_HANDLER_ID = 903 # CLMassnahmeträger
|
||||
CODELIST_CONSERVATION_OFFICE_ID = 907 # CLNaturschutzbehörden
|
||||
CODELIST_REGISTRATION_OFFICE_ID = 1053 # CLZulassungsbehörden
|
||||
CODELIST_BIOTOPES_ID = 654 # CL_Biotoptypen
|
||||
CODELIST_AFTER_STATE_BIOTOPES__ID = 974 # CL-KSP_ZielBiotoptypen - USAGE HAS BEEN DROPPED IN 2022 IN FAVOR OF 654
|
||||
CODELIST_AFTER_STATE_BIOTOPES_ID = 974 # CL-KSP_ZielBiotoptypen - USAGE HAS BEEN DROPPED IN 2022 IN FAVOR OF 654
|
||||
CODELIST_BIOTOPES_EXTRA_CODES_ID = 975 # CLZusatzbezeichnung
|
||||
CODELIST_LAW_ID = 1048 # CLVerfahrensrecht
|
||||
CODELIST_PROCESS_TYPE_ID = 44382 # CLVerfahrenstyp
|
||||
|
||||
@@ -55,6 +55,7 @@ class CompensationAdmin(AbstractCompensationAdmin):
|
||||
return super().get_fields(request, obj) + [
|
||||
"is_cef",
|
||||
"is_coherence_keeping",
|
||||
"is_pik",
|
||||
"intervention",
|
||||
]
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ class CheckboxCompensationTableFilter(CheckboxTableFilter):
|
||||
if not value:
|
||||
return queryset.filter(
|
||||
Q(intervention__users__in=[self.user]) | # requesting user has access
|
||||
Q(intervention__teams__users__in=[self.user])
|
||||
Q(intervention__teams__in=self.user.shared_teams)
|
||||
).distinct()
|
||||
else:
|
||||
return queryset
|
||||
|
||||
@@ -160,7 +160,23 @@ class CoherenceCompensationFormMixin(forms.Form):
|
||||
)
|
||||
|
||||
|
||||
class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, CoherenceCompensationFormMixin):
|
||||
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.
|
||||
@@ -191,6 +207,7 @@ class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, Co
|
||||
"identifier",
|
||||
"title",
|
||||
"intervention",
|
||||
"is_pik",
|
||||
"is_cef",
|
||||
"is_coherence_keeping",
|
||||
"comment",
|
||||
@@ -234,6 +251,7 @@ class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, Co
|
||||
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
|
||||
@@ -249,6 +267,7 @@ class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, Co
|
||||
created=action,
|
||||
is_cef=is_cef,
|
||||
is_coherence_keeping=is_coherence_keeping,
|
||||
is_pik=is_pik,
|
||||
geometry=geometry,
|
||||
comment=comment,
|
||||
)
|
||||
@@ -281,6 +300,7 @@ class EditCompensationForm(NewCompensationForm):
|
||||
"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 = []
|
||||
@@ -297,6 +317,7 @@ class EditCompensationForm(NewCompensationForm):
|
||||
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
|
||||
@@ -313,6 +334,7 @@ class EditCompensationForm(NewCompensationForm):
|
||||
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()
|
||||
|
||||
@@ -322,7 +344,7 @@ class EditCompensationForm(NewCompensationForm):
|
||||
return self.instance
|
||||
|
||||
|
||||
class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
|
||||
class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMixin, PikCompensationFormMixin):
|
||||
""" Form for creating eco accounts
|
||||
|
||||
Inherits from basic AbstractCompensationForm and further form fields from CompensationResponsibleFormMixin
|
||||
@@ -363,6 +385,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
|
||||
"registration_date",
|
||||
"surface",
|
||||
"conservation_file_number",
|
||||
"is_pik",
|
||||
"handler_type",
|
||||
"handler_detail",
|
||||
"comment",
|
||||
@@ -392,6 +415,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
|
||||
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
|
||||
@@ -423,6 +447,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
|
||||
created=action,
|
||||
geometry=geometry,
|
||||
comment=comment,
|
||||
is_pik=is_pik,
|
||||
legal=legal
|
||||
)
|
||||
acc.share_with_user(user)
|
||||
@@ -458,6 +483,7 @@ class EditEcoAccountForm(NewEcoAccountForm):
|
||||
"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 = []
|
||||
@@ -478,6 +504,7 @@ class EditEcoAccountForm(NewEcoAccountForm):
|
||||
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)
|
||||
@@ -503,6 +530,7 @@ class EditEcoAccountForm(NewEcoAccountForm):
|
||||
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()
|
||||
|
||||
|
||||
@@ -17,9 +17,10 @@ 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
|
||||
from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple, \
|
||||
CompensationStateTreeRadioSelect
|
||||
from konova.contexts import BaseContext
|
||||
from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm
|
||||
from konova.forms.modals 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
|
||||
@@ -156,22 +157,12 @@ class NewStateModalForm(BaseModalForm):
|
||||
What has been on this area before changes/compensations have been applied and what will be the result ('after')?
|
||||
|
||||
"""
|
||||
biotope_type = forms.ModelChoiceField(
|
||||
biotope_type = forms.ChoiceField(
|
||||
label=_("Biotope Type"),
|
||||
label_suffix="",
|
||||
required=True,
|
||||
help_text=_("Select the biotope type"),
|
||||
queryset=KonovaCode.objects.filter(
|
||||
is_archived=False,
|
||||
is_leaf=True,
|
||||
code_lists__in=[CODELIST_BIOTOPES_ID],
|
||||
),
|
||||
widget=autocomplete.ModelSelect2(
|
||||
url="codes-biotope-autocomplete",
|
||||
attrs={
|
||||
"data-placeholder": _("Biotope Type"),
|
||||
}
|
||||
),
|
||||
widget=CompensationStateTreeRadioSelect(),
|
||||
)
|
||||
biotope_extra = forms.ModelMultipleChoiceField(
|
||||
label=_("Biotope additional type"),
|
||||
@@ -209,6 +200,16 @@ class NewStateModalForm(BaseModalForm):
|
||||
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)
|
||||
@@ -271,8 +272,9 @@ class EditCompensationStateModalForm(NewStateModalForm):
|
||||
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": self.state.biotope_type,
|
||||
"biotope_type": biotope_type_id,
|
||||
"biotope_extra": self.state.biotope_type_details.all(),
|
||||
"surface": self.state.surface,
|
||||
}
|
||||
@@ -280,7 +282,8 @@ class EditCompensationStateModalForm(NewStateModalForm):
|
||||
|
||||
def save(self, is_before_state: bool = False):
|
||||
state = self.state
|
||||
state.biotope_type = self.cleaned_data.get("biotope_type", None)
|
||||
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()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from django.db import migrations, models, transaction
|
||||
import django.db.models.deletion
|
||||
|
||||
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_AFTER_STATE_BIOTOPES__ID
|
||||
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_AFTER_STATE_BIOTOPES_ID
|
||||
|
||||
|
||||
def migrate_entries_974_to_654(apps, schema_editor):
|
||||
@@ -23,7 +23,7 @@ def migrate_entries_974_to_654(apps, schema_editor):
|
||||
state.save()
|
||||
|
||||
old_list_states = CompensationState.objects.filter(
|
||||
biotope_type__code_lists__in=[CODELIST_AFTER_STATE_BIOTOPES__ID]
|
||||
biotope_type__code_lists__in=[CODELIST_AFTER_STATE_BIOTOPES_ID]
|
||||
)
|
||||
if old_list_states.count() > 0:
|
||||
raise Exception("Still unmigrated values!")
|
||||
|
||||
23
compensation/migrations/0007_auto_20220531_1245.py
Normal file
23
compensation/migrations/0007_auto_20220531_1245.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.3 on 2022-05-31 10:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('compensation', '0006_ecoaccount_teams'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='compensation',
|
||||
name='is_pik',
|
||||
field=models.BooleanField(blank=True, default=False, help_text="Flag if compensation is a 'Produktonsintegrierte Kompensation'", null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ecoaccount',
|
||||
name='is_pik',
|
||||
field=models.BooleanField(blank=True, default=False, help_text="Flag if compensation is a 'Produktonsintegrierte Kompensation'", null=True),
|
||||
),
|
||||
]
|
||||
24
compensation/migrations/0008_auto_20220815_0803.py
Normal file
24
compensation/migrations/0008_auto_20220815_0803.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
32
compensation/migrations/0009_auto_20220815_0803.py
Normal file
32
compensation/migrations/0009_auto_20220815_0803.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
24
compensation/migrations/0010_auto_20220815_1030.py
Normal file
24
compensation/migrations/0010_auto_20220815_1030.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
@@ -8,25 +8,28 @@ Created on: 16.11.21
|
||||
import shutil
|
||||
|
||||
from django.contrib import messages
|
||||
|
||||
from codelist.models import KonovaCode
|
||||
from user.models import User, Team
|
||||
from django.db import models, transaction
|
||||
from django.db.models import QuerySet, Sum
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from compensation.managers import CompensationManager
|
||||
from compensation.models import CompensationState, CompensationAction
|
||||
from compensation.utils.quality import CompensationQualityChecker
|
||||
from konova.models import BaseObject, AbstractDocument, Deadline, generate_document_file_upload_path, \
|
||||
GeoReferencedMixin
|
||||
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
|
||||
GeoReferencedMixin, DeadlineType, ResubmitableObjectMixin
|
||||
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, COMPENSATION_REMOVED_TEMPLATE, \
|
||||
DOCUMENT_REMOVED_TEMPLATE, COMPENSATION_EDITED_TEMPLATE, DEADLINE_REMOVED, ADDED_DEADLINE, \
|
||||
DOCUMENT_REMOVED_TEMPLATE, DEADLINE_REMOVED, ADDED_DEADLINE, \
|
||||
COMPENSATION_ACTION_REMOVED, COMPENSATION_STATE_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE
|
||||
from user.models import UserActionLogEntry
|
||||
|
||||
|
||||
class AbstractCompensation(BaseObject, GeoReferencedMixin):
|
||||
class AbstractCompensation(BaseObject,
|
||||
GeoReferencedMixin,
|
||||
ResubmitableObjectMixin
|
||||
):
|
||||
"""
|
||||
Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation,
|
||||
EMA or EcoAccount.
|
||||
@@ -142,8 +145,10 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
|
||||
"""
|
||||
form_data = form.cleaned_data
|
||||
with transaction.atomic():
|
||||
biotope_type_id = form_data["biotope_type"]
|
||||
code = KonovaCode.objects.get(id=biotope_type_id)
|
||||
state = CompensationState.objects.create(
|
||||
biotope_type=form_data["biotope_type"],
|
||||
biotope_type=code,
|
||||
surface=form_data["surface"],
|
||||
)
|
||||
state_additional_types = form_data["biotope_extra"]
|
||||
@@ -222,6 +227,15 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
|
||||
request = self.set_geometry_conflict_message(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):
|
||||
""" Provides CEF flag as Mixin
|
||||
@@ -253,7 +267,22 @@ class CoherenceMixin(models.Model):
|
||||
abstract = True
|
||||
|
||||
|
||||
class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
|
||||
class PikMixin(models.Model):
|
||||
""" Provides PIK flag as Mixin
|
||||
|
||||
"""
|
||||
is_pik = models.BooleanField(
|
||||
blank=True,
|
||||
null=True,
|
||||
default=False,
|
||||
help_text="Flag if compensation is a 'Produktonsintegrierte Kompensation'"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
|
||||
"""
|
||||
Regular compensation, linked to an intervention
|
||||
"""
|
||||
|
||||
@@ -17,14 +17,14 @@ from django.db.models import Sum, QuerySet
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from compensation.managers import EcoAccountManager, EcoAccountDeductionManager
|
||||
from compensation.models.compensation import AbstractCompensation
|
||||
from compensation.models.compensation import AbstractCompensation, PikMixin
|
||||
from compensation.utils.quality import EcoAccountQualityChecker
|
||||
from konova.models import ShareableObjectMixin, RecordableObjectMixin, AbstractDocument, BaseResource, \
|
||||
generate_document_file_upload_path
|
||||
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
|
||||
from konova.tasks import celery_send_mail_deduction_changed, celery_send_mail_deduction_changed_team
|
||||
|
||||
|
||||
class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin):
|
||||
class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, PikMixin):
|
||||
"""
|
||||
An eco account is a kind of 'prepaid' compensation. It can be compared to an account that already has been filled
|
||||
with some kind of currency. From this account one is able to deduct currency for current projects.
|
||||
@@ -162,6 +162,25 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
|
||||
"""
|
||||
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.identifier, self.title, 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.identifier, self.title, team_id, data_change)
|
||||
|
||||
|
||||
class EcoAccountDocument(AbstractDocument):
|
||||
"""
|
||||
@@ -252,4 +271,4 @@ class EcoAccountDeduction(BaseResource):
|
||||
if user is not None:
|
||||
self.intervention.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)
|
||||
|
||||
@@ -12,7 +12,6 @@ from codelist.models import KonovaCode
|
||||
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
|
||||
from compensation.managers import CompensationStateManager
|
||||
from konova.models import UuidModel
|
||||
from konova.utils.message_templates import COMPENSATION_STATE_REMOVED
|
||||
|
||||
|
||||
class CompensationState(UuidModel):
|
||||
|
||||
@@ -5,17 +5,15 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 01.12.20
|
||||
|
||||
"""
|
||||
from user.models import User
|
||||
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.timezone import localtime
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from compensation.filters import CompensationTableFilter, EcoAccountTableFilter
|
||||
from compensation.models import Compensation, EcoAccount
|
||||
from konova.sub_settings.django_settings import DEFAULT_DATE_TIME_FORMAT
|
||||
from konova.utils.tables import BaseTable, TableRenderMixin
|
||||
import django_tables2 as tables
|
||||
|
||||
@@ -111,16 +109,21 @@ class CompensationTable(BaseTable, TableRenderMixin):
|
||||
"""
|
||||
html = ""
|
||||
checked = value is not None
|
||||
tooltip = _("Not checked yet")
|
||||
tooltip = DATA_IS_UNCHECKED
|
||||
previously_checked = record.intervention.get_last_checked_action()
|
||||
if checked:
|
||||
value = value.timestamp
|
||||
value = localtime(value)
|
||||
checked_on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
|
||||
tooltip = _("Checked on {} by {}").format(checked_on, record.intervention.checked.user)
|
||||
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):
|
||||
@@ -159,9 +162,7 @@ class CompensationTable(BaseTable, TableRenderMixin):
|
||||
recorded = value is not None
|
||||
tooltip = _("Not recorded yet")
|
||||
if recorded:
|
||||
value = value.timestamp
|
||||
value = localtime(value)
|
||||
on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
|
||||
on = value.get_timestamp_str_formatted()
|
||||
tooltip = _("Recorded on {} by {}").format(on, record.intervention.recorded.user)
|
||||
html += self.render_bookmark(
|
||||
tooltip=tooltip,
|
||||
@@ -179,8 +180,6 @@ class CompensationTable(BaseTable, TableRenderMixin):
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if value is None:
|
||||
value = User.objects.none()
|
||||
has_access = record.is_shared_with(self.user)
|
||||
|
||||
html = self.render_icn(
|
||||
@@ -318,9 +317,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
|
||||
checked = value is not None
|
||||
tooltip = _("Not recorded yet. Can not be used for deductions, yet.")
|
||||
if checked:
|
||||
value = value.timestamp
|
||||
value = localtime(value)
|
||||
on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
|
||||
on = value.get_timestamp_str_formatted()
|
||||
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
|
||||
html += self.render_bookmark(
|
||||
tooltip=tooltip,
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
</button>
|
||||
</a>
|
||||
{% 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 %}
|
||||
<a href="{% url 'compensation:edit' obj.id %}" class="mr-2">
|
||||
<button class="btn btn-default" title="{% trans 'Edit' %}">
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if not has_finished_deadlines %}
|
||||
<div class="alert alert-danger mb-0">
|
||||
{% trans 'Missing finished deadline ' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card-body scroll-300 p-2">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
|
||||
@@ -39,6 +39,16 @@
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Is PIK' %}</th>
|
||||
<td class="align-middle">
|
||||
{% if obj.is_pik %}
|
||||
{% trans 'Yes' %}
|
||||
{% else %}
|
||||
{% trans 'No' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Is CEF compensation' %}</th>
|
||||
<td class="align-middle">
|
||||
@@ -66,6 +76,11 @@
|
||||
<span>
|
||||
{% fa5_icon 'star' 'far' %}
|
||||
</span>
|
||||
{% if last_checked %}
|
||||
<span class="rlp-gd-inv" title="{{last_checked_tooltip}}">
|
||||
{% fa5_icon 'star' 'fas' %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="check-star" title="{% trans 'Checked on '%} {{obj.intervention.checked.timestamp}} {% trans 'by' %} {{obj.intervention.checked.user}}">
|
||||
{% fa5_icon 'star' %}
|
||||
@@ -104,13 +119,20 @@
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Shared with' %}</th>
|
||||
<td class="align-middle">
|
||||
{% for team in obj.intervention.teams.all %}
|
||||
{% for team in obj.intervention.shared_teams %}
|
||||
{% include 'user/includes/team_data_modal_button.html' %}
|
||||
{% endfor %}
|
||||
<hr>
|
||||
{% for user in obj.intervention.users.all %}
|
||||
{% include 'user/includes/contact_modal_button.html' %}
|
||||
{% endfor %}
|
||||
{% if has_access %}
|
||||
{% for user in obj.users.all %}
|
||||
{% include 'user/includes/contact_modal_button.html' %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span title="{% trans 'The data must be shared with you, if you want to see which other users have shared access as well.' %}">
|
||||
{% fa5_icon 'eye-slash' %}
|
||||
{{obj.users.count}} {% trans 'other users' %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -119,7 +141,9 @@
|
||||
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
{% include 'map/geom_form.html' %}
|
||||
<div class="col-sm-12">
|
||||
{% include 'map/geom_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/parcels/parcels.html' %}
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
</button>
|
||||
</a>
|
||||
{% if has_access %}
|
||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'compensation:acc:resubmission-create' obj.id %}">
|
||||
{% fa5_icon 'bell' %}
|
||||
</button>
|
||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'compensation:acc:share-create' obj.id %}">
|
||||
{% fa5_icon 'share-alt' %}
|
||||
</button>
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if not has_finished_deadlines %}
|
||||
<div class="alert alert-danger mb-0">
|
||||
{% trans 'Missing finished deadline ' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card-body scroll-300 p-2">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
|
||||
@@ -70,6 +70,16 @@
|
||||
<th scope="row">{% trans 'Action handler' %}</th>
|
||||
<td class="align-middle">{{obj.responsible.handler|default_if_none:""}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Is PIK' %}</th>
|
||||
<td class="align-middle">
|
||||
{% if obj.is_pik %}
|
||||
{% trans 'Yes' %}
|
||||
{% else %}
|
||||
{% trans 'No' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Last modified' %}</th>
|
||||
<td class="align-middle">
|
||||
@@ -87,13 +97,20 @@
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Shared with' %}</th>
|
||||
<td class="align-middle">
|
||||
{% for team in obj.teams.all %}
|
||||
{% for team in obj.shared_teams %}
|
||||
{% include 'user/includes/team_data_modal_button.html' %}
|
||||
{% endfor %}
|
||||
<hr>
|
||||
{% for user in obj.users.all %}
|
||||
{% include 'user/includes/contact_modal_button.html' %}
|
||||
{% endfor %}
|
||||
{% if has_access %}
|
||||
{% for user in obj.users.all %}
|
||||
{% include 'user/includes/contact_modal_button.html' %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span title="{% trans 'The data must be shared with you, if you want to see which other users have shared access as well.' %}">
|
||||
{% fa5_icon 'eye-slash' %}
|
||||
{{obj.users.count}} {% trans 'other users' %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -101,7 +118,9 @@
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
|
||||
<div class="row">
|
||||
{% include 'map/geom_form.html' %}
|
||||
<div class="col-sm-12">
|
||||
{% include 'map/geom_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/parcels/parcels.html' %}
|
||||
|
||||
@@ -20,6 +20,36 @@
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Is PIK' %}</th>
|
||||
<td class="align-middle">
|
||||
{% if obj.is_pik %}
|
||||
{% trans 'Yes' %}
|
||||
{% else %}
|
||||
{% trans 'No' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Is CEF' %}</th>
|
||||
<td class="align-middle">
|
||||
{% if obj.is_cef %}
|
||||
{% trans 'Yes' %}
|
||||
{% else %}
|
||||
{% trans 'No' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Is coherence keeping' %}</th>
|
||||
<td class="align-middle">
|
||||
{% if obj.is_coherence_keeping %}
|
||||
{% trans 'Yes' %}
|
||||
{% else %}
|
||||
{% trans 'No' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Last modified' %}</th>
|
||||
<td class="align-middle">
|
||||
@@ -35,7 +65,9 @@
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
|
||||
<div class="row">
|
||||
{% include 'map/geom_form.html' %}
|
||||
<div class="col-sm-12">
|
||||
{% include 'map/geom_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/parcels/parcels.html' %}
|
||||
|
||||
@@ -20,6 +20,16 @@
|
||||
<th scope="row">{% trans 'Conservation office file number' %}</th>
|
||||
<td class="align-middle">{{obj.responsible.conservation_file_number|default_if_none:""}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Is PIK' %}</th>
|
||||
<td class="align-middle">
|
||||
{% if obj.is_pik %}
|
||||
{% trans 'Yes' %}
|
||||
{% else %}
|
||||
{% trans 'No' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Deductions for' %}</th>
|
||||
<td class="align-middle">
|
||||
@@ -48,7 +58,9 @@
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
|
||||
<div class="row">
|
||||
{% include 'map/geom_form.html' %}
|
||||
<div class="col-sm-12">
|
||||
{% include 'map/geom_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/parcels/parcels.html' %}
|
||||
|
||||
@@ -50,10 +50,11 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
|
||||
test_id = self.create_dummy_string()
|
||||
test_title = self.create_dummy_string()
|
||||
test_geom = self.create_dummy_geometry()
|
||||
geom_json = self.create_geojson(test_geom)
|
||||
post_data = {
|
||||
"identifier": test_id,
|
||||
"title": test_title,
|
||||
"geom": test_geom.geojson,
|
||||
"geom": geom_json,
|
||||
"intervention": self.intervention.id,
|
||||
}
|
||||
pre_creation_intervention_log_count = self.intervention.log.count()
|
||||
@@ -88,10 +89,11 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
|
||||
test_id = self.create_dummy_string()
|
||||
test_title = self.create_dummy_string()
|
||||
test_geom = self.create_dummy_geometry()
|
||||
geom_json = self.create_geojson(test_geom)
|
||||
post_data = {
|
||||
"identifier": test_id,
|
||||
"title": test_title,
|
||||
"geom": test_geom.geojson,
|
||||
"geom": geom_json,
|
||||
}
|
||||
pre_creation_intervention_log_count = self.intervention.log.count()
|
||||
|
||||
@@ -126,6 +128,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
|
||||
new_identifier = self.create_dummy_string()
|
||||
new_comment = self.create_dummy_string()
|
||||
new_geometry = MultiPolygon(srid=4326) # Create an empty geometry
|
||||
geojson = self.create_geojson(new_geometry)
|
||||
|
||||
check_on_elements = {
|
||||
self.compensation.title: new_title,
|
||||
@@ -140,7 +143,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
|
||||
"title": new_title,
|
||||
"intervention": self.intervention.id, # just keep the intervention as it is
|
||||
"comment": new_comment,
|
||||
"geom": new_geometry.geojson,
|
||||
"geom": geojson,
|
||||
}
|
||||
self.client_user.post(url, post_data)
|
||||
self.compensation.refresh_from_db()
|
||||
|
||||
@@ -40,12 +40,13 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
|
||||
test_id = self.create_dummy_string()
|
||||
test_title = self.create_dummy_string()
|
||||
test_geom = self.create_dummy_geometry()
|
||||
geom_json = self.create_geojson(test_geom)
|
||||
test_deductable_surface = 1000
|
||||
test_conservation_office = self.get_conservation_office_code()
|
||||
post_data = {
|
||||
"identifier": test_id,
|
||||
"title": test_title,
|
||||
"geom": test_geom.geojson,
|
||||
"geom": geom_json,
|
||||
"deductable_surface": test_deductable_surface,
|
||||
"conservation_office": test_conservation_office.id
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ urlpatterns = [
|
||||
path('<id>/deadline/<deadline_id>/edit', deadline_edit_view, name='deadline-edit'),
|
||||
path('<id>/deadline/<deadline_id>/remove', deadline_remove_view, name='deadline-remove'),
|
||||
path('<id>/report', report_view, name='report'),
|
||||
path('<id>/resub', create_resubmission_view, name='resubmission-create'),
|
||||
|
||||
# Documents
|
||||
path('<id>/document/new/', new_document_view, name='new-doc'),
|
||||
|
||||
@@ -19,6 +19,7 @@ urlpatterns = [
|
||||
path('<id>/report', report_view, name='report'),
|
||||
path('<id>/edit', edit_view, name='edit'),
|
||||
path('<id>/remove', remove_view, name='remove'),
|
||||
path('<id>/resub', create_resubmission_view, name='resubmission-create'),
|
||||
|
||||
path('<id>/state/new', state_new_view, name='new-state'),
|
||||
path('<id>/state/<state_id>/edit', state_edit_view, name='state-edit'),
|
||||
|
||||
@@ -19,6 +19,7 @@ class CompensationQualityChecker(AbstractQualityChecker):
|
||||
self._check_states()
|
||||
self._check_actions()
|
||||
self._check_geometry()
|
||||
self._check_deadlines()
|
||||
self.valid = len(self.messages) == 0
|
||||
|
||||
def _check_states(self):
|
||||
@@ -47,6 +48,16 @@ class CompensationQualityChecker(AbstractQualityChecker):
|
||||
if not self.obj.actions.all():
|
||||
self._add_missing_attr_name(_con("Compensation", "Actions"))
|
||||
|
||||
def _check_deadlines(self):
|
||||
""" Checks data quality for related Deadline objects
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
finished_deadlines = self.obj.get_finished_deadlines()
|
||||
if not finished_deadlines.exists():
|
||||
self._add_missing_attr_name(_("Finished deadlines"))
|
||||
|
||||
|
||||
class EcoAccountQualityChecker(CompensationQualityChecker):
|
||||
def run_check(self):
|
||||
|
||||
@@ -14,7 +14,9 @@ from compensation.tables import CompensationTable
|
||||
from intervention.models import Intervention
|
||||
from konova.contexts import BaseContext
|
||||
from konova.decorators import *
|
||||
from konova.forms import RemoveModalForm, SimpleGeomForm, RemoveDeadlineModalForm, EditDocumentModalForm
|
||||
from konova.forms.modals import RemoveModalForm,RemoveDeadlineModalForm, EditDocumentModalForm, \
|
||||
ResubmissionModalForm
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.models import Deadline
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.utils.documents import get_document, remove_document
|
||||
@@ -23,7 +25,7 @@ from konova.utils.message_templates import FORM_INVALID, IDENTIFIER_REPLACED, DA
|
||||
CHECKED_RECORDED_RESET, COMPENSATION_ADDED_TEMPLATE, COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, \
|
||||
COMPENSATION_STATE_REMOVED, COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_ADDED, \
|
||||
DEADLINE_ADDED, DEADLINE_REMOVED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, \
|
||||
DEADLINE_EDITED, RECORDED_BLOCKS_EDIT, PARAMS_INVALID
|
||||
DEADLINE_EDITED, RECORDED_BLOCKS_EDIT, PARAMS_INVALID, DATA_CHECKED_PREVIOUSLY_TEMPLATE
|
||||
from konova.utils.user_checks import in_group
|
||||
|
||||
|
||||
@@ -217,8 +219,15 @@ def detail_view(request: HttpRequest, id: str):
|
||||
|
||||
request = comp.set_status_messages(request)
|
||||
|
||||
last_checked = comp.intervention.get_last_checked_action()
|
||||
last_checked_tooltip = ""
|
||||
if last_checked:
|
||||
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user)
|
||||
|
||||
context = {
|
||||
"obj": comp,
|
||||
"last_checked": last_checked,
|
||||
"last_checked_tooltip": last_checked_tooltip,
|
||||
"geom_form": geom_form,
|
||||
"parcels": parcels,
|
||||
"has_access": is_data_shared,
|
||||
@@ -233,6 +242,7 @@ def detail_view(request: HttpRequest, id: str):
|
||||
"is_ets_member": in_group(_user, ETS_GROUP),
|
||||
"LANIS_LINK": comp.get_LANIS_link(),
|
||||
TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}",
|
||||
"has_finished_deadlines": comp.get_finished_deadlines().exists(),
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, template, context)
|
||||
@@ -648,3 +658,26 @@ def report_view(request: HttpRequest, id: str):
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, template, context)
|
||||
|
||||
|
||||
@login_required
|
||||
@default_group_required
|
||||
@shared_access_required(Compensation, "id")
|
||||
def create_resubmission_view(request: HttpRequest, id: str):
|
||||
""" Renders resubmission form for a compensation
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): Compensation's id
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
com = get_object_or_404(Compensation, id=id)
|
||||
form = ResubmissionModalForm(request.POST or None, instance=com, request=request)
|
||||
form.action_url = reverse("compensation:resubmission-create", args=(id,))
|
||||
return form.process_request(
|
||||
request,
|
||||
msg_success=_("Resubmission set"),
|
||||
redirect_url=reverse("compensation:detail", args=(id,))
|
||||
)
|
||||
|
||||
@@ -25,14 +25,15 @@ from intervention.forms.modalForms import NewDeductionModalForm, ShareModalForm,
|
||||
from konova.contexts import BaseContext
|
||||
from konova.decorators import any_group_check, default_group_required, conservation_office_group_required, \
|
||||
shared_access_required
|
||||
from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentModalForm, RecordModalForm, \
|
||||
RemoveDeadlineModalForm, EditDocumentModalForm
|
||||
from konova.forms.modals import RemoveModalForm, RecordModalForm, \
|
||||
RemoveDeadlineModalForm, EditDocumentModalForm, ResubmissionModalForm
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.models import Deadline
|
||||
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.utils.documents import get_document, remove_document
|
||||
from konova.utils.generators import generate_qr_code
|
||||
from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \
|
||||
from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, \
|
||||
CANCEL_ACC_RECORDED_OR_DEDUCTED, DEDUCTION_REMOVED, DEDUCTION_ADDED, DOCUMENT_ADDED, COMPENSATION_STATE_REMOVED, \
|
||||
COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_ADDED, DEADLINE_ADDED, DEADLINE_REMOVED, \
|
||||
DEDUCTION_EDITED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED, \
|
||||
@@ -242,6 +243,7 @@ def detail_view(request: HttpRequest, id: str):
|
||||
"deductions": deductions,
|
||||
"actions": actions,
|
||||
TAB_TITLE_IDENTIFIER: f"{acc.identifier} - {acc.title}",
|
||||
"has_finished_deadlines": acc.get_finished_deadlines().exists(),
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, template, context)
|
||||
@@ -837,4 +839,27 @@ def create_share_view(request: HttpRequest, id: str):
|
||||
return form.process_request(
|
||||
request,
|
||||
msg_success=_("Share settings updated")
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@default_group_required
|
||||
@shared_access_required(EcoAccount, "id")
|
||||
def create_resubmission_view(request: HttpRequest, id: str):
|
||||
""" Renders resubmission form for an eco account
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): EcoAccount's id
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
acc = get_object_or_404(EcoAccount, id=id)
|
||||
form = ResubmissionModalForm(request.POST or None, instance=acc, request=request)
|
||||
form.action_url = reverse("compensation:acc:resubmission-create", args=(id,))
|
||||
return form.process_request(
|
||||
request,
|
||||
msg_success=_("Resubmission set"),
|
||||
redirect_url=reverse("compensation:acc:detail", args=(id,))
|
||||
)
|
||||
@@ -15,7 +15,6 @@ from compensation.forms.modalForms import NewPaymentForm, RemovePaymentModalForm
|
||||
from compensation.models import Payment
|
||||
from intervention.models import Intervention
|
||||
from konova.decorators import default_group_required, shared_access_required
|
||||
from konova.forms import RemoveModalForm
|
||||
from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED, PAYMENT_EDITED
|
||||
|
||||
|
||||
|
||||
16
ema/forms.py
16
ema/forms.py
@@ -5,21 +5,21 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 06.10.21
|
||||
|
||||
"""
|
||||
from dal import autocomplete
|
||||
from django import forms
|
||||
from user.models import User
|
||||
from django.db import transaction
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from compensation.forms.forms import AbstractCompensationForm, CompensationResponsibleFormMixin
|
||||
from compensation.forms.forms import AbstractCompensationForm, CompensationResponsibleFormMixin, \
|
||||
PikCompensationFormMixin
|
||||
from ema.models import Ema, EmaDocument
|
||||
from intervention.models import Responsibility, Handler
|
||||
from konova.forms import SimpleGeomForm, NewDocumentModalForm
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.forms.modals import NewDocumentModalForm
|
||||
from user.models import UserActionLogEntry
|
||||
|
||||
|
||||
class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
|
||||
class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin, PikCompensationFormMixin):
|
||||
""" Form for creating new EMA objects.
|
||||
|
||||
Inherits basic form fields from AbstractCompensationForm and additional from CompensationResponsibleFormMixin.
|
||||
@@ -31,6 +31,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
|
||||
"title",
|
||||
"conservation_office",
|
||||
"conservation_file_number",
|
||||
"is_pik",
|
||||
"handler_type",
|
||||
"handler_detail",
|
||||
"comment",
|
||||
@@ -58,6 +59,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
|
||||
handler_detail = self.cleaned_data.get("handler_detail", 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
|
||||
@@ -83,6 +85,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
|
||||
created=action,
|
||||
geometry=geometry,
|
||||
comment=comment,
|
||||
is_pik=is_pik,
|
||||
)
|
||||
|
||||
# Add the creating user to the list of shared users
|
||||
@@ -116,6 +119,7 @@ class EditEmaForm(NewEmaForm):
|
||||
"conservation_office": self.instance.responsible.conservation_office,
|
||||
"conservation_file_number": self.instance.responsible.conservation_file_number,
|
||||
"comment": self.instance.comment,
|
||||
"is_pik": self.instance.is_pik,
|
||||
}
|
||||
disabled_fields = []
|
||||
self.load_initial_data(
|
||||
@@ -133,6 +137,7 @@ class EditEmaForm(NewEmaForm):
|
||||
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)
|
||||
@@ -152,6 +157,7 @@ class EditEmaForm(NewEmaForm):
|
||||
self.instance.title = title
|
||||
self.instance.geometry = geometry
|
||||
self.instance.comment = comment
|
||||
self.instance.is_pik = is_pik
|
||||
self.instance.modified = action
|
||||
self.instance.save()
|
||||
|
||||
|
||||
18
ema/migrations/0004_ema_is_pik.py
Normal file
18
ema/migrations/0004_ema_is_pik.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.3 on 2022-05-31 10:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ema', '0003_ema_teams'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ema',
|
||||
name='is_pik',
|
||||
field=models.BooleanField(blank=True, default=False, help_text="Flag if compensation is a 'Produktonsintegrierte Kompensation'", null=True),
|
||||
),
|
||||
]
|
||||
19
ema/migrations/0005_ema_resubmission.py
Normal file
19
ema/migrations/0005_ema_resubmission.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# 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'),
|
||||
('ema', '0004_ema_is_pik'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ema',
|
||||
name='resubmission',
|
||||
field=models.ManyToManyField(blank=True, null=True, related_name='_ema_resubmission_+', to='konova.Resubmission'),
|
||||
),
|
||||
]
|
||||
23
ema/migrations/0006_auto_20220815_0803.py
Normal file
23
ema/migrations/0006_auto_20220815_0803.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# 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'),
|
||||
('ema', '0005_ema_resubmission'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='ema',
|
||||
name='resubmission',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ema',
|
||||
name='resubmissions',
|
||||
field=models.ManyToManyField(blank=True, null=True, related_name='_ema_resubmissions_+', to='konova.Resubmission'),
|
||||
),
|
||||
]
|
||||
19
ema/migrations/0007_auto_20220815_1030.py
Normal file
19
ema/migrations/0007_auto_20220815_1030.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# 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'),
|
||||
('ema', '0006_auto_20220815_0803'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ema',
|
||||
name='resubmissions',
|
||||
field=models.ManyToManyField(blank=True, related_name='_ema_resubmissions_+', to='konova.Resubmission'),
|
||||
),
|
||||
]
|
||||
@@ -13,15 +13,14 @@ from django.db.models import QuerySet
|
||||
from django.http import HttpRequest
|
||||
from django.urls import reverse
|
||||
|
||||
from compensation.models import AbstractCompensation
|
||||
from compensation.models import AbstractCompensation, PikMixin
|
||||
from ema.managers import EmaManager
|
||||
from ema.utils.quality import EmaQualityChecker
|
||||
from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObjectMixin, ShareableObjectMixin
|
||||
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
|
||||
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE
|
||||
|
||||
|
||||
class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin):
|
||||
class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, PikMixin):
|
||||
"""
|
||||
EMA = Ersatzzahlungsmaßnahme
|
||||
(compensation actions from payments)
|
||||
|
||||
@@ -115,7 +115,6 @@ class EmaTable(BaseTable, TableRenderMixin):
|
||||
)
|
||||
return html
|
||||
|
||||
|
||||
def render_r(self, value, record: Ema):
|
||||
""" Renders the registered column for a EMA
|
||||
|
||||
@@ -130,9 +129,7 @@ class EmaTable(BaseTable, TableRenderMixin):
|
||||
recorded = value is not None
|
||||
tooltip = _("Not recorded yet")
|
||||
if recorded:
|
||||
value = value.timestamp
|
||||
value = localtime(value)
|
||||
on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
|
||||
on = value.get_timestamp_str_formatted()
|
||||
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
|
||||
html += self.render_bookmark(
|
||||
tooltip=tooltip,
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
</button>
|
||||
</a>
|
||||
{% if has_access %}
|
||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'ema:resubmission-create' obj.id %}">
|
||||
{% fa5_icon 'bell' %}
|
||||
</button>
|
||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'ema:share-create' obj.id %}">
|
||||
{% fa5_icon 'share-alt' %}
|
||||
</button>
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if not has_finished_deadlines %}
|
||||
<div class="alert alert-danger mb-0">
|
||||
{% trans 'Missing finished deadline ' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card-body scroll-300 p-2">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
|
||||
@@ -56,6 +56,16 @@
|
||||
<th scope="row">{% trans 'Action handler' %}</th>
|
||||
<td class="align-middle">{{obj.responsible.handler|default_if_none:""}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Is PIK' %}</th>
|
||||
<td class="align-middle">
|
||||
{% if obj.is_pik %}
|
||||
{% trans 'Yes' %}
|
||||
{% else %}
|
||||
{% trans 'No' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Last modified' %}</th>
|
||||
<td class="align-middle">
|
||||
@@ -73,13 +83,20 @@
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Shared with' %}</th>
|
||||
<td class="align-middle">
|
||||
{% for team in obj.teams.all %}
|
||||
{% for team in obj.shared_teams %}
|
||||
{% include 'user/includes/team_data_modal_button.html' %}
|
||||
{% endfor %}
|
||||
<hr>
|
||||
{% for user in obj.users.all %}
|
||||
{% include 'user/includes/contact_modal_button.html' %}
|
||||
{% endfor %}
|
||||
{% if has_access %}
|
||||
{% for user in obj.users.all %}
|
||||
{% include 'user/includes/contact_modal_button.html' %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span title="{% trans 'The data must be shared with you, if you want to see which other users have shared access as well.' %}">
|
||||
{% fa5_icon 'eye-slash' %}
|
||||
{{obj.users.count}} {% trans 'other users' %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -87,7 +104,9 @@
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
|
||||
<div class="row">
|
||||
{% include 'map/geom_form.html' %}
|
||||
<div class="col-sm-12">
|
||||
{% include 'map/geom_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/parcels/parcels.html' %}
|
||||
|
||||
@@ -20,6 +20,16 @@
|
||||
<th scope="row">{% trans 'Conservation office file number' %}</th>
|
||||
<td class="align-middle">{{obj.responsible.conservation_file_number|default_if_none:""}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Is PIK' %}</th>
|
||||
<td class="align-middle">
|
||||
{% if obj.is_pik %}
|
||||
{% trans 'Yes' %}
|
||||
{% else %}
|
||||
{% trans 'No' %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Last modified' %}</th>
|
||||
<td class="align-middle">
|
||||
@@ -35,7 +45,9 @@
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
|
||||
<div class="row">
|
||||
{% include 'map/geom_form.html' %}
|
||||
<div class="col-sm-12">
|
||||
{% include 'map/geom_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/parcels/parcels.html' %}
|
||||
|
||||
@@ -41,11 +41,12 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
|
||||
test_id = self.create_dummy_string()
|
||||
test_title = self.create_dummy_string()
|
||||
test_geom = self.create_dummy_geometry()
|
||||
geom_json = self.create_geojson(test_geom)
|
||||
test_conservation_office = self.get_conservation_office_code()
|
||||
post_data = {
|
||||
"identifier": test_id,
|
||||
"title": test_title,
|
||||
"geom": test_geom.geojson,
|
||||
"geom": geom_json,
|
||||
"conservation_office": test_conservation_office.id
|
||||
}
|
||||
self.client_user.post(new_url, post_data)
|
||||
|
||||
@@ -19,6 +19,7 @@ urlpatterns = [
|
||||
path('<id>/remove', remove_view, name='remove'),
|
||||
path('<id>/record', record_view, name='record'),
|
||||
path('<id>/report', report_view, name='report'),
|
||||
path('<id>/resub', create_resubmission_view, name='resubmission-create'),
|
||||
|
||||
path('<id>/state/new', state_new_view, name='new-state'),
|
||||
path('<id>/state/<state_id>/remove', state_remove_view, name='state-remove'),
|
||||
|
||||
31
ema/views.py
31
ema/views.py
@@ -16,8 +16,9 @@ from intervention.forms.modalForms import ShareModalForm
|
||||
from konova.contexts import BaseContext
|
||||
from konova.decorators import conservation_office_group_required, shared_access_required
|
||||
from ema.models import Ema, EmaDocument
|
||||
from konova.forms import RemoveModalForm, SimpleGeomForm, RecordModalForm, RemoveDeadlineModalForm, \
|
||||
EditDocumentModalForm
|
||||
from konova.forms.modals import RemoveModalForm, RecordModalForm, RemoveDeadlineModalForm, \
|
||||
EditDocumentModalForm, ResubmissionModalForm
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.models import Deadline
|
||||
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
@@ -166,6 +167,7 @@ def detail_view(request: HttpRequest, id: str):
|
||||
"is_ets_member": in_group(_user, ETS_GROUP),
|
||||
"LANIS_LINK": ema.get_LANIS_link(),
|
||||
TAB_TITLE_IDENTIFIER: f"{ema.identifier} - {ema.title}",
|
||||
"has_finished_deadlines": ema.get_finished_deadlines().exists(),
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, template, context)
|
||||
@@ -709,4 +711,27 @@ def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str):
|
||||
request,
|
||||
msg_success=DEADLINE_REMOVED,
|
||||
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@conservation_office_group_required
|
||||
@shared_access_required(Ema, "id")
|
||||
def create_resubmission_view(request: HttpRequest, id: str):
|
||||
""" Renders resubmission form for an EMA
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): EMA's id
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
ema = get_object_or_404(Ema, id=id)
|
||||
form = ResubmissionModalForm(request.POST or None, instance=ema, request=request)
|
||||
form.action_url = reverse("ema:resubmission-create", args=(id,))
|
||||
return form.process_request(
|
||||
request,
|
||||
msg_success=_("Resubmission set"),
|
||||
redirect_url=reverse("ema:detail", args=(id,))
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ Created on: 02.12.20
|
||||
from dal import autocomplete
|
||||
from django import forms
|
||||
|
||||
from konova.forms.base_form import BaseForm
|
||||
from konova.utils.message_templates import EDITED_GENERAL_DATA
|
||||
from user.models import User
|
||||
from django.db import transaction
|
||||
@@ -19,7 +20,7 @@ from codelist.settings import CODELIST_PROCESS_TYPE_ID, CODELIST_LAW_ID, \
|
||||
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID
|
||||
from intervention.inputs import GenerateInput
|
||||
from intervention.models import Intervention, Legal, Responsibility, Handler
|
||||
from konova.forms import BaseForm, SimpleGeomForm
|
||||
from konova.forms.geometry_form import SimpleGeomForm
|
||||
from user.models import UserActionLogEntry
|
||||
|
||||
|
||||
@@ -216,6 +217,10 @@ class NewInterventionForm(BaseForm):
|
||||
identifier = tmp_intervention.generate_new_identifier()
|
||||
self.initialize_form_field("identifier", identifier)
|
||||
|
||||
def is_valid(self):
|
||||
super_valid_result = super().is_valid()
|
||||
return super_valid_result
|
||||
|
||||
def save(self, user: User, geom_form: SimpleGeomForm):
|
||||
with transaction.atomic():
|
||||
# Fetch data from cleaned POST values
|
||||
|
||||
@@ -19,7 +19,8 @@ from django.utils.translation import gettext_lazy as _
|
||||
from compensation.models import EcoAccount, EcoAccountDeduction
|
||||
from intervention.inputs import TextToClipboardInput
|
||||
from intervention.models import Intervention, InterventionDocument, RevocationDocument
|
||||
from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm
|
||||
from konova.forms.modals import BaseModalForm
|
||||
from konova.forms.modals import NewDocumentModalForm, RemoveModalForm
|
||||
from konova.utils.general import format_german_float
|
||||
from konova.utils.user_checks import is_default_group_only
|
||||
|
||||
@@ -508,28 +509,44 @@ class EditEcoAccountDeductionModalForm(NewDeductionModalForm):
|
||||
deduction = self.deduction
|
||||
form_account = self.cleaned_data.get("account", None)
|
||||
form_intervention = self.cleaned_data.get("intervention", None)
|
||||
current_account = deduction.account
|
||||
current_intervention = deduction.intervention
|
||||
|
||||
old_account = deduction.account
|
||||
old_intervention = deduction.intervention
|
||||
old_surface = deduction.surface
|
||||
|
||||
# If account or intervention has been changed, we put that change in the logs just as if the deduction has
|
||||
# been removed for this entry. Act as if the deduction is newly created for the new entries
|
||||
if current_account != form_account:
|
||||
current_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED)
|
||||
if old_account != form_account:
|
||||
old_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED)
|
||||
form_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_ADDED)
|
||||
else:
|
||||
current_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED)
|
||||
old_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED)
|
||||
|
||||
if current_intervention != form_intervention:
|
||||
current_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED)
|
||||
if old_intervention != form_intervention:
|
||||
old_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED)
|
||||
form_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_ADDED)
|
||||
else:
|
||||
current_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED)
|
||||
old_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED)
|
||||
|
||||
deduction.account = form_account
|
||||
deduction.intervention = self.cleaned_data.get("intervention", None)
|
||||
deduction.surface = self.cleaned_data.get("surface", None)
|
||||
deduction.save()
|
||||
|
||||
data_changes = {
|
||||
"surface": {
|
||||
"old": old_surface,
|
||||
"new": deduction.surface,
|
||||
},
|
||||
"intervention": {
|
||||
"old": old_intervention.identifier,
|
||||
"new": deduction.intervention.identifier,
|
||||
},
|
||||
"account": {
|
||||
"old": old_account.identifier,
|
||||
"new": deduction.account.identifier,
|
||||
}
|
||||
}
|
||||
old_account.send_notification_mail_on_deduction_change(data_changes)
|
||||
return deduction
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django import forms
|
||||
from codelist.models import KonovaCode
|
||||
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID
|
||||
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID
|
||||
|
||||
|
||||
class DummyFilterInput(forms.HiddenInput):
|
||||
@@ -38,7 +38,17 @@ class TreeCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
|
||||
""" Provides multiple selection of parent-child data
|
||||
|
||||
"""
|
||||
template_name = "konova/widgets/checkbox-tree-select.html"
|
||||
template_name = "konova/widgets/tree/checkbox/checkbox-tree-select.html"
|
||||
|
||||
class meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class TreeRadioSelect(forms.RadioSelect):
|
||||
""" Provides single selection of parent-child data
|
||||
|
||||
"""
|
||||
template_name = "konova/widgets/tree/radio/radio-tree-select.html"
|
||||
|
||||
class meta:
|
||||
abstract = True
|
||||
@@ -68,6 +78,30 @@ class KonovaCodeTreeCheckboxSelectMultiple(TreeCheckboxSelectMultiple):
|
||||
return context
|
||||
|
||||
|
||||
class KonovaCodeTreeRadioSelect(TreeRadioSelect):
|
||||
""" Provides single selection of KonovaCode
|
||||
|
||||
"""
|
||||
filter = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.code_list = kwargs.pop("code_list", None)
|
||||
self.filter = kwargs.pop("filter", {})
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super().get_context(name, value, attrs)
|
||||
codes = KonovaCode.objects.filter(
|
||||
**self.filter,
|
||||
)
|
||||
codes = [
|
||||
parent_code.add_children()
|
||||
for parent_code in codes
|
||||
]
|
||||
context["codes"] = codes
|
||||
return context
|
||||
|
||||
|
||||
class CompensationActionTreeCheckboxSelectMultiple(KonovaCodeTreeCheckboxSelectMultiple):
|
||||
""" Provides multiple selection of CompensationActions
|
||||
|
||||
@@ -79,4 +113,31 @@ class CompensationActionTreeCheckboxSelectMultiple(KonovaCodeTreeCheckboxSelectM
|
||||
self.filter = {
|
||||
"code_lists__in": [CODELIST_COMPENSATION_ACTION_ID],
|
||||
"parent": None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class CompensationStateTreeRadioSelect(KonovaCodeTreeRadioSelect):
|
||||
""" Provides single selection of CompensationState
|
||||
|
||||
"""
|
||||
filter = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.filter = {
|
||||
"code_lists__in": [CODELIST_BIOTOPES_ID],
|
||||
"parent": None,
|
||||
"is_archived": False,
|
||||
}
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super().get_context(name, value, attrs)
|
||||
codes = KonovaCode.objects.filter(
|
||||
**self.filter,
|
||||
)
|
||||
codes = [
|
||||
parent_code.add_children("short_name")
|
||||
for parent_code in codes
|
||||
]
|
||||
context["codes"] = codes
|
||||
return context
|
||||
19
intervention/migrations/0005_intervention_resubmission.py
Normal file
19
intervention/migrations/0005_intervention_resubmission.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# 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'),
|
||||
('intervention', '0004_auto_20220303_0956'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='intervention',
|
||||
name='resubmission',
|
||||
field=models.ManyToManyField(blank=True, null=True, related_name='_intervention_resubmission_+', to='konova.Resubmission'),
|
||||
),
|
||||
]
|
||||
23
intervention/migrations/0006_auto_20220815_0803.py
Normal file
23
intervention/migrations/0006_auto_20220815_0803.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# 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'),
|
||||
('intervention', '0005_intervention_resubmission'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='intervention',
|
||||
name='resubmission',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='intervention',
|
||||
name='resubmissions',
|
||||
field=models.ManyToManyField(blank=True, null=True, related_name='_intervention_resubmissions_+', to='konova.Resubmission'),
|
||||
),
|
||||
]
|
||||
19
intervention/migrations/0007_auto_20220815_1030.py
Normal file
19
intervention/migrations/0007_auto_20220815_1030.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# 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'),
|
||||
('intervention', '0006_auto_20220815_0803'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='intervention',
|
||||
name='resubmissions',
|
||||
field=models.ManyToManyField(blank=True, related_name='_intervention_resubmissions_+', to='konova.Resubmission'),
|
||||
),
|
||||
]
|
||||
@@ -26,14 +26,19 @@ from intervention.models.revocation import RevocationDocument, Revocation
|
||||
from intervention.utils.quality import InterventionQualityChecker
|
||||
from konova.models import generate_document_file_upload_path, AbstractDocument, BaseObject, \
|
||||
ShareableObjectMixin, \
|
||||
RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin
|
||||
from konova.settings import LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT, DEFAULT_SRID_RLP
|
||||
RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin, ResubmitableObjectMixin
|
||||
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE, \
|
||||
PAYMENT_REMOVED, PAYMENT_ADDED, REVOCATION_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE
|
||||
from user.models import UserActionLogEntry
|
||||
|
||||
|
||||
class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin):
|
||||
class Intervention(BaseObject,
|
||||
ShareableObjectMixin,
|
||||
RecordableObjectMixin,
|
||||
CheckableObjectMixin,
|
||||
GeoReferencedMixin,
|
||||
ResubmitableObjectMixin
|
||||
):
|
||||
"""
|
||||
Interventions are e.g. construction sites where nature used to be.
|
||||
"""
|
||||
|
||||
@@ -9,12 +9,11 @@ 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.timezone import localtime
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from intervention.filters import InterventionTableFilter
|
||||
from intervention.models import Intervention
|
||||
from konova.sub_settings.django_settings import DEFAULT_DATE_TIME_FORMAT, DEFAULT_DATE_FORMAT
|
||||
from konova.utils.message_templates import DATA_CHECKED_ON_TEMPLATE, DATA_IS_UNCHECKED, DATA_CHECKED_PREVIOUSLY_TEMPLATE
|
||||
from konova.utils.tables import BaseTable, TableRenderMixin
|
||||
import django_tables2 as tables
|
||||
|
||||
@@ -108,16 +107,21 @@ class InterventionTable(BaseTable, TableRenderMixin):
|
||||
"""
|
||||
html = ""
|
||||
checked = value is not None
|
||||
tooltip = _("Not checked yet")
|
||||
previously_checked = record.get_last_checked_action()
|
||||
tooltip = DATA_IS_UNCHECKED
|
||||
if checked:
|
||||
value = value.timestamp
|
||||
value = localtime(value)
|
||||
checked_on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
|
||||
tooltip = _("Checked on {} by {}").format(checked_on, record.checked.user)
|
||||
checked_on = value.get_timestamp_str_formatted()
|
||||
tooltip = DATA_CHECKED_ON_TEMPLATE.format(checked_on, record.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: Intervention):
|
||||
@@ -156,9 +160,7 @@ class InterventionTable(BaseTable, TableRenderMixin):
|
||||
checked = value is not None
|
||||
tooltip = _("Not recorded yet")
|
||||
if checked:
|
||||
value = value.timestamp
|
||||
value = localtime(value)
|
||||
on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
|
||||
on = value.get_timestamp_str_formatted()
|
||||
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
|
||||
html += self.render_bookmark(
|
||||
tooltip=tooltip,
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
</button>
|
||||
</a>
|
||||
{% if has_access %}
|
||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'intervention:resubmission-create' obj.id %}">
|
||||
{% fa5_icon 'bell' %}
|
||||
</button>
|
||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'intervention:share-create' obj.id %}">
|
||||
{% fa5_icon 'share-alt' %}
|
||||
</button>
|
||||
|
||||
@@ -70,6 +70,11 @@
|
||||
<span>
|
||||
{% fa5_icon 'star' 'far' %}
|
||||
</span>
|
||||
{% if last_checked %}
|
||||
<span class="rlp-gd-inv" title="{{last_checked_tooltip}}">
|
||||
{% fa5_icon 'star' 'fas' %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="check-star" title="{% trans 'Checked on '%} {{obj.checked.timestamp}} {% trans 'by' %} {{obj.checked.user}}">
|
||||
{% fa5_icon 'star' %}
|
||||
@@ -120,13 +125,20 @@
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Shared with' %}</th>
|
||||
<td class="align-middle">
|
||||
{% for team in obj.teams.all %}
|
||||
{% for team in obj.shared_teams %}
|
||||
{% include 'user/includes/team_data_modal_button.html' %}
|
||||
{% endfor %}
|
||||
<hr>
|
||||
{% for user in obj.users.all %}
|
||||
{% include 'user/includes/contact_modal_button.html' %}
|
||||
{% endfor %}
|
||||
{% if has_access %}
|
||||
{% for user in obj.users.all %}
|
||||
{% include 'user/includes/contact_modal_button.html' %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span title="{% trans 'The data must be shared with you, if you want to see which other users have shared access as well.' %}">
|
||||
{% fa5_icon 'eye-slash' %}
|
||||
{{obj.users.count}} {% trans 'other users' %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -134,7 +146,9 @@
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
|
||||
<div class="row">
|
||||
{% include 'map/geom_form.html' %}
|
||||
<div class="col-sm-12">
|
||||
{% include 'map/geom_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/parcels/parcels.html' %}
|
||||
|
||||
@@ -94,7 +94,9 @@
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-6">
|
||||
<div class="row">
|
||||
{% include 'map/geom_form.html' %}
|
||||
<div class="col-sm-12">
|
||||
{% include 'map/geom_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% include 'konova/includes/parcels/parcels.html' %}
|
||||
|
||||
@@ -46,6 +46,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
|
||||
test_id = self.create_dummy_string()
|
||||
test_title = self.create_dummy_string()
|
||||
test_geom = self.create_dummy_geometry()
|
||||
geom_json = self.create_geojson(test_geom)
|
||||
|
||||
new_url = reverse("intervention:new", args=())
|
||||
|
||||
@@ -59,7 +60,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
|
||||
post_data = {
|
||||
"identifier": test_id,
|
||||
"title": test_title,
|
||||
"geom": test_geom.geojson,
|
||||
"geom": geom_json,
|
||||
}
|
||||
response = self.client_user.post(
|
||||
new_url,
|
||||
|
||||
@@ -10,7 +10,8 @@ from django.urls import path
|
||||
from intervention.views import index_view, new_view, detail_view, edit_view, remove_view, new_document_view, share_view, \
|
||||
create_share_view, remove_revocation_view, new_revocation_view, check_view, log_view, new_deduction_view, \
|
||||
record_view, remove_document_view, get_document_view, get_revocation_view, new_id_view, report_view, \
|
||||
remove_deduction_view, remove_compensation_view, edit_deduction_view, edit_revocation_view, edit_document_view
|
||||
remove_deduction_view, remove_compensation_view, edit_deduction_view, edit_revocation_view, edit_document_view, \
|
||||
create_resubmission_view
|
||||
|
||||
app_name = "intervention"
|
||||
urlpatterns = [
|
||||
@@ -26,6 +27,7 @@ urlpatterns = [
|
||||
path('<id>/check', check_view, name='check'),
|
||||
path('<id>/record', record_view, name='record'),
|
||||
path('<id>/report', report_view, name='report'),
|
||||
path('<id>/resub', create_resubmission_view, name='resubmission-create'),
|
||||
|
||||
# Compensations
|
||||
path('<id>/compensation/<comp_id>/remove', remove_compensation_view, name='remove-compensation'),
|
||||
|
||||
@@ -12,14 +12,15 @@ from intervention.models import Intervention, Revocation, InterventionDocument,
|
||||
from intervention.tables import InterventionTable
|
||||
from konova.contexts import BaseContext
|
||||
from konova.decorators import *
|
||||
from konova.forms import SimpleGeomForm, RemoveModalForm, RecordModalForm, EditDocumentModalForm
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.forms.modals import RemoveModalForm, RecordModalForm, EditDocumentModalForm, ResubmissionModalForm
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.utils.documents import remove_document, get_document
|
||||
from konova.utils.generators import generate_qr_code
|
||||
from konova.utils.message_templates import INTERVENTION_INVALID, FORM_INVALID, IDENTIFIER_REPLACED, \
|
||||
CHECKED_RECORDED_RESET, DEDUCTION_REMOVED, DEDUCTION_ADDED, REVOCATION_ADDED, REVOCATION_REMOVED, \
|
||||
COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, DEDUCTION_EDITED, REVOCATION_EDITED, DOCUMENT_EDITED, \
|
||||
RECORDED_BLOCKS_EDIT
|
||||
RECORDED_BLOCKS_EDIT, DATA_CHECKED_PREVIOUSLY_TEMPLATE
|
||||
from konova.utils.user_checks import in_group
|
||||
|
||||
|
||||
@@ -265,15 +266,18 @@ def detail_view(request: HttpRequest, id: str):
|
||||
geom_form = SimpleGeomForm(
|
||||
instance=intervention,
|
||||
)
|
||||
|
||||
parcels = intervention.get_underlying_parcels()
|
||||
last_checked = intervention.get_last_checked_action()
|
||||
last_checked_tooltip = ""
|
||||
if last_checked:
|
||||
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user)
|
||||
|
||||
context = {
|
||||
"obj": intervention,
|
||||
"last_checked": last_checked,
|
||||
"last_checked_tooltip": last_checked_tooltip,
|
||||
"compensations": compensations,
|
||||
"has_access": is_data_shared,
|
||||
"geom_form": geom_form,
|
||||
"parcels": parcels,
|
||||
"is_default_member": in_group(_user, DEFAULT_GROUP),
|
||||
"is_zb_member": in_group(_user, ZB_GROUP),
|
||||
"is_ets_member": in_group(_user, ETS_GROUP),
|
||||
@@ -472,6 +476,29 @@ def create_share_view(request: HttpRequest, id: str):
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@default_group_required
|
||||
@shared_access_required(Intervention, "id")
|
||||
def create_resubmission_view(request: HttpRequest, id: str):
|
||||
""" Renders resubmission form for an intervention
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): Intervention's id
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
intervention = get_object_or_404(Intervention, id=id)
|
||||
form = ResubmissionModalForm(request.POST or None, instance=intervention, request=request)
|
||||
form.action_url = reverse("intervention:resubmission-create", args=(id,))
|
||||
return form.process_request(
|
||||
request,
|
||||
msg_success=_("Resubmission set"),
|
||||
redirect_url=reverse("intervention:detail", args=(id,))
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@registration_office_group_required
|
||||
@shared_access_required(Intervention, "id")
|
||||
|
||||
@@ -7,7 +7,7 @@ Created on: 22.07.21
|
||||
"""
|
||||
from django.contrib import admin
|
||||
|
||||
from konova.models import Geometry, Deadline, GeometryConflict, Parcel, District, Municipal, ParcelGroup
|
||||
from konova.models import Geometry, Deadline, GeometryConflict, Parcel, District, Municipal, ParcelGroup, Resubmission
|
||||
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
|
||||
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
|
||||
from user.models import UserAction
|
||||
@@ -98,6 +98,18 @@ class DeadlineAdmin(admin.ModelAdmin):
|
||||
]
|
||||
|
||||
|
||||
class DeletableObjectMixinAdmin(admin.ModelAdmin):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def restore_deleted_data(self, request, queryset):
|
||||
queryset = queryset.filter(
|
||||
deleted__isnull=False
|
||||
)
|
||||
for entry in queryset:
|
||||
entry.deleted.delete()
|
||||
|
||||
|
||||
class BaseResourceAdmin(admin.ModelAdmin):
|
||||
fields = [
|
||||
"created",
|
||||
@@ -109,7 +121,7 @@ class BaseResourceAdmin(admin.ModelAdmin):
|
||||
]
|
||||
|
||||
|
||||
class BaseObjectAdmin(BaseResourceAdmin):
|
||||
class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin):
|
||||
search_fields = [
|
||||
"identifier",
|
||||
"title",
|
||||
@@ -126,13 +138,16 @@ class BaseObjectAdmin(BaseResourceAdmin):
|
||||
"deleted",
|
||||
]
|
||||
|
||||
def restore_deleted_data(self, request, queryset):
|
||||
queryset = queryset.filter(
|
||||
deleted__isnull=False
|
||||
)
|
||||
for entry in queryset:
|
||||
entry.deleted.delete()
|
||||
|
||||
class ResubmissionAdmin(BaseResourceAdmin):
|
||||
list_display = [
|
||||
"resubmit_on"
|
||||
]
|
||||
fields = [
|
||||
"comment",
|
||||
"resubmit_on",
|
||||
"resubmission_sent",
|
||||
]
|
||||
|
||||
|
||||
# Outcommented for a cleaner admin backend on production
|
||||
@@ -143,3 +158,4 @@ class BaseObjectAdmin(BaseResourceAdmin):
|
||||
#admin.site.register(ParcelGroup, ParcelGroupAdmin)
|
||||
#admin.site.register(GeometryConflict, GeometryConflictAdmin)
|
||||
#admin.site.register(Deadline, DeadlineAdmin)
|
||||
#admin.site.register(Resubmission, ResubmissionAdmin)
|
||||
|
||||
@@ -96,7 +96,9 @@ class ShareTeamAutocomplete(Select2QuerySetView):
|
||||
def get_queryset(self):
|
||||
if self.request.user.is_anonymous:
|
||||
return Team.objects.none()
|
||||
qs = Team.objects.all()
|
||||
qs = Team.objects.filter(
|
||||
deleted__isnull=True
|
||||
)
|
||||
if self.q:
|
||||
# Due to privacy concerns only a full username match will return the proper user entry
|
||||
qs = qs.filter(
|
||||
@@ -108,6 +110,29 @@ class ShareTeamAutocomplete(Select2QuerySetView):
|
||||
return qs
|
||||
|
||||
|
||||
class TeamAdminAutocomplete(Select2QuerySetView):
|
||||
""" Autocomplete for share with teams
|
||||
|
||||
"""
|
||||
def get_queryset(self):
|
||||
if self.request.user.is_anonymous:
|
||||
return User.objects.none()
|
||||
qs = User.objects.filter(
|
||||
id__in=self.forwarded.get("members", [])
|
||||
).exclude(
|
||||
id__in=self.forwarded.get("admins", [])
|
||||
)
|
||||
if self.q:
|
||||
# Due to privacy concerns only a full username match will return the proper user entry
|
||||
qs = qs.filter(
|
||||
name__icontains=self.q
|
||||
)
|
||||
qs = qs.order_by(
|
||||
"username"
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class KonovaCodeAutocomplete(Select2GroupQuerySetView):
|
||||
"""
|
||||
Provides simple autocomplete functionality for codes
|
||||
|
||||
@@ -7,7 +7,8 @@ Created on: 16.11.20
|
||||
"""
|
||||
from django.http import HttpRequest
|
||||
|
||||
from konova.sub_settings.context_settings import BASE_TITLE, HELP_LINK, BASE_FRONTEND_TITLE, TAB_TITLE_IDENTIFIER
|
||||
from konova.sub_settings.context_settings import BASE_TITLE, HELP_LINK, BASE_FRONTEND_TITLE, TAB_TITLE_IDENTIFIER, \
|
||||
IMPRESSUM_LINK
|
||||
from konova.sub_settings.django_settings import EMAIL_REPLY_TO
|
||||
|
||||
|
||||
@@ -25,6 +26,7 @@ class BaseContext:
|
||||
"user": request.user,
|
||||
"current_role": None,
|
||||
"help_link": HELP_LINK,
|
||||
"impressum_link": IMPRESSUM_LINK,
|
||||
"CONTACT_MAIL": EMAIL_REPLY_TO,
|
||||
}
|
||||
|
||||
|
||||
@@ -305,7 +305,7 @@ class ShareableTableFilterMixin(django_filters.FilterSet):
|
||||
if not value:
|
||||
return queryset.filter(
|
||||
Q(users__in=[self.user]) | # requesting user has access
|
||||
Q(teams__users__in=[self.user])
|
||||
Q(teams__in=self.user.shared_teams)
|
||||
).distinct()
|
||||
else:
|
||||
return queryset
|
||||
|
||||
641
konova/forms.py
641
konova/forms.py
@@ -1,641 +0,0 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 16.11.20
|
||||
|
||||
"""
|
||||
|
||||
from abc import abstractmethod
|
||||
|
||||
from bootstrap_modal_forms.forms import BSModalForm
|
||||
from bootstrap_modal_forms.utils import is_ajax
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.db.models.fields.files import FieldFile
|
||||
|
||||
from user.models import User
|
||||
from django.contrib.gis.forms import OSMWidget, MultiPolygonField
|
||||
from django.contrib.gis.geos import MultiPolygon
|
||||
from django.db import transaction
|
||||
from django.http import HttpRequest, HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from konova.contexts import BaseContext
|
||||
from konova.models import BaseObject, Geometry, RecordableObjectMixin, AbstractDocument
|
||||
from konova.settings import DEFAULT_SRID
|
||||
from konova.tasks import celery_update_parcels
|
||||
from konova.utils.message_templates import FORM_INVALID, FILE_TYPE_UNSUPPORTED, FILE_SIZE_TOO_LARGE, DOCUMENT_EDITED
|
||||
from user.models import UserActionLogEntry
|
||||
|
||||
|
||||
class BaseForm(forms.Form):
|
||||
"""
|
||||
Basic form for that holds attributes needed in all other forms
|
||||
"""
|
||||
template = None
|
||||
action_url = None
|
||||
action_btn_label = _("Save")
|
||||
form_title = None
|
||||
cancel_redirect = None
|
||||
form_caption = None
|
||||
instance = None # The data holding model object
|
||||
request = None
|
||||
form_attrs = {} # Holds additional attributes, that can be used in the template
|
||||
has_required_fields = False # Automatically set. Triggers hint rendering in templates
|
||||
show_cancel_btn = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.instance = kwargs.pop("instance", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.request is not None:
|
||||
self.user = self.request.user
|
||||
# Check for required fields
|
||||
for _field_name, _field_val in self.fields.items():
|
||||
if _field_val.required:
|
||||
self.has_required_fields = True
|
||||
break
|
||||
|
||||
self.check_for_recorded_instance()
|
||||
|
||||
@abstractmethod
|
||||
def save(self):
|
||||
# To be implemented in subclasses!
|
||||
pass
|
||||
|
||||
def disable_form_field(self, field: str):
|
||||
"""
|
||||
Disables a form field for user editing
|
||||
"""
|
||||
self.fields[field].widget.attrs["readonly"] = True
|
||||
self.fields[field].disabled = True
|
||||
self.fields[field].widget.attrs["title"] = _("Not editable")
|
||||
|
||||
def initialize_form_field(self, field: str, val):
|
||||
"""
|
||||
Initializes a form field with a value
|
||||
"""
|
||||
self.fields[field].initial = val
|
||||
|
||||
def add_placeholder_for_field(self, field: str, val):
|
||||
"""
|
||||
Adds a placeholder to a field after initialization without the need to redefine the form widget
|
||||
|
||||
Args:
|
||||
field (str): Field name
|
||||
val (str): Placeholder
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self.fields[field].widget.attrs["placeholder"] = val
|
||||
|
||||
def load_initial_data(self, form_data: dict, disabled_fields: list = None):
|
||||
""" Initializes form data from instance
|
||||
|
||||
Inserts instance data into form and disables form fields
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if self.instance is None:
|
||||
return
|
||||
for k, v in form_data.items():
|
||||
self.initialize_form_field(k, v)
|
||||
if disabled_fields:
|
||||
for field in disabled_fields:
|
||||
self.disable_form_field(field)
|
||||
|
||||
def add_widget_html_class(self, field: str, cls: str):
|
||||
""" Adds a HTML class string to the widget of a field
|
||||
|
||||
Args:
|
||||
field (str): The field's name
|
||||
cls (str): The new class string
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
set_class = self.fields[field].widget.attrs.get("class", "")
|
||||
if cls in set_class:
|
||||
return
|
||||
else:
|
||||
set_class += " " + cls
|
||||
self.fields[field].widget.attrs["class"] = set_class
|
||||
|
||||
def remove_widget_html_class(self, field: str, cls: str):
|
||||
""" Removes a HTML class string from the widget of a field
|
||||
|
||||
Args:
|
||||
field (str): The field's name
|
||||
cls (str): The new class string
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
set_class = self.fields[field].widget.attrs.get("class", "")
|
||||
set_class = set_class.replace(cls, "")
|
||||
self.fields[field].widget.attrs["class"] = set_class
|
||||
|
||||
def check_for_recorded_instance(self):
|
||||
""" Checks if the instance is recorded and runs some special logic if yes
|
||||
|
||||
If the instance is recorded, the form shall not display any possibility to
|
||||
edit any data. Instead, the users should get some information about why they can not edit anything.
|
||||
|
||||
There are situations where the form should be rendered regularly,
|
||||
e.g deduction forms for (recorded) eco accounts.
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
from intervention.forms.modalForms import NewDeductionModalForm, EditEcoAccountDeductionModalForm, \
|
||||
RemoveEcoAccountDeductionModalForm
|
||||
is_none = self.instance is None
|
||||
is_other_data_type = not isinstance(self.instance, BaseObject)
|
||||
is_deduction_form = isinstance(
|
||||
self,
|
||||
(
|
||||
NewDeductionModalForm,
|
||||
EditEcoAccountDeductionModalForm,
|
||||
RemoveEcoAccountDeductionModalForm,
|
||||
)
|
||||
)
|
||||
|
||||
if is_none or is_other_data_type or is_deduction_form:
|
||||
# Do nothing
|
||||
return
|
||||
|
||||
if self.instance.is_recorded:
|
||||
self.template = "form/recorded_no_edit.html"
|
||||
|
||||
|
||||
class RemoveForm(BaseForm):
|
||||
check = forms.BooleanField(
|
||||
label=_("Confirm"),
|
||||
label_suffix=_(""),
|
||||
required=True,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.object_to_remove = kwargs.pop("object_to_remove", None)
|
||||
self.remove_post_url = kwargs.pop("remove_post_url", "")
|
||||
self.cancel_url = kwargs.pop("cancel_url", "")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.form_title = _("Remove")
|
||||
if self.object_to_remove is not None:
|
||||
self.form_caption = _("You are about to remove {} {}").format(self.object_to_remove.__class__.__name__, self.object_to_remove)
|
||||
self.action_url = self.remove_post_url
|
||||
self.cancel_redirect = self.cancel_url
|
||||
|
||||
def is_checked(self) -> bool:
|
||||
return self.cleaned_data.get("check", False)
|
||||
|
||||
def save(self, user: User):
|
||||
""" Perform generic removing by running the form typical 'save()' method
|
||||
|
||||
Args:
|
||||
user (User): The performing user
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if self.object_to_remove is not None and self.is_checked():
|
||||
with transaction.atomic():
|
||||
self.object_to_remove.is_active = False
|
||||
action = UserActionLogEntry.get_deleted_action(user)
|
||||
self.object_to_remove.deleted = action
|
||||
self.object_to_remove.save()
|
||||
return self.object_to_remove
|
||||
|
||||
|
||||
class BaseModalForm(BaseForm, BSModalForm):
|
||||
""" A specialzed form class for modal form handling
|
||||
|
||||
"""
|
||||
is_modal_form = True
|
||||
render_submit = True
|
||||
template = "modal/modal_form.html"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.action_btn_label = _("Continue")
|
||||
|
||||
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
|
||||
|
||||
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():
|
||||
if not is_ajax(request.META):
|
||||
# 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.
|
||||
self.save()
|
||||
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 SimpleGeomForm(BaseForm):
|
||||
""" A geometry form for rendering geometry read-only using a widget
|
||||
|
||||
"""
|
||||
geom = MultiPolygonField(
|
||||
srid=DEFAULT_SRID,
|
||||
label=_("Geometry"),
|
||||
help_text=_(""),
|
||||
label_suffix="",
|
||||
required=False,
|
||||
disabled=False,
|
||||
widget=OSMWidget(
|
||||
attrs={
|
||||
"map_width": 600,
|
||||
"map_height": 400,
|
||||
# default_zoom defines the nearest possible zoom level from which the JS automatically
|
||||
# zooms out if geometry requires a larger view port. So define a larger range for smaller geometries
|
||||
"default_zoom": 25,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
read_only = kwargs.pop("read_only", True)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Initialize geometry
|
||||
try:
|
||||
geom = self.instance.geometry.geom
|
||||
self.empty = geom.empty
|
||||
except AttributeError:
|
||||
# If no geometry exists for this form, we simply set the value to None and zoom to the maximum level
|
||||
geom = None
|
||||
self.empty = True
|
||||
self.fields["geom"].widget.attrs["default_zoom"] = 1
|
||||
|
||||
self.initialize_form_field("geom", geom)
|
||||
if read_only:
|
||||
self.fields["geom"].disabled = True
|
||||
|
||||
def save(self, action: UserActionLogEntry):
|
||||
""" Saves the form's geometry
|
||||
|
||||
Creates a new geometry entry if none is set, yet
|
||||
|
||||
Args:
|
||||
action ():
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
try:
|
||||
if self.instance is None or self.instance.geometry is None:
|
||||
raise LookupError
|
||||
geometry = self.instance.geometry
|
||||
geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID))
|
||||
geometry.modified = action
|
||||
|
||||
geometry.save()
|
||||
except LookupError:
|
||||
# No geometry or linked instance holding a geometry exist --> create a new one!
|
||||
geometry = Geometry.objects.create(
|
||||
geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID)),
|
||||
created=action,
|
||||
)
|
||||
# Start the parcel update procedure in a background process
|
||||
celery_update_parcels.delay(geometry.id)
|
||||
return geometry
|
||||
|
||||
|
||||
class RemoveModalForm(BaseModalForm):
|
||||
""" Generic removing modal form
|
||||
|
||||
Can be used for anything, where removing shall be confirmed by the user a second time.
|
||||
|
||||
"""
|
||||
confirm = forms.BooleanField(
|
||||
label=_("Confirm"),
|
||||
label_suffix=_(""),
|
||||
widget=forms.CheckboxInput(),
|
||||
required=True,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.template = "modal/modal_form.html"
|
||||
super().__init__(*args, **kwargs)
|
||||
self.form_title = _("Remove")
|
||||
self.form_caption = _("Are you sure?")
|
||||
# Disable automatic w-100 setting for this type of modal form. Looks kinda strange
|
||||
self.fields["confirm"].widget.attrs["class"] = ""
|
||||
|
||||
def save(self):
|
||||
if isinstance(self.instance, BaseObject):
|
||||
self.instance.mark_as_deleted(self.user)
|
||||
else:
|
||||
# If the class does not provide restorable delete functionality, we must delete the entry finally
|
||||
self.instance.delete()
|
||||
|
||||
|
||||
class RemoveDeadlineModalForm(RemoveModalForm):
|
||||
""" Removing modal form for deadlines
|
||||
|
||||
Can be used for anything, where removing shall be confirmed by the user a second time.
|
||||
|
||||
"""
|
||||
deadline = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
deadline = kwargs.pop("deadline", None)
|
||||
self.deadline = deadline
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def save(self):
|
||||
self.instance.remove_deadline(self)
|
||||
|
||||
|
||||
class NewDocumentModalForm(BaseModalForm):
|
||||
""" Modal form for new documents
|
||||
|
||||
"""
|
||||
title = forms.CharField(
|
||||
label=_("Title"),
|
||||
label_suffix=_(""),
|
||||
max_length=500,
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
}
|
||||
)
|
||||
)
|
||||
creation_date = forms.DateField(
|
||||
label=_("Created on"),
|
||||
label_suffix=_(""),
|
||||
help_text=_("When has this file been created? Important for photos."),
|
||||
widget=forms.DateInput(
|
||||
attrs={
|
||||
"type": "date",
|
||||
"data-provide": "datepicker",
|
||||
"class": "form-control",
|
||||
},
|
||||
format="%d.%m.%Y"
|
||||
)
|
||||
)
|
||||
file = forms.FileField(
|
||||
label=_("File"),
|
||||
label_suffix=_(""),
|
||||
help_text=_("Allowed formats: pdf, jpg, png. Max size 15 MB."),
|
||||
widget=forms.FileInput(
|
||||
attrs={
|
||||
"class": "form-control-file",
|
||||
}
|
||||
),
|
||||
)
|
||||
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",
|
||||
}
|
||||
)
|
||||
)
|
||||
document_model = None
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.form_title = _("Add new document")
|
||||
self.form_caption = _("")
|
||||
self.form_attrs = {
|
||||
"enctype": "multipart/form-data", # important for file upload
|
||||
}
|
||||
if not self.document_model:
|
||||
raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__))
|
||||
|
||||
def is_valid(self):
|
||||
super_valid = super().is_valid()
|
||||
|
||||
_file = self.cleaned_data.get("file", None)
|
||||
|
||||
if _file is None or isinstance(_file, FieldFile):
|
||||
# FieldFile declares that no new file has been uploaded and we do not need to check on the file again
|
||||
return super_valid
|
||||
|
||||
mime_type_valid = self.document_model.is_mime_type_valid(_file)
|
||||
if not mime_type_valid:
|
||||
self.add_error(
|
||||
"file",
|
||||
FILE_TYPE_UNSUPPORTED
|
||||
)
|
||||
|
||||
file_size_valid = self.document_model.is_file_size_valid(_file)
|
||||
if not file_size_valid:
|
||||
self.add_error(
|
||||
"file",
|
||||
FILE_SIZE_TOO_LARGE
|
||||
)
|
||||
|
||||
file_valid = mime_type_valid and file_size_valid
|
||||
return super_valid and file_valid
|
||||
|
||||
def save(self):
|
||||
with transaction.atomic():
|
||||
action = UserActionLogEntry.get_created_action(self.user)
|
||||
edited_action = UserActionLogEntry.get_edited_action(self.user, _("Added document"))
|
||||
|
||||
doc = self.document_model.objects.create(
|
||||
created=action,
|
||||
title=self.cleaned_data["title"],
|
||||
comment=self.cleaned_data["comment"],
|
||||
file=self.cleaned_data["file"],
|
||||
date_of_creation=self.cleaned_data["creation_date"],
|
||||
instance=self.instance,
|
||||
)
|
||||
|
||||
self.instance.log.add(edited_action)
|
||||
self.instance.modified = edited_action
|
||||
self.instance.save()
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
class EditDocumentModalForm(NewDocumentModalForm):
|
||||
document = None
|
||||
document_model = AbstractDocument
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.document = kwargs.pop("document", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.form_title = _("Edit document")
|
||||
form_data = {
|
||||
"title": self.document.title,
|
||||
"comment": self.document.comment,
|
||||
"creation_date": str(self.document.date_of_creation),
|
||||
"file": self.document.file,
|
||||
}
|
||||
self.load_initial_data(form_data)
|
||||
|
||||
|
||||
def save(self):
|
||||
with transaction.atomic():
|
||||
document = self.document
|
||||
file = self.cleaned_data.get("file", None)
|
||||
|
||||
document.title = self.cleaned_data.get("title", None)
|
||||
document.comment = self.cleaned_data.get("comment", None)
|
||||
document.date_of_creation = self.cleaned_data.get("creation_date", None)
|
||||
if not isinstance(file, FieldFile):
|
||||
document.replace_file(file)
|
||||
document.save()
|
||||
|
||||
self.instance.mark_as_edited(self.user, self.request, edit_comment=DOCUMENT_EDITED)
|
||||
|
||||
return document
|
||||
|
||||
|
||||
class RecordModalForm(BaseModalForm):
|
||||
""" Modal form for recording data
|
||||
|
||||
"""
|
||||
confirm = forms.BooleanField(
|
||||
label=_("Confirm record"),
|
||||
label_suffix="",
|
||||
widget=forms.CheckboxInput(),
|
||||
required=True,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.form_title = _("Record data")
|
||||
self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name)
|
||||
# Disable automatic w-100 setting for this type of modal form. Looks kinda strange
|
||||
self.fields["confirm"].widget.attrs["class"] = ""
|
||||
|
||||
if self.instance.recorded:
|
||||
# unrecord!
|
||||
self.fields["confirm"].label = _("Confirm unrecord")
|
||||
self.form_title = _("Unrecord data")
|
||||
self.form_caption = _("I, {} {}, confirm that this data must be unrecorded.").format(self.user.first_name, self.user.last_name)
|
||||
|
||||
if not isinstance(self.instance, RecordableObjectMixin):
|
||||
raise NotImplementedError
|
||||
|
||||
def is_valid(self):
|
||||
""" Checks for instance's validity and data quality
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
from intervention.models import Intervention
|
||||
super_val = super().is_valid()
|
||||
if self.instance.recorded:
|
||||
# If user wants to unrecord an already recorded dataset, we do not need to perform custom checks
|
||||
return super_val
|
||||
checker = self.instance.quality_check()
|
||||
for msg in checker.messages:
|
||||
self.add_error(
|
||||
"confirm",
|
||||
msg
|
||||
)
|
||||
valid = checker.valid
|
||||
# Special case: Intervention
|
||||
# Add direct checks for related compensations
|
||||
if isinstance(self.instance, Intervention):
|
||||
comps_valid = self._are_compensations_valid()
|
||||
valid = valid and comps_valid
|
||||
return super_val and valid
|
||||
|
||||
def _are_deductions_valid(self):
|
||||
""" Performs validity checks on deductions and their eco-account
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
deductions = self.instance.deductions.all()
|
||||
for deduction in deductions:
|
||||
checker = deduction.account.quality_check()
|
||||
for msg in checker.messages:
|
||||
self.add_error(
|
||||
"confirm",
|
||||
f"{deduction.account.identifier}: {msg}"
|
||||
)
|
||||
return checker.valid
|
||||
return True
|
||||
|
||||
def _are_compensations_valid(self):
|
||||
""" Runs a special case for intervention-compensations validity
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
comps = self.instance.compensations.filter(
|
||||
deleted=None,
|
||||
)
|
||||
comps_valid = True
|
||||
for comp in comps:
|
||||
checker = comp.quality_check()
|
||||
comps_valid = comps_valid and checker.valid
|
||||
for msg in checker.messages:
|
||||
self.add_error(
|
||||
"confirm",
|
||||
f"{comp.identifier}: {msg}"
|
||||
)
|
||||
|
||||
deductions_valid = self._are_deductions_valid()
|
||||
|
||||
return comps_valid and deductions_valid
|
||||
|
||||
def save(self):
|
||||
with transaction.atomic():
|
||||
if self.cleaned_data["confirm"]:
|
||||
if self.instance.recorded:
|
||||
self.instance.set_unrecorded(self.user)
|
||||
else:
|
||||
self.instance.set_recorded(self.user)
|
||||
return self.instance
|
||||
|
||||
def check_for_recorded_instance(self):
|
||||
""" Overwrite the check method for doing nothing on the RecordModalForm
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
pass
|
||||
11
konova/forms/__init__.py
Normal file
11
konova/forms/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 15.08.22
|
||||
|
||||
"""
|
||||
|
||||
from .base_form import *
|
||||
from .geometry_form import *
|
||||
from .remove_form import *
|
||||
157
konova/forms/base_form.py
Normal file
157
konova/forms/base_form.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 15.08.22
|
||||
|
||||
"""
|
||||
from abc import abstractmethod
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from compensation.models import EcoAccount
|
||||
from konova.models import BaseObject
|
||||
|
||||
|
||||
class BaseForm(forms.Form):
|
||||
"""
|
||||
Basic form for that holds attributes needed in all other forms
|
||||
"""
|
||||
template = None
|
||||
action_url = None
|
||||
action_btn_label = _("Save")
|
||||
form_title = None
|
||||
cancel_redirect = None
|
||||
form_caption = None
|
||||
instance = None # The data holding model object
|
||||
request = None
|
||||
form_attrs = {} # Holds additional attributes, that can be used in the template
|
||||
has_required_fields = False # Automatically set. Triggers hint rendering in templates
|
||||
show_cancel_btn = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.instance = kwargs.pop("instance", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.request is not None:
|
||||
self.user = self.request.user
|
||||
# Check for required fields
|
||||
for _field_name, _field_val in self.fields.items():
|
||||
if _field_val.required:
|
||||
self.has_required_fields = True
|
||||
break
|
||||
|
||||
self.check_for_recorded_instance()
|
||||
|
||||
@abstractmethod
|
||||
def save(self):
|
||||
# To be implemented in subclasses!
|
||||
pass
|
||||
|
||||
def disable_form_field(self, field: str):
|
||||
"""
|
||||
Disables a form field for user editing
|
||||
"""
|
||||
self.fields[field].widget.attrs["readonly"] = True
|
||||
self.fields[field].disabled = True
|
||||
self.fields[field].widget.attrs["title"] = _("Not editable")
|
||||
|
||||
def initialize_form_field(self, field: str, val):
|
||||
"""
|
||||
Initializes a form field with a value
|
||||
"""
|
||||
self.fields[field].initial = val
|
||||
|
||||
def add_placeholder_for_field(self, field: str, val):
|
||||
"""
|
||||
Adds a placeholder to a field after initialization without the need to redefine the form widget
|
||||
|
||||
Args:
|
||||
field (str): Field name
|
||||
val (str): Placeholder
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self.fields[field].widget.attrs["placeholder"] = val
|
||||
|
||||
def load_initial_data(self, form_data: dict, disabled_fields: list = None):
|
||||
""" Initializes form data from instance
|
||||
|
||||
Inserts instance data into form and disables form fields
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if self.instance is None:
|
||||
return
|
||||
for k, v in form_data.items():
|
||||
self.initialize_form_field(k, v)
|
||||
if disabled_fields:
|
||||
for field in disabled_fields:
|
||||
self.disable_form_field(field)
|
||||
|
||||
def add_widget_html_class(self, field: str, cls: str):
|
||||
""" Adds a HTML class string to the widget of a field
|
||||
|
||||
Args:
|
||||
field (str): The field's name
|
||||
cls (str): The new class string
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
set_class = self.fields[field].widget.attrs.get("class", "")
|
||||
if cls in set_class:
|
||||
return
|
||||
else:
|
||||
set_class += " " + cls
|
||||
self.fields[field].widget.attrs["class"] = set_class
|
||||
|
||||
def remove_widget_html_class(self, field: str, cls: str):
|
||||
""" Removes a HTML class string from the widget of a field
|
||||
|
||||
Args:
|
||||
field (str): The field's name
|
||||
cls (str): The new class string
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
set_class = self.fields[field].widget.attrs.get("class", "")
|
||||
set_class = set_class.replace(cls, "")
|
||||
self.fields[field].widget.attrs["class"] = set_class
|
||||
|
||||
def check_for_recorded_instance(self):
|
||||
""" Checks if the instance is recorded and runs some special logic if yes
|
||||
|
||||
If the instance is recorded, the form shall not display any possibility to
|
||||
edit any data. Instead, the users should get some information about why they can not edit anything.
|
||||
|
||||
There are situations where the form should be rendered regularly,
|
||||
e.g deduction forms for (recorded) eco accounts.
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
from intervention.forms.modalForms import NewDeductionModalForm, EditEcoAccountDeductionModalForm, \
|
||||
RemoveEcoAccountDeductionModalForm
|
||||
from konova.forms.modals.resubmission_form import ResubmissionModalForm
|
||||
is_none = self.instance is None
|
||||
is_other_data_type = not isinstance(self.instance, BaseObject)
|
||||
is_deduction_form_from_account = isinstance(
|
||||
self,
|
||||
(
|
||||
NewDeductionModalForm,
|
||||
ResubmissionModalForm,
|
||||
EditEcoAccountDeductionModalForm,
|
||||
RemoveEcoAccountDeductionModalForm,
|
||||
)
|
||||
) and isinstance(self.instance, EcoAccount)
|
||||
|
||||
if is_none or is_other_data_type or is_deduction_form_from_account:
|
||||
# Do nothing
|
||||
return
|
||||
|
||||
if self.instance.is_recorded:
|
||||
self.template = "form/recorded_no_edit.html"
|
||||
133
konova/forms/geometry_form.py
Normal file
133
konova/forms/geometry_form.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 15.08.22
|
||||
|
||||
"""
|
||||
import json
|
||||
|
||||
from django.contrib.gis import gdal
|
||||
from django.contrib.gis.forms import MultiPolygonField
|
||||
from django.contrib.gis.geos import MultiPolygon, Polygon
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from konova.forms.base_form import BaseForm
|
||||
from konova.models import Geometry
|
||||
from konova.tasks import celery_update_parcels
|
||||
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
|
||||
from user.models import UserActionLogEntry
|
||||
|
||||
|
||||
class SimpleGeomForm(BaseForm):
|
||||
""" A geometry form for rendering geometry read-only using a widget
|
||||
|
||||
"""
|
||||
read_only = True
|
||||
geom = MultiPolygonField(
|
||||
srid=DEFAULT_SRID_RLP,
|
||||
label=_("Geometry"),
|
||||
help_text=_(""),
|
||||
label_suffix="",
|
||||
required=False,
|
||||
disabled=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.read_only = kwargs.pop("read_only", True)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Initialize geometry
|
||||
try:
|
||||
geom = self.instance.geometry.geom
|
||||
self.empty = geom.empty
|
||||
|
||||
if self.empty:
|
||||
raise AttributeError
|
||||
|
||||
geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP)
|
||||
geom = json.dumps(geojson)
|
||||
except AttributeError:
|
||||
# If no geometry exists for this form, we simply set the value to None and zoom to the maximum level
|
||||
geom = ""
|
||||
self.empty = True
|
||||
|
||||
self.initialize_form_field("geom", geom)
|
||||
|
||||
def is_valid(self):
|
||||
super().is_valid()
|
||||
is_valid = True
|
||||
|
||||
# Get geojson from form
|
||||
geom = self.data["geom"]
|
||||
if geom is None or len(geom) == 0:
|
||||
# empty geometry is a valid geometry
|
||||
return is_valid
|
||||
geom = json.loads(geom)
|
||||
|
||||
# Write submitted data back into form field to make sure invalid geometry
|
||||
# will be rendered again on failed submit
|
||||
self.initialize_form_field("geom", self.data["geom"])
|
||||
|
||||
# Read geojson into gdal geometry
|
||||
# HINT: This can be simplified if the geojson format holds data in epsg:4326 (GDAL provides direct creation for
|
||||
# this case)
|
||||
features = []
|
||||
features_json = geom.get("features", [])
|
||||
for feature in features_json:
|
||||
g = gdal.OGRGeometry(json.dumps(feature.get("geometry", feature)), srs=DEFAULT_SRID_RLP)
|
||||
if g.geom_type not in ["Polygon", "MultiPolygon"]:
|
||||
self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered."))
|
||||
is_valid = False
|
||||
return is_valid
|
||||
|
||||
polygon = Polygon.from_ewkt(g.ewkt)
|
||||
is_valid = polygon.valid
|
||||
if not is_valid:
|
||||
self.add_error("geom", polygon.valid_reason)
|
||||
return is_valid
|
||||
|
||||
features.append(polygon)
|
||||
form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
|
||||
for feature in features:
|
||||
form_geom = form_geom.union(feature)
|
||||
|
||||
# Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
|
||||
if form_geom.geom_type != "MultiPolygon":
|
||||
form_geom = MultiPolygon(form_geom, srid=DEFAULT_SRID_RLP)
|
||||
|
||||
# Write unioned Multipolygon into cleaned data
|
||||
if self.cleaned_data is None:
|
||||
self.cleaned_data = {}
|
||||
self.cleaned_data["geom"] = form_geom.ewkt
|
||||
|
||||
return is_valid
|
||||
|
||||
def save(self, action: UserActionLogEntry):
|
||||
""" Saves the form's geometry
|
||||
|
||||
Creates a new geometry entry if none is set, yet
|
||||
|
||||
Args:
|
||||
action ():
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
try:
|
||||
if self.instance is None or self.instance.geometry is None:
|
||||
raise LookupError
|
||||
geometry = self.instance.geometry
|
||||
geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP))
|
||||
geometry.modified = action
|
||||
|
||||
geometry.save()
|
||||
except LookupError:
|
||||
# No geometry or linked instance holding a geometry exist --> create a new one!
|
||||
geometry = Geometry.objects.create(
|
||||
geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)),
|
||||
created=action,
|
||||
)
|
||||
# Start the parcel update procedure in a background process
|
||||
celery_update_parcels.delay(geometry.id)
|
||||
return geometry
|
||||
12
konova/forms/modals/__init__.py
Normal file
12
konova/forms/modals/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 15.08.22
|
||||
|
||||
"""
|
||||
from .base_form import *
|
||||
from .document_form import *
|
||||
from .record_form import *
|
||||
from .remove_form import *
|
||||
from .resubmission_form import *
|
||||
73
konova/forms/modals/base_form.py
Normal file
73
konova/forms/modals/base_form.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 15.08.22
|
||||
|
||||
"""
|
||||
from bootstrap_modal_forms.forms import BSModalForm
|
||||
from bootstrap_modal_forms.utils import is_ajax
|
||||
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 konova.contexts import BaseContext
|
||||
from konova.forms.base_form import BaseForm
|
||||
from konova.utils.message_templates import FORM_INVALID
|
||||
|
||||
|
||||
class BaseModalForm(BaseForm, BSModalForm):
|
||||
""" A specialzed form class for modal form handling
|
||||
|
||||
"""
|
||||
is_modal_form = True
|
||||
render_submit = True
|
||||
template = "modal/modal_form.html"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.action_btn_label = _("Continue")
|
||||
|
||||
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
|
||||
|
||||
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():
|
||||
if not is_ajax(request.META):
|
||||
# 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.
|
||||
self.save()
|
||||
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
|
||||
163
konova/forms/modals/document_form.py
Normal file
163
konova/forms/modals/document_form.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 15.08.22
|
||||
|
||||
"""
|
||||
from django import forms
|
||||
from django.db import transaction
|
||||
from django.db.models.fields.files import FieldFile
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from konova.forms.modals.base_form import BaseModalForm
|
||||
from konova.models import AbstractDocument
|
||||
from konova.utils.message_templates import DOCUMENT_EDITED, FILE_SIZE_TOO_LARGE, FILE_TYPE_UNSUPPORTED
|
||||
from user.models import UserActionLogEntry
|
||||
|
||||
|
||||
class NewDocumentModalForm(BaseModalForm):
|
||||
""" Modal form for new documents
|
||||
|
||||
"""
|
||||
title = forms.CharField(
|
||||
label=_("Title"),
|
||||
label_suffix=_(""),
|
||||
max_length=500,
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
}
|
||||
)
|
||||
)
|
||||
creation_date = forms.DateField(
|
||||
label=_("Created on"),
|
||||
label_suffix=_(""),
|
||||
help_text=_("When has this file been created? Important for photos."),
|
||||
widget=forms.DateInput(
|
||||
attrs={
|
||||
"type": "date",
|
||||
"data-provide": "datepicker",
|
||||
"class": "form-control",
|
||||
},
|
||||
format="%d.%m.%Y"
|
||||
)
|
||||
)
|
||||
file = forms.FileField(
|
||||
label=_("File"),
|
||||
label_suffix=_(""),
|
||||
help_text=_("Allowed formats: pdf, jpg, png. Max size 15 MB."),
|
||||
widget=forms.FileInput(
|
||||
attrs={
|
||||
"class": "form-control-file",
|
||||
}
|
||||
),
|
||||
)
|
||||
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",
|
||||
}
|
||||
)
|
||||
)
|
||||
document_model = None
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.form_title = _("Add new document")
|
||||
self.form_caption = _("")
|
||||
self.form_attrs = {
|
||||
"enctype": "multipart/form-data", # important for file upload
|
||||
}
|
||||
if not self.document_model:
|
||||
raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__))
|
||||
|
||||
def is_valid(self):
|
||||
super_valid = super().is_valid()
|
||||
|
||||
_file = self.cleaned_data.get("file", None)
|
||||
|
||||
if _file is None or isinstance(_file, FieldFile):
|
||||
# FieldFile declares that no new file has been uploaded and we do not need to check on the file again
|
||||
return super_valid
|
||||
|
||||
mime_type_valid = self.document_model.is_mime_type_valid(_file)
|
||||
if not mime_type_valid:
|
||||
self.add_error(
|
||||
"file",
|
||||
FILE_TYPE_UNSUPPORTED
|
||||
)
|
||||
|
||||
file_size_valid = self.document_model.is_file_size_valid(_file)
|
||||
if not file_size_valid:
|
||||
self.add_error(
|
||||
"file",
|
||||
FILE_SIZE_TOO_LARGE
|
||||
)
|
||||
|
||||
file_valid = mime_type_valid and file_size_valid
|
||||
return super_valid and file_valid
|
||||
|
||||
def save(self):
|
||||
with transaction.atomic():
|
||||
action = UserActionLogEntry.get_created_action(self.user)
|
||||
edited_action = UserActionLogEntry.get_edited_action(self.user, _("Added document"))
|
||||
|
||||
doc = self.document_model.objects.create(
|
||||
created=action,
|
||||
title=self.cleaned_data["title"],
|
||||
comment=self.cleaned_data["comment"],
|
||||
file=self.cleaned_data["file"],
|
||||
date_of_creation=self.cleaned_data["creation_date"],
|
||||
instance=self.instance,
|
||||
)
|
||||
|
||||
self.instance.log.add(edited_action)
|
||||
self.instance.modified = edited_action
|
||||
self.instance.save()
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
class EditDocumentModalForm(NewDocumentModalForm):
|
||||
document = None
|
||||
document_model = AbstractDocument
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.document = kwargs.pop("document", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.form_title = _("Edit document")
|
||||
form_data = {
|
||||
"title": self.document.title,
|
||||
"comment": self.document.comment,
|
||||
"creation_date": str(self.document.date_of_creation),
|
||||
"file": self.document.file,
|
||||
}
|
||||
self.load_initial_data(form_data)
|
||||
|
||||
def save(self):
|
||||
with transaction.atomic():
|
||||
document = self.document
|
||||
file = self.cleaned_data.get("file", None)
|
||||
|
||||
document.title = self.cleaned_data.get("title", None)
|
||||
document.comment = self.cleaned_data.get("comment", None)
|
||||
document.date_of_creation = self.cleaned_data.get("creation_date", None)
|
||||
if not isinstance(file, FieldFile):
|
||||
document.replace_file(file)
|
||||
document.save()
|
||||
|
||||
self.instance.mark_as_edited(self.user, self.request, edit_comment=DOCUMENT_EDITED)
|
||||
|
||||
return document
|
||||
|
||||
123
konova/forms/modals/record_form.py
Normal file
123
konova/forms/modals/record_form.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 15.08.22
|
||||
|
||||
"""
|
||||
from django import forms
|
||||
from django.db import transaction
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from konova.forms.modals.base_form import BaseModalForm
|
||||
from konova.models import RecordableObjectMixin
|
||||
|
||||
|
||||
class RecordModalForm(BaseModalForm):
|
||||
""" Modal form for recording data
|
||||
|
||||
"""
|
||||
confirm = forms.BooleanField(
|
||||
label=_("Confirm record"),
|
||||
label_suffix="",
|
||||
widget=forms.CheckboxInput(),
|
||||
required=True,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.form_title = _("Record data")
|
||||
self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name)
|
||||
# Disable automatic w-100 setting for this type of modal form. Looks kinda strange
|
||||
self.fields["confirm"].widget.attrs["class"] = ""
|
||||
|
||||
if self.instance.recorded:
|
||||
# unrecord!
|
||||
self.fields["confirm"].label = _("Confirm unrecord")
|
||||
self.form_title = _("Unrecord data")
|
||||
self.form_caption = _("I, {} {}, confirm that this data must be unrecorded.").format(self.user.first_name, self.user.last_name)
|
||||
|
||||
if not isinstance(self.instance, RecordableObjectMixin):
|
||||
raise NotImplementedError
|
||||
|
||||
def is_valid(self):
|
||||
""" Checks for instance's validity and data quality
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
from intervention.models import Intervention
|
||||
super_val = super().is_valid()
|
||||
if self.instance.recorded:
|
||||
# If user wants to unrecord an already recorded dataset, we do not need to perform custom checks
|
||||
return super_val
|
||||
checker = self.instance.quality_check()
|
||||
for msg in checker.messages:
|
||||
self.add_error(
|
||||
"confirm",
|
||||
msg
|
||||
)
|
||||
valid = checker.valid
|
||||
# Special case: Intervention
|
||||
# Add direct checks for related compensations
|
||||
if isinstance(self.instance, Intervention):
|
||||
comps_valid = self._are_compensations_valid()
|
||||
valid = valid and comps_valid
|
||||
return super_val and valid
|
||||
|
||||
def _are_deductions_valid(self):
|
||||
""" Performs validity checks on deductions and their eco-account
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
deductions = self.instance.deductions.all()
|
||||
for deduction in deductions:
|
||||
checker = deduction.account.quality_check()
|
||||
for msg in checker.messages:
|
||||
self.add_error(
|
||||
"confirm",
|
||||
f"{deduction.account.identifier}: {msg}"
|
||||
)
|
||||
return checker.valid
|
||||
return True
|
||||
|
||||
def _are_compensations_valid(self):
|
||||
""" Runs a special case for intervention-compensations validity
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
comps = self.instance.compensations.filter(
|
||||
deleted=None,
|
||||
)
|
||||
comps_valid = True
|
||||
for comp in comps:
|
||||
checker = comp.quality_check()
|
||||
comps_valid = comps_valid and checker.valid
|
||||
for msg in checker.messages:
|
||||
self.add_error(
|
||||
"confirm",
|
||||
f"{comp.identifier}: {msg}"
|
||||
)
|
||||
|
||||
deductions_valid = self._are_deductions_valid()
|
||||
|
||||
return comps_valid and deductions_valid
|
||||
|
||||
def save(self):
|
||||
with transaction.atomic():
|
||||
if self.cleaned_data["confirm"]:
|
||||
if self.instance.recorded:
|
||||
self.instance.set_unrecorded(self.user)
|
||||
else:
|
||||
self.instance.set_recorded(self.user)
|
||||
return self.instance
|
||||
|
||||
def check_for_recorded_instance(self):
|
||||
""" Overwrite the check method for doing nothing on the RecordModalForm
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
pass
|
||||
58
konova/forms/modals/remove_form.py
Normal file
58
konova/forms/modals/remove_form.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 15.08.22
|
||||
|
||||
"""
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from konova.forms.modals.base_form import BaseModalForm
|
||||
from konova.models import BaseObject
|
||||
|
||||
|
||||
class RemoveModalForm(BaseModalForm):
|
||||
""" Generic removing modal form
|
||||
|
||||
Can be used for anything, where removing shall be confirmed by the user a second time.
|
||||
|
||||
"""
|
||||
confirm = forms.BooleanField(
|
||||
label=_("Confirm"),
|
||||
label_suffix=_(""),
|
||||
widget=forms.CheckboxInput(),
|
||||
required=True,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.template = "modal/modal_form.html"
|
||||
super().__init__(*args, **kwargs)
|
||||
self.form_title = _("Remove")
|
||||
self.form_caption = _("Are you sure?")
|
||||
# Disable automatic w-100 setting for this type of modal form. Looks kinda strange
|
||||
self.fields["confirm"].widget.attrs["class"] = ""
|
||||
|
||||
def save(self):
|
||||
if isinstance(self.instance, BaseObject):
|
||||
self.instance.mark_as_deleted(self.user)
|
||||
else:
|
||||
# If the class does not provide restorable delete functionality, we must delete the entry finally
|
||||
self.instance.delete()
|
||||
|
||||
|
||||
class RemoveDeadlineModalForm(RemoveModalForm):
|
||||
""" Removing modal form for deadlines
|
||||
|
||||
Can be used for anything, where removing shall be confirmed by the user a second time.
|
||||
|
||||
"""
|
||||
deadline = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
deadline = kwargs.pop("deadline", None)
|
||||
self.deadline = deadline
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def save(self):
|
||||
self.instance.remove_deadline(self)
|
||||
85
konova/forms/modals/resubmission_form.py
Normal file
85
konova/forms/modals/resubmission_form.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 15.08.22
|
||||
|
||||
"""
|
||||
import datetime
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import transaction
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from konova.forms.modals.base_form import BaseModalForm
|
||||
from konova.models import Resubmission
|
||||
|
||||
|
||||
class ResubmissionModalForm(BaseModalForm):
|
||||
date = forms.DateField(
|
||||
label_suffix=_(""),
|
||||
label=_("Date"),
|
||||
help_text=_("When do you want to be reminded?"),
|
||||
widget=forms.DateInput(
|
||||
attrs={
|
||||
"type": "date",
|
||||
"data-provide": "datepicker",
|
||||
"class": "form-control",
|
||||
},
|
||||
format="%d.%m.%Y"
|
||||
)
|
||||
)
|
||||
comment = forms.CharField(
|
||||
required=False,
|
||||
label=_("Comment"),
|
||||
label_suffix=_(""),
|
||||
help_text=_("Additional comment"),
|
||||
widget=forms.Textarea(
|
||||
attrs={
|
||||
"cols": 30,
|
||||
"rows": 5,
|
||||
"class": "form-control",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.form_title = _("Resubmission")
|
||||
self.form_caption = _("Set your resubmission for this entry.")
|
||||
self.action_url = None
|
||||
|
||||
try:
|
||||
self.resubmission = self.instance.resubmissions.get(
|
||||
user=self.user
|
||||
)
|
||||
self.initialize_form_field("date", str(self.resubmission.resubmit_on))
|
||||
self.initialize_form_field("comment", self.resubmission.comment)
|
||||
except ObjectDoesNotExist:
|
||||
self.resubmission = Resubmission()
|
||||
|
||||
def is_valid(self):
|
||||
super_valid = super().is_valid()
|
||||
self_valid = True
|
||||
|
||||
date = self.cleaned_data.get("date")
|
||||
today = datetime.date.today()
|
||||
if date <= today:
|
||||
self.add_error(
|
||||
"date",
|
||||
_("The date should be in the future")
|
||||
)
|
||||
self_valid = False
|
||||
|
||||
return super_valid and self_valid
|
||||
|
||||
def save(self):
|
||||
with transaction.atomic():
|
||||
self.resubmission.user = self.user
|
||||
self.resubmission.resubmit_on = self.cleaned_data.get("date")
|
||||
self.resubmission.comment = self.cleaned_data.get("comment")
|
||||
self.resubmission.save()
|
||||
self.instance.resubmissions.add(self.resubmission)
|
||||
return self.resubmission
|
||||
|
||||
54
konova/forms/remove_form.py
Normal file
54
konova/forms/remove_form.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 15.08.22
|
||||
|
||||
"""
|
||||
from django import forms
|
||||
from django.db import transaction
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from konova.forms.base_form import BaseForm
|
||||
from user.models import UserActionLogEntry, User
|
||||
|
||||
|
||||
class RemoveForm(BaseForm):
|
||||
check = forms.BooleanField(
|
||||
label=_("Confirm"),
|
||||
label_suffix=_(""),
|
||||
required=True,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.object_to_remove = kwargs.pop("object_to_remove", None)
|
||||
self.remove_post_url = kwargs.pop("remove_post_url", "")
|
||||
self.cancel_url = kwargs.pop("cancel_url", "")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.form_title = _("Remove")
|
||||
if self.object_to_remove is not None:
|
||||
self.form_caption = _("You are about to remove {} {}").format(self.object_to_remove.__class__.__name__, self.object_to_remove)
|
||||
self.action_url = self.remove_post_url
|
||||
self.cancel_redirect = self.cancel_url
|
||||
|
||||
def is_checked(self) -> bool:
|
||||
return self.cleaned_data.get("check", False)
|
||||
|
||||
def save(self, user: User):
|
||||
""" Perform generic removing by running the form typical 'save()' method
|
||||
|
||||
Args:
|
||||
user (User): The performing user
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if self.object_to_remove is not None and self.is_checked():
|
||||
with transaction.atomic():
|
||||
self.object_to_remove.is_active = False
|
||||
action = UserActionLogEntry.get_deleted_action(user)
|
||||
self.object_to_remove.deleted = action
|
||||
self.object_to_remove.save()
|
||||
return self.object_to_remove
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user