From 0d1186f97f7e88a19a1576011b5b42fba9778ccd Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Thu, 14 Dec 2023 10:39:20 +0100 Subject: [PATCH 001/114] add load river command --- georiviere/river/management/__init__.py | 0 .../river/management/commands/__init__.py | 0 .../river/management/commands/load_rivers.py | 56 +++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 georiviere/river/management/__init__.py create mode 100644 georiviere/river/management/commands/__init__.py create mode 100644 georiviere/river/management/commands/load_rivers.py diff --git a/georiviere/river/management/__init__.py b/georiviere/river/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/georiviere/river/management/commands/__init__.py b/georiviere/river/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/georiviere/river/management/commands/load_rivers.py b/georiviere/river/management/commands/load_rivers.py new file mode 100644 index 00000000..4bd56e76 --- /dev/null +++ b/georiviere/river/management/commands/load_rivers.py @@ -0,0 +1,56 @@ +from django.contrib.gis.gdal import DataSource +from django.contrib.gis.geos import Point +from django.core.management import BaseCommand +from django.utils.timezone import now +from django.utils.translation import gettext as _ + +from georiviere.description.models import Morphology, Status +from georiviere.river.models import Stream, Topology + + +class Command(BaseCommand): + help = 'Load Rivers' + + def add_arguments(self, parser): + parser.add_argument('file_path', help="File's path to import.") + parser.add_argument('--name-attribute', '-n', action='store', dest='name', default='nom', + help="Name of the name's attribute inside the file") + parser.add_argument('--flush-streams', '-f', action='store_true', dest='flush', default=False, + help="Flush current streams") + parser.add_argument('--default-name-attribute', '-nd', action='store', dest='default_name', default=_('River'), + help="Default name to use if name is empty") + + def handle(self, *args, **options): + file_path = options.get('file_path') + name_column = options.get('name') + default_name = options.get('default_name') + flush = options.get('flush') + data_source = DataSource(file_path) + layer = data_source[0] + total_count = len(layer) + self.stdout.write(f"Load rivers: {total_count} features to import") + count = 0 + if flush: + self.stdout.write(f"Delete streams.....", ending="") + Stream.objects.all().delete() + self.stdout.write(self.style.SUCCESS("done!")) + bulks = [] + for feat in layer: + geom = feat.geom.geos + bulks.append(Stream(geom=geom, + source_location=Point(geom[0]), + name=feat.get(name_column) or default_name)) + count += 1 + if len(bulks) == 100: + objs = Stream.objects.bulk_create(bulks) + # for obj in objs: + # topo = Topology.objects.create(start_position=0, end_position=1, stream=obj) + # Morphology.objects.create(topology=topo, geom=obj.geom) + # topo = Topology.objects.create(start_position=0, end_position=1, stream=obj) + # Status.objects.create(topology=topo, geom=obj.geom) + + bulks = [] + now_2 = now() + self.stdout.write(f"{count} / {total_count}") + + self.stdout.write(f"Morpholgy creation") From 1bdaec9d027a2b516a9e17f416c5139d28664150 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 19 Dec 2023 10:10:32 +0100 Subject: [PATCH 002/114] use pg triggers --- dev-requirements.txt | 70 ++++++++++++------- docs/changelog.rst | 4 ++ georiviere/altimetry.py | 48 ++++++------- .../migrations/0025_auto_20231215_1124.py | 31 ++++++++ .../migrations/0026_auto_20231218_1222.py | 47 +++++++++++++ georiviere/description/models.py | 4 ++ .../main/management/commands/migrate.py | 11 +-- .../migrations/0004_auto_20231215_1124.py | 19 +++++ .../migrations/0005_auto_20231218_1222.py | 23 ++++++ georiviere/proceeding/models.py | 1 + .../river/management/commands/load_rivers.py | 50 ++++++------- .../migrations/0020_auto_20231215_1020.py | 19 +++++ .../migrations/0021_auto_20231215_1051.py | 23 ++++++ .../migrations/0022_auto_20231215_1052.py | 23 ++++++ .../migrations/0023_auto_20231215_1056.py | 23 ++++++ .../migrations/0024_auto_20231218_1226.py | 27 +++++++ .../migrations/0025_auto_20231218_1325.py | 19 +++++ .../migrations/0026_auto_20231218_1356.py | 23 ++++++ .../migrations/0027_auto_20231218_1400.py | 23 ++++++ .../migrations/0028_auto_20231218_1402.py | 23 ++++++ .../migrations/0029_auto_20231218_1403.py | 23 ++++++ .../migrations/0030_auto_20231218_1405.py | 23 ++++++ .../migrations/0031_auto_20231218_1405.py | 23 ++++++ .../migrations/0032_auto_20231218_1410.py | 19 +++++ .../migrations/0033_auto_20231218_1412.py | 23 ++++++ .../migrations/0034_auto_20231218_1416.py | 23 ++++++ .../migrations/0035_auto_20231218_1418.py | 23 ++++++ .../migrations/0036_auto_20231218_1420.py | 23 ++++++ .../migrations/0037_auto_20231218_1421.py | 23 ++++++ .../migrations/0038_auto_20231218_1431.py | 23 ++++++ .../migrations/0039_auto_20231218_1601.py | 23 ++++++ .../migrations/0040_auto_20231218_1608.py | 23 ++++++ georiviere/river/models.py | 63 +++++++++++++---- georiviere/river/signals.py | 17 +---- georiviere/settings/__init__.py | 1 + requirements.in | 1 + requirements.txt | 5 +- 37 files changed, 756 insertions(+), 114 deletions(-) create mode 100644 georiviere/description/migrations/0025_auto_20231215_1124.py create mode 100644 georiviere/description/migrations/0026_auto_20231218_1222.py create mode 100644 georiviere/proceeding/migrations/0004_auto_20231215_1124.py create mode 100644 georiviere/proceeding/migrations/0005_auto_20231218_1222.py create mode 100644 georiviere/river/migrations/0020_auto_20231215_1020.py create mode 100644 georiviere/river/migrations/0021_auto_20231215_1051.py create mode 100644 georiviere/river/migrations/0022_auto_20231215_1052.py create mode 100644 georiviere/river/migrations/0023_auto_20231215_1056.py create mode 100644 georiviere/river/migrations/0024_auto_20231218_1226.py create mode 100644 georiviere/river/migrations/0025_auto_20231218_1325.py create mode 100644 georiviere/river/migrations/0026_auto_20231218_1356.py create mode 100644 georiviere/river/migrations/0027_auto_20231218_1400.py create mode 100644 georiviere/river/migrations/0028_auto_20231218_1402.py create mode 100644 georiviere/river/migrations/0029_auto_20231218_1403.py create mode 100644 georiviere/river/migrations/0030_auto_20231218_1405.py create mode 100644 georiviere/river/migrations/0031_auto_20231218_1405.py create mode 100644 georiviere/river/migrations/0032_auto_20231218_1410.py create mode 100644 georiviere/river/migrations/0033_auto_20231218_1412.py create mode 100644 georiviere/river/migrations/0034_auto_20231218_1416.py create mode 100644 georiviere/river/migrations/0035_auto_20231218_1418.py create mode 100644 georiviere/river/migrations/0036_auto_20231218_1420.py create mode 100644 georiviere/river/migrations/0037_auto_20231218_1421.py create mode 100644 georiviere/river/migrations/0038_auto_20231218_1431.py create mode 100644 georiviere/river/migrations/0039_auto_20231218_1601.py create mode 100644 georiviere/river/migrations/0040_auto_20231218_1608.py diff --git a/dev-requirements.txt b/dev-requirements.txt index 58b97518..eb2033f4 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,17 +1,19 @@ # -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: # # pip-compile dev-requirements.in # -alabaster==0.7.12 +alabaster==0.7.13 # via sphinx asgiref==3.3.1 # via # -c requirements.txt # django -babel==2.9.1 +babel==2.14.0 # via sphinx +build==1.0.3 + # via pip-tools certifi==2020.12.5 # via # -c requirements.txt @@ -54,7 +56,7 @@ faker==9.7.1 # via # -c requirements.txt # factory-boy -flake8==4.0.1 +flake8==6.1.0 # via -r dev-requirements.in freezegun==1.1.0 # via @@ -64,8 +66,12 @@ idna==2.10 # via # -c requirements.txt # requests -imagesize==1.3.0 +imagesize==1.4.1 # via sphinx +importlib-metadata==7.0.0 + # via + # build + # sphinx jinja2==2.11.3 # via # -c requirements.txt @@ -74,28 +80,29 @@ markupsafe==1.1.1 # via # -c requirements.txt # jinja2 -mccabe==0.6.1 +mccabe==0.7.0 # via flake8 packaging==20.9 # via # -c requirements.txt + # build # sphinx -pep517==0.12.0 - # via pip-tools -pip-tools==6.5.1 +pip-tools==6.10.0 # via -r dev-requirements.in -pycodestyle==2.8.0 +pycodestyle==2.11.1 # via flake8 -pyflakes==2.4.0 +pyflakes==3.1.0 # via flake8 -pygments==2.15.0 +pygments==2.17.2 # via sphinx -pygraphviz==1.9 +pygraphviz==1.11 # via -r dev-requirements.in pyparsing==2.4.7 # via # -c requirements.txt # packaging +pyproject-hooks==1.0.0 + # via build python-dateutil==2.8.1 # via # -c requirements.txt @@ -104,7 +111,6 @@ python-dateutil==2.8.1 pytz==2021.1 # via # -c requirements.txt - # babel # django requests==2.26.0 # via @@ -116,43 +122,55 @@ six==1.15.0 # python-dateutil snowballstemmer==2.2.0 # via sphinx -sphinx==4.4.0 +sphinx==5.1.1 # via # -r dev-requirements.in # sphinx-rtd-theme -sphinx-rtd-theme==1.0.0 + # sphinxcontrib-applehelp + # sphinxcontrib-devhelp + # sphinxcontrib-htmlhelp + # sphinxcontrib-jquery + # sphinxcontrib-qthelp + # sphinxcontrib-serializinghtml +sphinx-rtd-theme==2.0.0 # via -r dev-requirements.in -sphinxcontrib-applehelp==1.0.2 +sphinxcontrib-applehelp==1.0.7 # via sphinx -sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-devhelp==1.0.5 # via sphinx -sphinxcontrib-htmlhelp==2.0.0 +sphinxcontrib-htmlhelp==2.0.4 # via sphinx +sphinxcontrib-jquery==4.1 + # via sphinx-rtd-theme sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-qthelp==1.0.6 # via sphinx -sphinxcontrib-serializinghtml==1.1.5 +sphinxcontrib-serializinghtml==1.1.9 # via sphinx sqlparse==0.4.1 # via # -c requirements.txt # django # django-debug-toolbar -tblib==1.7.0 +tblib==3.0.0 # via -r dev-requirements.in text-unidecode==1.3 # via # -c requirements.txt # faker -tomli==2.0.0 - # via pep517 +tomli==2.0.1 + # via + # build + # pyproject-hooks urllib3==1.26.3 # via # -c requirements.txt # requests -wheel==0.38.1 +wheel==0.42.0 # via pip-tools +zipp==3.17.0 + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/docs/changelog.rst b/docs/changelog.rst index 176bd7a2..8c92b620 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,10 @@ CHANGELOG 1.3.0+dev (XXXX-XX-XX) --------------------- +**New features** + +- add load_rivers command + **Bug fix** - Force translation defined in API url /api/portal/ (fix #222) diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index 64091013..e9b7335f 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -1,33 +1,27 @@ +import pgtrigger from geotrek.altimetry.models import AltimetryMixin as BaseAltimetryMixin -from georiviere.functions import ElevationInfos, Length3D - class AltimetryMixin(BaseAltimetryMixin): class Meta: abstract = True - - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - elevation_infos = self._meta.model.objects.filter(pk=self.pk) \ - .annotate(infos=ElevationInfos('geom')).first().infos - draped_geom = elevation_infos.get('draped') - self.geom_3d = draped_geom - self.slope = elevation_infos.get('slope') - self.min_elevation = elevation_infos.get('min_elevation') - self.max_elevation = elevation_infos.get('max_elevation') - self.ascent = elevation_infos.get('positive_gain') - self.descent = elevation_infos.get('negative_gain') - compute_results = self._meta.model.objects.filter(pk=self.pk) \ - .annotate(length_3d=Length3D(draped_geom)).first() - self.length = compute_results.length_3d - super().save(force_insert=False, - update_fields=[ - 'geom_3d', - 'slope', - 'min_elevation', - 'max_elevation', - 'ascent', - 'descent', - 'length' - ]) + triggers = [ + pgtrigger.Trigger( + name="keep_in_sync", + operation=pgtrigger.UpdateOf('geom') | pgtrigger.Insert, + when=pgtrigger.Before, + declare=[('elevation', 'elevation_infos')], + func=""" + SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation; + -- Update path geometry + NEW.geom_3d := elevation.draped; + NEW.length := ST_3DLength(elevation.draped); + NEW.slope := elevation.slope; + NEW.min_elevation := elevation.min_elevation; + NEW.max_elevation := elevation.max_elevation; + NEW.ascent := elevation.positive_gain; + NEW.descent := elevation.negative_gain; + RETURN NEW; + """ + ) + ] diff --git a/georiviere/description/migrations/0025_auto_20231215_1124.py b/georiviere/description/migrations/0025_auto_20231215_1124.py new file mode 100644 index 00000000..957a97dd --- /dev/null +++ b/georiviere/description/migrations/0025_auto_20231215_1124.py @@ -0,0 +1,31 @@ +# Generated by Django 3.1.14 on 2023-12-15 11:24 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('description', '0024_auto_20230323_1621'), + ] + + operations = [ + pgtrigger.migrations.AddTrigger( + model_name='land', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='82ffa7255d1e65c383b05ef0fa226da73a7ad6c8', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_fb8bb', table='description_land', when='BEFORE')), + ), + pgtrigger.migrations.AddTrigger( + model_name='morphology', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='f2e3c5183c820b762d1336a39135727ffc39c5e3', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_472ec', table='description_morphology', when='BEFORE')), + ), + pgtrigger.migrations.AddTrigger( + model_name='status', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='9ebcbb87cf2196d3c04e0e624571d3a49b1ebb69', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_aa87c', table='description_status', when='BEFORE')), + ), + pgtrigger.migrations.AddTrigger( + model_name='usage', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='c18fa3553e1c897d5a5e5f3b6cc5836c86429190', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_71b71', table='description_usage', when='BEFORE')), + ), + ] diff --git a/georiviere/description/migrations/0026_auto_20231218_1222.py b/georiviere/description/migrations/0026_auto_20231218_1222.py new file mode 100644 index 00000000..b16732c9 --- /dev/null +++ b/georiviere/description/migrations/0026_auto_20231218_1222.py @@ -0,0 +1,47 @@ +# Generated by Django 3.1.14 on 2023-12-18 12:22 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('description', '0025_auto_20231215_1124'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='land', + name='keep_in_sync', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='morphology', + name='keep_in_sync', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='status', + name='keep_in_sync', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='usage', + name='keep_in_sync', + ), + pgtrigger.migrations.AddTrigger( + model_name='land', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='d639f6850243942a05ed1f44f6cf79b88b563bad', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_fb8bb', table='description_land', when='BEFORE')), + ), + pgtrigger.migrations.AddTrigger( + model_name='morphology', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='048fe3697643f7682b5336def02a2a9d345947ae', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_472ec', table='description_morphology', when='BEFORE')), + ), + pgtrigger.migrations.AddTrigger( + model_name='status', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='967fa66b6c8eb5e17d5bfc49594c2573241d073b', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_aa87c', table='description_status', when='BEFORE')), + ), + pgtrigger.migrations.AddTrigger( + model_name='usage', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='69cb0cd06f0dccdca1f09d54169f5a673b87fca5', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_71b71', table='description_usage', when='BEFORE')), + ), + ] diff --git a/georiviere/description/models.py b/georiviere/description/models.py index ca1aaaf2..cd686bb5 100644 --- a/georiviere/description/models.py +++ b/georiviere/description/models.py @@ -199,6 +199,7 @@ class Morphology(AddPropertyBufferMixin, TopologyMixin, TimeStampedModelMixin, class Meta: verbose_name = _("Morphology") verbose_name_plural = _("Morphologies") + triggers = AltimetryMixin.Meta.triggers def __str__(self): if self.main_flow: @@ -268,6 +269,7 @@ class Land(AddPropertyBufferMixin, TimeStampedModelMixin, WatershedPropertiesMix class Meta: verbose_name = _("Land") verbose_name_plural = _("Lands") + triggers = AltimetryMixin.Meta.triggers def __str__(self): return f"{self.land_type}" @@ -306,6 +308,7 @@ class Usage(AddPropertyBufferMixin, TimeStampedModelMixin, WatershedPropertiesMi class Meta: verbose_name = _("Usage") verbose_name_plural = _("Usages") + triggers = AltimetryMixin.Meta.triggers def __str__(self): return ', '.join(self.usage_types.values_list("label", flat=True)) @@ -354,6 +357,7 @@ class Status(TopologyMixin, AddPropertyBufferMixin, TimeStampedModelMixin, Water class Meta: verbose_name = _("Status") verbose_name_plural = _("Statuses") + triggers = AltimetryMixin.Meta.triggers def __str__(self): if self.status_types.count(): diff --git a/georiviere/main/management/commands/migrate.py b/georiviere/main/management/commands/migrate.py index 218fd87a..0219458b 100644 --- a/georiviere/main/management/commands/migrate.py +++ b/georiviere/main/management/commands/migrate.py @@ -21,12 +21,15 @@ class Command(BaseCommand): def handle(self, *args, **options): check_srid_has_meter_unit() # set_search_path() - for app in apps.get_app_configs(): - # move_models_to_schemas(app) - load_sql_files(app, 'pre') + # for app in apps.get_app_configs(): + # # move_models_to_schemas(app) + # load_sql_files(app, 'pre') super().handle(*args, **options) for app in apps.get_app_configs(): # move_models_to_schemas(app) - load_sql_files(app, 'post') + try: + load_sql_files(app, 'post') + except Exception: + pass call_command('sync_translation_fields', '--noinput') call_command('update_translation_fields') diff --git a/georiviere/proceeding/migrations/0004_auto_20231215_1124.py b/georiviere/proceeding/migrations/0004_auto_20231215_1124.py new file mode 100644 index 00000000..01a23413 --- /dev/null +++ b/georiviere/proceeding/migrations/0004_auto_20231215_1124.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.14 on 2023-12-15 11:24 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('proceeding', '0003_auto_20210804_1014'), + ] + + operations = [ + pgtrigger.migrations.AddTrigger( + model_name='proceeding', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='4580ed3c25a4cd5a156ff3a0c1efd6c8301d8e25', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_6ba14', table='proceeding_proceeding', when='BEFORE')), + ), + ] diff --git a/georiviere/proceeding/migrations/0005_auto_20231218_1222.py b/georiviere/proceeding/migrations/0005_auto_20231218_1222.py new file mode 100644 index 00000000..5a7e4ac6 --- /dev/null +++ b/georiviere/proceeding/migrations/0005_auto_20231218_1222.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 12:22 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('proceeding', '0004_auto_20231215_1124'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='proceeding', + name='keep_in_sync', + ), + pgtrigger.migrations.AddTrigger( + model_name='proceeding', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='8be1e3d34cf0ef421d9f1750b10e7ef296d6a20d', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_6ba14', table='proceeding_proceeding', when='BEFORE')), + ), + ] diff --git a/georiviere/proceeding/models.py b/georiviere/proceeding/models.py index ab6627a3..c88520b7 100644 --- a/georiviere/proceeding/models.py +++ b/georiviere/proceeding/models.py @@ -49,6 +49,7 @@ def get_create_label(cls): class Meta: verbose_name = _("Proceeding") verbose_name_plural = _("Proceedings") + triggers = AltimetryMixin.Meta.triggers class Event(models.Model): diff --git a/georiviere/river/management/commands/load_rivers.py b/georiviere/river/management/commands/load_rivers.py index 4bd56e76..eca252d8 100644 --- a/georiviere/river/management/commands/load_rivers.py +++ b/georiviere/river/management/commands/load_rivers.py @@ -1,11 +1,11 @@ +from itertools import islice + from django.contrib.gis.gdal import DataSource from django.contrib.gis.geos import Point from django.core.management import BaseCommand -from django.utils.timezone import now from django.utils.translation import gettext as _ -from georiviere.description.models import Morphology, Status -from georiviere.river.models import Stream, Topology +from georiviere.river.models import Stream class Command(BaseCommand): @@ -14,11 +14,11 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument('file_path', help="File's path to import.") parser.add_argument('--name-attribute', '-n', action='store', dest='name', default='nom', - help="Name of the name's attribute inside the file") + help="Attribute name in file to use as river name") parser.add_argument('--flush-streams', '-f', action='store_true', dest='flush', default=False, help="Flush current streams") parser.add_argument('--default-name-attribute', '-nd', action='store', dest='default_name', default=_('River'), - help="Default name to use if name is empty") + help="Default name to use if attribute name specified is empty") def handle(self, *args, **options): file_path = options.get('file_path') @@ -29,28 +29,24 @@ def handle(self, *args, **options): layer = data_source[0] total_count = len(layer) self.stdout.write(f"Load rivers: {total_count} features to import") - count = 0 if flush: - self.stdout.write(f"Delete streams.....", ending="") + self.stdout.write("Delete streams.....", ending="") Stream.objects.all().delete() self.stdout.write(self.style.SUCCESS("done!")) - bulks = [] - for feat in layer: - geom = feat.geom.geos - bulks.append(Stream(geom=geom, - source_location=Point(geom[0]), - name=feat.get(name_column) or default_name)) - count += 1 - if len(bulks) == 100: - objs = Stream.objects.bulk_create(bulks) - # for obj in objs: - # topo = Topology.objects.create(start_position=0, end_position=1, stream=obj) - # Morphology.objects.create(topology=topo, geom=obj.geom) - # topo = Topology.objects.create(start_position=0, end_position=1, stream=obj) - # Status.objects.create(topology=topo, geom=obj.geom) - - bulks = [] - now_2 = now() - self.stdout.write(f"{count} / {total_count}") - - self.stdout.write(f"Morpholgy creation") + + batch_size = 100 + + objs = (Stream(geom=feat.geom.geos, + source_location=Point(feat.geom.geos[0]), + name=feat.get(name_column) or default_name) for feat in layer) + count = 0 + while True: + batch = list(islice(objs, batch_size)) + count += len(batch) + if not batch: + break + self.stdout.write(f"{count} / {total_count}", ending="") + Stream.objects.bulk_create(batch, batch_size) + self.stdout.write(self.style.SUCCESS(" ok!")) + + self.stdout.write(self.style.SUCCESS(f"Successfully import {total_count} rivers and associated morphologies / status")) diff --git a/georiviere/river/migrations/0020_auto_20231215_1020.py b/georiviere/river/migrations/0020_auto_20231215_1020.py new file mode 100644 index 00000000..e5421117 --- /dev/null +++ b/georiviere/river/migrations/0020_auto_20231215_1020.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.14 on 2023-12-15 10:20 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0019_stream_description'), + ] + + operations = [ + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation JSONB;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='6c62fd64699ff351273ae8753db0b43dd5defaf7', operation='UPDATE OR INSERT', pgid='pgtrigger_keep_in_sync_7a86b', table='river_stream', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0021_auto_20231215_1051.py b/georiviere/river/migrations/0021_auto_20231215_1051.py new file mode 100644 index 00000000..bd951d1f --- /dev/null +++ b/georiviere/river/migrations/0021_auto_20231215_1051.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-15 10:51 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0020_auto_20231215_1020'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='stream', + name='keep_in_sync', + ), + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := element.draped;\n NEW.length := ST_3DLength(element.draped);\n NEW.slope := element.slope;\n NEW.min_elevation := element.min_elevation;\n NEW.max_elevation := element.max_elevation;\n NEW.ascent := element.positive_gain;\n NEW.descent := element.negative_gain;\n\n RETURN NEW;\n ', hash='fdeef3b3432dda345b1ec896b91e84eb120a9986', operation='UPDATE OR INSERT', pgid='pgtrigger_keep_in_sync_7a86b', table='river_stream', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0022_auto_20231215_1052.py b/georiviere/river/migrations/0022_auto_20231215_1052.py new file mode 100644 index 00000000..9a4e0b26 --- /dev/null +++ b/georiviere/river/migrations/0022_auto_20231215_1052.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-15 10:52 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0021_auto_20231215_1051'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='stream', + name='keep_in_sync', + ), + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='24de00d1e6f9be9e088db3410e54b0de328653a3', operation='UPDATE OR INSERT', pgid='pgtrigger_keep_in_sync_7a86b', table='river_stream', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0023_auto_20231215_1056.py b/georiviere/river/migrations/0023_auto_20231215_1056.py new file mode 100644 index 00000000..6e669b8a --- /dev/null +++ b/georiviere/river/migrations/0023_auto_20231215_1056.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-15 10:56 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0022_auto_20231215_1052'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='stream', + name='keep_in_sync', + ), + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='168b84532fa3b4230f53fbd1cfdb1169c5d72a0a', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_7a86b', table='river_stream', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0024_auto_20231218_1226.py b/georiviere/river/migrations/0024_auto_20231218_1226.py new file mode 100644 index 00000000..4d4f21a1 --- /dev/null +++ b/georiviere/river/migrations/0024_auto_20231218_1226.py @@ -0,0 +1,27 @@ +# Generated by Django 3.1.14 on 2023-12-18 12:26 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0023_auto_20231215_1056'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='stream', + name='keep_in_sync', + ), + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='e66f827b3e0e2d3e5f822a259752f2d8a48a7c26', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_7a86b', table='river_stream', when='BEFORE')), + ), + pgtrigger.migrations.AddTrigger( + model_name='topology', + trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; geom geometry;', func='\n IF (NEW.start_point != OLD.start_point OR NEW.end_point != OLD.end_point) THEN \n SELECT geom FROM river_stream r WHERE NEW.id = r.id INTO geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(geom, NEW.START_POINT, NEW.END_POINT)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(geom, NEW.START_POINT, NEW.END_POINT)\n WHERE topology_id = NEW.id; \n END IF;\n RETURN NEW;\n ', hash='db1df9343d9079232a59df61b34bfcd7bc2df48a', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0025_auto_20231218_1325.py b/georiviere/river/migrations/0025_auto_20231218_1325.py new file mode 100644 index 00000000..74c03ca5 --- /dev/null +++ b/georiviere/river/migrations/0025_auto_20231218_1325.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.14 on 2023-12-18 13:25 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0024_auto_20231218_1226'), + ] + + operations = [ + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func='\n INSERT INTO river_topology (stream_id, start_position, end_position)\n VALUES (NEW.id, 0, 1) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom) VALUES (topology_morphology, NEW.geom);\n INSERT INTO river_topology (stream_id, start_position, end_position)\n VALUES (NEW.id, 0, 1) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom) VALUES (topology_status, NEW.geom);\n RETURN NEW;\n ', hash='c0b7e1615c64b92e36e49803f3001ebde7dd517e', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), + ), + ] diff --git a/georiviere/river/migrations/0026_auto_20231218_1356.py b/georiviere/river/migrations/0026_auto_20231218_1356.py new file mode 100644 index 00000000..45b90ad4 --- /dev/null +++ b/georiviere/river/migrations/0026_auto_20231218_1356.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 13:56 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0025_auto_20231218_1325'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='topology', + name='update_topology_geom', + ), + pgtrigger.migrations.AddTrigger( + model_name='topology', + trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; geom geometry;', func='\n IF (NEW.start_position != OLD.start_position OR NEW.end_position != OLD.end_position) THEN \n SELECT geom FROM river_stream r WHERE NEW.id = r.id INTO geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n END IF;\n RETURN NEW;\n ', hash='10d2e4894631bee97f91deeb607004d3a5295448', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0027_auto_20231218_1400.py b/georiviere/river/migrations/0027_auto_20231218_1400.py new file mode 100644 index 00000000..f32c8f08 --- /dev/null +++ b/georiviere/river/migrations/0027_auto_20231218_1400.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:00 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0026_auto_20231218_1356'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='stream', + name='create_topologies', + ), + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func='\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom) VALUES (topology_morphology, NEW.geom);\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom) VALUES (topology_status, NEW.geom);\n RETURN NEW;\n ', hash='4ebe61234dd666bf424de96761a8af7e0a72560f', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), + ), + ] diff --git a/georiviere/river/migrations/0028_auto_20231218_1402.py b/georiviere/river/migrations/0028_auto_20231218_1402.py new file mode 100644 index 00000000..227b45d4 --- /dev/null +++ b/georiviere/river/migrations/0028_auto_20231218_1402.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:02 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0027_auto_20231218_1400'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='stream', + name='create_topologies', + ), + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func='\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom, date_insert, date_update) VALUES (topology_morphology, NEW.geom, NOW(), NOW());\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom, date_insert, date_update) VALUES (topology_status, NEW.geom, NOW(), NOW());\n RETURN NEW;\n ', hash='606f9274946baa820c7792e04a1f6559948a2e8e', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), + ), + ] diff --git a/georiviere/river/migrations/0029_auto_20231218_1403.py b/georiviere/river/migrations/0029_auto_20231218_1403.py new file mode 100644 index 00000000..8416c313 --- /dev/null +++ b/georiviere/river/migrations/0029_auto_20231218_1403.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:03 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0028_auto_20231218_1402'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='stream', + name='create_topologies', + ), + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func="\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update)\n VALUES (topology_morphology, NEW.geom, '', NOW(), NOW());\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom, date_insert, date_update) VALUES (topology_status, NEW.geom, NOW(), NOW());\n RETURN NEW;\n ", hash='cfb353801531d9e2c134a09c85d8a69807704a71', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), + ), + ] diff --git a/georiviere/river/migrations/0030_auto_20231218_1405.py b/georiviere/river/migrations/0030_auto_20231218_1405.py new file mode 100644 index 00000000..1ef79185 --- /dev/null +++ b/georiviere/river/migrations/0030_auto_20231218_1405.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:05 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0029_auto_20231218_1403'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='stream', + name='create_topologies', + ), + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func="\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update)\n VALUES (topology_morphology, NEW.geom, '', NOW(), NOW());\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom, regulation, referencial, descrption, date_insert, date_update)\n VALUES (topology_status, NEW.geom, FALSE, FALSE, '', NOW(), NOW());\n RETURN NEW;\n ", hash='f5c7079e6d4e21c67d642ea14d61d4e2f39787b0', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), + ), + ] diff --git a/georiviere/river/migrations/0031_auto_20231218_1405.py b/georiviere/river/migrations/0031_auto_20231218_1405.py new file mode 100644 index 00000000..2dbd1031 --- /dev/null +++ b/georiviere/river/migrations/0031_auto_20231218_1405.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:05 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0030_auto_20231218_1405'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='stream', + name='create_topologies', + ), + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func="\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update)\n VALUES (topology_morphology, NEW.geom, '', NOW(), NOW());\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom, regulation, referencial, description, date_insert, date_update)\n VALUES (topology_status, NEW.geom, FALSE, FALSE, '', NOW(), NOW());\n RETURN NEW;\n ", hash='e846f76834647fd2338681a9530765b6d1883cc9', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), + ), + ] diff --git a/georiviere/river/migrations/0032_auto_20231218_1410.py b/georiviere/river/migrations/0032_auto_20231218_1410.py new file mode 100644 index 00000000..1e19b353 --- /dev/null +++ b/georiviere/river/migrations/0032_auto_20231218_1410.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:10 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0031_auto_20231218_1405'), + ] + + operations = [ + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='update_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(func='\n UPDATE river_topology SET start_position=start_position WHERE stream_id=NEW.id;\n RETURN NEW;\n ', hash='0b6322b6b455eed7b6a2a78c79b07e08e9a86611', operation='UPDATE OF "geom"', pgid='pgtrigger_update_topologies_6ab45', table='river_stream', when='AFTER')), + ), + ] diff --git a/georiviere/river/migrations/0033_auto_20231218_1412.py b/georiviere/river/migrations/0033_auto_20231218_1412.py new file mode 100644 index 00000000..1b5d52e2 --- /dev/null +++ b/georiviere/river/migrations/0033_auto_20231218_1412.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:12 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0032_auto_20231218_1410'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='topology', + name='update_topology_geom', + ), + pgtrigger.migrations.AddTrigger( + model_name='topology', + trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; geom geometry;', func='\n SELECT geom FROM river_stream r WHERE NEW.id = r.id INTO geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='2b83b20dab6aa7e5456c1e10694f527dc98eb281', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0034_auto_20231218_1416.py b/georiviere/river/migrations/0034_auto_20231218_1416.py new file mode 100644 index 00000000..653b8de5 --- /dev/null +++ b/georiviere/river/migrations/0034_auto_20231218_1416.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:16 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0033_auto_20231218_1412'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='topology', + name='update_topology_geom', + ), + pgtrigger.migrations.AddTrigger( + model_name='topology', + trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.id = r.id INTO geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='1e3e1dbd526ffd01e8155aec62fad614e188471d', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0035_auto_20231218_1418.py b/georiviere/river/migrations/0035_auto_20231218_1418.py new file mode 100644 index 00000000..e92cd864 --- /dev/null +++ b/georiviere/river/migrations/0035_auto_20231218_1418.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:18 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0034_auto_20231218_1416'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='topology', + name='update_topology_geom', + ), + pgtrigger.migrations.AddTrigger( + model_name='topology', + trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.id = r.id INTO geom;\n UPDATE description_morphology m\n SET m.geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status s\n SET s.geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='a3d43086e66e04cde5bf979df1f4565a3bb9e791', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0036_auto_20231218_1420.py b/georiviere/river/migrations/0036_auto_20231218_1420.py new file mode 100644 index 00000000..4421a74f --- /dev/null +++ b/georiviere/river/migrations/0036_auto_20231218_1420.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:20 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0035_auto_20231218_1418'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='topology', + name='update_topology_geom', + ), + pgtrigger.migrations.AddTrigger( + model_name='topology', + trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; stream_geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.id = r.id INTO stream_geom;\n UPDATE description_morphology m\n SET m.geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status s\n SET s.geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='1671ae29035c32a992993d3518965e8e9c0a605f', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0037_auto_20231218_1421.py b/georiviere/river/migrations/0037_auto_20231218_1421.py new file mode 100644 index 00000000..1739897b --- /dev/null +++ b/georiviere/river/migrations/0037_auto_20231218_1421.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:21 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0036_auto_20231218_1420'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='topology', + name='update_topology_geom', + ), + pgtrigger.migrations.AddTrigger( + model_name='topology', + trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; stream_geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.id = r.id INTO stream_geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='444948eb36585ca5ea4dfdad5e9be32137e67844', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0038_auto_20231218_1431.py b/georiviere/river/migrations/0038_auto_20231218_1431.py new file mode 100644 index 00000000..26019979 --- /dev/null +++ b/georiviere/river/migrations/0038_auto_20231218_1431.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:31 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0037_auto_20231218_1421'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='topology', + name='update_topology_geom', + ), + pgtrigger.migrations.AddTrigger( + model_name='topology', + trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE stream_geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.id = r.id INTO stream_geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='f0a3386a26d41f8268db5bbdb554ba4cc3ae1612', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='AFTER')), + ), + ] diff --git a/georiviere/river/migrations/0039_auto_20231218_1601.py b/georiviere/river/migrations/0039_auto_20231218_1601.py new file mode 100644 index 00000000..06a328cb --- /dev/null +++ b/georiviere/river/migrations/0039_auto_20231218_1601.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 16:01 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0038_auto_20231218_1431'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='stream', + name='update_topologies', + ), + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='update_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(func='\n UPDATE river_topology SET id=id WHERE stream_id=NEW.id;\n RETURN NEW;\n ', hash='704192b93012d12fa95b06b03618981a49609a4e', operation='UPDATE OF "geom"', pgid='pgtrigger_update_topologies_6ab45', table='river_stream', when='AFTER')), + ), + ] diff --git a/georiviere/river/migrations/0040_auto_20231218_1608.py b/georiviere/river/migrations/0040_auto_20231218_1608.py new file mode 100644 index 00000000..ad4c8f4d --- /dev/null +++ b/georiviere/river/migrations/0040_auto_20231218_1608.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 16:08 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0039_auto_20231218_1601'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='topology', + name='update_topology_geom', + ), + pgtrigger.migrations.AddTrigger( + model_name='topology', + trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE stream_geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.stream_id = r.id INTO stream_geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='4d63b6e0ef200235882742af37d834d00b5a271a', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='AFTER')), + ), + ] diff --git a/georiviere/river/models.py b/georiviere/river/models.py index 41f37cbd..9283a84c 100644 --- a/georiviere/river/models.py +++ b/georiviere/river/models.py @@ -1,3 +1,4 @@ +import pgtrigger from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.contrib.gis.db import models @@ -19,7 +20,7 @@ from georiviere.main.models import AddPropertyBufferMixin from georiviere.altimetry import AltimetryMixin from georiviere.finances_administration.models import AdministrativeFile -from georiviere.functions import ClosestPoint, LineSubString +from georiviere.functions import ClosestPoint from georiviere.knowledge.models import Knowledge, FollowUp from georiviere.main.models import DistanceToSource from georiviere.observations.models import Station @@ -96,6 +97,37 @@ class FlowChoices(models.IntegerChoices): class Meta: verbose_name = _("Stream") verbose_name_plural = _("Streams") + triggers = AltimetryMixin.Meta.triggers + [ + pgtrigger.Trigger( + name="create_topologies", + operation=pgtrigger.Insert, + declare=[ + ('topology_morphology', 'integer'), + ('topology_status', 'integer') + ], + when=pgtrigger.After, + func=""" + INSERT INTO river_topology (stream_id, start_position, end_position, qualified) + VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology; + INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update) + VALUES (topology_morphology, NEW.geom, '', NOW(), NOW()); + INSERT INTO river_topology (stream_id, start_position, end_position, qualified) + VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status; + INSERT INTO description_status (topology_id, geom, regulation, referencial, description, date_insert, date_update) + VALUES (topology_status, NEW.geom, FALSE, FALSE, '', NOW(), NOW()); + RETURN NEW; + """ + ), + pgtrigger.Trigger( + name="update_topologies", + operation=pgtrigger.UpdateOf('geom'), + when=pgtrigger.After, + func=""" + UPDATE river_topology SET id=id WHERE stream_id=NEW.id; + RETURN NEW; + """ + ) + ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -216,21 +248,26 @@ def __str__(self): else: return _("Topology {}").format(self.pk) - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - geom_topology = self._meta.model.objects.filter(pk=self.pk) \ - .annotate(substring=LineSubString(self.stream.geom, self.start_position, self.end_position)).first().substring - if hasattr(self, 'status'): - self.status.geom = geom_topology - self.status.save() - elif hasattr(self, 'morphology'): - self.morphology.geom = geom_topology - self.morphology.save() - super().save(force_insert=False) - class Meta: verbose_name = _("Topology") verbose_name_plural = _("Topologies") + triggers = [ + pgtrigger.Trigger( + name="update_topology_geom", + operation=pgtrigger.Update | pgtrigger.Insert, + when=pgtrigger.After, + declare=[('stream_geom', 'geometry')], + func=""" + SELECT r.geom FROM river_stream r WHERE NEW.stream_id = r.id INTO stream_geom; + UPDATE description_morphology + SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position) + WHERE topology_id = NEW.id; + UPDATE description_status + SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position) + WHERE topology_id = NEW.id; + RETURN NEW; + """ + )] Study.add_property('stations', Station.within_buffer, _("Stations")) diff --git a/georiviere/river/signals.py b/georiviere/river/signals.py index 77e46a0e..3be48afa 100644 --- a/georiviere/river/signals.py +++ b/georiviere/river/signals.py @@ -3,33 +3,18 @@ from django.contrib.contenttypes.models import ContentType from django.contrib.gis.db.models.functions import Distance, Length, LineLocatePoint from django.db.models.signals import post_save -from django.db.models.fields.reverse_related import OneToOneRel from django.db.models import F, FloatField, Case, When from django.dispatch import receiver from georiviere.functions import ClosestPoint, LineSubString from georiviere.main.models import DistanceToSource -from georiviere.river.models import Stream, Topology, TopologyMixin +from georiviere.river.models import Stream, TopologyMixin from mapentity.models import MapEntityMixin -@receiver(post_save, sender=Stream) -def save_stream(sender, instance, **kwargs): - if kwargs['created']: - class_topos = [field.related_model for field in instance.topologies.model._meta.get_fields() - if isinstance(field, OneToOneRel)] - for class_topo in class_topos: - topology = Topology.objects.create(start_position=0, end_position=1, stream=instance) - class_topo.objects.create(topology=topology, geom=instance.geom) - else: - for topology in instance.topologies.all(): - topology.save() - - @receiver(post_save, sender=Stream) def save_stream_generate_distance_to_source(sender, instance, **kwargs): - for model in apps.get_models(): if issubclass(model, MapEntityMixin) and not issubclass(model, TopologyMixin) and model != Stream and 'geom' in [field.name for field in model._meta.get_fields()]: distances_to_sources = [] diff --git a/georiviere/settings/__init__.py b/georiviere/settings/__init__.py index 4daf8d0f..00e4741c 100644 --- a/georiviere/settings/__init__.py +++ b/georiviere/settings/__init__.py @@ -135,6 +135,7 @@ def construct_relative_path_mock(current_template_name, relative_name): 'embed_video', 'djgeojson', 'django_filters', + 'pgtrigger', 'compressor', 'mapentity', # mapentity should be placed after app declaring paperclip models 'leaflet', diff --git a/requirements.in b/requirements.in index a4d9353d..c09d1eb1 100644 --- a/requirements.in +++ b/requirements.in @@ -14,6 +14,7 @@ sentry-sdk django-admin-ordering djangorestframework-camel-case jsonschema +django-pgtrigger # Use until latest version of geotrek (> 2.99.0) drf-spectacular diff --git a/requirements.txt b/requirements.txt index b6af05d8..34e85eab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # pip-compile @@ -91,6 +91,7 @@ django==3.1.14 # django-leaflet # django-modeltranslation # django-mptt + # django-pgtrigger # django-querysetsequence # django-weasyprint # djangorestframework @@ -144,6 +145,8 @@ django-modeltranslation==0.17.3 # via mapentity django-mptt==0.11.0 # via geotrek +django-pgtrigger==4.11.0 + # via -r requirements.in django-querysetsequence==0.14 # via django-autocomplete-light django-tinymce==2.9.0 From d32ed9219a522a2d94ea7457c9ed292324d102ab Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 19 Dec 2023 10:32:17 +0100 Subject: [PATCH 003/114] use pg triggers --- .../migrations/0041_auto_20231219_0932.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 georiviere/river/migrations/0041_auto_20231219_0932.py diff --git a/georiviere/river/migrations/0041_auto_20231219_0932.py b/georiviere/river/migrations/0041_auto_20231219_0932.py new file mode 100644 index 00000000..12429cd4 --- /dev/null +++ b/georiviere/river/migrations/0041_auto_20231219_0932.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-19 09:32 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0040_auto_20231218_1608'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='topology', + name='update_topology_geom', + ), + pgtrigger.migrations.AddTrigger( + model_name='topology', + trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE stream_geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.stream_id = r.id INTO stream_geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id;\n UPDATE description_status\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id;\n RETURN NEW;\n ', hash='0d8dce1088e59bd290e9ffe0d6bfb8a2493a5c1c', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='AFTER')), + ), + ] From fa5fcce78981b5b7a6f5c599987b1142644d0d06 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 19 Dec 2023 10:40:38 +0100 Subject: [PATCH 004/114] use pg triggers --- georiviere/river/templates/river/sql/pre_types.sql | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 georiviere/river/templates/river/sql/pre_types.sql diff --git a/georiviere/river/templates/river/sql/pre_types.sql b/georiviere/river/templates/river/sql/pre_types.sql new file mode 100644 index 00000000..cc8532dc --- /dev/null +++ b/georiviere/river/templates/river/sql/pre_types.sql @@ -0,0 +1,9 @@ +DROP TYPE IF EXISTS elevation_infos; +CREATE TYPE elevation_infos AS ( + draped geometry, + slope float, + min_elevation integer, + max_elevation integer, + positive_gain integer, + negative_gain integer +); \ No newline at end of file From e80aabee234a585a6bddcda8198f58f23dd0c266 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 19 Dec 2023 10:45:08 +0100 Subject: [PATCH 005/114] use pg triggers --- georiviere/main/management/commands/migrate.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/georiviere/main/management/commands/migrate.py b/georiviere/main/management/commands/migrate.py index 0219458b..8d08a70f 100644 --- a/georiviere/main/management/commands/migrate.py +++ b/georiviere/main/management/commands/migrate.py @@ -20,16 +20,10 @@ def check_srid_has_meter_unit(): class Command(BaseCommand): def handle(self, *args, **options): check_srid_has_meter_unit() - # set_search_path() - # for app in apps.get_app_configs(): - # # move_models_to_schemas(app) - # load_sql_files(app, 'pre') + for app in apps.get_app_configs(): + load_sql_files(app, 'pre') super().handle(*args, **options) for app in apps.get_app_configs(): - # move_models_to_schemas(app) - try: - load_sql_files(app, 'post') - except Exception: - pass + load_sql_files(app, 'post') call_command('sync_translation_fields', '--noinput') call_command('update_translation_fields') From 29fb3b7f9f3fb198a243c07f50549481d5060579 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 19 Dec 2023 16:28:49 +0100 Subject: [PATCH 006/114] use pg triggers --- .../templates/river => main/templates/main}/sql/pre_types.sql | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename georiviere/{river/templates/river => main/templates/main}/sql/pre_types.sql (100%) diff --git a/georiviere/river/templates/river/sql/pre_types.sql b/georiviere/main/templates/main/sql/pre_types.sql similarity index 100% rename from georiviere/river/templates/river/sql/pre_types.sql rename to georiviere/main/templates/main/sql/pre_types.sql From cafa103e18f0fdf48cb582f43265ae01d2bbc0ce Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 19 Dec 2023 16:44:48 +0100 Subject: [PATCH 007/114] use pg triggers --- .../{templates/main/sql/pre_types.sql => sql/pre_10_types.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename georiviere/main/{templates/main/sql/pre_types.sql => sql/pre_10_types.sql} (100%) diff --git a/georiviere/main/templates/main/sql/pre_types.sql b/georiviere/main/sql/pre_10_types.sql similarity index 100% rename from georiviere/main/templates/main/sql/pre_types.sql rename to georiviere/main/sql/pre_10_types.sql From 0e8dd4b42e18241ae6ec95287d0f8ad8b9b67e9f Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 19 Dec 2023 16:55:38 +0100 Subject: [PATCH 008/114] use pg triggers --- georiviere/main/management/commands/migrate.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/georiviere/main/management/commands/migrate.py b/georiviere/main/management/commands/migrate.py index 8d08a70f..8b607a0e 100644 --- a/georiviere/main/management/commands/migrate.py +++ b/georiviere/main/management/commands/migrate.py @@ -21,9 +21,12 @@ class Command(BaseCommand): def handle(self, *args, **options): check_srid_has_meter_unit() for app in apps.get_app_configs(): - load_sql_files(app, 'pre') + load_sql_files(app, 'pre') super().handle(*args, **options) for app in apps.get_app_configs(): - load_sql_files(app, 'post') + try: + load_sql_files(app, 'post') + except Exception: + pass call_command('sync_translation_fields', '--noinput') call_command('update_translation_fields') From c06dcc2fdf9ed531887ccb87d4c0d0bf11af1f9b Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 19 Dec 2023 17:37:57 +0100 Subject: [PATCH 009/114] use pg triggers --- georiviere/main/management/commands/migrate.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/georiviere/main/management/commands/migrate.py b/georiviere/main/management/commands/migrate.py index 8b607a0e..70fef977 100644 --- a/georiviere/main/management/commands/migrate.py +++ b/georiviere/main/management/commands/migrate.py @@ -22,11 +22,8 @@ def handle(self, *args, **options): check_srid_has_meter_unit() for app in apps.get_app_configs(): load_sql_files(app, 'pre') + load_sql_files(app, 'post') super().handle(*args, **options) - for app in apps.get_app_configs(): - try: - load_sql_files(app, 'post') - except Exception: - pass + call_command('sync_translation_fields', '--noinput') call_command('update_translation_fields') From 5f5628a08555bafe0953a95c8ba5cdd8f613cd10 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 19 Dec 2023 20:54:05 +0100 Subject: [PATCH 010/114] use pg triggers --- georiviere/main/sql/pre_10_types.sql | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 georiviere/main/sql/pre_10_types.sql diff --git a/georiviere/main/sql/pre_10_types.sql b/georiviere/main/sql/pre_10_types.sql deleted file mode 100644 index cc8532dc..00000000 --- a/georiviere/main/sql/pre_10_types.sql +++ /dev/null @@ -1,9 +0,0 @@ -DROP TYPE IF EXISTS elevation_infos; -CREATE TYPE elevation_infos AS ( - draped geometry, - slope float, - min_elevation integer, - max_elevation integer, - positive_gain integer, - negative_gain integer -); \ No newline at end of file From 555aaae6bcbc410701818152046ea4341fc82319 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 12:47:53 +0100 Subject: [PATCH 011/114] use internal triggers --- docker-compose.yml | 1 - georiviere/altimetry.py | 21 ---- .../migrations/0025_auto_20231215_1124.py | 31 ------ .../migrations/0026_auto_20231218_1222.py | 47 --------- georiviere/description/models.py | 6 +- .../description/sql/post_10_triggers.sql | 7 ++ .../main/management/commands/migrate.py | 7 +- georiviere/main/sql/post_10_functions.sql | 16 +++ georiviere/main/sql/pre_10_cleanup.sql | 1 + .../migrations/0004_auto_20231215_1124.py | 19 ---- .../migrations/0005_auto_20231218_1222.py | 23 ----- georiviere/proceeding/models.py | 1 - .../river/management/commands/load_rivers.py | 2 +- .../migrations/0020_auto_20231215_1020.py | 19 ---- .../migrations/0021_auto_20231215_1051.py | 23 ----- .../migrations/0022_auto_20231215_1052.py | 23 ----- .../migrations/0023_auto_20231215_1056.py | 23 ----- .../migrations/0024_auto_20231218_1226.py | 27 ------ .../migrations/0025_auto_20231218_1325.py | 19 ---- .../migrations/0026_auto_20231218_1356.py | 23 ----- .../migrations/0027_auto_20231218_1400.py | 23 ----- .../migrations/0028_auto_20231218_1402.py | 23 ----- .../migrations/0029_auto_20231218_1403.py | 23 ----- .../migrations/0030_auto_20231218_1405.py | 23 ----- .../migrations/0031_auto_20231218_1405.py | 23 ----- .../migrations/0032_auto_20231218_1410.py | 19 ---- .../migrations/0033_auto_20231218_1412.py | 23 ----- .../migrations/0034_auto_20231218_1416.py | 23 ----- .../migrations/0035_auto_20231218_1418.py | 23 ----- .../migrations/0036_auto_20231218_1420.py | 23 ----- .../migrations/0037_auto_20231218_1421.py | 23 ----- .../migrations/0038_auto_20231218_1431.py | 23 ----- .../migrations/0039_auto_20231218_1601.py | 23 ----- .../migrations/0040_auto_20231218_1608.py | 23 ----- .../migrations/0041_auto_20231219_0932.py | 23 ----- georiviere/river/models.py | 97 +++++++++---------- georiviere/river/sql/post_10_triggers.sql | 43 ++++++++ georiviere/river/sql/pre_10_cleanup.sql | 3 + georiviere/settings/__init__.py | 2 +- requirements.in | 1 - requirements.txt | 3 - 41 files changed, 127 insertions(+), 702 deletions(-) delete mode 100644 georiviere/description/migrations/0025_auto_20231215_1124.py delete mode 100644 georiviere/description/migrations/0026_auto_20231218_1222.py create mode 100644 georiviere/description/sql/post_10_triggers.sql create mode 100644 georiviere/main/sql/post_10_functions.sql create mode 100644 georiviere/main/sql/pre_10_cleanup.sql delete mode 100644 georiviere/proceeding/migrations/0004_auto_20231215_1124.py delete mode 100644 georiviere/proceeding/migrations/0005_auto_20231218_1222.py delete mode 100644 georiviere/river/migrations/0020_auto_20231215_1020.py delete mode 100644 georiviere/river/migrations/0021_auto_20231215_1051.py delete mode 100644 georiviere/river/migrations/0022_auto_20231215_1052.py delete mode 100644 georiviere/river/migrations/0023_auto_20231215_1056.py delete mode 100644 georiviere/river/migrations/0024_auto_20231218_1226.py delete mode 100644 georiviere/river/migrations/0025_auto_20231218_1325.py delete mode 100644 georiviere/river/migrations/0026_auto_20231218_1356.py delete mode 100644 georiviere/river/migrations/0027_auto_20231218_1400.py delete mode 100644 georiviere/river/migrations/0028_auto_20231218_1402.py delete mode 100644 georiviere/river/migrations/0029_auto_20231218_1403.py delete mode 100644 georiviere/river/migrations/0030_auto_20231218_1405.py delete mode 100644 georiviere/river/migrations/0031_auto_20231218_1405.py delete mode 100644 georiviere/river/migrations/0032_auto_20231218_1410.py delete mode 100644 georiviere/river/migrations/0033_auto_20231218_1412.py delete mode 100644 georiviere/river/migrations/0034_auto_20231218_1416.py delete mode 100644 georiviere/river/migrations/0035_auto_20231218_1418.py delete mode 100644 georiviere/river/migrations/0036_auto_20231218_1420.py delete mode 100644 georiviere/river/migrations/0037_auto_20231218_1421.py delete mode 100644 georiviere/river/migrations/0038_auto_20231218_1431.py delete mode 100644 georiviere/river/migrations/0039_auto_20231218_1601.py delete mode 100644 georiviere/river/migrations/0040_auto_20231218_1608.py delete mode 100644 georiviere/river/migrations/0041_auto_20231219_0932.py create mode 100644 georiviere/river/sql/post_10_triggers.sql create mode 100644 georiviere/river/sql/pre_10_cleanup.sql diff --git a/docker-compose.yml b/docker-compose.yml index 64b778d6..f519fdc4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,6 @@ services: web: image: georiviere:latest - user: $UID:$GID build: context: . target: dev diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index e9b7335f..d36c1c74 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -1,27 +1,6 @@ -import pgtrigger from geotrek.altimetry.models import AltimetryMixin as BaseAltimetryMixin class AltimetryMixin(BaseAltimetryMixin): class Meta: abstract = True - triggers = [ - pgtrigger.Trigger( - name="keep_in_sync", - operation=pgtrigger.UpdateOf('geom') | pgtrigger.Insert, - when=pgtrigger.Before, - declare=[('elevation', 'elevation_infos')], - func=""" - SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation; - -- Update path geometry - NEW.geom_3d := elevation.draped; - NEW.length := ST_3DLength(elevation.draped); - NEW.slope := elevation.slope; - NEW.min_elevation := elevation.min_elevation; - NEW.max_elevation := elevation.max_elevation; - NEW.ascent := elevation.positive_gain; - NEW.descent := elevation.negative_gain; - RETURN NEW; - """ - ) - ] diff --git a/georiviere/description/migrations/0025_auto_20231215_1124.py b/georiviere/description/migrations/0025_auto_20231215_1124.py deleted file mode 100644 index 957a97dd..00000000 --- a/georiviere/description/migrations/0025_auto_20231215_1124.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-15 11:24 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('description', '0024_auto_20230323_1621'), - ] - - operations = [ - pgtrigger.migrations.AddTrigger( - model_name='land', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='82ffa7255d1e65c383b05ef0fa226da73a7ad6c8', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_fb8bb', table='description_land', when='BEFORE')), - ), - pgtrigger.migrations.AddTrigger( - model_name='morphology', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='f2e3c5183c820b762d1336a39135727ffc39c5e3', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_472ec', table='description_morphology', when='BEFORE')), - ), - pgtrigger.migrations.AddTrigger( - model_name='status', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='9ebcbb87cf2196d3c04e0e624571d3a49b1ebb69', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_aa87c', table='description_status', when='BEFORE')), - ), - pgtrigger.migrations.AddTrigger( - model_name='usage', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='c18fa3553e1c897d5a5e5f3b6cc5836c86429190', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_71b71', table='description_usage', when='BEFORE')), - ), - ] diff --git a/georiviere/description/migrations/0026_auto_20231218_1222.py b/georiviere/description/migrations/0026_auto_20231218_1222.py deleted file mode 100644 index b16732c9..00000000 --- a/georiviere/description/migrations/0026_auto_20231218_1222.py +++ /dev/null @@ -1,47 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 12:22 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('description', '0025_auto_20231215_1124'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='land', - name='keep_in_sync', - ), - pgtrigger.migrations.RemoveTrigger( - model_name='morphology', - name='keep_in_sync', - ), - pgtrigger.migrations.RemoveTrigger( - model_name='status', - name='keep_in_sync', - ), - pgtrigger.migrations.RemoveTrigger( - model_name='usage', - name='keep_in_sync', - ), - pgtrigger.migrations.AddTrigger( - model_name='land', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='d639f6850243942a05ed1f44f6cf79b88b563bad', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_fb8bb', table='description_land', when='BEFORE')), - ), - pgtrigger.migrations.AddTrigger( - model_name='morphology', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='048fe3697643f7682b5336def02a2a9d345947ae', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_472ec', table='description_morphology', when='BEFORE')), - ), - pgtrigger.migrations.AddTrigger( - model_name='status', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='967fa66b6c8eb5e17d5bfc49594c2573241d073b', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_aa87c', table='description_status', when='BEFORE')), - ), - pgtrigger.migrations.AddTrigger( - model_name='usage', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='69cb0cd06f0dccdca1f09d54169f5a673b87fca5', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_71b71', table='description_usage', when='BEFORE')), - ), - ] diff --git a/georiviere/description/models.py b/georiviere/description/models.py index cd686bb5..5be80af1 100644 --- a/georiviere/description/models.py +++ b/georiviere/description/models.py @@ -199,7 +199,7 @@ class Morphology(AddPropertyBufferMixin, TopologyMixin, TimeStampedModelMixin, class Meta: verbose_name = _("Morphology") verbose_name_plural = _("Morphologies") - triggers = AltimetryMixin.Meta.triggers + # triggers = AltimetryMixin.Meta.triggers def __str__(self): if self.main_flow: @@ -269,7 +269,6 @@ class Land(AddPropertyBufferMixin, TimeStampedModelMixin, WatershedPropertiesMix class Meta: verbose_name = _("Land") verbose_name_plural = _("Lands") - triggers = AltimetryMixin.Meta.triggers def __str__(self): return f"{self.land_type}" @@ -308,7 +307,6 @@ class Usage(AddPropertyBufferMixin, TimeStampedModelMixin, WatershedPropertiesMi class Meta: verbose_name = _("Usage") verbose_name_plural = _("Usages") - triggers = AltimetryMixin.Meta.triggers def __str__(self): return ', '.join(self.usage_types.values_list("label", flat=True)) @@ -357,7 +355,7 @@ class Status(TopologyMixin, AddPropertyBufferMixin, TimeStampedModelMixin, Water class Meta: verbose_name = _("Status") verbose_name_plural = _("Statuses") - triggers = AltimetryMixin.Meta.triggers + # triggers = AltimetryMixin.Meta.triggers def __str__(self): if self.status_types.count(): diff --git a/georiviere/description/sql/post_10_triggers.sql b/georiviere/description/sql/post_10_triggers.sql new file mode 100644 index 00000000..e801d311 --- /dev/null +++ b/georiviere/description/sql/post_10_triggers.sql @@ -0,0 +1,7 @@ +CREATE TRIGGER description_morphology_10_elevation +BEFORE INSERT OR UPDATE OF geom ON description_morphology +FOR EACH ROW EXECUTE PROCEDURE elevation(); + +CREATE TRIGGER description_status_10_elevation +BEFORE INSERT OR UPDATE OF geom ON description_status +FOR EACH ROW EXECUTE PROCEDURE elevation(); \ No newline at end of file diff --git a/georiviere/main/management/commands/migrate.py b/georiviere/main/management/commands/migrate.py index 70fef977..8b607a0e 100644 --- a/georiviere/main/management/commands/migrate.py +++ b/georiviere/main/management/commands/migrate.py @@ -22,8 +22,11 @@ def handle(self, *args, **options): check_srid_has_meter_unit() for app in apps.get_app_configs(): load_sql_files(app, 'pre') - load_sql_files(app, 'post') super().handle(*args, **options) - + for app in apps.get_app_configs(): + try: + load_sql_files(app, 'post') + except Exception: + pass call_command('sync_translation_fields', '--noinput') call_command('update_translation_fields') diff --git a/georiviere/main/sql/post_10_functions.sql b/georiviere/main/sql/post_10_functions.sql new file mode 100644 index 00000000..c11aba35 --- /dev/null +++ b/georiviere/main/sql/post_10_functions.sql @@ -0,0 +1,16 @@ +CREATE FUNCTION elevation() RETURNS trigger SECURITY DEFINER AS $$ +DECLARE + elevation elevation_infos; +BEGIN + SELECT * FROM ft_elevation_infos(NEW.geom, {{ ALTIMETRIC_PROFILE_STEP }}) INTO elevation; + -- Update path geometry + NEW.geom_3d := elevation.draped; + NEW.length := ST_3DLength(elevation.draped); + NEW.slope := elevation.slope; + NEW.min_elevation := elevation.min_elevation; + NEW.max_elevation := elevation.max_elevation; + NEW.ascent := elevation.positive_gain; + NEW.descent := elevation.negative_gain; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/georiviere/main/sql/pre_10_cleanup.sql b/georiviere/main/sql/pre_10_cleanup.sql new file mode 100644 index 00000000..1c034839 --- /dev/null +++ b/georiviere/main/sql/pre_10_cleanup.sql @@ -0,0 +1 @@ +DROP FUNCTION IF EXISTS elevation() CASCADE; \ No newline at end of file diff --git a/georiviere/proceeding/migrations/0004_auto_20231215_1124.py b/georiviere/proceeding/migrations/0004_auto_20231215_1124.py deleted file mode 100644 index 01a23413..00000000 --- a/georiviere/proceeding/migrations/0004_auto_20231215_1124.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-15 11:24 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('proceeding', '0003_auto_20210804_1014'), - ] - - operations = [ - pgtrigger.migrations.AddTrigger( - model_name='proceeding', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='4580ed3c25a4cd5a156ff3a0c1efd6c8301d8e25', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_6ba14', table='proceeding_proceeding', when='BEFORE')), - ), - ] diff --git a/georiviere/proceeding/migrations/0005_auto_20231218_1222.py b/georiviere/proceeding/migrations/0005_auto_20231218_1222.py deleted file mode 100644 index 5a7e4ac6..00000000 --- a/georiviere/proceeding/migrations/0005_auto_20231218_1222.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 12:22 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('proceeding', '0004_auto_20231215_1124'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='proceeding', - name='keep_in_sync', - ), - pgtrigger.migrations.AddTrigger( - model_name='proceeding', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='8be1e3d34cf0ef421d9f1750b10e7ef296d6a20d', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_6ba14', table='proceeding_proceeding', when='BEFORE')), - ), - ] diff --git a/georiviere/proceeding/models.py b/georiviere/proceeding/models.py index c88520b7..ab6627a3 100644 --- a/georiviere/proceeding/models.py +++ b/georiviere/proceeding/models.py @@ -49,7 +49,6 @@ def get_create_label(cls): class Meta: verbose_name = _("Proceeding") verbose_name_plural = _("Proceedings") - triggers = AltimetryMixin.Meta.triggers class Event(models.Model): diff --git a/georiviere/river/management/commands/load_rivers.py b/georiviere/river/management/commands/load_rivers.py index eca252d8..07b1acca 100644 --- a/georiviere/river/management/commands/load_rivers.py +++ b/georiviere/river/management/commands/load_rivers.py @@ -34,7 +34,7 @@ def handle(self, *args, **options): Stream.objects.all().delete() self.stdout.write(self.style.SUCCESS("done!")) - batch_size = 100 + batch_size = 50 objs = (Stream(geom=feat.geom.geos, source_location=Point(feat.geom.geos[0]), diff --git a/georiviere/river/migrations/0020_auto_20231215_1020.py b/georiviere/river/migrations/0020_auto_20231215_1020.py deleted file mode 100644 index e5421117..00000000 --- a/georiviere/river/migrations/0020_auto_20231215_1020.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-15 10:20 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0019_stream_description'), - ] - - operations = [ - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation JSONB;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='6c62fd64699ff351273ae8753db0b43dd5defaf7', operation='UPDATE OR INSERT', pgid='pgtrigger_keep_in_sync_7a86b', table='river_stream', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0021_auto_20231215_1051.py b/georiviere/river/migrations/0021_auto_20231215_1051.py deleted file mode 100644 index bd951d1f..00000000 --- a/georiviere/river/migrations/0021_auto_20231215_1051.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-15 10:51 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0020_auto_20231215_1020'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='stream', - name='keep_in_sync', - ), - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := element.draped;\n NEW.length := ST_3DLength(element.draped);\n NEW.slope := element.slope;\n NEW.min_elevation := element.min_elevation;\n NEW.max_elevation := element.max_elevation;\n NEW.ascent := element.positive_gain;\n NEW.descent := element.negative_gain;\n\n RETURN NEW;\n ', hash='fdeef3b3432dda345b1ec896b91e84eb120a9986', operation='UPDATE OR INSERT', pgid='pgtrigger_keep_in_sync_7a86b', table='river_stream', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0022_auto_20231215_1052.py b/georiviere/river/migrations/0022_auto_20231215_1052.py deleted file mode 100644 index 9a4e0b26..00000000 --- a/georiviere/river/migrations/0022_auto_20231215_1052.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-15 10:52 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0021_auto_20231215_1051'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='stream', - name='keep_in_sync', - ), - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='24de00d1e6f9be9e088db3410e54b0de328653a3', operation='UPDATE OR INSERT', pgid='pgtrigger_keep_in_sync_7a86b', table='river_stream', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0023_auto_20231215_1056.py b/georiviere/river/migrations/0023_auto_20231215_1056.py deleted file mode 100644 index 6e669b8a..00000000 --- a/georiviere/river/migrations/0023_auto_20231215_1056.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-15 10:56 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0022_auto_20231215_1052'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='stream', - name='keep_in_sync', - ), - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='168b84532fa3b4230f53fbd1cfdb1169c5d72a0a', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_7a86b', table='river_stream', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0024_auto_20231218_1226.py b/georiviere/river/migrations/0024_auto_20231218_1226.py deleted file mode 100644 index 4d4f21a1..00000000 --- a/georiviere/river/migrations/0024_auto_20231218_1226.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 12:26 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0023_auto_20231215_1056'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='stream', - name='keep_in_sync', - ), - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='e66f827b3e0e2d3e5f822a259752f2d8a48a7c26', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_7a86b', table='river_stream', when='BEFORE')), - ), - pgtrigger.migrations.AddTrigger( - model_name='topology', - trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; geom geometry;', func='\n IF (NEW.start_point != OLD.start_point OR NEW.end_point != OLD.end_point) THEN \n SELECT geom FROM river_stream r WHERE NEW.id = r.id INTO geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(geom, NEW.START_POINT, NEW.END_POINT)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(geom, NEW.START_POINT, NEW.END_POINT)\n WHERE topology_id = NEW.id; \n END IF;\n RETURN NEW;\n ', hash='db1df9343d9079232a59df61b34bfcd7bc2df48a', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0025_auto_20231218_1325.py b/georiviere/river/migrations/0025_auto_20231218_1325.py deleted file mode 100644 index 74c03ca5..00000000 --- a/georiviere/river/migrations/0025_auto_20231218_1325.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 13:25 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0024_auto_20231218_1226'), - ] - - operations = [ - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func='\n INSERT INTO river_topology (stream_id, start_position, end_position)\n VALUES (NEW.id, 0, 1) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom) VALUES (topology_morphology, NEW.geom);\n INSERT INTO river_topology (stream_id, start_position, end_position)\n VALUES (NEW.id, 0, 1) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom) VALUES (topology_status, NEW.geom);\n RETURN NEW;\n ', hash='c0b7e1615c64b92e36e49803f3001ebde7dd517e', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), - ), - ] diff --git a/georiviere/river/migrations/0026_auto_20231218_1356.py b/georiviere/river/migrations/0026_auto_20231218_1356.py deleted file mode 100644 index 45b90ad4..00000000 --- a/georiviere/river/migrations/0026_auto_20231218_1356.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 13:56 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0025_auto_20231218_1325'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='topology', - name='update_topology_geom', - ), - pgtrigger.migrations.AddTrigger( - model_name='topology', - trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; geom geometry;', func='\n IF (NEW.start_position != OLD.start_position OR NEW.end_position != OLD.end_position) THEN \n SELECT geom FROM river_stream r WHERE NEW.id = r.id INTO geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n END IF;\n RETURN NEW;\n ', hash='10d2e4894631bee97f91deeb607004d3a5295448', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0027_auto_20231218_1400.py b/georiviere/river/migrations/0027_auto_20231218_1400.py deleted file mode 100644 index f32c8f08..00000000 --- a/georiviere/river/migrations/0027_auto_20231218_1400.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:00 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0026_auto_20231218_1356'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='stream', - name='create_topologies', - ), - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func='\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom) VALUES (topology_morphology, NEW.geom);\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom) VALUES (topology_status, NEW.geom);\n RETURN NEW;\n ', hash='4ebe61234dd666bf424de96761a8af7e0a72560f', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), - ), - ] diff --git a/georiviere/river/migrations/0028_auto_20231218_1402.py b/georiviere/river/migrations/0028_auto_20231218_1402.py deleted file mode 100644 index 227b45d4..00000000 --- a/georiviere/river/migrations/0028_auto_20231218_1402.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:02 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0027_auto_20231218_1400'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='stream', - name='create_topologies', - ), - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func='\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom, date_insert, date_update) VALUES (topology_morphology, NEW.geom, NOW(), NOW());\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom, date_insert, date_update) VALUES (topology_status, NEW.geom, NOW(), NOW());\n RETURN NEW;\n ', hash='606f9274946baa820c7792e04a1f6559948a2e8e', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), - ), - ] diff --git a/georiviere/river/migrations/0029_auto_20231218_1403.py b/georiviere/river/migrations/0029_auto_20231218_1403.py deleted file mode 100644 index 8416c313..00000000 --- a/georiviere/river/migrations/0029_auto_20231218_1403.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:03 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0028_auto_20231218_1402'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='stream', - name='create_topologies', - ), - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func="\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update)\n VALUES (topology_morphology, NEW.geom, '', NOW(), NOW());\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom, date_insert, date_update) VALUES (topology_status, NEW.geom, NOW(), NOW());\n RETURN NEW;\n ", hash='cfb353801531d9e2c134a09c85d8a69807704a71', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), - ), - ] diff --git a/georiviere/river/migrations/0030_auto_20231218_1405.py b/georiviere/river/migrations/0030_auto_20231218_1405.py deleted file mode 100644 index 1ef79185..00000000 --- a/georiviere/river/migrations/0030_auto_20231218_1405.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:05 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0029_auto_20231218_1403'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='stream', - name='create_topologies', - ), - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func="\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update)\n VALUES (topology_morphology, NEW.geom, '', NOW(), NOW());\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom, regulation, referencial, descrption, date_insert, date_update)\n VALUES (topology_status, NEW.geom, FALSE, FALSE, '', NOW(), NOW());\n RETURN NEW;\n ", hash='f5c7079e6d4e21c67d642ea14d61d4e2f39787b0', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), - ), - ] diff --git a/georiviere/river/migrations/0031_auto_20231218_1405.py b/georiviere/river/migrations/0031_auto_20231218_1405.py deleted file mode 100644 index 2dbd1031..00000000 --- a/georiviere/river/migrations/0031_auto_20231218_1405.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:05 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0030_auto_20231218_1405'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='stream', - name='create_topologies', - ), - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func="\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update)\n VALUES (topology_morphology, NEW.geom, '', NOW(), NOW());\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom, regulation, referencial, description, date_insert, date_update)\n VALUES (topology_status, NEW.geom, FALSE, FALSE, '', NOW(), NOW());\n RETURN NEW;\n ", hash='e846f76834647fd2338681a9530765b6d1883cc9', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), - ), - ] diff --git a/georiviere/river/migrations/0032_auto_20231218_1410.py b/georiviere/river/migrations/0032_auto_20231218_1410.py deleted file mode 100644 index 1e19b353..00000000 --- a/georiviere/river/migrations/0032_auto_20231218_1410.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:10 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0031_auto_20231218_1405'), - ] - - operations = [ - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='update_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(func='\n UPDATE river_topology SET start_position=start_position WHERE stream_id=NEW.id;\n RETURN NEW;\n ', hash='0b6322b6b455eed7b6a2a78c79b07e08e9a86611', operation='UPDATE OF "geom"', pgid='pgtrigger_update_topologies_6ab45', table='river_stream', when='AFTER')), - ), - ] diff --git a/georiviere/river/migrations/0033_auto_20231218_1412.py b/georiviere/river/migrations/0033_auto_20231218_1412.py deleted file mode 100644 index 1b5d52e2..00000000 --- a/georiviere/river/migrations/0033_auto_20231218_1412.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:12 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0032_auto_20231218_1410'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='topology', - name='update_topology_geom', - ), - pgtrigger.migrations.AddTrigger( - model_name='topology', - trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; geom geometry;', func='\n SELECT geom FROM river_stream r WHERE NEW.id = r.id INTO geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='2b83b20dab6aa7e5456c1e10694f527dc98eb281', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0034_auto_20231218_1416.py b/georiviere/river/migrations/0034_auto_20231218_1416.py deleted file mode 100644 index 653b8de5..00000000 --- a/georiviere/river/migrations/0034_auto_20231218_1416.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:16 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0033_auto_20231218_1412'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='topology', - name='update_topology_geom', - ), - pgtrigger.migrations.AddTrigger( - model_name='topology', - trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.id = r.id INTO geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='1e3e1dbd526ffd01e8155aec62fad614e188471d', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0035_auto_20231218_1418.py b/georiviere/river/migrations/0035_auto_20231218_1418.py deleted file mode 100644 index e92cd864..00000000 --- a/georiviere/river/migrations/0035_auto_20231218_1418.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:18 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0034_auto_20231218_1416'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='topology', - name='update_topology_geom', - ), - pgtrigger.migrations.AddTrigger( - model_name='topology', - trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.id = r.id INTO geom;\n UPDATE description_morphology m\n SET m.geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status s\n SET s.geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='a3d43086e66e04cde5bf979df1f4565a3bb9e791', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0036_auto_20231218_1420.py b/georiviere/river/migrations/0036_auto_20231218_1420.py deleted file mode 100644 index 4421a74f..00000000 --- a/georiviere/river/migrations/0036_auto_20231218_1420.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:20 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0035_auto_20231218_1418'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='topology', - name='update_topology_geom', - ), - pgtrigger.migrations.AddTrigger( - model_name='topology', - trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; stream_geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.id = r.id INTO stream_geom;\n UPDATE description_morphology m\n SET m.geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status s\n SET s.geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='1671ae29035c32a992993d3518965e8e9c0a605f', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0037_auto_20231218_1421.py b/georiviere/river/migrations/0037_auto_20231218_1421.py deleted file mode 100644 index 1739897b..00000000 --- a/georiviere/river/migrations/0037_auto_20231218_1421.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:21 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0036_auto_20231218_1420'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='topology', - name='update_topology_geom', - ), - pgtrigger.migrations.AddTrigger( - model_name='topology', - trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; stream_geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.id = r.id INTO stream_geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='444948eb36585ca5ea4dfdad5e9be32137e67844', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0038_auto_20231218_1431.py b/georiviere/river/migrations/0038_auto_20231218_1431.py deleted file mode 100644 index 26019979..00000000 --- a/georiviere/river/migrations/0038_auto_20231218_1431.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:31 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0037_auto_20231218_1421'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='topology', - name='update_topology_geom', - ), - pgtrigger.migrations.AddTrigger( - model_name='topology', - trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE stream_geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.id = r.id INTO stream_geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='f0a3386a26d41f8268db5bbdb554ba4cc3ae1612', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='AFTER')), - ), - ] diff --git a/georiviere/river/migrations/0039_auto_20231218_1601.py b/georiviere/river/migrations/0039_auto_20231218_1601.py deleted file mode 100644 index 06a328cb..00000000 --- a/georiviere/river/migrations/0039_auto_20231218_1601.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 16:01 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0038_auto_20231218_1431'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='stream', - name='update_topologies', - ), - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='update_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(func='\n UPDATE river_topology SET id=id WHERE stream_id=NEW.id;\n RETURN NEW;\n ', hash='704192b93012d12fa95b06b03618981a49609a4e', operation='UPDATE OF "geom"', pgid='pgtrigger_update_topologies_6ab45', table='river_stream', when='AFTER')), - ), - ] diff --git a/georiviere/river/migrations/0040_auto_20231218_1608.py b/georiviere/river/migrations/0040_auto_20231218_1608.py deleted file mode 100644 index ad4c8f4d..00000000 --- a/georiviere/river/migrations/0040_auto_20231218_1608.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 16:08 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0039_auto_20231218_1601'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='topology', - name='update_topology_geom', - ), - pgtrigger.migrations.AddTrigger( - model_name='topology', - trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE stream_geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.stream_id = r.id INTO stream_geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='4d63b6e0ef200235882742af37d834d00b5a271a', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='AFTER')), - ), - ] diff --git a/georiviere/river/migrations/0041_auto_20231219_0932.py b/georiviere/river/migrations/0041_auto_20231219_0932.py deleted file mode 100644 index 12429cd4..00000000 --- a/georiviere/river/migrations/0041_auto_20231219_0932.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-19 09:32 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0040_auto_20231218_1608'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='topology', - name='update_topology_geom', - ), - pgtrigger.migrations.AddTrigger( - model_name='topology', - trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE stream_geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.stream_id = r.id INTO stream_geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id;\n UPDATE description_status\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id;\n RETURN NEW;\n ', hash='0d8dce1088e59bd290e9ffe0d6bfb8a2493a5c1c', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='AFTER')), - ), - ] diff --git a/georiviere/river/models.py b/georiviere/river/models.py index 9283a84c..041cab52 100644 --- a/georiviere/river/models.py +++ b/georiviere/river/models.py @@ -1,4 +1,3 @@ -import pgtrigger from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.contrib.gis.db import models @@ -97,37 +96,37 @@ class FlowChoices(models.IntegerChoices): class Meta: verbose_name = _("Stream") verbose_name_plural = _("Streams") - triggers = AltimetryMixin.Meta.triggers + [ - pgtrigger.Trigger( - name="create_topologies", - operation=pgtrigger.Insert, - declare=[ - ('topology_morphology', 'integer'), - ('topology_status', 'integer') - ], - when=pgtrigger.After, - func=""" - INSERT INTO river_topology (stream_id, start_position, end_position, qualified) - VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology; - INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update) - VALUES (topology_morphology, NEW.geom, '', NOW(), NOW()); - INSERT INTO river_topology (stream_id, start_position, end_position, qualified) - VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status; - INSERT INTO description_status (topology_id, geom, regulation, referencial, description, date_insert, date_update) - VALUES (topology_status, NEW.geom, FALSE, FALSE, '', NOW(), NOW()); - RETURN NEW; - """ - ), - pgtrigger.Trigger( - name="update_topologies", - operation=pgtrigger.UpdateOf('geom'), - when=pgtrigger.After, - func=""" - UPDATE river_topology SET id=id WHERE stream_id=NEW.id; - RETURN NEW; - """ - ) - ] + # triggers = AltimetryMixin.Meta.triggers + [ + # pgtrigger.Trigger( + # name="create_topologies", + # operation=pgtrigger.Insert, + # declare=[ + # ('topology_morphology', 'integer'), + # ('topology_status', 'integer') + # ], + # when=pgtrigger.After, + # func=""" + # INSERT INTO river_topology (stream_id, start_position, end_position, qualified) + # VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology; + # INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update) + # VALUES (topology_morphology, NEW.geom, '', NOW(), NOW()); + # INSERT INTO river_topology (stream_id, start_position, end_position, qualified) + # VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status; + # INSERT INTO description_status (topology_id, geom, regulation, referencial, description, date_insert, date_update) + # VALUES (topology_status, NEW.geom, FALSE, FALSE, '', NOW(), NOW()); + # RETURN NEW; + # """ + # ), + # pgtrigger.Trigger( + # name="update_topologies", + # operation=pgtrigger.UpdateOf('geom'), + # when=pgtrigger.After, + # func=""" + # UPDATE river_topology SET id=id WHERE stream_id=NEW.id; + # RETURN NEW; + # """ + # ) + # ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -251,23 +250,23 @@ def __str__(self): class Meta: verbose_name = _("Topology") verbose_name_plural = _("Topologies") - triggers = [ - pgtrigger.Trigger( - name="update_topology_geom", - operation=pgtrigger.Update | pgtrigger.Insert, - when=pgtrigger.After, - declare=[('stream_geom', 'geometry')], - func=""" - SELECT r.geom FROM river_stream r WHERE NEW.stream_id = r.id INTO stream_geom; - UPDATE description_morphology - SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position) - WHERE topology_id = NEW.id; - UPDATE description_status - SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position) - WHERE topology_id = NEW.id; - RETURN NEW; - """ - )] + # triggers = [ + # pgtrigger.Trigger( + # name="update_topology_geom", + # operation=pgtrigger.Update | pgtrigger.Insert, + # when=pgtrigger.After, + # declare=[('stream_geom', 'geometry')], + # func=""" + # SELECT r.geom FROM river_stream r WHERE NEW.stream_id = r.id INTO stream_geom; + # UPDATE description_morphology + # SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position) + # WHERE topology_id = NEW.id; + # UPDATE description_status + # SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position) + # WHERE topology_id = NEW.id; + # RETURN NEW; + # """ + # )] Study.add_property('stations', Station.within_buffer, _("Stations")) diff --git a/georiviere/river/sql/post_10_triggers.sql b/georiviere/river/sql/post_10_triggers.sql new file mode 100644 index 00000000..f83233af --- /dev/null +++ b/georiviere/river/sql/post_10_triggers.sql @@ -0,0 +1,43 @@ +CREATE TRIGGER river_stream_10_elevation +BEFORE INSERT OR UPDATE OF geom ON river_stream +FOR EACH ROW EXECUTE PROCEDURE elevation(); + +CREATE FUNCTION update_topology_geom() RETURNS trigger SECURITY DEFINER AS $$ +DECLARE + stream_geom geometry; +BEGIN + SELECT r.geom FROM river_stream r WHERE NEW.stream_id = r.id INTO stream_geom; + UPDATE description_morphology + SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position) + WHERE topology_id = NEW.id; + UPDATE description_status + SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position) + WHERE topology_id = NEW.id; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER core_path_10_elevation_iu_tgr +BEFORE INSERT OR UPDATE ON river_topology +FOR EACH ROW EXECUTE PROCEDURE update_topology_geom(); + +CREATE FUNCTION create_topologies() RETURNS trigger SECURITY DEFINER AS $$ +DECLARE + topology_morphology integer; + topology_status integer; +BEGIN + INSERT INTO river_topology (stream_id, start_position, end_position, qualified) + VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology; + INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update) + VALUES (topology_morphology, NEW.geom, '', NOW(), NOW()); + INSERT INTO river_topology (stream_id, start_position, end_position, qualified) + VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status; + INSERT INTO description_status (topology_id, geom, regulation, referencial, description, date_insert, date_update) + VALUES (topology_status, NEW.geom, FALSE, FALSE, '', NOW(), NOW()); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER core_path_10_elevation_iu_tgr +AFTER INSERT ON river_stream +FOR EACH ROW EXECUTE PROCEDURE create_topologies(); \ No newline at end of file diff --git a/georiviere/river/sql/pre_10_cleanup.sql b/georiviere/river/sql/pre_10_cleanup.sql new file mode 100644 index 00000000..79b37e8f --- /dev/null +++ b/georiviere/river/sql/pre_10_cleanup.sql @@ -0,0 +1,3 @@ +DROP FUNCTION IF EXISTS update_topology_geom() CASCADE; +DROP FUNCTION IF EXISTS create_topologies() CASCADE; + diff --git a/georiviere/settings/__init__.py b/georiviere/settings/__init__.py index 00e4741c..56221baf 100644 --- a/georiviere/settings/__init__.py +++ b/georiviere/settings/__init__.py @@ -135,7 +135,7 @@ def construct_relative_path_mock(current_template_name, relative_name): 'embed_video', 'djgeojson', 'django_filters', - 'pgtrigger', + #'pgtrigger', 'compressor', 'mapentity', # mapentity should be placed after app declaring paperclip models 'leaflet', diff --git a/requirements.in b/requirements.in index c09d1eb1..a4d9353d 100644 --- a/requirements.in +++ b/requirements.in @@ -14,7 +14,6 @@ sentry-sdk django-admin-ordering djangorestframework-camel-case jsonschema -django-pgtrigger # Use until latest version of geotrek (> 2.99.0) drf-spectacular diff --git a/requirements.txt b/requirements.txt index 34e85eab..03f5a151 100644 --- a/requirements.txt +++ b/requirements.txt @@ -91,7 +91,6 @@ django==3.1.14 # django-leaflet # django-modeltranslation # django-mptt - # django-pgtrigger # django-querysetsequence # django-weasyprint # djangorestframework @@ -145,8 +144,6 @@ django-modeltranslation==0.17.3 # via mapentity django-mptt==0.11.0 # via geotrek -django-pgtrigger==4.11.0 - # via -r requirements.in django-querysetsequence==0.14 # via django-autocomplete-light django-tinymce==2.9.0 From dbd71b0d11341b60a324202542dddf6084244922 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 14:20:09 +0100 Subject: [PATCH 012/114] use internal triggers --- georiviere/altimetry.py | 11 +++++++++++ georiviere/river/models.py | 31 ------------------------------- 2 files changed, 11 insertions(+), 31 deletions(-) diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index d36c1c74..9306f7e7 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -2,5 +2,16 @@ class AltimetryMixin(BaseAltimetryMixin): + def reload(self): + # Update object's computed values (reload from database) + if self.pk: + fromdb = self.__class__.objects.get(pk=self.pk) + BaseAltimetryMixin.reload(self, fromdb) + return self + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + self.reload() + class Meta: abstract = True diff --git a/georiviere/river/models.py b/georiviere/river/models.py index 041cab52..4aa38376 100644 --- a/georiviere/river/models.py +++ b/georiviere/river/models.py @@ -96,37 +96,6 @@ class FlowChoices(models.IntegerChoices): class Meta: verbose_name = _("Stream") verbose_name_plural = _("Streams") - # triggers = AltimetryMixin.Meta.triggers + [ - # pgtrigger.Trigger( - # name="create_topologies", - # operation=pgtrigger.Insert, - # declare=[ - # ('topology_morphology', 'integer'), - # ('topology_status', 'integer') - # ], - # when=pgtrigger.After, - # func=""" - # INSERT INTO river_topology (stream_id, start_position, end_position, qualified) - # VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology; - # INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update) - # VALUES (topology_morphology, NEW.geom, '', NOW(), NOW()); - # INSERT INTO river_topology (stream_id, start_position, end_position, qualified) - # VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status; - # INSERT INTO description_status (topology_id, geom, regulation, referencial, description, date_insert, date_update) - # VALUES (topology_status, NEW.geom, FALSE, FALSE, '', NOW(), NOW()); - # RETURN NEW; - # """ - # ), - # pgtrigger.Trigger( - # name="update_topologies", - # operation=pgtrigger.UpdateOf('geom'), - # when=pgtrigger.After, - # func=""" - # UPDATE river_topology SET id=id WHERE stream_id=NEW.id; - # RETURN NEW; - # """ - # ) - # ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) From 7099bbf3d5a07fd5429d1103d9f54aea621e23b9 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 14:22:24 +0100 Subject: [PATCH 013/114] use internal triggers --- georiviere/altimetry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index 9306f7e7..fb32ca56 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -8,10 +8,10 @@ def reload(self): fromdb = self.__class__.objects.get(pk=self.pk) BaseAltimetryMixin.reload(self, fromdb) return self - + def save(self, *args, **kwargs): super().save(*args, **kwargs) self.reload() - + class Meta: abstract = True From 1b400a253e59e37bd89f8b78645eee09bcc0aedd Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 14:30:17 +0100 Subject: [PATCH 014/114] use internal triggers --- georiviere/altimetry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index fb32ca56..a4c58d43 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -11,7 +11,7 @@ def reload(self): def save(self, *args, **kwargs): super().save(*args, **kwargs) - self.reload() + self.reload(self) class Meta: abstract = True From 99ff1b3ae927887cfa2204552ebe45746d11f7c3 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 14:31:46 +0100 Subject: [PATCH 015/114] use internal triggers --- georiviere/altimetry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index a4c58d43..1426eacf 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -6,12 +6,12 @@ def reload(self): # Update object's computed values (reload from database) if self.pk: fromdb = self.__class__.objects.get(pk=self.pk) - BaseAltimetryMixin.reload(self, fromdb) + super().reload(fromdb) return self def save(self, *args, **kwargs): super().save(*args, **kwargs) - self.reload(self) + self.reload() class Meta: abstract = True From 973d1348982f0a81a942a4219051811dea306d50 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 14:32:08 +0100 Subject: [PATCH 016/114] use internal triggers --- georiviere/altimetry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index 1426eacf..41d40b49 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -2,7 +2,7 @@ class AltimetryMixin(BaseAltimetryMixin): - def reload(self): + def reload(self, *args, **kwargs): # Update object's computed values (reload from database) if self.pk: fromdb = self.__class__.objects.get(pk=self.pk) From f02e638f2118a2969e94d0e40b29bdb29448b05c Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 14:51:20 +0100 Subject: [PATCH 017/114] use internal triggers --- georiviere/altimetry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index 41d40b49..cf1d67ce 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -2,16 +2,16 @@ class AltimetryMixin(BaseAltimetryMixin): - def reload(self, *args, **kwargs): + def refresh(self): # Update object's computed values (reload from database) if self.pk: fromdb = self.__class__.objects.get(pk=self.pk) - super().reload(fromdb) + BaseAltimetryMixin.reload(fromdb) return self def save(self, *args, **kwargs): super().save(*args, **kwargs) - self.reload() + self.refresh() class Meta: abstract = True From fb86bd793318a79df581e0ee4fb83ada49a0c4f1 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 14:59:29 +0100 Subject: [PATCH 018/114] use internal triggers --- georiviere/altimetry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index cf1d67ce..fb32ca56 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -2,16 +2,16 @@ class AltimetryMixin(BaseAltimetryMixin): - def refresh(self): + def reload(self): # Update object's computed values (reload from database) if self.pk: fromdb = self.__class__.objects.get(pk=self.pk) - BaseAltimetryMixin.reload(fromdb) + BaseAltimetryMixin.reload(self, fromdb) return self def save(self, *args, **kwargs): super().save(*args, **kwargs) - self.refresh() + self.reload() class Meta: abstract = True From 190dd0a1afeecb20941a6913c37592160f20b1c1 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 15:07:47 +0100 Subject: [PATCH 019/114] use internal triggers --- georiviere/altimetry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index fb32ca56..f2bf5482 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -2,7 +2,7 @@ class AltimetryMixin(BaseAltimetryMixin): - def reload(self): + def refresh(self): # Update object's computed values (reload from database) if self.pk: fromdb = self.__class__.objects.get(pk=self.pk) @@ -11,7 +11,7 @@ def reload(self): def save(self, *args, **kwargs): super().save(*args, **kwargs) - self.reload() + self.refresh() class Meta: abstract = True From 111e06ded495db016c0c13b5cb50252687c459bb Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 15:27:07 +0100 Subject: [PATCH 020/114] use internal triggers --- georiviere/description/sql/post_10_triggers.sql | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/georiviere/description/sql/post_10_triggers.sql b/georiviere/description/sql/post_10_triggers.sql index e801d311..25bdd79b 100644 --- a/georiviere/description/sql/post_10_triggers.sql +++ b/georiviere/description/sql/post_10_triggers.sql @@ -4,4 +4,8 @@ FOR EACH ROW EXECUTE PROCEDURE elevation(); CREATE TRIGGER description_status_10_elevation BEFORE INSERT OR UPDATE OF geom ON description_status +FOR EACH ROW EXECUTE PROCEDURE elevation(); + +CREATE TRIGGER description_land_10_elevation +BEFORE INSERT OR UPDATE OF geom ON description_land FOR EACH ROW EXECUTE PROCEDURE elevation(); \ No newline at end of file From e50be9da37bd6e1dab1af9465477ecf74db56ae6 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 15:57:53 +0100 Subject: [PATCH 021/114] use internal triggers --- georiviere/description/models.py | 2 -- georiviere/description/sql/post_10_triggers.sql | 4 ++++ georiviere/proceeding/sql/post_10_triggers.sql | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 georiviere/proceeding/sql/post_10_triggers.sql diff --git a/georiviere/description/models.py b/georiviere/description/models.py index 5be80af1..ca1aaaf2 100644 --- a/georiviere/description/models.py +++ b/georiviere/description/models.py @@ -199,7 +199,6 @@ class Morphology(AddPropertyBufferMixin, TopologyMixin, TimeStampedModelMixin, class Meta: verbose_name = _("Morphology") verbose_name_plural = _("Morphologies") - # triggers = AltimetryMixin.Meta.triggers def __str__(self): if self.main_flow: @@ -355,7 +354,6 @@ class Status(TopologyMixin, AddPropertyBufferMixin, TimeStampedModelMixin, Water class Meta: verbose_name = _("Status") verbose_name_plural = _("Statuses") - # triggers = AltimetryMixin.Meta.triggers def __str__(self): if self.status_types.count(): diff --git a/georiviere/description/sql/post_10_triggers.sql b/georiviere/description/sql/post_10_triggers.sql index 25bdd79b..958dbec4 100644 --- a/georiviere/description/sql/post_10_triggers.sql +++ b/georiviere/description/sql/post_10_triggers.sql @@ -8,4 +8,8 @@ FOR EACH ROW EXECUTE PROCEDURE elevation(); CREATE TRIGGER description_land_10_elevation BEFORE INSERT OR UPDATE OF geom ON description_land +FOR EACH ROW EXECUTE PROCEDURE elevation(); + +CREATE TRIGGER description_usage_10_elevation +BEFORE INSERT OR UPDATE OF geom ON description_usage FOR EACH ROW EXECUTE PROCEDURE elevation(); \ No newline at end of file diff --git a/georiviere/proceeding/sql/post_10_triggers.sql b/georiviere/proceeding/sql/post_10_triggers.sql new file mode 100644 index 00000000..fd0e2b8f --- /dev/null +++ b/georiviere/proceeding/sql/post_10_triggers.sql @@ -0,0 +1,3 @@ +CREATE TRIGGER proceeding_proceeding_10_elevation +BEFORE INSERT OR UPDATE OF geom ON proceeding_proceeding +FOR EACH ROW EXECUTE PROCEDURE elevation(); From d90e7751c6a6e81a3b938941e336c79554905581 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 3 Jan 2024 14:36:57 +0100 Subject: [PATCH 022/114] use internal triggers --- georiviere/altimetry.py | 2 ++ georiviere/description/sql/post_20_defaults.sql | 3 +++ georiviere/description/tests/test_views.py | 8 ++++---- georiviere/river/tests/test_models.py | 8 ++++---- georiviere/river/tests/test_signals.py | 2 +- 5 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 georiviere/description/sql/post_20_defaults.sql diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index f2bf5482..cd7ee6b9 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -1,4 +1,5 @@ from geotrek.altimetry.models import AltimetryMixin as BaseAltimetryMixin +from geotrek.common.mixins import TimeStampedModelMixin class AltimetryMixin(BaseAltimetryMixin): @@ -7,6 +8,7 @@ def refresh(self): if self.pk: fromdb = self.__class__.objects.get(pk=self.pk) BaseAltimetryMixin.reload(self, fromdb) + TimeStampedModelMixin.reload(self, fromdb) return self def save(self, *args, **kwargs): diff --git a/georiviere/description/sql/post_20_defaults.sql b/georiviere/description/sql/post_20_defaults.sql new file mode 100644 index 00000000..5128eb1b --- /dev/null +++ b/georiviere/description/sql/post_20_defaults.sql @@ -0,0 +1,3 @@ +ALTER TABLE description_morphology ALTER COLUMN full_edge_height SET DEFAULT 0.0; +ALTER TABLE description_morphology ALTER COLUMN full_edge_width SET DEFAULT 0.0; +ALTER TABLE description_morphology ALTER COLUMN description SET DEFAULT ''; diff --git a/georiviere/description/tests/test_views.py b/georiviere/description/tests/test_views.py index 86940023..5e2b4f54 100644 --- a/georiviere/description/tests/test_views.py +++ b/georiviere/description/tests/test_views.py @@ -105,8 +105,8 @@ class SatusViewTestCase(TopologyTestCase): def get_expected_json_attrs(self): return { 'id': self.obj.pk, - 'date_update': '2020-03-17T00:00:00Z', - 'date_insert': '2020-03-17T00:00:00Z', + 'date_update': self.obj.date_update.isoformat().replace('+00:00', 'Z'), + 'date_insert': self.obj.date_insert.isoformat().replace('+00:00', 'Z'), 'description': '', 'length': self.obj.length, 'geom_3d': self.obj.geom_3d.ewkt, @@ -147,8 +147,8 @@ class MorphologyViewTestCase(TopologyTestCase): def get_expected_json_attrs(self): return { 'id': self.obj.pk, - 'date_update': '2020-03-17T00:00:00Z', - 'date_insert': '2020-03-17T00:00:00Z', + 'date_update': self.obj.date_update.isoformat().replace('+00:00', 'Z'), + 'date_insert': self.obj.date_insert.isoformat().replace('+00:00', 'Z'), 'description': '', 'bank_state_left': None, 'bank_state_right': None, diff --git a/georiviere/river/tests/test_models.py b/georiviere/river/tests/test_models.py index d37a240e..69c8ac89 100644 --- a/georiviere/river/tests/test_models.py +++ b/georiviere/river/tests/test_models.py @@ -90,16 +90,16 @@ def test_get_map_image_extent(self): self.assertAlmostEqual(lat_max, -5.655019875165679) def test_distance_to_source(self): - """Test distance from a given object to stream source according to differents geom""" + """Test distance from a given object to stream source according to different geom""" self.assertAlmostEqual(self.stream1.distance_to_source(self.usage_point), 28.2842712) self.assertEqual(self.stream1.distance_to_source(self.administrative_file), None) - self.assertEqual(DistanceToSource.objects.count(), 6) + self.assertEqual(DistanceToSource.objects.count(), 2) usage_point = UsageFactory.create( geom=Point(10000, 10010) ) - self.assertEqual(DistanceToSource.objects.count(), 8) + self.assertEqual(DistanceToSource.objects.count(), 4) usage_point.delete() - self.assertEqual(DistanceToSource.objects.count(), 6) + self.assertEqual(DistanceToSource.objects.count(), 2) class SnapTest(TestCase): diff --git a/georiviere/river/tests/test_signals.py b/georiviere/river/tests/test_signals.py index 148669ad..f947d67d 100644 --- a/georiviere/river/tests/test_signals.py +++ b/georiviere/river/tests/test_signals.py @@ -30,5 +30,5 @@ def test_update_stream_move_topologies(self): morphologies = Morphology.objects.values_list('geom', flat=True) status = Status.objects.values_list('geom', flat=True) - self.assertEqual(morphologies[0], morphologies[1]) + self.assertEqual(morphologies[0], morphologies[1], f"{morphologies[0].ewkt} - {morphologies[1].ewkt}", ) self.assertEqual(status[0], status[1]) From 93bf9ba69645524db0ead9cdc14a9068316ef65a Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 28 Feb 2024 10:30:12 +0100 Subject: [PATCH 023/114] use internal triggers --- georiviere/main/apps.py | 1 + georiviere/main/signals.py | 3 ++- georiviere/settings/__init__.py | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/georiviere/main/apps.py b/georiviere/main/apps.py index 3cb9f0b9..8e84238c 100644 --- a/georiviere/main/apps.py +++ b/georiviere/main/apps.py @@ -15,5 +15,6 @@ def ready(self): from georiviere.river.models import Stream for model in apps.get_models(): if issubclass(model, MapEntityMixin) and model != Stream: + print("signal for model {}".format(model)) post_save.connect(signals.save_objects_generate_distance_to_source, sender=model) post_delete.connect(signals.delete_objects_remove_distance_to_source, sender=model) diff --git a/georiviere/main/signals.py b/georiviere/main/signals.py index f10be4a8..e2c2aa74 100644 --- a/georiviere/main/signals.py +++ b/georiviere/main/signals.py @@ -45,7 +45,8 @@ def save_objects_generate_distance_to_source(sender, instance, **kwargs): ).exclude(stream__in=streams).delete() elif hasattr(instance, 'topology'): - stream = annotate_distance_to_source(Stream.objects.all(), instance).get(pk=instance.topology.stream.pk) + raise Exception("yes") + stream = annotate_distance_to_source(Stream.objects.all(), instance).get(pk=instance.topology.stream_id) DistanceToSource.objects.update_or_create( object_id=instance.pk, content_type=ContentType.objects.get_for_model(instance._meta.model), diff --git a/georiviere/settings/__init__.py b/georiviere/settings/__init__.py index 56221baf..4daf8d0f 100644 --- a/georiviere/settings/__init__.py +++ b/georiviere/settings/__init__.py @@ -135,7 +135,6 @@ def construct_relative_path_mock(current_template_name, relative_name): 'embed_video', 'djgeojson', 'django_filters', - #'pgtrigger', 'compressor', 'mapentity', # mapentity should be placed after app declaring paperclip models 'leaflet', From 7f8d61caa067407c5bc8671edc95dffe893a72d3 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 28 Feb 2024 10:56:50 +0100 Subject: [PATCH 024/114] use internal triggers --- georiviere/main/signals.py | 1 - 1 file changed, 1 deletion(-) diff --git a/georiviere/main/signals.py b/georiviere/main/signals.py index e2c2aa74..758e1bf2 100644 --- a/georiviere/main/signals.py +++ b/georiviere/main/signals.py @@ -45,7 +45,6 @@ def save_objects_generate_distance_to_source(sender, instance, **kwargs): ).exclude(stream__in=streams).delete() elif hasattr(instance, 'topology'): - raise Exception("yes") stream = annotate_distance_to_source(Stream.objects.all(), instance).get(pk=instance.topology.stream_id) DistanceToSource.objects.update_or_create( object_id=instance.pk, From 18617915263b56896186ab1583e6bbc10d665d26 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Thu, 11 Apr 2024 09:54:39 +0200 Subject: [PATCH 025/114] fix command --- georiviere/contribution/schema.py | 79 ++++++++++--------- .../river/management/commands/load_rivers.py | 19 +++-- georiviere/river/managers.py | 6 ++ georiviere/river/models.py | 20 +---- georiviere/utils/__init__.py | 0 georiviere/utils/mixins/managers.py | 7 ++ 6 files changed, 70 insertions(+), 61 deletions(-) create mode 100644 georiviere/river/managers.py create mode 100644 georiviere/utils/__init__.py create mode 100644 georiviere/utils/mixins/managers.py diff --git a/georiviere/contribution/schema.py b/georiviere/contribution/schema.py index 0c85d301..1315881c 100644 --- a/georiviere/contribution/schema.py +++ b/georiviere/contribution/schema.py @@ -7,9 +7,9 @@ InvasiveSpecies, HeritageSpecies, HeritageObservation, FishSpecies, NaturePollution, TypePollution) -# The json schema is summarize on : +# The json schema is summarized on : # https://github.com/Georiviere/Georiviere-admin/issues/139 -# Depending of the category and type of the contributions, some fields are available or not. +# Depending on the category and type of the contributions, some fields are available or not. # Here is the generation of the json schema used by the website portal. # The fields available depending on the type of contributions follow the documentation of jsonschema : # https://json-schema.org/understanding-json-schema/reference/conditionals.html @@ -18,37 +18,43 @@ def get_contribution_properties(): """ Feature properties as form initial data format (name / value) """ # TODO: Use directly field definition for type / title / max length - results = {'name_author': { - 'type': "string", - 'title': _("Name author"), - "maxLength": 128 - }, 'first_name_author': { - 'type': "string", - 'title': _("First name author"), - "maxLength": 128 - }, 'email_author': { - 'type': "string", - 'title': _("Email"), - 'format': "email" - }, 'date_observation': { - 'type': "string", - 'title': _("Observation's date"), - 'format': 'date' - }, 'description': { - 'type': "string", - 'title': _('Description') - }, 'category': { - "type": "string", - "title": _("Category"), - # TODO: Loop on contribution one to one field to get all possibilities - "enum": [ - str(ContributionQuantity._meta.verbose_name.title()), - str(ContributionQuality._meta.verbose_name.title()), - str(ContributionFaunaFlora._meta.verbose_name.title()), - str(ContributionLandscapeElements._meta.verbose_name.title()), - str(ContributionPotentialDamage._meta.verbose_name.title()) - ], - } + results = { + 'name_author': { + 'type': "string", + 'title': _("Name author"), + "maxLength": 128 + }, + 'first_name_author': { + 'type': "string", + 'title': _("First name author"), + "maxLength": 128 + }, + 'email_author': { + 'type': "string", + 'title': _("Email"), + 'format': "email" + }, + 'date_observation': { + 'type': "string", + 'title': _("Observation's date"), + 'format': 'date' + }, + 'description': { + 'type': "string", + 'title': _('Description') + }, + 'category': { + "type": "string", + "title": _("Category"), + # TODO: Loop on contribution one to one field to get all possibilities + "enum": [ + str(ContributionQuantity._meta.verbose_name.title()), + str(ContributionQuality._meta.verbose_name.title()), + str(ContributionFaunaFlora._meta.verbose_name.title()), + str(ContributionLandscapeElements._meta.verbose_name.title()), + str(ContributionPotentialDamage._meta.verbose_name.title()) + ], + } } if SeverityType.objects.exists(): results['severity'] = { @@ -110,8 +116,7 @@ def get_disruptive_jam(choices, meta): 'jam_type': { 'type': "string", - 'title': str(meta.get_field( - 'jam_type').related_model._meta.verbose_name.capitalize()), + 'title': str(meta.get_field('jam_type').related_model._meta.verbose_name.capitalize()), 'enum': list(JamType.objects.values_list('label', flat=True)) } }, @@ -125,7 +130,9 @@ def get_bank_erosion(choices, meta): 'if': { 'properties': { 'type': { - 'const': str(choices.BANK_EROSION.label)}} + 'const': str(choices.BANK_EROSION.label) + } + } }, 'then': { 'properties': { diff --git a/georiviere/river/management/commands/load_rivers.py b/georiviere/river/management/commands/load_rivers.py index 07b1acca..905c4983 100644 --- a/georiviere/river/management/commands/load_rivers.py +++ b/georiviere/river/management/commands/load_rivers.py @@ -15,8 +15,10 @@ def add_arguments(self, parser): parser.add_argument('file_path', help="File's path to import.") parser.add_argument('--name-attribute', '-n', action='store', dest='name', default='nom', help="Attribute name in file to use as river name") - parser.add_argument('--flush-streams', '-f', action='store_true', dest='flush', default=False, - help="Flush current streams") + parser.add_argument('--flush', '-f', action='store_true', dest='flush', default=False, + help="Flush rivers before import.") + parser.add_argument('--batch-size', '-bs', action='store', dest='batch_size', default=50, + help="Size of batch to use for bulk_create. Default is 50.") parser.add_argument('--default-name-attribute', '-nd', action='store', dest='default_name', default=_('River'), help="Default name to use if attribute name specified is empty") @@ -25,17 +27,17 @@ def handle(self, *args, **options): name_column = options.get('name') default_name = options.get('default_name') flush = options.get('flush') + batch_size = options.get('batch_size') data_source = DataSource(file_path) layer = data_source[0] total_count = len(layer) + self.stdout.write(f"Load rivers: {total_count} features to import") if flush: self.stdout.write("Delete streams.....", ending="") - Stream.objects.all().delete() + Stream.objects.truncate() self.stdout.write(self.style.SUCCESS("done!")) - batch_size = 50 - objs = (Stream(geom=feat.geom.geos, source_location=Point(feat.geom.geos[0]), name=feat.get(name_column) or default_name) for feat in layer) @@ -46,7 +48,10 @@ def handle(self, *args, **options): if not batch: break self.stdout.write(f"{count} / {total_count}", ending="") - Stream.objects.bulk_create(batch, batch_size) - self.stdout.write(self.style.SUCCESS(" ok!")) + try: + Stream.objects.bulk_create(batch, batch_size) + self.stdout.write(self.style.SUCCESS(" ok!")) + except Exception as e: + self.stdout.write(self.style.ERROR(" error!")) self.stdout.write(self.style.SUCCESS(f"Successfully import {total_count} rivers and associated morphologies / status")) diff --git a/georiviere/river/managers.py b/georiviere/river/managers.py new file mode 100644 index 00000000..b3060af7 --- /dev/null +++ b/georiviere/river/managers.py @@ -0,0 +1,6 @@ +from django.db import models +from georiviere.utils.mixins.managers import TruncateManagerMixin + + +class RiverManager(TruncateManagerMixin, models.Manager): + pass diff --git a/georiviere/river/models.py b/georiviere/river/models.py index 4aa38376..af65cd85 100644 --- a/georiviere/river/models.py +++ b/georiviere/river/models.py @@ -25,6 +25,7 @@ from georiviere.observations.models import Station from georiviere.proceeding.models import Proceeding from georiviere.maintenance.models import Intervention +from georiviere.river.managers import RiverManager from georiviere.studies.models import Study from georiviere.watershed.mixins import WatershedPropertiesMixin @@ -90,7 +91,7 @@ class FlowChoices(models.IntegerChoices): portals = models.ManyToManyField('portal.Portal', blank=True, related_name='streams', verbose_name=_("Published portals")) - + objects = RiverManager() capture_map_image_waitfor = '.other_object_enum_loaded' class Meta: @@ -219,23 +220,6 @@ def __str__(self): class Meta: verbose_name = _("Topology") verbose_name_plural = _("Topologies") - # triggers = [ - # pgtrigger.Trigger( - # name="update_topology_geom", - # operation=pgtrigger.Update | pgtrigger.Insert, - # when=pgtrigger.After, - # declare=[('stream_geom', 'geometry')], - # func=""" - # SELECT r.geom FROM river_stream r WHERE NEW.stream_id = r.id INTO stream_geom; - # UPDATE description_morphology - # SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position) - # WHERE topology_id = NEW.id; - # UPDATE description_status - # SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position) - # WHERE topology_id = NEW.id; - # RETURN NEW; - # """ - # )] Study.add_property('stations', Station.within_buffer, _("Stations")) diff --git a/georiviere/utils/__init__.py b/georiviere/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/georiviere/utils/mixins/managers.py b/georiviere/utils/mixins/managers.py new file mode 100644 index 00000000..afb01020 --- /dev/null +++ b/georiviere/utils/mixins/managers.py @@ -0,0 +1,7 @@ +from django.db import connection + + +class TruncateManagerMixin: + def truncate(self): + with connection.cursor() as cursor: + cursor.execute('TRUNCATE TABLE "{0}" CASCADE'.format(self.model._meta.db_table)) From d4b75ca5cdb13af356109f453e7e2fc6a305cb45 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Thu, 11 Apr 2024 09:55:58 +0200 Subject: [PATCH 026/114] lint --- georiviere/river/management/commands/load_rivers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/georiviere/river/management/commands/load_rivers.py b/georiviere/river/management/commands/load_rivers.py index 905c4983..38a8668c 100644 --- a/georiviere/river/management/commands/load_rivers.py +++ b/georiviere/river/management/commands/load_rivers.py @@ -51,7 +51,7 @@ def handle(self, *args, **options): try: Stream.objects.bulk_create(batch, batch_size) self.stdout.write(self.style.SUCCESS(" ok!")) - except Exception as e: + except Exception: self.stdout.write(self.style.ERROR(" error!")) self.stdout.write(self.style.SUCCESS(f"Successfully import {total_count} rivers and associated morphologies / status")) From 2304208a44d95748205a9dd332863956e3c7a451 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Thu, 14 Dec 2023 10:39:20 +0100 Subject: [PATCH 027/114] add load river command --- georiviere/river/management/__init__.py | 0 .../river/management/commands/__init__.py | 0 .../river/management/commands/load_rivers.py | 56 +++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 georiviere/river/management/__init__.py create mode 100644 georiviere/river/management/commands/__init__.py create mode 100644 georiviere/river/management/commands/load_rivers.py diff --git a/georiviere/river/management/__init__.py b/georiviere/river/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/georiviere/river/management/commands/__init__.py b/georiviere/river/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/georiviere/river/management/commands/load_rivers.py b/georiviere/river/management/commands/load_rivers.py new file mode 100644 index 00000000..4bd56e76 --- /dev/null +++ b/georiviere/river/management/commands/load_rivers.py @@ -0,0 +1,56 @@ +from django.contrib.gis.gdal import DataSource +from django.contrib.gis.geos import Point +from django.core.management import BaseCommand +from django.utils.timezone import now +from django.utils.translation import gettext as _ + +from georiviere.description.models import Morphology, Status +from georiviere.river.models import Stream, Topology + + +class Command(BaseCommand): + help = 'Load Rivers' + + def add_arguments(self, parser): + parser.add_argument('file_path', help="File's path to import.") + parser.add_argument('--name-attribute', '-n', action='store', dest='name', default='nom', + help="Name of the name's attribute inside the file") + parser.add_argument('--flush-streams', '-f', action='store_true', dest='flush', default=False, + help="Flush current streams") + parser.add_argument('--default-name-attribute', '-nd', action='store', dest='default_name', default=_('River'), + help="Default name to use if name is empty") + + def handle(self, *args, **options): + file_path = options.get('file_path') + name_column = options.get('name') + default_name = options.get('default_name') + flush = options.get('flush') + data_source = DataSource(file_path) + layer = data_source[0] + total_count = len(layer) + self.stdout.write(f"Load rivers: {total_count} features to import") + count = 0 + if flush: + self.stdout.write(f"Delete streams.....", ending="") + Stream.objects.all().delete() + self.stdout.write(self.style.SUCCESS("done!")) + bulks = [] + for feat in layer: + geom = feat.geom.geos + bulks.append(Stream(geom=geom, + source_location=Point(geom[0]), + name=feat.get(name_column) or default_name)) + count += 1 + if len(bulks) == 100: + objs = Stream.objects.bulk_create(bulks) + # for obj in objs: + # topo = Topology.objects.create(start_position=0, end_position=1, stream=obj) + # Morphology.objects.create(topology=topo, geom=obj.geom) + # topo = Topology.objects.create(start_position=0, end_position=1, stream=obj) + # Status.objects.create(topology=topo, geom=obj.geom) + + bulks = [] + now_2 = now() + self.stdout.write(f"{count} / {total_count}") + + self.stdout.write(f"Morpholgy creation") From 1efd2bb7f4dff0e9a27136e160ec95e945e2986f Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 19 Dec 2023 10:10:32 +0100 Subject: [PATCH 028/114] use pg triggers --- dev-requirements.txt | 70 ++++++++++++------- docs/changelog.rst | 4 ++ georiviere/altimetry.py | 48 ++++++------- .../migrations/0025_auto_20231215_1124.py | 31 ++++++++ .../migrations/0026_auto_20231218_1222.py | 47 +++++++++++++ georiviere/description/models.py | 4 ++ .../main/management/commands/migrate.py | 11 +-- .../migrations/0004_auto_20231215_1124.py | 19 +++++ .../migrations/0005_auto_20231218_1222.py | 23 ++++++ georiviere/proceeding/models.py | 1 + .../river/management/commands/load_rivers.py | 50 ++++++------- .../migrations/0020_auto_20231215_1020.py | 19 +++++ .../migrations/0021_auto_20231215_1051.py | 23 ++++++ .../migrations/0022_auto_20231215_1052.py | 23 ++++++ .../migrations/0023_auto_20231215_1056.py | 23 ++++++ .../migrations/0024_auto_20231218_1226.py | 27 +++++++ .../migrations/0025_auto_20231218_1325.py | 19 +++++ .../migrations/0026_auto_20231218_1356.py | 23 ++++++ .../migrations/0027_auto_20231218_1400.py | 23 ++++++ .../migrations/0028_auto_20231218_1402.py | 23 ++++++ .../migrations/0029_auto_20231218_1403.py | 23 ++++++ .../migrations/0030_auto_20231218_1405.py | 23 ++++++ .../migrations/0031_auto_20231218_1405.py | 23 ++++++ .../migrations/0032_auto_20231218_1410.py | 19 +++++ .../migrations/0033_auto_20231218_1412.py | 23 ++++++ .../migrations/0034_auto_20231218_1416.py | 23 ++++++ .../migrations/0035_auto_20231218_1418.py | 23 ++++++ .../migrations/0036_auto_20231218_1420.py | 23 ++++++ .../migrations/0037_auto_20231218_1421.py | 23 ++++++ .../migrations/0038_auto_20231218_1431.py | 23 ++++++ .../migrations/0039_auto_20231218_1601.py | 23 ++++++ .../migrations/0040_auto_20231218_1608.py | 23 ++++++ georiviere/river/models.py | 63 +++++++++++++---- georiviere/river/signals.py | 17 +---- georiviere/settings/__init__.py | 1 + requirements.in | 1 + requirements.txt | 5 +- 37 files changed, 756 insertions(+), 114 deletions(-) create mode 100644 georiviere/description/migrations/0025_auto_20231215_1124.py create mode 100644 georiviere/description/migrations/0026_auto_20231218_1222.py create mode 100644 georiviere/proceeding/migrations/0004_auto_20231215_1124.py create mode 100644 georiviere/proceeding/migrations/0005_auto_20231218_1222.py create mode 100644 georiviere/river/migrations/0020_auto_20231215_1020.py create mode 100644 georiviere/river/migrations/0021_auto_20231215_1051.py create mode 100644 georiviere/river/migrations/0022_auto_20231215_1052.py create mode 100644 georiviere/river/migrations/0023_auto_20231215_1056.py create mode 100644 georiviere/river/migrations/0024_auto_20231218_1226.py create mode 100644 georiviere/river/migrations/0025_auto_20231218_1325.py create mode 100644 georiviere/river/migrations/0026_auto_20231218_1356.py create mode 100644 georiviere/river/migrations/0027_auto_20231218_1400.py create mode 100644 georiviere/river/migrations/0028_auto_20231218_1402.py create mode 100644 georiviere/river/migrations/0029_auto_20231218_1403.py create mode 100644 georiviere/river/migrations/0030_auto_20231218_1405.py create mode 100644 georiviere/river/migrations/0031_auto_20231218_1405.py create mode 100644 georiviere/river/migrations/0032_auto_20231218_1410.py create mode 100644 georiviere/river/migrations/0033_auto_20231218_1412.py create mode 100644 georiviere/river/migrations/0034_auto_20231218_1416.py create mode 100644 georiviere/river/migrations/0035_auto_20231218_1418.py create mode 100644 georiviere/river/migrations/0036_auto_20231218_1420.py create mode 100644 georiviere/river/migrations/0037_auto_20231218_1421.py create mode 100644 georiviere/river/migrations/0038_auto_20231218_1431.py create mode 100644 georiviere/river/migrations/0039_auto_20231218_1601.py create mode 100644 georiviere/river/migrations/0040_auto_20231218_1608.py diff --git a/dev-requirements.txt b/dev-requirements.txt index 58b97518..eb2033f4 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,17 +1,19 @@ # -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: # # pip-compile dev-requirements.in # -alabaster==0.7.12 +alabaster==0.7.13 # via sphinx asgiref==3.3.1 # via # -c requirements.txt # django -babel==2.9.1 +babel==2.14.0 # via sphinx +build==1.0.3 + # via pip-tools certifi==2020.12.5 # via # -c requirements.txt @@ -54,7 +56,7 @@ faker==9.7.1 # via # -c requirements.txt # factory-boy -flake8==4.0.1 +flake8==6.1.0 # via -r dev-requirements.in freezegun==1.1.0 # via @@ -64,8 +66,12 @@ idna==2.10 # via # -c requirements.txt # requests -imagesize==1.3.0 +imagesize==1.4.1 # via sphinx +importlib-metadata==7.0.0 + # via + # build + # sphinx jinja2==2.11.3 # via # -c requirements.txt @@ -74,28 +80,29 @@ markupsafe==1.1.1 # via # -c requirements.txt # jinja2 -mccabe==0.6.1 +mccabe==0.7.0 # via flake8 packaging==20.9 # via # -c requirements.txt + # build # sphinx -pep517==0.12.0 - # via pip-tools -pip-tools==6.5.1 +pip-tools==6.10.0 # via -r dev-requirements.in -pycodestyle==2.8.0 +pycodestyle==2.11.1 # via flake8 -pyflakes==2.4.0 +pyflakes==3.1.0 # via flake8 -pygments==2.15.0 +pygments==2.17.2 # via sphinx -pygraphviz==1.9 +pygraphviz==1.11 # via -r dev-requirements.in pyparsing==2.4.7 # via # -c requirements.txt # packaging +pyproject-hooks==1.0.0 + # via build python-dateutil==2.8.1 # via # -c requirements.txt @@ -104,7 +111,6 @@ python-dateutil==2.8.1 pytz==2021.1 # via # -c requirements.txt - # babel # django requests==2.26.0 # via @@ -116,43 +122,55 @@ six==1.15.0 # python-dateutil snowballstemmer==2.2.0 # via sphinx -sphinx==4.4.0 +sphinx==5.1.1 # via # -r dev-requirements.in # sphinx-rtd-theme -sphinx-rtd-theme==1.0.0 + # sphinxcontrib-applehelp + # sphinxcontrib-devhelp + # sphinxcontrib-htmlhelp + # sphinxcontrib-jquery + # sphinxcontrib-qthelp + # sphinxcontrib-serializinghtml +sphinx-rtd-theme==2.0.0 # via -r dev-requirements.in -sphinxcontrib-applehelp==1.0.2 +sphinxcontrib-applehelp==1.0.7 # via sphinx -sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-devhelp==1.0.5 # via sphinx -sphinxcontrib-htmlhelp==2.0.0 +sphinxcontrib-htmlhelp==2.0.4 # via sphinx +sphinxcontrib-jquery==4.1 + # via sphinx-rtd-theme sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-qthelp==1.0.6 # via sphinx -sphinxcontrib-serializinghtml==1.1.5 +sphinxcontrib-serializinghtml==1.1.9 # via sphinx sqlparse==0.4.1 # via # -c requirements.txt # django # django-debug-toolbar -tblib==1.7.0 +tblib==3.0.0 # via -r dev-requirements.in text-unidecode==1.3 # via # -c requirements.txt # faker -tomli==2.0.0 - # via pep517 +tomli==2.0.1 + # via + # build + # pyproject-hooks urllib3==1.26.3 # via # -c requirements.txt # requests -wheel==0.38.1 +wheel==0.42.0 # via pip-tools +zipp==3.17.0 + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/docs/changelog.rst b/docs/changelog.rst index e0019e6e..6dce6fcc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,10 @@ CHANGELOG 1.3.0+dev (XXXX-XX-XX) --------------------- +**New features** + +- add load_rivers command + **Bug fix** - Force translation defined in API url /api/portal/ (fix #222) diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index 64091013..e9b7335f 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -1,33 +1,27 @@ +import pgtrigger from geotrek.altimetry.models import AltimetryMixin as BaseAltimetryMixin -from georiviere.functions import ElevationInfos, Length3D - class AltimetryMixin(BaseAltimetryMixin): class Meta: abstract = True - - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - elevation_infos = self._meta.model.objects.filter(pk=self.pk) \ - .annotate(infos=ElevationInfos('geom')).first().infos - draped_geom = elevation_infos.get('draped') - self.geom_3d = draped_geom - self.slope = elevation_infos.get('slope') - self.min_elevation = elevation_infos.get('min_elevation') - self.max_elevation = elevation_infos.get('max_elevation') - self.ascent = elevation_infos.get('positive_gain') - self.descent = elevation_infos.get('negative_gain') - compute_results = self._meta.model.objects.filter(pk=self.pk) \ - .annotate(length_3d=Length3D(draped_geom)).first() - self.length = compute_results.length_3d - super().save(force_insert=False, - update_fields=[ - 'geom_3d', - 'slope', - 'min_elevation', - 'max_elevation', - 'ascent', - 'descent', - 'length' - ]) + triggers = [ + pgtrigger.Trigger( + name="keep_in_sync", + operation=pgtrigger.UpdateOf('geom') | pgtrigger.Insert, + when=pgtrigger.Before, + declare=[('elevation', 'elevation_infos')], + func=""" + SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation; + -- Update path geometry + NEW.geom_3d := elevation.draped; + NEW.length := ST_3DLength(elevation.draped); + NEW.slope := elevation.slope; + NEW.min_elevation := elevation.min_elevation; + NEW.max_elevation := elevation.max_elevation; + NEW.ascent := elevation.positive_gain; + NEW.descent := elevation.negative_gain; + RETURN NEW; + """ + ) + ] diff --git a/georiviere/description/migrations/0025_auto_20231215_1124.py b/georiviere/description/migrations/0025_auto_20231215_1124.py new file mode 100644 index 00000000..957a97dd --- /dev/null +++ b/georiviere/description/migrations/0025_auto_20231215_1124.py @@ -0,0 +1,31 @@ +# Generated by Django 3.1.14 on 2023-12-15 11:24 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('description', '0024_auto_20230323_1621'), + ] + + operations = [ + pgtrigger.migrations.AddTrigger( + model_name='land', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='82ffa7255d1e65c383b05ef0fa226da73a7ad6c8', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_fb8bb', table='description_land', when='BEFORE')), + ), + pgtrigger.migrations.AddTrigger( + model_name='morphology', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='f2e3c5183c820b762d1336a39135727ffc39c5e3', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_472ec', table='description_morphology', when='BEFORE')), + ), + pgtrigger.migrations.AddTrigger( + model_name='status', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='9ebcbb87cf2196d3c04e0e624571d3a49b1ebb69', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_aa87c', table='description_status', when='BEFORE')), + ), + pgtrigger.migrations.AddTrigger( + model_name='usage', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='c18fa3553e1c897d5a5e5f3b6cc5836c86429190', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_71b71', table='description_usage', when='BEFORE')), + ), + ] diff --git a/georiviere/description/migrations/0026_auto_20231218_1222.py b/georiviere/description/migrations/0026_auto_20231218_1222.py new file mode 100644 index 00000000..b16732c9 --- /dev/null +++ b/georiviere/description/migrations/0026_auto_20231218_1222.py @@ -0,0 +1,47 @@ +# Generated by Django 3.1.14 on 2023-12-18 12:22 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('description', '0025_auto_20231215_1124'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='land', + name='keep_in_sync', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='morphology', + name='keep_in_sync', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='status', + name='keep_in_sync', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='usage', + name='keep_in_sync', + ), + pgtrigger.migrations.AddTrigger( + model_name='land', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='d639f6850243942a05ed1f44f6cf79b88b563bad', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_fb8bb', table='description_land', when='BEFORE')), + ), + pgtrigger.migrations.AddTrigger( + model_name='morphology', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='048fe3697643f7682b5336def02a2a9d345947ae', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_472ec', table='description_morphology', when='BEFORE')), + ), + pgtrigger.migrations.AddTrigger( + model_name='status', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='967fa66b6c8eb5e17d5bfc49594c2573241d073b', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_aa87c', table='description_status', when='BEFORE')), + ), + pgtrigger.migrations.AddTrigger( + model_name='usage', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='69cb0cd06f0dccdca1f09d54169f5a673b87fca5', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_71b71', table='description_usage', when='BEFORE')), + ), + ] diff --git a/georiviere/description/models.py b/georiviere/description/models.py index ca1aaaf2..cd686bb5 100644 --- a/georiviere/description/models.py +++ b/georiviere/description/models.py @@ -199,6 +199,7 @@ class Morphology(AddPropertyBufferMixin, TopologyMixin, TimeStampedModelMixin, class Meta: verbose_name = _("Morphology") verbose_name_plural = _("Morphologies") + triggers = AltimetryMixin.Meta.triggers def __str__(self): if self.main_flow: @@ -268,6 +269,7 @@ class Land(AddPropertyBufferMixin, TimeStampedModelMixin, WatershedPropertiesMix class Meta: verbose_name = _("Land") verbose_name_plural = _("Lands") + triggers = AltimetryMixin.Meta.triggers def __str__(self): return f"{self.land_type}" @@ -306,6 +308,7 @@ class Usage(AddPropertyBufferMixin, TimeStampedModelMixin, WatershedPropertiesMi class Meta: verbose_name = _("Usage") verbose_name_plural = _("Usages") + triggers = AltimetryMixin.Meta.triggers def __str__(self): return ', '.join(self.usage_types.values_list("label", flat=True)) @@ -354,6 +357,7 @@ class Status(TopologyMixin, AddPropertyBufferMixin, TimeStampedModelMixin, Water class Meta: verbose_name = _("Status") verbose_name_plural = _("Statuses") + triggers = AltimetryMixin.Meta.triggers def __str__(self): if self.status_types.count(): diff --git a/georiviere/main/management/commands/migrate.py b/georiviere/main/management/commands/migrate.py index 218fd87a..0219458b 100644 --- a/georiviere/main/management/commands/migrate.py +++ b/georiviere/main/management/commands/migrate.py @@ -21,12 +21,15 @@ class Command(BaseCommand): def handle(self, *args, **options): check_srid_has_meter_unit() # set_search_path() - for app in apps.get_app_configs(): - # move_models_to_schemas(app) - load_sql_files(app, 'pre') + # for app in apps.get_app_configs(): + # # move_models_to_schemas(app) + # load_sql_files(app, 'pre') super().handle(*args, **options) for app in apps.get_app_configs(): # move_models_to_schemas(app) - load_sql_files(app, 'post') + try: + load_sql_files(app, 'post') + except Exception: + pass call_command('sync_translation_fields', '--noinput') call_command('update_translation_fields') diff --git a/georiviere/proceeding/migrations/0004_auto_20231215_1124.py b/georiviere/proceeding/migrations/0004_auto_20231215_1124.py new file mode 100644 index 00000000..01a23413 --- /dev/null +++ b/georiviere/proceeding/migrations/0004_auto_20231215_1124.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.14 on 2023-12-15 11:24 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('proceeding', '0003_auto_20210804_1014'), + ] + + operations = [ + pgtrigger.migrations.AddTrigger( + model_name='proceeding', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='4580ed3c25a4cd5a156ff3a0c1efd6c8301d8e25', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_6ba14', table='proceeding_proceeding', when='BEFORE')), + ), + ] diff --git a/georiviere/proceeding/migrations/0005_auto_20231218_1222.py b/georiviere/proceeding/migrations/0005_auto_20231218_1222.py new file mode 100644 index 00000000..5a7e4ac6 --- /dev/null +++ b/georiviere/proceeding/migrations/0005_auto_20231218_1222.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 12:22 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('proceeding', '0004_auto_20231215_1124'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='proceeding', + name='keep_in_sync', + ), + pgtrigger.migrations.AddTrigger( + model_name='proceeding', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='8be1e3d34cf0ef421d9f1750b10e7ef296d6a20d', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_6ba14', table='proceeding_proceeding', when='BEFORE')), + ), + ] diff --git a/georiviere/proceeding/models.py b/georiviere/proceeding/models.py index ab6627a3..c88520b7 100644 --- a/georiviere/proceeding/models.py +++ b/georiviere/proceeding/models.py @@ -49,6 +49,7 @@ def get_create_label(cls): class Meta: verbose_name = _("Proceeding") verbose_name_plural = _("Proceedings") + triggers = AltimetryMixin.Meta.triggers class Event(models.Model): diff --git a/georiviere/river/management/commands/load_rivers.py b/georiviere/river/management/commands/load_rivers.py index 4bd56e76..eca252d8 100644 --- a/georiviere/river/management/commands/load_rivers.py +++ b/georiviere/river/management/commands/load_rivers.py @@ -1,11 +1,11 @@ +from itertools import islice + from django.contrib.gis.gdal import DataSource from django.contrib.gis.geos import Point from django.core.management import BaseCommand -from django.utils.timezone import now from django.utils.translation import gettext as _ -from georiviere.description.models import Morphology, Status -from georiviere.river.models import Stream, Topology +from georiviere.river.models import Stream class Command(BaseCommand): @@ -14,11 +14,11 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument('file_path', help="File's path to import.") parser.add_argument('--name-attribute', '-n', action='store', dest='name', default='nom', - help="Name of the name's attribute inside the file") + help="Attribute name in file to use as river name") parser.add_argument('--flush-streams', '-f', action='store_true', dest='flush', default=False, help="Flush current streams") parser.add_argument('--default-name-attribute', '-nd', action='store', dest='default_name', default=_('River'), - help="Default name to use if name is empty") + help="Default name to use if attribute name specified is empty") def handle(self, *args, **options): file_path = options.get('file_path') @@ -29,28 +29,24 @@ def handle(self, *args, **options): layer = data_source[0] total_count = len(layer) self.stdout.write(f"Load rivers: {total_count} features to import") - count = 0 if flush: - self.stdout.write(f"Delete streams.....", ending="") + self.stdout.write("Delete streams.....", ending="") Stream.objects.all().delete() self.stdout.write(self.style.SUCCESS("done!")) - bulks = [] - for feat in layer: - geom = feat.geom.geos - bulks.append(Stream(geom=geom, - source_location=Point(geom[0]), - name=feat.get(name_column) or default_name)) - count += 1 - if len(bulks) == 100: - objs = Stream.objects.bulk_create(bulks) - # for obj in objs: - # topo = Topology.objects.create(start_position=0, end_position=1, stream=obj) - # Morphology.objects.create(topology=topo, geom=obj.geom) - # topo = Topology.objects.create(start_position=0, end_position=1, stream=obj) - # Status.objects.create(topology=topo, geom=obj.geom) - - bulks = [] - now_2 = now() - self.stdout.write(f"{count} / {total_count}") - - self.stdout.write(f"Morpholgy creation") + + batch_size = 100 + + objs = (Stream(geom=feat.geom.geos, + source_location=Point(feat.geom.geos[0]), + name=feat.get(name_column) or default_name) for feat in layer) + count = 0 + while True: + batch = list(islice(objs, batch_size)) + count += len(batch) + if not batch: + break + self.stdout.write(f"{count} / {total_count}", ending="") + Stream.objects.bulk_create(batch, batch_size) + self.stdout.write(self.style.SUCCESS(" ok!")) + + self.stdout.write(self.style.SUCCESS(f"Successfully import {total_count} rivers and associated morphologies / status")) diff --git a/georiviere/river/migrations/0020_auto_20231215_1020.py b/georiviere/river/migrations/0020_auto_20231215_1020.py new file mode 100644 index 00000000..e5421117 --- /dev/null +++ b/georiviere/river/migrations/0020_auto_20231215_1020.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.14 on 2023-12-15 10:20 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0019_stream_description'), + ] + + operations = [ + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation JSONB;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='6c62fd64699ff351273ae8753db0b43dd5defaf7', operation='UPDATE OR INSERT', pgid='pgtrigger_keep_in_sync_7a86b', table='river_stream', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0021_auto_20231215_1051.py b/georiviere/river/migrations/0021_auto_20231215_1051.py new file mode 100644 index 00000000..bd951d1f --- /dev/null +++ b/georiviere/river/migrations/0021_auto_20231215_1051.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-15 10:51 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0020_auto_20231215_1020'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='stream', + name='keep_in_sync', + ), + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := element.draped;\n NEW.length := ST_3DLength(element.draped);\n NEW.slope := element.slope;\n NEW.min_elevation := element.min_elevation;\n NEW.max_elevation := element.max_elevation;\n NEW.ascent := element.positive_gain;\n NEW.descent := element.negative_gain;\n\n RETURN NEW;\n ', hash='fdeef3b3432dda345b1ec896b91e84eb120a9986', operation='UPDATE OR INSERT', pgid='pgtrigger_keep_in_sync_7a86b', table='river_stream', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0022_auto_20231215_1052.py b/georiviere/river/migrations/0022_auto_20231215_1052.py new file mode 100644 index 00000000..9a4e0b26 --- /dev/null +++ b/georiviere/river/migrations/0022_auto_20231215_1052.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-15 10:52 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0021_auto_20231215_1051'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='stream', + name='keep_in_sync', + ), + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='24de00d1e6f9be9e088db3410e54b0de328653a3', operation='UPDATE OR INSERT', pgid='pgtrigger_keep_in_sync_7a86b', table='river_stream', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0023_auto_20231215_1056.py b/georiviere/river/migrations/0023_auto_20231215_1056.py new file mode 100644 index 00000000..6e669b8a --- /dev/null +++ b/georiviere/river/migrations/0023_auto_20231215_1056.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-15 10:56 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0022_auto_20231215_1052'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='stream', + name='keep_in_sync', + ), + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='168b84532fa3b4230f53fbd1cfdb1169c5d72a0a', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_7a86b', table='river_stream', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0024_auto_20231218_1226.py b/georiviere/river/migrations/0024_auto_20231218_1226.py new file mode 100644 index 00000000..4d4f21a1 --- /dev/null +++ b/georiviere/river/migrations/0024_auto_20231218_1226.py @@ -0,0 +1,27 @@ +# Generated by Django 3.1.14 on 2023-12-18 12:26 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0023_auto_20231215_1056'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='stream', + name='keep_in_sync', + ), + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='e66f827b3e0e2d3e5f822a259752f2d8a48a7c26', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_7a86b', table='river_stream', when='BEFORE')), + ), + pgtrigger.migrations.AddTrigger( + model_name='topology', + trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; geom geometry;', func='\n IF (NEW.start_point != OLD.start_point OR NEW.end_point != OLD.end_point) THEN \n SELECT geom FROM river_stream r WHERE NEW.id = r.id INTO geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(geom, NEW.START_POINT, NEW.END_POINT)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(geom, NEW.START_POINT, NEW.END_POINT)\n WHERE topology_id = NEW.id; \n END IF;\n RETURN NEW;\n ', hash='db1df9343d9079232a59df61b34bfcd7bc2df48a', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0025_auto_20231218_1325.py b/georiviere/river/migrations/0025_auto_20231218_1325.py new file mode 100644 index 00000000..74c03ca5 --- /dev/null +++ b/georiviere/river/migrations/0025_auto_20231218_1325.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.14 on 2023-12-18 13:25 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0024_auto_20231218_1226'), + ] + + operations = [ + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func='\n INSERT INTO river_topology (stream_id, start_position, end_position)\n VALUES (NEW.id, 0, 1) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom) VALUES (topology_morphology, NEW.geom);\n INSERT INTO river_topology (stream_id, start_position, end_position)\n VALUES (NEW.id, 0, 1) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom) VALUES (topology_status, NEW.geom);\n RETURN NEW;\n ', hash='c0b7e1615c64b92e36e49803f3001ebde7dd517e', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), + ), + ] diff --git a/georiviere/river/migrations/0026_auto_20231218_1356.py b/georiviere/river/migrations/0026_auto_20231218_1356.py new file mode 100644 index 00000000..45b90ad4 --- /dev/null +++ b/georiviere/river/migrations/0026_auto_20231218_1356.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 13:56 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0025_auto_20231218_1325'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='topology', + name='update_topology_geom', + ), + pgtrigger.migrations.AddTrigger( + model_name='topology', + trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; geom geometry;', func='\n IF (NEW.start_position != OLD.start_position OR NEW.end_position != OLD.end_position) THEN \n SELECT geom FROM river_stream r WHERE NEW.id = r.id INTO geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n END IF;\n RETURN NEW;\n ', hash='10d2e4894631bee97f91deeb607004d3a5295448', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0027_auto_20231218_1400.py b/georiviere/river/migrations/0027_auto_20231218_1400.py new file mode 100644 index 00000000..f32c8f08 --- /dev/null +++ b/georiviere/river/migrations/0027_auto_20231218_1400.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:00 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0026_auto_20231218_1356'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='stream', + name='create_topologies', + ), + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func='\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom) VALUES (topology_morphology, NEW.geom);\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom) VALUES (topology_status, NEW.geom);\n RETURN NEW;\n ', hash='4ebe61234dd666bf424de96761a8af7e0a72560f', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), + ), + ] diff --git a/georiviere/river/migrations/0028_auto_20231218_1402.py b/georiviere/river/migrations/0028_auto_20231218_1402.py new file mode 100644 index 00000000..227b45d4 --- /dev/null +++ b/georiviere/river/migrations/0028_auto_20231218_1402.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:02 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0027_auto_20231218_1400'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='stream', + name='create_topologies', + ), + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func='\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom, date_insert, date_update) VALUES (topology_morphology, NEW.geom, NOW(), NOW());\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom, date_insert, date_update) VALUES (topology_status, NEW.geom, NOW(), NOW());\n RETURN NEW;\n ', hash='606f9274946baa820c7792e04a1f6559948a2e8e', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), + ), + ] diff --git a/georiviere/river/migrations/0029_auto_20231218_1403.py b/georiviere/river/migrations/0029_auto_20231218_1403.py new file mode 100644 index 00000000..8416c313 --- /dev/null +++ b/georiviere/river/migrations/0029_auto_20231218_1403.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:03 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0028_auto_20231218_1402'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='stream', + name='create_topologies', + ), + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func="\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update)\n VALUES (topology_morphology, NEW.geom, '', NOW(), NOW());\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom, date_insert, date_update) VALUES (topology_status, NEW.geom, NOW(), NOW());\n RETURN NEW;\n ", hash='cfb353801531d9e2c134a09c85d8a69807704a71', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), + ), + ] diff --git a/georiviere/river/migrations/0030_auto_20231218_1405.py b/georiviere/river/migrations/0030_auto_20231218_1405.py new file mode 100644 index 00000000..1ef79185 --- /dev/null +++ b/georiviere/river/migrations/0030_auto_20231218_1405.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:05 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0029_auto_20231218_1403'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='stream', + name='create_topologies', + ), + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func="\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update)\n VALUES (topology_morphology, NEW.geom, '', NOW(), NOW());\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom, regulation, referencial, descrption, date_insert, date_update)\n VALUES (topology_status, NEW.geom, FALSE, FALSE, '', NOW(), NOW());\n RETURN NEW;\n ", hash='f5c7079e6d4e21c67d642ea14d61d4e2f39787b0', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), + ), + ] diff --git a/georiviere/river/migrations/0031_auto_20231218_1405.py b/georiviere/river/migrations/0031_auto_20231218_1405.py new file mode 100644 index 00000000..2dbd1031 --- /dev/null +++ b/georiviere/river/migrations/0031_auto_20231218_1405.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:05 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0030_auto_20231218_1405'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='stream', + name='create_topologies', + ), + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func="\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update)\n VALUES (topology_morphology, NEW.geom, '', NOW(), NOW());\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom, regulation, referencial, description, date_insert, date_update)\n VALUES (topology_status, NEW.geom, FALSE, FALSE, '', NOW(), NOW());\n RETURN NEW;\n ", hash='e846f76834647fd2338681a9530765b6d1883cc9', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), + ), + ] diff --git a/georiviere/river/migrations/0032_auto_20231218_1410.py b/georiviere/river/migrations/0032_auto_20231218_1410.py new file mode 100644 index 00000000..1e19b353 --- /dev/null +++ b/georiviere/river/migrations/0032_auto_20231218_1410.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:10 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0031_auto_20231218_1405'), + ] + + operations = [ + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='update_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(func='\n UPDATE river_topology SET start_position=start_position WHERE stream_id=NEW.id;\n RETURN NEW;\n ', hash='0b6322b6b455eed7b6a2a78c79b07e08e9a86611', operation='UPDATE OF "geom"', pgid='pgtrigger_update_topologies_6ab45', table='river_stream', when='AFTER')), + ), + ] diff --git a/georiviere/river/migrations/0033_auto_20231218_1412.py b/georiviere/river/migrations/0033_auto_20231218_1412.py new file mode 100644 index 00000000..1b5d52e2 --- /dev/null +++ b/georiviere/river/migrations/0033_auto_20231218_1412.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:12 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0032_auto_20231218_1410'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='topology', + name='update_topology_geom', + ), + pgtrigger.migrations.AddTrigger( + model_name='topology', + trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; geom geometry;', func='\n SELECT geom FROM river_stream r WHERE NEW.id = r.id INTO geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='2b83b20dab6aa7e5456c1e10694f527dc98eb281', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0034_auto_20231218_1416.py b/georiviere/river/migrations/0034_auto_20231218_1416.py new file mode 100644 index 00000000..653b8de5 --- /dev/null +++ b/georiviere/river/migrations/0034_auto_20231218_1416.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:16 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0033_auto_20231218_1412'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='topology', + name='update_topology_geom', + ), + pgtrigger.migrations.AddTrigger( + model_name='topology', + trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.id = r.id INTO geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='1e3e1dbd526ffd01e8155aec62fad614e188471d', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0035_auto_20231218_1418.py b/georiviere/river/migrations/0035_auto_20231218_1418.py new file mode 100644 index 00000000..e92cd864 --- /dev/null +++ b/georiviere/river/migrations/0035_auto_20231218_1418.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:18 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0034_auto_20231218_1416'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='topology', + name='update_topology_geom', + ), + pgtrigger.migrations.AddTrigger( + model_name='topology', + trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.id = r.id INTO geom;\n UPDATE description_morphology m\n SET m.geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status s\n SET s.geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='a3d43086e66e04cde5bf979df1f4565a3bb9e791', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0036_auto_20231218_1420.py b/georiviere/river/migrations/0036_auto_20231218_1420.py new file mode 100644 index 00000000..4421a74f --- /dev/null +++ b/georiviere/river/migrations/0036_auto_20231218_1420.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:20 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0035_auto_20231218_1418'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='topology', + name='update_topology_geom', + ), + pgtrigger.migrations.AddTrigger( + model_name='topology', + trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; stream_geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.id = r.id INTO stream_geom;\n UPDATE description_morphology m\n SET m.geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status s\n SET s.geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='1671ae29035c32a992993d3518965e8e9c0a605f', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0037_auto_20231218_1421.py b/georiviere/river/migrations/0037_auto_20231218_1421.py new file mode 100644 index 00000000..1739897b --- /dev/null +++ b/georiviere/river/migrations/0037_auto_20231218_1421.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:21 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0036_auto_20231218_1420'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='topology', + name='update_topology_geom', + ), + pgtrigger.migrations.AddTrigger( + model_name='topology', + trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; stream_geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.id = r.id INTO stream_geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='444948eb36585ca5ea4dfdad5e9be32137e67844', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), + ), + ] diff --git a/georiviere/river/migrations/0038_auto_20231218_1431.py b/georiviere/river/migrations/0038_auto_20231218_1431.py new file mode 100644 index 00000000..26019979 --- /dev/null +++ b/georiviere/river/migrations/0038_auto_20231218_1431.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 14:31 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0037_auto_20231218_1421'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='topology', + name='update_topology_geom', + ), + pgtrigger.migrations.AddTrigger( + model_name='topology', + trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE stream_geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.id = r.id INTO stream_geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='f0a3386a26d41f8268db5bbdb554ba4cc3ae1612', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='AFTER')), + ), + ] diff --git a/georiviere/river/migrations/0039_auto_20231218_1601.py b/georiviere/river/migrations/0039_auto_20231218_1601.py new file mode 100644 index 00000000..06a328cb --- /dev/null +++ b/georiviere/river/migrations/0039_auto_20231218_1601.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 16:01 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0038_auto_20231218_1431'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='stream', + name='update_topologies', + ), + pgtrigger.migrations.AddTrigger( + model_name='stream', + trigger=pgtrigger.compiler.Trigger(name='update_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(func='\n UPDATE river_topology SET id=id WHERE stream_id=NEW.id;\n RETURN NEW;\n ', hash='704192b93012d12fa95b06b03618981a49609a4e', operation='UPDATE OF "geom"', pgid='pgtrigger_update_topologies_6ab45', table='river_stream', when='AFTER')), + ), + ] diff --git a/georiviere/river/migrations/0040_auto_20231218_1608.py b/georiviere/river/migrations/0040_auto_20231218_1608.py new file mode 100644 index 00000000..ad4c8f4d --- /dev/null +++ b/georiviere/river/migrations/0040_auto_20231218_1608.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-18 16:08 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0039_auto_20231218_1601'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='topology', + name='update_topology_geom', + ), + pgtrigger.migrations.AddTrigger( + model_name='topology', + trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE stream_geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.stream_id = r.id INTO stream_geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='4d63b6e0ef200235882742af37d834d00b5a271a', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='AFTER')), + ), + ] diff --git a/georiviere/river/models.py b/georiviere/river/models.py index 41f37cbd..9283a84c 100644 --- a/georiviere/river/models.py +++ b/georiviere/river/models.py @@ -1,3 +1,4 @@ +import pgtrigger from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.contrib.gis.db import models @@ -19,7 +20,7 @@ from georiviere.main.models import AddPropertyBufferMixin from georiviere.altimetry import AltimetryMixin from georiviere.finances_administration.models import AdministrativeFile -from georiviere.functions import ClosestPoint, LineSubString +from georiviere.functions import ClosestPoint from georiviere.knowledge.models import Knowledge, FollowUp from georiviere.main.models import DistanceToSource from georiviere.observations.models import Station @@ -96,6 +97,37 @@ class FlowChoices(models.IntegerChoices): class Meta: verbose_name = _("Stream") verbose_name_plural = _("Streams") + triggers = AltimetryMixin.Meta.triggers + [ + pgtrigger.Trigger( + name="create_topologies", + operation=pgtrigger.Insert, + declare=[ + ('topology_morphology', 'integer'), + ('topology_status', 'integer') + ], + when=pgtrigger.After, + func=""" + INSERT INTO river_topology (stream_id, start_position, end_position, qualified) + VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology; + INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update) + VALUES (topology_morphology, NEW.geom, '', NOW(), NOW()); + INSERT INTO river_topology (stream_id, start_position, end_position, qualified) + VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status; + INSERT INTO description_status (topology_id, geom, regulation, referencial, description, date_insert, date_update) + VALUES (topology_status, NEW.geom, FALSE, FALSE, '', NOW(), NOW()); + RETURN NEW; + """ + ), + pgtrigger.Trigger( + name="update_topologies", + operation=pgtrigger.UpdateOf('geom'), + when=pgtrigger.After, + func=""" + UPDATE river_topology SET id=id WHERE stream_id=NEW.id; + RETURN NEW; + """ + ) + ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -216,21 +248,26 @@ def __str__(self): else: return _("Topology {}").format(self.pk) - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - geom_topology = self._meta.model.objects.filter(pk=self.pk) \ - .annotate(substring=LineSubString(self.stream.geom, self.start_position, self.end_position)).first().substring - if hasattr(self, 'status'): - self.status.geom = geom_topology - self.status.save() - elif hasattr(self, 'morphology'): - self.morphology.geom = geom_topology - self.morphology.save() - super().save(force_insert=False) - class Meta: verbose_name = _("Topology") verbose_name_plural = _("Topologies") + triggers = [ + pgtrigger.Trigger( + name="update_topology_geom", + operation=pgtrigger.Update | pgtrigger.Insert, + when=pgtrigger.After, + declare=[('stream_geom', 'geometry')], + func=""" + SELECT r.geom FROM river_stream r WHERE NEW.stream_id = r.id INTO stream_geom; + UPDATE description_morphology + SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position) + WHERE topology_id = NEW.id; + UPDATE description_status + SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position) + WHERE topology_id = NEW.id; + RETURN NEW; + """ + )] Study.add_property('stations', Station.within_buffer, _("Stations")) diff --git a/georiviere/river/signals.py b/georiviere/river/signals.py index 77e46a0e..3be48afa 100644 --- a/georiviere/river/signals.py +++ b/georiviere/river/signals.py @@ -3,33 +3,18 @@ from django.contrib.contenttypes.models import ContentType from django.contrib.gis.db.models.functions import Distance, Length, LineLocatePoint from django.db.models.signals import post_save -from django.db.models.fields.reverse_related import OneToOneRel from django.db.models import F, FloatField, Case, When from django.dispatch import receiver from georiviere.functions import ClosestPoint, LineSubString from georiviere.main.models import DistanceToSource -from georiviere.river.models import Stream, Topology, TopologyMixin +from georiviere.river.models import Stream, TopologyMixin from mapentity.models import MapEntityMixin -@receiver(post_save, sender=Stream) -def save_stream(sender, instance, **kwargs): - if kwargs['created']: - class_topos = [field.related_model for field in instance.topologies.model._meta.get_fields() - if isinstance(field, OneToOneRel)] - for class_topo in class_topos: - topology = Topology.objects.create(start_position=0, end_position=1, stream=instance) - class_topo.objects.create(topology=topology, geom=instance.geom) - else: - for topology in instance.topologies.all(): - topology.save() - - @receiver(post_save, sender=Stream) def save_stream_generate_distance_to_source(sender, instance, **kwargs): - for model in apps.get_models(): if issubclass(model, MapEntityMixin) and not issubclass(model, TopologyMixin) and model != Stream and 'geom' in [field.name for field in model._meta.get_fields()]: distances_to_sources = [] diff --git a/georiviere/settings/__init__.py b/georiviere/settings/__init__.py index 4daf8d0f..00e4741c 100644 --- a/georiviere/settings/__init__.py +++ b/georiviere/settings/__init__.py @@ -135,6 +135,7 @@ def construct_relative_path_mock(current_template_name, relative_name): 'embed_video', 'djgeojson', 'django_filters', + 'pgtrigger', 'compressor', 'mapentity', # mapentity should be placed after app declaring paperclip models 'leaflet', diff --git a/requirements.in b/requirements.in index a4d9353d..c09d1eb1 100644 --- a/requirements.in +++ b/requirements.in @@ -14,6 +14,7 @@ sentry-sdk django-admin-ordering djangorestframework-camel-case jsonschema +django-pgtrigger # Use until latest version of geotrek (> 2.99.0) drf-spectacular diff --git a/requirements.txt b/requirements.txt index 60c38c92..9ebbca33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # pip-compile @@ -91,6 +91,7 @@ django==3.1.14 # django-leaflet # django-modeltranslation # django-mptt + # django-pgtrigger # django-querysetsequence # django-weasyprint # djangorestframework @@ -144,6 +145,8 @@ django-modeltranslation==0.17.3 # via mapentity django-mptt==0.11.0 # via geotrek +django-pgtrigger==4.11.0 + # via -r requirements.in django-querysetsequence==0.14 # via django-autocomplete-light django-tinymce==2.9.0 From 4d0eae74d80b3f46cd141ff8f9ee10d4d482f997 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 19 Dec 2023 10:32:17 +0100 Subject: [PATCH 029/114] use pg triggers --- .../migrations/0041_auto_20231219_0932.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 georiviere/river/migrations/0041_auto_20231219_0932.py diff --git a/georiviere/river/migrations/0041_auto_20231219_0932.py b/georiviere/river/migrations/0041_auto_20231219_0932.py new file mode 100644 index 00000000..12429cd4 --- /dev/null +++ b/georiviere/river/migrations/0041_auto_20231219_0932.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2023-12-19 09:32 + +from django.db import migrations +import pgtrigger.compiler +import pgtrigger.migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('river', '0040_auto_20231218_1608'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='topology', + name='update_topology_geom', + ), + pgtrigger.migrations.AddTrigger( + model_name='topology', + trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE stream_geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.stream_id = r.id INTO stream_geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id;\n UPDATE description_status\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id;\n RETURN NEW;\n ', hash='0d8dce1088e59bd290e9ffe0d6bfb8a2493a5c1c', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='AFTER')), + ), + ] From 4e91e6f7ff5d6c75577628bb9e0b0753869a0d4b Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 19 Dec 2023 10:40:38 +0100 Subject: [PATCH 030/114] use pg triggers --- georiviere/river/templates/river/sql/pre_types.sql | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 georiviere/river/templates/river/sql/pre_types.sql diff --git a/georiviere/river/templates/river/sql/pre_types.sql b/georiviere/river/templates/river/sql/pre_types.sql new file mode 100644 index 00000000..cc8532dc --- /dev/null +++ b/georiviere/river/templates/river/sql/pre_types.sql @@ -0,0 +1,9 @@ +DROP TYPE IF EXISTS elevation_infos; +CREATE TYPE elevation_infos AS ( + draped geometry, + slope float, + min_elevation integer, + max_elevation integer, + positive_gain integer, + negative_gain integer +); \ No newline at end of file From 28e4cb1a8d5689cdf1d9fe903452844448d09dba Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 19 Dec 2023 10:45:08 +0100 Subject: [PATCH 031/114] use pg triggers --- georiviere/main/management/commands/migrate.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/georiviere/main/management/commands/migrate.py b/georiviere/main/management/commands/migrate.py index 0219458b..8d08a70f 100644 --- a/georiviere/main/management/commands/migrate.py +++ b/georiviere/main/management/commands/migrate.py @@ -20,16 +20,10 @@ def check_srid_has_meter_unit(): class Command(BaseCommand): def handle(self, *args, **options): check_srid_has_meter_unit() - # set_search_path() - # for app in apps.get_app_configs(): - # # move_models_to_schemas(app) - # load_sql_files(app, 'pre') + for app in apps.get_app_configs(): + load_sql_files(app, 'pre') super().handle(*args, **options) for app in apps.get_app_configs(): - # move_models_to_schemas(app) - try: - load_sql_files(app, 'post') - except Exception: - pass + load_sql_files(app, 'post') call_command('sync_translation_fields', '--noinput') call_command('update_translation_fields') From 68361ca57801cbdb02bad2a8986b796d000680e0 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 19 Dec 2023 16:28:49 +0100 Subject: [PATCH 032/114] use pg triggers --- .../templates/river => main/templates/main}/sql/pre_types.sql | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename georiviere/{river/templates/river => main/templates/main}/sql/pre_types.sql (100%) diff --git a/georiviere/river/templates/river/sql/pre_types.sql b/georiviere/main/templates/main/sql/pre_types.sql similarity index 100% rename from georiviere/river/templates/river/sql/pre_types.sql rename to georiviere/main/templates/main/sql/pre_types.sql From 44e04fbabd1c8a239b791126cb674e85edee5111 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 19 Dec 2023 16:44:48 +0100 Subject: [PATCH 033/114] use pg triggers --- .../{templates/main/sql/pre_types.sql => sql/pre_10_types.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename georiviere/main/{templates/main/sql/pre_types.sql => sql/pre_10_types.sql} (100%) diff --git a/georiviere/main/templates/main/sql/pre_types.sql b/georiviere/main/sql/pre_10_types.sql similarity index 100% rename from georiviere/main/templates/main/sql/pre_types.sql rename to georiviere/main/sql/pre_10_types.sql From 9a9c382400c37eb9af8e4b75092318a221bba035 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 19 Dec 2023 16:55:38 +0100 Subject: [PATCH 034/114] use pg triggers --- georiviere/main/management/commands/migrate.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/georiviere/main/management/commands/migrate.py b/georiviere/main/management/commands/migrate.py index 8d08a70f..8b607a0e 100644 --- a/georiviere/main/management/commands/migrate.py +++ b/georiviere/main/management/commands/migrate.py @@ -21,9 +21,12 @@ class Command(BaseCommand): def handle(self, *args, **options): check_srid_has_meter_unit() for app in apps.get_app_configs(): - load_sql_files(app, 'pre') + load_sql_files(app, 'pre') super().handle(*args, **options) for app in apps.get_app_configs(): - load_sql_files(app, 'post') + try: + load_sql_files(app, 'post') + except Exception: + pass call_command('sync_translation_fields', '--noinput') call_command('update_translation_fields') From 61420aed661f6a902ab987ce3176d56eb4f1eb8a Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 19 Dec 2023 17:37:57 +0100 Subject: [PATCH 035/114] use pg triggers --- georiviere/main/management/commands/migrate.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/georiviere/main/management/commands/migrate.py b/georiviere/main/management/commands/migrate.py index 8b607a0e..70fef977 100644 --- a/georiviere/main/management/commands/migrate.py +++ b/georiviere/main/management/commands/migrate.py @@ -22,11 +22,8 @@ def handle(self, *args, **options): check_srid_has_meter_unit() for app in apps.get_app_configs(): load_sql_files(app, 'pre') + load_sql_files(app, 'post') super().handle(*args, **options) - for app in apps.get_app_configs(): - try: - load_sql_files(app, 'post') - except Exception: - pass + call_command('sync_translation_fields', '--noinput') call_command('update_translation_fields') From 935ebbf9294e35420cf4ce99ef8e021ceb84b7df Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 19 Dec 2023 20:54:05 +0100 Subject: [PATCH 036/114] use pg triggers --- georiviere/main/sql/pre_10_types.sql | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 georiviere/main/sql/pre_10_types.sql diff --git a/georiviere/main/sql/pre_10_types.sql b/georiviere/main/sql/pre_10_types.sql deleted file mode 100644 index cc8532dc..00000000 --- a/georiviere/main/sql/pre_10_types.sql +++ /dev/null @@ -1,9 +0,0 @@ -DROP TYPE IF EXISTS elevation_infos; -CREATE TYPE elevation_infos AS ( - draped geometry, - slope float, - min_elevation integer, - max_elevation integer, - positive_gain integer, - negative_gain integer -); \ No newline at end of file From 89ea540e83f8dc7555a5b9bd3a0cdcb7df16f1ce Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 12:47:53 +0100 Subject: [PATCH 037/114] use internal triggers --- docker-compose.yml | 1 - georiviere/altimetry.py | 21 ---- .../migrations/0025_auto_20231215_1124.py | 31 ------ .../migrations/0026_auto_20231218_1222.py | 47 --------- georiviere/description/models.py | 6 +- .../description/sql/post_10_triggers.sql | 7 ++ .../main/management/commands/migrate.py | 7 +- georiviere/main/sql/post_10_functions.sql | 16 +++ georiviere/main/sql/pre_10_cleanup.sql | 1 + .../migrations/0004_auto_20231215_1124.py | 19 ---- .../migrations/0005_auto_20231218_1222.py | 23 ----- georiviere/proceeding/models.py | 1 - .../river/management/commands/load_rivers.py | 2 +- .../migrations/0020_auto_20231215_1020.py | 19 ---- .../migrations/0021_auto_20231215_1051.py | 23 ----- .../migrations/0022_auto_20231215_1052.py | 23 ----- .../migrations/0023_auto_20231215_1056.py | 23 ----- .../migrations/0024_auto_20231218_1226.py | 27 ------ .../migrations/0025_auto_20231218_1325.py | 19 ---- .../migrations/0026_auto_20231218_1356.py | 23 ----- .../migrations/0027_auto_20231218_1400.py | 23 ----- .../migrations/0028_auto_20231218_1402.py | 23 ----- .../migrations/0029_auto_20231218_1403.py | 23 ----- .../migrations/0030_auto_20231218_1405.py | 23 ----- .../migrations/0031_auto_20231218_1405.py | 23 ----- .../migrations/0032_auto_20231218_1410.py | 19 ---- .../migrations/0033_auto_20231218_1412.py | 23 ----- .../migrations/0034_auto_20231218_1416.py | 23 ----- .../migrations/0035_auto_20231218_1418.py | 23 ----- .../migrations/0036_auto_20231218_1420.py | 23 ----- .../migrations/0037_auto_20231218_1421.py | 23 ----- .../migrations/0038_auto_20231218_1431.py | 23 ----- .../migrations/0039_auto_20231218_1601.py | 23 ----- .../migrations/0040_auto_20231218_1608.py | 23 ----- .../migrations/0041_auto_20231219_0932.py | 23 ----- georiviere/river/models.py | 97 +++++++++---------- georiviere/river/sql/post_10_triggers.sql | 43 ++++++++ georiviere/river/sql/pre_10_cleanup.sql | 3 + georiviere/settings/__init__.py | 2 +- requirements.in | 1 - requirements.txt | 3 - 41 files changed, 127 insertions(+), 702 deletions(-) delete mode 100644 georiviere/description/migrations/0025_auto_20231215_1124.py delete mode 100644 georiviere/description/migrations/0026_auto_20231218_1222.py create mode 100644 georiviere/description/sql/post_10_triggers.sql create mode 100644 georiviere/main/sql/post_10_functions.sql create mode 100644 georiviere/main/sql/pre_10_cleanup.sql delete mode 100644 georiviere/proceeding/migrations/0004_auto_20231215_1124.py delete mode 100644 georiviere/proceeding/migrations/0005_auto_20231218_1222.py delete mode 100644 georiviere/river/migrations/0020_auto_20231215_1020.py delete mode 100644 georiviere/river/migrations/0021_auto_20231215_1051.py delete mode 100644 georiviere/river/migrations/0022_auto_20231215_1052.py delete mode 100644 georiviere/river/migrations/0023_auto_20231215_1056.py delete mode 100644 georiviere/river/migrations/0024_auto_20231218_1226.py delete mode 100644 georiviere/river/migrations/0025_auto_20231218_1325.py delete mode 100644 georiviere/river/migrations/0026_auto_20231218_1356.py delete mode 100644 georiviere/river/migrations/0027_auto_20231218_1400.py delete mode 100644 georiviere/river/migrations/0028_auto_20231218_1402.py delete mode 100644 georiviere/river/migrations/0029_auto_20231218_1403.py delete mode 100644 georiviere/river/migrations/0030_auto_20231218_1405.py delete mode 100644 georiviere/river/migrations/0031_auto_20231218_1405.py delete mode 100644 georiviere/river/migrations/0032_auto_20231218_1410.py delete mode 100644 georiviere/river/migrations/0033_auto_20231218_1412.py delete mode 100644 georiviere/river/migrations/0034_auto_20231218_1416.py delete mode 100644 georiviere/river/migrations/0035_auto_20231218_1418.py delete mode 100644 georiviere/river/migrations/0036_auto_20231218_1420.py delete mode 100644 georiviere/river/migrations/0037_auto_20231218_1421.py delete mode 100644 georiviere/river/migrations/0038_auto_20231218_1431.py delete mode 100644 georiviere/river/migrations/0039_auto_20231218_1601.py delete mode 100644 georiviere/river/migrations/0040_auto_20231218_1608.py delete mode 100644 georiviere/river/migrations/0041_auto_20231219_0932.py create mode 100644 georiviere/river/sql/post_10_triggers.sql create mode 100644 georiviere/river/sql/pre_10_cleanup.sql diff --git a/docker-compose.yml b/docker-compose.yml index 64b778d6..f519fdc4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,6 @@ services: web: image: georiviere:latest - user: $UID:$GID build: context: . target: dev diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index e9b7335f..d36c1c74 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -1,27 +1,6 @@ -import pgtrigger from geotrek.altimetry.models import AltimetryMixin as BaseAltimetryMixin class AltimetryMixin(BaseAltimetryMixin): class Meta: abstract = True - triggers = [ - pgtrigger.Trigger( - name="keep_in_sync", - operation=pgtrigger.UpdateOf('geom') | pgtrigger.Insert, - when=pgtrigger.Before, - declare=[('elevation', 'elevation_infos')], - func=""" - SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation; - -- Update path geometry - NEW.geom_3d := elevation.draped; - NEW.length := ST_3DLength(elevation.draped); - NEW.slope := elevation.slope; - NEW.min_elevation := elevation.min_elevation; - NEW.max_elevation := elevation.max_elevation; - NEW.ascent := elevation.positive_gain; - NEW.descent := elevation.negative_gain; - RETURN NEW; - """ - ) - ] diff --git a/georiviere/description/migrations/0025_auto_20231215_1124.py b/georiviere/description/migrations/0025_auto_20231215_1124.py deleted file mode 100644 index 957a97dd..00000000 --- a/georiviere/description/migrations/0025_auto_20231215_1124.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-15 11:24 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('description', '0024_auto_20230323_1621'), - ] - - operations = [ - pgtrigger.migrations.AddTrigger( - model_name='land', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='82ffa7255d1e65c383b05ef0fa226da73a7ad6c8', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_fb8bb', table='description_land', when='BEFORE')), - ), - pgtrigger.migrations.AddTrigger( - model_name='morphology', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='f2e3c5183c820b762d1336a39135727ffc39c5e3', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_472ec', table='description_morphology', when='BEFORE')), - ), - pgtrigger.migrations.AddTrigger( - model_name='status', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='9ebcbb87cf2196d3c04e0e624571d3a49b1ebb69', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_aa87c', table='description_status', when='BEFORE')), - ), - pgtrigger.migrations.AddTrigger( - model_name='usage', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='c18fa3553e1c897d5a5e5f3b6cc5836c86429190', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_71b71', table='description_usage', when='BEFORE')), - ), - ] diff --git a/georiviere/description/migrations/0026_auto_20231218_1222.py b/georiviere/description/migrations/0026_auto_20231218_1222.py deleted file mode 100644 index b16732c9..00000000 --- a/georiviere/description/migrations/0026_auto_20231218_1222.py +++ /dev/null @@ -1,47 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 12:22 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('description', '0025_auto_20231215_1124'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='land', - name='keep_in_sync', - ), - pgtrigger.migrations.RemoveTrigger( - model_name='morphology', - name='keep_in_sync', - ), - pgtrigger.migrations.RemoveTrigger( - model_name='status', - name='keep_in_sync', - ), - pgtrigger.migrations.RemoveTrigger( - model_name='usage', - name='keep_in_sync', - ), - pgtrigger.migrations.AddTrigger( - model_name='land', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='d639f6850243942a05ed1f44f6cf79b88b563bad', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_fb8bb', table='description_land', when='BEFORE')), - ), - pgtrigger.migrations.AddTrigger( - model_name='morphology', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='048fe3697643f7682b5336def02a2a9d345947ae', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_472ec', table='description_morphology', when='BEFORE')), - ), - pgtrigger.migrations.AddTrigger( - model_name='status', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='967fa66b6c8eb5e17d5bfc49594c2573241d073b', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_aa87c', table='description_status', when='BEFORE')), - ), - pgtrigger.migrations.AddTrigger( - model_name='usage', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='69cb0cd06f0dccdca1f09d54169f5a673b87fca5', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_71b71', table='description_usage', when='BEFORE')), - ), - ] diff --git a/georiviere/description/models.py b/georiviere/description/models.py index cd686bb5..5be80af1 100644 --- a/georiviere/description/models.py +++ b/georiviere/description/models.py @@ -199,7 +199,7 @@ class Morphology(AddPropertyBufferMixin, TopologyMixin, TimeStampedModelMixin, class Meta: verbose_name = _("Morphology") verbose_name_plural = _("Morphologies") - triggers = AltimetryMixin.Meta.triggers + # triggers = AltimetryMixin.Meta.triggers def __str__(self): if self.main_flow: @@ -269,7 +269,6 @@ class Land(AddPropertyBufferMixin, TimeStampedModelMixin, WatershedPropertiesMix class Meta: verbose_name = _("Land") verbose_name_plural = _("Lands") - triggers = AltimetryMixin.Meta.triggers def __str__(self): return f"{self.land_type}" @@ -308,7 +307,6 @@ class Usage(AddPropertyBufferMixin, TimeStampedModelMixin, WatershedPropertiesMi class Meta: verbose_name = _("Usage") verbose_name_plural = _("Usages") - triggers = AltimetryMixin.Meta.triggers def __str__(self): return ', '.join(self.usage_types.values_list("label", flat=True)) @@ -357,7 +355,7 @@ class Status(TopologyMixin, AddPropertyBufferMixin, TimeStampedModelMixin, Water class Meta: verbose_name = _("Status") verbose_name_plural = _("Statuses") - triggers = AltimetryMixin.Meta.triggers + # triggers = AltimetryMixin.Meta.triggers def __str__(self): if self.status_types.count(): diff --git a/georiviere/description/sql/post_10_triggers.sql b/georiviere/description/sql/post_10_triggers.sql new file mode 100644 index 00000000..e801d311 --- /dev/null +++ b/georiviere/description/sql/post_10_triggers.sql @@ -0,0 +1,7 @@ +CREATE TRIGGER description_morphology_10_elevation +BEFORE INSERT OR UPDATE OF geom ON description_morphology +FOR EACH ROW EXECUTE PROCEDURE elevation(); + +CREATE TRIGGER description_status_10_elevation +BEFORE INSERT OR UPDATE OF geom ON description_status +FOR EACH ROW EXECUTE PROCEDURE elevation(); \ No newline at end of file diff --git a/georiviere/main/management/commands/migrate.py b/georiviere/main/management/commands/migrate.py index 70fef977..8b607a0e 100644 --- a/georiviere/main/management/commands/migrate.py +++ b/georiviere/main/management/commands/migrate.py @@ -22,8 +22,11 @@ def handle(self, *args, **options): check_srid_has_meter_unit() for app in apps.get_app_configs(): load_sql_files(app, 'pre') - load_sql_files(app, 'post') super().handle(*args, **options) - + for app in apps.get_app_configs(): + try: + load_sql_files(app, 'post') + except Exception: + pass call_command('sync_translation_fields', '--noinput') call_command('update_translation_fields') diff --git a/georiviere/main/sql/post_10_functions.sql b/georiviere/main/sql/post_10_functions.sql new file mode 100644 index 00000000..c11aba35 --- /dev/null +++ b/georiviere/main/sql/post_10_functions.sql @@ -0,0 +1,16 @@ +CREATE FUNCTION elevation() RETURNS trigger SECURITY DEFINER AS $$ +DECLARE + elevation elevation_infos; +BEGIN + SELECT * FROM ft_elevation_infos(NEW.geom, {{ ALTIMETRIC_PROFILE_STEP }}) INTO elevation; + -- Update path geometry + NEW.geom_3d := elevation.draped; + NEW.length := ST_3DLength(elevation.draped); + NEW.slope := elevation.slope; + NEW.min_elevation := elevation.min_elevation; + NEW.max_elevation := elevation.max_elevation; + NEW.ascent := elevation.positive_gain; + NEW.descent := elevation.negative_gain; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/georiviere/main/sql/pre_10_cleanup.sql b/georiviere/main/sql/pre_10_cleanup.sql new file mode 100644 index 00000000..1c034839 --- /dev/null +++ b/georiviere/main/sql/pre_10_cleanup.sql @@ -0,0 +1 @@ +DROP FUNCTION IF EXISTS elevation() CASCADE; \ No newline at end of file diff --git a/georiviere/proceeding/migrations/0004_auto_20231215_1124.py b/georiviere/proceeding/migrations/0004_auto_20231215_1124.py deleted file mode 100644 index 01a23413..00000000 --- a/georiviere/proceeding/migrations/0004_auto_20231215_1124.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-15 11:24 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('proceeding', '0003_auto_20210804_1014'), - ] - - operations = [ - pgtrigger.migrations.AddTrigger( - model_name='proceeding', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='4580ed3c25a4cd5a156ff3a0c1efd6c8301d8e25', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_6ba14', table='proceeding_proceeding', when='BEFORE')), - ), - ] diff --git a/georiviere/proceeding/migrations/0005_auto_20231218_1222.py b/georiviere/proceeding/migrations/0005_auto_20231218_1222.py deleted file mode 100644 index 5a7e4ac6..00000000 --- a/georiviere/proceeding/migrations/0005_auto_20231218_1222.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 12:22 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('proceeding', '0004_auto_20231215_1124'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='proceeding', - name='keep_in_sync', - ), - pgtrigger.migrations.AddTrigger( - model_name='proceeding', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='8be1e3d34cf0ef421d9f1750b10e7ef296d6a20d', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_6ba14', table='proceeding_proceeding', when='BEFORE')), - ), - ] diff --git a/georiviere/proceeding/models.py b/georiviere/proceeding/models.py index c88520b7..ab6627a3 100644 --- a/georiviere/proceeding/models.py +++ b/georiviere/proceeding/models.py @@ -49,7 +49,6 @@ def get_create_label(cls): class Meta: verbose_name = _("Proceeding") verbose_name_plural = _("Proceedings") - triggers = AltimetryMixin.Meta.triggers class Event(models.Model): diff --git a/georiviere/river/management/commands/load_rivers.py b/georiviere/river/management/commands/load_rivers.py index eca252d8..07b1acca 100644 --- a/georiviere/river/management/commands/load_rivers.py +++ b/georiviere/river/management/commands/load_rivers.py @@ -34,7 +34,7 @@ def handle(self, *args, **options): Stream.objects.all().delete() self.stdout.write(self.style.SUCCESS("done!")) - batch_size = 100 + batch_size = 50 objs = (Stream(geom=feat.geom.geos, source_location=Point(feat.geom.geos[0]), diff --git a/georiviere/river/migrations/0020_auto_20231215_1020.py b/georiviere/river/migrations/0020_auto_20231215_1020.py deleted file mode 100644 index e5421117..00000000 --- a/georiviere/river/migrations/0020_auto_20231215_1020.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-15 10:20 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0019_stream_description'), - ] - - operations = [ - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation JSONB;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='6c62fd64699ff351273ae8753db0b43dd5defaf7', operation='UPDATE OR INSERT', pgid='pgtrigger_keep_in_sync_7a86b', table='river_stream', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0021_auto_20231215_1051.py b/georiviere/river/migrations/0021_auto_20231215_1051.py deleted file mode 100644 index bd951d1f..00000000 --- a/georiviere/river/migrations/0021_auto_20231215_1051.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-15 10:51 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0020_auto_20231215_1020'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='stream', - name='keep_in_sync', - ), - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := element.draped;\n NEW.length := ST_3DLength(element.draped);\n NEW.slope := element.slope;\n NEW.min_elevation := element.min_elevation;\n NEW.max_elevation := element.max_elevation;\n NEW.ascent := element.positive_gain;\n NEW.descent := element.negative_gain;\n\n RETURN NEW;\n ', hash='fdeef3b3432dda345b1ec896b91e84eb120a9986', operation='UPDATE OR INSERT', pgid='pgtrigger_keep_in_sync_7a86b', table='river_stream', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0022_auto_20231215_1052.py b/georiviere/river/migrations/0022_auto_20231215_1052.py deleted file mode 100644 index 9a4e0b26..00000000 --- a/georiviere/river/migrations/0022_auto_20231215_1052.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-15 10:52 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0021_auto_20231215_1051'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='stream', - name='keep_in_sync', - ), - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='24de00d1e6f9be9e088db3410e54b0de328653a3', operation='UPDATE OR INSERT', pgid='pgtrigger_keep_in_sync_7a86b', table='river_stream', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0023_auto_20231215_1056.py b/georiviere/river/migrations/0023_auto_20231215_1056.py deleted file mode 100644 index 6e669b8a..00000000 --- a/georiviere/river/migrations/0023_auto_20231215_1056.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-15 10:56 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0022_auto_20231215_1052'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='stream', - name='keep_in_sync', - ), - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n\n RETURN NEW;\n ', hash='168b84532fa3b4230f53fbd1cfdb1169c5d72a0a', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_7a86b', table='river_stream', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0024_auto_20231218_1226.py b/georiviere/river/migrations/0024_auto_20231218_1226.py deleted file mode 100644 index 4d4f21a1..00000000 --- a/georiviere/river/migrations/0024_auto_20231218_1226.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 12:26 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0023_auto_20231215_1056'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='stream', - name='keep_in_sync', - ), - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='keep_in_sync', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos;', func='\n SELECT * FROM ft_elevation_infos(NEW.geom, 20) INTO elevation;\n -- Update path geometry\n NEW.geom_3d := elevation.draped;\n NEW.length := ST_3DLength(elevation.draped);\n NEW.slope := elevation.slope;\n NEW.min_elevation := elevation.min_elevation;\n NEW.max_elevation := elevation.max_elevation;\n NEW.ascent := elevation.positive_gain;\n NEW.descent := elevation.negative_gain;\n RETURN NEW;\n ', hash='e66f827b3e0e2d3e5f822a259752f2d8a48a7c26', operation='UPDATE OF "geom" OR INSERT', pgid='pgtrigger_keep_in_sync_7a86b', table='river_stream', when='BEFORE')), - ), - pgtrigger.migrations.AddTrigger( - model_name='topology', - trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; geom geometry;', func='\n IF (NEW.start_point != OLD.start_point OR NEW.end_point != OLD.end_point) THEN \n SELECT geom FROM river_stream r WHERE NEW.id = r.id INTO geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(geom, NEW.START_POINT, NEW.END_POINT)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(geom, NEW.START_POINT, NEW.END_POINT)\n WHERE topology_id = NEW.id; \n END IF;\n RETURN NEW;\n ', hash='db1df9343d9079232a59df61b34bfcd7bc2df48a', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0025_auto_20231218_1325.py b/georiviere/river/migrations/0025_auto_20231218_1325.py deleted file mode 100644 index 74c03ca5..00000000 --- a/georiviere/river/migrations/0025_auto_20231218_1325.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 13:25 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0024_auto_20231218_1226'), - ] - - operations = [ - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func='\n INSERT INTO river_topology (stream_id, start_position, end_position)\n VALUES (NEW.id, 0, 1) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom) VALUES (topology_morphology, NEW.geom);\n INSERT INTO river_topology (stream_id, start_position, end_position)\n VALUES (NEW.id, 0, 1) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom) VALUES (topology_status, NEW.geom);\n RETURN NEW;\n ', hash='c0b7e1615c64b92e36e49803f3001ebde7dd517e', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), - ), - ] diff --git a/georiviere/river/migrations/0026_auto_20231218_1356.py b/georiviere/river/migrations/0026_auto_20231218_1356.py deleted file mode 100644 index 45b90ad4..00000000 --- a/georiviere/river/migrations/0026_auto_20231218_1356.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 13:56 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0025_auto_20231218_1325'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='topology', - name='update_topology_geom', - ), - pgtrigger.migrations.AddTrigger( - model_name='topology', - trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; geom geometry;', func='\n IF (NEW.start_position != OLD.start_position OR NEW.end_position != OLD.end_position) THEN \n SELECT geom FROM river_stream r WHERE NEW.id = r.id INTO geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n END IF;\n RETURN NEW;\n ', hash='10d2e4894631bee97f91deeb607004d3a5295448', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0027_auto_20231218_1400.py b/georiviere/river/migrations/0027_auto_20231218_1400.py deleted file mode 100644 index f32c8f08..00000000 --- a/georiviere/river/migrations/0027_auto_20231218_1400.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:00 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0026_auto_20231218_1356'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='stream', - name='create_topologies', - ), - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func='\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom) VALUES (topology_morphology, NEW.geom);\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom) VALUES (topology_status, NEW.geom);\n RETURN NEW;\n ', hash='4ebe61234dd666bf424de96761a8af7e0a72560f', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), - ), - ] diff --git a/georiviere/river/migrations/0028_auto_20231218_1402.py b/georiviere/river/migrations/0028_auto_20231218_1402.py deleted file mode 100644 index 227b45d4..00000000 --- a/georiviere/river/migrations/0028_auto_20231218_1402.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:02 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0027_auto_20231218_1400'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='stream', - name='create_topologies', - ), - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func='\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom, date_insert, date_update) VALUES (topology_morphology, NEW.geom, NOW(), NOW());\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom, date_insert, date_update) VALUES (topology_status, NEW.geom, NOW(), NOW());\n RETURN NEW;\n ', hash='606f9274946baa820c7792e04a1f6559948a2e8e', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), - ), - ] diff --git a/georiviere/river/migrations/0029_auto_20231218_1403.py b/georiviere/river/migrations/0029_auto_20231218_1403.py deleted file mode 100644 index 8416c313..00000000 --- a/georiviere/river/migrations/0029_auto_20231218_1403.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:03 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0028_auto_20231218_1402'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='stream', - name='create_topologies', - ), - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func="\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update)\n VALUES (topology_morphology, NEW.geom, '', NOW(), NOW());\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom, date_insert, date_update) VALUES (topology_status, NEW.geom, NOW(), NOW());\n RETURN NEW;\n ", hash='cfb353801531d9e2c134a09c85d8a69807704a71', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), - ), - ] diff --git a/georiviere/river/migrations/0030_auto_20231218_1405.py b/georiviere/river/migrations/0030_auto_20231218_1405.py deleted file mode 100644 index 1ef79185..00000000 --- a/georiviere/river/migrations/0030_auto_20231218_1405.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:05 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0029_auto_20231218_1403'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='stream', - name='create_topologies', - ), - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func="\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update)\n VALUES (topology_morphology, NEW.geom, '', NOW(), NOW());\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom, regulation, referencial, descrption, date_insert, date_update)\n VALUES (topology_status, NEW.geom, FALSE, FALSE, '', NOW(), NOW());\n RETURN NEW;\n ", hash='f5c7079e6d4e21c67d642ea14d61d4e2f39787b0', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), - ), - ] diff --git a/georiviere/river/migrations/0031_auto_20231218_1405.py b/georiviere/river/migrations/0031_auto_20231218_1405.py deleted file mode 100644 index 2dbd1031..00000000 --- a/georiviere/river/migrations/0031_auto_20231218_1405.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:05 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0030_auto_20231218_1405'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='stream', - name='create_topologies', - ), - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='create_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE topology_morphology integer; topology_status integer;', func="\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology;\n INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update)\n VALUES (topology_morphology, NEW.geom, '', NOW(), NOW());\n INSERT INTO river_topology (stream_id, start_position, end_position, qualified)\n VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status;\n INSERT INTO description_status (topology_id, geom, regulation, referencial, description, date_insert, date_update)\n VALUES (topology_status, NEW.geom, FALSE, FALSE, '', NOW(), NOW());\n RETURN NEW;\n ", hash='e846f76834647fd2338681a9530765b6d1883cc9', operation='INSERT', pgid='pgtrigger_create_topologies_e8d7d', table='river_stream', when='AFTER')), - ), - ] diff --git a/georiviere/river/migrations/0032_auto_20231218_1410.py b/georiviere/river/migrations/0032_auto_20231218_1410.py deleted file mode 100644 index 1e19b353..00000000 --- a/georiviere/river/migrations/0032_auto_20231218_1410.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:10 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0031_auto_20231218_1405'), - ] - - operations = [ - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='update_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(func='\n UPDATE river_topology SET start_position=start_position WHERE stream_id=NEW.id;\n RETURN NEW;\n ', hash='0b6322b6b455eed7b6a2a78c79b07e08e9a86611', operation='UPDATE OF "geom"', pgid='pgtrigger_update_topologies_6ab45', table='river_stream', when='AFTER')), - ), - ] diff --git a/georiviere/river/migrations/0033_auto_20231218_1412.py b/georiviere/river/migrations/0033_auto_20231218_1412.py deleted file mode 100644 index 1b5d52e2..00000000 --- a/georiviere/river/migrations/0033_auto_20231218_1412.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:12 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0032_auto_20231218_1410'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='topology', - name='update_topology_geom', - ), - pgtrigger.migrations.AddTrigger( - model_name='topology', - trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; geom geometry;', func='\n SELECT geom FROM river_stream r WHERE NEW.id = r.id INTO geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='2b83b20dab6aa7e5456c1e10694f527dc98eb281', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0034_auto_20231218_1416.py b/georiviere/river/migrations/0034_auto_20231218_1416.py deleted file mode 100644 index 653b8de5..00000000 --- a/georiviere/river/migrations/0034_auto_20231218_1416.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:16 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0033_auto_20231218_1412'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='topology', - name='update_topology_geom', - ), - pgtrigger.migrations.AddTrigger( - model_name='topology', - trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.id = r.id INTO geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='1e3e1dbd526ffd01e8155aec62fad614e188471d', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0035_auto_20231218_1418.py b/georiviere/river/migrations/0035_auto_20231218_1418.py deleted file mode 100644 index e92cd864..00000000 --- a/georiviere/river/migrations/0035_auto_20231218_1418.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:18 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0034_auto_20231218_1416'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='topology', - name='update_topology_geom', - ), - pgtrigger.migrations.AddTrigger( - model_name='topology', - trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.id = r.id INTO geom;\n UPDATE description_morphology m\n SET m.geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status s\n SET s.geom = ST_LINESUBSTRING(geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='a3d43086e66e04cde5bf979df1f4565a3bb9e791', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0036_auto_20231218_1420.py b/georiviere/river/migrations/0036_auto_20231218_1420.py deleted file mode 100644 index 4421a74f..00000000 --- a/georiviere/river/migrations/0036_auto_20231218_1420.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:20 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0035_auto_20231218_1418'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='topology', - name='update_topology_geom', - ), - pgtrigger.migrations.AddTrigger( - model_name='topology', - trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; stream_geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.id = r.id INTO stream_geom;\n UPDATE description_morphology m\n SET m.geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status s\n SET s.geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='1671ae29035c32a992993d3518965e8e9c0a605f', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0037_auto_20231218_1421.py b/georiviere/river/migrations/0037_auto_20231218_1421.py deleted file mode 100644 index 1739897b..00000000 --- a/georiviere/river/migrations/0037_auto_20231218_1421.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:21 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0036_auto_20231218_1420'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='topology', - name='update_topology_geom', - ), - pgtrigger.migrations.AddTrigger( - model_name='topology', - trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE elevation elevation_infos; stream_geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.id = r.id INTO stream_geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='444948eb36585ca5ea4dfdad5e9be32137e67844', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='BEFORE')), - ), - ] diff --git a/georiviere/river/migrations/0038_auto_20231218_1431.py b/georiviere/river/migrations/0038_auto_20231218_1431.py deleted file mode 100644 index 26019979..00000000 --- a/georiviere/river/migrations/0038_auto_20231218_1431.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 14:31 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0037_auto_20231218_1421'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='topology', - name='update_topology_geom', - ), - pgtrigger.migrations.AddTrigger( - model_name='topology', - trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE stream_geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.id = r.id INTO stream_geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='f0a3386a26d41f8268db5bbdb554ba4cc3ae1612', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='AFTER')), - ), - ] diff --git a/georiviere/river/migrations/0039_auto_20231218_1601.py b/georiviere/river/migrations/0039_auto_20231218_1601.py deleted file mode 100644 index 06a328cb..00000000 --- a/georiviere/river/migrations/0039_auto_20231218_1601.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 16:01 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0038_auto_20231218_1431'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='stream', - name='update_topologies', - ), - pgtrigger.migrations.AddTrigger( - model_name='stream', - trigger=pgtrigger.compiler.Trigger(name='update_topologies', sql=pgtrigger.compiler.UpsertTriggerSql(func='\n UPDATE river_topology SET id=id WHERE stream_id=NEW.id;\n RETURN NEW;\n ', hash='704192b93012d12fa95b06b03618981a49609a4e', operation='UPDATE OF "geom"', pgid='pgtrigger_update_topologies_6ab45', table='river_stream', when='AFTER')), - ), - ] diff --git a/georiviere/river/migrations/0040_auto_20231218_1608.py b/georiviere/river/migrations/0040_auto_20231218_1608.py deleted file mode 100644 index ad4c8f4d..00000000 --- a/georiviere/river/migrations/0040_auto_20231218_1608.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-18 16:08 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0039_auto_20231218_1601'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='topology', - name='update_topology_geom', - ), - pgtrigger.migrations.AddTrigger( - model_name='topology', - trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE stream_geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.stream_id = r.id INTO stream_geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n UPDATE description_status\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id; \n RETURN NEW;\n ', hash='4d63b6e0ef200235882742af37d834d00b5a271a', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='AFTER')), - ), - ] diff --git a/georiviere/river/migrations/0041_auto_20231219_0932.py b/georiviere/river/migrations/0041_auto_20231219_0932.py deleted file mode 100644 index 12429cd4..00000000 --- a/georiviere/river/migrations/0041_auto_20231219_0932.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.1.14 on 2023-12-19 09:32 - -from django.db import migrations -import pgtrigger.compiler -import pgtrigger.migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('river', '0040_auto_20231218_1608'), - ] - - operations = [ - pgtrigger.migrations.RemoveTrigger( - model_name='topology', - name='update_topology_geom', - ), - pgtrigger.migrations.AddTrigger( - model_name='topology', - trigger=pgtrigger.compiler.Trigger(name='update_topology_geom', sql=pgtrigger.compiler.UpsertTriggerSql(declare='DECLARE stream_geom geometry;', func='\n SELECT r.geom FROM river_stream r WHERE NEW.stream_id = r.id INTO stream_geom;\n UPDATE description_morphology\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id;\n UPDATE description_status\n SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position)\n WHERE topology_id = NEW.id;\n RETURN NEW;\n ', hash='0d8dce1088e59bd290e9ffe0d6bfb8a2493a5c1c', operation='UPDATE OR INSERT', pgid='pgtrigger_update_topology_geom_011ed', table='river_topology', when='AFTER')), - ), - ] diff --git a/georiviere/river/models.py b/georiviere/river/models.py index 9283a84c..041cab52 100644 --- a/georiviere/river/models.py +++ b/georiviere/river/models.py @@ -1,4 +1,3 @@ -import pgtrigger from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.contrib.gis.db import models @@ -97,37 +96,37 @@ class FlowChoices(models.IntegerChoices): class Meta: verbose_name = _("Stream") verbose_name_plural = _("Streams") - triggers = AltimetryMixin.Meta.triggers + [ - pgtrigger.Trigger( - name="create_topologies", - operation=pgtrigger.Insert, - declare=[ - ('topology_morphology', 'integer'), - ('topology_status', 'integer') - ], - when=pgtrigger.After, - func=""" - INSERT INTO river_topology (stream_id, start_position, end_position, qualified) - VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology; - INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update) - VALUES (topology_morphology, NEW.geom, '', NOW(), NOW()); - INSERT INTO river_topology (stream_id, start_position, end_position, qualified) - VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status; - INSERT INTO description_status (topology_id, geom, regulation, referencial, description, date_insert, date_update) - VALUES (topology_status, NEW.geom, FALSE, FALSE, '', NOW(), NOW()); - RETURN NEW; - """ - ), - pgtrigger.Trigger( - name="update_topologies", - operation=pgtrigger.UpdateOf('geom'), - when=pgtrigger.After, - func=""" - UPDATE river_topology SET id=id WHERE stream_id=NEW.id; - RETURN NEW; - """ - ) - ] + # triggers = AltimetryMixin.Meta.triggers + [ + # pgtrigger.Trigger( + # name="create_topologies", + # operation=pgtrigger.Insert, + # declare=[ + # ('topology_morphology', 'integer'), + # ('topology_status', 'integer') + # ], + # when=pgtrigger.After, + # func=""" + # INSERT INTO river_topology (stream_id, start_position, end_position, qualified) + # VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology; + # INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update) + # VALUES (topology_morphology, NEW.geom, '', NOW(), NOW()); + # INSERT INTO river_topology (stream_id, start_position, end_position, qualified) + # VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status; + # INSERT INTO description_status (topology_id, geom, regulation, referencial, description, date_insert, date_update) + # VALUES (topology_status, NEW.geom, FALSE, FALSE, '', NOW(), NOW()); + # RETURN NEW; + # """ + # ), + # pgtrigger.Trigger( + # name="update_topologies", + # operation=pgtrigger.UpdateOf('geom'), + # when=pgtrigger.After, + # func=""" + # UPDATE river_topology SET id=id WHERE stream_id=NEW.id; + # RETURN NEW; + # """ + # ) + # ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -251,23 +250,23 @@ def __str__(self): class Meta: verbose_name = _("Topology") verbose_name_plural = _("Topologies") - triggers = [ - pgtrigger.Trigger( - name="update_topology_geom", - operation=pgtrigger.Update | pgtrigger.Insert, - when=pgtrigger.After, - declare=[('stream_geom', 'geometry')], - func=""" - SELECT r.geom FROM river_stream r WHERE NEW.stream_id = r.id INTO stream_geom; - UPDATE description_morphology - SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position) - WHERE topology_id = NEW.id; - UPDATE description_status - SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position) - WHERE topology_id = NEW.id; - RETURN NEW; - """ - )] + # triggers = [ + # pgtrigger.Trigger( + # name="update_topology_geom", + # operation=pgtrigger.Update | pgtrigger.Insert, + # when=pgtrigger.After, + # declare=[('stream_geom', 'geometry')], + # func=""" + # SELECT r.geom FROM river_stream r WHERE NEW.stream_id = r.id INTO stream_geom; + # UPDATE description_morphology + # SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position) + # WHERE topology_id = NEW.id; + # UPDATE description_status + # SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position) + # WHERE topology_id = NEW.id; + # RETURN NEW; + # """ + # )] Study.add_property('stations', Station.within_buffer, _("Stations")) diff --git a/georiviere/river/sql/post_10_triggers.sql b/georiviere/river/sql/post_10_triggers.sql new file mode 100644 index 00000000..f83233af --- /dev/null +++ b/georiviere/river/sql/post_10_triggers.sql @@ -0,0 +1,43 @@ +CREATE TRIGGER river_stream_10_elevation +BEFORE INSERT OR UPDATE OF geom ON river_stream +FOR EACH ROW EXECUTE PROCEDURE elevation(); + +CREATE FUNCTION update_topology_geom() RETURNS trigger SECURITY DEFINER AS $$ +DECLARE + stream_geom geometry; +BEGIN + SELECT r.geom FROM river_stream r WHERE NEW.stream_id = r.id INTO stream_geom; + UPDATE description_morphology + SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position) + WHERE topology_id = NEW.id; + UPDATE description_status + SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position) + WHERE topology_id = NEW.id; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER core_path_10_elevation_iu_tgr +BEFORE INSERT OR UPDATE ON river_topology +FOR EACH ROW EXECUTE PROCEDURE update_topology_geom(); + +CREATE FUNCTION create_topologies() RETURNS trigger SECURITY DEFINER AS $$ +DECLARE + topology_morphology integer; + topology_status integer; +BEGIN + INSERT INTO river_topology (stream_id, start_position, end_position, qualified) + VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology; + INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update) + VALUES (topology_morphology, NEW.geom, '', NOW(), NOW()); + INSERT INTO river_topology (stream_id, start_position, end_position, qualified) + VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status; + INSERT INTO description_status (topology_id, geom, regulation, referencial, description, date_insert, date_update) + VALUES (topology_status, NEW.geom, FALSE, FALSE, '', NOW(), NOW()); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER core_path_10_elevation_iu_tgr +AFTER INSERT ON river_stream +FOR EACH ROW EXECUTE PROCEDURE create_topologies(); \ No newline at end of file diff --git a/georiviere/river/sql/pre_10_cleanup.sql b/georiviere/river/sql/pre_10_cleanup.sql new file mode 100644 index 00000000..79b37e8f --- /dev/null +++ b/georiviere/river/sql/pre_10_cleanup.sql @@ -0,0 +1,3 @@ +DROP FUNCTION IF EXISTS update_topology_geom() CASCADE; +DROP FUNCTION IF EXISTS create_topologies() CASCADE; + diff --git a/georiviere/settings/__init__.py b/georiviere/settings/__init__.py index 00e4741c..56221baf 100644 --- a/georiviere/settings/__init__.py +++ b/georiviere/settings/__init__.py @@ -135,7 +135,7 @@ def construct_relative_path_mock(current_template_name, relative_name): 'embed_video', 'djgeojson', 'django_filters', - 'pgtrigger', + #'pgtrigger', 'compressor', 'mapentity', # mapentity should be placed after app declaring paperclip models 'leaflet', diff --git a/requirements.in b/requirements.in index c09d1eb1..a4d9353d 100644 --- a/requirements.in +++ b/requirements.in @@ -14,7 +14,6 @@ sentry-sdk django-admin-ordering djangorestframework-camel-case jsonschema -django-pgtrigger # Use until latest version of geotrek (> 2.99.0) drf-spectacular diff --git a/requirements.txt b/requirements.txt index 9ebbca33..3a3449a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -91,7 +91,6 @@ django==3.1.14 # django-leaflet # django-modeltranslation # django-mptt - # django-pgtrigger # django-querysetsequence # django-weasyprint # djangorestframework @@ -145,8 +144,6 @@ django-modeltranslation==0.17.3 # via mapentity django-mptt==0.11.0 # via geotrek -django-pgtrigger==4.11.0 - # via -r requirements.in django-querysetsequence==0.14 # via django-autocomplete-light django-tinymce==2.9.0 From 9ddd1b659fb5d0717960054f009c96a8e5773514 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 14:20:09 +0100 Subject: [PATCH 038/114] use internal triggers --- georiviere/altimetry.py | 11 +++++++++++ georiviere/river/models.py | 31 ------------------------------- 2 files changed, 11 insertions(+), 31 deletions(-) diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index d36c1c74..9306f7e7 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -2,5 +2,16 @@ class AltimetryMixin(BaseAltimetryMixin): + def reload(self): + # Update object's computed values (reload from database) + if self.pk: + fromdb = self.__class__.objects.get(pk=self.pk) + BaseAltimetryMixin.reload(self, fromdb) + return self + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + self.reload() + class Meta: abstract = True diff --git a/georiviere/river/models.py b/georiviere/river/models.py index 041cab52..4aa38376 100644 --- a/georiviere/river/models.py +++ b/georiviere/river/models.py @@ -96,37 +96,6 @@ class FlowChoices(models.IntegerChoices): class Meta: verbose_name = _("Stream") verbose_name_plural = _("Streams") - # triggers = AltimetryMixin.Meta.triggers + [ - # pgtrigger.Trigger( - # name="create_topologies", - # operation=pgtrigger.Insert, - # declare=[ - # ('topology_morphology', 'integer'), - # ('topology_status', 'integer') - # ], - # when=pgtrigger.After, - # func=""" - # INSERT INTO river_topology (stream_id, start_position, end_position, qualified) - # VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_morphology; - # INSERT INTO description_morphology (topology_id, geom, description, date_insert, date_update) - # VALUES (topology_morphology, NEW.geom, '', NOW(), NOW()); - # INSERT INTO river_topology (stream_id, start_position, end_position, qualified) - # VALUES (NEW.id, 0, 1, FALSE) RETURNING id INTO topology_status; - # INSERT INTO description_status (topology_id, geom, regulation, referencial, description, date_insert, date_update) - # VALUES (topology_status, NEW.geom, FALSE, FALSE, '', NOW(), NOW()); - # RETURN NEW; - # """ - # ), - # pgtrigger.Trigger( - # name="update_topologies", - # operation=pgtrigger.UpdateOf('geom'), - # when=pgtrigger.After, - # func=""" - # UPDATE river_topology SET id=id WHERE stream_id=NEW.id; - # RETURN NEW; - # """ - # ) - # ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) From 6a191537479bf6a775e09757616e617736e27ef2 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 14:22:24 +0100 Subject: [PATCH 039/114] use internal triggers --- georiviere/altimetry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index 9306f7e7..fb32ca56 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -8,10 +8,10 @@ def reload(self): fromdb = self.__class__.objects.get(pk=self.pk) BaseAltimetryMixin.reload(self, fromdb) return self - + def save(self, *args, **kwargs): super().save(*args, **kwargs) self.reload() - + class Meta: abstract = True From 7aacca363345faa74e9edd9a93064724c85a374f Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 14:30:17 +0100 Subject: [PATCH 040/114] use internal triggers --- georiviere/altimetry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index fb32ca56..a4c58d43 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -11,7 +11,7 @@ def reload(self): def save(self, *args, **kwargs): super().save(*args, **kwargs) - self.reload() + self.reload(self) class Meta: abstract = True From ad335b385a0ced0b2089560f67a433b2adceb6a4 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 14:31:46 +0100 Subject: [PATCH 041/114] use internal triggers --- georiviere/altimetry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index a4c58d43..1426eacf 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -6,12 +6,12 @@ def reload(self): # Update object's computed values (reload from database) if self.pk: fromdb = self.__class__.objects.get(pk=self.pk) - BaseAltimetryMixin.reload(self, fromdb) + super().reload(fromdb) return self def save(self, *args, **kwargs): super().save(*args, **kwargs) - self.reload(self) + self.reload() class Meta: abstract = True From dc75d4f3f64f5d286c38c1a6bd03b7aa33635695 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 14:32:08 +0100 Subject: [PATCH 042/114] use internal triggers --- georiviere/altimetry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index 1426eacf..41d40b49 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -2,7 +2,7 @@ class AltimetryMixin(BaseAltimetryMixin): - def reload(self): + def reload(self, *args, **kwargs): # Update object's computed values (reload from database) if self.pk: fromdb = self.__class__.objects.get(pk=self.pk) From 74dbf8270c610ea24e37e1bb746a13456eff68a4 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 14:51:20 +0100 Subject: [PATCH 043/114] use internal triggers --- georiviere/altimetry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index 41d40b49..cf1d67ce 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -2,16 +2,16 @@ class AltimetryMixin(BaseAltimetryMixin): - def reload(self, *args, **kwargs): + def refresh(self): # Update object's computed values (reload from database) if self.pk: fromdb = self.__class__.objects.get(pk=self.pk) - super().reload(fromdb) + BaseAltimetryMixin.reload(fromdb) return self def save(self, *args, **kwargs): super().save(*args, **kwargs) - self.reload() + self.refresh() class Meta: abstract = True From 61e08dd1bac2fe10a5ce5b442fc6e6bd5af8925e Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 14:59:29 +0100 Subject: [PATCH 044/114] use internal triggers --- georiviere/altimetry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index cf1d67ce..fb32ca56 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -2,16 +2,16 @@ class AltimetryMixin(BaseAltimetryMixin): - def refresh(self): + def reload(self): # Update object's computed values (reload from database) if self.pk: fromdb = self.__class__.objects.get(pk=self.pk) - BaseAltimetryMixin.reload(fromdb) + BaseAltimetryMixin.reload(self, fromdb) return self def save(self, *args, **kwargs): super().save(*args, **kwargs) - self.refresh() + self.reload() class Meta: abstract = True From 11c071d06a48b5c5c0cd06830e425cf92d8a51c2 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 15:07:47 +0100 Subject: [PATCH 045/114] use internal triggers --- georiviere/altimetry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index fb32ca56..f2bf5482 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -2,7 +2,7 @@ class AltimetryMixin(BaseAltimetryMixin): - def reload(self): + def refresh(self): # Update object's computed values (reload from database) if self.pk: fromdb = self.__class__.objects.get(pk=self.pk) @@ -11,7 +11,7 @@ def reload(self): def save(self, *args, **kwargs): super().save(*args, **kwargs) - self.reload() + self.refresh() class Meta: abstract = True From 02ca87eb2fb0f8a513ba26029d244dbec9f84163 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 15:27:07 +0100 Subject: [PATCH 046/114] use internal triggers --- georiviere/description/sql/post_10_triggers.sql | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/georiviere/description/sql/post_10_triggers.sql b/georiviere/description/sql/post_10_triggers.sql index e801d311..25bdd79b 100644 --- a/georiviere/description/sql/post_10_triggers.sql +++ b/georiviere/description/sql/post_10_triggers.sql @@ -4,4 +4,8 @@ FOR EACH ROW EXECUTE PROCEDURE elevation(); CREATE TRIGGER description_status_10_elevation BEFORE INSERT OR UPDATE OF geom ON description_status +FOR EACH ROW EXECUTE PROCEDURE elevation(); + +CREATE TRIGGER description_land_10_elevation +BEFORE INSERT OR UPDATE OF geom ON description_land FOR EACH ROW EXECUTE PROCEDURE elevation(); \ No newline at end of file From a501ede10449892aae29e466b309976184401261 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 20 Dec 2023 15:57:53 +0100 Subject: [PATCH 047/114] use internal triggers --- georiviere/description/models.py | 2 -- georiviere/description/sql/post_10_triggers.sql | 4 ++++ georiviere/proceeding/sql/post_10_triggers.sql | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 georiviere/proceeding/sql/post_10_triggers.sql diff --git a/georiviere/description/models.py b/georiviere/description/models.py index 5be80af1..ca1aaaf2 100644 --- a/georiviere/description/models.py +++ b/georiviere/description/models.py @@ -199,7 +199,6 @@ class Morphology(AddPropertyBufferMixin, TopologyMixin, TimeStampedModelMixin, class Meta: verbose_name = _("Morphology") verbose_name_plural = _("Morphologies") - # triggers = AltimetryMixin.Meta.triggers def __str__(self): if self.main_flow: @@ -355,7 +354,6 @@ class Status(TopologyMixin, AddPropertyBufferMixin, TimeStampedModelMixin, Water class Meta: verbose_name = _("Status") verbose_name_plural = _("Statuses") - # triggers = AltimetryMixin.Meta.triggers def __str__(self): if self.status_types.count(): diff --git a/georiviere/description/sql/post_10_triggers.sql b/georiviere/description/sql/post_10_triggers.sql index 25bdd79b..958dbec4 100644 --- a/georiviere/description/sql/post_10_triggers.sql +++ b/georiviere/description/sql/post_10_triggers.sql @@ -8,4 +8,8 @@ FOR EACH ROW EXECUTE PROCEDURE elevation(); CREATE TRIGGER description_land_10_elevation BEFORE INSERT OR UPDATE OF geom ON description_land +FOR EACH ROW EXECUTE PROCEDURE elevation(); + +CREATE TRIGGER description_usage_10_elevation +BEFORE INSERT OR UPDATE OF geom ON description_usage FOR EACH ROW EXECUTE PROCEDURE elevation(); \ No newline at end of file diff --git a/georiviere/proceeding/sql/post_10_triggers.sql b/georiviere/proceeding/sql/post_10_triggers.sql new file mode 100644 index 00000000..fd0e2b8f --- /dev/null +++ b/georiviere/proceeding/sql/post_10_triggers.sql @@ -0,0 +1,3 @@ +CREATE TRIGGER proceeding_proceeding_10_elevation +BEFORE INSERT OR UPDATE OF geom ON proceeding_proceeding +FOR EACH ROW EXECUTE PROCEDURE elevation(); From 94474b1bfd723a3338acd3398313797d3670f992 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 3 Jan 2024 14:36:57 +0100 Subject: [PATCH 048/114] use internal triggers --- georiviere/altimetry.py | 2 ++ georiviere/description/sql/post_20_defaults.sql | 3 +++ georiviere/description/tests/test_views.py | 8 ++++---- georiviere/river/tests/test_models.py | 8 ++++---- georiviere/river/tests/test_signals.py | 2 +- 5 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 georiviere/description/sql/post_20_defaults.sql diff --git a/georiviere/altimetry.py b/georiviere/altimetry.py index f2bf5482..cd7ee6b9 100644 --- a/georiviere/altimetry.py +++ b/georiviere/altimetry.py @@ -1,4 +1,5 @@ from geotrek.altimetry.models import AltimetryMixin as BaseAltimetryMixin +from geotrek.common.mixins import TimeStampedModelMixin class AltimetryMixin(BaseAltimetryMixin): @@ -7,6 +8,7 @@ def refresh(self): if self.pk: fromdb = self.__class__.objects.get(pk=self.pk) BaseAltimetryMixin.reload(self, fromdb) + TimeStampedModelMixin.reload(self, fromdb) return self def save(self, *args, **kwargs): diff --git a/georiviere/description/sql/post_20_defaults.sql b/georiviere/description/sql/post_20_defaults.sql new file mode 100644 index 00000000..5128eb1b --- /dev/null +++ b/georiviere/description/sql/post_20_defaults.sql @@ -0,0 +1,3 @@ +ALTER TABLE description_morphology ALTER COLUMN full_edge_height SET DEFAULT 0.0; +ALTER TABLE description_morphology ALTER COLUMN full_edge_width SET DEFAULT 0.0; +ALTER TABLE description_morphology ALTER COLUMN description SET DEFAULT ''; diff --git a/georiviere/description/tests/test_views.py b/georiviere/description/tests/test_views.py index 86940023..5e2b4f54 100644 --- a/georiviere/description/tests/test_views.py +++ b/georiviere/description/tests/test_views.py @@ -105,8 +105,8 @@ class SatusViewTestCase(TopologyTestCase): def get_expected_json_attrs(self): return { 'id': self.obj.pk, - 'date_update': '2020-03-17T00:00:00Z', - 'date_insert': '2020-03-17T00:00:00Z', + 'date_update': self.obj.date_update.isoformat().replace('+00:00', 'Z'), + 'date_insert': self.obj.date_insert.isoformat().replace('+00:00', 'Z'), 'description': '', 'length': self.obj.length, 'geom_3d': self.obj.geom_3d.ewkt, @@ -147,8 +147,8 @@ class MorphologyViewTestCase(TopologyTestCase): def get_expected_json_attrs(self): return { 'id': self.obj.pk, - 'date_update': '2020-03-17T00:00:00Z', - 'date_insert': '2020-03-17T00:00:00Z', + 'date_update': self.obj.date_update.isoformat().replace('+00:00', 'Z'), + 'date_insert': self.obj.date_insert.isoformat().replace('+00:00', 'Z'), 'description': '', 'bank_state_left': None, 'bank_state_right': None, diff --git a/georiviere/river/tests/test_models.py b/georiviere/river/tests/test_models.py index d37a240e..69c8ac89 100644 --- a/georiviere/river/tests/test_models.py +++ b/georiviere/river/tests/test_models.py @@ -90,16 +90,16 @@ def test_get_map_image_extent(self): self.assertAlmostEqual(lat_max, -5.655019875165679) def test_distance_to_source(self): - """Test distance from a given object to stream source according to differents geom""" + """Test distance from a given object to stream source according to different geom""" self.assertAlmostEqual(self.stream1.distance_to_source(self.usage_point), 28.2842712) self.assertEqual(self.stream1.distance_to_source(self.administrative_file), None) - self.assertEqual(DistanceToSource.objects.count(), 6) + self.assertEqual(DistanceToSource.objects.count(), 2) usage_point = UsageFactory.create( geom=Point(10000, 10010) ) - self.assertEqual(DistanceToSource.objects.count(), 8) + self.assertEqual(DistanceToSource.objects.count(), 4) usage_point.delete() - self.assertEqual(DistanceToSource.objects.count(), 6) + self.assertEqual(DistanceToSource.objects.count(), 2) class SnapTest(TestCase): diff --git a/georiviere/river/tests/test_signals.py b/georiviere/river/tests/test_signals.py index 148669ad..f947d67d 100644 --- a/georiviere/river/tests/test_signals.py +++ b/georiviere/river/tests/test_signals.py @@ -30,5 +30,5 @@ def test_update_stream_move_topologies(self): morphologies = Morphology.objects.values_list('geom', flat=True) status = Status.objects.values_list('geom', flat=True) - self.assertEqual(morphologies[0], morphologies[1]) + self.assertEqual(morphologies[0], morphologies[1], f"{morphologies[0].ewkt} - {morphologies[1].ewkt}", ) self.assertEqual(status[0], status[1]) From 344aef84b763cde419f52f40c1426a19e195ba2e Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 28 Feb 2024 10:30:12 +0100 Subject: [PATCH 049/114] use internal triggers --- georiviere/main/apps.py | 1 + georiviere/main/signals.py | 3 ++- georiviere/settings/__init__.py | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/georiviere/main/apps.py b/georiviere/main/apps.py index 3cb9f0b9..8e84238c 100644 --- a/georiviere/main/apps.py +++ b/georiviere/main/apps.py @@ -15,5 +15,6 @@ def ready(self): from georiviere.river.models import Stream for model in apps.get_models(): if issubclass(model, MapEntityMixin) and model != Stream: + print("signal for model {}".format(model)) post_save.connect(signals.save_objects_generate_distance_to_source, sender=model) post_delete.connect(signals.delete_objects_remove_distance_to_source, sender=model) diff --git a/georiviere/main/signals.py b/georiviere/main/signals.py index f10be4a8..e2c2aa74 100644 --- a/georiviere/main/signals.py +++ b/georiviere/main/signals.py @@ -45,7 +45,8 @@ def save_objects_generate_distance_to_source(sender, instance, **kwargs): ).exclude(stream__in=streams).delete() elif hasattr(instance, 'topology'): - stream = annotate_distance_to_source(Stream.objects.all(), instance).get(pk=instance.topology.stream.pk) + raise Exception("yes") + stream = annotate_distance_to_source(Stream.objects.all(), instance).get(pk=instance.topology.stream_id) DistanceToSource.objects.update_or_create( object_id=instance.pk, content_type=ContentType.objects.get_for_model(instance._meta.model), diff --git a/georiviere/settings/__init__.py b/georiviere/settings/__init__.py index 56221baf..4daf8d0f 100644 --- a/georiviere/settings/__init__.py +++ b/georiviere/settings/__init__.py @@ -135,7 +135,6 @@ def construct_relative_path_mock(current_template_name, relative_name): 'embed_video', 'djgeojson', 'django_filters', - #'pgtrigger', 'compressor', 'mapentity', # mapentity should be placed after app declaring paperclip models 'leaflet', From 753d89fc8f4a1ee7417832c35032270274cfd3bf Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 28 Feb 2024 10:56:50 +0100 Subject: [PATCH 050/114] use internal triggers --- georiviere/main/signals.py | 1 - 1 file changed, 1 deletion(-) diff --git a/georiviere/main/signals.py b/georiviere/main/signals.py index e2c2aa74..758e1bf2 100644 --- a/georiviere/main/signals.py +++ b/georiviere/main/signals.py @@ -45,7 +45,6 @@ def save_objects_generate_distance_to_source(sender, instance, **kwargs): ).exclude(stream__in=streams).delete() elif hasattr(instance, 'topology'): - raise Exception("yes") stream = annotate_distance_to_source(Stream.objects.all(), instance).get(pk=instance.topology.stream_id) DistanceToSource.objects.update_or_create( object_id=instance.pk, From e1c97497ec6431d780b5b4e75ffc5ba51dffab8e Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Thu, 11 Apr 2024 09:54:39 +0200 Subject: [PATCH 051/114] fix command --- georiviere/contribution/schema.py | 79 ++++++++++--------- .../river/management/commands/load_rivers.py | 19 +++-- georiviere/river/managers.py | 6 ++ georiviere/river/models.py | 20 +---- georiviere/utils/__init__.py | 0 georiviere/utils/mixins/managers.py | 7 ++ 6 files changed, 70 insertions(+), 61 deletions(-) create mode 100644 georiviere/river/managers.py create mode 100644 georiviere/utils/__init__.py create mode 100644 georiviere/utils/mixins/managers.py diff --git a/georiviere/contribution/schema.py b/georiviere/contribution/schema.py index 0c85d301..1315881c 100644 --- a/georiviere/contribution/schema.py +++ b/georiviere/contribution/schema.py @@ -7,9 +7,9 @@ InvasiveSpecies, HeritageSpecies, HeritageObservation, FishSpecies, NaturePollution, TypePollution) -# The json schema is summarize on : +# The json schema is summarized on : # https://github.com/Georiviere/Georiviere-admin/issues/139 -# Depending of the category and type of the contributions, some fields are available or not. +# Depending on the category and type of the contributions, some fields are available or not. # Here is the generation of the json schema used by the website portal. # The fields available depending on the type of contributions follow the documentation of jsonschema : # https://json-schema.org/understanding-json-schema/reference/conditionals.html @@ -18,37 +18,43 @@ def get_contribution_properties(): """ Feature properties as form initial data format (name / value) """ # TODO: Use directly field definition for type / title / max length - results = {'name_author': { - 'type': "string", - 'title': _("Name author"), - "maxLength": 128 - }, 'first_name_author': { - 'type': "string", - 'title': _("First name author"), - "maxLength": 128 - }, 'email_author': { - 'type': "string", - 'title': _("Email"), - 'format': "email" - }, 'date_observation': { - 'type': "string", - 'title': _("Observation's date"), - 'format': 'date' - }, 'description': { - 'type': "string", - 'title': _('Description') - }, 'category': { - "type": "string", - "title": _("Category"), - # TODO: Loop on contribution one to one field to get all possibilities - "enum": [ - str(ContributionQuantity._meta.verbose_name.title()), - str(ContributionQuality._meta.verbose_name.title()), - str(ContributionFaunaFlora._meta.verbose_name.title()), - str(ContributionLandscapeElements._meta.verbose_name.title()), - str(ContributionPotentialDamage._meta.verbose_name.title()) - ], - } + results = { + 'name_author': { + 'type': "string", + 'title': _("Name author"), + "maxLength": 128 + }, + 'first_name_author': { + 'type': "string", + 'title': _("First name author"), + "maxLength": 128 + }, + 'email_author': { + 'type': "string", + 'title': _("Email"), + 'format': "email" + }, + 'date_observation': { + 'type': "string", + 'title': _("Observation's date"), + 'format': 'date' + }, + 'description': { + 'type': "string", + 'title': _('Description') + }, + 'category': { + "type": "string", + "title": _("Category"), + # TODO: Loop on contribution one to one field to get all possibilities + "enum": [ + str(ContributionQuantity._meta.verbose_name.title()), + str(ContributionQuality._meta.verbose_name.title()), + str(ContributionFaunaFlora._meta.verbose_name.title()), + str(ContributionLandscapeElements._meta.verbose_name.title()), + str(ContributionPotentialDamage._meta.verbose_name.title()) + ], + } } if SeverityType.objects.exists(): results['severity'] = { @@ -110,8 +116,7 @@ def get_disruptive_jam(choices, meta): 'jam_type': { 'type': "string", - 'title': str(meta.get_field( - 'jam_type').related_model._meta.verbose_name.capitalize()), + 'title': str(meta.get_field('jam_type').related_model._meta.verbose_name.capitalize()), 'enum': list(JamType.objects.values_list('label', flat=True)) } }, @@ -125,7 +130,9 @@ def get_bank_erosion(choices, meta): 'if': { 'properties': { 'type': { - 'const': str(choices.BANK_EROSION.label)}} + 'const': str(choices.BANK_EROSION.label) + } + } }, 'then': { 'properties': { diff --git a/georiviere/river/management/commands/load_rivers.py b/georiviere/river/management/commands/load_rivers.py index 07b1acca..905c4983 100644 --- a/georiviere/river/management/commands/load_rivers.py +++ b/georiviere/river/management/commands/load_rivers.py @@ -15,8 +15,10 @@ def add_arguments(self, parser): parser.add_argument('file_path', help="File's path to import.") parser.add_argument('--name-attribute', '-n', action='store', dest='name', default='nom', help="Attribute name in file to use as river name") - parser.add_argument('--flush-streams', '-f', action='store_true', dest='flush', default=False, - help="Flush current streams") + parser.add_argument('--flush', '-f', action='store_true', dest='flush', default=False, + help="Flush rivers before import.") + parser.add_argument('--batch-size', '-bs', action='store', dest='batch_size', default=50, + help="Size of batch to use for bulk_create. Default is 50.") parser.add_argument('--default-name-attribute', '-nd', action='store', dest='default_name', default=_('River'), help="Default name to use if attribute name specified is empty") @@ -25,17 +27,17 @@ def handle(self, *args, **options): name_column = options.get('name') default_name = options.get('default_name') flush = options.get('flush') + batch_size = options.get('batch_size') data_source = DataSource(file_path) layer = data_source[0] total_count = len(layer) + self.stdout.write(f"Load rivers: {total_count} features to import") if flush: self.stdout.write("Delete streams.....", ending="") - Stream.objects.all().delete() + Stream.objects.truncate() self.stdout.write(self.style.SUCCESS("done!")) - batch_size = 50 - objs = (Stream(geom=feat.geom.geos, source_location=Point(feat.geom.geos[0]), name=feat.get(name_column) or default_name) for feat in layer) @@ -46,7 +48,10 @@ def handle(self, *args, **options): if not batch: break self.stdout.write(f"{count} / {total_count}", ending="") - Stream.objects.bulk_create(batch, batch_size) - self.stdout.write(self.style.SUCCESS(" ok!")) + try: + Stream.objects.bulk_create(batch, batch_size) + self.stdout.write(self.style.SUCCESS(" ok!")) + except Exception as e: + self.stdout.write(self.style.ERROR(" error!")) self.stdout.write(self.style.SUCCESS(f"Successfully import {total_count} rivers and associated morphologies / status")) diff --git a/georiviere/river/managers.py b/georiviere/river/managers.py new file mode 100644 index 00000000..b3060af7 --- /dev/null +++ b/georiviere/river/managers.py @@ -0,0 +1,6 @@ +from django.db import models +from georiviere.utils.mixins.managers import TruncateManagerMixin + + +class RiverManager(TruncateManagerMixin, models.Manager): + pass diff --git a/georiviere/river/models.py b/georiviere/river/models.py index 4aa38376..af65cd85 100644 --- a/georiviere/river/models.py +++ b/georiviere/river/models.py @@ -25,6 +25,7 @@ from georiviere.observations.models import Station from georiviere.proceeding.models import Proceeding from georiviere.maintenance.models import Intervention +from georiviere.river.managers import RiverManager from georiviere.studies.models import Study from georiviere.watershed.mixins import WatershedPropertiesMixin @@ -90,7 +91,7 @@ class FlowChoices(models.IntegerChoices): portals = models.ManyToManyField('portal.Portal', blank=True, related_name='streams', verbose_name=_("Published portals")) - + objects = RiverManager() capture_map_image_waitfor = '.other_object_enum_loaded' class Meta: @@ -219,23 +220,6 @@ def __str__(self): class Meta: verbose_name = _("Topology") verbose_name_plural = _("Topologies") - # triggers = [ - # pgtrigger.Trigger( - # name="update_topology_geom", - # operation=pgtrigger.Update | pgtrigger.Insert, - # when=pgtrigger.After, - # declare=[('stream_geom', 'geometry')], - # func=""" - # SELECT r.geom FROM river_stream r WHERE NEW.stream_id = r.id INTO stream_geom; - # UPDATE description_morphology - # SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position) - # WHERE topology_id = NEW.id; - # UPDATE description_status - # SET geom = ST_LINESUBSTRING(stream_geom, NEW.start_position, NEW.end_position) - # WHERE topology_id = NEW.id; - # RETURN NEW; - # """ - # )] Study.add_property('stations', Station.within_buffer, _("Stations")) diff --git a/georiviere/utils/__init__.py b/georiviere/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/georiviere/utils/mixins/managers.py b/georiviere/utils/mixins/managers.py new file mode 100644 index 00000000..afb01020 --- /dev/null +++ b/georiviere/utils/mixins/managers.py @@ -0,0 +1,7 @@ +from django.db import connection + + +class TruncateManagerMixin: + def truncate(self): + with connection.cursor() as cursor: + cursor.execute('TRUNCATE TABLE "{0}" CASCADE'.format(self.model._meta.db_table)) From 4ecde5830fd85afd3aef83abf3b7360b4e69c147 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Thu, 11 Apr 2024 09:55:58 +0200 Subject: [PATCH 052/114] lint --- georiviere/river/management/commands/load_rivers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/georiviere/river/management/commands/load_rivers.py b/georiviere/river/management/commands/load_rivers.py index 905c4983..38a8668c 100644 --- a/georiviere/river/management/commands/load_rivers.py +++ b/georiviere/river/management/commands/load_rivers.py @@ -51,7 +51,7 @@ def handle(self, *args, **options): try: Stream.objects.bulk_create(batch, batch_size) self.stdout.write(self.style.SUCCESS(" ok!")) - except Exception as e: + except Exception: self.stdout.write(self.style.ERROR(" error!")) self.stdout.write(self.style.SUCCESS(f"Successfully import {total_count} rivers and associated morphologies / status")) From 480f98e9345ff78e8feafea2db39f4fcc300c400 Mon Sep 17 00:00:00 2001 From: Jean-Etienne Castagnede Date: Sat, 13 Apr 2024 16:13:17 +0200 Subject: [PATCH 053/114] Add custom contribution types (#244) * Configure custom contribution type in admin --- .github/workflows/ci.yml | 5 +- Dockerfile | 2 +- Makefile | 2 + dev-requirements.in | 2 +- dev-requirements.txt | 29 +- georiviere/contribution/admin.py | 95 +- georiviere/contribution/forms.py | 101 ++- .../locale/en/LC_MESSAGES/django.po | 130 ++- .../locale/fr/LC_MESSAGES/django.po | 121 ++- georiviere/contribution/managers.py | 7 - ...ibutiontype_customcontributiontypefield.py | 62 ++ .../migrations/0011_auto_20240411_0859.py | 23 + .../migrations/0012_auto_20240411_0921.py | 20 + ...ustomcontributiontype_linked_to_station.py | 18 + .../migrations/0014_auto_20240412_1505.py | 91 ++ .../migrations/0015_auto_20240412_2038.py | 25 + georiviere/contribution/models.py | 504 ----------- georiviere/contribution/models/__init__.py | 853 ++++++++++++++++++ georiviere/contribution/models/managers.py | 30 + georiviere/contribution/schema.py | 576 ++++++------ .../locale/en/LC_MESSAGES/django.po | 2 +- .../locale/fr/LC_MESSAGES/django.po | 2 +- .../locale/en/LC_MESSAGES/django.po | 2 +- .../locale/fr/LC_MESSAGES/django.po | 2 +- .../flatpages/locale/en/LC_MESSAGES/django.po | 2 +- .../flatpages/locale/fr/LC_MESSAGES/django.po | 2 +- .../knowledge/locale/en/LC_MESSAGES/django.po | 2 +- .../knowledge/locale/fr/LC_MESSAGES/django.po | 2 +- georiviere/main/apps.py | 1 - .../main/locale/en/LC_MESSAGES/django.po | 2 +- .../main/locale/fr/LC_MESSAGES/django.po | 2 +- .../locale/en/LC_MESSAGES/django.po | 2 +- .../locale/fr/LC_MESSAGES/django.po | 2 +- .../locale/en/LC_MESSAGES/django.po | 2 +- .../locale/fr/LC_MESSAGES/django.po | 2 +- .../portal/locale/en/LC_MESSAGES/django.po | 20 +- .../portal/locale/fr/LC_MESSAGES/django.po | 38 +- georiviere/portal/serializers/contribution.py | 146 ++- georiviere/portal/urls.py | 60 +- georiviere/portal/views/contribution.py | 159 +++- .../locale/en/LC_MESSAGES/django.po | 2 +- .../locale/fr/LC_MESSAGES/django.po | 2 +- .../river/locale/en/LC_MESSAGES/django.po | 2 +- .../river/locale/fr/LC_MESSAGES/django.po | 2 +- georiviere/settings/__init__.py | 6 + .../studies/locale/en/LC_MESSAGES/django.po | 2 +- .../studies/locale/fr/LC_MESSAGES/django.po | 2 +- .../locale/en/LC_MESSAGES/django.po | 2 +- .../locale/fr/LC_MESSAGES/django.po | 4 +- .../watershed/locale/en/LC_MESSAGES/django.po | 2 +- .../watershed/locale/fr/LC_MESSAGES/django.po | 2 +- requirements.in | 1 + requirements.txt | 3 + 53 files changed, 2141 insertions(+), 1039 deletions(-) create mode 100644 Makefile delete mode 100644 georiviere/contribution/managers.py create mode 100644 georiviere/contribution/migrations/0010_customcontribution_customcontributiontype_customcontributiontypefield.py create mode 100644 georiviere/contribution/migrations/0011_auto_20240411_0859.py create mode 100644 georiviere/contribution/migrations/0012_auto_20240411_0921.py create mode 100644 georiviere/contribution/migrations/0013_customcontributiontype_linked_to_station.py create mode 100644 georiviere/contribution/migrations/0014_auto_20240412_1505.py create mode 100644 georiviere/contribution/migrations/0015_auto_20240412_2038.py delete mode 100644 georiviere/contribution/models.py create mode 100644 georiviere/contribution/models/__init__.py create mode 100644 georiviere/contribution/models/managers.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db461ff8..120bd7c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,11 +27,12 @@ jobs: uses: actions/setup-python@v5 with: python-version: 3.9 + - name: Install dependencies run: | echo "${{ github.event_name }}! ${{ github.event.action }}" - python -m pip install --upgrade pip - python -m pip install flake8 + python -m pip install flake8 -c dev-requirements.txt + - name: Lint with flake8 run: | flake8 georiviere diff --git a/Dockerfile b/Dockerfile index 1650b8a8..92da6a5a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -76,7 +76,7 @@ RUN apt-get update -qq && apt-get install -y -qq \ USER django RUN python3.9 -m venv /opt/venv -RUN /opt/venv/bin/pip install --no-cache-dir pip setuptools wheel -U +RUN /opt/venv/bin/pip install --no-cache-dir pip setuptools wheel -U # geotrek setup fix : it required django before being installed... TODO: fix it in geotrek setup.py RUN /opt/venv/bin/pip install --no-cache-dir django==2.2.* diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..00beb2b5 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +deps: + docker compose run --rm web bash -c "pip-compile -q && pip-compile -q dev-requirements.in" diff --git a/dev-requirements.in b/dev-requirements.in index 3266d7d6..b6b9985c 100644 --- a/dev-requirements.in +++ b/dev-requirements.in @@ -10,7 +10,7 @@ coverage django-debug-toolbar django-extensions factory-boy -flake8 # WARNING : CI always use last flake8 published version +flake8 # doc sphinx diff --git a/dev-requirements.txt b/dev-requirements.txt index eb2033f4..5b2b772e 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -4,7 +4,7 @@ # # pip-compile dev-requirements.in # -alabaster==0.7.13 +alabaster==0.7.16 # via sphinx asgiref==3.3.1 # via @@ -12,7 +12,7 @@ asgiref==3.3.1 # django babel==2.14.0 # via sphinx -build==1.0.3 +build==1.2.1 # via pip-tools certifi==2020.12.5 # via @@ -56,7 +56,7 @@ faker==9.7.1 # via # -c requirements.txt # factory-boy -flake8==6.1.0 +flake8==7.0.0 # via -r dev-requirements.in freezegun==1.1.0 # via @@ -68,7 +68,7 @@ idna==2.10 # requests imagesize==1.4.1 # via sphinx -importlib-metadata==7.0.0 +importlib-metadata==7.1.0 # via # build # sphinx @@ -91,7 +91,7 @@ pip-tools==6.10.0 # via -r dev-requirements.in pycodestyle==2.11.1 # via flake8 -pyflakes==3.1.0 +pyflakes==3.2.0 # via flake8 pygments==2.17.2 # via sphinx @@ -126,27 +126,22 @@ sphinx==5.1.1 # via # -r dev-requirements.in # sphinx-rtd-theme - # sphinxcontrib-applehelp - # sphinxcontrib-devhelp - # sphinxcontrib-htmlhelp # sphinxcontrib-jquery - # sphinxcontrib-qthelp - # sphinxcontrib-serializinghtml sphinx-rtd-theme==2.0.0 # via -r dev-requirements.in -sphinxcontrib-applehelp==1.0.7 +sphinxcontrib-applehelp==1.0.8 # via sphinx -sphinxcontrib-devhelp==1.0.5 +sphinxcontrib-devhelp==1.0.6 # via sphinx -sphinxcontrib-htmlhelp==2.0.4 +sphinxcontrib-htmlhelp==2.0.5 # via sphinx sphinxcontrib-jquery==4.1 # via sphinx-rtd-theme sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.6 +sphinxcontrib-qthelp==1.0.7 # via sphinx -sphinxcontrib-serializinghtml==1.1.9 +sphinxcontrib-serializinghtml==1.1.10 # via sphinx sqlparse==0.4.1 # via @@ -167,9 +162,9 @@ urllib3==1.26.3 # via # -c requirements.txt # requests -wheel==0.42.0 +wheel==0.43.0 # via pip-tools -zipp==3.17.0 +zipp==3.18.1 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: diff --git a/georiviere/contribution/admin.py b/georiviere/contribution/admin.py index a1b3d9ca..51d50b3e 100644 --- a/georiviere/contribution/admin.py +++ b/georiviere/contribution/admin.py @@ -1,19 +1,80 @@ +from admin_ordering.admin import OrderableAdmin from django.contrib import admin +from django.utils.translation import gettext_lazy as _ +from leaflet.admin import LeafletGeoAdmin -from georiviere.contribution.models import ( - SeverityType, LandingType, JamType, DiseaseType, DeadSpecies, InvasiveSpecies, HeritageSpecies, HeritageObservation, - FishSpecies, NaturePollution, TypePollution, ContributionStatus -) - -admin.site.register(ContributionStatus, admin.ModelAdmin) -admin.site.register(SeverityType, admin.ModelAdmin) -admin.site.register(LandingType, admin.ModelAdmin) -admin.site.register(JamType, admin.ModelAdmin) -admin.site.register(DiseaseType, admin.ModelAdmin) -admin.site.register(DeadSpecies, admin.ModelAdmin) -admin.site.register(InvasiveSpecies, admin.ModelAdmin) -admin.site.register(HeritageSpecies, admin.ModelAdmin) -admin.site.register(HeritageObservation, admin.ModelAdmin) -admin.site.register(FishSpecies, admin.ModelAdmin) -admin.site.register(NaturePollution, admin.ModelAdmin) -admin.site.register(TypePollution, admin.ModelAdmin) +from . import models, forms + +admin.site.register(models.ContributionStatus, admin.ModelAdmin) +admin.site.register(models.SeverityType, admin.ModelAdmin) +admin.site.register(models.LandingType, admin.ModelAdmin) +admin.site.register(models.JamType, admin.ModelAdmin) +admin.site.register(models.DiseaseType, admin.ModelAdmin) +admin.site.register(models.DeadSpecies, admin.ModelAdmin) +admin.site.register(models.InvasiveSpecies, admin.ModelAdmin) +admin.site.register(models.HeritageSpecies, admin.ModelAdmin) +admin.site.register(models.HeritageObservation, admin.ModelAdmin) +admin.site.register(models.FishSpecies, admin.ModelAdmin) +admin.site.register(models.NaturePollution, admin.ModelAdmin) +admin.site.register(models.TypePollution, admin.ModelAdmin) + + +class CustomFieldInline(OrderableAdmin, admin.TabularInline): + verbose_name = _('Field') + verbose_name_plural = _('Fields') + model = models.CustomContributionTypeField + ordering_field = "order" + ordering = ('order', 'label') + form = forms.CustomContributionFieldInlineForm + fields = ('label', 'value_type', 'required', 'help_text', 'order') + extra = 0 + show_change_link = True + popup_link = 'change' + + +@admin.register(models.CustomContributionType) +class CustomContributionTypeAdmin(admin.ModelAdmin): + list_display = ('label', ) + search_fields = ('label', ) + filter_horizontal = ('stations', ) + inlines = [CustomFieldInline, ] + + +@admin.register(models.CustomContributionTypeField) +class CustomContributionTypeFieldAdmin(admin.ModelAdmin): + list_display = ('label', 'key', 'value_type', 'required', 'custom_type') + list_filter = ('custom_type', 'value_type', 'required') + search_fields = ('label', 'key', 'custom_type__label') + form = forms.CustomContributionFieldForm + fieldsets = ( + (None, { + 'fields': ('custom_type', 'label', 'key', 'value_type', 'required', 'help_text',) + }), + (_('Customization'), { + 'fields': ('customization', 'options'), + }), + ) + + def get_readonly_fields(self, request, obj=None): + if obj and obj.pk: + return ['custom_type', 'key', 'options'] + return [] + + def has_add_permission(self, request): + """ Disable addition in list view """ + return False + + def has_delete_permission(self, request, obj=None): + """ Disable deletion in list view """ + return False + +@admin.register(models.CustomContribution) +class CustomContributionAdmin(LeafletGeoAdmin, admin.ModelAdmin): + list_display = ('custom_type', 'portal', 'validated', 'date_insert', 'date_update') + list_filter = ('custom_type', 'portal', 'validated') + form = forms.CustomContributionForm + + def get_readonly_fields(self, request, obj=None): + if not obj or not obj.pk: + return ('data', ) + return [] diff --git a/georiviere/contribution/forms.py b/georiviere/contribution/forms.py index 2403ed84..7d7a7f8a 100644 --- a/georiviere/contribution/forms.py +++ b/georiviere/contribution/forms.py @@ -1,25 +1,27 @@ from crispy_forms.layout import Div, Field from dal import autocomplete +from django import forms from django.utils.translation import gettext_lazy as _ - +from django_jsonform.forms.fields import JSONFormField from geotrek.common.forms import CommonForm -from georiviere.contribution.models import Contribution from georiviere.knowledge.models import FollowUp, Knowledge from georiviere.maintenance.models import Intervention +from . import models + class ContributionForm(autocomplete.FutureModelForm, CommonForm): - geomfields = ['geom'] + geomfields = ["geom"] linked_object = autocomplete.Select2GenericForeignKeyModelField( model_choice=[ # Get the values 'name' for each object of each models - (Knowledge, 'name'), - (Intervention, 'name'), - (FollowUp, 'name') + (Knowledge, "name"), + (Intervention, "name"), + (FollowUp, "name"), ], - label=_('Linked object'), + label=_("Linked object"), required=False, initial=None, ) @@ -34,26 +36,95 @@ class ContributionForm(autocomplete.FutureModelForm, CommonForm): "email_author", "assigned_user", "status_contribution", - Field('linked_object', css_class="chosen-select"), + Field("linked_object", css_class="chosen-select"), ) ] class Meta(CommonForm): - fields = ["description", "severity", "published", "portal", "email_author", "geom", "assigned_user", - "status_contribution", "validated", "linked_object"] - model = Contribution + fields = [ + "description", + "severity", + "published", + "portal", + "email_author", + "geom", + "assigned_user", + "status_contribution", + "validated", + "linked_object", + ] + model = models.Contribution def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['portal'].widget.attrs['readonly'] = True - self.fields['geom'].widget.modifiable = False - self.fields['email_author'].widget.attrs['readonly'] = True + self.fields["portal"].widget.attrs["readonly"] = True + self.fields["geom"].widget.modifiable = False + self.fields["email_author"].widget.attrs["readonly"] = True def clean_portal(self): return self.instance.portal def clean_linked_object(self): - linked_object = self.cleaned_data['linked_object'] + linked_object = self.cleaned_data["linked_object"] if linked_object == "": return None return linked_object + + +class CustomContributionForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["station"].disabled = True + + if self.instance.pk: + schema = self.instance.custom_type.get_json_schema_form() + self.fields["data"] = JSONFormField(schema=schema, label=_("Data")) + + stations = self.instance.custom_type.stations.all() + self.fields["station"].queryset = stations + + if stations.exists(): + self.fields["station"].disabled = False + self.fields["station"].required = True + + class Meta: + model = models.CustomContribution + fields = "__all__" + + +class CustomContributionFieldForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance.pk: + schema = self.instance.get_customization_json_schema_form() + self.fields["customization"] = JSONFormField( + schema=schema, required=False, label=_("Customization") + ) + # self.fields["value_type"].choices = [ + # (self.instance.value_type, self.instance.get_value_type_display()) + # ] + + class Meta: + model = models.CustomContributionTypeField + fields = ( + "custom_type", + "label", + "value_type", + "required", + "help_text", + "customization", + ) + + +class CustomContributionFieldInlineForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance.pk: + self.fields["value_type"].disabled = True + self.fields["value_type"].help_text = _( + "You can't change value type after creation. Delete and/or create another one." + ) + + class Meta: + model = models.CustomContributionTypeField + fields = ("label", "value_type", "required", "help_text", "order") diff --git a/georiviere/contribution/locale/en/LC_MESSAGES/django.po b/georiviere/contribution/locale/en/LC_MESSAGES/django.po index 34fffc62..c14dde9a 100644 --- a/georiviere/contribution/locale/en/LC_MESSAGES/django.po +++ b/georiviere/contribution/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:15+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,6 +18,15 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +msgid "Field" +msgstr "" + +msgid "Fields" +msgstr "" + +msgid "Customization" +msgstr "" + msgid "Contribution" msgstr "" @@ -45,6 +54,13 @@ msgstr "" msgid "Linked object" msgstr "" +msgid "Data" +msgstr "" + +msgid "" +"You can't change value type after creation. Delete and/or create another one." +msgstr "" + msgid "Label" msgstr "" @@ -109,10 +125,10 @@ msgstr "" msgid "Feedback from {email}" msgstr "" -msgid "Contribution potential damage type" +msgid "Landing type" msgstr "" -msgid "Contribution potential damage types" +msgid "Landing types" msgstr "" msgid "Jam type" @@ -157,9 +173,6 @@ msgstr "" msgid "Trampling by livestock (impacting)" msgstr "" -msgid "Landing type" -msgstr "" - msgid "Excessive cutting length (in meters)" msgstr "" @@ -318,6 +331,111 @@ msgstr "" msgid "Knowledge" msgstr "" +msgid "Stations" +msgstr "" + +msgid "Custom contribution type" +msgstr "" + +msgid "Custom contribution types" +msgstr "" + +msgid "String" +msgstr "" + +msgid "Text" +msgstr "" + +msgid "Integer" +msgstr "" + +msgid "Float" +msgstr "" + +msgid "Date" +msgstr "" + +msgid "Datetime" +msgstr "" + +msgid "Boolean" +msgstr "" + +msgid "Field label." +msgstr "" + +msgid "Key" +msgstr "" + +msgid "Key used in JSON data field." +msgstr "" + +msgid "Required" +msgstr "" + +msgid "Set if field is required to validate form." +msgstr "" + +msgid "Help text" +msgstr "" + +msgid "Set a help text for the field." +msgstr "" + +msgid "Options" +msgstr "" + +msgid "Internal options for type JSON schema." +msgstr "" + +msgid "Field customization." +msgstr "" + +msgid "Order" +msgstr "" + +msgid "Order of field in form." +msgstr "" + +msgid "Custom contribution type." +msgstr "" + +msgid "Values" +msgstr "" + +msgid "Placeholder" +msgstr "" + +msgid "Min. length" +msgstr "" + +msgid "Max. length" +msgstr "" + +msgid "Min. value" +msgstr "" + +msgid "Max. value" +msgstr "" + +msgid "Custom contribution type field" +msgstr "" + +msgid "Custom contribution type fields" +msgstr "" + +msgid "Custom contribution" +msgstr "" + +msgid "Custom contributions" +msgstr "" + +msgid "Observed species" +msgstr "" + +msgid "Observation type" +msgstr "" + msgid "None" msgstr "" diff --git a/georiviere/contribution/locale/fr/LC_MESSAGES/django.po b/georiviere/contribution/locale/fr/LC_MESSAGES/django.po index 9e554886..600c04fb 100644 --- a/georiviere/contribution/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/contribution/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-11-10 12:01+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,6 +18,15 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +msgid "Field" +msgstr "Champ" + +msgid "Fields" +msgstr "Champs" + +msgid "Customization" +msgstr "Personnalisation" + msgid "Contribution" msgstr "Contribution" @@ -45,6 +54,15 @@ msgstr "Type" msgid "Linked object" msgstr "Objet lié" +msgid "Data" +msgstr "Données" + +msgid "" +"You can't change value type after creation. Delete and/or create another one." +msgstr "" +"Vous ne pouvez pas changer le type de valeur après la création. Supprimez et/" +"ou créez-en un autre." + msgid "Label" msgstr "Label" @@ -211,7 +229,7 @@ msgid "Number heritage species" msgstr "Nombre d'individus patrimoniale" msgid "Number fish species" -msgstr "Nombre d'espèces pisicole" +msgstr "Nombre d'espèces piscicole" msgid "Contribution fauna-flora" msgstr "Contribution faune-flore" @@ -315,6 +333,105 @@ msgstr "Contentieux" msgid "Knowledge" msgstr "Connaissance" +msgid "Stations" +msgstr "Stations" + +msgid "Custom contribution type" +msgstr "Type de Contribution personnalisée" + +msgid "Custom contribution types" +msgstr "Types de Contributions personnalisées" + +msgid "String" +msgstr "Chaîne de caractères" + +msgid "Text" +msgstr "Texte" + +msgid "Integer" +msgstr "Nombre entier" + +msgid "Float" +msgstr "Nombre réel" + +msgid "Date" +msgstr "Date" + +msgid "Datetime" +msgstr "Date / Heure" + +msgid "Boolean" +msgstr "Booléen" + +msgid "Field label." +msgstr "Libellé du champ." + +msgid "Key" +msgstr "Clé" + +msgid "Key used in JSON data field." +msgstr "Clé utilisée dans les données JSON." + +msgid "Required" +msgstr "Requis" + +msgid "Set if field is required to validate form." +msgstr "Définir si le champ est requis pour valider le formulaire." + +msgid "Help text" +msgstr "Texte d'aide" + +msgid "Set a help text for the field." +msgstr "Définir un texte d'aide pour le champ." + +msgid "Options" +msgstr "Options" + +msgid "Internal options for type JSON schema." +msgstr "Options interne du schéma JSON du type." + +msgid "Field customization." +msgstr "Personnalisation du champ" + +msgid "Order" +msgstr "Ordre" + +msgid "Order of field in form." +msgstr "Ordre du champ dans le formulaire." + +msgid "Custom contribution type." +msgstr "Type de Contribution personnalisée." + +msgid "Values" +msgstr "Valeurs" + +msgid "Placeholder" +msgstr "Remplissage" + +msgid "Min. length" +msgstr "Longueur mini." + +msgid "Max. length" +msgstr "Longueur maxi." + +msgid "Min. value" +msgstr "Valeur mini." + +msgid "Max. value" +msgstr "Valeur maxi." + +msgid "Custom contribution type field" +msgstr "Champ de type de contribution personnalisée" + +msgid "Custom contribution type fields" +msgstr "Champs de types de contributions personnalisées" + +msgid "Custom contribution" +msgstr "Contribution personnalisée" + +msgid "Custom contributions" +msgstr "Contributions personnalisées" + msgid "Observed species" msgstr "Espèce observée" diff --git a/georiviere/contribution/managers.py b/georiviere/contribution/managers.py deleted file mode 100644 index f2485506..00000000 --- a/georiviere/contribution/managers.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.contrib.gis.db import models - - -class SelectableUserManager(models.Manager): - - def get_queryset(self): - return super().get_queryset().filter(userprofile__isnull=False) diff --git a/georiviere/contribution/migrations/0010_customcontribution_customcontributiontype_customcontributiontypefield.py b/georiviere/contribution/migrations/0010_customcontribution_customcontributiontype_customcontributiontypefield.py new file mode 100644 index 00000000..df769db4 --- /dev/null +++ b/georiviere/contribution/migrations/0010_customcontribution_customcontributiontype_customcontributiontypefield.py @@ -0,0 +1,62 @@ +# Generated by Django 3.1.14 on 2024-04-11 08:30 + +import django.contrib.gis.db.models.fields +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('portal', '0006_auto_20230915_1756'), + ('contribution', '0009_auto_20231110_1214'), + ] + + operations = [ + migrations.CreateModel( + name='CustomContributionType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('label', models.CharField(max_length=128, unique=True, verbose_name='Label')), + ], + options={ + 'verbose_name': 'Custom contribution type', + 'verbose_name_plural': 'Custom contribution types', + }, + ), + migrations.CreateModel( + name='CustomContribution', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date_insert', models.DateTimeField(auto_now_add=True, verbose_name='Insertion date')), + ('date_update', models.DateTimeField(auto_now=True, db_index=True, verbose_name='Update date')), + ('geom', django.contrib.gis.db.models.fields.GeometryField(srid=2154)), + ('properties', models.JSONField(blank=True, default=dict)), + ('validated', models.BooleanField(default=False, verbose_name='Validated')), + ('custom_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='contributions', to='contribution.customcontributiontype')), + ('portal', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.PROTECT, related_name='custom_contributions', to='portal.portal', verbose_name='Portal')), + ], + options={ + 'verbose_name': 'Custom contribution', + 'verbose_name_plural': 'Custom contributions', + }, + ), + migrations.CreateModel( + name='CustomContributionTypeField', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('label', models.CharField(max_length=128, verbose_name='Label')), + ('key', models.SlugField(editable=False, help_text='Key used in JSON field', max_length=150, verbose_name='Type')), + ('value_type', models.CharField(choices=[('text', 'Text'), ('integer', 'Integer'), ('float', 'Float'), ('date', 'Date'), ('datetime', 'Datetime'), ('boolean', 'Boolean')], default='text', max_length=16, verbose_name='Label')), + ('required', models.BooleanField(default=False, verbose_name='Required')), + ('options', models.JSONField(blank=True, default=dict)), + ('order', models.PositiveSmallIntegerField(default=0, verbose_name='Order')), + ('custom_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='contribution.customcontributiontype')), + ], + options={ + 'verbose_name': 'Custom contribution type field', + 'verbose_name_plural': 'Custom contribution type fields', + 'unique_together': {('label', 'custom_type')}, + }, + ), + ] diff --git a/georiviere/contribution/migrations/0011_auto_20240411_0859.py b/georiviere/contribution/migrations/0011_auto_20240411_0859.py new file mode 100644 index 00000000..eee0e1b3 --- /dev/null +++ b/georiviere/contribution/migrations/0011_auto_20240411_0859.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2024-04-11 08:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contribution', '0010_customcontribution_customcontributiontype_customcontributiontypefield'), + ] + + operations = [ + migrations.AlterField( + model_name='customcontributiontypefield', + name='key', + field=models.SlugField(editable=False, help_text='Key used in JSON field', max_length=150, verbose_name='Key'), + ), + migrations.AlterField( + model_name='customcontributiontypefield', + name='value_type', + field=models.CharField(choices=[('text', 'Text'), ('integer', 'Integer'), ('float', 'Float'), ('date', 'Date'), ('datetime', 'Datetime'), ('boolean', 'Boolean')], default='text', max_length=16, verbose_name='Type'), + ), + ] diff --git a/georiviere/contribution/migrations/0012_auto_20240411_0921.py b/georiviere/contribution/migrations/0012_auto_20240411_0921.py new file mode 100644 index 00000000..c39ed157 --- /dev/null +++ b/georiviere/contribution/migrations/0012_auto_20240411_0921.py @@ -0,0 +1,20 @@ +# Generated by Django 3.1.14 on 2024-04-11 09:21 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('portal', '0006_auto_20230915_1756'), + ('contribution', '0011_auto_20240411_0859'), + ] + + operations = [ + migrations.AlterField( + model_name='customcontribution', + name='portal', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='custom_contributions', to='portal.portal', verbose_name='Portal'), + ), + ] diff --git a/georiviere/contribution/migrations/0013_customcontributiontype_linked_to_station.py b/georiviere/contribution/migrations/0013_customcontributiontype_linked_to_station.py new file mode 100644 index 00000000..a78c1113 --- /dev/null +++ b/georiviere/contribution/migrations/0013_customcontributiontype_linked_to_station.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.14 on 2024-04-11 09:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contribution', '0012_auto_20240411_0921'), + ] + + operations = [ + migrations.AddField( + model_name='customcontributiontype', + name='linked_to_station', + field=models.BooleanField(default=False, verbose_name='Linked to station'), + ), + ] diff --git a/georiviere/contribution/migrations/0014_auto_20240412_1505.py b/georiviere/contribution/migrations/0014_auto_20240412_1505.py new file mode 100644 index 00000000..96c16dc9 --- /dev/null +++ b/georiviere/contribution/migrations/0014_auto_20240412_1505.py @@ -0,0 +1,91 @@ +# Generated by Django 3.1.14 on 2024-04-12 15:05 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('observations', '0023_auto_20230220_1703'), + ('contribution', '0013_customcontributiontype_linked_to_station'), + ] + + operations = [ + migrations.AlterModelOptions( + name='customcontributiontypefield', + options={'ordering': ('order', 'custom_type'), 'verbose_name': 'Custom contribution type field', 'verbose_name_plural': 'Custom contribution type fields'}, + ), + migrations.RemoveField( + model_name='customcontribution', + name='properties', + ), + migrations.RemoveField( + model_name='customcontributiontype', + name='linked_to_station', + ), + migrations.AddField( + model_name='customcontribution', + name='data', + field=models.JSONField(blank=True, default=dict, verbose_name='Data'), + ), + migrations.AddField( + model_name='customcontributiontype', + name='stations', + field=models.ManyToManyField(blank=True, to='observations.Station', verbose_name='Stations'), + ), + migrations.AddField( + model_name='customcontributiontypefield', + name='customization', + field=models.JSONField(blank=True, default=dict, help_text='Field customization.', verbose_name='Customization'), + ), + migrations.AddField( + model_name='customcontributiontypefield', + name='help_text', + field=models.CharField(blank=True, default='', help_text='Set a help text for the field.', max_length=256, verbose_name='Help text'), + ), + migrations.AlterField( + model_name='customcontribution', + name='custom_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='contributions', to='contribution.customcontributiontype', verbose_name='Custom contribution type'), + ), + migrations.AlterField( + model_name='customcontributiontypefield', + name='custom_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='contribution.customcontributiontype', verbose_name='Custom contribution type.'), + ), + migrations.AlterField( + model_name='customcontributiontypefield', + name='key', + field=models.SlugField(editable=False, help_text='Key used in JSON data field.', max_length=150, verbose_name='Key'), + ), + migrations.AlterField( + model_name='customcontributiontypefield', + name='label', + field=models.CharField(help_text='Field label.', max_length=128, verbose_name='Label'), + ), + migrations.AlterField( + model_name='customcontributiontypefield', + name='options', + field=models.JSONField(blank=True, default=dict, editable=False, help_text='Internal options for type JSON schema.', verbose_name='Options'), + ), + migrations.AlterField( + model_name='customcontributiontypefield', + name='order', + field=models.PositiveSmallIntegerField(default=0, help_text='Order of field in form.', verbose_name='Order'), + ), + migrations.AlterField( + model_name='customcontributiontypefield', + name='required', + field=models.BooleanField(default=False, help_text='Set if field is required to validate form.', verbose_name='Required'), + ), + migrations.AlterField( + model_name='customcontributiontypefield', + name='value_type', + field=models.CharField(choices=[('string', 'String'), ('text', 'Text'), ('integer', 'Integer'), ('float', 'Float'), ('date', 'Date'), ('datetime', 'Datetime'), ('boolean', 'Boolean')], default='text', max_length=16, verbose_name='Type'), + ), + migrations.AlterIndexTogether( + name='customcontributiontypefield', + index_together={('order', 'custom_type')}, + ), + ] diff --git a/georiviere/contribution/migrations/0015_auto_20240412_2038.py b/georiviere/contribution/migrations/0015_auto_20240412_2038.py new file mode 100644 index 00000000..324bb9b6 --- /dev/null +++ b/georiviere/contribution/migrations/0015_auto_20240412_2038.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1.14 on 2024-04-12 20:38 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('observations', '0023_auto_20230220_1703'), + ('contribution', '0014_auto_20240412_1505'), + ] + + operations = [ + migrations.AddField( + model_name='customcontribution', + name='station', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='custom_contributions', to='observations.station', verbose_name='Station'), + ), + migrations.AlterField( + model_name='customcontributiontypefield', + name='value_type', + field=models.CharField(choices=[('string', 'String'), ('text', 'Text'), ('integer', 'Integer'), ('float', 'Float'), ('date', 'Date'), ('datetime', 'Datetime'), ('boolean', 'Boolean')], default='string', max_length=16, verbose_name='Type'), + ), + ] diff --git a/georiviere/contribution/models.py b/georiviere/contribution/models.py deleted file mode 100644 index 3009a5a4..00000000 --- a/georiviere/contribution/models.py +++ /dev/null @@ -1,504 +0,0 @@ -import logging - -from django.conf import settings -from django.contrib.auth.models import User -from django.contrib.contenttypes.fields import GenericForeignKey -from django.contrib.contenttypes.models import ContentType -from django.contrib.gis.db import models -from django.core.mail import mail_managers -from django.template.loader import render_to_string -from django.utils.translation import gettext_lazy as _ - -from mapentity.models import MapEntityMixin - -from geotrek.common.mixins import TimeStampedModelMixin, BasePublishableMixin -from geotrek.common.utils import classproperty -from geotrek.zoning.mixins import ZoningPropertiesMixin - -from georiviere.contribution.managers import SelectableUserManager -from georiviere.description.models import Status, Morphology, Usage -from georiviere.river.models import Stream -from georiviere.knowledge.models import Knowledge -from georiviere.main.models import AddPropertyBufferMixin -from georiviere.observations.models import Station -from georiviere.proceeding.models import Proceeding -from georiviere.studies.models import Study -from georiviere.watershed.mixins import WatershedPropertiesMixin - -logger = logging.getLogger(__name__) - - -class SeverityType(models.Model): - label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) - - class Meta: - verbose_name = _("Severity type") - verbose_name_plural = _("Severity types") - - def __str__(self): - return self.label - - -class ContributionStatus(TimeStampedModelMixin, models.Model): - label = models.CharField(verbose_name=_("Status"), max_length=128) - - class Meta: - verbose_name = _("Status") - verbose_name_plural = _("Status") - - def __str__(self): - return self.label - - -class SelectableUser(User): - - objects = SelectableUserManager() - - class Meta: - proxy = True - - -def status_default(): - """Set status to New by default""" - new_status_query = ContributionStatus.objects.filter(label="Informé") - if new_status_query: - return new_status_query.get().pk - return None - - -class Contribution(BasePublishableMixin, TimeStampedModelMixin, WatershedPropertiesMixin, ZoningPropertiesMixin, - AddPropertyBufferMixin, MapEntityMixin): - """contribution model""" - geom = models.GeometryField(srid=settings.SRID, spatial_index=True) - name_author = models.CharField(max_length=128, verbose_name=_("Name author"), blank=True) - first_name_author = models.CharField(max_length=128, verbose_name=_("First name author"), blank=True) - email_author = models.EmailField(verbose_name=_("Email")) - date_observation = models.DateTimeField(editable=True, verbose_name=_("Observation's date")) - severity = models.ForeignKey(SeverityType, verbose_name=_("Severity"), on_delete=models.PROTECT, null=True, - blank=True, related_name='contributions') - description = models.TextField(verbose_name=_("Description"), help_text=_("Description of the contribution"), - blank=True) - published = models.BooleanField(verbose_name=_("Published"), default=False, - help_text=_("Make it visible on portal")) - portal = models.ForeignKey('portal.Portal', - verbose_name=_("Portal"), blank=True, related_name='contributions', - on_delete=models.PROTECT) - assigned_user = models.ForeignKey( - SelectableUser, - blank=True, - on_delete=models.PROTECT, - null=True, - verbose_name=_("Supervisor"), - related_name="contributions" - ) - status_contribution = models.ForeignKey( - "ContributionStatus", - on_delete=models.PROTECT, - null=True, - blank=True, - default=status_default, - verbose_name=_("Status"), - ) - validated = models.BooleanField(verbose_name=_("Validated"), default=False, - help_text=_("Validate the contribution")) - linked_object_type = models.ForeignKey(ContentType, null=True, on_delete=models.CASCADE) - linked_object_id = models.PositiveIntegerField(blank=True, null=True) - linked_object = GenericForeignKey('linked_object_type', 'linked_object_id') - - class Meta: - verbose_name = _("Contribution") - verbose_name_plural = _("Contributions") - - @classproperty - def category_verbose_name(cls): - return _("Category") - - @classproperty - def type_verbose_name(cls): - return _("Type") - - @classproperty - def linked_object_verbose_name(cls): - return _("Linked object") - - @property - def linked_object_model_name(self): - if self.linked_object: - return self.linked_object._meta.verbose_name - return None - - def __str__(self): - # Use the category and the type (One to One field) to generate what it will show on the list / detail / etc... - # It will generate like that : - # test@test.test Potential Damage Landing - if hasattr(self, 'potential_damage'): - return f'{self.email_author} {ContributionPotentialDamage._meta.verbose_name.title()} ' \ - f'{self.potential_damage.get_type_display()}' - elif hasattr(self, 'fauna_flora'): - return f'{self.email_author} {ContributionFaunaFlora._meta.verbose_name.title()} ' \ - f'{self.fauna_flora.get_type_display()}' - elif hasattr(self, 'quality'): - return f'{self.email_author} {ContributionQuality._meta.verbose_name.title()} ' \ - f'{self.quality.get_type_display()}' - elif hasattr(self, 'quantity'): - return f'{self.email_author} {ContributionQuantity._meta.verbose_name.title()} ' \ - f'{self.quantity.get_type_display()}' - elif hasattr(self, 'landscape_element'): - return f'{self.email_author} {ContributionLandscapeElements._meta.verbose_name.title()} ' \ - f'{self.landscape_element.get_type_display()}' - return f'{self.email_author}' - - @property - def category(self): - # The category is the reverse of the one to one fields : - # For example : - # Potential damage - if hasattr(self, 'potential_damage'): - return self.potential_damage - elif hasattr(self, 'fauna_flora'): - return self.fauna_flora - elif hasattr(self, 'quality'): - return self.quality - elif hasattr(self, 'quantity'): - return self.quantity - elif hasattr(self, 'landscape_element'): - return self.landscape_element - return _('No category') - - @property - def type(self): - if hasattr(self.category, 'get_type_display'): - return self.category.get_type_display() - return _('No type') - - @property - def category_display(self): - s = '%s' % (self.pk, - self.get_detail_url(), - self.category, - self.category) - if self.published: - s = ' ' % _("Published") + s - return s - - def send_report_to_managers(self, template_name="contribution/report_email.txt"): - # Send report to managers when a contribution has been created (MANAGERS settings) - subject = _("Feedback from {email}").format(email=self.email_author) - message = render_to_string(template_name, {"contribution": self}) - mail_managers(subject, message, fail_silently=False) - - def try_send_report_to_managers(self): - try: - self.send_report_to_managers() - except Exception as e: - logger.error("Email could not be sent to managers.") - logger.exception(e) # This sends an email to admins :) - - def save(self, *args, **kwargs): - super().save(*args, **kwargs) # Contribution updates should do nothing more - self.try_send_report_to_managers() - - -# Contributions has a category in the list : -# Potential damage -# Fauna flora -# Quality -# Quantity -# Landscape elements - -# Contributions has a type depending on its category -# Potential damage => Landing, Excessive cutting of riparian forest, Rockslides, Disruptive jam, Bank erosion -# River bed incision (sinking), Fish diseases (appearance of fish), Fish mortality, -# Trampling by livestock (impacting) -# Fauna flora => Invasive species, Heritage species, Fish species -# Quantity => Dry, In the process of drying out, Overflow -# Quality => Algal development, Pollution, Water temperature -# Landscape elements => Sinkhole, Fountain, Chasm, Lesine, Pond, Losing stream, Resurgence - -# Depending on its type of contribution, some fields are available or not. -# Everything is summarize on : -# https://github.com/Georiviere/Georiviere-admin/issues/139 - - -class LandingType(models.Model): - label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) - - class Meta: - verbose_name = _("Landing type") - verbose_name_plural = _("Landing types") - - def __str__(self): - return self.label - - -class JamType(models.Model): - label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) - - class Meta: - verbose_name = _("Jam type") - verbose_name_plural = _("Jam types") - - def __str__(self): - return self.label - - -class DiseaseType(models.Model): - label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) - - class Meta: - verbose_name = _("Disease type") - verbose_name_plural = _("Disease types") - - def __str__(self): - return self.label - - -class DeadSpecies(models.Model): - label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) - - class Meta: - verbose_name = _("Dead species") - verbose_name_plural = _("Dead species") - - def __str__(self): - return self.label - - -class ContributionPotentialDamage(models.Model): - - class TypeChoice(models.IntegerChoices): - """Choices for local influence""" - LANDING = 1, _('Landing') - EXCESSIVE_CUTTING_RIPARIAN_FOREST = 2, _('Excessive cutting of riparian forest') - ROCKSLIDES = 3, _('Rockslides') - DISRUPTIVE_JAM = 4, _('Disruptive jam') - BANK_EROSION = 5, _('Bank erosion') - RIVER_BED_INCISION = 6, _('River bed incision (sinking)') - FISH_DISEASES = 7, _('Fish diseases (appearance of fish)') - FISH_MORTALITY = 8, _('Fish mortality') - TRAMPLING_LIVESTOCK = 9, _('Trampling by livestock (impacting)') - - type = models.IntegerField( - null=False, - choices=TypeChoice.choices, - default=TypeChoice.LANDING, - verbose_name=_("Type"), - ) - landing_type = models.ForeignKey(LandingType, on_delete=models.PROTECT, null=True, - verbose_name=_("Landing type")) - excessive_cutting_length = models.FloatField(default=0.0, null=True, blank=True, - verbose_name=_("Excessive cutting length (in meters)")) - jam_type = models.ForeignKey(JamType, on_delete=models.PROTECT, null=True) - length_bank_erosion = models.FloatField(default=0.0, null=True, blank=True, - verbose_name=_("Length bank erosion (in meters)"), - help_text=_('Distance between the foot of the bank and the foot of ' - 'the erosion.')) - bank_height = models.FloatField(default=0.0, null=True, blank=True, - verbose_name=_("Bank height (in meters)"), - help_text=_('Bank height (measured between the foot of the bank and the top ' - 'of the bank) in meters')) - disease_type = models.ForeignKey(DiseaseType, on_delete=models.PROTECT, null=True) - number_death = models.IntegerField(default=0, null=True, blank=True, - verbose_name=_("Number death"), - help_text=_('Number of dead individuals')) - dead_species = models.ForeignKey(DeadSpecies, on_delete=models.PROTECT, null=True) - contribution = models.OneToOneField(Contribution, parent_link=True, on_delete=models.CASCADE, - related_name='potential_damage') - - class Meta: - verbose_name = _("Contribution potential damage") - verbose_name_plural = _("contributions potential damage") - - def __str__(self): - return f'{ContributionPotentialDamage._meta.verbose_name.title()} {self.get_type_display()}' - - -class InvasiveSpecies(models.Model): - label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) - - class Meta: - verbose_name = _("Invasive species") - verbose_name_plural = _("Invasive species") - - def __str__(self): - return self.label - - -class HeritageSpecies(models.Model): - label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) - - class Meta: - verbose_name = _("Heritage species") - verbose_name_plural = _("Heritage species") - - def __str__(self): - return self.label - - -class HeritageObservation(models.Model): - label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) - - class Meta: - verbose_name = _("Heritage observation") - verbose_name_plural = _("Heritage observations") - - def __str__(self): - return self.label - - -class FishSpecies(models.Model): - label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) - - class Meta: - verbose_name = _("Fish species") - verbose_name_plural = _("Fish species") - - def __str__(self): - return self.label - - -class ContributionFaunaFlora(models.Model): - - class TypeChoice(models.IntegerChoices): - """Choices for local influence""" - INVASIVE_SPECIES = 1, _('Invasive species') - HERITAGE_SPECIES = 2, _('Heritage species') - FISH_SPECIES = 3, _('Fish species') - - type = models.IntegerField( - null=False, - choices=TypeChoice.choices, - default=TypeChoice.INVASIVE_SPECIES, - verbose_name=_("Type"), - ) - home_area = models.FloatField(default=0.0, null=True, blank=True, - verbose_name=_("Home area (in square meters)"), - help_text=_('Home area in square meters')) - invasive_species = models.ForeignKey(InvasiveSpecies, on_delete=models.PROTECT, null=True) - number_heritage_species = models.IntegerField(default=0, null=True, blank=True, - verbose_name=_("Number heritage species")) - heritage_species = models.ForeignKey(HeritageSpecies, on_delete=models.PROTECT, null=True) - heritage_observation = models.ForeignKey(HeritageObservation, on_delete=models.PROTECT, null=True) - number_fish_species = models.IntegerField(default=0, null=True, blank=True, - verbose_name=_("Number fish species")) - fish_species = models.ForeignKey(FishSpecies, on_delete=models.PROTECT, null=True) - contribution = models.OneToOneField(Contribution, parent_link=True, on_delete=models.CASCADE, - related_name='fauna_flora') - - class Meta: - verbose_name = _("Contribution fauna-flora") - verbose_name_plural = _("contributions fauna-flora") - - def __str__(self): - return f'{ContributionFaunaFlora._meta.verbose_name.title()} {self.get_type_display()}' - - -class ContributionQuantity(models.Model): - class TypeChoice(models.IntegerChoices): - """Choices for local influence""" - DRY = 1, _('Dry') - PROCESS_DRYING_OUT = 2, _('In the process of drying out') - OVERFLOW = 3, _('Overflow') - - type = models.IntegerField( - null=False, - choices=TypeChoice.choices, - default=TypeChoice.DRY, - verbose_name=_("Water level type"), - ) - landmark = models.TextField(blank=True, verbose_name='Landmark') - contribution = models.OneToOneField(Contribution, parent_link=True, on_delete=models.CASCADE, - related_name='quantity') - - class Meta: - verbose_name = _("Contribution quantity") - verbose_name_plural = _("contributions quantity") - - def __str__(self): - return f'{ContributionQuantity._meta.verbose_name.title()} {self.get_type_display()}' - - -class NaturePollution(models.Model): - label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) - - class Meta: - verbose_name = _("Nature pollution") - verbose_name_plural = _("Natures pollution") - - def __str__(self): - return self.label - - -class TypePollution(models.Model): - label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) - - class Meta: - verbose_name = _("Type pollution") - verbose_name_plural = _("Types pollution") - - def __str__(self): - return self.label - - -class ContributionQuality(models.Model): - class TypeChoice(models.IntegerChoices): - """Choices for local influence""" - ALGAL_DEVELOPMENT = 1, _('Algal development') - POLLUTION = 2, _('Pollution') - WATER_TEMPERATURE = 3, _('Water temperature') - - type = models.IntegerField( - null=False, - choices=TypeChoice.choices, - default=TypeChoice.ALGAL_DEVELOPMENT, - verbose_name=_("Quality water type"), - ) - nature_pollution = models.ForeignKey(NaturePollution, on_delete=models.PROTECT, null=True) - type_pollution = models.ForeignKey(TypePollution, on_delete=models.PROTECT, null=True) - contribution = models.OneToOneField(Contribution, parent_link=True, on_delete=models.CASCADE, - related_name='quality') - - class Meta: - verbose_name = _("Contribution quality") - verbose_name_plural = _("contributions quality") - - def __str__(self): - return f'{ContributionQuality._meta.verbose_name.title()} {self.get_type_display()}' - - -class ContributionLandscapeElements(models.Model): - class TypeChoice(models.IntegerChoices): - """Choices for local influence""" - SINKHOLE = 1, _('Sinkhole') - FOUNTAIN = 2, _('Fountain') - CHASM = 3, _('Chasm') - LESINE = 4, _('Lesine') - POND = 5, _('Pond') - LOSING_STREAM = 6, _('Losing stream') - RESURGENCE = 7, _('Resurgence') - - type = models.IntegerField( - null=False, - choices=TypeChoice.choices, - default=TypeChoice.SINKHOLE, - verbose_name=_("Type"), - ) - contribution = models.OneToOneField(Contribution, parent_link=True, on_delete=models.CASCADE, - related_name='landscape_element') - - class Meta: - verbose_name = _("Contribution landscape element") - verbose_name_plural = _("contributions landscape elements") - - def __str__(self): - return f'{ContributionLandscapeElements._meta.verbose_name.title()} {self.get_type_display()}' - - -Contribution.add_property('streams', Stream.within_buffer, _("Stream")) -Contribution.add_property('status', Status.within_buffer, _("Status")) -Contribution.add_property('morphologies', Morphology.within_buffer, _("Morphologies")) -Contribution.add_property('usages', Usage.within_buffer, _("Usages")) -Contribution.add_property('stations', Station.within_buffer, _("Station")) -Contribution.add_property('studies', Study.within_buffer, _("Study")) -Contribution.add_property('proceedings', Proceeding.within_buffer, _("Proceeding")) -Contribution.add_property('knowledges', Knowledge.within_buffer, _("Knowledge")) diff --git a/georiviere/contribution/models/__init__.py b/georiviere/contribution/models/__init__.py new file mode 100644 index 00000000..c4b0b0ed --- /dev/null +++ b/georiviere/contribution/models/__init__.py @@ -0,0 +1,853 @@ +import logging + +from django.conf import settings +from django.contrib.auth.models import User +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.contrib.gis.db import models +from django.core.mail import mail_managers +from django.template.loader import render_to_string +from django.utils.text import slugify +from django.utils.translation import gettext_lazy as _ +from geotrek.common.mixins import BasePublishableMixin, TimeStampedModelMixin +from geotrek.common.utils import classproperty +from geotrek.zoning.mixins import ZoningPropertiesMixin +from mapentity.models import MapEntityMixin + +from georiviere.description.models import Morphology, Status, Usage +from georiviere.knowledge.models import Knowledge +from georiviere.main.models import AddPropertyBufferMixin +from georiviere.observations.models import Station +from georiviere.proceeding.models import Proceeding +from georiviere.river.models import Stream +from georiviere.studies.models import Study +from georiviere.watershed.mixins import WatershedPropertiesMixin +from .managers import SelectableUserManager, CustomContributionManager + +logger = logging.getLogger(__name__) + + +class SeverityType(models.Model): + label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) + + class Meta: + verbose_name = _("Severity type") + verbose_name_plural = _("Severity types") + + def __str__(self): + return self.label + + +class ContributionStatus(TimeStampedModelMixin, models.Model): + label = models.CharField(verbose_name=_("Status"), max_length=128) + + class Meta: + verbose_name = _("Status") + verbose_name_plural = _("Status") + + def __str__(self): + return self.label + + +class SelectableUser(User): + objects = SelectableUserManager() + + class Meta: + proxy = True + + +def status_default(): + """Set status to New by default""" + new_status_query = ContributionStatus.objects.filter(label="Informé") + if new_status_query: + return new_status_query.get().pk + return None + + +class Contribution( + BasePublishableMixin, + TimeStampedModelMixin, + WatershedPropertiesMixin, + ZoningPropertiesMixin, + AddPropertyBufferMixin, + MapEntityMixin, +): + """contribution model""" + + geom = models.GeometryField(srid=settings.SRID, spatial_index=True) + name_author = models.CharField( + max_length=128, verbose_name=_("Name author"), blank=True + ) + first_name_author = models.CharField( + max_length=128, verbose_name=_("First name author"), blank=True + ) + email_author = models.EmailField(verbose_name=_("Email")) + date_observation = models.DateTimeField( + editable=True, verbose_name=_("Observation's date") + ) + severity = models.ForeignKey( + SeverityType, + verbose_name=_("Severity"), + on_delete=models.PROTECT, + null=True, + blank=True, + related_name="contributions", + ) + description = models.TextField( + verbose_name=_("Description"), + help_text=_("Description of the contribution"), + blank=True, + ) + published = models.BooleanField( + verbose_name=_("Published"), + default=False, + help_text=_("Make it visible on portal"), + ) + portal = models.ForeignKey( + "portal.Portal", + verbose_name=_("Portal"), + blank=True, + related_name="contributions", + on_delete=models.PROTECT, + ) + assigned_user = models.ForeignKey( + SelectableUser, + blank=True, + on_delete=models.PROTECT, + null=True, + verbose_name=_("Supervisor"), + related_name="contributions", + ) + status_contribution = models.ForeignKey( + "ContributionStatus", + on_delete=models.PROTECT, + null=True, + blank=True, + default=status_default, + verbose_name=_("Status"), + ) + validated = models.BooleanField( + verbose_name=_("Validated"), + default=False, + help_text=_("Validate the contribution"), + ) + linked_object_type = models.ForeignKey( + ContentType, null=True, on_delete=models.CASCADE + ) + linked_object_id = models.PositiveIntegerField(blank=True, null=True) + linked_object = GenericForeignKey("linked_object_type", "linked_object_id") + + class Meta: + verbose_name = _("Contribution") + verbose_name_plural = _("Contributions") + + @classproperty + def category_verbose_name(cls): + return _("Category") + + @classproperty + def type_verbose_name(cls): + return _("Type") + + @classproperty + def linked_object_verbose_name(cls): + return _("Linked object") + + @property + def linked_object_model_name(self): + if self.linked_object: + return self.linked_object._meta.verbose_name + return None + + def __str__(self): + # Use the category and the type (One to One field) to generate what it will show on the list / detail / etc... + # It will generate like that : + # test@test.test Potential Damage Landing + if hasattr(self, "potential_damage"): + return ( + f"{self.email_author} {ContributionPotentialDamage._meta.verbose_name.title()} " + f"{self.potential_damage.get_type_display()}" + ) + elif hasattr(self, "fauna_flora"): + return ( + f"{self.email_author} {ContributionFaunaFlora._meta.verbose_name.title()} " + f"{self.fauna_flora.get_type_display()}" + ) + elif hasattr(self, "quality"): + return ( + f"{self.email_author} {ContributionQuality._meta.verbose_name.title()} " + f"{self.quality.get_type_display()}" + ) + elif hasattr(self, "quantity"): + return ( + f"{self.email_author} {ContributionQuantity._meta.verbose_name.title()} " + f"{self.quantity.get_type_display()}" + ) + elif hasattr(self, "landscape_element"): + return ( + f"{self.email_author} {ContributionLandscapeElements._meta.verbose_name.title()} " + f"{self.landscape_element.get_type_display()}" + ) + return f"{self.email_author}" + + @property + def category(self): + # The category is the reverse of the one to one fields : + # For example : + # Potential damage + if hasattr(self, "potential_damage"): + return self.potential_damage + elif hasattr(self, "fauna_flora"): + return self.fauna_flora + elif hasattr(self, "quality"): + return self.quality + elif hasattr(self, "quantity"): + return self.quantity + elif hasattr(self, "landscape_element"): + return self.landscape_element + return _("No category") + + @property + def type(self): + if hasattr(self.category, "get_type_display"): + return self.category.get_type_display() + return _("No type") + + @property + def category_display(self): + s = '%s' % ( + self.pk, + self.get_detail_url(), + self.category, + self.category, + ) + if self.published: + s = ( + ' ' + % _("Published") + + s + ) + return s + + def send_report_to_managers(self, template_name="contribution/report_email.txt"): + # Send report to managers when a contribution has been created (MANAGERS settings) + subject = _("Feedback from {email}").format(email=self.email_author) + message = render_to_string(template_name, {"contribution": self}) + mail_managers(subject, message, fail_silently=False) + + def try_send_report_to_managers(self): + try: + self.send_report_to_managers() + except Exception as e: + logger.error("Email could not be sent to managers.") + logger.exception(e) # This sends an email to admins :) + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) # Contribution updates should do nothing more + self.try_send_report_to_managers() + + +# Contributions has a category in the list : +# Potential damage +# Fauna flora +# Quality +# Quantity +# Landscape elements + +# Contributions has a type depending on its category +# Potential damage => Landing, Excessive cutting of riparian forest, Rockslides, Disruptive jam, Bank erosion +# River bed incision (sinking), Fish diseases (appearance of fish), Fish mortality, +# Trampling by livestock (impacting) +# Fauna flora => Invasive species, Heritage species, Fish species +# Quantity => Dry, In the process of drying out, Overflow +# Quality => Algal development, Pollution, Water temperature +# Landscape elements => Sinkhole, Fountain, Chasm, Lesine, Pond, Losing stream, Resurgence + +# Depending on its type of contribution, some fields are available or not. +# Everything is summarize on : +# https://github.com/Georiviere/Georiviere-admin/issues/139 + + +class LandingType(models.Model): + label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) + + class Meta: + verbose_name = _("Landing type") + verbose_name_plural = _("Landing types") + + def __str__(self): + return self.label + + +class JamType(models.Model): + label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) + + class Meta: + verbose_name = _("Jam type") + verbose_name_plural = _("Jam types") + + def __str__(self): + return self.label + + +class DiseaseType(models.Model): + label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) + + class Meta: + verbose_name = _("Disease type") + verbose_name_plural = _("Disease types") + + def __str__(self): + return self.label + + +class DeadSpecies(models.Model): + label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) + + class Meta: + verbose_name = _("Dead species") + verbose_name_plural = _("Dead species") + + def __str__(self): + return self.label + + +class ContributionPotentialDamage(models.Model): + class TypeChoice(models.IntegerChoices): + """Choices for local influence""" + + LANDING = 1, _("Landing") + EXCESSIVE_CUTTING_RIPARIAN_FOREST = 2, _("Excessive cutting of riparian forest") + ROCKSLIDES = 3, _("Rockslides") + DISRUPTIVE_JAM = 4, _("Disruptive jam") + BANK_EROSION = 5, _("Bank erosion") + RIVER_BED_INCISION = 6, _("River bed incision (sinking)") + FISH_DISEASES = 7, _("Fish diseases (appearance of fish)") + FISH_MORTALITY = 8, _("Fish mortality") + TRAMPLING_LIVESTOCK = 9, _("Trampling by livestock (impacting)") + + type = models.IntegerField( + null=False, + choices=TypeChoice.choices, + default=TypeChoice.LANDING, + verbose_name=_("Type"), + ) + landing_type = models.ForeignKey( + LandingType, on_delete=models.PROTECT, null=True, verbose_name=_("Landing type") + ) + excessive_cutting_length = models.FloatField( + default=0.0, + null=True, + blank=True, + verbose_name=_("Excessive cutting length (in meters)"), + ) + jam_type = models.ForeignKey(JamType, on_delete=models.PROTECT, null=True) + length_bank_erosion = models.FloatField( + default=0.0, + null=True, + blank=True, + verbose_name=_("Length bank erosion (in meters)"), + help_text=_( + "Distance between the foot of the bank and the foot of " "the erosion." + ), + ) + bank_height = models.FloatField( + default=0.0, + null=True, + blank=True, + verbose_name=_("Bank height (in meters)"), + help_text=_( + "Bank height (measured between the foot of the bank and the top " + "of the bank) in meters" + ), + ) + disease_type = models.ForeignKey(DiseaseType, on_delete=models.PROTECT, null=True) + number_death = models.IntegerField( + default=0, + null=True, + blank=True, + verbose_name=_("Number death"), + help_text=_("Number of dead individuals"), + ) + dead_species = models.ForeignKey(DeadSpecies, on_delete=models.PROTECT, null=True) + contribution = models.OneToOneField( + Contribution, + parent_link=True, + on_delete=models.CASCADE, + related_name="potential_damage", + ) + + class Meta: + verbose_name = _("Contribution potential damage") + verbose_name_plural = _("contributions potential damage") + + def __str__(self): + return f"{ContributionPotentialDamage._meta.verbose_name.title()} {self.get_type_display()}" + + +class InvasiveSpecies(models.Model): + label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) + + class Meta: + verbose_name = _("Invasive species") + verbose_name_plural = _("Invasive species") + + def __str__(self): + return self.label + + +class HeritageSpecies(models.Model): + label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) + + class Meta: + verbose_name = _("Heritage species") + verbose_name_plural = _("Heritage species") + + def __str__(self): + return self.label + + +class HeritageObservation(models.Model): + label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) + + class Meta: + verbose_name = _("Heritage observation") + verbose_name_plural = _("Heritage observations") + + def __str__(self): + return self.label + + +class FishSpecies(models.Model): + label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) + + class Meta: + verbose_name = _("Fish species") + verbose_name_plural = _("Fish species") + + def __str__(self): + return self.label + + +class ContributionFaunaFlora(models.Model): + class TypeChoice(models.IntegerChoices): + """Choices for local influence""" + + INVASIVE_SPECIES = 1, _("Invasive species") + HERITAGE_SPECIES = 2, _("Heritage species") + FISH_SPECIES = 3, _("Fish species") + + type = models.IntegerField( + null=False, + choices=TypeChoice.choices, + default=TypeChoice.INVASIVE_SPECIES, + verbose_name=_("Type"), + ) + home_area = models.FloatField( + default=0.0, + null=True, + blank=True, + verbose_name=_("Home area (in square meters)"), + help_text=_("Home area in square meters"), + ) + invasive_species = models.ForeignKey( + InvasiveSpecies, on_delete=models.PROTECT, null=True + ) + number_heritage_species = models.IntegerField( + default=0, null=True, blank=True, verbose_name=_("Number heritage species") + ) + heritage_species = models.ForeignKey( + HeritageSpecies, on_delete=models.PROTECT, null=True + ) + heritage_observation = models.ForeignKey( + HeritageObservation, on_delete=models.PROTECT, null=True + ) + number_fish_species = models.IntegerField( + default=0, null=True, blank=True, verbose_name=_("Number fish species") + ) + fish_species = models.ForeignKey(FishSpecies, on_delete=models.PROTECT, null=True) + contribution = models.OneToOneField( + Contribution, + parent_link=True, + on_delete=models.CASCADE, + related_name="fauna_flora", + ) + + class Meta: + verbose_name = _("Contribution fauna-flora") + verbose_name_plural = _("contributions fauna-flora") + + def __str__(self): + return f"{ContributionFaunaFlora._meta.verbose_name.title()} {self.get_type_display()}" + + +class ContributionQuantity(models.Model): + class TypeChoice(models.IntegerChoices): + """Choices for local influence""" + + DRY = 1, _("Dry") + PROCESS_DRYING_OUT = 2, _("In the process of drying out") + OVERFLOW = 3, _("Overflow") + + type = models.IntegerField( + null=False, + choices=TypeChoice.choices, + default=TypeChoice.DRY, + verbose_name=_("Water level type"), + ) + landmark = models.TextField(blank=True, verbose_name="Landmark") + contribution = models.OneToOneField( + Contribution, + parent_link=True, + on_delete=models.CASCADE, + related_name="quantity", + ) + + class Meta: + verbose_name = _("Contribution quantity") + verbose_name_plural = _("contributions quantity") + + def __str__(self): + return f"{ContributionQuantity._meta.verbose_name.title()} {self.get_type_display()}" + + +class NaturePollution(models.Model): + label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) + + class Meta: + verbose_name = _("Nature pollution") + verbose_name_plural = _("Natures pollution") + + def __str__(self): + return self.label + + +class TypePollution(models.Model): + label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) + + class Meta: + verbose_name = _("Type pollution") + verbose_name_plural = _("Types pollution") + + def __str__(self): + return self.label + + +class ContributionQuality(models.Model): + class TypeChoice(models.IntegerChoices): + """Choices for local influence""" + + ALGAL_DEVELOPMENT = 1, _("Algal development") + POLLUTION = 2, _("Pollution") + WATER_TEMPERATURE = 3, _("Water temperature") + + type = models.IntegerField( + null=False, + choices=TypeChoice.choices, + default=TypeChoice.ALGAL_DEVELOPMENT, + verbose_name=_("Quality water type"), + ) + nature_pollution = models.ForeignKey( + NaturePollution, on_delete=models.PROTECT, null=True + ) + type_pollution = models.ForeignKey( + TypePollution, on_delete=models.PROTECT, null=True + ) + contribution = models.OneToOneField( + Contribution, parent_link=True, on_delete=models.CASCADE, related_name="quality" + ) + + class Meta: + verbose_name = _("Contribution quality") + verbose_name_plural = _("contributions quality") + + def __str__(self): + return f"{ContributionQuality._meta.verbose_name.title()} {self.get_type_display()}" + + +class ContributionLandscapeElements(models.Model): + class TypeChoice(models.IntegerChoices): + """Choices for local influence""" + + SINKHOLE = 1, _("Sinkhole") + FOUNTAIN = 2, _("Fountain") + CHASM = 3, _("Chasm") + LESINE = 4, _("Lesine") + POND = 5, _("Pond") + LOSING_STREAM = 6, _("Losing stream") + RESURGENCE = 7, _("Resurgence") + + type = models.IntegerField( + null=False, + choices=TypeChoice.choices, + default=TypeChoice.SINKHOLE, + verbose_name=_("Type"), + ) + contribution = models.OneToOneField( + Contribution, + parent_link=True, + on_delete=models.CASCADE, + related_name="landscape_element", + ) + + class Meta: + verbose_name = _("Contribution landscape element") + verbose_name_plural = _("contributions landscape elements") + + def __str__(self): + return f"{ContributionLandscapeElements._meta.verbose_name.title()} {self.get_type_display()}" + + +Contribution.add_property("streams", Stream.within_buffer, _("Stream")) +Contribution.add_property("status", Status.within_buffer, _("Status")) +Contribution.add_property("morphologies", Morphology.within_buffer, _("Morphologies")) +Contribution.add_property("usages", Usage.within_buffer, _("Usages")) +Contribution.add_property("stations", Station.within_buffer, _("Station")) +Contribution.add_property("studies", Study.within_buffer, _("Study")) +Contribution.add_property("proceedings", Proceeding.within_buffer, _("Proceeding")) +Contribution.add_property("knowledges", Knowledge.within_buffer, _("Knowledge")) + + +## Custom Contribution + + +class CustomContributionType(models.Model): + label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) + stations = models.ManyToManyField('observations.Station', verbose_name=_("Stations"), blank=True) + + def __str__(self): + return self.label + + def get_json_schema_form(self): + linked_fields = self.fields.all() + fields = {} + for field in self.fields.all(): + fields[field.key] = field.get_field_schema() + return { + # "title": self.label, + "type": "object", + "properties": fields, + "required": [field.key for field in linked_fields if field.required], + } + + @property + def json_schema_form(self): + return self.get_json_schema_form() + + class Meta: + verbose_name = _("Custom contribution type") + verbose_name_plural = _("Custom contribution types") + ordering = ("label",) + + +class CustomContributionTypeField(models.Model): + class FieldTypeChoices(models.TextChoices): + """Choices for field type""" + + STRING = "string", _("String") + TEXT = "text", _("Text") + INTEGER = "integer", _("Integer") + FLOAT = "float", _("Float") + DATE = "date", _("Date") + DATETIME = "datetime", _("Datetime") + BOOLEAN = "boolean", _("Boolean") + + label = models.CharField(max_length=128, verbose_name=_("Label"), help_text=_("Field label.")) + key = models.SlugField( + max_length=150, + verbose_name=_("Key"), + help_text=_("Key used in JSON data field."), + editable=False, + ) + value_type = models.CharField( + max_length=16, + verbose_name=_("Type"), + choices=FieldTypeChoices.choices, + default=FieldTypeChoices.STRING, + ) + required = models.BooleanField(default=False, verbose_name=_("Required"), help_text=_("Set if field is required to validate form.")) + help_text = models.CharField( + max_length=256, verbose_name=_("Help text"), blank=True, default="", help_text=_("Set a help text for the field.") + ) + options = models.JSONField(null=False, blank=True, editable=False, verbose_name=_("Options"), default=dict, help_text=_("Internal options for type JSON schema.")) + customization = models.JSONField( + null=False, blank=True, default=dict, verbose_name=_("Customization"), help_text=_("Field customization.") + ) + order = models.PositiveSmallIntegerField(default=0, verbose_name=_("Order"), help_text=_("Order of field in form.")) + custom_type = models.ForeignKey( + CustomContributionType, on_delete=models.CASCADE, related_name="fields", verbose_name=_("Custom contribution type.") + ) + + def __str__(self): + return f"{self.label}: ({self.value_type})" + + def get_slug_as_field_name(self, label): + return slugify(label).replace("-", "_") + + def save(self, *args, **kwargs): + if not self.pk: + self.key = self.get_slug_as_field_name(self.label) + return super().save(*args, **kwargs) + + def get_customization_json_schema_form(self): + base_schema = { + "type": "object", + "properties": {}, + } + if self.value_type == self.FieldTypeChoices.STRING: + base_schema["properties"] = { + "choices": { + "type": "array", + "items": { + "type": "string", + "title": _("Values"), + }, + }, + "placeholder": { + "type": "string", + "title": _("Placeholder"), + }, + "minLength": { + "type": "integer", + "title": _("Min. length"), + }, + "maxLength": { + "type": "integer", + "title": _("Max. length"), + }, + } + if self.value_type == self.FieldTypeChoices.TEXT: + base_schema["properties"] = { + "placeholder": { + "type": "string", + "title": _("Placeholder"), + }, + "minLength": { + "type": "integer", + "title": _("Min. length"), + }, + "maxLength": { + "type": "integer", + "title": _("Max. length"), + }, + } + if self.value_type == self.FieldTypeChoices.INTEGER: + base_schema["properties"] = { + "minimum": { + "type": "integer", + "title": _("Min. value"), + }, + "maximum": { + "type": "integer", + "title": _("Max. value"), + }, + } + if self.value_type == self.FieldTypeChoices.FLOAT: + base_schema["properties"] = { + "minimum": { + "type": "number", + "title": _("Min. value"), + }, + "maximum": { + "type": "number", + "title": _("Max. value"), + }, + } + if self.value_type == self.FieldTypeChoices.BOOLEAN: + base_schema["properties"] = { + "widget": { + "type": "string", + "choices": [ + "radio", + "checkbox", + "select", + ], + }, + "choices": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": {"type": "string", "title": "Title"}, + "value": { + "type": "boolean", + "title": "Value", + "choices": [ + {"title": "Yes", "value": True}, + {"title": "No", "value": False}, + ], + }, + }, + }, + "minItems": 2, + "maxItems": 2, + "uniqueItems": True, + }, + } + return base_schema + + def get_field_schema(self): + base_schema = { + "title": self.label, + "type": "string", + "helpText": self.help_text, + } + base_schema.update(self.options) + + if self.value_type == self.FieldTypeChoices.TEXT: + base_schema["widget"] = "textarea" + elif self.value_type == self.FieldTypeChoices.INTEGER: + base_schema["type"] = "integer" + elif self.value_type == self.FieldTypeChoices.FLOAT: + base_schema["type"] = "number" + elif self.value_type == self.FieldTypeChoices.BOOLEAN: + base_schema["type"] = "boolean" + elif self.value_type == self.FieldTypeChoices.DATE: + base_schema["format"] = "date" + elif self.value_type == self.FieldTypeChoices.DATETIME: + base_schema["format"] = "date-time" + + # drop empty choices + customization = self.customization + if "choices" in customization and not customization.get("choices"): + customization.pop('choices') + base_schema.update(self.customization) + + return base_schema + + class Meta: + verbose_name = _("Custom contribution type field") + verbose_name_plural = _("Custom contribution type fields") + unique_together = (("label", "custom_type"),) # label by type should be unique + index_together = (("order", "custom_type"),) + ordering = ("order", "custom_type") + + +class CustomContribution(TimeStampedModelMixin, models.Model): + geom = models.GeometryField(srid=settings.SRID, spatial_index=True) + station = models.ForeignKey('observations.Station', on_delete=models.PROTECT, + related_name='custom_contributions', verbose_name=_('Station'), blank=True, null=True) + custom_type = models.ForeignKey( + CustomContributionType, on_delete=models.PROTECT, related_name="contributions", + verbose_name=_("Custom contribution type") + ) + data = models.JSONField( + verbose_name=_("Data"), null=False, blank=True, default=dict + ) + portal = models.ForeignKey( + "portal.Portal", + verbose_name=_("Portal"), + blank=True, + null=True, + related_name="custom_contributions", + on_delete=models.PROTECT, + ) + validated = models.BooleanField(default=False, verbose_name=_("Validated")) + objects = CustomContributionManager() + + class Meta: + verbose_name = _("Custom contribution") + verbose_name_plural = _("Custom contributions") + + def __str__(self): + return f"{self.custom_type.label} - {self.pk}" diff --git a/georiviere/contribution/models/managers.py b/georiviere/contribution/models/managers.py new file mode 100644 index 00000000..b7b66328 --- /dev/null +++ b/georiviere/contribution/models/managers.py @@ -0,0 +1,30 @@ +from django.contrib.gis.db import models +from django.db.models.fields.json import KeyTextTransform +from django.db.models.functions import Cast + + +class SelectableUserManager(models.Manager): + def get_queryset(self): + return super().get_queryset().filter(userprofile__isnull=False) + + +class CustomContributionManager(models.Manager): + def with_type_values(self, custom_type): + annotations = {} + qs = self.get_queryset() + for field in custom_type.fields.all(): + output_field = models.CharField() + if field.value_type == 'integer': + output_field = models.IntegerField() + elif field.value_type == 'float': + output_field = models.FloatField() + elif field.value_type == 'boolean': + output_field = models.BooleanField() + elif field.value_type == 'date': + output_field = models.DateField() + elif field.value_type == 'datetime': + output_field = models.DateField() + annotations[field.key] = Cast(KeyTextTransform(field.key, 'data'), output_field=output_field) + if annotations: + qs = qs.annotate(**annotations) + return qs diff --git a/georiviere/contribution/schema.py b/georiviere/contribution/schema.py index 1315881c..506f6f12 100644 --- a/georiviere/contribution/schema.py +++ b/georiviere/contribution/schema.py @@ -1,11 +1,6 @@ from django.utils.translation import gettext as _ -from georiviere.contribution.models import (ContributionQuantity, ContributionQuality, - ContributionFaunaFlora, ContributionLandscapeElements, - ContributionPotentialDamage, SeverityType, - LandingType, JamType, DiseaseType, DeadSpecies, - InvasiveSpecies, HeritageSpecies, HeritageObservation, FishSpecies, - NaturePollution, TypePollution) +from . import models # The json schema is summarized on : # https://github.com/Georiviere/Georiviere-admin/issues/139 @@ -16,407 +11,364 @@ def get_contribution_properties(): - """ Feature properties as form initial data format (name / value) """ + """Feature properties as form initial data format (name / value)""" # TODO: Use directly field definition for type / title / max length results = { - 'name_author': { - 'type': "string", - 'title': _("Name author"), - "maxLength": 128 - }, - 'first_name_author': { - 'type': "string", - 'title': _("First name author"), - "maxLength": 128 - }, - 'email_author': { - 'type': "string", - 'title': _("Email"), - 'format': "email" - }, - 'date_observation': { - 'type': "string", - 'title': _("Observation's date"), - 'format': 'date' + "name_author": {"type": "string", "title": _("Name author"), "maxLength": 128}, + "first_name_author": { + "type": "string", + "title": _("First name author"), + "maxLength": 128, }, - 'description': { - 'type': "string", - 'title': _('Description') + "email_author": {"type": "string", "title": _("Email"), "format": "email"}, + "date_observation": { + "type": "string", + "title": _("Observation's date"), + "format": "date", }, - 'category': { + "description": {"type": "string", "title": _("Description")}, + "category": { "type": "string", "title": _("Category"), # TODO: Loop on contribution one to one field to get all possibilities "enum": [ - str(ContributionQuantity._meta.verbose_name.title()), - str(ContributionQuality._meta.verbose_name.title()), - str(ContributionFaunaFlora._meta.verbose_name.title()), - str(ContributionLandscapeElements._meta.verbose_name.title()), - str(ContributionPotentialDamage._meta.verbose_name.title()) + models.ContributionQuantity._meta.verbose_name.title(), + models.ContributionQuality._meta.verbose_name.title(), + models.ContributionFaunaFlora._meta.verbose_name.title(), + models.ContributionLandscapeElements._meta.verbose_name.title(), + models.ContributionPotentialDamage._meta.verbose_name.title(), ], - } + }, } - if SeverityType.objects.exists(): - results['severity'] = { - 'type': "string", - 'title': _('Severity'), - 'enum': list(SeverityType.objects.values_list('label', flat=True)) + if models.SeverityType.objects.exists(): + results["severity"] = { + "type": "string", + "title": _("Severity"), + "enum": list(models.SeverityType.objects.values_list("label", flat=True)), } return results def get_landing(choices, meta): landing = { - 'if': { - 'properties': {'type': {'const': str(choices.LANDING.label)}} - }, - 'then': { - 'properties': { - 'landing_type': - { - 'type': "string", - 'title': str(meta.get_field('landing_type').related_model._meta.verbose_name.capitalize()), - 'enum': list(LandingType.objects.values_list('label', flat=True)) - } + "if": {"properties": {"type": {"const": choices.LANDING.label}}}, + "then": { + "properties": { + "landing_type": { + "type": "string", + "title": meta.get_field( + "landing_type" + ).related_model._meta.verbose_name.capitalize(), + "enum": list( + Lmodels.andingType.objects.values_list("label", flat=True) + ), + } }, - } + }, } return landing def get_excessive_cutting_riparian_forest(choices, meta): excessive_cutting_riparian_forest = { - 'if': { - 'properties': { - 'type': {'const': str(choices.EXCESSIVE_CUTTING_RIPARIAN_FOREST.label)}} + "if": { + "properties": { + "type": {"const": choices.EXCESSIVE_CUTTING_RIPARIAN_FOREST.label} + } }, - 'then': { - 'properties': { - 'excessive_cutting_length': - { - 'type': "number", - 'title': str(meta.get_field( - 'excessive_cutting_length').verbose_name.capitalize()), - } + "then": { + "properties": { + "excessive_cutting_length": { + "type": "number", + "title": meta.get_field( + "excessive_cutting_length" + ).verbose_name.capitalize(), + } }, - } + }, } return excessive_cutting_riparian_forest def get_disruptive_jam(choices, meta): disruptive_jam = { - 'if': { - 'properties': { - 'type': { - 'const': str(choices.DISRUPTIVE_JAM.label)}} - }, - 'then': { - 'properties': { - 'jam_type': - { - 'type': "string", - 'title': str(meta.get_field('jam_type').related_model._meta.verbose_name.capitalize()), - 'enum': list(JamType.objects.values_list('label', flat=True)) - } + "if": {"properties": {"type": {"const": choices.DISRUPTIVE_JAM.label}}}, + "then": { + "properties": { + "jam_type": { + "type": "string", + "title": meta.get_field( + "jam_type" + ).related_model._meta.verbose_name.capitalize(), + "enum": list( + models.JamType.objects.values_list("label", flat=True) + ), + } }, - } + }, } return disruptive_jam def get_bank_erosion(choices, meta): bank_erosion = { - 'if': { - 'properties': { - 'type': { - 'const': str(choices.BANK_EROSION.label) + "if": {"properties": {"type": {"const": choices.BANK_EROSION.label}}}, + "then": { + "properties": { + "length_bank_erosion": { + "type": "string", + "title": meta.get_field( + "length_bank_erosion" + ).verbose_name.capitalize(), } - } - }, - 'then': { - 'properties': { - 'length_bank_erosion': - { - 'type': "string", - 'title': str(meta.get_field( - 'length_bank_erosion').verbose_name.capitalize()), - } }, - } + }, } return bank_erosion def get_river_bed_incision(choices, meta): river_bed_incision = { - 'if': { - 'properties': { - 'type': { - 'const': str(choices.RIVER_BED_INCISION.label)}} - }, - 'then': { - 'properties': { - 'bank_height': - { - 'type': "string", - 'title': str(meta.get_field( - 'bank_height').verbose_name.capitalize()), - } + "if": {"properties": {"type": {"const": choices.RIVER_BED_INCISION.label}}}, + "then": { + "properties": { + "bank_height": { + "type": "string", + "title": meta.get_field("bank_height").verbose_name.capitalize(), + } }, - } + }, } return river_bed_incision def get_fish_diseases(choices, meta): fish_diseases = { - 'if': { - 'properties': { - 'type': { - 'const': str(choices.FISH_DISEASES.label)}} - }, - 'then': { - 'properties': { - 'disease_type': - { - 'type': "string", - 'title': str(meta.get_field( - 'disease_type').related_model._meta.verbose_name.capitalize()), - 'enum': list(DiseaseType.objects.values_list('label', flat=True)) - } + "if": {"properties": {"type": {"const": choices.FISH_DISEASES.label}}}, + "then": { + "properties": { + "disease_type": { + "type": "string", + "title": meta.get_field( + "disease_type" + ).related_model._meta.verbose_name.capitalize(), + "enum": list( + models.DiseaseType.objects.values_list("label", flat=True) + ), + } }, - } + }, } return fish_diseases def get_fish_mortality(choices, meta): fish_mortality_property = { - 'number_death': - { - 'type': "number", - 'title': str(meta.get_field( - 'number_death').verbose_name.capitalize()) - }, + "number_death": { + "type": "number", + "title": meta.get_field("number_death").verbose_name.capitalize(), + }, } - if DeadSpecies.objects.exists(): - fish_mortality_property['dead_species'] = { - 'type': "string", - 'title': _("Observed species"), - 'enum': list(DeadSpecies.objects.values_list('label', flat=True)) + if models.DeadSpecies.objects.exists(): + fish_mortality_property["dead_species"] = { + "type": "string", + "title": _("Observed species"), + "enum": list(models.DeadSpecies.objects.values_list("label", flat=True)), } fish_mortality = { - 'if': { - 'properties': { - 'type': { - 'const': str(choices.FISH_MORTALITY.label)}} - }, - 'then': { - 'properties': fish_mortality_property - } + "if": {"properties": {"type": {"const": choices.FISH_MORTALITY.label}}}, + "then": {"properties": fish_mortality_property}, } return fish_mortality def get_potentialdamage_condition(): - potential_damage_choices = ContributionPotentialDamage.TypeChoice - meta_potential_damage = ContributionPotentialDamage._meta + potential_damage_choices = models.ContributionPotentialDamage.TypeChoice + meta_potential_damage = models.ContributionPotentialDamage._meta initial_condition = { - 'if': { - 'properties': {'category': {'const': str(meta_potential_damage.verbose_name.title())}} + "if": { + "properties": { + "category": {"const": meta_potential_damage.verbose_name.title()} + } }, - 'then': { - 'properties': { - 'type': { - 'type': "string", - 'title': str(meta_potential_damage.get_field('type').verbose_name.title()), - 'enum': list(potential_damage_choices.labels) + "then": { + "properties": { + "type": { + "type": "string", + "title": meta_potential_damage.get_field( + "type" + ).verbose_name.title(), + "enum": list(potential_damage_choices.labels), } }, - "required": ['type'], - } + "required": ["type"], + }, } conditions_each_type = [ initial_condition, - get_excessive_cutting_riparian_forest(potential_damage_choices, - meta_potential_damage), + get_excessive_cutting_riparian_forest( + potential_damage_choices, meta_potential_damage + ), get_bank_erosion(potential_damage_choices, meta_potential_damage), get_river_bed_incision(potential_damage_choices, meta_potential_damage), - get_fish_mortality(potential_damage_choices, meta_potential_damage) + get_fish_mortality(potential_damage_choices, meta_potential_damage), ] # 2 types in fish mortality - if LandingType.objects.exists(): - conditions_each_type.append(get_landing(potential_damage_choices, meta_potential_damage)) - if JamType.objects.exists(): - conditions_each_type.append(get_disruptive_jam(potential_damage_choices, meta_potential_damage)) - if DiseaseType.objects.exists(): - conditions_each_type.append(get_fish_diseases(potential_damage_choices, meta_potential_damage)) + if models.LandingType.objects.exists(): + conditions_each_type.append( + get_landing(potential_damage_choices, meta_potential_damage) + ) + if models.JamType.objects.exists(): + conditions_each_type.append( + get_disruptive_jam(potential_damage_choices, meta_potential_damage) + ) + if models.DiseaseType.objects.exists(): + conditions_each_type.append( + get_fish_diseases(potential_damage_choices, meta_potential_damage) + ) return conditions_each_type def get_invasive_species(choices, meta): invasive_species_property = { - 'home_area': - { - 'type': "string", - 'title': str(meta.get_field( - 'home_area').verbose_name.capitalize()) - }, + "home_area": { + "type": "string", + "title": meta.get_field("home_area").verbose_name.capitalize(), + }, } - if InvasiveSpecies.objects.exists(): - invasive_species_property['invasive_species'] = { - 'type': "string", - 'title': _("Observed species"), - 'enum': list(InvasiveSpecies.objects.values_list('label', flat=True)) + if models.InvasiveSpecies.objects.exists(): + invasive_species_property["invasive_species"] = { + "type": "string", + "title": _("Observed species"), + "enum": list( + models.InvasiveSpecies.objects.values_list("label", flat=True) + ), } invasive_species = { - 'if': { - 'properties': { - 'type': { - 'const': str(choices.INVASIVE_SPECIES.label)}} - }, - 'then': { - 'properties': invasive_species_property - } + "if": {"properties": {"type": {"const": choices.INVASIVE_SPECIES.label}}}, + "then": {"properties": invasive_species_property}, } return invasive_species def get_heritage_species(choices, meta): heritage_species_property = { - 'number_heritage_species': - { - 'type': "number", - 'title': str(meta.get_field( - 'number_heritage_species').verbose_name.capitalize()) - }, + "number_heritage_species": { + "type": "number", + "title": meta.get_field( + "number_heritage_species" + ).verbose_name.capitalize(), + }, } - if HeritageSpecies.objects.exists(): - heritage_species_property['heritage_species'] = { - 'type': "string", - 'title': _("Observed species"), - 'enum': list(HeritageSpecies.objects.values_list('label', flat=True)) + if models.HeritageSpecies.objects.exists(): + heritage_species_property["heritage_species"] = { + "type": "string", + "title": _("Observed species"), + "enum": list( + models.HeritageSpecies.objects.values_list("label", flat=True) + ), } - if HeritageObservation.objects.exists(): - heritage_species_property['heritage_observation'] = { - 'type': "string", - 'title': _("Observation type"), - 'enum': list(HeritageObservation.objects.values_list('label', flat=True)) + if models.HeritageObservation.objects.exists(): + heritage_species_property["heritage_observation"] = { + "type": "string", + "title": _("Observation type"), + "enum": list( + models.HeritageObservation.objects.values_list("label", flat=True) + ), } heritage_species = { - 'if': { - 'properties': { - 'type': { - 'const': str(choices.HERITAGE_SPECIES.label)}} - }, - 'then': { - 'properties': heritage_species_property - } + "if": {"properties": {"type": {"const": choices.HERITAGE_SPECIES.label}}}, + "then": {"properties": heritage_species_property}, } return heritage_species def get_fish_species(choices, meta): fish_species_property = { - 'number_fish_species': - { - 'type': "number", - 'title': str(meta.get_field( - 'number_fish_species').verbose_name.capitalize()) - }, + "number_fish_species": { + "type": "number", + "title": meta.get_field("number_fish_species").verbose_name.capitalize(), + }, } - if FishSpecies.objects.exists(): - fish_species_property['fish_species'] = { - 'type': "string", - 'title': _("Observed species"), - 'enum': list(FishSpecies.objects.values_list('label', flat=True)) + if models.FishSpecies.objects.exists(): + fish_species_property["fish_species"] = { + "type": "string", + "title": _("Observed species"), + "enum": list(models.FishSpecies.objects.values_list("label", flat=True)), } fish_species = { - 'if': { - 'properties': { - 'type': { - 'const': str(choices.FISH_SPECIES.label)}} - }, - 'then': { - 'properties': fish_species_property - } + "if": {"properties": {"type": {"const": choices.FISH_SPECIES.label}}}, + "then": {"properties": fish_species_property}, } return fish_species def get_faunaflora_condition(): - faunaflora_choices = ContributionFaunaFlora.TypeChoice - meta_faunaflora = ContributionFaunaFlora._meta + faunaflora_choices = models.ContributionFaunaFlora.TypeChoice + meta_faunaflora = models.ContributionFaunaFlora._meta initial_condition = { - 'if': { - 'properties': {'category': {'const': str(meta_faunaflora.verbose_name.title())}} + "if": { + "properties": {"category": {"const": meta_faunaflora.verbose_name.title()}} }, - 'then': { - 'properties': { - 'type': { - 'type': "string", - 'title': str(meta_faunaflora.get_field('type').verbose_name.title()), - 'enum': list(faunaflora_choices.labels) + "then": { + "properties": { + "type": { + "type": "string", + "title": meta_faunaflora.get_field("type").verbose_name.title(), + "enum": list(faunaflora_choices.labels), } }, - "required": ['type'], - } + "required": ["type"], + }, } conditions_each_type = [ initial_condition, get_invasive_species(faunaflora_choices, meta_faunaflora), get_heritage_species(faunaflora_choices, meta_faunaflora), - get_fish_species(faunaflora_choices, meta_faunaflora) + get_fish_species(faunaflora_choices, meta_faunaflora), ] return conditions_each_type def get_overflow(choices, meta): overflow = { - 'if': { - 'properties': { - 'type': { - 'const': str(choices.OVERFLOW.label)}} - }, - 'then': { - 'properties': { - 'landmark': - { - 'type': "string", - 'title': str(meta.get_field( - 'landmark').verbose_name.capitalize()) - }, + "if": {"properties": {"type": {"const": choices.OVERFLOW.label}}}, + "then": { + "properties": { + "landmark": { + "type": "string", + "title": meta.get_field("landmark").verbose_name.capitalize(), + }, } - } + }, } return overflow def get_quantity_condition(): - quantity_choices = ContributionQuantity.TypeChoice - meta_quantity = ContributionQuantity._meta + quantity_choices = models.ContributionQuantity.TypeChoice + meta_quantity = models.ContributionQuantity._meta initial_condition = { - 'if': { - 'properties': {'category': {'const': str(meta_quantity.verbose_name.title())}} + "if": { + "properties": {"category": {"const": meta_quantity.verbose_name.title()}} }, - 'then': { - 'properties': { - 'type': { - 'type': "string", - 'title': str(meta_quantity.get_field('type').verbose_name.capitalize()), - 'enum': list(quantity_choices.labels) + "then": { + "properties": { + "type": { + "type": "string", + "title": meta_quantity.get_field("type").verbose_name.capitalize(), + "enum": list(quantity_choices.labels), } }, - "required": ['type'], - } + "required": ["type"], + }, } conditions_each_type = [ @@ -428,85 +380,85 @@ def get_quantity_condition(): def get_pollution(choices, meta): pollution_property = {} - if NaturePollution.objects.exists(): - pollution_property['nature_pollution'] = { - 'type': "string", - 'title': str(meta.get_field( - 'nature_pollution').related_model._meta.verbose_name.capitalize()), - 'enum': list(NaturePollution.objects.values_list('label', flat=True)) + if models.NaturePollution.objects.exists(): + pollution_property["nature_pollution"] = { + "type": "string", + "title": meta.get_field( + "nature_pollution" + ).related_model._meta.verbose_name.capitalize(), + "enum": list( + models.NaturePollution.objects.values_list("label", flat=True) + ), } if TypePollution.objects.exists(): - pollution_property['type_pollution'] = { - 'type': "string", - 'title': str(meta.get_field( - 'type_pollution').related_model._meta.verbose_name.capitalize()), - 'enum': list(TypePollution.objects.values_list('label', flat=True)) + pollution_property["type_pollution"] = { + "type": "string", + "title": meta.get_field( + "type_pollution" + ).related_model._meta.verbose_name.capitalize(), + "enum": list(TypePollution.objects.values_list("label", flat=True)), } pollution = { - 'if': { - 'properties': { - 'type': { - 'const': str(choices.POLLUTION.label)}} - }, - 'then': { - 'properties': pollution_property - } + "if": {"properties": {"type": {"const": choices.POLLUTION.label}}}, + "then": {"properties": pollution_property}, } return pollution def get_quality_condition(): - quality_choices = ContributionQuality.TypeChoice - meta_quality = ContributionQuality._meta + quality_choices = models.ContributionQuality.TypeChoice + meta_quality = models.ContributionQuality._meta initial_condition = { - 'if': { - 'properties': {'category': {'const': str(meta_quality.verbose_name.title())}} + "if": { + "properties": {"category": {"const": meta_quality.verbose_name.title()}} }, - 'then': { - 'properties': { - 'type': { - 'type': "string", - 'title': str(meta_quality.get_field('type').verbose_name.title()), - 'enum': list(quality_choices.labels) + "then": { + "properties": { + "type": { + "type": "string", + "title": meta_quality.get_field("type").verbose_name.title(), + "enum": list(quality_choices.labels), } }, - "required": ['type'], - } + "required": ["type"], + }, } conditions_each_type = [ initial_condition, - ] - if NaturePollution.objects.exists() or TypePollution.objects.exists(): + if models.NaturePollution.objects.exists() or models.TypePollution.objects.exists(): conditions_each_type.append(get_pollution(quality_choices, meta_quality)) return conditions_each_type def get_landscapeelements_condition(): - landscapeelements_choices = ContributionLandscapeElements.TypeChoice - meta_landscapeelements = ContributionLandscapeElements._meta + landscapeelements_choices = models.ContributionLandscapeElements.TypeChoice + meta_landscapeelements = models.ContributionLandscapeElements._meta initial_condition = { - 'if': { - 'properties': {'category': {'const': str(meta_landscapeelements.verbose_name.title())}} + "if": { + "properties": { + "category": {"const": meta_landscapeelements.verbose_name.title()} + } }, - 'then': { - 'properties': { - 'type': { - 'type': "string", - 'title': str(meta_landscapeelements.get_field('type').verbose_name.title()), - 'enum': list(landscapeelements_choices.labels) + "then": { + "properties": { + "type": { + "type": "string", + "title": meta_landscapeelements.get_field( + "type" + ).verbose_name.title(), + "enum": list(landscapeelements_choices.labels), } }, - "required": ['type'], - } + "required": ["type"], + }, } conditions_each_type = [ initial_condition, - ] return conditions_each_type @@ -523,7 +475,7 @@ def get_contribution_allOf(): def get_contribution_json_schema(): return { "type": "object", - "required": ['email_author', 'date_observation', 'category'], + "required": ["email_author", "date_observation", "category"], "properties": get_contribution_properties(), - "allOf": get_contribution_allOf() + "allOf": get_contribution_allOf(), } diff --git a/georiviere/description/locale/en/LC_MESSAGES/django.po b/georiviere/description/locale/en/LC_MESSAGES/django.po index c5832267..825434de 100644 --- a/georiviere/description/locale/en/LC_MESSAGES/django.po +++ b/georiviere/description/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:15+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/description/locale/fr/LC_MESSAGES/django.po b/georiviere/description/locale/fr/LC_MESSAGES/django.po index 7fc0a941..57c5c31d 100644 --- a/georiviere/description/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/description/locale/fr/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:04+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: 2021-11-23 14:20+0000\n" "Last-Translator: Jean-Etienne Castagnede , 2021\n" "Language-Team: French (https://www.transifex.com/georiviere/teams/128005/" diff --git a/georiviere/finances_administration/locale/en/LC_MESSAGES/django.po b/georiviere/finances_administration/locale/en/LC_MESSAGES/django.po index 3eebe7f1..14db7191 100644 --- a/georiviere/finances_administration/locale/en/LC_MESSAGES/django.po +++ b/georiviere/finances_administration/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:15+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/finances_administration/locale/fr/LC_MESSAGES/django.po b/georiviere/finances_administration/locale/fr/LC_MESSAGES/django.po index 88c02ee3..68ec22f7 100644 --- a/georiviere/finances_administration/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/finances_administration/locale/fr/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:04+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: 2021-11-23 14:22+0000\n" "Last-Translator: Jean-Etienne Castagnede , 2021\n" "Language-Team: French (https://www.transifex.com/georiviere/teams/128005/" diff --git a/georiviere/flatpages/locale/en/LC_MESSAGES/django.po b/georiviere/flatpages/locale/en/LC_MESSAGES/django.po index 8abcfefa..abd8afd9 100644 --- a/georiviere/flatpages/locale/en/LC_MESSAGES/django.po +++ b/georiviere/flatpages/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:15+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/flatpages/locale/fr/LC_MESSAGES/django.po b/georiviere/flatpages/locale/fr/LC_MESSAGES/django.po index 727a87e7..3adf6bce 100644 --- a/georiviere/flatpages/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/flatpages/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:04+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/knowledge/locale/en/LC_MESSAGES/django.po b/georiviere/knowledge/locale/en/LC_MESSAGES/django.po index 82d091ac..db6c0c6a 100644 --- a/georiviere/knowledge/locale/en/LC_MESSAGES/django.po +++ b/georiviere/knowledge/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:15+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/knowledge/locale/fr/LC_MESSAGES/django.po b/georiviere/knowledge/locale/fr/LC_MESSAGES/django.po index f1c25eb9..89adc735 100644 --- a/georiviere/knowledge/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/knowledge/locale/fr/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:03+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: 2021-11-23 15:08+0000\n" "Last-Translator: Jean-Etienne Castagnede , 2021\n" "Language-Team: French (https://www.transifex.com/georiviere/teams/128005/" diff --git a/georiviere/main/apps.py b/georiviere/main/apps.py index 8e84238c..3cb9f0b9 100644 --- a/georiviere/main/apps.py +++ b/georiviere/main/apps.py @@ -15,6 +15,5 @@ def ready(self): from georiviere.river.models import Stream for model in apps.get_models(): if issubclass(model, MapEntityMixin) and model != Stream: - print("signal for model {}".format(model)) post_save.connect(signals.save_objects_generate_distance_to_source, sender=model) post_delete.connect(signals.delete_objects_remove_distance_to_source, sender=model) diff --git a/georiviere/main/locale/en/LC_MESSAGES/django.po b/georiviere/main/locale/en/LC_MESSAGES/django.po index f8e433c3..2f4f6895 100644 --- a/georiviere/main/locale/en/LC_MESSAGES/django.po +++ b/georiviere/main/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:15+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/main/locale/fr/LC_MESSAGES/django.po b/georiviere/main/locale/fr/LC_MESSAGES/django.po index 5ceaa9c9..cc9656f7 100644 --- a/georiviere/main/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/main/locale/fr/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:05+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: 2021-11-23 15:31+0000\n" "Last-Translator: Jean-Etienne Castagnede , 2021\n" "Language-Team: French (https://www.transifex.com/georiviere/teams/128005/" diff --git a/georiviere/maintenance/locale/en/LC_MESSAGES/django.po b/georiviere/maintenance/locale/en/LC_MESSAGES/django.po index 07dc4991..72bf54c5 100644 --- a/georiviere/maintenance/locale/en/LC_MESSAGES/django.po +++ b/georiviere/maintenance/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:15+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/maintenance/locale/fr/LC_MESSAGES/django.po b/georiviere/maintenance/locale/fr/LC_MESSAGES/django.po index a2ff4a4c..0de2f188 100644 --- a/georiviere/maintenance/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/maintenance/locale/fr/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:05+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: 2021-11-23 15:31+0000\n" "Last-Translator: Jean-Etienne Castagnede , 2021\n" "Language-Team: French (https://www.transifex.com/georiviere/teams/128005/" diff --git a/georiviere/observations/locale/en/LC_MESSAGES/django.po b/georiviere/observations/locale/en/LC_MESSAGES/django.po index 25ef1c36..38aa6685 100644 --- a/georiviere/observations/locale/en/LC_MESSAGES/django.po +++ b/georiviere/observations/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:15+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/observations/locale/fr/LC_MESSAGES/django.po b/georiviere/observations/locale/fr/LC_MESSAGES/django.po index 06c938b7..3b2da564 100644 --- a/georiviere/observations/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/observations/locale/fr/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:05+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: 2021-11-23 15:31+0000\n" "Last-Translator: Jean-Etienne Castagnede , 2021\n" "Language-Team: French (https://www.transifex.com/georiviere/teams/128005/" diff --git a/georiviere/portal/locale/en/LC_MESSAGES/django.po b/georiviere/portal/locale/en/LC_MESSAGES/django.po index 52da707d..84a815e5 100644 --- a/georiviere/portal/locale/en/LC_MESSAGES/django.po +++ b/georiviere/portal/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:40+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -93,39 +93,21 @@ msgstr "" msgid "Watershed" msgstr "" -msgid "watersheds" -msgstr "" - msgid "City" msgstr "" -msgid "cities" -msgstr "" - msgid "Sensitivity" msgstr "" -msgid "sensitivities" -msgstr "" - msgid "District" msgstr "" -msgid "districts" -msgstr "" - msgid "Contribution" msgstr "" -msgid "contributions" -msgstr "" - msgid "Stream" msgstr "" -msgid "streams" -msgstr "" - msgid "Georiviere : Contribution" msgstr "" diff --git a/georiviere/portal/locale/fr/LC_MESSAGES/django.po b/georiviere/portal/locale/fr/LC_MESSAGES/django.po index 1b6141f2..064bc9ba 100644 --- a/georiviere/portal/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/portal/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:39+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -93,39 +93,21 @@ msgstr "Une erreur s'es produite" msgid "Watershed" msgstr "Bassin versant" -msgid "watersheds" -msgstr "bassins versants" - msgid "City" msgstr "Ville" -msgid "cities" -msgstr "villes" - msgid "Sensitivity" msgstr "Zone sensible" -msgid "sensitivities" -msgstr "zones sensibles" - msgid "District" msgstr "Secteur" -msgid "districts" -msgstr "secteurs" - msgid "Contribution" msgstr "Contribution" -msgid "contributions" -msgstr "contributions" - msgid "Stream" msgstr "Rivière" -msgid "streams" -msgstr "rivières" - msgid "Georiviere : Contribution" msgstr "Georiviere : Contribution" @@ -149,3 +131,21 @@ msgstr "" "\n" "L'équipe Georiviere\n" "http://georiviere.fr" + +#~ msgid "watersheds" +#~ msgstr "bassins versants" + +#~ msgid "cities" +#~ msgstr "villes" + +#~ msgid "sensitivities" +#~ msgstr "zones sensibles" + +#~ msgid "districts" +#~ msgstr "secteurs" + +#~ msgid "contributions" +#~ msgstr "contributions" + +#~ msgid "streams" +#~ msgstr "rivières" diff --git a/georiviere/portal/serializers/contribution.py b/georiviere/portal/serializers/contribution.py index c9f5c1a4..6f9aaa77 100644 --- a/georiviere/portal/serializers/contribution.py +++ b/georiviere/portal/serializers/contribution.py @@ -8,26 +8,37 @@ from django.db.models import ForeignKey from django.utils.translation import gettext_lazy as _ -from georiviere.contribution.schema import (get_contribution_properties, get_contribution_allOf, - get_contribution_json_schema) -from georiviere.contribution.models import (Contribution, ContributionLandscapeElements, ContributionQuality, - ContributionQuantity, ContributionFaunaFlora, ContributionPotentialDamage, - SeverityType) +from georiviere.contribution.schema import ( + get_contribution_properties, + get_contribution_allOf, + get_contribution_json_schema, +) +from georiviere.contribution.models import ( + Contribution, + ContributionLandscapeElements, + ContributionQuality, + ContributionQuantity, + ContributionFaunaFlora, + ContributionPotentialDamage, + SeverityType, + CustomContributionType, + CustomContribution, +) from georiviere.portal.validators import validate_json_schema_data from georiviere.portal.serializers.main import AttachmentSerializer class ContributionGeojsonSerializer(geo_serializers.GeoFeatureModelSerializer): # Annotated geom field with API_SRID - geometry = geo_serializers.GeometryField(read_only=True, precision=7, source="geom_transformed") + geometry = geo_serializers.GeometryField( + read_only=True, precision=7, source="geom_transformed" + ) category = serializers.SerializerMethodField(read_only=True) class Meta: - geo_field = 'geometry' + geo_field = "geometry" model = Contribution - fields = ( - 'id', 'category', 'geometry' - ) + fields = ("id", "category", "geometry") def get_category(self, obj): return obj.category._meta.verbose_name.title() @@ -43,7 +54,13 @@ class ContributionSerializer(serializers.ModelSerializer): class Meta: model = Contribution fields = ( - 'id', 'properties', 'geom', 'type', 'category', 'description', 'attachments', + "id", + "properties", + "geom", + "type", + "category", + "description", + "attachments", ) def get_category(self, obj): @@ -59,7 +76,7 @@ def validate_properties(self, data): def create(self, validated_data): sid = transaction.savepoint() - msg = '' + msg = "" # Create a contribution depending on the data you get from the portal # The datas should follow the json schema generated in `georiviere/contribution/schema.py` # All the properties are flatten directly in the field properties @@ -67,28 +84,31 @@ def create(self, validated_data): # Check https://github.com/Georiviere/Georiviere-admin/issues/139 # For more informations try: - properties = validated_data.pop('properties') - category = properties.pop('category') - email_author = properties.pop('email_author') - name_author = properties.pop('name_author', '') - first_name_author = properties.pop('first_name_author', '') - date_observation = properties.pop('date_observation') - description = properties.pop('description', '') - severity = properties.pop('severity', '') + properties = validated_data.pop("properties") + category = properties.pop("category") + email_author = properties.pop("email_author") + name_author = properties.pop("name_author", "") + first_name_author = properties.pop("first_name_author", "") + date_observation = properties.pop("date_observation") + description = properties.pop("description", "") + severity = properties.pop("severity", "") severity_instance = False if severity: severity_instance = SeverityType.objects.filter(label=severity) - geom = validated_data.pop('geom') + geom = validated_data.pop("geom") geom = GEOSGeometry(geom, srid=4326) geom = geom.transform(settings.SRID, clone=True) - kwargs_contribution = {'geom': geom, 'email_author': email_author, - 'date_observation': date_observation, - 'portal_id': self.context.get('portal_pk'), - 'name_author': name_author, - 'description': description, - 'first_name_author': first_name_author} + kwargs_contribution = { + "geom": geom, + "email_author": email_author, + "date_observation": date_observation, + "portal_id": self.context.get("portal_pk"), + "name_author": name_author, + "description": description, + "first_name_author": first_name_author, + } if bool(severity_instance): - kwargs_contribution['severity'] = severity_instance.first() + kwargs_contribution["severity"] = severity_instance.first() main_contribution = Contribution.objects.create(**kwargs_contribution) model = None @@ -110,23 +130,25 @@ def create(self, validated_data): msg = _("Category is not valid") raise - type_prop = properties.pop('type') + type_prop = properties.pop("type") # All the categories have a field type. We get all choices available and check # if the type exists for this category types = {v: k for k, v in model.TypeChoice.choices} for key, prop in properties.items(): if isinstance(model._meta.get_field(key), ForeignKey): - properties[key] = model._meta.get_field(key).related_model.objects.get(label=prop) + properties[key] = model._meta.get_field( + key + ).related_model.objects.get(label=prop) # If the type doesn't exist for this category, the error is catched, a validationerror occur. # If it exists, the contribution of the category in properties is created. - model.objects.create(contribution=main_contribution, - type=types[type_prop], - **properties) + model.objects.create( + contribution=main_contribution, type=types[type_prop], **properties + ) transaction.savepoint_commit(sid) except Exception as e: transaction.savepoint_rollback(sid) if not msg: - msg = f'{e.__class__.__name__} {e}' + msg = f"{e.__class__.__name__} {e}" raise serializers.ValidationError({"Error": msg or _("An error occured")}) return main_contribution @@ -134,14 +156,14 @@ def create(self, validated_data): # Serializer for the contribution json schema's following the jsonschema reference : # https://json-schema.org/understanding-json-schema/reference/conditionals.html class ContributionSchemaSerializer(serializers.Serializer): - type = serializers.CharField(default='object') - required = serializers.SerializerMethodField(method_name='get_required') + type = serializers.CharField(default="object") + required = serializers.SerializerMethodField(method_name="get_required") properties = serializers.SerializerMethodField() allOf = serializers.SerializerMethodField() def get_required(self, obj): # TODO: Loop on fields to get required - return ['email_author', 'date_observation', 'category'] + return ["email_author", "date_observation", "category"] def get_properties(self, obj): return get_contribution_properties() @@ -150,7 +172,49 @@ def get_allOf(self, obj): return get_contribution_allOf() class Meta: - geo_field = 'geom' - fields = ( - 'type', 'required', 'properties', 'allOf' - ) + geo_field = "geom" + fields = ("type", "required", "properties", "allOf") + + +class CustomContributionTypeSerializer(serializers.ModelSerializer): + class Meta: + model = CustomContributionType + fields = ("id", "label", "json_schema_form", "stations") + + +class CustomContributionSerializer(serializers.ModelSerializer): + def __init__(self, *args, **kwargs): + custom_type = kwargs.pop("custom_type") + super().__init__(*args, **kwargs) + schema = custom_type.get_json_schema_form() + for key in schema.get("properties", {}).keys(): + field = schema.get("properties", {}).get(key) + output_field = serializers.CharField + if field.get("type") == "number": + output_field = serializers.FloatField + elif field.get("type") == "integer": + output_field = serializers.IntegerField + elif field.get("type") == "boolean": + output_field = serializers.BooleanField + elif field.get("type") == "date": + output_field = serializers.DateField + elif field.get("type") == "datetime": + output_field = serializers.DateTimeField + self.fields[key] = output_field(label=field.get("title")) + + class Meta: + model = CustomContribution + exclude = ("data", "custom_type") + + +class CustomContributionSerializerGeoJSONSerializer(geo_serializers.GeoFeatureModelSerializer, + CustomContributionSerializer, + +): + geometry = geo_serializers.GeometryField( + read_only=True, precision=7 + ) + + class Meta(CustomContributionSerializer.Meta): + geo_field = "geometry" + exclude = ("geom", "data", "custom_type") diff --git a/georiviere/portal/urls.py b/georiviere/portal/urls.py index 9cbc9b73..fa5359c1 100644 --- a/georiviere/portal/urls.py +++ b/georiviere/portal/urls.py @@ -4,12 +4,19 @@ from georiviere.portal.views import GeoriviereVersionAPIView from georiviere.portal.views.flatpage import FlatPageViewSet -from georiviere.portal.views.contribution import ContributionViewSet +from georiviere.portal.views.contribution import ( + ContributionViewSet, + CustomContributionTypeViewSet, +) from georiviere.portal.views.portal import PortalViewSet from georiviere.portal.views.river import StreamViewSet from georiviere.portal.views.sensitivity import SensitivityViewSet from georiviere.portal.views.valorization import POIViewSet -from georiviere.portal.views.zoning import CityViewSet, DistrictViewSet, WatershedViewSet +from georiviere.portal.views.zoning import ( + CityViewSet, + DistrictViewSet, + WatershedViewSet, +) from drf_spectacular.views import ( SpectacularAPIView, @@ -19,20 +26,43 @@ router = routers.DefaultRouter() # Datas are available depending on portal or not. -router.register(r'(?P[a-z]{2})/(?P\d+)/pois', POIViewSet, basename='pois') +router.register( + r"(?P[a-z]{2})/(?P\d+)/pois", POIViewSet, basename="pois" +) -router.register(r'(?P[a-z]{2})/(?P\d+)/streams', StreamViewSet, basename='streams') +router.register( + r"(?P[a-z]{2})/(?P\d+)/streams", StreamViewSet, basename="streams" +) -router.register(r'(?P[a-z]{2})/(?P\d+)/flatpages', FlatPageViewSet, basename='flatpages') -router.register(r'(?P[a-z]{2})/(?P\d+)/contributions', ContributionViewSet, basename='contributions') -router.register(r'(?P[a-z]{2})/(?P\d+)/watersheds', WatershedViewSet, basename='watersheds') +router.register( + r"(?P[a-z]{2})/(?P\d+)/flatpages", + FlatPageViewSet, + basename="flatpages", +) +router.register( + r"(?P[a-z]{2})/(?P\d+)/contributions", + ContributionViewSet, + basename="contributions", +) +router.register( + r"(?P[a-z]{2})/(?P\d+)/custom-contribution-types", + CustomContributionTypeViewSet, + basename="custom_contribution_types", +) +router.register( + r"(?P[a-z]{2})/(?P\d+)/watersheds", + WatershedViewSet, + basename="watersheds", +) -router.register(r'(?P[a-z]{2})/portal', PortalViewSet, basename='portal') -router.register(r'(?P[a-z]{2})/cities', CityViewSet, basename='cities') -router.register(r'(?P[a-z]{2})/districts', DistrictViewSet, basename='districts') -router.register(r'(?P[a-z]{2})/sensitivities', SensitivityViewSet, basename='sensitivities') +router.register(r"(?P[a-z]{2})/portal", PortalViewSet, basename="portal") +router.register(r"(?P[a-z]{2})/cities", CityViewSet, basename="cities") +router.register(r"(?P[a-z]{2})/districts", DistrictViewSet, basename="districts") +router.register( + r"(?P[a-z]{2})/sensitivities", SensitivityViewSet, basename="sensitivities" +) -app_name = 'api_portal' +app_name = "api_portal" _urlpatterns = [] if settings.API_SCHEMA: # pragma: no cover @@ -54,7 +84,7 @@ ) ] _urlpatterns += [ - path('version', GeoriviereVersionAPIView.as_view(), name='version'), - path('', include(router.urls)), + path("version", GeoriviereVersionAPIView.as_view(), name="version"), + path("", include(router.urls)), ] -urlpatterns = [path('api/portal/', include(_urlpatterns))] +urlpatterns = [path("api/portal/", include(_urlpatterns))] diff --git a/georiviere/portal/views/contribution.py b/georiviere/portal/views/contribution.py index a45579ca..a848cf0d 100644 --- a/georiviere/portal/views/contribution.py +++ b/georiviere/portal/views/contribution.py @@ -16,16 +16,23 @@ from djangorestframework_camel_case.render import CamelCaseJSONRenderer -from georiviere.contribution.models import Contribution +from georiviere.contribution.models import ( + Contribution, + CustomContributionType, + CustomContribution, +) from georiviere.main.models import Attachment, FileType from georiviere.main.renderers import GeoJSONRenderer -from georiviere.portal.serializers.contribution import (ContributionSchemaSerializer, - ContributionSerializer, ContributionGeojsonSerializer) - -from rest_framework import filters -from rest_framework import viewsets -from rest_framework import mixins -from rest_framework import renderers +from georiviere.portal.serializers.contribution import ( + ContributionSchemaSerializer, + ContributionSerializer, + ContributionGeojsonSerializer, + CustomContributionTypeSerializer, + CustomContributionSerializer, + CustomContributionSerializerGeoJSONSerializer, +) + +from rest_framework import filters, viewsets, mixins, renderers from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.pagination import LimitOffsetPagination @@ -36,23 +43,36 @@ logger = logging.getLogger(__name__) -class ContributionViewSet(mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, - viewsets.GenericViewSet): +class ContributionViewSet( + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet, +): model = Contribution - permission_classes = [AllowAny, ] + permission_classes = [ + AllowAny, + ] geojson_serializer_class = ContributionGeojsonSerializer serializer_class = ContributionSerializer parser_classes = (MultiPartParser, FormParser, JSONParser) pagination_class = LimitOffsetPagination - renderer_classes = [CamelCaseJSONRenderer, GeoJSONRenderer, ] + renderer_classes = [ + CamelCaseJSONRenderer, + GeoJSONRenderer, + ] filter_backends = [filters.OrderingFilter, filters.SearchFilter] # TODO: Fix search filter with IntegerField (choices). It might be possible using an annotate on this view. # search_fields = ['potential_damage__type', 'fauna_flora__type', 'quality__type', 'quantity__type', # 'landscape_element__type'] - @action(detail=False, url_name="json_schema", methods=['get'], - renderer_classes=[renderers.JSONRenderer], - serializer_class=ContributionSchemaSerializer) + @action( + detail=False, + url_name="json_schema", + methods=["get"], + renderer_classes=[renderers.JSONRenderer], + serializer_class=ContributionSchemaSerializer, + ) def json_schema(self, request, *args, **kwargs): serializer = self.get_serializer({}) return Response(serializer.data) @@ -62,24 +82,29 @@ def get_serializer_context(self): Extra context provided to the serializer class. """ context = super().get_serializer_context() - context['portal_pk'] = self.kwargs['portal_pk'] - translation.activate(self.kwargs['lang']) + context["portal_pk"] = self.kwargs["portal_pk"] + translation.activate(self.kwargs["lang"]) return context def get_queryset(self): - portal_pk = self.kwargs['portal_pk'] + portal_pk = self.kwargs["portal_pk"] queryset = Contribution.objects.filter(portal_id=portal_pk, published=True) queryset = queryset.exclude( - Q(potential_damage__isnull=True) & Q(fauna_flora__isnull=True) & Q(quantity__isnull=True) - & Q(quality__isnull=True) & Q(landscape_element__isnull=True) + Q(potential_damage__isnull=True) + & Q(fauna_flora__isnull=True) + & Q(quantity__isnull=True) + & Q(quality__isnull=True) + & Q(landscape_element__isnull=True) + ) + queryset = queryset.annotate( + geom_transformed=Transform(F("geom"), settings.API_SRID) ) - queryset = queryset.annotate(geom_transformed=Transform(F('geom'), settings.API_SRID)) return queryset def get_serializer_class(self): - """ Use specific Serializer for GeoJSON """ + """Use specific Serializer for GeoJSON""" renderer, media_type = self.perform_content_negotiation(self.request) - if getattr(renderer, 'format') == 'geojson': + if getattr(renderer, "format") == "geojson": return self.geojson_serializer_class return self.serializer_class @@ -88,9 +113,9 @@ def create(self, request, *args, **kwargs): for file in request._request.FILES.values(): attachment = Attachment( - filetype=FileType.objects.get_or_create(type=settings.CONTRIBUTION_FILETYPE)[ - 0 - ], + filetype=FileType.objects.get_or_create( + type=settings.CONTRIBUTION_FILETYPE + )[0], content_type=ContentType.objects.get_for_model(Contribution), object_id=response.data.get("id"), attachment_file=file, @@ -99,26 +124,35 @@ def create(self, request, *args, **kwargs): try: attachment.full_clean() # Check that file extension and mimetypes are allowed except ValidationError as e: - logger.error(f"Invalid attachment {name}{extension} for contribution {response.data.get('id')} : " - + str(e)) + logger.error( + f"Invalid attachment {name}{extension} for contribution {response.data.get('id')} : " + + str(e) + ) else: try: # Reencode file to bitmap then back to jpeg lfor safety if not os.path.exists(f"{settings.TMP_DIR}/contribution_file/"): os.mkdir(f"{settings.TMP_DIR}/contribution_file/") - tmp_bmp_path = os.path.join(f"{settings.TMP_DIR}/contribution_file/", f"{name}.bmp") - tmp_jpeg_path = os.path.join(f"{settings.TMP_DIR}/contribution_file/", f"{name}.jpeg") + tmp_bmp_path = os.path.join( + f"{settings.TMP_DIR}/contribution_file/", f"{name}.bmp" + ) + tmp_jpeg_path = os.path.join( + f"{settings.TMP_DIR}/contribution_file/", f"{name}.jpeg" + ) Image.open(file).save(tmp_bmp_path) Image.open(tmp_bmp_path).save(tmp_jpeg_path) - with open(tmp_jpeg_path, 'rb') as converted_file: - attachment.attachment_file = File(converted_file, name=f"{name}.jpeg") + with open(tmp_jpeg_path, "rb") as converted_file: + attachment.attachment_file = File( + converted_file, name=f"{name}.jpeg" + ) attachment.save() os.remove(tmp_bmp_path) os.remove(tmp_jpeg_path) except Exception as e: logger.error( - f"Failed to convert attachment {name}{extension} for report {response.data.get('id')}: " + str( - e)) + f"Failed to convert attachment {name}{extension} for report {response.data.get('id')}: " + + str(e) + ) if settings.SEND_REPORT_ACK and response.status_code == 201: send_mail( _("Georiviere : Contribution"), @@ -133,6 +167,61 @@ def create(self, request, *args, **kwargs): http://georiviere.fr""" ), settings.DEFAULT_FROM_EMAIL, - [json.loads(request.data.get("properties")).get('email_author'), ], + [ + json.loads(request.data.get("properties")).get("email_author"), + ], ) return response + + +class CustomContributionTypeViewSet( + mixins.RetrieveModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet, +): + queryset = CustomContributionType.objects.all().prefetch_related( + "stations", "fields" + ) + permission_classes = [ + AllowAny, + ] + serializer_class = CustomContributionTypeSerializer + + @action( + detail=True, + url_name="custom-contribution-create", + url_path="contributions", + methods=["post"], + renderer_classes=[renderers.JSONRenderer], + serializer_class=CustomContributionSerializer, + ) + def create_contribution(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + contribution = serializer.save() + return Response( + ContributionSerializer( + contribution, context=self.get_serializer_context() + ).data + ) + + @action( + detail=True, + url_name="custom-contribution-list", + url_path="contributions", + methods=["get"], + renderer_classes=[renderers.JSONRenderer, GeoJSONRenderer], + serializer_class=CustomContributionSerializer, + ) + def list_contributions(self, request, *args, **kwargs): + custom_type = self.get_object() + qs = CustomContribution.objects.with_type_values(custom_type) + + renderer, media_type = self.perform_content_negotiation(self.request) + if getattr(renderer, "format") == "geojson": + self.serializer_class = CustomContributionSerializerGeoJSONSerializer + qs = qs.annotate(geometry=Transform(F("geom"), settings.API_SRID)) + + serializer = self.get_serializer(qs, custom_type=custom_type, many=True) + + return Response(serializer.data) diff --git a/georiviere/proceeding/locale/en/LC_MESSAGES/django.po b/georiviere/proceeding/locale/en/LC_MESSAGES/django.po index 369ecfc3..f98f3019 100644 --- a/georiviere/proceeding/locale/en/LC_MESSAGES/django.po +++ b/georiviere/proceeding/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:15+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/proceeding/locale/fr/LC_MESSAGES/django.po b/georiviere/proceeding/locale/fr/LC_MESSAGES/django.po index 1cfa78ae..3c6e92dc 100644 --- a/georiviere/proceeding/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/proceeding/locale/fr/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:05+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: 2021-11-23 15:30+0000\n" "Last-Translator: Jean-Etienne Castagnede , 2021\n" "Language-Team: French (https://www.transifex.com/georiviere/teams/128005/" diff --git a/georiviere/river/locale/en/LC_MESSAGES/django.po b/georiviere/river/locale/en/LC_MESSAGES/django.po index 374ee70f..5bae68ba 100644 --- a/georiviere/river/locale/en/LC_MESSAGES/django.po +++ b/georiviere/river/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:16+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/river/locale/fr/LC_MESSAGES/django.po b/georiviere/river/locale/fr/LC_MESSAGES/django.po index ea0c1f51..d3372859 100644 --- a/georiviere/river/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/river/locale/fr/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:05+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: 2021-11-23 15:31+0000\n" "Last-Translator: Jean-Etienne Castagnede , 2021\n" "Language-Team: French (https://www.transifex.com/georiviere/teams/128005/" diff --git a/georiviere/settings/__init__.py b/georiviere/settings/__init__.py index 4daf8d0f..fc40a458 100644 --- a/georiviere/settings/__init__.py +++ b/georiviere/settings/__init__.py @@ -139,6 +139,7 @@ def construct_relative_path_mock(current_template_name, relative_name): 'mapentity', # mapentity should be placed after app declaring paperclip models 'leaflet', 'paperclip', + 'django_jsonform', 'crispy_forms', 'rest_framework', 'geotrek.altimetry', @@ -206,6 +207,11 @@ def construct_relative_path_mock(current_template_name, relative_name): 'mapentity.middleware.AutoLoginMiddleware', ] +LOCALE_PATHS = ( + # override locale + os.path.join(PROJECT_DIR, 'locales'), +) + ROOT_URLCONF = 'georiviere.urls' TEMPLATES = [ diff --git a/georiviere/studies/locale/en/LC_MESSAGES/django.po b/georiviere/studies/locale/en/LC_MESSAGES/django.po index 021d3de5..1b3e0bfc 100644 --- a/georiviere/studies/locale/en/LC_MESSAGES/django.po +++ b/georiviere/studies/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:16+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/studies/locale/fr/LC_MESSAGES/django.po b/georiviere/studies/locale/fr/LC_MESSAGES/django.po index d4079ad8..0a6f87e1 100644 --- a/georiviere/studies/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/studies/locale/fr/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:05+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: 2021-11-23 15:31+0000\n" "Last-Translator: Jean-Etienne Castagnede , 2021\n" "Language-Team: French (https://www.transifex.com/georiviere/teams/128005/" diff --git a/georiviere/valorization/locale/en/LC_MESSAGES/django.po b/georiviere/valorization/locale/en/LC_MESSAGES/django.po index d244a0e7..a8a46e5c 100644 --- a/georiviere/valorization/locale/en/LC_MESSAGES/django.po +++ b/georiviere/valorization/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:16+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/valorization/locale/fr/LC_MESSAGES/django.po b/georiviere/valorization/locale/fr/LC_MESSAGES/django.po index a79b6d08..044195b9 100644 --- a/georiviere/valorization/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/valorization/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:06+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -31,7 +31,7 @@ msgid "POI Category" msgstr "Catégorie POI" msgid "POI Categories" -msgstr "Catgéories POI" +msgstr "Catégories POI" msgid "Category" msgstr "Catégorie" diff --git a/georiviere/watershed/locale/en/LC_MESSAGES/django.po b/georiviere/watershed/locale/en/LC_MESSAGES/django.po index 7c0cda06..12689c32 100644 --- a/georiviere/watershed/locale/en/LC_MESSAGES/django.po +++ b/georiviere/watershed/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:16+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/watershed/locale/fr/LC_MESSAGES/django.po b/georiviere/watershed/locale/fr/LC_MESSAGES/django.po index 3cf35592..00ab3917 100644 --- a/georiviere/watershed/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/watershed/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-15 17:06+0000\n" +"POT-Creation-Date: 2024-04-12 19:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/requirements.in b/requirements.in index a4d9353d..22f972ec 100644 --- a/requirements.in +++ b/requirements.in @@ -14,6 +14,7 @@ sentry-sdk django-admin-ordering djangorestframework-camel-case jsonschema +django-jsonform # Use until latest version of geotrek (> 2.99.0) drf-spectacular diff --git a/requirements.txt b/requirements.txt index 3a3449a4..a45942d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -88,6 +88,7 @@ django==3.1.14 # django-filter # django-geojson # django-js-asset + # django-jsonform # django-leaflet # django-modeltranslation # django-mptt @@ -136,6 +137,8 @@ django-js-asset==2.0.0 # via # django-admin-ordering # django-mptt +django-jsonform==2.22.0 + # via -r requirements.in django-leaflet==0.19.post9 # via mapentity django-modelcluster==5.2 From a22c7a98ea4704e7abefe0340ddcb87892ced871 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 10:00:25 +0200 Subject: [PATCH 054/114] hotfix pillow --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a45942d9..bc2e80d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -254,7 +254,7 @@ persistent==4.7.0 # via # btrees # zodb -pillow==10.3.0 +pillow==9.5.0 # via # cairosvg # easy-thumbnails From e594723ca29ed6d0880669c3c24a388e99e0cdd3 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 13:53:39 +0200 Subject: [PATCH 055/114] improve data perfs --- .../description_detail_fragment.html | 10 ++-- georiviere/portal/serializers/river.py | 2 + georiviere/portal/views/contribution.py | 1 - georiviere/portal/views/river.py | 56 +++++++++++++------ georiviere/river/models.py | 15 +++-- 5 files changed, 54 insertions(+), 30 deletions(-) diff --git a/georiviere/description/templates/description/description_detail_fragment.html b/georiviere/description/templates/description/description_detail_fragment.html index 38c0dc95..f7013bc6 100644 --- a/georiviere/description/templates/description/description_detail_fragment.html +++ b/georiviere/description/templates/description/description_detail_fragment.html @@ -45,9 +45,9 @@

{% trans "Description" %}

{% if modelname != "status" %} - {% trans "Status" %} + {% trans "Statuses" %} - {% if object.status %} + {% if object.statuses %} {% valuelist object.status field="status_types" %} {% else %} {% valuelist object.statuses field="status_types" %} @@ -58,10 +58,10 @@

{% trans "Description" %}

{% if modelname != "morphology" %} - {% trans "Morphology" %} + {% trans "Morphologies" %} - {% if object.morphology %} - {% valuelist object.morphology field="name" %} + {% if object.morphologies %} + {% valuelist object.morphologies field="name" %} {% else %} {% valuelist object.morphologies field="name" %} {% endif %} diff --git a/georiviere/portal/serializers/river.py b/georiviere/portal/serializers/river.py index 9fdf35d0..6ef35198 100644 --- a/georiviere/portal/serializers/river.py +++ b/georiviere/portal/serializers/river.py @@ -1,3 +1,5 @@ +from rest_framework import serializers + from georiviere.river.models import Stream from georiviere.portal.serializers.main import AttachmentSerializer diff --git a/georiviere/portal/views/contribution.py b/georiviere/portal/views/contribution.py index a848cf0d..9874b1a1 100644 --- a/georiviere/portal/views/contribution.py +++ b/georiviere/portal/views/contribution.py @@ -223,5 +223,4 @@ def list_contributions(self, request, *args, **kwargs): qs = qs.annotate(geometry=Transform(F("geom"), settings.API_SRID)) serializer = self.get_serializer(qs, custom_type=custom_type, many=True) - return Response(serializer.data) diff --git a/georiviere/portal/views/river.py b/georiviere/portal/views/river.py index 9deca0a0..e14adc4c 100644 --- a/georiviere/portal/views/river.py +++ b/georiviere/portal/views/river.py @@ -1,41 +1,61 @@ -from georiviere.river.models import Stream from django.conf import settings -from django.db.models import F from django.contrib.gis.db.models.functions import Centroid, Transform - -from georiviere.portal.filters import SearchNoAccentFilter -from georiviere.portal.serializers.river import StreamGeojsonSerializer, StreamSerializer -from georiviere.main.renderers import GeoJSONRenderer - - +from django.db.models import F, Prefetch from djangorestframework_camel_case.render import CamelCaseJSONRenderer - -from rest_framework import filters, viewsets -from rest_framework import permissions as rest_permissions +from rest_framework import filters, viewsets, permissions as rest_permissions from rest_framework.pagination import LimitOffsetPagination +from rest_framework.renderers import BrowsableAPIRenderer + +from georiviere.decorators import view_cache_response_content, view_cache_latest +from georiviere.main.models import Attachment +from georiviere.main.renderers import GeoJSONRenderer +from georiviere.portal.filters import SearchNoAccentFilter +from georiviere.portal.serializers.river import ( + StreamGeojsonSerializer, + StreamSerializer, +) +from georiviere.river.models import Stream class StreamViewSet(viewsets.ReadOnlyModelViewSet): model = Stream geojson_serializer_class = StreamGeojsonSerializer serializer_class = StreamSerializer - renderer_classes = (CamelCaseJSONRenderer, GeoJSONRenderer, ) + renderer_classes = ( + BrowsableAPIRenderer, + CamelCaseJSONRenderer, + GeoJSONRenderer, + ) permission_classes = [rest_permissions.DjangoModelPermissionsOrAnonReadOnly] filter_backends = [filters.OrderingFilter, SearchNoAccentFilter] pagination_class = LimitOffsetPagination ordering_fields = ['name', 'date_insert'] search_fields = ['&name'] + ordering_fields = ["name", "date_insert"] + search_fields = ["&name"] + + return self.model + def get_queryset(self): - portal_pk = self.kwargs['portal_pk'] - queryset = Stream.objects.filter(portals__id=portal_pk) - queryset = queryset.annotate(geom_transformed=Transform(F('geom'), settings.API_SRID)).annotate( - centroid=Centroid('geom_transformed')) + portal_pk = self.kwargs["portal_pk"] + queryset = Stream.objects.filter(portals__id=portal_pk).prefetch_related( + Prefetch("attachments", + queryset=Attachment.objects.all().select_related('filetype'))) + queryset = queryset.annotate( + geom_transformed=Transform(F("geom"), settings.API_SRID) + ).annotate(centroid=Centroid("geom_transformed")) return queryset def get_serializer_class(self): - """ Use specific Serializer for GeoJSON """ + """Use specific Serializer for GeoJSON""" renderer, media_type = self.perform_content_negotiation(self.request) - if getattr(renderer, 'format') == 'geojson': + if getattr(renderer, "format") == "geojson": return self.geojson_serializer_class return self.serializer_class + + return f"stream-{self.kwargs['portal_pk']}" + + @view_cache_latest() + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) diff --git a/georiviere/river/models.py b/georiviere/river/models.py index af65cd85..37b50b2c 100644 --- a/georiviere/river/models.py +++ b/georiviere/river/models.py @@ -98,17 +98,20 @@ class Meta: verbose_name = _("Stream") verbose_name_plural = _("Streams") - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - for model_topology in self.model_topologies: - setattr(self, model_topology._meta.model_name, self.get_topology(model_topology._meta.model_name)) - def __str__(self): return self.name def is_public(self): return self.portals.exists() + @property + def statuses(self): + return self.get_topologies('status') + + @property + def morphologies(self): + return self.get_topologies('morphology') + def get_printcontext_with_other_objects(self, modelnames): maplayers = [ settings.LEAFLET_CONFIG['TILES'][0][0], @@ -167,7 +170,7 @@ def name_display(self): self, self) - def get_topology(self, value): + def get_topologies(self, value): topologies = self.topologies.filter(**{f'{value}__isnull': False}) topologies = [getattr(topology, value) for topology in topologies] return topologies From 7509c0a983b406364cb6d9a78e775cdd54cc3844 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 13:58:23 +0200 Subject: [PATCH 056/114] improve data perfs --- georiviere/portal/views/river.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/georiviere/portal/views/river.py b/georiviere/portal/views/river.py index e14adc4c..e36d7d9d 100644 --- a/georiviere/portal/views/river.py +++ b/georiviere/portal/views/river.py @@ -29,14 +29,9 @@ class StreamViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = [rest_permissions.DjangoModelPermissionsOrAnonReadOnly] filter_backends = [filters.OrderingFilter, SearchNoAccentFilter] pagination_class = LimitOffsetPagination - ordering_fields = ['name', 'date_insert'] - search_fields = ['&name'] ordering_fields = ["name", "date_insert"] search_fields = ["&name"] - return self.model - - def get_queryset(self): portal_pk = self.kwargs["portal_pk"] queryset = Stream.objects.filter(portals__id=portal_pk).prefetch_related( @@ -54,8 +49,10 @@ def get_serializer_class(self): return self.geojson_serializer_class return self.serializer_class + def view_cache_key(self): return f"stream-{self.kwargs['portal_pk']}" @view_cache_latest() + @view_cache_response_content() def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) From 815c8b434b6f5ca2dfeb04aec47497ce4d453e4d Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 14:02:02 +0200 Subject: [PATCH 057/114] improve data perfs --- georiviere/portal/views/river.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/georiviere/portal/views/river.py b/georiviere/portal/views/river.py index e36d7d9d..c27a463a 100644 --- a/georiviere/portal/views/river.py +++ b/georiviere/portal/views/river.py @@ -6,7 +6,7 @@ from rest_framework.pagination import LimitOffsetPagination from rest_framework.renderers import BrowsableAPIRenderer -from georiviere.decorators import view_cache_response_content, view_cache_latest +#from georiviere.decorators import view_cache_response_content, view_cache_latest from georiviere.main.models import Attachment from georiviere.main.renderers import GeoJSONRenderer from georiviere.portal.filters import SearchNoAccentFilter @@ -32,6 +32,9 @@ class StreamViewSet(viewsets.ReadOnlyModelViewSet): ordering_fields = ["name", "date_insert"] search_fields = ["&name"] + #def get_model(self): + # return self.model + def get_queryset(self): portal_pk = self.kwargs["portal_pk"] queryset = Stream.objects.filter(portals__id=portal_pk).prefetch_related( @@ -52,7 +55,7 @@ def get_serializer_class(self): def view_cache_key(self): return f"stream-{self.kwargs['portal_pk']}" - @view_cache_latest() - @view_cache_response_content() + #@view_cache_latest() + #@view_cache_response_content() def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) From b757b8ac3a47397e4853afe863c97a2b190dcdef Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 16:53:20 +0200 Subject: [PATCH 058/114] add custom contrib type description --- .../migrations/0016_auto_20240423_1452.py | 22 +++++++++++++++++++ georiviere/contribution/models/__init__.py | 1 + georiviere/portal/serializers/contribution.py | 2 +- 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 georiviere/contribution/migrations/0016_auto_20240423_1452.py diff --git a/georiviere/contribution/migrations/0016_auto_20240423_1452.py b/georiviere/contribution/migrations/0016_auto_20240423_1452.py new file mode 100644 index 00000000..f06e94cb --- /dev/null +++ b/georiviere/contribution/migrations/0016_auto_20240423_1452.py @@ -0,0 +1,22 @@ +# Generated by Django 3.1.14 on 2024-04-23 14:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contribution', '0015_auto_20240412_2038'), + ] + + operations = [ + migrations.AlterModelOptions( + name='customcontributiontype', + options={'ordering': ('label',), 'verbose_name': 'Custom contribution type', 'verbose_name_plural': 'Custom contribution types'}, + ), + migrations.AddField( + model_name='customcontributiontype', + name='description', + field=models.CharField(blank=True, default='', max_length=512, verbose_name='Description'), + ), + ] diff --git a/georiviere/contribution/models/__init__.py b/georiviere/contribution/models/__init__.py index c4b0b0ed..986dff93 100644 --- a/georiviere/contribution/models/__init__.py +++ b/georiviere/contribution/models/__init__.py @@ -613,6 +613,7 @@ def __str__(self): class CustomContributionType(models.Model): label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) + description = models.CharField(max_length=512, verbose_name=_("Description"), blank=True, default="") stations = models.ManyToManyField('observations.Station', verbose_name=_("Stations"), blank=True) def __str__(self): diff --git a/georiviere/portal/serializers/contribution.py b/georiviere/portal/serializers/contribution.py index 6f9aaa77..6d9c3238 100644 --- a/georiviere/portal/serializers/contribution.py +++ b/georiviere/portal/serializers/contribution.py @@ -179,7 +179,7 @@ class Meta: class CustomContributionTypeSerializer(serializers.ModelSerializer): class Meta: model = CustomContributionType - fields = ("id", "label", "json_schema_form", "stations") + fields = ("id", "label", "description", "json_schema_form", "stations") class CustomContributionSerializer(serializers.ModelSerializer): From 85495919fcf24a230c4e86b407f63efff1d8ab7d Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 17:43:21 +0200 Subject: [PATCH 059/114] add station endpoint --- .../migrations/0017_auto_20240423_1542.py | 19 ++++++++++++ georiviere/contribution/models/__init__.py | 2 +- georiviere/portal/serializers/observations.py | 31 +++++++++++++++++++ georiviere/portal/urls.py | 5 +++ georiviere/portal/views/mixins.py | 22 +++++++++++++ georiviere/portal/views/observations.py | 23 ++++++++++++++ georiviere/portal/views/river.py | 8 ++--- georiviere/portal/views/valorization.py | 19 ++---------- 8 files changed, 107 insertions(+), 22 deletions(-) create mode 100644 georiviere/contribution/migrations/0017_auto_20240423_1542.py create mode 100644 georiviere/portal/serializers/observations.py create mode 100644 georiviere/portal/views/mixins.py create mode 100644 georiviere/portal/views/observations.py diff --git a/georiviere/contribution/migrations/0017_auto_20240423_1542.py b/georiviere/contribution/migrations/0017_auto_20240423_1542.py new file mode 100644 index 00000000..3d9270a3 --- /dev/null +++ b/georiviere/contribution/migrations/0017_auto_20240423_1542.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.14 on 2024-04-23 15:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('observations', '0023_auto_20230220_1703'), + ('contribution', '0016_auto_20240423_1452'), + ] + + operations = [ + migrations.AlterField( + model_name='customcontributiontype', + name='stations', + field=models.ManyToManyField(blank=True, related_name='custom_contribution_types', to='observations.Station', verbose_name='Stations'), + ), + ] diff --git a/georiviere/contribution/models/__init__.py b/georiviere/contribution/models/__init__.py index 986dff93..c3b48e8e 100644 --- a/georiviere/contribution/models/__init__.py +++ b/georiviere/contribution/models/__init__.py @@ -614,7 +614,7 @@ def __str__(self): class CustomContributionType(models.Model): label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) description = models.CharField(max_length=512, verbose_name=_("Description"), blank=True, default="") - stations = models.ManyToManyField('observations.Station', verbose_name=_("Stations"), blank=True) + stations = models.ManyToManyField('observations.Station', verbose_name=_("Stations"), related_name='custom_contribution_types', blank=True) def __str__(self): return self.label diff --git a/georiviere/portal/serializers/observations.py b/georiviere/portal/serializers/observations.py new file mode 100644 index 00000000..7602c007 --- /dev/null +++ b/georiviere/portal/serializers/observations.py @@ -0,0 +1,31 @@ +from rest_framework.serializers import ModelSerializer +from rest_framework_gis import serializers as geo_serializers + +from georiviere.observations.models import Station + + +class StationMixin: + geometry = geo_serializers.GeometryField( + read_only=True, precision=7, source="geom_transformed" + ) + + class Meta: + model = Station + fields = ( + "id", + "code", + "label", + "description", + "custom_contribution_types", + ) + + +class StationGeojsonSerializer(StationMixin, geo_serializers.GeoFeatureModelSerializer): + class Meta(StationMixin.Meta): + geo_field = "geometry" + id_field = False + + +class StationSerializer(StationMixin, ModelSerializer): + class Meta(StationMixin.Meta): + pass diff --git a/georiviere/portal/urls.py b/georiviere/portal/urls.py index fa5359c1..1bc4c459 100644 --- a/georiviere/portal/urls.py +++ b/georiviere/portal/urls.py @@ -8,6 +8,7 @@ ContributionViewSet, CustomContributionTypeViewSet, ) +from georiviere.portal.views.observations import StationViewSet from georiviere.portal.views.portal import PortalViewSet from georiviere.portal.views.river import StreamViewSet from georiviere.portal.views.sensitivity import SensitivityViewSet @@ -34,6 +35,10 @@ r"(?P[a-z]{2})/(?P\d+)/streams", StreamViewSet, basename="streams" ) +router.register( + r"(?P[a-z]{2})/(?P\d+)/stations", StationViewSet, basename="stations" +) + router.register( r"(?P[a-z]{2})/(?P\d+)/flatpages", FlatPageViewSet, diff --git a/georiviere/portal/views/mixins.py b/georiviere/portal/views/mixins.py new file mode 100644 index 00000000..9d9385a5 --- /dev/null +++ b/georiviere/portal/views/mixins.py @@ -0,0 +1,22 @@ +from djangorestframework_camel_case.render import CamelCaseJSONRenderer +from rest_framework import renderers, permissions +from rest_framework.pagination import LimitOffsetPagination + +from georiviere.main.renderers import GeoJSONRenderer + + +class GeoriviereAPIMixin: + renderer_classes = ( + renderers.BrowsableAPIRenderer, + CamelCaseJSONRenderer, + GeoJSONRenderer, + ) + permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] + pagination_class = LimitOffsetPagination + + def get_serializer_class(self): + """Use specific Serializer for GeoJSON""" + renderer, media_type = self.perform_content_negotiation(self.request) + if getattr(renderer, "format") == "geojson": + return self.geojson_serializer_class + return self.serializer_class diff --git a/georiviere/portal/views/observations.py b/georiviere/portal/views/observations.py new file mode 100644 index 00000000..b06a185c --- /dev/null +++ b/georiviere/portal/views/observations.py @@ -0,0 +1,23 @@ +from django.conf import settings +from django.db.models import Transform, F +from rest_framework import viewsets + +from georiviere.observations.models import Station +from georiviere.portal.serializers.observations import ( + StationGeojsonSerializer, + StationSerializer, +) +from georiviere.portal.views.mixins import GeoriviereAPIMixin + + +class StationViewSet(GeoriviereAPIMixin, viewsets.ReadOnlyModelViewSet): + serializer_class = StationSerializer + geojson_serializer_class = StationGeojsonSerializer + + def get_queryset(self): + portal_pk = self.kwargs["portal_pk"] + return ( + Station.objects.filter(custom_contribution_types__portal_id=portal_pk) + .distinct() + .annotate(geom_transformed=Transform(F("geom"), settings.API_SRID)) + ) diff --git a/georiviere/portal/views/river.py b/georiviere/portal/views/river.py index c27a463a..2f32cf76 100644 --- a/georiviere/portal/views/river.py +++ b/georiviere/portal/views/river.py @@ -3,10 +3,9 @@ from django.db.models import F, Prefetch from djangorestframework_camel_case.render import CamelCaseJSONRenderer from rest_framework import filters, viewsets, permissions as rest_permissions -from rest_framework.pagination import LimitOffsetPagination from rest_framework.renderers import BrowsableAPIRenderer -#from georiviere.decorators import view_cache_response_content, view_cache_latest +from georiviere.decorators import view_cache_response_content, view_cache_latest from georiviere.main.models import Attachment from georiviere.main.renderers import GeoJSONRenderer from georiviere.portal.filters import SearchNoAccentFilter @@ -14,10 +13,11 @@ StreamGeojsonSerializer, StreamSerializer, ) +from georiviere.portal.views.mixins import GeoriviereAPIMixin from georiviere.river.models import Stream -class StreamViewSet(viewsets.ReadOnlyModelViewSet): +class StreamViewSet(GeoriviereAPIMixin, viewsets.ReadOnlyModelViewSet): model = Stream geojson_serializer_class = StreamGeojsonSerializer serializer_class = StreamSerializer @@ -28,7 +28,7 @@ class StreamViewSet(viewsets.ReadOnlyModelViewSet): ) permission_classes = [rest_permissions.DjangoModelPermissionsOrAnonReadOnly] filter_backends = [filters.OrderingFilter, SearchNoAccentFilter] - pagination_class = LimitOffsetPagination + ordering_fields = ["name", "date_insert"] search_fields = ["&name"] diff --git a/georiviere/portal/views/valorization.py b/georiviere/portal/views/valorization.py index 00a12e4b..7d5ca110 100644 --- a/georiviere/portal/views/valorization.py +++ b/georiviere/portal/views/valorization.py @@ -4,26 +4,18 @@ from django.shortcuts import get_object_or_404 from georiviere.portal.serializers.valorization import POIGeojsonSerializer, POISerializer -from georiviere.main.renderers import GeoJSONRenderer +from georiviere.portal.views.mixins import GeoriviereAPIMixin from georiviere.valorization.models import POI, POICategory from rest_framework import viewsets, filters -from rest_framework.permissions import AllowAny from rest_framework.decorators import action -from rest_framework.pagination import LimitOffsetPagination from rest_framework.response import Response -from djangorestframework_camel_case.render import CamelCaseJSONRenderer - - -class POIViewSet(viewsets.ReadOnlyModelViewSet): +class POIViewSet(GeoriviereAPIMixin, viewsets.ReadOnlyModelViewSet): geojson_serializer_class = POIGeojsonSerializer serializer_class = POISerializer - permission_classes = [AllowAny, ] - renderer_classes = [CamelCaseJSONRenderer, GeoJSONRenderer] filter_backends = [filters.OrderingFilter, filters.SearchFilter] - pagination_class = LimitOffsetPagination ordering_fields = ['name', 'date_insert'] search_fields = ['name', 'type__label', 'type__category__label'] @@ -33,13 +25,6 @@ def get_queryset(self): queryset = queryset.filter(portals__id=portal_pk).annotate(geom_transformed=Transform(F('geom'), settings.API_SRID)) return queryset - def get_serializer_class(self): - """ Use specific Serializer for GeoJSON """ - renderer, media_type = self.perform_content_negotiation(self.request) - if getattr(renderer, 'format') == 'geojson': - return self.geojson_serializer_class - return self.serializer_class - @action(detail=False, methods=['get'], permission_classes=[], url_path=r'category/(?P\d+)', url_name='category') def category(self, request, *args, **kwargs): From ba2d0f150c0a64b5d16bad86ca9887dd3787f1d4 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 17:46:05 +0200 Subject: [PATCH 060/114] add station endpoint --- georiviere/portal/views/river.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/georiviere/portal/views/river.py b/georiviere/portal/views/river.py index 2f32cf76..7a0a11a8 100644 --- a/georiviere/portal/views/river.py +++ b/georiviere/portal/views/river.py @@ -5,7 +5,7 @@ from rest_framework import filters, viewsets, permissions as rest_permissions from rest_framework.renderers import BrowsableAPIRenderer -from georiviere.decorators import view_cache_response_content, view_cache_latest +#from georiviere.decorators import view_cache_response_content, view_cache_latest from georiviere.main.models import Attachment from georiviere.main.renderers import GeoJSONRenderer from georiviere.portal.filters import SearchNoAccentFilter @@ -32,8 +32,8 @@ class StreamViewSet(GeoriviereAPIMixin, viewsets.ReadOnlyModelViewSet): ordering_fields = ["name", "date_insert"] search_fields = ["&name"] - #def get_model(self): - # return self.model + def get_model(self): + return self.model def get_queryset(self): portal_pk = self.kwargs["portal_pk"] From caef99d7cd3e3b3c0dd76f9d63eb016c2dcee144 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 17:49:10 +0200 Subject: [PATCH 061/114] add station endpoint --- georiviere/portal/views/observations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/georiviere/portal/views/observations.py b/georiviere/portal/views/observations.py index b06a185c..0ff8cb3a 100644 --- a/georiviere/portal/views/observations.py +++ b/georiviere/portal/views/observations.py @@ -17,7 +17,7 @@ class StationViewSet(GeoriviereAPIMixin, viewsets.ReadOnlyModelViewSet): def get_queryset(self): portal_pk = self.kwargs["portal_pk"] return ( - Station.objects.filter(custom_contribution_types__portal_id=portal_pk) + Station.objects.filter(custom_contribution_types__portal__id=portal_pk) .distinct() .annotate(geom_transformed=Transform(F("geom"), settings.API_SRID)) ) From 6fa15a84609413e02af4c43edcc4428dd53c3512 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 17:54:26 +0200 Subject: [PATCH 062/114] handle 500 --- georiviere/main/views.py | 5 +++++ georiviere/urls.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/georiviere/main/views.py b/georiviere/main/views.py index 23c55cd5..a4913b4f 100644 --- a/georiviere/main/views.py +++ b/georiviere/main/views.py @@ -1,11 +1,16 @@ from django.contrib.auth.decorators import login_required from django.conf import settings +from django.http import HttpResponse from django.shortcuts import redirect from django.urls import reverse from mapentity.registry import registry from mapentity import views as mapentity_views +def handler500(request, *args, **kwargs): + return HttpResponse(status=500) + + @login_required(login_url='login') def home(request): last = request.session.get('last_list') # set in MapEntityList diff --git a/georiviere/urls.py b/georiviere/urls.py index bbb94289..ba81c48b 100644 --- a/georiviere/urls.py +++ b/georiviere/urls.py @@ -12,6 +12,8 @@ admin.autodiscover() +handler500 = 'georiviere.main.views.handler500' + urlpatterns = [ path('', home, name='home'), path('login/', views.LoginView.as_view(), name='login'), From 43cebd2002b9552d35517bf0d552bea17b9e3b83 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 17:57:08 +0200 Subject: [PATCH 063/114] handle 500 --- georiviere/main/views.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/georiviere/main/views.py b/georiviere/main/views.py index a4913b4f..aff65fc5 100644 --- a/georiviere/main/views.py +++ b/georiviere/main/views.py @@ -5,9 +5,14 @@ from django.urls import reverse from mapentity.registry import registry from mapentity import views as mapentity_views +import logging -def handler500(request, *args, **kwargs): +logger = logging.getLogger(__name__) + + +def handler500(request, exception, *args, **kwargs): + logger.error('Internal Server Error: %s', str(exception)) return HttpResponse(status=500) From 2eb0b43c88d1af1190c00a8dc8f466b9a2b3693c Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 18:00:52 +0200 Subject: [PATCH 064/114] handle 500 --- georiviere/main/views.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/georiviere/main/views.py b/georiviere/main/views.py index aff65fc5..22a67709 100644 --- a/georiviere/main/views.py +++ b/georiviere/main/views.py @@ -1,6 +1,6 @@ from django.contrib.auth.decorators import login_required from django.conf import settings -from django.http import HttpResponse +from django.http import HttpResponse, HttpResponseServerError from django.shortcuts import redirect from django.urls import reverse from mapentity.registry import registry @@ -10,30 +10,30 @@ logger = logging.getLogger(__name__) +def handler500(request, *args, **kwargs): + return HttpResponseServerError() -def handler500(request, exception, *args, **kwargs): - logger.error('Internal Server Error: %s', str(exception)) - return HttpResponse(status=500) - -@login_required(login_url='login') +@login_required(login_url="login") def home(request): - last = request.session.get('last_list') # set in MapEntityList + last = request.session.get("last_list") # set in MapEntityList for entity in registry.entities: - if reverse(entity.url_list) == last and request.user.has_perm(entity.model.get_permission_codename('list')): + if reverse(entity.url_list) == last and request.user.has_perm( + entity.model.get_permission_codename("list") + ): return redirect(entity.url_list) for entity in registry.entities: - if entity.menu and request.user.has_perm(entity.model.get_permission_codename('list')): + if entity.menu and request.user.has_perm( + entity.model.get_permission_codename("list") + ): return redirect(entity.url_list) - return redirect('river:river_list') + return redirect("river:river_list") class JSSettings(mapentity_views.JSSettings): def get_context_data(self): dictsettings = super().get_context_data() - dictsettings['map'].update( - snap_distance=settings.SNAP_DISTANCE - ) + dictsettings["map"].update(snap_distance=settings.SNAP_DISTANCE) return dictsettings @@ -42,6 +42,7 @@ class FormsetMixin: WARNING: this will only work for a single formset in form TODO: move this to Mapentity """ + context_name = None formset_class = None @@ -61,8 +62,8 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) if self.request.POST: context[self.context_name] = self.formset_class( - self.request.POST, instance=self.object) + self.request.POST, instance=self.object + ) else: - context[self.context_name] = self.formset_class( - instance=self.object) + context[self.context_name] = self.formset_class(instance=self.object) return context From ed5f0b11dbc03ceec236d3887bc18e20d590fe21 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 18:02:28 +0200 Subject: [PATCH 065/114] handle 500 --- georiviere/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/georiviere/urls.py b/georiviere/urls.py index ba81c48b..6ae95539 100644 --- a/georiviere/urls.py +++ b/georiviere/urls.py @@ -12,7 +12,7 @@ admin.autodiscover() -handler500 = 'georiviere.main.views.handler500' +#handler500 = 'georiviere.main.views.handler500' urlpatterns = [ path('', home, name='home'), From 8e401fd665909b425d14c543c9f7c5366cdf627b Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 18:08:03 +0200 Subject: [PATCH 066/114] fix station endpoint --- georiviere/portal/views/observations.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/georiviere/portal/views/observations.py b/georiviere/portal/views/observations.py index 0ff8cb3a..133080b6 100644 --- a/georiviere/portal/views/observations.py +++ b/georiviere/portal/views/observations.py @@ -15,9 +15,10 @@ class StationViewSet(GeoriviereAPIMixin, viewsets.ReadOnlyModelViewSet): geojson_serializer_class = StationGeojsonSerializer def get_queryset(self): - portal_pk = self.kwargs["portal_pk"] + # portal_pk = self.kwargs["portal_pk"] return ( - Station.objects.filter(custom_contribution_types__portal__id=portal_pk) + # Station.objects.filter(custom_contribution_types__portal__id=portal_pk) + Station.objects.all() .distinct() .annotate(geom_transformed=Transform(F("geom"), settings.API_SRID)) ) From 4ed1f173515f487bd101ac6c7b16d71cd0e612f9 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 18:16:07 +0200 Subject: [PATCH 067/114] fix station endpoint --- georiviere/portal/views/observations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/georiviere/portal/views/observations.py b/georiviere/portal/views/observations.py index 133080b6..716a8492 100644 --- a/georiviere/portal/views/observations.py +++ b/georiviere/portal/views/observations.py @@ -1,5 +1,6 @@ from django.conf import settings -from django.db.models import Transform, F +from django.contrib.gis.db.models.functions import Transform +from django.db.models import F from rest_framework import viewsets from georiviere.observations.models import Station From aa8640b86bda4d5bf3dd9cb482071676a930a5a6 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 18:19:22 +0200 Subject: [PATCH 068/114] fix station endpoint --- georiviere/portal/serializers/observations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/georiviere/portal/serializers/observations.py b/georiviere/portal/serializers/observations.py index 7602c007..89a11c70 100644 --- a/georiviere/portal/serializers/observations.py +++ b/georiviere/portal/serializers/observations.py @@ -17,6 +17,7 @@ class Meta: "label", "description", "custom_contribution_types", + "geometry" ) From ebf2452050a734fff19902adc36204b448660cd6 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 18:22:07 +0200 Subject: [PATCH 069/114] fix station endpoint --- georiviere/portal/serializers/observations.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/georiviere/portal/serializers/observations.py b/georiviere/portal/serializers/observations.py index 89a11c70..778c23cb 100644 --- a/georiviere/portal/serializers/observations.py +++ b/georiviere/portal/serializers/observations.py @@ -17,13 +17,17 @@ class Meta: "label", "description", "custom_contribution_types", - "geometry" + "geometry", ) class StationGeojsonSerializer(StationMixin, geo_serializers.GeoFeatureModelSerializer): + geom_transformed = geo_serializers.GeometryField( + read_only=True, precision=7 + ) + class Meta(StationMixin.Meta): - geo_field = "geometry" + geo_field = "geom_transformed" id_field = False From 17e4d9b621976ea2be52ebb618020457d6edfe93 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 18:29:11 +0200 Subject: [PATCH 070/114] fix station endpoint --- georiviere/portal/serializers/observations.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/georiviere/portal/serializers/observations.py b/georiviere/portal/serializers/observations.py index 778c23cb..c305e18f 100644 --- a/georiviere/portal/serializers/observations.py +++ b/georiviere/portal/serializers/observations.py @@ -22,12 +22,8 @@ class Meta: class StationGeojsonSerializer(StationMixin, geo_serializers.GeoFeatureModelSerializer): - geom_transformed = geo_serializers.GeometryField( - read_only=True, precision=7 - ) - class Meta(StationMixin.Meta): - geo_field = "geom_transformed" + geo_field = "geometry" id_field = False From 6c610f0d73e060299a88870bd02565c65f1dc4ee Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 18:32:18 +0200 Subject: [PATCH 071/114] fix station endpoint --- georiviere/portal/serializers/observations.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/georiviere/portal/serializers/observations.py b/georiviere/portal/serializers/observations.py index c305e18f..a3baf94f 100644 --- a/georiviere/portal/serializers/observations.py +++ b/georiviere/portal/serializers/observations.py @@ -22,9 +22,17 @@ class Meta: class StationGeojsonSerializer(StationMixin, geo_serializers.GeoFeatureModelSerializer): - class Meta(StationMixin.Meta): + class Meta: geo_field = "geometry" id_field = False + fields = ( + "id", + "code", + "label", + "description", + "custom_contribution_types", + "geometry", + ) class StationSerializer(StationMixin, ModelSerializer): From 9bab6e927a1047cffcf8c275b1eb5332fc3db692 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 18:32:28 +0200 Subject: [PATCH 072/114] fix station endpoint --- georiviere/portal/serializers/observations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/georiviere/portal/serializers/observations.py b/georiviere/portal/serializers/observations.py index a3baf94f..77079337 100644 --- a/georiviere/portal/serializers/observations.py +++ b/georiviere/portal/serializers/observations.py @@ -23,6 +23,7 @@ class Meta: class StationGeojsonSerializer(StationMixin, geo_serializers.GeoFeatureModelSerializer): class Meta: + model = Station geo_field = "geometry" id_field = False fields = ( From a43f58472930ecd4d9a142cd00cbed81f9e41369 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 18:38:06 +0200 Subject: [PATCH 073/114] fix station endpoint --- georiviere/portal/serializers/observations.py | 4 ++++ georiviere/portal/views/observations.py | 8 ++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/georiviere/portal/serializers/observations.py b/georiviere/portal/serializers/observations.py index 77079337..1bd026a5 100644 --- a/georiviere/portal/serializers/observations.py +++ b/georiviere/portal/serializers/observations.py @@ -22,6 +22,10 @@ class Meta: class StationGeojsonSerializer(StationMixin, geo_serializers.GeoFeatureModelSerializer): + geometry = geo_serializers.GeometryField( + read_only=True, precision=7, source="geom_transformed" + ) + class Meta: model = Station geo_field = "geometry" diff --git a/georiviere/portal/views/observations.py b/georiviere/portal/views/observations.py index 716a8492..e38a5632 100644 --- a/georiviere/portal/views/observations.py +++ b/georiviere/portal/views/observations.py @@ -17,9 +17,5 @@ class StationViewSet(GeoriviereAPIMixin, viewsets.ReadOnlyModelViewSet): def get_queryset(self): # portal_pk = self.kwargs["portal_pk"] - return ( - # Station.objects.filter(custom_contribution_types__portal__id=portal_pk) - Station.objects.all() - .distinct() - .annotate(geom_transformed=Transform(F("geom"), settings.API_SRID)) - ) + # Station.objects.filter(custom_contribution_types__portal__id=portal_pk) + return Station.objects.all().annotate(geom_transformed=Transform(F("geom"), settings.API_SRID)) From 444c12535f0f52dde330990e3fbc83941235a085 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Tue, 23 Apr 2024 18:41:31 +0200 Subject: [PATCH 074/114] fix station endpoint --- georiviere/portal/serializers/observations.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/georiviere/portal/serializers/observations.py b/georiviere/portal/serializers/observations.py index 1bd026a5..abb9018e 100644 --- a/georiviere/portal/serializers/observations.py +++ b/georiviere/portal/serializers/observations.py @@ -4,13 +4,15 @@ from georiviere.observations.models import Station -class StationMixin: +class StationGeojsonSerializer(geo_serializers.GeoFeatureModelSerializer): geometry = geo_serializers.GeometryField( read_only=True, precision=7, source="geom_transformed" ) class Meta: model = Station + geo_field = "geometry" + id_field = False fields = ( "id", "code", @@ -21,15 +23,13 @@ class Meta: ) -class StationGeojsonSerializer(StationMixin, geo_serializers.GeoFeatureModelSerializer): +class StationSerializer(ModelSerializer): geometry = geo_serializers.GeometryField( read_only=True, precision=7, source="geom_transformed" ) class Meta: model = Station - geo_field = "geometry" - id_field = False fields = ( "id", "code", @@ -38,8 +38,3 @@ class Meta: "custom_contribution_types", "geometry", ) - - -class StationSerializer(StationMixin, ModelSerializer): - class Meta(StationMixin.Meta): - pass From 0a9ad8698e695b51b85ec0d2c4667eb3439067a0 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 24 Apr 2024 08:43:48 +0200 Subject: [PATCH 075/114] revert default json --- georiviere/portal/views/river.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/georiviere/portal/views/river.py b/georiviere/portal/views/river.py index 7a0a11a8..1ad570d4 100644 --- a/georiviere/portal/views/river.py +++ b/georiviere/portal/views/river.py @@ -25,7 +25,7 @@ class StreamViewSet(GeoriviereAPIMixin, viewsets.ReadOnlyModelViewSet): BrowsableAPIRenderer, CamelCaseJSONRenderer, GeoJSONRenderer, - ) + ) if settings.DEBUG else (CamelCaseJSONRenderer, GeoJSONRenderer) permission_classes = [rest_permissions.DjangoModelPermissionsOrAnonReadOnly] filter_backends = [filters.OrderingFilter, SearchNoAccentFilter] From 59bc7631d83d689218347d374c204df9c5c577cc Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 24 Apr 2024 10:12:05 +0200 Subject: [PATCH 076/114] fix and explicit default json in api responses --- georiviere/portal/serializers/map.py | 2 +- georiviere/portal/views/river.py | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/georiviere/portal/serializers/map.py b/georiviere/portal/serializers/map.py index c1de1a4a..2f33f0a5 100644 --- a/georiviere/portal/serializers/map.py +++ b/georiviere/portal/serializers/map.py @@ -58,7 +58,7 @@ def get_url(self, obj): if layer_type[0] not in ['pois', 'streams', 'contributions', 'sensitivities']: return None # TODO: Make lang dynamic - reverse_kwargs = {'lang': 'fr'} + reverse_kwargs = {'lang': 'fr', 'format': 'json' } if layer_type[0] in ['watersheds', 'pois', 'streams', 'contributions']: reverse_kwargs['portal_pk'] = obj.portal.pk return reverse(f'api_portal:{layer_type[0]}-list', kwargs=reverse_kwargs) diff --git a/georiviere/portal/views/river.py b/georiviere/portal/views/river.py index 1ad570d4..1031fbeb 100644 --- a/georiviere/portal/views/river.py +++ b/georiviere/portal/views/river.py @@ -5,7 +5,7 @@ from rest_framework import filters, viewsets, permissions as rest_permissions from rest_framework.renderers import BrowsableAPIRenderer -#from georiviere.decorators import view_cache_response_content, view_cache_latest +from georiviere.decorators import view_cache_response_content, view_cache_latest from georiviere.main.models import Attachment from georiviere.main.renderers import GeoJSONRenderer from georiviere.portal.filters import SearchNoAccentFilter @@ -21,11 +21,6 @@ class StreamViewSet(GeoriviereAPIMixin, viewsets.ReadOnlyModelViewSet): model = Stream geojson_serializer_class = StreamGeojsonSerializer serializer_class = StreamSerializer - renderer_classes = ( - BrowsableAPIRenderer, - CamelCaseJSONRenderer, - GeoJSONRenderer, - ) if settings.DEBUG else (CamelCaseJSONRenderer, GeoJSONRenderer) permission_classes = [rest_permissions.DjangoModelPermissionsOrAnonReadOnly] filter_backends = [filters.OrderingFilter, SearchNoAccentFilter] From b0c0cf2e9b4c7bd59617b49de82a8efa028604f6 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 24 Apr 2024 10:16:51 +0200 Subject: [PATCH 077/114] fix and explicit default json in api responses --- georiviere/portal/views/river.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/georiviere/portal/views/river.py b/georiviere/portal/views/river.py index 1031fbeb..2792ddab 100644 --- a/georiviere/portal/views/river.py +++ b/georiviere/portal/views/river.py @@ -1,13 +1,10 @@ from django.conf import settings from django.contrib.gis.db.models.functions import Centroid, Transform from django.db.models import F, Prefetch -from djangorestframework_camel_case.render import CamelCaseJSONRenderer from rest_framework import filters, viewsets, permissions as rest_permissions -from rest_framework.renderers import BrowsableAPIRenderer -from georiviere.decorators import view_cache_response_content, view_cache_latest +#from georiviere.decorators import view_cache_response_content, view_cache_latest from georiviere.main.models import Attachment -from georiviere.main.renderers import GeoJSONRenderer from georiviere.portal.filters import SearchNoAccentFilter from georiviere.portal.serializers.river import ( StreamGeojsonSerializer, From c39dba460b8a97f16fb5ab5fa0d6f63e00f51904 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 24 Apr 2024 10:19:57 +0200 Subject: [PATCH 078/114] fix and explicit default json in api responses --- georiviere/portal/views/mixins.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/georiviere/portal/views/mixins.py b/georiviere/portal/views/mixins.py index 9d9385a5..d6fe70fe 100644 --- a/georiviere/portal/views/mixins.py +++ b/georiviere/portal/views/mixins.py @@ -1,3 +1,4 @@ +from django.conf import settings from djangorestframework_camel_case.render import CamelCaseJSONRenderer from rest_framework import renderers, permissions from rest_framework.pagination import LimitOffsetPagination @@ -10,7 +11,7 @@ class GeoriviereAPIMixin: renderers.BrowsableAPIRenderer, CamelCaseJSONRenderer, GeoJSONRenderer, - ) + ) if settings.DEBUG else (CamelCaseJSONRenderer, GeoJSONRenderer) permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] pagination_class = LimitOffsetPagination From 33ce6d3c487f30e446509640a707d99203780451 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 24 Apr 2024 15:30:40 +0200 Subject: [PATCH 079/114] fix and add contribution endpoint --- georiviere/portal/serializers/contribution.py | 24 ++-- georiviere/portal/serializers/mixins.py | 8 ++ georiviere/portal/serializers/river.py | 60 +++++++--- georiviere/portal/serializers/valorization.py | 67 +++++++---- georiviere/portal/views/contribution.py | 109 +++++++++--------- georiviere/portal/views/mixins.py | 6 + georiviere/portal/views/river.py | 7 -- georiviere/portal/views/valorization.py | 5 +- 8 files changed, 174 insertions(+), 112 deletions(-) create mode 100644 georiviere/portal/serializers/mixins.py diff --git a/georiviere/portal/serializers/contribution.py b/georiviere/portal/serializers/contribution.py index 6d9c3238..c175a57f 100644 --- a/georiviere/portal/serializers/contribution.py +++ b/georiviere/portal/serializers/contribution.py @@ -184,8 +184,10 @@ class Meta: class CustomContributionSerializer(serializers.ModelSerializer): def __init__(self, *args, **kwargs): - custom_type = kwargs.pop("custom_type") super().__init__(*args, **kwargs) + custom_type = self.context.get("custom_type") + + # add and customize fields from json schema schema = custom_type.get_json_schema_form() for key in schema.get("properties", {}).keys(): field = schema.get("properties", {}).get(key) @@ -200,20 +202,28 @@ def __init__(self, *args, **kwargs): output_field = serializers.DateField elif field.get("type") == "datetime": output_field = serializers.DateTimeField - self.fields[key] = output_field(label=field.get("title")) + # make field required or not + self.fields[key] = output_field( + label=field.get("title"), required=key in schema.get("required", []) + ) + + # station is required if defined at custom_type level. Geom is not required because replace by station geom + if custom_type.stations.exists(): + self.fields["station"] = serializers.PrimaryKeyRelatedField( + queryset=custom_type.stations.all(), required=True + ) + self.fields["geom"].required = False class Meta: model = CustomContribution exclude = ("data", "custom_type") -class CustomContributionSerializerGeoJSONSerializer(geo_serializers.GeoFeatureModelSerializer, +class CustomContributionSerializerGeoJSONSerializer( + geo_serializers.GeoFeatureModelSerializer, CustomContributionSerializer, - ): - geometry = geo_serializers.GeometryField( - read_only=True, precision=7 - ) + geometry = geo_serializers.GeometryField(read_only=True, precision=7) class Meta(CustomContributionSerializer.Meta): geo_field = "geometry" diff --git a/georiviere/portal/serializers/mixins.py b/georiviere/portal/serializers/mixins.py new file mode 100644 index 00000000..165c1b67 --- /dev/null +++ b/georiviere/portal/serializers/mixins.py @@ -0,0 +1,8 @@ +class SerializerAPIMixin: + def _get_url_detail_kwargs(self, pk, format="json"): + return { + "lang": self.context["lang"], + "portal_pk": self.context["portal_pk"], + "format": format, + "pk": pk, + } \ No newline at end of file diff --git a/georiviere/portal/serializers/river.py b/georiviere/portal/serializers/river.py index 6ef35198..95cfd57a 100644 --- a/georiviere/portal/serializers/river.py +++ b/georiviere/portal/serializers/river.py @@ -1,5 +1,7 @@ from rest_framework import serializers +from rest_framework.reverse import reverse +from georiviere.portal.serializers.mixins import SerializerAPIMixin from georiviere.river.models import Stream from georiviere.portal.serializers.main import AttachmentSerializer @@ -8,33 +10,57 @@ from rest_framework_gis.serializers import GeoFeatureModelSerializer -class StreamGeojsonSerializer(GeoFeatureModelSerializer): - geometry = geo_serializers.GeometryField(read_only=True, precision=7, source="geom_transformed") +class StreamMixin(SerializerAPIMixin, ModelSerializer): flow = SerializerMethodField() attachments = AttachmentSerializer(many=True) - - class Meta: - model = Stream - geo_field = 'geometry' - id_field = False - fields = ( - 'id', 'name', 'description', 'length', 'descent', 'flow', 'attachments', 'geometry' - ) + json_url = serializers.SerializerMethodField() + geojson_url = serializers.SerializerMethodField() def get_flow(self, obj): return obj.get_flow_display() + def get_json_url(self, obj): + return reverse( + "api_portal:streams-detail", + kwargs=self._get_url_detail_kwargs(pk=obj.pk, format="json"), + ) -class StreamSerializer(ModelSerializer): - attachments = AttachmentSerializer(many=True) - flow = SerializerMethodField() - geometry_center = geo_serializers.GeometryField(read_only=True, precision=7, source="centroid") + def get_geojson_url(self, obj): + return reverse( + "api_portal:streams-detail", + kwargs=self._get_url_detail_kwargs(pk=obj.pk, format="geojson"), + ) class Meta: model = Stream fields = ( - 'id', 'name', 'description', 'length', 'descent', 'flow', 'attachments', 'geometry_center' + "id", + "name", + "description", + "length", + "descent", + "flow", + "attachments", + "json_url", + "geojson_url", ) - def get_flow(self, obj): - return obj.get_flow_display() + +class StreamGeojsonSerializer(StreamMixin, GeoFeatureModelSerializer): + geometry = geo_serializers.GeometryField( + read_only=True, precision=7, source="geom_transformed" + ) + + class Meta(StreamMixin.Meta): + geo_field = "geometry" + id_field = False + fields = StreamMixin.Meta.fields + ("geometry",) + + +class StreamSerializer(StreamMixin, ModelSerializer): + geometry_center = geo_serializers.GeometryField( + read_only=True, precision=7, source="centroid" + ) + + class Meta(StreamMixin.Meta): + fields = StreamMixin.Meta.fields + ("geometry_center",) diff --git a/georiviere/portal/serializers/valorization.py b/georiviere/portal/serializers/valorization.py index cec2d9f0..cd4983c7 100644 --- a/georiviere/portal/serializers/valorization.py +++ b/georiviere/portal/serializers/valorization.py @@ -1,17 +1,18 @@ -from georiviere.valorization.models import POI, POICategory, POIType -from georiviere.portal.serializers.main import AttachmentSerializer - +from rest_framework import serializers +from rest_framework.reverse import reverse from rest_framework.serializers import ModelSerializer from rest_framework_gis import serializers as geo_serializers from rest_framework_gis.serializers import GeoFeatureModelSerializer +from georiviere.portal.serializers.main import AttachmentSerializer +from georiviere.portal.serializers.mixins import SerializerAPIMixin +from georiviere.valorization.models import POI, POICategory, POIType + class POICategorySerializer(ModelSerializer): class Meta: model = POICategory - fields = ( - 'id', 'label' - ) + fields = ("id", "label") class POITypeSerializer(ModelSerializer): @@ -19,31 +20,53 @@ class POITypeSerializer(ModelSerializer): class Meta: model = POIType - fields = ( - 'id', 'label', 'category', 'pictogram' - ) + fields = ("id", "label", "category", "pictogram") -class POIGeojsonSerializer(GeoFeatureModelSerializer): - type = POITypeSerializer() - geometry = geo_serializers.GeometryField(read_only=True, precision=7, source="geom_transformed") +class POIMixin(SerializerAPIMixin, ModelSerializer): attachments = AttachmentSerializer(many=True) + type = POITypeSerializer() + json_url = serializers.SerializerMethodField() + geojson_url = serializers.SerializerMethodField() + + def get_json_url(self, obj): + return reverse( + "api_portal:pois-detail", + kwargs=self._get_url_detail_kwargs(pk=obj.pk, format="json"), + ) + + def get_geojson_url(self, obj): + return reverse( + "api_portal:pois-detail", + kwargs=self._get_url_detail_kwargs(pk=obj.pk, format="geojson"), + ) class Meta: model = POI - geo_field = 'geometry' - id_field = False fields = ( - 'id', 'name', 'description', 'type', 'attachments', 'geometry' + "id", + "name", + "description", + "type", + "attachments", + "json_url", + "geojson_url", ) -class POISerializer(ModelSerializer): - type = POITypeSerializer() - attachments = AttachmentSerializer(many=True) +class POIGeojsonSerializer(POIMixin, GeoFeatureModelSerializer): + geometry = geo_serializers.GeometryField( + read_only=True, precision=7, source="geom_transformed" + ) - class Meta: - model = POI - fields = ( - 'id', 'name', 'description', 'type', 'attachments' + class Meta(POIMixin.Meta): + geo_field = "geometry" + id_field = False + fields = POIMixin.Meta.fields + ( + "geometry", ) + + +class POISerializer(POIMixin, ModelSerializer): + class Meta(POIMixin.Meta): + pass diff --git a/georiviere/portal/views/contribution.py b/georiviere/portal/views/contribution.py index 9874b1a1..fd8c6977 100644 --- a/georiviere/portal/views/contribution.py +++ b/georiviere/portal/views/contribution.py @@ -1,20 +1,23 @@ import json +import logging import os -from PIL import Image - -from rest_framework.permissions import AllowAny +from PIL import Image from django.conf import settings from django.contrib.contenttypes.models import ContentType +from django.contrib.gis.db.models.functions import Transform from django.core.exceptions import ValidationError from django.core.files import File from django.core.mail import send_mail from django.db.models import F, Q -from django.contrib.gis.db.models.functions import Transform from django.utils import translation from django.utils.translation import gettext_lazy as _ - -from djangorestframework_camel_case.render import CamelCaseJSONRenderer +from rest_framework import filters, viewsets, mixins, renderers, permissions +from rest_framework.decorators import action +from rest_framework.pagination import LimitOffsetPagination +from rest_framework.parsers import JSONParser, MultiPartParser, FormParser +from rest_framework.renderers import JSONRenderer +from rest_framework.response import Response from georiviere.contribution.models import ( Contribution, @@ -31,36 +34,22 @@ CustomContributionSerializer, CustomContributionSerializerGeoJSONSerializer, ) - -from rest_framework import filters, viewsets, mixins, renderers -from rest_framework.decorators import action -from rest_framework.response import Response -from rest_framework.pagination import LimitOffsetPagination -from rest_framework.parsers import JSONParser, MultiPartParser, FormParser - -import logging +from georiviere.portal.views.mixins import GeoriviereAPIMixin logger = logging.getLogger(__name__) class ContributionViewSet( + GeoriviereAPIMixin, mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet, ): model = Contribution - permission_classes = [ - AllowAny, - ] geojson_serializer_class = ContributionGeojsonSerializer serializer_class = ContributionSerializer parser_classes = (MultiPartParser, FormParser, JSONParser) - pagination_class = LimitOffsetPagination - renderer_classes = [ - CamelCaseJSONRenderer, - GeoJSONRenderer, - ] filter_backends = [filters.OrderingFilter, filters.SearchFilter] # TODO: Fix search filter with IntegerField (choices). It might be possible using an annotate on this view. # search_fields = ['potential_damage__type', 'fauna_flora__type', 'quality__type', 'quantity__type', @@ -82,7 +71,6 @@ def get_serializer_context(self): Extra context provided to the serializer class. """ context = super().get_serializer_context() - context["portal_pk"] = self.kwargs["portal_pk"] translation.activate(self.kwargs["lang"]) return context @@ -101,13 +89,6 @@ def get_queryset(self): ) return queryset - def get_serializer_class(self): - """Use specific Serializer for GeoJSON""" - renderer, media_type = self.perform_content_negotiation(self.request) - if getattr(renderer, "format") == "geojson": - return self.geojson_serializer_class - return self.serializer_class - def create(self, request, *args, **kwargs): response = super().create(request) @@ -175,6 +156,7 @@ def create(self, request, *args, **kwargs): class CustomContributionTypeViewSet( + GeoriviereAPIMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet, @@ -182,45 +164,60 @@ class CustomContributionTypeViewSet( queryset = CustomContributionType.objects.all().prefetch_related( "stations", "fields" ) - permission_classes = [ - AllowAny, - ] serializer_class = CustomContributionTypeSerializer - - @action( - detail=True, - url_name="custom-contribution-create", - url_path="contributions", - methods=["post"], - renderer_classes=[renderers.JSONRenderer], - serializer_class=CustomContributionSerializer, + renderer_classes = ( + ( + renderers.BrowsableAPIRenderer, + JSONRenderer, + ) + if settings.DEBUG + else (JSONRenderer,) ) + permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] + pagination_class = LimitOffsetPagination + def create_contribution(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) + context = self.get_serializer_context() + custom_type = self.get_object() + context["custom_type"] = custom_type + serializer = self.get_serializer(data=request.data, context=context) serializer.is_valid(raise_exception=True) - contribution = serializer.save() + extra_save_params = {} + # if station selected, save its geom + if "station" in serializer.validated_data: + extra_save_params["geom"] = serializer.validated_data["station"].geom + contribution = serializer.save(custom_type=custom_type, **extra_save_params) + return Response( - ContributionSerializer( - contribution, context=self.get_serializer_context() - ).data + CustomContributionSerializer(contribution, context=context).data ) - @action( - detail=True, - url_name="custom-contribution-list", - url_path="contributions", - methods=["get"], - renderer_classes=[renderers.JSONRenderer, GeoJSONRenderer], - serializer_class=CustomContributionSerializer, - ) def list_contributions(self, request, *args, **kwargs): custom_type = self.get_object() + context = self.get_serializer_context() + context["custom_type"] = custom_type qs = CustomContribution.objects.with_type_values(custom_type) renderer, media_type = self.perform_content_negotiation(self.request) if getattr(renderer, "format") == "geojson": - self.serializer_class = CustomContributionSerializerGeoJSONSerializer + self.geojson_serializer_class = ( + CustomContributionSerializerGeoJSONSerializer + ) qs = qs.annotate(geometry=Transform(F("geom"), settings.API_SRID)) - serializer = self.get_serializer(qs, custom_type=custom_type, many=True) + serializer = self.get_serializer(qs, context=context, many=True) return Response(serializer.data) + + @action( + detail=True, + url_name="custom-contributions", + url_path="contributions", + methods=["get", "post"], + renderer_classes=[renderers.JSONRenderer, GeoJSONRenderer], + serializer_class=CustomContributionSerializer, + ) + def contributions(self, request, *args, **kwargs): + if request.method == "GET": + return self.list_contributions(request, *args, **kwargs) + elif request.method == "POST": + return self.create_contribution(request, *args, **kwargs) diff --git a/georiviere/portal/views/mixins.py b/georiviere/portal/views/mixins.py index d6fe70fe..b8db9662 100644 --- a/georiviere/portal/views/mixins.py +++ b/georiviere/portal/views/mixins.py @@ -21,3 +21,9 @@ def get_serializer_class(self): if getattr(renderer, "format") == "geojson": return self.geojson_serializer_class return self.serializer_class + + def get_serializer_context(self): + context = super().get_serializer_context() + context["lang"] = self.kwargs["lang"] + context["portal_pk"] = self.kwargs["portal_pk"] + return context \ No newline at end of file diff --git a/georiviere/portal/views/river.py b/georiviere/portal/views/river.py index 2792ddab..1cac2fde 100644 --- a/georiviere/portal/views/river.py +++ b/georiviere/portal/views/river.py @@ -37,13 +37,6 @@ def get_queryset(self): ).annotate(centroid=Centroid("geom_transformed")) return queryset - def get_serializer_class(self): - """Use specific Serializer for GeoJSON""" - renderer, media_type = self.perform_content_negotiation(self.request) - if getattr(renderer, "format") == "geojson": - return self.geojson_serializer_class - return self.serializer_class - def view_cache_key(self): return f"stream-{self.kwargs['portal_pk']}" diff --git a/georiviere/portal/views/valorization.py b/georiviere/portal/views/valorization.py index 7d5ca110..c7d14404 100644 --- a/georiviere/portal/views/valorization.py +++ b/georiviere/portal/views/valorization.py @@ -21,7 +21,7 @@ class POIViewSet(GeoriviereAPIMixin, viewsets.ReadOnlyModelViewSet): def get_queryset(self): portal_pk = self.kwargs['portal_pk'] - queryset = POI.objects.select_related('type') + queryset = POI.objects.select_related('type__category') queryset = queryset.filter(portals__id=portal_pk).annotate(geom_transformed=Transform(F('geom'), settings.API_SRID)) return queryset @@ -30,7 +30,6 @@ def get_queryset(self): def category(self, request, *args, **kwargs): category_pk = self.kwargs['category_pk'] category = get_object_or_404(POICategory.objects.all(), pk=category_pk) - qs = self.filter_queryset(POI.objects.filter(type__in=category.types.all()).annotate( - geom_transformed=Transform(F('geom'), settings.API_SRID))) + qs = self.filter_queryset(self.get_queryset().filter(type__in=category.types.all())) serializer = self.get_serializer(qs, many=True) return Response(serializer.data) From fd2a3fe096a06d820b3e18cb658e2cb382648bb3 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 24 Apr 2024 15:30:51 +0200 Subject: [PATCH 080/114] improve load_river command --- georiviere/river/management/commands/load_rivers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/georiviere/river/management/commands/load_rivers.py b/georiviere/river/management/commands/load_rivers.py index 38a8668c..1d522725 100644 --- a/georiviere/river/management/commands/load_rivers.py +++ b/georiviere/river/management/commands/load_rivers.py @@ -40,7 +40,7 @@ def handle(self, *args, **options): objs = (Stream(geom=feat.geom.geos, source_location=Point(feat.geom.geos[0]), - name=feat.get(name_column) or default_name) for feat in layer) + name=feat.get(name_column) or default_name) for feat in layer if feat.geom.geos.geom_typeid == 1) count = 0 while True: batch = list(islice(objs, batch_size)) From e1fa406059c3550ddaf50aca529d635c2a4eb953 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 24 Apr 2024 16:20:50 +0200 Subject: [PATCH 081/114] fix contributions --- georiviere/portal/serializers/contribution.py | 12 ++++++++++++ georiviere/portal/views/contribution.py | 10 ++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/georiviere/portal/serializers/contribution.py b/georiviere/portal/serializers/contribution.py index c175a57f..12a02c92 100644 --- a/georiviere/portal/serializers/contribution.py +++ b/georiviere/portal/serializers/contribution.py @@ -214,6 +214,18 @@ def __init__(self, *args, **kwargs): ) self.fields["geom"].required = False + def create(self, validated_data): + custom_type = self.context.get("custom_type") + # add and customize fields from json schema + schema = custom_type.get_json_schema_form() + + data = {} + for key, value in schema.get("properties").items(): + if key in validated_data: + data[key] = validated_data.pop(key) + validated_data["data"] = data + return super().create(validated_data) + class Meta: model = CustomContribution exclude = ("data", "custom_type") diff --git a/georiviere/portal/views/contribution.py b/georiviere/portal/views/contribution.py index fd8c6977..ed00ee9d 100644 --- a/georiviere/portal/views/contribution.py +++ b/georiviere/portal/views/contribution.py @@ -12,7 +12,7 @@ from django.db.models import F, Q from django.utils import translation from django.utils.translation import gettext_lazy as _ -from rest_framework import filters, viewsets, mixins, renderers, permissions +from rest_framework import filters, viewsets, mixins, renderers, permissions, status from rest_framework.decorators import action from rest_framework.pagination import LimitOffsetPagination from rest_framework.parsers import JSONParser, MultiPartParser, FormParser @@ -173,7 +173,7 @@ class CustomContributionTypeViewSet( if settings.DEBUG else (JSONRenderer,) ) - permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] + permission_classes = [permissions.AllowAny] pagination_class = LimitOffsetPagination def create_contribution(self, request, *args, **kwargs): @@ -187,9 +187,11 @@ def create_contribution(self, request, *args, **kwargs): if "station" in serializer.validated_data: extra_save_params["geom"] = serializer.validated_data["station"].geom contribution = serializer.save(custom_type=custom_type, **extra_save_params) - + # reload with extra fields + contribution = CustomContribution.objects.with_type_values(custom_type).get(pk=contribution.pk) return Response( - CustomContributionSerializer(contribution, context=context).data + CustomContributionSerializer(contribution, context=context).data, + status=status.HTTP_201_CREATED ) def list_contributions(self, request, *args, **kwargs): From 80a7a21092639dcfc28c02d788ab4ae976b75a11 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 24 Apr 2024 16:23:00 +0200 Subject: [PATCH 082/114] fix contributions --- georiviere/portal/views/river.py | 7 +++-- georiviere/portal/views/valorization.py | 36 ++++++++++++++++++------- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/georiviere/portal/views/river.py b/georiviere/portal/views/river.py index 1cac2fde..e1afe2c3 100644 --- a/georiviere/portal/views/river.py +++ b/georiviere/portal/views/river.py @@ -30,8 +30,11 @@ def get_model(self): def get_queryset(self): portal_pk = self.kwargs["portal_pk"] queryset = Stream.objects.filter(portals__id=portal_pk).prefetch_related( - Prefetch("attachments", - queryset=Attachment.objects.all().select_related('filetype'))) + Prefetch( + "attachments", + queryset=Attachment.objects.all().select_related("filetype"), + ) + ) queryset = queryset.annotate( geom_transformed=Transform(F("geom"), settings.API_SRID) ).annotate(centroid=Centroid("geom_transformed")) diff --git a/georiviere/portal/views/valorization.py b/georiviere/portal/views/valorization.py index c7d14404..74730a8c 100644 --- a/georiviere/portal/views/valorization.py +++ b/georiviere/portal/views/valorization.py @@ -2,8 +2,12 @@ from django.db.models import F from django.contrib.gis.db.models.functions import Transform from django.shortcuts import get_object_or_404 +from rest_framework.permissions import AllowAny -from georiviere.portal.serializers.valorization import POIGeojsonSerializer, POISerializer +from georiviere.portal.serializers.valorization import ( + POIGeojsonSerializer, + POISerializer, +) from georiviere.portal.views.mixins import GeoriviereAPIMixin from georiviere.valorization.models import POI, POICategory @@ -16,20 +20,32 @@ class POIViewSet(GeoriviereAPIMixin, viewsets.ReadOnlyModelViewSet): geojson_serializer_class = POIGeojsonSerializer serializer_class = POISerializer filter_backends = [filters.OrderingFilter, filters.SearchFilter] - ordering_fields = ['name', 'date_insert'] - search_fields = ['name', 'type__label', 'type__category__label'] + permission_classes = [ + AllowAny, + ] + ordering_fields = ["name", "date_insert"] + search_fields = ["name", "type__label", "type__category__label"] def get_queryset(self): - portal_pk = self.kwargs['portal_pk'] - queryset = POI.objects.select_related('type__category') - queryset = queryset.filter(portals__id=portal_pk).annotate(geom_transformed=Transform(F('geom'), settings.API_SRID)) + portal_pk = self.kwargs["portal_pk"] + queryset = POI.objects.select_related("type__category") + queryset = queryset.filter(portals__id=portal_pk).annotate( + geom_transformed=Transform(F("geom"), settings.API_SRID) + ) return queryset - @action(detail=False, methods=['get'], permission_classes=[], - url_path=r'category/(?P\d+)', url_name='category') + @action( + detail=False, + methods=["get"], + permission_classes=[], + url_path=r"category/(?P\d+)", + url_name="category", + ) def category(self, request, *args, **kwargs): - category_pk = self.kwargs['category_pk'] + category_pk = self.kwargs["category_pk"] category = get_object_or_404(POICategory.objects.all(), pk=category_pk) - qs = self.filter_queryset(self.get_queryset().filter(type__in=category.types.all())) + qs = self.filter_queryset( + self.get_queryset().filter(type__in=category.types.all()) + ) serializer = self.get_serializer(qs, many=True) return Response(serializer.data) From 5088e0326f87a1b92b789ac892ff35066f2f6c57 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 24 Apr 2024 16:46:49 +0200 Subject: [PATCH 083/114] pep8 --- georiviere/contribution/admin.py | 3 ++- georiviere/contribution/models/__init__.py | 3 +-- georiviere/contribution/models/managers.py | 2 +- georiviere/contribution/schema.py | 6 +++--- georiviere/main/views.py | 4 ---- georiviere/portal/serializers/map.py | 2 +- georiviere/portal/serializers/mixins.py | 2 +- georiviere/portal/views/mixins.py | 2 +- georiviere/portal/views/river.py | 7 +++---- georiviere/tests/__init__.py | 2 +- georiviere/urls.py | 1 - 11 files changed, 14 insertions(+), 20 deletions(-) diff --git a/georiviere/contribution/admin.py b/georiviere/contribution/admin.py index 51d50b3e..356b7d51 100644 --- a/georiviere/contribution/admin.py +++ b/georiviere/contribution/admin.py @@ -26,7 +26,7 @@ class CustomFieldInline(OrderableAdmin, admin.TabularInline): ordering_field = "order" ordering = ('order', 'label') form = forms.CustomContributionFieldInlineForm - fields = ('label', 'value_type', 'required', 'help_text', 'order') + fields = ('label', 'value_type', 'required', 'help_text', 'order') extra = 0 show_change_link = True popup_link = 'change' @@ -68,6 +68,7 @@ def has_delete_permission(self, request, obj=None): """ Disable deletion in list view """ return False + @admin.register(models.CustomContribution) class CustomContributionAdmin(LeafletGeoAdmin, admin.ModelAdmin): list_display = ('custom_type', 'portal', 'validated', 'date_insert', 'date_update') diff --git a/georiviere/contribution/models/__init__.py b/georiviere/contribution/models/__init__.py index c3b48e8e..6b8f7e44 100644 --- a/georiviere/contribution/models/__init__.py +++ b/georiviere/contribution/models/__init__.py @@ -608,8 +608,7 @@ def __str__(self): Contribution.add_property("knowledges", Knowledge.within_buffer, _("Knowledge")) -## Custom Contribution - +# Custom Contribution class CustomContributionType(models.Model): label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) diff --git a/georiviere/contribution/models/managers.py b/georiviere/contribution/models/managers.py index b7b66328..f61eede8 100644 --- a/georiviere/contribution/models/managers.py +++ b/georiviere/contribution/models/managers.py @@ -24,7 +24,7 @@ def with_type_values(self, custom_type): output_field = models.DateField() elif field.value_type == 'datetime': output_field = models.DateField() - annotations[field.key] = Cast(KeyTextTransform(field.key, 'data'), output_field=output_field) + annotations[field.key] = Cast(KeyTextTransform(field.key, 'data'), output_field=output_field) if annotations: qs = qs.annotate(**annotations) return qs diff --git a/georiviere/contribution/schema.py b/georiviere/contribution/schema.py index 506f6f12..6e5197c0 100644 --- a/georiviere/contribution/schema.py +++ b/georiviere/contribution/schema.py @@ -60,7 +60,7 @@ def get_landing(choices, meta): "landing_type" ).related_model._meta.verbose_name.capitalize(), "enum": list( - Lmodels.andingType.objects.values_list("label", flat=True) + models.LandingType.objects.values_list("label", flat=True) ), } }, @@ -390,13 +390,13 @@ def get_pollution(choices, meta): models.NaturePollution.objects.values_list("label", flat=True) ), } - if TypePollution.objects.exists(): + if models.TypePollution.objects.exists(): pollution_property["type_pollution"] = { "type": "string", "title": meta.get_field( "type_pollution" ).related_model._meta.verbose_name.capitalize(), - "enum": list(TypePollution.objects.values_list("label", flat=True)), + "enum": list(models.TypePollution.objects.values_list("label", flat=True)), } pollution = { "if": {"properties": {"type": {"const": choices.POLLUTION.label}}}, diff --git a/georiviere/main/views.py b/georiviere/main/views.py index 22a67709..517f88e5 100644 --- a/georiviere/main/views.py +++ b/georiviere/main/views.py @@ -1,6 +1,5 @@ from django.contrib.auth.decorators import login_required from django.conf import settings -from django.http import HttpResponse, HttpResponseServerError from django.shortcuts import redirect from django.urls import reverse from mapentity.registry import registry @@ -10,9 +9,6 @@ logger = logging.getLogger(__name__) -def handler500(request, *args, **kwargs): - return HttpResponseServerError() - @login_required(login_url="login") def home(request): diff --git a/georiviere/portal/serializers/map.py b/georiviere/portal/serializers/map.py index 2f33f0a5..12fe8ce1 100644 --- a/georiviere/portal/serializers/map.py +++ b/georiviere/portal/serializers/map.py @@ -58,7 +58,7 @@ def get_url(self, obj): if layer_type[0] not in ['pois', 'streams', 'contributions', 'sensitivities']: return None # TODO: Make lang dynamic - reverse_kwargs = {'lang': 'fr', 'format': 'json' } + reverse_kwargs = {'lang': 'fr', 'format': 'json'} if layer_type[0] in ['watersheds', 'pois', 'streams', 'contributions']: reverse_kwargs['portal_pk'] = obj.portal.pk return reverse(f'api_portal:{layer_type[0]}-list', kwargs=reverse_kwargs) diff --git a/georiviere/portal/serializers/mixins.py b/georiviere/portal/serializers/mixins.py index 165c1b67..85780c96 100644 --- a/georiviere/portal/serializers/mixins.py +++ b/georiviere/portal/serializers/mixins.py @@ -5,4 +5,4 @@ def _get_url_detail_kwargs(self, pk, format="json"): "portal_pk": self.context["portal_pk"], "format": format, "pk": pk, - } \ No newline at end of file + } diff --git a/georiviere/portal/views/mixins.py b/georiviere/portal/views/mixins.py index b8db9662..43694f79 100644 --- a/georiviere/portal/views/mixins.py +++ b/georiviere/portal/views/mixins.py @@ -26,4 +26,4 @@ def get_serializer_context(self): context = super().get_serializer_context() context["lang"] = self.kwargs["lang"] context["portal_pk"] = self.kwargs["portal_pk"] - return context \ No newline at end of file + return context diff --git a/georiviere/portal/views/river.py b/georiviere/portal/views/river.py index e1afe2c3..4ad45a9e 100644 --- a/georiviere/portal/views/river.py +++ b/georiviere/portal/views/river.py @@ -3,7 +3,7 @@ from django.db.models import F, Prefetch from rest_framework import filters, viewsets, permissions as rest_permissions -#from georiviere.decorators import view_cache_response_content, view_cache_latest +# from georiviere.decorators import view_cache_response_content, view_cache_latest from georiviere.main.models import Attachment from georiviere.portal.filters import SearchNoAccentFilter from georiviere.portal.serializers.river import ( @@ -18,7 +18,6 @@ class StreamViewSet(GeoriviereAPIMixin, viewsets.ReadOnlyModelViewSet): model = Stream geojson_serializer_class = StreamGeojsonSerializer serializer_class = StreamSerializer - permission_classes = [rest_permissions.DjangoModelPermissionsOrAnonReadOnly] filter_backends = [filters.OrderingFilter, SearchNoAccentFilter] ordering_fields = ["name", "date_insert"] @@ -43,7 +42,7 @@ def get_queryset(self): def view_cache_key(self): return f"stream-{self.kwargs['portal_pk']}" - #@view_cache_latest() - #@view_cache_response_content() + # @view_cache_latest() + # @view_cache_response_content() def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) diff --git a/georiviere/tests/__init__.py b/georiviere/tests/__init__.py index 405b4d65..4d45ac5b 100644 --- a/georiviere/tests/__init__.py +++ b/georiviere/tests/__init__.py @@ -120,4 +120,4 @@ def test_distance_to_source_is_available(self): object_id=obj.pk, content_type=ContentType.objects.get_for_model(obj) ).distance - self.assertContains(response, f'''({round(distance , 1 ) if distance else 0} m)''') + self.assertContains(response, f'''({round(distance, 1) if distance else 0} m)''') diff --git a/georiviere/urls.py b/georiviere/urls.py index 6ae95539..374483ee 100644 --- a/georiviere/urls.py +++ b/georiviere/urls.py @@ -12,7 +12,6 @@ admin.autodiscover() -#handler500 = 'georiviere.main.views.handler500' urlpatterns = [ path('', home, name='home'), From fb6d015406bceeac761c51ada9064900e1ac2e34 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 24 Apr 2024 16:52:34 +0200 Subject: [PATCH 084/114] pep8 --- georiviere/portal/views/river.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/georiviere/portal/views/river.py b/georiviere/portal/views/river.py index 4ad45a9e..10b36e5a 100644 --- a/georiviere/portal/views/river.py +++ b/georiviere/portal/views/river.py @@ -1,7 +1,7 @@ from django.conf import settings from django.contrib.gis.db.models.functions import Centroid, Transform from django.db.models import F, Prefetch -from rest_framework import filters, viewsets, permissions as rest_permissions +from rest_framework import filters, viewsets # from georiviere.decorators import view_cache_response_content, view_cache_latest from georiviere.main.models import Attachment From 884771b3d35cf09333dc1819f804df2c9bcbbe27 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Wed, 24 Apr 2024 16:55:11 +0200 Subject: [PATCH 085/114] fix migration --- ...tion_customcontributiontype_customcontributiontypefield.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/georiviere/contribution/migrations/0010_customcontribution_customcontributiontype_customcontributiontypefield.py b/georiviere/contribution/migrations/0010_customcontribution_customcontributiontype_customcontributiontypefield.py index df769db4..95b63268 100644 --- a/georiviere/contribution/migrations/0010_customcontribution_customcontributiontype_customcontributiontypefield.py +++ b/georiviere/contribution/migrations/0010_customcontribution_customcontributiontype_customcontributiontypefield.py @@ -1,5 +1,5 @@ # Generated by Django 3.1.14 on 2024-04-11 08:30 - +from django.conf import settings import django.contrib.gis.db.models.fields from django.db import migrations, models import django.db.models.deletion @@ -30,7 +30,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('date_insert', models.DateTimeField(auto_now_add=True, verbose_name='Insertion date')), ('date_update', models.DateTimeField(auto_now=True, db_index=True, verbose_name='Update date')), - ('geom', django.contrib.gis.db.models.fields.GeometryField(srid=2154)), + ('geom', django.contrib.gis.db.models.fields.GeometryField(srid=settings.SRID)), ('properties', models.JSONField(blank=True, default=dict)), ('validated', models.BooleanField(default=False, verbose_name='Validated')), ('custom_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='contributions', to='contribution.customcontributiontype')), From 856f3a0b75bc1be0aeec53f8102bd82b804dd564 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Thu, 25 Apr 2024 11:26:23 +0200 Subject: [PATCH 086/114] fix tests --- georiviere/portal/serializers/map.py | 6 +- .../portal/tests/test_views/test_river.py | 91 +++++++++++++++---- .../tests/test_views/test_valorization.py | 8 +- georiviere/portal/views/contribution.py | 10 +- 4 files changed, 89 insertions(+), 26 deletions(-) diff --git a/georiviere/portal/serializers/map.py b/georiviere/portal/serializers/map.py index 12fe8ce1..3f3762f1 100644 --- a/georiviere/portal/serializers/map.py +++ b/georiviere/portal/serializers/map.py @@ -44,7 +44,7 @@ def get_geojson_url(self, obj): layer_type = obj.layer_type.split('-') # TODO: Make lang dynamic reverse_kwargs = {'lang': 'fr', 'format': 'geojson'} - if layer_type[0] in ['watersheds', 'pois', 'streams', 'contributions']: + if layer_type[0] in ['watersheds', 'pois', 'streams', 'contributions', 'stations']: reverse_kwargs['portal_pk'] = obj.portal.pk if len(layer_type) == 2: # If the layer type is poi, it's separated by category. @@ -55,11 +55,11 @@ def get_geojson_url(self, obj): def get_url(self, obj): layer_type = obj.layer_type.split('-') - if layer_type[0] not in ['pois', 'streams', 'contributions', 'sensitivities']: + if layer_type[0] not in ['pois', 'streams', 'contributions', 'sensitivities', 'stations']: return None # TODO: Make lang dynamic reverse_kwargs = {'lang': 'fr', 'format': 'json'} - if layer_type[0] in ['watersheds', 'pois', 'streams', 'contributions']: + if layer_type[0] in ['watersheds', 'pois', 'streams', 'contributions', 'stations']: reverse_kwargs['portal_pk'] = obj.portal.pk return reverse(f'api_portal:{layer_type[0]}-list', kwargs=reverse_kwargs) diff --git a/georiviere/portal/tests/test_views/test_river.py b/georiviere/portal/tests/test_views/test_river.py index 6f13fff5..5f1d6321 100644 --- a/georiviere/portal/tests/test_views/test_river.py +++ b/georiviere/portal/tests/test_views/test_river.py @@ -13,42 +13,97 @@ def setUpTestData(cls): cls.stream.portals.add(cls.portal) def test_stream_detail_geojson_structure(self): - url = reverse('api_portal:streams-detail', - kwargs={'portal_pk': self.portal.pk, 'pk': self.stream.pk, 'lang': 'fr', 'format': 'geojson'}) + url = reverse( + "api_portal:streams-detail", + kwargs={ + "portal_pk": self.portal.pk, + "pk": self.stream.pk, + "lang": "fr", + "format": "geojson", + }, + ) response = self.client.get(url) self.assertEqual(response.status_code, 200) - self.assertSetEqual(set(response.json().keys()), {'geometry', 'properties', 'type'}) + self.assertSetEqual( + set(response.json().keys()), {"geometry", "properties", "type"} + ) def test_stream_detail_json_structure(self): - url = reverse('api_portal:streams-detail', - kwargs={'portal_pk': self.portal.pk, 'pk': self.stream.pk, 'lang': 'fr', 'format': 'json'}) + url = reverse( + "api_portal:streams-detail", + kwargs={ + "portal_pk": self.portal.pk, + "pk": self.stream.pk, + "lang": "fr", + "format": "json", + }, + ) response = self.client.get(url) self.assertEqual(response.status_code, 200) - self.assertSetEqual(set(response.json().keys()), {'attachments', 'length', 'flow', 'descent', 'name', - 'id', 'description', 'geometryCenter'}) + self.assertListEqual( + response.json().keys(), + [ + "attachments", + "length", + "flow", + "descent", + "name", + "id", + "description", + "geometryCenter", + "geojsonUrl", + "jsonUrl", + ], + ) def test_stream_list_geojson_structure(self): - url = reverse('api_portal:streams-list', - kwargs={'portal_pk': self.portal.pk, 'lang': 'fr', 'format': 'geojson'}) + url = reverse( + "api_portal:streams-list", + kwargs={"portal_pk": self.portal.pk, "lang": "fr", "format": "geojson"}, + ) response = self.client.get(url) self.assertEqual(response.status_code, 200) - self.assertSetEqual(set(response.json().keys()), {'type', 'features'}) + self.assertSetEqual(set(response.json().keys()), {"type", "features"}) def test_stream_list_json_structure(self): - url = reverse('api_portal:streams-list', - kwargs={'portal_pk': self.portal.pk, 'lang': 'fr', 'format': 'json'}) + url = reverse( + "api_portal:streams-list", + kwargs={"portal_pk": self.portal.pk, "lang": "fr", "format": "json"}, + ) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json()), 1) - self.assertSetEqual(set(response.json()[0].keys()), {'attachments', 'flow', 'descent', 'length', - 'name', 'id', 'description', 'geometryCenter'}) + self.assertListEqual( + response.json().keys(), + [ + "attachments", + "length", + "flow", + "descent", + "name", + "id", + "description", + "geometryCenter", + "geojsonUrl", + "jsonUrl", + ], + ) def test_stream_ranslation_according_url(self): - for lang in ['en', 'fr']: - url = reverse('api_portal:streams-detail', - kwargs={'portal_pk': self.portal.pk, 'pk': self.stream.pk, 'lang': lang, 'format': 'json'}) + for lang in ["en", "fr"]: + url = reverse( + "api_portal:streams-detail", + kwargs={ + "portal_pk": self.portal.pk, + "pk": self.stream.pk, + "lang": lang, + "format": "json", + }, + ) response = self.client.get(url) self.assertEqual(response.status_code, 200) data = response.json() - self.assertEqual(data['flow'], 'To be defined' if lang == 'en' else 'À définir') + self.assertEqual( + data["flow"], "To be defined" if lang == "en" else "À définir" + ) diff --git a/georiviere/portal/tests/test_views/test_valorization.py b/georiviere/portal/tests/test_views/test_valorization.py index 2cfd6558..9bb26666 100644 --- a/georiviere/portal/tests/test_views/test_valorization.py +++ b/georiviere/portal/tests/test_views/test_valorization.py @@ -25,7 +25,8 @@ def test_poi_detail_json_structure(self): kwargs={'portal_pk': self.portal.pk, 'pk': self.poi.pk, 'lang': 'fr', 'format': 'json'}) response = self.client.get(url) self.assertEqual(response.status_code, 200) - self.assertSetEqual(set(response.json().keys()), {'attachments', 'type', 'name', 'id', 'description'}) + self.assertSetEqual(set(response.json().keys()), {'attachments', 'name', 'type', 'id', 'description', + 'geojsonUrl', 'jsonUrl'}) def test_poi_list_geojson_structure(self): url = reverse('api_portal:pois-list', @@ -40,7 +41,7 @@ def test_poi_list_json_structure(self): response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json()), 1) - self.assertSetEqual(set(response.json()[0].keys()), {'attachments', 'type', 'name', 'id', 'description'}) + self.assertSetEqual(set(response.json()[0].keys()), {'attachments', 'name', 'type', 'id', 'description', 'geojsonUrl', 'jsonUrl'}) def test_poi_category_list_json_structure(self): url = reverse('api_portal:pois-category', @@ -49,7 +50,8 @@ def test_poi_category_list_json_structure(self): response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json()), 1) - self.assertSetEqual(set(response.json()[0].keys()), {'attachments', 'name', 'type', 'id', 'description'}) + self.assertSetEqual(set(response.json()[0].keys()), + {'attachments', 'name', 'type', 'id', 'description', 'geojsonUrl', 'jsonUrl'}) def test_poi_category_list_geojson_structure(self): url = reverse('api_portal:pois-category', diff --git a/georiviere/portal/views/contribution.py b/georiviere/portal/views/contribution.py index ed00ee9d..89982236 100644 --- a/georiviere/portal/views/contribution.py +++ b/georiviere/portal/views/contribution.py @@ -16,6 +16,7 @@ from rest_framework.decorators import action from rest_framework.pagination import LimitOffsetPagination from rest_framework.parsers import JSONParser, MultiPartParser, FormParser +from rest_framework.permissions import AllowAny from rest_framework.renderers import JSONRenderer from rest_framework.response import Response @@ -49,6 +50,9 @@ class ContributionViewSet( model = Contribution geojson_serializer_class = ContributionGeojsonSerializer serializer_class = ContributionSerializer + permission_classes = [ + AllowAny, + ] parser_classes = (MultiPartParser, FormParser, JSONParser) filter_backends = [filters.OrderingFilter, filters.SearchFilter] # TODO: Fix search filter with IntegerField (choices). It might be possible using an annotate on this view. @@ -188,10 +192,12 @@ def create_contribution(self, request, *args, **kwargs): extra_save_params["geom"] = serializer.validated_data["station"].geom contribution = serializer.save(custom_type=custom_type, **extra_save_params) # reload with extra fields - contribution = CustomContribution.objects.with_type_values(custom_type).get(pk=contribution.pk) + contribution = CustomContribution.objects.with_type_values(custom_type).get( + pk=contribution.pk + ) return Response( CustomContributionSerializer(contribution, context=context).data, - status=status.HTTP_201_CREATED + status=status.HTTP_201_CREATED, ) def list_contributions(self, request, *args, **kwargs): From a68359c9766430af895ffa780d20ae2d5f9cc1da Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Thu, 25 Apr 2024 14:30:43 +0200 Subject: [PATCH 087/114] fix tests and station layer migration --- georiviere/knowledge/tests/test_views.py | 2 +- georiviere/observations/tests/test_views.py | 2 +- .../migrations/0007_auto_20240423_1647.py | 30 +++++++++++++++++++ .../tests/test_serializers/test_portal.py | 4 +-- georiviere/portal/tests/test_signals.py | 8 ++--- .../portal/tests/test_views/test_river.py | 12 ++++---- 6 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 georiviere/portal/migrations/0007_auto_20240423_1647.py diff --git a/georiviere/knowledge/tests/test_views.py b/georiviere/knowledge/tests/test_views.py index 87cb717d..33248ff7 100644 --- a/georiviere/knowledge/tests/test_views.py +++ b/georiviere/knowledge/tests/test_views.py @@ -167,7 +167,7 @@ def test_detail_number_queries(self): self.login() station = self.modelfactory.create() - with self.assertNumQueries(45): + with self.assertNumQueries(47): self.client.get(station.get_detail_url()) diff --git a/georiviere/observations/tests/test_views.py b/georiviere/observations/tests/test_views.py index f76965f4..b04468fd 100644 --- a/georiviere/observations/tests/test_views.py +++ b/georiviere/observations/tests/test_views.py @@ -100,5 +100,5 @@ def test_detail_number_queries(self): self.login() station = self.modelfactory.create() - with self.assertNumQueries(50): + with self.assertNumQueries(52): self.client.get(station.get_detail_url()) diff --git a/georiviere/portal/migrations/0007_auto_20240423_1647.py b/georiviere/portal/migrations/0007_auto_20240423_1647.py new file mode 100644 index 00000000..2d73bc6b --- /dev/null +++ b/georiviere/portal/migrations/0007_auto_20240423_1647.py @@ -0,0 +1,30 @@ +# Generated by Django 3.1.14 on 2024-04-23 16:47 + +from django.db import migrations +from django.utils.translation import gettext_lazy as _ + + +def add_station_map_layer_to_portals(apps, schema_editor): + MapLayer = apps.get_model("portal", "MapLayer") + Portal = apps.get_model("portal", "Portal") + + for portal in Portal.objects.all(): + MapLayer.objects.create( + portal=portal, + layer_type="stations", + name=_("Stations"), + visible=False, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("portal", "0006_auto_20230915_1756"), + ] + + operations = [ + migrations.RunPython( + add_station_map_layer_to_portals, reverse_code=migrations.RunPython.noop + ), + ] diff --git a/georiviere/portal/tests/test_serializers/test_portal.py b/georiviere/portal/tests/test_serializers/test_portal.py index 5dcdae95..93f0e65e 100644 --- a/georiviere/portal/tests/test_serializers/test_portal.py +++ b/georiviere/portal/tests/test_serializers/test_portal.py @@ -39,7 +39,7 @@ def test_portal_content(self): data = self.serializer_portal.data self.assertSetEqual(set(data.keys()), {'id', 'map', 'name', 'flatpages', 'title', 'description', 'extent', 'max_zoom', 'min_zoom', 'main_color'}) - self.assertEqual(len(data['map']['group'][0]['layers']), 5) + self.assertEqual(len(data['map']['group'][0]['layers']), 6) def test_portal_all_layers_grouped_content(self): data = self.serializer_portal_layers_all_group.data @@ -47,7 +47,7 @@ def test_portal_all_layers_grouped_content(self): self.assertEqual(data['map']['group'][0]['label'], 'Bar') self.assertSetEqual(set(data.keys()), {'id', 'map', 'name', 'flatpages', 'title', 'description', 'extent', 'max_zoom', 'min_zoom', 'main_color'}) - self.assertEqual(len(data['map']['group'][0]['layers']), 5) + self.assertEqual(len(data['map']['group'][0]['layers']), 6) def test_portal_without_se_content(self): data = self.serializer_portal_without_spatial_extent.data diff --git a/georiviere/portal/tests/test_signals.py b/georiviere/portal/tests/test_signals.py index 027b4724..5fd8d5fb 100644 --- a/georiviere/portal/tests/test_signals.py +++ b/georiviere/portal/tests/test_signals.py @@ -11,17 +11,17 @@ class PortalTest(TestCase): def test_create_portal(self): self.assertEqual(0, MapLayer.objects.count()) factories.PortalFactory.create() - self.assertEqual(6, MapLayer.objects.count()) + self.assertEqual(7, MapLayer.objects.count()) def test_create_portal_poi_category(self): POICategoryFactory.create() self.assertEqual(0, MapLayer.objects.count()) factories.PortalFactory.create() - self.assertEqual(7, MapLayer.objects.count()) + self.assertEqual(8, MapLayer.objects.count()) category = POICategoryFactory.create(label="New category") - self.assertEqual(8, MapLayer.objects.count()) + self.assertEqual(9, MapLayer.objects.count()) category.delete() - self.assertEqual(7, MapLayer.objects.count()) + self.assertEqual(8, MapLayer.objects.count()) diff --git a/georiviere/portal/tests/test_views/test_river.py b/georiviere/portal/tests/test_views/test_river.py index 5f1d6321..76906142 100644 --- a/georiviere/portal/tests/test_views/test_river.py +++ b/georiviere/portal/tests/test_views/test_river.py @@ -42,8 +42,8 @@ def test_stream_detail_json_structure(self): response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertListEqual( - response.json().keys(), - [ + sorted(list(response.json().keys())), + sorted([ "attachments", "length", "flow", @@ -54,7 +54,7 @@ def test_stream_detail_json_structure(self): "geometryCenter", "geojsonUrl", "jsonUrl", - ], + ]), ) def test_stream_list_geojson_structure(self): @@ -75,8 +75,8 @@ def test_stream_list_json_structure(self): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json()), 1) self.assertListEqual( - response.json().keys(), - [ + sorted(list(response.json()[0].keys())), + sorted([ "attachments", "length", "flow", @@ -87,7 +87,7 @@ def test_stream_list_json_structure(self): "geometryCenter", "geojsonUrl", "jsonUrl", - ], + ]), ) def test_stream_ranslation_according_url(self): From dfdf435f172ad75c44508b62cd06ffc02be3d4fa Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Thu, 25 Apr 2024 15:30:38 +0200 Subject: [PATCH 088/114] fix migration --- georiviere/portal/migrations/0007_auto_20240423_1647.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/georiviere/portal/migrations/0007_auto_20240423_1647.py b/georiviere/portal/migrations/0007_auto_20240423_1647.py index 2d73bc6b..da5f45d7 100644 --- a/georiviere/portal/migrations/0007_auto_20240423_1647.py +++ b/georiviere/portal/migrations/0007_auto_20240423_1647.py @@ -12,8 +12,8 @@ def add_station_map_layer_to_portals(apps, schema_editor): MapLayer.objects.create( portal=portal, layer_type="stations", - name=_("Stations"), - visible=False, + label=_("Stations"), + hidden=True, ) From 02376f88ce056783d2c6bd48273d51fd92f4d253 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Thu, 25 Apr 2024 15:30:54 +0200 Subject: [PATCH 089/114] add shared contribution_at field --- .../migrations/0018_auto_20240425_1328.py | 23 +++++++++++++++++++ georiviere/contribution/models/__init__.py | 3 +++ 2 files changed, 26 insertions(+) create mode 100644 georiviere/contribution/migrations/0018_auto_20240425_1328.py diff --git a/georiviere/contribution/migrations/0018_auto_20240425_1328.py b/georiviere/contribution/migrations/0018_auto_20240425_1328.py new file mode 100644 index 00000000..12b97fe0 --- /dev/null +++ b/georiviere/contribution/migrations/0018_auto_20240425_1328.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.14 on 2024-04-25 13:28 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('contribution', '0017_auto_20240423_1542'), + ] + + operations = [ + migrations.AlterModelOptions( + name='customcontribution', + options={'ordering': ('-contributed_at',), 'verbose_name': 'Custom contribution', 'verbose_name_plural': 'Custom contributions'}, + ), + migrations.AddField( + model_name='customcontribution', + name='contributed_at', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Contributed at'), + ), + ] diff --git a/georiviere/contribution/models/__init__.py b/georiviere/contribution/models/__init__.py index 6b8f7e44..e0b7e395 100644 --- a/georiviere/contribution/models/__init__.py +++ b/georiviere/contribution/models/__init__.py @@ -8,6 +8,7 @@ from django.core.mail import mail_managers from django.template.loader import render_to_string from django.utils.text import slugify +from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from geotrek.common.mixins import BasePublishableMixin, TimeStampedModelMixin from geotrek.common.utils import classproperty @@ -842,12 +843,14 @@ class CustomContribution(TimeStampedModelMixin, models.Model): related_name="custom_contributions", on_delete=models.PROTECT, ) + contributed_at = models.DateTimeField(verbose_name=_("Contributed at"), default=now) validated = models.BooleanField(default=False, verbose_name=_("Validated")) objects = CustomContributionManager() class Meta: verbose_name = _("Custom contribution") verbose_name_plural = _("Custom contributions") + ordering = ('-contributed_at',) def __str__(self): return f"{self.custom_type.label} - {self.pk}" From 3ffddadd2defdb2518c157ee273099cf67a2bf78 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Thu, 25 Apr 2024 15:35:35 +0200 Subject: [PATCH 090/114] add shared contribution_at field --- georiviere/portal/serializers/contribution.py | 8 ++- georiviere/portal/signals.py | 66 ++++++++++++++----- georiviere/portal/views/observations.py | 3 +- georiviere/river/tests/test_signals.py | 12 +++- 4 files changed, 64 insertions(+), 25 deletions(-) diff --git a/georiviere/portal/serializers/contribution.py b/georiviere/portal/serializers/contribution.py index 12a02c92..c11a2ca7 100644 --- a/georiviere/portal/serializers/contribution.py +++ b/georiviere/portal/serializers/contribution.py @@ -183,6 +183,8 @@ class Meta: class CustomContributionSerializer(serializers.ModelSerializer): + contributed_at = serializers.DateTimeField(required=True) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) custom_type = self.context.get("custom_type") @@ -228,15 +230,15 @@ def create(self, validated_data): class Meta: model = CustomContribution - exclude = ("data", "custom_type") + exclude = ("data", "custom_type", "validated") class CustomContributionSerializerGeoJSONSerializer( - geo_serializers.GeoFeatureModelSerializer, CustomContributionSerializer, + geo_serializers.GeoFeatureModelSerializer, ): geometry = geo_serializers.GeometryField(read_only=True, precision=7) class Meta(CustomContributionSerializer.Meta): geo_field = "geometry" - exclude = ("geom", "data", "custom_type") + exclude = ("geom", "data", "custom_type", "validated") diff --git a/georiviere/portal/signals.py b/georiviere/portal/signals.py index e089c956..227fcb2d 100644 --- a/georiviere/portal/signals.py +++ b/georiviere/portal/signals.py @@ -7,38 +7,70 @@ from georiviere.valorization.models import POICategory -@receiver(post_delete, sender=POICategory, dispatch_uid='delete_category_maplayer') +@receiver(post_delete, sender=POICategory, dispatch_uid="delete_category_maplayer") def delete_category_maplayer(sender, instance, **kwargs): # Remove every map layer (all portals) when a category is deleted - MapLayer.objects.filter(layer_type__startswith='pois', - layer_type__endswith=instance.pk).delete() + MapLayer.objects.filter( + layer_type__startswith="pois", layer_type__endswith=instance.pk + ).delete() -@receiver(post_save, sender=POICategory, dispatch_uid='save_category') +@receiver(post_save, sender=POICategory, dispatch_uid="save_category") def save_category_maplayer(sender, instance, created, **kwargs): if created: for portal in Portal.objects.all(): # Create a map layer for each portal for this POICategory - MapLayer.objects.create(label=instance.label, order=0, layer_type=f'pois-{instance.pk}', - portal=portal) + MapLayer.objects.create( + label=instance.label, + order=0, + layer_type=f"pois-{instance.pk}", + portal=portal, + ) -@receiver(post_save, sender=Portal, dispatch_uid='save_portal') +@receiver(post_save, sender=Portal, dispatch_uid="save_portal") def save_portal(sender, instance, created, **kwargs): if created: # Generate a default base layer - MapBaseLayer.objects.create(label='OSM', order=0, url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - attribution='© Contributeurs OpenStreetMap', portal=instance) + MapBaseLayer.objects.create( + label="OSM", + order=0, + url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", + attribution="© Contributeurs OpenStreetMap", + portal=instance, + ) # Generate all layers - MapLayer.objects.create(label=_('Watershed'), order=0, layer_type='watersheds', portal=instance) - MapLayer.objects.create(label=_('City'), order=0, layer_type='cities', portal=instance) - MapLayer.objects.create(label=_('Sensitivity'), order=0, layer_type='sensitivities', portal=instance) - MapLayer.objects.create(label=_('District'), order=0, layer_type='districts', portal=instance) - MapLayer.objects.create(label=_('Contribution'), order=0, layer_type='contributions', portal=instance) + MapLayer.objects.create( + label=_("Watershed"), order=0, layer_type="watersheds", portal=instance + ) + MapLayer.objects.create( + label=_("City"), order=0, layer_type="cities", portal=instance + ) + MapLayer.objects.create( + label=_("Sensitivity"), order=0, layer_type="sensitivities", portal=instance + ) + MapLayer.objects.create( + label=_("District"), order=0, layer_type="districts", portal=instance + ) + MapLayer.objects.create( + label=_("Contribution"), + order=0, + layer_type="contributions", + portal=instance, + ) # We generate a map layer for each category of poi with the layer type separated by a - # We use it after in the serializer / view to generate a geojson url for each of them for category in POICategory.objects.all(): - MapLayer.objects.create(label=f'{category.label}', order=0, layer_type=f'pois-{category.pk}', - portal=instance) + MapLayer.objects.create( + label=f"{category.label}", + order=0, + layer_type=f"pois-{category.pk}", + portal=instance, + ) - MapLayer.objects.create(label=_('Stream'), order=0, layer_type='streams', portal=instance) + MapLayer.objects.create( + label=_("Stream"), order=0, layer_type="streams", portal=instance + ) + MapLayer.objects.create( + label=_("Station"), order=0, layer_type="stations", portal=instance + ) diff --git a/georiviere/portal/views/observations.py b/georiviere/portal/views/observations.py index e38a5632..c573755b 100644 --- a/georiviere/portal/views/observations.py +++ b/georiviere/portal/views/observations.py @@ -16,6 +16,5 @@ class StationViewSet(GeoriviereAPIMixin, viewsets.ReadOnlyModelViewSet): geojson_serializer_class = StationGeojsonSerializer def get_queryset(self): - # portal_pk = self.kwargs["portal_pk"] - # Station.objects.filter(custom_contribution_types__portal__id=portal_pk) + Station.objects.filter(custom_contribution_types__isnull=False).distinct('pk') # filter station linked to custom contrib return Station.objects.all().annotate(geom_transformed=Transform(F("geom"), settings.API_SRID)) diff --git a/georiviere/river/tests/test_signals.py b/georiviere/river/tests/test_signals.py index f947d67d..807a40e4 100644 --- a/georiviere/river/tests/test_signals.py +++ b/georiviere/river/tests/test_signals.py @@ -27,8 +27,14 @@ def test_update_stream_move_topologies(self): self.assertEqual(str(stream), stream.name) - morphologies = Morphology.objects.values_list('geom', flat=True) - status = Status.objects.values_list('geom', flat=True) + stream_1_morpho_geom = stream.morphologies.first().geom.ewkt + stream_2_morpho_geom = stream_2.morphologies.first().geom.ewkt - self.assertEqual(morphologies[0], morphologies[1], f"{morphologies[0].ewkt} - {morphologies[1].ewkt}", ) + status = Status.objects.values_list("geom", flat=True) + + self.assertEqual( + stream_1_morpho_geom, + stream_2_morpho_geom, + f"{stream_1_morpho_geom} - {stream_2_morpho_geom}", + ) self.assertEqual(status[0], status[1]) From 8e6260af4561fb2cf6b3080c0b87792bde8a9b76 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Thu, 25 Apr 2024 15:53:51 +0200 Subject: [PATCH 091/114] add station json / geojson urls --- georiviere/portal/serializers/observations.py | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/georiviere/portal/serializers/observations.py b/georiviere/portal/serializers/observations.py index abb9018e..ca552322 100644 --- a/georiviere/portal/serializers/observations.py +++ b/georiviere/portal/serializers/observations.py @@ -1,32 +1,30 @@ +from rest_framework import serializers +from rest_framework.reverse import reverse from rest_framework.serializers import ModelSerializer from rest_framework_gis import serializers as geo_serializers from georiviere.observations.models import Station +from georiviere.portal.serializers.mixins import SerializerAPIMixin -class StationGeojsonSerializer(geo_serializers.GeoFeatureModelSerializer): +class StationMixin(SerializerAPIMixin, ModelSerializer): + json_url = serializers.SerializerMethodField() + geojson_url = serializers.SerializerMethodField() geometry = geo_serializers.GeometryField( read_only=True, precision=7, source="geom_transformed" ) - class Meta: - model = Station - geo_field = "geometry" - id_field = False - fields = ( - "id", - "code", - "label", - "description", - "custom_contribution_types", - "geometry", + def get_json_url(self, obj): + return reverse( + "api_portal:stations-detail", + kwargs=self._get_url_detail_kwargs(pk=obj.pk, format="json"), ) - -class StationSerializer(ModelSerializer): - geometry = geo_serializers.GeometryField( - read_only=True, precision=7, source="geom_transformed" - ) + def get_geojson_url(self, obj): + return reverse( + "api_portal:stations-detail", + kwargs=self._get_url_detail_kwargs(pk=obj.pk, format="geojson"), + ) class Meta: model = Station @@ -38,3 +36,14 @@ class Meta: "custom_contribution_types", "geometry", ) + + +class StationGeojsonSerializer(StationMixin, geo_serializers.GeoFeatureModelSerializer): + class Meta(StationMixin.Meta): + geo_field = "geometry" + id_field = False + + +class StationSerializer(StationMixin, ModelSerializer): + class Meta(StationMixin.Meta): + pass From 894829972b2bcb358e4055e4e4200a4050795d0f Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Thu, 25 Apr 2024 20:57:56 +0200 Subject: [PATCH 092/114] add contributions by station endpoint --- georiviere/portal/serializers/contribution.py | 19 ++++++++++-- georiviere/portal/views/contribution.py | 4 +-- georiviere/portal/views/observations.py | 29 +++++++++++++++++-- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/georiviere/portal/serializers/contribution.py b/georiviere/portal/serializers/contribution.py index c11a2ca7..e3182684 100644 --- a/georiviere/portal/serializers/contribution.py +++ b/georiviere/portal/serializers/contribution.py @@ -233,12 +233,27 @@ class Meta: exclude = ("data", "custom_type", "validated") -class CustomContributionSerializerGeoJSONSerializer( +class CustomContributionGeoJSONSerializer( CustomContributionSerializer, geo_serializers.GeoFeatureModelSerializer, ): geometry = geo_serializers.GeometryField(read_only=True, precision=7) + geom = geo_serializers.GeometryField(write_only=True) + + class Meta(CustomContributionSerializer.Meta): + geo_field = "geometry" + exclude = ("data", "custom_type", "validated") + +class CustomContributionByStationSerializer(CustomContributionSerializer): class Meta(CustomContributionSerializer.Meta): + model = CustomContribution + exclude = ("data", "validated") + + +class CustomContributionByStationGeoJSONSerializer( + CustomContributionGeoJSONSerializer, +): + class Meta(CustomContributionGeoJSONSerializer.Meta): geo_field = "geometry" - exclude = ("geom", "data", "custom_type", "validated") + exclude = ("data", "validated") diff --git a/georiviere/portal/views/contribution.py b/georiviere/portal/views/contribution.py index 89982236..e3be27f1 100644 --- a/georiviere/portal/views/contribution.py +++ b/georiviere/portal/views/contribution.py @@ -33,7 +33,7 @@ ContributionGeojsonSerializer, CustomContributionTypeSerializer, CustomContributionSerializer, - CustomContributionSerializerGeoJSONSerializer, + CustomContributionGeoJSONSerializer, ) from georiviere.portal.views.mixins import GeoriviereAPIMixin @@ -209,7 +209,7 @@ def list_contributions(self, request, *args, **kwargs): renderer, media_type = self.perform_content_negotiation(self.request) if getattr(renderer, "format") == "geojson": self.geojson_serializer_class = ( - CustomContributionSerializerGeoJSONSerializer + CustomContributionGeoJSONSerializer ) qs = qs.annotate(geometry=Transform(F("geom"), settings.API_SRID)) diff --git a/georiviere/portal/views/observations.py b/georiviere/portal/views/observations.py index c573755b..eee1cc9b 100644 --- a/georiviere/portal/views/observations.py +++ b/georiviere/portal/views/observations.py @@ -2,8 +2,14 @@ from django.contrib.gis.db.models.functions import Transform from django.db.models import F from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework.generics import get_object_or_404 +from rest_framework.response import Response +from georiviere.contribution.models import CustomContribution from georiviere.observations.models import Station +from georiviere.portal.serializers.contribution import CustomContributionByStationSerializer, \ + CustomContributionByStationGeoJSONSerializer from georiviere.portal.serializers.observations import ( StationGeojsonSerializer, StationSerializer, @@ -16,5 +22,24 @@ class StationViewSet(GeoriviereAPIMixin, viewsets.ReadOnlyModelViewSet): geojson_serializer_class = StationGeojsonSerializer def get_queryset(self): - Station.objects.filter(custom_contribution_types__isnull=False).distinct('pk') # filter station linked to custom contrib - return Station.objects.all().annotate(geom_transformed=Transform(F("geom"), settings.API_SRID)) + qs = Station.objects.filter(custom_contribution_types__isnull=False).distinct( + "pk" + ) # filter station linked to custom contrib + return qs.annotate(geom_transformed=Transform(F("geom"), settings.API_SRID)) + + @action(detail=True, methods=["get"], + url_path=r"type/(?P\d+)/custom-contributions", url_name='custom-contributions', + serializer_class=CustomContributionByStationSerializer) + def custom_contributions(self, request, *args, **kwargs): + station = self.get_object() + custom_type = get_object_or_404(station.custom_contribution_types.all(), pk=kwargs["type_pk"]) + context = self.get_serializer_context() + context["custom_type"] = custom_type + qs = CustomContribution.objects.with_type_values(custom_type).filter(station=station, validated=True) + renderer, media_type = self.perform_content_negotiation(self.request) + if getattr(renderer, "format") == "geojson": + self.geojson_serializer_class = CustomContributionByStationGeoJSONSerializer + qs = qs.annotate(geometry=Transform(F("geom"), settings.API_SRID)) + + serializer = self.get_serializer(qs, context=context, many=True) + return Response(serializer.data) From f1668ab80bdc0739a488c3c3eed6c470af19f5da Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Thu, 25 Apr 2024 22:56:25 +0200 Subject: [PATCH 093/114] add contributions by station endpoint --- georiviere/portal/serializers/contribution.py | 12 +++++++----- georiviere/portal/views/observations.py | 9 +++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/georiviere/portal/serializers/contribution.py b/georiviere/portal/serializers/contribution.py index e3182684..e72fb952 100644 --- a/georiviere/portal/serializers/contribution.py +++ b/georiviere/portal/serializers/contribution.py @@ -245,15 +245,17 @@ class Meta(CustomContributionSerializer.Meta): exclude = ("data", "custom_type", "validated") -class CustomContributionByStationSerializer(CustomContributionSerializer): +class CustomContributionByStationSerializer(serializers.ModelSerializer): class Meta(CustomContributionSerializer.Meta): model = CustomContribution - exclude = ("data", "validated") + exclude = ("data", "validated", "station") class CustomContributionByStationGeoJSONSerializer( - CustomContributionGeoJSONSerializer, + CustomContributionByStationSerializer, + geo_serializers.GeoFeatureModelSerializer ): - class Meta(CustomContributionGeoJSONSerializer.Meta): + geometry = geo_serializers.GeometryField(read_only=True, precision=7) + + class Meta(CustomContributionByStationSerializer.Meta): geo_field = "geometry" - exclude = ("data", "validated") diff --git a/georiviere/portal/views/observations.py b/georiviere/portal/views/observations.py index eee1cc9b..2ec3ff92 100644 --- a/georiviere/portal/views/observations.py +++ b/georiviere/portal/views/observations.py @@ -28,18 +28,15 @@ def get_queryset(self): return qs.annotate(geom_transformed=Transform(F("geom"), settings.API_SRID)) @action(detail=True, methods=["get"], - url_path=r"type/(?P\d+)/custom-contributions", url_name='custom-contributions', + url_name='custom-contributions', serializer_class=CustomContributionByStationSerializer) def custom_contributions(self, request, *args, **kwargs): station = self.get_object() - custom_type = get_object_or_404(station.custom_contribution_types.all(), pk=kwargs["type_pk"]) - context = self.get_serializer_context() - context["custom_type"] = custom_type - qs = CustomContribution.objects.with_type_values(custom_type).filter(station=station, validated=True) + qs = station.custom_contributions.filter(validated=True) renderer, media_type = self.perform_content_negotiation(self.request) if getattr(renderer, "format") == "geojson": self.geojson_serializer_class = CustomContributionByStationGeoJSONSerializer qs = qs.annotate(geometry=Transform(F("geom"), settings.API_SRID)) - serializer = self.get_serializer(qs, context=context, many=True) + serializer = self.get_serializer(qs, many=True) return Response(serializer.data) From 4ca4f605ed31f78782791a7e1a276865bf183f5c Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Thu, 25 Apr 2024 23:01:08 +0200 Subject: [PATCH 094/114] add contributions by station endpoint --- georiviere/portal/serializers/contribution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/georiviere/portal/serializers/contribution.py b/georiviere/portal/serializers/contribution.py index e72fb952..02d066a4 100644 --- a/georiviere/portal/serializers/contribution.py +++ b/georiviere/portal/serializers/contribution.py @@ -248,7 +248,7 @@ class Meta(CustomContributionSerializer.Meta): class CustomContributionByStationSerializer(serializers.ModelSerializer): class Meta(CustomContributionSerializer.Meta): model = CustomContribution - exclude = ("data", "validated", "station") + exclude = ("data", "validated", "station", "portal") class CustomContributionByStationGeoJSONSerializer( From 0d49878cdf95f0c0fbc70abea2b6ee32dfda7b80 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Fri, 26 Apr 2024 11:48:38 +0200 Subject: [PATCH 095/114] add contributions by station endpoint --- georiviere/portal/serializers/contribution.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/georiviere/portal/serializers/contribution.py b/georiviere/portal/serializers/contribution.py index 02d066a4..50d31b0e 100644 --- a/georiviere/portal/serializers/contribution.py +++ b/georiviere/portal/serializers/contribution.py @@ -187,7 +187,9 @@ class CustomContributionSerializer(serializers.ModelSerializer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - custom_type = self.context.get("custom_type") + custom_type = self.context.get("custom_type", None) + if not custom_type: + return # add and customize fields from json schema schema = custom_type.get_json_schema_form() From 143001e44add5d00e65ef9ed48d35c9a2704c8a7 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Fri, 26 Apr 2024 17:33:21 +0200 Subject: [PATCH 096/114] add internal field --- .../migrations/0019_auto_20240426_1532.py | 27 ++++++ georiviere/contribution/models/__init__.py | 91 +++++++++++++++---- 2 files changed, 102 insertions(+), 16 deletions(-) create mode 100644 georiviere/contribution/migrations/0019_auto_20240426_1532.py diff --git a/georiviere/contribution/migrations/0019_auto_20240426_1532.py b/georiviere/contribution/migrations/0019_auto_20240426_1532.py new file mode 100644 index 00000000..d5490988 --- /dev/null +++ b/georiviere/contribution/migrations/0019_auto_20240426_1532.py @@ -0,0 +1,27 @@ +# Generated by Django 3.1.14 on 2024-04-26 15:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contribution', '0018_auto_20240425_1328'), + ] + + operations = [ + migrations.AddField( + model_name='customcontributiontypefield', + name='internal_identifier', + field=models.CharField(blank=True, help_text='Internal identifier for field.', max_length=128, null=True, unique=True, verbose_name='Internal identifier'), + ), + migrations.AlterField( + model_name='customcontributiontypefield', + name='label', + field=models.CharField(help_text='Field label in forms and public portal.', max_length=128, verbose_name='Label'), + ), + migrations.AlterUniqueTogether( + name='customcontributiontypefield', + unique_together={('label', 'custom_type'), ('internal_identifier', 'custom_type')}, + ), + ] diff --git a/georiviere/contribution/models/__init__.py b/georiviere/contribution/models/__init__.py index e0b7e395..d0610cb5 100644 --- a/georiviere/contribution/models/__init__.py +++ b/georiviere/contribution/models/__init__.py @@ -611,10 +611,18 @@ def __str__(self): # Custom Contribution + class CustomContributionType(models.Model): label = models.CharField(max_length=128, verbose_name=_("Label"), unique=True) - description = models.CharField(max_length=512, verbose_name=_("Description"), blank=True, default="") - stations = models.ManyToManyField('observations.Station', verbose_name=_("Stations"), related_name='custom_contribution_types', blank=True) + description = models.CharField( + max_length=512, verbose_name=_("Description"), blank=True, default="" + ) + stations = models.ManyToManyField( + "observations.Station", + verbose_name=_("Stations"), + related_name="custom_contribution_types", + blank=True, + ) def __str__(self): return self.label @@ -641,6 +649,7 @@ class Meta: ordering = ("label",) +# noinspection PyTypedDict class CustomContributionTypeField(models.Model): class FieldTypeChoices(models.TextChoices): """Choices for field type""" @@ -653,7 +662,19 @@ class FieldTypeChoices(models.TextChoices): DATETIME = "datetime", _("Datetime") BOOLEAN = "boolean", _("Boolean") - label = models.CharField(max_length=128, verbose_name=_("Label"), help_text=_("Field label.")) + label = models.CharField( + max_length=128, + verbose_name=_("Label"), + help_text=_("Field label in forms and public portal."), + ) + internal_identifier = models.CharField( + max_length=128, + verbose_name=_("Internal identifier"), + help_text=_("Internal identifier for field."), + unique=True, + blank=True, + null=True, # allow null because it is not mandatory but unique if valued + ) key = models.SlugField( max_length=150, verbose_name=_("Key"), @@ -666,17 +687,41 @@ class FieldTypeChoices(models.TextChoices): choices=FieldTypeChoices.choices, default=FieldTypeChoices.STRING, ) - required = models.BooleanField(default=False, verbose_name=_("Required"), help_text=_("Set if field is required to validate form.")) + required = models.BooleanField( + default=False, + verbose_name=_("Required"), + help_text=_("Set if field is required to validate form."), + ) help_text = models.CharField( - max_length=256, verbose_name=_("Help text"), blank=True, default="", help_text=_("Set a help text for the field.") + max_length=256, + verbose_name=_("Help text"), + blank=True, + default="", + help_text=_("Set a help text for the field."), + ) + options = models.JSONField( + null=False, + blank=True, + editable=False, + verbose_name=_("Options"), + default=dict, + help_text=_("Internal options for type JSON schema."), ) - options = models.JSONField(null=False, blank=True, editable=False, verbose_name=_("Options"), default=dict, help_text=_("Internal options for type JSON schema.")) customization = models.JSONField( - null=False, blank=True, default=dict, verbose_name=_("Customization"), help_text=_("Field customization.") + null=False, + blank=True, + default=dict, + verbose_name=_("Customization"), + help_text=_("Field customization."), + ) + order = models.PositiveSmallIntegerField( + default=0, verbose_name=_("Order"), help_text=_("Order of field in form.") ) - order = models.PositiveSmallIntegerField(default=0, verbose_name=_("Order"), help_text=_("Order of field in form.")) custom_type = models.ForeignKey( - CustomContributionType, on_delete=models.CASCADE, related_name="fields", verbose_name=_("Custom contribution type.") + CustomContributionType, + on_delete=models.CASCADE, + related_name="fields", + verbose_name=_("Custom contribution type."), ) def __str__(self): @@ -811,7 +856,7 @@ def get_field_schema(self): # drop empty choices customization = self.customization if "choices" in customization and not customization.get("choices"): - customization.pop('choices') + customization.pop("choices") base_schema.update(self.customization) return base_schema @@ -819,18 +864,32 @@ def get_field_schema(self): class Meta: verbose_name = _("Custom contribution type field") verbose_name_plural = _("Custom contribution type fields") - unique_together = (("label", "custom_type"),) # label by type should be unique + unique_together = ( + ("label", "custom_type"), # label by type should be unique + ( + "internal_identifier", + "custom_type", + ), # internal_identifier by type should be unique + ) index_together = (("order", "custom_type"),) ordering = ("order", "custom_type") class CustomContribution(TimeStampedModelMixin, models.Model): geom = models.GeometryField(srid=settings.SRID, spatial_index=True) - station = models.ForeignKey('observations.Station', on_delete=models.PROTECT, - related_name='custom_contributions', verbose_name=_('Station'), blank=True, null=True) + station = models.ForeignKey( + "observations.Station", + on_delete=models.PROTECT, + related_name="custom_contributions", + verbose_name=_("Station"), + blank=True, + null=True, + ) custom_type = models.ForeignKey( - CustomContributionType, on_delete=models.PROTECT, related_name="contributions", - verbose_name=_("Custom contribution type") + CustomContributionType, + on_delete=models.PROTECT, + related_name="contributions", + verbose_name=_("Custom contribution type"), ) data = models.JSONField( verbose_name=_("Data"), null=False, blank=True, default=dict @@ -850,7 +909,7 @@ class CustomContribution(TimeStampedModelMixin, models.Model): class Meta: verbose_name = _("Custom contribution") verbose_name_plural = _("Custom contributions") - ordering = ('-contributed_at',) + ordering = ("-contributed_at",) def __str__(self): return f"{self.custom_type.label} - {self.pk}" From 18d113673261b81c5eae25aa267ef6023f5ca148 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Fri, 26 Apr 2024 17:37:06 +0200 Subject: [PATCH 097/114] add internal field --- georiviere/contribution/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/georiviere/contribution/admin.py b/georiviere/contribution/admin.py index 356b7d51..cb124290 100644 --- a/georiviere/contribution/admin.py +++ b/georiviere/contribution/admin.py @@ -26,7 +26,7 @@ class CustomFieldInline(OrderableAdmin, admin.TabularInline): ordering_field = "order" ordering = ('order', 'label') form = forms.CustomContributionFieldInlineForm - fields = ('label', 'value_type', 'required', 'help_text', 'order') + fields = ('label', 'internal_identifier', 'value_type', 'required', 'help_text', 'order') extra = 0 show_change_link = True popup_link = 'change' @@ -48,7 +48,7 @@ class CustomContributionTypeFieldAdmin(admin.ModelAdmin): form = forms.CustomContributionFieldForm fieldsets = ( (None, { - 'fields': ('custom_type', 'label', 'key', 'value_type', 'required', 'help_text',) + 'fields': ('custom_type', 'label', 'internal_identifier', 'key', 'value_type', 'required', 'help_text',) }), (_('Customization'), { 'fields': ('customization', 'options'), From 9f988046018daed6a660c55b64de4189dae95bf8 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Fri, 26 Apr 2024 17:40:48 +0200 Subject: [PATCH 098/114] fix translations --- .../locale/fr/LC_MESSAGES/django.po | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/georiviere/contribution/locale/fr/LC_MESSAGES/django.po b/georiviere/contribution/locale/fr/LC_MESSAGES/django.po index 600c04fb..6a7ec503 100644 --- a/georiviere/contribution/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/contribution/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-26 15:38+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -82,7 +82,7 @@ msgid "First name author" msgstr "Prénom auteur" msgid "Email" -msgstr "Mail" +msgstr "Courriel" msgid "Observation's date" msgstr "Date de l'observation" @@ -363,8 +363,14 @@ msgstr "Date / Heure" msgid "Boolean" msgstr "Booléen" -msgid "Field label." -msgstr "Libellé du champ." +msgid "Field label in forms and public portal." +msgstr "Libellé du champ dans les formulaires et sur les portails publics." + +msgid "Internal identifier" +msgstr "Identifiant interne" + +msgid "Internal identifier for field." +msgstr "Identifiant interne du champ." msgid "Key" msgstr "Clé" @@ -426,6 +432,9 @@ msgstr "Champ de type de contribution personnalisée" msgid "Custom contribution type fields" msgstr "Champs de types de contributions personnalisées" +msgid "Contributed at" +msgstr "Contribué le" + msgid "Custom contribution" msgstr "Contribution personnalisée" From 39532e1149313a8c00b918a03333c56c76cd746d Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Mon, 29 Apr 2024 09:34:16 +0200 Subject: [PATCH 099/114] add password management and allow send files --- .../locale/en/LC_MESSAGES/django.po | 19 +++- .../locale/fr/LC_MESSAGES/django.po | 10 +- .../0020_customcontributiontype_password.py | 18 ++++ georiviere/contribution/models/__init__.py | 2 + georiviere/portal/serializers/contribution.py | 25 ++++- georiviere/portal/views/contribution.py | 102 ++++++++++++++---- georiviere/portal/views/observations.py | 2 - 7 files changed, 147 insertions(+), 31 deletions(-) create mode 100644 georiviere/contribution/migrations/0020_customcontributiontype_password.py diff --git a/georiviere/contribution/locale/en/LC_MESSAGES/django.po b/georiviere/contribution/locale/en/LC_MESSAGES/django.po index c14dde9a..8cf9f157 100644 --- a/georiviere/contribution/locale/en/LC_MESSAGES/django.po +++ b/georiviere/contribution/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 07:30+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -334,6 +334,12 @@ msgstr "" msgid "Stations" msgstr "" +msgid "Password" +msgstr "" + +msgid "Define if password is required to send the form" +msgstr "" + msgid "Custom contribution type" msgstr "" @@ -361,7 +367,13 @@ msgstr "" msgid "Boolean" msgstr "" -msgid "Field label." +msgid "Field label in forms and public portal." +msgstr "" + +msgid "Internal identifier" +msgstr "" + +msgid "Internal identifier for field." msgstr "" msgid "Key" @@ -424,6 +436,9 @@ msgstr "" msgid "Custom contribution type fields" msgstr "" +msgid "Contributed at" +msgstr "" + msgid "Custom contribution" msgstr "" diff --git a/georiviere/contribution/locale/fr/LC_MESSAGES/django.po b/georiviere/contribution/locale/fr/LC_MESSAGES/django.po index 6a7ec503..441bad08 100644 --- a/georiviere/contribution/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/contribution/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-26 15:38+0000\n" +"POT-Creation-Date: 2024-04-29 07:30+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -336,6 +336,14 @@ msgstr "Connaissance" msgid "Stations" msgstr "Stations" +msgid "Password" +msgstr "Mot de passe" + +msgid "Define if password is required to send the form" +msgstr "" +"Définir si un mot de passe est nécessaire pour envoyer / valider le " +"formulaire." + msgid "Custom contribution type" msgstr "Type de Contribution personnalisée" diff --git a/georiviere/contribution/migrations/0020_customcontributiontype_password.py b/georiviere/contribution/migrations/0020_customcontributiontype_password.py new file mode 100644 index 00000000..a7613993 --- /dev/null +++ b/georiviere/contribution/migrations/0020_customcontributiontype_password.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.14 on 2024-04-29 07:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contribution', '0019_auto_20240426_1532'), + ] + + operations = [ + migrations.AddField( + model_name='customcontributiontype', + name='password', + field=models.CharField(blank=True, default='', help_text='Define if password is required to send the form', max_length=128, verbose_name='Password'), + ), + ] diff --git a/georiviere/contribution/models/__init__.py b/georiviere/contribution/models/__init__.py index d0610cb5..51c0596c 100644 --- a/georiviere/contribution/models/__init__.py +++ b/georiviere/contribution/models/__init__.py @@ -623,6 +623,8 @@ class CustomContributionType(models.Model): related_name="custom_contribution_types", blank=True, ) + password = models.CharField(max_length=128, verbose_name=_("Password"), blank=True, default="", + help_text=_("Define if password is required to send the form")) def __str__(self): return self.label diff --git a/georiviere/portal/serializers/contribution.py b/georiviere/portal/serializers/contribution.py index 50d31b0e..b8aa6a6f 100644 --- a/georiviere/portal/serializers/contribution.py +++ b/georiviere/portal/serializers/contribution.py @@ -177,9 +177,14 @@ class Meta: class CustomContributionTypeSerializer(serializers.ModelSerializer): + password_required = serializers.SerializerMethodField() + + def get_password_required(self, obj): + return bool(obj.password) + class Meta: model = CustomContributionType - fields = ("id", "label", "description", "json_schema_form", "stations") + fields = ("id", "label", "description", "json_schema_form", "stations", "password_required") class CustomContributionSerializer(serializers.ModelSerializer): @@ -218,6 +223,10 @@ def __init__(self, *args, **kwargs): ) self.fields["geom"].required = False + # add password field if required + if custom_type.password: + self.fields["password"] = serializers.CharField(required=True, write_only=True) + def create(self, validated_data): custom_type = self.context.get("custom_type") # add and customize fields from json schema @@ -230,6 +239,17 @@ def create(self, validated_data): validated_data["data"] = data return super().create(validated_data) + def validate(self, data): + # check password if required + custom_type = self.context.get("custom_type") + if ( + custom_type + and custom_type.password + and data.get("password", "") != custom_type.password + ): + raise serializers.ValidationError(_("Password mismatch.")) + return data + class Meta: model = CustomContribution exclude = ("data", "custom_type", "validated") @@ -254,8 +274,7 @@ class Meta(CustomContributionSerializer.Meta): class CustomContributionByStationGeoJSONSerializer( - CustomContributionByStationSerializer, - geo_serializers.GeoFeatureModelSerializer + CustomContributionByStationSerializer, geo_serializers.GeoFeatureModelSerializer ): geometry = geo_serializers.GeometryField(read_only=True, precision=7) diff --git a/georiviere/portal/views/contribution.py b/georiviere/portal/views/contribution.py index e3be27f1..f1a101d7 100644 --- a/georiviere/portal/views/contribution.py +++ b/georiviere/portal/views/contribution.py @@ -9,6 +9,7 @@ from django.core.exceptions import ValidationError from django.core.files import File from django.core.mail import send_mail +from django.db import transaction from django.db.models import F, Q from django.utils import translation from django.utils.translation import gettext_lazy as _ @@ -135,7 +136,7 @@ def create(self, request, *args, **kwargs): os.remove(tmp_jpeg_path) except Exception as e: logger.error( - f"Failed to convert attachment {name}{extension} for report {response.data.get('id')}: " + f"Failed to convert attachment {name}{extension} for contribution {response.data.get('id')}: " + str(e) ) if settings.SEND_REPORT_ACK and response.status_code == 201: @@ -181,24 +182,80 @@ class CustomContributionTypeViewSet( pagination_class = LimitOffsetPagination def create_contribution(self, request, *args, **kwargs): - context = self.get_serializer_context() - custom_type = self.get_object() - context["custom_type"] = custom_type - serializer = self.get_serializer(data=request.data, context=context) - serializer.is_valid(raise_exception=True) - extra_save_params = {} - # if station selected, save its geom - if "station" in serializer.validated_data: - extra_save_params["geom"] = serializer.validated_data["station"].geom - contribution = serializer.save(custom_type=custom_type, **extra_save_params) - # reload with extra fields - contribution = CustomContribution.objects.with_type_values(custom_type).get( - pk=contribution.pk - ) - return Response( - CustomContributionSerializer(contribution, context=context).data, - status=status.HTTP_201_CREATED, - ) + sid = transaction.savepoint() + + try: + context = self.get_serializer_context() + custom_type = self.get_object() + context["custom_type"] = custom_type + serializer = self.get_serializer(data=request.data, context=context) + serializer.is_valid(raise_exception=True) + extra_save_params = {} + # if station selected, save its geom + if "station" in serializer.validated_data: + extra_save_params["geom"] = serializer.validated_data["station"].geom + contribution = serializer.save(custom_type=custom_type, **extra_save_params) + # reload with extra fields + contribution = CustomContribution.objects.with_type_values(custom_type).get( + pk=contribution.pk + ) + + for file in request._request.FILES.values(): + attachment = Attachment( + filetype=FileType.objects.get_or_create( + type=settings.CONTRIBUTION_FILETYPE + )[0], + content_type=ContentType.objects.get_for_model(CustomContribution), + object_id=contribution.pk, + attachment_file=file, + ) + name, extension = os.path.splitext(file.name) + try: + attachment.full_clean() # Check that file extension and mimetypes are allowed + except ValidationError as e: + logger.error( + f"Invalid attachment {name}{extension} for contribution {contribution.pk} : " + + str(e) + ) + else: + try: + # Re-encode file to bitmap then back to jpeg for safety + if not os.path.exists(f"{settings.TMP_DIR}/contribution_file/"): + os.mkdir(f"{settings.TMP_DIR}/contribution_file/") + tmp_bmp_path = os.path.join( + f"{settings.TMP_DIR}/contribution_file/", f"{name}.bmp" + ) + tmp_jpeg_path = os.path.join( + f"{settings.TMP_DIR}/contribution_file/", f"{name}.jpeg" + ) + Image.open(file).save(tmp_bmp_path) + Image.open(tmp_bmp_path).save(tmp_jpeg_path) + with open(tmp_jpeg_path, "rb") as converted_file: + attachment.attachment_file = File( + converted_file, name=f"{name}.jpeg" + ) + attachment.save() + os.remove(tmp_bmp_path) + os.remove(tmp_jpeg_path) + except Exception as e: + logger.error( + f"Failed to convert attachment {name}{extension} for contribution {contribution.pk}: " + + str(e) + ) + transaction.savepoint_commit(sid) + + return Response( + CustomContributionSerializer(contribution, context=context).data, + status=status.HTTP_201_CREATED, + ) + + except Exception as e: + transaction.savepoint_rollback(sid) + logger.error(f"Error {str(e)}") + return Response( + str(e), + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) def list_contributions(self, request, *args, **kwargs): custom_type = self.get_object() @@ -208,9 +265,7 @@ def list_contributions(self, request, *args, **kwargs): renderer, media_type = self.perform_content_negotiation(self.request) if getattr(renderer, "format") == "geojson": - self.geojson_serializer_class = ( - CustomContributionGeoJSONSerializer - ) + self.geojson_serializer_class = CustomContributionGeoJSONSerializer qs = qs.annotate(geometry=Transform(F("geom"), settings.API_SRID)) serializer = self.get_serializer(qs, context=context, many=True) @@ -221,7 +276,8 @@ def list_contributions(self, request, *args, **kwargs): url_name="custom-contributions", url_path="contributions", methods=["get", "post"], - renderer_classes=[renderers.JSONRenderer, GeoJSONRenderer], + renderer_classes=(renderers.JSONRenderer, GeoJSONRenderer), + parser_classes=(MultiPartParser, FormParser, JSONParser), serializer_class=CustomContributionSerializer, ) def contributions(self, request, *args, **kwargs): diff --git a/georiviere/portal/views/observations.py b/georiviere/portal/views/observations.py index 2ec3ff92..fd775319 100644 --- a/georiviere/portal/views/observations.py +++ b/georiviere/portal/views/observations.py @@ -3,10 +3,8 @@ from django.db.models import F from rest_framework import viewsets from rest_framework.decorators import action -from rest_framework.generics import get_object_or_404 from rest_framework.response import Response -from georiviere.contribution.models import CustomContribution from georiviere.observations.models import Station from georiviere.portal.serializers.contribution import CustomContributionByStationSerializer, \ CustomContributionByStationGeoJSONSerializer From b2c25a015a5f723529f9f9db73445dc55b14ac5f Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Mon, 29 Apr 2024 09:56:05 +0200 Subject: [PATCH 100/114] fix password check --- .../contribution/locale/en/LC_MESSAGES/django.po | 2 +- .../contribution/locale/fr/LC_MESSAGES/django.po | 2 +- georiviere/portal/serializers/contribution.py | 14 +++++--------- georiviere/portal/views/contribution.py | 3 ++- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/georiviere/contribution/locale/en/LC_MESSAGES/django.po b/georiviere/contribution/locale/en/LC_MESSAGES/django.po index 8cf9f157..d6151e07 100644 --- a/georiviere/contribution/locale/en/LC_MESSAGES/django.po +++ b/georiviere/contribution/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-29 07:30+0000\n" +"POT-Creation-Date: 2024-04-29 07:55+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/contribution/locale/fr/LC_MESSAGES/django.po b/georiviere/contribution/locale/fr/LC_MESSAGES/django.po index 441bad08..079aeddf 100644 --- a/georiviere/contribution/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/contribution/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-29 07:30+0000\n" +"POT-Creation-Date: 2024-04-29 07:55+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/portal/serializers/contribution.py b/georiviere/portal/serializers/contribution.py index b8aa6a6f..5a0f3832 100644 --- a/georiviere/portal/serializers/contribution.py +++ b/georiviere/portal/serializers/contribution.py @@ -6,7 +6,7 @@ from django.contrib.gis.geos import GEOSGeometry from django.db import transaction from django.db.models import ForeignKey -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import gettext as _ from georiviere.contribution.schema import ( get_contribution_properties, @@ -228,6 +228,7 @@ def __init__(self, *args, **kwargs): self.fields["password"] = serializers.CharField(required=True, write_only=True) def create(self, validated_data): + validated_data.pop("password", None) custom_type = self.context.get("custom_type") # add and customize fields from json schema schema = custom_type.get_json_schema_form() @@ -239,16 +240,11 @@ def create(self, validated_data): validated_data["data"] = data return super().create(validated_data) - def validate(self, data): - # check password if required + def validate_password(self, value): custom_type = self.context.get("custom_type") - if ( - custom_type - and custom_type.password - and data.get("password", "") != custom_type.password - ): + if custom_type and custom_type.password and value != custom_type.password: raise serializers.ValidationError(_("Password mismatch.")) - return data + return value class Meta: model = CustomContribution diff --git a/georiviere/portal/views/contribution.py b/georiviere/portal/views/contribution.py index f1a101d7..a33e4a4b 100644 --- a/georiviere/portal/views/contribution.py +++ b/georiviere/portal/views/contribution.py @@ -189,7 +189,8 @@ def create_contribution(self, request, *args, **kwargs): custom_type = self.get_object() context["custom_type"] = custom_type serializer = self.get_serializer(data=request.data, context=context) - serializer.is_valid(raise_exception=True) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) extra_save_params = {} # if station selected, save its geom if "station" in serializer.validated_data: From 3efdf3bf6b223d74dade48b7153731e62017663f Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Mon, 29 Apr 2024 11:56:30 +0200 Subject: [PATCH 101/114] Improve contribution API by station --- georiviere/portal/serializers/contribution.py | 11 +---------- georiviere/portal/views/observations.py | 10 ++-------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/georiviere/portal/serializers/contribution.py b/georiviere/portal/serializers/contribution.py index 5a0f3832..542c67bd 100644 --- a/georiviere/portal/serializers/contribution.py +++ b/georiviere/portal/serializers/contribution.py @@ -266,13 +266,4 @@ class Meta(CustomContributionSerializer.Meta): class CustomContributionByStationSerializer(serializers.ModelSerializer): class Meta(CustomContributionSerializer.Meta): model = CustomContribution - exclude = ("data", "validated", "station", "portal") - - -class CustomContributionByStationGeoJSONSerializer( - CustomContributionByStationSerializer, geo_serializers.GeoFeatureModelSerializer -): - geometry = geo_serializers.GeometryField(read_only=True, precision=7) - - class Meta(CustomContributionByStationSerializer.Meta): - geo_field = "geometry" + exclude = ("data", "validated", "station", "portal", "geom") diff --git a/georiviere/portal/views/observations.py b/georiviere/portal/views/observations.py index fd775319..7f1b7bcc 100644 --- a/georiviere/portal/views/observations.py +++ b/georiviere/portal/views/observations.py @@ -6,8 +6,7 @@ from rest_framework.response import Response from georiviere.observations.models import Station -from georiviere.portal.serializers.contribution import CustomContributionByStationSerializer, \ - CustomContributionByStationGeoJSONSerializer +from georiviere.portal.serializers.contribution import CustomContributionByStationSerializer from georiviere.portal.serializers.observations import ( StationGeojsonSerializer, StationSerializer, @@ -30,11 +29,6 @@ def get_queryset(self): serializer_class=CustomContributionByStationSerializer) def custom_contributions(self, request, *args, **kwargs): station = self.get_object() - qs = station.custom_contributions.filter(validated=True) - renderer, media_type = self.perform_content_negotiation(self.request) - if getattr(renderer, "format") == "geojson": - self.geojson_serializer_class = CustomContributionByStationGeoJSONSerializer - qs = qs.annotate(geometry=Transform(F("geom"), settings.API_SRID)) - + qs = station.custom_contributions.filter(validated=True).defer(*CustomContributionByStationSerializer.Meta.exclude) serializer = self.get_serializer(qs, many=True) return Response(serializer.data) From 88794b4f22d19ebdc23c7e9418bdb6a53cede6e7 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Mon, 29 Apr 2024 11:57:06 +0200 Subject: [PATCH 102/114] fix test --- georiviere/river/tests/test_signals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/georiviere/river/tests/test_signals.py b/georiviere/river/tests/test_signals.py index 807a40e4..21ecaa71 100644 --- a/georiviere/river/tests/test_signals.py +++ b/georiviere/river/tests/test_signals.py @@ -27,8 +27,8 @@ def test_update_stream_move_topologies(self): self.assertEqual(str(stream), stream.name) - stream_1_morpho_geom = stream.morphologies.first().geom.ewkt - stream_2_morpho_geom = stream_2.morphologies.first().geom.ewkt + stream_1_morpho_geom = stream.morphologies[0].geom.ewkt + stream_2_morpho_geom = stream_2.morphologies[0].geom.ewkt status = Status.objects.values_list("geom", flat=True) From 34ac7d21524043af34610f04ef60b65bfb13c501 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Mon, 29 Apr 2024 15:19:06 +0200 Subject: [PATCH 103/114] add custom contributions endpoint and layer --- .../locale/en/LC_MESSAGES/django.po | 2 +- .../locale/fr/LC_MESSAGES/django.po | 2 +- .../locale/en/LC_MESSAGES/django.po | 2 +- .../locale/fr/LC_MESSAGES/django.po | 2 +- .../locale/en/LC_MESSAGES/django.po | 2 +- .../locale/fr/LC_MESSAGES/django.po | 7 +-- .../flatpages/locale/en/LC_MESSAGES/django.po | 2 +- .../flatpages/locale/fr/LC_MESSAGES/django.po | 2 +- .../knowledge/locale/en/LC_MESSAGES/django.po | 2 +- .../knowledge/locale/fr/LC_MESSAGES/django.po | 2 +- .../main/locale/en/LC_MESSAGES/django.po | 2 +- .../main/locale/fr/LC_MESSAGES/django.po | 2 +- .../locale/en/LC_MESSAGES/django.po | 2 +- .../locale/fr/LC_MESSAGES/django.po | 2 +- .../locale/en/LC_MESSAGES/django.po | 2 +- .../locale/fr/LC_MESSAGES/django.po | 2 +- .../portal/locale/en/LC_MESSAGES/django.po | 16 +++++- .../portal/locale/fr/LC_MESSAGES/django.po | 36 ++++++------- .../migrations/0008_auto_20240429_1233.py | 30 +++++++++++ georiviere/portal/serializers/contribution.py | 51 ++++++++++++++++--- georiviere/portal/serializers/map.py | 6 +-- georiviere/portal/signals.py | 3 ++ georiviere/portal/urls.py | 14 +++-- georiviere/portal/views/contribution.py | 35 +++++++++++++ .../locale/en/LC_MESSAGES/django.po | 2 +- .../locale/fr/LC_MESSAGES/django.po | 2 +- .../river/locale/en/LC_MESSAGES/django.po | 2 +- .../river/locale/fr/LC_MESSAGES/django.po | 2 +- .../studies/locale/en/LC_MESSAGES/django.po | 2 +- .../studies/locale/fr/LC_MESSAGES/django.po | 2 +- .../locale/en/LC_MESSAGES/django.po | 2 +- .../locale/fr/LC_MESSAGES/django.po | 2 +- .../watershed/locale/en/LC_MESSAGES/django.po | 2 +- .../watershed/locale/fr/LC_MESSAGES/django.po | 2 +- 34 files changed, 181 insertions(+), 67 deletions(-) create mode 100644 georiviere/portal/migrations/0008_auto_20240429_1233.py diff --git a/georiviere/contribution/locale/en/LC_MESSAGES/django.po b/georiviere/contribution/locale/en/LC_MESSAGES/django.po index d6151e07..58230866 100644 --- a/georiviere/contribution/locale/en/LC_MESSAGES/django.po +++ b/georiviere/contribution/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-29 07:55+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/contribution/locale/fr/LC_MESSAGES/django.po b/georiviere/contribution/locale/fr/LC_MESSAGES/django.po index 079aeddf..ba1a05a8 100644 --- a/georiviere/contribution/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/contribution/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-29 07:55+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/description/locale/en/LC_MESSAGES/django.po b/georiviere/description/locale/en/LC_MESSAGES/django.po index 825434de..e342172d 100644 --- a/georiviere/description/locale/en/LC_MESSAGES/django.po +++ b/georiviere/description/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/description/locale/fr/LC_MESSAGES/django.po b/georiviere/description/locale/fr/LC_MESSAGES/django.po index 57c5c31d..7f868a5a 100644 --- a/georiviere/description/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/description/locale/fr/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: 2021-11-23 14:20+0000\n" "Last-Translator: Jean-Etienne Castagnede , 2021\n" "Language-Team: French (https://www.transifex.com/georiviere/teams/128005/" diff --git a/georiviere/finances_administration/locale/en/LC_MESSAGES/django.po b/georiviere/finances_administration/locale/en/LC_MESSAGES/django.po index 14db7191..4e253175 100644 --- a/georiviere/finances_administration/locale/en/LC_MESSAGES/django.po +++ b/georiviere/finances_administration/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/finances_administration/locale/fr/LC_MESSAGES/django.po b/georiviere/finances_administration/locale/fr/LC_MESSAGES/django.po index 68ec22f7..5a46fd1d 100644 --- a/georiviere/finances_administration/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/finances_administration/locale/fr/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: 2021-11-23 14:22+0000\n" "Last-Translator: Jean-Etienne Castagnede , 2021\n" "Language-Team: French (https://www.transifex.com/georiviere/teams/128005/" @@ -38,7 +38,7 @@ msgid "Revised budget" msgstr "Budget révisé" msgid "Revised budget of this phase" -msgstr "" +msgstr "Budget révisé pour cette phase" msgid "Actual budget" msgstr "Budget réel" @@ -282,6 +282,3 @@ msgstr "Ajouter un dossier administratif" msgid "Edit phase for" msgstr "Éditer les phases pour" - -#~ msgid "Create operation on" -#~ msgstr "Créer une opération sur" diff --git a/georiviere/flatpages/locale/en/LC_MESSAGES/django.po b/georiviere/flatpages/locale/en/LC_MESSAGES/django.po index abd8afd9..514843fa 100644 --- a/georiviere/flatpages/locale/en/LC_MESSAGES/django.po +++ b/georiviere/flatpages/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/flatpages/locale/fr/LC_MESSAGES/django.po b/georiviere/flatpages/locale/fr/LC_MESSAGES/django.po index 3adf6bce..e2ea1601 100644 --- a/georiviere/flatpages/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/flatpages/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/knowledge/locale/en/LC_MESSAGES/django.po b/georiviere/knowledge/locale/en/LC_MESSAGES/django.po index db6c0c6a..94354787 100644 --- a/georiviere/knowledge/locale/en/LC_MESSAGES/django.po +++ b/georiviere/knowledge/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/knowledge/locale/fr/LC_MESSAGES/django.po b/georiviere/knowledge/locale/fr/LC_MESSAGES/django.po index 89adc735..11ea9087 100644 --- a/georiviere/knowledge/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/knowledge/locale/fr/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: 2021-11-23 15:08+0000\n" "Last-Translator: Jean-Etienne Castagnede , 2021\n" "Language-Team: French (https://www.transifex.com/georiviere/teams/128005/" diff --git a/georiviere/main/locale/en/LC_MESSAGES/django.po b/georiviere/main/locale/en/LC_MESSAGES/django.po index 2f4f6895..1a6a058a 100644 --- a/georiviere/main/locale/en/LC_MESSAGES/django.po +++ b/georiviere/main/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/main/locale/fr/LC_MESSAGES/django.po b/georiviere/main/locale/fr/LC_MESSAGES/django.po index cc9656f7..dbb6e8ec 100644 --- a/georiviere/main/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/main/locale/fr/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: 2021-11-23 15:31+0000\n" "Last-Translator: Jean-Etienne Castagnede , 2021\n" "Language-Team: French (https://www.transifex.com/georiviere/teams/128005/" diff --git a/georiviere/maintenance/locale/en/LC_MESSAGES/django.po b/georiviere/maintenance/locale/en/LC_MESSAGES/django.po index 72bf54c5..7fc23816 100644 --- a/georiviere/maintenance/locale/en/LC_MESSAGES/django.po +++ b/georiviere/maintenance/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/maintenance/locale/fr/LC_MESSAGES/django.po b/georiviere/maintenance/locale/fr/LC_MESSAGES/django.po index 0de2f188..a1ec9142 100644 --- a/georiviere/maintenance/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/maintenance/locale/fr/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: 2021-11-23 15:31+0000\n" "Last-Translator: Jean-Etienne Castagnede , 2021\n" "Language-Team: French (https://www.transifex.com/georiviere/teams/128005/" diff --git a/georiviere/observations/locale/en/LC_MESSAGES/django.po b/georiviere/observations/locale/en/LC_MESSAGES/django.po index 38aa6685..b91040dc 100644 --- a/georiviere/observations/locale/en/LC_MESSAGES/django.po +++ b/georiviere/observations/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/observations/locale/fr/LC_MESSAGES/django.po b/georiviere/observations/locale/fr/LC_MESSAGES/django.po index 3b2da564..20d77f13 100644 --- a/georiviere/observations/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/observations/locale/fr/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: 2021-11-23 15:31+0000\n" "Last-Translator: Jean-Etienne Castagnede , 2021\n" "Language-Team: French (https://www.transifex.com/georiviere/teams/128005/" diff --git a/georiviere/portal/locale/en/LC_MESSAGES/django.po b/georiviere/portal/locale/en/LC_MESSAGES/django.po index 84a815e5..e7c3b15a 100644 --- a/georiviere/portal/locale/en/LC_MESSAGES/django.po +++ b/georiviere/portal/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -39,6 +39,12 @@ msgstr "" msgid "Portal" msgstr "" +msgid "Stations" +msgstr "" + +msgid "Custom contributions" +msgstr "" + msgid "URL" msgstr "" @@ -87,7 +93,10 @@ msgstr "" msgid "Category is not valid" msgstr "" -msgid "An error occured" +msgid "An error occurred" +msgstr "" + +msgid "Password mismatch." msgstr "" msgid "Watershed" @@ -108,6 +117,9 @@ msgstr "" msgid "Stream" msgstr "" +msgid "Station" +msgstr "" + msgid "Georiviere : Contribution" msgstr "" diff --git a/georiviere/portal/locale/fr/LC_MESSAGES/django.po b/georiviere/portal/locale/fr/LC_MESSAGES/django.po index 064bc9ba..20f879be 100644 --- a/georiviere/portal/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/portal/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -39,6 +39,12 @@ msgstr "Fonds de cartes" msgid "Portal" msgstr "Portail" +msgid "Stations" +msgstr "Stations" + +msgid "Custom contributions" +msgstr "Contributions personnalisées" + msgid "URL" msgstr "URL" @@ -87,8 +93,11 @@ msgstr "Portails" msgid "Category is not valid" msgstr "La catégorie n'est pas valide" -msgid "An error occured" -msgstr "Une erreur s'es produite" +msgid "An error occurred" +msgstr "Une erreur s'est produite" + +msgid "Password mismatch." +msgstr "Le mot de passe ne correspond pas." msgid "Watershed" msgstr "Bassin versant" @@ -108,6 +117,9 @@ msgstr "Contribution" msgid "Stream" msgstr "Rivière" +msgid "Station" +msgstr "Station" + msgid "Georiviere : Contribution" msgstr "Georiviere : Contribution" @@ -131,21 +143,3 @@ msgstr "" "\n" "L'équipe Georiviere\n" "http://georiviere.fr" - -#~ msgid "watersheds" -#~ msgstr "bassins versants" - -#~ msgid "cities" -#~ msgstr "villes" - -#~ msgid "sensitivities" -#~ msgstr "zones sensibles" - -#~ msgid "districts" -#~ msgstr "secteurs" - -#~ msgid "contributions" -#~ msgstr "contributions" - -#~ msgid "streams" -#~ msgstr "rivières" diff --git a/georiviere/portal/migrations/0008_auto_20240429_1233.py b/georiviere/portal/migrations/0008_auto_20240429_1233.py new file mode 100644 index 00000000..cfaf76c7 --- /dev/null +++ b/georiviere/portal/migrations/0008_auto_20240429_1233.py @@ -0,0 +1,30 @@ +# Generated by Django 3.1.14 on 2024-04-29 12:33 + +from django.db import migrations +from django.utils.translation import gettext_lazy as _ + + +def add_custom_contributions_map_layer_to_portals(apps, schema_editor): + MapLayer = apps.get_model("portal", "MapLayer") + Portal = apps.get_model("portal", "Portal") + + for portal in Portal.objects.all(): + MapLayer.objects.create( + portal=portal, + layer_type="contributions-custom", + label=_("Custom contributions"), + hidden=True, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("portal", "0007_auto_20240423_1647"), + ] + + operations = [ + migrations.RunPython( + add_custom_contributions_map_layer_to_portals, reverse_code=migrations.RunPython.noop + ), + ] diff --git a/georiviere/portal/serializers/contribution.py b/georiviere/portal/serializers/contribution.py index 542c67bd..40dbbc0d 100644 --- a/georiviere/portal/serializers/contribution.py +++ b/georiviere/portal/serializers/contribution.py @@ -1,5 +1,6 @@ from copy import deepcopy from rest_framework import serializers +from rest_framework.reverse import reverse from rest_framework_gis import serializers as geo_serializers from django.conf import settings @@ -24,6 +25,7 @@ CustomContributionType, CustomContribution, ) +from georiviere.portal.serializers.mixins import SerializerAPIMixin from georiviere.portal.validators import validate_json_schema_data from georiviere.portal.serializers.main import AttachmentSerializer @@ -149,7 +151,7 @@ def create(self, validated_data): transaction.savepoint_rollback(sid) if not msg: msg = f"{e.__class__.__name__} {e}" - raise serializers.ValidationError({"Error": msg or _("An error occured")}) + raise serializers.ValidationError({"Error": msg or _("An error occurred")}) return main_contribution @@ -184,11 +186,34 @@ def get_password_required(self, obj): class Meta: model = CustomContributionType - fields = ("id", "label", "description", "json_schema_form", "stations", "password_required") + fields = ( + "id", + "label", + "description", + "json_schema_form", + "stations", + "password_required", + ) -class CustomContributionSerializer(serializers.ModelSerializer): +class CustomContributionSerializer(SerializerAPIMixin, serializers.ModelSerializer): + geometry = geo_serializers.GeometryField(read_only=True, precision=7) + geom = geo_serializers.GeometryField(write_only=True) contributed_at = serializers.DateTimeField(required=True) + json_url = serializers.SerializerMethodField() + geojson_url = serializers.SerializerMethodField() + + def get_json_url(self, obj): + return reverse( + "api_portal:custom-contributions-detail", + kwargs=self._get_url_detail_kwargs(pk=obj.pk, format="json"), + ) + + def get_geojson_url(self, obj): + return reverse( + "api_portal:custom-contributions-detail", + kwargs=self._get_url_detail_kwargs(pk=obj.pk, format="geojson"), + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -225,7 +250,9 @@ def __init__(self, *args, **kwargs): # add password field if required if custom_type.password: - self.fields["password"] = serializers.CharField(required=True, write_only=True) + self.fields["password"] = serializers.CharField( + required=True, write_only=True + ) def create(self, validated_data): validated_data.pop("password", None) @@ -248,7 +275,11 @@ def validate_password(self, value): class Meta: model = CustomContribution - exclude = ("data", "custom_type", "validated") + exclude = ( + "data", + "custom_type", + "validated", + ) class CustomContributionGeoJSONSerializer( @@ -260,10 +291,16 @@ class CustomContributionGeoJSONSerializer( class Meta(CustomContributionSerializer.Meta): geo_field = "geometry" - exclude = ("data", "custom_type", "validated") + exclude = ("data", "custom_type", "validated",) class CustomContributionByStationSerializer(serializers.ModelSerializer): class Meta(CustomContributionSerializer.Meta): model = CustomContribution - exclude = ("data", "validated", "station", "portal", "geom") + exclude = ( + "data", + "validated", + "station", + "portal", + ) + write_only_fields = ("geom",) diff --git a/georiviere/portal/serializers/map.py b/georiviere/portal/serializers/map.py index 3f3762f1..62bf9da1 100644 --- a/georiviere/portal/serializers/map.py +++ b/georiviere/portal/serializers/map.py @@ -44,7 +44,7 @@ def get_geojson_url(self, obj): layer_type = obj.layer_type.split('-') # TODO: Make lang dynamic reverse_kwargs = {'lang': 'fr', 'format': 'geojson'} - if layer_type[0] in ['watersheds', 'pois', 'streams', 'contributions', 'stations']: + if layer_type[0] in ['watersheds', 'pois', 'streams', 'contributions', 'stations', 'contributions-custom']: reverse_kwargs['portal_pk'] = obj.portal.pk if len(layer_type) == 2: # If the layer type is poi, it's separated by category. @@ -55,11 +55,11 @@ def get_geojson_url(self, obj): def get_url(self, obj): layer_type = obj.layer_type.split('-') - if layer_type[0] not in ['pois', 'streams', 'contributions', 'sensitivities', 'stations']: + if layer_type[0] not in ['pois', 'streams', 'contributions', 'sensitivities', 'stations', 'contributions-custom']: return None # TODO: Make lang dynamic reverse_kwargs = {'lang': 'fr', 'format': 'json'} - if layer_type[0] in ['watersheds', 'pois', 'streams', 'contributions', 'stations']: + if layer_type[0] in ['watersheds', 'pois', 'streams', 'contributions', 'stations', 'contributions-custom']: reverse_kwargs['portal_pk'] = obj.portal.pk return reverse(f'api_portal:{layer_type[0]}-list', kwargs=reverse_kwargs) diff --git a/georiviere/portal/signals.py b/georiviere/portal/signals.py index 227fcb2d..68116b8e 100644 --- a/georiviere/portal/signals.py +++ b/georiviere/portal/signals.py @@ -74,3 +74,6 @@ def save_portal(sender, instance, created, **kwargs): MapLayer.objects.create( label=_("Station"), order=0, layer_type="stations", portal=instance ) + MapLayer.objects.create( + label=_("Custom contributions"), order=0, layer_type="'contributions-custom", portal=instance + ) diff --git a/georiviere/portal/urls.py b/georiviere/portal/urls.py index 1bc4c459..e6d57cde 100644 --- a/georiviere/portal/urls.py +++ b/georiviere/portal/urls.py @@ -7,6 +7,7 @@ from georiviere.portal.views.contribution import ( ContributionViewSet, CustomContributionTypeViewSet, + CustomContributionViewSet, ) from georiviere.portal.views.observations import StationViewSet from georiviere.portal.views.portal import PortalViewSet @@ -26,19 +27,24 @@ ) router = routers.DefaultRouter() + # Datas are available depending on portal or not. router.register( r"(?P[a-z]{2})/(?P\d+)/pois", POIViewSet, basename="pois" ) - router.register( r"(?P[a-z]{2})/(?P\d+)/streams", StreamViewSet, basename="streams" ) - router.register( - r"(?P[a-z]{2})/(?P\d+)/stations", StationViewSet, basename="stations" + r"(?P[a-z]{2})/(?P\d+)/custom-contributions", + CustomContributionViewSet, + basename="custom-contributions", +) +router.register( + r"(?P[a-z]{2})/(?P\d+)/stations", + StationViewSet, + basename="stations", ) - router.register( r"(?P[a-z]{2})/(?P\d+)/flatpages", FlatPageViewSet, diff --git a/georiviere/portal/views/contribution.py b/georiviere/portal/views/contribution.py index a33e4a4b..e91785f8 100644 --- a/georiviere/portal/views/contribution.py +++ b/georiviere/portal/views/contribution.py @@ -286,3 +286,38 @@ def contributions(self, request, *args, **kwargs): return self.list_contributions(request, *args, **kwargs) elif request.method == "POST": return self.create_contribution(request, *args, **kwargs) + + +class CustomContributionViewSet( + GeoriviereAPIMixin, + mixins.RetrieveModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet, +): + queryset = CustomContribution.objects.filter(validated=True).annotate( + geometry=Transform(F("geom"), settings.API_SRID) + ) + serializer_class = CustomContributionSerializer + geojson_serializer_class = CustomContributionGeoJSONSerializer + renderer_classes = ( + ( + renderers.BrowsableAPIRenderer, + JSONRenderer, + GeoJSONRenderer, + ) + if settings.DEBUG + else (JSONRenderer,) + ) + permission_classes = [permissions.AllowAny] + pagination_class = LimitOffsetPagination + + def retrieve(self, request, *args, **kwargs): + """ Customize retrieve method to add custom type values to the response""" + object = self.get_object() + object = CustomContribution.objects.with_type_values(object.custom_type).annotate( + geometry=Transform(F("geom"), settings.API_SRID) + ).get(pk=object.pk) + context = self.get_serializer_context() + context['custom_type'] = object.custom_type + serializer = self.get_serializer(object, context=context) + return Response(serializer.data) diff --git a/georiviere/proceeding/locale/en/LC_MESSAGES/django.po b/georiviere/proceeding/locale/en/LC_MESSAGES/django.po index f98f3019..66dd8f2d 100644 --- a/georiviere/proceeding/locale/en/LC_MESSAGES/django.po +++ b/georiviere/proceeding/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/proceeding/locale/fr/LC_MESSAGES/django.po b/georiviere/proceeding/locale/fr/LC_MESSAGES/django.po index 3c6e92dc..9e70eb69 100644 --- a/georiviere/proceeding/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/proceeding/locale/fr/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: 2021-11-23 15:30+0000\n" "Last-Translator: Jean-Etienne Castagnede , 2021\n" "Language-Team: French (https://www.transifex.com/georiviere/teams/128005/" diff --git a/georiviere/river/locale/en/LC_MESSAGES/django.po b/georiviere/river/locale/en/LC_MESSAGES/django.po index 5bae68ba..52dd1685 100644 --- a/georiviere/river/locale/en/LC_MESSAGES/django.po +++ b/georiviere/river/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/river/locale/fr/LC_MESSAGES/django.po b/georiviere/river/locale/fr/LC_MESSAGES/django.po index d3372859..fdccb02f 100644 --- a/georiviere/river/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/river/locale/fr/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: 2021-11-23 15:31+0000\n" "Last-Translator: Jean-Etienne Castagnede , 2021\n" "Language-Team: French (https://www.transifex.com/georiviere/teams/128005/" diff --git a/georiviere/studies/locale/en/LC_MESSAGES/django.po b/georiviere/studies/locale/en/LC_MESSAGES/django.po index 1b3e0bfc..f47440ea 100644 --- a/georiviere/studies/locale/en/LC_MESSAGES/django.po +++ b/georiviere/studies/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/studies/locale/fr/LC_MESSAGES/django.po b/georiviere/studies/locale/fr/LC_MESSAGES/django.po index 0a6f87e1..2f953198 100644 --- a/georiviere/studies/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/studies/locale/fr/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: 2021-11-23 15:31+0000\n" "Last-Translator: Jean-Etienne Castagnede , 2021\n" "Language-Team: French (https://www.transifex.com/georiviere/teams/128005/" diff --git a/georiviere/valorization/locale/en/LC_MESSAGES/django.po b/georiviere/valorization/locale/en/LC_MESSAGES/django.po index a8a46e5c..162750a0 100644 --- a/georiviere/valorization/locale/en/LC_MESSAGES/django.po +++ b/georiviere/valorization/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/valorization/locale/fr/LC_MESSAGES/django.po b/georiviere/valorization/locale/fr/LC_MESSAGES/django.po index 044195b9..155f4188 100644 --- a/georiviere/valorization/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/valorization/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/watershed/locale/en/LC_MESSAGES/django.po b/georiviere/watershed/locale/en/LC_MESSAGES/django.po index 12689c32..8b48c85a 100644 --- a/georiviere/watershed/locale/en/LC_MESSAGES/django.po +++ b/georiviere/watershed/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/georiviere/watershed/locale/fr/LC_MESSAGES/django.po b/georiviere/watershed/locale/fr/LC_MESSAGES/django.po index 00ab3917..0c8b932b 100644 --- a/georiviere/watershed/locale/fr/LC_MESSAGES/django.po +++ b/georiviere/watershed/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-04-12 19:23+0000\n" +"POT-Creation-Date: 2024-04-29 13:13+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" From 18a78fef7be6d785ab393216a43efeb2ed534198 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Mon, 29 Apr 2024 15:39:12 +0200 Subject: [PATCH 104/114] fix serializer --- georiviere/portal/serializers/map.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/georiviere/portal/serializers/map.py b/georiviere/portal/serializers/map.py index 62bf9da1..3b610e75 100644 --- a/georiviere/portal/serializers/map.py +++ b/georiviere/portal/serializers/map.py @@ -44,9 +44,9 @@ def get_geojson_url(self, obj): layer_type = obj.layer_type.split('-') # TODO: Make lang dynamic reverse_kwargs = {'lang': 'fr', 'format': 'geojson'} - if layer_type[0] in ['watersheds', 'pois', 'streams', 'contributions', 'stations', 'contributions-custom']: + if layer_type[0] in ['watersheds', 'pois', 'streams', 'contributions', 'stations', ]: reverse_kwargs['portal_pk'] = obj.portal.pk - if len(layer_type) == 2: + if layer_type[0] == 'pois': # If the layer type is poi, it's separated by category. filter_type = layer_type[-1] reverse_kwargs['category_pk'] = filter_type @@ -55,7 +55,7 @@ def get_geojson_url(self, obj): def get_url(self, obj): layer_type = obj.layer_type.split('-') - if layer_type[0] not in ['pois', 'streams', 'contributions', 'sensitivities', 'stations', 'contributions-custom']: + if layer_type[0] not in ['pois', 'streams', 'contributions', 'sensitivities', 'stations', ]: return None # TODO: Make lang dynamic reverse_kwargs = {'lang': 'fr', 'format': 'json'} From 08c75d295fc7ceb867a12d67b0156a077723be47 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Mon, 29 Apr 2024 16:03:09 +0200 Subject: [PATCH 105/114] add serializer for attachments --- georiviere/contribution/models/__init__.py | 12 +++++++++--- georiviere/portal/serializers/contribution.py | 9 ++++++++- georiviere/portal/views/contribution.py | 6 +++--- georiviere/portal/views/observations.py | 2 +- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/georiviere/contribution/models/__init__.py b/georiviere/contribution/models/__init__.py index 51c0596c..bf1e828a 100644 --- a/georiviere/contribution/models/__init__.py +++ b/georiviere/contribution/models/__init__.py @@ -2,7 +2,7 @@ from django.conf import settings from django.contrib.auth.models import User -from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.contrib.gis.db import models from django.core.mail import mail_managers @@ -623,8 +623,13 @@ class CustomContributionType(models.Model): related_name="custom_contribution_types", blank=True, ) - password = models.CharField(max_length=128, verbose_name=_("Password"), blank=True, default="", - help_text=_("Define if password is required to send the form")) + password = models.CharField( + max_length=128, + verbose_name=_("Password"), + blank=True, + default="", + help_text=_("Define if password is required to send the form"), + ) def __str__(self): return self.label @@ -879,6 +884,7 @@ class Meta: class CustomContribution(TimeStampedModelMixin, models.Model): geom = models.GeometryField(srid=settings.SRID, spatial_index=True) + attachments = GenericRelation(settings.PAPERCLIP_ATTACHMENT_MODEL) station = models.ForeignKey( "observations.Station", on_delete=models.PROTECT, diff --git a/georiviere/portal/serializers/contribution.py b/georiviere/portal/serializers/contribution.py index 40dbbc0d..2dfb26cd 100644 --- a/georiviere/portal/serializers/contribution.py +++ b/georiviere/portal/serializers/contribution.py @@ -202,6 +202,7 @@ class CustomContributionSerializer(SerializerAPIMixin, serializers.ModelSerializ contributed_at = serializers.DateTimeField(required=True) json_url = serializers.SerializerMethodField() geojson_url = serializers.SerializerMethodField() + attachments = AttachmentSerializer(many=True, read_only=True) def get_json_url(self, obj): return reverse( @@ -291,10 +292,16 @@ class CustomContributionGeoJSONSerializer( class Meta(CustomContributionSerializer.Meta): geo_field = "geometry" - exclude = ("data", "custom_type", "validated",) + exclude = ( + "data", + "custom_type", + "validated", + ) class CustomContributionByStationSerializer(serializers.ModelSerializer): + attachments = AttachmentSerializer(many=True, read_only=True) + class Meta(CustomContributionSerializer.Meta): model = CustomContribution exclude = ( diff --git a/georiviere/portal/views/contribution.py b/georiviere/portal/views/contribution.py index e91785f8..25e899f3 100644 --- a/georiviere/portal/views/contribution.py +++ b/georiviere/portal/views/contribution.py @@ -262,7 +262,7 @@ def list_contributions(self, request, *args, **kwargs): custom_type = self.get_object() context = self.get_serializer_context() context["custom_type"] = custom_type - qs = CustomContribution.objects.with_type_values(custom_type) + qs = CustomContribution.objects.with_type_values(custom_type).prefetch_related("attachments") renderer, media_type = self.perform_content_negotiation(self.request) if getattr(renderer, "format") == "geojson": @@ -296,7 +296,7 @@ class CustomContributionViewSet( ): queryset = CustomContribution.objects.filter(validated=True).annotate( geometry=Transform(F("geom"), settings.API_SRID) - ) + ).prefetch_related("attachments") serializer_class = CustomContributionSerializer geojson_serializer_class = CustomContributionGeoJSONSerializer renderer_classes = ( @@ -316,7 +316,7 @@ def retrieve(self, request, *args, **kwargs): object = self.get_object() object = CustomContribution.objects.with_type_values(object.custom_type).annotate( geometry=Transform(F("geom"), settings.API_SRID) - ).get(pk=object.pk) + ).prefetch_related("attachments").get(pk=object.pk) context = self.get_serializer_context() context['custom_type'] = object.custom_type serializer = self.get_serializer(object, context=context) diff --git a/georiviere/portal/views/observations.py b/georiviere/portal/views/observations.py index 7f1b7bcc..777c4875 100644 --- a/georiviere/portal/views/observations.py +++ b/georiviere/portal/views/observations.py @@ -29,6 +29,6 @@ def get_queryset(self): serializer_class=CustomContributionByStationSerializer) def custom_contributions(self, request, *args, **kwargs): station = self.get_object() - qs = station.custom_contributions.filter(validated=True).defer(*CustomContributionByStationSerializer.Meta.exclude) + qs = station.custom_contributions.filter(validated=True).prefetch_related("attachments").defer(*CustomContributionByStationSerializer.Meta.exclude) serializer = self.get_serializer(qs, many=True) return Response(serializer.data) From 8a00a64b34a83d853c06078170863f3a9184ab45 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Mon, 29 Apr 2024 16:08:15 +0200 Subject: [PATCH 106/114] add attachments in admin --- georiviere/contribution/admin.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/georiviere/contribution/admin.py b/georiviere/contribution/admin.py index cb124290..80e1cabb 100644 --- a/georiviere/contribution/admin.py +++ b/georiviere/contribution/admin.py @@ -1,9 +1,11 @@ from admin_ordering.admin import OrderableAdmin from django.contrib import admin +from django.contrib.contenttypes.admin import GenericTabularInline from django.utils.translation import gettext_lazy as _ from leaflet.admin import LeafletGeoAdmin from . import models, forms +from ..main.models import Attachment admin.site.register(models.ContributionStatus, admin.ModelAdmin) admin.site.register(models.SeverityType, admin.ModelAdmin) @@ -69,11 +71,17 @@ def has_delete_permission(self, request, obj=None): return False +class CustomContribAttachmentInline(GenericTabularInline): + model = Attachment + extra = 0 + + @admin.register(models.CustomContribution) class CustomContributionAdmin(LeafletGeoAdmin, admin.ModelAdmin): list_display = ('custom_type', 'portal', 'validated', 'date_insert', 'date_update') list_filter = ('custom_type', 'portal', 'validated') form = forms.CustomContributionForm + inlines = [CustomContribAttachmentInline] def get_readonly_fields(self, request, obj=None): if not obj or not obj.pk: From dae36552d7c1201292ed62c9c8c16983585bc104 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Mon, 29 Apr 2024 16:10:59 +0200 Subject: [PATCH 107/114] add attachments in admin --- georiviere/contribution/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/georiviere/contribution/admin.py b/georiviere/contribution/admin.py index 80e1cabb..e26aa55b 100644 --- a/georiviere/contribution/admin.py +++ b/georiviere/contribution/admin.py @@ -74,6 +74,7 @@ def has_delete_permission(self, request, obj=None): class CustomContribAttachmentInline(GenericTabularInline): model = Attachment extra = 0 + exclude = ('attachment_video', 'attachment_link', ) @admin.register(models.CustomContribution) From f746b8020ad9af1b50119a285f768330283821e1 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Mon, 29 Apr 2024 16:19:10 +0200 Subject: [PATCH 108/114] add attachments in admin --- georiviere/contribution/admin.py | 91 +++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 25 deletions(-) diff --git a/georiviere/contribution/admin.py b/georiviere/contribution/admin.py index e26aa55b..c5d25da8 100644 --- a/georiviere/contribution/admin.py +++ b/georiviere/contribution/admin.py @@ -1,6 +1,9 @@ from admin_ordering.admin import OrderableAdmin from django.contrib import admin +from django.contrib.admin.widgets import AdminFileWidget from django.contrib.contenttypes.admin import GenericTabularInline +from django.db.models import FileField +from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from leaflet.admin import LeafletGeoAdmin @@ -22,69 +25,107 @@ class CustomFieldInline(OrderableAdmin, admin.TabularInline): - verbose_name = _('Field') - verbose_name_plural = _('Fields') + verbose_name = _("Field") + verbose_name_plural = _("Fields") model = models.CustomContributionTypeField ordering_field = "order" - ordering = ('order', 'label') + ordering = ("order", "label") form = forms.CustomContributionFieldInlineForm - fields = ('label', 'internal_identifier', 'value_type', 'required', 'help_text', 'order') + fields = ( + "label", + "internal_identifier", + "value_type", + "required", + "help_text", + "order", + ) extra = 0 show_change_link = True - popup_link = 'change' + popup_link = "change" @admin.register(models.CustomContributionType) class CustomContributionTypeAdmin(admin.ModelAdmin): - list_display = ('label', ) - search_fields = ('label', ) - filter_horizontal = ('stations', ) - inlines = [CustomFieldInline, ] + list_display = ("label",) + search_fields = ("label",) + filter_horizontal = ("stations",) + inlines = [ + CustomFieldInline, + ] @admin.register(models.CustomContributionTypeField) class CustomContributionTypeFieldAdmin(admin.ModelAdmin): - list_display = ('label', 'key', 'value_type', 'required', 'custom_type') - list_filter = ('custom_type', 'value_type', 'required') - search_fields = ('label', 'key', 'custom_type__label') + list_display = ("label", "key", "value_type", "required", "custom_type") + list_filter = ("custom_type", "value_type", "required") + search_fields = ("label", "key", "custom_type__label") form = forms.CustomContributionFieldForm fieldsets = ( - (None, { - 'fields': ('custom_type', 'label', 'internal_identifier', 'key', 'value_type', 'required', 'help_text',) - }), - (_('Customization'), { - 'fields': ('customization', 'options'), - }), + ( + None, + { + "fields": ( + "custom_type", + "label", + "internal_identifier", + "key", + "value_type", + "required", + "help_text", + ) + }, + ), + ( + _("Customization"), + { + "fields": ("customization", "options"), + }, + ), ) def get_readonly_fields(self, request, obj=None): if obj and obj.pk: - return ['custom_type', 'key', 'options'] + return ["custom_type", "key", "options"] return [] def has_add_permission(self, request): - """ Disable addition in list view """ + """Disable addition in list view""" return False def has_delete_permission(self, request, obj=None): - """ Disable deletion in list view """ + """Disable deletion in list view""" return False +class AdminImageWidget(AdminFileWidget): + def render(self, name, value, attrs=None, renderer=None): + output = [] + if value and getattr(value, "url", None): + image_url = value.url + file_name = str(value) + output.append( + ' %s %s ' + % (image_url, image_url, file_name, _("")) + ) + output.append(super().render(name, value, attrs)) + return mark_safe("".join(output)) + + class CustomContribAttachmentInline(GenericTabularInline): model = Attachment extra = 0 - exclude = ('attachment_video', 'attachment_link', ) + exclude = ("attachment_video", "attachment_link", "creator", "legend", "starred") + formfield_overrides = {FileField: {"widget": AdminImageWidget}} @admin.register(models.CustomContribution) class CustomContributionAdmin(LeafletGeoAdmin, admin.ModelAdmin): - list_display = ('custom_type', 'portal', 'validated', 'date_insert', 'date_update') - list_filter = ('custom_type', 'portal', 'validated') + list_display = ("custom_type", "portal", "validated", "date_insert", "date_update") + list_filter = ("custom_type", "portal", "validated") form = forms.CustomContributionForm inlines = [CustomContribAttachmentInline] def get_readonly_fields(self, request, obj=None): if not obj or not obj.pk: - return ('data', ) + return ("data",) return [] From 902561c510590fe845a939adf59278a775a61c1e Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Mon, 29 Apr 2024 16:21:39 +0200 Subject: [PATCH 109/114] add attachments in admin --- georiviere/contribution/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/georiviere/contribution/admin.py b/georiviere/contribution/admin.py index c5d25da8..24c58338 100644 --- a/georiviere/contribution/admin.py +++ b/georiviere/contribution/admin.py @@ -104,7 +104,7 @@ def render(self, name, value, attrs=None, renderer=None): image_url = value.url file_name = str(value) output.append( - ' %s %s ' + ' %s %s ' % (image_url, image_url, file_name, _("")) ) output.append(super().render(name, value, attrs)) From 95cb9e98b8a8240ff47321b804f1e2ba477dc626 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Mon, 29 Apr 2024 17:49:02 +0200 Subject: [PATCH 110/114] fix signal --- georiviere/portal/signals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/georiviere/portal/signals.py b/georiviere/portal/signals.py index 68116b8e..1fa75639 100644 --- a/georiviere/portal/signals.py +++ b/georiviere/portal/signals.py @@ -75,5 +75,5 @@ def save_portal(sender, instance, created, **kwargs): label=_("Station"), order=0, layer_type="stations", portal=instance ) MapLayer.objects.create( - label=_("Custom contributions"), order=0, layer_type="'contributions-custom", portal=instance + label=_("Custom contributions"), order=0, layer_type="contributions-custom", portal=instance ) From f375b80f10f166c677f616968c9c67a969256f23 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Mon, 29 Apr 2024 21:34:10 +0200 Subject: [PATCH 111/114] fix river trigger --- georiviere/river/sql/post_10_triggers.sql | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/georiviere/river/sql/post_10_triggers.sql b/georiviere/river/sql/post_10_triggers.sql index f83233af..a291aea6 100644 --- a/georiviere/river/sql/post_10_triggers.sql +++ b/georiviere/river/sql/post_10_triggers.sql @@ -40,4 +40,23 @@ $$ LANGUAGE plpgsql; CREATE TRIGGER core_path_10_elevation_iu_tgr AFTER INSERT ON river_stream -FOR EACH ROW EXECUTE PROCEDURE create_topologies(); \ No newline at end of file +FOR EACH ROW EXECUTE PROCEDURE create_topologies(); + +CREATE FUNCTION update_topologies() RETURNS trigger SECURITY DEFINER AS $$ +BEGIN + UPDATE description_morphology m + SET geom = ST_LINESUBSTRING(NEW.geom, t.start_position, t.end_position) + FROM river_topology t + WHERE m.topology_id = t.id AND t.stream_id = NEW.id; + + UPDATE description_status s + SET geom = ST_LINESUBSTRING(NEW.geom, t.start_position, t.end_position) + FROM river_topology t + WHERE s.topology_id = t.id AND t.stream_id = NEW.id; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER river_change_geom_update_topology_tgr +BEFORE UPDATE OF geom ON river_stream +FOR EACH ROW EXECUTE PROCEDURE update_topologies(); \ No newline at end of file From 5c19ac5048b690d08f38be51fb3eb20d25a85c96 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Mon, 29 Apr 2024 21:34:20 +0200 Subject: [PATCH 112/114] fix test --- georiviere/river/tests/test_signals.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/georiviere/river/tests/test_signals.py b/georiviere/river/tests/test_signals.py index 21ecaa71..7924962e 100644 --- a/georiviere/river/tests/test_signals.py +++ b/georiviere/river/tests/test_signals.py @@ -24,17 +24,20 @@ def test_update_stream_move_topologies(self): stream_2 = StreamFactory.create() stream.geom = stream_2.geom stream.save() + stream.refresh_from_db() self.assertEqual(str(stream), stream.name) - stream_1_morpho_geom = stream.morphologies[0].geom.ewkt - stream_2_morpho_geom = stream_2.morphologies[0].geom.ewkt + stream_1_morpho_geom = stream.morphologies[0].geom + stream_2_morpho_geom = stream_2.morphologies[0].geom + self.assertEqual(len(stream.morphologies), 1) + self.assertEqual(len(stream_2.morphologies), 1) status = Status.objects.values_list("geom", flat=True) - + self.assertEqual(stream_1_morpho_geom.length, stream_1_morpho_geom.length) self.assertEqual( - stream_1_morpho_geom, - stream_2_morpho_geom, - f"{stream_1_morpho_geom} - {stream_2_morpho_geom}", + stream_1_morpho_geom.ewkt, + stream_2_morpho_geom.ewkt, + f"{stream_1_morpho_geom.ewkt} - {stream_2_morpho_geom.ewkt}", ) self.assertEqual(status[0], status[1]) From 7033b0bfce15aeb2127dcb975c1b696e4a9b4777 Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Mon, 29 Apr 2024 21:34:41 +0200 Subject: [PATCH 113/114] allow deploy develop branch --- .github/workflows/ci.yml | 40 ++++------------------------------------ 1 file changed, 4 insertions(+), 36 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 120bd7c9..c5f0c8a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,8 @@ on: push: branches: - master + - develop + release: types: - created @@ -91,40 +93,6 @@ jobs: verbose: true fail_ci_if_error: true # optional (default = false) - build-and-push-dev-image: - runs-on: ubuntu-latest - permissions: - packages: write # required to publish docker image - env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - if: ${{(github.base_ref == 'develop')}} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - build-and-push-image: runs-on: ubuntu-latest needs: [flake8, doc_build, unittests] @@ -133,7 +101,7 @@ jobs: env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} - if: ${{ (github.event_name == 'release' && github.event.action == 'created') || (github.ref == 'refs/heads/master' && github.event_name != 'pull_request')}} + if: ${{ (github.event_name == 'release' && github.event.action == 'created') || github.event_name != 'pull_request'}} steps: - name: Checkout repository uses: actions/checkout@v4 @@ -164,7 +132,7 @@ jobs: needs: [ build-and-push-image ] permissions: contents: write # required to attach zip to release - if: ${{ github.event_name == 'release' && github.event.action == 'created' }} + if: ${{ (github.event_name == 'release' && github.event.action == 'created' }} steps: - name: Checkout repository uses: actions/checkout@v4 From 08e89d5d0e12b30135ae5a2eb6dce89347f9eecf Mon Sep 17 00:00:00 2001 From: J-E Castagnede Date: Mon, 29 Apr 2024 21:42:26 +0200 Subject: [PATCH 114/114] set changelog --- docs/changelog.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6dce6fcc..516f478f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,7 +7,8 @@ CHANGELOG **New features** -- add load_rivers command +- Add load_rivers command +- Create custom contribution types from the admin with specific field schema **Bug fix**