diff --git a/CHANGELOG.md b/CHANGELOG.md index ddf3082..1907e02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ -## [0.2.x] - xxxx-xx-xx +## [0.2.1] - 2025-07-01 ### Added ### Changed +- New fields `q_raw`, `v_av`, `v_bulk` in `api.models.time_series.py` for storing raw + optical discharge, average surface velocity and bulk velocity. +- Added new time series fields to admin and API views. ### Deprecated ### Removed ### Fixed @@ -8,6 +11,7 @@ - fixed test assertions by using `assertEqual` instead of deprecated `assertEquals` - fixed wrongly preferred HTML serializer for API POST/GET request to time series objects. This now is JSON by default. +- storage port mapping for internal storage set to correct port. ### Security diff --git a/LiveORC/settings.py b/LiveORC/settings.py index e9f42d0..2c3b8fe 100644 --- a/LiveORC/settings.py +++ b/LiveORC/settings.py @@ -16,7 +16,7 @@ import boto3 # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ -VERSION = "0.2.0" +VERSION = "0.2.1" # Build paths inside the project like this: BASE_DIR / 'subdir'. # try to get BASE_DIR from env variable BASE_DIR = Path(__file__).resolve().parent.parent diff --git a/api/admin/time_series.py b/api/admin/time_series.py index a8018f8..783a869 100644 --- a/api/admin/time_series.py +++ b/api/admin/time_series.py @@ -65,7 +65,7 @@ class TimeSeriesInline(admin.TabularInline): class TimeSeriesAdmin(ExportActionModelAdmin, BaseAdmin): resource_classes = [TimeSeriesResource] export_form_class = CustomExportForm - list_display = ["get_site_name", "timestamp", "str_h", "str_fraction_velocimetry", "str_q_50", 'thumbnail_preview'] + list_display = ["get_site_name", "timestamp", "str_h", "str_fraction_velocimetry", "str_q_50", "str_q_raw", "str_v_bulk", "str_v_av", "thumbnail_preview"] list_filter = ["site__name"] readonly_fields = ( "image_preview", @@ -74,6 +74,11 @@ class TimeSeriesAdmin(ExportActionModelAdmin, BaseAdmin): "str_q_50", "str_q_75", "str_q_95", + "str_q_raw", + "str_v_av", + "str_v_bulk", + "str_wetted_surface", + "str_wetted_perimeter", "str_fraction_velocimetry", "link_video" ) @@ -90,8 +95,11 @@ class TimeSeriesAdmin(ExportActionModelAdmin, BaseAdmin): "str_q_50", "str_q_75", "str_q_95", - "wetted_surface", - "wetted_perimeter", + "str_q_raw", + "str_v_av", + "str_v_bulk", + "str_wetted_surface", + "str_wetted_perimeter", "str_fraction_velocimetry" ] } @@ -142,8 +150,11 @@ def get_readonly_fields(self, request, obj=None): "str_q_50", "str_q_75", "str_q_95", - "wetted_surface", - "wetted_perimeter", + "q_raw", + "str_v_av", + "str_v_bulk", + "str_wetted_surface", + "str_wetted_perimeter", "str_fraction_velocimetry" ) return self.readonly_fields @@ -202,6 +213,37 @@ def str_q_95(self, obj): str_q_95.short_description = 'Discharge 95% [m3/s]' str_q_95.allow_tags = True + def str_q_raw(self, obj): + if obj.q_raw: + return round(obj.q_raw, 2) + str_q_raw.short_description = 'Optical discharge [m3/s]' + str_q_raw.allow_tags = True + + def str_v_av(self, obj): + if obj.v_av: + return round(obj.v_av, 2) + str_v_av.short_description = 'Av. surface velocity [m/s]' + str_v_av.allow_tags = True + + + def str_wetted_surface(self, obj): + if obj.wetted_surface: + return round(obj.wetted_surface, 2) + str_wetted_surface.short_description = 'Wetted surface [m2]' + str_wetted_surface.allow_tags = True + + def str_wetted_perimeter(self, obj): + if obj.wetted_perimeter: + return round(obj.wetted_perimeter, 2) + str_wetted_perimeter.short_description = 'Wetted perimeter [m]' + str_wetted_perimeter.allow_tags = True + + def str_v_bulk(self, obj): + if obj.v_bulk: + return round(obj.v_bulk, 2) + str_v_bulk.short_description = 'Bulk velocity [m/s]' + str_v_bulk.allow_tags = True + def str_fraction_velocimetry(self, obj): if obj.fraction_velocimetry: return round(obj.fraction_velocimetry, 2) diff --git a/api/custom_renderers.py b/api/custom_renderers.py index 4ef1986..607653b 100644 --- a/api/custom_renderers.py +++ b/api/custom_renderers.py @@ -60,6 +60,9 @@ def pijsonify(self, data): "q_50": "q.50", "q_75": "q.75", "q_95": "q.95", + "q_raw": "q.raw", + "v_av": "v.av", + "v_bulk": "v.bulk", "wetted_surface": "A.wet", "wetted_perimeter": "P.wet", "fraction_velocimetry": "v.frac" @@ -71,6 +74,9 @@ def pijsonify(self, data): "q_50": "m3/s", "q_75": "m3/s", "q_95": "m3/s", + "q_raw": "m3/s", + "v_av": "m/s", + "v_bulk": "m/s", "wetted_surface": "m2", "wetted_perimeter": "m", "fraction_velocimetry": "%" diff --git a/api/migrations/0004_timeseries_q_raw_timeseries_v_av_timeseries_v_bulk.py b/api/migrations/0004_timeseries_q_raw_timeseries_v_av_timeseries_v_bulk.py new file mode 100644 index 0000000..a144c95 --- /dev/null +++ b/api/migrations/0004_timeseries_q_raw_timeseries_v_av_timeseries_v_bulk.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.1 on 2025-06-30 15:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0003_alter_cameraconfig_version_alter_recipe_version'), + ] + + operations = [ + migrations.AddField( + model_name='timeseries', + name='q_raw', + field=models.FloatField(blank=True, help_text='River flow measured optically', null=True, verbose_name='Raw discharge'), + ), + migrations.AddField( + model_name='timeseries', + name='v_av', + field=models.FloatField(blank=True, help_text='Average surface velocity [m/s]', null=True), + ), + migrations.AddField( + model_name='timeseries', + name='v_bulk', + field=models.FloatField(blank=True, help_text='Bulk velocity [m/s]', null=True), + ), + ] diff --git a/api/models/time_series.py b/api/models/time_series.py index 0aec206..3026f4d 100644 --- a/api/models/time_series.py +++ b/api/models/time_series.py @@ -37,9 +37,12 @@ class TimeSeries(BaseModel): q_50 = models.FloatField("Discharge median", help_text="Median river flow", null=True, blank=True) q_75 = models.FloatField("Discharge 75%", help_text="River flow with probability of non-exceedance of 75% [m3/s]", null=True, blank=True) q_95 = models.FloatField("Discharge 95%", help_text="River flow with probability of non-exceedance of 95% [m3/s]", null=True, blank=True) + q_raw = models.FloatField("Raw discharge", help_text="River flow measured optically", null=True, blank=True) wetted_surface = models.FloatField(help_text="Wetted surface area with given water level [m2]", null=True, blank=True) wetted_perimeter = models.FloatField(help_text="Wetted perimeter with given water level [m]", null=True, blank=True) fraction_velocimetry = models.FloatField(help_text="Fraction of discharge resolved using velocimetry [-]", null=True, blank=True) + v_bulk = models.FloatField(help_text="Bulk velocity [m/s]", null=True, blank=True) + v_av = models.FloatField(help_text="Average surface velocity [m/s]", null=True, blank=True) # TODO: create link with videos, filtered on site, to add water level to those videos. def save(self, *args, **kwargs): @@ -65,9 +68,13 @@ class Meta: verbose_name_plural = "time series" def __str__(self): - return "{:s} h [m]: {:s}, Q [m3/s]: {:s}, f [-]: {:s}".format( + return "{:s} h [m]: {:s}, Q [m3/s]: {:s}, Q_raw [m3/s]: {:s}, v_av [m/s]: {:s}, v_bulk [m/s]: {:s} f [-]: {:s}".format( self.timestamp.strftime("%Y-%m-%dT%H:%M:%S"), get_str(self.h, 3), get_str(self.q_50, 2), + get_str(self.q_raw, 2), + get_str(self.v_av, 2), + get_str(self.v_bulk, 2), get_str(self.fraction_velocimetry, 1) + ) \ No newline at end of file diff --git a/api/serializers/time_series.py b/api/serializers/time_series.py index 839db2f..8933386 100644 --- a/api/serializers/time_series.py +++ b/api/serializers/time_series.py @@ -25,6 +25,9 @@ class Meta: "q_50", "q_75", "q_95", + "q_raw", + "v_av", + "v_bulk", "wetted_surface", "wetted_perimeter", "fraction_velocimetry", diff --git a/api/tests/test_api_timeseries.py b/api/tests/test_api_timeseries.py index 1bcae89..6c2260d 100644 --- a/api/tests/test_api_timeseries.py +++ b/api/tests/test_api_timeseries.py @@ -89,10 +89,10 @@ def test_add_longer_timeseries_and_query(self): uri + "?startDateTime=2000-01-01T04:00:00:00Z" + "&endDateTime=2000-01-01T07:00:00:00Z" + "&format=pijson" ) self.assertEqual(len(r.json()), 3) # test for number of fields in pijson (always 3) - self.assertEqual(len(r.json()["timeSeries"]), 9) # test for number of variables (currently 9) + self.assertEqual(len(r.json()["timeSeries"]), 12) # test for number of variables (currently 9) self.assertEqual(len(r.json()["timeSeries"][0]["events"]), 4) # test for number of records # query in csv format r = client.get( uri + "?startDateTime=2000-01-01T04:00:00:00Z" + "&endDateTime=2000-01-01T07:00:00:00Z" + "&format=csv" ) - self.assertEqual(len(r.content), 277) + self.assertEqual(len(r.content), 307) diff --git a/docker-compose.s3.yml b/docker-compose.s3.yml index 590a8b2..73bd4bb 100644 --- a/docker-compose.s3.yml +++ b/docker-compose.s3.yml @@ -8,13 +8,14 @@ services: storage: image: minio/minio ports: - - ${LORC_STORAGE_PORT}:9000 + - ${LORC_STORAGE_PORT}:${LORC_STORAGE_PORT} - 9001:9001 environment: MINIO_ROOT_USER: "${LORC_STORAGE_ACCESS}" MINIO_ROOT_PASSWORD: "${LORC_STORAGE_SECRET}" + MINIO_API_PORT: ${LORC_STORAGE_PORT} MINIO_CONSOLE_PORT: 9001 volumes: - ${LORC_STORAGE_DIR}:/data - command: server /data --console-address ":9001" + command: server /data --console-address ":9001" --address ":${LORC_STORAGE_PORT}" diff --git a/liveorc.sh b/liveorc.sh index 1538016..2257ad9 100755 --- a/liveorc.sh +++ b/liveorc.sh @@ -120,7 +120,7 @@ case $key in shift # past value ;; --storage-port) - LORC_STORAGE_PORT=$(realpath "$2") + LORC_STORAGE_PORT=$2 export LORC_STORAGE_PORT shift # past argument shift # past value