From 0b2cf2a0a49e81670d57d61e8e1f64aea2aa7c67 Mon Sep 17 00:00:00 2001
From: mpeltriaux <michel.peltriaux@sgdnord.rlp.de>
Date: Wed, 23 Nov 2022 13:51:05 +0100
Subject: [PATCH 1/2] Geometry race condition fix

* fixes race condition for geometry conflict and parcel calculation
* harmonizes empty geometries from None/MultiPolygonEmpty to MultiPolygonEmpty
---
 compensation/forms/compensation.py            | 36 ++++++++++---------
 compensation/forms/eco_account.py             | 15 ++++----
 ema/forms.py                                  | 16 +++++----
 intervention/forms/intervention.py            | 18 ++++++----
 konova/forms/geometry_form.py                 |  3 ++
 konova/models/geometry.py                     |  5 +++
 .../konova/includes/parcels/parcels.html      |  6 ++++
 konova/views/geometry.py                      |  6 ++--
 8 files changed, 65 insertions(+), 40 deletions(-)

diff --git a/compensation/forms/compensation.py b/compensation/forms/compensation.py
index c5186793..647051d3 100644
--- a/compensation/forms/compensation.py
+++ b/compensation/forms/compensation.py
@@ -129,12 +129,11 @@ class NewCompensationForm(AbstractCompensationForm,
         self.initialize_form_field("identifier", identifier)
         self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:new-id")
 
-    def __create_comp(self, user, geom_form) -> Compensation:
+    def __create_comp(self, user):
         """ Creates the compensation from form data
 
         Args:
             user (User): The performing user
-            geom_form (SimpleGeomForm): The geometry form
 
         Returns:
             comp (Compensation): The compensation object
@@ -150,8 +149,6 @@ class NewCompensationForm(AbstractCompensationForm,
 
         # Create log entry
         action = UserActionLogEntry.get_created_action(user)
-        # Process the geometry form
-        geometry = geom_form.save(action)
 
         # Finally create main object
         comp = Compensation.objects.create(
@@ -162,18 +159,23 @@ class NewCompensationForm(AbstractCompensationForm,
             is_cef=is_cef,
             is_coherence_keeping=is_coherence_keeping,
             is_pik=is_pik,
-            geometry=geometry,
             comment=comment,
         )
 
         # Add the log entry to the main objects log list
         comp.log.add(action)
-        return comp
+        return comp, action
 
     def save(self, user: User, geom_form: SimpleGeomForm):
         with transaction.atomic():
-            comp = self.__create_comp(user, geom_form)
+            comp, action = self.__create_comp(user)
             comp.intervention.mark_as_edited(user, edit_comment=COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
+
+        # Process the geometry form
+        geometry = geom_form.save(action)
+        comp.geometry = geometry
+        comp.save()
+
         return comp
 
 
@@ -205,6 +207,9 @@ class EditCompensationForm(NewCompensationForm):
 
     def save(self, user: User, geom_form: SimpleGeomForm):
         with transaction.atomic():
+            # Create log entry
+            action = UserActionLogEntry.get_edited_action(user)
+
             # Fetch data from cleaned POST values
             identifier = self.cleaned_data.get("identifier", None)
             title = self.cleaned_data.get("title", None)
@@ -214,17 +219,9 @@ class EditCompensationForm(NewCompensationForm):
             is_pik = self.cleaned_data.get("is_pik", None)
             comment = self.cleaned_data.get("comment", None)
 
-            # Create log entry
-            action = UserActionLogEntry.get_edited_action(user)
-
-            # Process the geometry form
-            geometry = geom_form.save(action)
-
-            # Finally create main object
             self.instance.identifier = identifier
             self.instance.title = title
             self.instance.intervention = intervention
-            self.instance.geometry = geometry
             self.instance.is_cef = is_cef
             self.instance.is_coherence_keeping = is_coherence_keeping
             self.instance.comment = comment
@@ -233,6 +230,11 @@ class EditCompensationForm(NewCompensationForm):
             self.instance.save()
 
             self.instance.log.add(action)
-
             intervention.mark_as_edited(user, self.request, EDITED_GENERAL_DATA)
-        return self.instance
\ No newline at end of file
+
+        # Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
+        geometry = geom_form.save(action)
+        self.instance.geometry = geometry
+        self.instance.save()
+
+        return self.instance
diff --git a/compensation/forms/eco_account.py b/compensation/forms/eco_account.py
index 360dd343..dd167c6e 100644
--- a/compensation/forms/eco_account.py
+++ b/compensation/forms/eco_account.py
@@ -94,8 +94,6 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
 
             # Create log entry
             action = UserActionLogEntry.get_created_action(user)
-            # Process the geometry form
-            geometry = geom_form.save(action)
 
             handler = Handler.objects.create(
                 type=handler_type,
@@ -119,7 +117,6 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
                 responsible=responsible,
                 deductable_surface=surface,
                 created=action,
-                geometry=geometry,
                 comment=comment,
                 is_pik=is_pik,
                 legal=legal
@@ -129,6 +126,10 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
             # Add the log entry to the main objects log list
             acc.log.add(action)
 
+        # Process the geometry form
+        geometry = geom_form.save(action)
+        acc.geometry = geometry
+        acc.save()
         acc.update_deductable_rest()
         return acc
 
@@ -185,9 +186,6 @@ class EditEcoAccountForm(NewEcoAccountForm):
             # Create log entry
             action = UserActionLogEntry.get_edited_action(user)
 
-            # Process the geometry form
-            geometry = geom_form.save(action)
-
             # Update responsible data
             self.instance.responsible.handler.type = handler_type
             self.instance.responsible.handler.detail = handler_detail
@@ -204,7 +202,6 @@ class EditEcoAccountForm(NewEcoAccountForm):
             self.instance.identifier = identifier
             self.instance.title = title
             self.instance.deductable_surface = surface
-            self.instance.geometry = geometry
             self.instance.comment = comment
             self.instance.is_pik = is_pik
             self.instance.modified = action
@@ -213,6 +210,10 @@ class EditEcoAccountForm(NewEcoAccountForm):
             # Add the log entry to the main objects log list
             self.instance.log.add(action)
 
+        # Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
+        geometry = geom_form.save(action)
+        self.instance.geometry = geometry
+        self.instance.save()
         self.instance.update_deductable_rest()
         return self.instance
 
diff --git a/ema/forms.py b/ema/forms.py
index bbe09a88..bca72247 100644
--- a/ema/forms.py
+++ b/ema/forms.py
@@ -64,8 +64,6 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin, Pik
 
             # Create log entry
             action = UserActionLogEntry.get_created_action(user)
-            # Process the geometry form
-            geometry = geom_form.save(action)
 
             handler = Handler.objects.create(
                 type=handler_type,
@@ -83,7 +81,6 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin, Pik
                 title=title,
                 responsible=responsible,
                 created=action,
-                geometry=geometry,
                 comment=comment,
                 is_pik=is_pik,
             )
@@ -93,6 +90,11 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin, Pik
 
             # Add the log entry to the main objects log list
             acc.log.add(action)
+
+        # Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
+        geometry = geom_form.save(action)
+        acc.geometry = geometry
+        acc.save()
         return acc
 
 
@@ -141,8 +143,6 @@ class EditEmaForm(NewEmaForm):
 
             # Create log entry
             action = UserActionLogEntry.get_edited_action(user)
-            # Process the geometry form
-            geometry = geom_form.save(action)
 
             # Update responsible data
             self.instance.responsible.handler.type = handler_type
@@ -155,7 +155,6 @@ class EditEmaForm(NewEmaForm):
             # Update main oject data
             self.instance.identifier = identifier
             self.instance.title = title
-            self.instance.geometry = geometry
             self.instance.comment = comment
             self.instance.is_pik = is_pik
             self.instance.modified = action
@@ -163,6 +162,11 @@ class EditEmaForm(NewEmaForm):
 
             # Add the log entry to the main objects log list
             self.instance.log.add(action)
+
+        # Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
+        geometry = geom_form.save(action)
+        self.instance.geometry = geometry
+        self.instance.save()
         return self.instance
 
 
diff --git a/intervention/forms/intervention.py b/intervention/forms/intervention.py
index 00b772b1..8086fd7f 100644
--- a/intervention/forms/intervention.py
+++ b/intervention/forms/intervention.py
@@ -263,9 +263,6 @@ class NewInterventionForm(BaseForm):
                 handler=handler,
             )
 
-            # Process the geometry form
-            geometry = geom_form.save(action)
-
             # Finally create main object, holding the other objects
             intervention = Intervention.objects.create(
                 identifier=identifier,
@@ -273,7 +270,6 @@ class NewInterventionForm(BaseForm):
                 responsible=responsibility_data,
                 legal=legal_data,
                 created=action,
-                geometry=geometry,
                 comment=comment,
             )
 
@@ -282,6 +278,12 @@ class NewInterventionForm(BaseForm):
 
             # Add the performing user as the first user having access to the data
             intervention.share_with_user(user)
+
+        # Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
+        geometry = geom_form.save(action)
+        intervention.geometry = geometry
+        intervention.save()
+
         return intervention
 
 
@@ -370,9 +372,6 @@ class EditInterventionForm(NewInterventionForm):
 
             user_action = self.instance.mark_as_edited(user, edit_comment=EDITED_GENERAL_DATA)
 
-            geometry = geom_form.save(user_action)
-            self.instance.geometry = geometry
-
             self.instance.log.add(user_action)
 
             self.instance.identifier = identifier
@@ -381,5 +380,10 @@ class EditInterventionForm(NewInterventionForm):
             self.instance.modified = user_action
             self.instance.save()
 
+        # Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
+        geometry = geom_form.save(user_action)
+        self.instance.geometry = geometry
+        self.instance.save()
+
         return self.instance
 
diff --git a/konova/forms/geometry_form.py b/konova/forms/geometry_form.py
index 95e0a7db..09449af5 100644
--- a/konova/forms/geometry_form.py
+++ b/konova/forms/geometry_form.py
@@ -63,6 +63,7 @@ class SimpleGeomForm(BaseForm):
         geom = self.data["geom"]
         if geom is None or len(geom) == 0:
             # empty geometry is a valid geometry
+            self.cleaned_data["geom"] = MultiPolygon(srid=DEFAULT_SRID_RLP).ewkt
             return is_valid
         geom = json.loads(geom)
 
@@ -106,6 +107,8 @@ class SimpleGeomForm(BaseForm):
                 return is_valid
 
             features.append(polygon)
+
+        # Unionize all geometry features into one new MultiPolygon
         form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
         for feature in features:
             form_geom = form_geom.union(feature)
diff --git a/konova/models/geometry.py b/konova/models/geometry.py
index 492ba439..f2169032 100644
--- a/konova/models/geometry.py
+++ b/konova/models/geometry.py
@@ -116,6 +116,11 @@ class Geometry(BaseResource):
 
         """
         from konova.models import Parcel, District, ParcelIntersection, Municipal, ParcelGroup
+
+        if self.geom.empty:
+            # Nothing to do
+            return
+
         parcel_fetcher = ParcelWFSFetcher(
             geometry_id=self.id,
         )
diff --git a/konova/templates/konova/includes/parcels/parcels.html b/konova/templates/konova/includes/parcels/parcels.html
index 9512c09c..ae61c56c 100644
--- a/konova/templates/konova/includes/parcels/parcels.html
+++ b/konova/templates/konova/includes/parcels/parcels.html
@@ -17,11 +17,17 @@
             </div>
         </div>
         <div class="card-body">
+            {% if geom_form.instance.geometry %}
             <div hx-trigger="load, every 5s" hx-get="{% url 'geometry-parcels' geom_form.instance.geometry.id %}">
                 <div class="row justify-content-center">
                     <span class="spinner-border rlp-r-inv" role="status"></span>
                 </div>
             </div>
+            {% else %}
+            <div class="alert alert-danger">
+                {% translate 'No geometry entry found on database. Please contact an admin!' %}
+            </div>
+            {% endif %}
         </div>
     </div>
 </div>
\ No newline at end of file
diff --git a/konova/views/geometry.py b/konova/views/geometry.py
index e2e8737f..868d629f 100644
--- a/konova/views/geometry.py
+++ b/konova/views/geometry.py
@@ -32,16 +32,16 @@ def get_geom_parcels(request: HttpRequest, id: str):
     parcels = geom.get_underlying_parcels()
     geos_geom = geom.geom
 
-    parcels_are_currently_calculated = geos_geom is not None and geos_geom.area > 0 and len(parcels) == 0
+    geometry_exists = not geos_geom.empty
+    parcels_are_currently_calculated = geometry_exists and geos_geom.area > 0 and len(parcels) == 0
     parcels_available = len(parcels) > 0
-    no_geometry_given = geos_geom is None
 
     if parcels_are_currently_calculated:
         # Parcels are being calculated right now. Change the status code, so polling stays active for fetching
         # resutls after the calculation
         status_code = 200
 
-    if parcels_available or no_geometry_given:
+    if parcels_available or not geometry_exists:
         parcels = parcels.order_by("-municipal", "flr", "flrstck_zhlr", "flrstck_nnr")
         municipals = parcels.order_by("municipal").distinct("municipal").values("municipal__id")
         municipals = Municipal.objects.filter(id__in=municipals)

From 49d02b31f56180b1a5c240fa5e20473589feb889 Mon Sep 17 00:00:00 2001
From: mpeltriaux <michel.peltriaux@sgdnord.rlp.de>
Date: Wed, 23 Nov 2022 16:05:27 +0100
Subject: [PATCH 2/2] API - Geometry empty

* removes mapping of empty geometry to None due to general switch to empty geometry usage
---
 api/utils/serializer/serializer.py | 2 --
 1 file changed, 2 deletions(-)

diff --git a/api/utils/serializer/serializer.py b/api/utils/serializer/serializer.py
index e0511b85..04a03123 100644
--- a/api/utils/serializer/serializer.py
+++ b/api/utils/serializer/serializer.py
@@ -136,8 +136,6 @@ class AbstractModelAPISerializer:
         geometry = geos.fromstr(geojson)
         if geometry.srid != DEFAULT_SRID_RLP:
             geometry.transform(DEFAULT_SRID_RLP)
-        if geometry.empty:
-            geometry = None
         return geometry
 
     def _get_obj_from_db(self, id, user):