From 93d1cb9330226db246af3022d833375e9d6e73a6 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 16 Dec 2021 12:21:31 +0100 Subject: [PATCH 1/9] #49 Parcels and Districts * introduces new models: Parcel and District --- konova/models/__init__.py | 1 + konova/models/geometry.py | 2 -- konova/models/parcel.py | 70 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 konova/models/parcel.py diff --git a/konova/models/__init__.py b/konova/models/__init__.py index c60ecaa9..c9156061 100644 --- a/konova/models/__init__.py +++ b/konova/models/__init__.py @@ -9,3 +9,4 @@ from .object import * from .deadline import * from .document import * from .geometry import * +from .parcel import * diff --git a/konova/models/geometry.py b/konova/models/geometry.py index 8c155732..db2f8203 100644 --- a/konova/models/geometry.py +++ b/konova/models/geometry.py @@ -7,7 +7,6 @@ Created on: 15.11.21 """ from django.contrib.gis.db.models import MultiPolygonField from django.db import models -from django.db.models import Q from konova.models import BaseResource, UuidModel @@ -69,7 +68,6 @@ class Geometry(BaseResource): resolved_conflicts = all_conflicted_by_conflicts.exclude(id__in=still_conflicting_conflicts) resolved_conflicts.delete() - def get_data_objects(self): """ Getter for all objects which are related to this geometry diff --git a/konova/models/parcel.py b/konova/models/parcel.py new file mode 100644 index 00000000..4165d2db --- /dev/null +++ b/konova/models/parcel.py @@ -0,0 +1,70 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 16.12.21 + +""" +from django.db import models + +from konova.models import UuidModel + + +class Parcel(UuidModel): + """ The Parcel model holds administrative data on the covered properties. + + Due to the unique but relevant naming of the administrative data, we have to use these namings as field + names in german. Any try to translate them to English result in strange or insufficient translations. + + All fields have to be CharFields as well, since there are e.g. Flurstücksnummer holding e.g. '123____' which + can not be realized using numerical fields. + + To avoid conflicts due to german Umlaute, the field names are shortened and vocals are dropped. + + """ + geometries = models.ManyToManyField("konova.Geometry", related_name="parcels", null=True, blank=True) + district = models.ForeignKey("konova.District", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels") + flrstck_nnr = models.CharField( + max_length=1000, + help_text="Flurstücksnenner" + ) + flrstck_zhlr = models.CharField( + max_length=1000, + help_text="Flurstückszähler" + ) + flr = models.CharField( + max_length=1000, + help_text="Flur" + ) + gmrkng = models.CharField( + max_length=1000, + help_text="Gemarkung" + ) + + def __str__(self): + return f"{self.gmrkng} | {self.flr} | {self.flrstck_nnr} | {self.flrstck_zhlr}" + + +class District(UuidModel): + """ The model District holds more coarse information, such as Kreis, Verbandsgemeinde and Gemeinde. + + There might be the case that a geometry lies on a hundred Parcel entries but only on one District entry. + Therefore a geometry can have a lot of relations to Parcel entries but only a few or only a single one to one + District. + + """ + gmnd = models.CharField( + max_length=1000, + help_text="Gemeinde" + ) + vg = models.CharField( + max_length=1000, + help_text="Verbandsgemeinde", + ) + krs = models.CharField( + max_length=1000, + help_text="Kreis" + ) + + def __str__(self): + return f"{self.krs} | {self.vg} | {self.gmnd}" From 46e237f0e2cd9538b64b1b09bb61ad9a50aabf1c Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 17 Dec 2021 17:30:12 +0100 Subject: [PATCH 2/9] #49 Parcels and Districts * fixes bug in GeometryConflict conflict checking * WIP: introduces new konova/utils/wfs/spatial holding SpatialWFSFetcher, which can be fed any geometry and it returns found features * WIP: adds tests for wfs fetching * updates requirements.txt --- konova/models/geometry.py | 12 ++++- konova/models/parcel.py | 1 + konova/tests/test_geometries.py | 28 +++++++--- konova/utils/wfs/spatial.py | 92 +++++++++++++++++++++++++++++++++ requirements.txt | 7 ++- 5 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 konova/utils/wfs/spatial.py diff --git a/konova/models/geometry.py b/konova/models/geometry.py index db2f8203..5954558f 100644 --- a/konova/models/geometry.py +++ b/konova/models/geometry.py @@ -21,6 +21,7 @@ class Geometry(BaseResource): def save(self, *args, **kwargs): super().save(*args, **kwargs) self.check_for_conflicts() + self.update_parcels() def check_for_conflicts(self): """ Checks for new geometry overlaps @@ -31,7 +32,7 @@ class Geometry(BaseResource): """ # If no geometry is given or important data is missing, we can not perform any checks - if self.geom is None or (self.created is None and self.modified is None): + if self.geom is None: return None self.recheck_existing_conflicts() @@ -43,7 +44,10 @@ class Geometry(BaseResource): ).distinct() for match in overlapping_geoms: - GeometryConflict.objects.get_or_create(conflicting_geometry=self, affected_geometry=match) + # Make sure this conflict is not already known but in a swapped constellation + conflict_exists_swapped = GeometryConflict.objects.filter(conflicting_geometry=match, affected_geometry=self).exists() + if not conflict_exists_swapped: + GeometryConflict.objects.get_or_create(conflicting_geometry=self, affected_geometry=match) def recheck_existing_conflicts(self): """ Rechecks GeometryConflict entries @@ -88,6 +92,10 @@ class Geometry(BaseResource): objs += set_objs return objs + def update_parcels(self): + # ToDo + pass + class GeometryConflict(UuidModel): """ diff --git a/konova/models/parcel.py b/konova/models/parcel.py index 4165d2db..ad855a21 100644 --- a/konova/models/parcel.py +++ b/konova/models/parcel.py @@ -40,6 +40,7 @@ class Parcel(UuidModel): max_length=1000, help_text="Gemarkung" ) + updated_on = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.gmrkng} | {self.flr} | {self.flrstck_nnr} | {self.flrstck_zhlr}" diff --git a/konova/tests/test_geometries.py b/konova/tests/test_geometries.py index a9a840f5..cf06e79c 100644 --- a/konova/tests/test_geometries.py +++ b/konova/tests/test_geometries.py @@ -9,7 +9,7 @@ from django.contrib.gis.db.models.functions import Translate from konova.models import Geometry, GeometryConflict from konova.tests.test_views import BaseTestCase -from user.models import UserActionLogEntry +from konova.utils.wfs.spatial import SpatialWFSFetcher class GeometryTestCase(BaseTestCase): @@ -17,14 +17,11 @@ class GeometryTestCase(BaseTestCase): def setUpTestData(cls): super().setUpTestData() geom = cls.create_dummy_geometry() - action = UserActionLogEntry.get_created_action(cls.superuser) cls.geom_1 = Geometry.objects.create( geom=geom, - created=action, ) cls.geom_2 = Geometry.objects.create( geom=geom, - created=action, ) def test_geometry_conflict(self): @@ -34,8 +31,9 @@ class GeometryTestCase(BaseTestCase): Returns: """ - self.geom_1.check_for_conflicts() - conflict = GeometryConflict.objects.all().first() + conflict = GeometryConflict.objects.all() + self.assertEqual(1, conflict.count()) + conflict = conflict.first() self.assertEqual(conflict.conflicting_geometry, self.geom_2) self.assertEqual(conflict.affected_geometry, self.geom_1) @@ -47,3 +45,21 @@ class GeometryTestCase(BaseTestCase): self.geom_1.check_for_conflicts() num_conflict = GeometryConflict.objects.all().count() self.assertEqual(0, num_conflict) + + def test_wfs_fetch(self): + """ Tests the fetching functionality of SpatialWFSFetcher + + +++ Test relies on the availability of the RLP Gemarkung WFS +++ + + Returns: + + """ + fetcher = SpatialWFSFetcher( + base_url="http://geo5.service24.rlp.de/wfs/verwaltungsgrenzen_rp.fcgi", + version="1.1.0", + geometry=self.geom_1 + ) + features = fetcher.get_features( + "vermkv:fluren_rlp" + ) + self.assertNotEqual(0, len(features), msg="Spatial wfs get feature did not work!") diff --git a/konova/utils/wfs/spatial.py b/konova/utils/wfs/spatial.py new file mode 100644 index 00000000..c763d530 --- /dev/null +++ b/konova/utils/wfs/spatial.py @@ -0,0 +1,92 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 17.12.21 + +""" +from abc import abstractmethod + +import requests +import xmltodict +from django.contrib.gis.db.models.functions import AsGML, Transform +from django.contrib.gis.geos import MultiPolygon +from owslib.wfs import WebFeatureService + +from konova.models import Geometry +from konova.settings import DEFAULT_SRID_RLP + + +class BaseWFSFetcher: + # base_url represents not the capabilities url but the parameter-free base url + base_url = "" + version = "" + wfs = None + + def __init__(self, base_url: str, version: str = "1.1.0", *args, **kwargs): + self.base_url = base_url + self.version = version + self.wfs = WebFeatureService( + url=base_url, + version=version, + ) + + @abstractmethod + def get_features(self, feature_identifier: str, filter: str): + raise NotImplementedError + + +class SpatialWFSFetcher(BaseWFSFetcher): + """ Fetches features from a parcel WFS + + """ + geometry = None + geometry_property_name = "" + + def __init__(self, geometry: MultiPolygon, geometry_property_name: str = "msGeometry", *args, **kwargs): + super().__init__(*args, **kwargs) + self.geometry = geometry + self.geometry_property_name = geometry_property_name + + def _create_geometry_filter(self, geometry_operation: str, filter_srid: str = None): + """ Creates an + + Args: + geometry_operation (): + filter_srid (): + + Returns: + + """ + if filter_srid is None: + filter_srid = DEFAULT_SRID_RLP + geom_gml = Geometry.objects.filter( + id=self.geometry.id + ).annotate( + transformed=Transform(srid=filter_srid, expression="geom") + ).annotate( + gml=AsGML('transformed') + ).first().gml + _filter = f"<{geometry_operation}>{self.geometry_property_name}{geom_gml}" + return _filter + + def get_features(self, typenames: str, geometry_operation: str = "Intersects", filter_srid: str = None, filter: str = None): + if filter is None: + filter = self._create_geometry_filter(geometry_operation, filter_srid) + response = requests.post( + url=f"{self.base_url}?request=GetFeature&service=WFS&version={self.version}&typenames={typenames}&count=10", + data={ + "filter": filter, + } + ) + content = response.content.decode("utf-8") + content = xmltodict.parse(content) + features = content.get( + "wfs:FeatureCollection", + {}, + ).get( + "gml:featureMember", + [], + ) + return features + diff --git a/requirements.txt b/requirements.txt index c29cb37f..620316ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,10 +14,14 @@ django-tables2==2.3.4 et-xmlfile==1.1.0 idna==2.10 importlib-metadata==2.1.1 -itsdangerous<1.0.0 +itsdangerous==0.24 openpyxl==3.0.9 +OWSLib==0.25.0 psycopg2-binary==2.9.1 +pyproj==3.2.1 +python-dateutil==2.8.2 pytz==2020.4 +PyYAML==6.0 qrcode==7.3.1 requests==2.25.0 six==1.15.0 @@ -25,4 +29,5 @@ soupsieve==2.2.1 sqlparse==0.4.1 urllib3==1.26.2 webservices==0.7 +xmltodict==0.12.0 zipp==3.4.1 From b5cf18ac7de4efc39dd588b2242f1ad3dfffb7e0 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 4 Jan 2022 13:03:21 +0100 Subject: [PATCH 3/9] #49 Parcels and Districts * refactors WFS fetching to proper POST handling * adds authentication support to WFS handling * reduces dummy geometry for tests to a small area to reduce test network traffic overhead --- konova/settings.py | 5 ++ konova/tests/test_geometries.py | 17 ++-- konova/tests/test_views.py | 2 +- konova/utils/wfs/spatial.py | 148 +++++++++++++++++++++++--------- 4 files changed, 124 insertions(+), 48 deletions(-) diff --git a/konova/settings.py b/konova/settings.py index 1355fbae..f24e7f8a 100644 --- a/konova/settings.py +++ b/konova/settings.py @@ -80,3 +80,8 @@ LANIS_ZOOM_LUT = { 1000: 30, 500: 31, } + +# Parcel WFS settings +PARCEL_WFS_BASE_URL = "https://www.geoportal.rlp.de/registry/wfs/519" +PARCEL_WFS_USER = "ksp" +PARCEL_WFS_PW = "CHANGE_ME" \ No newline at end of file diff --git a/konova/tests/test_geometries.py b/konova/tests/test_geometries.py index cf06e79c..da5e82d0 100644 --- a/konova/tests/test_geometries.py +++ b/konova/tests/test_geometries.py @@ -8,8 +8,9 @@ Created on: 15.12.21 from django.contrib.gis.db.models.functions import Translate from konova.models import Geometry, GeometryConflict +from konova.settings import PARCEL_WFS_USER, PARCEL_WFS_PW from konova.tests.test_views import BaseTestCase -from konova.utils.wfs.spatial import SpatialWFSFetcher +from konova.utils.wfs.spatial import ParcelWFSFetcher class GeometryTestCase(BaseTestCase): @@ -47,19 +48,21 @@ class GeometryTestCase(BaseTestCase): self.assertEqual(0, num_conflict) def test_wfs_fetch(self): - """ Tests the fetching functionality of SpatialWFSFetcher + """ Tests the fetching functionality of ParcelWFSFetcher +++ Test relies on the availability of the RLP Gemarkung WFS +++ Returns: """ - fetcher = SpatialWFSFetcher( - base_url="http://geo5.service24.rlp.de/wfs/verwaltungsgrenzen_rp.fcgi", - version="1.1.0", - geometry=self.geom_1 + fetcher = ParcelWFSFetcher( + base_url="https://www.geoportal.rlp.de/registry/wfs/519", + version="2.0.0", + geometry=self.geom_1, + auth_user=PARCEL_WFS_USER, + auth_pw=PARCEL_WFS_PW ) features = fetcher.get_features( - "vermkv:fluren_rlp" + "ave:Flurstueck", ) self.assertNotEqual(0, len(features), msg="Spatial wfs get feature did not work!") diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py index ae5d4524..72cbbea7 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -214,7 +214,7 @@ class BaseTestCase(TestCase): Returns: """ - polygon = Polygon.from_bbox((7.157593, 49.882247, 7.816772, 50.266521)) + polygon = Polygon.from_bbox((7.5971391556, 50.3600032354, 7.5993975756, 50.3612420894)) polygon.srid = 4326 polygon = polygon.transform(3857, clone=True) return MultiPolygon(polygon, srid=3857) # 3857 is the default srid used for MultiPolygonField in the form diff --git a/konova/utils/wfs/spatial.py b/konova/utils/wfs/spatial.py index c763d530..c53a6b7f 100644 --- a/konova/utils/wfs/spatial.py +++ b/konova/utils/wfs/spatial.py @@ -11,52 +11,68 @@ import requests import xmltodict from django.contrib.gis.db.models.functions import AsGML, Transform from django.contrib.gis.geos import MultiPolygon -from owslib.wfs import WebFeatureService +from requests.auth import HTTPDigestAuth from konova.models import Geometry from konova.settings import DEFAULT_SRID_RLP -class BaseWFSFetcher: - # base_url represents not the capabilities url but the parameter-free base url - base_url = "" - version = "" - wfs = None +class AbstractWFSFetcher: + """ Base class for fetching WFS data - def __init__(self, base_url: str, version: str = "1.1.0", *args, **kwargs): + """ + # base_url represents not the capabilities url but the parameter-free base url + base_url = None + version = None + wfs = 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.wfs = WebFeatureService( - url=base_url, - version=version, - ) + self.auth_pw = auth_pw + self.auth_user = auth_user + + 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): + def get_features(self, feature_identifier: str, filter_str: str): raise NotImplementedError -class SpatialWFSFetcher(BaseWFSFetcher): - """ Fetches features from a parcel WFS +class ParcelWFSFetcher(AbstractWFSFetcher): + """ Fetches features from a special parcel WFS """ geometry = None - geometry_property_name = "" + geometry_property_name = None + count = 100 def __init__(self, geometry: MultiPolygon, geometry_property_name: str = "msGeometry", *args, **kwargs): super().__init__(*args, **kwargs) self.geometry = geometry self.geometry_property_name = geometry_property_name - def _create_geometry_filter(self, geometry_operation: str, filter_srid: str = None): - """ Creates an + def _create_spatial_filter(self, + geometry_operation: str, + filter_srid: str = None): + """ Creates a xml spatial filter according to the WFS filter specification Args: - geometry_operation (): - filter_srid (): + geometry_operation (str): One of the WFS supported spatial filter operations (according to capabilities) + filter_srid (str): Used to transform the geometry into the spatial reference system identified by this srid Returns: - + spatial_filter (str): The spatial filter element """ if filter_srid is None: filter_srid = DEFAULT_SRID_RLP @@ -67,26 +83,78 @@ class SpatialWFSFetcher(BaseWFSFetcher): ).annotate( gml=AsGML('transformed') ).first().gml - _filter = f"<{geometry_operation}>{self.geometry_property_name}{geom_gml}" + spatial_filter = f"<{geometry_operation}>{self.geometry_property_name}{geom_gml}" + return spatial_filter + + def _create_post_data(self, + geometry_operation: str, + filter_srid: str = None, + 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) + filter_srid (str): Used to transform the geometry into the spatial reference system identified by this srid + + Returns: + _filter (str): A proper xml WFS filter + """ + start_index = str(start_index) + spatial_filter = self._create_spatial_filter( + geometry_operation, + filter_srid + ) + _filter = f'{spatial_filter}' return _filter - def get_features(self, typenames: str, geometry_operation: str = "Intersects", filter_srid: str = None, filter: str = None): - if filter is None: - filter = self._create_geometry_filter(geometry_operation, filter_srid) - response = requests.post( - url=f"{self.base_url}?request=GetFeature&service=WFS&version={self.version}&typenames={typenames}&count=10", - data={ - "filter": filter, - } - ) - content = response.content.decode("utf-8") - content = xmltodict.parse(content) - features = content.get( - "wfs:FeatureCollection", - {}, - ).get( - "gml:featureMember", - [], - ) - return features + def get_features(self, + typenames: str, + spatial_operator: str = "Intersects", + filter_srid: str = None, + start_index: int = 0, + ): + """ 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 + + Returns: + features (list): A list of returned features + """ + features = [] + while start_index is not None: + post_body = self._create_post_data( + spatial_operator, + filter_srid, + typenames, + start_index + ) + response = requests.post( + url=self.base_url, + data=post_body, + auth=self.auth_digest_obj + ) + + content = response.content.decode("utf-8") + content = xmltodict.parse(content) + collection = content.get( + "wfs:FeatureCollection", + {}, + ) + features += collection.get( + "wfs:member", + [], + ) + if collection.get("@next", None) is not None: + start_index += self.count + else: + start_index = None + + return features From 632fb0f48a2608dee1125f7350b814c2a1726ce4 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 4 Jan 2022 15:59:53 +0100 Subject: [PATCH 4/9] #49 Calculation implementation * implements update routine for Geometry model * reorganizes fields of Parcel and District * adds tests * simplifies usage of ParcelWFSFetcher --- konova/models/geometry.py | 37 +++++++++++++++++++++++++++--- konova/models/parcel.py | 40 ++++++++++++++++++++------------- konova/tests/test_geometries.py | 25 +++++++++++++++------ konova/tests/test_views.py | 2 +- konova/utils/wfs/spatial.py | 26 ++++++++++++++------- 5 files changed, 95 insertions(+), 35 deletions(-) diff --git a/konova/models/geometry.py b/konova/models/geometry.py index 5954558f..e9d8c61c 100644 --- a/konova/models/geometry.py +++ b/konova/models/geometry.py @@ -7,8 +7,10 @@ Created on: 15.11.21 """ from django.contrib.gis.db.models import MultiPolygonField from django.db import models +from django.utils import timezone from konova.models import BaseResource, UuidModel +from konova.utils.wfs.spatial import ParcelWFSFetcher class Geometry(BaseResource): @@ -21,7 +23,6 @@ class Geometry(BaseResource): def save(self, *args, **kwargs): super().save(*args, **kwargs) self.check_for_conflicts() - self.update_parcels() def check_for_conflicts(self): """ Checks for new geometry overlaps @@ -93,8 +94,38 @@ class Geometry(BaseResource): return objs def update_parcels(self): - # ToDo - pass + """ Updates underlying parcel information + + Returns: + + """ + from konova.models import Parcel, District + parcel_fetcher = ParcelWFSFetcher( + geometry_id=self.id, + ) + typename = "ave:Flurstueck" + fetched_parcels = parcel_fetcher.get_features( + typename + ) + underlying_parcels = [] + for result in fetched_parcels: + fetched_parcel = result[typename] + parcel_obj = Parcel.objects.get_or_create( + flr=fetched_parcel["ave:flur"], + flrstck_nnr=fetched_parcel['ave:flstnrnen'], + flrstck_zhlr=fetched_parcel['ave:flstnrzae'], + )[0] + district = District.objects.get_or_create( + gmrkng=fetched_parcel["ave:gemarkung"], + gmnd=fetched_parcel["ave:gemeinde"], + krs=fetched_parcel["ave:kreis"], + )[0] + parcel_obj.district = district + parcel_obj.updated_on = timezone.now() + parcel_obj.save() + underlying_parcels.append(parcel_obj) + + self.parcels.set(underlying_parcels) class GeometryConflict(UuidModel): diff --git a/konova/models/parcel.py b/konova/models/parcel.py index ad855a21..35f5978b 100644 --- a/konova/models/parcel.py +++ b/konova/models/parcel.py @@ -22,28 +22,30 @@ class Parcel(UuidModel): To avoid conflicts due to german Umlaute, the field names are shortened and vocals are dropped. """ - geometries = models.ManyToManyField("konova.Geometry", related_name="parcels", null=True, blank=True) + geometries = models.ManyToManyField("konova.Geometry", related_name="parcels", blank=True) district = models.ForeignKey("konova.District", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels") flrstck_nnr = models.CharField( max_length=1000, - help_text="Flurstücksnenner" + help_text="Flurstücksnenner", + null=True, + blank=True, ) flrstck_zhlr = models.CharField( max_length=1000, - help_text="Flurstückszähler" + help_text="Flurstückszähler", + null=True, + blank=True, ) flr = models.CharField( max_length=1000, - help_text="Flur" - ) - gmrkng = models.CharField( - max_length=1000, - help_text="Gemarkung" + help_text="Flur", + null=True, + blank=True, ) updated_on = models.DateTimeField(auto_now_add=True) def __str__(self): - return f"{self.gmrkng} | {self.flr} | {self.flrstck_nnr} | {self.flrstck_zhlr}" + return f"{self.flr} | {self.flrstck_nnr} | {self.flrstck_zhlr}" class District(UuidModel): @@ -54,18 +56,24 @@ class District(UuidModel): District. """ + gmrkng = models.CharField( + max_length=1000, + help_text="Gemarkung", + null=True, + blank=True, + ) gmnd = models.CharField( max_length=1000, - help_text="Gemeinde" - ) - vg = models.CharField( - max_length=1000, - help_text="Verbandsgemeinde", + help_text="Gemeinde", + null=True, + blank=True, ) krs = models.CharField( max_length=1000, - help_text="Kreis" + help_text="Kreis", + null=True, + blank=True, ) def __str__(self): - return f"{self.krs} | {self.vg} | {self.gmnd}" + return f"{self.gmrkng} | {self.gmnd} | {self.krs}" diff --git a/konova/tests/test_geometries.py b/konova/tests/test_geometries.py index da5e82d0..69a4ef71 100644 --- a/konova/tests/test_geometries.py +++ b/konova/tests/test_geometries.py @@ -8,7 +8,6 @@ Created on: 15.12.21 from django.contrib.gis.db.models.functions import Translate from konova.models import Geometry, GeometryConflict -from konova.settings import PARCEL_WFS_USER, PARCEL_WFS_PW from konova.tests.test_views import BaseTestCase from konova.utils.wfs.spatial import ParcelWFSFetcher @@ -25,6 +24,22 @@ class GeometryTestCase(BaseTestCase): geom=geom, ) + def test_geometry_parcel_caluclation(self): + """ Tests whether newly created geometries already have parcels calculated during save + + Returns: + + """ + has_parcels = self.geom_1.parcels.all().exists() + self.assertFalse(has_parcels, msg=f"{self.geom_1.id} has parcels but should not!") + self.geom_1.update_parcels() + self.geom_1.refresh_from_db() + parcels = self.geom_1.parcels.all() + has_parcels = parcels.exists() + parcel_districts = parcels.values_list("district", flat=True) + self.assertTrue(has_parcels, msg=f"{self.geom_1.id} has no parcels but should!") + self.assertEqual(parcels.count(), len(parcel_districts), msg=f"Not every parcel has exactly one district!") + def test_geometry_conflict(self): """ Tests whether a geometry conflict will be present in case of identical/overlaying geometries and if the conflict will be resolved if one geometry is edited. @@ -50,17 +65,13 @@ class GeometryTestCase(BaseTestCase): def test_wfs_fetch(self): """ Tests the fetching functionality of ParcelWFSFetcher - +++ Test relies on the availability of the RLP Gemarkung WFS +++ + +++ Test relies on the availability of the RLP Flurstück WFS +++ Returns: """ fetcher = ParcelWFSFetcher( - base_url="https://www.geoportal.rlp.de/registry/wfs/519", - version="2.0.0", - geometry=self.geom_1, - auth_user=PARCEL_WFS_USER, - auth_pw=PARCEL_WFS_PW + geometry_id=self.geom_1.id, ) features = fetcher.get_features( "ave:Flurstueck", diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py index 72cbbea7..d31726ff 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -214,7 +214,7 @@ class BaseTestCase(TestCase): Returns: """ - polygon = Polygon.from_bbox((7.5971391556, 50.3600032354, 7.5993975756, 50.3612420894)) + polygon = Polygon.from_bbox((7.592449, 50.359385, 7.593382, 50.359874)) polygon.srid = 4326 polygon = polygon.transform(3857, clone=True) return MultiPolygon(polygon, srid=3857) # 3857 is the default srid used for MultiPolygonField in the form diff --git a/konova/utils/wfs/spatial.py b/konova/utils/wfs/spatial.py index c53a6b7f..62f71f65 100644 --- a/konova/utils/wfs/spatial.py +++ b/konova/utils/wfs/spatial.py @@ -10,11 +10,9 @@ from abc import abstractmethod import requests import xmltodict from django.contrib.gis.db.models.functions import AsGML, Transform -from django.contrib.gis.geos import MultiPolygon from requests.auth import HTTPDigestAuth -from konova.models import Geometry -from konova.settings import DEFAULT_SRID_RLP +from konova.settings import DEFAULT_SRID_RLP, PARCEL_WFS_USER, PARCEL_WFS_PW class AbstractWFSFetcher: @@ -38,6 +36,9 @@ class AbstractWFSFetcher: 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, @@ -53,13 +54,20 @@ class ParcelWFSFetcher(AbstractWFSFetcher): """ Fetches features from a special parcel WFS """ - geometry = None + geometry_id = None geometry_property_name = None count = 100 - def __init__(self, geometry: MultiPolygon, geometry_property_name: str = "msGeometry", *args, **kwargs): - super().__init__(*args, **kwargs) - self.geometry = geometry + 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, @@ -74,10 +82,11 @@ class ParcelWFSFetcher(AbstractWFSFetcher): Returns: spatial_filter (str): The spatial filter element """ + from konova.models import Geometry if filter_srid is None: filter_srid = DEFAULT_SRID_RLP geom_gml = Geometry.objects.filter( - id=self.geometry.id + id=self.geometry_id ).annotate( transformed=Transform(srid=filter_srid, expression="geom") ).annotate( @@ -124,6 +133,7 @@ class ParcelWFSFetcher(AbstractWFSFetcher): 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 From ecb67809da494f10b884f73494a67e7efddd9cc9 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 4 Jan 2022 16:25:17 +0100 Subject: [PATCH 5/9] #49 Extends sanitize db command * extends sanitize db command to remove unrelated parcels and district from the database * fixes bug where single parcel wfs match would lead to unexpected behaviour * adds admin interface for parcels and districts * adds updating of parcels in case of SimpleGeomForm saving --- konova/admin.py | 23 ++++++++++++++- konova/forms.py | 1 + konova/management/commands/sanitize_db.py | 34 ++++++++++++++++++++++- konova/utils/wfs/spatial.py | 8 +++++- 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/konova/admin.py b/konova/admin.py index 213e0412..23e29a65 100644 --- a/konova/admin.py +++ b/konova/admin.py @@ -7,7 +7,7 @@ Created on: 22.07.21 """ from django.contrib import admin -from konova.models import Geometry, Deadline, GeometryConflict +from konova.models import Geometry, Deadline, GeometryConflict, Parcel, District class GeometryAdmin(admin.ModelAdmin): @@ -17,6 +17,25 @@ class GeometryAdmin(admin.ModelAdmin): ] +class ParcelAdmin(admin.ModelAdmin): + list_display = [ + "id", + "flr", + "flrstck_nnr", + "flrstck_zhlr", + "updated_on", + ] + + +class DistrictAdmin(admin.ModelAdmin): + list_display = [ + "id", + "gmrkng", + "gmnd", + "krs", + ] + + class GeometryConflictAdmin(admin.ModelAdmin): list_display = [ "conflicting_geometry", @@ -52,5 +71,7 @@ class BaseObjectAdmin(admin.ModelAdmin): admin.site.register(Geometry, GeometryAdmin) +admin.site.register(Parcel, ParcelAdmin) +admin.site.register(District, DistrictAdmin) admin.site.register(GeometryConflict, GeometryConflictAdmin) admin.site.register(Deadline, DeadlineAdmin) diff --git a/konova/forms.py b/konova/forms.py index 43c83498..e9d9c8d7 100644 --- a/konova/forms.py +++ b/konova/forms.py @@ -287,6 +287,7 @@ class SimpleGeomForm(BaseForm): geometry = self.instance.geometry geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID)) geometry.modified = action + geometry.update_parcels() geometry.save() except LookupError: # No geometry or linked instance holding a geometry exist --> create a new one! diff --git a/konova/management/commands/sanitize_db.py b/konova/management/commands/sanitize_db.py index c5526517..0eecdc03 100644 --- a/konova/management/commands/sanitize_db.py +++ b/konova/management/commands/sanitize_db.py @@ -9,7 +9,7 @@ from compensation.models import CompensationState, Compensation, EcoAccount, Com from ema.models import Ema from intervention.models import Intervention from konova.management.commands.setup import BaseKonovaCommand -from konova.models import Deadline, Geometry +from konova.models import Deadline, Geometry, Parcel, District from user.models import UserActionLogEntry @@ -23,6 +23,7 @@ class Command(BaseKonovaCommand): self.sanitize_actions() self.sanitize_deadlines() self.sanitize_geometries() + self.sanitize_parcels_and_districts() except KeyboardInterrupt: self._break_line() exit(-1) @@ -266,3 +267,34 @@ class Command(BaseKonovaCommand): self._write_success("No unused states found.") self._break_line() + def sanitize_parcels_and_districts(self): + """ Removes unattached parcels and districts + + Returns: + + """ + self._write_warning("=== Sanitize parcels and districts ===") + unrelated_parcels = Parcel.objects.filter( + geometries=None, + ) + num_unrelated_parcels = unrelated_parcels.count() + if num_unrelated_parcels > 0: + self._write_error(f"Found {num_unrelated_parcels} unrelated parcel entries. Delete now...") + unrelated_parcels.delete() + self._write_success("Unrelated parcels deleted.") + else: + self._write_success("No unrelated parcels found.") + + unrelated_districts = District.objects.filter( + parcels=None, + ) + num_unrelated_districts = unrelated_districts.count() + if num_unrelated_districts > 0: + self._write_error(f"Found {num_unrelated_districts} unrelated district entries. Delete now...") + unrelated_districts.delete() + self._write_success("Unrelated districts deleted.") + else: + self._write_success("No unrelated districts found.") + + self._break_line() + diff --git a/konova/utils/wfs/spatial.py b/konova/utils/wfs/spatial.py index 62f71f65..ed64f3cf 100644 --- a/konova/utils/wfs/spatial.py +++ b/konova/utils/wfs/spatial.py @@ -158,10 +158,16 @@ class ParcelWFSFetcher(AbstractWFSFetcher): "wfs:FeatureCollection", {}, ) - features += collection.get( + + members = collection.get( "wfs:member", [], ) + if len(members) > 1: + features += members + else: + features += [members] + if collection.get("@next", None) is not None: start_index += self.count else: From b43beffc6be373dbea75d39273758846acfc3459 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 4 Jan 2022 16:50:55 +0100 Subject: [PATCH 6/9] #49 Update all parcels command * adds update_all_parcels command which can be used e.g. with cronjob once a month to update all parcels and districts --- .../management/commands/update_all_parcels.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 konova/management/commands/update_all_parcels.py diff --git a/konova/management/commands/update_all_parcels.py b/konova/management/commands/update_all_parcels.py new file mode 100644 index 00000000..9d96ebae --- /dev/null +++ b/konova/management/commands/update_all_parcels.py @@ -0,0 +1,41 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 04.01.22 + +""" +from konova.management.commands.setup import BaseKonovaCommand +from konova.models import Geometry, Parcel, District + + +class Command(BaseKonovaCommand): + help = "Checks the database' sanity and removes unused entries" + + def handle(self, *args, **options): + try: + self.update_all_parcels() + except KeyboardInterrupt: + self._break_line() + exit(-1) + + def update_all_parcels(self): + num_parcels_before = Parcel.objects.count() + num_districts_before = District.objects.count() + self._write_warning("=== Update parcels and districts ===") + geometries = Geometry.objects.all().exclude( + geom=None + ) + self._write_warning(f"Process parcels for {geometries.count()} geometry entries now ...") + for geometry in geometries: + geometry.update_parcels() + + num_parcels_after = Parcel.objects.count() + num_districts_after = District.objects.count() + if num_parcels_after != num_parcels_before: + self._write_error(f"Parcels have changed: {num_parcels_before} to {num_parcels_after} entries. You should run the sanitize command.") + if num_districts_after != num_districts_before: + self._write_error(f"Districts have changed: {num_districts_before} to {num_districts_after} entries. You should run the sanitize command.") + + self._write_success("Updating parcels done!") + self._break_line() From 49859d17d23c3e49c629f4b04b70f57b6a75b7f1 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 5 Jan 2022 14:13:26 +0100 Subject: [PATCH 7/9] #49 Frontend rendering * adds rendering for detail view * adds new includable html snippet for parcel rendering * refactors generic includables in konova/ app into konova/templates/includes/... * fixes bug where parcels have been reused from the database due to wrong model structure * adds get_underlying_parcels() for Geometry model * adds get_underlying_parcels() for GeoReferencedMixin models * fixes bug where missing geometry would lead to an error during geometry conflict check * removes unused wfs attribute from AbstractWFSFetcher * adds/updates translations --- compensation/models/compensation.py | 2 +- .../detail/compensation/view.html | 5 +- .../compensation/detail/eco_account/view.html | 5 +- compensation/views/compensation.py | 2 + compensation/views/eco_account.py | 2 + ema/models/ema.py | 2 +- ema/templates/ema/detail/view.html | 8 + ema/views.py | 2 + intervention/models/intervention.py | 2 +- .../templates/intervention/detail/view.html | 5 +- intervention/views.py | 3 + konova/admin.py | 2 +- konova/models/geometry.py | 16 +- konova/models/object.py | 18 ++- konova/models/parcel.py | 16 +- .../konova/{ => includes}/comment_card.html | 0 konova/templates/konova/includes/parcels.html | 29 ++++ konova/utils/wfs/spatial.py | 1 - locale/de/LC_MESSAGES/django.mo | Bin 28510 -> 28769 bytes locale/de/LC_MESSAGES/django.po | 138 ++++++++++-------- 20 files changed, 183 insertions(+), 75 deletions(-) rename konova/templates/konova/{ => includes}/comment_card.html (100%) create mode 100644 konova/templates/konova/includes/parcels.html diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py index 703f7cd4..65a62b6e 100644 --- a/compensation/models/compensation.py +++ b/compensation/models/compensation.py @@ -170,7 +170,7 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin): """ if not self.is_shared_with(request.user): messages.info(request, DATA_UNSHARED_EXPLANATION) - request = self._set_geometry_conflict_message(request) + request = self.set_geometry_conflict_message(request) return request diff --git a/compensation/templates/compensation/detail/compensation/view.html b/compensation/templates/compensation/detail/compensation/view.html index 581bf799..c210986a 100644 --- a/compensation/templates/compensation/detail/compensation/view.html +++ b/compensation/templates/compensation/detail/compensation/view.html @@ -110,7 +110,10 @@ {% include 'map/geom_form.html' %}
- {% include 'konova/comment_card.html' %} + {% include 'konova/includes/parcels.html' %} +
+
+ {% include 'konova/includes/comment_card.html' %}
diff --git a/compensation/templates/compensation/detail/eco_account/view.html b/compensation/templates/compensation/detail/eco_account/view.html index 717d6d53..493a9292 100644 --- a/compensation/templates/compensation/detail/eco_account/view.html +++ b/compensation/templates/compensation/detail/eco_account/view.html @@ -93,7 +93,10 @@ {% include 'map/geom_form.html' %}
- {% include 'konova/comment_card.html' %} + {% include 'konova/includes/parcels.html' %} +
+
+ {% include 'konova/includes/comment_card.html' %}
diff --git a/compensation/views/compensation.py b/compensation/views/compensation.py index b5efc984..bfd7ba62 100644 --- a/compensation/views/compensation.py +++ b/compensation/views/compensation.py @@ -170,6 +170,7 @@ def detail_view(request: HttpRequest, id: str): template = "compensation/detail/compensation/view.html" comp = get_object_or_404(Compensation, id=id) geom_form = SimpleGeomForm(instance=comp) + parcels = comp.get_underlying_parcels() _user = request.user is_data_shared = comp.intervention.is_shared_with(_user) @@ -189,6 +190,7 @@ def detail_view(request: HttpRequest, id: str): context = { "obj": comp, "geom_form": geom_form, + "parcels": parcels, "has_access": is_data_shared, "actions": actions, "before_states": before_states, diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py index 7b4c27e2..118a9f6e 100644 --- a/compensation/views/eco_account.py +++ b/compensation/views/eco_account.py @@ -181,6 +181,7 @@ def detail_view(request: HttpRequest, id: str): id=id ) geom_form = SimpleGeomForm(instance=acc) + parcels = acc.get_underlying_parcels() _user = request.user is_data_shared = acc.is_shared_with(_user) @@ -207,6 +208,7 @@ def detail_view(request: HttpRequest, id: str): context = { "obj": acc, "geom_form": geom_form, + "parcels": parcels, "has_access": is_data_shared, "before_states": before_states, "after_states": after_states, diff --git a/ema/models/ema.py b/ema/models/ema.py index 8bc4ac65..2c61a241 100644 --- a/ema/models/ema.py +++ b/ema/models/ema.py @@ -106,7 +106,7 @@ class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin): """ if not self.is_shared_with(request.user): messages.info(request, DATA_UNSHARED_EXPLANATION) - self._set_geometry_conflict_message(request) + self.set_geometry_conflict_message(request) return request diff --git a/ema/templates/ema/detail/view.html b/ema/templates/ema/detail/view.html index 6635569a..230363b4 100644 --- a/ema/templates/ema/detail/view.html +++ b/ema/templates/ema/detail/view.html @@ -77,7 +77,15 @@
+
{% include 'map/geom_form.html' %} +
+
+ {% include 'konova/includes/parcels.html' %} +
+
+ {% include 'konova/includes/comment_card.html' %} +

diff --git a/ema/views.py b/ema/views.py index dc2fa49a..fc94686b 100644 --- a/ema/views.py +++ b/ema/views.py @@ -125,6 +125,7 @@ def detail_view(request: HttpRequest, id: str): ema = get_object_or_404(Ema, id=id, deleted=None) geom_form = SimpleGeomForm(instance=ema) + parcels = ema.get_underlying_parcels() _user = request.user is_data_shared = ema.is_shared_with(_user) @@ -143,6 +144,7 @@ def detail_view(request: HttpRequest, id: str): context = { "obj": ema, "geom_form": geom_form, + "parcels": parcels, "has_access": is_data_shared, "before_states": before_states, "after_states": after_states, diff --git a/intervention/models/intervention.py b/intervention/models/intervention.py index a31eb9a3..b54bbcf5 100644 --- a/intervention/models/intervention.py +++ b/intervention/models/intervention.py @@ -278,7 +278,7 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec """ if not self.is_shared_with(request.user): messages.info(request, DATA_UNSHARED_EXPLANATION) - request = self._set_geometry_conflict_message(request) + request = self.set_geometry_conflict_message(request) return request diff --git a/intervention/templates/intervention/detail/view.html b/intervention/templates/intervention/detail/view.html index 408b3939..e51f3ecd 100644 --- a/intervention/templates/intervention/detail/view.html +++ b/intervention/templates/intervention/detail/view.html @@ -127,7 +127,10 @@ {% include 'map/geom_form.html' %}
- {% include 'konova/comment_card.html' %} + {% include 'konova/includes/parcels.html' %} +
+
+ {% include 'konova/includes/comment_card.html' %}
diff --git a/intervention/views.py b/intervention/views.py index 60d75249..d18dce23 100644 --- a/intervention/views.py +++ b/intervention/views.py @@ -236,6 +236,8 @@ def detail_view(request: HttpRequest, id: str): instance=intervention, ) + parcels = intervention.get_underlying_parcels() + # Inform user about revocation if intervention.legal.revocations.exists(): messages.error( @@ -249,6 +251,7 @@ def detail_view(request: HttpRequest, id: str): "compensations": compensations, "has_access": is_data_shared, "geom_form": geom_form, + "parcels": parcels, "is_default_member": in_group(_user, DEFAULT_GROUP), "is_zb_member": in_group(_user, ZB_GROUP), "is_ets_member": in_group(_user, ETS_GROUP), diff --git a/konova/admin.py b/konova/admin.py index 23e29a65..02568de9 100644 --- a/konova/admin.py +++ b/konova/admin.py @@ -20,6 +20,7 @@ class GeometryAdmin(admin.ModelAdmin): class ParcelAdmin(admin.ModelAdmin): list_display = [ "id", + "gmrkng", "flr", "flrstck_nnr", "flrstck_zhlr", @@ -30,7 +31,6 @@ class ParcelAdmin(admin.ModelAdmin): class DistrictAdmin(admin.ModelAdmin): list_display = [ "id", - "gmrkng", "gmnd", "krs", ] diff --git a/konova/models/geometry.py b/konova/models/geometry.py index e9d8c61c..aad39d6f 100644 --- a/konova/models/geometry.py +++ b/konova/models/geometry.py @@ -111,12 +111,12 @@ class Geometry(BaseResource): for result in fetched_parcels: fetched_parcel = result[typename] parcel_obj = Parcel.objects.get_or_create( + gmrkng=fetched_parcel["ave:gemarkung"], flr=fetched_parcel["ave:flur"], flrstck_nnr=fetched_parcel['ave:flstnrnen'], flrstck_zhlr=fetched_parcel['ave:flstnrzae'], )[0] district = District.objects.get_or_create( - gmrkng=fetched_parcel["ave:gemarkung"], gmnd=fetched_parcel["ave:gemeinde"], krs=fetched_parcel["ave:kreis"], )[0] @@ -127,6 +127,20 @@ class Geometry(BaseResource): self.parcels.set(underlying_parcels) + def get_underlying_parcels(self): + """ Getter for related parcels and their districts + + Returns: + parcels (QuerySet): The related parcels as queryset + """ + parcels = self.parcels.all().prefetch_related( + "district" + ).order_by( + "gmrkng", + ) + + return parcels + class GeometryConflict(UuidModel): """ diff --git a/konova/models/object.py b/konova/models/object.py index ded39ad1..49c1f4e0 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -420,13 +420,29 @@ class GeoReferencedMixin(models.Model): class Meta: abstract = True - def _set_geometry_conflict_message(self, request: HttpRequest): + def get_underlying_parcels(self): + """ Getter for related parcels + + Returns: + parcels (Iterable): An empty list or a Queryset + """ + if self.geometry is not None: + return self.geometry.get_underlying_parcels() + else: + return [] + + def set_geometry_conflict_message(self, request: HttpRequest): + if self.geometry is None: + return request + instance_objs = [] add_message = False conflicts = self.geometry.conflicts_geometries.all() + for conflict in conflicts: instance_objs += conflict.affected_geometry.get_data_objects() add_message = True + conflicts = self.geometry.conflicted_by_geometries.all() for conflict in conflicts: instance_objs += conflict.conflicting_geometry.get_data_objects() diff --git a/konova/models/parcel.py b/konova/models/parcel.py index 35f5978b..487225e6 100644 --- a/konova/models/parcel.py +++ b/konova/models/parcel.py @@ -24,6 +24,12 @@ class Parcel(UuidModel): """ geometries = models.ManyToManyField("konova.Geometry", related_name="parcels", blank=True) district = models.ForeignKey("konova.District", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels") + gmrkng = models.CharField( + max_length=1000, + help_text="Gemarkung", + null=True, + blank=True, + ) flrstck_nnr = models.CharField( max_length=1000, help_text="Flurstücksnenner", @@ -45,7 +51,7 @@ class Parcel(UuidModel): updated_on = models.DateTimeField(auto_now_add=True) def __str__(self): - return f"{self.flr} | {self.flrstck_nnr} | {self.flrstck_zhlr}" + return f"{self.gmrkng} | {self.flr} | {self.flrstck_zhlr} | {self.flrstck_nnr}" class District(UuidModel): @@ -56,12 +62,6 @@ class District(UuidModel): District. """ - gmrkng = models.CharField( - max_length=1000, - help_text="Gemarkung", - null=True, - blank=True, - ) gmnd = models.CharField( max_length=1000, help_text="Gemeinde", @@ -76,4 +76,4 @@ class District(UuidModel): ) def __str__(self): - return f"{self.gmrkng} | {self.gmnd} | {self.krs}" + return f"{self.gmnd} | {self.krs}" diff --git a/konova/templates/konova/comment_card.html b/konova/templates/konova/includes/comment_card.html similarity index 100% rename from konova/templates/konova/comment_card.html rename to konova/templates/konova/includes/comment_card.html diff --git a/konova/templates/konova/includes/parcels.html b/konova/templates/konova/includes/parcels.html new file mode 100644 index 00000000..e15fa3d0 --- /dev/null +++ b/konova/templates/konova/includes/parcels.html @@ -0,0 +1,29 @@ +{% load i18n %} +
+

{% trans 'Spatial reference' %}

+
+
+ + + + + + + + + + + + {% for parcel in parcels %} + + + + + + + + {% endfor %} + + +
{% trans 'Kreis' %}{% trans 'Gemarkung' %}{% trans 'Parcel' %}{% trans 'Parcel counter' %}{% trans 'Parcel number' %}
{{parcel.district.krs|default_if_none:"-"}}{{parcel.gmrkng|default_if_none:"-"}}{{parcel.flr|default_if_none:"-"}}{{parcel.flrstck_zhlr|default_if_none:"-"}}{{parcel.flrstck_nnr|default_if_none:"-"}}
+
\ No newline at end of file diff --git a/konova/utils/wfs/spatial.py b/konova/utils/wfs/spatial.py index ed64f3cf..6d0ddda9 100644 --- a/konova/utils/wfs/spatial.py +++ b/konova/utils/wfs/spatial.py @@ -22,7 +22,6 @@ class AbstractWFSFetcher: # base_url represents not the capabilities url but the parameter-free base url base_url = None version = None - wfs = None auth_user = None auth_pw = None auth_digest_obj = None diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index bd3e82d8ebcb6ae0e59f48644b44c20fb80aa7a9..b03903cb8b526ec706d92655f9bfdeb88cfe99c5 100644 GIT binary patch delta 8962 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 C$KV72 delta 8766 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 diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index 04aefdf0..fbfccf67 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -11,15 +11,15 @@ #: intervention/forms/forms.py:52 intervention/forms/forms.py:154 #: intervention/forms/forms.py:166 intervention/forms/modalForms.py:125 #: intervention/forms/modalForms.py:138 intervention/forms/modalForms.py:151 -#: konova/forms.py:139 konova/forms.py:240 konova/forms.py:308 -#: konova/forms.py:335 konova/forms.py:345 konova/forms.py:358 -#: konova/forms.py:370 konova/forms.py:388 user/forms.py:38 +#: konova/forms.py:139 konova/forms.py:240 konova/forms.py:309 +#: konova/forms.py:336 konova/forms.py:346 konova/forms.py:359 +#: konova/forms.py:371 konova/forms.py:389 user/forms.py:38 #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-12-16 09:17+0100\n" +"POT-Creation-Date: 2022-01-05 14:04+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -329,7 +329,7 @@ msgstr "Automatisch generiert" #: intervention/templates/intervention/detail/includes/documents.html:28 #: intervention/templates/intervention/detail/view.html:31 #: intervention/templates/intervention/report/report.html:12 -#: konova/forms.py:334 +#: konova/forms.py:335 msgid "Title" msgstr "Bezeichnung" @@ -356,7 +356,7 @@ msgstr "Kompensation XY; Flur ABC" #: intervention/templates/intervention/detail/includes/documents.html:31 #: intervention/templates/intervention/detail/includes/payments.html:34 #: intervention/templates/intervention/detail/includes/revocation.html:38 -#: konova/forms.py:369 konova/templates/konova/comment_card.html:16 +#: konova/forms.py:370 konova/templates/konova/includes/comment_card.html:16 msgid "Comment" msgstr "Kommentar" @@ -472,7 +472,7 @@ msgstr "Zahlung wird an diesem Datum erwartet" #: compensation/forms/modalForms.py:62 compensation/forms/modalForms.py:239 #: compensation/forms/modalForms.py:317 intervention/forms/modalForms.py:152 -#: konova/forms.py:371 +#: konova/forms.py:372 msgid "Additional comment, maximum {} letters" msgstr "Zusätzlicher Kommentar, maximal {} Zeichen" @@ -793,7 +793,7 @@ msgstr "Dokumente" #: compensation/templates/compensation/detail/eco_account/includes/documents.html:14 #: ema/templates/ema/detail/includes/documents.html:14 #: intervention/templates/intervention/detail/includes/documents.html:14 -#: konova/forms.py:387 +#: konova/forms.py:388 msgid "Add new document" msgstr "Neues Dokument hinzufügen" @@ -1056,41 +1056,41 @@ msgstr "Kompensation {} hinzugefügt" msgid "Compensation {} edited" msgstr "Kompensation {} bearbeitet" -#: compensation/views/compensation.py:228 compensation/views/eco_account.py:307 -#: ema/views.py:181 intervention/views.py:474 +#: compensation/views/compensation.py:230 compensation/views/eco_account.py:309 +#: ema/views.py:181 intervention/views.py:477 msgid "Log" msgstr "Log" -#: compensation/views/compensation.py:251 +#: compensation/views/compensation.py:253 msgid "Compensation removed" msgstr "Kompensation entfernt" -#: compensation/views/compensation.py:272 compensation/views/eco_account.py:459 +#: compensation/views/compensation.py:274 compensation/views/eco_account.py:461 #: ema/views.py:348 intervention/views.py:129 msgid "Document added" msgstr "Dokument hinzugefügt" -#: compensation/views/compensation.py:341 compensation/views/eco_account.py:353 +#: compensation/views/compensation.py:343 compensation/views/eco_account.py:355 #: ema/views.py:286 msgid "State added" msgstr "Zustand hinzugefügt" -#: compensation/views/compensation.py:362 compensation/views/eco_account.py:374 +#: compensation/views/compensation.py:364 compensation/views/eco_account.py:376 #: ema/views.py:307 msgid "Action added" msgstr "Maßnahme hinzugefügt" -#: compensation/views/compensation.py:383 compensation/views/eco_account.py:439 +#: compensation/views/compensation.py:385 compensation/views/eco_account.py:441 #: ema/views.py:328 msgid "Deadline added" msgstr "Frist/Termin hinzugefügt" -#: compensation/views/compensation.py:405 compensation/views/eco_account.py:396 +#: compensation/views/compensation.py:407 compensation/views/eco_account.py:398 #: ema/views.py:418 msgid "State removed" msgstr "Zustand gelöscht" -#: compensation/views/compensation.py:427 compensation/views/eco_account.py:418 +#: compensation/views/compensation.py:429 compensation/views/eco_account.py:420 #: ema/views.py:440 msgid "Action removed" msgstr "Maßnahme entfernt" @@ -1103,45 +1103,45 @@ msgstr "Ökokonto {} hinzugefügt" msgid "Eco-Account {} edited" msgstr "Ökokonto {} bearbeitet" -#: compensation/views/eco_account.py:255 +#: compensation/views/eco_account.py:257 msgid "Eco-account removed" msgstr "Ökokonto entfernt" -#: compensation/views/eco_account.py:283 +#: compensation/views/eco_account.py:285 msgid "Deduction removed" msgstr "Abbuchung entfernt" -#: compensation/views/eco_account.py:328 ema/views.py:261 -#: intervention/views.py:516 +#: compensation/views/eco_account.py:330 ema/views.py:261 +#: intervention/views.py:519 msgid "{} unrecorded" msgstr "{} entzeichnet" -#: compensation/views/eco_account.py:328 ema/views.py:261 -#: intervention/views.py:516 +#: compensation/views/eco_account.py:330 ema/views.py:261 +#: intervention/views.py:519 msgid "{} recorded" msgstr "{} verzeichnet" -#: compensation/views/eco_account.py:529 intervention/views.py:497 +#: compensation/views/eco_account.py:531 intervention/views.py:500 msgid "Deduction added" msgstr "Abbuchung hinzugefügt" -#: compensation/views/eco_account.py:612 ema/views.py:516 -#: intervention/views.py:372 +#: compensation/views/eco_account.py:614 ema/views.py:516 +#: intervention/views.py:375 msgid "{} has already been shared with you" msgstr "{} wurde bereits für Sie freigegeben" -#: compensation/views/eco_account.py:617 ema/views.py:521 -#: intervention/views.py:377 +#: compensation/views/eco_account.py:619 ema/views.py:521 +#: intervention/views.py:380 msgid "{} has been shared with you" msgstr "{} ist nun für Sie freigegeben" -#: compensation/views/eco_account.py:624 ema/views.py:528 -#: intervention/views.py:384 +#: compensation/views/eco_account.py:626 ema/views.py:528 +#: intervention/views.py:387 msgid "Share link invalid" msgstr "Freigabelink ungültig" -#: compensation/views/eco_account.py:647 ema/views.py:551 -#: intervention/views.py:407 +#: compensation/views/eco_account.py:649 ema/views.py:551 +#: intervention/views.py:410 msgid "Share settings updated" msgstr "Freigabe Einstellungen aktualisiert" @@ -1333,7 +1333,7 @@ msgstr "Kompensationen und Zahlungen geprüft" msgid "Run check" msgstr "Prüfung vornehmen" -#: intervention/forms/modalForms.py:196 konova/forms.py:453 +#: intervention/forms/modalForms.py:196 konova/forms.py:454 msgid "" "I, {} {}, confirm that all necessary control steps have been performed by " "myself." @@ -1472,31 +1472,31 @@ msgstr "" msgid "Intervention {} added" msgstr "Eingriff {} hinzugefügt" -#: intervention/views.py:243 +#: intervention/views.py:245 msgid "This intervention has {} revocations" msgstr "Dem Eingriff liegen {} Widersprüche vor" -#: intervention/views.py:290 +#: intervention/views.py:293 msgid "Intervention {} edited" msgstr "Eingriff {} bearbeitet" -#: intervention/views.py:325 +#: intervention/views.py:328 msgid "{} removed" msgstr "{} entfernt" -#: intervention/views.py:346 +#: intervention/views.py:349 msgid "Revocation removed" msgstr "Widerspruch entfernt" -#: intervention/views.py:428 +#: intervention/views.py:431 msgid "Check performed" msgstr "Prüfung durchgeführt" -#: intervention/views.py:450 +#: intervention/views.py:453 msgid "Revocation added" msgstr "Widerspruch hinzugefügt" -#: intervention/views.py:521 +#: intervention/views.py:524 msgid "There are errors on this intervention:" msgstr "Es liegen Fehler in diesem Eingriff vor:" @@ -1525,11 +1525,11 @@ msgstr "Speichern" msgid "Not editable" msgstr "Nicht editierbar" -#: konova/forms.py:138 konova/forms.py:307 +#: konova/forms.py:138 konova/forms.py:308 msgid "Confirm" msgstr "Bestätige" -#: konova/forms.py:150 konova/forms.py:316 +#: konova/forms.py:150 konova/forms.py:317 msgid "Remove" msgstr "Löschen" @@ -1542,56 +1542,56 @@ msgstr "Sie sind dabei {} {} zu löschen" msgid "Geometry" msgstr "Geometrie" -#: konova/forms.py:317 +#: konova/forms.py:318 msgid "Are you sure?" msgstr "Sind Sie sicher?" -#: konova/forms.py:344 +#: konova/forms.py:345 msgid "Created on" msgstr "Erstellt" -#: konova/forms.py:346 +#: konova/forms.py:347 msgid "When has this file been created? Important for photos." msgstr "Wann wurde diese Datei erstellt oder das Foto aufgenommen?" -#: konova/forms.py:357 +#: konova/forms.py:358 #: venv/lib/python3.7/site-packages/django/db/models/fields/files.py:231 msgid "File" msgstr "Datei" -#: konova/forms.py:359 +#: konova/forms.py:360 msgid "Allowed formats: pdf, jpg, png. Max size 15 MB." msgstr "Formate: pdf, jpg, png. Maximal 15 MB." -#: konova/forms.py:405 +#: konova/forms.py:406 msgid "Unsupported file type" msgstr "Dateiformat nicht unterstützt" -#: konova/forms.py:412 +#: konova/forms.py:413 msgid "File too large" msgstr "Datei zu groß" -#: konova/forms.py:421 +#: konova/forms.py:422 msgid "Added document" msgstr "Dokument hinzugefügt" -#: konova/forms.py:444 +#: konova/forms.py:445 msgid "Confirm record" msgstr "Verzeichnen bestätigen" -#: konova/forms.py:452 +#: konova/forms.py:453 msgid "Record data" msgstr "Daten verzeichnen" -#: konova/forms.py:459 +#: konova/forms.py:460 msgid "Confirm unrecord" msgstr "Entzeichnen bestätigen" -#: konova/forms.py:460 +#: konova/forms.py:461 msgid "Unrecord data" msgstr "Daten entzeichnen" -#: konova/forms.py:461 +#: konova/forms.py:462 msgid "I, {} {}, confirm that this data must be unrecorded." msgstr "" "Ich, {} {}, bestätige, dass diese Daten wieder entzeichnet werden müssen." @@ -1663,6 +1663,30 @@ msgstr "Anzeigen" msgid "Deduct" msgstr "Abbuchen" +#: konova/templates/konova/includes/parcels.html:3 +msgid "Spatial reference" +msgstr "Raumreferenz" + +#: konova/templates/konova/includes/parcels.html:9 +msgid "Kreis" +msgstr "Kreis" + +#: konova/templates/konova/includes/parcels.html:10 +msgid "Gemarkung" +msgstr "Gemarkung" + +#: konova/templates/konova/includes/parcels.html:11 +msgid "Parcel" +msgstr "Flur" + +#: konova/templates/konova/includes/parcels.html:12 +msgid "Parcel counter" +msgstr "Flurstückzähler" + +#: konova/templates/konova/includes/parcels.html:13 +msgid "Parcel number" +msgstr "Flurstücknenner" + #: konova/templates/konova/widgets/generate-content-input.html:6 msgid "Generate new" msgstr "Neu generieren" @@ -1726,8 +1750,8 @@ msgid "" "Action canceled. Eco account is recorded or deductions exist. Only " "conservation office member can perform this action." msgstr "" -"Aktion abgebrochen. Ökokonto ist bereits verzeichnet oder Abbuchungen liegen vor. Nur " -"Eintragungsstellennutzer können diese Aktion jetzt durchführen." +"Aktion abgebrochen. Ökokonto ist bereits verzeichnet oder Abbuchungen liegen " +"vor. Nur Eintragungsstellennutzer können diese Aktion jetzt durchführen." #: konova/utils/message_templates.py:25 msgid "Edited general data" From a09fdae58c50f3a929c95a82ee6bbcc70be23614 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 5 Jan 2022 14:41:32 +0100 Subject: [PATCH 8/9] #49 Parcels on report * adds parcel displaying on public reports * fixes bug in EMA where autocomplete js would not load for modal forms * fixes bug where BaseContext cached data from last request and reused it, if not overwritten --- .../report/compensation/report.html | 3 +++ .../report/eco_account/report.html | 3 +++ compensation/views/compensation.py | 2 ++ compensation/views/eco_account.py | 2 ++ ema/templates/ema/detail/view.html | 8 +++++++- ema/templates/ema/report/report.html | 3 +++ ema/views.py | 2 ++ .../templates/intervention/report/report.html | 3 +++ intervention/views.py | 2 ++ konova/contexts.py | 19 +++++++++---------- 10 files changed, 36 insertions(+), 11 deletions(-) diff --git a/compensation/templates/compensation/report/compensation/report.html b/compensation/templates/compensation/report/compensation/report.html index 711ce16d..df991ad0 100644 --- a/compensation/templates/compensation/report/compensation/report.html +++ b/compensation/templates/compensation/report/compensation/report.html @@ -37,6 +37,9 @@
{% include 'map/geom_form.html' %}
+
+ {% include 'konova/includes/parcels.html' %} +

{% trans 'Open in browser' %}

diff --git a/compensation/templates/compensation/report/eco_account/report.html b/compensation/templates/compensation/report/eco_account/report.html index e71670a1..1aef1d4b 100644 --- a/compensation/templates/compensation/report/eco_account/report.html +++ b/compensation/templates/compensation/report/eco_account/report.html @@ -54,6 +54,9 @@
{% include 'map/geom_form.html' %}
+
+ {% include 'konova/includes/parcels.html' %} +

{% trans 'Open in browser' %}

diff --git a/compensation/views/compensation.py b/compensation/views/compensation.py index bfd7ba62..71efb5ee 100644 --- a/compensation/views/compensation.py +++ b/compensation/views/compensation.py @@ -453,6 +453,7 @@ def report_view(request: HttpRequest, id: str): geom_form = SimpleGeomForm( instance=comp ) + parcels = comp.get_underlying_parcels() qrcode_img = generate_qr_code( request.build_absolute_uri(reverse("compensation:report", args=(id,))), 10 @@ -474,6 +475,7 @@ def report_view(request: HttpRequest, id: str): "before_states": before_states, "after_states": after_states, "geom_form": geom_form, + "parcels": parcels, "actions": actions, } context = BaseContext(request, context).context diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py index 118a9f6e..0895823e 100644 --- a/compensation/views/eco_account.py +++ b/compensation/views/eco_account.py @@ -555,6 +555,7 @@ def report_view(request:HttpRequest, id: str): geom_form = SimpleGeomForm( instance=acc ) + parcels = acc.get_underlying_parcels() qrcode_img = generate_qr_code( request.build_absolute_uri(reverse("ema:report", args=(id,))), 10 @@ -582,6 +583,7 @@ def report_view(request:HttpRequest, id: str): "before_states": before_states, "after_states": after_states, "geom_form": geom_form, + "parcels": parcels, "actions": actions, "deductions": deductions, } diff --git a/ema/templates/ema/detail/view.html b/ema/templates/ema/detail/view.html index 230363b4..8ed7aa26 100644 --- a/ema/templates/ema/detail/view.html +++ b/ema/templates/ema/detail/view.html @@ -2,7 +2,13 @@ {% load i18n l10n static fontawesome_5 humanize %} {% block head %} - + {% comment %} + dal documentation (django-autocomplete-light) states using form.media for adding needed scripts. + This does not work properly with modal forms, as the scripts are not loaded properly inside the modal. + Therefore the script linkages from form.media have been extracted and put inside dal/scripts.html to ensure + these scripts are loaded when needed. + {% endcomment %} + {% include 'dal/scripts.html' %} {% endblock %} {% block body %} diff --git a/ema/templates/ema/report/report.html b/ema/templates/ema/report/report.html index 38dd7f23..10ec7aa1 100644 --- a/ema/templates/ema/report/report.html +++ b/ema/templates/ema/report/report.html @@ -41,6 +41,9 @@
{% include 'map/geom_form.html' %}
+
+ {% include 'konova/includes/parcels.html' %} +

{% trans 'Open in browser' %}

diff --git a/ema/views.py b/ema/views.py index fc94686b..0ae886e4 100644 --- a/ema/views.py +++ b/ema/views.py @@ -466,6 +466,7 @@ def report_view(request:HttpRequest, id: str): geom_form = SimpleGeomForm( instance=ema, ) + parcels = ema.get_underlying_parcels() qrcode_img = generate_qr_code( request.build_absolute_uri(reverse("ema:report", args=(id,))), 10 @@ -487,6 +488,7 @@ def report_view(request:HttpRequest, id: str): "before_states": before_states, "after_states": after_states, "geom_form": geom_form, + "parcels": parcels, "actions": actions, } context = BaseContext(request, context).context diff --git a/intervention/templates/intervention/report/report.html b/intervention/templates/intervention/report/report.html index 25e6aac6..ac6c29ea 100644 --- a/intervention/templates/intervention/report/report.html +++ b/intervention/templates/intervention/report/report.html @@ -100,6 +100,9 @@
{% include 'map/geom_form.html' %}
+
+ {% include 'konova/includes/parcels.html' %} +

{% trans 'Open in browser' %}

diff --git a/intervention/views.py b/intervention/views.py index d18dce23..fc1625da 100644 --- a/intervention/views.py +++ b/intervention/views.py @@ -547,6 +547,7 @@ def report_view(request:HttpRequest, id: str): geom_form = SimpleGeomForm( instance=intervention ) + parcels = intervention.get_underlying_parcels() distinct_deductions = intervention.deductions.all().distinct( "account" @@ -565,6 +566,7 @@ def report_view(request:HttpRequest, id: str): "qrcode": qrcode_img, "qrcode_lanis": qrcode_img_lanis, "geom_form": geom_form, + "parcels": parcels, } context = BaseContext(request, context).context return render(request, template, context) diff --git a/konova/contexts.py b/konova/contexts.py index b415889b..470161c9 100644 --- a/konova/contexts.py +++ b/konova/contexts.py @@ -15,18 +15,17 @@ class BaseContext: """ Holds all base data which is needed for every context rendering """ - context = { - "base_title": BASE_TITLE, - "base_frontend_title": BASE_FRONTEND_TITLE, - "language": LANGUAGE_CODE, - "user": None, - "current_role": None, - "help_link": HELP_LINK, - } + context = None def __init__(self, request: HttpRequest, additional_context: dict = {}): - self.context["language"] = request.LANGUAGE_CODE - self.context["user"] = request.user + self.context = { + "base_title": BASE_TITLE, + "base_frontend_title": BASE_FRONTEND_TITLE, + "language": request.LANGUAGE_CODE, + "user": request.user, + "current_role": None, + "help_link": HELP_LINK + } # Add additional context, derived from given parameters self.context.update(additional_context) From 7caf7687099e9eb22d613a09e541bd40f9ce614d Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 5 Jan 2022 15:26:16 +0100 Subject: [PATCH 9/9] #49 Annual report improve * improves the filtering of annual report timespan on a date base instead of timestamp base --- analysis/utils/report.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/analysis/utils/report.py b/analysis/utils/report.py index 3bfd6370..960ca2cc 100644 --- a/analysis/utils/report.py +++ b/analysis/utils/report.py @@ -64,8 +64,8 @@ class TimespanReport: responsible__conservation_office__id=id, legal__registration_date__gt=LKOMPVZVO_PUBLISH_DATE, deleted=None, - created__timestamp__gte=date_from, - created__timestamp__lte=date_to, + created__timestamp__date__gte=date_from, + created__timestamp__date__lte=date_to, ) self.queryset_checked = self.queryset.filter( checked__isnull=False @@ -231,8 +231,8 @@ class TimespanReport: intervention__responsible__conservation_office__id=id, intervention__legal__registration_date__gt=LKOMPVZVO_PUBLISH_DATE, deleted=None, - intervention__created__timestamp__gte=date_from, - intervention__created__timestamp__lte=date_to, + intervention__created__date__timestamp__gte=date_from, + intervention__created__date__timestamp__lte=date_to, ) self.queryset_checked = self.queryset.filter( intervention__checked__isnull=False @@ -400,8 +400,8 @@ class TimespanReport: self.queryset = EcoAccount.objects.filter( responsible__conservation_office__id=id, deleted=None, - created__timestamp__gte=date_from, - created__timestamp__lte=date_to, + created__timestamp__date__gte=date_from, + created__timestamp__date__lte=date_to, ) self.queryset_recorded = self.queryset.filter( recorded__isnull=False @@ -479,8 +479,8 @@ class TimespanReport: legal__registration_date__lte=LKOMPVZVO_PUBLISH_DATE, responsible__conservation_office__id=id, deleted=None, - created__timestamp__gte=date_from, - created__timestamp__lte=date_to, + created__timestamp__date__gte=date_from, + created__timestamp__date__lte=date_to, ) self.queryset_intervention_recorded = self.queryset_intervention.filter( recorded__isnull=False