loko/streetup/assets/views/location_editing.py
2026-07-22 14:48:40 +02:00

221 lines
8.3 KiB
Python

# --------------------
# 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/<thematic_code>/<location_type>/form/
→ Returns an HTML fragment with an empty form (for create).
GET locations/<thematic_code>/<location_type>/<int:location_id>/form/
→ Returns an HTML fragment pre-filled with the existing location data.
POST locations/<thematic_code>/<location_type>/create/
→ Creates a new location; returns JSON with {success, redirect_url, errors}.
POST locations/<thematic_code>/<location_type>/<int:location_id>/update/
→ Updates the location; returns JSON with {success, errors}.
POST locations/<thematic_code>/<location_type>/<int:location_id>/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 '/'