From 5922d5ce06ae02d38f2e52c3bd4d7969a5aa4327 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 9 Jan 2024 13:11:04 +0100 Subject: [PATCH 1/8] # Issue #381 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * adds another validity check to SimpleGeomForm (is_size_valid) to make sure the area of the entered geometry is somehow rational (>= 1m²) * optimizes performance of django command sanitize_db * extends Geometry model with two new attributes, holding timestamps when a parcel calculation has been started and ended * finally drops unused update_parcel_wfs in favor of update_parcels in Geometry model * refactors update_parcel method * adds geometry buffer fallback in schneider/fetcher.py to avoid emptying of geometries when parcels shall be fetched * finally removes utils/wfs/spatial.py * extends GeomParcelsView according to #381 * updates translations * removes redundant psycopg2-binary requirement --- konova/forms/geometry_form.py | 26 +- konova/management/commands/sanitize_db.py | 107 +- ...eometry_parcel_calculation_end_and_more.py | 23 + konova/models/geometry.py | 196 +- konova/settings.py | 4 + konova/utils/schneider/fetcher.py | 6 +- konova/utils/wfs/spatial.py | 189 -- konova/views/geometry.py | 34 +- locale/de/LC_MESSAGES/django.mo | Bin 47228 -> 45416 bytes locale/de/LC_MESSAGES/django.po | 1714 ++--------------- requirements.txt | 1 - 11 files changed, 431 insertions(+), 1869 deletions(-) create mode 100644 konova/migrations/0015_geometry_parcel_calculation_end_and_more.py delete mode 100644 konova/utils/wfs/spatial.py diff --git a/konova/forms/geometry_form.py b/konova/forms/geometry_form.py index 59664fdc..33137cb6 100644 --- a/konova/forms/geometry_form.py +++ b/konova/forms/geometry_form.py @@ -98,12 +98,14 @@ class SimpleGeomForm(BaseForm): if g.geom_type not in accepted_ogr_types: self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered.")) - is_valid = False + is_valid &= False return is_valid + is_valid &= self.__is_size_valid(g) + polygon = Polygon.from_ewkt(g.ewkt) - is_valid = polygon.valid - if not is_valid: + is_valid &= polygon.valid + if not polygon.valid: self.add_error("geom", polygon.valid_reason) return is_valid @@ -137,6 +139,24 @@ class SimpleGeomForm(BaseForm): return num_vertices <= GEOM_MAX_VERTICES + def __is_size_valid(self, geom: gdal.OGRGeometry): + """ Checks whether the number of vertices in the geometry is not too high + + Returns: + + """ + is_area_valid = geom.area > 1 # > 1m² (SRID:25832) + + if not is_area_valid: + self.add_error( + "geom", + _("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): """ Simplifies a geometry diff --git a/konova/management/commands/sanitize_db.py b/konova/management/commands/sanitize_db.py index b296b2a7..e78b0e66 100644 --- a/konova/management/commands/sanitize_db.py +++ b/konova/management/commands/sanitize_db.py @@ -61,14 +61,24 @@ class Command(BaseKonovaCommand): action=UserAction.CREATED ) - intervention_log_entries_ids = self.get_all_log_entries_ids(Intervention) - attached_log_entries_id = intervention_log_entries_ids.union( - self.get_all_log_entries_ids(Compensation), - self.get_all_log_entries_ids(EcoAccount), - self.get_all_log_entries_ids(Ema), - ) + EIV_log_entries_ids = self.get_all_log_entries_ids(Intervention) + self._write_warning(f" EIV: {EIV_log_entries_ids.count()} attached log entries") + KOM_log_entries_ids = self.get_all_log_entries_ids(Compensation) + self._write_warning(f" KOM: {KOM_log_entries_ids.count()} attached log entries") + OEK_log_entries_ids = self.get_all_log_entries_ids(EcoAccount) + self._write_warning(f" OEK: {OEK_log_entries_ids.count()} attached log entries") + EMA_log_entries_ids = self.get_all_log_entries_ids(Ema) + self._write_warning(f" EMA: {EMA_log_entries_ids.count()} attached log entries") - unattached_log_entries = all_log_entries.exclude(id__in=attached_log_entries_id) + unattached_log_entries = all_log_entries.exclude( + id__in=EIV_log_entries_ids + ).exclude( + id__in=KOM_log_entries_ids + ).exclude( + id__in=OEK_log_entries_ids + ).exclude( + id__in=EMA_log_entries_ids + ) num_entries = unattached_log_entries.count() if num_entries > 0: @@ -108,13 +118,20 @@ class Command(BaseKonovaCommand): self._write_warning("=== Sanitize compensation actions ===") all_actions = CompensationAction.objects.all() - compensation_action_ids = self.get_all_action_ids(Compensation) - attached_action_ids = compensation_action_ids.union( - self.get_all_action_ids(EcoAccount), - self.get_all_action_ids(Ema), - ) + kom_action_ids = self.get_all_action_ids(Compensation) + self._write_warning(f" KOM: {kom_action_ids.count()} attached actions") + oek_action_ids = self.get_all_action_ids(EcoAccount) + self._write_warning(f" OEK: {oek_action_ids.count()} attached actions") + ema_action_ids = self.get_all_action_ids(Ema) + self._write_warning(f" EMA: {ema_action_ids.count()} attached actions") - unattached_actions = all_actions.exclude(id__in=attached_action_ids) + unattached_actions = all_actions.exclude( + id__in=kom_action_ids + ).exclude( + id__in=oek_action_ids + ).exclude( + id__in=ema_action_ids + ) num_entries = unattached_actions.count() if num_entries > 0: @@ -125,7 +142,7 @@ class Command(BaseKonovaCommand): self._write_success("No unattached actions found.") self._break_line() - def get_all_deadline_ids(self, cls): + def _get_all_deadline_ids(self, cls): """ Getter for all deadline ids of a model Args: @@ -154,13 +171,20 @@ class Command(BaseKonovaCommand): self._write_warning("=== Sanitize deadlines ===") all_deadlines = Deadline.objects.all() - compensation_deadline_ids = self.get_all_deadline_ids(Compensation) - attached_deadline_ids = compensation_deadline_ids.union( - self.get_all_deadline_ids(EcoAccount), - self.get_all_deadline_ids(Ema), - ) + kom_deadline_ids = self._get_all_deadline_ids(Compensation) + self._write_warning(f" KOM: {kom_deadline_ids.count()} attached deadlines") + oek_deadline_ids = self._get_all_deadline_ids(EcoAccount) + self._write_warning(f" OEK: {kom_deadline_ids.count()} attached deadlines") + ema_deadline_ids = self._get_all_deadline_ids(Ema) + self._write_warning(f" EMA: {kom_deadline_ids.count()} attached deadlines") - unattached_deadlines = all_deadlines.exclude(id__in=attached_deadline_ids) + unattached_deadlines = all_deadlines.exclude( + id__in=kom_deadline_ids + ).exclude( + id__in=oek_deadline_ids + ).exclude( + id__in=ema_deadline_ids + ) num_entries = unattached_deadlines.count() if num_entries > 0: @@ -171,7 +195,7 @@ class Command(BaseKonovaCommand): self._write_success("No unattached deadlines found.") self._break_line() - def get_all_geometry_ids(self, cls): + def _get_all_geometry_ids(self, cls): """ Getter for all geometry ids of a model Args: @@ -200,14 +224,24 @@ class Command(BaseKonovaCommand): self._write_warning("=== Sanitize geometries ===") all_geometries = Geometry.objects.all() - compensation_geometry_ids = self.get_all_geometry_ids(Compensation) - attached_geometry_ids = compensation_geometry_ids.union( - self.get_all_geometry_ids(Intervention), - self.get_all_geometry_ids(EcoAccount), - self.get_all_geometry_ids(Ema), - ) + kom_geometry_ids = self._get_all_geometry_ids(Compensation) + self._write_warning(f" EMA: {kom_geometry_ids.count()} attached geometries") + eiv_geometry_ids = self._get_all_geometry_ids(Intervention) + self._write_warning(f" EMA: {eiv_geometry_ids.count()} attached geometries") + oek_geometry_ids = self._get_all_geometry_ids(EcoAccount) + self._write_warning(f" EMA: {oek_geometry_ids.count()} attached geometries") + ema_geometry_ids = self._get_all_geometry_ids(Ema) + self._write_warning(f" EMA: {ema_geometry_ids.count()} attached geometries") - unattached_geometries = all_geometries.exclude(id__in=attached_geometry_ids) + unattached_geometries = all_geometries.exclude( + id__in=kom_geometry_ids + ).exclude( + id__in=eiv_geometry_ids + ).exclude( + id__in=oek_geometry_ids + ).exclude( + id__in=ema_geometry_ids + ) num_entries = unattached_geometries.count() if num_entries > 0: @@ -218,7 +252,7 @@ class Command(BaseKonovaCommand): self._write_success("No unattached geometries found.") self._break_line() - def get_all_state_ids(self, cls): + def _get_all_state_ids(self, cls): """ Getter for all states (before and after) of a class Args: @@ -254,14 +288,19 @@ class Command(BaseKonovaCommand): """ self._write_warning("=== Sanitize compensation states ===") all_states = CompensationState.objects.all() - compensation_state_ids = self.get_all_state_ids(Compensation) - account_state_ids = self.get_all_state_ids(EcoAccount) - ema_state_ids = self.get_all_state_ids(Ema) - attached_state_ids = compensation_state_ids.union(account_state_ids, ema_state_ids) + + kom_state_ids = self._get_all_state_ids(Compensation) + oek_state_ids = self._get_all_state_ids(EcoAccount) + ema_state_ids = self._get_all_state_ids(Ema) unattached_states = all_states.exclude( - id__in=attached_state_ids + id__in=kom_state_ids + ).exclude( + id__in=oek_state_ids + ).exclude( + id__in=ema_state_ids ) + num_unattached_states = unattached_states.count() if num_unattached_states > 0: self._write_error(f"Found {num_unattached_states} unused compensation states. Delete now...") diff --git a/konova/migrations/0015_geometry_parcel_calculation_end_and_more.py b/konova/migrations/0015_geometry_parcel_calculation_end_and_more.py new file mode 100644 index 00000000..c3eac77b --- /dev/null +++ b/konova/migrations/0015_geometry_parcel_calculation_end_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.1 on 2024-01-09 10:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('konova', '0014_resubmission'), + ] + + operations = [ + migrations.AddField( + model_name='geometry', + name='parcel_update_end', + field=models.DateTimeField(blank=True, db_comment='When the last parcel calculation finished', help_text='When the last parcel calculation finished', null=True), + ), + migrations.AddField( + model_name='geometry', + name='parcel_update_start', + field=models.DateTimeField(blank=True, db_comment='When the last parcel calculation started', help_text='When the last parcel calculation started', null=True), + ), + ] diff --git a/konova/models/geometry.py b/konova/models/geometry.py index 81ba2d77..0d348c39 100644 --- a/konova/models/geometry.py +++ b/konova/models/geometry.py @@ -14,13 +14,24 @@ from django.utils import timezone from konova.models import BaseResource, UuidModel from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP from konova.utils.schneider.fetcher import ParcelFetcher -from konova.utils.wfs.spatial import ParcelWFSFetcher class Geometry(BaseResource): """ Geometry model """ + parcel_update_start = models.DateTimeField( + blank=True, + null=True, + db_comment="When the last parcel calculation started", + help_text="When the last parcel calculation started" + ) + parcel_update_end = models.DateTimeField( + blank=True, + null=True, + db_comment="When the last parcel calculation finished", + help_text="When the last parcel calculation finished", + ) geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID_RLP) def __str__(self): @@ -109,82 +120,14 @@ class Geometry(BaseResource): objs += set_objs return objs - @transaction.atomic - def update_parcels_wfs(self): - """ Updates underlying parcel information using the WFS of LVermGeo - - Returns: - + def get_data_object(self): """ - from konova.models import Parcel, District, ParcelIntersection, Municipal, ParcelGroup - - if self.geom.empty: - # Nothing to do - return - - parcel_fetcher = ParcelWFSFetcher( - geometry_id=self.id, - ) - typename = "ave:Flurstueck" - fetched_parcels = parcel_fetcher.get_features( - typename - ) - _now = timezone.now() - underlying_parcels = [] - for result in fetched_parcels: - parcel_properties = result["properties"] - # There could be parcels which include the word 'Flur', - # which needs to be deleted and just keep the numerical values - ## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE! - flr_val = parcel_properties["flur"].replace("Flur ", "") - district = District.objects.get_or_create( - key=parcel_properties["kreisschl"], - name=parcel_properties["kreis"], - )[0] - municipal = Municipal.objects.get_or_create( - key=parcel_properties["gmdschl"], - name=parcel_properties["gemeinde"], - district=district, - )[0] - parcel_group = ParcelGroup.objects.get_or_create( - key=parcel_properties["gemaschl"], - name=parcel_properties["gemarkung"], - municipal=municipal, - )[0] - flrstck_nnr = parcel_properties['flstnrnen'] - if not flrstck_nnr: - flrstck_nnr = None - flrstck_zhlr = parcel_properties['flstnrzae'] - if not flrstck_zhlr: - flrstck_zhlr = None - parcel_obj = Parcel.objects.get_or_create( - district=district, - municipal=municipal, - parcel_group=parcel_group, - flr=flr_val, - flrstck_nnr=flrstck_nnr, - flrstck_zhlr=flrstck_zhlr, - )[0] - parcel_obj.district = district - parcel_obj.updated_on = _now - parcel_obj.save() - underlying_parcels.append(parcel_obj) - - # Update the linked parcels - self.parcels.clear() - self.parcels.set(underlying_parcels) - - # Set the calculated_on intermediate field, so this related data will be found on lookups - intersections_without_ts = self.parcelintersection_set.filter( - parcel__in=self.parcels.all(), - calculated_on__isnull=True, - ) - for entry in intersections_without_ts: - entry.calculated_on = _now - ParcelIntersection.objects.bulk_update( - intersections_without_ts, - ["calculated_on"] - ) + Getter for the specific data object which is related to this geometry + """ + objs = self.get_data_objects() + assert (len(objs) <= 1) + result = objs.pop() + return result def update_parcels(self): """ Updates underlying parcel information @@ -192,55 +135,63 @@ class Geometry(BaseResource): Returns: """ - from konova.models import Parcel, District, ParcelIntersection, Municipal, ParcelGroup - if self.geom.empty: # Nothing to do return + self._set_parcel_update_start_time() + self._perform_parcel_update() + self._set_parcel_update_end_time() + + def _perform_parcel_update(self): + """ + Performs the main logic of parcel updating. + """ + from konova.models import Parcel, District, ParcelIntersection, Municipal, ParcelGroup + parcel_fetcher = ParcelFetcher( geometry=self ) fetched_parcels = parcel_fetcher.get_parcels() - _now = timezone.now() underlying_parcels = [] for result in fetched_parcels: - # There could be parcels which include the word 'Flur', - # which needs to be deleted and just keep the numerical values - ## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE! - flr_val = result["flur"].replace("Flur ", "") - district = District.objects.get_or_create( - key=result["kreisschl"], - name=result["kreis"], - )[0] - municipal = Municipal.objects.get_or_create( - key=result["gmdschl"], - name=result["gemeinde"], - district=district, - )[0] - parcel_group = ParcelGroup.objects.get_or_create( - key=result["gemaschl"], - name=result["gemarkung"], - municipal=municipal, - )[0] - flrstck_nnr = result['flstnrnen'] - if not flrstck_nnr: - flrstck_nnr = None - flrstck_zhlr = result['flstnrzae'] - if not flrstck_zhlr: - flrstck_zhlr = None - parcel_obj = Parcel.objects.get_or_create( - district=district, - municipal=municipal, - parcel_group=parcel_group, - flr=flr_val, - flrstck_nnr=flrstck_nnr, - flrstck_zhlr=flrstck_zhlr, - )[0] - parcel_obj.district = district - parcel_obj.updated_on = _now - parcel_obj.save() + with transaction.atomic(): + # There could be parcels which include the word 'Flur', + # which needs to be deleted and just keep the numerical values + ## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE! + flr_val = result["flur"].replace("Flur ", "") + district = District.objects.get_or_create( + key=result["kreisschl"], + name=result["kreis"], + )[0] + municipal = Municipal.objects.get_or_create( + key=result["gmdschl"], + name=result["gemeinde"], + district=district, + )[0] + parcel_group = ParcelGroup.objects.get_or_create( + key=result["gemaschl"], + name=result["gemarkung"], + municipal=municipal, + )[0] + flrstck_nnr = result['flstnrnen'] + if not flrstck_nnr: + flrstck_nnr = None + flrstck_zhlr = result['flstnrzae'] + if not flrstck_zhlr: + flrstck_zhlr = None + parcel_obj = Parcel.objects.get_or_create( + district=district, + municipal=municipal, + parcel_group=parcel_group, + flr=flr_val, + flrstck_nnr=flrstck_nnr, + flrstck_zhlr=flrstck_zhlr, + )[0] + parcel_obj.district = district + parcel_obj.updated_on = _now + parcel_obj.save() underlying_parcels.append(parcel_obj) # Update the linked parcels @@ -259,6 +210,23 @@ class Geometry(BaseResource): ["calculated_on"] ) + @transaction.atomic + def _set_parcel_update_start_time(self): + """ + Sets the current time for the parcel calculation begin + """ + self.parcel_update_start = timezone.now() + self.parcel_update_end = None + self.save() + + @transaction.atomic + def _set_parcel_update_end_time(self): + """ + Sets the current time for the parcel calculation end + """ + self.parcel_update_end = timezone.now() + self.save() + def get_underlying_parcels(self): """ Getter for related parcels and their districts diff --git a/konova/settings.py b/konova/settings.py index e1c2fb4b..3e5c9ae7 100644 --- a/konova/settings.py +++ b/konova/settings.py @@ -46,4 +46,8 @@ DEFAULT_GROUP = "Default" ZB_GROUP = "Registration office" ETS_GROUP = "Conservation office" +# GEOMETRY +## Max number of allowed vertices. Geometries larger will be simplified until they reach this threshold GEOM_MAX_VERTICES = 10000 +## Max seconds to wait for a parcel calculation result before a new request will be started (default: 5 minutes) +GEOM_THRESHOLD_RECALCULATION_SECONDS = 300 diff --git a/konova/utils/schneider/fetcher.py b/konova/utils/schneider/fetcher.py index 0bc87f7e..9eefb612 100644 --- a/konova/utils/schneider/fetcher.py +++ b/konova/utils/schneider/fetcher.py @@ -28,7 +28,11 @@ class ParcelFetcher: self.geometry = geometry # Reduce size of geometry to avoid "intersections" because of exact border matching - geom = geometry.geom.buffer(-0.001) + buffer_threshold = 0.001 + geom = geometry.geom.buffer(-buffer_threshold) + if geom.area < buffer_threshold: + # Fallback for malicious geometries which are way too small but would disappear on buffering + geom = geometry.geom self.geojson = geom.ewkt self.results = [] diff --git a/konova/utils/wfs/spatial.py b/konova/utils/wfs/spatial.py deleted file mode 100644 index a3d4bd62..00000000 --- a/konova/utils/wfs/spatial.py +++ /dev/null @@ -1,189 +0,0 @@ -""" -Author: Michel Peltriaux -Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 17.12.21 - -""" -import json -from abc import abstractmethod -from json import JSONDecodeError -from time import sleep - -import requests -from django.contrib.gis.db.models.functions import AsGML, MakeValid -from django.db.models import Func, F -from requests.auth import HTTPDigestAuth - -from konova.settings import PARCEL_WFS_USER, PARCEL_WFS_PW, PROXIES - - -class AbstractWFSFetcher: - """ Base class for fetching WFS data - - """ - # base_url represents not the capabilities url but the parameter-free base url - base_url = None - version = None - auth_user = None - auth_pw = None - auth_digest_obj = None - - class Meta: - abstract = True - - def __init__(self, base_url: str, version: str = "1.1.0", auth_user: str = None, auth_pw: str = None, *args, **kwargs): - self.base_url = base_url - self.version = version - self.auth_pw = auth_pw - self.auth_user = auth_user - - self._create_auth_obj() - - def _create_auth_obj(self): - if self.auth_pw is not None and self.auth_user is not None: - self.auth_digest_obj = HTTPDigestAuth( - self.auth_user, - self.auth_pw - ) - - @abstractmethod - def get_features(self, feature_identifier: str, filter_str: str): - raise NotImplementedError - - -class ParcelWFSFetcher(AbstractWFSFetcher): - """ Fetches features from a special parcel WFS - - """ - geometry_id = None - geometry_property_name = None - count = 100 - - def __init__(self, geometry_id: str, geometry_property_name: str = "msGeometry", *args, **kwargs): - super().__init__( - version="2.0.0", - base_url="https://www.geoportal.rlp.de/registry/wfs/519", - auth_user=PARCEL_WFS_USER, - auth_pw=PARCEL_WFS_PW, - *args, - **kwargs - ) - self.geometry_id = geometry_id - self.geometry_property_name = geometry_property_name - - def _create_spatial_filter(self, - geometry_operation: str): - """ Creates a xml spatial filter according to the WFS filter specification - - The geometry needs to be shrinked by a very small factor (-0.01) before a GML can be created for intersection - checking. Otherwise perfect parcel outline placement on top of a neighbouring parcel would result in an - intersection hit, despite the fact they do not truly intersect just because their vertices match. - - Args: - geometry_operation (str): One of the WFS supported spatial filter operations (according to capabilities) - - Returns: - spatial_filter (str): The spatial filter element - """ - from konova.models import Geometry - - geom = Geometry.objects.filter( - id=self.geometry_id - ).annotate( - smaller=Func(F('geom'), -0.001, function="ST_Buffer") # same as geometry.geom_small_buffered but for QuerySet - ).annotate( - gml=AsGML(MakeValid('smaller')) - ).first() - geom_gml = geom.gml - spatial_filter = f"<{geometry_operation}>{self.geometry_property_name}{geom_gml}" - return spatial_filter - - def _create_post_data(self, - geometry_operation: str, - typenames: str = None, - start_index: int = 0, - ): - """ Creates a POST body content for fetching features - - Args: - geometry_operation (str): One of the WFS supported spatial filter operations (according to capabilities) - - Returns: - _filter (str): A proper xml WFS filter - """ - start_index = str(start_index) - spatial_filter = self._create_spatial_filter( - geometry_operation - ) - _filter = f'{spatial_filter}' - return _filter - - def get_features(self, - typenames: str, - spatial_operator: str = "Intersects", - filter_srid: str = None, - start_index: int = 0, - rerun_on_exception: bool = True - ): - """ Fetches features from the WFS using POST - - POST is required since GET has a character limit around 4000. Having a larger filter would result in errors, - which do not occur in case of POST. - - Args: - typenames (str): References to parameter 'typenames' in a WFS GetFeature request - spatial_operator (str): Defines the spatial operation for filtering - filter_srid (str): Defines the spatial reference system, the geometry shall be transformed into for filtering - start_index (str): References to parameter 'startindex' in a - - Returns: - features (list): A list of returned features - """ - found_features = [] - while start_index is not None: - post_body = self._create_post_data( - spatial_operator, - typenames, - start_index - ) - response = requests.post( - url=self.base_url, - data=post_body, - auth=self.auth_digest_obj, - proxies=PROXIES, - ) - - content = response.content.decode("utf-8") - try: - # Check if collection is an exception and does not contain the requested data - content = json.loads(content) - except JSONDecodeError as e: - if rerun_on_exception: - # Wait a second before another try - sleep(1) - self.get_features( - typenames, - spatial_operator, - filter_srid, - start_index, - rerun_on_exception=False - ) - else: - e.msg += content - raise e - fetched_features = content.get( - "features", - {}, - ) - - found_features += fetched_features - - if len(fetched_features) < self.count: - # The response was not 'full', so we got everything to fetch - start_index = None - else: - # If a 'full' response returned, there might be more to fetch. Increase the start_index! - start_index += self.count - - return found_features diff --git a/konova/views/geometry.py b/konova/views/geometry.py index bf34e612..d0ab642c 100644 --- a/konova/views/geometry.py +++ b/konova/views/geometry.py @@ -10,10 +10,13 @@ from django.contrib.gis.geos import MultiPolygon from django.http import HttpResponse, HttpRequest from django.shortcuts import get_object_or_404 from django.template.loader import render_to_string +from django.utils import timezone from django.views import View from konova.models import Geometry +from konova.settings import GEOM_THRESHOLD_RECALCULATION_SECONDS from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP +from konova.tasks import celery_update_parcels class GeomParcelsView(LoginRequiredMixin, View): @@ -38,15 +41,25 @@ class GeomParcelsView(LoginRequiredMixin, View): parcels = geom.get_underlying_parcels() geos_geom = geom.geom or MultiPolygon(srid=DEFAULT_SRID_RLP) - geometry_exists = not geos_geom.empty - parcels_are_currently_calculated = geometry_exists and geos_geom.area > 0 and len(parcels) == 0 + waiting_too_long = self._check_waiting_too_long(geom) + + geometry_exists = not geos_geom.empty and geos_geom.area > 0 + parcels_are_currently_calculated = ( + geometry_exists and + geom.parcel_update_start and + not geom.parcel_update_end + ) parcels_available = len(parcels) > 0 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 + # results after the calculation status_code = 200 + if waiting_too_long: + # Trigger calculation again + celery_update_parcels.delay(geom.id) + if parcels_available or not geometry_exists: municipals = geom.get_underlying_municipals(parcels) @@ -69,6 +82,21 @@ class GeomParcelsView(LoginRequiredMixin, View): else: return HttpResponse(None, status=404) + def _check_waiting_too_long(self, geom: Geometry): + """ Check whether the client is waiting too long for a parcel calculation result + + Depending on the geometry's modified attribute + + """ + try: + pcs_diff = (timezone.now() - geom.parcel_update_start).seconds + except TypeError: + pcs_diff = GEOM_THRESHOLD_RECALCULATION_SECONDS + + calculation_not_finished = geom.parcel_update_end is None + waiting_too_long = (pcs_diff >= GEOM_THRESHOLD_RECALCULATION_SECONDS) and calculation_not_finished + return waiting_too_long + class GeomParcelsContentView(LoginRequiredMixin, View): diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index cbfcbda46c4d13446b591eba2bfb1aa43bd5b008..ff48ee9b246e525b380b8e45bd5bad366e374a04 100644 GIT binary patch delta 12041 zcmZA72YgOv|Htu@L_{QFM}+8Y#U3$I6hXv{y=x`75t1N@S={#CQM+baHLBWMt9I3+ zT5Y8*rK)roRihNurTjnN`?~xe|NnVCKkwglopY{pt#gv{Ji6NxrR9Op00rW{<|aq>}q1;3!YqlV*j$B8u^Cml~?Cu|nu zIPGyc&cMqUg+psOPAtyCP{;8&he&2qaS{t*=h{2~-KcUV=EhkVf(vc^YNP>YCyv2= zsQYTxahx!0Y;A|SuMd{Rp|*Yj7UB8MS`r5B?8bt4)Rs?Ue#%#{Fh0N{n5V9Ja1`o6 zl`$OaTRUQL%0n;$r=qS~gBs8_jK+glo##83Ni@Pj%!@88jsaK^nO&zU>U2S%(q09rc`JsP;caE%~?gnSV8WlZr@ug3n=O1M{E; zs2h7?9!#?36x4mAP#u|xTB4PxnOKi{z%JDNM^H0*0`-=Ch3eoP4@otWe=!$UdBMy? zP1FVPs2+DhHJpTMcqD3#ry{H3%(v%{p*nOLb=_54eu(Noo`$Bq!Wc=}Q-(x6Y=jy? zf-NVa)~qX*z$DZROhhfsOzeTnZ2b+?Qrt&%IJlANa0IHO6;bWiLUpjdIqz}WlITIb zQ4J44jXcX-;7miU?J|3Q8P;>?mnqefH(H3JP$QyY)I8A6RH3Dxlo>l9nR2(`I4qB`V7E#(K8 z*F*9JiKgfRYALRw9&{TuWe-su{1@Y~U=y>;yP-y&hOKZGsslNw4qe1Ryn|}zchpio zLhZGPrmVk45JjR9Rzf|v9;(MpQ5SYab)Xk&1Zma@sFBRZXk3Z9|1j#iZ~|lT2C4(4 zo0$$)M9pw)Gv;3pXi9}_fxPNYTkM20kgu+D7Ii}i!)KYDaBPBIk=1fuM(u?&I1=xm z9-P#iR~J`eI^M-n*gwH>TsS$wW8U)(RA|Z$p+=a4#qc}Srh0&yy5JV(i&h@B+v8B1 zsvGvhRMZlDidvfUsF}QnxiPS%naNNrP1)rk(TQfL2en3Zq#tTej6^j!0RwQ3tzUqe zu@$J9S!?TGx8>ca>kpvXKZ07Ck5EgQgF)!IY?GU)DZGzWF^nBm4PQV#CDUYB zA(Q7^!3-SK+I09Fs>45`+Pi};%-hCvs0!+Q4Wt7erxA%ZQ3B@0Zm8EQ3AGfXFc>G> z`Z>0|8ntP+AU{BzcTgR@j{bNXwKR88OBvMGaaexmIaGNN7S#|YlIX@|s0Xe@J@8dj zM|Pn$(-G8~KSYfnG|?$L z!8L40{bSS!6Dd@}X{fhkC+5Ses2RMEIv>#9tZg;qYwXlTCc()-y;X-W5c79n{)>|o zCDA6SfngYnYB&Kyu?zZ^#MY0(2Go0Oc|Wq2&d0X=1*)A(s6BHV^~nzIXgV5=c__zr zWd1`);;4{qQ61@ldOiAKHJpO{&)LNvnfL^|VkX1sf*+$gTC%h0cqLT%c~pDN(bqoe z!98p_sWbDh3sb4k+Kfl7*&@`MZp7zs53a{Es0K56Y5|;pnyLAy>sMobeA9XeH4~qs z+WijoykAir`o}|}27?%mHc1$2DkD%!<3c^C0qVxKsD^sl`eD|WFfa8BP#sul>$jq= ze;*6sNz{Wc+Op>t5Mh9K&2%UP^{Fg`g)s)TNfT_j531pjsE$rUeeu>=-$mVj z3iTbhjdd|kceAvOk&bwrB$6mjq@rGnd8mdqV;ml^=l?-{`-}H5BkX}SDGx!uSi!z2z0w@DCpxRF_dl6LU#1bL2KJ!V`XkhXPTTVLs0MD? z^53=`^rC67C>G;!J%+A+zu7 z!;x5^kK;_jsaOV|U?(io*Zdxkh1DsqMHim5-o&DmL-|qJ1k3be{Ub;QQc(^kVjbLs z4e>1Mi&nTly~RfSp%J`-`hsmmee2)E3b-3f;WzgDAE+7l7j>V1k|~FxW~gA2hjk$- zPKBnvHEQj8qo#TgYSUz()_M|Z1Pf6OZ$Yi)8>svDqBhqjsQbS|wRZ!x1ou((Pf#7m z=SeodzZb>&RJ6mgI2&EK17qf(Fnz=2gCHw}fV!%Ltlg65; z&G;H>W)Gsyd;TKvCy5?pUb{-Do;9T7ObfE|8tU7R6NA?7&qATbP;NAyoQl@*p|;*?_qK3Lxz|iGUZWA)fCHO zB5G+z+VdMx9Y2V=?qkfy^PSTq>@(*_Z06@UO@{J$q&z)^4Tj0X`27!qhnv6gc#P_3 zPAdNqf~C^hh-<~zN4rmI*q#T8rHxEs2MDmVfIcOYG(VQM<>RR$ffqg zPShqljvB#b)Pn;v%?G9=7NA@QbzcJNd=eJHv8cCX0qTRa8MRmTV=P|7{1}zR{1+mr z#um+mO)wf0Fc4F54`!emsxaEz7>DXW2lPD{gDH-6IKyAWX zqnUqpp%XsFoQOj;Gzbe~x-Cz!&O>!@Ifml_)KYzgiMQjSJ-ARg6z zC)5CXdr0)4RMcxW8P)Uos2Ny++U4s|H@t^x@C0h=ub@W!1j8_5oOv56pxTMYV%Xj~ z81=l#s2T7qA<-vuKStnr48nVu8~;Xi%x}EuNEqtD6)^zgQA^koH6t&gW@MCg0_t^~ zj#~4r*b(<41N1m~CYT$;P&byx;#eKk^H%nJ5^62SVpp7R>wmEIe_#>nLtZi+a-rV$ z+Nha}!$|Cidd_g4tbZ1X)?^ZDq_a_*WI1XH_Mv)y1bbo*s)33VO$TGJDCO3u4)#a2 zmx)?}NvORs2Lt)O>_RpvdjH28R^`MEU>lfSd>!=ysH-+`rRD42(*6J*(13zI2yp8oSc&Zt3JZi-K z@I@SlTHDj82VFr8mlkGkP4 z)PoMAru0kH1OLHT44lqiVqinm+cOMw8^_NAZ)-9>f$FVv1v0eE0$C##6R@5%r$mL~YK1Mdqz}0drAqiF!aH>Mcpe_V|)5 zpG57EE9i$67W4jV>MD_hVFT1sw8mDLgc{j4>p|3B$U$9y8}s8M)JQ{@n7vQ{Lnue0 zIvR~tu^(2$`B)wIEn)s^kX)xiU#5tq=C|24*n;xwSPO4sdvr1H1#kqaV^gpRW@9j( z!J2pp<1l}=X{Q5fsa9YlZa~f0{%q#IAjuIb)WDb43#h5QihAJR*bzgPo00WFZK9#5 z>qcTBoQYbZRagplquS3w&D4*mnfndZ0mt*Q+2w&)iE=1vjbf3X6V7;ChYyiYgOk0& zy#K{knmv+&MX4W)>cB$O)Ne#Rc&9BNMm_kX^*laD*>jyFnk3IE(?AW>iDvjY4nR#^ z&DG{LtZ!|H+AGPZhR2~EINQ1!)v+D6d=z!vY1GX9glu+?bAv<=_#I2)W7LPI#2Pcg zrl^s1Ks7i3^`LR6k_UsLeAFV=)t(;C9qVZelHbjCw%KdNZSO7)rSns=aPl3RAHx&PR_b z-XbZ1r?4d6K-C9rFh-*~n1Gtn?x-ajj_N=b>Or$H3YVkWdj~brQ>X!4vGotIE9C+k znSb5r-e?-gKs~^NYIr$nDYl~4dLOExunb)d3>blCP*SY~}^EJl~ z*agSomQ5ZrbtO0R1B8kWs0KgBf%pJtVb3k*Ke@g~y{6+{FL4Rd4QHnSH(QA-zv zTB^#Z&DRL^L#(Ym-yOAg2BJEWg>`Wv)?=lecSvHWxQQ-|+HQU$YK;Dr$D`^eVp)6z z``}Tmh^2Ozsc(tVl&9ll+>V-oN;}QJ{nSG}XDe!k&ZF<&|1XlLXZNgsqeklgrkSE( zR70h(8dk&V*c)r&JgkogurdB_>+9??4|1dGQ&9K4ft~Rj_F(@xmESTSnrYUxsMlr> zy6_~H#+#@a$hX@VgCUf=VR=l(8aNxZC-!0(od`BJfB1BtC3$Ze>n>bIo0WKxSGig3~Lu{eT>@fpBrcTGVINhYqeOs5u`VR&W#fY;+HW9`%enA~yS@nG9 zH44Q!ssESDLn4j1O4KCE5dPYQ97o7EAh$TTiNA@4#M5I8$vWaM;#Z<5*XJUNaQ}GZ zR|sbpb|b2gpC){bX)W$isiPe>AqsNSOSsxpJHv4Y@gcE?sKB|$#MeX_%EyVxg&T+n(t^0wz648$GZ?-Pp`jlBDdZS8nosMy+uk0(D|6~eNi0MRe-)Vj#T6<8} z*XG5TDIFWAD@f=lg?ES?;sNFNuz-9BAHTy3#QQ`&B2$~GAn_8>f(jjR zM0avOVl;U_Vh}OIp1(-mf#5sk`$wsssAC!D-ymGXAVQzz%f2)J{bMibEY9hNRY}6L zhQbAsR|)+{e0oeF`PUYfTlug#^SJ&aVlMIYNV7>_>aLLUqr&%Z!m92~6eGG4Z8`r1 z>ZnLwP5XZzi62w;J@zu^e7{^ABY#4jj>4QTL0*7pOe7G06AOq4;^~p9GKDSp2mV7; zBR^rUSxGsB_z&fqL~lRle=Ze#ivQ2C#NMFF*U6U?d!MN*`An{BPCrwwZS%|4cb++y zL>@%^hkL$Mz1F`pq2nimvl;8^Bu5SFGz?%C_Smw{eX2h=u29D*-|rDC$omtys91-G zs4t8<&JwL`o}2UeDC_SZoeWzUL*9qb@u$HV&bjx=N7!p-qkfwGZqF~XmZiRe%_DJz z%}3e$hFDddi#TP<9_I{&O{(R{P5j7-RQwsgRV7C)@+EkWxI{itl^myt>b{I$FsN@u zo|lo;!X~!j!gmQBZi5rVOzF`7r!<6XIuJSr<6V>b{_f~KJ^z+HsrmypUyX~1L}G!xA<`-M7%@j zm}>C-g-uV+52IX|SVeSvdJgLm?{Yqih$ioc<#8m@kzB`N;t{c*avPjXc+S|9Cn@O2 zPlOTQ*!tgaG37$G{4$0xlJ;1cdL5k%&LRvX2HNr~_MYakY`4t1xeqz_F_ z&1zsrl{|M+?(92@kCjQu8j_mg9^}q&TDXU%y3+?a-O@7MSrbO556PrwX({R1WtR@{ zE?)XkE^prz^8&ngSI-FYdNu{*_AcC7HPCx}=SaWo_+8_@AMa}8=Pk9nZ-8qIBN=Xn dnBvaJawRZb87YGY|L^c}?J4iAwWm$4{{b}ILI?l= delta 13857 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 B%&GtY diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index 4eef6ffc..c34cf2d9 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -29,7 +29,7 @@ #: konova/filters/mixins/office.py:25 konova/filters/mixins/office.py:56 #: konova/filters/mixins/office.py:57 konova/filters/mixins/record.py:23 #: konova/filters/mixins/self_created.py:24 konova/filters/mixins/share.py:23 -#: konova/forms/geometry_form.py:32 konova/forms/modals/document_form.py:26 +#: konova/forms/geometry_form.py:33 konova/forms/modals/document_form.py:26 #: konova/forms/modals/document_form.py:36 #: konova/forms/modals/document_form.py:50 #: konova/forms/modals/document_form.py:62 @@ -43,7 +43,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-08 11:30+0200\n" +"POT-Creation-Date: 2024-01-09 09:46+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -90,8 +90,8 @@ msgstr "Verantwortliche Stelle" #: intervention/forms/intervention.py:67 intervention/forms/intervention.py:84 #: intervention/forms/intervention.py:100 #: intervention/forms/intervention.py:116 -#: intervention/forms/intervention.py:157 intervention/forms/modals/share.py:41 -#: intervention/forms/modals/share.py:55 user/forms/modals/team.py:48 +#: intervention/forms/intervention.py:157 intervention/forms/modals/share.py:40 +#: intervention/forms/modals/share.py:54 user/forms/modals/team.py:48 #: user/forms/modals/team.py:112 msgid "Click for selection" msgstr "Auswählen..." @@ -267,8 +267,8 @@ msgstr "" #: ema/templates/ema/detail/includes/states-after.html:36 #: ema/templates/ema/detail/includes/states-before.html:36 #: intervention/forms/modals/deduction.py:47 -#: templates/email/other/deduction_changed.html:31 -#: templates/email/other/deduction_changed_team.html:31 +#: templates/email/other/deduction_changed.html:32 +#: templates/email/other/deduction_changed_team.html:32 msgid "Surface" msgstr "Fläche" @@ -336,8 +336,8 @@ msgstr "Typ" #: intervention/forms/modals/deduction.py:65 intervention/tables.py:87 #: intervention/templates/intervention/detail/view.html:19 #: konova/templates/konova/includes/quickstart/interventions.html:4 -#: templates/email/other/deduction_changed.html:26 -#: templates/email/other/deduction_changed_team.html:26 +#: templates/email/other/deduction_changed.html:27 +#: templates/email/other/deduction_changed_team.html:27 #: templates/navbars/navbar.html:22 msgid "Intervention" msgstr "Eingriff" @@ -447,7 +447,7 @@ msgid "Select the intervention for which this compensation compensates" msgstr "Wählen Sie den Eingriff, für den diese Kompensation bestimmt ist" #: compensation/forms/compensation.py:114 -#: compensation/views/compensation/compensation.py:120 +#: compensation/views/compensation/compensation.py:119 msgid "New compensation" msgstr "Neue Kompensation" @@ -474,7 +474,7 @@ msgid "When did the parties agree on this?" msgstr "Wann wurde dieses Ökokonto offiziell vereinbart?" #: compensation/forms/eco_account.py:72 -#: compensation/views/eco_account/eco_account.py:101 +#: compensation/views/eco_account/eco_account.py:100 msgid "New Eco-Account" msgstr "Neues Ökokonto" @@ -691,11 +691,11 @@ msgstr "Zahlung wird an diesem Datum erwartet" msgid "Add a payment for intervention '{}'" msgstr "Neue Ersatzzahlung zu Eingriff '{}' hinzufügen" -#: compensation/forms/modals/payment.py:86 +#: compensation/forms/modals/payment.py:89 msgid "If there is no date you can enter, please explain why." msgstr "Falls Sie kein Datum angeben können, erklären Sie bitte weshalb." -#: compensation/forms/modals/payment.py:105 +#: compensation/forms/modals/payment.py:108 #: intervention/templates/intervention/detail/includes/payments.html:59 msgid "Edit payment" msgstr "Zahlung bearbeiten" @@ -959,7 +959,6 @@ msgstr "Log anzeigen" #: compensation/templates/compensation/detail/eco_account/includes/controls.html:41 #: ema/templates/ema/detail/includes/controls.html:41 #: intervention/templates/intervention/detail/includes/controls.html:46 -#: venv/lib/python3.7/site-packages/django/forms/formsets.py:391 msgid "Delete" msgstr "Löschen" @@ -1101,7 +1100,6 @@ msgstr "Fehlende Flächenmengen laut Zielzustand: " #: compensation/templates/compensation/report/eco_account/report.html:27 #: ema/templates/ema/detail/view.html:64 #: ema/templates/ema/report/report.html:27 -#: venv/lib/python3.7/site-packages/django/forms/widgets.py:710 msgid "Yes" msgstr "Ja" @@ -1115,7 +1113,6 @@ msgstr "Ja" #: compensation/templates/compensation/report/eco_account/report.html:29 #: ema/templates/ema/detail/view.html:66 #: ema/templates/ema/report/report.html:29 -#: venv/lib/python3.7/site-packages/django/forms/widgets.py:711 msgid "No" msgstr "Nein" @@ -1187,7 +1184,7 @@ msgstr "weitere Nutzer" #: compensation/templates/compensation/detail/eco_account/includes/controls.html:18 #: ema/templates/ema/detail/includes/controls.html:18 -#: intervention/forms/modals/share.py:63 +#: intervention/forms/modals/share.py:62 #: intervention/templates/intervention/detail/includes/controls.html:18 #: intervention/tests/unit/test_forms.py:150 msgid "Share" @@ -1290,18 +1287,18 @@ msgstr "" msgid "Responsible data" msgstr "Daten zu den verantwortlichen Stellen" -#: compensation/views/compensation/compensation.py:58 +#: compensation/views/compensation/compensation.py:57 msgid "Compensations - Overview" msgstr "Kompensationen - Übersicht" -#: compensation/views/compensation/compensation.py:181 +#: compensation/views/compensation/compensation.py:180 #: konova/utils/message_templates.py:40 msgid "Compensation {} edited" msgstr "Kompensation {} bearbeitet" -#: compensation/views/compensation/compensation.py:196 -#: compensation/views/eco_account/eco_account.py:173 ema/views/ema.py:231 -#: intervention/views/intervention.py:252 +#: compensation/views/compensation/compensation.py:195 +#: compensation/views/eco_account/eco_account.py:172 ema/views/ema.py:230 +#: intervention/views/intervention.py:251 msgid "Edit {}" msgstr "Bearbeite {}" @@ -1311,23 +1308,23 @@ msgstr "Bearbeite {}" msgid "Report {}" msgstr "Bericht {}" -#: compensation/views/eco_account/eco_account.py:53 +#: compensation/views/eco_account/eco_account.py:52 msgid "Eco-account - Overview" msgstr "Ökokonten - Übersicht" -#: compensation/views/eco_account/eco_account.py:86 +#: compensation/views/eco_account/eco_account.py:85 msgid "Eco-Account {} added" msgstr "Ökokonto {} hinzugefügt" -#: compensation/views/eco_account/eco_account.py:158 +#: compensation/views/eco_account/eco_account.py:157 msgid "Eco-Account {} edited" msgstr "Ökokonto {} bearbeitet" -#: compensation/views/eco_account/eco_account.py:287 +#: compensation/views/eco_account/eco_account.py:286 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:101 msgid "New EMA" msgstr "Neue EMA hinzufügen" @@ -1355,19 +1352,19 @@ msgstr "" msgid "Payment funded compensation" msgstr "Ersatzzahlungsmaßnahme" -#: ema/views/ema.py:53 +#: ema/views/ema.py:52 msgid "EMAs - Overview" msgstr "EMAs - Übersicht" -#: ema/views/ema.py:86 +#: ema/views/ema.py:85 msgid "EMA {} added" msgstr "EMA {} hinzugefügt" -#: ema/views/ema.py:216 +#: ema/views/ema.py:215 msgid "EMA {} edited" msgstr "EMA {} bearbeitet" -#: ema/views/ema.py:255 +#: ema/views/ema.py:254 msgid "EMA removed" msgstr "EMA entfernt" @@ -1431,7 +1428,7 @@ msgstr "Datum Bestandskraft bzw. Rechtskraft" #: intervention/forms/intervention.py:216 #: intervention/tests/unit/test_forms.py:36 -#: intervention/views/intervention.py:105 +#: intervention/views/intervention.py:104 msgid "New intervention" msgstr "Neuer Eingriff" @@ -1525,19 +1522,19 @@ msgstr "Widerspruch hinzufügen" msgid "Edit revocation" msgstr "Widerspruch bearbeiten" -#: intervention/forms/modals/share.py:21 +#: intervention/forms/modals/share.py:20 msgid "Share link" msgstr "Freigabelink" -#: intervention/forms/modals/share.py:23 +#: intervention/forms/modals/share.py:22 msgid "Send this link to users who you want to have writing access on the data" msgstr "Einzelne Nutzer erhalten über diesen Link Zugriff auf die Daten" -#: intervention/forms/modals/share.py:33 +#: intervention/forms/modals/share.py:32 msgid "Add team to share with" msgstr "Team hinzufügen" -#: intervention/forms/modals/share.py:35 +#: intervention/forms/modals/share.py:34 msgid "" "Multiple selection possible - You can only select teams which do not already " "have access." @@ -1545,11 +1542,11 @@ msgstr "" "Mehrfachauswahl möglich - Sie können nur Teams wählen, für die der Eintrag " "noch nicht freigegeben wurde." -#: intervention/forms/modals/share.py:47 +#: intervention/forms/modals/share.py:46 msgid "Add user to share with" msgstr "Nutzer einzeln hinzufügen" -#: intervention/forms/modals/share.py:49 +#: intervention/forms/modals/share.py:48 msgid "" "Multiple selection possible - You can only select users which do not already " "have access. Enter the full username." @@ -1557,7 +1554,7 @@ msgstr "" "Mehrfachauswahl möglich - Sie können nur Nutzer wählen, für die der Eintrag " "noch nicht freigegeben wurde. Geben Sie den ganzen Nutzernamen an." -#: intervention/forms/modals/share.py:64 +#: intervention/forms/modals/share.py:63 #: intervention/tests/unit/test_forms.py:151 msgid "Share settings for {}" msgstr "Freigabe Einstellungen für {}" @@ -1666,19 +1663,19 @@ msgstr "" msgid "Check performed" msgstr "Prüfung durchgeführt" -#: intervention/views/intervention.py:57 +#: intervention/views/intervention.py:56 msgid "Interventions - Overview" msgstr "Eingriffe - Übersicht" -#: intervention/views/intervention.py:90 +#: intervention/views/intervention.py:89 msgid "Intervention {} added" msgstr "Eingriff {} hinzugefügt" -#: intervention/views/intervention.py:235 +#: intervention/views/intervention.py:234 msgid "Intervention {} edited" msgstr "Eingriff {} bearbeitet" -#: intervention/views/intervention.py:277 +#: intervention/views/intervention.py:276 msgid "{} removed" msgstr "{} entfernt" @@ -1796,22 +1793,25 @@ msgstr "Speichern" msgid "Not editable" msgstr "Nicht editierbar" -#: konova/forms/geometry_form.py:31 konova/utils/quality.py:44 +#: konova/forms/geometry_form.py:32 konova/utils/quality.py:44 #: konova/utils/quality.py:46 templates/form/collapsable/form.html:45 msgid "Geometry" msgstr "Geometrie" -#: konova/forms/geometry_form.py:100 +#: konova/forms/geometry_form.py:101 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:154 +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?" #: konova/forms/modals/document_form.py:49 -#: venv/lib/python3.7/site-packages/django/db/models/fields/files.py:231 msgid "File" msgstr "Datei" @@ -1915,11 +1915,11 @@ msgstr "Kontrolle am" msgid "Other" msgstr "Sonstige" -#: konova/sub_settings/django_settings.py:160 +#: konova/sub_settings/django_settings.py:166 msgid "German" msgstr "" -#: konova/sub_settings/django_settings.py:161 +#: konova/sub_settings/django_settings.py:167 msgid "English" msgstr "" @@ -2005,39 +2005,39 @@ msgstr "In Zwischenablage kopiert" msgid "Search" msgstr "Suchen" -#: konova/utils/mailer.py:71 konova/utils/mailer.py:154 +#: konova/utils/mailer.py:69 konova/utils/mailer.py:146 msgid "{} - Shared access removed" msgstr "{} - Zugriff entzogen" -#: konova/utils/mailer.py:98 konova/utils/mailer.py:126 +#: konova/utils/mailer.py:94 konova/utils/mailer.py:120 msgid "{} - Shared access given" msgstr "{} - Zugriff freigegeben" -#: konova/utils/mailer.py:182 konova/utils/mailer.py:347 +#: konova/utils/mailer.py:172 konova/utils/mailer.py:325 msgid "{} - Shared data unrecorded" msgstr "{} - Freigegebene Daten entzeichnet" -#: konova/utils/mailer.py:210 konova/utils/mailer.py:320 +#: konova/utils/mailer.py:198 konova/utils/mailer.py:300 msgid "{} - Shared data recorded" msgstr "{} - Freigegebene Daten verzeichnet" -#: konova/utils/mailer.py:238 konova/utils/mailer.py:401 +#: konova/utils/mailer.py:224 konova/utils/mailer.py:375 msgid "{} - Shared data checked" msgstr "{} - Freigegebene Daten geprüft" -#: konova/utils/mailer.py:265 konova/utils/mailer.py:429 +#: konova/utils/mailer.py:249 konova/utils/mailer.py:401 msgid "{} - Deduction changed" msgstr "{} - Abbuchung geändert" -#: konova/utils/mailer.py:293 konova/utils/mailer.py:374 +#: konova/utils/mailer.py:275 konova/utils/mailer.py:350 msgid "{} - Shared data deleted" msgstr "{} - Freigegebene Daten gelöscht" -#: konova/utils/mailer.py:450 templates/email/api/verify_token.html:4 +#: konova/utils/mailer.py:422 templates/email/api/verify_token.html:4 msgid "Request for new API token" msgstr "Anfrage für neuen API Token" -#: konova/utils/mailer.py:475 +#: konova/utils/mailer.py:447 msgid "Resubmission - {}" msgstr "Wiedervorlage - {}" @@ -2046,7 +2046,6 @@ msgid "no further details" msgstr "keine weitere Angabe" #: konova/utils/message_templates.py:13 -#: venv/lib/python3.7/site-packages/django/forms/widgets.py:709 msgid "Unknown" msgstr "Unbekannt" @@ -2417,8 +2416,8 @@ msgstr "" #: templates/email/checking/shared_data_checked_team.html:30 #: templates/email/deleting/shared_data_deleted.html:30 #: templates/email/deleting/shared_data_deleted_team.html:30 -#: templates/email/other/deduction_changed.html:41 -#: templates/email/other/deduction_changed_team.html:41 +#: templates/email/other/deduction_changed.html:42 +#: templates/email/other/deduction_changed_team.html:42 #: templates/email/recording/shared_data_recorded.html:30 #: templates/email/recording/shared_data_recorded_team.html:30 #: templates/email/recording/shared_data_unrecorded.html:30 @@ -2515,8 +2514,8 @@ msgstr "der folgende Datensatz wurde soeben gelöscht " #: templates/email/deleting/shared_data_deleted.html:27 #: templates/email/deleting/shared_data_deleted_team.html:27 -#: templates/email/other/deduction_changed.html:38 -#: templates/email/other/deduction_changed_team.html:38 +#: templates/email/other/deduction_changed.html:39 +#: templates/email/other/deduction_changed_team.html:39 msgid "" "If this should not have been happened, please contact us. See the signature " "for details." @@ -2534,24 +2533,24 @@ msgstr "Abbuchung geändert" msgid "a deduction of this eco account has changed:" msgstr "eine Abbuchung des Ökokontos hat sich geändert:" -#: templates/email/other/deduction_changed.html:16 -#: templates/email/other/deduction_changed_team.html:16 +#: templates/email/other/deduction_changed.html:17 +#: templates/email/other/deduction_changed_team.html:17 msgid "Attribute" msgstr "Attribute" -#: templates/email/other/deduction_changed.html:17 -#: templates/email/other/deduction_changed_team.html:17 +#: templates/email/other/deduction_changed.html:18 +#: templates/email/other/deduction_changed_team.html:18 msgid "Old" msgstr "Alt" -#: templates/email/other/deduction_changed.html:18 -#: templates/email/other/deduction_changed_team.html:18 +#: templates/email/other/deduction_changed.html:19 +#: templates/email/other/deduction_changed_team.html:19 #: templates/generic_index.html:43 user/templates/user/team/index.html:22 msgid "New" msgstr "Neu" -#: templates/email/other/deduction_changed.html:21 -#: templates/email/other/deduction_changed_team.html:21 +#: templates/email/other/deduction_changed.html:22 +#: templates/email/other/deduction_changed_team.html:22 msgid "EcoAccount" msgstr "Ökokonto" @@ -2877,11 +2876,11 @@ msgstr "" "Mehrfachauswahl möglich - Sie können nur Nutzer wählen, die noch nicht " "Mitglieder dieses Teams sind. Geben Sie den ganzen Nutzernamen an." -#: user/forms/modals/team.py:56 +#: user/forms/modals/team.py:56 user/tests/unit/test_forms.py:31 msgid "Create new team" msgstr "Neues Team anlegen" -#: user/forms/modals/team.py:57 +#: user/forms/modals/team.py:57 user/tests/unit/test_forms.py:32 msgid "" "You will become the administrator for this group by default. You do not need " "to add yourself to the list of members." @@ -2910,10 +2909,11 @@ msgid "There must be at least one admin on this team." msgstr "Es muss mindestens einen Administrator für das Team geben." #: user/forms/modals/team.py:160 user/templates/user/team/index.html:60 +#: user/tests/unit/test_forms.py:88 msgid "Edit team" msgstr "Team bearbeiten" -#: user/forms/modals/team.py:187 +#: user/forms/modals/team.py:187 user/tests/unit/test_forms.py:165 msgid "" "ATTENTION!\n" "\n" @@ -2930,6 +2930,7 @@ msgstr "" "Sind Sie sicher, dass Sie dieses Team löschen möchten?" #: user/forms/modals/team.py:197 user/templates/user/team/index.html:56 +#: user/tests/unit/test_forms.py:198 msgid "Leave team" msgstr "Team verlassen" @@ -2961,7 +2962,7 @@ msgstr "Benachrichtigungen" msgid "Select the situations when you want to receive a notification" msgstr "Wann wollen Sie per E-Mail benachrichtigt werden?" -#: user/forms/user.py:38 +#: user/forms/user.py:38 user/tests/unit/test_forms.py:234 msgid "Edit notifications" msgstr "Benachrichtigungen bearbeiten" @@ -2969,11 +2970,11 @@ msgstr "Benachrichtigungen bearbeiten" msgid "Token" msgstr "" -#: user/forms/user.py:88 +#: user/forms/user.py:88 user/tests/unit/test_forms.py:260 msgid "Create new token" msgstr "Neuen Token generieren" -#: user/forms/user.py:89 +#: user/forms/user.py:89 user/tests/unit/test_forms.py:261 msgid "A new token needs to be validated by an administrator!" msgstr "Neue Tokens müssen durch Administratoren freigeschaltet werden!" @@ -3123,1504 +3124,169 @@ msgstr "Sie sind kein Mitglied dieses Teams" msgid "Left Team" msgstr "Team verlassen" -#: venv/lib/python3.7/site-packages/bootstrap4/components.py:17 -#: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/form_errors.html:3 -#: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/messages.html:4 -msgid "close" -msgstr "Schließen" +#~ msgid "close" +#~ msgstr "Schließen" -#: venv/lib/python3.7/site-packages/click/_termui_impl.py:496 -#, python-brace-format -msgid "{editor}: Editing failed" -msgstr "" +#~ msgid "Options" +#~ msgstr "Optionen" -#: venv/lib/python3.7/site-packages/click/_termui_impl.py:500 -#, python-brace-format -msgid "{editor}: Editing failed: {e}" -msgstr "" +#~ msgid "Commands" +#~ msgstr "Befehle" -#: venv/lib/python3.7/site-packages/click/_unicodefun.py:20 -msgid "" -"Click will abort further execution because Python was configured to use " -"ASCII as encoding for the environment. Consult https://click.palletsprojects." -"com/unicode-support/ for mitigation steps." -msgstr "" +#~ msgid "Missing command." +#~ msgstr "Befehl fehlt" -#: venv/lib/python3.7/site-packages/click/_unicodefun.py:56 -msgid "" -"Additional information: on this system no suitable UTF-8 locales were " -"discovered. This most likely requires resolving by reconfiguring the locale " -"system." -msgstr "" +#~ msgid "Missing argument" +#~ msgstr "Argument fehlt" -#: venv/lib/python3.7/site-packages/click/_unicodefun.py:65 -msgid "" -"This system supports the C.UTF-8 locale which is recommended. You might be " -"able to resolve your issue by exporting the following environment variables:" -msgstr "" +#~ msgid "Missing option" +#~ msgstr "Option fehlt" -#: venv/lib/python3.7/site-packages/click/_unicodefun.py:75 -#, python-brace-format -msgid "" -"This system lists some UTF-8 supporting locales that you can pick from. The " -"following suitable locales were discovered: {locales}" -msgstr "" +#~ msgid "Missing parameter" +#~ msgstr "Parameter fehlt" -#: venv/lib/python3.7/site-packages/click/_unicodefun.py:93 -msgid "" -"Click discovered that you exported a UTF-8 locale but the locale system " -"could not pick up from it because it does not exist. The exported locale is " -"{locale!r} but it is not supported." -msgstr "" +#~ msgid "Messages" +#~ msgstr "Nachrichten" -#: venv/lib/python3.7/site-packages/click/core.py:1095 -msgid "Aborted!" -msgstr "" +#~ msgid "This field is required." +#~ msgstr "Pflichtfeld" -#: venv/lib/python3.7/site-packages/click/core.py:1279 -#: venv/lib/python3.7/site-packages/click/decorators.py:434 -msgid "Show this message and exit." -msgstr "" +#~ msgid "Monday" +#~ msgstr "Montag" -#: venv/lib/python3.7/site-packages/click/core.py:1308 -#: venv/lib/python3.7/site-packages/click/core.py:1334 -#, python-brace-format -msgid "(Deprecated) {text}" -msgstr "" +#~ msgid "Tuesday" +#~ msgstr "Dienstag" -#: venv/lib/python3.7/site-packages/click/core.py:1351 -msgid "Options" -msgstr "Optionen" +#~ msgid "Wednesday" +#~ msgstr "Mittwoch" -#: venv/lib/python3.7/site-packages/click/core.py:1375 -#, python-brace-format -msgid "Got unexpected extra argument ({args})" -msgid_plural "Got unexpected extra arguments ({args})" -msgstr[0] "" -msgstr[1] "" +#~ msgid "Thursday" +#~ msgstr "Donnerstag" -#: venv/lib/python3.7/site-packages/click/core.py:1390 -msgid "DeprecationWarning: The command {name!r} is deprecated." -msgstr "" +#~ msgid "Friday" +#~ msgstr "Freitag" -#: venv/lib/python3.7/site-packages/click/core.py:1607 -msgid "Commands" -msgstr "Befehle" +#~ msgid "Saturday" +#~ msgstr "Samstag" -#: venv/lib/python3.7/site-packages/click/core.py:1639 -msgid "Missing command." -msgstr "Befehl fehlt" +#~ msgid "Sunday" +#~ msgstr "Sonntag" -#: venv/lib/python3.7/site-packages/click/core.py:1717 -msgid "No such command {name!r}." -msgstr "" +#~ msgid "Mon" +#~ msgstr "Mo" -#: venv/lib/python3.7/site-packages/click/core.py:2258 -msgid "Value must be an iterable." -msgstr "" +#~ msgid "Tue" +#~ msgstr "Di" -#: venv/lib/python3.7/site-packages/click/core.py:2278 -#, python-brace-format -msgid "Takes {nargs} values but 1 was given." -msgid_plural "Takes {nargs} values but {len} were given." -msgstr[0] "" -msgstr[1] "" +#~ msgid "Wed" +#~ msgstr "Mi" -#: venv/lib/python3.7/site-packages/click/core.py:2701 -#, python-brace-format -msgid "env var: {var}" -msgstr "" +#~ msgid "Thu" +#~ msgstr "Do" -#: venv/lib/python3.7/site-packages/click/core.py:2724 -msgid "(dynamic)" -msgstr "" +#~ msgid "Fri" +#~ msgstr "Fr" -#: venv/lib/python3.7/site-packages/click/core.py:2735 -#, python-brace-format -msgid "default: {default}" -msgstr "" +#~ msgid "Sat" +#~ msgstr "Sa" -#: venv/lib/python3.7/site-packages/click/core.py:2748 -msgid "required" -msgstr "" +#~ msgid "Sun" +#~ msgstr "So" -#: venv/lib/python3.7/site-packages/click/decorators.py:339 -#, python-format -msgid "%(prog)s, version %(version)s" -msgstr "" +#~ msgid "January" +#~ msgstr "Januar" -#: venv/lib/python3.7/site-packages/click/decorators.py:403 -msgid "Show the version and exit." -msgstr "" +#~ msgid "February" +#~ msgstr "Februar" -#: venv/lib/python3.7/site-packages/click/exceptions.py:43 -#: venv/lib/python3.7/site-packages/click/exceptions.py:79 -#, python-brace-format -msgid "Error: {message}" -msgstr "" +#~ msgid "March" +#~ msgstr "März" -#: venv/lib/python3.7/site-packages/click/exceptions.py:71 -#, python-brace-format -msgid "Try '{command} {option}' for help." -msgstr "" +#~ msgid "May" +#~ msgstr "Mai" -#: venv/lib/python3.7/site-packages/click/exceptions.py:120 -#, python-brace-format -msgid "Invalid value: {message}" -msgstr "" +#~ msgid "June" +#~ msgstr "Juni" -#: venv/lib/python3.7/site-packages/click/exceptions.py:122 -#, python-brace-format -msgid "Invalid value for {param_hint}: {message}" -msgstr "" +#~ msgid "July" +#~ msgstr "Juli" -#: venv/lib/python3.7/site-packages/click/exceptions.py:178 -msgid "Missing argument" -msgstr "Argument fehlt" +#~ msgid "October" +#~ msgstr "Oktober" -#: venv/lib/python3.7/site-packages/click/exceptions.py:180 -msgid "Missing option" -msgstr "Option fehlt" +#~ msgid "December" +#~ msgstr "Dezember" -#: venv/lib/python3.7/site-packages/click/exceptions.py:182 -msgid "Missing parameter" -msgstr "Parameter fehlt" +#~ msgid "mar" +#~ msgstr "mär" -#: venv/lib/python3.7/site-packages/click/exceptions.py:184 -#, python-brace-format -msgid "Missing {param_type}" -msgstr "" +#~ msgid "may" +#~ msgstr "mai" -#: venv/lib/python3.7/site-packages/click/exceptions.py:191 -#, python-brace-format -msgid "Missing parameter: {param_name}" -msgstr "" +#~ msgid "oct" +#~ msgstr "okt" -#: venv/lib/python3.7/site-packages/click/exceptions.py:211 -#, python-brace-format -msgid "No such option: {name}" -msgstr "" +#~ msgid "dec" +#~ msgstr "dez" -#: venv/lib/python3.7/site-packages/click/exceptions.py:223 -#, python-brace-format -msgid "Did you mean {possibility}?" -msgid_plural "(Possible options: {possibilities})" -msgstr[0] "" -msgstr[1] "" +#~ msgctxt "abbrev. month" +#~ msgid "March" +#~ msgstr "Mär" -#: venv/lib/python3.7/site-packages/click/exceptions.py:261 -msgid "unknown error" -msgstr "" +#~ msgctxt "abbrev. month" +#~ msgid "May" +#~ msgstr "Mai" -#: venv/lib/python3.7/site-packages/click/exceptions.py:268 -msgid "Could not open file {filename!r}: {message}" -msgstr "" +#~ msgctxt "abbrev. month" +#~ msgid "June" +#~ msgstr "Juni" -#: venv/lib/python3.7/site-packages/click/parser.py:231 -msgid "Argument {name!r} takes {nargs} values." -msgstr "" +#~ msgctxt "abbrev. month" +#~ msgid "July" +#~ msgstr "Juli" -#: venv/lib/python3.7/site-packages/click/parser.py:413 -msgid "Option {name!r} does not take a value." -msgstr "" +#~ msgctxt "abbrev. month" +#~ msgid "Oct." +#~ msgstr "Okt." -#: venv/lib/python3.7/site-packages/click/parser.py:474 -msgid "Option {name!r} requires an argument." -msgid_plural "Option {name!r} requires {nargs} arguments." -msgstr[0] "" -msgstr[1] "" +#~ msgctxt "abbrev. month" +#~ msgid "Dec." +#~ msgstr "Dez." -#: venv/lib/python3.7/site-packages/click/shell_completion.py:316 -msgid "Shell completion is not supported for Bash versions older than 4.4." -msgstr "" +#~ msgctxt "alt. month" +#~ msgid "January" +#~ msgstr "Januar" -#: venv/lib/python3.7/site-packages/click/shell_completion.py:322 -msgid "Couldn't detect Bash version, shell completion is not supported." -msgstr "" +#~ msgctxt "alt. month" +#~ msgid "February" +#~ msgstr "Februar" -#: venv/lib/python3.7/site-packages/click/termui.py:161 -msgid "Repeat for confirmation" -msgstr "" +#~ msgctxt "alt. month" +#~ msgid "March" +#~ msgstr "März" -#: venv/lib/python3.7/site-packages/click/termui.py:178 -msgid "Error: The value you entered was invalid." -msgstr "" +#~ msgctxt "alt. month" +#~ msgid "May" +#~ msgstr "Mai" -#: venv/lib/python3.7/site-packages/click/termui.py:180 -#, python-brace-format -msgid "Error: {e.message}" -msgstr "" +#~ msgctxt "alt. month" +#~ msgid "June" +#~ msgstr "Juni" -#: venv/lib/python3.7/site-packages/click/termui.py:191 -msgid "Error: The two entered values do not match." -msgstr "" +#~ msgctxt "alt. month" +#~ msgid "July" +#~ msgstr "Juli" -#: venv/lib/python3.7/site-packages/click/termui.py:247 -msgid "Error: invalid input" -msgstr "" +#~ msgctxt "alt. month" +#~ msgid "October" +#~ msgstr "Oktober" -#: venv/lib/python3.7/site-packages/click/termui.py:798 -msgid "Press any key to continue..." -msgstr "" +#~ msgctxt "alt. month" +#~ msgid "December" +#~ msgstr "Dezember" -#: venv/lib/python3.7/site-packages/click/types.py:258 -#, python-brace-format -msgid "" -"Choose from:\n" -"\t{choices}" -msgstr "" - -#: venv/lib/python3.7/site-packages/click/types.py:290 -msgid "{value!r} is not {choice}." -msgid_plural "{value!r} is not one of {choices}." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.7/site-packages/click/types.py:380 -msgid "{value!r} does not match the format {format}." -msgid_plural "{value!r} does not match the formats {formats}." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.7/site-packages/click/types.py:402 -msgid "{value!r} is not a valid {number_type}." -msgstr "" - -#: venv/lib/python3.7/site-packages/click/types.py:458 -#, python-brace-format -msgid "{value} is not in the range {range}." -msgstr "" - -#: venv/lib/python3.7/site-packages/click/types.py:599 -msgid "{value!r} is not a valid boolean." -msgstr "" - -#: venv/lib/python3.7/site-packages/click/types.py:623 -msgid "{value!r} is not a valid UUID." -msgstr "" - -#: venv/lib/python3.7/site-packages/click/types.py:801 -msgid "file" -msgstr "" - -#: venv/lib/python3.7/site-packages/click/types.py:803 -msgid "directory" -msgstr "" - -#: venv/lib/python3.7/site-packages/click/types.py:805 -msgid "path" -msgstr "" - -#: venv/lib/python3.7/site-packages/click/types.py:851 -msgid "{name} {filename!r} does not exist." -msgstr "" - -#: venv/lib/python3.7/site-packages/click/types.py:860 -msgid "{name} {filename!r} is a file." -msgstr "" - -#: venv/lib/python3.7/site-packages/click/types.py:868 -msgid "{name} {filename!r} is a directory." -msgstr "" - -#: venv/lib/python3.7/site-packages/click/types.py:876 -msgid "{name} {filename!r} is not writable." -msgstr "" - -#: venv/lib/python3.7/site-packages/click/types.py:884 -msgid "{name} {filename!r} is not readable." -msgstr "" - -#: venv/lib/python3.7/site-packages/click/types.py:951 -#, python-brace-format -msgid "{len_type} values are required, but {len_value} was given." -msgid_plural "{len_type} values are required, but {len_value} were given." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.7/site-packages/django/contrib/messages/apps.py:7 -msgid "Messages" -msgstr "Nachrichten" - -#: venv/lib/python3.7/site-packages/django/contrib/sitemaps/apps.py:7 -msgid "Site Maps" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/contrib/staticfiles/apps.py:9 -msgid "Static Files" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/contrib/syndication/apps.py:7 -msgid "Syndication" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/core/paginator.py:48 -msgid "That page number is not an integer" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/core/paginator.py:50 -msgid "That page number is less than 1" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/core/paginator.py:55 -msgid "That page contains no results" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/core/validators.py:20 -msgid "Enter a valid value." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/core/validators.py:91 -#: venv/lib/python3.7/site-packages/django/forms/fields.py:671 -msgid "Enter a valid URL." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/core/validators.py:145 -msgid "Enter a valid integer." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/core/validators.py:156 -msgid "Enter a valid email address." -msgstr "" - -#. Translators: "letters" means latin letters: a-z and A-Z. -#: venv/lib/python3.7/site-packages/django/core/validators.py:230 -msgid "" -"Enter a valid “slug” consisting of letters, numbers, underscores or hyphens." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/core/validators.py:237 -msgid "" -"Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or " -"hyphens." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/core/validators.py:246 -#: venv/lib/python3.7/site-packages/django/core/validators.py:266 -msgid "Enter a valid IPv4 address." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/core/validators.py:251 -#: venv/lib/python3.7/site-packages/django/core/validators.py:267 -msgid "Enter a valid IPv6 address." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/core/validators.py:261 -#: venv/lib/python3.7/site-packages/django/core/validators.py:265 -msgid "Enter a valid IPv4 or IPv6 address." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/core/validators.py:295 -msgid "Enter only digits separated by commas." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/core/validators.py:301 -#, python-format -msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/core/validators.py:334 -#, python-format -msgid "Ensure this value is less than or equal to %(limit_value)s." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/core/validators.py:343 -#, python-format -msgid "Ensure this value is greater than or equal to %(limit_value)s." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/core/validators.py:353 -#, python-format -msgid "" -"Ensure this value has at least %(limit_value)d character (it has " -"%(show_value)d)." -msgid_plural "" -"Ensure this value has at least %(limit_value)d characters (it has " -"%(show_value)d)." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.7/site-packages/django/core/validators.py:368 -#, python-format -msgid "" -"Ensure this value has at most %(limit_value)d character (it has " -"%(show_value)d)." -msgid_plural "" -"Ensure this value has at most %(limit_value)d characters (it has " -"%(show_value)d)." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.7/site-packages/django/core/validators.py:387 -#: venv/lib/python3.7/site-packages/django/forms/fields.py:292 -#: venv/lib/python3.7/site-packages/django/forms/fields.py:327 -msgid "Enter a number." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/core/validators.py:389 -#, python-format -msgid "Ensure that there are no more than %(max)s digit in total." -msgid_plural "Ensure that there are no more than %(max)s digits in total." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.7/site-packages/django/core/validators.py:394 -#, python-format -msgid "Ensure that there are no more than %(max)s decimal place." -msgid_plural "Ensure that there are no more than %(max)s decimal places." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.7/site-packages/django/core/validators.py:399 -#, python-format -msgid "" -"Ensure that there are no more than %(max)s digit before the decimal point." -msgid_plural "" -"Ensure that there are no more than %(max)s digits before the decimal point." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.7/site-packages/django/core/validators.py:461 -#, python-format -msgid "" -"File extension “%(extension)s” is not allowed. Allowed extensions are: " -"%(allowed_extensions)s." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/core/validators.py:513 -msgid "Null characters are not allowed." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/base.py:1190 -#: venv/lib/python3.7/site-packages/django/forms/models.py:760 -msgid "and" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/base.py:1192 -#, python-format -msgid "%(model_name)s with this %(field_labels)s already exists." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:100 -#, python-format -msgid "Value %(value)r is not a valid choice." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:101 -msgid "This field cannot be null." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:102 -msgid "This field cannot be blank." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:103 -#, python-format -msgid "%(model_name)s with this %(field_label)s already exists." -msgstr "" - -#. Translators: The 'lookup_type' is one of 'date', 'year' or 'month'. -#. Eg: "Title must be unique for pub_date year" -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:107 -#, python-format -msgid "" -"%(field_label)s must be unique for %(date_field_label)s %(lookup_type)s." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:126 -#, python-format -msgid "Field of type: %(field_type)s" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:939 -#, python-format -msgid "“%(value)s” value must be either True or False." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:940 -#, python-format -msgid "“%(value)s” value must be either True, False, or None." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:942 -msgid "Boolean (Either True or False)" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:983 -#, python-format -msgid "String (up to %(max_length)s)" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1047 -msgid "Comma-separated integers" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1096 -#, python-format -msgid "" -"“%(value)s” value has an invalid date format. It must be in YYYY-MM-DD " -"format." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1098 -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1241 -#, python-format -msgid "" -"“%(value)s” value has the correct format (YYYY-MM-DD) but it is an invalid " -"date." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1101 -msgid "Date (without time)" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1239 -#, python-format -msgid "" -"“%(value)s” value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[." -"uuuuuu]][TZ] format." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1243 -#, python-format -msgid "" -"“%(value)s” value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]]" -"[TZ]) but it is an invalid date/time." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1247 -msgid "Date (with time)" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1395 -#, python-format -msgid "“%(value)s” value must be a decimal number." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1397 -msgid "Decimal number" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1536 -#, python-format -msgid "" -"“%(value)s” value has an invalid format. It must be in [DD] [[HH:]MM:]ss[." -"uuuuuu] format." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1539 -msgid "Duration" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1589 -msgid "Email address" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1612 -msgid "File path" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1678 -#, python-format -msgid "“%(value)s” value must be a float." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1680 -msgid "Floating point number" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1718 -#, python-format -msgid "“%(value)s” value must be an integer." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1720 -msgid "Integer" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1803 -msgid "Big (8 byte) integer" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1819 -msgid "IPv4 address" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1850 -msgid "IP address" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1930 -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1931 -#, python-format -msgid "“%(value)s” value must be either None, True or False." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1933 -msgid "Boolean (Either True, False or None)" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1976 -msgid "Positive big integer" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:1989 -msgid "Positive integer" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:2002 -msgid "Positive small integer" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:2016 -#, python-format -msgid "Slug (up to %(max_length)s)" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:2048 -msgid "Small integer" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:2055 -msgid "Text" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:2083 -#, python-format -msgid "" -"“%(value)s” value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] " -"format." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:2085 -#, python-format -msgid "" -"“%(value)s” value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an " -"invalid time." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:2088 -msgid "Time" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:2214 -msgid "URL" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:2236 -msgid "Raw binary data" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:2301 -#, python-format -msgid "“%(value)s” is not a valid UUID." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/__init__.py:2303 -msgid "Universally unique identifier" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/files.py:379 -msgid "Image" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/json.py:18 -msgid "A JSON object" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/json.py:20 -msgid "Value must be valid JSON." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/related.py:790 -#, python-format -msgid "%(model)s instance with %(field)s %(value)r does not exist." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/related.py:792 -msgid "Foreign Key (type determined by related field)" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/related.py:1045 -msgid "One-to-one relationship" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/related.py:1099 -#, python-format -msgid "%(from)s-%(to)s relationship" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/related.py:1100 -#, python-format -msgid "%(from)s-%(to)s relationships" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/db/models/fields/related.py:1142 -msgid "Many-to-many relationship" -msgstr "" - -#. Translators: If found as last label character, these punctuation -#. characters will prevent the default label_suffix to be appended to the label -#: venv/lib/python3.7/site-packages/django/forms/boundfield.py:150 -msgid ":?.!" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/fields.py:54 -msgid "This field is required." -msgstr "Pflichtfeld" - -#: venv/lib/python3.7/site-packages/django/forms/fields.py:247 -msgid "Enter a whole number." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/fields.py:398 -#: venv/lib/python3.7/site-packages/django/forms/fields.py:1139 -msgid "Enter a valid date." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/fields.py:422 -#: venv/lib/python3.7/site-packages/django/forms/fields.py:1140 -msgid "Enter a valid time." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/fields.py:450 -msgid "Enter a valid date/time." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/fields.py:484 -msgid "Enter a valid duration." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/fields.py:485 -#, python-brace-format -msgid "The number of days must be between {min_days} and {max_days}." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/fields.py:545 -msgid "No file was submitted. Check the encoding type on the form." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/fields.py:546 -msgid "No file was submitted." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/fields.py:547 -msgid "The submitted file is empty." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/fields.py:549 -#, python-format -msgid "Ensure this filename has at most %(max)d character (it has %(length)d)." -msgid_plural "" -"Ensure this filename has at most %(max)d characters (it has %(length)d)." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.7/site-packages/django/forms/fields.py:552 -msgid "Please either submit a file or check the clear checkbox, not both." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/fields.py:613 -msgid "" -"Upload a valid image. The file you uploaded was either not an image or a " -"corrupted image." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/fields.py:775 -#: venv/lib/python3.7/site-packages/django/forms/fields.py:865 -#: venv/lib/python3.7/site-packages/django/forms/models.py:1296 -#, python-format -msgid "Select a valid choice. %(value)s is not one of the available choices." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/fields.py:866 -#: venv/lib/python3.7/site-packages/django/forms/fields.py:981 -#: venv/lib/python3.7/site-packages/django/forms/models.py:1295 -msgid "Enter a list of values." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/fields.py:982 -msgid "Enter a complete value." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/fields.py:1198 -msgid "Enter a valid UUID." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/fields.py:1228 -msgid "Enter a valid JSON." -msgstr "" - -#. Translators: This is the default suffix added to form field labels -#: venv/lib/python3.7/site-packages/django/forms/forms.py:78 -msgid ":" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/forms.py:205 -#, python-format -msgid "(Hidden field %(name)s) %(error)s" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/formsets.py:93 -msgid "ManagementForm data is missing or has been tampered with" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/formsets.py:345 -#, python-format -msgid "Please submit %d or fewer forms." -msgid_plural "Please submit %d or fewer forms." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.7/site-packages/django/forms/formsets.py:352 -#, python-format -msgid "Please submit %d or more forms." -msgid_plural "Please submit %d or more forms." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.7/site-packages/django/forms/formsets.py:379 -#: venv/lib/python3.7/site-packages/django/forms/formsets.py:386 -msgid "Order" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/models.py:755 -#, python-format -msgid "Please correct the duplicate data for %(field)s." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/models.py:759 -#, python-format -msgid "Please correct the duplicate data for %(field)s, which must be unique." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/models.py:765 -#, python-format -msgid "" -"Please correct the duplicate data for %(field_name)s which must be unique " -"for the %(lookup)s in %(date_field)s." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/models.py:774 -msgid "Please correct the duplicate values below." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/models.py:1096 -msgid "The inline value did not match the parent instance." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/models.py:1180 -msgid "Select a valid choice. That choice is not one of the available choices." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/models.py:1298 -#, python-format -msgid "“%(pk)s” is not a valid value." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/utils.py:167 -#, python-format -msgid "" -"%(datetime)s couldn’t be interpreted in time zone %(current_timezone)s; it " -"may be ambiguous or it may not exist." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/widgets.py:398 -msgid "Clear" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/widgets.py:399 -msgid "Currently" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/forms/widgets.py:400 -msgid "Change" -msgstr "" - -#. Translators: Please do not add spaces around commas. -#: venv/lib/python3.7/site-packages/django/template/defaultfilters.py:790 -msgid "yes,no,maybe" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/template/defaultfilters.py:819 -#: venv/lib/python3.7/site-packages/django/template/defaultfilters.py:836 -#, python-format -msgid "%(size)d byte" -msgid_plural "%(size)d bytes" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.7/site-packages/django/template/defaultfilters.py:838 -#, python-format -msgid "%s KB" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/template/defaultfilters.py:840 -#, python-format -msgid "%s MB" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/template/defaultfilters.py:842 -#, python-format -msgid "%s GB" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/template/defaultfilters.py:844 -#, python-format -msgid "%s TB" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/template/defaultfilters.py:846 -#, python-format -msgid "%s PB" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dateformat.py:65 -msgid "p.m." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dateformat.py:66 -msgid "a.m." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dateformat.py:71 -msgid "PM" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dateformat.py:72 -msgid "AM" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dateformat.py:149 -msgid "midnight" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dateformat.py:151 -msgid "noon" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:6 -msgid "Monday" -msgstr "Montag" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:6 -msgid "Tuesday" -msgstr "Dienstag" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:6 -msgid "Wednesday" -msgstr "Mittwoch" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:6 -msgid "Thursday" -msgstr "Donnerstag" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:6 -msgid "Friday" -msgstr "Freitag" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:7 -msgid "Saturday" -msgstr "Samstag" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:7 -msgid "Sunday" -msgstr "Sonntag" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:10 -msgid "Mon" -msgstr "Mo" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:10 -msgid "Tue" -msgstr "Di" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:10 -msgid "Wed" -msgstr "Mi" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:10 -msgid "Thu" -msgstr "Do" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:10 -msgid "Fri" -msgstr "Fr" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:11 -msgid "Sat" -msgstr "Sa" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:11 -msgid "Sun" -msgstr "So" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:14 -msgid "January" -msgstr "Januar" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:14 -msgid "February" -msgstr "Februar" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:14 -msgid "March" -msgstr "März" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:14 -msgid "April" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:14 -msgid "May" -msgstr "Mai" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:14 -msgid "June" -msgstr "Juni" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:15 -msgid "July" -msgstr "Juli" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:15 -msgid "August" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:15 -msgid "September" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:15 -msgid "October" -msgstr "Oktober" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:15 -msgid "November" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:16 -msgid "December" -msgstr "Dezember" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:19 -msgid "jan" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:19 -msgid "feb" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:19 -msgid "mar" -msgstr "mär" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:19 -msgid "apr" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:19 -msgid "may" -msgstr "mai" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:19 -msgid "jun" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:20 -msgid "jul" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:20 -msgid "aug" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:20 -msgid "sep" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:20 -msgid "oct" -msgstr "okt" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:20 -msgid "nov" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:20 -msgid "dec" -msgstr "dez" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:23 -msgctxt "abbrev. month" -msgid "Jan." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:24 -msgctxt "abbrev. month" -msgid "Feb." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:25 -msgctxt "abbrev. month" -msgid "March" -msgstr "Mär" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:26 -msgctxt "abbrev. month" -msgid "April" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:27 -msgctxt "abbrev. month" -msgid "May" -msgstr "Mai" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:28 -msgctxt "abbrev. month" -msgid "June" -msgstr "Juni" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:29 -msgctxt "abbrev. month" -msgid "July" -msgstr "Juli" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:30 -msgctxt "abbrev. month" -msgid "Aug." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:31 -msgctxt "abbrev. month" -msgid "Sept." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:32 -msgctxt "abbrev. month" -msgid "Oct." -msgstr "Okt." - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:33 -msgctxt "abbrev. month" -msgid "Nov." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:34 -msgctxt "abbrev. month" -msgid "Dec." -msgstr "Dez." - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:37 -msgctxt "alt. month" -msgid "January" -msgstr "Januar" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:38 -msgctxt "alt. month" -msgid "February" -msgstr "Februar" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:39 -msgctxt "alt. month" -msgid "March" -msgstr "März" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:40 -msgctxt "alt. month" -msgid "April" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:41 -msgctxt "alt. month" -msgid "May" -msgstr "Mai" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:42 -msgctxt "alt. month" -msgid "June" -msgstr "Juni" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:43 -msgctxt "alt. month" -msgid "July" -msgstr "Juli" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:44 -msgctxt "alt. month" -msgid "August" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:45 -msgctxt "alt. month" -msgid "September" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:46 -msgctxt "alt. month" -msgid "October" -msgstr "Oktober" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:47 -msgctxt "alt. month" -msgid "November" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/dates.py:48 -msgctxt "alt. month" -msgid "December" -msgstr "Dezember" - -#: venv/lib/python3.7/site-packages/django/utils/ipv6.py:8 -msgid "This is not a valid IPv6 address." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/text.py:70 -#, python-format -msgctxt "String to return when truncating text" -msgid "%(truncated_text)s…" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/text.py:236 -msgid "or" -msgstr "oder" - -#. Translators: This string is used as a separator between list elements -#: venv/lib/python3.7/site-packages/django/utils/text.py:255 -#: venv/lib/python3.7/site-packages/django/utils/timesince.py:83 -msgid ", " -msgstr "" - -#: venv/lib/python3.7/site-packages/django/utils/timesince.py:9 -#, python-format -msgid "%d year" -msgid_plural "%d years" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.7/site-packages/django/utils/timesince.py:10 -#, python-format -msgid "%d month" -msgid_plural "%d months" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.7/site-packages/django/utils/timesince.py:11 -#, python-format -msgid "%d week" -msgid_plural "%d weeks" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.7/site-packages/django/utils/timesince.py:12 -#, python-format -msgid "%d day" -msgid_plural "%d days" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.7/site-packages/django/utils/timesince.py:13 -#, python-format -msgid "%d hour" -msgid_plural "%d hours" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.7/site-packages/django/utils/timesince.py:14 -#, python-format -msgid "%d minute" -msgid_plural "%d minutes" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.7/site-packages/django/views/csrf.py:110 -msgid "Forbidden" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/csrf.py:111 -msgid "CSRF verification failed. Request aborted." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/csrf.py:115 -msgid "" -"You are seeing this message because this HTTPS site requires a “Referer " -"header” to be sent by your Web browser, but none was sent. This header is " -"required for security reasons, to ensure that your browser is not being " -"hijacked by third parties." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/csrf.py:120 -msgid "" -"If you have configured your browser to disable “Referer” headers, please re-" -"enable them, at least for this site, or for HTTPS connections, or for “same-" -"origin” requests." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/csrf.py:124 -msgid "" -"If you are using the tag or " -"including the “Referrer-Policy: no-referrer” header, please remove them. The " -"CSRF protection requires the “Referer” header to do strict referer checking. " -"If you’re concerned about privacy, use alternatives like for links to third-party sites." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/csrf.py:132 -msgid "" -"You are seeing this message because this site requires a CSRF cookie when " -"submitting forms. This cookie is required for security reasons, to ensure " -"that your browser is not being hijacked by third parties." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/csrf.py:137 -msgid "" -"If you have configured your browser to disable cookies, please re-enable " -"them, at least for this site, or for “same-origin” requests." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/csrf.py:142 -msgid "More information is available with DEBUG=True." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/generic/dates.py:41 -msgid "No year specified" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/generic/dates.py:61 -#: venv/lib/python3.7/site-packages/django/views/generic/dates.py:111 -#: venv/lib/python3.7/site-packages/django/views/generic/dates.py:208 -msgid "Date out of range" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/generic/dates.py:90 -msgid "No month specified" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/generic/dates.py:142 -msgid "No day specified" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/generic/dates.py:188 -msgid "No week specified" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/generic/dates.py:338 -#: venv/lib/python3.7/site-packages/django/views/generic/dates.py:367 -#, python-format -msgid "No %(verbose_name_plural)s available" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/generic/dates.py:589 -#, python-format -msgid "" -"Future %(verbose_name_plural)s not available because %(class_name)s." -"allow_future is False." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/generic/dates.py:623 -#, python-format -msgid "Invalid date string “%(datestr)s” given format “%(format)s”" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/generic/detail.py:54 -#, python-format -msgid "No %(verbose_name)s found matching the query" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/generic/list.py:67 -msgid "Page is not “last”, nor can it be converted to an int." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/generic/list.py:72 -#, python-format -msgid "Invalid page (%(page_number)s): %(message)s" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/generic/list.py:154 -#, python-format -msgid "Empty list and “%(class_name)s.allow_empty” is False." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/static.py:40 -msgid "Directory indexes are not allowed here." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/static.py:42 -#, python-format -msgid "“%(path)s” does not exist" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/static.py:80 -#, python-format -msgid "Index of %(directory)s" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/templates/default_urlconf.html:7 -msgid "Django: the Web framework for perfectionists with deadlines." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/templates/default_urlconf.html:346 -#, python-format -msgid "" -"View release notes for Django %(version)s" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/templates/default_urlconf.html:368 -msgid "The install worked successfully! Congratulations!" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/templates/default_urlconf.html:369 -#, python-format -msgid "" -"You are seeing this page because DEBUG=True is in your settings file and you have not configured any " -"URLs." -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/templates/default_urlconf.html:384 -msgid "Django Documentation" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/templates/default_urlconf.html:385 -msgid "Topics, references, & how-to’s" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/templates/default_urlconf.html:396 -msgid "Tutorial: A Polling App" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/templates/default_urlconf.html:397 -msgid "Get started with Django" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/templates/default_urlconf.html:408 -msgid "Django Community" -msgstr "" - -#: venv/lib/python3.7/site-packages/django/views/templates/default_urlconf.html:409 -msgid "Connect, get help, or contribute" -msgstr "" - -#: venv/lib/python3.7/site-packages/fontawesome_5/fields.py:16 -msgid "A fontawesome icon field" -msgstr "" - -#: venv/lib/python3.7/site-packages/kombu/transport/qpid.py:1310 -#, python-format -msgid "Attempting to connect to qpid with SASL mechanism %s" -msgstr "" - -#: venv/lib/python3.7/site-packages/kombu/transport/qpid.py:1315 -#, python-format -msgid "Connected to qpid with SASL mechanism %s" -msgstr "" - -#: venv/lib/python3.7/site-packages/kombu/transport/qpid.py:1333 -#, python-format -msgid "Unable to connect to qpid with SASL mechanism %s" -msgstr "" +#~ msgid "or" +#~ msgstr "oder" #~ msgid "" #~ "Deductable surface can not be larger than existing surfaces in after " diff --git a/requirements.txt b/requirements.txt index 52c2c9af..3ae99023 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,7 +34,6 @@ pika==1.3.2 prompt-toolkit==3.0.43 psycopg==3.1.16 psycopg-binary==3.1.16 -psycopg2-binary==2.9.9 pyparsing==3.1.1 pypng==0.20220715.0 pyproj==3.6.1 From 523e338b1babfc14440aafa2f901c4c7f83b967b Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 16 Jan 2024 07:57:29 +0100 Subject: [PATCH 2/8] # WIP: Performance boost parcel calculation * improves handling of parcel calculation (speed up by ~30%) * ToDo: Clean up code --- konova/models/geometry.py | 199 ++++++++++++++++++++----- konova/settings.py | 4 +- konova/sub_settings/django_settings.py | 1 + konova/tasks.py | 15 +- konova/views/geometry.py | 35 +++-- 5 files changed, 193 insertions(+), 61 deletions(-) diff --git a/konova/models/geometry.py b/konova/models/geometry.py index 0d348c39..b08def6e 100644 --- a/konova/models/geometry.py +++ b/konova/models/geometry.py @@ -6,6 +6,7 @@ Created on: 15.11.21 """ import json +from time import process_time from django.contrib.gis.db.models import MultiPolygonField from django.db import models, transaction @@ -140,7 +141,10 @@ class Geometry(BaseResource): return self._set_parcel_update_start_time() - self._perform_parcel_update() + + t1 = process_time() + self._perform_parcel_update_fast() + print(f"Parcel processing: {process_time() - t1}") self._set_parcel_update_end_time() def _perform_parcel_update(self): @@ -155,61 +159,151 @@ class Geometry(BaseResource): fetched_parcels = parcel_fetcher.get_parcels() _now = timezone.now() underlying_parcels = [] + i = 0 + len_fetched_parcels = len(fetched_parcels) + print("Process fetched parcels:") for result in fetched_parcels: - with transaction.atomic(): - # There could be parcels which include the word 'Flur', - # which needs to be deleted and just keep the numerical values - ## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE! - flr_val = result["flur"].replace("Flur ", "") + # There could be parcels which include the word 'Flur', + # which needs to be deleted and just keep the numerical values + ## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE! + flr_val = result["flur"].replace("Flur ", "") + district = District.objects.get_or_create( + key=result["kreisschl"], + name=result["kreis"], + )[0] + municipal = Municipal.objects.get_or_create( + key=result["gmdschl"], + name=result["gemeinde"], + district=district, + )[0] + parcel_group = ParcelGroup.objects.get_or_create( + key=result["gemaschl"], + name=result["gemarkung"], + municipal=municipal, + )[0] + flrstck_nnr = result['flstnrnen'] + if not flrstck_nnr: + flrstck_nnr = None + flrstck_zhlr = result['flstnrzae'] + if not flrstck_zhlr: + flrstck_zhlr = None + parcel_obj = Parcel.objects.get_or_create( + district=district, + municipal=municipal, + parcel_group=parcel_group, + flr=flr_val, + flrstck_nnr=flrstck_nnr, + flrstck_zhlr=flrstck_zhlr, + )[0] + parcel_obj.district = district + parcel_obj.updated_on = _now + parcel_obj.save() + underlying_parcels.append(parcel_obj) + i += 1 + if i % 100 == 0: + print(f" {i}/{len_fetched_parcels}") + + # Update the linked parcels + #self.parcels.clear() + self.parcels.set(underlying_parcels) + + # Set the calculated_on intermediate field, so this related data will be found on lookups + #intersections_without_ts = self.parcelintersection_set.filter( + # parcel__in=self.parcels.all(), + # calculated_on__isnull=True, + #) + #for entry in intersections_without_ts: + # entry.calculated_on = _now + #ParcelIntersection.objects.bulk_update( + # intersections_without_ts, + # ["calculated_on"] + #) + + def _perform_parcel_update_fast(self): + """ + Performs the main logic of parcel updating. + """ + from konova.models import Parcel, District, Municipal, ParcelGroup + + parcel_fetcher = ParcelFetcher( + geometry=self + ) + fetched_parcels = parcel_fetcher.get_parcels() + _now = timezone.now() + underlying_parcels = [] + + i = 0 + len_fetched_parcels = len(fetched_parcels) + print("Process fetched parcels:") + + districts = {} + municipals = {} + parcel_groups = {} + + for result in fetched_parcels: + # There could be parcels which include the word 'Flur', + # which needs to be deleted and just keep the numerical values + ## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE! + flr_val = result["flur"].replace("Flur ", "") + + # Get district (cache in dict) + try: + district = districts["kreisschl"] + except KeyError: district = District.objects.get_or_create( key=result["kreisschl"], name=result["kreis"], )[0] + districts[district.key] = district + + # Get municipal (cache in dict) + try: + municipal = municipals["gmdschl"] + except KeyError: municipal = Municipal.objects.get_or_create( key=result["gmdschl"], name=result["gemeinde"], district=district, )[0] + municipals[municipal.key] = municipal + + # Get parcel group (cache in dict) + try: + parcel_group = parcel_groups["gemaschl"] + except KeyError: parcel_group = ParcelGroup.objects.get_or_create( key=result["gemaschl"], name=result["gemarkung"], municipal=municipal, )[0] - flrstck_nnr = result['flstnrnen'] - if not flrstck_nnr: - flrstck_nnr = None - flrstck_zhlr = result['flstnrzae'] - if not flrstck_zhlr: - flrstck_zhlr = None - parcel_obj = Parcel.objects.get_or_create( - district=district, - municipal=municipal, - parcel_group=parcel_group, - flr=flr_val, - flrstck_nnr=flrstck_nnr, - flrstck_zhlr=flrstck_zhlr, - )[0] - parcel_obj.district = district - parcel_obj.updated_on = _now - parcel_obj.save() + parcel_groups[parcel_group.key] = parcel_group + + # Preprocess parcel data + flrstck_nnr = result['flstnrnen'] + if not flrstck_nnr: + flrstck_nnr = None + flrstck_zhlr = result['flstnrzae'] + if not flrstck_zhlr: + flrstck_zhlr = None + + parcel_obj = Parcel.objects.get_or_create( + district=district, + municipal=municipal, + parcel_group=parcel_group, + flr=flr_val, + flrstck_nnr=flrstck_nnr, + flrstck_zhlr=flrstck_zhlr, + )[0] + parcel_obj.updated_on = _now + parcel_obj.save() underlying_parcels.append(parcel_obj) + i += 1 + if i % 100 == 0: + print(f" {i}/{len_fetched_parcels}") - # Update the linked parcels - self.parcels.clear() + # Update linked parcels self.parcels.set(underlying_parcels) - # Set the calculated_on intermediate field, so this related data will be found on lookups - intersections_without_ts = self.parcelintersection_set.filter( - parcel__in=self.parcels.all(), - calculated_on__isnull=True, - ) - for entry in intersections_without_ts: - entry.calculated_on = _now - ParcelIntersection.objects.bulk_update( - intersections_without_ts, - ["calculated_on"] - ) - @transaction.atomic def _set_parcel_update_start_time(self): """ @@ -233,9 +327,7 @@ class Geometry(BaseResource): Returns: parcels (QuerySet): The related parcels as queryset """ - parcels = self.parcels.filter( - parcelintersection__calculated_on__isnull=False, - ).prefetch_related( + parcels = self.parcels.prefetch_related( "district", "municipal", ).order_by( @@ -305,6 +397,33 @@ class Geometry(BaseResource): } return geojson + @property + def complexity_factor(self) -> float: + """ Calculates a factor to estimate the complexity of a Geometry + + 0 = very low complexity + 1 = very high complexity + + ASSUMPTION: + The envelope is the bounding box of a geometry. If the geometry's area is similar to the area of it's bounding + box, it is considered as rather simple, since it seems to be a closer shape like a simple box. + If the geometry has a very big bounding box, but the geometry's own area is rather small, + compared to the one of the bounding box, the complexity can be higher. + + Example: + geometry area similar to bounding box --> geometry / bounding_box ~ 1 + geometry area far smaller than bb --> geometry / bounding_box ~ 0 + + Result is being inverted for better understanding of 'low' and 'high' complexity. + + Returns: + complexity_factor (float): The estimated complexity + """ + geom_envelope = self.geom.envelope + diff = geom_envelope - self.geom + complexity_factor = 1 - self.geom.area / diff.area + return complexity_factor + class GeometryConflict(UuidModel): """ diff --git a/konova/settings.py b/konova/settings.py index 3e5c9ae7..e1e0e46b 100644 --- a/konova/settings.py +++ b/konova/settings.py @@ -49,5 +49,5 @@ ETS_GROUP = "Conservation office" # GEOMETRY ## Max number of allowed vertices. Geometries larger will be simplified until they reach this threshold GEOM_MAX_VERTICES = 10000 -## Max seconds to wait for a parcel calculation result before a new request will be started (default: 5 minutes) -GEOM_THRESHOLD_RECALCULATION_SECONDS = 300 +## Max seconds to wait for a parcel calculation result before a new request will be started (default: 30 minutes) +GEOM_THRESHOLD_RECALCULATION_SECONDS = 60 * 30 diff --git a/konova/sub_settings/django_settings.py b/konova/sub_settings/django_settings.py index 7eefa7e9..8962a813 100644 --- a/konova/sub_settings/django_settings.py +++ b/konova/sub_settings/django_settings.py @@ -135,6 +135,7 @@ DATABASES = { 'USER': 'postgres', 'HOST': '127.0.0.1', 'PORT': '5432', + 'CONN_MAX_AGE': 120, } } DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/konova/tasks.py b/konova/tasks.py index aa8a65ba..f333bb25 100644 --- a/konova/tasks.py +++ b/konova/tasks.py @@ -10,13 +10,14 @@ def celery_update_parcels(geometry_id: str, recheck: bool = True): from konova.models import Geometry, ParcelIntersection try: geom = Geometry.objects.get(id=geometry_id) - objs = geom.parcelintersection_set.all() - for obj in objs: - obj.calculated_on = None - ParcelIntersection.objects.bulk_update( - objs, - ["calculated_on"] - ) + geom.parcels.clear() + #objs = geom.parcelintersection_set.all() + #for obj in objs: + # obj.calculated_on = None + #ParcelIntersection.objects.bulk_update( + # objs, + # ["calculated_on"] + #) geom.update_parcels() except ObjectDoesNotExist: diff --git a/konova/views/geometry.py b/konova/views/geometry.py index d0ab642c..ead32b91 100644 --- a/konova/views/geometry.py +++ b/konova/views/geometry.py @@ -37,30 +37,38 @@ class GeomParcelsView(LoginRequiredMixin, View): # https://htmx.org/docs/#polling status_code = 286 template = "konova/includes/parcels/parcel_table_frame.html" + geom = get_object_or_404(Geometry, id=id) - parcels = geom.get_underlying_parcels() geos_geom = geom.geom or MultiPolygon(srid=DEFAULT_SRID_RLP) + geometry_exists = not geos_geom.empty and geos_geom.area > 0 + geom_parcel_update_started = geom.parcel_update_start is not None + geom_parcel_update_finished = geom.parcel_update_end is not None + + parcels = geom.get_underlying_parcels() + parcels_exist = len(parcels) > 0 waiting_too_long = self._check_waiting_too_long(geom) - geometry_exists = not geos_geom.empty and geos_geom.area > 0 parcels_are_currently_calculated = ( geometry_exists and - geom.parcel_update_start and - not geom.parcel_update_end + not parcels_exist and + geom_parcel_update_started and + not geom_parcel_update_finished ) - parcels_available = len(parcels) > 0 + + if not parcels_exist and waiting_too_long: + # Trigger calculation again - process may have failed in the background + celery_update_parcels.delay(geom.id) + parcels_are_currently_calculated = True if parcels_are_currently_calculated: # Parcels are being calculated right now. Change the status code, so polling stays active for fetching # results after the calculation status_code = 200 - if waiting_too_long: - # Trigger calculation again - celery_update_parcels.delay(geom.id) - - if parcels_available or not geometry_exists: + if parcels_exist or not geometry_exists: + # Default case: Parcels are calculated or there is no geometry at all + # (so there will be no parcels to expect) municipals = geom.get_underlying_municipals(parcels) rpp = 100 @@ -88,13 +96,16 @@ class GeomParcelsView(LoginRequiredMixin, View): Depending on the geometry's modified attribute """ + # Scale time to wait longer with increasing geometry complexity + complexity_factor = geom.complexity_factor + 1 + wait_for_seconds = int(GEOM_THRESHOLD_RECALCULATION_SECONDS * complexity_factor) try: pcs_diff = (timezone.now() - geom.parcel_update_start).seconds except TypeError: - pcs_diff = GEOM_THRESHOLD_RECALCULATION_SECONDS + pcs_diff = wait_for_seconds calculation_not_finished = geom.parcel_update_end is None - waiting_too_long = (pcs_diff >= GEOM_THRESHOLD_RECALCULATION_SECONDS) and calculation_not_finished + waiting_too_long = (pcs_diff >= wait_for_seconds) and calculation_not_finished return waiting_too_long From d639a4e530cf34db8fdf82b90dd227e77da273fc Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 17 Jan 2024 11:22:21 +0100 Subject: [PATCH 3/8] # Geom parcel performance improvement * refactors parcel calculating, resulting in 1.3-1.6x better performance * optimizes parcel fetching view --- konova/models/geometry.py | 153 +++++++++++++------------------------- konova/views/geometry.py | 11 ++- 2 files changed, 57 insertions(+), 107 deletions(-) diff --git a/konova/models/geometry.py b/konova/models/geometry.py index b08def6e..d87e557f 100644 --- a/konova/models/geometry.py +++ b/konova/models/geometry.py @@ -6,9 +6,9 @@ Created on: 15.11.21 """ import json -from time import process_time from django.contrib.gis.db.models import MultiPolygonField +from django.core.exceptions import ObjectDoesNotExist from django.db import models, transaction from django.utils import timezone @@ -141,85 +141,10 @@ class Geometry(BaseResource): return self._set_parcel_update_start_time() - - t1 = process_time() - self._perform_parcel_update_fast() - print(f"Parcel processing: {process_time() - t1}") + self._perform_parcel_update() self._set_parcel_update_end_time() def _perform_parcel_update(self): - """ - Performs the main logic of parcel updating. - """ - from konova.models import Parcel, District, ParcelIntersection, Municipal, ParcelGroup - - parcel_fetcher = ParcelFetcher( - geometry=self - ) - fetched_parcels = parcel_fetcher.get_parcels() - _now = timezone.now() - underlying_parcels = [] - i = 0 - len_fetched_parcels = len(fetched_parcels) - print("Process fetched parcels:") - for result in fetched_parcels: - # There could be parcels which include the word 'Flur', - # which needs to be deleted and just keep the numerical values - ## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE! - flr_val = result["flur"].replace("Flur ", "") - district = District.objects.get_or_create( - key=result["kreisschl"], - name=result["kreis"], - )[0] - municipal = Municipal.objects.get_or_create( - key=result["gmdschl"], - name=result["gemeinde"], - district=district, - )[0] - parcel_group = ParcelGroup.objects.get_or_create( - key=result["gemaschl"], - name=result["gemarkung"], - municipal=municipal, - )[0] - flrstck_nnr = result['flstnrnen'] - if not flrstck_nnr: - flrstck_nnr = None - flrstck_zhlr = result['flstnrzae'] - if not flrstck_zhlr: - flrstck_zhlr = None - parcel_obj = Parcel.objects.get_or_create( - district=district, - municipal=municipal, - parcel_group=parcel_group, - flr=flr_val, - flrstck_nnr=flrstck_nnr, - flrstck_zhlr=flrstck_zhlr, - )[0] - parcel_obj.district = district - parcel_obj.updated_on = _now - parcel_obj.save() - underlying_parcels.append(parcel_obj) - i += 1 - if i % 100 == 0: - print(f" {i}/{len_fetched_parcels}") - - # Update the linked parcels - #self.parcels.clear() - self.parcels.set(underlying_parcels) - - # Set the calculated_on intermediate field, so this related data will be found on lookups - #intersections_without_ts = self.parcelintersection_set.filter( - # parcel__in=self.parcels.all(), - # calculated_on__isnull=True, - #) - #for entry in intersections_without_ts: - # entry.calculated_on = _now - #ParcelIntersection.objects.bulk_update( - # intersections_without_ts, - # ["calculated_on"] - #) - - def _perform_parcel_update_fast(self): """ Performs the main logic of parcel updating. """ @@ -230,16 +155,13 @@ class Geometry(BaseResource): ) fetched_parcels = parcel_fetcher.get_parcels() _now = timezone.now() - underlying_parcels = [] - - i = 0 - len_fetched_parcels = len(fetched_parcels) - print("Process fetched parcels:") districts = {} municipals = {} parcel_groups = {} + parcels_to_update = [] + parcels_to_create = [] for result in fetched_parcels: # There could be parcels which include the word 'Flur', # which needs to be deleted and just keep the numerical values @@ -280,28 +202,57 @@ class Geometry(BaseResource): # Preprocess parcel data flrstck_nnr = result['flstnrnen'] - if not flrstck_nnr: - flrstck_nnr = None + match flrstck_nnr: + case "": + flrstck_nnr = None + flrstck_zhlr = result['flstnrzae'] - if not flrstck_zhlr: - flrstck_zhlr = None + match flrstck_zhlr: + case "": + flrstck_zhlr = None - parcel_obj = Parcel.objects.get_or_create( - district=district, - municipal=municipal, - parcel_group=parcel_group, - flr=flr_val, - flrstck_nnr=flrstck_nnr, - flrstck_zhlr=flrstck_zhlr, - )[0] - parcel_obj.updated_on = _now - parcel_obj.save() - underlying_parcels.append(parcel_obj) - i += 1 - if i % 100 == 0: - print(f" {i}/{len_fetched_parcels}") + try: + # Try to fetch parcel from db. If it already exists, just update timestamp. + parcel_obj = Parcel.objects.get( + district=district, + municipal=municipal, + parcel_group=parcel_group, + flr=flr_val, + flrstck_nnr=flrstck_nnr, + flrstck_zhlr=flrstck_zhlr, + ) + parcel_obj.updated_on = _now + parcels_to_update.append(parcel_obj) + except ObjectDoesNotExist: + # If not existing, create object but do not commit, yet + parcel_obj = Parcel( + district=district, + municipal=municipal, + parcel_group=parcel_group, + flr=flr_val, + flrstck_nnr=flrstck_nnr, + flrstck_zhlr=flrstck_zhlr, + updated_on=_now, + ) + parcels_to_create.append(parcel_obj) - # Update linked parcels + # Create new parcels + Parcel.objects.bulk_create( + parcels_to_create, + batch_size=500 + ) + # Update existing parcels + Parcel.objects.bulk_update( + parcels_to_update, + [ + "updated_on" + ], + batch_size=500 + ) + + # Update linking to geometry + parcel_ids = [x.id for x in parcels_to_update] + [x.id for x in parcels_to_create] + underlying_parcels = Parcel.objects.filter(id__in=parcel_ids) self.parcels.set(underlying_parcels) @transaction.atomic diff --git a/konova/views/geometry.py b/konova/views/geometry.py index ead32b91..0a58a3c6 100644 --- a/konova/views/geometry.py +++ b/konova/views/geometry.py @@ -45,18 +45,18 @@ class GeomParcelsView(LoginRequiredMixin, View): geom_parcel_update_finished = geom.parcel_update_end is not None parcels = geom.get_underlying_parcels() - parcels_exist = len(parcels) > 0 + parcels_are_available = len(parcels) > 0 waiting_too_long = self._check_waiting_too_long(geom) parcels_are_currently_calculated = ( geometry_exists and - not parcels_exist and + not parcels_are_available and geom_parcel_update_started and not geom_parcel_update_finished ) - if not parcels_exist and waiting_too_long: + if not parcels_are_available and waiting_too_long: # Trigger calculation again - process may have failed in the background celery_update_parcels.delay(geom.id) parcels_are_currently_calculated = True @@ -66,7 +66,7 @@ class GeomParcelsView(LoginRequiredMixin, View): # results after the calculation status_code = 200 - if parcels_exist or not geometry_exists: + if parcels_are_available or not geometry_exists: # Default case: Parcels are calculated or there is no geometry at all # (so there will be no parcels to expect) municipals = geom.get_underlying_municipals(parcels) @@ -104,8 +104,7 @@ class GeomParcelsView(LoginRequiredMixin, View): except TypeError: pcs_diff = wait_for_seconds - calculation_not_finished = geom.parcel_update_end is None - waiting_too_long = (pcs_diff >= wait_for_seconds) and calculation_not_finished + waiting_too_long = (pcs_diff >= wait_for_seconds) return waiting_too_long From 6945b1711759392b820398b308508951b9c05bec Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 8 Feb 2024 07:31:19 +0100 Subject: [PATCH 4/8] # Optimizations and fixes * drops identifier handling on all edit-forms (identifier editing has been disabled on the frontend for a while now) * updates test cases * updates form caption for checking and recording action (less intimidating) * optimizes district column width * fixes bug on frontend parcel fetching on detail view * adds extended tooltip for title column on index tables * retraslates 'Law' to 'Rechtsgrundlage' --- compensation/forms/compensation.py | 2 -- compensation/forms/eco_account.py | 2 -- compensation/models/compensation.py | 1 - .../tests/compensation/test_workflow.py | 14 +++++++--- .../tests/ecoaccount/test_workflow.py | 3 ++- ema/forms.py | 2 -- ema/tests/test_workflow.py | 3 ++- ema/tests/unit/test_forms.py | 2 +- intervention/forms/intervention.py | 2 -- intervention/forms/modals/check.py | 2 +- intervention/tables.py | 5 ++++ intervention/views/intervention.py | 2 +- konova/forms/modals/record_form.py | 2 +- konova/models/geometry.py | 3 +++ konova/tests/test_views.py | 8 +----- konova/tests/unit/test_forms.py | 2 +- konova/utils/tables.py | 4 +++ konova/views/geometry.py | 25 +++++++++--------- locale/de/LC_MESSAGES/django.mo | Bin 45416 -> 45363 bytes locale/de/LC_MESSAGES/django.po | 8 +++--- 20 files changed, 49 insertions(+), 43 deletions(-) diff --git a/compensation/forms/compensation.py b/compensation/forms/compensation.py index cc3fa57b..bb1532d6 100644 --- a/compensation/forms/compensation.py +++ b/compensation/forms/compensation.py @@ -213,7 +213,6 @@ class EditCompensationForm(NewCompensationForm): 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) intervention = self.cleaned_data.get("intervention", None) is_cef = self.cleaned_data.get("is_cef", None) @@ -221,7 +220,6 @@ class EditCompensationForm(NewCompensationForm): is_pik = self.cleaned_data.get("is_pik", None) comment = self.cleaned_data.get("comment", None) - self.instance.identifier = identifier self.instance.title = title self.instance.intervention = intervention self.instance.is_cef = is_cef diff --git a/compensation/forms/eco_account.py b/compensation/forms/eco_account.py index 0352383c..42443025 100644 --- a/compensation/forms/eco_account.py +++ b/compensation/forms/eco_account.py @@ -192,7 +192,6 @@ class EditEcoAccountForm(NewEcoAccountForm): def save(self, user: User, geom_form: SimpleGeomForm): with transaction.atomic(): # Fetch data from cleaned POST values - identifier = self.cleaned_data.get("identifier", None) title = self.cleaned_data.get("title", None) registration_date = self.cleaned_data.get("registration_date", None) handler_type = self.cleaned_data.get("handler_type", None) @@ -219,7 +218,6 @@ class EditEcoAccountForm(NewEcoAccountForm): self.instance.legal.save() # Update main oject data - self.instance.identifier = identifier self.instance.title = title self.instance.deductable_surface = surface self.instance.comment = comment diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py index eaf8ccbb..49accd46 100644 --- a/compensation/models/compensation.py +++ b/compensation/models/compensation.py @@ -315,7 +315,6 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin): def get_detail_url_absolute(self): return BASE_URL + self.get_detail_url() - def save(self, *args, **kwargs): if self.identifier is None or len(self.identifier) == 0: # Create new identifier is none was given diff --git a/compensation/tests/compensation/test_workflow.py b/compensation/tests/compensation/test_workflow.py index ee520a57..3f024483 100644 --- a/compensation/tests/compensation/test_workflow.py +++ b/compensation/tests/compensation/test_workflow.py @@ -125,10 +125,16 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase): self.compensation = self.fill_out_compensation(self.compensation) pre_edit_log_count = self.compensation.log.count() + self.assertTrue(self.compensation.is_shared_with(self.superuser)) + + old_identifier = self.compensation.identifier new_title = self.create_dummy_string() new_identifier = self.create_dummy_string() new_comment = self.create_dummy_string() - new_geometry = MultiPolygon(srid=4326) # Create an empty geometry + new_geometry = MultiPolygon( + self.compensation.geometry.geom.buffer(10), + srid=self.compensation.geometry.geom.srid + ) # Create a geometry which differs from the stored one geojson = self.create_geojson(new_geometry) check_on_elements = { @@ -151,19 +157,21 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase): check_on_elements = { self.compensation.title: new_title, - self.compensation.identifier: new_identifier, self.compensation.comment: new_comment, } for k, v in check_on_elements.items(): self.assertEqual(k, v) - self.assert_equal_geometries(self.compensation.geometry.geom, new_geometry) + # Expect identifier to not be editable + self.assertEqual(self.compensation.identifier, old_identifier, msg="Identifier was editable!") # Expect logs to be set self.assertEqual(pre_edit_log_count + 1, self.compensation.log.count()) self.assertEqual(self.compensation.log.first().action, UserAction.EDITED) + self.assert_equal_geometries(self.compensation.geometry.geom, new_geometry) + def test_checkability(self): """ This tests if the checkability of the compensation (which is defined by the linked intervention's checked diff --git a/compensation/tests/ecoaccount/test_workflow.py b/compensation/tests/ecoaccount/test_workflow.py index 3bfaffeb..85f7db54 100644 --- a/compensation/tests/ecoaccount/test_workflow.py +++ b/compensation/tests/ecoaccount/test_workflow.py @@ -82,6 +82,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase): url = reverse("compensation:acc:edit", args=(self.eco_account.id,)) pre_edit_log_count = self.eco_account.log.count() + old_identifier = self.eco_account.identifier new_title = self.create_dummy_string() new_identifier = self.create_dummy_string() new_comment = self.create_dummy_string() @@ -114,7 +115,6 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase): check_on_elements = { self.eco_account.title: new_title, - self.eco_account.identifier: new_identifier, self.eco_account.deductable_surface: test_deductable_surface, self.eco_account.deductable_rest: test_deductable_surface - deductions_surface, self.eco_account.comment: new_comment, @@ -123,6 +123,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase): for k, v in check_on_elements.items(): self.assertEqual(k, v) + self.assertEqual(self.eco_account.identifier, old_identifier) self.assert_equal_geometries(self.eco_account.geometry.geom, new_geometry) # Expect logs to be set diff --git a/ema/forms.py b/ema/forms.py index d6b77a4d..26bbc2db 100644 --- a/ema/forms.py +++ b/ema/forms.py @@ -133,7 +133,6 @@ class EditEmaForm(NewEmaForm): def save(self, user: User, geom_form: SimpleGeomForm): with transaction.atomic(): # Fetch data from cleaned POST values - identifier = self.cleaned_data.get("identifier", None) title = self.cleaned_data.get("title", None) handler_type = self.cleaned_data.get("handler_type", None) handler_detail = self.cleaned_data.get("handler_detail", None) @@ -154,7 +153,6 @@ class EditEmaForm(NewEmaForm): self.instance.responsible.save() # Update main oject data - self.instance.identifier = identifier self.instance.title = title self.instance.comment = comment self.instance.is_pik = is_pik diff --git a/ema/tests/test_workflow.py b/ema/tests/test_workflow.py index 3e5bd291..c6228112 100644 --- a/ema/tests/test_workflow.py +++ b/ema/tests/test_workflow.py @@ -80,6 +80,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase): self.ema = self.fill_out_ema(self.ema) pre_edit_log_count = self.ema.log.count() + old_identifier = self.ema.identifier new_title = self.create_dummy_string() new_identifier = self.create_dummy_string() new_comment = self.create_dummy_string() @@ -106,13 +107,13 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase): check_on_elements = { self.ema.title: new_title, - self.ema.identifier: new_identifier, self.ema.comment: new_comment, } for k, v in check_on_elements.items(): self.assertEqual(k, v) + self.assertEqual(self.ema.identifier, old_identifier) self.assert_equal_geometries(self.ema.geometry.geom, new_geometry) # Expect logs to be set diff --git a/ema/tests/unit/test_forms.py b/ema/tests/unit/test_forms.py index ff87b9fa..d66fd14b 100644 --- a/ema/tests/unit/test_forms.py +++ b/ema/tests/unit/test_forms.py @@ -130,7 +130,7 @@ class EditEmaFormTestCase(BaseTestCase): self.assertIsNotNone(obj.responsible.handler) self.assertEqual(obj.responsible.conservation_office, data["conservation_office"]) self.assertEqual(obj.responsible.conservation_file_number, data["conservation_file_number"]) - self.assertEqual(obj.identifier, data["identifier"]) + self.assertNotEqual(obj.identifier, data["identifier"], msg="Identifier editable via form!") self.assertEqual(obj.comment, data["comment"]) last_log = obj.log.first() diff --git a/intervention/forms/intervention.py b/intervention/forms/intervention.py index 72986add..629bfe27 100644 --- a/intervention/forms/intervention.py +++ b/intervention/forms/intervention.py @@ -345,7 +345,6 @@ class EditInterventionForm(NewInterventionForm): """ with transaction.atomic(): - identifier = self.cleaned_data.get("identifier", None) title = self.cleaned_data.get("title", None) process_type = self.cleaned_data.get("type", None) laws = self.cleaned_data.get("laws", None) @@ -379,7 +378,6 @@ class EditInterventionForm(NewInterventionForm): self.instance.log.add(user_action) - self.instance.identifier = identifier self.instance.title = title self.instance.comment = comment self.instance.modified = user_action diff --git a/intervention/forms/modals/check.py b/intervention/forms/modals/check.py index cc06e63b..61111340 100644 --- a/intervention/forms/modals/check.py +++ b/intervention/forms/modals/check.py @@ -33,7 +33,7 @@ class CheckModalForm(BaseModalForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.form_title = _("Run check") - self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name) + self.form_caption = _("The necessary control steps have been performed:").format(self.user.first_name, self.user.last_name) self.valid = False def _are_deductions_valid(self): diff --git a/intervention/tables.py b/intervention/tables.py index 39cae2a9..76d4018b 100644 --- a/intervention/tables.py +++ b/intervention/tables.py @@ -33,6 +33,11 @@ class InterventionTable(BaseTable, TableRenderMixin, TableOrderMixin): verbose_name=_("Parcel gmrkng"), orderable=False, accessor="geometry", + attrs={ + "th": { + "class": "w-25", + } + } ) c = tables.Column( verbose_name=_("Checked"), diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py index e04f6281..3371167b 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -39,7 +39,7 @@ def index_view(request: HttpRequest): """ template = "generic_index.html" - # Filtering by user access is performed in table filter inside of InterventionTableFilter class + # Filtering by user access is performed in table filter inside InterventionTableFilter class interventions = Intervention.objects.filter( deleted=None, # not deleted ).select_related( diff --git a/konova/forms/modals/record_form.py b/konova/forms/modals/record_form.py index 812b697a..b27111a3 100644 --- a/konova/forms/modals/record_form.py +++ b/konova/forms/modals/record_form.py @@ -27,7 +27,7 @@ class RecordModalForm(BaseModalForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.form_title = _("Record data") - self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name) + self.form_caption = _("The necessary control steps have been performed:").format(self.user.first_name, self.user.last_name) # Disable automatic w-100 setting for this type of modal form. Looks kinda strange self.fields["confirm"].widget.attrs["class"] = "" diff --git a/konova/models/geometry.py b/konova/models/geometry.py index d87e557f..938c8526 100644 --- a/konova/models/geometry.py +++ b/konova/models/geometry.py @@ -370,6 +370,9 @@ class Geometry(BaseResource): Returns: complexity_factor (float): The estimated complexity """ + if self.geom.empty: + return 0 + geom_envelope = self.geom.envelope diff = geom_envelope - self.geom complexity_factor = 1 - self.geom.area / diff.area diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py index c540e785..860b3616 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -146,7 +146,6 @@ class BaseTestCase(TestCase): geometry = Geometry.objects.create() # Finally create main object, holding the other objects intervention = Intervention.objects.create( - identifier="TEST", title="Test_title", responsible=responsibility_data, legal=legal_data, @@ -174,7 +173,6 @@ class BaseTestCase(TestCase): geometry = Geometry.objects.create() # Finally create main object, holding the other objects compensation = Compensation.objects.create( - identifier="TEST", title="Test_title", intervention=interv, created=action, @@ -200,10 +198,8 @@ class BaseTestCase(TestCase): responsible_data.handler = handler responsible_data.save() - identifier = EcoAccount().generate_new_identifier() # Finally create main object, holding the other objects eco_account = EcoAccount.objects.create( - identifier=identifier, title="Test_title", deductable_surface=500, legal=lega_data, @@ -230,7 +226,6 @@ class BaseTestCase(TestCase): responsible_data.save() # Finally create main object, holding the other objects ema = Ema.objects.create( - identifier="TEST", title="Test_title", responsible=responsible_data, created=action, @@ -474,7 +469,7 @@ class BaseTestCase(TestCase): eco_account.save() return eco_account - def assert_equal_geometries(self, geom1: MultiPolygon, geom2: MultiPolygon): + def assert_equal_geometries(self, geom1: MultiPolygon, geom2: MultiPolygon, tolerance = 0.001): """ Assert for geometries to be equal Transforms the geometries to matching srids before checking @@ -491,7 +486,6 @@ class BaseTestCase(TestCase): self.assertTrue(True) return - tolerance = 0.001 if geom1.srid != geom2.srid: # Due to prior possible transformation of any of these geometries, we need to make sure there exists a # transformation from one coordinate system into the other, which is valid diff --git a/konova/tests/unit/test_forms.py b/konova/tests/unit/test_forms.py index 3cbb4371..190a756e 100644 --- a/konova/tests/unit/test_forms.py +++ b/konova/tests/unit/test_forms.py @@ -152,7 +152,7 @@ class RecordModalFormTestCase(BaseTestCase): ) self.assertEqual(form.form_title, str(_("Record data"))) self.assertEqual(form.form_caption, str( - _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format( + _("The necessary control steps have been performed:").format( self.user.first_name, self.user.last_name ) diff --git a/konova/utils/tables.py b/konova/utils/tables.py index d64c7666..48785481 100644 --- a/konova/utils/tables.py +++ b/konova/utils/tables.py @@ -173,9 +173,13 @@ class TableRenderMixin: Returns: """ + value_orig = value max_length = 75 if len(value) > max_length: value = f"{value[:max_length]}..." + value = format_html( + f'
{value}
' + ) return value def render_d(self, value, record: GeoReferencedMixin): diff --git a/konova/views/geometry.py b/konova/views/geometry.py index 0a58a3c6..767762fd 100644 --- a/konova/views/geometry.py +++ b/konova/views/geometry.py @@ -33,9 +33,6 @@ class GeomParcelsView(LoginRequiredMixin, View): Returns: A rendered piece of HTML """ - # HTTP code 286 states that the HTMX should stop polling for updates - # https://htmx.org/docs/#polling - status_code = 286 template = "konova/includes/parcels/parcel_table_frame.html" geom = get_object_or_404(Geometry, id=id) @@ -49,22 +46,26 @@ class GeomParcelsView(LoginRequiredMixin, View): waiting_too_long = self._check_waiting_too_long(geom) - parcels_are_currently_calculated = ( - geometry_exists and - not parcels_are_available and - geom_parcel_update_started and - not geom_parcel_update_finished - ) - - if not parcels_are_available and waiting_too_long: - # Trigger calculation again - process may have failed in the background + if geometry_exists and not parcels_are_available and waiting_too_long: + # Trigger calculation again - process may have failed silently celery_update_parcels.delay(geom.id) parcels_are_currently_calculated = True + else: + parcels_are_currently_calculated = ( + geometry_exists and + not parcels_are_available and + geom_parcel_update_started and + not geom_parcel_update_finished + ) if parcels_are_currently_calculated: # Parcels are being calculated right now. Change the status code, so polling stays active for fetching # results after the calculation status_code = 200 + else: + # HTTP code 286 states that the HTMX should stop polling for updates + # https://htmx.org/docs/#polling + status_code = 286 if parcels_are_available or not geometry_exists: # Default case: Parcels are calculated or there is no geometry at all diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index ff48ee9b246e525b380b8e45bd5bad366e374a04..1db23bbc18df338c482d750c6dbcfbc6c146d82e 100644 GIT binary patch delta 9263 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 AQvd(} delta 9266 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 diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index c34cf2d9..cd34fbe4 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -321,7 +321,7 @@ msgstr "" #: intervention/templates/intervention/detail/view.html:39 #: intervention/templates/intervention/report/report.html:20 msgid "Law" -msgstr "Gesetz" +msgstr "Rechtsgrundlage" #: analysis/templates/analysis/reports/includes/old_data/amount.html:17 #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:33 @@ -1452,11 +1452,9 @@ msgstr "Prüfung vornehmen" #: intervention/forms/modals/check.py:36 konova/forms/modals/record_form.py:30 #: konova/tests/unit/test_forms.py:155 msgid "" -"I, {} {}, confirm that all necessary control steps have been performed by " -"myself." +"The necessary control steps have been performed:" msgstr "" -"Ich, {} {}, bestätige, dass die notwendigen Kontrollschritte durchgeführt " -"wurden:" +"Die notwendigen Kontrollschritte wurden durchgeführt:" #: intervention/forms/modals/deduction.py:33 msgid "Only recorded accounts can be selected for deductions" From 4f02bf1da2c569f2badf8e1ad94a66c14ace5fdd Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 16 Feb 2024 08:13:10 +0100 Subject: [PATCH 5/8] # Renaming * renames a method and fixes doc string --- konova/forms/geometry_form.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/konova/forms/geometry_form.py b/konova/forms/geometry_form.py index 33137cb6..75b6e97c 100644 --- a/konova/forms/geometry_form.py +++ b/konova/forms/geometry_form.py @@ -101,7 +101,7 @@ class SimpleGeomForm(BaseForm): is_valid &= False return is_valid - is_valid &= self.__is_size_valid(g) + is_valid &= self.__is_area_valid(g) polygon = Polygon.from_ewkt(g.ewkt) is_valid &= polygon.valid @@ -139,8 +139,8 @@ class SimpleGeomForm(BaseForm): return num_vertices <= GEOM_MAX_VERTICES - def __is_size_valid(self, geom: gdal.OGRGeometry): - """ Checks whether the number of vertices in the geometry is not too high + def __is_area_valid(self, geom: gdal.OGRGeometry): + """ Checks whether the area is at least > 1m² Returns: From 689a2d6acba1073b1929b469e0b16cbc457700de Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 16 Feb 2024 08:14:42 +0100 Subject: [PATCH 6/8] # Typo * fixes typo --- konova/management/commands/sanitize_db.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/konova/management/commands/sanitize_db.py b/konova/management/commands/sanitize_db.py index e78b0e66..e8ee1ce2 100644 --- a/konova/management/commands/sanitize_db.py +++ b/konova/management/commands/sanitize_db.py @@ -225,11 +225,11 @@ class Command(BaseKonovaCommand): all_geometries = Geometry.objects.all() kom_geometry_ids = self._get_all_geometry_ids(Compensation) - self._write_warning(f" EMA: {kom_geometry_ids.count()} attached geometries") + self._write_warning(f" KOM: {kom_geometry_ids.count()} attached geometries") eiv_geometry_ids = self._get_all_geometry_ids(Intervention) - self._write_warning(f" EMA: {eiv_geometry_ids.count()} attached geometries") + self._write_warning(f" EIV: {eiv_geometry_ids.count()} attached geometries") oek_geometry_ids = self._get_all_geometry_ids(EcoAccount) - self._write_warning(f" EMA: {oek_geometry_ids.count()} attached geometries") + self._write_warning(f" OEK: {oek_geometry_ids.count()} attached geometries") ema_geometry_ids = self._get_all_geometry_ids(Ema) self._write_warning(f" EMA: {ema_geometry_ids.count()} attached geometries") From 590a510880eff90de6c55b8549f68c0f843d777e Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 16 Feb 2024 08:23:14 +0100 Subject: [PATCH 7/8] # CONN_MAX_AGE * dropping conn_max_age due to problems with usage in gunicorn --- konova/sub_settings/django_settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/konova/sub_settings/django_settings.py b/konova/sub_settings/django_settings.py index 8962a813..7eefa7e9 100644 --- a/konova/sub_settings/django_settings.py +++ b/konova/sub_settings/django_settings.py @@ -135,7 +135,6 @@ DATABASES = { 'USER': 'postgres', 'HOST': '127.0.0.1', 'PORT': '5432', - 'CONN_MAX_AGE': 120, } } DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" From 69a8139601efdf32444746086850b1e69edd6ed6 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 16 Feb 2024 08:41:03 +0100 Subject: [PATCH 8/8] # Fixes * drops unused methods * fixes typos * updates comments * drops unused model attribute --- konova/admin.py | 2 +- ...6_remove_parcelintersection_calculated_on.py | 17 +++++++++++++++++ konova/models/geometry.py | 11 ----------- konova/models/object.py | 11 ----------- konova/models/parcel.py | 14 ++------------ konova/tasks.py | 9 +-------- konova/utils/schneider/fetcher.py | 2 +- 7 files changed, 22 insertions(+), 44 deletions(-) create mode 100644 konova/migrations/0016_remove_parcelintersection_calculated_on.py diff --git a/konova/admin.py b/konova/admin.py index 07d692d7..ff82ac2b 100644 --- a/konova/admin.py +++ b/konova/admin.py @@ -151,7 +151,7 @@ class ResubmissionAdmin(BaseResourceAdmin): # Outcommented for a cleaner admin backend on production -#admin.site.register(Geometry, GeometryAdmin) +admin.site.register(Geometry, GeometryAdmin) #admin.site.register(Parcel, ParcelAdmin) #admin.site.register(District, DistrictAdmin) #admin.site.register(Municipal, MunicipalAdmin) diff --git a/konova/migrations/0016_remove_parcelintersection_calculated_on.py b/konova/migrations/0016_remove_parcelintersection_calculated_on.py new file mode 100644 index 00000000..c4a0f92d --- /dev/null +++ b/konova/migrations/0016_remove_parcelintersection_calculated_on.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.1 on 2024-02-16 07:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('konova', '0015_geometry_parcel_calculation_end_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='parcelintersection', + name='calculated_on', + ), + ] diff --git a/konova/models/geometry.py b/konova/models/geometry.py index 938c8526..254ace44 100644 --- a/konova/models/geometry.py +++ b/konova/models/geometry.py @@ -303,17 +303,6 @@ class Geometry(BaseResource): municipals = Municipal.objects.filter(id__in=municipals).order_by("name") return municipals - def count_underlying_parcels(self): - """ Getter for number of underlying parcels - - Returns: - - """ - num_parcels = self.parcels.filter( - parcelintersection__calculated_on__isnull=False, - ).count() - return num_parcels - def as_feature_collection(self, srid=DEFAULT_SRID_RLP): """ Returns a FeatureCollection structure holding all polygons of the MultiPolygon as single features diff --git a/konova/models/object.py b/konova/models/object.py index e2acaebd..f1a41c97 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -672,17 +672,6 @@ class GeoReferencedMixin(models.Model): result = self.geometry.get_underlying_parcels() return result - def count_underlying_parcels(self): - """ Getter for number of underlying parcels - - Returns: - - """ - result = 0 - if self.geometry is not None: - result = self.geometry.count_underlying_parcels() - return result - def set_geometry_conflict_message(self, request: HttpRequest): if self.geometry is None: return request diff --git a/konova/models/parcel.py b/konova/models/parcel.py index cc91e006..7cf00448 100644 --- a/konova/models/parcel.py +++ b/konova/models/parcel.py @@ -160,19 +160,9 @@ class Parcel(UuidModel): class ParcelIntersection(UuidModel): - """ ParcelIntersection is an intermediary model, which is used to configure the + """ + ParcelIntersection is an intermediary model, which is used to add extras to the M2M relation between Parcel and Geometry. - - Based on uuids, we will not have (practically) any problems on outrunning primary keys - and extending the model with calculated_on timestamp, we can 'hide' entries while they - are being recalculated and keep track on the last time they have been calculated this - way. - - Please note: The calculated_on describes when the relation between the Parcel and the Geometry - has been established. The updated_on field of Parcel describes when this Parcel has been - changed the last time. - """ parcel = models.ForeignKey(Parcel, on_delete=models.CASCADE) geometry = models.ForeignKey("konova.Geometry", on_delete=models.CASCADE) - calculated_on = models.DateTimeField(auto_now_add=True, null=True, blank=True) diff --git a/konova/tasks.py b/konova/tasks.py index f333bb25..ea68cde0 100644 --- a/konova/tasks.py +++ b/konova/tasks.py @@ -11,15 +11,8 @@ def celery_update_parcels(geometry_id: str, recheck: bool = True): try: geom = Geometry.objects.get(id=geometry_id) geom.parcels.clear() - #objs = geom.parcelintersection_set.all() - #for obj in objs: - # obj.calculated_on = None - #ParcelIntersection.objects.bulk_update( - # objs, - # ["calculated_on"] - #) - geom.update_parcels() + except ObjectDoesNotExist: if recheck: sleep(5) diff --git a/konova/utils/schneider/fetcher.py b/konova/utils/schneider/fetcher.py index 9eefb612..87aa7517 100644 --- a/konova/utils/schneider/fetcher.py +++ b/konova/utils/schneider/fetcher.py @@ -31,7 +31,7 @@ class ParcelFetcher: buffer_threshold = 0.001 geom = geometry.geom.buffer(-buffer_threshold) if geom.area < buffer_threshold: - # Fallback for malicious geometries which are way too small but would disappear on buffering + # Fallback for malicious geometries which are way too small and would disappear on negative buffering geom = geometry.geom self.geojson = geom.ewkt self.results = []