Compare commits

...

35 Commits
v0.3.1 ... v0.4

Author SHA1 Message Date
92211445e7 Merge pull request '#156 Parcel WFS as geojson' (#157) from 156_Parcel_WFS_as_geojson into master
Reviewed-on: SGD-Nord/konova#157
2022-04-27 12:18:47 +02:00
73c61e96f5 #156 Parcel WFS as geojson
* refactors fetching of parcels via wfs from xml to json for easier and faster processing
2022-04-27 12:12:56 +02:00
339f074681 HOTFIX: API
* hardens atom_id input to be integer or string compatible
2022-04-25 13:47:07 +02:00
989d256521 HOTFIX: EGON sending via API
* adds EGON message triggering on API payment changes
2022-04-25 13:28:51 +02:00
623c29f827 Merge pull request '#149 Send on changes' (#154) from 149_EGON_sending into master
Reviewed-on: SGD-Nord/konova#154
2022-04-25 11:17:34 +02:00
eb975cd3c5 #149 Send on changes
* changes trigger for sending data to EGON: on each new payment, edited payment or deleted payment action, the data will be sent to EGON instead only once on "recording"
2022-04-25 11:16:51 +02:00
5a8765f638 Merge pull request '151_Parcel_table_infinite_scroll' (#153) from 151_Parcel_table_infinite_scroll into master
Reviewed-on: SGD-Nord/konova#153
2022-04-21 14:37:38 +02:00
9c2bdcdacf #151 Parcel table infinite scroll
* refactors button for further loading to infinite scroll
* adds code documentation
2022-04-21 14:36:55 +02:00
48e3e84b4c #151 Dynamic parcel table
* refactors parcel table into a dynamic table, which does not show all content at once but rather supports pagination and a button which triggers loading of more content
* adds translation
2022-04-21 14:19:35 +02:00
940aa38154 Merge pull request '140_Improve_check-record_reset' (#152) from 140_Improve_check-record_reset into master
Reviewed-on: SGD-Nord/konova#152
2022-04-19 14:06:42 +02:00
887a3552b4 #140 Tests
* adds workflow tests for major datatypes
2022-04-19 14:04:20 +02:00
3b36193566 #140 Enhancements
* fixes InterventionAutocomplete bug, where team-shared entries would not pop up as valid option
* fixes bug where form opening for new compensation without direct intervention link resulted in 404
* adds intervention-recorded check on deduction forms: Form is invalid if intervention is currently recorded and therefore blocked for any editing
* extends basic check_for_recorded_instance() method to let some forms pass, e.g. deduction related forms on ecoaccounts which only have a reason to be rendered IF the entry is recorded
* adds/updates translations
2022-04-19 13:37:29 +02:00
090f6faa4e #140 Block edit on recorded
* adds new modal form content template recorded_no_edit.html
* adds modal content change, such that no data can be edited on any form as long as the entry is recorded -> instead, users are informed on the form, that the recording state prohibits editing
* adds translations
2022-04-19 09:43:36 +02:00
d2ec3d9c08 Merge pull request '146_Minor_improvements' (#150) from 146_Minor_improvements into master
Reviewed-on: SGD-Nord/konova#150
2022-04-14 14:13:14 +02:00
8165540c00 #146 Record-unshare with default
* adds automatic unsharing with default-only users if entry is recorded
2022-04-14 08:37:43 +02:00
87fae51144 #146 Team leave
* adds button and functionality for leaving a team
   * if the admin leaves the team, another user will be chosen as new admin automatically
* improves Team (django) admin backend
   * better control over user adding-removing
   * only added team members are selectable as admin
2022-04-13 15:52:41 +02:00
bf1c0e2078 #146 Clickable QR codes
* refactors QR codes on report views to be clickable as well (even supported through saved pdf)
2022-04-13 14:57:05 +02:00
b85e33dc22 #146 Share with fix
* fixes bug where editable icon on overview table would not glow if user has only team based shared access
2022-04-13 14:18:32 +02:00
83d70b6d59 #146 (Parcel) table
* set default rpp for overview tables from 5 to 10
* improves loading speed of parcel table
2022-04-13 14:07:01 +02:00
60e23d15fc #146 Admins and update_all_parcels.py
* extends admin backend
    * adds found_in_codelists to KonovaCodeAdmin to see where a KonovaCode can be found in
    * improves rendering of after_states and before_states for all AbstractCompensationAdmins
    * adds geometry_id to all major datatype admin backends
    * adds st_area like calculation to geometry admin backend
* update_all_parcels
    * orders geometries by size (small to big) to process smaller geometries first and bigger later
    * adds more output to command for a better overview of what is just going on
2022-04-13 11:42:04 +02:00
fb1dce9d3c Merge pull request '#144 Report improved' (#145) from 144_Improve_report into master
Reviewed-on: SGD-Nord/konova#145
2022-04-12 10:46:06 +02:00
6ecbd74b93 #144 Report improved
* fixes bug in egon_export.py where missing payment date would result in non writing of gml
* fixes bug in egon_export.py which occured due to extension of parcel data fetching
* updates unavailable.html report content, such that users will understand why a recorded entry might not be visible, yet
2022-04-12 10:33:03 +02:00
a6551534dc Merge pull request '#142 Localized date improved' (#143) from 142_Localized_date_format into master
Reviewed-on: SGD-Nord/konova#143
2022-04-12 09:06:05 +02:00
6060f1c1bd #142 Localized date improved
* fixes bug where created timestamp has been displayed on modified attribute on detail views
* enhances localized date and datetime rendering
* reorders sub menus in user's profile hub
2022-04-12 09:05:33 +02:00
59ff1c79a8 Merge pull request '139_Improve_parcel_reference' (#141) from 139_Improve_parcel_reference into master
Reviewed-on: SGD-Nord/konova#141
2022-04-11 12:21:41 +02:00
64d0a3bd12 # 139 Doc update
* updates doc
2022-04-11 10:55:15 +02:00
a34a0b4d8a #139 Parcel filter improved
* improves frontend filtering for district, municipal, ..., so keys can be used for a lookup as well
2022-04-11 10:51:15 +02:00
1be77e8b22 # 139 Parcel reference improved
* improves frontend layout to display more details on district, municipal and parce group
* improves ordering of parcels
* refactors parcel related models
* improves parcel fetching
* extends and simplifies sanitize_db parcel related code
2022-04-11 10:23:28 +02:00
fc31ad4ae0 Merge pull request '131_EGON_connection' (#135) from 131_EGON_connection into master
Reviewed-on: SGD-Nord/konova#135
2022-03-21 12:20:55 +01:00
7689e0b80d #131 EGON export
* finishes egon compatible (tested) data export
* moves egon export into celery process
* adds export of data in case of intervention recording
* adds _RABBITMQ_ settings for intervention/settings.py
* adds new dependency for requirements.txt
2022-03-21 12:14:55 +01:00
17c954e844 #131 EGON exporter
* enhances EGON exporter code structure
2022-03-09 08:34:26 +01:00
06ad0fdc2d #131 WIP: EGON exporter
* adds incomplete WIP implementation of an EGON exporter
2022-03-08 11:54:26 +01:00
98a1a70a69 Merge pull request '129_Handler_code' (#133) from 129_Handler_code into master
Reviewed-on: SGD-Nord/konova#133
2022-03-04 13:23:11 +01:00
22a3339157 # 129 Handler code renaming
* renames handler code list
* improves missing handler data rendering on detail view
2022-03-03 12:09:09 +01:00
c98f41c9a8 # 129 Handler code
* adds handler code list usage to forms and models
* updates tests
* extends API for handler code handling
2022-03-03 12:05:22 +01:00
87 changed files with 1970 additions and 590 deletions

View File

@@ -8,7 +8,10 @@
"responsible": {
"conservation_office": null,
"conservation_file_number": null,
"handler": null
"handler": {
"type": null,
"detail": "Someone"
}
},
"legal": {
"agreement_date": null

View File

@@ -7,7 +7,10 @@
"responsible": {
"conservation_office": null,
"conservation_file_number": null,
"handler": null
"handler": {
"type": null,
"detail": "Someone"
}
},
"before_states": [
],

View File

@@ -9,7 +9,10 @@
"registration_file_number": null,
"conservation_office": null,
"conservation_file_number": null,
"handler": null
"handler": {
"type": null,
"detail": "Someone"
}
},
"legal": {
"registration_date": null,

View File

@@ -48,7 +48,10 @@
"responsible": {
"conservation_office": null,
"conservation_file_number": "123-TEST",
"handler": "TEST_HANDLER_CHANGED"
"handler": {
"type": null,
"detail": "TEST HANDLER CHANGED"
}
},
"legal": {
"agreement_date": "2022-01-11"

View File

@@ -47,7 +47,10 @@
"responsible": {
"conservation_office": null,
"conservation_file_number": "TEST_CHANGED",
"handler": "TEST_HANDLER_CHANGED"
"handler": {
"type": null,
"detail": "TEST_HANDLER_CHANGED"
}
},
"before_states": [],
"after_states": [],

View File

@@ -49,7 +49,10 @@
"registration_file_number": "CHANGED",
"conservation_office": null,
"conservation_file_number": "CHANGED",
"handler": null
"handler": {
"type": null,
"detail": "TEST_HANDLER_CHANGED"
}
},
"legal": {
"registration_date": "2022-02-01",

View File

@@ -126,7 +126,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
self.assertEqual(put_props["deductable_surface"], str(self.eco_account.deductable_surface))
self.assertEqual(put_props["responsible"]["conservation_office"], self.eco_account.responsible.conservation_office)
self.assertEqual(put_props["responsible"]["conservation_file_number"], self.eco_account.responsible.conservation_file_number)
self.assertEqual(put_props["responsible"]["handler"], self.eco_account.responsible.handler)
self.assertEqual(put_props["responsible"]["handler"]["detail"], self.eco_account.responsible.handler.detail)
self.assertEqual(put_props["legal"]["agreement_date"], str(self.eco_account.legal.registration_date))
self.assertEqual(len(put_props["actions"]), self.eco_account.actions.count())
self.assertEqual(len(put_props["before_states"]), self.eco_account.before_states.count())
@@ -156,7 +156,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
self.assertNotEqual(modified_on, self.ema.modified)
self.assertEqual(put_props["responsible"]["conservation_office"], self.ema.responsible.conservation_office)
self.assertEqual(put_props["responsible"]["conservation_file_number"], self.ema.responsible.conservation_file_number)
self.assertEqual(put_props["responsible"]["handler"], self.ema.responsible.handler)
self.assertEqual(put_props["responsible"]["handler"]["detail"], self.ema.responsible.handler.detail)
self.assertEqual(len(put_props["actions"]), self.ema.actions.count())
self.assertEqual(len(put_props["before_states"]), self.ema.before_states.count())
self.assertEqual(len(put_props["after_states"]), self.ema.after_states.count())

View File

@@ -9,9 +9,9 @@ from django.db import transaction
from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin, \
LegalAPISerializerV1Mixin, ResponsibilityAPISerializerV1Mixin, DeductableAPISerializerV1Mixin
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID
from compensation.models import EcoAccount
from intervention.models import Legal, Responsibility
from intervention.models import Legal, Responsibility, Handler
from konova.models import Geometry
from konova.tasks import celery_update_parcels
from user.models import UserActionLogEntry
@@ -44,7 +44,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
return {
"conservation_office": self._konova_code_to_json(responsible.conservation_office),
"conservation_file_number": responsible.conservation_file_number,
"handler": responsible.handler,
"handler": self._handler_to_json(responsible.handler),
}
def _set_responsibility(self, obj, responsibility_data: dict):
@@ -64,7 +64,11 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
CODELIST_CONSERVATION_OFFICE_ID,
)
obj.responsible.conservation_file_number = responsibility_data["conservation_file_number"]
obj.responsible.handler = responsibility_data["handler"]
obj.responsible.handler.type = self._konova_code_from_json(
responsibility_data["handler"]["type"],
CODELIST_HANDLER_ID,
)
obj.responsible.handler.detail = responsibility_data["handler"]["detail"]
return obj
def _set_legal(self, obj, legal_data):
@@ -92,7 +96,9 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
# Create linked objects
obj = EcoAccount()
obj.responsible = Responsibility()
obj.responsible = Responsibility(
handler=Handler()
)
obj.legal = Legal()
created = create_action
obj.created = created
@@ -128,6 +134,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
obj = self._set_legal(obj, properties["legal"])
obj.geometry.save()
obj.responsible.handler.save()
obj.responsible.save()
obj.legal.save()
obj.save()
@@ -170,6 +177,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
obj = self._set_legal(obj, properties["legal"])
obj.geometry.save()
obj.responsible.handler.save()
obj.responsible.save()
obj.legal.save()
obj.save()

View File

@@ -9,9 +9,9 @@ from django.db import transaction
from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin, \
ResponsibilityAPISerializerV1Mixin
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID
from ema.models import Ema
from intervention.models import Responsibility
from intervention.models import Responsibility, Handler
from konova.models import Geometry
from konova.tasks import celery_update_parcels
from user.models import UserActionLogEntry
@@ -31,7 +31,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
return {
"conservation_office": self._konova_code_to_json(responsible.conservation_office),
"conservation_file_number": responsible.conservation_file_number,
"handler": responsible.handler,
"handler": self._handler_to_json(responsible.handler),
}
def _set_responsibility(self, obj, responsibility_data: dict):
@@ -51,7 +51,11 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
CODELIST_CONSERVATION_OFFICE_ID,
)
obj.responsible.conservation_file_number = responsibility_data["conservation_file_number"]
obj.responsible.handler = responsibility_data["handler"]
obj.responsible.handler.type = self._konova_code_from_json(
responsibility_data["handler"]["type"],
CODELIST_HANDLER_ID,
)
obj.responsible.handler.detail = responsibility_data["handler"]["detail"]
return obj
def _initialize_objects(self, json_model, user):
@@ -75,7 +79,9 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
# Create linked objects
obj = Ema()
obj.responsible = Responsibility()
obj.responsible = Responsibility(
handler=Handler()
)
created = create_action
obj.created = created
obj.geometry = geometry
@@ -101,6 +107,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
obj = self._set_responsibility(obj, properties["responsible"])
obj.geometry.save()
obj.responsible.handler.save()
obj.responsible.save()
obj.save()
@@ -140,6 +147,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
obj = self._set_responsibility(obj, properties["responsible"])
obj.geometry.save()
obj.responsible.handler.save()
obj.responsible.save()
obj.save()

View File

@@ -11,7 +11,7 @@ from django.db.models import QuerySet
from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, \
ResponsibilityAPISerializerV1Mixin, LegalAPISerializerV1Mixin, DeductableAPISerializerV1Mixin
from compensation.models import Payment
from intervention.models import Intervention, Responsibility, Legal
from intervention.models import Intervention, Responsibility, Legal, Handler
from konova.models import Geometry
from konova.tasks import celery_update_parcels
from user.models import UserActionLogEntry
@@ -69,7 +69,9 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
# Create linked objects
obj = Intervention()
resp = Responsibility()
resp = Responsibility(
handler=Handler()
)
legal = Legal()
created = create_action
obj.legal = legal
@@ -130,6 +132,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
id__in=payments
)
obj.payments.set(payments)
obj.send_data_to_egon()
return obj
def create_model_from_json(self, json_model, user):
@@ -152,6 +155,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
self._set_responsibility(obj, properties["responsible"])
self._set_legal(obj, properties["legal"])
obj.responsible.handler.save()
obj.responsible.save()
obj.geometry.save()
obj.legal.save()
@@ -188,12 +192,13 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
obj.geometry.geom = self._create_geometry_from_json(json_model)
obj.geometry.modified = update_action
obj.responsible.handler.save()
obj.responsible.save()
obj.geometry.save()
obj.legal.save()
obj.save()
obj.mark_as_edited(user)
obj.mark_as_edited(user, edit_comment="API update")
celery_update_parcels.delay(obj.geometry.id)

View File

@@ -15,9 +15,9 @@ from api.utils.serializer.serializer import AbstractModelAPISerializer
from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_PROCESS_TYPE_ID, \
CODELIST_LAW_ID, CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, \
CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, CODELIST_HANDLER_ID
from compensation.models import CompensationAction, UnitChoices, CompensationState
from intervention.models import Responsibility, Legal
from intervention.models import Responsibility, Legal, Handler
from konova.models import Deadline, DeadlineType
from konova.utils.message_templates import DATA_UNSHARED
@@ -75,7 +75,10 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer):
Returns:
"""
if json_str is None or len(json_str) == 0:
if json_str is None:
return None
json_str = str(json_str)
if len(json_str) == 0:
return None
code = KonovaCode.objects.get(
atom_id=json_str,
@@ -176,6 +179,12 @@ class ResponsibilityAPISerializerV1Mixin:
class Meta:
abstract = True
def _handler_to_json(self, handler: Handler):
return {
"type": self._konova_code_to_json(handler.type),
"detail": handler.detail
}
def _responsible_to_json(self, responsible: Responsibility):
""" Serializes Responsibility model into json
@@ -190,7 +199,7 @@ class ResponsibilityAPISerializerV1Mixin:
"registration_file_number": responsible.registration_file_number,
"conservation_office": self._konova_code_to_json(responsible.conservation_office),
"conservation_file_number": responsible.conservation_file_number,
"handler": responsible.handler,
"handler": self._handler_to_json(responsible.handler),
}
def _set_responsibility(self, obj, responsibility_data: dict):
@@ -215,7 +224,11 @@ class ResponsibilityAPISerializerV1Mixin:
CODELIST_CONSERVATION_OFFICE_ID,
)
obj.responsible.conservation_file_number = responsibility_data["conservation_file_number"]
obj.responsible.handler = responsibility_data["handler"]
obj.responsible.handler.type = self._konova_code_from_json(
responsibility_data["handler"]["type"],
CODELIST_HANDLER_ID,
)
obj.responsible.handler.detail = responsibility_data["handler"]["detail"]
return obj

View File

@@ -33,6 +33,7 @@ class KonovaCodeAdmin(admin.ModelAdmin):
"is_selectable",
"is_leaf",
"parent",
"found_in_codelists",
]
search_fields = [
@@ -42,6 +43,12 @@ class KonovaCodeAdmin(admin.ModelAdmin):
"short_name",
]
def found_in_codelists(self, obj):
codelists = KonovaCodeList.objects.filter(
codes__in=[obj]
).values_list("id", flat=True)
codelists = "\n".join(str(x) for x in codelists)
return codelists
#admin.site.register(KonovaCodeList, KonovaCodeListAdmin)
admin.site.register(KonovaCode, KonovaCodeAdmin)

View File

@@ -11,7 +11,7 @@ from xml.etree import ElementTree as etree
from codelist.models import KonovaCode, KonovaCodeList
from codelist.settings import CODELIST_INTERVENTION_HANDLER_ID, CODELIST_CONSERVATION_OFFICE_ID, \
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_BIOTOPES_ID, CODELIST_LAW_ID, CODELIST_COMPENSATION_HANDLER_ID, \
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_BIOTOPES_ID, CODELIST_LAW_ID, CODELIST_HANDLER_ID, \
CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_CLASS_ID, CODELIST_COMPENSATION_ADDITIONAL_TYPE_ID, \
CODELIST_BASE_URL, CODELIST_PROCESS_TYPE_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, \
CODELIST_COMPENSATION_ACTION_DETAIL_ID
@@ -36,7 +36,7 @@ class Command(BaseKonovaCommand):
CODELIST_BIOTOPES_ID,
CODELIST_BIOTOPES_EXTRA_CODES_ID,
CODELIST_LAW_ID,
CODELIST_COMPENSATION_HANDLER_ID,
CODELIST_HANDLER_ID,
CODELIST_COMPENSATION_ACTION_ID,
CODELIST_COMPENSATION_ACTION_CLASS_ID,
CODELIST_COMPENSATION_ACTION_DETAIL_ID,

View File

@@ -1,9 +1,13 @@
# Generated by Django 3.1.3 on 2022-01-14 08:36
from django.core.management import call_command
from django.db import migrations, models
import django.db.models.deletion
def load_initial_codes(apps, schema_editor):
call_command('update_codelist')
class Migration(migrations.Migration):
initial = True
@@ -32,4 +36,5 @@ class Migration(migrations.Migration):
('codes', models.ManyToManyField(blank=True, help_text='Codes for this list', related_name='code_lists', to='codelist.KonovaCode')),
],
),
migrations.RunPython(load_initial_codes),
]

View File

@@ -19,7 +19,7 @@ CODELIST_BIOTOPES_EXTRA_CODES_ID = 975 # CLZusatzbezeichnung
CODELIST_LAW_ID = 1048 # CLVerfahrensrecht
CODELIST_PROCESS_TYPE_ID = 44382 # CLVerfahrenstyp
CODELIST_COMPENSATION_HANDLER_ID = 1052 # CLEingreifer
CODELIST_HANDLER_ID = 1052 # CLEingreifer
CODELIST_COMPENSATION_ACTION_ID = 1026 # CLMassnahmedetail
CODELIST_COMPENSATION_ACTION_DETAIL_ID = 1035 # CLZusatzmerkmal
CODELIST_COMPENSATION_ACTION_CLASS_ID = 1034 # CLMassnahmeklasse

View File

@@ -21,16 +21,30 @@ class AbstractCompensationAdmin(BaseObjectAdmin):
"identifier",
"title",
"comment",
"after_states",
"before_states",
"list_after_states",
"list_before_states",
"geometry",
]
def get_readonly_fields(self, request, obj=None):
return super().get_readonly_fields(request, obj) + [
"after_states",
"before_states",
"list_after_states",
"list_before_states",
"geometry",
]
def list_after_states(self, obj):
states = obj.after_states.all()
states = [str(state) for state in states]
states = "\n".join(states)
return states
def list_before_states(self, obj):
states = obj.before_states.all()
states = [str(state) for state in states]
states = "\n".join(states)
return states
class CompensationAdmin(AbstractCompensationAdmin):
autocomplete_fields = [

View File

@@ -13,10 +13,10 @@ from django.utils.translation import gettext_lazy as _
from django import forms
from codelist.models import KonovaCode
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID
from compensation.models import Compensation, EcoAccount
from intervention.inputs import GenerateInput
from intervention.models import Intervention, Responsibility, Legal
from intervention.models import Intervention, Responsibility, Legal, Handler
from konova.forms import BaseForm, SimpleGeomForm
from konova.utils.message_templates import EDITED_GENERAL_DATA, COMPENSATION_ADDED_TEMPLATE
from user.models import UserActionLogEntry
@@ -101,12 +101,30 @@ class CompensationResponsibleFormMixin(forms.Form):
}
)
)
handler = forms.CharField(
label=_("Eco-account handler"),
handler_type = forms.ModelChoiceField(
label=_("Eco-Account handler type"),
label_suffix="",
help_text=_("What type of handler is responsible for the ecoaccount?"),
required=False,
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_HANDLER_ID],
),
widget=autocomplete.ModelSelect2(
url="codes-handler-autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
}
),
)
handler_detail = forms.CharField(
label=_("Eco-Account handler detail"),
label_suffix="",
max_length=255,
required=False,
help_text=_("Who handles the eco-account"),
help_text=_("Detail input on the handler"),
widget=forms.TextInput(
attrs={
"placeholder": _("Company Mustermann"),
@@ -345,7 +363,8 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
"registration_date",
"surface",
"conservation_file_number",
"handler",
"handler_type",
"handler_detail",
"comment",
]
@@ -368,7 +387,8 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None)
handler = self.cleaned_data.get("handler", None)
handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None)
surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
@@ -379,6 +399,11 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
# Process the geometry form
geometry = geom_form.save(action)
handler = Handler.objects.create(
type=handler_type,
detail=handler_detail,
)
responsible = Responsibility.objects.create(
handler=handler,
conservation_file_number=conservation_file_number,
@@ -423,11 +448,13 @@ class EditEcoAccountForm(NewEcoAccountForm):
reg_date = self.instance.legal.registration_date
if reg_date is not None:
reg_date = reg_date.isoformat()
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"surface": self.instance.deductable_surface,
"handler": self.instance.responsible.handler,
"handler_type": self.instance.responsible.handler.type,
"handler_detail": self.instance.responsible.handler.detail,
"registration_date": reg_date,
"conservation_office": self.instance.responsible.conservation_office,
"conservation_file_number": self.instance.responsible.conservation_file_number,
@@ -445,7 +472,8 @@ class EditEcoAccountForm(NewEcoAccountForm):
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None)
handler = self.cleaned_data.get("handler", None)
handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None)
surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
@@ -458,7 +486,9 @@ class EditEcoAccountForm(NewEcoAccountForm):
geometry = geom_form.save(action)
# Update responsible data
self.instance.responsible.handler = handler
self.instance.responsible.handler.type = handler_type
self.instance.responsible.handler.detail = handler_detail
self.instance.responsible.handler.save()
self.instance.responsible.conservation_office = conservation_office
self.instance.responsible.conservation_file_number = conservation_file_number
self.instance.responsible.save()

View File

@@ -128,6 +128,7 @@ class EditPaymentModalForm(NewPaymentForm):
payment.comment = self.cleaned_data.get("comment", None)
payment.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=PAYMENT_EDITED)
self.instance.send_data_to_egon()
return payment

View File

@@ -418,6 +418,18 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
super().set_status_messages(request)
return request
@property
def is_recorded(self):
""" Getter for record status as property
Since compensations inherit their record status from their intervention, the intervention's status is being
returned
Returns:
"""
return self.intervention.is_recorded
class CompensationDocument(AbstractDocument):
"""

View File

@@ -134,7 +134,7 @@ class CompensationTable(BaseTable, TableRenderMixin):
"""
parcels = value.get_underlying_parcels().values_list(
"gmrkng",
"parcel_group__name",
flat=True
).distinct()
html = render_to_string(
@@ -181,9 +181,7 @@ class CompensationTable(BaseTable, TableRenderMixin):
"""
if value is None:
value = User.objects.none()
has_access = value.filter(
id=self.user.id
).exists()
has_access = record.is_shared_with(self.user)
html = self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),
@@ -295,7 +293,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
"""
parcels = value.get_underlying_parcels().values_list(
"gmrkng",
"parcel_group__name",
flat=True
).distinct()
html = render_to_string(
@@ -343,7 +341,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
html = ""
# Do not use value in here, since value does use unprefetched 'users' manager, where record has already
# prefetched users data
has_access = self.user in record.users.all()
has_access = record.is_shared_with(self.user)
html += self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),
icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit",

View File

@@ -90,9 +90,15 @@
<tr>
<th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle">
{{obj.modified.timestamp|default_if_none:""|naturalday}}
<br>
{{obj.modified.user.username}}
{% if obj.modified %}
{{obj.modified.timestamp|default_if_none:""}}
<br>
{{obj.modified.user.username}}
{% else %}
{{obj.created.timestamp|default_if_none:""}}
<br>
{{obj.created.user.username}}
{% endif %}
</td>
</tr>
<tr>
@@ -116,7 +122,7 @@
{% include 'map/geom_form.html' %}
</div>
<div class="row">
{% include 'konova/includes/parcels.html' %}
{% include 'konova/includes/parcels/parcels.html' %}
</div>
<div class="row">
{% include 'konova/includes/comment_card.html' %}

View File

@@ -73,9 +73,15 @@
<tr>
<th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle">
{{obj.modified.timestamp|default_if_none:""|naturalday}}
<br>
{{obj.modified.user.username}}
{% if obj.modified %}
{{obj.modified.timestamp|default_if_none:""}}
<br>
{{obj.modified.user.username}}
{% else %}
{{obj.created.timestamp|default_if_none:""}}
<br>
{{obj.created.user.username}}
{% endif %}
</td>
</tr>
<tr>
@@ -98,7 +104,7 @@
{% include 'map/geom_form.html' %}
</div>
<div class="row">
{% include 'konova/includes/parcels.html' %}
{% include 'konova/includes/parcels/parcels.html' %}
</div>
<div class="row">
{% include 'konova/includes/comment_card.html' %}

View File

@@ -38,17 +38,10 @@
{% include 'map/geom_form.html' %}
</div>
<div class="row">
{% include 'konova/includes/parcels.html' %}
{% include 'konova/includes/parcels/parcels.html' %}
</div>
<div class="row">
<div class="col-sm-6 col-md-6 col-lg-6">
<h4>{% trans 'Open in browser' %}</h4>
{{ qrcode|safe }}
</div>
<div class="col-sm-6 col-md-6 col-lg-6">
<h4>{% trans 'View in LANIS' %}</h4>
{{ qrcode_lanis|safe }}
</div>
{% include 'konova/includes/report/qrcodes.html' %}
</div>
</div>

View File

@@ -51,17 +51,10 @@
{% include 'map/geom_form.html' %}
</div>
<div class="row">
{% include 'konova/includes/parcels.html' %}
{% include 'konova/includes/parcels/parcels.html' %}
</div>
<div class="row">
<div class="col-sm-6 col-md-6 col-lg-6">
<h4>{% trans 'Open in browser' %}</h4>
{{ qrcode|safe }}
</div>
<div class="col-sm-6 col-md-6 col-lg-6">
<h4>{% trans 'View in LANIS' %}</h4>
{{ qrcode_lanis|safe }}
</div>
{% include 'konova/includes/report/qrcodes.html' %}
</div>
</div>

View File

@@ -60,8 +60,9 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
# Preserve the current number of intervention's compensations
num_compensations = self.intervention.compensations.count()
self.client_user.post(new_url, post_data)
response = self.client_user.post(new_url, post_data)
self.assertEqual(302, response.status_code)
self.intervention.refresh_from_db()
self.assertEqual(num_compensations + 1, self.intervention.compensations.count())
new_compensation = self.intervention.compensations.get(identifier=test_id)
@@ -261,3 +262,26 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
self.assertIn(recorded, self.compensation.log.all())
self.assertEqual(pre_record_log_count + 1, self.compensation.log.count())
def test_non_editable_after_recording(self):
""" Tests that the compensation can not be edited after being recorded
User must be redirected to another page
Returns:
"""
self.assertIsNotNone(self.compensation)
self.assertFalse(self.compensation.is_recorded)
edit_url = reverse("compensation:edit", args=(self.compensation.id,))
response = self.client_user.get(edit_url)
has_redirect = response.status_code == 302
self.assertFalse(has_redirect)
self.compensation.intervention.set_recorded(self.user)
self.assertTrue(self.compensation.is_recorded)
edit_url = reverse("compensation:edit", args=(self.compensation.id,))
response = self.client_user.get(edit_url)
has_redirect = response.status_code == 302
self.assertTrue(has_redirect)
self.compensation.intervention.set_unrecorded(self.user)

View File

@@ -302,3 +302,27 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.assertEqual(pre_edit_account_log_count + 1, account.log.count())
self.assertEqual(intervention.log.first().action, UserAction.EDITED)
self.assertEqual(account.log.first().action, UserAction.EDITED)
def test_non_editable_after_recording(self):
""" Tests that the eco_account can not be edited after being recorded
User must be redirected to another page
Returns:
"""
self.assertIsNotNone(self.eco_account)
self.assertFalse(self.eco_account.is_recorded)
edit_url = reverse("compensation:acc:edit", args=(self.eco_account.id,))
response = self.client_user.get(edit_url)
has_redirect = response.status_code == 302
self.assertFalse(has_redirect)
self.eco_account.set_recorded(self.user)
self.assertTrue(self.eco_account.is_recorded)
edit_url = reverse("compensation:acc:edit", args=(self.eco_account.id,))
response = self.client_user.get(edit_url)
has_redirect = response.status_code == 302
self.assertTrue(has_redirect)
self.eco_account.set_unrecorded(self.user)

View File

@@ -1,4 +1,5 @@
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Sum
from django.http import HttpRequest, JsonResponse
from django.shortcuts import render
@@ -22,7 +23,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
DEADLINE_EDITED, RECORDED_BLOCKS_EDIT, PARAMS_INVALID
from konova.utils.user_checks import in_group
@@ -69,6 +70,19 @@ def new_view(request: HttpRequest, intervention_id: str = None):
"""
template = "compensation/form/view.html"
if intervention_id is not None:
try:
intervention = Intervention.objects.get(id=intervention_id)
except ObjectDoesNotExist:
messages.error(request, PARAMS_INVALID)
return redirect("home")
if intervention.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("intervention:detail", id=intervention_id)
data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST":
@@ -134,6 +148,13 @@ def edit_view(request: HttpRequest, id: str):
template = "compensation/form/view.html"
# Get object from db
comp = get_object_or_404(Compensation, id=id)
if comp.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("compensation:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditCompensationForm(request.POST or None, instance=comp)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp)
@@ -596,14 +617,12 @@ def report_view(request: HttpRequest, id: str):
instance=comp
)
parcels = comp.get_underlying_parcels()
qrcode_img = generate_qr_code(
request.build_absolute_uri(reverse("compensation:report", args=(id,))),
10
)
qrcode_img_lanis = generate_qr_code(
comp.get_LANIS_link(),
7
)
qrcode_url = request.build_absolute_uri(reverse("compensation:report", args=(id,)))
qrcode_img = generate_qr_code(qrcode_url, 10)
qrcode_lanis_url = comp.get_LANIS_link()
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
# Order states by surface
before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type")
after_states = comp.after_states.all().order_by("-surface").prefetch_related("biotope_type")
@@ -611,8 +630,14 @@ def report_view(request: HttpRequest, id: str):
context = {
"obj": comp,
"qrcode": qrcode_img,
"qrcode_lanis": qrcode_img_lanis,
"qrcode": {
"img": qrcode_img,
"url": qrcode_url,
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url,
},
"has_access": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,

View File

@@ -35,7 +35,8 @@ from konova.utils.generators import generate_qr_code
from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \
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
DEDUCTION_EDITED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED, \
RECORDED_BLOCKS_EDIT
from konova.utils.user_checks import in_group
@@ -145,6 +146,13 @@ def edit_view(request: HttpRequest, id: str):
template = "compensation/form/view.html"
# Get object from db
acc = get_object_or_404(EcoAccount, id=id)
if acc.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("compensation:acc:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditEcoAccountForm(request.POST or None, instance=acc)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc)
@@ -731,18 +739,16 @@ def report_view(request:HttpRequest, id: str):
instance=acc
)
parcels = acc.get_underlying_parcels()
qrcode_img = generate_qr_code(
request.build_absolute_uri(reverse("ema:report", args=(id,))),
10
)
qrcode_img_lanis = generate_qr_code(
acc.get_LANIS_link(),
7
)
qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,)))
qrcode_img = generate_qr_code(qrcode_url, 10)
qrcode_lanis_url = acc.get_LANIS_link()
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
# Order states by surface
before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent")
after_states = acc.after_states.all().order_by("-surface").select_related("biotope_type__parent")
actions = acc.actions.all().select_related("action_type__parent")
actions = acc.actions.all().prefetch_related("action_type__parent")
# Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier)
deductions = acc.deductions.all()\
@@ -752,8 +758,14 @@ def report_view(request:HttpRequest, id: str):
context = {
"obj": acc,
"qrcode": qrcode_img,
"qrcode_lanis": qrcode_img_lanis,
"qrcode": {
"img": qrcode_img,
"url": qrcode_url,
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url,
},
"has_access": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,

View File

@@ -14,7 +14,7 @@ from django.utils.translation import gettext_lazy as _
from compensation.forms.forms import AbstractCompensationForm, CompensationResponsibleFormMixin
from ema.models import Ema, EmaDocument
from intervention.models import Responsibility
from intervention.models import Responsibility, Handler
from konova.forms import SimpleGeomForm, NewDocumentModalForm
from user.models import UserActionLogEntry
@@ -31,7 +31,8 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
"title",
"conservation_office",
"conservation_file_number",
"handler",
"handler_type",
"handler_detail",
"comment",
]
@@ -53,7 +54,8 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
handler = self.cleaned_data.get("handler", None)
handler_type = self.cleaned_data.get("handler_type", None)
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)
comment = self.cleaned_data.get("comment", None)
@@ -63,6 +65,10 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
# Process the geometry form
geometry = geom_form.save(action)
handler = Handler.objects.create(
type=handler_type,
detail=handler_detail
)
responsible = Responsibility.objects.create(
handler=handler,
conservation_file_number=conservation_file_number,
@@ -105,7 +111,8 @@ class EditEmaForm(NewEmaForm):
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"handler": self.instance.responsible.handler,
"handler_type": self.instance.responsible.handler.type,
"handler_detail": self.instance.responsible.handler.detail,
"conservation_office": self.instance.responsible.conservation_office,
"conservation_file_number": self.instance.responsible.conservation_file_number,
"comment": self.instance.comment,
@@ -121,7 +128,8 @@ class EditEmaForm(NewEmaForm):
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
handler = self.cleaned_data.get("handler", None)
handler_type = self.cleaned_data.get("handler_type", None)
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)
comment = self.cleaned_data.get("comment", None)
@@ -132,7 +140,9 @@ class EditEmaForm(NewEmaForm):
geometry = geom_form.save(action)
# Update responsible data
self.instance.responsible.handler = handler
self.instance.responsible.handler.type = handler_type
self.instance.responsible.handler.detail = handler_detail
self.instance.responsible.handler.save()
self.instance.responsible.conservation_office = conservation_office
self.instance.responsible.conservation_file_number = conservation_file_number
self.instance.responsible.save()

View File

@@ -104,7 +104,7 @@ class EmaTable(BaseTable, TableRenderMixin):
"""
parcels = value.get_underlying_parcels().values_list(
"gmrkng",
"parcel_group__name",
flat=True
).distinct()
html = render_to_string(
@@ -151,9 +151,7 @@ class EmaTable(BaseTable, TableRenderMixin):
"""
html = ""
has_access = value.filter(
id=self.user.id
).exists()
has_access = record.is_shared_with(self.user)
html += self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),

View File

@@ -60,14 +60,13 @@
<th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle">
{% if obj.modified %}
{{obj.modified.timestamp|default_if_none:""|naturalday}}
{{obj.modified.timestamp|default_if_none:""}}
<br>
{{obj.modified.user.username}}
{% else %}
{{obj.created.timestamp|default_if_none:""|naturalday}}
{{obj.created.timestamp|default_if_none:""}}
<br>
{{obj.created.user.username}}
{% endif %}
</td>
</tr>
@@ -91,7 +90,7 @@
{% include 'map/geom_form.html' %}
</div>
<div class="row">
{% include 'konova/includes/parcels.html' %}
{% include 'konova/includes/parcels/parcels.html' %}
</div>
<div class="row">
{% include 'konova/includes/comment_card.html' %}

View File

@@ -38,17 +38,10 @@
{% include 'map/geom_form.html' %}
</div>
<div class="row">
{% include 'konova/includes/parcels.html' %}
{% include 'konova/includes/parcels/parcels.html' %}
</div>
<div class="row">
<div class="col-sm-6 col-md-6 col-lg-6">
<h4>{% trans 'Open in browser' %}</h4>
{{ qrcode|safe }}
</div>
<div class="col-sm-6 col-md-6 col-lg-6">
<h4>{% trans 'View in LANIS' %}</h4>
{{ qrcode_lanis|safe }}
</div>
{% include 'konova/includes/report/qrcodes.html' %}
</div>
</div>

View File

@@ -79,7 +79,9 @@ class EmaViewTestCase(CompensationViewTestCase):
# Create log entry
action = UserActionLogEntry.get_created_action(self.superuser)
# Create responsible data object
responsibility_data = Responsibility.objects.create()
responsibility_data = Responsibility.objects.create(
handler=self.handler
)
geometry = Geometry.objects.create()
self.ema = Ema.objects.create(
identifier="TEST",

View File

@@ -117,6 +117,32 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
self.assertEqual(pre_edit_log_count + 1, self.ema.log.count())
self.assertEqual(self.ema.log.first().action, UserAction.EDITED)
def test_non_editable_after_recording(self):
""" Tests that the EMA can not be edited after being recorded
User must be redirected to another page
Returns:
"""
self.superuser.groups.add(self.groups.get(name=ETS_GROUP))
self.assertIsNotNone(self.ema)
self.ema.share_with_user(self.superuser)
self.assertFalse(self.ema.is_recorded)
edit_url = reverse("ema:edit", args=(self.ema.id,))
response = self.client_user.get(edit_url)
has_redirect = response.status_code == 302
self.assertFalse(has_redirect)
self.ema.set_recorded(self.superuser)
self.assertTrue(self.ema.is_recorded)
edit_url = reverse("ema:edit", args=(self.ema.id,))
response = self.client_user.get(edit_url)
has_redirect = response.status_code == 302
self.assertTrue(has_redirect)
self.ema.set_unrecorded(self.superuser)
def test_recordability(self):
"""
This tests if the recordability of the Ema is triggered by the quality of it's data (e.g. not all fields filled)

View File

@@ -26,7 +26,7 @@ from konova.utils.generators import generate_qr_code
from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \
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
COMPENSATION_ACTION_EDITED, DEADLINE_EDITED, RECORDED_BLOCKS_EDIT
from konova.utils.user_checks import in_group
@@ -213,6 +213,13 @@ def edit_view(request: HttpRequest, id: str):
template = "compensation/form/view.html"
# Get object from db
ema = get_object_or_404(Ema, id=id)
if ema.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("ema:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditEmaForm(request.POST or None, instance=ema)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=ema)
@@ -563,14 +570,12 @@ def report_view(request:HttpRequest, id: str):
instance=ema,
)
parcels = ema.get_underlying_parcels()
qrcode_img = generate_qr_code(
request.build_absolute_uri(reverse("ema:report", args=(id,))),
10
)
qrcode_img_lanis = generate_qr_code(
ema.get_LANIS_link(),
7
)
qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,)))
qrcode_img = generate_qr_code(qrcode_url, 10)
qrcode_lanis_url = ema.get_LANIS_link()
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
# Order states by surface
before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type")
after_states = ema.after_states.all().order_by("-surface").prefetch_related("biotope_type")
@@ -578,8 +583,14 @@ def report_view(request:HttpRequest, id: str):
context = {
"obj": ema,
"qrcode": qrcode_img,
"qrcode_lanis": qrcode_img_lanis,
"qrcode": {
"img": qrcode_img,
"url": qrcode_url
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url
},
"has_access": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,

View File

@@ -25,12 +25,14 @@ class InterventionAdmin(BaseObjectAdmin):
"checked",
"recorded",
"users",
"geometry",
]
def get_readonly_fields(self, request, obj=None):
return super().get_readonly_fields(request, obj) + [
"checked",
"recorded",
"geometry",
]

View File

@@ -16,9 +16,9 @@ from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_PROCESS_TYPE_ID, CODELIST_LAW_ID, \
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID
from intervention.inputs import GenerateInput
from intervention.models import Intervention, Legal, Responsibility
from intervention.models import Intervention, Legal, Responsibility, Handler
from konova.forms import BaseForm, SimpleGeomForm
from user.models import UserActionLogEntry
@@ -138,12 +138,29 @@ class NewInterventionForm(BaseForm):
}
)
)
handler = forms.CharField(
label=_("Intervention handler"),
handler_type = forms.ModelChoiceField(
label=_("Intervention handler type"),
label_suffix="",
help_text=_("What type of handler is responsible for the intervention?"),
required=False,
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_HANDLER_ID],
),
widget=autocomplete.ModelSelect2(
url="codes-handler-autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
}
),
)
handler_detail = forms.CharField(
label=_("Intervention handler detail"),
label_suffix="",
max_length=255,
required=False,
help_text=_("Who performs the intervention"),
help_text=_("Detail input on the handler"),
widget=forms.TextInput(
attrs={
"placeholder": _("Company Mustermann"),
@@ -151,6 +168,7 @@ class NewInterventionForm(BaseForm):
}
)
)
registration_date = forms.DateField(
label=_("Registration date"),
label_suffix=_(""),
@@ -205,7 +223,8 @@ class NewInterventionForm(BaseForm):
title = self.cleaned_data.get("title", None)
_type = self.cleaned_data.get("type", None)
laws = self.cleaned_data.get("laws", None)
handler = self.cleaned_data.get("handler", None)
handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None)
registration_office = self.cleaned_data.get("registration_office", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
@@ -226,6 +245,10 @@ class NewInterventionForm(BaseForm):
# Then add the M2M laws to the object
legal_data.laws.set(laws)
handler = Handler.objects.create(
type=handler_type,
detail=handler_detail
)
# Create responsible data object
responsibility_data = Responsibility.objects.create(
registration_office=registration_office,
@@ -284,7 +307,8 @@ class EditInterventionForm(NewInterventionForm):
"title": self.instance.title,
"type": self.instance.legal.process_type,
"laws": list(self.instance.legal.laws.values_list("id", flat=True)),
"handler": self.instance.responsible.handler,
"handler_type": self.instance.responsible.handler.type,
"handler_detail": self.instance.responsible.handler.detail,
"registration_office": self.instance.responsible.registration_office,
"registration_file_number": self.instance.responsible.registration_file_number,
"conservation_office": self.instance.responsible.conservation_office,
@@ -313,7 +337,8 @@ class EditInterventionForm(NewInterventionForm):
title = self.cleaned_data.get("title", None)
process_type = self.cleaned_data.get("type", None)
laws = self.cleaned_data.get("laws", None)
handler = self.cleaned_data.get("handler", None)
handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None)
registration_office = self.cleaned_data.get("registration_office", None)
registration_file_number = self.cleaned_data.get("registration_file_number", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
@@ -328,7 +353,10 @@ class EditInterventionForm(NewInterventionForm):
self.instance.legal.laws.set(laws)
self.instance.legal.save()
self.instance.responsible.handler = handler
self.instance.responsible.handler.type = handler_type
self.instance.responsible.handler.detail = handler_detail
self.instance.responsible.handler.save()
self.instance.responsible.registration_office = registration_office
self.instance.responsible.registration_file_number = registration_file_number
self.instance.responsible.conservation_office = conservation_office

View File

@@ -427,13 +427,22 @@ class NewDeductionModalForm(BaseModalForm):
"""
super_result = super().is_valid()
acc = self.cleaned_data["account"]
intervention = self.cleaned_data["intervention"]
objects_valid = True
if not acc.recorded:
self.add_error(
"account",
_("Eco-account {} is not recorded yet. You can only deduct from recorded accounts.").format(acc.identifier)
)
return False
objects_valid = False
if intervention.is_recorded:
self.add_error(
"intervention",
_("Intervention {} is currently recorded. To change any data on it, the entry must be unrecorded.").format(intervention.identifier)
)
objects_valid = False
rest_surface = self._get_available_surface(acc)
form_surface = float(self.cleaned_data["surface"])
@@ -447,7 +456,7 @@ class NewDeductionModalForm(BaseModalForm):
format_german_float(rest_surface),
),
)
return is_valid_surface and super_result
return is_valid_surface and objects_valid and super_result
def __create_deduction(self):
""" Creates the deduction

View File

@@ -0,0 +1,64 @@
# Generated by Django 3.1.3 on 2022-03-03 08:56
from django.db import migrations, models, transaction
import django.db.models.deletion
import uuid
def migrate_handler(apps, schema_editor):
KonovaCode = apps.get_model('codelist', 'KonovaCode')
Responsibility = apps.get_model('intervention', 'Responsibility')
Handler = apps.get_model('intervention', 'Handler')
all_responsibs = Responsibility.objects.all()
if all_responsibs.exists():
handler_tmp_code = KonovaCode.objects.get(
atom_id=710185,
)
with transaction.atomic():
for resp in all_responsibs:
handler_old = resp.handler_old
handler = Handler.objects.create(
type=handler_tmp_code,
detail=handler_old
)
resp.handler = handler
resp.save()
class Migration(migrations.Migration):
dependencies = [
('codelist', '0001_initial'),
('intervention', '0003_intervention_teams'),
]
operations = [
migrations.CreateModel(
name='Handler',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('detail', models.CharField(blank=True, max_length=500, null=True)),
('type', models.ForeignKey(blank=True, limit_choices_to={'code_lists__in': [1052], 'is_archived': False, 'is_selectable': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, to='codelist.konovacode')),
],
options={
'abstract': False,
},
),
migrations.RenameField(
model_name='responsibility',
old_name='handler',
new_name='handler_old',
),
migrations.AddField(
model_name='responsibility',
name='handler',
field=models.ForeignKey(blank=True, help_text="Refers to 'Eingriffsverursacher' or 'Maßnahmenträger'", null=True, on_delete=django.db.models.deletion.SET_NULL, to='intervention.handler'),
),
migrations.RunPython(migrate_handler),
migrations.RemoveField(
model_name='responsibility',
name='handler_old'
),
]

View File

@@ -13,6 +13,7 @@ from django.db.models.fields.files import FieldFile
from django.urls import reverse
from django.utils import timezone
from intervention.tasks import celery_export_to_egon
from user.models import User
from django.db import models, transaction
from django.db.models import QuerySet
@@ -131,6 +132,16 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
self.add_log_entry_to_compensations(log_entry)
return log_entry
def send_data_to_egon(self):
""" Performs the export to rabbitmq of this intervention's data
FOLLOWING BACKWARDS COMPATIBILITY LOGIC
Returns:
"""
celery_export_to_egon.delay(self.id)
def set_recorded(self, user: User) -> UserActionLogEntry:
log_entry = super().set_recorded(user)
self.add_log_entry_to_compensations(log_entry)
@@ -171,6 +182,8 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
intervention=self,
)
self.mark_as_edited(user, form.request, edit_comment=PAYMENT_ADDED)
self.send_data_to_egon()
return pay
def add_revocation(self, form):
@@ -335,6 +348,7 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
with transaction.atomic():
payment.delete()
self.mark_as_edited(user, request=form.request, edit_comment=PAYMENT_REMOVED)
self.send_data_to_egon()
class InterventionDocument(AbstractDocument):

View File

@@ -6,10 +6,42 @@ Created on: 15.11.21
"""
from django.db import models
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_REGISTRATION_OFFICE_ID
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_REGISTRATION_OFFICE_ID, \
CODELIST_HANDLER_ID
from konova.models import UuidModel
from konova.utils.message_templates import UNKNOWN, NO_DETAILS
class Handler(UuidModel):
""" The handler of an entry
Refers to 'Eingriffsverursacher' or 'Maßnahmenträger'
"""
type = models.ForeignKey(
KonovaCode,
on_delete=models.SET_NULL,
null=True,
blank=True,
limit_choices_to={
"code_lists__in": [CODELIST_HANDLER_ID],
"is_selectable": True,
"is_archived": False,
}
)
detail = models.CharField(
max_length=500,
null=True,
blank=True,
)
def __str__(self):
detail = self.detail or NO_DETAILS
_type = self.type.long_name if self.type is not None else UNKNOWN
return f'{_type}, {detail}'
class Responsibility(UuidModel):
@@ -43,11 +75,17 @@ class Responsibility(UuidModel):
}
)
conservation_file_number = models.CharField(max_length=1000, blank=True, null=True)
handler = models.CharField(max_length=500, null=True, blank=True, help_text="Refers to 'Eingriffsverursacher' or 'Maßnahmenträger'")
handler = models.ForeignKey(
Handler,
null=True,
blank=True,
help_text="Refers to 'Eingriffsverursacher' or 'Maßnahmenträger'",
on_delete=models.SET_NULL,
)
def __str__(self):
return "ZB: {} | ETS: {} | Handler: {}".format(
self.registration_office,
self.conservation_office,
self.handler
str(self.handler)
)

View File

@@ -6,4 +6,11 @@ Created on: 30.11.20
"""
INTERVENTION_IDENTIFIER_LENGTH = 6
INTERVENTION_IDENTIFIER_TEMPLATE = "EIV-{}"
INTERVENTION_IDENTIFIER_TEMPLATE = "EIV-{}"
# EGON connection settings via rabbitmq
# NEEDED FOR BACKWARDS COMPATIBILITY
EGON_RABBITMQ_HOST = "CHANGE_ME"
EGON_RABBITMQ_PORT = "CHANGE_ME"
EGON_RABBITMQ_USER = "CHANGE_ME"
EGON_RABBITMQ_PW = "CHANGE_ME"

View File

@@ -131,7 +131,7 @@ class InterventionTable(BaseTable, TableRenderMixin):
"""
parcels = value.get_underlying_parcels().values_list(
"gmrkng",
"parcel_group__name",
flat=True
).distinct()
html = render_to_string(
@@ -177,9 +177,7 @@ class InterventionTable(BaseTable, TableRenderMixin):
"""
html = ""
has_access = value.filter(
id=self.user.id
).exists()
has_access = record.is_shared_with(self.user)
html += self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),

18
intervention/tasks.py Normal file
View File

@@ -0,0 +1,18 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 21.03.22
"""
from celery import shared_task
from intervention.utils.egon_export import EgonExporter
@shared_task
def celery_export_to_egon(intervention_id: str):
from intervention.models import Intervention
intervention = Intervention.objects.get(id=intervention_id)
egon_exporter = EgonExporter(intervention)
egon_exporter.export_to_rabbitmq()

View File

@@ -1,5 +1,5 @@
{% extends 'base.html' %}
{% load i18n l10n static fontawesome_5 humanize %}
{% load i18n l10n static fontawesome_5 %}
{% block head %}
{% comment %}
@@ -106,9 +106,15 @@
<tr>
<th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle">
{{obj.created.timestamp|default_if_none:""|naturalday}}
<br>
{{obj.created.user.username}}
{% if obj.modified %}
{{obj.modified.timestamp|default_if_none:""}}
<br>
{{obj.modified.user.username}}
{% else %}
{{obj.created.timestamp|default_if_none:""}}
<br>
{{obj.created.user.username}}
{% endif %}
</td>
</tr>
<tr>
@@ -131,7 +137,7 @@
{% include 'map/geom_form.html' %}
</div>
<div class="row">
{% include 'konova/includes/parcels.html' %}
{% include 'konova/includes/parcels/parcels.html' %}
</div>
<div class="row">
{% include 'konova/includes/comment_card.html' %}

View File

@@ -97,17 +97,10 @@
{% include 'map/geom_form.html' %}
</div>
<div class="row">
{% include 'konova/includes/parcels.html' %}
{% include 'konova/includes/parcels/parcels.html' %}
</div>
<div class="row">
<div class="col-sm-6 col-md-6 col-lg-6">
<h4>{% trans 'Open in browser' %}</h4>
{{ qrcode|safe }}
</div>
<div class="col-sm-6 col-md-6 col-lg-6">
<h4>{% trans 'View in LANIS' %}</h4>
{{ qrcode_lanis|safe }}
</div>
{% include 'konova/includes/report/qrcodes.html' %}
</div>
</div>

View File

@@ -89,6 +89,30 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
self.assertIn(self.superuser, obj.users.all())
self.assertEqual(1, obj.users.count())
def test_non_editable_after_recording(self):
""" Tests that the intervention can not be edited after being recorded
User must be redirected to another page
Returns:
"""
self.assertIsNotNone(self.intervention)
self.assertFalse(self.intervention.is_recorded)
edit_url = reverse("intervention:edit", args=(self.intervention.id,))
response = self.client_user.get(edit_url)
has_redirect = response.status_code == 302
self.assertFalse(has_redirect)
self.intervention.set_recorded(self.user)
self.assertTrue(self.intervention.is_recorded)
edit_url = reverse("intervention:edit", args=(self.intervention.id,))
response = self.client_user.get(edit_url)
has_redirect = response.status_code == 302
self.assertTrue(has_redirect)
self.intervention.set_unrecorded(self.user)
def test_checkability(self):
""" Tests that the intervention can only be checked if all required data has been added

View File

@@ -0,0 +1,250 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 07.03.22
"""
import base64
import json
import pika
import xmltodict
from django.db.models import Sum
from intervention.settings import EGON_RABBITMQ_HOST, EGON_RABBITMQ_USER, EGON_RABBITMQ_PW, EGON_RABBITMQ_PORT
from konova.sub_settings.django_settings import DEFAULT_DATE_FORMAT
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
class EgonExporter:
"""
EGON is the payment management system of SNU RLP. Due to compatibility reasons we need to provide the old style
of data transmission between KSP and EGON:
1. Create GML from intervention object
2. Send created GML to the appropriate RabbitMQ channel
"""
intervention = None
gml_builder = None
def __init__(self, intervention):
self.intervention = intervention
self.gml_builder = EgonGmlBuilder(intervention)
def export_to_rabbitmq(self):
""" Sends the exporter gml to message broker rabbitmq to be fetched by EGON application from there
Returns:
"""
msg = {
"nachricht": self.gml_builder.gml,
}
msg = json.dumps(msg)
print(msg)
credentials = pika.PlainCredentials(EGON_RABBITMQ_USER, EGON_RABBITMQ_PW)
params = pika.ConnectionParameters(
EGON_RABBITMQ_HOST,
EGON_RABBITMQ_PORT,
"/",
credentials
)
conn = pika.BlockingConnection(params)
channel = conn.channel()
channel.basic_publish(
exchange="",
routing_key="KSP_EGON",
body=msg.encode("utf-8"),
)
conn.close()
class EgonGmlBuilder:
"""
Creates the GML for EGON export
"""
intervention = None
gml = None
def __init__(self, intervention):
self.intervention = intervention
self.gml = self.build_gml()
def _gen_flurstuecksKennzeichen(self, parcel):
""" Generates oneo:flurstuecksKennzeichen to provide backwards compatibility
Args:
parcel (Parcel): The requested parcel
Returns:
str
"""
gmrkng_code = "{0:06d}".format(int(parcel.parcel_group.key) or 0)
flr_code = "{0:03d}".format(int(parcel.flr or 0))
flrstckzhlr_code = "{0:05d}".format(int(parcel.flrstck_zhlr or 0))
flrstcknnr_code = "{0:06d}".format(int(parcel.flrstck_nnr or 0))
return gmrkng_code + flr_code + flrstckzhlr_code + flrstcknnr_code
def _sum_all_payments(self):
all_payments = self.intervention.payments.aggregate(
summed=Sum("amount")
)["summed"]
return all_payments
def _gen_kompensationsArt(self) -> (str, int):
comp_type = "Ersatzzahlung"
comp_type_code = 774898901
if self.intervention.compensations.exists():
comp_type += " und Kompensation"
comp_type_code = 771655351
return comp_type, comp_type_code
def _gen_geometry_list(self):
geom = self.intervention.geometry.geom
geom.transform(DEFAULT_SRID_RLP)
geoms_list = [
{
"gml:Polygon": {
"gml:exterior": {
"gml:LinearRing": {
"gml:posList": " ".join([f"{str(coord[0])} {str(coord[1])}" for coord in coords[0]])
}
}
}
} for coords in geom.coords
]
return geoms_list
def _gen_raumreferenz(self):
parcels = self.intervention.get_underlying_parcels()
spatial_reference_list = [
{
"oneo:datumAbgleich": None,
"oneo:ortsangabe": {
"oneo:Ortsangaben": {
"oneo:kreisSchluessel": {
"xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/588/{parcel.district.key}",
},
"oneo:gemeindeSchluessel": {
"xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/910/{parcel.municipal.key}",
},
"oneo:verbandsgemeindeSchluessel": {
"xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/589/{None}",
},
"oneo:flurstuecksKennzeichen": self._gen_flurstuecksKennzeichen(parcel),
}
},
} for parcel in parcels
]
return spatial_reference_list
def _gen_foto(self):
revoc_docs, regular_docs = self.intervention.get_documents()
docs_list = [
{
"oneo:Foto": {
"oneo:aufnahmezeitpunkt": doc.date_of_creation.strftime(DEFAULT_DATE_FORMAT),
"oneo:bemerkung": doc.comment,
"oneo:fotoverweis": base64.b64encode(doc.file.read()).decode("utf-8"),
"oneo:dateiname": doc.title,
"oneo:hauptfoto": False,
}
} for doc in regular_docs
]
return docs_list
def build_gml(self):
comp_type, comp_type_code = self._gen_kompensationsArt()
payment = self.intervention.payments.first()
payment_date = None
if payment is not None:
payment_date = payment.due_on
payment_date = payment_date.strftime(DEFAULT_DATE_FORMAT)
cons_office = self.intervention.responsible.conservation_office
reg_office = self.intervention.responsible.registration_office
law = self.intervention.legal.laws.first()
process_type = self.intervention.legal.process_type
handler = self.intervention.responsible.handler
reg_date = self.intervention.legal.registration_date
bind_date = self.intervention.legal.binding_date
xml_dict = {
"wfs:FeatureCollection": {
"@xmlns:wfs": "http://www.opengis.net/wfs",
"@xmlns:xlink": "http://www.w3.org/1999/xlink",
"@xmlns:oneo": "http://www.osiris-projekt.rlp.de/oneo",
"@xmlns:gmlexr": "http://www.opengis.net/gml/3.3/exr",
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
"@xmlns:gml": "http://www.opengis.net/gml/3.2",
"oneo:Eingriffsverfahren": {
"@gml:id": self.intervention.identifier,
"oneo:azEintragungsstelle": self.intervention.responsible.conservation_file_number,
"oneo:azZulassungsstelle": self.intervention.responsible.registration_file_number,
"oneo:bemerkungZulassungsstelle": None,
"oneo:eintragungsstelle": {
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/907/{cons_office.atom_id if cons_office else None}",
"#text": cons_office.long_name if cons_office else None
},
"oneo:zulassungsstelle": {
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/1053/{reg_office.atom_id if reg_office else None}",
"#text": reg_office.long_name if reg_office else None
},
"oneo:ersatzzahlung": self._sum_all_payments(),
"oneo:kompensationsart": {
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/88140/{comp_type_code}",
"#text": comp_type
},
"oneo:verfahrensrecht": {
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/1048/{law.atom_id if law else None}",
"#text": law.short_name if law else None
},
"oneo:verfahrenstyp": {
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/44382/{process_type.atom_id if process_type else None}",
"#text": process_type.long_name if process_type else None,
},
"oneo:eingreifer": {
"oneo:Eingreifer": {
"oneo:art": {
"@xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/1053/{handler.type.atom_id if handler.type else None}",
"#text": handler.type.long_name if handler.type else None,
},
"oneo:bemerkung": handler.detail if handler else None,
}
},
"oneo:erfasser": {
"oneo:Erfasser": {
"oneo:name": None,
"oneo:bemerkung": None,
}
},
"oneo:zulassung": {
"oneo:Zulassungstermin": {
"oneo:bauBeginn": payment_date,
"oneo:erlass": reg_date.strftime(DEFAULT_DATE_FORMAT) if reg_date else None,
"oneo:rechtsKraft": bind_date.strftime(DEFAULT_DATE_FORMAT) if bind_date else None,
}
},
"oneo:geometrie": {
"gml:multiSurfaceProperty": {
"gml:MultiPolygon": {
"@srsName": f"http://www.opengis.net/gml/srs/epsg.xml#{DEFAULT_SRID_RLP}",
"gml:polygonMember": self._gen_geometry_list(),
}
},
},
"oneo:kennung": self.intervention.identifier,
"oneo:bezeichnung": self.intervention.title,
"oneo:bemerkung": self.intervention.comment,
"oneo:verantwortlicheStelle": None,
"oneo:veroffentlichtAm": None,
"oneo:raumreferenz": {
"oneo:Raumreferenz": self._gen_raumreferenz(),
},
"oneo:foto": self._gen_foto(),
}
},
}
gml = xmltodict.unparse(xml_dict)
return gml

View File

@@ -18,7 +18,8 @@ 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
COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, DEDUCTION_EDITED, REVOCATION_EDITED, DOCUMENT_EDITED, \
RECORDED_BLOCKS_EDIT
from konova.utils.user_checks import in_group
@@ -302,6 +303,13 @@ def edit_view(request: HttpRequest, id: str):
template = "intervention/form/view.html"
# Get object from db
intervention = get_object_or_404(Intervention, id=id)
if intervention.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("intervention:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditInterventionForm(request.POST or None, instance=intervention)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention)
@@ -693,19 +701,22 @@ def report_view(request:HttpRequest, id: str):
distinct_deductions = intervention.deductions.all().distinct(
"account"
)
qrcode_img = generate_qr_code(
request.build_absolute_uri(reverse("intervention:report", args=(id,))),
10
)
qrcode_img_lanis = generate_qr_code(
intervention.get_LANIS_link(),
7
)
qrcode_url = request.build_absolute_uri(reverse("intervention:report", args=(id,)))
qrcode_img = generate_qr_code(qrcode_url, 10)
qrcode_lanis_url = intervention.get_LANIS_link()
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
context = {
"obj": intervention,
"deductions": distinct_deductions,
"qrcode": qrcode_img,
"qrcode_lanis": qrcode_img_lanis,
"qrcode": {
"img": qrcode_img,
"url": qrcode_url,
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url,
},
"geom_form": geom_form,
"parcels": parcels,
TAB_TITLE_IDENTIFIER: tab_title,

View File

@@ -7,7 +7,8 @@ Created on: 22.07.21
"""
from django.contrib import admin
from konova.models import Geometry, Deadline, GeometryConflict, Parcel, District
from konova.models import Geometry, Deadline, GeometryConflict, Parcel, District, Municipal, ParcelGroup
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
from user.models import UserAction
@@ -16,13 +17,28 @@ class GeometryAdmin(admin.ModelAdmin):
list_display = [
"id",
"created",
"st_area",
]
readonly_fields = [
"st_area",
"created",
"modified",
]
def st_area(self, obj):
val = None
geom = obj.geom
if geom is not None:
geom.transform(ct=DEFAULT_SRID_RLP)
val = geom.area
return val
st_area.short_description = f"Area (srid={DEFAULT_SRID_RLP})"
class ParcelAdmin(admin.ModelAdmin):
list_display = [
"id",
"gmrkng",
"parcel_group",
"flr",
"flrstck_nnr",
"flrstck_zhlr",
@@ -32,9 +48,27 @@ class ParcelAdmin(admin.ModelAdmin):
class DistrictAdmin(admin.ModelAdmin):
list_display = [
"name",
"key",
"id",
]
class MunicipalAdmin(admin.ModelAdmin):
list_display = [
"name",
"key",
"district",
"id",
]
class ParcelGroupAdmin(admin.ModelAdmin):
list_display = [
"name",
"key",
"municipal",
"id",
"gmnd",
"krs",
]
@@ -105,5 +139,7 @@ class BaseObjectAdmin(BaseResourceAdmin):
#admin.site.register(Geometry, GeometryAdmin)
#admin.site.register(Parcel, ParcelAdmin)
#admin.site.register(District, DistrictAdmin)
#admin.site.register(Municipal, MunicipalAdmin)
#admin.site.register(ParcelGroup, ParcelGroupAdmin)
#admin.site.register(GeometryConflict, GeometryConflictAdmin)
#admin.site.register(Deadline, DeadlineAdmin)

View File

@@ -17,7 +17,7 @@ from django.db.models import Q
from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_LAW_ID, \
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_PROCESS_TYPE_ID, \
CODELIST_BIOTOPES_EXTRA_CODES_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID
CODELIST_BIOTOPES_EXTRA_CODES_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_HANDLER_ID
from compensation.models import EcoAccount
from intervention.models import Intervention
@@ -52,14 +52,16 @@ class InterventionAutocomplete(Select2QuerySetView):
"""
def get_queryset(self):
if self.request.user.is_anonymous:
user = self.request.user
if user.is_anonymous:
return Intervention.objects.none()
qs = Intervention.objects.filter(
deleted=None,
users__in=[self.request.user],
Q(deleted=None) &
Q(users__in=[user]) |
Q(teams__in=user.teams.all())
).order_by(
"identifier"
)
).distinct()
if self.q:
qs = qs.filter(
Q(identifier__icontains=self.q) |
@@ -357,3 +359,18 @@ class ConservationOfficeCodeAutocomplete(KonovaCodeAutocomplete):
def get_result_label(self, result):
return f"{result.long_name} ({result.short_name})"
class HandlerCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_HANDLER_ID
super().__init__(*args, **kwargs)
def get_result_label(self, result):
return result.long_name

View File

@@ -145,26 +145,20 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
class Meta:
abstract = True
def _filter_parcel_reference(self, queryset, name, value, filter_value) -> QuerySet:
""" Filters the parcel entries by a given filter_value.
filter_value may already include further filter annotations like 'xy__icontains'
def _filter_parcel_reference(self, queryset, filter_q) -> QuerySet:
""" Filters the parcel entries by a given filter_q
Args:
queryset ():
name ():
value ():
filter_value ():
queryset (QuerySet): The queryset
filter_q (Q): The Q-style filter expression
Returns:
"""
_filter = {
filter_value: value
}
matching_parcels = Parcel.objects.filter(
**_filter
filter_q
)
related_geoms = matching_parcels.values(
"geometries"
).distinct()
@@ -185,8 +179,9 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
"""
matching_districts = District.objects.filter(
krs__icontains=value
)
Q(name__icontains=value) |
Q(key__icontains=value)
).distinct()
matching_parcels = Parcel.objects.filter(
district__in=matching_districts
)
@@ -209,7 +204,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
Returns:
"""
queryset = self._filter_parcel_reference(queryset, name, value, "gmrkng__icontains")
queryset = self._filter_parcel_reference(
queryset,
Q(parcel_group__name__icontains=value) | Q(parcel_group__key__icontains=value),
)
return queryset
def filter_parcel(self, queryset, name, value) -> QuerySet:
@@ -224,7 +222,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
"""
value = value.replace("-", "")
queryset = self._filter_parcel_reference(queryset, name, value, "flr")
queryset = self._filter_parcel_reference(
queryset,
Q(flr=value),
)
return queryset
def filter_parcel_counter(self, queryset, name, value) -> QuerySet:
@@ -239,7 +240,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
"""
value = value.replace("-", "")
queryset = self._filter_parcel_reference(queryset, name, value, "flrstck_zhlr")
queryset = self._filter_parcel_reference(
queryset,
Q(flrstck_zhlr=value)
)
return queryset
def filter_parcel_number(self, queryset, name, value) -> QuerySet:
@@ -254,7 +258,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
"""
value = value.replace("-", "")
queryset = self._filter_parcel_reference(queryset, name, value, "flrstck_nnr")
queryset = self._filter_parcel_reference(
queryset,
Q(flrstck_nnr=value),
)
return queryset

View File

@@ -57,6 +57,8 @@ class BaseForm(forms.Form):
self.has_required_fields = True
break
self.check_for_recorded_instance()
@abstractmethod
def save(self):
# To be implemented in subclasses!
@@ -136,6 +138,38 @@ class BaseForm(forms.Form):
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(
@@ -410,7 +444,6 @@ class NewDocumentModalForm(BaseModalForm):
super().__init__(*args, **kwargs)
self.form_title = _("Add new document")
self.form_caption = _("")
self.template = "modal/modal_form.html"
self.form_attrs = {
"enctype": "multipart/form-data", # important for file upload
}
@@ -597,4 +630,12 @@ class RecordModalForm(BaseModalForm):
self.instance.set_unrecorded(self.user)
else:
self.instance.set_recorded(self.user)
return self.instance
return self.instance
def check_for_recorded_instance(self):
""" Overwrite the check method for doing nothing on the RecordModalForm
Returns:
"""
pass

View File

@@ -9,7 +9,7 @@ from compensation.models import CompensationState, Compensation, EcoAccount, Com
from ema.models import Ema
from intervention.models import Intervention
from konova.management.commands.setup import BaseKonovaCommand
from konova.models import Deadline, Geometry, Parcel, District
from konova.models import Deadline, Geometry, Parcel, District, Municipal, ParcelGroup
from user.models import UserActionLogEntry, UserAction
@@ -271,13 +271,26 @@ class Command(BaseKonovaCommand):
self._write_success("No unused states found.")
self._break_line()
def __sanitize_parcel_sub_type(self, cls):
unrelated_entries = cls.objects.filter(
parcels=None,
)
num_unrelated_entries = unrelated_entries.count()
cls_name = cls.__name__
if num_unrelated_entries > 0:
self._write_error(f"Found {num_unrelated_entries} unrelated {cls_name} entries. Delete now...")
unrelated_entries.delete()
self._write_success(f"Unrelated {cls_name} deleted.")
else:
self._write_success(f"No unrelated {cls_name} found.")
def sanitize_parcels_and_districts(self):
""" Removes unattached parcels and districts
Returns:
"""
self._write_warning("=== Sanitize parcels and districts ===")
self._write_warning("=== Sanitize administrative spatial references ===")
unrelated_parcels = Parcel.objects.filter(
geometries=None,
)
@@ -289,16 +302,12 @@ class Command(BaseKonovaCommand):
else:
self._write_success("No unrelated parcels found.")
unrelated_districts = District.objects.filter(
parcels=None,
)
num_unrelated_districts = unrelated_districts.count()
if num_unrelated_districts > 0:
self._write_error(f"Found {num_unrelated_districts} unrelated district entries. Delete now...")
unrelated_districts.delete()
self._write_success("Unrelated districts deleted.")
else:
self._write_success("No unrelated districts found.")
self._break_line()
sub_types = [
District,
Municipal,
ParcelGroup
]
for sub_type in sub_types:
self.__sanitize_parcel_sub_type(sub_type)
self._break_line()

View File

@@ -5,6 +5,10 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.01.22
"""
import datetime
from django.contrib.gis.db.models.functions import Area
from konova.management.commands.setup import BaseKonovaCommand
from konova.models import Geometry, Parcel, District
@@ -23,12 +27,21 @@ class Command(BaseKonovaCommand):
num_parcels_before = Parcel.objects.count()
num_districts_before = District.objects.count()
self._write_warning("=== Update parcels and districts ===")
# Order geometries by size to process smaller once at first
geometries = Geometry.objects.all().exclude(
geom=None
).annotate(area=Area("geom")).order_by(
'area'
)
self._write_warning(f"Process parcels for {geometries.count()} geometry entries now ...")
i = 0
num_geoms = geometries.count()
for geometry in geometries:
self._write_warning(f"--- {datetime.datetime.now()} Process {geometry.id} now ...")
geometry.update_parcels()
self._write_warning(f"--- Processed {geometry.get_underlying_parcels().count()} underlying parcels")
i += 1
self._write_warning(f"--- {i}/{num_geoms} processed")
num_parcels_after = Parcel.objects.count()
num_districts_after = District.objects.count()

View File

@@ -0,0 +1,71 @@
# Generated by Django 3.1.3 on 2022-04-11 06:35
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('konova', '0005_auto_20220216_0856'),
]
operations = [
migrations.CreateModel(
name='Municipal',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('key', models.IntegerField(blank=True, help_text='Represents Gemeindeschlüssel', null=True)),
('name', models.CharField(blank=True, help_text='Gemeinde', max_length=1000, null=True)),
],
options={
'abstract': False,
},
),
migrations.RenameField(
model_name='district',
old_name='krs',
new_name='name',
),
migrations.RemoveField(
model_name='district',
name='gmnd',
),
migrations.RemoveField(
model_name='parcel',
name='gmrkng',
),
migrations.AddField(
model_name='district',
name='key',
field=models.IntegerField(blank=True, help_text='Represents Kreisschlüssel', null=True),
),
migrations.CreateModel(
name='ParcelGroup',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('key', models.IntegerField(blank=True, help_text='Represents Gemarkungsschlüssel', null=True)),
('name', models.CharField(blank=True, help_text='Gemarkung', max_length=1000, null=True)),
('municipal', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='konova.municipal')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='municipal',
name='district',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='konova.district'),
),
migrations.AddField(
model_name='parcel',
name='municipal',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parcels', to='konova.municipal'),
),
migrations.AddField(
model_name='parcel',
name='parcel_group',
field=models.ForeignKey(blank=True, help_text='Gemarkung', null=True, on_delete=django.db.models.deletion.SET_NULL, to='konova.parcelgroup'),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 3.1.3 on 2022-04-11 06:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('konova', '0006_auto_20220411_0835'),
]
operations = [
migrations.AlterField(
model_name='district',
name='key',
field=models.CharField(blank=True, help_text='Represents Kreisschlüssel', max_length=255, null=True),
),
migrations.AlterField(
model_name='municipal',
name='key',
field=models.CharField(blank=True, help_text='Represents Gemeindeschlüssel', max_length=255, null=True),
),
migrations.AlterField(
model_name='parcelgroup',
name='key',
field=models.CharField(blank=True, help_text='Represents Gemarkungsschlüssel', max_length=255, null=True),
),
]

View File

@@ -0,0 +1,48 @@
# Generated by Django 3.1.3 on 2022-04-11 07:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('konova', '0007_auto_20220411_0848'),
]
operations = [
migrations.AlterField(
model_name='municipal',
name='key',
field=models.CharField(blank=True, help_text='Represents Kreisschlüssel', max_length=255, null=True),
),
migrations.AlterField(
model_name='municipal',
name='name',
field=models.CharField(blank=True, help_text='Kreis', max_length=1000, null=True),
),
migrations.AlterField(
model_name='parcel',
name='flr',
field=models.IntegerField(blank=True, help_text='Flur', null=True),
),
migrations.AlterField(
model_name='parcel',
name='flrstck_nnr',
field=models.IntegerField(blank=True, help_text='Flurstücksnenner', null=True),
),
migrations.AlterField(
model_name='parcel',
name='flrstck_zhlr',
field=models.IntegerField(blank=True, help_text='Flurstückszähler', null=True),
),
migrations.AlterField(
model_name='parcelgroup',
name='key',
field=models.CharField(blank=True, help_text='Represents Kreisschlüssel', max_length=255, null=True),
),
migrations.AlterField(
model_name='parcelgroup',
name='name',
field=models.CharField(blank=True, help_text='Kreis', max_length=1000, null=True),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.1.3 on 2022-04-11 08:04
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('konova', '0008_auto_20220411_0914'),
]
operations = [
migrations.AlterField(
model_name='parcel',
name='parcel_group',
field=models.ForeignKey(blank=True, help_text='Gemarkung', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parcels', to='konova.parcelgroup'),
),
]

View File

@@ -20,6 +20,9 @@ class Geometry(BaseResource):
from konova.settings import DEFAULT_SRID
geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID)
def __str__(self):
return str(self.id)
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.check_for_conflicts()
@@ -99,7 +102,7 @@ class Geometry(BaseResource):
Returns:
"""
from konova.models import Parcel, District, ParcelIntersection
from konova.models import Parcel, District, ParcelIntersection, Municipal, ParcelGroup
parcel_fetcher = ParcelWFSFetcher(
geometry_id=self.id,
)
@@ -110,20 +113,38 @@ class Geometry(BaseResource):
_now = timezone.now()
underlying_parcels = []
for result in fetched_parcels:
fetched_parcel = result[typename]
parcel_properties = result["properties"]
# There could be parcels which include the word 'Flur',
# which needs to be deleted and just keep the numerical values
## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE!
flr_val = fetched_parcel["ave:flur"].replace("Flur ", "")
parcel_obj = Parcel.objects.get_or_create(
gmrkng=fetched_parcel["ave:gemarkung"],
flr=flr_val,
flrstck_nnr=fetched_parcel['ave:flstnrnen'],
flrstck_zhlr=fetched_parcel['ave:flstnrzae'],
)[0]
flr_val = parcel_properties["flur"].replace("Flur ", "")
district = District.objects.get_or_create(
gmnd=fetched_parcel["ave:gemeinde"],
krs=fetched_parcel["ave:kreis"],
key=parcel_properties["kreisschl"],
name=parcel_properties["kreis"],
)[0]
municipal = Municipal.objects.get_or_create(
key=parcel_properties["gmdschl"],
name=parcel_properties["gemeinde"],
district=district,
)[0]
parcel_group = ParcelGroup.objects.get_or_create(
key=parcel_properties["gemaschl"],
name=parcel_properties["gemarkung"],
municipal=municipal,
)[0]
flrstck_nnr = parcel_properties['flstnrnen']
if not flrstck_nnr:
flrstck_nnr = None
flrstck_zhlr = parcel_properties['flstnrzae']
if not flrstck_zhlr:
flrstck_zhlr = None
parcel_obj = Parcel.objects.get_or_create(
district=district,
municipal=municipal,
parcel_group=parcel_group,
flr=flr_val,
flrstck_nnr=flrstck_nnr,
flrstck_zhlr=flrstck_zhlr,
)[0]
parcel_obj.district = district
parcel_obj.updated_on = _now
@@ -155,9 +176,10 @@ class Geometry(BaseResource):
parcels = self.parcels.filter(
parcelintersection__calculated_on__isnull=False,
).prefetch_related(
"district"
"district",
"municipal",
).order_by(
"gmrkng",
"municipal__name",
)
return parcels

View File

@@ -289,6 +289,8 @@ class RecordableObjectMixin(models.Model):
from user.models import UserActionLogEntry
if self.recorded:
return None
self.unshare_with_default_users()
action = UserActionLogEntry.get_recorded_action(user)
self.recorded = action
self.save()
@@ -335,6 +337,15 @@ class RecordableObjectMixin(models.Model):
"""
raise NotImplementedError("Implement this in the subclass!")
@property
def is_recorded(self):
""" Getter for record status as property
Returns:
"""
return self.recorded is not None
class CheckableObjectMixin(models.Model):
# Checks - Refers to "Genehmigen" but optional
@@ -608,6 +619,26 @@ class ShareableObjectMixin(models.Model):
"""
raise NotImplementedError("Must be implemented in subclasses!")
def unshare_with_default_users(self):
""" Removes all shared users from direct shared access which are only default group users
Returns:
"""
from konova.utils.user_checks import is_default_group_only
users = self.shared_users
cleaned_users = []
default_users = []
for user in users:
if not is_default_group_only(user):
cleaned_users.append(user)
else:
default_users.append(user)
self.share_with_user_list(cleaned_users)
for user in default_users:
celery_send_mail_shared_access_removed.delay(self.identifier, self.title, user.id)
class GeoReferencedMixin(models.Model):
geometry = models.ForeignKey("konova.Geometry", null=True, blank=True, on_delete=models.SET_NULL)

View File

@@ -10,8 +10,64 @@ from django.db import models
from konova.models import UuidModel
class AdministrativeSpatialReference(models.Model):
key = models.CharField(
max_length=255,
help_text="Represents Kreisschlüssel",
null=True,
blank=True
)
name = models.CharField(
max_length=1000,
help_text="Kreis",
null=True,
blank=True,
)
class Meta:
abstract = True
def __str__(self):
return f"{self.name} ({self.key})"
@property
def table_str(self):
return f"{self.name} ({self.key})"
class District(UuidModel, AdministrativeSpatialReference):
""" The model District refers to "Kreis"
"""
pass
class Municipal(UuidModel, AdministrativeSpatialReference):
""" The model Municipal refers to "Gemeinde"
"""
district = models.ForeignKey(
District,
on_delete=models.SET_NULL,
null=True,
blank=True,
)
class ParcelGroup(UuidModel, AdministrativeSpatialReference):
""" The model ParcelGroup refers to "Gemarkung", which is defined as a loose group of parcels
"""
municipal = models.ForeignKey(
Municipal,
on_delete=models.SET_NULL,
null=True,
blank=True,
)
class Parcel(UuidModel):
""" The Parcel model holds administrative data on the covered properties.
""" The Parcel model holds administrative data on covered properties.
Due to the unique but relevant naming of the administrative data, we have to use these namings as field
names in german. Any try to translate them to English result in strange or insufficient translations.
@@ -24,59 +80,34 @@ class Parcel(UuidModel):
"""
geometries = models.ManyToManyField("konova.Geometry", blank=True, related_name="parcels", through='ParcelIntersection')
district = models.ForeignKey("konova.District", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels")
gmrkng = models.CharField(
max_length=1000,
municipal = models.ForeignKey("konova.Municipal", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels")
parcel_group = models.ForeignKey(
"konova.ParcelGroup",
on_delete=models.SET_NULL,
help_text="Gemarkung",
null=True,
blank=True,
related_name="parcels"
)
flrstck_nnr = models.CharField(
max_length=1000,
flr = models.IntegerField(
help_text="Flur",
null=True,
blank=True,
)
flrstck_nnr = models.IntegerField(
help_text="Flurstücksnenner",
null=True,
blank=True,
)
flrstck_zhlr = models.CharField(
max_length=1000,
flrstck_zhlr = models.IntegerField(
help_text="Flurstückszähler",
null=True,
blank=True,
)
flr = models.CharField(
max_length=1000,
help_text="Flur",
null=True,
blank=True,
)
updated_on = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.gmrkng} | {self.flr} | {self.flrstck_zhlr} | {self.flrstck_nnr}"
class District(UuidModel):
""" The model District holds more coarse information, such as Kreis, Verbandsgemeinde and Gemeinde.
There might be the case that a geometry lies on a hundred Parcel entries but only on one District entry.
Therefore a geometry can have a lot of relations to Parcel entries but only a few or only a single one to one
District.
"""
gmnd = models.CharField(
max_length=1000,
help_text="Gemeinde",
null=True,
blank=True,
)
krs = models.CharField(
max_length=1000,
help_text="Kreis",
null=True,
blank=True,
)
def __str__(self):
return f"{self.gmnd} | {self.krs}"
return f"{self.parcel_group} | {self.flr} | {self.flrstck_zhlr} | {self.flrstck_nnr}"
class ParcelIntersection(UuidModel):

View File

@@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import os
from django.utils.translation import gettext_lazy as _
from django.conf.locale.de import formats as de_formats
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = os.path.dirname(
@@ -162,9 +163,15 @@ LANGUAGES = [
USE_THOUSAND_SEPARATOR = True
# Regular python relevant date/datetime formatting
DEFAULT_DATE_TIME_FORMAT = '%d.%m.%Y %H:%M:%S'
DEFAULT_DATE_FORMAT = '%d.%m.%Y'
# Template relevant date/datetime formatting
# See the Note on here: https://docs.djangoproject.com/en/3.2/ref/templates/builtins/#date
de_formats.DATETIME_FORMAT = "d.m.Y, H:i"
de_formats.DATE_FORMAT = "d.m.Y"
TIME_ZONE = 'Europe/Berlin'
USE_I18N = True

View File

@@ -19,6 +19,6 @@ PAGE_SIZE_OPTIONS_TUPLES = [
(50, 50),
(100, 100),
]
PAGE_SIZE_DEFAULT = 5
PAGE_SIZE_DEFAULT = 10
PAGE_SIZE_MAX = 100
PAGE_DEFAULT = 1

View File

@@ -1,32 +0,0 @@
{% load i18n %}
<div class="table-container w-100 scroll-300">
{% if parcels|length == 0 %}
<article class="alert alert-info">
{% trans 'Parcels can not be calculated, since no geometry is given.' %}
</article>
{% else %}
<table class="table table-hover">
<thead>
<tr>
<th scope="col">{% trans 'Kreis' %}</th>
<th scope="col">{% trans 'Gemarkung' %}</th>
<th scope="col">{% trans 'Parcel' %}</th>
<th scope="col">{% trans 'Parcel counter' %}</th>
<th scope="col">{% trans 'Parcel number' %}</th>
</tr>
</thead>
<tbody>
{% for parcel in parcels %}
<tr>
<td>{{parcel.district.krs|default_if_none:"-"}}</td>
<td>{{parcel.gmrkng|default_if_none:"-"}}</td>
<td>{{parcel.flr|default_if_none:"-"}}</td>
<td>{{parcel.flrstck_zhlr|default_if_none:"-"}}</td>
<td>{{parcel.flrstck_nnr|default_if_none:"-"}}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>

View File

@@ -0,0 +1,22 @@
{% load l10n i18n %}
{% for parcel in parcels %}
{% if forloop.last and next_page %}
<tr hx-get="{% url 'geometry-parcels-content' geom_id next_page %}"
hx-trigger="intersect once"
hx-swap="afterend">
<td>{{parcel.parcel_group.name|default_if_none:"-"}}</td>
<td>{{parcel.parcel_group.key|default_if_none:"-"}}</td>
<td>{{parcel.flr|default_if_none:"-"|unlocalize}}</td>
<td>{{parcel.flrstck_zhlr|default_if_none:"-"|unlocalize}}</td>
<td>{{parcel.flrstck_nnr|default_if_none:"-"|unlocalize}}</td>
</tr>
{% else %}
<tr>
<td>{{parcel.parcel_group.name|default_if_none:"-"}}</td>
<td>{{parcel.parcel_group.key|default_if_none:"-"}}</td>
<td>{{parcel.flr|default_if_none:"-"|unlocalize}}</td>
<td>{{parcel.flrstck_zhlr|default_if_none:"-"|unlocalize}}</td>
<td>{{parcel.flrstck_nnr|default_if_none:"-"|unlocalize}}</td>
</tr>
{% endif %}
{% endfor %}

View File

@@ -0,0 +1,44 @@
{% load i18n l10n %}
<div class="table-container w-100 scroll-300">
{% if parcels|length == 0 %}
<article class="alert alert-info">
{% trans 'Parcels can not be calculated, since no geometry is given.' %}
</article>
{% else %}
<table id="upper-spatial-table" class="table table-hover">
<thead>
<tr>
<th scope="col">{% trans 'Municipal' %}</th>
<th scope="col">{% trans 'Municipal key' %}</th>
<th scope="col">{% trans 'District' %}</th>
<th scope="col">{% trans 'District key' %}</th>
</tr>
</thead>
<tbody>
{% for municipal in municipals %}
<tr>
<td>{{municipal.name}}</td>
<td>{{municipal.key|unlocalize}}</td>
<td>{{municipal.district.name}}</td>
<td>{{municipal.district.key|unlocalize}}</td>
</tr>
{% endfor %}
</tbody>
</table>
<table id="lower-spatial-table" class="table table-hover">
<thead>
<tr>
<th scope="col">{% trans 'Parcel group' %}</th>
<th scope="col">{% trans 'Parcel group key' %}</th>
<th scope="col">{% trans 'Parcel' %}</th>
<th scope="col">{% trans 'Parcel counter' %}</th>
<th scope="col">{% trans 'Parcel number' %}</th>
</tr>
</thead>
<tbody>
{% include 'konova/includes/parcels/parcel_table_content.html' %}
</tbody>
</table>
{% endif %}
</div>

View File

@@ -8,7 +8,7 @@
</h5>
</div>
<div class="card-body">
<div hx-trigger="every 2s" hx-get="{% url 'geometry-parcels' geom_form.instance.geometry.id %}" title="{% trans 'Loading...' %}">
<div hx-trigger="load, every 5s" hx-get="{% url 'geometry-parcels' geom_form.instance.geometry.id %}">
<div class="row justify-content-center">
<span class="spinner-border rlp-r-inv" role="status"></span>
</div>

View File

@@ -0,0 +1,19 @@
{% load i18n %}
<div class="col-sm-6 col-md-6 col-lg-6">
<button class="btn btn-outline-default col-sm-12">
<a href="{{qrcode.url}}" target="_blank">
<h4>{% trans 'Open in browser' %}</h4>
{{ qrcode.img|safe }}
</a>
</button>
</div>
<div class="col-sm-6 col-md-6 col-lg-6">
<button class="btn btn-outline-default col-sm-12">
<a href="{{qrcode_lanis.url}}" target="_blank">
<h4>{% trans 'View in LANIS' %}</h4>
{{ qrcode_lanis.img|safe }}
</a>
</button>
</div>

View File

@@ -18,7 +18,7 @@ from django.urls import reverse
from codelist.models import KonovaCode, KonovaCodeList
from compensation.models import Compensation, CompensationState, CompensationAction, EcoAccount, EcoAccountDeduction
from intervention.models import Legal, Responsibility, Intervention
from intervention.models import Legal, Responsibility, Intervention, Handler
from konova.management.commands.setup_data import GROUPS_DATA
from konova.models import Geometry
from konova.settings import DEFAULT_GROUP
@@ -57,6 +57,7 @@ class BaseTestCase(TestCase):
self.create_users()
self.create_groups()
self.handler = self.create_dummy_handler()
self.intervention = self.create_dummy_intervention()
self.compensation = self.create_dummy_compensation()
self.eco_account = self.create_dummy_eco_account()
@@ -122,7 +123,9 @@ class BaseTestCase(TestCase):
# Create legal data object (without M2M laws first)
legal_data = Legal.objects.create()
# Create responsible data object
responsibility_data = Responsibility.objects.create()
responsibility_data = Responsibility.objects.create(
handler=self.handler
)
geometry = Geometry.objects.create()
# Finally create main object, holding the other objects
intervention = Intervention.objects.create(
@@ -173,6 +176,9 @@ class BaseTestCase(TestCase):
# Create responsible data object
lega_data = Legal.objects.create()
responsible_data = Responsibility.objects.create()
handler = self.handler
responsible_data.handler = handler
responsible_data.save()
# Finally create main object, holding the other objects
eco_account = EcoAccount.objects.create(
identifier="TEST",
@@ -197,6 +203,8 @@ class BaseTestCase(TestCase):
geometry = Geometry.objects.create()
# Create responsible data object
responsible_data = Responsibility.objects.create()
responsible_data.handler = self.handler
responsible_data.save()
# Finally create main object, holding the other objects
ema = Ema.objects.create(
identifier="TEST",
@@ -282,6 +290,18 @@ class BaseTestCase(TestCase):
polygon = polygon.transform(3857, clone=True)
return MultiPolygon(polygon, srid=3857) # 3857 is the default srid used for MultiPolygonField in the form
def create_dummy_handler(self) -> Handler:
""" Creates a Handler
Returns:
"""
handler = Handler.objects.get_or_create(
type=KonovaCode.objects.all().first(),
detail="Test handler"
)[0]
return handler
def fill_out_intervention(self, intervention: Intervention) -> Intervention:
""" Adds all required (dummy) data to an intervention
@@ -295,7 +315,7 @@ class BaseTestCase(TestCase):
intervention.responsible.conservation_office = KonovaCode.objects.get(id=2)
intervention.responsible.registration_file_number = "test"
intervention.responsible.conservation_file_number = "test"
intervention.responsible.handler = "handler"
intervention.responsible.handler = self.handler
intervention.responsible.save()
intervention.legal.registration_date = datetime.date.fromisoformat("1970-01-01")
intervention.legal.binding_date = datetime.date.fromisoformat("1970-01-01")
@@ -343,7 +363,7 @@ class BaseTestCase(TestCase):
"""
ema.responsible.conservation_office = self.get_conservation_office_code()
ema.responsible.conservation_file_number = "test"
ema.responsible.handler = "handler"
ema.responsible.handler = self.handler
ema.responsible.save()
ema.after_states.add(self.comp_state)
ema.before_states.add(self.comp_state)
@@ -361,7 +381,7 @@ class BaseTestCase(TestCase):
eco_account.legal.save()
eco_account.responsible.conservation_office = self.get_conservation_office_code()
eco_account.responsible.conservation_file_number = "test"
eco_account.responsible.handler = "handler"
eco_account.responsible.handler = self.handler
eco_account.responsible.save()
eco_account.after_states.add(self.comp_state)
eco_account.before_states.add(self.comp_state)

View File

@@ -20,10 +20,11 @@ from django.urls import path, include
from konova.autocompletes import EcoAccountAutocomplete, \
InterventionAutocomplete, CompensationActionCodeAutocomplete, BiotopeCodeAutocomplete, LawCodeAutocomplete, \
RegistrationOfficeCodeAutocomplete, ConservationOfficeCodeAutocomplete, ProcessTypeCodeAutocomplete, \
ShareUserAutocomplete, BiotopeExtraCodeAutocomplete, CompensationActionDetailCodeAutocomplete, ShareTeamAutocomplete
ShareUserAutocomplete, BiotopeExtraCodeAutocomplete, CompensationActionDetailCodeAutocomplete, \
ShareTeamAutocomplete, HandlerCodeAutocomplete
from konova.settings import SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY, DEBUG
from konova.sso.sso import KonovaSSOClient
from konova.views import logout_view, home_view, get_geom_parcels
from konova.views import logout_view, home_view, get_geom_parcels, get_geom_parcels_content
sso_client = KonovaSSOClient(SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY)
urlpatterns = [
@@ -39,7 +40,8 @@ urlpatterns = [
path('cl/', include("codelist.urls")),
path('analysis/', include("analysis.urls")),
path('api/', include("api.urls")),
path('geom/<id>/parcels', get_geom_parcels, name="geometry-parcels"),
path('geom/<id>/parcels/', get_geom_parcels, name="geometry-parcels"),
path('geom/<id>/parcels/<int:page>', get_geom_parcels_content, name="geometry-parcels-content"),
# Autocomplete paths for all apps
path("atcmplt/eco-accounts", EcoAccountAutocomplete.as_view(), name="accounts-autocomplete"),
@@ -52,6 +54,7 @@ urlpatterns = [
path("atcmplt/codes/prc-type", ProcessTypeCodeAutocomplete.as_view(), name="codes-process-type-autocomplete"),
path("atcmplt/codes/reg-off", RegistrationOfficeCodeAutocomplete.as_view(), name="codes-registration-office-autocomplete"),
path("atcmplt/codes/cons-off", ConservationOfficeCodeAutocomplete.as_view(), name="codes-conservation-office-autocomplete"),
path("atcmplt/codes/handler", HandlerCodeAutocomplete.as_view(), name="codes-handler-autocomplete"),
path("atcmplt/share/u", ShareUserAutocomplete.as_view(), name="share-user-autocomplete"),
path("atcmplt/share/t", ShareTeamAutocomplete.as_view(), name="share-team-autocomplete"),
]

View File

@@ -7,6 +7,8 @@ Created on: 02.08.21
"""
from django.utils.translation import gettext_lazy as _
NO_DETAILS = _("no further details")
UNKNOWN = _("Unknown")
UNGROUPED = _("Ungrouped")
FORM_INVALID = _("There was an error on this form.")
PARAMS_INVALID = _("Invalid parameters")
@@ -15,6 +17,7 @@ IDENTIFIER_REPLACED = _("The identifier '{}' had to be changed to '{}' since ano
ENTRY_REMOVE_MISSING_PERMISSION = _("Only conservation or registration office users are allowed to remove entries.")
MISSING_GROUP_PERMISSION = _("You need to be part of another user group.")
CHECKED_RECORDED_RESET = _("Status of Checked and Recorded reseted")
RECORDED_BLOCKS_EDIT = _("Entry is recorded. To edit data, the entry first needs to be unrecorded.")
# SHARE
DATA_UNSHARED = _("This data is not shared with you")

View File

@@ -5,11 +5,12 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 17.12.21
"""
import json
from abc import abstractmethod
from json import JSONDecodeError
from time import sleep
import requests
import xmltodict
from django.contrib.gis.db.models.functions import AsGML, Transform
from requests.auth import HTTPDigestAuth
@@ -115,7 +116,7 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
geometry_operation,
filter_srid
)
_filter = f'<wfs:GetFeature service="WFS" version="{self.version}" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:fes="http://www.opengis.net/fes/2.0" xmlns:myns="http://www.someserver.com/myns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0.0/wfs.xsd" count="{self.count}" startindex="{start_index}"><wfs:Query typeNames="{typenames}">{spatial_filter}</wfs:Query></wfs:GetFeature>'
_filter = f'<wfs:GetFeature service="WFS" version="{self.version}" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:fes="http://www.opengis.net/fes/2.0" xmlns:myns="http://www.someserver.com/myns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0.0/wfs.xsd" count="{self.count}" startindex="{start_index}" outputFormat="application/json; subtype=geojson"><wfs:Query typeNames="{typenames}">{spatial_filter}</wfs:Query></wfs:GetFeature>'
return _filter
def get_features(self,
@@ -139,7 +140,7 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
Returns:
features (list): A list of returned features
"""
features = []
found_features = []
while start_index is not None:
post_body = self._create_post_data(
spatial_operator,
@@ -155,19 +156,11 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
)
content = response.content.decode("utf-8")
content = xmltodict.parse(content)
collection = content.get(
"wfs:FeatureCollection",
{},
)
# Check if collection is an exception and does not contain the requested data
if len(collection) == 0:
exception = content.get(
"ows:ExceptionReport",
{}
)
if len(exception) > 0 and rerun_on_exception:
try:
# Check if collection is an exception and does not contain the requested data
content = json.loads(content)
except JSONDecodeError as e:
if rerun_on_exception:
# Wait a second before another try
sleep(1)
self.get_features(
@@ -177,22 +170,21 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
start_index,
rerun_on_exception=False
)
members = collection.get(
"wfs:member",
None,
)
if members is not None:
if len(members) > 1:
# extend feature list with found list of new feature members
features += members
else:
# convert single found feature member into list and extent feature list
features += [members]
e.msg += content
raise e
fetched_features = content.get(
"features",
{},
)
if collection.get("@next", None) is not None:
start_index += self.count
else:
found_features += fetched_features
if len(fetched_features) < self.count:
# The response was not 'full', so we got everything to fetch
start_index = None
else:
# If a 'full' response returned, there might be more to fetch. Increase the start_index!
start_index += self.count
return features
return found_features

View File

@@ -17,7 +17,7 @@ from compensation.models import Compensation, EcoAccount
from intervention.models import Intervention
from konova.contexts import BaseContext
from konova.decorators import any_group_check
from konova.models import Deadline, Geometry
from konova.models import Deadline, Geometry, Municipal
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from news.models import ServerMessage
from konova.settings import SSO_SERVER_BASE
@@ -110,12 +110,12 @@ def get_geom_parcels(request: HttpRequest, id: str):
id (str): The geometry's id
Returns:
A rendered piece of HTML
"""
# HTTP code 286 states that the HTMX should stop polling for updates
# https://htmx.org/docs/#polling
status_code = 286
template = "konova/includes/parcel_table.html"
template = "konova/includes/parcels/parcel_table_frame.html"
geom = get_object_or_404(Geometry, id=id)
parcels = geom.get_underlying_parcels()
geos_geom = geom.geom
@@ -130,8 +130,21 @@ def get_geom_parcels(request: HttpRequest, id: str):
status_code = 200
if parcels_available or no_geometry_given:
parcels = parcels.order_by("-municipal", "flr", "flrstck_zhlr", "flrstck_nnr")
municipals = parcels.order_by("municipal").distinct("municipal").values("municipal__id")
municipals = Municipal.objects.filter(id__in=municipals)
rpp = 100
parcels = parcels[:rpp]
next_page = 1
if len(parcels) < rpp:
next_page = None
context = {
"parcels": parcels,
"municipals": municipals,
"geom_id": str(id),
"next_page": next_page,
}
html = render_to_string(template, context, request)
return HttpResponse(html, status=status_code)
@@ -139,6 +152,49 @@ def get_geom_parcels(request: HttpRequest, id: str):
return HttpResponse(None, status=404)
@login_required
def get_geom_parcels_content(request: HttpRequest, id: str, page: int):
""" Getter for infinite scroll of HTMX
Returns parcels of a specific page/slice of the found parcel set.
Implementation of infinite scroll htmx example: https://htmx.org/examples/infinite-scroll/
Args:
request (HttpRequest): The incoming request
id (str): The geometry's id
page (int): The requested page number
Returns:
A rendered piece of HTML
"""
if page < 0:
raise AssertionError("Parcel page can not be negative")
# HTTP code 286 states that the HTMX should stop polling for updates
# https://htmx.org/docs/#polling
status_code = 286
template = "konova/includes/parcels/parcel_table_content.html"
geom = get_object_or_404(Geometry, id=id)
parcels = geom.get_underlying_parcels()
parcels = parcels.order_by("-municipal", "flr", "flrstck_zhlr", "flrstck_nnr")
rpp = 100
from_p = rpp * (page-1)
to_p = rpp * (page)
next_page = page + 1
parcels = parcels[from_p:to_p]
if len(parcels) < rpp:
next_page = None
context = {
"parcels": parcels,
"geom_id": str(id),
"next_page": next_page,
}
html = render_to_string(template, context, request)
return HttpResponse(html, status=status_code)
def get_404_view(request: HttpRequest, exception=None):
""" Returns a 404 handling view

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,7 @@ kombu==5.2.3
openpyxl==3.0.9
OWSLib==0.25.0
packaging==21.3
pika==1.2.0
prompt-toolkit==3.0.24
psycopg2-binary==2.9.1
pyparsing==3.0.6

View File

@@ -0,0 +1,19 @@
{% load i18n fontawesome_5 %}
<div class="p-5 col-sm-12">
<h4>
<span class="registered-bookmark">
{% fa5_icon 'bookmark' %}
</span>
<span>
{% trans 'This data is recorded' %}
</span>
</h4>
<hr>
<article>
{% blocktrans %}
Whilst recorded the data is published publicly. If you wish to edit any information on this data, the data needs
to be unrecorded first. Do not forget to record it afterwards, again.
{% endblocktrans %}
</article>
</div>

View File

@@ -7,7 +7,9 @@
<hr>
<p class="lead">
{% blocktrans %}
The data you want to see is not recorded and might still be work in progress. Please come back another time.
The data you want to see is not recorded and might still be work in progress or the legal binding date has
not been reached yet. We can not publish this report as long as revocations could occur.
Please come back later.
{% endblocktrans %}
</p>
</div>

View File

@@ -74,6 +74,15 @@ class TeamAdmin(admin.ModelAdmin):
"name",
"description",
]
filter_horizontal = [
"users"
]
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "admin":
team_id = request.resolver_match.kwargs.get("object_id", None)
kwargs["queryset"] = User.objects.filter(teams__id__in=[team_id])
return super().formfield_for_foreignkey(db_field, request, **kwargs)
admin.site.register(User, UserAdmin)

View File

@@ -317,6 +317,15 @@ class RemoveTeamModalForm(RemoveModalForm):
pass
class LeaveTeamModalForm(RemoveModalForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Leave team")
def save(self):
self.instance.remove_user(self.user)
class TeamDataForm(BaseModalForm):
name = forms.CharField(
label_suffix="",

View File

@@ -93,3 +93,17 @@ class Team(UuidModel):
"""
mailer = Mailer()
mailer.send_mail_shared_data_deleted_team(obj_identifier, obj_title, self)
def remove_user(self, user):
""" Removes a user from the team
Args:
user (User): The user to be removed
Returns:
"""
self.users.remove(user)
if self.admin == user:
self.admin = self.users.first()
self.save()

View File

@@ -10,7 +10,7 @@ import uuid
from django.db import models
from django.utils.translation import gettext_lazy as _
from konova.sub_settings.django_settings import DEFAULT_DATE_FORMAT, DEFAULT_DATE_TIME_FORMAT
from konova.sub_settings.django_settings import DEFAULT_DATE_TIME_FORMAT
class UserAction(models.TextChoices):

View File

@@ -54,14 +54,6 @@
</button>
</a>
</div>
<div class="row mb-2">
<a href="{% url 'user:api-token' %}" title="{% trans 'See or edit your API token' %}">
<button class="btn btn-default">
{% fa5_icon 'code' %}
<span>{% trans 'API' %}</span>
</button>
</a>
</div>
<div class="row mb-2">
<a href="{% url 'user:team-index' %}" title="{% trans 'Manage teams' %}">
<button class="btn btn-default">
@@ -70,6 +62,14 @@
</button>
</a>
</div>
<div class="row mb-2">
<a href="{% url 'user:api-token' %}" title="{% trans 'See or edit your API token' %}">
<button class="btn btn-default">
{% fa5_icon 'code' %}
<span>{% trans 'API' %}</span>
</button>
</a>
</div>
</div>
</div>
</div>

View File

@@ -46,6 +46,9 @@
{% endfor %}
</td>
<td>
<button class="btn rlp-r btn-modal" data-form-url="{% url 'user:team-leave' team.id %}" title="{% trans 'Leave team' %}">
{% fa5_icon 'sign-out-alt' %}
</button>
{% if team.admin == user %}
<button class="btn rlp-r btn-modal" data-form-url="{% url 'user:team-edit' team.id %}" title="{% trans 'Edit team' %}">
{% fa5_icon 'edit' %}

View File

@@ -20,5 +20,6 @@ urlpatterns = [
path("team/<id>", data_team_view, name="team-data"),
path("team/<id>/edit", edit_team_view, name="team-edit"),
path("team/<id>/remove", remove_team_view, name="team-remove"),
path("team/<id>/leave", leave_team_view, name="team-leave"),
]

View File

@@ -13,7 +13,7 @@ from django.utils.translation import gettext_lazy as _
from konova.contexts import BaseContext
from konova.decorators import any_group_check, default_group_required
from user.forms import UserNotificationForm, UserContactForm, UserAPITokenForm, NewTeamModalForm, EditTeamModalForm, \
RemoveTeamModalForm, TeamDataForm
RemoveTeamModalForm, TeamDataForm, LeaveTeamModalForm
@login_required
@@ -204,3 +204,24 @@ def remove_team_view(request: HttpRequest, id: str):
_("Team removed"),
redirect_url=reverse("user:team-index")
)
@login_required
def leave_team_view(request: HttpRequest, id: str):
team = get_object_or_404(Team, id=id)
user = request.user
is_user_team_member = team.users.filter(id=user.id).exists()
if not is_user_team_member:
messages.info(
request,
_("You are not a member of this team")
)
return redirect("user:team-index")
form = LeaveTeamModalForm(request.POST or None, instance=team, request=request)
return form.process_request(
request,
_("Left Team"),
redirect_url=reverse("user:team-index")
)