From 2bd94a76188fb064ca51227e956067be8c6960bc Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 15 Dec 2021 13:59:52 +0100 Subject: [PATCH 1/3] #50 Overlaying geometries * refactors geometry field into GeoReferencedMixin, holding more handy methods and used in all models, formerly holding the geometry field * refactors backend admin configuration, so modified, deleted and created are not editable in the backend which also skips loading of all possible choices * fixes typo in sanitize_db command * introduces GeometryConflict model, holding a link between two geometries, where one overlaps the other * adds first (WIP) messages into detail views of ema and intervention for test purposes --- compensation/admin.py | 3 +- compensation/models/compensation.py | 7 +- ema/models/ema.py | 11 ++- ema/views.py | 3 +- intervention/admin.py | 6 +- intervention/models/intervention.py | 17 +++-- intervention/views.py | 3 +- konova/admin.py | 19 ++++- konova/management/commands/sanitize_db.py | 2 +- konova/models/geometry.py | 86 ++++++++++++++++++++++- konova/models/object.py | 39 +++++++++- konova/utils/message_templates.py | 6 +- 12 files changed, 179 insertions(+), 23 deletions(-) diff --git a/compensation/admin.py b/compensation/admin.py index 4d148582..ced9bfe7 100644 --- a/compensation/admin.py +++ b/compensation/admin.py @@ -2,6 +2,7 @@ from django.contrib import admin from compensation.models import Compensation, CompensationAction, CompensationState, Payment, \ EcoAccountDeduction, EcoAccount +from konova.admin import BaseObjectAdmin class CompensationStateAdmin(admin.ModelAdmin): @@ -22,7 +23,7 @@ class CompensationActionAdmin(admin.ModelAdmin): ] -class CompensationAdmin(admin.ModelAdmin): +class CompensationAdmin(BaseObjectAdmin): list_display = [ "id", "identifier", diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py index ccf511a1..1759523b 100644 --- a/compensation/models/compensation.py +++ b/compensation/models/compensation.py @@ -16,12 +16,13 @@ from django.utils.translation import gettext_lazy as _ from compensation.managers import CompensationManager from compensation.models import CompensationState, CompensationAction from compensation.utils.quality import CompensationQualityChecker -from konova.models import BaseObject, AbstractDocument, Geometry, Deadline, generate_document_file_upload_path +from konova.models import BaseObject, AbstractDocument, Deadline, generate_document_file_upload_path, \ + GeoReferencedMixin from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE from user.models import UserActionLogEntry -class AbstractCompensation(BaseObject): +class AbstractCompensation(BaseObject, GeoReferencedMixin): """ Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation, EMA or EcoAccount. @@ -41,8 +42,6 @@ class AbstractCompensation(BaseObject): deadlines = models.ManyToManyField("konova.Deadline", blank=True, related_name="+") - geometry = models.ForeignKey(Geometry, null=True, blank=True, on_delete=models.SET_NULL) - class Meta: abstract = True diff --git a/ema/models/ema.py b/ema/models/ema.py index 8c55bd4b..e6d87f3f 100644 --- a/ema/models/ema.py +++ b/ema/models/ema.py @@ -7,15 +7,17 @@ Created on: 15.11.21 """ import shutil -from django.contrib.auth.models import User +from django.contrib import messages from django.db import models from django.db.models import QuerySet +from django.http import HttpRequest from compensation.models import AbstractCompensation from ema.managers import EmaManager from ema.utils.quality import EmaQualityChecker from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObjectMixin, ShareableObjectMixin from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE +from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin): @@ -91,6 +93,13 @@ class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin): ) return docs + def set_status_messages(self, request: HttpRequest): + if not self.is_shared_with(request.user): + messages.info(request, DATA_UNSHARED_EXPLANATION) + self._set_overlapped_by_message(request) + self._set_overlapping_message(request) + return request + class EmaDocument(AbstractDocument): """ diff --git a/ema/views.py b/ema/views.py index 02bf53e1..dc2fa49a 100644 --- a/ema/views.py +++ b/ema/views.py @@ -138,8 +138,7 @@ def detail_view(request: HttpRequest, id: str): sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0 diff_states = abs(sum_before_states - sum_after_states) - if not is_data_shared: - messages.info(request, DATA_UNSHARED_EXPLANATION) + ema.set_status_messages(request) context = { "obj": ema, diff --git a/intervention/admin.py b/intervention/admin.py index f65cb332..bae89830 100644 --- a/intervention/admin.py +++ b/intervention/admin.py @@ -1,10 +1,10 @@ from django.contrib import admin from intervention.models import Intervention, Responsibility, Legal, Revocation, InterventionDocument -from konova.admin import AbstractDocumentAdmin +from konova.admin import AbstractDocumentAdmin, BaseObjectAdmin -class InterventionAdmin(admin.ModelAdmin): +class InterventionAdmin(BaseObjectAdmin): list_display = [ "id", "identifier", @@ -13,9 +13,11 @@ class InterventionAdmin(admin.ModelAdmin): "deleted", ] + class InterventionDocumentAdmin(AbstractDocumentAdmin): pass + class ResponsibilityAdmin(admin.ModelAdmin): list_display = [ "id", diff --git a/intervention/models/intervention.py b/intervention/models/intervention.py index fd1a1c2a..ef7f6040 100644 --- a/intervention/models/intervention.py +++ b/intervention/models/intervention.py @@ -7,6 +7,7 @@ Created on: 15.11.21 """ import shutil +from django.contrib import messages from django.contrib.auth.models import User from django.db import models, transaction from django.db.models import QuerySet @@ -18,13 +19,15 @@ from intervention.models.legal import Legal from intervention.models.responsibility import Responsibility from intervention.models.revocation import RevocationDocument, Revocation from intervention.utils.quality import InterventionQualityChecker -from konova.models import generate_document_file_upload_path, AbstractDocument, Geometry, BaseObject, ShareableObjectMixin, \ - RecordableObjectMixin, CheckableObjectMixin +from konova.models import generate_document_file_upload_path, AbstractDocument, BaseObject, \ + ShareableObjectMixin, \ + RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin from konova.settings import LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT, DEFAULT_SRID_RLP +from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION from user.models import UserActionLogEntry -class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, CheckableObjectMixin): +class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin): """ Interventions are e.g. construction sites where nature used to be. """ @@ -42,7 +45,6 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec blank=True, help_text="Holds data on legal dates or law" ) - geometry = models.ForeignKey(Geometry, null=True, blank=True, on_delete=models.SET_NULL) objects = InterventionManager() @@ -263,6 +265,13 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec if self.checked: self.set_unchecked() + def set_status_messages(self, request: HttpRequest): + if not self.is_shared_with(request.user): + messages.info(request, DATA_UNSHARED_EXPLANATION) + request = self._set_overlapping_message(request) + request = self._set_overlapped_by_message(request) + return request + class InterventionDocument(AbstractDocument): """ diff --git a/intervention/views.py b/intervention/views.py index a3f05f7d..60d75249 100644 --- a/intervention/views.py +++ b/intervention/views.py @@ -255,8 +255,7 @@ def detail_view(request: HttpRequest, id: str): "LANIS_LINK": intervention.get_LANIS_link() } - if not is_data_shared: - messages.info(request, DATA_UNSHARED_EXPLANATION) + request = intervention.set_status_messages(request) context = BaseContext(request, context).context return render(request, template, context) diff --git a/konova/admin.py b/konova/admin.py index c57117af..e62a8cff 100644 --- a/konova/admin.py +++ b/konova/admin.py @@ -7,7 +7,7 @@ Created on: 22.07.21 """ from django.contrib import admin -from konova.models import Geometry, Deadline +from konova.models import Geometry, Deadline, GeometryConflict class GeometryAdmin(admin.ModelAdmin): @@ -17,6 +17,14 @@ class GeometryAdmin(admin.ModelAdmin): ] +class GeometryConflictAdmin(admin.ModelAdmin): + list_display = [ + "conflicting_geometry", + "existing_geometry", + "detected_on", + ] + + class AbstractDocumentAdmin(admin.ModelAdmin): list_display = [ "id", @@ -35,5 +43,14 @@ class DeadlineAdmin(admin.ModelAdmin): ] +class BaseObjectAdmin(admin.ModelAdmin): + readonly_fields = [ + "modified", + "deleted", + "created", + ] + + admin.site.register(Geometry, GeometryAdmin) +admin.site.register(GeometryConflict, GeometryConflictAdmin) admin.site.register(Deadline, DeadlineAdmin) diff --git a/konova/management/commands/sanitize_db.py b/konova/management/commands/sanitize_db.py index de3c249d..c5526517 100644 --- a/konova/management/commands/sanitize_db.py +++ b/konova/management/commands/sanitize_db.py @@ -208,7 +208,7 @@ class Command(BaseKonovaCommand): if num_entries > 0: self._write_error(f"Found {num_entries} geometries not attached to anything. Delete now...") unattached_geometries.delete() - self._write_success("Deadlines deleted.") + self._write_success("Geometries deleted.") else: self._write_success("No unattached geometries found.") self._break_line() diff --git a/konova/models/geometry.py b/konova/models/geometry.py index 736e1b3b..db36c03b 100644 --- a/konova/models/geometry.py +++ b/konova/models/geometry.py @@ -6,13 +6,93 @@ Created on: 15.11.21 """ from django.contrib.gis.db.models import MultiPolygonField +from django.db import models -from konova.models import BaseResource +from konova.models import BaseResource, UuidModel class Geometry(BaseResource): """ - Outsourced geometry model so multiple versions of the same object can refer to the same geometry if it is not changed + Geometry model """ from konova.settings import DEFAULT_SRID - geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID) \ No newline at end of file + geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID) + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + self.check_for_conflicts() + + def check_for_conflicts(self): + """ Checks for geometry overlaps + + Creates a new GeometryConflict entry for each overlap to another geometry, which has already been there before + + Returns: + + """ + # If no geometry is given or important data is missing, we can not perform any checks + if self.geom is None or (self.created is None and self.modified is None): + return None + + check_timestamp_obj = self.modified or self.created + ts = check_timestamp_obj.timestamp + overlapping_geoms = Geometry.objects.filter( + modified__timestamp__lte=ts, + geom__overlaps=self.geom, + ) + + # Drop known conflicts for this object to replace with new ones + self.conflicts_geometries.all().delete() + for match in overlapping_geoms: + GeometryConflict.objects.get_or_create(conflicting_geometry=self, existing_geometry=match) + + # Rerun the conflict check for all conflicts where this object is not the cause but the one that already existed. + # It may be possible that this object has been edited, so the conflicts would be resolved in the newer entries. + existing_conflicts = self.conflicted_by_geometries.all() + for conflict in existing_conflicts: + conflicting_geom = conflict.conflicting_geometry + conflicting_geom.check_for_conflicts() + + def get_data_objects(self): + """ Getter for all objects which are related to this geometry + + Returns: + objs (list): The list of objects + """ + objs = [] + sets = [ + self.intervention_set, + self.compensation_set, + self.ema_set, + self.ecoaccount_set, + ] + for _set in sets: + set_objs = _set.filter( + deleted=None + ) + objs += set_objs + return objs + + +class GeometryConflict(UuidModel): + """ + Geometry conflicts model + + If a new/edited geometry overlays an existing geometry, there will be a new GeometryConflict on the db + """ + conflicting_geometry = models.ForeignKey( + Geometry, + on_delete=models.CASCADE, + help_text="The geometry which came second", + related_name="conflicts_geometries" + ) + existing_geometry = models.ForeignKey( + Geometry, + on_delete=models.CASCADE, + help_text="The geometry which came first", + related_name="conflicted_by_geometries" + ) + detected_on = models.DateTimeField(auto_now_add=True, null=True) + + def __str__(self): + return f"{self.conflicting_geometry.id} conflicts with {self.existing_geometry.id}" diff --git a/konova/models/object.py b/konova/models/object.py index 5b8f9b72..b1e78f8d 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -7,6 +7,7 @@ Created on: 15.11.21 """ import uuid +from abc import abstractmethod from django.contrib import messages from django.contrib.auth.models import User @@ -20,7 +21,8 @@ from ema.settings import EMA_ACCOUNT_IDENTIFIER_LENGTH, EMA_ACCOUNT_IDENTIFIER_T from intervention.settings import INTERVENTION_IDENTIFIER_LENGTH, INTERVENTION_IDENTIFIER_TEMPLATE from konova.utils import generators from konova.utils.generators import generate_random_string -from konova.utils.message_templates import CHECKED_RECORDED_RESET +from konova.utils.message_templates import CHECKED_RECORDED_RESET, GEOMETRY_OVERLAPS_WITH_TEMPLATE, \ + GEOMETRY_OVERLAPPED_BY_TEMPLATE from user.models import UserActionLogEntry, UserAction @@ -94,6 +96,10 @@ class BaseObject(BaseResource): class Meta: abstract = True + @abstractmethod + def set_status_messages(self, request: HttpRequest): + raise NotImplementedError + def mark_as_deleted(self, user: User): """ Mark an entry as deleted @@ -407,3 +413,34 @@ class ShareableObjectMixin(models.Model): id__in=accessing_users ) self.share_with_list(users) + + +class GeoReferencedMixin(models.Model): + geometry = models.ForeignKey("konova.Geometry", null=True, blank=True, on_delete=models.SET_NULL) + + class Meta: + abstract = True + + def _set_overlapping_message(self, request: HttpRequest): + geom_conflicts = self.geometry.conflicts_geometries.all() + if geom_conflicts: + data_objs = [] + for conflict in geom_conflicts: + data_objs += conflict.existing_geometry.get_data_objects() + data_identifiers = [x.identifier for x in data_objs] + data_identifiers = ", ".join(data_identifiers) + message_str = GEOMETRY_OVERLAPS_WITH_TEMPLATE.format(data_identifiers) + messages.info(request, message_str) + return request + + def _set_overlapped_by_message(self, request: HttpRequest): + geom_conflicts = self.geometry.conflicted_by_geometries.all() + if geom_conflicts: + data_objs = [] + for conflict in geom_conflicts: + data_objs += conflict.conflicting_geometry.get_data_objects() + data_identifiers = [x.identifier for x in data_objs] + data_identifiers = ", ".join(data_identifiers) + message_str = GEOMETRY_OVERLAPPED_BY_TEMPLATE.format(data_identifiers) + messages.info(request, message_str) + return request \ No newline at end of file diff --git a/konova/utils/message_templates.py b/konova/utils/message_templates.py index 5b26c460..260435ff 100644 --- a/konova/utils/message_templates.py +++ b/konova/utils/message_templates.py @@ -25,4 +25,8 @@ CANCEL_ACC_RECORDED_OR_DEDUCTED = _("Action canceled. Eco account is recorded or EDITED_GENERAL_DATA = _("Edited general data") ADDED_COMPENSATION_STATE = _("Added compensation state") ADDED_DEADLINE = _("Added deadline") -ADDED_COMPENSATION_ACTION = _("Added compensation action") \ No newline at end of file +ADDED_COMPENSATION_ACTION = _("Added compensation action") + +# Geometry conflicts +GEOMETRY_OVERLAPS_WITH_TEMPLATE = _("Geometry overlaps {}") +GEOMETRY_OVERLAPPED_BY_TEMPLATE = _("Geometry overlapped by {}") From 286ed609da0d2f55157ad009471619aa72cb32aa Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 15 Dec 2021 15:10:35 +0100 Subject: [PATCH 2/3] #50 Overlaying geometries KOM + OEK * removes unused messages * adds geometry conflict message rendering for KOM and OEK * removes unused methods in GeoReferencedMixin * generalizes geometrical lookup for conflicts from overlaps to intersects --- compensation/models/compensation.py | 18 +++++++++++++ compensation/views/compensation.py | 3 +-- compensation/views/eco_account.py | 3 +-- ema/models/ema.py | 13 ++++++++-- intervention/models/intervention.py | 13 ++++++++-- konova/forms.py | 4 ++- konova/models/geometry.py | 10 +++++--- konova/models/object.py | 40 ++++++++++++----------------- konova/utils/message_templates.py | 3 +-- 9 files changed, 70 insertions(+), 37 deletions(-) diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py index 1759523b..703f7cd4 100644 --- a/compensation/models/compensation.py +++ b/compensation/models/compensation.py @@ -7,6 +7,7 @@ Created on: 16.11.21 """ import shutil +from django.contrib import messages from django.contrib.auth.models import User from django.db import models, transaction from django.db.models import QuerySet, Sum @@ -19,6 +20,7 @@ from compensation.utils.quality import CompensationQualityChecker from konova.models import BaseObject, AbstractDocument, Deadline, generate_document_file_upload_path, \ GeoReferencedMixin from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE +from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION from user.models import UserActionLogEntry @@ -155,6 +157,22 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin): checker.run_check() return checker + def set_status_messages(self, request: HttpRequest): + """ Setter for different information that need to be rendered + + Adds messages to the given HttpRequest + + Args: + request (HttpRequest): The incoming request + + Returns: + request (HttpRequest): The modified request + """ + if not self.is_shared_with(request.user): + messages.info(request, DATA_UNSHARED_EXPLANATION) + request = self._set_geometry_conflict_message(request) + return request + class CEFMixin(models.Model): """ Provides CEF flag as Mixin diff --git a/compensation/views/compensation.py b/compensation/views/compensation.py index 678504e3..b5efc984 100644 --- a/compensation/views/compensation.py +++ b/compensation/views/compensation.py @@ -184,8 +184,7 @@ def detail_view(request: HttpRequest, id: str): sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0 diff_states = abs(sum_before_states - sum_after_states) - if not is_data_shared: - messages.info(request, DATA_UNSHARED_EXPLANATION) + request = comp.set_status_messages(request) context = { "obj": comp, diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py index 7b7d1eae..7b4c27e2 100644 --- a/compensation/views/eco_account.py +++ b/compensation/views/eco_account.py @@ -202,8 +202,7 @@ def detail_view(request: HttpRequest, id: str): ) actions = acc.actions.all() - if not is_data_shared: - messages.info(request, DATA_UNSHARED_EXPLANATION) + request = acc.set_status_messages(request) context = { "obj": acc, diff --git a/ema/models/ema.py b/ema/models/ema.py index e6d87f3f..8bc4ac65 100644 --- a/ema/models/ema.py +++ b/ema/models/ema.py @@ -94,10 +94,19 @@ class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin): return docs def set_status_messages(self, request: HttpRequest): + """ Setter for different information that need to be rendered + + Adds messages to the given HttpRequest + + Args: + request (HttpRequest): The incoming request + + Returns: + request (HttpRequest): The modified request + """ if not self.is_shared_with(request.user): messages.info(request, DATA_UNSHARED_EXPLANATION) - self._set_overlapped_by_message(request) - self._set_overlapping_message(request) + self._set_geometry_conflict_message(request) return request diff --git a/intervention/models/intervention.py b/intervention/models/intervention.py index ef7f6040..a31eb9a3 100644 --- a/intervention/models/intervention.py +++ b/intervention/models/intervention.py @@ -266,10 +266,19 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec self.set_unchecked() def set_status_messages(self, request: HttpRequest): + """ Setter for different information that need to be rendered + + Adds messages to the given HttpRequest + + Args: + request (HttpRequest): The incoming request + + Returns: + request (HttpRequest): The modified request + """ if not self.is_shared_with(request.user): messages.info(request, DATA_UNSHARED_EXPLANATION) - request = self._set_overlapping_message(request) - request = self._set_overlapped_by_message(request) + request = self._set_geometry_conflict_message(request) return request diff --git a/konova/forms.py b/konova/forms.py index 359b78bc..43c83498 100644 --- a/konova/forms.py +++ b/konova/forms.py @@ -282,11 +282,13 @@ class SimpleGeomForm(BaseForm): """ try: + if self.instance is None or self.instance.geometry is None: + raise LookupError geometry = self.instance.geometry geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID)) geometry.modified = action geometry.save() - except AttributeError: + except LookupError: # No geometry or linked instance holding a geometry exist --> create a new one! geometry = Geometry.objects.create( geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID)), diff --git a/konova/models/geometry.py b/konova/models/geometry.py index db36c03b..7dddcf0e 100644 --- a/konova/models/geometry.py +++ b/konova/models/geometry.py @@ -7,6 +7,7 @@ Created on: 15.11.21 """ from django.contrib.gis.db.models import MultiPolygonField from django.db import models +from django.db.models import Q from konova.models import BaseResource, UuidModel @@ -37,9 +38,12 @@ class Geometry(BaseResource): check_timestamp_obj = self.modified or self.created ts = check_timestamp_obj.timestamp overlapping_geoms = Geometry.objects.filter( - modified__timestamp__lte=ts, - geom__overlaps=self.geom, - ) + Q(modified__timestamp__lte=ts) | + Q(created__timestamp__lte=ts), + geom__intersects=self.geom, + ).exclude( + id=self.id + ).distinct() # Drop known conflicts for this object to replace with new ones self.conflicts_geometries.all().delete() diff --git a/konova/models/object.py b/konova/models/object.py index b1e78f8d..764e7079 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -21,8 +21,7 @@ from ema.settings import EMA_ACCOUNT_IDENTIFIER_LENGTH, EMA_ACCOUNT_IDENTIFIER_T from intervention.settings import INTERVENTION_IDENTIFIER_LENGTH, INTERVENTION_IDENTIFIER_TEMPLATE from konova.utils import generators from konova.utils.generators import generate_random_string -from konova.utils.message_templates import CHECKED_RECORDED_RESET, GEOMETRY_OVERLAPS_WITH_TEMPLATE, \ - GEOMETRY_OVERLAPPED_BY_TEMPLATE +from konova.utils.message_templates import CHECKED_RECORDED_RESET, GEOMETRY_CONFLICT_WITH_TEMPLATE from user.models import UserActionLogEntry, UserAction @@ -421,26 +420,21 @@ class GeoReferencedMixin(models.Model): class Meta: abstract = True - def _set_overlapping_message(self, request: HttpRequest): - geom_conflicts = self.geometry.conflicts_geometries.all() - if geom_conflicts: - data_objs = [] - for conflict in geom_conflicts: - data_objs += conflict.existing_geometry.get_data_objects() - data_identifiers = [x.identifier for x in data_objs] - data_identifiers = ", ".join(data_identifiers) - message_str = GEOMETRY_OVERLAPS_WITH_TEMPLATE.format(data_identifiers) + def _set_geometry_conflict_message(self, request: HttpRequest): + instance_objs = [] + add_message = False + conflicts = self.geometry.conflicts_geometries.all() + for conflict in conflicts: + instance_objs += conflict.existing_geometry.get_data_objects() + add_message = True + conflicts = self.geometry.conflicted_by_geometries.all() + for conflict in conflicts: + instance_objs += conflict.conflicting_geometry.get_data_objects() + add_message = True + + if add_message: + instance_identifiers = [x.identifier for x in instance_objs] + instance_identifiers = ", ".join(instance_identifiers) + message_str = GEOMETRY_CONFLICT_WITH_TEMPLATE.format(instance_identifiers) messages.info(request, message_str) return request - - def _set_overlapped_by_message(self, request: HttpRequest): - geom_conflicts = self.geometry.conflicted_by_geometries.all() - if geom_conflicts: - data_objs = [] - for conflict in geom_conflicts: - data_objs += conflict.conflicting_geometry.get_data_objects() - data_identifiers = [x.identifier for x in data_objs] - data_identifiers = ", ".join(data_identifiers) - message_str = GEOMETRY_OVERLAPPED_BY_TEMPLATE.format(data_identifiers) - messages.info(request, message_str) - return request \ No newline at end of file diff --git a/konova/utils/message_templates.py b/konova/utils/message_templates.py index 260435ff..29f8197c 100644 --- a/konova/utils/message_templates.py +++ b/konova/utils/message_templates.py @@ -28,5 +28,4 @@ ADDED_DEADLINE = _("Added deadline") ADDED_COMPENSATION_ACTION = _("Added compensation action") # Geometry conflicts -GEOMETRY_OVERLAPS_WITH_TEMPLATE = _("Geometry overlaps {}") -GEOMETRY_OVERLAPPED_BY_TEMPLATE = _("Geometry overlapped by {}") +GEOMETRY_CONFLICT_WITH_TEMPLATE = _("Geometry conflict detected with {}") From 5df84bb7a148c8e25a3c4decfdb51d37cc04a9f1 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 16 Dec 2021 09:58:59 +0100 Subject: [PATCH 3/3] #50 Overlaying geometries Tests * adds test for geometry conflicts * refactors rechecking of existing conflicts to avoid recursion in certain cases * adds/updates translations --- konova/admin.py | 2 +- konova/models/geometry.py | 45 +++++++---- konova/models/object.py | 2 +- konova/tests/test_geometries.py | 49 ++++++++++++ locale/de/LC_MESSAGES/django.mo | Bin 28116 -> 28510 bytes locale/de/LC_MESSAGES/django.po | 132 +++++++++++++++++--------------- 6 files changed, 149 insertions(+), 81 deletions(-) create mode 100644 konova/tests/test_geometries.py diff --git a/konova/admin.py b/konova/admin.py index e62a8cff..213e0412 100644 --- a/konova/admin.py +++ b/konova/admin.py @@ -20,7 +20,7 @@ class GeometryAdmin(admin.ModelAdmin): class GeometryConflictAdmin(admin.ModelAdmin): list_display = [ "conflicting_geometry", - "existing_geometry", + "affected_geometry", "detected_on", ] diff --git a/konova/models/geometry.py b/konova/models/geometry.py index 7dddcf0e..8c155732 100644 --- a/konova/models/geometry.py +++ b/konova/models/geometry.py @@ -24,7 +24,7 @@ class Geometry(BaseResource): self.check_for_conflicts() def check_for_conflicts(self): - """ Checks for geometry overlaps + """ Checks for new geometry overlaps Creates a new GeometryConflict entry for each overlap to another geometry, which has already been there before @@ -35,27 +35,40 @@ class Geometry(BaseResource): if self.geom is None or (self.created is None and self.modified is None): return None - check_timestamp_obj = self.modified or self.created - ts = check_timestamp_obj.timestamp + self.recheck_existing_conflicts() + overlapping_geoms = Geometry.objects.filter( - Q(modified__timestamp__lte=ts) | - Q(created__timestamp__lte=ts), geom__intersects=self.geom, ).exclude( id=self.id ).distinct() - # Drop known conflicts for this object to replace with new ones - self.conflicts_geometries.all().delete() for match in overlapping_geoms: - GeometryConflict.objects.get_or_create(conflicting_geometry=self, existing_geometry=match) + GeometryConflict.objects.get_or_create(conflicting_geometry=self, affected_geometry=match) + + def recheck_existing_conflicts(self): + """ Rechecks GeometryConflict entries + + If a conflict seems to be resolved due to no longer intersection between the two geometries, the entry + will be deleted. + + Returns: + + """ + all_conflicts_as_conflicting = self.conflicts_geometries.all() + still_conflicting_conflicts = all_conflicts_as_conflicting.filter( + affected_geometry__geom__intersects=self.geom + ) + resolved_conflicts = all_conflicts_as_conflicting.exclude(id__in=still_conflicting_conflicts) + resolved_conflicts.delete() + + all_conflicted_by_conflicts = self.conflicted_by_geometries.all() + still_conflicting_conflicts = all_conflicted_by_conflicts.filter( + conflicting_geometry__geom__intersects=self.geom + ) + resolved_conflicts = all_conflicted_by_conflicts.exclude(id__in=still_conflicting_conflicts) + resolved_conflicts.delete() - # Rerun the conflict check for all conflicts where this object is not the cause but the one that already existed. - # It may be possible that this object has been edited, so the conflicts would be resolved in the newer entries. - existing_conflicts = self.conflicted_by_geometries.all() - for conflict in existing_conflicts: - conflicting_geom = conflict.conflicting_geometry - conflicting_geom.check_for_conflicts() def get_data_objects(self): """ Getter for all objects which are related to this geometry @@ -90,7 +103,7 @@ class GeometryConflict(UuidModel): help_text="The geometry which came second", related_name="conflicts_geometries" ) - existing_geometry = models.ForeignKey( + affected_geometry = models.ForeignKey( Geometry, on_delete=models.CASCADE, help_text="The geometry which came first", @@ -99,4 +112,4 @@ class GeometryConflict(UuidModel): detected_on = models.DateTimeField(auto_now_add=True, null=True) def __str__(self): - return f"{self.conflicting_geometry.id} conflicts with {self.existing_geometry.id}" + return f"{self.conflicting_geometry.id} conflicts with {self.affected_geometry.id}" diff --git a/konova/models/object.py b/konova/models/object.py index 764e7079..ded39ad1 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -425,7 +425,7 @@ class GeoReferencedMixin(models.Model): add_message = False conflicts = self.geometry.conflicts_geometries.all() for conflict in conflicts: - instance_objs += conflict.existing_geometry.get_data_objects() + instance_objs += conflict.affected_geometry.get_data_objects() add_message = True conflicts = self.geometry.conflicted_by_geometries.all() for conflict in conflicts: diff --git a/konova/tests/test_geometries.py b/konova/tests/test_geometries.py new file mode 100644 index 00000000..a9a840f5 --- /dev/null +++ b/konova/tests/test_geometries.py @@ -0,0 +1,49 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 15.12.21 + +""" +from django.contrib.gis.db.models.functions import Translate + +from konova.models import Geometry, GeometryConflict +from konova.tests.test_views import BaseTestCase +from user.models import UserActionLogEntry + + +class GeometryTestCase(BaseTestCase): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + geom = cls.create_dummy_geometry() + action = UserActionLogEntry.get_created_action(cls.superuser) + cls.geom_1 = Geometry.objects.create( + geom=geom, + created=action, + ) + cls.geom_2 = Geometry.objects.create( + geom=geom, + created=action, + ) + + def test_geometry_conflict(self): + """ Tests whether a geometry conflict will be present in case of identical/overlaying geometries and + if the conflict will be resolved if one geometry is edited. + + Returns: + + """ + self.geom_1.check_for_conflicts() + conflict = GeometryConflict.objects.all().first() + self.assertEqual(conflict.conflicting_geometry, self.geom_2) + self.assertEqual(conflict.affected_geometry, self.geom_1) + + # Move geom_2 somewhere else, expect the conflict to be resolved + Geometry.objects.filter(id=self.geom_2.id).update( + geom=Translate('geom', 100000, 100000) + ) + self.geom_2.refresh_from_db() + self.geom_1.check_for_conflicts() + num_conflict = GeometryConflict.objects.all().count() + self.assertEqual(0, num_conflict) diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 94f1f4261103119e00e1404a6d82c66f36b65d81..bd3e82d8ebcb6ae0e59f48644b44c20fb80aa7a9 100644 GIT binary patch delta 9049 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 KvxLl7WBw06Y9a#w delta 8678 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\n" "Language-Team: LANGUAGE \n" @@ -329,7 +329,7 @@ msgstr "Automatisch generiert" #: intervention/templates/intervention/detail/includes/documents.html:28 #: intervention/templates/intervention/detail/view.html:31 #: intervention/templates/intervention/report/report.html:12 -#: konova/forms.py:332 +#: konova/forms.py:334 msgid "Title" msgstr "Bezeichnung" @@ -356,7 +356,7 @@ msgstr "Kompensation XY; Flur ABC" #: intervention/templates/intervention/detail/includes/documents.html:31 #: intervention/templates/intervention/detail/includes/payments.html:34 #: intervention/templates/intervention/detail/includes/revocation.html:38 -#: konova/forms.py:367 konova/templates/konova/comment_card.html:16 +#: konova/forms.py:369 konova/templates/konova/comment_card.html:16 msgid "Comment" msgstr "Kommentar" @@ -472,7 +472,7 @@ msgstr "Zahlung wird an diesem Datum erwartet" #: compensation/forms/modalForms.py:62 compensation/forms/modalForms.py:239 #: compensation/forms/modalForms.py:317 intervention/forms/modalForms.py:152 -#: konova/forms.py:369 +#: konova/forms.py:371 msgid "Additional comment, maximum {} letters" msgstr "Zusätzlicher Kommentar, maximal {} Zeichen" @@ -614,7 +614,7 @@ msgstr "" msgid "Pieces" msgstr "Stück" -#: compensation/models/compensation.py:62 konova/utils/message_templates.py:27 +#: compensation/models/compensation.py:63 konova/utils/message_templates.py:27 msgid "Added deadline" msgstr "Frist/Termin hinzugefügt" @@ -793,7 +793,7 @@ msgstr "Dokumente" #: compensation/templates/compensation/detail/eco_account/includes/documents.html:14 #: ema/templates/ema/detail/includes/documents.html:14 #: intervention/templates/intervention/detail/includes/documents.html:14 -#: konova/forms.py:385 +#: konova/forms.py:387 msgid "Add new document" msgstr "Neues Dokument hinzufügen" @@ -1056,42 +1056,42 @@ msgstr "Kompensation {} hinzugefügt" msgid "Compensation {} edited" msgstr "Kompensation {} bearbeitet" -#: compensation/views/compensation.py:229 compensation/views/eco_account.py:308 -#: ema/views.py:182 intervention/views.py:475 +#: compensation/views/compensation.py:228 compensation/views/eco_account.py:307 +#: ema/views.py:181 intervention/views.py:474 msgid "Log" msgstr "Log" -#: compensation/views/compensation.py:252 +#: compensation/views/compensation.py:251 msgid "Compensation removed" msgstr "Kompensation entfernt" -#: compensation/views/compensation.py:273 compensation/views/eco_account.py:460 -#: ema/views.py:349 intervention/views.py:129 +#: compensation/views/compensation.py:272 compensation/views/eco_account.py:459 +#: ema/views.py:348 intervention/views.py:129 msgid "Document added" msgstr "Dokument hinzugefügt" -#: compensation/views/compensation.py:342 compensation/views/eco_account.py:354 -#: ema/views.py:287 +#: compensation/views/compensation.py:341 compensation/views/eco_account.py:353 +#: ema/views.py:286 msgid "State added" msgstr "Zustand hinzugefügt" -#: compensation/views/compensation.py:363 compensation/views/eco_account.py:375 -#: ema/views.py:308 +#: compensation/views/compensation.py:362 compensation/views/eco_account.py:374 +#: ema/views.py:307 msgid "Action added" msgstr "Maßnahme hinzugefügt" -#: compensation/views/compensation.py:384 compensation/views/eco_account.py:440 -#: ema/views.py:329 +#: compensation/views/compensation.py:383 compensation/views/eco_account.py:439 +#: ema/views.py:328 msgid "Deadline added" msgstr "Frist/Termin hinzugefügt" -#: compensation/views/compensation.py:406 compensation/views/eco_account.py:397 -#: ema/views.py:419 +#: compensation/views/compensation.py:405 compensation/views/eco_account.py:396 +#: ema/views.py:418 msgid "State removed" msgstr "Zustand gelöscht" -#: compensation/views/compensation.py:428 compensation/views/eco_account.py:419 -#: ema/views.py:441 +#: compensation/views/compensation.py:427 compensation/views/eco_account.py:418 +#: ema/views.py:440 msgid "Action removed" msgstr "Maßnahme entfernt" @@ -1103,45 +1103,45 @@ msgstr "Ökokonto {} hinzugefügt" msgid "Eco-Account {} edited" msgstr "Ökokonto {} bearbeitet" -#: compensation/views/eco_account.py:256 +#: compensation/views/eco_account.py:255 msgid "Eco-account removed" msgstr "Ökokonto entfernt" -#: compensation/views/eco_account.py:284 +#: compensation/views/eco_account.py:283 msgid "Deduction removed" msgstr "Abbuchung entfernt" -#: compensation/views/eco_account.py:329 ema/views.py:262 -#: intervention/views.py:517 +#: compensation/views/eco_account.py:328 ema/views.py:261 +#: intervention/views.py:516 msgid "{} unrecorded" msgstr "{} entzeichnet" -#: compensation/views/eco_account.py:329 ema/views.py:262 -#: intervention/views.py:517 +#: compensation/views/eco_account.py:328 ema/views.py:261 +#: intervention/views.py:516 msgid "{} recorded" msgstr "{} verzeichnet" -#: compensation/views/eco_account.py:530 intervention/views.py:498 +#: compensation/views/eco_account.py:529 intervention/views.py:497 msgid "Deduction added" msgstr "Abbuchung hinzugefügt" -#: compensation/views/eco_account.py:613 ema/views.py:517 -#: intervention/views.py:373 +#: compensation/views/eco_account.py:612 ema/views.py:516 +#: intervention/views.py:372 msgid "{} has already been shared with you" msgstr "{} wurde bereits für Sie freigegeben" -#: compensation/views/eco_account.py:618 ema/views.py:522 -#: intervention/views.py:378 +#: compensation/views/eco_account.py:617 ema/views.py:521 +#: intervention/views.py:377 msgid "{} has been shared with you" msgstr "{} ist nun für Sie freigegeben" -#: compensation/views/eco_account.py:625 ema/views.py:529 -#: intervention/views.py:385 +#: compensation/views/eco_account.py:624 ema/views.py:528 +#: intervention/views.py:384 msgid "Share link invalid" msgstr "Freigabelink ungültig" -#: compensation/views/eco_account.py:648 ema/views.py:552 -#: intervention/views.py:408 +#: compensation/views/eco_account.py:647 ema/views.py:551 +#: intervention/views.py:407 msgid "Share settings updated" msgstr "Freigabe Einstellungen aktualisiert" @@ -1185,11 +1185,11 @@ msgstr "Ersatzzahlungsmaßnahme" msgid "EMA {} added" msgstr "EMA {} hinzugefügt" -#: ema/views.py:211 +#: ema/views.py:210 msgid "EMA {} edited" msgstr "EMA {} bearbeitet" -#: ema/views.py:243 +#: ema/views.py:242 msgid "EMA removed" msgstr "EMA entfernt" @@ -1333,7 +1333,7 @@ msgstr "Kompensationen und Zahlungen geprüft" msgid "Run check" msgstr "Prüfung vornehmen" -#: intervention/forms/modalForms.py:196 konova/forms.py:451 +#: intervention/forms/modalForms.py:196 konova/forms.py:453 msgid "" "I, {} {}, confirm that all necessary control steps have been performed by " "myself." @@ -1476,27 +1476,27 @@ msgstr "Eingriff {} hinzugefügt" msgid "This intervention has {} revocations" msgstr "Dem Eingriff liegen {} Widersprüche vor" -#: intervention/views.py:291 +#: intervention/views.py:290 msgid "Intervention {} edited" msgstr "Eingriff {} bearbeitet" -#: intervention/views.py:326 +#: intervention/views.py:325 msgid "{} removed" msgstr "{} entfernt" -#: intervention/views.py:347 +#: intervention/views.py:346 msgid "Revocation removed" msgstr "Widerspruch entfernt" -#: intervention/views.py:429 +#: intervention/views.py:428 msgid "Check performed" msgstr "Prüfung durchgeführt" -#: intervention/views.py:451 +#: intervention/views.py:450 msgid "Revocation added" msgstr "Widerspruch hinzugefügt" -#: intervention/views.py:522 +#: intervention/views.py:521 msgid "There are errors on this intervention:" msgstr "Es liegen Fehler in diesem Eingriff vor:" @@ -1525,11 +1525,11 @@ msgstr "Speichern" msgid "Not editable" msgstr "Nicht editierbar" -#: konova/forms.py:138 konova/forms.py:305 +#: konova/forms.py:138 konova/forms.py:307 msgid "Confirm" msgstr "Bestätige" -#: konova/forms.py:150 konova/forms.py:314 +#: konova/forms.py:150 konova/forms.py:316 msgid "Remove" msgstr "Löschen" @@ -1542,56 +1542,56 @@ msgstr "Sie sind dabei {} {} zu löschen" msgid "Geometry" msgstr "Geometrie" -#: konova/forms.py:315 +#: konova/forms.py:317 msgid "Are you sure?" msgstr "Sind Sie sicher?" -#: konova/forms.py:342 +#: konova/forms.py:344 msgid "Created on" msgstr "Erstellt" -#: konova/forms.py:344 +#: konova/forms.py:346 msgid "When has this file been created? Important for photos." msgstr "Wann wurde diese Datei erstellt oder das Foto aufgenommen?" -#: konova/forms.py:355 +#: konova/forms.py:357 #: venv/lib/python3.7/site-packages/django/db/models/fields/files.py:231 msgid "File" msgstr "Datei" -#: konova/forms.py:357 +#: konova/forms.py:359 msgid "Allowed formats: pdf, jpg, png. Max size 15 MB." msgstr "Formate: pdf, jpg, png. Maximal 15 MB." -#: konova/forms.py:403 +#: konova/forms.py:405 msgid "Unsupported file type" msgstr "Dateiformat nicht unterstützt" -#: konova/forms.py:410 +#: konova/forms.py:412 msgid "File too large" msgstr "Datei zu groß" -#: konova/forms.py:419 +#: konova/forms.py:421 msgid "Added document" msgstr "Dokument hinzugefügt" -#: konova/forms.py:442 +#: konova/forms.py:444 msgid "Confirm record" msgstr "Verzeichnen bestätigen" -#: konova/forms.py:450 +#: konova/forms.py:452 msgid "Record data" msgstr "Daten verzeichnen" -#: konova/forms.py:457 +#: konova/forms.py:459 msgid "Confirm unrecord" msgstr "Entzeichnen bestätigen" -#: konova/forms.py:458 +#: konova/forms.py:460 msgid "Unrecord data" msgstr "Daten entzeichnen" -#: konova/forms.py:459 +#: konova/forms.py:461 msgid "I, {} {}, confirm that this data must be unrecorded." msgstr "" "Ich, {} {}, bestätige, dass diese Daten wieder entzeichnet werden müssen." @@ -1726,6 +1726,8 @@ msgid "" "Action canceled. Eco account is recorded or deductions exist. Only " "conservation office member can perform this action." msgstr "" +"Aktion abgebrochen. Ökokonto ist bereits verzeichnet oder Abbuchungen liegen vor. Nur " +"Eintragungsstellennutzer können diese Aktion jetzt durchführen." #: konova/utils/message_templates.py:25 msgid "Edited general data" @@ -1739,6 +1741,10 @@ msgstr "Zustand hinzugefügt" msgid "Added compensation action" msgstr "Maßnahme hinzufügen" +#: konova/utils/message_templates.py:31 +msgid "Geometry conflict detected with {}" +msgstr "Geometriekonflikt mit folgenden Einträgen erkannt: {}" + #: konova/utils/messenger.py:69 msgid "{} checked" msgstr "{} geprüft"