from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseRedirect from django.shortcuts import render, get_object_or_404, redirect from django.template.loader import render_to_string from django.urls import reverse, NoReverseMatch from django.core.serializers import serialize from django.core.exceptions import FieldDoesNotExist from django.core.files.base import ContentFile from django.contrib.gis.db.models.functions import Transform, Centroid, AsGeoJSON, Distance from django.contrib.gis.geos import GEOSGeometry, Point, MultiPolygon from django.contrib.gis.measure import D from django.contrib.contenttypes.models import ContentType from django.contrib import messages from django.views.decorators.http import require_POST, require_GET, require_http_methods from django.contrib.auth.decorators import login_required from django.utils.timezone import now, localtime, make_aware, get_current_timezone, is_naive from django.utils import timezone as django_timezone from django.utils import timezone from django.utils.dateparse import parse_datetime from django.utils.dateformat import format as dj_format_date from django.utils.text import Truncator from django.utils.html import strip_tags from django.template.defaultfilters import linebreaksbr from django.utils.translation import get_language, gettext as _, gettext_lazy as _lazy, ngettext from django.apps import apps from django.conf import settings from django.core.mail import EmailMultiAlternatives from django.db.models import F, DateTimeField, Q, Case, When, IntegerField, CharField, Value, Count, Prefetch, Sum, Max, QuerySet, OuterRef, Subquery, Exists from django.db.models.functions import Coalesce, Lower, Cast from django.db.models.expressions import ExpressionWrapper from django.db.models import DurationField from django.db.models.fields.related import ForeignKey from django.db import transaction from collections import defaultdict, OrderedDict from decimal import Decimal, InvalidOperation import json import os import io import zipfile from fpdf import FPDF import pytz from urllib.parse import urlencode import re from html import unescape from typing import List, Iterable, Tuple, Dict, Optional, Set import mimetypes import csv from io import BytesIO from datetime import timedelta, datetime from dateutil.relativedelta import relativedelta from common.private_files.helpers import build_private_url from common.utils import get_short_name from interventions.models import (InterventionSubscription, Intervention, InterventionAsset, InterventionTimeLine, InterventionContractPost, InterventionDocument, InterventionNote, InterventionLocation, InterventionLink, ContractPauseReasonAssignment, Symptom, SourceCategory, InterventionPlanificationTimeLine, InterventionRealizationTimeLine, InterventionOccupation, Operation, OperationTemplate, OperationTemplateItem, OperationStatus, OperationStatusSet, MeasurementDefinition, GuidedOperationTemplate, GuidedOperationData, GuidedOperationMedia, InterventionPostPeriodQuantity, InterventionEmailLog, InterventionAssignment) from interventions.models import STATUS_CHOICES, STATUS_ORDERS, MAINTAIN_CHOICES, TYPE_CHOICES, ORIGIN_TYPE_CHOICES, PAUSE_REASON_CHOICES from interventions.views.forms import (InterventionForm, InterventionNoteForm, InterventionDocumentForm, InterventionOccupationForm, PreventiveInterventionForm, AssetsLinkForm, LinkExistingInterventionsForm) from interventions.permissions import (intervention_permission_required, intervention_add_permission_required, intervention_edit_permission_required, intervention_add_preventive_permission_required, get_allowed_statuses_for_user, get_allowed_update_fields, can_view_intervention, can_control_intervention, can_edit_intervention, can_view_intervention_summary, get_accessible_interventions_for_technician, filter_viewable_interventions_for_user, can_edit_note, can_delete_document, can_manage_occupations, can_add_intervention, can_add_repair_intervention, intervention_manage_occupations_required, get_contracts_for_intervention_creation, is_external_manager_with_contract_creation_rights, should_show_created_by_provider_badge, can_view_correction_messages) from interventions.permissions import ALLOWED_TRANSITIONS, get_allowed_transitions, user_allowed_transitions from interventions.views.drafts import InterventionDraftManager from interventions.utils import resolve_intervention_localisations from common.models import UserConfig, UserContractAccess, UserContractStatusPermission, Thematic, UserThematics from contracts.models import ContractOrder, ContractPost, Contract, Company, CompanyTeam, CompanyMember, UserCompanyTeamAccess, CompanyMemberHistory from contracts.permissions import get_allowed_contracts_for_user, user_has_access_to_contract_order from projects.models import Project, ProjectIntervention from assets.models import ( PublicLightingStreet, TrafficLightIntersection, TrafficLightContract, NatureLocation, StructureLocation, Structure, StructureContract, RoadStreet, SignStreet, AssetCategory, AbstractAsset, AbstractAssetModel, ITSGeoAsset, ITSGeoAssetContract, CleanLocation, CleanLocationContract, ) from assets.permissions import get_allowed_actions_for_asset from assets.views.locations import LOCATION_REGISTRY, registry_items_for_thematics, base_search_qs, SEARCH_LIMIT_PER_TYPE, SEARCH_LIMIT_TOTAL from common.utils import redirect_with_variant, measure_time, strip_accents_lower from observations.permissions import get_observation_access_context from observations.models import Observation as ObservationModel from controls.permissions import user_can_view_control from interventions.views.views_pdf_reporting import ( generate_intervention_pdf, preview_intervention_pdf, download_intervention_report_pdf, preview_intervention_report_pdf, bulk_execution_report_pdf, start_bulk_execution_report, poll_bulk_execution_report, download_bulk_execution_report, ) import logging import smtplib import socket import ssl logger = logging.getLogger(__name__) from interventions.views.helpers import * def intervention_add_message(request, intervention_id): """ Poste un message dans la discussion liée à cette intervention. Tout utilisateur ayant accès à l'intervention peut poster. """ intervention = get_object_or_404(Intervention, pk=intervention_id) from notifications.services import ( can_post_intervention_message, get_or_create_intervention_discussion, post_message, ) if not can_post_intervention_message(request.user, intervention): messages.error(request, _("Vous n'avez pas la permission de poster un message sur cette intervention.")) return redirect( reverse('interventions:intervention_detail', args=[intervention_id]) + '#discussion' ) content = (request.POST.get('content') or '').strip() if not content: messages.warning(request, _("Le message ne peut pas être vide.")) return redirect( reverse('interventions:intervention_detail', args=[intervention_id]) + '#discussion' ) discussion, _ = get_or_create_intervention_discussion(intervention) files = request.FILES.getlist('attachments') or None post_message(discussion=discussion, author=request.user, content=content, files=files) return redirect( reverse('interventions:intervention_detail', args=[intervention_id]) + '#discussion' ) def intervention_add_note(request, intervention_id): intervention = get_object_or_404(Intervention, id=intervention_id) form = InterventionNoteForm(request.POST) if form.is_valid(): note_instance = form.save(commit=False) note_instance.intervention = intervention note_instance.note_author = request.user note_instance.note_time = now() note_instance.save() return JsonResponse({ 'success': True, 'note': _serialize_note(note_instance, request.user) }) return JsonResponse({'success': False, 'errors': form.errors}, status=400) def intervention_update_note(request, intervention_id, note_id): intervention = get_object_or_404(Intervention, id=intervention_id) note = get_object_or_404(InterventionNote, id=note_id, intervention=intervention) if not can_edit_note(request.user, request.user.config, note): return JsonResponse({'success': False, 'error': _('Vous ne pouvez pas modifier cette note.')}, status=403) form = InterventionNoteForm(request.POST, instance=note) if form.is_valid(): note_instance = form.save(commit=False) note_instance.updated_by = request.user note_instance.save(update_fields=['content', 'note_type', 'updated_at', 'updated_by']) note.refresh_from_db(fields=['content', 'note_type', 'updated_at', 'updated_by']) return JsonResponse({ 'success': True, 'note': _serialize_note(note, request.user) }) return JsonResponse({'success': False, 'errors': form.errors}, status=400) def intervention_delete_note(request, intervention_id, note_id): intervention = get_object_or_404(Intervention, id=intervention_id) note = get_object_or_404(InterventionNote, id=note_id, intervention=intervention) if not can_edit_note(request.user, request.user.config, note): return JsonResponse({'success': False, 'error': _('Vous ne pouvez pas supprimer cette note.')}, status=403) note.delete() return JsonResponse({'success': True}) def intervention_add_document(request, intervention_id): intervention = get_object_or_404(Intervention, id=intervention_id) variant = request.POST.get('v') or request.GET.get('v') if request.method == 'POST': form = InterventionDocumentForm(request.POST, request.FILES) if form.is_valid(): files = form.cleaned_data["documents"] for file in files: InterventionDocument.objects.create(intervention=intervention, file=file, uploaded_by=request.user) messages.success(request, _("Document(s) sauvegardé(s) avec succès !")) return redirect_with_variant('interventions:intervention_detail', variant=variant, url_kwargs={'intervention_id': intervention_id}) else: messages.error(request, _("Erreur lors de la sauvegarde des documents.")) return redirect_with_variant('interventions:intervention_detail', variant=variant, url_kwargs={'intervention_id': intervention_id}) else: form = InterventionDocumentForm() template_name = 'interventions/add/add_intervention_document_mobile.html' if variant == 'mobile' else 'interventions/add/add_intervention_document.html' return render(request, template_name, {'form': form, 'intervention': intervention}) def intervention_delete_document(request, intervention_id, document_id): intervention = get_object_or_404(Intervention, id=intervention_id) document = get_object_or_404(InterventionDocument, id=document_id, intervention=intervention) try: user_config = request.user.config except UserConfig.DoesNotExist: user_config = None if not can_delete_document(request.user, user_config, document): return JsonResponse({'success': False, 'error': _('Vous ne pouvez pas supprimer ce document.')}, status=403) if document.file: document.file.delete(save=False) if document.thumbnail: document.thumbnail.delete(save=False) document.delete() return JsonResponse({'success': True}) def intervention_get_copy_target_tags(request, intervention_id): """ Returns available DocumentTags for a given content-type target (used in the copy-document modal). GET ?ct_id=X&obj_id=Y """ from django.db.models import Count, Q from documents.models import DocumentTag intervention = get_object_or_404(Intervention, pk=intervention_id) ct_id = request.GET.get('ct_id') obj_id_raw = request.GET.get('obj_id') if not ct_id: return JsonResponse({'tags': []}) try: from django.contrib.contenttypes.models import ContentType as _CT ct = _CT.objects.get(pk=int(ct_id)) except (Exception,): return JsonResponse({'tags': []}) model_class = ct.model_class() # --- Determine thematic --- thematic = None # 1) Check LOCATION_REGISTRY for location models for _key, info in LOCATION_REGISTRY.items(): if info['model'] == model_class: try: from common.models import Thematic as _Thematic thematic = _Thematic.objects.filter(code=info['thematic']).first() except Exception: pass break # 2) For asset models: get thematic via category if thematic is None and model_class and obj_id_raw: try: obj = model_class.objects.select_related('category__thematic').get(pk=int(obj_id_raw)) cat = getattr(obj, 'category', None) if cat: thematic = getattr(cat, 'thematic', None) except Exception: pass # 3) Fallback: intervention's own thematic if thematic is None: thematic = intervention.thematic if thematic is None: return JsonResponse({'tags': []}) # Filter tags: same logic as asset_documents template tag qs = DocumentTag.objects.filter(thematics=thematic) qs = (qs .annotate(ct_count=Count('related_content_types')) .filter(Q(ct_count=0) | Q(related_content_types=ct)) .distinct() .order_by('order', 'name_fr')) lang = get_language() tags_data = [ { 'id': t.pk, 'name': t.name_nl if lang.startswith('nl') and t.name_nl else t.name_fr, 'extra_params_schema': t.extra_params_schema or [], } for t in qs ] return JsonResponse({'tags': tags_data}) def intervention_copy_document(request, intervention_id, document_id): """ Copy an InterventionDocument to a linked location or asset as a ManagedDocument. POST params: ct_id, obj_id — target content-type / object tag_ids[] — selected tag PKs (optional) ep_{tag_id}_{key} — extra param values per tag (optional) """ import os from django.core.files.base import ContentFile from django.contrib.contenttypes.models import ContentType as _CT from documents.models import ManagedDocument, DocumentVersion, DocumentAttachment, DocumentTag from documents.services import DocumentManagementService intervention = get_object_or_404(Intervention, pk=intervention_id) document = get_object_or_404(InterventionDocument, pk=document_id, intervention=intervention) ct_id = request.POST.get('ct_id') obj_id_raw = request.POST.get('obj_id') tag_ids = request.POST.getlist('tag_ids') if not ct_id or not obj_id_raw: return JsonResponse({'success': False, 'error': _('Cible non spécifiée')}, status=400) try: ct = _CT.objects.get(pk=int(ct_id)) obj_id = int(obj_id_raw) except Exception: return JsonResponse({'success': False, 'error': _('Cible invalide')}, status=400) # Verify the target is actually linked to this intervention is_linked = ( InterventionLocation.objects.filter( intervention=intervention, content_type=ct, object_id=obj_id ).exists() or InterventionAsset.objects.filter( intervention=intervention, content_type=ct, object_id=obj_id ).exists() ) if not is_linked: return JsonResponse( {'success': False, 'error': _("La cible n'est pas liée à cette intervention")}, status=400, ) filename = os.path.basename(document.file.name) title = os.path.splitext(filename)[0] # Collect extra params per tag from POST keys ep_{tag_id}_{param_key} tag_extra_params = {} for key, value in request.POST.items(): if key.startswith('ep_'): parts = key[3:].split('_', 1) if len(parts) == 2: t_id, param_key = parts tag_extra_params.setdefault(t_id, {})[param_key] = value.strip() # Resolve target object and its document folder (for service-based versioning) target_obj = None document_folder = None try: model_class = ct.model_class() if model_class: target_obj = model_class.objects.get(pk=obj_id) document_folder = DocumentManagementService.get_default_folder_for_object(target_obj) except Exception: pass try: with transaction.atomic(): if tag_ids and document_folder is not None: # Service path: one document per tag with proper unique-tag versioning valid_tags = DocumentTag.objects.filter(pk__in=tag_ids) for tag in valid_tags: with document.file.open('rb') as src: file_copy = ContentFile(src.read(), name=filename) extra = {k: v for k, v in tag_extra_params.get(str(tag.pk), {}).items() if v} DocumentManagementService.add_document( file=file_copy, tag=tag, folder=document_folder, user=request.user, title=title, content_object=target_obj, unique_params=extra or None, ) else: # Direct path: single document with all tags (no folder configured) with document.file.open('rb') as src: file_copy = ContentFile(src.read(), name=filename) merged_params = {} for params in tag_extra_params.values(): merged_params.update({k: v for k, v in params.items() if v}) doc = ManagedDocument.objects.create( title=title, created_by=request.user, unique_params=merged_params, ) if tag_ids: valid_tags = DocumentTag.objects.filter(pk__in=tag_ids) doc.tags.set(valid_tags) version = DocumentVersion( document=doc, uploaded_by=request.user, version_number=1, ) version.file.save(filename, file_copy, save=True) DocumentAttachment.objects.create( document=doc, content_type=ct, object_id=obj_id, attached_by=request.user, ) except Exception as e: return JsonResponse({'success': False, 'error': str(e)}, status=500) return JsonResponse({'success': True}) def linked_interventions_summary(request): """ Renvoie le total d'interventions liées (en cours + récentes terminées): - ?location_ct=&location_id= - ?equipment_ct=&equipment_id= - ?lon=&lat=[&radius=50] Retourne: {total: int, total_recent: int} """ base_qs = filter_viewable_interventions_for_user(request.user) thematic_code = request.GET.get('thematic') if thematic_code: base_qs = base_qs.filter(thematic__code=thematic_code) location_ct = request.GET.get('location_ct') location_id = request.GET.get('location_id') equipment_ct = request.GET.get('equipment_ct') equipment_id = request.GET.get('equipment_id') category_id = request.GET.get('category_id') or request.GET.get('asset_category_id') if category_id: try: base_qs = base_qs.filter(asset_category_id=int(category_id)) except (TypeError, ValueError): pass lon = request.GET.get('lon') lat = request.GET.get('lat') radius = float(request.GET.get('radius', 50.0)) if location_ct and location_id: base_qs = base_qs.filter( locations__content_type_id=location_ct, locations__object_id=location_id ) elif equipment_ct and equipment_id: base_qs = base_qs.filter( interventionasset__content_type_id=equipment_ct, interventionasset__object_id=equipment_id ) elif lon and lat: try: pt = Point(float(lon), float(lat), srid=4326) pt.transform(3812) base_qs = base_qs.filter(geom__dwithin=(pt, D(m=radius))) except Exception: return JsonResponse({'total': 0, 'total_recent': 0}) else: return JsonResponse({'total': 0, 'total_recent': 0}) # Interventions en cours total = base_qs.filter(status__in=LINKED_TARGET_STATUSES).count() # Interventions récentes terminées (3 derniers mois) three_months_ago = timezone.now() - timedelta(days=90) total_recent = base_qs.filter( status__in=LINKED_RECENT_STATUSES ).annotate( effective_end=Coalesce('end_time', 'planned_end_time', 'expected_end_time', 'planned_begin_time', 'creation_time', output_field=DateTimeField()) ).filter( effective_end__gte=three_months_ago ).count() return JsonResponse({'total': total, 'total_recent': total_recent}) def intervention_summary_modal(request, intervention_id): intervention = get_object_or_404( Intervention.objects.annotate( effective_end=Coalesce( 'end_time', 'planned_end_time', 'expected_end_time', 'creation_time', output_field=DateTimeField(), ), ), pk=intervention_id, ) if not can_view_intervention_summary(request.user, intervention): return HttpResponseForbidden(_("Vous n'avez pas la permission de voir cette intervention.")) work_done_notes = ( InterventionNote.objects .filter(intervention=intervention, note_type=InterventionNote.NoteType.WORK_DONE) .select_related('note_author', 'updated_by') .order_by('-note_time') ) summary_documents = ( intervention.documents .select_related('uploaded_by') .order_by('-upload_date') ) variant = request.GET.get('variant') template_name = ( 'interventions/partials/intervention_summary_modal_body_mobile.html' if variant == 'mobile' else 'interventions/partials/intervention_summary_modal_body.html' ) return render(request, template_name, { 'intervention': intervention, 'work_done_notes': work_done_notes, 'summary_documents': summary_documents, }) def linked_interventions_list(request): base_qs = filter_viewable_interventions_for_user(request.user) thematic_code = request.GET.get('thematic') if thematic_code: base_qs = base_qs.filter(thematic__code=thematic_code) location_ct = request.GET.get('location_ct') location_id = request.GET.get('location_id') equipment_ct = request.GET.get('equipment_ct') equipment_id = request.GET.get('equipment_id') category_id = request.GET.get('category_id') or request.GET.get('asset_category_id') if category_id: try: base_qs = base_qs.filter(asset_category_id=int(category_id)) except (TypeError, ValueError): pass lon = request.GET.get('lon') lat = request.GET.get('lat') radius = float(request.GET.get('radius', 50.0)) list_type = request.GET.get('list_type', 'ongoing') # 'ongoing' ou 'recent' if location_ct and location_id: base_qs = base_qs.filter( locations__content_type_id=location_ct, locations__object_id=location_id ) elif equipment_ct and equipment_id: base_qs = base_qs.filter( interventionasset__content_type_id=equipment_ct, interventionasset__object_id=equipment_id ) elif lon and lat: try: pt = Point(float(lon), float(lat), srid=4326) pt.transform(3812) base_qs = base_qs.filter(geom__dwithin=(pt, D(m=radius))) except Exception: return JsonResponse({'items': [], 'counts': {}}) # ⚠️ Si les jointures du dessus peuvent dupliquer des interventions, protège avec distinct() base_qs = base_qs.distinct() # Filtrer selon le type de liste demandé if list_type == 'recent': # Interventions récentes terminées (3 derniers mois) three_months_ago = timezone.now() - timedelta(days=90) filtered_qs = base_qs.filter( status__in=LINKED_RECENT_STATUSES ).annotate( effective_end=Coalesce('end_time', 'planned_end_time', 'expected_end_time', 'planned_begin_time', 'creation_time', output_field=DateTimeField()) ).filter( effective_end__gte=three_months_ago ) # ✅ Comptages pour interventions récentes counts = filtered_qs.aggregate( finished=Count('id', filter=Q(status='finished'), distinct=True), processed=Count('id', filter=Q(status='processed'), distinct=True), corrected=Count('id', filter=Q(status='corrected'), distinct=True), validated=Count('id', filter=Q(status='validated'), distinct=True), invoiced=Count('id', filter=Q(status='invoiced'), distinct=True), closed=Count('id', filter=Q(status='closed'), distinct=True), canceled=Count('id', filter=Q(status='canceled'), distinct=True), ) else: # Interventions en cours (par défaut) filtered_qs = base_qs.filter(status__in=LINKED_TARGET_STATUSES) # ✅ Comptages AVANT tout slice, en une requête counts = filtered_qs.aggregate( to_be_processed=Count('id', filter=Q(status='to_be_processed'), distinct=True), in_progress=Count('id', filter=Q(status='in_progress'), distinct=True), on_pause=Count('id', filter=Q(status='on_pause'), distinct=True), ) # ✅ Liste (slice uniquement ici) items_qs = ( filtered_qs.select_related('assigned_provider') .order_by('-creation_time') )[:200] items = [{ 'id': itv.id, 'code': itv.code, 'title': itv.title, 'status': itv.status, 'begin_time': dj_format_date(itv.begin_time or itv.planned_begin_time, 'd/m/Y H:i') if (itv.begin_time or itv.planned_begin_time) else '', 'url': reverse('interventions:intervention_detail', kwargs={'intervention_id': itv.id}), } for itv in items_qs] return JsonResponse({'items': items, 'counts': counts}) def linked_interventions_geojson(request): """ Retourne un GeoJSON FeatureCollection avec toutes les interventions liées (en cours + récentes terminées), pour affichage sur la carte. Les points n'ont pas de position si lon/lat est null. """ base_qs = filter_viewable_interventions_for_user(request.user) thematic_code = request.GET.get('thematic') if thematic_code: base_qs = base_qs.filter(thematic__code=thematic_code) location_ct = request.GET.get('location_ct') location_id = request.GET.get('location_id') equipment_ct = request.GET.get('equipment_ct') equipment_id = request.GET.get('equipment_id') category_id = request.GET.get('category_id') or request.GET.get('asset_category_id') if category_id: try: base_qs = base_qs.filter(asset_category_id=int(category_id)) except (TypeError, ValueError): pass lon = request.GET.get('lon') lat = request.GET.get('lat') radius = float(request.GET.get('radius', 200.0)) if location_ct and location_id: base_qs = base_qs.filter( locations__content_type_id=location_ct, locations__object_id=location_id ) elif equipment_ct and equipment_id: base_qs = base_qs.filter( interventionasset__content_type_id=equipment_ct, interventionasset__object_id=equipment_id ) elif lon and lat: try: pt = Point(float(lon), float(lat), srid=4326) pt.transform(3812) base_qs = base_qs.filter(geom__dwithin=(pt, D(m=radius))) except Exception: return JsonResponse({'type': 'FeatureCollection', 'features': []}) else: return JsonResponse({'type': 'FeatureCollection', 'features': []}) base_qs = base_qs.distinct() exclude_id = request.GET.get('exclude_id') if exclude_id: try: base_qs = base_qs.exclude(pk=int(exclude_id)) except (ValueError, TypeError): pass three_months_ago = timezone.now() - timedelta(days=90) # Ongoing ongoing_qs = base_qs.filter( status__in=LINKED_TARGET_STATUSES, lon__isnull=False, lat__isnull=False, ).values('id', 'code', 'title', 'status', 'lon', 'lat')[:500] # Recent (terminated, within 3 months) recent_qs = base_qs.filter( status__in=LINKED_RECENT_STATUSES, lon__isnull=False, lat__isnull=False, ).annotate( effective_end=Coalesce('end_time', 'planned_end_time', 'expected_end_time', 'planned_begin_time', 'creation_time', output_field=DateTimeField()) ).filter(effective_end__gte=three_months_ago).values('id', 'code', 'title', 'status', 'lon', 'lat')[:500] features = [] seen_ids = set() for itv in ongoing_qs: seen_ids.add(itv['id']) features.append({ 'type': 'Feature', 'geometry': {'type': 'Point', 'coordinates': [itv['lon'], itv['lat']]}, 'properties': { 'id': itv['id'], 'code': itv['code'], 'title': itv['title'], 'status': itv['status'], 'layer': 'ongoing', 'url': reverse('interventions:intervention_detail', kwargs={'intervention_id': itv['id']}), }, }) for itv in recent_qs: if itv['id'] in seen_ids: continue features.append({ 'type': 'Feature', 'geometry': {'type': 'Point', 'coordinates': [itv['lon'], itv['lat']]}, 'properties': { 'id': itv['id'], 'code': itv['code'], 'title': itv['title'], 'status': itv['status'], 'layer': 'recent', 'url': reverse('interventions:intervention_detail', kwargs={'intervention_id': itv['id']}), }, }) return JsonResponse({'type': 'FeatureCollection', 'features': features}) def intervention_add_occupation(request, intervention_id): intervention = get_object_or_404(Intervention, pk=intervention_id) members_qs = _get_available_occupation_members(request.user, intervention) form = InterventionOccupationForm(request.POST, members_queryset=members_qs, user=request.user) if form.is_valid(): occupation = form.save(commit=False) occupation.intervention = intervention occupation.created_by = request.user occupation.save() occupation.refresh_from_db() return JsonResponse({ 'success': True, 'occupation': _serialize_occupation(occupation), 'total_minutes': intervention.get_total_occupation_minutes(), }) return JsonResponse({'success': False, 'errors': form.errors}, status=400) def intervention_update_occupation(request, intervention_id, occupation_id): intervention = get_object_or_404(Intervention, pk=intervention_id) occupation = get_object_or_404(InterventionOccupation, pk=occupation_id, intervention=intervention) members_qs = _get_available_occupation_members(request.user, intervention) form = InterventionOccupationForm(request.POST, instance=occupation, members_queryset=members_qs, user=request.user) if form.is_valid(): occupation = form.save(commit=False) occupation.updated_by = request.user occupation.save() occupation.refresh_from_db() return JsonResponse({ 'success': True, 'occupation': _serialize_occupation(occupation), 'total_minutes': intervention.get_total_occupation_minutes(), }) return JsonResponse({'success': False, 'errors': form.errors}, status=400)