loko/loko/assets/views/nature.py

1689 lines
No EOL
66 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from django.shortcuts import render, get_object_or_404, redirect
from django.http import JsonResponse, Http404
from django.contrib.gis.db.models.functions import Transform
from django.contrib.gis.geos import GEOSGeometry, Point, MultiPolygon
from django.contrib.gis.db.models.functions import Distance
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import get_language, gettext as _
from django.db.models import F, Func, Q, Case, When, CharField, IntegerField, Value, Sum, Count
from django.db.models import Max
from django.db.models.functions import Lower
from django.views.decorators.http import require_GET
from django.urls import reverse
import uuid
import datetime
import json
from collections import defaultdict
import unicodedata
from common.utils import check_thematic_access, measure_time
from common.models import UserThematics
from assets.models import NatureLocation, NatureGreenSurface, NatureAssetModel, NatureTree, NatureShrub, NatureBench, AssetCategory, NatureRWIZ, NatureRWIAsset
from assets.models import GreenSurfacesComment, GreenSurfacesCommentPhoto
from interventions.models import Intervention, InterventionLocation, InterventionAsset
from interventions.views.drafts import InterventionDraftManager
@check_thematic_access('nature')
def nature_index(request):
query_params = request.GET.copy()
if 'thematic' not in query_params:
query_params['thematic'] = 'nature'
return redirect(f"{reverse('assets:index')}?{query_params.urlencode()}")
def nature_locations(request):
return redirect(f"{reverse('assets:index')}?thematic=nature")
@check_thematic_access('nature')
def nature_locations_detail(request, location_id):
section = get_object_or_404(NatureLocation, id=location_id)
content_type = ContentType.objects.get_for_model(section)
# 1. Direct assets linked to this location (evaluated once)
trees = list(section.nature_trees.select_related('model', 'category').all())
green_surfaces = list(section.nature_green_surfaces.select_related('model', 'category').all())
shrubs = list(section.nature_shrubs.select_related('model', 'category').all())
benches = list(section.nature_benches.select_related('model', 'category').all())
# 2. Key figures & metrics
trees_count = len(trees)
trees_active_count = sum(1 for t in trees if getattr(t, 'status', None) == 'active')
trees_remarkable_count = sum(1 for t in trees if getattr(t, 'is_remarkable', False))
green_surfaces_count = len(green_surfaces)
total_green_surface = sum((gs.surface_area or 0.0) for gs in green_surfaces)
def is_massif_arbore(gs):
if not getattr(gs, 'model', None):
return False
n_fr = (gs.model.name_fr or '').lower()
n_nl = (gs.model.name_nl or '').lower()
code = (gs.model.code or '').upper()
return (
'arboré' in n_fr or 'arbore' in n_fr or
'boom' in n_nl or 'hout' in n_nl or
'ARBORE' in code
)
def is_pre_fleuri(gs):
if not getattr(gs, 'model', None):
return False
n_fr = (gs.model.name_fr or '').lower()
n_nl = (gs.model.name_nl or '').lower()
code = (gs.model.code or '').upper()
return (
'fleuri' in n_fr or 'fleur' in n_fr or
'bloem' in n_nl or
'FLEURI' in code
)
surface_massif_arbore = sum((gs.surface_area or 0.0) for gs in green_surfaces if is_massif_arbore(gs))
massif_arbore_count = sum(1 for gs in green_surfaces if is_massif_arbore(gs))
surface_pre_fleuri = sum((gs.surface_area or 0.0) for gs in green_surfaces if is_pre_fleuri(gs))
pre_fleuri_count = sum(1 for gs in green_surfaces if is_pre_fleuri(gs))
# Breakdown by green surface typology
type_map = {}
for gs in green_surfaces:
m_id = gs.model_id or 0
if m_id not in type_map:
m_name = gs.model.get_name() if getattr(gs, 'model', None) else str(_("Autre / Non défini"))
m_color = (gs.model.color if (getattr(gs, 'model', None) and gs.model.color) else '#4caf50')
type_map[m_id] = {
'name': m_name,
'color': m_color,
'count': 0,
'total_area': 0.0,
}
type_map[m_id]['count'] += 1
type_map[m_id]['total_area'] += (gs.surface_area or 0.0)
surfaces_by_type = sorted(type_map.values(), key=lambda x: x['total_area'], reverse=True)
for item in surfaces_by_type:
item['percent'] = round((item['total_area'] / total_green_surface * 100), 1) if total_green_surface > 0 else 0
shrubs_count = len(shrubs)
benches_count = len(benches)
total_assets_count = trees_count + green_surfaces_count + shrubs_count + benches_count
# 3. Interventions linked to location + linked to assets on this location
location_intervention_ids = set(InterventionLocation.objects.filter(
content_type=content_type, object_id=section.id
).values_list('intervention_id', flat=True))
tree_ids = [t.id for t in trees]
gs_ids = [gs.id for gs in green_surfaces]
shrub_ids = [s.id for s in shrubs]
bench_ids = [b.id for b in benches]
asset_tuples = [
(ContentType.objects.get_for_model(NatureTree), tree_ids),
(ContentType.objects.get_for_model(NatureGreenSurface), gs_ids),
(ContentType.objects.get_for_model(NatureShrub), shrub_ids),
(ContentType.objects.get_for_model(NatureBench), bench_ids),
]
for ct, ids in asset_tuples:
if ids:
int_ids = InterventionAsset.objects.filter(
content_type=ct, object_id__in=ids
).values_list('intervention_id', flat=True)
location_intervention_ids.update(int_ids)
interventions = list(Intervention.objects.filter(
id__in=location_intervention_ids
).select_related('contract').order_by('-creation_time'))
# 4. Documents
documents = []
try:
from documents.models import DocumentAttachment
attachments = DocumentAttachment.objects.filter(
content_type=content_type, object_id=section.id
).select_related('document').prefetch_related('document__versions')
for att in attachments:
doc = att.document
latest_version = doc.latest_version if hasattr(doc, 'latest_version') else None
documents.append({
'title': doc.title or getattr(latest_version, 'filename', str(doc)),
'date': doc.created_at,
'tag': doc.tag.name_fr if (hasattr(doc, 'tag') and doc.tag) else '',
'url': latest_version.file.url if (latest_version and latest_version.file) else None,
'thumbnail_url': latest_version.thumbnail.url if (latest_version and getattr(latest_version, 'thumbnail', None)) else None,
})
except Exception:
pass
if hasattr(section, 'documents'):
for doc in section.documents.all():
documents.append({
'title': doc.file.name if hasattr(doc, 'file') and doc.file else str(doc),
'date': getattr(doc, 'upload_date', None),
'tag': '',
'url': doc.file.url if (hasattr(doc, 'file') and doc.file) else None,
'thumbnail_url': doc.thumbnail.url if (hasattr(doc, 'thumbnail') and doc.thumbnail) else None,
})
# 5. Serialized GeoJSON for map display
assets_features = []
for gs in green_surfaces:
geom = None
if gs.geojson:
try:
geom = json.loads(gs.geojson)
except Exception:
geom = None
elif gs.geom:
try:
geom = json.loads(gs.geom.transform(4326, clone=True).geojson)
except Exception:
geom = None
if geom:
assets_features.append({
"type": "Feature",
"id": f"gs_{gs.id}",
"geometry": geom,
"properties": {
"id": gs.id,
"code": gs.code,
"name": gs.get_name(),
"model": gs.model.get_name() if gs.model else '',
"asset_type": "naturegreensurface",
"area": f"{gs.surface_area:.1f} m²" if gs.surface_area else '',
"color": gs.model.color if (gs.model and gs.model.color) else '#4caf50',
"status": gs.get_status_display(),
"lon": gs.lon,
"lat": gs.lat,
"detail_url": reverse('assets:nature_assets_detail', kwargs={'asset_model': 'naturegreensurface', 'asset_id': gs.id}),
}
})
for tree in trees:
geom = None
if tree.geojson:
try:
geom = json.loads(tree.geojson)
except Exception:
geom = None
elif tree.lon is not None and tree.lat is not None:
geom = {"type": "Point", "coordinates": [tree.lon, tree.lat]}
if geom:
assets_features.append({
"type": "Feature",
"id": f"tree_{tree.id}",
"geometry": geom,
"properties": {
"id": tree.id,
"code": tree.code,
"name": tree.get_name() or (tree.model.get_name() if tree.model else ''),
"model": tree.model.get_name() if tree.model else '',
"scientific_name": tree.model.scientific_name if (tree.model and tree.model.scientific_name) else '',
"asset_type": "naturetree",
"color": "#2e7d32",
"vitality": tree.get_vitality_display() if tree.vitality else '',
"stage": tree.get_development_stage_display() if tree.development_stage else '',
"height": f"{tree.height} m" if tree.height else '',
"status": tree.get_status_display(),
"is_remarkable": tree.is_remarkable,
"lon": tree.lon,
"lat": tree.lat,
"detail_url": reverse('assets:nature_assets_detail', kwargs={'asset_model': 'naturetree', 'asset_id': tree.id}),
}
})
for shrub in shrubs:
geom = None
if shrub.geojson:
try:
geom = json.loads(shrub.geojson)
except Exception:
geom = None
elif shrub.lon is not None and shrub.lat is not None:
geom = {"type": "Point", "coordinates": [shrub.lon, shrub.lat]}
if geom:
assets_features.append({
"type": "Feature",
"id": f"shrub_{shrub.id}",
"geometry": geom,
"properties": {
"id": shrub.id,
"code": shrub.code,
"model": shrub.model.get_name() if shrub.model else '',
"asset_type": "natureshrub",
"color": "#81c784",
"height": f"{shrub.height} m" if shrub.height else '',
"status": shrub.get_status_display(),
"lon": shrub.lon,
"lat": shrub.lat,
"detail_url": reverse('assets:nature_assets_detail', kwargs={'asset_model': 'natureshrub', 'asset_id': shrub.id}),
}
})
for bench in benches:
geom = None
if bench.geojson:
try:
geom = json.loads(bench.geojson)
except Exception:
geom = None
elif bench.lon is not None and bench.lat is not None:
geom = {"type": "Point", "coordinates": [bench.lon, bench.lat]}
if geom:
assets_features.append({
"type": "Feature",
"id": f"bench_{bench.id}",
"geometry": geom,
"properties": {
"id": bench.id,
"code": bench.code,
"model": bench.model.get_name() if bench.model else '',
"asset_type": "naturebench",
"color": "#8d6e63",
"material": bench.material or '',
"status": bench.get_status_display(),
"lon": bench.lon,
"lat": bench.lat,
"detail_url": reverse('assets:nature_assets_detail', kwargs={'asset_model': 'naturebench', 'asset_id': bench.id}),
}
})
assets_geojson = json.dumps({"type": "FeatureCollection", "features": assets_features})
# Location geometry for map
section_geojson = section.geojson
if not section_geojson and section.geom:
try:
section_geojson = section.geom.transform(4326, clone=True).geojson
except Exception:
section_geojson = None
feature_lon = section.lon
feature_lat = section.lat
if (feature_lon is None or feature_lat is None) and section.geom:
try:
pt = section.geom.transform(4326, clone=True).centroid
feature_lon = pt.x
feature_lat = pt.y
except Exception:
pass
context = {
'section': section,
'trees': trees,
'green_surfaces': green_surfaces,
'shrubs': shrubs,
'benches': benches,
'trees_count': trees_count,
'trees_active_count': trees_active_count,
'trees_remarkable_count': trees_remarkable_count,
'green_surfaces_count': green_surfaces_count,
'total_green_surface': total_green_surface,
'surface_massif_arbore': surface_massif_arbore,
'massif_arbore_count': massif_arbore_count,
'surface_pre_fleuri': surface_pre_fleuri,
'pre_fleuri_count': pre_fleuri_count,
'surfaces_by_type': surfaces_by_type,
'shrubs_count': shrubs_count,
'benches_count': benches_count,
'total_assets_count': total_assets_count,
'interventions': interventions,
'documents': documents,
'assets_geojson': assets_geojson,
'section_geojson': section_geojson,
'feature_lon': feature_lon,
'feature_lat': feature_lat,
}
return render(request, "assets/nature_locations_detail.html", context)
@check_thematic_access('nature')
def nature_locations_geojson(request):
if request.method == "POST":
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON"}, status=400)
highway = data.get('highway') == "1"
location_type = data.get('location_type', '')
# Récupération et parsing de la géométrie
filter_geom_raw = data.get("filter_geom")
if filter_geom_raw:
try:
filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326)
filter_geom.transform(3812)
except Exception as e:
return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400)
else:
filter_geom = None
else:
highway = request.GET.get('highway') == "1"
location_type = request.GET.get('location_type', '')
filter_geom = None
from assets.permissions import get_user_accessible_municipalities
user = request.user
user_config = getattr(user, 'config', None)
is_admin = getattr(user, 'is_superuser', False) or (user_config and (user_config.has_role('admin') or user_config.has_role('top_manager')))
is_external = (user_config is None) or (not user_config.is_intern)
must_limit = is_external or (user_config and user_config.limit_assets_to_contracts)
municipalities = ensure_list(data.get('municipality')) if request.method == "POST" else request.GET.getlist('municipality')
include_naturelocation = location_type in ('', 'naturelocation')
nature_locations = NatureLocation.objects.all() if include_naturelocation else NatureLocation.objects.none()
if not is_admin and must_limit:
accessible_munis = get_user_accessible_municipalities(user)
accessible_muni_ids = list(accessible_munis.values_list('id', flat=True))
if municipalities:
valid_munis = [mid for mid in municipalities if (int(mid) if str(mid).isdigit() else mid) in accessible_muni_ids]
nature_locations = nature_locations.filter(municipality_id__in=valid_munis)
else:
nature_locations = nature_locations.filter(municipality_id__in=accessible_muni_ids)
elif municipalities:
nature_locations = nature_locations.filter(municipality_id__in=municipalities)
if highway:
nature_locations = nature_locations.filter(is_highway=highway)
if filter_geom:
nature_locations = nature_locations.filter(geom__intersects=filter_geom)
# Recherche plein texte
if request.method == 'POST':
from assets.views.core import apply_search
nature_locations = apply_search(nature_locations, data)
from assets.views.core import get_element_type_label
def obj_to_feature(obj):
geometry = json.loads(obj.geojson) if obj.geojson else None
return {
"type": "Feature",
"id": obj.id,
"geometry": geometry,
"properties": {
"code": obj.code,
"name": obj.get_name(),
"highway": getattr(obj, 'is_highway', False),
"location_type": obj.__class__.__name__,
"lon": obj.lon,
"lat": obj.lat,
"detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None,
"element_type": get_element_type_label(obj.__class__.__name__.lower()),
"thematic": "nature",
"is_location": True,
}
}
features = []
for section in nature_locations:
features.append(obj_to_feature(section))
geojson_data = {
"type": "FeatureCollection",
"features": features,
}
return JsonResponse(geojson_data, safe=False)
def nature_assets(request):
pass
@check_thematic_access('nature')
def nature_assets_detail(request, asset_model, asset_id):
content_type = ContentType.objects.filter(app_label='assets', model=asset_model.lower()).first() or ContentType.objects.filter(model=asset_model.lower()).first()
if not content_type:
raise Http404(_("Modèle %(asset_model)s inconnu.") % {'asset_model': asset_model})
model_class = content_type.model_class()
if not model_class:
raise Http404(_("Modèle %(asset_model)s inconnu.") % {'asset_model': asset_model})
# Récupérer l’objet asset
asset = get_object_or_404(model_class, id=asset_id)
# Choisir dynamiquement le template à utiliser
template_name = f"assets/{asset_model}_detail.html"
# Si le template spécifique n’existe pas, tu peux fallback vers un template générique :
from django.template.loader import select_template
try:
template = select_template([template_name, "assets/detail_generic.html"])
except:
raise Http404(_("Aucun template trouvé pour ce type d’asset."))
# Get edit context
from assets.views.core import get_asset_edit_context
context = {
"asset": asset,
"content_type": content_type,
}
context.update(get_asset_edit_context(request, asset, content_type))
# Override thematic_code for nature
context['thematic_code'] = 'nature'
return render(request, template.template.name, context)
def ensure_list(value):
if isinstance(value, list):
return value
if value is None:
return []
return [value]
MAX_ASSET_FEATURES = 10000 # limite imposée
@check_thematic_access('nature')
def nature_assets_geojson(request):
lang = get_language() if get_language() in ('fr', 'nl') else 'fr'
if request.method == "POST":
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON"}, status=400)
sections_ids = data.get('locations_ids', [])
statuses = data.get('asset_status', [])
begin_date = data.get('asset_begin_date')
categories = ensure_list(data.get('category'))
models = ensure_list(data.get('model'))
highway = data.get('highway') == "1"
location_type = data.get('location_type', '')
management_types = ensure_list(data.get('management_type'))
# Récupération et parsing de la géométrie
filter_geom_raw = data.get("filter_geom")
if filter_geom_raw:
try:
filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326)
except Exception as e:
return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400)
else:
filter_geom = None
else:
section_id = request.GET.get('location')
sections_ids = [section_id] if section_id else []
statuses = request.GET.getlist('asset_status')
begin_date = request.GET.get('asset_begin_date')
categories = request.GET.getlist('category', [])
models = request.GET.getlist('model', [])
highway = request.GET.get('highway') == "1"
location_type = request.GET.get('location_type', '')
management_types = request.GET.getlist('management_type', [])
filter_geom = None
_search_q = (data.get('q') or '').strip() if request.method == 'POST' else ''
if not request.GET.get('unified') and not sections_ids and not categories and not models and not filter_geom and not _search_q and not highway and not location_type and not management_types: # return empty geojson if no param with section_id
geojson_data = {
"type": "FeatureCollection",
"features": [],
}
return JsonResponse(geojson_data)
from assets.views.core import get_element_type_label
def build_geometry_and_coords(obj):
geometry = None
if obj.get('geojson'):
try:
geometry = json.loads(obj['geojson'])
except (TypeError, ValueError, json.JSONDecodeError):
geometry = None
lon = obj.get('lon')
lat = obj.get('lat')
if lon is None or lat is None:
if lon is None:
lon = obj.get('location_lon')
if lat is None:
lat = obj.get('location_lat')
if geometry is None and lon is not None and lat is not None:
geometry = {
"type": "Point",
"coordinates": [lon, lat],
}
return geometry, lon, lat
def obj_to_feature(obj, asset_type=None):
geometry, lon, lat = build_geometry_and_coords(obj)
detail_url = reverse('assets:nature_assets_detail', kwargs={'asset_model': asset_type, 'asset_id': obj['id']}) if asset_type else None
model_name = obj.get('model_name') or ''
code = obj.get('code') or ''
is_planter = asset_type == 'naturebench' and (
any(w in model_name.lower() for w in ('bac', 'jardin', 'bloem', 'plante', 'fleur'))
or code.startswith('CSE-BAC')
)
element_type_label = _('Bac à fleurs') if is_planter else get_element_type_label(asset_type or '')
return {
"type": "Feature",
"id": obj['id'],
"geometry": geometry,
"properties": {
"code": obj['code'],
"name": obj['name'],
"category": obj['category_name'],
"model": obj['model_name'],
"category_id": obj.get('category_id'),
"model_id": obj.get('model_id'),
"status": obj['status'],
"validation_status": obj.get('validation_status'),
"lon": lon,
"lat": lat,
"asset_type": asset_type,
"asset_sub_type": "planter" if is_planter else ("bench" if asset_type == "naturebench" else ""),
"area": obj.get('surface_area', ''),
"color": obj['color'] if obj['color'] else '#ccc',
"detail_url": detail_url,
"bulb_types": obj.get('bulb_types') or [],
"element_type": element_type_label,
"thematic": "nature",
"is_location": False,
"structure_type": obj.get('structure_type', ''),
"managing_authority": obj.get('managing_authority') or '',
"management_type": obj.get('management_type') or '',
"is_remarkable": obj.get('is_remarkable', False),
"scientific_name": obj.get('scientific_name') or '',
"vernacular_name": obj.get('vernacular_name') or '',
"is_dead": bool(obj.get('is_dead') or obj.get('vitality') == 'dead' or obj.get('status') in ('dead', 'archived')),
"vitality": obj.get('vitality') or '',
}
}
def add_assets_to_features(queryset, asset_type, features, feature_count):
for obj in queryset:
if feature_count >= MAX_ASSET_FEATURES:
return feature_count, True # early stop
features.append(obj_to_feature(obj, asset_type=asset_type))
feature_count += 1
return feature_count, False
naturegreensurface_qs = (
NatureGreenSurface.objects.all()
)
naturetree_qs = (
NatureTree.objects.all()
)
natureshrub_qs = (
NatureShrub.objects.all()
)
naturebench_qs = (
NatureBench.objects.all()
)
from assets.permissions import get_user_accessible_municipalities
user = request.user
user_config = getattr(user, 'config', None)
is_admin = getattr(user, 'is_superuser', False) or (user_config and (user_config.has_role('admin') or user_config.has_role('top_manager')))
is_external = (user_config is None) or (not user_config.is_intern)
must_limit = is_external or (user_config and user_config.limit_assets_to_contracts)
municipalities = ensure_list(data.get('municipality')) if request.method == "POST" else request.GET.getlist('municipality')
if not is_admin and must_limit:
accessible_munis = get_user_accessible_municipalities(user)
accessible_muni_ids = list(accessible_munis.values_list('id', flat=True))
if municipalities:
valid_munis = [mid for mid in municipalities if (int(mid) if str(mid).isdigit() else mid) in accessible_muni_ids]
naturegreensurface_qs = naturegreensurface_qs.filter(location__municipality_id__in=valid_munis)
naturetree_qs = naturetree_qs.filter(location__municipality_id__in=valid_munis)
natureshrub_qs = natureshrub_qs.filter(location__municipality_id__in=valid_munis)
naturebench_qs = naturebench_qs.filter(location__municipality_id__in=valid_munis)
else:
naturegreensurface_qs = naturegreensurface_qs.filter(location__municipality_id__in=accessible_muni_ids)
naturetree_qs = naturetree_qs.filter(location__municipality_id__in=accessible_muni_ids)
natureshrub_qs = natureshrub_qs.filter(location__municipality_id__in=accessible_muni_ids)
naturebench_qs = naturebench_qs.filter(location__municipality_id__in=accessible_muni_ids)
elif municipalities:
naturegreensurface_qs = naturegreensurface_qs.filter(location__municipality_id__in=municipalities)
naturetree_qs = naturetree_qs.filter(location__municipality_id__in=municipalities)
natureshrub_qs = natureshrub_qs.filter(location__municipality_id__in=municipalities)
naturebench_qs = naturebench_qs.filter(location__municipality_id__in=municipalities)
include_naturelocation = location_type in ('', 'naturelocation')
if not include_naturelocation:
naturegreensurface_qs = naturegreensurface_qs.none()
naturetree_qs = naturetree_qs.none()
natureshrub_qs = natureshrub_qs.none()
naturebench_qs = naturebench_qs.none()
if sections_ids:
naturegreensurface_qs = naturegreensurface_qs.filter(location__id__in=sections_ids)
naturetree_qs = naturetree_qs.filter(location__id__in=sections_ids)
natureshrub_qs = natureshrub_qs.filter(location__id__in=sections_ids)
naturebench_qs = naturebench_qs.filter(location__id__in=sections_ids)
if highway:
naturegreensurface_qs = naturegreensurface_qs.filter(location__is_highway=True)
naturetree_qs = naturetree_qs.filter(location__is_highway=True)
natureshrub_qs = natureshrub_qs.filter(location__is_highway=True)
naturebench_qs = naturebench_qs.filter(location__is_highway=True)
if models:
naturegreensurface_qs = naturegreensurface_qs.filter(model__id__in=models)
naturetree_qs = naturetree_qs.filter(model__id__in=models)
natureshrub_qs = natureshrub_qs.filter(model__id__in=models)
naturebench_qs = naturebench_qs.filter(model__id__in=models)
if management_types:
q_mgmt = Q()
for val in management_types:
val_str = str(val).strip()
if val_str == 'municipal':
q_mgmt |= Q(management_type='municipal')
elif val_str == 'regional':
q_mgmt |= Q(management_type='regional')
elif val_str == 'regional_bruxelles':
q_mgmt |= Q(management_type='regional') & (
Q(location__municipality__niscode__startswith='21') |
Q(location__municipality__name_fr__icontains='Bruxelles') |
Q(location__municipality__name_fr__icontains='Watermael') |
Q(location__municipality__name_fr__icontains='Berchem')
)
elif val_str == 'regional_wallonie':
q_mgmt |= Q(management_type='regional') & (
Q(location__municipality__niscode__startswith=('25', '5', '6', '8', '9')) |
Q(location__municipality__name_fr__icontains='Court-Saint')
)
elif val_str == 'regional_flandre':
q_mgmt |= Q(management_type='regional') & (
Q(location__municipality__niscode__startswith=('1', '3', '4', '7', '23', '24'))
)
elif val_str.startswith('municipal_'):
m_id = val_str.split('_', 1)[1]
if m_id.isdigit():
q_mgmt |= Q(management_type='municipal', location__municipality_id=int(m_id))
elif val_str.startswith('authority:'):
auth_name = val_str.split(':', 1)[1]
q_mgmt |= Q(managing_authority__iexact=auth_name)
else:
q_mgmt |= Q(managing_authority__icontains=val_str) | Q(management_type=val_str)
naturetree_qs = naturetree_qs.filter(q_mgmt)
naturegreensurface_qs = naturegreensurface_qs.none()
natureshrub_qs = natureshrub_qs.none()
naturebench_qs = naturebench_qs.none()
if categories:
naturegreensurface_qs = naturegreensurface_qs.filter(category__id__in=categories)
naturetree_qs = naturetree_qs.filter(category__id__in=categories)
natureshrub_qs = natureshrub_qs.filter(category__id__in=categories)
naturebench_qs = naturebench_qs.filter(category__id__in=categories)
if filter_geom:
filter_geom_3812 = filter_geom.transform(3812, clone=True)
naturegreensurface_qs = naturegreensurface_qs.filter(
Q(geom__intersects=filter_geom_3812) |
Q(geom__isnull=True, location__geom__intersects=filter_geom_3812)
)
naturetree_qs = naturetree_qs.filter(
Q(geom__intersects=filter_geom_3812) |
Q(geom__isnull=True, location__geom__intersects=filter_geom_3812)
)
natureshrub_qs = natureshrub_qs.filter(
Q(geom__intersects=filter_geom_3812) |
Q(geom__isnull=True, location__geom__intersects=filter_geom_3812)
)
naturebench_qs = naturebench_qs.filter(
Q(geom__intersects=filter_geom_3812) |
Q(geom__isnull=True, location__geom__intersects=filter_geom_3812)
)
if statuses:
naturegreensurface_qs = naturegreensurface_qs.filter(status__in=statuses)
naturetree_qs = naturetree_qs.filter(status__in=statuses)
natureshrub_qs = natureshrub_qs.filter(status__in=statuses)
naturebench_qs = naturebench_qs.filter(status__in=statuses)
else:
# Default behavior: exclude removed assets
naturegreensurface_qs = naturegreensurface_qs.exclude(status='removed')
naturetree_qs = naturetree_qs.exclude(status='removed')
natureshrub_qs = natureshrub_qs.exclude(status='removed')
naturebench_qs = naturebench_qs.exclude(status='removed')
# Recherche plein texte
if request.method == 'POST':
from assets.views.core import apply_search
naturegreensurface_qs = apply_search(naturegreensurface_qs, data)
naturetree_qs = apply_search(naturetree_qs, data)
natureshrub_qs = apply_search(natureshrub_qs, data)
naturebench_qs = apply_search(naturebench_qs, data)
if (naturegreensurface_qs.count() + naturetree_qs.count() + natureshrub_qs.count() + naturebench_qs.count()) > MAX_ASSET_FEATURES:
return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413)
naturegreensurface_qs = (naturegreensurface_qs.annotate(
name=F(f'name_{lang}'),
category_name=F(f'category__name_{lang}'),
model_name=F(f'model__name_{lang}'),
color = F('model__color'),
location_lon=F('location__lon'),
location_lat=F('location__lat'),
)
.values( # on limite strictement les colonnes
'id', 'code', 'name', 'status', 'validation_status', 'lon', 'lat', 'surface_area',
'category_name', 'model_name', 'color', 'geojson',
'category_id', 'model_id', 'bulb_types', 'location_lon', 'location_lat'
)
.order_by('code')
)[:MAX_ASSET_FEATURES + 1]
naturetree_qs = (naturetree_qs.annotate(
name=F(f'name_{lang}'),
category_name=F(f'category__name_{lang}'),
model_name=F(f'model__name_{lang}'),
color = F('model__color'),
location_lon=F('location__lon'),
location_lat=F('location__lat'),
)
.values( # on limite strictement les colonnes
'id', 'code', 'name', 'status', 'validation_status', 'lon', 'lat',
'category_name', 'model_name', 'color', 'geojson',
'category_id', 'model_id', 'location_lon', 'location_lat',
'managing_authority', 'management_type', 'is_remarkable',
'scientific_name', 'vernacular_name',
'is_dead', 'vitality'
)
.order_by('code')
)[:MAX_ASSET_FEATURES + 1]
natureshrub_qs = (natureshrub_qs.annotate(
name=F(f'name_{lang}'),
category_name=F(f'category__name_{lang}'),
model_name=F(f'model__name_{lang}'),
color=F('model__color'),
location_lon=F('location__lon'),
location_lat=F('location__lat'),
)
.values(
'id', 'code', 'name', 'status', 'validation_status', 'lon', 'lat',
'category_name', 'model_name', 'color', 'geojson',
'category_id', 'model_id', 'location_lon', 'location_lat'
)
.order_by('code')
)[:MAX_ASSET_FEATURES + 1]
naturebench_qs = (naturebench_qs.annotate(
name=F(f'name_{lang}'),
category_name=F(f'category__name_{lang}'),
model_name=F(f'model__name_{lang}'),
color=F('model__color'),
location_lon=F('location__lon'),
location_lat=F('location__lat'),
)
.values(
'id', 'code', 'name', 'status', 'validation_status', 'lon', 'lat',
'category_name', 'model_name', 'color', 'geojson',
'category_id', 'model_id', 'location_lon', 'location_lat'
)
.order_by('code')
)[:MAX_ASSET_FEATURES + 1]
features = []
feature_count = 0
for qs, asset_type in [
(naturegreensurface_qs, "naturegreensurface"),
(naturetree_qs, "naturetree"),
(natureshrub_qs, "natureshrub"),
(naturebench_qs, "naturebench"),
]:
feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count)
if limit_reached:
return JsonResponse({
"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES}). {_("Limitez votre sélection.")}"
}, status=413)
features = sorted(features, key=lambda f: f["properties"]["code"])
geojson_data = {
"type": "FeatureCollection",
"features": features,
}
return JsonResponse(geojson_data, safe=False)
@require_GET
def nature_locations_autocomplete(request):
raw_query = (request.GET.get('q') or '').strip()
if len(raw_query) < 2:
return JsonResponse([], safe=False)
user = request.user
user_config = getattr(user, 'config', None)
is_admin = getattr(user, 'is_superuser', False) or (user_config and (user_config.has_role('admin') or user_config.has_role('top_manager')))
is_external = (user_config is None) or (not user_config.is_intern)
must_limit = is_external or (user_config and user_config.limit_assets_to_contracts)
# Normalise la requête (équivalent de unaccent+lower côté Python)
qn = strip_accents_lower(raw_query)
# Prépare des colonnes "recherchables" unaccentées + minuscules
qs = NatureLocation.objects.all()
if not is_admin and must_limit:
from assets.permissions import get_user_accessible_municipalities
accessible_munis = get_user_accessible_municipalities(user)
qs = qs.filter(municipality__in=accessible_munis)
qs = (
qs
.annotate(
code_u=Lower(Unaccent('code')),
name_fr_u=Lower(Unaccent('name_fr')),
name_nl_u=Lower(Unaccent('name_nl')),
)
.filter(
Q(code_u__contains=qn) | # __contains car on a déjà des champs en lower
Q(name_fr_u__contains=qn) |
Q(name_nl_u__contains=qn)
)
)
# Priorité : exact > startswith > nom startswith > contient
priority = Case(
When(code_u=qn, then=0),
When(code_u__startswith=qn, then=1),
When(name_fr_u__startswith=qn, then=2),
When(name_nl_u__startswith=qn, then=3),
When(code_u__contains=qn, then=4),
When(name_fr_u__contains=qn, then=5),
When(name_nl_u__contains=qn, then=6),
default=7,
output_field=IntegerField(),
)
qs = qs.annotate(_prio=priority).order_by('_prio', 'code')[:50]
data = [{
"label": f"{loc.code} - {loc.get_name()}",
"location_id": loc.id,
"location_type": loc.__class__.__name__,
"location_label": f"{loc.code} - {loc.get_name()}",
"lon": loc.lon,
"lat": loc.lat
} for loc in list(qs)]
data.sort(key=lambda x: x["label"])
return JsonResponse(data, safe=False)
@require_GET
def nature_assets_autocomplete(request):
raw_query_original = (request.GET.get('q') or '')
raw_query = raw_query_original.strip()
qn = strip_accents_lower(raw_query)
has_search_term = len(qn) > 0
location_id = request.GET.get('location_id') # = id de la rue
category_id = request.GET.get('asset_category')
try:
category_id = int(category_id)
except (TypeError, ValueError):
category_id = None
# Prépare filtre "dans cette rue uniquement" si location_id fourni
location_filter = {}
if location_id:
location_filter = {'location_id': location_id}
user = request.user
user_config = getattr(user, 'config', None)
is_admin = getattr(user, 'is_superuser', False) or (user_config and (user_config.has_role('admin') or user_config.has_role('top_manager')))
is_external = (user_config is None) or (not user_config.is_intern)
must_limit = is_external or (user_config and user_config.limit_assets_to_contracts)
def base_qs(model, extra_filter=None):
ann = {
'code_u': Lower(Unaccent('code')),
'name_fr_u': Lower(Unaccent('name_fr')),
'name_nl_u': Lower(Unaccent('name_nl')),
}
q = model.objects.all()
if not is_admin and must_limit:
from assets.permissions import get_user_accessible_municipalities
accessible_munis = get_user_accessible_municipalities(user)
q = q.filter(location__municipality__in=accessible_munis)
q = q.annotate(**ann)
# Si un terme de recherche est fourni, filtrer dessus
if has_search_term:
q = q.filter(
Q(code_u__contains=qn) |
Q(name_fr_u__contains=qn) |
Q(name_nl_u__contains=qn)
)
# Filtrer uniquement les non supprimés
q = q.exclude(status='removed')
if extra_filter:
q = q.filter(**extra_filter)
if category_id is not None:
q = q.filter(
Q(category_id=category_id) |
Q(category__isnull=True, model__category_id=category_id)
)
q = q.select_related('location', 'model')
return q
# Récupère QS pour chaque type (limités grossièrement pour éviter de charger trop, puis triés/merge côté Python)
trees_qs = base_qs(NatureTree, extra_filter=location_filter)[:100]
greensurfaces_qs = base_qs(NatureGreenSurface, extra_filter=location_filter)[:100]
# Concatène en mémoire (on priorise ensuite)
items = []
for a in list(trees_qs) + list(greensurfaces_qs):
# priorisation locale (recalcule ici car on merge des modèles différents)
code_u = getattr(a, 'code_u', None)
name_fr_u = getattr(a, 'name_fr_u', None)
name_nl_u = getattr(a, 'name_nl_u', None)
prio = 7
# Priorisation uniquement si un terme de recherche est fourni
if has_search_term:
if code_u == qn:
prio = 0
elif code_u and code_u.startswith(qn):
prio = 1
elif (name_fr_u and name_fr_u.startswith(qn)) or (name_nl_u and name_nl_u.startswith(qn)):
prio = 2
elif (code_u and qn in code_u) or (name_fr_u and qn in name_fr_u) or (name_nl_u and qn in name_nl_u):
prio = 4
else:
# Sans terme de recherche, on trie par code
prio = 5
location = a.location if hasattr(a, 'location') else None
equipment_type = a.model.get_name() if hasattr(a, 'model') else None
parts = [a.code, a.get_name()]
equipment_label = " - ".join(p for p in parts if p)
code = getattr(a, 'code', '') or ''
label = equipment_label
lon, lat = [getattr(a, 'lon', None), getattr(a, 'lat', None)]
location_id_val = location.id if location else None
location_label = (f"{location.code} - {location.get_name()}" if location else None)
equipment_ct_id = ContentType.objects.get_for_model(a).id
location_ct_id = ContentType.objects.get_for_model(location).id if location else None
items.append({
"prio": prio,
"code": code or "",
"id": getattr(a, 'id', None),
"label": label,
# attendus par ton autocomplete équipements
"equipment_id": getattr(a, 'id', None),
"equipment_label": equipment_label,
"equipment_type": equipment_type,
"equipment_ct": equipment_ct_id,
"location_id": location_id_val,
"location_label": location_label,
"location_ct": location_ct_id,
"lon": lon,
"lat": lat,
})
# Tri global par priorité puis code, coupe à 100
items.sort(key=lambda x: (x["prio"], x["code"]))
data = items[:100]
return JsonResponse(data, safe=False)
@require_GET
def get_nearest_nature_location(request):
try:
lon = float(request.GET.get('lon'))
lat = float(request.GET.get('lat'))
except (TypeError, ValueError):
return JsonResponse({"error": "Coordonnées invalides"}, status=400)
user_point = Point(lon, lat, srid=4326)
user_point.transform(3812)
nearest = (
NatureLocation.objects
.filter(geom__isnull=False)
.annotate(distance=Distance('geom', user_point))
.filter(distance__lte=50) # filtre à 50 mètres
.order_by('distance')
.first()
)
location_ct_id = ContentType.objects.get_for_model(nearest).id
if nearest:
return JsonResponse({
"location_id": nearest.id,
"location_ct": location_ct_id,
"location_label": f"{nearest.code} - {nearest.get_name()}",
"lon": nearest.lon, "lat": nearest.lat
})
else:
return JsonResponse({"error": _("Aucune section trouvée")})
# Appelle la fonction PostgreSQL unaccent(...)
class Unaccent(Func):
function = 'public.unaccent_immutable'
output_field = CharField()
def strip_accents_lower(s: str) -> str:
if not s:
return ''
s = ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn')
return s.lower()
@check_thematic_access('nature')
def nature_bulk_update_assets(request):
"""Bulk update category and/or model for selected nature assets."""
if request.method != 'POST':
return JsonResponse({'error': 'POST method required'}, status=405)
try:
data = json.loads(request.body)
asset_ids = data.get('asset_ids', [])
category_id = data.get('category_id')
model_id = data.get('model_id')
bulb_types = data.get('bulb_types') # list or None
if not asset_ids:
return JsonResponse({'error': _('Aucun asset sélectionné')}, status=400)
updated_count = 0
# Update green surfaces
green_surfaces = NatureGreenSurface.objects.filter(id__in=asset_ids)
if category_id:
green_surfaces.update(category_id=category_id)
if model_id:
green_surfaces.update(model_id=model_id)
if bulb_types is not None:
green_surfaces.update(bulb_types=bulb_types)
updated_count += green_surfaces.count()
# Update trees (if any in selection)
trees = NatureTree.objects.filter(id__in=asset_ids)
if category_id:
trees.update(category_id=category_id)
if model_id:
trees.update(model_id=model_id)
updated_count += trees.count()
return JsonResponse({
'success': True,
'updated_count': updated_count
})
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
@check_thematic_access('nature')
def nature_bulk_delete_assets(request):
"""Bulk delete (archive) selected nature assets."""
if request.method != 'POST':
return JsonResponse({'error': 'POST method required'}, status=405)
try:
data = json.loads(request.body)
asset_ids = data.get('asset_ids', [])
if not asset_ids:
return JsonResponse({'error': _('Aucun asset sélectionné')}, status=400)
deleted_count = 0
# Archive green surfaces
green_surfaces = NatureGreenSurface.objects.filter(id__in=asset_ids)
green_surfaces.update(status='removed')
deleted_count += green_surfaces.count()
# Archive trees
trees = NatureTree.objects.filter(id__in=asset_ids)
trees.update(status='removed')
deleted_count += trees.count()
return JsonResponse({
'success': True,
'deleted_count': deleted_count
})
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
@check_thematic_access('nature')
def nature_undo_action(request):
"""
Undo the last action (simplified version).
NOTE: This is a basic implementation for demonstration purposes.
In a production environment, you would need to:
1. Store the previous state of modified assets before changes
2. Implement a proper undo stack with complete state management
3. Handle more complex undo scenarios (geometry changes, creations, etc.)
"""
if request.method != 'POST':
return JsonResponse({'error': 'POST method required'}, status=405)
try:
data = json.loads(request.body)
action = data.get('action')
# Simplified undo - currently only supports undoing delete actions
if action == 'delete':
asset_ids = data.get('assets', [])
# Restore archived assets
NatureGreenSurface.objects.filter(id__in=asset_ids).update(status='active')
NatureTree.objects.filter(id__in=asset_ids).update(status='active')
elif action == 'bulk-edit':
# For bulk edit, you'd need to restore the old values
# This would require storing them before the edit
pass
return JsonResponse({'success': True})
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
@check_thematic_access('nature')
def nature_save_geometries(request):
"""
Save geometry modifications for nature assets.
Expects a list of modifications with asset_id, asset_type, and geometry.
Updates the geom field for each asset with the new geometry.
"""
if request.method != 'POST':
return JsonResponse({'error': 'POST method required'}, status=405)
try:
data = json.loads(request.body)
modifications = data.get('modifications', [])
deletions = data.get('deletions', []) # New: handle deletions for merged assets
if not modifications and not deletions:
return JsonResponse({'error': _('Aucune modification fournie')}, status=400)
updated_count = 0
deleted_count = 0
id_mapping = {}
# Handle deletions first (for merged assets)
for deletion in deletions:
asset_id = deletion.get('asset_id')
asset_type = deletion.get('asset_type')
if not all([asset_id, asset_type]):
continue
try:
if asset_type == 'naturegreensurface':
asset = NatureGreenSurface.objects.filter(id=asset_id).first()
if asset:
asset.delete()
deleted_count += 1
elif asset_type == 'naturetree':
asset = NatureTree.objects.filter(id=asset_id).first()
if asset:
asset.delete()
deleted_count += 1
except Exception as e:
print(f"Error deleting asset {asset_id}: {str(e)}")
continue
# Handle modifications
for mod in modifications:
asset_id = mod.get('asset_id')
asset_type = mod.get('asset_type')
geometry_geojson = mod.get('geometry')
category_id = mod.get('category_id')
model_id = mod.get('model_id')
bulb_types = mod.get('bulb_types') # list or None
if not all([asset_id, asset_type, geometry_geojson]):
continue
# Check if this is a new asset
is_new = str(asset_id).startswith('new_')
# Convert GeoJSON to GEOS geometry
try:
# The geometry comes as GeoJSON, convert to GEOS
geom = GEOSGeometry(json.dumps(geometry_geojson), srid=4326)
# Transform to the storage SRID (3812)
geom.transform(3812)
# Update the appropriate model
if asset_type == 'naturegreensurface':
asset = None
if is_new:
# Generate a unique code for the new asset
code = f"SURF-{uuid.uuid4().hex[:8].upper()}"
asset = NatureGreenSurface(code=code, status='active')
else:
asset = NatureGreenSurface.objects.filter(id=asset_id).first()
if asset:
# Force multipolygon for green surfaces
if geom.geom_type == 'Polygon':
geom = MultiPolygon(geom)
asset.geom = geom
# Update centroid coordinates
centroid = geom.centroid
centroid.transform(4326)
asset.lon = centroid.x
asset.lat = centroid.y
geom_4326 = geom.transform(4326, clone=True)
asset.geojson = geom_4326.geojson
# Recalculate area for polygons
if hasattr(geom, 'area'):
asset.surface_area = geom.area # in square meters
# Update attributes if provided
if category_id is not None:
if category_id:
try:
category = AssetCategory.objects.get(id=category_id)
asset.category = category
except AssetCategory.DoesNotExist:
pass
else:
if not is_new: # Don't clear category on creation if not provided (though defaults are null)
asset.category = None
if model_id is not None:
if model_id:
try:
model = NatureAssetModel.objects.get(id=model_id)
asset.model = model
# If model has a category and no category was provided, use model's category
if not asset.category and model.category:
asset.category = model.category
except NatureAssetModel.DoesNotExist:
pass
else:
if not is_new:
asset.model = None
if bulb_types is not None:
asset.bulb_types = bulb_types
asset.save()
updated_count += 1
if is_new:
id_mapping[asset_id] = asset.id
elif asset_type == 'naturetree':
asset = NatureTree.objects.filter(id=asset_id).first()
if asset:
asset.geom = geom
# Update coordinates
centroid = geom.centroid
centroid.transform(4326)
asset.lon = centroid.x
asset.lat = centroid.y
asset.save()
updated_count += 1
except Exception as e:
# Log the error but continue with other modifications
print(f"Error updating geometry for asset {asset_id}: {str(e)}")
continue
return JsonResponse({
'success': True,
'updated_count': updated_count,
'deleted_count': deleted_count,
'id_mapping': id_mapping
})
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
# ============================================================================
# Green Surfaces Comments API
# ============================================================================
@check_thematic_access('nature')
def green_surfaces_comments_geojson(request):
"""
Retrieve geolocated comments as GeoJSON.
Optionally filter by geographic bounds.
"""
if request.method == 'POST':
try:
data = json.loads(request.body)
filter_geom_data = data.get('filter_geom')
comments = GreenSurfacesComment.objects.select_related('created_by').prefetch_related('photos')
# Apply geographic filter if provided
if filter_geom_data:
filter_geom = GEOSGeometry(json.dumps(filter_geom_data['geometry']))
filter_geom.srid = 4326
filter_geom.transform(3812)
comments = comments.filter(geom__intersects=filter_geom)
# Build GeoJSON features
features = []
for comment in comments:
if comment.geom:
geom_4326 = comment.geom.transform(4326, clone=True)
feature = {
'type': 'Feature',
'id': comment.id,
'geometry': json.loads(geom_4326.geojson),
'properties': {
'id': comment.id,
'comment': comment.comment,
'status': comment.status,
'address': comment.address or '',
'created_by': comment.created_by.username if comment.created_by else '',
'created_at': comment.created_at.isoformat(),
'photo_count': comment.photos.count(),
}
}
features.append(feature)
return JsonResponse({
'type': 'FeatureCollection',
'features': features
})
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
return JsonResponse({'error': 'Method not allowed'}, status=405)
@check_thematic_access('nature')
def green_surfaces_comment_detail(request, comment_id):
"""
Retrieve detailed information about a specific comment including photos.
"""
from common.private_files.helpers import build_private_url
try:
comment = GreenSurfacesComment.objects.select_related('created_by').prefetch_related('photos').get(id=comment_id)
photos = []
for photo in comment.photos.all():
photos.append({
'id': photo.id,
'url': build_private_url('assets', 'GreenSurfacesCommentPhoto', photo.pk, 'photo'),
'heading': photo.heading,
'order': photo.order,
'created_at': photo.created_at.isoformat(),
})
# Check if user can edit (owner or staff with edit permission)
can_edit = comment.created_by == request.user or request.user.is_staff
data = {
'id': comment.id,
'comment': comment.comment,
'status': comment.status,
'latitude': comment.latitude,
'longitude': comment.longitude,
'address': comment.address or '',
'created_by': comment.created_by.username if comment.created_by else '',
'created_at': comment.created_at.isoformat(),
'updated_at': comment.updated_at.isoformat(),
'photos': photos,
'can_edit': can_edit,
}
return JsonResponse({'success': True, 'data': data})
except GreenSurfacesComment.DoesNotExist:
return JsonResponse({'error': 'Comment not found'}, status=404)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
@check_thematic_access('nature')
def green_surfaces_comment_save(request):
"""
Create or update a geolocated comment with photos.
"""
if request.method != 'POST':
return JsonResponse({'error': 'Method not allowed'}, status=405)
try:
comment_id = request.POST.get('comment_id')
comment_text = request.POST.get('comment', '').strip()
status = request.POST.get('status', 'memo') # Default to 'memo'
latitude = request.POST.get('latitude')
longitude = request.POST.get('longitude')
address = request.POST.get('address', '')
heading = request.POST.get('heading') # Device compass heading when photos were taken
photos_to_delete = request.POST.getlist('photos_to_delete') # Photos to delete during edit
# Validate status
valid_statuses = ['memo', 'attention', 'todo', 'ok', 'problem']
if status not in valid_statuses:
status = 'memo'
# Comment text is now optional
if not latitude or not longitude:
return JsonResponse({'error': 'Location is required'}, status=400)
try:
latitude = float(latitude)
longitude = float(longitude)
except ValueError:
return JsonResponse({'error': 'Invalid coordinates'}, status=400)
# Create or update comment
if comment_id:
comment = GreenSurfacesComment.objects.get(id=comment_id)
# Check permission for editing
if comment.created_by != request.user and not request.user.is_staff:
return JsonResponse({'error': 'Permission denied'}, status=403)
comment.comment = comment_text
comment.status = status
comment.latitude = latitude
comment.longitude = longitude
comment.address = address
else:
comment = GreenSurfacesComment(
comment=comment_text,
status=status,
latitude=latitude,
longitude=longitude,
address=address,
created_by=request.user
)
# Create geometry
point = Point(longitude, latitude, srid=4326)
point.transform(3812)
comment.geom = point
comment.save()
# Delete photos if editing
if comment_id and photos_to_delete:
GreenSurfacesCommentPhoto.objects.filter(
id__in=photos_to_delete,
comment=comment
).delete()
# Handle photo uploads
photos = request.FILES.getlist('photos')
photo_headings = request.POST.getlist('photo_headings') # Individual headings for each photo
# Get current max order for photos
max_order = comment.photos.aggregate(Max('order'))['order__max'] or 0
for i, photo_file in enumerate(photos):
# Use individual photo heading if provided, otherwise use global heading
photo_heading = None
if i < len(photo_headings) and photo_headings[i]:
try:
photo_heading = float(photo_headings[i])
except ValueError:
pass
elif heading:
try:
photo_heading = float(heading)
except ValueError:
pass
GreenSurfacesCommentPhoto.objects.create(
comment=comment,
photo=photo_file,
heading=photo_heading,
order=max_order + i + 1
)
return JsonResponse({
'success': True,
'comment_id': comment.id,
'message': _('Commentaire enregistré avec succès')
})
except GreenSurfacesComment.DoesNotExist:
return JsonResponse({'error': 'Comment not found'}, status=404)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
@check_thematic_access('nature')
def green_surfaces_comment_delete(request, comment_id):
"""
Delete a geolocated comment and all its associated photos.
"""
if request.method != 'POST':
return JsonResponse({'error': 'Method not allowed'}, status=405)
try:
comment = GreenSurfacesComment.objects.get(id=comment_id)
# Check if user owns the comment or has admin rights
if comment.created_by != request.user and not request.user.is_staff:
return JsonResponse({'error': 'Permission denied'}, status=403)
comment.delete() # Cascade delete will remove photos
return JsonResponse({
'success': True,
'message': _('Commentaire supprimé avec succès')
})
except GreenSurfacesComment.DoesNotExist:
return JsonResponse({'error': 'Comment not found'}, status=404)
@check_thematic_access('nature')
def green_surfaces_comment_add_photos(request, comment_id):
"""
Add photos to an existing comment.
"""
if request.method != 'POST':
return JsonResponse({'error': 'Method not allowed'}, status=405)
try:
comment = GreenSurfacesComment.objects.get(id=comment_id)
# Check if user owns the comment or has admin rights
if comment.created_by != request.user and not request.user.is_staff:
return JsonResponse({'error': 'Permission denied'}, status=403)
# Handle photo uploads
photos = request.FILES.getlist('photos')
photo_headings = request.POST.getlist('photo_headings')
if not photos:
return JsonResponse({'error': 'No photos provided'}, status=400)
# Get current max order
max_order = comment.photos.aggregate(Max('order'))['order__max'] or 0
# Save each photo with heading
for i, photo in enumerate(photos):
heading_str = photo_headings[i] if i < len(photo_headings) else ''
heading = None
if heading_str and heading_str.strip():
try:
heading = float(heading_str)
except ValueError:
pass
GreenSurfacesCommentPhoto.objects.create(
comment=comment,
photo=photo,
heading=heading,
order=max_order + i + 1
)
return JsonResponse({
'success': True,
'message': _('Photos ajoutées avec succès'),
'comment_id': comment.id
})
except GreenSurfacesComment.DoesNotExist:
return JsonResponse({'error': 'Comment not found'}, status=404)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)