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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@ -26,3 +26,7 @@ EDITED_GENERAL_DATA = _("Edited general data")
ADDED_COMPENSATION_STATE = _("Added compensation state")
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 {}")