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

810 lines
32 KiB
Python

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=<id>&location_id=<id>
- ?equipment_ct=<id>&equipment_id=<id>
- ?lon=<x>&lat=<y>[&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)