#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
This commit is contained in:
2021-12-15 13:59:52 +01:00
parent 262f85e0e0
commit f4541abf20
12 changed files with 179 additions and 23 deletions

View File

@@ -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)

View File

@@ -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()

View File

@@ -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)
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}"

View File

@@ -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

View File

@@ -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")
ADDED_COMPENSATION_ACTION = _("Added compensation action")
# Geometry conflicts
GEOMETRY_OVERLAPS_WITH_TEMPLATE = _("Geometry overlaps {}")
GEOMETRY_OVERLAPPED_BY_TEMPLATE = _("Geometry overlapped by {}")