loko/loko/common/views.py

838 lines
32 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.core.cache import cache
from django.http import JsonResponse, FileResponse, Http404
from django.shortcuts import render, redirect
from django.views.decorators.http import require_GET, require_POST
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from django.contrib import messages
from django.contrib.gis.db.models.functions import Distance
from django.contrib.gis.geos import Point, Polygon
from django.contrib.auth import login as auth_login
from django.contrib.auth.decorators import login_not_required
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth import update_session_auth_hash
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.translation import gettext as _
from django.conf import settings
from django.urls import reverse
import re
import os
from urllib.parse import urlencode
from common.context_processors import (
SESSION_ALLOWED_IDS_KEY,
SESSION_ORIGIN_ID_KEY,
SESSION_MAP_BOUNDS_KEY,
compute_initial_allowed_ids,
)
from common.models import AdrStreet, AdrPoint, UserConfig
from assets.models import AssetCategory
@login_not_required
@require_GET
def autocomplete_street(request):
query = request.GET.get('q', '').strip()
if len(query) < 2:
return JsonResponse([], safe=False)
# Regex patterns to detect number and street in different positions
patterns = [
("number_first", r'^(\d+\w*)[\s,]+(.+)$'), # ex: "12A rue de la Loi"
("street_first", r'^(.+)[\s,]+(\d+\w*)$'), # ex: "rue de la Loi 12A"
]
number_part = None
street_part = None
for mode, pat in patterns:
match = re.match(pat, query)
if match:
g1, g2 = match.groups()
if mode == "number_first":
number_part, street_part = g1.strip(), g2.strip()
else:
street_part, number_part = g1.strip(), g2.strip()
break
if number_part and street_part:
# Recherche par AdrPoint avec numéro ET nom de rue
points = (
AdrPoint.objects
.filter(number__iexact=number_part)
.filter(
Q(street_name__name_fr__icontains=street_part) |
Q(street_name__name_nl__icontains=street_part)
)
.select_related('street_name', 'municipality')[:10]
)
data = [{
"id": point.fid,
"label": f"{point.number} {point.street_name.name_fr} / {point.street_name.name_nl}"
+ (f" ({point.municipality.name})" if point.municipality else ""),
"lon": point.lon,
"lat": point.lat
} for point in points]
else:
# Recherche standard par rue uniquement
streets = (
AdrStreet.objects
.filter(Q(name_fr__icontains=query) | Q(name_nl__icontains=query))
.select_related('municipality')[:10]
)
data = [{
"id": street.fid,
"label": f"{street.name_fr} / {street.name_nl}"
+ (f" ({street.municipality.name})" if street.municipality else ""),
"lon": street.lon,
"lat": street.lat
} for street in streets]
return JsonResponse(data, safe=False)
@login_not_required
@require_GET
def get_nearest_address(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)
input_point = Point(lon, lat, srid=4326)
input_point.transform(3812)
# Étape 1 : Trouver la rue la plus proche
nearest_street = (
AdrStreet.objects
.filter(geom__isnull=False)
.annotate(distance=Distance("geom", input_point))
.filter(distance__lte=50) # filtre à 50 mètres
.order_by("distance")
.first()
)
if not nearest_street:
return JsonResponse({"error": _("Aucune rue trouvée")}, status=404)
# Étape 2 : Rechercher un point dans un rayon de 50 m pour cette rue
nearest_point = (
AdrPoint.objects
.filter(street_name=nearest_street, geom__isnull=False)
.annotate(distance=Distance("geom", input_point))
.filter(distance__lte=50) # filtre à 50 mètres
.order_by("distance")
.first()
)
# Étape 3 : Si un point est trouvé dans les 50 m, on le retourne
if nearest_point:
return JsonResponse({
"id": nearest_point.id,
"label": f"{nearest_point.number} {nearest_point.street_name.name_fr} / {nearest_point.street_name.name_nl}"
+ (f" ({nearest_point.municipality.name})" if nearest_point.municipality else "")
})
# Sinon : on retourne uniquement le nom de la rue
return JsonResponse({
"id": nearest_street.id,
"label": f"{nearest_street.name_fr} / {nearest_street.name_nl}"
+ (f" ({nearest_street.municipality.name})" if nearest_street.municipality else ""),
})
@login_not_required
@require_GET
def get_territory_mask_geojson(request):
"""
Renvoie le masque cartographique inversé (calque grisé transparent en dehors du territoire)
et le tracé du contour pour mettre en évidence le territoire à gérer.
Supporte un modèle Territory par défaut (contour direct sans jointure de communes),
ou une liste de communes (filtrage par contrat/utilisateur).
"""
import json
user = request.user
muni_params = request.GET.getlist('municipality')
territory_param = request.GET.get('territory_id') or request.GET.get('territory')
def _build_mask_geojson(target_geom, territory_name, cache_key):
cached = cache.get(cache_key)
if cached is not None:
return JsonResponse(cached)
g = target_geom.transform(4326, clone=True) if target_geom.srid != 4326 else target_geom
try:
num_points = sum(len(ring) for poly in g for ring in poly)
except Exception:
num_points = 0
simp_geom = g.simplify(0.00003, preserve_topology=True) if num_points > 800 else g
world = Polygon.from_bbox((-180, -85, 180, 85))
try:
mask = world.difference(simp_geom)
except Exception:
mask = world.difference(simp_geom.buffer(0))
features = [
{
"type": "Feature",
"id": "territory_mask",
"properties": {
"layer": "mask",
"type": "mask",
"name": territory_name
},
"geometry": json.loads(mask.geojson)
},
{
"type": "Feature",
"id": "territory_boundary",
"properties": {
"layer": "boundary",
"type": "boundary",
"name": territory_name
},
"geometry": json.loads(simp_geom.geojson)
}
]
result = {
"type": "FeatureCollection",
"features": features
}
cache.set(cache_key, result, 86400)
return JsonResponse(result)
from common.models import Municipality, Territory
from contracts.models import Contract
from assets.permissions import get_user_accessible_municipalities
# 1. Si un territoire explicite est demandé par identifiant ou code
if territory_param:
t_filter = Q(code=territory_param)
if str(territory_param).isdigit():
t_filter |= Q(id=int(territory_param))
explicit_territory = Territory.objects.filter(t_filter, is_active=True, geom__isnull=False).first()
if explicit_territory:
return _build_mask_geojson(
explicit_territory.geom,
explicit_territory.name,
f"territory_mask_geojson_territory_{explicit_territory.id}"
)
muni_ids = []
if muni_params:
for p in muni_params:
if str(p).isdigit():
muni_ids.append(int(p))
default_territory = Territory.objects.filter(is_active=True, is_default=True, geom__isnull=False).first()
total_munis_count = Municipality.objects.filter(geom__isnull=False).count()
if not muni_ids:
# Vérifier si l'utilisateur est restreint à un sous-ensemble strict de communes
user_config = getattr(user, 'config', None) if user and user.is_authenticated else None
user_restricted_muni_ids = []
if user and user.is_authenticated and not user.is_superuser:
if user_config and user_config.default_contract and user_config.default_contract.municipalities.exists():
c_munis = list(user_config.default_contract.municipalities.values_list('id', flat=True))
if len(c_munis) < total_munis_count:
user_restricted_muni_ids = c_munis
elif user_config and user_config.contracts.filter(municipalities__isnull=False).exists():
c_munis = list(Municipality.objects.filter(contracts__in=user_config.contracts.all()).values_list('id', flat=True).distinct())
if len(c_munis) < total_munis_count:
user_restricted_muni_ids = c_munis
elif Contract.objects.filter(municipalities__isnull=False).count() == 1:
c_munis = list(Contract.objects.filter(municipalities__isnull=False).first().municipalities.values_list('id', flat=True))
if len(c_munis) < total_munis_count:
user_restricted_muni_ids = c_munis
else:
accessible = list(get_user_accessible_municipalities(user).values_list('id', flat=True))
if accessible and len(accessible) < total_munis_count:
user_restricted_muni_ids = accessible
if user_restricted_muni_ids:
muni_ids = user_restricted_muni_ids
elif default_territory:
return _build_mask_geojson(
default_territory.geom,
default_territory.name,
f"territory_mask_geojson_default_{default_territory.id}"
)
else:
muni_ids = list(Municipality.objects.filter(geom__isnull=False).values_list('id', flat=True))
if default_territory and total_munis_count > 0 and len(set(muni_ids)) >= total_munis_count:
return _build_mask_geojson(
default_territory.geom,
default_territory.name,
f"territory_mask_geojson_default_{default_territory.id}"
)
if not muni_ids:
return JsonResponse({"type": "FeatureCollection", "features": []})
muni_ids = sorted(list(set(muni_ids)))
cache_key = f"territory_mask_geojson_{'_'.join(map(str, muni_ids))}"
cached_data = cache.get(cache_key)
if cached_data is not None:
return JsonResponse(cached_data)
munis = Municipality.objects.filter(id__in=muni_ids, geom__isnull=False)
if not munis.exists():
empty_res = {"type": "FeatureCollection", "features": []}
return JsonResponse(empty_res)
combined_geom = None
names = []
for m in munis:
names.append(m.name)
g = m.geom.transform(4326, clone=True) if m.geom.srid != 4326 else m.geom
g_simp = g.simplify(0.00003, preserve_topology=True)
if combined_geom is None:
combined_geom = g_simp
else:
combined_geom = combined_geom.union(g_simp)
if not combined_geom:
return JsonResponse({"type": "FeatureCollection", "features": []})
return _build_mask_geojson(combined_geom, ", ".join(names), cache_key)
def get_regional_roads_geojson(request):
file_path = os.path.join(settings.BASE_DIR, 'common', 'static', 'geojson', 'regional_roads_simplified_4326.geojson')
if not os.path.exists(file_path):
raise Http404("GeoJSON file not found")
return FileResponse(open(file_path, 'rb'), content_type='application/json')
def get_categories_for_thematics(request):
ids_string = request.GET.get('thematic_ids', '')
if ids_string:
try:
thematic_ids = [int(x) for x in ids_string.split(',') if x.strip().isdigit()]
except ValueError:
return JsonResponse({"results": []})
else:
thematic_ids = []
categories = AssetCategory.objects.filter(thematic__id__in=thematic_ids).distinct()
results = [
{"id": cat.id, "text": cat.get_name(), "parent_id": cat.parent_id} for cat in categories
]
return JsonResponse({"results": results})
def _safe_origin_next(request):
# 1) prendre 'next' si présent à l’arrivée
origin_next = request.GET.get("origin_next") or request.GET.get("next")
# 2) sinon, si c’est un POST (ex: retour form), reprendre la valeur transmise
origin_next = origin_next or request.POST.get("origin_next")
# 3) safe-guard (évite open redirect)
if origin_next and not url_has_allowed_host_and_scheme(
url=origin_next,
allowed_hosts={request.get_host()},
require_https=request.is_secure(),
):
origin_next = None
return origin_next
@login_required
def account_info(request):
origin_next = _safe_origin_next(request) # on capture une fois, puis on la véhicule
open_password_modal = False
if request.method == "POST" and "password_change" in request.POST:
form = PasswordChangeForm(request.user, request.POST)
if form.is_valid():
user = form.save()
update_session_auth_hash(request, user)
messages.success(request, _("Mot de passe mis à jour avec succès."))
# On redirige vers account_info en conservant origin_next + flag
params = {"password_changed": "1"}
if origin_next:
params["origin_next"] = origin_next
return redirect(f"{request.path}?{urlencode(params)}")
else:
open_password_modal = True
else:
form = PasswordChangeForm(request.user)
if request.GET.get("password_changed") == "1":
open_password_modal = True
return render(request, "common/account_info.html", {
"form": form,
"open_password_modal": open_password_modal,
"origin_next": origin_next, # <- clé : toujours renvoyer au template
})
@login_required
@require_POST
def switch_account(request):
origin_next = _safe_origin_next(request)
target_user_id = request.POST.get("target_user_id")
try:
current_config: UserConfig = request.user.config
except UserConfig.DoesNotExist:
messages.error(request, _("Aucun compte lié n'est disponible."))
return redirect(origin_next or reverse("home:index"))
if not target_user_id:
messages.error(request, _("Aucun compte cible n'a été sélectionné."))
return redirect(origin_next or reverse("home:index"))
if str(request.user.pk) == str(target_user_id):
messages.info(request, _("Ce compte est déjà actif."))
return redirect(origin_next or reverse("home:index"))
session = request.session
allowed_ids = session.get(SESSION_ALLOWED_IDS_KEY)
origin_user_id = session.get(SESSION_ORIGIN_ID_KEY)
if allowed_ids is None:
allowed_ids = compute_initial_allowed_ids(current_config)
if origin_user_id is None:
origin_user_id = request.user.pk
allowed_ids = list(dict.fromkeys(int(pk) for pk in allowed_ids))
if request.user.pk not in allowed_ids:
allowed_ids.append(request.user.pk)
try:
target_user_pk = int(target_user_id)
except (TypeError, ValueError):
messages.error(request, _("Vous ne pouvez pas accéder à ce compte."))
return redirect(origin_next or reverse("home:index"))
if target_user_pk not in allowed_ids:
messages.error(request, _("Vous ne pouvez pas accéder à ce compte."))
return redirect(origin_next or reverse("home:index"))
session[SESSION_ALLOWED_IDS_KEY] = allowed_ids
session[SESSION_ORIGIN_ID_KEY] = origin_user_id
try:
target_config = (
UserConfig.objects.select_related("user").get(user__pk=target_user_pk)
)
except UserConfig.DoesNotExist:
messages.error(request, _("Vous ne pouvez pas accéder à ce compte."))
return redirect(origin_next or reverse("home:index"))
target_user = target_config.user
target_user.backend = "django.contrib.auth.backends.ModelBackend"
auth_login(request, target_user)
messages.success(request, _("Changement de compte effectué."))
session[SESSION_ALLOWED_IDS_KEY] = allowed_ids
session[SESSION_ORIGIN_ID_KEY] = origin_user_id
session.pop(SESSION_MAP_BOUNDS_KEY, None)
return redirect(origin_next or reverse("home:index"))
@login_required
@require_POST
def save_intervention_list_layout(request):
"""API endpoint to save the user's preferred layout for the interventions list."""
import json
try:
data = json.loads(request.body)
layout = data.get('layout')
except (json.JSONDecodeError, KeyError):
return JsonResponse({'error': _('Données invalides')}, status=400)
valid_layouts = ['both', 'table_only', 'map_only']
if layout not in valid_layouts:
return JsonResponse({'error': _('Layout invalide')}, status=400)
try:
user_config = request.user.config
user_config.intervention_list_layout = layout
user_config.save(update_fields=['intervention_list_layout'])
return JsonResponse({'success': True, 'layout': layout})
except UserConfig.DoesNotExist:
return JsonResponse({'error': _('Configuration utilisateur non trouvée')}, status=404)
@login_required
@require_POST
def save_assets_list_layout(request):
"""API endpoint to save the user's preferred layout for the assets list."""
import json
try:
data = json.loads(request.body)
layout = data.get('layout')
except (json.JSONDecodeError, KeyError):
return JsonResponse({'error': _('Données invalides')}, status=400)
valid_layouts = ['both', 'table_only', 'map_only']
if layout not in valid_layouts:
return JsonResponse({'error': _('Layout invalide')}, status=400)
try:
user_config = request.user.config
user_config.assets_list_layout = layout
user_config.save(update_fields=['assets_list_layout'])
return JsonResponse({'success': True, 'layout': layout})
except UserConfig.DoesNotExist:
return JsonResponse({'error': _('Configuration utilisateur non trouvée')}, status=404)
@login_required
@require_POST
def save_intervention_list_preferences(request):
"""API endpoint to save the user's preferred filters and sort for the interventions list."""
import json
try:
data = json.loads(request.body)
filters = data.get('filters', {})
sort = data.get('sort', {})
columns = data.get('columns', None)
except (json.JSONDecodeError, KeyError):
return JsonResponse({'error': _('Données invalides')}, status=400)
try:
user_config = request.user.config
# Validate filters structure (should be a dict with filter names as keys)
if not isinstance(filters, dict):
return JsonResponse({'error': _('Format de filtres invalide')}, status=400)
# Validate sort structure (should have sortName and sortOrder)
if not isinstance(sort, dict):
return JsonResponse({'error': _('Format de tri invalide')}, status=400)
update_fields = ['intervention_list_filters', 'intervention_list_sort']
user_config.intervention_list_filters = filters
user_config.intervention_list_sort = sort
if columns is not None:
if not isinstance(columns, list):
return JsonResponse({'error': _('Format de colonnes invalide')}, status=400)
user_config.intervention_list_columns = columns
update_fields.append('intervention_list_columns')
user_config.save(update_fields=update_fields)
return JsonResponse({'success': True, 'filters': filters, 'sort': sort})
except UserConfig.DoesNotExist:
return JsonResponse({'error': _('Configuration utilisateur non trouvée')}, status=404)
@login_required
@require_POST
def save_observation_list_preferences(request):
"""API endpoint to save the user's preferred filters and sort for the observations list."""
import json
try:
data = json.loads(request.body)
filters = data.get('filters', {})
sort = data.get('sort', {})
except (json.JSONDecodeError, KeyError):
return JsonResponse({'error': _('Données invalides')}, status=400)
try:
user_config = request.user.config
if not isinstance(filters, dict):
return JsonResponse({'error': _('Format de filtres invalide')}, status=400)
if not isinstance(sort, dict):
return JsonResponse({'error': _('Format de tri invalide')}, status=400)
user_config.observation_list_filters = filters
user_config.observation_list_sort = sort
user_config.save(update_fields=['observation_list_filters', 'observation_list_sort'])
return JsonResponse({'success': True, 'filters': filters, 'sort': sort})
except UserConfig.DoesNotExist:
return JsonResponse({'error': _('Configuration utilisateur non trouvée')}, status=404)
@login_required
@require_POST
def save_control_list_preferences(request):
"""API endpoint to save the user's preferred filters and sort for the controls list."""
import json
try:
data = json.loads(request.body)
filters = data.get('filters', {})
sort = data.get('sort', {})
except (json.JSONDecodeError, KeyError):
return JsonResponse({'error': _('Données invalides')}, status=400)
try:
user_config = request.user.config
if not isinstance(filters, dict):
return JsonResponse({'error': _('Format de filtres invalide')}, status=400)
if not isinstance(sort, dict):
return JsonResponse({'error': _('Format de tri invalide')}, status=400)
user_config.control_list_filters = filters
user_config.control_list_sort = sort
user_config.save(update_fields=['control_list_filters', 'control_list_sort'])
return JsonResponse({'success': True, 'filters': filters, 'sort': sort})
except UserConfig.DoesNotExist:
return JsonResponse({'error': _('Configuration utilisateur non trouvée')}, status=404)
@login_required
@require_POST
def save_inspection_list_preferences(request):
"""API endpoint to save the user's preferred filters and sort for the inspections list."""
import json
try:
data = json.loads(request.body)
filters = data.get('filters', {})
sort = data.get('sort', {})
except (json.JSONDecodeError, KeyError):
return JsonResponse({'error': _('Données invalides')}, status=400)
try:
user_config = request.user.config
if not isinstance(filters, dict):
return JsonResponse({'error': _('Format de filtres invalide')}, status=400)
if not isinstance(sort, dict):
return JsonResponse({'error': _('Format de tri invalide')}, status=400)
user_config.inspection_list_filters = filters
user_config.inspection_list_sort = sort
user_config.save(update_fields=['inspection_list_filters', 'inspection_list_sort'])
return JsonResponse({'success': True, 'filters': filters, 'sort': sort})
except UserConfig.DoesNotExist:
return JsonResponse({'error': _('Configuration utilisateur non trouvée')}, status=404)
@login_required
@require_POST
def save_assets_index_preferences(request):
"""API endpoint to save the user's preferences for the unified assets index page."""
import json
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'error': _('Données invalides')}, status=400)
thematics = data.get('thematics', [])
categories = data.get('categories', [])
models = data.get('models', [])
conditional_filters = data.get('conditional_filters', {})
visible_columns = data.get('visible_columns')
type_filter = data.get('type_filter', 'all')
status_filters = data.get('status_filters', [])
validation_status_filters = data.get('validation_status_filters', [])
include_no_geom = data.get('include_no_geom', True)
map_center = data.get('map_center')
map_zoom = data.get('map_zoom')
if not isinstance(thematics, list):
return JsonResponse({'error': _('Format de thématiques invalide')}, status=400)
if not isinstance(categories, list):
return JsonResponse({'error': _('Format de catégories invalide')}, status=400)
if not isinstance(models, list):
return JsonResponse({'error': _('Format de modèles invalide')}, status=400)
if not isinstance(conditional_filters, dict):
return JsonResponse({'error': _('Format de filtres thématiques invalide')}, status=400)
if visible_columns is not None and not isinstance(visible_columns, list):
return JsonResponse({'error': _('Format de colonnes visibles invalide')}, status=400)
if not (isinstance(type_filter, str) or (isinstance(type_filter, list) and all(isinstance(x, str) for x in type_filter))):
return JsonResponse({'error': _('Format de filtre type invalide')}, status=400)
if not isinstance(status_filters, list):
return JsonResponse({'error': _('Format de filtres statut invalide')}, status=400)
if not isinstance(validation_status_filters, list):
return JsonResponse({'error': _('Format de filtres statut de validation invalide')}, status=400)
try:
user_config, _created = UserConfig.objects.get_or_create(user=request.user)
existing_prefs = user_config.assets_index_preferences or {}
existing_visible_columns = existing_prefs.get('visible_columns', []) if isinstance(existing_prefs, dict) else []
if not isinstance(existing_visible_columns, list):
existing_visible_columns = []
user_config.assets_index_preferences = {
'thematics': thematics,
'categories': categories,
'models': models,
'conditional_filters': conditional_filters,
'visible_columns': visible_columns if visible_columns is not None else existing_visible_columns,
'type_filter': type_filter,
'status_filters': status_filters,
'validation_status_filters': validation_status_filters,
'include_no_geom': include_no_geom,
'map_center': map_center,
'map_zoom': map_zoom,
}
user_config.save(update_fields=['assets_index_preferences'])
return JsonResponse({'success': True})
except UserConfig.DoesNotExist:
return JsonResponse({'error': _('Configuration utilisateur non trouvée')}, status=404)
@login_required
@require_POST
def save_calendar_preferences(request):
"""API endpoint to save the user's preferred filters, view_mode, zoom and backlog states for the calendar."""
import json
try:
data = json.loads(request.body)
preferences = data.get('preferences', {})
except (json.JSONDecodeError, KeyError):
return JsonResponse({'error': _('Données invalides')}, status=400)
try:
user_config = request.user.config
if not isinstance(preferences, dict):
return JsonResponse({'error': _('Format de préférences invalide')}, status=400)
user_config.calendar_preferences = preferences
user_config.save(update_fields=['calendar_preferences'])
return JsonResponse({'success': True, 'preferences': preferences})
except UserConfig.DoesNotExist:
return JsonResponse({'error': _('Configuration utilisateur non trouvée')}, status=404)
@login_required
@require_POST
def save_project_list_preferences(request):
"""API endpoint to save the user's preferred filters and sort for the projects list."""
import json
try:
data = json.loads(request.body)
filters = data.get('filters', {})
sort = data.get('sort', {})
except (json.JSONDecodeError, KeyError):
return JsonResponse({'error': _('Données invalides')}, status=400)
try:
user_config = request.user.config
if not isinstance(filters, dict):
return JsonResponse({'error': _('Format de filtres invalide')}, status=400)
if not isinstance(sort, dict):
return JsonResponse({'error': _('Format de tri invalide')}, status=400)
user_config.project_list_filters = filters
user_config.project_list_sort = sort
user_config.save(update_fields=['project_list_filters', 'project_list_sort'])
return JsonResponse({'success': True, 'filters': filters, 'sort': sort})
except UserConfig.DoesNotExist:
return JsonResponse({'error': _('Configuration utilisateur non trouvée')}, status=404)
@login_required
@require_GET
def get_help_document(request, doc_name):
"""View to securely load and render Markdown help documents based on user roles and permissions."""
import markdown
from django.utils import translation
from django.http import HttpResponseForbidden
ALLOWED_DOCS = ['prise_en_main', 'concepts', 'assets', 'approuver_interventions', 'gerer_projets', 'statuts_intervention', 'creer_controle']
if doc_name not in ALLOWED_DOCS:
return JsonResponse({'error': _('Document non trouvé')}, status=404)
try:
user_config = request.user.config
except UserConfig.DoesNotExist:
return JsonResponse({'error': _('Configuration utilisateur non trouvée')}, status=404)
# Permission check for specific guides
if doc_name == 'approuver_interventions':
if not user_config.can_approve_any_intervention:
return HttpResponseForbidden(_('Accès interdit'))
elif doc_name in ['gerer_projets', 'creer_controle']:
has_project_role = user_config.roles.filter(name__in=["admin", "manager", "controller", "external_manager", "top_manager"]).exists()
if not (has_project_role or request.user.is_superuser):
return HttpResponseForbidden(_('Accès interdit'))
# Determine language directory
lang = translation.get_language()
if lang and lang.startswith('nl'):
lang_dir = 'nl'
else:
lang_dir = 'fr'
# Resolve file path
file_path = settings.BASE_DIR / 'common' / 'help_docs' / lang_dir / f'{doc_name}.md'
if not os.path.exists(file_path):
return JsonResponse({'error': _('Document non trouvé')}, status=404)
try:
with open(file_path, 'r', encoding='utf-8') as f:
md_content = f.read()
except Exception:
return JsonResponse({'error': _('Erreur lors de la lecture du document')}, status=500)
# Convert Markdown to HTML
html_content = markdown.markdown(md_content, extensions=['extra', 'nl2br'])
return JsonResponse({'html': html_content})
from django.views.i18n import set_language as django_set_language
@login_not_required
def set_language(request):
"""
Wraps Django's set_language to persist the chosen language in UserConfig.preferred_language
when the user is authenticated.
"""
response = django_set_language(request)
if request.user.is_authenticated and request.method == 'POST':
lang_code = request.POST.get('language')
valid_langs = [code for code, _lang_name in getattr(settings, 'LANGUAGES', [('fr', 'Français'), ('nl', 'Nederlands'), ('en', 'English')])]
if lang_code and lang_code in valid_langs:
config, _created = UserConfig.objects.get_or_create(user=request.user)
if config.preferred_language != lang_code:
config.preferred_language = lang_code
config.save(update_fields=['preferred_language'])
return response