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": { "responsible": {
"conservation_office": null, "conservation_office": null,
"conservation_file_number": null, "conservation_file_number": null,
"handler": null "handler": {
"type": null,
"detail": "Someone"
}
}, },
"legal": { "legal": {
"agreement_date": null "agreement_date": null

View File

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

View File

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

View File

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

View File

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

View File

@@ -49,7 +49,10 @@
"registration_file_number": "CHANGED", "registration_file_number": "CHANGED",
"conservation_office": null, "conservation_office": null,
"conservation_file_number": "CHANGED", "conservation_file_number": "CHANGED",
"handler": null "handler": {
"type": null,
"detail": "TEST_HANDLER_CHANGED"
}
}, },
"legal": { "legal": {
"registration_date": "2022-02-01", "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["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_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"]["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(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["actions"]), self.eco_account.actions.count())
self.assertEqual(len(put_props["before_states"]), self.eco_account.before_states.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.assertNotEqual(modified_on, self.ema.modified)
self.assertEqual(put_props["responsible"]["conservation_office"], self.ema.responsible.conservation_office) 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"]["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["actions"]), self.ema.actions.count())
self.assertEqual(len(put_props["before_states"]), self.ema.before_states.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()) 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, \ from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin, \
LegalAPISerializerV1Mixin, ResponsibilityAPISerializerV1Mixin, DeductableAPISerializerV1Mixin 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 compensation.models import EcoAccount
from intervention.models import Legal, Responsibility from intervention.models import Legal, Responsibility, Handler
from konova.models import Geometry from konova.models import Geometry
from konova.tasks import celery_update_parcels from konova.tasks import celery_update_parcels
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@@ -44,7 +44,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
return { return {
"conservation_office": self._konova_code_to_json(responsible.conservation_office), "conservation_office": self._konova_code_to_json(responsible.conservation_office),
"conservation_file_number": responsible.conservation_file_number, "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): def _set_responsibility(self, obj, responsibility_data: dict):
@@ -64,7 +64,11 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
CODELIST_CONSERVATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID,
) )
obj.responsible.conservation_file_number = responsibility_data["conservation_file_number"] 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 return obj
def _set_legal(self, obj, legal_data): def _set_legal(self, obj, legal_data):
@@ -92,7 +96,9 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
# Create linked objects # Create linked objects
obj = EcoAccount() obj = EcoAccount()
obj.responsible = Responsibility() obj.responsible = Responsibility(
handler=Handler()
)
obj.legal = Legal() obj.legal = Legal()
created = create_action created = create_action
obj.created = created obj.created = created
@@ -128,6 +134,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
obj = self._set_legal(obj, properties["legal"]) obj = self._set_legal(obj, properties["legal"])
obj.geometry.save() obj.geometry.save()
obj.responsible.handler.save()
obj.responsible.save() obj.responsible.save()
obj.legal.save() obj.legal.save()
obj.save() obj.save()
@@ -170,6 +177,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
obj = self._set_legal(obj, properties["legal"]) obj = self._set_legal(obj, properties["legal"])
obj.geometry.save() obj.geometry.save()
obj.responsible.handler.save()
obj.responsible.save() obj.responsible.save()
obj.legal.save() obj.legal.save()
obj.save() obj.save()

View File

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

View File

@@ -11,7 +11,7 @@ from django.db.models import QuerySet
from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, \ from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, \
ResponsibilityAPISerializerV1Mixin, LegalAPISerializerV1Mixin, DeductableAPISerializerV1Mixin ResponsibilityAPISerializerV1Mixin, LegalAPISerializerV1Mixin, DeductableAPISerializerV1Mixin
from compensation.models import Payment 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.models import Geometry
from konova.tasks import celery_update_parcels from konova.tasks import celery_update_parcels
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@@ -69,7 +69,9 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
# Create linked objects # Create linked objects
obj = Intervention() obj = Intervention()
resp = Responsibility() resp = Responsibility(
handler=Handler()
)
legal = Legal() legal = Legal()
created = create_action created = create_action
obj.legal = legal obj.legal = legal
@@ -130,6 +132,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
id__in=payments id__in=payments
) )
obj.payments.set(payments) obj.payments.set(payments)
obj.send_data_to_egon()
return obj return obj
def create_model_from_json(self, json_model, user): def create_model_from_json(self, json_model, user):
@@ -152,6 +155,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
self._set_responsibility(obj, properties["responsible"]) self._set_responsibility(obj, properties["responsible"])
self._set_legal(obj, properties["legal"]) self._set_legal(obj, properties["legal"])
obj.responsible.handler.save()
obj.responsible.save() obj.responsible.save()
obj.geometry.save() obj.geometry.save()
obj.legal.save() obj.legal.save()
@@ -188,12 +192,13 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
obj.geometry.geom = self._create_geometry_from_json(json_model) obj.geometry.geom = self._create_geometry_from_json(json_model)
obj.geometry.modified = update_action obj.geometry.modified = update_action
obj.responsible.handler.save()
obj.responsible.save() obj.responsible.save()
obj.geometry.save() obj.geometry.save()
obj.legal.save() obj.legal.save()
obj.save() obj.save()
obj.mark_as_edited(user) obj.mark_as_edited(user, edit_comment="API update")
celery_update_parcels.delay(obj.geometry.id) 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.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_PROCESS_TYPE_ID, \ from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_PROCESS_TYPE_ID, \
CODELIST_LAW_ID, CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, \ CODELIST_LAW_ID, CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, \
CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_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 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.models import Deadline, DeadlineType
from konova.utils.message_templates import DATA_UNSHARED from konova.utils.message_templates import DATA_UNSHARED
@@ -75,7 +75,10 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer):
Returns: 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 return None
code = KonovaCode.objects.get( code = KonovaCode.objects.get(
atom_id=json_str, atom_id=json_str,
@@ -176,6 +179,12 @@ class ResponsibilityAPISerializerV1Mixin:
class Meta: class Meta:
abstract = True 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): def _responsible_to_json(self, responsible: Responsibility):
""" Serializes Responsibility model into json """ Serializes Responsibility model into json
@@ -190,7 +199,7 @@ class ResponsibilityAPISerializerV1Mixin:
"registration_file_number": responsible.registration_file_number, "registration_file_number": responsible.registration_file_number,
"conservation_office": self._konova_code_to_json(responsible.conservation_office), "conservation_office": self._konova_code_to_json(responsible.conservation_office),
"conservation_file_number": responsible.conservation_file_number, "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): def _set_responsibility(self, obj, responsibility_data: dict):
@@ -215,7 +224,11 @@ class ResponsibilityAPISerializerV1Mixin:
CODELIST_CONSERVATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID,
) )
obj.responsible.conservation_file_number = responsibility_data["conservation_file_number"] 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 return obj

View File

@@ -33,6 +33,7 @@ class KonovaCodeAdmin(admin.ModelAdmin):
"is_selectable", "is_selectable",
"is_leaf", "is_leaf",
"parent", "parent",
"found_in_codelists",
] ]
search_fields = [ search_fields = [
@@ -42,6 +43,12 @@ class KonovaCodeAdmin(admin.ModelAdmin):
"short_name", "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(KonovaCodeList, KonovaCodeListAdmin)
admin.site.register(KonovaCode, KonovaCodeAdmin) 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.models import KonovaCode, KonovaCodeList
from codelist.settings import CODELIST_INTERVENTION_HANDLER_ID, CODELIST_CONSERVATION_OFFICE_ID, \ 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_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_CLASS_ID, CODELIST_COMPENSATION_ADDITIONAL_TYPE_ID, \
CODELIST_BASE_URL, CODELIST_PROCESS_TYPE_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, \ CODELIST_BASE_URL, CODELIST_PROCESS_TYPE_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, \
CODELIST_COMPENSATION_ACTION_DETAIL_ID CODELIST_COMPENSATION_ACTION_DETAIL_ID
@@ -36,7 +36,7 @@ class Command(BaseKonovaCommand):
CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_ID,
CODELIST_BIOTOPES_EXTRA_CODES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID,
CODELIST_LAW_ID, CODELIST_LAW_ID,
CODELIST_COMPENSATION_HANDLER_ID, CODELIST_HANDLER_ID,
CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_ID,
CODELIST_COMPENSATION_ACTION_CLASS_ID, CODELIST_COMPENSATION_ACTION_CLASS_ID,
CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID,

View File

@@ -1,9 +1,13 @@
# Generated by Django 3.1.3 on 2022-01-14 08:36 # 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 from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
def load_initial_codes(apps, schema_editor):
call_command('update_codelist')
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True 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')), ('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_LAW_ID = 1048 # CLVerfahrensrecht
CODELIST_PROCESS_TYPE_ID = 44382 # CLVerfahrenstyp 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_ID = 1026 # CLMassnahmedetail
CODELIST_COMPENSATION_ACTION_DETAIL_ID = 1035 # CLZusatzmerkmal CODELIST_COMPENSATION_ACTION_DETAIL_ID = 1035 # CLZusatzmerkmal
CODELIST_COMPENSATION_ACTION_CLASS_ID = 1034 # CLMassnahmeklasse CODELIST_COMPENSATION_ACTION_CLASS_ID = 1034 # CLMassnahmeklasse

View File

@@ -21,16 +21,30 @@ class AbstractCompensationAdmin(BaseObjectAdmin):
"identifier", "identifier",
"title", "title",
"comment", "comment",
"after_states", "list_after_states",
"before_states", "list_before_states",
"geometry",
] ]
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
return super().get_readonly_fields(request, obj) + [ return super().get_readonly_fields(request, obj) + [
"after_states", "list_after_states",
"before_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): class CompensationAdmin(AbstractCompensationAdmin):
autocomplete_fields = [ autocomplete_fields = [

View File

@@ -13,10 +13,10 @@ from django.utils.translation import gettext_lazy as _
from django import forms from django import forms
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID
from compensation.models import Compensation, EcoAccount from compensation.models import Compensation, EcoAccount
from intervention.inputs import GenerateInput 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.forms import BaseForm, SimpleGeomForm
from konova.utils.message_templates import EDITED_GENERAL_DATA, COMPENSATION_ADDED_TEMPLATE from konova.utils.message_templates import EDITED_GENERAL_DATA, COMPENSATION_ADDED_TEMPLATE
from user.models import UserActionLogEntry 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="", label_suffix="",
max_length=255, max_length=255,
required=False, required=False,
help_text=_("Who handles the eco-account"), help_text=_("Detail input on the handler"),
widget=forms.TextInput( widget=forms.TextInput(
attrs={ attrs={
"placeholder": _("Company Mustermann"), "placeholder": _("Company Mustermann"),
@@ -345,7 +363,8 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
"registration_date", "registration_date",
"surface", "surface",
"conservation_file_number", "conservation_file_number",
"handler", "handler_type",
"handler_detail",
"comment", "comment",
] ]
@@ -368,7 +387,8 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
identifier = self.cleaned_data.get("identifier", None) identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None) title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None) registration_date = self.cleaned_data.get("registration_date", None)
handler = 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) surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None) conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None) conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
@@ -379,6 +399,11 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
# Process the geometry form # Process the geometry form
geometry = geom_form.save(action) geometry = geom_form.save(action)
handler = Handler.objects.create(
type=handler_type,
detail=handler_detail,
)
responsible = Responsibility.objects.create( responsible = Responsibility.objects.create(
handler=handler, handler=handler,
conservation_file_number=conservation_file_number, conservation_file_number=conservation_file_number,
@@ -423,11 +448,13 @@ class EditEcoAccountForm(NewEcoAccountForm):
reg_date = self.instance.legal.registration_date reg_date = self.instance.legal.registration_date
if reg_date is not None: if reg_date is not None:
reg_date = reg_date.isoformat() reg_date = reg_date.isoformat()
form_data = { form_data = {
"identifier": self.instance.identifier, "identifier": self.instance.identifier,
"title": self.instance.title, "title": self.instance.title,
"surface": self.instance.deductable_surface, "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, "registration_date": reg_date,
"conservation_office": self.instance.responsible.conservation_office, "conservation_office": self.instance.responsible.conservation_office,
"conservation_file_number": self.instance.responsible.conservation_file_number, "conservation_file_number": self.instance.responsible.conservation_file_number,
@@ -445,7 +472,8 @@ class EditEcoAccountForm(NewEcoAccountForm):
identifier = self.cleaned_data.get("identifier", None) identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None) title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None) registration_date = self.cleaned_data.get("registration_date", None)
handler = 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) surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None) conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None) conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
@@ -458,7 +486,9 @@ class EditEcoAccountForm(NewEcoAccountForm):
geometry = geom_form.save(action) geometry = geom_form.save(action)
# Update responsible data # 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_office = conservation_office
self.instance.responsible.conservation_file_number = conservation_file_number self.instance.responsible.conservation_file_number = conservation_file_number
self.instance.responsible.save() self.instance.responsible.save()

View File

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

View File

@@ -418,6 +418,18 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
super().set_status_messages(request) super().set_status_messages(request)
return 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): class CompensationDocument(AbstractDocument):
""" """

View File

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

View File

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

View File

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

View File

@@ -38,17 +38,10 @@
{% include 'map/geom_form.html' %} {% include 'map/geom_form.html' %}
</div> </div>
<div class="row"> <div class="row">
{% include 'konova/includes/parcels.html' %} {% include 'konova/includes/parcels/parcels.html' %}
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-6 col-md-6 col-lg-6"> {% include 'konova/includes/report/qrcodes.html' %}
<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>
</div> </div>
</div> </div>

View File

@@ -51,17 +51,10 @@
{% include 'map/geom_form.html' %} {% include 'map/geom_form.html' %}
</div> </div>
<div class="row"> <div class="row">
{% include 'konova/includes/parcels.html' %} {% include 'konova/includes/parcels/parcels.html' %}
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-6 col-md-6 col-lg-6"> {% include 'konova/includes/report/qrcodes.html' %}
<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>
</div> </div>
</div> </div>

View File

@@ -60,8 +60,9 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
# Preserve the current number of intervention's compensations # Preserve the current number of intervention's compensations
num_compensations = self.intervention.compensations.count() 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.intervention.refresh_from_db()
self.assertEqual(num_compensations + 1, self.intervention.compensations.count()) self.assertEqual(num_compensations + 1, self.intervention.compensations.count())
new_compensation = self.intervention.compensations.get(identifier=test_id) new_compensation = self.intervention.compensations.get(identifier=test_id)
@@ -261,3 +262,26 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
self.assertIn(recorded, self.compensation.log.all()) self.assertIn(recorded, self.compensation.log.all())
self.assertEqual(pre_record_log_count + 1, self.compensation.log.count()) 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(pre_edit_account_log_count + 1, account.log.count())
self.assertEqual(intervention.log.first().action, UserAction.EDITED) self.assertEqual(intervention.log.first().action, UserAction.EDITED)
self.assertEqual(account.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.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Sum from django.db.models import Sum
from django.http import HttpRequest, JsonResponse from django.http import HttpRequest, JsonResponse
from django.shortcuts import render 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, \ CHECKED_RECORDED_RESET, COMPENSATION_ADDED_TEMPLATE, COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, \
COMPENSATION_STATE_REMOVED, COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_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_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 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" 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) data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
geom_form = SimpleGeomForm(request.POST or None, read_only=False) geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST": if request.method == "POST":
@@ -134,6 +148,13 @@ def edit_view(request: HttpRequest, id: str):
template = "compensation/form/view.html" template = "compensation/form/view.html"
# Get object from db # Get object from db
comp = get_object_or_404(Compensation, id=id) 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 # Create forms, initialize with values from db/from POST request
data_form = EditCompensationForm(request.POST or None, instance=comp) data_form = EditCompensationForm(request.POST or None, instance=comp)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, 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 instance=comp
) )
parcels = comp.get_underlying_parcels() parcels = comp.get_underlying_parcels()
qrcode_img = generate_qr_code(
request.build_absolute_uri(reverse("compensation:report", args=(id,))), qrcode_url = request.build_absolute_uri(reverse("compensation:report", args=(id,)))
10 qrcode_img = generate_qr_code(qrcode_url, 10)
) qrcode_lanis_url = comp.get_LANIS_link()
qrcode_img_lanis = generate_qr_code( qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
comp.get_LANIS_link(),
7
)
# Order states by surface # Order states by surface
before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type") 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") 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 = { context = {
"obj": comp, "obj": comp,
"qrcode": qrcode_img, "qrcode": {
"qrcode_lanis": qrcode_img_lanis, "img": qrcode_img,
"url": qrcode_url,
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url,
},
"has_access": False, # disables action buttons during rendering "has_access": False, # disables action buttons during rendering
"before_states": before_states, "before_states": before_states,
"after_states": after_states, "after_states": after_states,

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, \ 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, \ 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, \ 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 from konova.utils.user_checks import in_group
@@ -145,6 +146,13 @@ def edit_view(request: HttpRequest, id: str):
template = "compensation/form/view.html" template = "compensation/form/view.html"
# Get object from db # Get object from db
acc = get_object_or_404(EcoAccount, id=id) 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 # Create forms, initialize with values from db/from POST request
data_form = EditEcoAccountForm(request.POST or None, instance=acc) data_form = EditEcoAccountForm(request.POST or None, instance=acc)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc) geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc)
@@ -731,18 +739,16 @@ def report_view(request:HttpRequest, id: str):
instance=acc instance=acc
) )
parcels = acc.get_underlying_parcels() parcels = acc.get_underlying_parcels()
qrcode_img = generate_qr_code(
request.build_absolute_uri(reverse("ema:report", args=(id,))), qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,)))
10 qrcode_img = generate_qr_code(qrcode_url, 10)
) qrcode_lanis_url = acc.get_LANIS_link()
qrcode_img_lanis = generate_qr_code( qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
acc.get_LANIS_link(),
7
)
# Order states by surface # Order states by surface
before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent") 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") 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) # 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()\ deductions = acc.deductions.all()\
@@ -752,8 +758,14 @@ def report_view(request:HttpRequest, id: str):
context = { context = {
"obj": acc, "obj": acc,
"qrcode": qrcode_img, "qrcode": {
"qrcode_lanis": qrcode_img_lanis, "img": qrcode_img,
"url": qrcode_url,
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url,
},
"has_access": False, # disables action buttons during rendering "has_access": False, # disables action buttons during rendering
"before_states": before_states, "before_states": before_states,
"after_states": after_states, "after_states": after_states,

View File

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

View File

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

View File

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

View File

@@ -38,17 +38,10 @@
{% include 'map/geom_form.html' %} {% include 'map/geom_form.html' %}
</div> </div>
<div class="row"> <div class="row">
{% include 'konova/includes/parcels.html' %} {% include 'konova/includes/parcels/parcels.html' %}
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-6 col-md-6 col-lg-6"> {% include 'konova/includes/report/qrcodes.html' %}
<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>
</div> </div>
</div> </div>

View File

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

View File

@@ -117,6 +117,32 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
self.assertEqual(pre_edit_log_count + 1, self.ema.log.count()) self.assertEqual(pre_edit_log_count + 1, self.ema.log.count())
self.assertEqual(self.ema.log.first().action, UserAction.EDITED) 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): 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) 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, \ 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, \ 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_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 from konova.utils.user_checks import in_group
@@ -213,6 +213,13 @@ def edit_view(request: HttpRequest, id: str):
template = "compensation/form/view.html" template = "compensation/form/view.html"
# Get object from db # Get object from db
ema = get_object_or_404(Ema, id=id) 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 # Create forms, initialize with values from db/from POST request
data_form = EditEmaForm(request.POST or None, instance=ema) data_form = EditEmaForm(request.POST or None, instance=ema)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, 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, instance=ema,
) )
parcels = ema.get_underlying_parcels() parcels = ema.get_underlying_parcels()
qrcode_img = generate_qr_code(
request.build_absolute_uri(reverse("ema:report", args=(id,))), qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,)))
10 qrcode_img = generate_qr_code(qrcode_url, 10)
) qrcode_lanis_url = ema.get_LANIS_link()
qrcode_img_lanis = generate_qr_code( qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
ema.get_LANIS_link(),
7
)
# Order states by surface # Order states by surface
before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type") 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") 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 = { context = {
"obj": ema, "obj": ema,
"qrcode": qrcode_img, "qrcode": {
"qrcode_lanis": qrcode_img_lanis, "img": qrcode_img,
"url": qrcode_url
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url
},
"has_access": False, # disables action buttons during rendering "has_access": False, # disables action buttons during rendering
"before_states": before_states, "before_states": before_states,
"after_states": after_states, "after_states": after_states,

View File

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

View File

@@ -16,9 +16,9 @@ from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_PROCESS_TYPE_ID, CODELIST_LAW_ID, \ 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.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 konova.forms import BaseForm, SimpleGeomForm
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@@ -138,12 +138,29 @@ class NewInterventionForm(BaseForm):
} }
) )
) )
handler = forms.CharField( handler_type = forms.ModelChoiceField(
label=_("Intervention handler"), 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="", label_suffix="",
max_length=255, max_length=255,
required=False, required=False,
help_text=_("Who performs the intervention"), help_text=_("Detail input on the handler"),
widget=forms.TextInput( widget=forms.TextInput(
attrs={ attrs={
"placeholder": _("Company Mustermann"), "placeholder": _("Company Mustermann"),
@@ -151,6 +168,7 @@ class NewInterventionForm(BaseForm):
} }
) )
) )
registration_date = forms.DateField( registration_date = forms.DateField(
label=_("Registration date"), label=_("Registration date"),
label_suffix=_(""), label_suffix=_(""),
@@ -205,7 +223,8 @@ class NewInterventionForm(BaseForm):
title = self.cleaned_data.get("title", None) title = self.cleaned_data.get("title", None)
_type = self.cleaned_data.get("type", None) _type = self.cleaned_data.get("type", None)
laws = self.cleaned_data.get("laws", 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_office = self.cleaned_data.get("registration_office", None)
conservation_office = self.cleaned_data.get("conservation_office", None) conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", 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 # Then add the M2M laws to the object
legal_data.laws.set(laws) legal_data.laws.set(laws)
handler = Handler.objects.create(
type=handler_type,
detail=handler_detail
)
# Create responsible data object # Create responsible data object
responsibility_data = Responsibility.objects.create( responsibility_data = Responsibility.objects.create(
registration_office=registration_office, registration_office=registration_office,
@@ -284,7 +307,8 @@ class EditInterventionForm(NewInterventionForm):
"title": self.instance.title, "title": self.instance.title,
"type": self.instance.legal.process_type, "type": self.instance.legal.process_type,
"laws": list(self.instance.legal.laws.values_list("id", flat=True)), "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_office": self.instance.responsible.registration_office,
"registration_file_number": self.instance.responsible.registration_file_number, "registration_file_number": self.instance.responsible.registration_file_number,
"conservation_office": self.instance.responsible.conservation_office, "conservation_office": self.instance.responsible.conservation_office,
@@ -313,7 +337,8 @@ class EditInterventionForm(NewInterventionForm):
title = self.cleaned_data.get("title", None) title = self.cleaned_data.get("title", None)
process_type = self.cleaned_data.get("type", None) process_type = self.cleaned_data.get("type", None)
laws = self.cleaned_data.get("laws", 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_office = self.cleaned_data.get("registration_office", None)
registration_file_number = self.cleaned_data.get("registration_file_number", None) registration_file_number = self.cleaned_data.get("registration_file_number", None)
conservation_office = self.cleaned_data.get("conservation_office", 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.laws.set(laws)
self.instance.legal.save() 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_office = registration_office
self.instance.responsible.registration_file_number = registration_file_number self.instance.responsible.registration_file_number = registration_file_number
self.instance.responsible.conservation_office = conservation_office self.instance.responsible.conservation_office = conservation_office

View File

@@ -427,13 +427,22 @@ class NewDeductionModalForm(BaseModalForm):
""" """
super_result = super().is_valid() super_result = super().is_valid()
acc = self.cleaned_data["account"] acc = self.cleaned_data["account"]
intervention = self.cleaned_data["intervention"]
objects_valid = True
if not acc.recorded: if not acc.recorded:
self.add_error( self.add_error(
"account", "account",
_("Eco-account {} is not recorded yet. You can only deduct from recorded accounts.").format(acc.identifier) _("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) rest_surface = self._get_available_surface(acc)
form_surface = float(self.cleaned_data["surface"]) form_surface = float(self.cleaned_data["surface"])
@@ -447,7 +456,7 @@ class NewDeductionModalForm(BaseModalForm):
format_german_float(rest_surface), 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): def __create_deduction(self):
""" Creates the deduction """ 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.urls import reverse
from django.utils import timezone from django.utils import timezone
from intervention.tasks import celery_export_to_egon
from user.models import User from user.models import User
from django.db import models, transaction from django.db import models, transaction
from django.db.models import QuerySet from django.db.models import QuerySet
@@ -131,6 +132,16 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
self.add_log_entry_to_compensations(log_entry) self.add_log_entry_to_compensations(log_entry)
return 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: def set_recorded(self, user: User) -> UserActionLogEntry:
log_entry = super().set_recorded(user) log_entry = super().set_recorded(user)
self.add_log_entry_to_compensations(log_entry) self.add_log_entry_to_compensations(log_entry)
@@ -171,6 +182,8 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
intervention=self, intervention=self,
) )
self.mark_as_edited(user, form.request, edit_comment=PAYMENT_ADDED) self.mark_as_edited(user, form.request, edit_comment=PAYMENT_ADDED)
self.send_data_to_egon()
return pay return pay
def add_revocation(self, form): def add_revocation(self, form):
@@ -335,6 +348,7 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
with transaction.atomic(): with transaction.atomic():
payment.delete() payment.delete()
self.mark_as_edited(user, request=form.request, edit_comment=PAYMENT_REMOVED) self.mark_as_edited(user, request=form.request, edit_comment=PAYMENT_REMOVED)
self.send_data_to_egon()
class InterventionDocument(AbstractDocument): class InterventionDocument(AbstractDocument):

View File

@@ -6,10 +6,42 @@ Created on: 15.11.21
""" """
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, 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.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): class Responsibility(UuidModel):
@@ -43,11 +75,17 @@ class Responsibility(UuidModel):
} }
) )
conservation_file_number = models.CharField(max_length=1000, blank=True, null=True) 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): def __str__(self):
return "ZB: {} | ETS: {} | Handler: {}".format( return "ZB: {} | ETS: {} | Handler: {}".format(
self.registration_office, self.registration_office,
self.conservation_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_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( parcels = value.get_underlying_parcels().values_list(
"gmrkng", "parcel_group__name",
flat=True flat=True
).distinct() ).distinct()
html = render_to_string( html = render_to_string(
@@ -177,9 +177,7 @@ class InterventionTable(BaseTable, TableRenderMixin):
""" """
html = "" html = ""
has_access = value.filter( has_access = record.is_shared_with(self.user)
id=self.user.id
).exists()
html += self.render_icn( html += self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"), 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' %} {% extends 'base.html' %}
{% load i18n l10n static fontawesome_5 humanize %} {% load i18n l10n static fontawesome_5 %}
{% block head %} {% block head %}
{% comment %} {% comment %}
@@ -106,9 +106,15 @@
<tr> <tr>
<th scope="row">{% trans 'Last modified' %}</th> <th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle"> <td class="align-middle">
{{obj.created.timestamp|default_if_none:""|naturalday}} {% if obj.modified %}
<br> {{obj.modified.timestamp|default_if_none:""}}
{{obj.created.user.username}} <br>
{{obj.modified.user.username}}
{% else %}
{{obj.created.timestamp|default_if_none:""}}
<br>
{{obj.created.user.username}}
{% endif %}
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -131,7 +137,7 @@
{% include 'map/geom_form.html' %} {% include 'map/geom_form.html' %}
</div> </div>
<div class="row"> <div class="row">
{% include 'konova/includes/parcels.html' %} {% include 'konova/includes/parcels/parcels.html' %}
</div> </div>
<div class="row"> <div class="row">
{% include 'konova/includes/comment_card.html' %} {% include 'konova/includes/comment_card.html' %}

View File

@@ -97,17 +97,10 @@
{% include 'map/geom_form.html' %} {% include 'map/geom_form.html' %}
</div> </div>
<div class="row"> <div class="row">
{% include 'konova/includes/parcels.html' %} {% include 'konova/includes/parcels/parcels.html' %}
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-6 col-md-6 col-lg-6"> {% include 'konova/includes/report/qrcodes.html' %}
<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>
</div> </div>
</div> </div>

View File

@@ -89,6 +89,30 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
self.assertIn(self.superuser, obj.users.all()) self.assertIn(self.superuser, obj.users.all())
self.assertEqual(1, obj.users.count()) 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): def test_checkability(self):
""" Tests that the intervention can only be checked if all required data has been added """ 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.generators import generate_qr_code
from konova.utils.message_templates import INTERVENTION_INVALID, FORM_INVALID, IDENTIFIER_REPLACED, \ from konova.utils.message_templates import INTERVENTION_INVALID, FORM_INVALID, IDENTIFIER_REPLACED, \
CHECKED_RECORDED_RESET, DEDUCTION_REMOVED, DEDUCTION_ADDED, REVOCATION_ADDED, REVOCATION_REMOVED, \ 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 from konova.utils.user_checks import in_group
@@ -302,6 +303,13 @@ def edit_view(request: HttpRequest, id: str):
template = "intervention/form/view.html" template = "intervention/form/view.html"
# Get object from db # Get object from db
intervention = get_object_or_404(Intervention, id=id) 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 # Create forms, initialize with values from db/from POST request
data_form = EditInterventionForm(request.POST or None, instance=intervention) data_form = EditInterventionForm(request.POST or None, instance=intervention)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, 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( distinct_deductions = intervention.deductions.all().distinct(
"account" "account"
) )
qrcode_img = generate_qr_code( qrcode_url = request.build_absolute_uri(reverse("intervention:report", args=(id,)))
request.build_absolute_uri(reverse("intervention:report", args=(id,))), qrcode_img = generate_qr_code(qrcode_url, 10)
10 qrcode_lanis_url = intervention.get_LANIS_link()
) qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
qrcode_img_lanis = generate_qr_code(
intervention.get_LANIS_link(),
7
)
context = { context = {
"obj": intervention, "obj": intervention,
"deductions": distinct_deductions, "deductions": distinct_deductions,
"qrcode": qrcode_img, "qrcode": {
"qrcode_lanis": qrcode_img_lanis, "img": qrcode_img,
"url": qrcode_url,
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url,
},
"geom_form": geom_form, "geom_form": geom_form,
"parcels": parcels, "parcels": parcels,
TAB_TITLE_IDENTIFIER: tab_title, TAB_TITLE_IDENTIFIER: tab_title,

View File

@@ -7,7 +7,8 @@ Created on: 22.07.21
""" """
from django.contrib import admin 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 konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
from user.models import UserAction from user.models import UserAction
@@ -16,13 +17,28 @@ class GeometryAdmin(admin.ModelAdmin):
list_display = [ list_display = [
"id", "id",
"created", "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): class ParcelAdmin(admin.ModelAdmin):
list_display = [ list_display = [
"id", "id",
"gmrkng", "parcel_group",
"flr", "flr",
"flrstck_nnr", "flrstck_nnr",
"flrstck_zhlr", "flrstck_zhlr",
@@ -32,9 +48,27 @@ class ParcelAdmin(admin.ModelAdmin):
class DistrictAdmin(admin.ModelAdmin): class DistrictAdmin(admin.ModelAdmin):
list_display = [ 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", "id",
"gmnd",
"krs",
] ]
@@ -105,5 +139,7 @@ class BaseObjectAdmin(BaseResourceAdmin):
#admin.site.register(Geometry, GeometryAdmin) #admin.site.register(Geometry, GeometryAdmin)
#admin.site.register(Parcel, ParcelAdmin) #admin.site.register(Parcel, ParcelAdmin)
#admin.site.register(District, DistrictAdmin) #admin.site.register(District, DistrictAdmin)
#admin.site.register(Municipal, MunicipalAdmin)
#admin.site.register(ParcelGroup, ParcelGroupAdmin)
#admin.site.register(GeometryConflict, GeometryConflictAdmin) #admin.site.register(GeometryConflict, GeometryConflictAdmin)
#admin.site.register(Deadline, DeadlineAdmin) #admin.site.register(Deadline, DeadlineAdmin)

View File

@@ -17,7 +17,7 @@ from django.db.models import Q
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_LAW_ID, \ 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_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 compensation.models import EcoAccount
from intervention.models import Intervention from intervention.models import Intervention
@@ -52,14 +52,16 @@ class InterventionAutocomplete(Select2QuerySetView):
""" """
def get_queryset(self): def get_queryset(self):
if self.request.user.is_anonymous: user = self.request.user
if user.is_anonymous:
return Intervention.objects.none() return Intervention.objects.none()
qs = Intervention.objects.filter( qs = Intervention.objects.filter(
deleted=None, Q(deleted=None) &
users__in=[self.request.user], Q(users__in=[user]) |
Q(teams__in=user.teams.all())
).order_by( ).order_by(
"identifier" "identifier"
) ).distinct()
if self.q: if self.q:
qs = qs.filter( qs = qs.filter(
Q(identifier__icontains=self.q) | Q(identifier__icontains=self.q) |
@@ -357,3 +359,18 @@ class ConservationOfficeCodeAutocomplete(KonovaCodeAutocomplete):
def get_result_label(self, result): def get_result_label(self, result):
return f"{result.long_name} ({result.short_name})" return f"{result.long_name} ({result.short_name})"
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: class Meta:
abstract = True abstract = True
def _filter_parcel_reference(self, queryset, name, value, filter_value) -> QuerySet: def _filter_parcel_reference(self, queryset, filter_q) -> QuerySet:
""" Filters the parcel entries by a given filter_value. """ Filters the parcel entries by a given filter_q
filter_value may already include further filter annotations like 'xy__icontains'
Args: Args:
queryset (): queryset (QuerySet): The queryset
name (): filter_q (Q): The Q-style filter expression
value ():
filter_value ():
Returns: Returns:
""" """
_filter = {
filter_value: value
}
matching_parcels = Parcel.objects.filter( matching_parcels = Parcel.objects.filter(
**_filter filter_q
) )
related_geoms = matching_parcels.values( related_geoms = matching_parcels.values(
"geometries" "geometries"
).distinct() ).distinct()
@@ -185,8 +179,9 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
""" """
matching_districts = District.objects.filter( matching_districts = District.objects.filter(
krs__icontains=value Q(name__icontains=value) |
) Q(key__icontains=value)
).distinct()
matching_parcels = Parcel.objects.filter( matching_parcels = Parcel.objects.filter(
district__in=matching_districts district__in=matching_districts
) )
@@ -209,7 +204,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
Returns: 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 return queryset
def filter_parcel(self, queryset, name, value) -> QuerySet: def filter_parcel(self, queryset, name, value) -> QuerySet:
@@ -224,7 +222,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
""" """
value = value.replace("-", "") value = value.replace("-", "")
queryset = self._filter_parcel_reference(queryset, name, value, "flr") queryset = self._filter_parcel_reference(
queryset,
Q(flr=value),
)
return queryset return queryset
def filter_parcel_counter(self, queryset, name, value) -> QuerySet: def filter_parcel_counter(self, queryset, name, value) -> QuerySet:
@@ -239,7 +240,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
""" """
value = value.replace("-", "") 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 return queryset
def filter_parcel_number(self, queryset, name, value) -> QuerySet: def filter_parcel_number(self, queryset, name, value) -> QuerySet:
@@ -254,7 +258,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
""" """
value = value.replace("-", "") 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 return queryset

View File

@@ -57,6 +57,8 @@ class BaseForm(forms.Form):
self.has_required_fields = True self.has_required_fields = True
break break
self.check_for_recorded_instance()
@abstractmethod @abstractmethod
def save(self): def save(self):
# To be implemented in subclasses! # To be implemented in subclasses!
@@ -136,6 +138,38 @@ class BaseForm(forms.Form):
set_class = set_class.replace(cls, "") set_class = set_class.replace(cls, "")
self.fields[field].widget.attrs["class"] = set_class 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): class RemoveForm(BaseForm):
check = forms.BooleanField( check = forms.BooleanField(
@@ -410,7 +444,6 @@ class NewDocumentModalForm(BaseModalForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("Add new document") self.form_title = _("Add new document")
self.form_caption = _("") self.form_caption = _("")
self.template = "modal/modal_form.html"
self.form_attrs = { self.form_attrs = {
"enctype": "multipart/form-data", # important for file upload "enctype": "multipart/form-data", # important for file upload
} }
@@ -597,4 +630,12 @@ class RecordModalForm(BaseModalForm):
self.instance.set_unrecorded(self.user) self.instance.set_unrecorded(self.user)
else: else:
self.instance.set_recorded(self.user) 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 ema.models import Ema
from intervention.models import Intervention from intervention.models import Intervention
from konova.management.commands.setup import BaseKonovaCommand 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 from user.models import UserActionLogEntry, UserAction
@@ -271,13 +271,26 @@ class Command(BaseKonovaCommand):
self._write_success("No unused states found.") self._write_success("No unused states found.")
self._break_line() 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): def sanitize_parcels_and_districts(self):
""" Removes unattached parcels and districts """ Removes unattached parcels and districts
Returns: Returns:
""" """
self._write_warning("=== Sanitize parcels and districts ===") self._write_warning("=== Sanitize administrative spatial references ===")
unrelated_parcels = Parcel.objects.filter( unrelated_parcels = Parcel.objects.filter(
geometries=None, geometries=None,
) )
@@ -289,16 +302,12 @@ class Command(BaseKonovaCommand):
else: else:
self._write_success("No unrelated parcels found.") self._write_success("No unrelated parcels found.")
unrelated_districts = District.objects.filter( sub_types = [
parcels=None, District,
) Municipal,
num_unrelated_districts = unrelated_districts.count() ParcelGroup
if num_unrelated_districts > 0: ]
self._write_error(f"Found {num_unrelated_districts} unrelated district entries. Delete now...") for sub_type in sub_types:
unrelated_districts.delete() self.__sanitize_parcel_sub_type(sub_type)
self._write_success("Unrelated districts deleted.")
else:
self._write_success("No unrelated districts found.")
self._break_line()
self._break_line()

View File

@@ -5,6 +5,10 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.01.22 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.management.commands.setup import BaseKonovaCommand
from konova.models import Geometry, Parcel, District from konova.models import Geometry, Parcel, District
@@ -23,12 +27,21 @@ class Command(BaseKonovaCommand):
num_parcels_before = Parcel.objects.count() num_parcels_before = Parcel.objects.count()
num_districts_before = District.objects.count() num_districts_before = District.objects.count()
self._write_warning("=== Update parcels and districts ===") self._write_warning("=== Update parcels and districts ===")
# Order geometries by size to process smaller once at first
geometries = Geometry.objects.all().exclude( geometries = Geometry.objects.all().exclude(
geom=None geom=None
).annotate(area=Area("geom")).order_by(
'area'
) )
self._write_warning(f"Process parcels for {geometries.count()} geometry entries now ...") self._write_warning(f"Process parcels for {geometries.count()} geometry entries now ...")
i = 0
num_geoms = geometries.count()
for geometry in geometries: for geometry in geometries:
self._write_warning(f"--- {datetime.datetime.now()} Process {geometry.id} now ...")
geometry.update_parcels() 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_parcels_after = Parcel.objects.count()
num_districts_after = District.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 from konova.settings import DEFAULT_SRID
geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID) geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID)
def __str__(self):
return str(self.id)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs) super().save(*args, **kwargs)
self.check_for_conflicts() self.check_for_conflicts()
@@ -99,7 +102,7 @@ class Geometry(BaseResource):
Returns: Returns:
""" """
from konova.models import Parcel, District, ParcelIntersection from konova.models import Parcel, District, ParcelIntersection, Municipal, ParcelGroup
parcel_fetcher = ParcelWFSFetcher( parcel_fetcher = ParcelWFSFetcher(
geometry_id=self.id, geometry_id=self.id,
) )
@@ -110,20 +113,38 @@ class Geometry(BaseResource):
_now = timezone.now() _now = timezone.now()
underlying_parcels = [] underlying_parcels = []
for result in fetched_parcels: for result in fetched_parcels:
fetched_parcel = result[typename] parcel_properties = result["properties"]
# There could be parcels which include the word 'Flur', # There could be parcels which include the word 'Flur',
# which needs to be deleted and just keep the numerical values # which needs to be deleted and just keep the numerical values
## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE! ## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE!
flr_val = fetched_parcel["ave:flur"].replace("Flur ", "") flr_val = parcel_properties["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]
district = District.objects.get_or_create( district = District.objects.get_or_create(
gmnd=fetched_parcel["ave:gemeinde"], key=parcel_properties["kreisschl"],
krs=fetched_parcel["ave:kreis"], 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] )[0]
parcel_obj.district = district parcel_obj.district = district
parcel_obj.updated_on = _now parcel_obj.updated_on = _now
@@ -155,9 +176,10 @@ class Geometry(BaseResource):
parcels = self.parcels.filter( parcels = self.parcels.filter(
parcelintersection__calculated_on__isnull=False, parcelintersection__calculated_on__isnull=False,
).prefetch_related( ).prefetch_related(
"district" "district",
"municipal",
).order_by( ).order_by(
"gmrkng", "municipal__name",
) )
return parcels return parcels

View File

@@ -289,6 +289,8 @@ class RecordableObjectMixin(models.Model):
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
if self.recorded: if self.recorded:
return None return None
self.unshare_with_default_users()
action = UserActionLogEntry.get_recorded_action(user) action = UserActionLogEntry.get_recorded_action(user)
self.recorded = action self.recorded = action
self.save() self.save()
@@ -335,6 +337,15 @@ class RecordableObjectMixin(models.Model):
""" """
raise NotImplementedError("Implement this in the subclass!") 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): class CheckableObjectMixin(models.Model):
# Checks - Refers to "Genehmigen" but optional # Checks - Refers to "Genehmigen" but optional
@@ -608,6 +619,26 @@ class ShareableObjectMixin(models.Model):
""" """
raise NotImplementedError("Must be implemented in subclasses!") 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): class GeoReferencedMixin(models.Model):
geometry = models.ForeignKey("konova.Geometry", null=True, blank=True, on_delete=models.SET_NULL) 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 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): 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 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. 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') 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") district = models.ForeignKey("konova.District", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels")
gmrkng = models.CharField( municipal = models.ForeignKey("konova.Municipal", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels")
max_length=1000, parcel_group = models.ForeignKey(
"konova.ParcelGroup",
on_delete=models.SET_NULL,
help_text="Gemarkung", help_text="Gemarkung",
null=True, null=True,
blank=True, blank=True,
related_name="parcels"
) )
flrstck_nnr = models.CharField( flr = models.IntegerField(
max_length=1000, help_text="Flur",
null=True,
blank=True,
)
flrstck_nnr = models.IntegerField(
help_text="Flurstücksnenner", help_text="Flurstücksnenner",
null=True, null=True,
blank=True, blank=True,
) )
flrstck_zhlr = models.CharField( flrstck_zhlr = models.IntegerField(
max_length=1000,
help_text="Flurstückszähler", help_text="Flurstückszähler",
null=True, null=True,
blank=True, blank=True,
) )
flr = models.CharField(
max_length=1000,
help_text="Flur",
null=True,
blank=True,
)
updated_on = models.DateTimeField(auto_now_add=True) updated_on = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
return f"{self.gmrkng} | {self.flr} | {self.flrstck_zhlr} | {self.flrstck_nnr}" return f"{self.parcel_group} | {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}"
class ParcelIntersection(UuidModel): class ParcelIntersection(UuidModel):

View File

@@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/3.1/ref/settings/
""" """
import os import os
from django.utils.translation import gettext_lazy as _ 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'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = os.path.dirname( BASE_DIR = os.path.dirname(
@@ -162,9 +163,15 @@ LANGUAGES = [
USE_THOUSAND_SEPARATOR = True USE_THOUSAND_SEPARATOR = True
# Regular python relevant date/datetime formatting
DEFAULT_DATE_TIME_FORMAT = '%d.%m.%Y %H:%M:%S' DEFAULT_DATE_TIME_FORMAT = '%d.%m.%Y %H:%M:%S'
DEFAULT_DATE_FORMAT = '%d.%m.%Y' 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' TIME_ZONE = 'Europe/Berlin'
USE_I18N = True USE_I18N = True

View File

@@ -19,6 +19,6 @@ PAGE_SIZE_OPTIONS_TUPLES = [
(50, 50), (50, 50),
(100, 100), (100, 100),
] ]
PAGE_SIZE_DEFAULT = 5 PAGE_SIZE_DEFAULT = 10
PAGE_SIZE_MAX = 100 PAGE_SIZE_MAX = 100
PAGE_DEFAULT = 1 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> </h5>
</div> </div>
<div class="card-body"> <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"> <div class="row justify-content-center">
<span class="spinner-border rlp-r-inv" role="status"></span> <span class="spinner-border rlp-r-inv" role="status"></span>
</div> </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 codelist.models import KonovaCode, KonovaCodeList
from compensation.models import Compensation, CompensationState, CompensationAction, EcoAccount, EcoAccountDeduction 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.management.commands.setup_data import GROUPS_DATA
from konova.models import Geometry from konova.models import Geometry
from konova.settings import DEFAULT_GROUP from konova.settings import DEFAULT_GROUP
@@ -57,6 +57,7 @@ class BaseTestCase(TestCase):
self.create_users() self.create_users()
self.create_groups() self.create_groups()
self.handler = self.create_dummy_handler()
self.intervention = self.create_dummy_intervention() self.intervention = self.create_dummy_intervention()
self.compensation = self.create_dummy_compensation() self.compensation = self.create_dummy_compensation()
self.eco_account = self.create_dummy_eco_account() self.eco_account = self.create_dummy_eco_account()
@@ -122,7 +123,9 @@ class BaseTestCase(TestCase):
# Create legal data object (without M2M laws first) # Create legal data object (without M2M laws first)
legal_data = Legal.objects.create() legal_data = Legal.objects.create()
# Create responsible data object # Create responsible data object
responsibility_data = Responsibility.objects.create() responsibility_data = Responsibility.objects.create(
handler=self.handler
)
geometry = Geometry.objects.create() geometry = Geometry.objects.create()
# Finally create main object, holding the other objects # Finally create main object, holding the other objects
intervention = Intervention.objects.create( intervention = Intervention.objects.create(
@@ -173,6 +176,9 @@ class BaseTestCase(TestCase):
# Create responsible data object # Create responsible data object
lega_data = Legal.objects.create() lega_data = Legal.objects.create()
responsible_data = Responsibility.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 # Finally create main object, holding the other objects
eco_account = EcoAccount.objects.create( eco_account = EcoAccount.objects.create(
identifier="TEST", identifier="TEST",
@@ -197,6 +203,8 @@ class BaseTestCase(TestCase):
geometry = Geometry.objects.create() geometry = Geometry.objects.create()
# Create responsible data object # Create responsible data object
responsible_data = Responsibility.objects.create() responsible_data = Responsibility.objects.create()
responsible_data.handler = self.handler
responsible_data.save()
# Finally create main object, holding the other objects # Finally create main object, holding the other objects
ema = Ema.objects.create( ema = Ema.objects.create(
identifier="TEST", identifier="TEST",
@@ -282,6 +290,18 @@ class BaseTestCase(TestCase):
polygon = polygon.transform(3857, clone=True) polygon = polygon.transform(3857, clone=True)
return MultiPolygon(polygon, srid=3857) # 3857 is the default srid used for MultiPolygonField in the form 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: def fill_out_intervention(self, intervention: Intervention) -> Intervention:
""" Adds all required (dummy) data to an 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.conservation_office = KonovaCode.objects.get(id=2)
intervention.responsible.registration_file_number = "test" intervention.responsible.registration_file_number = "test"
intervention.responsible.conservation_file_number = "test" intervention.responsible.conservation_file_number = "test"
intervention.responsible.handler = "handler" intervention.responsible.handler = self.handler
intervention.responsible.save() intervention.responsible.save()
intervention.legal.registration_date = datetime.date.fromisoformat("1970-01-01") intervention.legal.registration_date = datetime.date.fromisoformat("1970-01-01")
intervention.legal.binding_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_office = self.get_conservation_office_code()
ema.responsible.conservation_file_number = "test" ema.responsible.conservation_file_number = "test"
ema.responsible.handler = "handler" ema.responsible.handler = self.handler
ema.responsible.save() ema.responsible.save()
ema.after_states.add(self.comp_state) ema.after_states.add(self.comp_state)
ema.before_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.legal.save()
eco_account.responsible.conservation_office = self.get_conservation_office_code() eco_account.responsible.conservation_office = self.get_conservation_office_code()
eco_account.responsible.conservation_file_number = "test" eco_account.responsible.conservation_file_number = "test"
eco_account.responsible.handler = "handler" eco_account.responsible.handler = self.handler
eco_account.responsible.save() eco_account.responsible.save()
eco_account.after_states.add(self.comp_state) eco_account.after_states.add(self.comp_state)
eco_account.before_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, \ from konova.autocompletes import EcoAccountAutocomplete, \
InterventionAutocomplete, CompensationActionCodeAutocomplete, BiotopeCodeAutocomplete, LawCodeAutocomplete, \ InterventionAutocomplete, CompensationActionCodeAutocomplete, BiotopeCodeAutocomplete, LawCodeAutocomplete, \
RegistrationOfficeCodeAutocomplete, ConservationOfficeCodeAutocomplete, ProcessTypeCodeAutocomplete, \ 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.settings import SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY, DEBUG
from konova.sso.sso import KonovaSSOClient 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) sso_client = KonovaSSOClient(SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY)
urlpatterns = [ urlpatterns = [
@@ -39,7 +40,8 @@ urlpatterns = [
path('cl/', include("codelist.urls")), path('cl/', include("codelist.urls")),
path('analysis/', include("analysis.urls")), path('analysis/', include("analysis.urls")),
path('api/', include("api.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 # Autocomplete paths for all apps
path("atcmplt/eco-accounts", EcoAccountAutocomplete.as_view(), name="accounts-autocomplete"), 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/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/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/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/u", ShareUserAutocomplete.as_view(), name="share-user-autocomplete"),
path("atcmplt/share/t", ShareTeamAutocomplete.as_view(), name="share-team-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 _ from django.utils.translation import gettext_lazy as _
NO_DETAILS = _("no further details")
UNKNOWN = _("Unknown")
UNGROUPED = _("Ungrouped") UNGROUPED = _("Ungrouped")
FORM_INVALID = _("There was an error on this form.") FORM_INVALID = _("There was an error on this form.")
PARAMS_INVALID = _("Invalid parameters") 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.") 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.") MISSING_GROUP_PERMISSION = _("You need to be part of another user group.")
CHECKED_RECORDED_RESET = _("Status of Checked and Recorded reseted") 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 # SHARE
DATA_UNSHARED = _("This data is not shared with you") 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 Created on: 17.12.21
""" """
import json
from abc import abstractmethod from abc import abstractmethod
from json import JSONDecodeError
from time import sleep from time import sleep
import requests import requests
import xmltodict
from django.contrib.gis.db.models.functions import AsGML, Transform from django.contrib.gis.db.models.functions import AsGML, Transform
from requests.auth import HTTPDigestAuth from requests.auth import HTTPDigestAuth
@@ -115,7 +116,7 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
geometry_operation, geometry_operation,
filter_srid 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 return _filter
def get_features(self, def get_features(self,
@@ -139,7 +140,7 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
Returns: Returns:
features (list): A list of returned features features (list): A list of returned features
""" """
features = [] found_features = []
while start_index is not None: while start_index is not None:
post_body = self._create_post_data( post_body = self._create_post_data(
spatial_operator, spatial_operator,
@@ -155,19 +156,11 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
) )
content = response.content.decode("utf-8") content = response.content.decode("utf-8")
content = xmltodict.parse(content) try:
collection = content.get( # Check if collection is an exception and does not contain the requested data
"wfs:FeatureCollection", content = json.loads(content)
{}, except JSONDecodeError as e:
) if rerun_on_exception:
# 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:
# Wait a second before another try # Wait a second before another try
sleep(1) sleep(1)
self.get_features( self.get_features(
@@ -177,22 +170,21 @@ class ParcelWFSFetcher(AbstractWFSFetcher):
start_index, start_index,
rerun_on_exception=False 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: else:
# convert single found feature member into list and extent feature list e.msg += content
features += [members] raise e
fetched_features = content.get(
"features",
{},
)
if collection.get("@next", None) is not None: found_features += fetched_features
start_index += self.count
else: if len(fetched_features) < self.count:
# The response was not 'full', so we got everything to fetch
start_index = None 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 intervention.models import Intervention
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import any_group_check 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 konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from news.models import ServerMessage from news.models import ServerMessage
from konova.settings import SSO_SERVER_BASE 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 id (str): The geometry's id
Returns: Returns:
A rendered piece of HTML
""" """
# HTTP code 286 states that the HTMX should stop polling for updates # HTTP code 286 states that the HTMX should stop polling for updates
# https://htmx.org/docs/#polling # https://htmx.org/docs/#polling
status_code = 286 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) geom = get_object_or_404(Geometry, id=id)
parcels = geom.get_underlying_parcels() parcels = geom.get_underlying_parcels()
geos_geom = geom.geom geos_geom = geom.geom
@@ -130,8 +130,21 @@ def get_geom_parcels(request: HttpRequest, id: str):
status_code = 200 status_code = 200
if parcels_available or no_geometry_given: 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 = { context = {
"parcels": parcels, "parcels": parcels,
"municipals": municipals,
"geom_id": str(id),
"next_page": next_page,
} }
html = render_to_string(template, context, request) html = render_to_string(template, context, request)
return HttpResponse(html, status=status_code) return HttpResponse(html, status=status_code)
@@ -139,6 +152,49 @@ def get_geom_parcels(request: HttpRequest, id: str):
return HttpResponse(None, status=404) 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): def get_404_view(request: HttpRequest, exception=None):
""" Returns a 404 handling view """ 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 openpyxl==3.0.9
OWSLib==0.25.0 OWSLib==0.25.0
packaging==21.3 packaging==21.3
pika==1.2.0
prompt-toolkit==3.0.24 prompt-toolkit==3.0.24
psycopg2-binary==2.9.1 psycopg2-binary==2.9.1
pyparsing==3.0.6 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> <hr>
<p class="lead"> <p class="lead">
{% blocktrans %} {% 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 %} {% endblocktrans %}
</p> </p>
</div> </div>

View File

@@ -74,6 +74,15 @@ class TeamAdmin(admin.ModelAdmin):
"name", "name",
"description", "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) admin.site.register(User, UserAdmin)

View File

@@ -317,6 +317,15 @@ class RemoveTeamModalForm(RemoveModalForm):
pass 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): class TeamDataForm(BaseModalForm):
name = forms.CharField( name = forms.CharField(
label_suffix="", label_suffix="",

View File

@@ -93,3 +93,17 @@ class Team(UuidModel):
""" """
mailer = Mailer() mailer = Mailer()
mailer.send_mail_shared_data_deleted_team(obj_identifier, obj_title, self) 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.db import models
from django.utils.translation import gettext_lazy as _ 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): class UserAction(models.TextChoices):

View File

@@ -54,14 +54,6 @@
</button> </button>
</a> </a>
</div> </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"> <div class="row mb-2">
<a href="{% url 'user:team-index' %}" title="{% trans 'Manage teams' %}"> <a href="{% url 'user:team-index' %}" title="{% trans 'Manage teams' %}">
<button class="btn btn-default"> <button class="btn btn-default">
@@ -70,6 +62,14 @@
</button> </button>
</a> </a>
</div> </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> </div>
</div> </div>

View File

@@ -46,6 +46,9 @@
{% endfor %} {% endfor %}
</td> </td>
<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 %} {% if team.admin == user %}
<button class="btn rlp-r btn-modal" data-form-url="{% url 'user:team-edit' team.id %}" title="{% trans 'Edit team' %}"> <button class="btn rlp-r btn-modal" data-form-url="{% url 'user:team-edit' team.id %}" title="{% trans 'Edit team' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}

View File

@@ -20,5 +20,6 @@ urlpatterns = [
path("team/<id>", data_team_view, name="team-data"), path("team/<id>", data_team_view, name="team-data"),
path("team/<id>/edit", edit_team_view, name="team-edit"), path("team/<id>/edit", edit_team_view, name="team-edit"),
path("team/<id>/remove", remove_team_view, name="team-remove"), 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.contexts import BaseContext
from konova.decorators import any_group_check, default_group_required from konova.decorators import any_group_check, default_group_required
from user.forms import UserNotificationForm, UserContactForm, UserAPITokenForm, NewTeamModalForm, EditTeamModalForm, \ from user.forms import UserNotificationForm, UserContactForm, UserAPITokenForm, NewTeamModalForm, EditTeamModalForm, \
RemoveTeamModalForm, TeamDataForm RemoveTeamModalForm, TeamDataForm, LeaveTeamModalForm
@login_required @login_required
@@ -204,3 +204,24 @@ def remove_team_view(request: HttpRequest, id: str):
_("Team removed"), _("Team removed"),
redirect_url=reverse("user:team-index") 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")
)