From 442f3ceb37312606a16105af7baa7a3d731f51db Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 15 Oct 2025 09:50:59 +0200 Subject: [PATCH] # Small geometry processing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * changes SimpleGeomForm behaviour on small geometries (<1m²): These geometries will now be dismissed on processing * adds a new info message in case of automatically removed geometries on saving * updates tests --- .../tests/compensation/test_workflow.py | 6 +- .../tests/ecoaccount/test_workflow.py | 4 +- .../views/compensation/compensation.py | 22 ++++++- compensation/views/eco_account/eco_account.py | 22 ++++++- ema/tests/test_workflow.py | 4 +- ema/tests/unit/test_forms.py | 4 +- ema/views/ema.py | 20 +++++- intervention/tests/test_workflow.py | 2 +- intervention/tests/unit/test_forms.py | 4 +- intervention/views/intervention.py | 23 ++++++- konova/forms/geometry_form.py | 46 +++++++++----- konova/tests/test_geometries.py | 1 + konova/utils/message_templates.py | 1 + locale/de/LC_MESSAGES/django.mo | Bin 46166 -> 46187 bytes locale/de/LC_MESSAGES/django.po | 57 +++++++++--------- 15 files changed, 148 insertions(+), 68 deletions(-) diff --git a/compensation/tests/compensation/test_workflow.py b/compensation/tests/compensation/test_workflow.py index 3f024483..7b6c89a0 100644 --- a/compensation/tests/compensation/test_workflow.py +++ b/compensation/tests/compensation/test_workflow.py @@ -54,7 +54,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase): post_data = { "identifier": test_id, "title": test_title, - "geom": geom_json, + "output": geom_json, "intervention": self.intervention.id, } pre_creation_intervention_log_count = self.intervention.log.count() @@ -94,7 +94,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase): post_data = { "identifier": test_id, "title": test_title, - "geom": geom_json, + "output": geom_json, } pre_creation_intervention_log_count = self.intervention.log.count() @@ -150,7 +150,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase): "title": new_title, "intervention": self.intervention.id, # just keep the intervention as it is "comment": new_comment, - "geom": geojson, + "output": geojson, } self.client_user.post(url, post_data) self.compensation.refresh_from_db() diff --git a/compensation/tests/ecoaccount/test_workflow.py b/compensation/tests/ecoaccount/test_workflow.py index 27eb0a99..efbd6342 100644 --- a/compensation/tests/ecoaccount/test_workflow.py +++ b/compensation/tests/ecoaccount/test_workflow.py @@ -46,7 +46,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase): post_data = { "identifier": test_id, "title": test_title, - "geom": geom_json, + "output": geom_json, "surface": test_deductable_surface, "conservation_office": test_conservation_office.id } @@ -103,7 +103,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase): "identifier": new_identifier, "title": new_title, "comment": new_comment, - "geom": self.create_geojson(new_geometry), + "output": self.create_geojson(new_geometry), "surface": test_deductable_surface, "conservation_office": test_conservation_office.id } diff --git a/compensation/views/compensation/compensation.py b/compensation/views/compensation/compensation.py index 158495cc..15bac1f8 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -27,7 +27,7 @@ from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \ RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \ - COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED + COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE @login_required @@ -103,11 +103,19 @@ def new_view(request: HttpRequest, intervention_id: str = None): ) ) messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier)) - if geom_form.geometry_simplified: + if geom_form.has_geometry_simplified(): messages.info( request, GEOMETRY_SIMPLIFIED ) + + num_ignored_geometries = geom_form.get_num_geometries_ignored() + if num_ignored_geometries > 0: + messages.info( + request, + GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) + ) + return redirect("compensation:detail", id=comp.id) else: messages.error(request, FORM_INVALID, extra_tags="danger",) @@ -179,11 +187,19 @@ def edit_view(request: HttpRequest, id: str): if intervention_is_checked: messages.info(request, CHECK_STATE_RESET) messages.success(request, _("Compensation {} edited").format(comp.identifier)) - if geom_form.geometry_simplified: + if geom_form.has_geometry_simplified(): messages.info( request, GEOMETRY_SIMPLIFIED ) + + num_ignored_geometries = geom_form.get_num_geometries_ignored() + if num_ignored_geometries > 0: + messages.info( + request, + GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) + ) + return redirect("compensation:detail", id=comp.id) else: messages.error(request, FORM_INVALID, extra_tags="danger",) diff --git a/compensation/views/eco_account/eco_account.py b/compensation/views/eco_account/eco_account.py index 798e73c0..28dbfc10 100644 --- a/compensation/views/eco_account/eco_account.py +++ b/compensation/views/eco_account/eco_account.py @@ -23,7 +23,7 @@ from konova.forms import SimpleGeomForm from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \ - IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED + IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE @login_required @@ -84,11 +84,19 @@ def new_view(request: HttpRequest): ) ) messages.success(request, _("Eco-Account {} added").format(acc.identifier)) - if geom_form.geometry_simplified: + if geom_form.has_geometry_simplified(): messages.info( request, GEOMETRY_SIMPLIFIED ) + + num_ignored_geometries = geom_form.get_num_geometries_ignored() + if num_ignored_geometries > 0: + messages.info( + request, + GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) + ) + return redirect("compensation:acc:detail", id=acc.id) else: messages.error(request, FORM_INVALID, extra_tags="danger",) @@ -156,11 +164,19 @@ def edit_view(request: HttpRequest, id: str): # The data form takes the geom form for processing, as well as the performing user acc = data_form.save(request.user, geom_form) messages.success(request, _("Eco-Account {} edited").format(acc.identifier)) - if geom_form.geometry_simplified: + if geom_form.has_geometry_simplified(): messages.info( request, GEOMETRY_SIMPLIFIED ) + + num_ignored_geometries = geom_form.get_num_geometries_ignored() + if num_ignored_geometries > 0: + messages.info( + request, + GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) + ) + return redirect("compensation:acc:detail", id=acc.id) else: messages.error(request, FORM_INVALID, extra_tags="danger",) diff --git a/ema/tests/test_workflow.py b/ema/tests/test_workflow.py index 7fb3c6a3..f99e080b 100644 --- a/ema/tests/test_workflow.py +++ b/ema/tests/test_workflow.py @@ -46,7 +46,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase): post_data = { "identifier": test_id, "title": test_title, - "geom": geom_json, + "output": geom_json, "conservation_office": test_conservation_office.id } self.client_user.post(new_url, post_data) @@ -99,7 +99,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase): "identifier": new_identifier, "title": new_title, "comment": new_comment, - "geom": self.create_geojson(new_geometry), + "output": self.create_geojson(new_geometry), "conservation_office": test_conservation_office.id } self.client_user.post(url, post_data) diff --git a/ema/tests/unit/test_forms.py b/ema/tests/unit/test_forms.py index d66fd14b..40865bb8 100644 --- a/ema/tests/unit/test_forms.py +++ b/ema/tests/unit/test_forms.py @@ -48,7 +48,7 @@ class NewEmaFormTestCase(BaseTestCase): ) geom_form_data = json.loads(geom_form_data) geom_form_data = { - "geom": json.dumps(geom_form_data) + "output": json.dumps(geom_form_data) } geom_form = SimpleGeomForm(geom_form_data) @@ -116,7 +116,7 @@ class EditEmaFormTestCase(BaseTestCase): ) geom_form_data = json.loads(geom_form_data) geom_form_data = { - "geom": json.dumps(geom_form_data) + "output": json.dumps(geom_form_data) } geom_form = SimpleGeomForm(geom_form_data) diff --git a/ema/views/ema.py b/ema/views/ema.py index 4136e916..5322b5ef 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.py @@ -24,7 +24,7 @@ from konova.forms.modals import RemoveModalForm from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \ - DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED + DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE @login_required @@ -84,11 +84,17 @@ def new_view(request: HttpRequest): ) ) messages.success(request, _("EMA {} added").format(ema.identifier)) - if geom_form.geometry_simplified: + if geom_form.has_geometry_simplified(): messages.info( request, GEOMETRY_SIMPLIFIED ) + num_ignored_geometries = geom_form.get_num_geometries_ignored() + if num_ignored_geometries > 0: + messages.info( + request, + GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) + ) return redirect("ema:detail", id=ema.id) else: @@ -215,11 +221,19 @@ def edit_view(request: HttpRequest, id: str): # The data form takes the geom form for processing, as well as the performing user ema = data_form.save(request.user, geom_form) messages.success(request, _("EMA {} edited").format(ema.identifier)) - if geom_form.geometry_simplified: + if geom_form.has_geometry_simplified(): messages.info( request, GEOMETRY_SIMPLIFIED ) + + num_ignored_geometries = geom_form.get_num_geometries_ignored() + if num_ignored_geometries > 0: + messages.info( + request, + GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) + ) + return redirect("ema:detail", id=ema.id) else: messages.error(request, FORM_INVALID, extra_tags="danger",) diff --git a/intervention/tests/test_workflow.py b/intervention/tests/test_workflow.py index 4bd61d92..962aead2 100644 --- a/intervention/tests/test_workflow.py +++ b/intervention/tests/test_workflow.py @@ -60,7 +60,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase): post_data = { "identifier": test_id, "title": test_title, - "geom": geom_json, + "output": geom_json, } response = self.client_user.post( new_url, diff --git a/intervention/tests/unit/test_forms.py b/intervention/tests/unit/test_forms.py index 6435e21e..20a6d749 100644 --- a/intervention/tests/unit/test_forms.py +++ b/intervention/tests/unit/test_forms.py @@ -62,7 +62,7 @@ class NewInterventionFormTestCase(BaseTestCase): ) geom_form_data = json.loads(geom_form_data) geom_form_data = { - "geom": json.dumps(geom_form_data) + "output": json.dumps(geom_form_data) } geom_form = SimpleGeomForm(geom_form_data) @@ -104,7 +104,7 @@ class EditInterventionFormTestCase(NewInterventionFormTestCase): ) geom_form_data = json.loads(geom_form_data) geom_form_data = { - "geom": json.dumps(geom_form_data) + "output": json.dumps(geom_form_data) } geom_form = SimpleGeomForm(geom_form_data) diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py index be89056c..0d6cc369 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -23,7 +23,8 @@ from konova.forms.modals import RemoveModalForm from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \ - CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED + CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, \ + GEOMETRIES_IGNORED_TEMPLATE @login_required @@ -88,11 +89,19 @@ def new_view(request: HttpRequest): ) ) messages.success(request, _("Intervention {} added").format(intervention.identifier)) - if geom_form.geometry_simplified: + if geom_form.has_geometry_simplified(): messages.info( request, GEOMETRY_SIMPLIFIED ) + + num_ignored_geometries = geom_form.get_num_geometries_ignored() + if num_ignored_geometries > 0: + messages.info( + request, + GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) + ) + return redirect("intervention:detail", id=intervention.id) else: messages.error(request, FORM_INVALID, extra_tags="danger",) @@ -236,11 +245,19 @@ def edit_view(request: HttpRequest, id: str): messages.success(request, _("Intervention {} edited").format(intervention.identifier)) if intervention_is_checked: messages.info(request, CHECK_STATE_RESET) - if geom_form.geometry_simplified: + if geom_form.has_geometry_simplified(): messages.info( request, GEOMETRY_SIMPLIFIED ) + + num_ignored_geometries = geom_form.get_num_geometries_ignored() + if num_ignored_geometries > 0: + messages.info( + request, + GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) + ) + return redirect("intervention:detail", id=intervention.id) else: messages.error(request, FORM_INVALID, extra_tags="danger",) diff --git a/konova/forms/geometry_form.py b/konova/forms/geometry_form.py index c8c49113..66be2bb3 100644 --- a/konova/forms/geometry_form.py +++ b/konova/forms/geometry_form.py @@ -25,8 +25,8 @@ class SimpleGeomForm(BaseForm): """ A geometry form for rendering geometry read-only using a widget """ - read_only = True - geometry_simplified = False + read_only: bool = True + _geometry_simplified: bool = False output = JSONField( label=_("Geometry"), help_text=_(""), @@ -34,6 +34,7 @@ class SimpleGeomForm(BaseForm): required=False, disabled=False, ) + _num_geometries_ignored: int = 0 def __init__(self, *args, **kwargs): self.read_only = kwargs.pop("read_only", True) @@ -48,6 +49,7 @@ class SimpleGeomForm(BaseForm): raise AttributeError geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP) + self._set_geojson_properties(geojson, title=self.instance.identifier or None) geom = json.dumps(geojson) except AttributeError: # If no geometry exists for this form, we simply set the value to None and zoom to the maximum level @@ -61,7 +63,7 @@ class SimpleGeomForm(BaseForm): is_valid = True # Get geojson from form - geom = self.data["output"] + geom = self.data.get("output", None) if geom is None or len(geom) == 0: # empty geometry is a valid geometry self.cleaned_data["output"] = MultiPolygon(srid=DEFAULT_SRID_RLP).ewkt @@ -100,7 +102,13 @@ class SimpleGeomForm(BaseForm): is_valid &= False return is_valid - is_valid &= self.__is_area_valid(g) + is_area_valid = self.__is_area_valid(g) + if not is_area_valid: + # Geometries with an invalid size will not be saved to the db + # We assume these are malicious snippets which are not supposed to be in the geometry in the first place + self._num_geometries_ignored += 1 + continue + g = Polygon.from_ewkt(g.ewkt) is_valid &= g.valid if not g.valid: @@ -147,15 +155,6 @@ class SimpleGeomForm(BaseForm): """ is_area_valid = geom.area > 1 # > 1m² (SRID:25832) - - if not is_area_valid: - self.add_error( - "output", - _("Geometry must be greater than 1m². Currently is {}m²").format( - float(geom.area) - ) - ) - return is_area_valid def __simplify_geometry(self, geom, max_vert: int): @@ -208,13 +207,29 @@ class SimpleGeomForm(BaseForm): if not is_vertices_num_valid: geometry.geom = self.__simplify_geometry(geometry.geom, max_vert=GEOM_MAX_VERTICES) geometry.save() - self.geometry_simplified = True + self._geometry_simplified = True # 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 get_num_geometries_ignored(self): + """ Returns the number of geometries which had to be ignored for various reasons + + Returns: + + """ + return self._num_geometries_ignored + + def has_geometry_simplified(self): + """ Returns whether the geometry has been simplified or not. + + Returns: + + """ + return self._geometry_simplified + def __flatten_geom_to_2D(self, geom): """ Enforces a given OGRGeometry from higher dimensions into 2D @@ -225,11 +240,12 @@ class SimpleGeomForm(BaseForm): geom = gdal.OGRGeometry(g_wkt) return geom - def _set_properties(self, geojson: dict, title: str): + def _set_geojson_properties(self, geojson: dict, title: str = None): """ Toggles the editable property of the geojson for proper handling in map client Args: geojson (dict): The GeoJson + title (str): An alternative title for the geometry Returns: geojson (dict): The altered GeoJson diff --git a/konova/tests/test_geometries.py b/konova/tests/test_geometries.py index 047e285d..7850ac7d 100644 --- a/konova/tests/test_geometries.py +++ b/konova/tests/test_geometries.py @@ -124,6 +124,7 @@ class GeometryTestCase(BaseTestCase): { "type": "Feature", "geometry": json.loads(p.json), + "properties": {} } for p in polygons ] diff --git a/konova/utils/message_templates.py b/konova/utils/message_templates.py index e5e71557..ba44934b 100644 --- a/konova/utils/message_templates.py +++ b/konova/utils/message_templates.py @@ -83,6 +83,7 @@ EDITED_GENERAL_DATA = _("Edited general data") # Geometry GEOMETRY_CONFLICT_WITH_TEMPLATE = _("Geometry conflict detected with {}") GEOMETRY_SIMPLIFIED = _("The geometry contained more than {} vertices. It had to be simplified to match the allowed limit of {} vertices.").format(GEOM_MAX_VERTICES, GEOM_MAX_VERTICES) +GEOMETRIES_IGNORED_TEMPLATE = _("The geometry contained {} parts which have been detected as invalid (e.g. too small to be valid). These parts have been removed. Please check the stored geometry.") # INTERVENTION INTERVENTION_HAS_REVOCATIONS_TEMPLATE = _("This intervention has {} revocations") diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index b56f73a1acae404e9195dd294f8bf0f63b1defb9..4f5c1c6cdbcd85104ef8449094f69ddd59f917aa 100644 GIT binary patch delta 11753 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 delta 11740 zcmYk?3w)2||HturY;)KQJ7AmHmodb~oXKI%l+$v^nc>UEHrnPGZf7|azLZjvLyDZ~ zK*=vl`Vl#Vlv7GMMF$l*{Qj@^uFLj3w(j&RoZFo$_QtDOiUk@BJ1_3ByuY1xsQB48wM)2l}Eq9*yc~GM2#w z)-@POyc^5mNmRX?s0rT3cno0~60tr8(!Vo|j4F=8!Z-<8U1zE);1=9~@1X`XsG%81 z4r;}wVHtc8i{V>X7Ps2?AZmigQ4{#V`YXB`*+VjFxKty@DUOv<6>DKxY--~kr~##- zjz=15r50cjT!~e2EovnWqqg7*s{P-r0gX-l$i}R{22ha#tw23g$E{II-VODj7h`ZD zY9_B>1a3ghd>?9{r%@|%3(MgH)W9N|nD%2(TU-;>ZnGw=e-xP%3Suz>)zL!KgB!6Z z?y&Jks0Z^=1380Qk!z@x_!ZTG^Q3vcG-^d7QEyje)Bsyz0(N!D_>-A}T8SC9!gAD# ztVK1v1J&?;)E=KicFj3&??*H>1FC|m*U-itPy^_TYR`*Nn2VaA`w|(=;1yethuX9C z7>zqnD{vIGHD~Zy{Lz*-ZDzKjEvkclr~!{e4RjK!{aL6LTVU_Mf%M}#o5`r*-KeGc z+*bG+wYNXo`}eUtana^x231gdoQygveNbm23)S8%)I^q`+Ia(YICo)9JcnU=|BJO? zA1H`HEmbCJOD3RZGzGN+3sFnE91B(mHKQGXg5Yn)yELfZw17P_d;MP%R7~Zi#BAJ!&hvpe8mF zHG$FSYKG5~QOEO9Ble*xu15{vUDOQr+52DG_#$erZ(=+?K(!Os%6uPcVgur?sDaEu z4RjG|1y;6V{R=vxK)#E7Bb;r>=h?YtD^_6oY`;?-lW_#Hea<_m)BH1z!9s0JhdJ1r zcq3+E;kJ%b0khGAOE3m^v}OIZhsP+;3@@XW_%`ZL6=`Rdt~^E(H$ok6n6pq6lp zE#Gb9d{q6Ts69S`fp`J6HCHeRZy}58xE@ZBMm7|S;CQQxTH3j&*JTYR;8&;tJVYI; zLLGVaumb8ZPrz)vi^Xu%Q)VJ#QSHq{59T2Qb)9{-LOyB$$5DssEEdOWsMqce7Q9AJ zn}$PC<>hUhfI7?#u|Bp#4RkUV!s)0jn}yoqmDol@-f0W&qV~2_C-Y!Ds>2$nnKVLm z*a~&3yQAvQM{U)csIAIpft}5WqEUyZE^4HmtV8Yn zNytCWQvT2kFJW~o)5W|s$ykDTB5DQaqVB(fTG>yLueS4FWHFtQIzK)p7#vS)*n&H@qFT{jmcY_j3B#}+#$pG|!!%TbMd-a0hM+o(Mb%G0O|Ydk1+@~XsCLJ= zWHizls1dz{YH%g$kgP#1_Rns6xHEn)boF!8gzP^@=$9W>Mg2=8b}LU-W65f z9ZV*aOg3r&vu(TtHGp-f*JulBU^`Kt(mkkwoJAJF`2}@oTlO;Ly-^)xpawn_b+}hp zw=+AE#+Uq8$0rf|HtJAP7&OtSlhb`~})cwCvKO-V~o0)dU+QdV# zJuX8H=p0tTTUb=@e>k5no&FfqQpKTGBoQO9C6>WwZJdRg`7G-S3@6@%s`n{s;NPH@ z{72LZ-a!rE9_q{m^`$=jJK<#1Kn>L4Nk$!#E;b&3ZHPyq8rX)~^ZlrfzOwOmsOPWR z_%9n5?q}L7jb*v-L7k~a=xSuiWYkbw)SjoHI_QTjF%9|0*~lMHU|@g88G=dZ!DYzq zIq%^Z3?9I5O*jR$1^2NM*5pTJ3UxO7?$dq)BL2Z&=<8LgHaE9Z9E#a zWVu)iC!@Av6KXF%K`s43)EPR3+WSkW3H*v`H;`r1)&`-Tk8;WAu+>C0&;r$9XVjMT zvgJci0~mu1aRN5RZ5W5w(SspF%@3zqSeSS?a?qWT7=!s(5x+-msatrMS;9f6y z;9)@l%ad+K+6=Xq9Z_4;139105NzegdyeOc{YUXDIbOy)I6uSu&4`<62G%Xhao(qV z74jlDy+)gVgW83A>E8+Fok_++s6!F-96zHm2CL!Es1Hu5Y;(Ud7A07*u@w4H?XI);_hC5kY1CVBBbW8p1Ab%8p@_r=#BEWp&t%ky7otC|!gySV0eBd9 z;c-+ubHsE(s1n5{~}D#T5&EDpwCoZylvPi8i1Pu8G1*oK&@u;P5jpcA4hTwRN#yO~V+%;r0qRrL=s1DDgR^T>Pz{p9a zp+;DQI0XZ-4{Bh8F%mN|3g=>BT!Y%e^{5rujXJ!?jIMK*j9$OXs67vS-u!M?1~tPG zs0TAq56(h$v4@ay{v<0 zvLsr{krZezr=dEWgL++jSnyh+8rp`1@l$Mw2WN5myBMczfm1MLd`IEwi!@a^e2wNCRhod#sR3;a07P7?brna=9pixdf*ep>v16- z$5uFSF5enljUS?0^#$|ky@XXM2%cx+dZ-l`j5^JuQ7bS3HPd-m1@lmQ`5Ed%a~ZXj z_i+%0&Nnm9MwL%Sodq9qs9oo6lW}&VhZ~1&eAC8{P!Gl~FiYCN+7b1==#OeJ1GOU4 zPy>I#`U+~`>rq?x0crvtVZrbJ$83eO*p3_5QHLjqbyLNKSR7ZQI@p97&@Oxe4`U+! zh1#kri_A(qff>ZDkz39-T#C(JG%I=xLpguWSu#4!H?b}jeaVcpDTWXyqXyCyOX4tF zo{Rb}%tQ@vIhMdT?foq_-ivw-52Ft44;X~ei&=k-xGEWSP!sj~w8Sph&&C^3hvzf& z!y-$}k`_Y^FbcII)v*I6qfY-k>+7g9vK3YT2$sZiOW1$y$xRB1;jgHX{ev25AnR2V zn_~iw!bE%(YvC7|gm@6+;OJV>0F}WBt|fE(&VnC)fgS zqs~IDTra0B1RA|u<>ft0Cu8Yzx}AwehAgUDJ+i{Py@M-`Y?qsea*BUs=O7d z-JYoaMq$Cv|0!g222BVfZ3xjbyYGr0&1zd@7xW~p9 zFq-%wY9f)VO}z%zo~QxlqE>eHYW80pyhecruol(P`xt}ys0P1B&GaE^24QPVc^vj2 zZh?AknRPX)gDt3b^HD2w8gmX1fxkn8x!1X8dA zbvSmT_VN&F@6MnO<8>^KckTVcn@s&usDV_(`dA$s;ULthUx6MxgiY`U7Q*<=`ckt0 z)yb&BdW^$^*dK48ma@YZvlY)_Jn?3nieF<0cHe6LBhwI6hsRMXS$LaSi6W>0mbX?$ zO}M7+)4x-fj2h~I3HU4~;%uyqyRb1{!R8pd-INbRb@URd+=qJZ1U`d)JIr4+^uTh& zo2&;=Z_@>IJ!I~XsfZCf%?dQM_CtM2UDR243pLOK7=&Nj_!15$zKwpU#&?nU+2im{ zbJ|fj5|`;Fmxol(-&ekyerhkrq9!jios?6)9^< z+U@(kQrVuX$h|`1=ofr&TiDv+)GTWASGb=}(lv#2n>3hI!IqINIQDWXWqW*`<6@ir zPPCV3GX8FBPQ*vVMNzx)@9RxlM`cTW8{(onKP0F3IE6c@r2mlDl}o-FsWxc{aST3B zx?XUXAG4@EUTov;=<}7X9OibX*7rnb@Ed!#G?r(!NtAIq3;wgx&*U$Ve*<;R$DTI- z6>%{2`w{;{zL70gy@TZcv~fG)W2DQ(pO7x=OL5ZPtiz45Kf})Yni)dJ>6Gpfj1cE^Z+`zVwyoM0p2nf>8x+@zEMgpz8>U54)3QHb;6AiN^9{fmE?R61r*!=4_iTf*Te9J!58T(PzniNI83uz{4p{=)x{7Uj;Q2)1< zuJWX_q#Azw)nq!E0o-gr`is1-zNGu4rzv}tG?BbNCX;mCA_W)Z_!&Z3AM%B<5@{Ow zeOMWP^fil*?s<;r@zsHRB0(?GALPfAN|XK}-6iQdWsN7FS5RX9_|xY7xF6u#93NZy zyscT*PHd>}e0;R~1f>b~&f~|Y{44&Id;m|h=jK{lAq~B@&Kf-Xull3Nm!N**$BkJ( zrMv>=&)B-J8>5^rY=!?(P?G#hq{F1`z9m(o-QVopp4f{Tku-6Ld_&ueen2lGztF}R z7(<=qc%Sqn_g#CBtTTlCQf!HRNCU{{l0GNp5Fa8PBP}60evYq3wWO3n{SLnx1B;kgK+Dz&~jS-~NzHveDp zN%sCt%0@pf$CC6KLdqjw6jxw0X{#UKrz|oZd2lg4z7~*aLt0B&F&?-^zAE_*q<2X{ z#9@@TBLDB}GaKKwwzW3H5S#y!2X@)~Hb0l6ZG-RdC^x^vZ%Ln$uTA=Zq^mJy8%cFY zy1uZ|gtLx(8ymN_KExxWj|*yZJ~4v$DybUzWYUW|$aP6&N%^EtDAe^Vd0j=AWpVOf zk)9(C#X(r!-VfsbrBTNNHIU~M-(?xNRO{kWP&LF z*0;SztlO1baY`;?InsFY#YksJb4a@OkS=iVX*@@Ii{v9^Q+5*d3tBhQ52WUll_JHF zPLOnct}=#N!GunA;_2jf=!66l{D-6~9^be5E!dl~udxd0Bhu-DTIN3~l^|c-u1}6F z7dxq^>x97xv~~JZex9BnbT~E^WAx7H#OV&=BMKOXADk$ztN(|eXnQ$V!L3{7$OmA-X zL{H|}oLtWkuV+NIH#OIr?a56`&GIB=?%$EKX1G@5y8cS-ClhiN5pwBYm^`58OFtK&`6n?9kG^8e0wn%YQ$A6(dW{$SD{Y-KJ+H zdRmUk9qY}=;6}TQ{B=Xqyjkg4X{i~xP3%y2%}Qim*&Z`mucuAAH;bvJlO~PL_Vo3p z=X$czho\n" "Language-Team: LANGUAGE \n" @@ -1298,13 +1298,13 @@ msgid "Compensation {} edited" msgstr "Kompensation {} bearbeitet" #: compensation/views/compensation/compensation.py:196 -#: compensation/views/eco_account/eco_account.py:173 ema/views/ema.py:232 +#: compensation/views/eco_account/eco_account.py:173 ema/views/ema.py:238 #: intervention/views/intervention.py:253 msgid "Edit {}" msgstr "Bearbeite {}" #: compensation/views/compensation/report.py:35 -#: compensation/views/eco_account/report.py:35 ema/views/report.py:35 +#: compensation/views/eco_account/report.py:36 ema/views/report.py:35 #: intervention/views/report.py:35 msgid "Report {}" msgstr "Bericht {}" @@ -1325,7 +1325,7 @@ msgstr "Ökokonto {} bearbeitet" msgid "Eco-account removed" msgstr "Ökokonto entfernt" -#: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:102 +#: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:108 msgid "New EMA" msgstr "Neue EMA hinzufügen" @@ -1361,11 +1361,11 @@ msgstr "EMAs - Übersicht" msgid "EMA {} added" msgstr "EMA {} hinzugefügt" -#: ema/views/ema.py:217 +#: ema/views/ema.py:223 msgid "EMA {} edited" msgstr "EMA {} bearbeitet" -#: ema/views/ema.py:256 +#: ema/views/ema.py:262 msgid "EMA removed" msgstr "EMA entfernt" @@ -1815,10 +1815,6 @@ msgid "Only surfaces allowed. Points or lines must be buffered." msgstr "" "Nur Flächen erlaubt. Punkte oder Linien müssen zu Flächen gepuffert werden." -#: konova/forms/geometry_form.py:153 -msgid "Geometry must be greater than 1m². Currently is {}m²" -msgstr "Geometrie muss größer als 1m² sein. Aktueller Flächeninhalt: {}m²" - #: konova/forms/modals/document_form.py:37 msgid "When has this file been created? Important for photos." msgstr "Wann wurde diese Datei erstellt oder das Foto aufgenommen?" @@ -2266,24 +2262,33 @@ msgstr "" "Die Geometrie enthielt mehr als {} Eckpunkte. Sie musste vereinfacht werden " "um die Obergrenze von {} erlaubten Eckpunkten einzuhalten." -#: konova/utils/message_templates.py:88 +#: konova/utils/message_templates.py:86 +msgid "" +"The geometry contained {} parts which have been detected as invalid (e.g. " +"too small to be valid). These parts have been removed. Please check the " +"stored geometry." +msgstr "" +"Die Geometrie enthielt {} invalide Bestandteile (z.B. unaussagekräftige Kleinstflächen)." +"Diese Bestandteile wurden automatisch entfernt. Bitte überprüfen Sie die angepasste Geometrie." + +#: konova/utils/message_templates.py:89 msgid "This intervention has {} revocations" msgstr "Dem Eingriff liegen {} Widersprüche vor" -#: konova/utils/message_templates.py:91 +#: konova/utils/message_templates.py:92 msgid "Checked on {} by {}" msgstr "Am {} von {} geprüft worden" -#: konova/utils/message_templates.py:92 +#: konova/utils/message_templates.py:93 msgid "Data has changed since last check on {} by {}" msgstr "" "Daten wurden nach der letzten Prüfung geändert. Letzte Prüfung am {} durch {}" -#: konova/utils/message_templates.py:93 +#: konova/utils/message_templates.py:94 msgid "Current data not checked yet" msgstr "Momentane Daten noch nicht geprüft" -#: konova/utils/message_templates.py:96 +#: konova/utils/message_templates.py:97 msgid "New token generated. Administrators need to validate." msgstr "Neuer Token generiert. Administratoren sind informiert." @@ -2313,14 +2318,6 @@ msgstr "Home" msgid "Log" msgstr "Log" -#: konova/views/map_proxy.py:84 -msgid "" -"The external service is currently unavailable.
Please try again in a few " -"moments..." -msgstr "" -"Der externe Dienst ist zur Zeit nicht erreichbar.
Versuchen Sie es in ein " -"paar Sekunden nochmal." - #: konova/views/record.py:30 msgid "{} unrecorded" msgstr "{} entzeichnet" @@ -2377,17 +2374,19 @@ msgstr "Alle" msgid "News" msgstr "Neuigkeiten" -#: templates/400.html:7 +#: templates/400.html:12 msgid "Request was invalid" msgstr "Anfrage fehlerhaft" -#: templates/400.html:10 +#: templates/400.html:17 msgid "There seems to be a problem with the link you opened." msgstr "Es scheint ein Problem mit dem Link zu geben, den Sie geöffnet haben." -#: templates/400.html:11 +#: templates/400.html:18 msgid "Make sure the URL is valid (no whitespaces, properly copied, ...)." -msgstr "Stellen Sie sicher, dass die URL gültig ist (keine Leerzeichen, fehlerfrei kopiert, ...)." +msgstr "" +"Stellen Sie sicher, dass die URL gültig ist (keine Leerzeichen, fehlerfrei " +"kopiert, ...)." #: templates/404.html:7 msgid "Not found" @@ -2401,11 +2400,11 @@ msgstr "Die angeforderten Daten existieren nicht." msgid "Make sure the URL is valid (no whitespaces, ...)." msgstr "Stellen Sie sicher, dass die URL gültig ist (keine Leerzeichen, ...)." -#: templates/500.html:7 +#: templates/500.html:12 msgid "Server Error" msgstr "" -#: templates/500.html:10 +#: templates/500.html:17 msgid "Something happened. Admins have been informed. We are working on it!" msgstr "" "Irgendetwas ist passiert. Die Administratoren wurden informiert. Wir "