diff --git a/loko/assets/management/commands/sync_all_geojson.py b/loko/assets/management/commands/sync_all_geojson.py new file mode 100644 index 0000000..b17a144 --- /dev/null +++ b/loko/assets/management/commands/sync_all_geojson.py @@ -0,0 +1,85 @@ +from django.core.management.base import BaseCommand +from django.apps import apps +from django.db import connection, transaction + + +class Command(BaseCommand): + help = "Synchronizes the geojson field (and lon/lat if missing) from geom for all assets/models using PostGIS SQL." + + def add_arguments(self, parser): + parser.add_argument( + '--dry-run', + action='store_true', + help='Perform a dry run without modifying the database.', + ) + parser.add_argument( + '--only-missing', + action='store_true', + help='Only update records where geojson is null or empty.', + ) + + def handle(self, *args, **options): + dry_run = options['dry_run'] + only_missing = options['only_missing'] + + total_scanned = 0 + total_updated = 0 + + with connection.cursor() as cursor: + for model in apps.get_models(): + fields = [f.name for f in model._meta.get_fields()] + if 'geom' not in fields or 'geojson' not in fields: + continue + + table = model._meta.db_table + is_json_field = model._meta.get_field('geojson').get_internal_type() == 'JSONField' + has_lon = 'lon' in fields + has_lat = 'lat' in fields + + # Count total candidates with non-null geom + cursor.execute(f'SELECT COUNT(*) FROM "{table}" WHERE geom IS NOT NULL;') + count = cursor.fetchone()[0] + if count == 0: + continue + + self.stdout.write(f"Processing {model._meta.app_label}.{model.__name__} ({table}, {count} candidate records)...") + + if only_missing: + where_clause = "geom IS NOT NULL AND (geojson IS NULL OR geojson::text = '' OR geojson::text = 'null')" + else: + where_clause = "geom IS NOT NULL" + + cursor.execute(f'SELECT COUNT(*) FROM "{table}" WHERE {where_clause};') + records_to_update = cursor.fetchone()[0] + + if records_to_update == 0: + self.stdout.write(self.style.SUCCESS(f" {model.__name__}: 0 records need update.")) + continue + + if dry_run: + self.stdout.write(self.style.SUCCESS(f" [DRY-RUN] {model.__name__}: Would update {records_to_update}/{count} records.")) + total_scanned += count + total_updated += records_to_update + continue + + cast_geojson = "ST_AsGeoJSON(ST_Transform(geom, 4326))::jsonb" if is_json_field else "ST_AsGeoJSON(ST_Transform(geom, 4326))" + + set_clauses = [f"geojson = {cast_geojson}"] + if has_lon: + set_clauses.append("lon = COALESCE(lon, ST_X(ST_PointOnSurface(ST_Transform(geom, 4326))))") + if has_lat: + set_clauses.append("lat = COALESCE(lat, ST_Y(ST_PointOnSurface(ST_Transform(geom, 4326))))") + + set_str = ", ".join(set_clauses) + update_sql = f'UPDATE "{table}" SET {set_str} WHERE {where_clause};' + + with transaction.atomic(): + cursor.execute(update_sql) + updated_count = cursor.rowcount + + self.stdout.write(self.style.SUCCESS(f" {model.__name__}: {updated_count}/{count} records updated in DB.")) + total_scanned += count + total_updated += updated_count + + prefix = "[DRY-RUN] Would update" if dry_run else "Successfully updated" + self.stdout.write(self.style.SUCCESS(f"\n{prefix} {total_updated} / {total_scanned} records across all models.")) diff --git a/loko/assets/migrations/0021_sync_asset_parent_data.py b/loko/assets/migrations/0021_sync_asset_parent_data.py index cd382a4..e013395 100644 --- a/loko/assets/migrations/0021_sync_asset_parent_data.py +++ b/loko/assets/migrations/0021_sync_asset_parent_data.py @@ -10,6 +10,7 @@ def sync_parent_data(apps, schema_editor): class Migration(migrations.Migration): + atomic = False dependencies = [ ('assets', '0020_trafficlightlantern_geojson_trafficlightlantern_geom_and_more'), diff --git a/loko/assets/migrations/0025_artworkfountain_external_reference_and_more.py b/loko/assets/migrations/0025_artworkfountain_external_reference_and_more.py new file mode 100644 index 0000000..dac8e1a --- /dev/null +++ b/loko/assets/migrations/0025_artworkfountain_external_reference_and_more.py @@ -0,0 +1,213 @@ +# Generated by Django 6.0.7 on 2026-08-26 13:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0024_structureexceptionaltransport_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='artworkfountain', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='artworkmonument', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='buildingasset', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='cleanglasscontainer', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='cleanlitterbin', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='controlcentergeoasset', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='itsgeoasset', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='naturebench', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='naturegreensurface', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='naturerwiasset', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='natureshrub', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='naturetree', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='outdoorasset', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='parkingspot', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='publiclightingelectricalcabinet', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='publiclightinglightfixture', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='publiclightingpole', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='roadconcreteblockset', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='roaddrain', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='roadfurniture', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='roadsidewalk', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='roadway', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='signpanel', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='signparkingspot', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='signpole', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='structuregeoasset', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='trafficlightaccessory', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='trafficlightcable', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='trafficlightcontroller', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='trafficlightcontrollerhardware', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='trafficlightdetector', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='trafficlightelectricalcabinet', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='trafficlightelectricalcabinetcontent', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='trafficlightlantern', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='trafficlightnetworkhardware', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='trafficlightpole', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='trafficlightprogramming', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='trafficlightradar', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='trafficlightradarpole', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + migrations.AddField( + model_name='trafficlightvoltagehardware', + name='external_reference', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe'), + ), + ] diff --git a/loko/assets/models/core.py b/loko/assets/models/core.py index 99b887d..d8ab50e 100644 --- a/loko/assets/models/core.py +++ b/loko/assets/models/core.py @@ -268,6 +268,7 @@ class AbstractAsset(models.Model): code = models.CharField(max_length=50) name_fr = models.CharField(max_length=200,null=True,blank=True) name_nl = models.CharField(max_length=200,null=True,blank=True) + external_reference = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Référence externe")) status = models.CharField(choices=ASSET_STATUS_CHOICES, default='active', max_length=50) obsolescence = models.CharField(choices=ASSET_OBSOLESCENCE_CHOICES, default='unknown', null=True, blank=True) installation_date = models.DateTimeField(null=True,blank=True) diff --git a/loko/assets/permissions.py b/loko/assets/permissions.py index 27213ee..1decb21 100644 --- a/loko/assets/permissions.py +++ b/loko/assets/permissions.py @@ -14,32 +14,32 @@ from common.models import UserConfig, UserThematics, UserAssetAccess, UserAssetT # Fields editable by role for assets ASSET_EDITABLE_FIELDS_BY_ROLE = { 'admin': [ - 'code', 'name_fr', 'name_nl', 'status', 'model', 'category', + 'code', 'external_reference', 'name_fr', 'name_nl', 'status', 'model', 'category', 'serial_number', 'installation_date', 'brand', 'warranty_duration', 'last_inspection_date', 'funding_program', 'geom', 'lon', 'lat', 'location_id', 'intersection_id', ], 'manager': [ - 'code', 'name_fr', 'name_nl', 'status', 'model', 'category', + 'code', 'external_reference', 'name_fr', 'name_nl', 'status', 'model', 'category', 'serial_number', 'installation_date', 'brand', 'warranty_duration', 'last_inspection_date', 'funding_program', 'geom', 'lon', 'lat', 'location_id', 'intersection_id', ], 'controller': [ - 'code', 'name_fr', 'name_nl', 'status', 'model', 'category', + 'code', 'external_reference', 'name_fr', 'name_nl', 'status', 'model', 'category', 'serial_number', 'installation_date', 'brand', 'warranty_duration', 'last_inspection_date', 'funding_program', 'geom', 'lon', 'lat', 'location_id', 'intersection_id', ], 'external_manager': [ - 'name_fr', 'name_nl', 'category', 'brand', 'serial_number', 'funding_program' + 'name_fr', 'name_nl', 'external_reference', 'category', 'brand', 'serial_number', 'funding_program' ], - 'technician': ['name_fr', 'name_nl', 'category', 'brand', 'serial_number', 'funding_program'], + 'technician': ['name_fr', 'name_nl', 'external_reference', 'category', 'brand', 'serial_number', 'funding_program'], 'operator': [], 'observer': [], 'viewer': [], 'editor': [ - 'code', 'name_fr', 'name_nl', 'status', 'model', 'category', + 'code', 'external_reference', 'name_fr', 'name_nl', 'status', 'model', 'category', 'serial_number', 'installation_date', 'brand', 'warranty_duration', 'last_inspection_date', 'funding_program', 'geom', 'lon', 'lat', 'location_id', 'intersection_id', diff --git a/loko/assets/templates/assets/detail_generic.html b/loko/assets/templates/assets/detail_generic.html index 4b43268..c222753 100644 --- a/loko/assets/templates/assets/detail_generic.html +++ b/loko/assets/templates/assets/detail_generic.html @@ -222,6 +222,21 @@ + + + {% if asset.external_reference or 'external_reference' in allowed_update_fields %} +