From 4138b056dfb0591e4f2dc966a8c70c20378f60af Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 18 Nov 2022 13:24:36 +0100 Subject: [PATCH 1/7] #238 Fix * adds casting from Decimal() to primitive float for proper calculation --- api/tests/v1/create/deduction_create_post_body.json | 2 +- api/tests/v1/update/deduction_update_put_body.json | 2 +- compensation/models/eco_account.py | 11 +++++++++-- compensation/tests/ecoaccount/test_workflow.py | 6 +++--- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/api/tests/v1/create/deduction_create_post_body.json b/api/tests/v1/create/deduction_create_post_body.json index b467637c..69a1466d 100644 --- a/api/tests/v1/create/deduction_create_post_body.json +++ b/api/tests/v1/create/deduction_create_post_body.json @@ -1,5 +1,5 @@ { "eco_account": "CHANGE_BEFORE_RUN!!!", - "surface": 500.0, + "surface": 500.50, "intervention": "CHANGE_BEFORE_RUN!!!" } \ No newline at end of file diff --git a/api/tests/v1/update/deduction_update_put_body.json b/api/tests/v1/update/deduction_update_put_body.json index 4968bf19..e8589be2 100644 --- a/api/tests/v1/update/deduction_update_put_body.json +++ b/api/tests/v1/update/deduction_update_put_body.json @@ -1,5 +1,5 @@ { "eco_account": "CHANGE_BEFORE_RUN!!!", - "surface": 523400.0, + "surface": 523400.50, "intervention": "CHANGE_BEFORE_RUN!!!" } \ No newline at end of file diff --git a/compensation/models/eco_account.py b/compensation/models/eco_account.py index 48414cca..35e4c02b 100644 --- a/compensation/models/eco_account.py +++ b/compensation/models/eco_account.py @@ -118,8 +118,15 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix intervention__deleted=None, ) deductions_surfaces = deductions.aggregate(Sum("surface"))["surface__sum"] or 0 - available_surfaces = self.deductable_surface or deductions_surfaces ## no division by zero - ret_val = available_surfaces - deductions_surfaces + + available_surface = self.deductable_surface + if available_surface is None: + # Fallback! + available_surface = deductions_surfaces + else: + available_surface = float(available_surface) + + ret_val = available_surface - deductions_surfaces return ret_val diff --git a/compensation/tests/ecoaccount/test_workflow.py b/compensation/tests/ecoaccount/test_workflow.py index ef65edaa..b1c9a8f4 100644 --- a/compensation/tests/ecoaccount/test_workflow.py +++ b/compensation/tests/ecoaccount/test_workflow.py @@ -188,7 +188,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase): # Prepare data for deduction creation deduct_url = reverse("compensation:acc:new-deduction", args=(self.eco_account.id,)) - test_surface = 10.00 + test_surface = 10.50 post_data = { "surface": test_surface, "account": self.eco_account.id, @@ -207,7 +207,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase): # Make sure the deductible surface is valid for the request self.eco_account.set_recorded(self.superuser) self.eco_account.refresh_from_db() - self.eco_account.deductable_surface = test_surface + 1.00 + self.eco_account.deductable_surface = test_surface + 1.0 self.eco_account.save() self.assertIsNotNone(self.eco_account.recorded) self.assertGreater(self.eco_account.deductable_surface, test_surface) @@ -244,7 +244,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase): deduction = EcoAccountDeduction.objects.create( intervention=self.intervention, account=self.eco_account, - surface=0 + surface=1.10 ) self.assertEqual(1, self.intervention.deductions.count()) self.assertEqual(1, self.eco_account.deductions.count()) From 689f3b6d28104d06b04f86575ad80ee03efb48f7 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 18 Nov 2022 13:28:13 +0100 Subject: [PATCH 2/7] Updates LANIS link * changes LANIS link to new layer declaration --- konova/sub_settings/lanis_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/konova/sub_settings/lanis_settings.py b/konova/sub_settings/lanis_settings.py index ac3610b5..ae71866d 100644 --- a/konova/sub_settings/lanis_settings.py +++ b/konova/sub_settings/lanis_settings.py @@ -15,7 +15,7 @@ DEFAULT_SRID_RLP = 25832 # Needed to redirect to LANIS ## Values to be inserted are [zoom_level, x_coord, y_coord] -LANIS_LINK_TEMPLATE = "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/index.php?lang=de&zl={}&x={}&y={}&bl=tk_rlp_tms_grau&bo=1&lo=0.8,0.8,0.8,0.6,0.8,0.8,0.8,0.8,0.8&layers=eiv_f,eiv_l,eiv_p,kom_f,kom_l,kom_p,oek_f,ema_f,mae&service=kartendienste_naturschutz" +LANIS_LINK_TEMPLATE = "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/index.php?lang=de&zl={}&x={}&y={}&bl=tk_rlp_tms_grau&bo=1&lo=0.8,0.8,0.8,0.6,0.8,0.8,0.8,0.8,0.8&layers=eiv_recorded,eiv_unrecorded,kom_recorded,kom_unrecorded,oek_recorded,oek_unrecorded,ema_recorded,ema_unrecorded,mae&service=kartendienste_naturschutz" ## This look up table (LUT) defines different zoom levels on the size of the calculate area of a geometry. LANIS_ZOOM_LUT = { 1000000000: 6, From d6e76f7a2a28087bb5ba786627bdfd156866d294 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 18 Nov 2022 16:22:24 +0100 Subject: [PATCH 3/7] Hotfix * adds missing migration --- .../migrations/0014_auto_20221118_1620.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 compensation/migrations/0014_auto_20221118_1620.py diff --git a/compensation/migrations/0014_auto_20221118_1620.py b/compensation/migrations/0014_auto_20221118_1620.py new file mode 100644 index 00000000..29df0a3f --- /dev/null +++ b/compensation/migrations/0014_auto_20221118_1620.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.3 on 2022-11-18 15:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('compensation', '0013_auto_20221117_0819'), + ] + + operations = [ + migrations.AlterField( + model_name='compensationaction', + name='unit', + field=models.CharField(blank=True, choices=[('cm', 'cm'), ('m', 'm'), ('m2', 'm²'), ('m3', 'm³'), ('km', 'km'), ('ha', 'ha'), ('pcs', 'Pieces')], max_length=100, null=True), + ), + ] From 5594250d59fdcc37e32364ca00d4b191737ce462 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 22 Nov 2022 14:49:51 +0100 Subject: [PATCH 4/7] #243 Feature without geometry * fixes GDALException in case of provided feature (import) without geometry content * modifies 500.html template to inform the user about the admins being informed automatically --- konova/forms/geometry_form.py | 7 ++++++- locale/de/LC_MESSAGES/django.mo | Bin 45638 -> 45704 bytes locale/de/LC_MESSAGES/django.po | 4 ++-- templates/500.html | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/konova/forms/geometry_form.py b/konova/forms/geometry_form.py index f76b907f..b9658b47 100644 --- a/konova/forms/geometry_form.py +++ b/konova/forms/geometry_form.py @@ -82,7 +82,12 @@ class SimpleGeomForm(BaseForm): "MultiPolygon25D", ] for feature in features_json: - feature_geom = json.dumps(feature.get("geometry", feature)) + feature_geom = feature.get("geometry", feature) + if feature_geom is None: + # Fallback for rare cases where a feature does not contain any geometry + continue + + feature_geom = json.dumps(feature_geom) g = gdal.OGRGeometry(feature_geom, srs=DEFAULT_SRID_RLP) flatten_geometry = g.coord_dim > 2 diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 422ac7b6279015669d8aad1186b2f3fbf9e85d4e..b87493bc78dc6505adb6b03dd72ed2545d8d3e0b 100644 GIT binary patch delta 5865 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 diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index 96bfa072..fb261034 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -2307,8 +2307,8 @@ msgid "Server Error" msgstr "" #: templates/500.html:10 -msgid "Something happened. We are working on it!" -msgstr "Irgendetwas ist passiert. Wir arbeiten daran!" +msgid "Something happened. Admins have been informed. We are working on it!" +msgstr "Irgendetwas ist passiert. Die Administratoren wurden informiert. Wir arbeiten daran!" #: templates/email/api/verify_token.html:7 msgid "Hello support" diff --git a/templates/500.html b/templates/500.html index 5299a693..f3839716 100644 --- a/templates/500.html +++ b/templates/500.html @@ -7,7 +7,7 @@

{% trans 'Server Error' %}


- {% trans 'Something happened. We are working on it!' %} + {% trans 'Something happened. Admins have been informed. We are working on it!' %}

{% endblock %} \ No newline at end of file From a6a5bd54505c6dd6580bb87b862b52f42b81281e Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 22 Nov 2022 15:38:03 +0100 Subject: [PATCH 5/7] Further fixes * fixes race condition on geometry conflict calculation if performed in background process * simplifies access to smaller buffered geometry * adds mapping of "qm"->"m2" for UnitChoice in API usage for backwards compatibility --- api/utils/serializer/v1/compensation.py | 3 ++- api/utils/serializer/v1/ecoaccount.py | 3 ++- api/utils/serializer/v1/ema.py | 3 ++- api/utils/serializer/v1/intervention.py | 3 ++- api/utils/serializer/v1/serializer.py | 3 ++- konova/forms/geometry_form.py | 5 +++-- konova/models/geometry.py | 22 +++++++++++++++------- konova/utils/wfs/spatial.py | 2 +- 8 files changed, 29 insertions(+), 15 deletions(-) diff --git a/api/utils/serializer/v1/compensation.py b/api/utils/serializer/v1/compensation.py index 6bddadbf..fbdbba62 100644 --- a/api/utils/serializer/v1/compensation.py +++ b/api/utils/serializer/v1/compensation.py @@ -11,7 +11,7 @@ from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, Abs from compensation.models import Compensation from intervention.models import Intervention from konova.models import Geometry -from konova.tasks import celery_update_parcels +from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts from konova.utils.message_templates import DATA_UNSHARED from user.models import UserActionLogEntry @@ -128,6 +128,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa obj.log.add(obj.created) celery_update_parcels.delay(obj.geometry.id) + celery_check_for_geometry_conflicts.delay(obj.geometry.id) return obj.id diff --git a/api/utils/serializer/v1/ecoaccount.py b/api/utils/serializer/v1/ecoaccount.py index d466c34f..c3d27867 100644 --- a/api/utils/serializer/v1/ecoaccount.py +++ b/api/utils/serializer/v1/ecoaccount.py @@ -13,7 +13,7 @@ from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ from compensation.models import EcoAccount from intervention.models import Legal, Responsibility, Handler from konova.models import Geometry -from konova.tasks import celery_update_parcels +from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts from user.models import UserActionLogEntry @@ -150,6 +150,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1, obj.users.add(user) celery_update_parcels.delay(obj.geometry.id) + celery_check_for_geometry_conflicts.delay(obj.geometry.id) return obj.id diff --git a/api/utils/serializer/v1/ema.py b/api/utils/serializer/v1/ema.py index 2f5c596e..4bbb6d9e 100644 --- a/api/utils/serializer/v1/ema.py +++ b/api/utils/serializer/v1/ema.py @@ -13,7 +13,7 @@ from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ from ema.models import Ema from intervention.models import Responsibility, Handler from konova.models import Geometry -from konova.tasks import celery_update_parcels +from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts from user.models import UserActionLogEntry @@ -122,6 +122,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe obj.users.add(user) celery_update_parcels.delay(obj.geometry.id) + celery_check_for_geometry_conflicts.delay(obj.geometry.id) return obj.id diff --git a/api/utils/serializer/v1/intervention.py b/api/utils/serializer/v1/intervention.py index a6d5084c..dca53d72 100644 --- a/api/utils/serializer/v1/intervention.py +++ b/api/utils/serializer/v1/intervention.py @@ -13,7 +13,7 @@ from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, \ from compensation.models import Payment from intervention.models import Intervention, Responsibility, Legal, Handler from konova.models import Geometry -from konova.tasks import celery_update_parcels +from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts from user.models import UserActionLogEntry @@ -165,6 +165,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1, obj.log.add(obj.created) celery_update_parcels.delay(obj.geometry.id) + celery_check_for_geometry_conflicts.delay(obj.geometry.id) return obj.id diff --git a/api/utils/serializer/v1/serializer.py b/api/utils/serializer/v1/serializer.py index fe979775..78f8d1f6 100644 --- a/api/utils/serializer/v1/serializer.py +++ b/api/utils/serializer/v1/serializer.py @@ -392,7 +392,8 @@ class AbstractCompensationAPISerializerV1Mixin: self._konova_code_from_json(e, CODELIST_COMPENSATION_ACTION_DETAIL_ID) for e in entry["action_details"] ] amount = float(entry["amount"]) - unit = entry["unit"] + # Mapping of old "qm" into "m²" + unit = UnitChoices.m2.value if entry["unit"] == "qm" else entry["unit"] comment = entry["comment"] # Check on validity diff --git a/konova/forms/geometry_form.py b/konova/forms/geometry_form.py index b9658b47..95e0a7db 100644 --- a/konova/forms/geometry_form.py +++ b/konova/forms/geometry_form.py @@ -15,7 +15,7 @@ from django.utils.translation import gettext_lazy as _ from konova.forms.base_form import BaseForm from konova.models import Geometry -from konova.tasks import celery_update_parcels +from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP from user.models import UserActionLogEntry @@ -146,8 +146,9 @@ class SimpleGeomForm(BaseForm): geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)), created=action, ) - # Start the parcel update procedure in a background process + # Start parcel update and geometry conflict checking procedure in a background process celery_update_parcels.delay(geometry.id) + celery_check_for_geometry_conflicts.delay(geometry.id) return geometry def __flatten_geom_to_2D(self, geom): diff --git a/konova/models/geometry.py b/konova/models/geometry.py index f8c272f9..492ba439 100644 --- a/konova/models/geometry.py +++ b/konova/models/geometry.py @@ -8,13 +8,11 @@ Created on: 15.11.21 import json from django.contrib.gis.db.models import MultiPolygonField -from django.contrib.gis.geos import Polygon from django.db import models, transaction from django.utils import timezone from konova.models import BaseResource, UuidModel from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP -from konova.tasks import celery_check_for_geometry_conflicts from konova.utils.wfs.spatial import ParcelWFSFetcher @@ -29,7 +27,18 @@ class Geometry(BaseResource): def save(self, *args, **kwargs): super().save(*args, **kwargs) - celery_check_for_geometry_conflicts.delay(self.id) + + @property + def geom_small_buffered(self): + """ + Returns a smaller buffered version of the geometry. + Can be used to shrink the geometry used for intersection purposes to avoid intersection detection on + neighbouring geometries. + + Returns: + + """ + return self.geom.buffer(-0.001) def check_for_conflicts(self): """ Checks for new geometry overlaps @@ -44,9 +53,8 @@ class Geometry(BaseResource): return None self.recheck_existing_conflicts() - overlapping_geoms = Geometry.objects.filter( - geom__intersects=self.geom, + geom__intersects=self.geom_small_buffered, ).exclude( id=self.id ).distinct() @@ -68,14 +76,14 @@ class Geometry(BaseResource): """ all_conflicts_as_conflicting = self.conflicts_geometries.all() still_conflicting_conflicts = all_conflicts_as_conflicting.filter( - affected_geometry__geom__intersects=self.geom + affected_geometry__geom__intersects=self.geom_small_buffered ) 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 + conflicting_geometry__geom__intersects=self.geom_small_buffered ) resolved_conflicts = all_conflicted_by_conflicts.exclude(id__in=still_conflicting_conflicts) resolved_conflicts.delete() diff --git a/konova/utils/wfs/spatial.py b/konova/utils/wfs/spatial.py index 5578c35a..a3d4bd62 100644 --- a/konova/utils/wfs/spatial.py +++ b/konova/utils/wfs/spatial.py @@ -91,7 +91,7 @@ class ParcelWFSFetcher(AbstractWFSFetcher): geom = Geometry.objects.filter( id=self.geometry_id ).annotate( - smaller=Func(F('geom'), -0.001, function="ST_Buffer") + smaller=Func(F('geom'), -0.001, function="ST_Buffer") # same as geometry.geom_small_buffered but for QuerySet ).annotate( gml=AsGML(MakeValid('smaller')) ).first() From 2ef643f4e004bfe43ddffe3dd03ce9fce856f063 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 23 Nov 2022 13:51:05 +0100 Subject: [PATCH 6/7] 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 @@
+ {% if geom_form.instance.geometry %}
+ {% else %} +
+ {% translate 'No geometry entry found on database. Please contact an admin!' %} +
+ {% endif %}
\ 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 79fd3ad29d90e750794cdff8ff119f7760f25f5b Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 23 Nov 2022 16:05:27 +0100 Subject: [PATCH 7/7] 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):