# -------------------- # Location Editing Views (generic: create / update / delete) # -------------------- """ Generic CRUD views for editable location types. New location types can be supported by adding an entry to assets.forms.LOCATION_FORM_REGISTRY. URL conventions (see assets/urls.py): GET locations///form/ → Returns an HTML fragment with an empty form (for create). GET locations////form/ → Returns an HTML fragment pre-filled with the existing location data. POST locations///create/ → Creates a new location; returns JSON with {success, redirect_url, errors}. POST locations////update/ → Updates the location; returns JSON with {success, errors}. POST locations////delete/ → Deletes the location; returns JSON with {success, redirect_url}. """ from django.http import JsonResponse, Http404 from django.shortcuts import get_object_or_404 from django.template.loader import render_to_string from django.views.decorators.http import require_GET, require_POST from django.utils.translation import gettext as _ from django.contrib.gis.geos import Point, GEOSGeometry, MultiPolygon, Polygon from assets.permissions import can_edit_location_for_thematic from assets.forms import LOCATION_FORM_REGISTRY def _get_registry_entry(thematic_code, location_type): """Return registry entry or raise Http404.""" entry = LOCATION_FORM_REGISTRY.get(location_type) if not entry: raise Http404(f"Location type '{location_type}' not found in registry.") if entry.get('thematic_code') != thematic_code: raise Http404(f"Location type '{location_type}' does not belong to thematic '{thematic_code}'.") return entry def _check_permission(request, thematic_code): """Return 403 JSON response if the user cannot edit locations, otherwise None.""" if not request.user.is_authenticated: return JsonResponse({'success': False, 'error': _("Authentification requise.")}, status=401) if not can_edit_location_for_thematic(request.user, thematic_code): return JsonResponse({'success': False, 'error': _("Permission refusée.")}, status=403) return None @require_GET def location_form_html(request, thematic_code, location_type, location_id=None): """ Returns an HTML fragment containing the form to create or edit a location. Used by the frontend to populate a Bootstrap modal dynamically. """ if not request.user.is_authenticated or not can_edit_location_for_thematic(request.user, thematic_code): return JsonResponse({'error': _("Permission refusée.")}, status=403) entry = _get_registry_entry(thematic_code, location_type) FormClass = entry['form_class'] model_class = FormClass._meta.model instance = None if location_id: instance = get_object_or_404(model_class, pk=location_id) form = FormClass(instance=instance) html = render_to_string( 'assets/includes/location_form_fragment.html', { 'form': form, 'location_type': location_type, 'thematic_code': thematic_code, 'instance': instance, 'entry_label': entry.get('label', location_type), }, request=request, ) return JsonResponse({'html': html}) @require_POST def create_location(request, thematic_code, location_type): """Create a new location of the given type for the given thematic.""" denied = _check_permission(request, thematic_code) if denied: return denied entry = _get_registry_entry(thematic_code, location_type) FormClass = entry['form_class'] form = FormClass(request.POST) if form.is_valid(): instance = form.save(commit=False) # Sync geom from lat/lon if provided and geom is a PointField _sync_geom(instance, form.cleaned_data) instance.save() redirect_url = instance.get_absolute_url() if hasattr(instance, 'get_absolute_url') else None return JsonResponse({ 'success': True, 'redirect_url': redirect_url, 'id': instance.pk, 'label': str(instance), }) else: return JsonResponse({'success': False, 'errors': form.errors}, status=400) @require_POST def update_location(request, thematic_code, location_type, location_id): """Update an existing location.""" denied = _check_permission(request, thematic_code) if denied: return denied entry = _get_registry_entry(thematic_code, location_type) FormClass = entry['form_class'] model_class = FormClass._meta.model instance = get_object_or_404(model_class, pk=location_id) form = FormClass(request.POST, instance=instance) if form.is_valid(): updated = form.save(commit=False) _sync_geom(updated, form.cleaned_data) updated.save() return JsonResponse({'success': True, 'label': str(updated)}) else: return JsonResponse({'success': False, 'errors': form.errors}, status=400) @require_POST def delete_location(request, thematic_code, location_type, location_id): """Delete an existing location.""" denied = _check_permission(request, thematic_code) if denied: return denied entry = _get_registry_entry(thematic_code, location_type) FormClass = entry['form_class'] model_class = FormClass._meta.model instance = get_object_or_404(model_class, pk=location_id) # Build listing redirect URL before deleting redirect_url = _get_listing_url(thematic_code) instance.delete() return JsonResponse({'success': True, 'redirect_url': redirect_url}) def _sync_geom(instance, cleaned_data): """ Synchronise the geometry field of *instance* from the form's cleaned data. - Point / MultiPoint fields: built from lat / lon (WGS84). - Polygon / MultiPolygon fields: built from a GeoJSON string stored in geom_geojson (drawn by the polygon picker on the frontend). The geometry is reprojected to the field's native SRID when needed. """ if not hasattr(instance, 'geom'): return from django.contrib.gis.db.models import PointField, MultiPointField field = type(instance)._meta.get_field('geom') geom_type = getattr(field, 'geom_type', '').upper() if geom_type in ('POINT', 'MULTIPOINT') or isinstance(field, (PointField, MultiPointField)): lat = cleaned_data.get('lat') lon = cleaned_data.get('lon') if lat is not None and lon is not None: srid = getattr(field, 'srid', 4326) if srid == 4326: instance.geom = Point(lon, lat, srid=4326) else: pt = Point(lon, lat, srid=4326) pt.transform(srid) instance.geom = pt else: # Polygon / MultiPolygon (and any other geometry): read GeoJSON drawn # by the polygon picker widget. geojson_str = (cleaned_data.get('geom_geojson') or '').strip() if not geojson_str: return try: geom = GEOSGeometry(geojson_str, srid=4326) # Promote a simple Polygon to MultiPolygon if the field expects it if geom_type == 'MULTIPOLYGON' and isinstance(geom, Polygon): geom = MultiPolygon(geom, srid=4326) srid = getattr(field, 'srid', 4326) if srid != 4326: geom.transform(srid) instance.geom = geom except Exception: pass def _get_listing_url(thematic_code): """Return the URL of the listing page for a given thematic, used after deletion.""" from django.urls import reverse, NoReverseMatch listing_url_names = { 'trafficlights': 'assets:trafficlights_index', 'publiclighting': 'assets:publiclighting_index', 'sign': 'assets:sign_index', 'roads': 'assets:roads_index', 'structures': 'assets:structures_index', 'nature': 'assets:nature_index', 'controlcenters': 'assets:controlcenters_index', 'its': 'assets:its_index', 'clean': 'assets:clean_index', } url_name = listing_url_names.get(thematic_code) if url_name: try: return reverse(url_name) except NoReverseMatch: pass return '/'