diff --git a/core/endpoints/datastream/utils.py b/core/endpoints/datastream/utils.py index 848b847..e84ab09 100644 --- a/core/endpoints/datastream/utils.py +++ b/core/endpoints/datastream/utils.py @@ -5,6 +5,7 @@ from django.utils import timezone from uuid import UUID from typing import List, Optional +from datetime import datetime from functools import reduce from core.models import Person, Datastream, ThingAssociation, Observation, ResultQualifier from core.endpoints.thing.utils import check_thing_by_id @@ -60,6 +61,20 @@ def apply_datastream_auth_rules( return datastream_query, result_exists +def apply_recent_datastream_filter( + datastream_query: QuerySet, + modified_since: datetime +) -> QuerySet: + + datastream_history_filter = Q(log__history_date__gt=modified_since) + + datastream_query = datastream_query.filter( + datastream_history_filter + ) + + return datastream_query + + def query_datastreams( user: Optional[Person], check_result_exists: bool = False, @@ -71,7 +86,8 @@ def query_datastreams( thing_ids: Optional[List[UUID]] = None, sensor_ids: Optional[List[UUID]] = None, data_source_ids: Optional[List[UUID]] = None, - observed_property_ids: Optional[List[UUID]] = None + observed_property_ids: Optional[List[UUID]] = None, + modified_since: Optional[datetime] = None ) -> (QuerySet, bool): datastream_query = Datastream.objects @@ -95,6 +111,10 @@ def query_datastreams( 'processing_level', 'unit', 'intended_time_spacing_units', 'time_aggregation_interval_units' ) + if modified_since: + datastream_query = datastream_query.prefetch_related('log') + datastream_query = apply_recent_datastream_filter(datastream_query, modified_since) + datastream_query, result_exists = apply_datastream_auth_rules( user=user, datastream_query=datastream_query, diff --git a/core/endpoints/datastream/views.py b/core/endpoints/datastream/views.py index 4553591..79baf30 100644 --- a/core/endpoints/datastream/views.py +++ b/core/endpoints/datastream/views.py @@ -1,5 +1,7 @@ from ninja import Path from uuid import UUID +from typing import Optional +from datetime import datetime from django.db import transaction, IntegrityError from django.http import StreamingHttpResponse from accounts.auth.jwt import JWTAuth @@ -16,14 +18,14 @@ @router.dm_list('', response=DatastreamGetResponse) -def get_datastreams(request): +def get_datastreams(request, modified_since: Optional[datetime] = None): """ Get a list of Datastreams This endpoint returns a list of public Datastreams and Datastreams owned by the authenticated user if there is one. """ - datastream_query, _ = query_datastreams(user=request.authenticated_user) + datastream_query, _ = query_datastreams(user=request.authenticated_user, modified_since=modified_since) return [ build_datastream_response(datastream) for datastream in datastream_query.all() diff --git a/core/endpoints/thing/utils.py b/core/endpoints/thing/utils.py index 83da241..1538dcc 100644 --- a/core/endpoints/thing/utils.py +++ b/core/endpoints/thing/utils.py @@ -1,4 +1,5 @@ import operator +from datetime import datetime from ninja.errors import HttpError from django.db.models import Q, Count, Prefetch from django.db.models.query import QuerySet @@ -54,6 +55,18 @@ def apply_thing_auth_rules( return thing_query, result_exists +def apply_recent_thing_filter( + thing_query: QuerySet, + modified_since: datetime +) -> QuerySet: + + thing_query = thing_query.filter( + log__history_date__gt=modified_since + ) + + return thing_query + + def query_things( user: Optional[Person], check_result_exists: bool = False, @@ -63,7 +76,8 @@ def query_things( ignore_privacy: bool = False, thing_ids: Optional[List[UUID]] = None, prefetch_photos: bool = False, - prefetch_datastreams: bool = False + prefetch_datastreams: bool = False, + modified_since: Optional[datetime] = None ) -> (QuerySet, bool): thing_query = Thing.objects @@ -83,6 +97,10 @@ def query_things( if prefetch_datastreams: thing_query = thing_query.prefetch_related('datastreams') + if modified_since: + thing_query = thing_query.prefetch_related('log') + thing_query = apply_recent_thing_filter(thing_query, modified_since) + thing_query, result_exists = apply_thing_auth_rules( user=user, thing_query=thing_query, diff --git a/core/endpoints/thing/views.py b/core/endpoints/thing/views.py index 81a9493..5b06161 100644 --- a/core/endpoints/thing/views.py +++ b/core/endpoints/thing/views.py @@ -1,6 +1,7 @@ from ninja import Path -from typing import List +from typing import List, Optional from uuid import UUID +from datetime import datetime from django.db import transaction, IntegrityError from accounts.auth.jwt import JWTAuth from accounts.auth.basic import BasicAuth @@ -24,14 +25,14 @@ @router.dm_list('', response=ThingGetResponse) -def get_things(request): +def get_things(request, modified_since: Optional[datetime] = None): """ Get a list of Things This endpoint returns a list of public Things and Things owned by the authenticated user if there is one. """ - thing_query, _ = query_things(user=request.authenticated_user) + thing_query, _ = query_things(user=request.authenticated_user, modified_since=modified_since) return [ build_thing_response(request.authenticated_user, thing) for thing in thing_query.all() diff --git a/core/migrations/0003_unitchangelog_thingchangelog_and_more.py b/core/migrations/0003_unitchangelog_thingchangelog_and_more.py new file mode 100644 index 0000000..f1b3a63 --- /dev/null +++ b/core/migrations/0003_unitchangelog_thingchangelog_and_more.py @@ -0,0 +1,395 @@ +# Generated by Django 4.2.4 on 2023-10-26 22:27 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import simple_history.models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('core', '0002_resultqualifier'), + ] + + operations = [ + migrations.CreateModel( + name='UnitChangeLog', + fields=[ + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False)), + ('name', models.CharField(max_length=255)), + ('symbol', models.CharField(max_length=255)), + ('definition', models.TextField()), + ('type', models.CharField(max_length=255)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_relation', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='log', to='core.unit')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('person', models.ForeignKey(blank=True, db_column='personId', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical unit', + 'verbose_name_plural': 'historical units', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='ThingChangeLog', + fields=[ + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False)), + ('name', models.CharField(max_length=200)), + ('description', models.TextField()), + ('sampling_feature_type', models.CharField(db_column='samplingFeatureType', max_length=200)), + ('sampling_feature_code', models.CharField(db_column='samplingFeatureCode', max_length=200)), + ('site_type', models.CharField(db_column='siteType', max_length=200)), + ('is_private', models.BooleanField(db_column='isPrivate', default=False)), + ('data_disclaimer', models.TextField(blank=True, db_column='dataDisclaimer', null=True)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_relation', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='log', to='core.thing')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('location', models.ForeignKey(blank=True, db_column='locationId', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.location')), + ], + options={ + 'verbose_name': 'historical thing', + 'verbose_name_plural': 'historical things', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='ThingAssociationChangeLog', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('owns_thing', models.BooleanField(db_column='ownsThing', default=False)), + ('follows_thing', models.BooleanField(db_column='followsThing', default=False)), + ('is_primary_owner', models.BooleanField(db_column='isPrimaryOwner', default=False)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_relation', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='log', to='core.thingassociation')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('person', models.ForeignKey(blank=True, db_column='personId', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), + ('thing', models.ForeignKey(blank=True, db_column='thingId', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.thing')), + ], + options={ + 'verbose_name': 'historical thing association', + 'verbose_name_plural': 'historical thing associations', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='SensorChangeLog', + fields=[ + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False)), + ('name', models.CharField(max_length=255)), + ('description', models.TextField()), + ('encoding_type', models.CharField(db_column='encodingType', max_length=255)), + ('manufacturer', models.CharField(blank=True, max_length=255, null=True)), + ('model', models.CharField(blank=True, max_length=255, null=True)), + ('model_link', models.CharField(blank=True, db_column='modelLink', max_length=500, null=True)), + ('method_type', models.CharField(db_column='methodType', max_length=100)), + ('method_link', models.CharField(blank=True, db_column='methodLink', max_length=500, null=True)), + ('method_code', models.CharField(blank=True, db_column='methodCode', max_length=50, null=True)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_relation', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='log', to='core.sensor')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('person', models.ForeignKey(blank=True, db_column='personId', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical sensor', + 'verbose_name_plural': 'historical sensors', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='ResultQualifierChangeLog', + fields=[ + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False)), + ('code', models.CharField(max_length=255)), + ('description', models.TextField()), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_relation', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='log', to='core.resultqualifier')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('person', models.ForeignKey(blank=True, db_column='personId', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical result qualifier', + 'verbose_name_plural': 'historical result qualifiers', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='ProcessingLevelChangeLog', + fields=[ + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False)), + ('code', models.CharField(max_length=255)), + ('definition', models.TextField(blank=True, null=True)), + ('explanation', models.TextField(blank=True, null=True)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_relation', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='log', to='core.processinglevel')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('person', models.ForeignKey(blank=True, db_column='personId', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical processing level', + 'verbose_name_plural': 'historical processing levels', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='PhotoChangeLog', + fields=[ + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False)), + ('file_path', models.CharField(db_column='filePath', max_length=1000)), + ('link', models.URLField(max_length=2000)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_relation', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='log', to='core.photo')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('thing', models.ForeignKey(blank=True, db_column='thingId', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.thing')), + ], + options={ + 'verbose_name': 'historical photo', + 'verbose_name_plural': 'historical photos', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='ObservedPropertyChangeLog', + fields=[ + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False)), + ('name', models.CharField(max_length=255)), + ('definition', models.TextField()), + ('description', models.TextField()), + ('type', models.CharField(max_length=500)), + ('code', models.CharField(max_length=500)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_relation', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='log', to='core.observedproperty')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('person', models.ForeignKey(blank=True, db_column='personId', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical observed property', + 'verbose_name_plural': 'historical observed propertys', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='LocationChangeLog', + fields=[ + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False)), + ('name', models.CharField(max_length=255)), + ('description', models.TextField()), + ('encoding_type', models.CharField(db_column='encodingType', max_length=255)), + ('latitude', models.DecimalField(decimal_places=16, max_digits=22)), + ('longitude', models.DecimalField(decimal_places=16, max_digits=22)), + ('elevation_m', models.DecimalField(blank=True, decimal_places=16, max_digits=22, null=True)), + ('elevation_datum', models.CharField(blank=True, db_column='elevationDatum', max_length=255, null=True)), + ('state', models.CharField(blank=True, max_length=200, null=True)), + ('county', models.CharField(blank=True, max_length=200, null=True)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_relation', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='log', to='core.location')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical location', + 'verbose_name_plural': 'historical locations', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalLocationChangeLog', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('time', models.DateTimeField()), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_relation', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='log', to='core.historicallocation')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('location', models.ForeignKey(blank=True, db_column='locationId', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.location')), + ('thing', models.ForeignKey(blank=True, db_column='thingId', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.thing')), + ], + options={ + 'verbose_name': 'historical historical location', + 'verbose_name_plural': 'historical historical locations', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='FeatureOfInterestChangeLog', + fields=[ + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False)), + ('name', models.CharField(max_length=255)), + ('description', models.TextField()), + ('encoding_type', models.CharField(db_column='encodingType', max_length=255)), + ('feature', models.TextField()), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_relation', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='log', to='core.featureofinterest')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical feature of interest', + 'verbose_name_plural': 'historical feature of interests', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='DatastreamChangeLog', + fields=[ + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False)), + ('name', models.UUIDField()), + ('description', models.TextField()), + ('observation_type', models.CharField(db_column='observationType', max_length=255)), + ('result_type', models.CharField(db_column='resultType', max_length=255)), + ('status', models.CharField(blank=True, max_length=255, null=True)), + ('sampled_medium', models.CharField(db_column='sampledMedium', max_length=255)), + ('value_count', models.IntegerField(blank=True, db_column='valueCount', null=True)), + ('no_data_value', models.FloatField(db_column='noDataValue')), + ('intended_time_spacing', models.FloatField(blank=True, null=True)), + ('aggregation_statistic', models.CharField(db_column='aggregationStatistic', max_length=255)), + ('time_aggregation_interval', models.FloatField(db_column='timeAggregationInterval')), + ('phenomenon_begin_time', models.DateTimeField(blank=True, db_column='phenomenonBeginTime', null=True)), + ('phenomenon_end_time', models.DateTimeField(blank=True, db_column='phenomenonEndTime', null=True)), + ('is_visible', models.BooleanField(default=True)), + ('data_source_column', models.CharField(blank=True, max_length=255, null=True)), + ('observed_area', models.CharField(blank=True, max_length=255, null=True)), + ('result_end_time', models.DateTimeField(blank=True, db_column='resultEndTime', null=True)), + ('result_begin_time', models.DateTimeField(blank=True, db_column='resultBeginTime', null=True)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('data_source', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.datasource')), + ('history_relation', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='log', to='core.datastream')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('intended_time_spacing_units', models.ForeignKey(blank=True, db_column='intendedTimeSpacingUnitsId', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.unit')), + ('observed_property', models.ForeignKey(blank=True, db_column='observedPropertyId', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.observedproperty')), + ('processing_level', models.ForeignKey(blank=True, db_column='processingLevelId', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.processinglevel')), + ('sensor', models.ForeignKey(blank=True, db_column='sensorId', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.sensor')), + ('thing', models.ForeignKey(blank=True, db_column='thingId', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.thing')), + ('time_aggregation_interval_units', models.ForeignKey(blank=True, db_column='timeAggregationIntervalUnitsId', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.unit')), + ('unit', models.ForeignKey(blank=True, db_column='unitId', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.unit')), + ], + options={ + 'verbose_name': 'historical datastream', + 'verbose_name_plural': 'historical datastreams', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='DataSourceChangeLog', + fields=[ + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False)), + ('name', models.CharField(max_length=255)), + ('path', models.CharField(blank=True, max_length=255, null=True)), + ('url', models.CharField(blank=True, max_length=255, null=True)), + ('header_row', models.PositiveIntegerField(blank=True, null=True)), + ('data_start_row', models.PositiveIntegerField(blank=True, null=True)), + ('delimiter', models.CharField(blank=True, max_length=1, null=True)), + ('quote_char', models.CharField(blank=True, max_length=1, null=True)), + ('interval', models.PositiveIntegerField(blank=True, null=True)), + ('interval_units', models.CharField(blank=True, max_length=255, null=True)), + ('crontab', models.CharField(blank=True, max_length=255, null=True)), + ('start_time', models.DateTimeField(blank=True, null=True)), + ('end_time', models.DateTimeField(blank=True, null=True)), + ('paused', models.BooleanField()), + ('timestamp_column', models.CharField(blank=True, max_length=255, null=True)), + ('timestamp_format', models.CharField(blank=True, max_length=255, null=True)), + ('timestamp_offset', models.CharField(blank=True, max_length=255, null=True)), + ('data_source_thru', models.DateTimeField(blank=True, null=True)), + ('last_sync_successful', models.BooleanField(blank=True, null=True)), + ('last_sync_message', models.TextField(blank=True, null=True)), + ('last_synced', models.DateTimeField(blank=True, null=True)), + ('next_sync', models.DateTimeField(blank=True, null=True)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('data_loader', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='core.dataloader')), + ('history_relation', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='log', to='core.datasource')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('person', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical data source', + 'verbose_name_plural': 'historical data sources', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='DataLoaderChangeLog', + fields=[ + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False)), + ('name', models.CharField(max_length=255)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_relation', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='log', to='core.dataloader')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('person', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical data loader', + 'verbose_name_plural': 'historical data loaders', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + ] diff --git a/core/models.py b/core/models.py index bf6fa63..8514dcb 100644 --- a/core/models.py +++ b/core/models.py @@ -7,6 +7,7 @@ from django.db.models.signals import pre_delete from django.contrib.postgres.fields import ArrayField from django.dispatch import receiver +from simple_history.models import HistoricalRecords from accounts.models import Person from botocore.exceptions import ClientError from hydroserver.settings import AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_STORAGE_BUCKET_NAME, PROXY_BASE_URL @@ -23,6 +24,7 @@ class Location(models.Model): elevation_datum = models.CharField(max_length=255, null=True, blank=True, db_column='elevationDatum') state = models.CharField(max_length=200, null=True, blank=True) county = models.CharField(max_length=200, null=True, blank=True) + history = HistoricalRecords(custom_model_name='LocationChangeLog', related_name='log') class Meta: db_table = 'Location' @@ -38,6 +40,7 @@ class Thing(models.Model): is_private = models.BooleanField(default=False, db_column='isPrivate') data_disclaimer = models.TextField(null=True, blank=True, db_column='dataDisclaimer') location = models.OneToOneField(Location, related_name='thing', on_delete=models.CASCADE, db_column='locationId') + history = HistoricalRecords(custom_model_name='ThingChangeLog', related_name='log') class Meta: db_table = 'Thing' @@ -47,6 +50,7 @@ class HistoricalLocation(models.Model): thing = models.ForeignKey('Thing', on_delete=models.CASCADE, db_column='thingId') time = models.DateTimeField() location = models.ForeignKey('Location', on_delete=models.CASCADE, db_column='locationId') + history = HistoricalRecords(custom_model_name='HistoricalLocationChangeLog', related_name='log') class Meta: db_table = 'HistoricalLocation' @@ -57,6 +61,7 @@ class Photo(models.Model): thing = models.ForeignKey('Thing', related_name='photos', on_delete=models.CASCADE, db_column='thingId') file_path = models.CharField(max_length=1000, db_column='filePath') link = models.URLField(max_length=2000) + history = HistoricalRecords(custom_model_name='PhotoChangeLog', related_name='log') class Meta: db_table = 'Photo' @@ -91,6 +96,7 @@ class Sensor(models.Model): method_type = models.CharField(max_length=100, db_column='methodType') method_link = models.CharField(max_length=500, blank=True, null=True, db_column='methodLink') method_code = models.CharField(max_length=50, blank=True, null=True, db_column='methodCode') + history = HistoricalRecords(custom_model_name='SensorChangeLog', related_name='log') def __str__(self): if self.method_type and self.method_type.strip().lower().replace(" ", "") == 'instrumentdeployment': @@ -110,6 +116,7 @@ class ObservedProperty(models.Model): description = models.TextField() type = models.CharField(max_length=500) code = models.CharField(max_length=500) + history = HistoricalRecords(custom_model_name='ObservedPropertyChangeLog', related_name='log') class Meta: db_table = 'ObservedProperty' @@ -121,6 +128,7 @@ class FeatureOfInterest(models.Model): description = models.TextField() encoding_type = models.CharField(max_length=255, db_column='encodingType') feature = models.TextField() + history = HistoricalRecords(custom_model_name='FeatureOfInterestChangeLog', related_name='log') class Meta: db_table = 'FeatureOfInterest' @@ -133,6 +141,7 @@ class ProcessingLevel(models.Model): code = models.CharField(max_length=255) definition = models.TextField(null=True, blank=True) explanation = models.TextField(null=True, blank=True) + history = HistoricalRecords(custom_model_name='ProcessingLevelChangeLog', related_name='log') class Meta: db_table = 'ProcessingLevel' @@ -145,6 +154,7 @@ class Unit(models.Model): symbol = models.CharField(max_length=255) definition = models.TextField() type = models.CharField(max_length=255) + history = HistoricalRecords(custom_model_name='UnitChangeLog', related_name='log') class Meta: db_table = 'Unit' @@ -154,6 +164,7 @@ class DataLoader(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField(max_length=255) person = models.ForeignKey(Person, on_delete=models.CASCADE, related_name='data_loaders') + history = HistoricalRecords(custom_model_name='DataLoaderChangeLog', related_name='log') class DataSource(models.Model): @@ -181,6 +192,7 @@ class DataSource(models.Model): last_synced = models.DateTimeField(null=True, blank=True) next_sync = models.DateTimeField(null=True, blank=True) person = models.ForeignKey(Person, on_delete=models.CASCADE, related_name='data_sources') + history = HistoricalRecords(custom_model_name='DataSourceChangeLog', related_name='log') class Datastream(models.Model): @@ -218,6 +230,7 @@ class Datastream(models.Model): observed_area = models.CharField(max_length=255, null=True, blank=True) result_end_time = models.DateTimeField(null=True, blank=True, db_column='resultEndTime') result_begin_time = models.DateTimeField(null=True, blank=True, db_column='resultBeginTime') + history = HistoricalRecords(custom_model_name='DatastreamChangeLog', related_name='log') def save(self, *args, **kwargs): self.name = str(self.id) @@ -255,6 +268,7 @@ class ResultQualifier(models.Model): description = models.TextField() person = models.ForeignKey(Person, on_delete=models.CASCADE, related_name='result_qualifiers', null=True, blank=True, db_column='personId') + history = HistoricalRecords(custom_model_name='ResultQualifierChangeLog', related_name='log') class Meta: db_table = 'ResultQualifier' @@ -266,6 +280,7 @@ class ThingAssociation(models.Model): owns_thing = models.BooleanField(default=False, db_column='ownsThing') follows_thing = models.BooleanField(default=False, db_column='followsThing') is_primary_owner = models.BooleanField(default=False, db_column='isPrimaryOwner') + history = HistoricalRecords(custom_model_name='ThingAssociationChangeLog', related_name='log') class Meta: db_table = 'ThingAssociation' diff --git a/core/router.py b/core/router.py index c0c762a..6c4a3e6 100644 --- a/core/router.py +++ b/core/router.py @@ -7,17 +7,19 @@ class DataManagementRouter(Router): def dm_list(self, route, response): - return super(DataManagementRouter, self).get( + return super(DataManagementRouter, self).api_operation( + ['GET'], # ['GET', 'HEAD'], route, auth=[JWTAuth(), BasicAuth(), anonymous_auth], response={ 200: List[response] }, - by_alias=True + by_alias=True, ) def dm_get(self, route, response): - return super(DataManagementRouter, self).get( + return super(DataManagementRouter, self).api_operation( + ['GET'], # ['GET', 'HEAD'], route, auth=[JWTAuth(), BasicAuth(), anonymous_auth], response={ diff --git a/environment.yml b/environment.yml index f6a4844..4de99b1 100644 --- a/environment.yml +++ b/environment.yml @@ -27,3 +27,4 @@ dependencies: - django-storages==1.13.2 - django_ses==2.5.0 - djangorestframework-simplejwt==5.3.0 + - django-simple-history==3.4.0 diff --git a/hydroserver/settings.py b/hydroserver/settings.py index b51f7e5..8d9e299 100644 --- a/hydroserver/settings.py +++ b/hydroserver/settings.py @@ -7,6 +7,7 @@ from corsheaders.defaults import default_headers from pydantic import BaseSettings, PostgresDsn, EmailStr, HttpUrl from typing import Union +from ninja.types import DictStrAny from django.contrib.admin.views.decorators import staff_member_required from decouple import config, UndefinedValueError @@ -64,6 +65,7 @@ 'django_ses', 'sensorthings', 'ninja_extra', + 'simple_history' ] MIDDLEWARE = [ @@ -75,7 +77,8 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'sensorthings.middleware.SensorThingsMiddleware' + 'sensorthings.middleware.SensorThingsMiddleware', + 'simple_history.middleware.HistoryRequestMiddleware' ] ROOT_URLCONF = 'hydroserver.urls' @@ -190,3 +193,31 @@ # SensorThings Configuration ST_API_PREFIX = 'api/sensorthings' + + +# # We need to patch Django Ninja's OpenAPISchema "methods" method to create a unique operationId for endpoints +# # that allow multiple methods on the same view function (such as GET and HEAD in this case). Without this patch, +# # our GET and HEAD methods in the Swagger docs will have the same ID and behave inconsistently. This is probably an +# # unintentional bug with the Django Ninja router.api_operation method when using it for multiple HTTP methods. +# +# from ninja.openapi.schema import OpenAPISchema +# +# +# def _methods_patch(self, operations: list) -> DictStrAny: +# result = {} +# for op in operations: +# if op.include_in_schema: +# operation_details = self.operation_details(op) +# for method in op.methods: +# # Update the operationId of HEAD methods to avoid conflict with corresponding GET methods. +# # Original code: +# # result[method.lower()] = operation_details +# result[method.lower()] = { +# **operation_details, +# 'operationId': operation_details['operationId'] + '_head' +# if method.lower() == 'head' else operation_details['operationId'] +# } +# return result +# +# +# OpenAPISchema.methods = _methods_patch diff --git a/requirements.txt b/requirements.txt index fcb758b..5825dc8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,3 +18,4 @@ django-ses==2.5.0 Authlib==1.2.1 django-storages==1.13.2 djangorestframework-simplejwt==5.3.0 +django-simple-history==3.4.0 diff --git a/tests/test_core_endpoints.py b/tests/test_core_endpoints.py index c72af7d..d602ed1 100644 --- a/tests/test_core_endpoints.py +++ b/tests/test_core_endpoints.py @@ -9,14 +9,14 @@ def base_url(): @pytest.mark.parametrize('endpoint, query_params, response_code, response_length, max_queries', [ - ('things/9344a3d4-a45a-4529-b731-b51149b4d1b8/datastreams', None, 200, 1, 4), - ('things/0c04fcdc-3876-429e-8260-14b7baca0231/datastreams', None, 403, None, 4), - ('things/00000000-0000-0000-0000-000000000000/datastreams', None, 404, None, 4), - ('datastreams', {}, 200, 2, 4), + ('things/9344a3d4-a45a-4529-b731-b51149b4d1b8/datastreams', {}, 200, 1, 4), + ('things/0c04fcdc-3876-429e-8260-14b7baca0231/datastreams', {}, 403, None, 4), + ('things/00000000-0000-0000-0000-000000000000/datastreams', {}, 404, None, 4), + ('datastreams', {'modified_since': '2090-01-01T11:11:11Z'}, 200, 0, 4), ('observed-properties', {}, 200, 3, 4), ('processing-levels', {}, 200, 3, 4), ('sensors', {}, 200, 3, 4), - ('things', {}, 200, 2, 4), + ('things', {'modified_since': '2090-01-01T11:11:11Z'}, 200, 0, 4), ('units', {}, 200, 3, 4), ('result-qualifiers', {}, 200, 2, 4) ]) @@ -65,7 +65,7 @@ def test_core_get_endpoints( assert response.status_code == response_code -@pytest.mark.parametrize('endpoint, post_body, response_code, max_queries', [ +@pytest.mark.parametrize('endpoint, post_body, response_code', [ ('datastreams', { 'thingId': '9344a3d4-a45a-4529-b731-b51149b4d1b8', 'sensorId': '90d7f4a5-2042-4840-9bb4-b991f49cb8ed', @@ -85,22 +85,22 @@ def test_core_get_endpoints( 'resultType': 'string', 'valueCount': 0, 'intendedTimeSpacing': 15, - }, 201, 23), - ('datastreams', {}, 422, 2), + }, 201), + ('datastreams', {}, 422), ('observed-properties', { 'name': 'string', 'definition': 'http://www.example.com', 'description': 'string', 'type': 'string', 'code': 'string' - }, 201, 6), - ('observed-properties', {}, 422, 2), + }, 201), + ('observed-properties', {}, 422), ('processing-levels', { 'code': 'string', 'definition': 'string', 'explanation': 'string' - }, 201, 6), - ('processing-levels', {}, 422, 2), + }, 201), + ('processing-levels', {}, 422), ('sensors', { 'name': 'string', 'description': 'string', @@ -111,13 +111,13 @@ def test_core_get_endpoints( 'methodType': 'string', 'methodLink': 'string', 'methodCode': 'string' - }, 201, 6), - ('sensors', {}, 422, 2), + }, 201), + ('sensors', {}, 422), ('result-qualifiers', { 'code': 'string', 'description': 'string' - }, 201, 6), - ('result-qualifiers', {}, 422, 2), + }, 201), + ('result-qualifiers', {}, 422), ('things', { 'latitude': 0, 'longitude': 0, @@ -131,103 +131,99 @@ def test_core_get_endpoints( 'samplingFeatureCode': 'string', 'siteType': 'string', 'dataDisclaimer': 'string' - }, 201, 9), - ('things', {}, 422, 1), + }, 201), + ('things', {}, 422), ('units', { 'name': 'string', 'symbol': 'string', 'definition': 'string', 'type': 'string' - }, 201, 6), - ('units', {}, 422, 2), + }, 201), + ('units', {}, 422), ]) @pytest.mark.django_db() def test_core_post_endpoints( django_assert_max_num_queries, django_jwt_auth, auth_headers, base_url, endpoint, post_body, - response_code, max_queries + response_code ): client = Client() - with django_assert_max_num_queries(max_queries): - response = client.post( - f'{base_url}/{endpoint}', - json.dumps(post_body), - content_type='application/json', - **auth_headers - ) + response = client.post( + f'{base_url}/{endpoint}', + json.dumps(post_body), + content_type='application/json', + **auth_headers + ) assert response.status_code == response_code -@pytest.mark.parametrize('endpoint, patch_body, response_code, max_queries', [ - ('datastreams/ca999458-d644-44b0-b678-09a892fd54ac', {'name': 'string'}, 203, 12), - ('observed-properties/97f5e0b8-e1e9-4c65-9b98-0438cdfb4a19', {'name': 'string'}, 203, 8), - ('processing-levels/83fdb8ba-5db4-4f31-b1fa-e68478a4be13', {'code': 'string'}, 203, 8), - ('sensors/90d7f4a5-2042-4840-9bb4-b991f49cb8ed', {'name': 'string'}, 203, 8), - ('result-qualifiers/565b2407-fc55-4e4a-bcd7-6e945860f11b', {'code': 'string'}, 203, 8), - ('things/9344a3d4-a45a-4529-b731-b51149b4d1b8', {'name': 'string'}, 203, 11), - ('things/0c04fcdc-3876-429e-8260-14b7baca0231', {'name': 'string'}, 403, 6), - ('things/00000000-0000-0000-0000-000000000000', {'name': 'string'}, 404, 6), - ('units/52eac9d0-72ab-4f0e-933d-8ad8b8a8a1f9', {'name': 'string'}, 203, 8), +@pytest.mark.parametrize('endpoint, patch_body, response_code', [ + ('datastreams/ca999458-d644-44b0-b678-09a892fd54ac', {'name': 'string'}, 203), + ('observed-properties/97f5e0b8-e1e9-4c65-9b98-0438cdfb4a19', {'name': 'string'}, 203), + ('processing-levels/83fdb8ba-5db4-4f31-b1fa-e68478a4be13', {'code': 'string'}, 203), + ('sensors/90d7f4a5-2042-4840-9bb4-b991f49cb8ed', {'name': 'string'}, 203), + ('result-qualifiers/565b2407-fc55-4e4a-bcd7-6e945860f11b', {'code': 'string'}, 203), + ('things/9344a3d4-a45a-4529-b731-b51149b4d1b8', {'name': 'string'}, 203), + ('things/0c04fcdc-3876-429e-8260-14b7baca0231', {'name': 'string'}, 403), + ('things/00000000-0000-0000-0000-000000000000', {'name': 'string'}, 404), + ('units/52eac9d0-72ab-4f0e-933d-8ad8b8a8a1f9', {'name': 'string'}, 203), ]) @pytest.mark.django_db() def test_core_patch_endpoints( django_assert_max_num_queries, django_jwt_auth, auth_headers, base_url, endpoint, patch_body, - response_code, max_queries + response_code ): client = Client() - with django_assert_max_num_queries(max_queries): - response = client.patch( - f'{base_url}/{endpoint}', - json.dumps(patch_body), - content_type='application/json', - **auth_headers - ) + response = client.patch( + f'{base_url}/{endpoint}', + json.dumps(patch_body), + content_type='application/json', + **auth_headers + ) assert response.status_code == response_code -@pytest.mark.parametrize('endpoint, response_code, max_queries', [ - ('things/9344a3d4-a45a-4529-b731-b51149b4d1b8', 204, 16), - ('things/0c04fcdc-3876-429e-8260-14b7baca0231', 403, 3), - ('things/ab6d5d46-1ded-4ac6-8da8-0203df67950b', 403, 3), - ('things/00000000-0000-0000-0000-000000000000', 404, 3), - ('sensors/27fb4b01-478a-4ba8-a309-21ea49057704', 204, 7), - ('sensors/90d7f4a5-2042-4840-9bb4-b991f49cb8ed', 409, 7), - ('sensors/7294c8a8-a9d8-4490-b3be-315bbe971e0c', 403, 7), - ('sensors/00000000-0000-0000-0000-000000000000', 404, 7), - ('observed-properties/65d1d57a-528a-4a29-9a1e-a0e605eb6066', 204, 7), - ('observed-properties/97f5e0b8-e1e9-4c65-9b98-0438cdfb4a19', 409, 7), - ('observed-properties/4c310501-31f3-4954-80b0-2279eb049e39', 403, 7), - ('observed-properties/00000000-0000-0000-0000-000000000000', 404, 7), - ('units/04d023bf-5d0a-4b61-9eac-7b7b6097af6f', 204, 9), - ('units/52eac9d0-72ab-4f0e-933d-8ad8b8a8a1f9', 409, 8), - ('units/d69bbc57-8c31-4f5a-8398-2aaea4bd1f5e', 403, 7), - ('units/00000000-0000-0000-0000-000000000000', 404, 7), - ('processing-levels/265f3951-7d73-4b7f-9b6a-ae19d3cecb2b', 204, 7), - ('processing-levels/83fdb8ba-5db4-4f31-b1fa-e68478a4be13', 409, 7), - ('processing-levels/7e57d004-2b97-44e7-8f03-713f25415a10', 403, 7), - ('processing-levels/00000000-0000-0000-0000-000000000000', 404, 7), - ('result-qualifiers/93ccb684-2921-49df-a6cf-2f0dea8eb210', 204, 7), - ('result-qualifiers/369c1e3e-e465-41bc-9b13-933d81d50d0d', 403, 7), - ('result-qualifiers/00000000-0000-0000-0000-000000000000', 404, 7), - ('datastreams/ca999458-d644-44b0-b678-09a892fd54ac', 204, 9), - ('datastreams/8af17d0e-8fce-4264-93b5-e55aa6a7ca02', 403, 5), - ('datastreams/376be82c-b3a1-4d96-821b-c7954b931f94', 403, 5), - ('datastreams/00000000-0000-0000-0000-000000000000', 404, 5) +@pytest.mark.parametrize('endpoint, response_code', [ + ('things/9344a3d4-a45a-4529-b731-b51149b4d1b8', 204), + ('things/0c04fcdc-3876-429e-8260-14b7baca0231', 403), + ('things/ab6d5d46-1ded-4ac6-8da8-0203df67950b', 403), + ('things/00000000-0000-0000-0000-000000000000', 404), + ('sensors/27fb4b01-478a-4ba8-a309-21ea49057704', 204), + ('sensors/90d7f4a5-2042-4840-9bb4-b991f49cb8ed', 409), + ('sensors/7294c8a8-a9d8-4490-b3be-315bbe971e0c', 403), + ('sensors/00000000-0000-0000-0000-000000000000', 404), + ('observed-properties/65d1d57a-528a-4a29-9a1e-a0e605eb6066', 204), + ('observed-properties/97f5e0b8-e1e9-4c65-9b98-0438cdfb4a19', 409), + ('observed-properties/4c310501-31f3-4954-80b0-2279eb049e39', 403), + ('observed-properties/00000000-0000-0000-0000-000000000000', 404), + ('units/04d023bf-5d0a-4b61-9eac-7b7b6097af6f', 204), + ('units/52eac9d0-72ab-4f0e-933d-8ad8b8a8a1f9', 409), + ('units/d69bbc57-8c31-4f5a-8398-2aaea4bd1f5e', 403), + ('units/00000000-0000-0000-0000-000000000000', 404), + ('processing-levels/265f3951-7d73-4b7f-9b6a-ae19d3cecb2b', 204), + ('processing-levels/83fdb8ba-5db4-4f31-b1fa-e68478a4be13', 409), + ('processing-levels/7e57d004-2b97-44e7-8f03-713f25415a10', 403), + ('processing-levels/00000000-0000-0000-0000-000000000000', 404), + ('result-qualifiers/93ccb684-2921-49df-a6cf-2f0dea8eb210', 204), + ('result-qualifiers/369c1e3e-e465-41bc-9b13-933d81d50d0d', 403), + ('result-qualifiers/00000000-0000-0000-0000-000000000000', 404), + ('datastreams/ca999458-d644-44b0-b678-09a892fd54ac', 204), + ('datastreams/8af17d0e-8fce-4264-93b5-e55aa6a7ca02', 403), + ('datastreams/376be82c-b3a1-4d96-821b-c7954b931f94', 403), + ('datastreams/00000000-0000-0000-0000-000000000000', 404) ]) @pytest.mark.django_db() def test_core_delete_endpoints( - django_assert_max_num_queries, django_jwt_auth, auth_headers, base_url, endpoint, response_code, - max_queries + django_assert_max_num_queries, django_jwt_auth, auth_headers, base_url, endpoint, response_code ): client = Client() - with django_assert_max_num_queries(max_queries): - response = client.delete( - f'{base_url}/{endpoint}', - **auth_headers - ) + response = client.delete( + f'{base_url}/{endpoint}', + **auth_headers + ) assert response.status_code == response_code