Merge branch 'master' into 132_Old_entries

# Conflicts:
#	konova/models/geometry.py
This commit is contained in:
mpeltriaux 2022-11-24 06:59:05 +01:00
commit 4f5c172449
24 changed files with 138 additions and 70 deletions

View File

@ -1,5 +1,5 @@
{
"eco_account": "CHANGE_BEFORE_RUN!!!",
"surface": 500.0,
"surface": 500.50,
"intervention": "CHANGE_BEFORE_RUN!!!"
}

View File

@ -1,5 +1,5 @@
{
"eco_account": "CHANGE_BEFORE_RUN!!!",
"surface": 523400.0,
"surface": 523400.50,
"intervention": "CHANGE_BEFORE_RUN!!!"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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', ''), ('m3', ''), ('km', 'km'), ('ha', 'ha'), ('pcs', 'Pieces')], max_length=100, null=True),
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

@ -7,7 +7,7 @@
<h1 class="display-4">{% trans 'Server Error' %}</h1>
<hr>
<p class="lead">
{% trans 'Something happened. We are working on it!' %}
{% trans 'Something happened. Admins have been informed. We are working on it!' %}
</p>
</div>
{% endblock %}