diff --git a/codelist/admin.py b/codelist/admin.py
index 55ce827d..ccdcb057 100644
--- a/codelist/admin.py
+++ b/codelist/admin.py
@@ -33,6 +33,7 @@ class KonovaCodeAdmin(admin.ModelAdmin):
"is_selectable",
"is_leaf",
"parent",
+ "found_in_codelists",
]
search_fields = [
@@ -42,6 +43,12 @@ class KonovaCodeAdmin(admin.ModelAdmin):
"short_name",
]
+ def found_in_codelists(self, obj):
+ codelists = KonovaCodeList.objects.filter(
+ codes__in=[obj]
+ ).values_list("id", flat=True)
+ codelists = "\n".join(str(x) for x in codelists)
+ return codelists
#admin.site.register(KonovaCodeList, KonovaCodeListAdmin)
admin.site.register(KonovaCode, KonovaCodeAdmin)
diff --git a/compensation/admin.py b/compensation/admin.py
index 5f792f76..a5cd1a88 100644
--- a/compensation/admin.py
+++ b/compensation/admin.py
@@ -21,16 +21,30 @@ class AbstractCompensationAdmin(BaseObjectAdmin):
"identifier",
"title",
"comment",
- "after_states",
- "before_states",
+ "list_after_states",
+ "list_before_states",
+ "geometry",
]
def get_readonly_fields(self, request, obj=None):
return super().get_readonly_fields(request, obj) + [
- "after_states",
- "before_states",
+ "list_after_states",
+ "list_before_states",
+ "geometry",
]
+ def list_after_states(self, obj):
+ states = obj.after_states.all()
+ states = [str(state) for state in states]
+ states = "\n".join(states)
+ return states
+
+ def list_before_states(self, obj):
+ states = obj.before_states.all()
+ states = [str(state) for state in states]
+ states = "\n".join(states)
+ return states
+
class CompensationAdmin(AbstractCompensationAdmin):
autocomplete_fields = [
diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py
index ea69cefc..3deff50a 100644
--- a/compensation/models/compensation.py
+++ b/compensation/models/compensation.py
@@ -418,6 +418,18 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
super().set_status_messages(request)
return request
+ @property
+ def is_recorded(self):
+ """ Getter for record status as property
+
+ Since compensations inherit their record status from their intervention, the intervention's status is being
+ returned
+
+ Returns:
+
+ """
+ return self.intervention.is_recorded
+
class CompensationDocument(AbstractDocument):
"""
diff --git a/compensation/tables.py b/compensation/tables.py
index 96888cc1..401a7416 100644
--- a/compensation/tables.py
+++ b/compensation/tables.py
@@ -134,7 +134,7 @@ class CompensationTable(BaseTable, TableRenderMixin):
"""
parcels = value.get_underlying_parcels().values_list(
- "gmrkng",
+ "parcel_group__name",
flat=True
).distinct()
html = render_to_string(
@@ -181,9 +181,7 @@ class CompensationTable(BaseTable, TableRenderMixin):
"""
if value is None:
value = User.objects.none()
- has_access = value.filter(
- id=self.user.id
- ).exists()
+ has_access = record.is_shared_with(self.user)
html = self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),
@@ -295,7 +293,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
"""
parcels = value.get_underlying_parcels().values_list(
- "gmrkng",
+ "parcel_group__name",
flat=True
).distinct()
html = render_to_string(
@@ -343,7 +341,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
html = ""
# Do not use value in here, since value does use unprefetched 'users' manager, where record has already
# prefetched users data
- has_access = self.user in record.users.all()
+ has_access = record.is_shared_with(self.user)
html += self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),
icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit",
diff --git a/compensation/templates/compensation/detail/compensation/view.html b/compensation/templates/compensation/detail/compensation/view.html
index cc9c4ebf..cd5332aa 100644
--- a/compensation/templates/compensation/detail/compensation/view.html
+++ b/compensation/templates/compensation/detail/compensation/view.html
@@ -90,9 +90,15 @@
{% trans 'Last modified' %} |
- {{obj.modified.timestamp|default_if_none:""|naturalday}}
-
- {{obj.modified.user.username}}
+ {% if obj.modified %}
+ {{obj.modified.timestamp|default_if_none:""}}
+
+ {{obj.modified.user.username}}
+ {% else %}
+ {{obj.created.timestamp|default_if_none:""}}
+
+ {{obj.created.user.username}}
+ {% endif %}
|
diff --git a/compensation/templates/compensation/detail/eco_account/view.html b/compensation/templates/compensation/detail/eco_account/view.html
index ba1ce96a..f5fc7cf3 100644
--- a/compensation/templates/compensation/detail/eco_account/view.html
+++ b/compensation/templates/compensation/detail/eco_account/view.html
@@ -73,9 +73,15 @@
{% trans 'Last modified' %} |
- {{obj.modified.timestamp|default_if_none:""|naturalday}}
-
- {{obj.modified.user.username}}
+ {% if obj.modified %}
+ {{obj.modified.timestamp|default_if_none:""}}
+
+ {{obj.modified.user.username}}
+ {% else %}
+ {{obj.created.timestamp|default_if_none:""}}
+
+ {{obj.created.user.username}}
+ {% endif %}
|
diff --git a/compensation/templates/compensation/report/compensation/report.html b/compensation/templates/compensation/report/compensation/report.html
index 4a773d6b..9caa7100 100644
--- a/compensation/templates/compensation/report/compensation/report.html
+++ b/compensation/templates/compensation/report/compensation/report.html
@@ -43,14 +43,7 @@
{% include 'konova/includes/parcels.html' %}
-
-
{% trans 'Open in browser' %}
- {{ qrcode|safe }}
-
-
-
{% trans 'View in LANIS' %}
- {{ qrcode_lanis|safe }}
-
+ {% include 'konova/includes/report/qrcodes.html' %}
diff --git a/compensation/templates/compensation/report/eco_account/report.html b/compensation/templates/compensation/report/eco_account/report.html
index c0959244..01a631a8 100644
--- a/compensation/templates/compensation/report/eco_account/report.html
+++ b/compensation/templates/compensation/report/eco_account/report.html
@@ -56,14 +56,7 @@
{% include 'konova/includes/parcels.html' %}
-
-
{% trans 'Open in browser' %}
- {{ qrcode|safe }}
-
-
-
{% trans 'View in LANIS' %}
- {{ qrcode_lanis|safe }}
-
+ {% include 'konova/includes/report/qrcodes.html' %}
diff --git a/compensation/tests/compensation/test_workflow.py b/compensation/tests/compensation/test_workflow.py
index 5b7decff..570045f2 100644
--- a/compensation/tests/compensation/test_workflow.py
+++ b/compensation/tests/compensation/test_workflow.py
@@ -60,8 +60,9 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
# Preserve the current number of intervention's compensations
num_compensations = self.intervention.compensations.count()
- self.client_user.post(new_url, post_data)
+ response = self.client_user.post(new_url, post_data)
+ self.assertEqual(302, response.status_code)
self.intervention.refresh_from_db()
self.assertEqual(num_compensations + 1, self.intervention.compensations.count())
new_compensation = self.intervention.compensations.get(identifier=test_id)
@@ -261,3 +262,26 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
self.assertIn(recorded, self.compensation.log.all())
self.assertEqual(pre_record_log_count + 1, self.compensation.log.count())
+ def test_non_editable_after_recording(self):
+ """ Tests that the compensation can not be edited after being recorded
+
+ User must be redirected to another page
+
+ Returns:
+
+ """
+ self.assertIsNotNone(self.compensation)
+ self.assertFalse(self.compensation.is_recorded)
+ edit_url = reverse("compensation:edit", args=(self.compensation.id,))
+ response = self.client_user.get(edit_url)
+ has_redirect = response.status_code == 302
+ self.assertFalse(has_redirect)
+
+ self.compensation.intervention.set_recorded(self.user)
+ self.assertTrue(self.compensation.is_recorded)
+
+ edit_url = reverse("compensation:edit", args=(self.compensation.id,))
+ response = self.client_user.get(edit_url)
+ has_redirect = response.status_code == 302
+ self.assertTrue(has_redirect)
+ self.compensation.intervention.set_unrecorded(self.user)
diff --git a/compensation/tests/ecoaccount/test_workflow.py b/compensation/tests/ecoaccount/test_workflow.py
index 03b4996a..bd6894ae 100644
--- a/compensation/tests/ecoaccount/test_workflow.py
+++ b/compensation/tests/ecoaccount/test_workflow.py
@@ -302,3 +302,27 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.assertEqual(pre_edit_account_log_count + 1, account.log.count())
self.assertEqual(intervention.log.first().action, UserAction.EDITED)
self.assertEqual(account.log.first().action, UserAction.EDITED)
+
+ def test_non_editable_after_recording(self):
+ """ Tests that the eco_account can not be edited after being recorded
+
+ User must be redirected to another page
+
+ Returns:
+
+ """
+ self.assertIsNotNone(self.eco_account)
+ self.assertFalse(self.eco_account.is_recorded)
+ edit_url = reverse("compensation:acc:edit", args=(self.eco_account.id,))
+ response = self.client_user.get(edit_url)
+ has_redirect = response.status_code == 302
+ self.assertFalse(has_redirect)
+
+ self.eco_account.set_recorded(self.user)
+ self.assertTrue(self.eco_account.is_recorded)
+
+ edit_url = reverse("compensation:acc:edit", args=(self.eco_account.id,))
+ response = self.client_user.get(edit_url)
+ has_redirect = response.status_code == 302
+ self.assertTrue(has_redirect)
+ self.eco_account.set_unrecorded(self.user)
diff --git a/compensation/views/compensation.py b/compensation/views/compensation.py
index 6dcf442b..c3d3e2b5 100644
--- a/compensation/views/compensation.py
+++ b/compensation/views/compensation.py
@@ -1,4 +1,5 @@
from django.contrib.auth.decorators import login_required
+from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Sum
from django.http import HttpRequest, JsonResponse
from django.shortcuts import render
@@ -22,7 +23,7 @@ from konova.utils.message_templates import FORM_INVALID, IDENTIFIER_REPLACED, DA
CHECKED_RECORDED_RESET, COMPENSATION_ADDED_TEMPLATE, COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, \
COMPENSATION_STATE_REMOVED, COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_ADDED, \
DEADLINE_ADDED, DEADLINE_REMOVED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, \
- DEADLINE_EDITED
+ DEADLINE_EDITED, RECORDED_BLOCKS_EDIT, PARAMS_INVALID
from konova.utils.user_checks import in_group
@@ -69,6 +70,19 @@ def new_view(request: HttpRequest, intervention_id: str = None):
"""
template = "compensation/form/view.html"
+ if intervention_id is not None:
+ try:
+ intervention = Intervention.objects.get(id=intervention_id)
+ except ObjectDoesNotExist:
+ messages.error(request, PARAMS_INVALID)
+ return redirect("home")
+ if intervention.is_recorded:
+ messages.info(
+ request,
+ RECORDED_BLOCKS_EDIT
+ )
+ return redirect("intervention:detail", id=intervention_id)
+
data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST":
@@ -134,6 +148,13 @@ def edit_view(request: HttpRequest, id: str):
template = "compensation/form/view.html"
# Get object from db
comp = get_object_or_404(Compensation, id=id)
+ if comp.is_recorded:
+ messages.info(
+ request,
+ RECORDED_BLOCKS_EDIT
+ )
+ return redirect("compensation:detail", id=id)
+
# Create forms, initialize with values from db/from POST request
data_form = EditCompensationForm(request.POST or None, instance=comp)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp)
@@ -596,14 +617,12 @@ def report_view(request: HttpRequest, id: str):
instance=comp
)
parcels = comp.get_underlying_parcels()
- qrcode_img = generate_qr_code(
- request.build_absolute_uri(reverse("compensation:report", args=(id,))),
- 10
- )
- qrcode_img_lanis = generate_qr_code(
- comp.get_LANIS_link(),
- 7
- )
+
+ qrcode_url = request.build_absolute_uri(reverse("compensation:report", args=(id,)))
+ qrcode_img = generate_qr_code(qrcode_url, 10)
+ qrcode_lanis_url = comp.get_LANIS_link()
+ qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
+
# Order states by surface
before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type")
after_states = comp.after_states.all().order_by("-surface").prefetch_related("biotope_type")
@@ -611,8 +630,14 @@ def report_view(request: HttpRequest, id: str):
context = {
"obj": comp,
- "qrcode": qrcode_img,
- "qrcode_lanis": qrcode_img_lanis,
+ "qrcode": {
+ "img": qrcode_img,
+ "url": qrcode_url,
+ },
+ "qrcode_lanis": {
+ "img": qrcode_img_lanis,
+ "url": qrcode_lanis_url,
+ },
"has_access": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py
index 85b13714..ecaccbe6 100644
--- a/compensation/views/eco_account.py
+++ b/compensation/views/eco_account.py
@@ -35,7 +35,8 @@ from konova.utils.generators import generate_qr_code
from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \
CANCEL_ACC_RECORDED_OR_DEDUCTED, DEDUCTION_REMOVED, DEDUCTION_ADDED, DOCUMENT_ADDED, COMPENSATION_STATE_REMOVED, \
COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_ADDED, DEADLINE_ADDED, DEADLINE_REMOVED, \
- DEDUCTION_EDITED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED
+ DEDUCTION_EDITED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED, \
+ RECORDED_BLOCKS_EDIT
from konova.utils.user_checks import in_group
@@ -145,6 +146,13 @@ def edit_view(request: HttpRequest, id: str):
template = "compensation/form/view.html"
# Get object from db
acc = get_object_or_404(EcoAccount, id=id)
+ if acc.is_recorded:
+ messages.info(
+ request,
+ RECORDED_BLOCKS_EDIT
+ )
+ return redirect("compensation:acc:detail", id=id)
+
# Create forms, initialize with values from db/from POST request
data_form = EditEcoAccountForm(request.POST or None, instance=acc)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc)
@@ -731,18 +739,16 @@ def report_view(request:HttpRequest, id: str):
instance=acc
)
parcels = acc.get_underlying_parcels()
- qrcode_img = generate_qr_code(
- request.build_absolute_uri(reverse("ema:report", args=(id,))),
- 10
- )
- qrcode_img_lanis = generate_qr_code(
- acc.get_LANIS_link(),
- 7
- )
+
+ qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,)))
+ qrcode_img = generate_qr_code(qrcode_url, 10)
+ qrcode_lanis_url = acc.get_LANIS_link()
+ qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
+
# Order states by surface
before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent")
after_states = acc.after_states.all().order_by("-surface").select_related("biotope_type__parent")
- actions = acc.actions.all().select_related("action_type__parent")
+ actions = acc.actions.all().prefetch_related("action_type__parent")
# Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier)
deductions = acc.deductions.all()\
@@ -752,8 +758,14 @@ def report_view(request:HttpRequest, id: str):
context = {
"obj": acc,
- "qrcode": qrcode_img,
- "qrcode_lanis": qrcode_img_lanis,
+ "qrcode": {
+ "img": qrcode_img,
+ "url": qrcode_url,
+ },
+ "qrcode_lanis": {
+ "img": qrcode_img_lanis,
+ "url": qrcode_lanis_url,
+ },
"has_access": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
diff --git a/ema/tables.py b/ema/tables.py
index f9689517..38d8a8c0 100644
--- a/ema/tables.py
+++ b/ema/tables.py
@@ -104,7 +104,7 @@ class EmaTable(BaseTable, TableRenderMixin):
"""
parcels = value.get_underlying_parcels().values_list(
- "gmrkng",
+ "parcel_group__name",
flat=True
).distinct()
html = render_to_string(
@@ -151,9 +151,7 @@ class EmaTable(BaseTable, TableRenderMixin):
"""
html = ""
- has_access = value.filter(
- id=self.user.id
- ).exists()
+ has_access = record.is_shared_with(self.user)
html += self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),
diff --git a/ema/templates/ema/detail/view.html b/ema/templates/ema/detail/view.html
index 38f2c1ed..14a6a11a 100644
--- a/ema/templates/ema/detail/view.html
+++ b/ema/templates/ema/detail/view.html
@@ -60,14 +60,13 @@
{% trans 'Last modified' %} |
{% if obj.modified %}
- {{obj.modified.timestamp|default_if_none:""|naturalday}}
+ {{obj.modified.timestamp|default_if_none:""}}
{{obj.modified.user.username}}
{% else %}
- {{obj.created.timestamp|default_if_none:""|naturalday}}
+ {{obj.created.timestamp|default_if_none:""}}
{{obj.created.user.username}}
-
{% endif %}
|
diff --git a/ema/templates/ema/report/report.html b/ema/templates/ema/report/report.html
index 4cc80fee..3f62e599 100644
--- a/ema/templates/ema/report/report.html
+++ b/ema/templates/ema/report/report.html
@@ -43,14 +43,7 @@
{% include 'konova/includes/parcels.html' %}
-
-
{% trans 'Open in browser' %}
- {{ qrcode|safe }}
-
-
-
{% trans 'View in LANIS' %}
- {{ qrcode_lanis|safe }}
-
+ {% include 'konova/includes/report/qrcodes.html' %}
diff --git a/ema/tests/test_workflow.py b/ema/tests/test_workflow.py
index 3306a21f..ecc3f195 100644
--- a/ema/tests/test_workflow.py
+++ b/ema/tests/test_workflow.py
@@ -117,6 +117,32 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
self.assertEqual(pre_edit_log_count + 1, self.ema.log.count())
self.assertEqual(self.ema.log.first().action, UserAction.EDITED)
+ def test_non_editable_after_recording(self):
+ """ Tests that the EMA can not be edited after being recorded
+
+ User must be redirected to another page
+
+ Returns:
+
+ """
+ self.superuser.groups.add(self.groups.get(name=ETS_GROUP))
+ self.assertIsNotNone(self.ema)
+ self.ema.share_with_user(self.superuser)
+ self.assertFalse(self.ema.is_recorded)
+ edit_url = reverse("ema:edit", args=(self.ema.id,))
+ response = self.client_user.get(edit_url)
+ has_redirect = response.status_code == 302
+ self.assertFalse(has_redirect)
+
+ self.ema.set_recorded(self.superuser)
+ self.assertTrue(self.ema.is_recorded)
+
+ edit_url = reverse("ema:edit", args=(self.ema.id,))
+ response = self.client_user.get(edit_url)
+ has_redirect = response.status_code == 302
+ self.assertTrue(has_redirect)
+ self.ema.set_unrecorded(self.superuser)
+
def test_recordability(self):
"""
This tests if the recordability of the Ema is triggered by the quality of it's data (e.g. not all fields filled)
diff --git a/ema/views.py b/ema/views.py
index c145511f..ce0d68f5 100644
--- a/ema/views.py
+++ b/ema/views.py
@@ -26,7 +26,7 @@ from konova.utils.generators import generate_qr_code
from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \
DOCUMENT_ADDED, COMPENSATION_STATE_REMOVED, COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, \
COMPENSATION_ACTION_ADDED, DEADLINE_ADDED, DEADLINE_REMOVED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, \
- COMPENSATION_ACTION_EDITED, DEADLINE_EDITED
+ COMPENSATION_ACTION_EDITED, DEADLINE_EDITED, RECORDED_BLOCKS_EDIT
from konova.utils.user_checks import in_group
@@ -213,6 +213,13 @@ def edit_view(request: HttpRequest, id: str):
template = "compensation/form/view.html"
# Get object from db
ema = get_object_or_404(Ema, id=id)
+ if ema.is_recorded:
+ messages.info(
+ request,
+ RECORDED_BLOCKS_EDIT
+ )
+ return redirect("ema:detail", id=id)
+
# Create forms, initialize with values from db/from POST request
data_form = EditEmaForm(request.POST or None, instance=ema)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=ema)
@@ -563,14 +570,12 @@ def report_view(request:HttpRequest, id: str):
instance=ema,
)
parcels = ema.get_underlying_parcels()
- qrcode_img = generate_qr_code(
- request.build_absolute_uri(reverse("ema:report", args=(id,))),
- 10
- )
- qrcode_img_lanis = generate_qr_code(
- ema.get_LANIS_link(),
- 7
- )
+
+ qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,)))
+ qrcode_img = generate_qr_code(qrcode_url, 10)
+ qrcode_lanis_url = ema.get_LANIS_link()
+ qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
+
# Order states by surface
before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type")
after_states = ema.after_states.all().order_by("-surface").prefetch_related("biotope_type")
@@ -578,8 +583,14 @@ def report_view(request:HttpRequest, id: str):
context = {
"obj": ema,
- "qrcode": qrcode_img,
- "qrcode_lanis": qrcode_img_lanis,
+ "qrcode": {
+ "img": qrcode_img,
+ "url": qrcode_url
+ },
+ "qrcode_lanis": {
+ "img": qrcode_img_lanis,
+ "url": qrcode_lanis_url
+ },
"has_access": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
diff --git a/intervention/admin.py b/intervention/admin.py
index 3d874df6..932ddb93 100644
--- a/intervention/admin.py
+++ b/intervention/admin.py
@@ -25,12 +25,14 @@ class InterventionAdmin(BaseObjectAdmin):
"checked",
"recorded",
"users",
+ "geometry",
]
def get_readonly_fields(self, request, obj=None):
return super().get_readonly_fields(request, obj) + [
"checked",
"recorded",
+ "geometry",
]
diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py
index 6a02962e..07911be9 100644
--- a/intervention/forms/modalForms.py
+++ b/intervention/forms/modalForms.py
@@ -427,13 +427,22 @@ class NewDeductionModalForm(BaseModalForm):
"""
super_result = super().is_valid()
acc = self.cleaned_data["account"]
+ intervention = self.cleaned_data["intervention"]
+ objects_valid = True
if not acc.recorded:
self.add_error(
"account",
_("Eco-account {} is not recorded yet. You can only deduct from recorded accounts.").format(acc.identifier)
)
- return False
+ objects_valid = False
+
+ if intervention.is_recorded:
+ self.add_error(
+ "intervention",
+ _("Intervention {} is currently recorded. To change any data on it, the entry must be unrecorded.").format(intervention.identifier)
+ )
+ objects_valid = False
rest_surface = self._get_available_surface(acc)
form_surface = float(self.cleaned_data["surface"])
@@ -447,7 +456,7 @@ class NewDeductionModalForm(BaseModalForm):
format_german_float(rest_surface),
),
)
- return is_valid_surface and super_result
+ return is_valid_surface and objects_valid and super_result
def __create_deduction(self):
""" Creates the deduction
diff --git a/intervention/tables.py b/intervention/tables.py
index f535039d..8f312099 100644
--- a/intervention/tables.py
+++ b/intervention/tables.py
@@ -131,7 +131,7 @@ class InterventionTable(BaseTable, TableRenderMixin):
"""
parcels = value.get_underlying_parcels().values_list(
- "gmrkng",
+ "parcel_group__name",
flat=True
).distinct()
html = render_to_string(
@@ -177,9 +177,7 @@ class InterventionTable(BaseTable, TableRenderMixin):
"""
html = ""
- has_access = value.filter(
- id=self.user.id
- ).exists()
+ has_access = record.is_shared_with(self.user)
html += self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),
diff --git a/intervention/templates/intervention/detail/view.html b/intervention/templates/intervention/detail/view.html
index 5247a84c..41c59621 100644
--- a/intervention/templates/intervention/detail/view.html
+++ b/intervention/templates/intervention/detail/view.html
@@ -1,5 +1,5 @@
{% extends 'base.html' %}
-{% load i18n l10n static fontawesome_5 humanize %}
+{% load i18n l10n static fontawesome_5 %}
{% block head %}
{% comment %}
@@ -106,9 +106,15 @@
{% trans 'Last modified' %} |
- {{obj.created.timestamp|default_if_none:""|naturalday}}
-
- {{obj.created.user.username}}
+ {% if obj.modified %}
+ {{obj.modified.timestamp|default_if_none:""}}
+
+ {{obj.modified.user.username}}
+ {% else %}
+ {{obj.created.timestamp|default_if_none:""}}
+
+ {{obj.created.user.username}}
+ {% endif %}
|
diff --git a/intervention/templates/intervention/report/report.html b/intervention/templates/intervention/report/report.html
index 97981c3c..b435bfcb 100644
--- a/intervention/templates/intervention/report/report.html
+++ b/intervention/templates/intervention/report/report.html
@@ -102,14 +102,7 @@
{% include 'konova/includes/parcels.html' %}
-
-
{% trans 'Open in browser' %}
- {{ qrcode|safe }}
-
-
-
{% trans 'View in LANIS' %}
- {{ qrcode_lanis|safe }}
-
+ {% include 'konova/includes/report/qrcodes.html' %}
diff --git a/intervention/tests/test_workflow.py b/intervention/tests/test_workflow.py
index c5290503..66770977 100644
--- a/intervention/tests/test_workflow.py
+++ b/intervention/tests/test_workflow.py
@@ -89,6 +89,30 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
self.assertIn(self.superuser, obj.users.all())
self.assertEqual(1, obj.users.count())
+ def test_non_editable_after_recording(self):
+ """ Tests that the intervention can not be edited after being recorded
+
+ User must be redirected to another page
+
+ Returns:
+
+ """
+ self.assertIsNotNone(self.intervention)
+ self.assertFalse(self.intervention.is_recorded)
+ edit_url = reverse("intervention:edit", args=(self.intervention.id,))
+ response = self.client_user.get(edit_url)
+ has_redirect = response.status_code == 302
+ self.assertFalse(has_redirect)
+
+ self.intervention.set_recorded(self.user)
+ self.assertTrue(self.intervention.is_recorded)
+
+ edit_url = reverse("intervention:edit", args=(self.intervention.id,))
+ response = self.client_user.get(edit_url)
+ has_redirect = response.status_code == 302
+ self.assertTrue(has_redirect)
+ self.intervention.set_unrecorded(self.user)
+
def test_checkability(self):
""" Tests that the intervention can only be checked if all required data has been added
diff --git a/intervention/utils/egon_export.py b/intervention/utils/egon_export.py
index 39a4c318..1ef9f727 100644
--- a/intervention/utils/egon_export.py
+++ b/intervention/utils/egon_export.py
@@ -80,7 +80,7 @@ class EgonGmlBuilder:
Returns:
str
"""
- gmrkng_code = "000000"
+ 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))
@@ -124,13 +124,13 @@ class EgonGmlBuilder:
"oneo:ortsangabe": {
"oneo:Ortsangaben": {
"oneo:kreisSchluessel": {
- "xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/588/{parcel.district.krs}",
+ "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.district.gmnd}",
+ "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/{parcel.gmrkng}",
+ "xlink:href": f"http://register.naturschutz.rlp.de/repository/services/referenzliste/589/{None}",
},
"oneo:flurstuecksKennzeichen": self._gen_flurstuecksKennzeichen(parcel),
}
@@ -156,6 +156,10 @@ class EgonGmlBuilder:
def build_gml(self):
comp_type, comp_type_code = self._gen_kompensationsArt()
+ payment_date = self.intervention.payments.first().due_on
+ if payment_date is not None:
+ payment_date = payment_date.strftime(DEFAULT_DATE_FORMAT)
+
xml_dict = {
"wfs:FeatureCollection": {
"@xmlns:wfs": "http://www.opengis.net/wfs",
@@ -207,7 +211,7 @@ class EgonGmlBuilder:
},
"oneo:zulassung": {
"oneo:Zulassungstermin": {
- "oneo:bauBeginn": self.intervention.payments.first().due_on.strftime(DEFAULT_DATE_FORMAT),
+ "oneo:bauBeginn": payment_date,
"oneo:erlass": self.intervention.legal.registration_date.strftime(DEFAULT_DATE_FORMAT),
"oneo:rechtsKraft": self.intervention.legal.binding_date.strftime(DEFAULT_DATE_FORMAT),
}
diff --git a/intervention/views.py b/intervention/views.py
index 3004a79f..f882f214 100644
--- a/intervention/views.py
+++ b/intervention/views.py
@@ -18,7 +18,8 @@ from konova.utils.documents import remove_document, get_document
from konova.utils.generators import generate_qr_code
from konova.utils.message_templates import INTERVENTION_INVALID, FORM_INVALID, IDENTIFIER_REPLACED, \
CHECKED_RECORDED_RESET, DEDUCTION_REMOVED, DEDUCTION_ADDED, REVOCATION_ADDED, REVOCATION_REMOVED, \
- COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, DEDUCTION_EDITED, REVOCATION_EDITED, DOCUMENT_EDITED
+ COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, DEDUCTION_EDITED, REVOCATION_EDITED, DOCUMENT_EDITED, \
+ RECORDED_BLOCKS_EDIT
from konova.utils.user_checks import in_group
@@ -302,6 +303,13 @@ def edit_view(request: HttpRequest, id: str):
template = "intervention/form/view.html"
# Get object from db
intervention = get_object_or_404(Intervention, id=id)
+ if intervention.is_recorded:
+ messages.info(
+ request,
+ RECORDED_BLOCKS_EDIT
+ )
+ return redirect("intervention:detail", id=id)
+
# Create forms, initialize with values from db/from POST request
data_form = EditInterventionForm(request.POST or None, instance=intervention)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention)
@@ -693,19 +701,22 @@ def report_view(request:HttpRequest, id: str):
distinct_deductions = intervention.deductions.all().distinct(
"account"
)
- qrcode_img = generate_qr_code(
- request.build_absolute_uri(reverse("intervention:report", args=(id,))),
- 10
- )
- qrcode_img_lanis = generate_qr_code(
- intervention.get_LANIS_link(),
- 7
- )
+ qrcode_url = request.build_absolute_uri(reverse("intervention:report", args=(id,)))
+ qrcode_img = generate_qr_code(qrcode_url, 10)
+ qrcode_lanis_url = intervention.get_LANIS_link()
+ qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
+
context = {
"obj": intervention,
"deductions": distinct_deductions,
- "qrcode": qrcode_img,
- "qrcode_lanis": qrcode_img_lanis,
+ "qrcode": {
+ "img": qrcode_img,
+ "url": qrcode_url,
+ },
+ "qrcode_lanis": {
+ "img": qrcode_img_lanis,
+ "url": qrcode_lanis_url,
+ },
"geom_form": geom_form,
"parcels": parcels,
TAB_TITLE_IDENTIFIER: tab_title,
diff --git a/konova/admin.py b/konova/admin.py
index 81db8fe2..213120ea 100644
--- a/konova/admin.py
+++ b/konova/admin.py
@@ -7,7 +7,8 @@ Created on: 22.07.21
"""
from django.contrib import admin
-from konova.models import Geometry, Deadline, GeometryConflict, Parcel, District
+from konova.models import Geometry, Deadline, GeometryConflict, Parcel, District, Municipal, ParcelGroup
+from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
from user.models import UserAction
@@ -16,13 +17,28 @@ class GeometryAdmin(admin.ModelAdmin):
list_display = [
"id",
"created",
+ "st_area",
]
+ readonly_fields = [
+ "st_area",
+ "created",
+ "modified",
+ ]
+
+ def st_area(self, obj):
+ val = None
+ geom = obj.geom
+ if geom is not None:
+ geom.transform(ct=DEFAULT_SRID_RLP)
+ val = geom.area
+ return val
+ st_area.short_description = f"Area (srid={DEFAULT_SRID_RLP})"
class ParcelAdmin(admin.ModelAdmin):
list_display = [
"id",
- "gmrkng",
+ "parcel_group",
"flr",
"flrstck_nnr",
"flrstck_zhlr",
@@ -32,9 +48,27 @@ class ParcelAdmin(admin.ModelAdmin):
class DistrictAdmin(admin.ModelAdmin):
list_display = [
+ "name",
+ "key",
+ "id",
+ ]
+
+
+class MunicipalAdmin(admin.ModelAdmin):
+ list_display = [
+ "name",
+ "key",
+ "district",
+ "id",
+ ]
+
+
+class ParcelGroupAdmin(admin.ModelAdmin):
+ list_display = [
+ "name",
+ "key",
+ "municipal",
"id",
- "gmnd",
- "krs",
]
@@ -105,5 +139,7 @@ class BaseObjectAdmin(BaseResourceAdmin):
#admin.site.register(Geometry, GeometryAdmin)
#admin.site.register(Parcel, ParcelAdmin)
#admin.site.register(District, DistrictAdmin)
+#admin.site.register(Municipal, MunicipalAdmin)
+#admin.site.register(ParcelGroup, ParcelGroupAdmin)
#admin.site.register(GeometryConflict, GeometryConflictAdmin)
#admin.site.register(Deadline, DeadlineAdmin)
diff --git a/konova/autocompletes.py b/konova/autocompletes.py
index 9f60d54f..e6036f02 100644
--- a/konova/autocompletes.py
+++ b/konova/autocompletes.py
@@ -52,14 +52,16 @@ class InterventionAutocomplete(Select2QuerySetView):
"""
def get_queryset(self):
- if self.request.user.is_anonymous:
+ user = self.request.user
+ if user.is_anonymous:
return Intervention.objects.none()
qs = Intervention.objects.filter(
- deleted=None,
- users__in=[self.request.user],
+ Q(deleted=None) &
+ Q(users__in=[user]) |
+ Q(teams__in=user.teams.all())
).order_by(
"identifier"
- )
+ ).distinct()
if self.q:
qs = qs.filter(
Q(identifier__icontains=self.q) |
diff --git a/konova/filters/mixins.py b/konova/filters/mixins.py
index e6a841ea..beb44dac 100644
--- a/konova/filters/mixins.py
+++ b/konova/filters/mixins.py
@@ -145,26 +145,20 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
class Meta:
abstract = True
- def _filter_parcel_reference(self, queryset, name, value, filter_value) -> QuerySet:
- """ Filters the parcel entries by a given filter_value.
-
- filter_value may already include further filter annotations like 'xy__icontains'
+ def _filter_parcel_reference(self, queryset, filter_q) -> QuerySet:
+ """ Filters the parcel entries by a given filter_q
Args:
- queryset ():
- name ():
- value ():
- filter_value ():
+ queryset (QuerySet): The queryset
+ filter_q (Q): The Q-style filter expression
Returns:
"""
- _filter = {
- filter_value: value
- }
matching_parcels = Parcel.objects.filter(
- **_filter
+ filter_q
)
+
related_geoms = matching_parcels.values(
"geometries"
).distinct()
@@ -185,8 +179,9 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
"""
matching_districts = District.objects.filter(
- krs__icontains=value
- )
+ Q(name__icontains=value) |
+ Q(key__icontains=value)
+ ).distinct()
matching_parcels = Parcel.objects.filter(
district__in=matching_districts
)
@@ -209,7 +204,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
Returns:
"""
- queryset = self._filter_parcel_reference(queryset, name, value, "gmrkng__icontains")
+ queryset = self._filter_parcel_reference(
+ queryset,
+ Q(parcel_group__name__icontains=value) | Q(parcel_group__key__icontains=value),
+ )
return queryset
def filter_parcel(self, queryset, name, value) -> QuerySet:
@@ -224,7 +222,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
"""
value = value.replace("-", "")
- queryset = self._filter_parcel_reference(queryset, name, value, "flr")
+ queryset = self._filter_parcel_reference(
+ queryset,
+ Q(flr=value),
+ )
return queryset
def filter_parcel_counter(self, queryset, name, value) -> QuerySet:
@@ -239,7 +240,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
"""
value = value.replace("-", "")
- queryset = self._filter_parcel_reference(queryset, name, value, "flrstck_zhlr")
+ queryset = self._filter_parcel_reference(
+ queryset,
+ Q(flrstck_zhlr=value)
+ )
return queryset
def filter_parcel_number(self, queryset, name, value) -> QuerySet:
@@ -254,7 +258,10 @@ class GeoReferencedTableFilterMixin(django_filters.FilterSet):
"""
value = value.replace("-", "")
- queryset = self._filter_parcel_reference(queryset, name, value, "flrstck_nnr")
+ queryset = self._filter_parcel_reference(
+ queryset,
+ Q(flrstck_nnr=value),
+ )
return queryset
diff --git a/konova/forms.py b/konova/forms.py
index b85501ba..d64ab364 100644
--- a/konova/forms.py
+++ b/konova/forms.py
@@ -57,6 +57,8 @@ class BaseForm(forms.Form):
self.has_required_fields = True
break
+ self.check_for_recorded_instance()
+
@abstractmethod
def save(self):
# To be implemented in subclasses!
@@ -136,6 +138,38 @@ class BaseForm(forms.Form):
set_class = set_class.replace(cls, "")
self.fields[field].widget.attrs["class"] = set_class
+ def check_for_recorded_instance(self):
+ """ Checks if the instance is recorded and runs some special logic if yes
+
+ If the instance is recorded, the form shall not display any possibility to
+ edit any data. Instead, the users should get some information about why they can not edit anything.
+
+ There are situations where the form should be rendered regularly,
+ e.g deduction forms for (recorded) eco accounts.
+
+ Returns:
+
+ """
+ from intervention.forms.modalForms import NewDeductionModalForm, EditEcoAccountDeductionModalForm, \
+ RemoveEcoAccountDeductionModalForm
+ is_none = self.instance is None
+ is_other_data_type = not isinstance(self.instance, BaseObject)
+ is_deduction_form = isinstance(
+ self,
+ (
+ NewDeductionModalForm,
+ EditEcoAccountDeductionModalForm,
+ RemoveEcoAccountDeductionModalForm,
+ )
+ )
+
+ if is_none or is_other_data_type or is_deduction_form:
+ # Do nothing
+ return
+
+ if self.instance.is_recorded:
+ self.template = "form/recorded_no_edit.html"
+
class RemoveForm(BaseForm):
check = forms.BooleanField(
@@ -400,7 +434,6 @@ class NewDocumentModalForm(BaseModalForm):
super().__init__(*args, **kwargs)
self.form_title = _("Add new document")
self.form_caption = _("")
- self.template = "modal/modal_form.html"
self.form_attrs = {
"enctype": "multipart/form-data", # important for file upload
}
@@ -587,4 +620,12 @@ class RecordModalForm(BaseModalForm):
self.instance.set_unrecorded(self.user)
else:
self.instance.set_recorded(self.user)
- return self.instance
\ No newline at end of file
+ return self.instance
+
+ def check_for_recorded_instance(self):
+ """ Overwrite the check method for doing nothing on the RecordModalForm
+
+ Returns:
+
+ """
+ pass
diff --git a/konova/management/commands/sanitize_db.py b/konova/management/commands/sanitize_db.py
index b5be8349..b296b2a7 100644
--- a/konova/management/commands/sanitize_db.py
+++ b/konova/management/commands/sanitize_db.py
@@ -9,7 +9,7 @@ from compensation.models import CompensationState, Compensation, EcoAccount, Com
from ema.models import Ema
from intervention.models import Intervention
from konova.management.commands.setup import BaseKonovaCommand
-from konova.models import Deadline, Geometry, Parcel, District
+from konova.models import Deadline, Geometry, Parcel, District, Municipal, ParcelGroup
from user.models import UserActionLogEntry, UserAction
@@ -271,13 +271,26 @@ class Command(BaseKonovaCommand):
self._write_success("No unused states found.")
self._break_line()
+ def __sanitize_parcel_sub_type(self, cls):
+ unrelated_entries = cls.objects.filter(
+ parcels=None,
+ )
+ num_unrelated_entries = unrelated_entries.count()
+ cls_name = cls.__name__
+ if num_unrelated_entries > 0:
+ self._write_error(f"Found {num_unrelated_entries} unrelated {cls_name} entries. Delete now...")
+ unrelated_entries.delete()
+ self._write_success(f"Unrelated {cls_name} deleted.")
+ else:
+ self._write_success(f"No unrelated {cls_name} found.")
+
def sanitize_parcels_and_districts(self):
""" Removes unattached parcels and districts
Returns:
"""
- self._write_warning("=== Sanitize parcels and districts ===")
+ self._write_warning("=== Sanitize administrative spatial references ===")
unrelated_parcels = Parcel.objects.filter(
geometries=None,
)
@@ -289,16 +302,12 @@ class Command(BaseKonovaCommand):
else:
self._write_success("No unrelated parcels found.")
- unrelated_districts = District.objects.filter(
- parcels=None,
- )
- num_unrelated_districts = unrelated_districts.count()
- if num_unrelated_districts > 0:
- self._write_error(f"Found {num_unrelated_districts} unrelated district entries. Delete now...")
- unrelated_districts.delete()
- self._write_success("Unrelated districts deleted.")
- else:
- self._write_success("No unrelated districts found.")
-
- self._break_line()
+ sub_types = [
+ District,
+ Municipal,
+ ParcelGroup
+ ]
+ for sub_type in sub_types:
+ self.__sanitize_parcel_sub_type(sub_type)
+ self._break_line()
\ No newline at end of file
diff --git a/konova/management/commands/update_all_parcels.py b/konova/management/commands/update_all_parcels.py
index 9d96ebae..c9dd5158 100644
--- a/konova/management/commands/update_all_parcels.py
+++ b/konova/management/commands/update_all_parcels.py
@@ -5,6 +5,10 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.01.22
"""
+import datetime
+
+from django.contrib.gis.db.models.functions import Area
+
from konova.management.commands.setup import BaseKonovaCommand
from konova.models import Geometry, Parcel, District
@@ -23,12 +27,21 @@ class Command(BaseKonovaCommand):
num_parcels_before = Parcel.objects.count()
num_districts_before = District.objects.count()
self._write_warning("=== Update parcels and districts ===")
+ # Order geometries by size to process smaller once at first
geometries = Geometry.objects.all().exclude(
geom=None
+ ).annotate(area=Area("geom")).order_by(
+ 'area'
)
self._write_warning(f"Process parcels for {geometries.count()} geometry entries now ...")
+ i = 0
+ num_geoms = geometries.count()
for geometry in geometries:
+ self._write_warning(f"--- {datetime.datetime.now()} Process {geometry.id} now ...")
geometry.update_parcels()
+ self._write_warning(f"--- Processed {geometry.get_underlying_parcels().count()} underlying parcels")
+ i += 1
+ self._write_warning(f"--- {i}/{num_geoms} processed")
num_parcels_after = Parcel.objects.count()
num_districts_after = District.objects.count()
diff --git a/konova/migrations/0006_auto_20220411_0835.py b/konova/migrations/0006_auto_20220411_0835.py
new file mode 100644
index 00000000..bf74643c
--- /dev/null
+++ b/konova/migrations/0006_auto_20220411_0835.py
@@ -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'),
+ ),
+ ]
diff --git a/konova/migrations/0007_auto_20220411_0848.py b/konova/migrations/0007_auto_20220411_0848.py
new file mode 100644
index 00000000..fcc2b45b
--- /dev/null
+++ b/konova/migrations/0007_auto_20220411_0848.py
@@ -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),
+ ),
+ ]
diff --git a/konova/migrations/0008_auto_20220411_0914.py b/konova/migrations/0008_auto_20220411_0914.py
new file mode 100644
index 00000000..c56b6215
--- /dev/null
+++ b/konova/migrations/0008_auto_20220411_0914.py
@@ -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),
+ ),
+ ]
diff --git a/konova/migrations/0009_auto_20220411_1004.py b/konova/migrations/0009_auto_20220411_1004.py
new file mode 100644
index 00000000..d0679bf3
--- /dev/null
+++ b/konova/migrations/0009_auto_20220411_1004.py
@@ -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'),
+ ),
+ ]
diff --git a/konova/models/geometry.py b/konova/models/geometry.py
index bec89c39..a71a2afa 100644
--- a/konova/models/geometry.py
+++ b/konova/models/geometry.py
@@ -20,6 +20,9 @@ class Geometry(BaseResource):
from konova.settings import DEFAULT_SRID
geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID)
+ def __str__(self):
+ return str(self.id)
+
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.check_for_conflicts()
@@ -99,7 +102,7 @@ class Geometry(BaseResource):
Returns:
"""
- from konova.models import Parcel, District, ParcelIntersection
+ from konova.models import Parcel, District, ParcelIntersection, Municipal, ParcelGroup
parcel_fetcher = ParcelWFSFetcher(
geometry_id=self.id,
)
@@ -115,16 +118,28 @@ class Geometry(BaseResource):
# which needs to be deleted and just keep the numerical values
## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE!
flr_val = fetched_parcel["ave:flur"].replace("Flur ", "")
+ district = District.objects.get_or_create(
+ key=fetched_parcel["ave:kreisschl"],
+ name=fetched_parcel["ave:kreis"],
+ )[0]
+ municipal = Municipal.objects.get_or_create(
+ key=fetched_parcel["ave:gmdschl"],
+ name=fetched_parcel["ave:gemeinde"],
+ district=district,
+ )[0]
+ parcel_group = ParcelGroup.objects.get_or_create(
+ key=fetched_parcel["ave:gemaschl"],
+ name=fetched_parcel["ave:gemarkung"],
+ municipal=municipal,
+ )[0]
parcel_obj = Parcel.objects.get_or_create(
- gmrkng=fetched_parcel["ave:gemarkung"],
+ district=district,
+ municipal=municipal,
+ parcel_group=parcel_group,
flr=flr_val,
flrstck_nnr=fetched_parcel['ave:flstnrnen'],
flrstck_zhlr=fetched_parcel['ave:flstnrzae'],
)[0]
- district = District.objects.get_or_create(
- gmnd=fetched_parcel["ave:gemeinde"],
- krs=fetched_parcel["ave:kreis"],
- )[0]
parcel_obj.district = district
parcel_obj.updated_on = _now
parcel_obj.save()
@@ -155,9 +170,10 @@ class Geometry(BaseResource):
parcels = self.parcels.filter(
parcelintersection__calculated_on__isnull=False,
).prefetch_related(
- "district"
+ "district",
+ "municipal",
).order_by(
- "gmrkng",
+ "municipal__name",
)
return parcels
diff --git a/konova/models/object.py b/konova/models/object.py
index f224b68c..a1cff71b 100644
--- a/konova/models/object.py
+++ b/konova/models/object.py
@@ -289,6 +289,8 @@ class RecordableObjectMixin(models.Model):
from user.models import UserActionLogEntry
if self.recorded:
return None
+
+ self.unshare_with_default_users()
action = UserActionLogEntry.get_recorded_action(user)
self.recorded = action
self.save()
@@ -335,6 +337,15 @@ class RecordableObjectMixin(models.Model):
"""
raise NotImplementedError("Implement this in the subclass!")
+ @property
+ def is_recorded(self):
+ """ Getter for record status as property
+
+ Returns:
+
+ """
+ return self.recorded is not None
+
class CheckableObjectMixin(models.Model):
# Checks - Refers to "Genehmigen" but optional
@@ -608,6 +619,26 @@ class ShareableObjectMixin(models.Model):
"""
raise NotImplementedError("Must be implemented in subclasses!")
+ def unshare_with_default_users(self):
+ """ Removes all shared users from direct shared access which are only default group users
+
+ Returns:
+
+ """
+ from konova.utils.user_checks import is_default_group_only
+ users = self.shared_users
+ cleaned_users = []
+ default_users = []
+ for user in users:
+ if not is_default_group_only(user):
+ cleaned_users.append(user)
+ else:
+ default_users.append(user)
+ self.share_with_user_list(cleaned_users)
+
+ for user in default_users:
+ celery_send_mail_shared_access_removed.delay(self.identifier, self.title, user.id)
+
class GeoReferencedMixin(models.Model):
geometry = models.ForeignKey("konova.Geometry", null=True, blank=True, on_delete=models.SET_NULL)
diff --git a/konova/models/parcel.py b/konova/models/parcel.py
index 9c887f1a..f74b7af9 100644
--- a/konova/models/parcel.py
+++ b/konova/models/parcel.py
@@ -10,8 +10,64 @@ from django.db import models
from konova.models import UuidModel
+class AdministrativeSpatialReference(models.Model):
+ key = models.CharField(
+ max_length=255,
+ help_text="Represents Kreisschlüssel",
+ null=True,
+ blank=True
+ )
+ name = models.CharField(
+ max_length=1000,
+ help_text="Kreis",
+ null=True,
+ blank=True,
+ )
+
+ class Meta:
+ abstract = True
+
+ def __str__(self):
+ return f"{self.name} ({self.key})"
+
+ @property
+ def table_str(self):
+ return f"{self.name} ({self.key})"
+
+
+class District(UuidModel, AdministrativeSpatialReference):
+ """ The model District refers to "Kreis"
+
+ """
+ pass
+
+
+class Municipal(UuidModel, AdministrativeSpatialReference):
+ """ The model Municipal refers to "Gemeinde"
+
+ """
+ district = models.ForeignKey(
+ District,
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ )
+
+
+class ParcelGroup(UuidModel, AdministrativeSpatialReference):
+ """ The model ParcelGroup refers to "Gemarkung", which is defined as a loose group of parcels
+
+ """
+ municipal = models.ForeignKey(
+ Municipal,
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ )
+
+
class Parcel(UuidModel):
- """ The Parcel model holds administrative data on the covered properties.
+ """ The Parcel model holds administrative data on covered properties.
Due to the unique but relevant naming of the administrative data, we have to use these namings as field
names in german. Any try to translate them to English result in strange or insufficient translations.
@@ -24,59 +80,34 @@ class Parcel(UuidModel):
"""
geometries = models.ManyToManyField("konova.Geometry", blank=True, related_name="parcels", through='ParcelIntersection')
district = models.ForeignKey("konova.District", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels")
- gmrkng = models.CharField(
- max_length=1000,
+ municipal = models.ForeignKey("konova.Municipal", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels")
+ parcel_group = models.ForeignKey(
+ "konova.ParcelGroup",
+ on_delete=models.SET_NULL,
help_text="Gemarkung",
null=True,
blank=True,
+ related_name="parcels"
)
- flrstck_nnr = models.CharField(
- max_length=1000,
+ flr = models.IntegerField(
+ help_text="Flur",
+ null=True,
+ blank=True,
+ )
+ flrstck_nnr = models.IntegerField(
help_text="Flurstücksnenner",
null=True,
blank=True,
)
- flrstck_zhlr = models.CharField(
- max_length=1000,
+ flrstck_zhlr = models.IntegerField(
help_text="Flurstückszähler",
null=True,
blank=True,
)
- flr = models.CharField(
- max_length=1000,
- help_text="Flur",
- null=True,
- blank=True,
- )
updated_on = models.DateTimeField(auto_now_add=True)
def __str__(self):
- return f"{self.gmrkng} | {self.flr} | {self.flrstck_zhlr} | {self.flrstck_nnr}"
-
-
-class District(UuidModel):
- """ The model District holds more coarse information, such as Kreis, Verbandsgemeinde and Gemeinde.
-
- There might be the case that a geometry lies on a hundred Parcel entries but only on one District entry.
- Therefore a geometry can have a lot of relations to Parcel entries but only a few or only a single one to one
- District.
-
- """
- gmnd = models.CharField(
- max_length=1000,
- help_text="Gemeinde",
- null=True,
- blank=True,
- )
- krs = models.CharField(
- max_length=1000,
- help_text="Kreis",
- null=True,
- blank=True,
- )
-
- def __str__(self):
- return f"{self.gmnd} | {self.krs}"
+ return f"{self.parcel_group} | {self.flr} | {self.flrstck_zhlr} | {self.flrstck_nnr}"
class ParcelIntersection(UuidModel):
diff --git a/konova/sub_settings/django_settings.py b/konova/sub_settings/django_settings.py
index 7a8f9067..e5de97a0 100644
--- a/konova/sub_settings/django_settings.py
+++ b/konova/sub_settings/django_settings.py
@@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import os
from django.utils.translation import gettext_lazy as _
+from django.conf.locale.de import formats as de_formats
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = os.path.dirname(
@@ -162,9 +163,15 @@ LANGUAGES = [
USE_THOUSAND_SEPARATOR = True
+# Regular python relevant date/datetime formatting
DEFAULT_DATE_TIME_FORMAT = '%d.%m.%Y %H:%M:%S'
DEFAULT_DATE_FORMAT = '%d.%m.%Y'
+# Template relevant date/datetime formatting
+# See the Note on here: https://docs.djangoproject.com/en/3.2/ref/templates/builtins/#date
+de_formats.DATETIME_FORMAT = "d.m.Y, H:i"
+de_formats.DATE_FORMAT = "d.m.Y"
+
TIME_ZONE = 'Europe/Berlin'
USE_I18N = True
diff --git a/konova/sub_settings/table_settings.py b/konova/sub_settings/table_settings.py
index f32a0835..1699ad1b 100644
--- a/konova/sub_settings/table_settings.py
+++ b/konova/sub_settings/table_settings.py
@@ -19,6 +19,6 @@ PAGE_SIZE_OPTIONS_TUPLES = [
(50, 50),
(100, 100),
]
-PAGE_SIZE_DEFAULT = 5
+PAGE_SIZE_DEFAULT = 10
PAGE_SIZE_MAX = 100
PAGE_DEFAULT = 1
diff --git a/konova/templates/konova/includes/parcel_table.html b/konova/templates/konova/includes/parcel_table.html
index 68904894..76503572 100644
--- a/konova/templates/konova/includes/parcel_table.html
+++ b/konova/templates/konova/includes/parcel_table.html
@@ -1,15 +1,36 @@
-{% load i18n %}
+{% load i18n l10n %}