diff --git a/compensation/tables.py b/compensation/tables.py index 6f29e0dd..5a3da24b 100644 --- a/compensation/tables.py +++ b/compensation/tables.py @@ -133,7 +133,9 @@ class CompensationTable(BaseTable, TableRenderMixin): Returns: """ - parcels = value.parcels.values_list( + parcels = value.parcels.filter( + parcelintersection__calculated_on__isnull=False, + ).values_list( "gmrkng", flat=True ).distinct() @@ -294,7 +296,9 @@ class EcoAccountTable(BaseTable, TableRenderMixin): Returns: """ - parcels = value.parcels.values_list( + parcels = value.parcels.filter( + parcelintersection__calculated_on__isnull=False, + ).values_list( "gmrkng", flat=True ).distinct() diff --git a/ema/tables.py b/ema/tables.py index 20ceb456..bf3709de 100644 --- a/ema/tables.py +++ b/ema/tables.py @@ -103,7 +103,9 @@ class EmaTable(BaseTable, TableRenderMixin): Returns: """ - parcels = value.parcels.values_list( + parcels = value.parcels.filter( + parcelintersection__calculated_on__isnull=False, + ).values_list( "gmrkng", flat=True ).distinct() diff --git a/intervention/tables.py b/intervention/tables.py index fd39ddcf..96d17c59 100644 --- a/intervention/tables.py +++ b/intervention/tables.py @@ -130,7 +130,9 @@ class InterventionTable(BaseTable, TableRenderMixin): Returns: """ - parcels = value.parcels.values_list( + parcels = value.parcels.filter( + parcelintersection__calculated_on__isnull=False, + ).values_list( "gmrkng", flat=True ).distinct() diff --git a/konova/migrations/0003_auto_20220208_1801.py b/konova/migrations/0003_auto_20220208_1801.py new file mode 100644 index 00000000..d1d9b5a0 --- /dev/null +++ b/konova/migrations/0003_auto_20220208_1801.py @@ -0,0 +1,54 @@ +# Generated by Django 3.1.3 on 2022-02-08 17:01 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +def migrate_parcels(apps, schema_editor): + Geometry = apps.get_model('konova', 'Geometry') + SpatialIntersection = apps.get_model('konova', 'SpatialIntersection') + + all_geoms = Geometry.objects.all() + for geom in all_geoms: + SpatialIntersection.objects.bulk_create([ + SpatialIntersection(geometry=geom, parcel=parcel) + for parcel in geom.parcels.all() + ]) + + +class Migration(migrations.Migration): + + dependencies = [ + ('konova', '0002_auto_20220114_0936'), + ] + + operations = [ + migrations.CreateModel( + name='SpatialIntersection', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('calculated_on', models.DateTimeField(auto_now_add=True, null=True)), + ('geometry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='konova.geometry')), + ('parcel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='konova.parcel')), + ], + options={ + 'abstract': False, + }, + ), + migrations.RunPython(migrate_parcels), + migrations.AddField( + model_name='parcel', + name='geometries_tmp', + field=models.ManyToManyField(blank=True, related_name='parcels', through='konova.SpatialIntersection', to='konova.Geometry'), + ), + migrations.RemoveField( + model_name='parcel', + name='geometries', + ), + migrations.RenameField( + model_name='parcel', + old_name='geometries_tmp', + new_name='geometries', + ), + ] diff --git a/konova/migrations/0004_auto_20220209_0839.py b/konova/migrations/0004_auto_20220209_0839.py new file mode 100644 index 00000000..fe41eada --- /dev/null +++ b/konova/migrations/0004_auto_20220209_0839.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1.3 on 2022-02-09 07:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('konova', '0003_auto_20220208_1801'), + ] + + operations = [ + migrations.RenameModel( + old_name='SpatialIntersection', + new_name='ParcelIntersection', + ), + ] diff --git a/konova/models/geometry.py b/konova/models/geometry.py index 0a380b48..bec89c39 100644 --- a/konova/models/geometry.py +++ b/konova/models/geometry.py @@ -99,7 +99,7 @@ class Geometry(BaseResource): Returns: """ - from konova.models import Parcel, District + from konova.models import Parcel, District, ParcelIntersection parcel_fetcher = ParcelWFSFetcher( geometry_id=self.id, ) @@ -107,6 +107,7 @@ class Geometry(BaseResource): fetched_parcels = parcel_fetcher.get_features( typename ) + _now = timezone.now() underlying_parcels = [] for result in fetched_parcels: fetched_parcel = result[typename] @@ -125,19 +126,35 @@ class Geometry(BaseResource): krs=fetched_parcel["ave:kreis"], )[0] parcel_obj.district = district - parcel_obj.updated_on = timezone.now() + parcel_obj.updated_on = _now parcel_obj.save() underlying_parcels.append(parcel_obj) + # Update the 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"] + ) + 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( + + parcels = self.parcels.filter( + parcelintersection__calculated_on__isnull=False, + ).prefetch_related( "district" ).order_by( "gmrkng", diff --git a/konova/models/parcel.py b/konova/models/parcel.py index 487225e6..9c887f1a 100644 --- a/konova/models/parcel.py +++ b/konova/models/parcel.py @@ -22,7 +22,7 @@ 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", blank=True) + geometries = models.ManyToManyField("konova.Geometry", blank=True, related_name="parcels", through='ParcelIntersection') district = models.ForeignKey("konova.District", on_delete=models.SET_NULL, null=True, blank=True, related_name="parcels") gmrkng = models.CharField( max_length=1000, @@ -77,3 +77,22 @@ class District(UuidModel): def __str__(self): return f"{self.gmnd} | {self.krs}" + + +class ParcelIntersection(UuidModel): + """ ParcelIntersection is an intermediary model, which is used to configure 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 4c528038..c74a2bd7 100644 --- a/konova/tasks.py +++ b/konova/tasks.py @@ -4,13 +4,19 @@ from celery import shared_task from django.core.exceptions import ObjectDoesNotExist - @shared_task def celery_update_parcels(geometry_id: str, recheck: bool = True): - from konova.models import Geometry + from konova.models import Geometry, ParcelIntersection 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: