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/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): 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/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/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), + ), + ] 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()) 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 f76b907f..09449af5 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 @@ -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) @@ -82,7 +83,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 @@ -101,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) @@ -141,8 +149,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 ad90b9d1..5e766946 100644 --- a/konova/models/geometry.py +++ b/konova/models/geometry.py @@ -9,16 +9,15 @@ import json from time import sleep from django.contrib.gis.db.models import MultiPolygonField -from django.contrib.gis.geos import Polygon from django.core.exceptions import MultipleObjectsReturned -from django.db import models +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.utils.wfs.spatial import ParcelWFSFetcher from konova.utils.mutex import cache_lock from konova.tasks import celery_check_for_geometry_conflicts -from konova.utils.wfs.spatial import ParcelWFSFetcher class Geometry(BaseResource): @@ -32,7 +31,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 @@ -47,9 +57,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() @@ -71,14 +80,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() @@ -110,6 +119,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/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, 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 @@
- {% 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