loko/streetup/interventions/permissions.py

1554 lines
No EOL
66 KiB
Python

from django.shortcuts import get_object_or_404
from django.core.exceptions import PermissionDenied
from django.db.models import Q
from django.utils.translation import gettext as _
from functools import wraps
from datetime import timedelta
from django.utils import timezone
from django.shortcuts import get_object_or_404
from collections import defaultdict
from functools import reduce
from operator import or_
from common.models import (
UserConfig, UserContractAccess, UserThematics, Thematic,
UserThematicStatusPermission, UserContractStatusPermission,
)
from interventions.models import Intervention, InterventionTimeLine, STATUS_ORDERS, STATUS_CHOICES
from contracts.models import CompanyMember
# Ordered list of all status keys (used by get_allowed_statuses_for_user and admin UI)
ALL_STATUS_KEYS = list(STATUS_CHOICES.keys())
# Permissions for viewing intervention
VIEW_ROLE_STATUS_PERMISSIONS = {
'manager': ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'processed','to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled'],
'technician': ['in_preparation','to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished'],
'external_manager': ['in_preparation','to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'processed', 'to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled'],
'controller': ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'processed','to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled'],
'operator': ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'processed','to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled'],
'admin': ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'processed','to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled'],
'observer': ['to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'processed','to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled'],
'viewer': ['to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'processed','to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled'],
'editor': ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'processed','to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled'],
'top_manager': ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'processed','to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled'],
'inspector': ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished', 'processed','to_be_corrected', 'corrected', 'validated', 'invoiced', 'closed', 'canceled'],
}
ALLOWED_TRANSITIONS = {
'in_preparation': ['to_be_approved', 'to_be_planned', 'to_be_processed', 'canceled'],
'to_be_approved': ['to_be_planned', 'to_be_processed', 'canceled'],
'to_be_planned': ['to_be_processed', 'canceled'],
'to_be_processed': ['to_be_planned', 'assigned', 'in_progress', 'processed', 'closed', 'canceled'],
'assigned': ['to_be_processed', 'in_progress', 'canceled'],
'in_progress': ['on_pause', 'finished', 'canceled'],
'on_pause': ['to_be_planned', 'to_be_processed', 'in_progress', 'finished', 'canceled'],
'finished': ['to_be_planned', 'to_be_processed', 'on_pause', 'processed', 'canceled'],
'processed': ['to_be_corrected', 'validated', 'closed'],
'to_be_corrected': ['to_be_planned', 'corrected'],
'corrected': ['to_be_corrected','validated'],
'validated': ['invoiced', 'closed'],
'invoiced': ['closed'],
'closed': [],
'canceled': [],
}
# Droits par rôle = sous-ensemble du graph
ROLE_TRANSITIONS = {
"manager": [
("in_preparation","to_be_approved"),
("in_preparation","to_be_planned"),
("in_preparation","to_be_processed"),
("in_preparation","canceled"),
("to_be_approved","to_be_planned"),
("to_be_approved","to_be_processed"),
("to_be_approved","canceled"),
("to_be_planned", "to_be_processed"),
("to_be_planned", "canceled"),
('to_be_processed', 'canceled'),
("processed","to_be_corrected"),
("processed","validated"),
("corrected","validated"),
("invoiced","closed"),
],
"technician": [
("in_preparation","to_be_approved"),
("in_preparation","to_be_planned"),
("in_preparation","to_be_processed"),
("in_preparation","canceled"),
("to_be_processed","assigned"),
("assigned","in_progress"),
("assigned","to_be_processed"),
("to_be_processed","in_progress"),
("in_progress","on_pause"),
("in_progress","finished"),
("on_pause","in_progress"),
("on_pause","finished"),
],
"external_manager": [
("in_preparation","to_be_approved"),
("in_preparation","to_be_planned"),
("in_preparation","to_be_processed"),
("in_preparation","canceled"),
("to_be_approved","canceled"),
("to_be_planned", "to_be_processed"),
("to_be_planned","canceled"),
("to_be_processed","assigned"),
("to_be_processed","in_progress"),
("to_be_processed","to_be_planned"),
("to_be_processed","canceled"),
("assigned","in_progress"),
("assigned","to_be_processed"),
("assigned","canceled"),
("in_progress","on_pause"),
("in_progress","finished"),
("in_progress","canceled"),
("on_pause","in_progress"),
("on_pause","finished"),
("on_pause","to_be_planned"),
("on_pause","to_be_processed"),
("on_pause","canceled"),
("finished","processed"),
("finished","to_be_processed"),
("finished","on_pause"),
("finished","canceled"),
("to_be_corrected","corrected"),
],
"controller": [
("in_preparation","to_be_approved"),
("in_preparation","to_be_planned"),
("in_preparation","to_be_processed"),
("to_be_approved","to_be_planned"),
("to_be_approved","to_be_processed"),
("to_be_approved","canceled"),
("to_be_planned", "to_be_processed"),
("to_be_planned", "canceled"),
('to_be_processed', 'canceled'),
("processed","validated"),
("processed","to_be_corrected"),
("corrected","to_be_corrected"),
("corrected","validated"),
],
"operator": [
("in_preparation","to_be_approved"),
("in_preparation","to_be_planned"),
("in_preparation","to_be_processed"),
("in_preparation","canceled"),
("to_be_approved","to_be_planned"),
("to_be_approved","to_be_processed"),
("to_be_approved","canceled"),
("to_be_planned", "to_be_processed"),
("to_be_planned", "canceled"),
('to_be_processed', 'canceled'),
],
"admin": "ALL", # interprété comme toutes les transitions du graph
}
TRANSITION_LABEL = {
"to_be_approved": _("Envoyer pour approbation"),
"to_be_planned": _("Envoyer pour planification"),
"to_be_processed": _("Envoyer pour traitement"),
"assigned": _("Prendre en charge"),
"in_progress": _("Démarrer l'intervention"),
"on_pause": _("Mettre en pause"),
"finished": _("Terminer l'intervention"),
"processed": _("Envoyer pour validation"),
"to_be_corrected": _("Demander correction"),
"corrected": _("Corriger"),
"validated": _("Valider"),
"invoiced": _("Facturer"),
"closed": _("Finaliser"),
"canceled": _("Annuler"),
}
def get_allowed_statuses_for_user(user):
"""
Retourne la liste des statuts que l'utilisateur peut voir,
basée sur les nouvelles permissions par statut (thématique + contrat).
"""
try:
user_config = UserConfig.objects.get(user=user)
except UserConfig.DoesNotExist:
return []
if user_config.roles.filter(name='admin').exists():
return ALL_STATUS_KEYS
statuses = set()
statuses.update(
UserThematicStatusPermission.objects.filter(
user_thematic__user_config=user_config,
can_view=True,
).values_list('status', flat=True)
)
statuses.update(
UserContractStatusPermission.objects.filter(
user_contract__user_config=user_config,
can_view=True,
).values_list('status', flat=True)
)
return list(statuses)
def can_view_intervention(user, intervention):
"""
Vérifie si l'utilisateur peut voir l'intervention.
Priorité : contrat > thématique.
Si une ligne UserContractStatusPermission existe pour ce contrat et ce statut,
c'est cette valeur qui fait foi (même si False) — pas de fallback thématique.
Si aucune ligne n'existe pour ce statut dans le contrat, on tombe sur la thématique.
En dernier recours : prestataire assigné (CompanyMember).
"""
try:
user_config = UserConfig.objects.get(user=user)
except UserConfig.DoesNotExist:
return False
if user_config.roles.filter(name='admin').exists():
return True
if intervention.created_by_id == user.id:
return True
# -- Restriction de visibilité quotidienne et par équipe --
company_member = CompanyMember.objects.filter(user=user).prefetch_related('teams').first()
if company_member:
user_roles = set(user_config.roles.values_list('name', flat=True))
privileged_roles = {'admin', 'top_manager', 'manager', 'controller', 'operator', 'external_manager'}
if not (user_roles & privileged_roles):
# 1. Restriction de visibilité quotidienne par équipe
restricted_teams = company_member.teams.filter(restrict_today_interventions_visibility=True)
if restricted_teams.exists():
local_now = timezone.localtime(timezone.now())
today = local_now.date()
# Vérifier la restriction de date (doit être aujourd'hui)
itv_date = (
intervention.begin_time or
intervention.planned_begin_time or
intervention.expected_begin_time or
intervention.creation_time
)
if itv_date:
if timezone.is_aware(itv_date):
itv_date = timezone.localtime(itv_date)
itv_date_only = itv_date.date()
else:
itv_date_only = None
if itv_date_only != today:
return False
# Vérifier la restriction d'heure
start_hours = [t.visibility_start_hour for t in restricted_teams if t.visibility_start_hour is not None]
if start_hours:
min_start_hour = min(start_hours)
if local_now.time() < min_start_hour:
return False
# 2. Restriction aux interventions assignées
assigned_only_teams = company_member.teams.filter(restrict_to_assigned_interventions=True)
if assigned_only_teams.exists():
unrestricted_teams = company_member.teams.filter(restrict_to_assigned_interventions=False)
unrestricted_companies = set(unrestricted_teams.values_list('company_id', flat=True))
is_assigned_to_user = (intervention.assigned_member == company_member)
is_assigned_to_user_team = (intervention.assigned_team in company_member.teams.all())
is_unrestricted_company = (intervention.assigned_provider_id in unrestricted_companies)
if not (is_assigned_to_user or is_assigned_to_user_team or is_unrestricted_company):
return False
contract = intervention.contract
thematic = intervention.thematic
status = intervention.status
# -- Priorité contrat --
if contract:
uca = UserContractAccess.objects.filter(
user_config=user_config,
contract=contract,
).first()
if uca:
perm = UserContractStatusPermission.objects.filter(
user_contract=uca,
status=status,
).first()
if perm is not None:
return perm.can_view # ligne définie → valeur finale, pas de fallback thématique
# Aucune ligne pour ce statut → fallback sur les droits par rôle (via l'accès contrat)
uca_roles = set(user_config.roles.values_list('name', flat=True))
for role in uca_roles:
if role in VIEW_ROLE_STATUS_PERMISSIONS and status in VIEW_ROLE_STATUS_PERMISSIONS[role]:
return True
# -- Accès thématique --
if thematic:
ut = UserThematics.objects.filter(
user_config=user_config,
thematic=thematic,
).first()
if ut:
return UserThematicStatusPermission.objects.filter(
user_thematic=ut,
status=status,
can_view=True,
).exists()
# -- Fallback prestataire assigné --
assigned_provider = intervention.assigned_provider
if assigned_provider:
company_member = CompanyMember.objects.filter(user=user).prefetch_related('teams__company').first()
if company_member:
return company_member.teams.filter(company=assigned_provider).exists()
return False
def can_view_intervention_summary(user, intervention):
"""
Vérifie si l'utilisateur peut voir le résumé de l'intervention (vue allégée).
Basé sur les permissions thématique uniquement (accès résumé = droit de visibilité
thématique sur le statut courant).
"""
try:
user_config = UserConfig.objects.get(user=user)
except UserConfig.DoesNotExist:
return False
if user_config.roles.filter(name='admin').exists():
return True
if not intervention.thematic:
return False
ut = UserThematics.objects.filter(
user_config=user_config,
thematic=intervention.thematic,
).first()
if not ut:
return False
return UserThematicStatusPermission.objects.filter(
user_thematic=ut,
status=intervention.status,
can_view=True,
).exists()
def intervention_permission_required(view_func):
@wraps(view_func)
def _wrapped_view(request, intervention_id, *args, **kwargs):
intervention = get_object_or_404(Intervention, pk=intervention_id)
if not can_view_intervention(request.user, intervention):
raise PermissionDenied(_("Vous n'avez pas la permission de voir cette intervention."))
return view_func(request, intervention_id, *args, **kwargs)
return _wrapped_view
def can_control_intervention(user, intervention):
"""
Vérifie si l'utilisateur peut contrôler (valider/corriger) l'intervention.
Réservé aux utilisateurs internes ayant le droit d'édition sur le statut courant.
"""
if intervention.status not in ('processed', 'corrected'):
return False
try:
user_config = UserConfig.objects.get(user=user)
except UserConfig.DoesNotExist:
return False
if not user_config.is_intern:
return False
return can_edit_intervention(user, intervention, user_config=user_config)
def get_allowed_update_fields(user, intervention):
"""
Get the fields that the user can update based on their roles and the intervention's status.
"""
allowed_fields = set()
try:
user_config = UserConfig.objects.get(user=user)
except UserConfig.DoesNotExist:
return []
if not can_edit_intervention(user, intervention, user_config=user_config):
return []
user_roles = user_config.roles.all().values_list('name', flat=True)
# Si l'utilisateur n'a pas de rôle 'admin', 'operator', 'manager', 'controller', 'external_manager', 'technician' ou 'inspector', il ne peut rien mettre à jour
if not any(role in ['admin', 'operator', 'manager', 'controller', 'external_manager', 'technician', 'inspector'] for role in user_roles):
return []
# Notes et Documents
allowed_fields.update(['notes', 'documents'])
# Opérations (statuts, mesures, commentaires) - uniquement pour technician, external_manager et inspector
if intervention.status_order < STATUS_ORDERS.get('validated') and any(role in user_roles for role in ['technician', 'external_manager', 'admin', 'inspector']):
allowed_fields.add('operations')
# Prestataire, Contrat et Type de maintenance (uniquement en phase initiale)
# Exception : si la thématique autorise la planification sans contrat, ces champs
# restent également modifiables au statut 'to_be_planned'.
_thematic_allows_planning_without_contract = getattr(intervention.thematic, 'allow_planning_without_contract', False)
_initial_statuses = ('in_preparation', 'to_be_approved')
if _thematic_allows_planning_without_contract:
_initial_statuses = ('in_preparation', 'to_be_approved', 'to_be_planned')
if intervention.status in _initial_statuses and (user_config.is_intern or (intervention.contract is None and intervention.created_by_id == user.id)):
allowed_fields.update(['assigned_provider', 'contract', 'maintain_type'])
# Localisation : modifiable par les internes sur toutes les interventions non validées
if intervention.status_order < STATUS_ORDERS.get('validated') and user_config.is_intern:
allowed_fields.update(['location', 'address', 'lat', 'lon', 'intersections'])
# Type d'intervention, Osiris, PMO, Assets
if intervention.status_order < STATUS_ORDERS.get('validated'):
allowed_fields.update(['assets'])
# Type d'intervention, Osiris, PMO, Assets
if intervention.status_order < STATUS_ORDERS.get('validated') and any(role in user_roles for role in ['admin', 'manager', 'controller', 'external_manager', 'inspector']):
allowed_fields.update(['type', 'osiris', 'pmo_number'])
if intervention.status_order < STATUS_ORDERS.get('validated'):
allowed_fields.add('category')
# Source, Référence, Détail, FMS, Type d'origine et Symptôme
if intervention.status_order < STATUS_ORDERS.get('to_be_processed') and user_config.is_intern:
allowed_fields.update(['source_category', 'source_ref', 'source_detail', 'origin_type', 'symptom'])
from common.hooks import hooks
allowed_fields.update(hooks.get_custom_allowed_fields())
# Priorité : modifiable par les internes tant que le statut n'est pas 'démarré' (in_progress)
if intervention.status_order < STATUS_ORDERS.get('in_progress') and user_config.is_intern:
allowed_fields.add('priority')
# Dossier de référence interne
if intervention.status_order < STATUS_ORDERS.get('validated') and user_config.is_intern and any(role in user_roles for role in ['admin', 'manager', 'controller']):
allowed_fields.update(['linked_folder'])
# Projet
if intervention.status_order < STATUS_ORDERS.get('validated') and user_config.is_intern and any(role in user_roles for role in ['admin', 'manager']):
allowed_fields.update(['project'])
# Réalisation et Planification (pour tous)
if intervention.status_order < STATUS_ORDERS.get('validated') and any(role in user_roles for role in ['admin', 'manager', 'controller', 'external_manager', 'technician', 'inspector']):
allowed_fields.update(['realization', 'planification'])
# Assignation (seulement pour admin, external_manager, technician, inspector - PAS manager)
if intervention.status_order < STATUS_ORDERS.get('validated') and any(role in user_roles for role in ['admin', 'external_manager', 'technician', 'inspector']):
allowed_fields.add('member_assignment')
# Rapports de suivi
if intervention.status_order < STATUS_ORDERS.get('validated') and user_config.is_intern and any(role in user_roles for role in ['admin', 'manager', 'controller']):
allowed_fields.update(['follow_up_reports'])
# Bon de commande et Titre
if intervention.status_order < STATUS_ORDERS.get('validated') and any(role in user_roles for role in ['admin', 'manager', 'controller', 'external_manager', 'inspector']):
allowed_fields.update(['order', 'title'])
# Postes
if intervention.status_order < STATUS_ORDERS.get('validated') and any(role in user_roles for role in ['admin', 'external_manager']):
allowed_fields.update(['posts'])
# Traitement via une autre intervention
if intervention.status_order < STATUS_ORDERS.get('validated') and any(role in user_roles for role in ['admin', 'manager', 'controller', 'external_manager']):
allowed_fields.add('processed_via_other_intervention')
return list(allowed_fields)
def get_visible_maintain_types_for_user(user):
"""
Retourne la liste des codes de types de maintenance visibles pour l'utilisateur.
Si l'utilisateur n'est pas membre d'une équipe ou si le membre n'a pas de restrictions,
retourne None (tous les types sont visibles).
"""
company_member = CompanyMember.objects.prefetch_related('teams', 'visible_maintain_types').filter(user=user).first()
if not company_member:
return None # Pas de restriction si pas membre d'une équipe
return company_member.get_visible_maintain_type_codes()
def apply_maintain_type_filter(queryset, user):
"""
Applique le filtre des types de maintenance visibles au queryset.
Si l'utilisateur n'a pas de restrictions, le queryset est retourné tel quel.
Les interventions assignées à l'utilisateur restent toujours visibles.
"""
visible_types = get_visible_maintain_types_for_user(user)
if visible_types is None:
return queryset # Pas de restriction
# Inclure les interventions du type visible OU celles assignées à l'utilisateur
return queryset.filter(
Q(maintain_type__in=visible_types) | Q(assigned_member__user=user)
)
def get_accessible_interventions_for_technician(user):
"""
Retourne un queryset des interventions que l'utilisateur a le droit de voir :
- Celles qui lui sont directement assignées
- Ou celles qui sont non assignées mais associées à la société à laquelle il appartient
- Filtré par les types de maintenance visibles pour l'utilisateur (sauf interventions assignées)
"""
base_qs = Intervention.objects.all()
company_member = CompanyMember.objects.prefetch_related('teams__company', 'visible_maintain_types').filter(user=user).first()
if company_member:
user_companies = company_member.teams.values_list('company_id', flat=True).distinct()
qs = base_qs.filter(
Q(assigned_member__user=user) |
Q(assigned_team__in=company_member.teams.all(), status__in=['to_be_processed', 'assigned']) |
Q(assigned_member__isnull=True, assigned_team__isnull=True, assigned_provider_id__in=user_companies, status__in=['to_be_processed', 'assigned'])
)
# Appliquer le filtre des types de maintenance visibles (sauf pour les interventions assignées)
visible_types = company_member.get_visible_maintain_type_codes()
if visible_types is not None:
qs = qs.filter(
Q(maintain_type__in=visible_types) | Q(assigned_member__user=user)
)
# -- Restriction de visibilité quotidienne par équipe --
user_config = UserConfig.objects.filter(user=user).first()
user_roles = set(user_config.roles.values_list('name', flat=True)) if user_config else set()
privileged_roles = {'admin', 'top_manager', 'manager', 'controller', 'operator', 'external_manager'}
if not (user_roles & privileged_roles):
restricted_teams = company_member.teams.filter(restrict_today_interventions_visibility=True)
if restricted_teams.exists():
local_now = timezone.localtime(timezone.now())
today = local_now.date()
# Vérifier la restriction d'heure
start_hours = [t.visibility_start_hour for t in restricted_teams if t.visibility_start_hour is not None]
if start_hours:
min_start_hour = min(start_hours)
if local_now.time() < min_start_hour:
return base_qs.none()
# Filtrer sur les interventions du jour uniquement
from django.db.models.functions import Coalesce, Cast
from django.db.models import DateTimeField, DateField
effective_begin_expr = Coalesce(
'begin_time',
'planned_begin_time',
'expected_begin_time',
'creation_time',
output_field=DateTimeField()
)
qs = qs.annotate(effective_begin_date=Cast(effective_begin_expr, output_field=DateField()))
qs = qs.filter(effective_begin_date=today)
return qs
else:
qs = base_qs.filter(assigned_member__user=user)
# -- Restriction de visibilité quotidienne par équipe --
user_config = UserConfig.objects.filter(user=user).first()
user_roles = set(user_config.roles.values_list('name', flat=True)) if user_config else set()
privileged_roles = {'admin', 'top_manager', 'manager', 'controller', 'operator', 'external_manager'}
if not (user_roles & privileged_roles):
company_member_basic = CompanyMember.objects.filter(user=user).first()
if company_member_basic:
restricted_teams = company_member_basic.teams.filter(restrict_today_interventions_visibility=True)
if restricted_teams.exists():
local_now = timezone.localtime(timezone.now())
today = local_now.date()
# Vérifier la restriction d'heure
start_hours = [t.visibility_start_hour for t in restricted_teams if t.visibility_start_hour is not None]
if start_hours:
min_start_hour = min(start_hours)
if local_now.time() < min_start_hour:
return base_qs.none()
# Filtrer sur les interventions du jour uniquement
from django.db.models.functions import Coalesce, Cast
from django.db.models import DateTimeField, DateField
effective_begin_expr = Coalesce(
'begin_time',
'planned_begin_time',
'expected_begin_time',
'creation_time',
output_field=DateTimeField()
)
qs = qs.annotate(effective_begin_date=Cast(effective_begin_expr, output_field=DateField()))
qs = qs.filter(effective_begin_date=today)
return qs
def filter_viewable_interventions_for_user(user, limit_to_default_thematic=False):
"""
Renvoie un queryset des interventions visibles par l'utilisateur
selon les règles de can_view_intervention() avec le nouveau système de permissions.
Logique (priorité contrat > thématique) :
- Si une ligne UserContractStatusPermission existe pour (user_config, contrat, statut)
→ la valeur can_view de cette ligne fait foi — pas de fallback thématique.
- Si aucune ligne n'existe pour ce statut dans le contrat
→ fallback sur UserThematicStatusPermission(status).can_view
- Fallback : prestataire assigné (CompanyMember)
"""
try:
user_config = UserConfig.objects.select_related('user').prefetch_related('roles').get(user=user)
except UserConfig.DoesNotExist:
return Intervention.objects.none()
# Admin ou top_manager → accès global
if user_config.roles.filter(name__in=['admin', 'top_manager']).exists():
qs = Intervention.objects.all()
if limit_to_default_thematic and user_config.default_thematic:
qs = qs.filter(thematic=user_config.default_thematic)
return qs
# -- Précharger les permissions depuis les petites tables de permissions --
# Remplace les sous-requêtes corrélées (Exists + OuterRef) par des ensembles
# calculés une seule fois en Python, traduits en conditions Q non-corrélées.
# 1. Permissions contrat-statut pour ce user_config
cp_rows = list(
UserContractStatusPermission.objects.filter(
user_contract__user_config=user_config,
).values_list('user_contract__contract_id', 'status', 'can_view')
)
explicit_cs_allowed = set() # (contract_id, status) avec can_view=True
denied_contracts_by_status = defaultdict(set) # status → contract_ids avec can_view=False
for c_id, st, cv in cp_rows:
if cv:
explicit_cs_allowed.add((c_id, st))
else:
denied_contracts_by_status[st].add(c_id)
# 2. Permissions thématique-statut (can_view=True uniquement)
thematic_ids_by_status = defaultdict(set) # status → set de thematic_id autorisés
for t_id, st in UserThematicStatusPermission.objects.filter(
user_thematic__user_config=user_config,
can_view=True,
).values_list('user_thematic__thematic_id', 'status'):
thematic_ids_by_status[st].add(t_id)
# -- Construire les conditions Q sans sous-requêtes corrélées --
# Règle (priorité contrat > thématique) :
# (a) Ligne contrat can_view=True → visible
# (b) Ligne contrat can_view=False → refusé, pas de fallback
# (c)+(d) Si limit_interventions_to_contracts=False : fallback thématique
# pour toute intervention sans refus explicite de contrat.
conditions = []
# Cas (a) : contrat + can_view=True explicite
for c_id, st in explicit_cs_allowed:
conditions.append(Q(contract_id=c_id, status=st))
# Cas (c)+(d) : fallback thématique — uniquement si l'utilisateur n'est PAS
# limité aux contrats. Quand limit_interventions_to_contracts=True (défaut),
# seules les permissions contrat explicites (cas a) s'appliquent.
if not user_config.limit_interventions_to_contracts:
for st, t_ids in thematic_ids_by_status.items():
if not t_ids:
continue
base_q = Q(status=st, thematic_id__in=sorted(t_ids))
denied_for_st = denied_contracts_by_status.get(st, set())
if denied_for_st:
# Exclure les interventions dont le contrat est explicitement refusé.
# contract=NULL doit être géré séparément : NULL NOT IN (...) = UNKNOWN en SQL.
base_q &= Q(contract__isnull=True) | ~Q(contract_id__in=sorted(denied_for_st))
conditions.append(base_q)
# Fallback prestataire assigné (technicians via CompanyMember)
company_member = (
CompanyMember.objects
.prefetch_related('teams__company', 'visible_maintain_types')
.filter(user=user)
.first()
)
if company_member:
user_roles = set(user_config.roles.values_list('name', flat=True)) if user_config else set()
privileged_roles = {'admin', 'top_manager', 'manager', 'controller', 'operator', 'external_manager'}
restricted_teams = company_member.teams.filter(restrict_to_assigned_interventions=True)
if restricted_teams.exists() and not (user_roles & privileged_roles):
unrestricted_teams = company_member.teams.filter(restrict_to_assigned_interventions=False)
unrestricted_companies = list(unrestricted_teams.values_list('company_id', flat=True).distinct())
company_q = Q(assigned_member=company_member) | Q(assigned_team__in=list(company_member.teams.all()))
if unrestricted_companies:
company_q |= Q(assigned_member__isnull=True, assigned_team__isnull=True, assigned_provider_id__in=unrestricted_companies)
else:
user_companies = company_member.teams.values_list('company_id', flat=True).distinct()
company_q = Q(assigned_provider_id__in=user_companies)
if user_config.limit_interventions_to_contracts:
# Limiter aux contrats configurés pour cet utilisateur afin d'exclure
# les interventions d'autres contrats de la même compagnie.
allowed_contract_ids = set(
UserContractAccess.objects.filter(
user_config=user_config,
can_view_interventions=True,
).values_list('contract_id', flat=True)
)
company_q &= Q(contract__isnull=True) | Q(contract_id__in=sorted(allowed_contract_ids))
conditions.append(company_q)
conditions.append(Q(created_by=user))
if not conditions:
return Intervention.objects.none()
q = reduce(or_, conditions)
qs = Intervention.objects.filter(q)
if limit_to_default_thematic and user_config.default_thematic:
qs = qs.filter(thematic=user_config.default_thematic)
if company_member:
qs = apply_maintain_type_filter(qs, user)
# -- Restriction de visibilité quotidienne par équipe --
user_roles = set(user_config.roles.values_list('name', flat=True)) if user_config else set()
privileged_roles = {'admin', 'top_manager', 'manager', 'controller', 'operator', 'external_manager'}
if not (user_roles & privileged_roles):
# 1. Restriction aux interventions assignées par équipe
assigned_only_teams = company_member.teams.filter(restrict_to_assigned_interventions=True)
if assigned_only_teams.exists():
unrestricted_teams = company_member.teams.filter(restrict_to_assigned_interventions=False)
unrestricted_companies = list(unrestricted_teams.values_list('company_id', flat=True).distinct())
assign_filter = Q(assigned_member=company_member) | Q(assigned_team__in=list(company_member.teams.all()))
if unrestricted_companies:
assign_filter |= Q(assigned_provider_id__in=unrestricted_companies)
qs = qs.filter(assign_filter)
# 2. Restriction de visibilité quotidienne par équipe
restricted_teams = company_member.teams.filter(restrict_today_interventions_visibility=True)
if restricted_teams.exists():
local_now = timezone.localtime(timezone.now())
today = local_now.date()
# Vérifier la restriction d'heure
start_hours = [t.visibility_start_hour for t in restricted_teams if t.visibility_start_hour is not None]
if start_hours:
min_start_hour = min(start_hours)
if local_now.time() < min_start_hour:
return Intervention.objects.none()
# Filtrer sur les interventions du jour uniquement
from django.db.models.functions import Coalesce, Cast
from django.db.models import DateTimeField, DateField
effective_begin_expr = Coalesce(
'begin_time',
'planned_begin_time',
'expected_begin_time',
'creation_time',
output_field=DateTimeField()
)
qs = qs.annotate(effective_begin_date=Cast(effective_begin_expr, output_field=DateField()))
qs = qs.filter(effective_begin_date=today)
return qs
def can_add_intervention(user, thematic, contract=None):
"""
Vérifie si l'utilisateur peut créer une nouvelle intervention.
La création correspond au statut 'in_preparation' : on vérifie le droit
can_edit sur ce statut dans les permissions thématique ou contrat.
Pour les prestataires (external_manager), le flag can_create_interventions
du contrat est également vérifié (rétrocompatibilité).
"""
try:
user_config = UserConfig.objects.get(user=user)
except UserConfig.DoesNotExist:
return False
if user_config.roles.filter(name__in=['admin', 'top_manager']).exists():
return True
# Vérification via permissions thématique
if thematic:
ut = UserThematics.objects.filter(
user_config=user_config,
thematic=thematic,
).first()
if ut:
# can_edit_interventions=True est suffisant pour créer une intervention
# (les opérateurs n'ont pas forcément de UserThematicStatusPermission pour in_preparation)
if ut.can_edit_interventions:
return True
if UserThematicStatusPermission.objects.filter(
user_thematic=ut,
status='in_preparation',
can_edit=True,
).exists():
return True
# Vérification via permissions contrat
if contract:
uca = UserContractAccess.objects.filter(
user_config=user_config,
contract=contract,
).first()
if uca:
if UserContractStatusPermission.objects.filter(
user_contract=uca,
status='in_preparation',
can_edit=True,
).exists():
return True
# Rétrocompatibilité : flag can_create_interventions
if uca.can_create_interventions:
return True
else:
# Pas de contrat spécifié : vérifier si au moins un contrat autorise la création
uca_qs = UserContractAccess.objects.filter(user_config=user_config)
if uca_qs.filter(can_create_interventions=True).exists():
return True
if UserContractStatusPermission.objects.filter(
user_contract__user_config=user_config,
status='in_preparation',
can_edit=True,
).exists():
return True
return False
def get_contracts_for_intervention_creation(user):
"""
Retourne les contrats pour lesquels l'utilisateur peut créer des interventions.
Utilisé pour les external_managers.
"""
try:
user_config = UserConfig.objects.get(user=user)
except UserConfig.DoesNotExist:
return []
if not user_config.roles.filter(name='external_manager').exists():
return []
contract_ids = UserContractAccess.objects.filter(
user_config=user_config,
can_create_interventions=True
).values_list('contract_id', flat=True)
from contracts.models import Contract
return Contract.objects.filter(id__in=contract_ids, is_active=True)
def intervention_add_permission_required(view_func):
@wraps(view_func)
def _wrapped_view(request, thematic_code=None, *args, **kwargs):
thematic = None
if thematic_code:
thematic = get_object_or_404(Thematic, code=thematic_code)
if not can_add_intervention(request.user, thematic):
raise PermissionDenied(_("Vous n'avez pas la permission d'ajouter une intervention pour cette thématique."))
return view_func(request, thematic_code, *args, **kwargs)
return _wrapped_view
def intervention_add_preventive_permission_required(view_func):
"""
Décorateur pour les vues de création d'intervention préventive.
Seuls les admin, manager, controller, top_manager et les external_managers
avec droit de création d'interventions sur au moins un contrat peuvent créer des
interventions préventives.
"""
@wraps(view_func)
def _wrapped_view(request, thematic_code=None, *args, **kwargs):
thematic = None
if thematic_code:
thematic = get_object_or_404(Thematic, code=thematic_code)
# Vérifie d'abord si l'utilisateur peut ajouter des interventions
if not can_add_intervention(request.user, thematic):
raise PermissionDenied(_("Vous n'avez pas la permission d'ajouter une intervention pour cette thématique."))
# Seuls admin, manager, controller, top_manager, et external_manager avec droits
try:
user_config = UserConfig.objects.get(user=request.user)
user_roles = set(user_config.roles.values_list('name', flat=True))
is_allowed = bool(user_roles & {'admin', 'manager', 'controller', 'top_manager'})
if not is_allowed and 'external_manager' in user_roles:
is_allowed = is_external_manager_with_contract_creation_rights(request.user)
if not is_allowed:
raise PermissionDenied(_("Vous n'avez pas la permission de créer des interventions préventives."))
except UserConfig.DoesNotExist:
raise PermissionDenied(_("Vous n'avez pas la permission de créer des interventions préventives."))
return view_func(request, thematic_code, *args, **kwargs)
return _wrapped_view
def can_add_repair_intervention(user, intervention):
"""
Vérifie si l'utilisateur peut ajouter une intervention de réparation liée à l'intervention donnée.
L'utilisateur doit avoir les droits d'édition sur l'intervention source ET pouvoir créer
une nouvelle intervention (can_edit sur in_preparation).
"""
try:
user_config = UserConfig.objects.get(user=user)
except UserConfig.DoesNotExist:
return False
if user_config.roles.filter(name='admin').exists():
return True
# Nécessite le droit d'édition sur l'intervention existante
if not can_edit_intervention(user, intervention, user_config=user_config):
return False
# Et le droit de créer sur la thématique/contrat de cette intervention
return can_add_intervention(user, intervention.thematic, contract=intervention.contract)
def can_edit_intervention(user, intervention, user_config=None):
"""
Vérifie si l'utilisateur peut modifier l'intervention.
Priorité : contrat > thématique.
Si une ligne UserContractStatusPermission existe pour ce contrat et ce statut,
c'est cette valeur qui fait foi (même si False) — pas de fallback thématique.
Si aucune ligne n'existe pour ce statut, fallback sur la thématique.
Pour les techniciens non-privilégiés, la contrainte d'assignation reste en place.
"""
if user_config is None:
try:
user_config = UserConfig.objects.get(user=user)
except UserConfig.DoesNotExist:
return False
if user_config.roles.filter(name__in=['admin', 'top_manager']).exists():
return True
# Pour les techniciens sans rôle privilégié, vérifier l'assignation
roles = set(user_config.roles.values_list('name', flat=True)) if user_config else set()
privileged_roles = {'admin', 'manager', 'controller', 'external_manager', 'operator'}
is_inspector_on_inspection = (intervention.type == 'inspection' and 'inspector' in roles)
if 'inspector' in roles:
# Si l'intervention n'est pas de type 'inspection', le rôle inspector ne permet pas de modifier.
# Mais si l'utilisateur a d'autres rôles d'édition valides, on poursuit.
other_edit_roles = {'admin', 'top_manager', 'manager', 'controller', 'external_manager', 'operator', 'technician'}
if intervention.type != 'inspection' and not (roles & other_edit_roles):
return False
if 'technician' in roles and not (roles & privileged_roles) and not is_inspector_on_inspection:
assigned_member = getattr(intervention, 'assigned_member', None)
assigned_user_id = getattr(assigned_member, 'user_id', None) if assigned_member else None
if assigned_user_id is not None and assigned_user_id != user.id:
return False
contract = intervention.contract
thematic = intervention.thematic
status = intervention.status
# -- Priorité contrat --
if contract:
uca = UserContractAccess.objects.filter(
user_config=user_config,
contract=contract,
).first()
if uca:
perm = UserContractStatusPermission.objects.filter(
user_contract=uca,
status=status,
).first()
# Droit d'édition explicite sur le statut courant → valeur finale
if perm is not None and perm.can_edit:
return True
# Pas de droit d'édition (ligne absente ou can_edit=False) :
# vérifier si l'utilisateur peut changer le statut vers une cible autorisée
# (ex : 'to_be_processed' → 'assigned' = prise en charge)
target_statuses = set(ALLOWED_TRANSITIONS.get(status, []))
if target_statuses and UserContractStatusPermission.objects.filter(
user_contract=uca,
status__in=target_statuses,
can_change_status_to=True,
).exists():
return True
# Sinon, droits insuffisants (droits explicites obligatoires)
return False
# -- Accès thématique --
if thematic:
ut = UserThematics.objects.filter(
user_config=user_config,
thematic=thematic,
).first()
if ut:
return UserThematicStatusPermission.objects.filter(
user_thematic=ut,
status=status,
can_edit=True,
).exists()
return False
def intervention_edit_permission_required(view_func):
@wraps(view_func)
def _wrapped_view(request, intervention_id, *args, **kwargs):
intervention = get_object_or_404(Intervention, pk=intervention_id)
if not can_edit_intervention(request.user, intervention):
raise PermissionDenied(_("Vous n'avez pas la permission de modifier cette intervention."))
return view_func(request, intervention_id, *args, **kwargs)
return _wrapped_view
def can_manage_occupations(user, intervention, user_config=None):
if not user.is_authenticated:
return False
if user_config is None:
try:
user_config = (UserConfig.objects
.select_related('user')
.prefetch_related('roles')
.get(user=user))
except UserConfig.DoesNotExist:
user_config = None
roles = set(user_config.roles.values_list('name', flat=True)) if user_config else set()
if roles.intersection({'admin', 'external_manager'}):
return True
if 'technician' in roles:
company_member = (CompanyMember.objects
.prefetch_related('teams__company')
.filter(user=user)
.first())
if not company_member:
return False
member_team_ids = company_member.teams.values_list('id', flat=True)
if intervention.assigned_member_id == getattr(company_member, 'id', None):
return True
# Autoriser tous les techniciens de l'équipe assignée, même si un membre
# spécifique est aussi assigné (ex : intervention démarrée par un collègue)
if (intervention.assigned_team_id is not None
and intervention.assigned_team_id in member_team_ids):
return True
# Autoriser tous les techniciens qui ont démarré cette intervention, même si elle est ensuite réassignée à une autre équipe ou un autre membre
if InterventionTimeLine.objects.filter(
intervention=intervention,
event_type='status_change',
to_status='in_progress',
event_user=user,
).exists():
return True
return False
return False
def intervention_manage_occupations_required(view_func):
@wraps(view_func)
def _wrapped_view(request, intervention_id, *args, **kwargs):
intervention = get_object_or_404(Intervention, pk=intervention_id)
if not can_manage_occupations(request.user, intervention):
raise PermissionDenied(_("Vous n'avez pas la permission de gérer les occupations pour cette intervention."))
return view_func(request, intervention_id, *args, **kwargs)
return _wrapped_view
def user_allowed_transitions(user, current_status, intervention=None):
"""
Retourne la liste des statuts vers lesquels l'utilisateur peut transitionner
depuis current_status.
Si une intervention est fournie, utilise les permissions spécifiques au contrat
ou à la thématique de cette intervention (priorité contrat > thématique).
Sans intervention, retourne l'union de toutes les permissions de l'utilisateur.
Le résultat est toujours intersecté avec ALLOWED_TRANSITIONS[current_status]
pour garantir la validité du graphe global de transitions.
"""
graph_targets = set(ALLOWED_TRANSITIONS.get(current_status, []))
try:
user_config = UserConfig.objects.get(user=user)
except UserConfig.DoesNotExist:
return []
if user_config.roles.filter(name='admin').exists():
return sorted(s for s in ALL_STATUS_KEYS if s != current_status)
if not graph_targets:
return []
def _filter_transitions(transitions):
if STATUS_ORDERS.get(current_status, 0) >= STATUS_ORDERS.get('assigned', 15):
return [s for s in transitions if s != 'canceled']
return transitions
# -- Cas avec intervention : priorité contrat > thématique --
if intervention is not None:
if intervention.contract_id:
uca = UserContractAccess.objects.filter(
user_config=user_config,
contract_id=intervention.contract_id,
).first()
if uca:
# Récupérer toutes les lignes contrat pour les statuts cibles
contract_rows = {
row.status: row.can_change_status_to
for row in UserContractStatusPermission.objects.filter(
user_contract=uca,
status__in=graph_targets,
)
}
contract_permitted = {s for s, v in contract_rows.items() if v}
# Statuts cibles sans ligne contrat → fallback thématique puis rôle
uncovered = graph_targets - set(contract_rows.keys())
thematic_extra = set()
if uncovered and intervention.thematic_id:
ut = UserThematics.objects.filter(
user_config=user_config,
thematic_id=intervention.thematic_id,
).first()
if ut:
thematic_extra = set(
UserThematicStatusPermission.objects.filter(
user_thematic=ut,
status__in=uncovered,
can_change_status_to=True,
).values_list('status', flat=True)
)
# Réduire uncovered aux statuts sans ligne thématique explicite
thematic_covered = set(
UserThematicStatusPermission.objects.filter(
user_thematic=ut,
status__in=uncovered,
).values_list('status', flat=True)
)
uncovered = uncovered - thematic_covered
# Pour les statuts encore non couverts : fallback sur ROLE_TRANSITIONS
if uncovered:
user_roles_names = set(user_config.roles.values_list('name', flat=True))
for role in user_roles_names:
role_extra = {dst for src, dst in ROLE_TRANSITIONS.get(role, []) if src == current_status}
thematic_extra |= role_extra & uncovered
return sorted(_filter_transitions(graph_targets & (contract_permitted | thematic_extra)))
# Pas d'accès contrat → fallback thématique
if intervention.thematic_id:
ut = UserThematics.objects.filter(
user_config=user_config,
thematic_id=intervention.thematic_id,
).first()
if ut:
permitted = set(
UserThematicStatusPermission.objects.filter(
user_thematic=ut,
can_change_status_to=True,
).values_list('status', flat=True)
)
return sorted(_filter_transitions(graph_targets & permitted))
return []
# -- Sans intervention : union de toutes les permissions de l'utilisateur --
permitted = set()
permitted.update(
UserThematicStatusPermission.objects.filter(
user_thematic__user_config=user_config,
can_change_status_to=True,
).values_list('status', flat=True)
)
permitted.update(
UserContractStatusPermission.objects.filter(
user_contract__user_config=user_config,
can_change_status_to=True,
).values_list('status', flat=True)
)
return sorted(_filter_transitions(graph_targets & permitted))
def can_approve_intervention(user, intervention):
"""
Vérifie si l'utilisateur peut faire passer une intervention du statut 'to_be_approved'
vers un autre statut, dans le cas où un contrat est renseigné.
Avec le nouveau système : l'utilisateur doit avoir can_change_status_to=True
pour au moins un des statuts cibles valides depuis 'to_be_approved'
sur ses permissions contrat. Rétrocompatibilité : can_approve=True est aussi accepté.
"""
if not intervention.contract_id:
return True # Pas de contrat → pas de restriction sur l'approbation
try:
user_config = UserConfig.objects.get(user=user)
except UserConfig.DoesNotExist:
return False
if user_config.roles.filter(name='admin').exists():
return True
uca = UserContractAccess.objects.filter(
user_config=user_config,
contract_id=intervention.contract_id,
).first()
if not uca:
return False
# Rétrocompatibilité : flag can_approve
if uca.can_approve:
return True
# Nouveau système : peut transitionner vers un statut cible depuis to_be_approved
approval_targets = set(ALLOWED_TRANSITIONS.get('to_be_approved', []))
return UserContractStatusPermission.objects.filter(
user_contract=uca,
status__in=approval_targets,
can_change_status_to=True,
).exists()
def get_allowed_transitions(request, intervention):
current_status = intervention.status
next_candidates = user_allowed_transitions(request.user, current_status, intervention=intervention)
# Si l'intervention est au statut 'to_be_approved' et qu'un contrat est renseigné,
# seuls les utilisateurs avec can_approve=True (ou admin) peuvent changer le statut.
if current_status == 'to_be_approved' and intervention.contract_id:
if not can_approve_intervention(request.user, intervention):
return []
# Si l'intervention est en préparation, que le contrat exige une approbation,
# que la priorité n'est pas urgente, et que l'utilisateur est opérateur,
# alors seul le passage à 'to_be_approved' est autorisé.
if (
current_status == 'in_preparation'
and intervention.contract_id
and getattr(intervention.contract, 'needs_approval', False)
and intervention.priority != '1'
):
try:
user_config = UserConfig.objects.get(user=request.user)
is_operator = user_config.roles.filter(name='operator').exists()
is_only_operator = is_operator and not user_config.roles.filter(
name__in=['admin', 'manager', 'controller', 'external_manager']
).exists()
except UserConfig.DoesNotExist:
is_only_operator = False
if is_only_operator:
next_candidates = [s for s in next_candidates if s in ('to_be_approved', 'canceled')]
# Un external_manager (sans rôle privilégié interne) ne peut annuler que les interventions
# créées par un prestataire (created_by_provider=True). On masque le bouton sinon.
if 'canceled' in next_candidates:
try:
user_config = UserConfig.objects.get(user=request.user)
roles = set(user_config.roles.values_list('name', flat=True))
except UserConfig.DoesNotExist:
roles = set()
is_external_manager_only = (
'external_manager' in roles and
not roles.intersection({'admin', 'manager', 'operator', 'controller'})
)
if is_external_manager_only and not intervention.created_by_provider:
next_candidates = [s for s in next_candidates if s != 'canceled']
items = []
for nxt in next_candidates:
items.append({
"from": current_status,
"to": nxt,
"allowed": True, # Could be used later with transition guards (False + reason if not allowed)
"label": TRANSITION_LABEL.get(nxt) or f"Passer à {nxt}",
"reason": '',
})
return items
def can_edit_note(user, user_config, note):
"""
Règle:
- Admin: peut tout modifier.
- Thématique 'structures': l'auteur OU rôle manager/external_manager (pas de contrainte horaire).
- Sinon: uniquement l'auteur ET la note a été créée il y a < 1h.
"""
if not user.is_authenticated or user_config is None:
return False
# 1) Admin => OK
if user_config.roles.filter(name='admin').exists():
return True
# 2) Thématique 'structures'
intervention = getattr(note, 'intervention', None)
thematic_code = getattr(getattr(intervention, 'thematic', None), 'code', None)
if thematic_code == 'structures':
# Auteur OU rôle manager/external_manager
if note.note_author_id == user.id:
return True
if user_config.roles.filter(name__in=['manager', 'external_manager']).exists():
return True
return False
# 3) Auteur + moins d'une heure (autres thématiques)
if note.note_author_id != user.id:
return False
created_at = getattr(note, 'note_time', None) or getattr(note, 'created_at', None)
if created_at is None:
return False
if timezone.is_naive(created_at):
created_at = timezone.make_aware(created_at, timezone.get_current_timezone())
return (timezone.now() - created_at) <= timedelta(hours=1)
def can_delete_document(user, user_config, document):
"""
Règles de suppression d'un document attaché à une intervention :
- Admin: peut tout supprimer.
- Thématique 'structures': l'auteur OU rôle manager/external_manager (pas de contrainte).
- Sinon (auteur uniquement) :
* Avant 'Pris en charge' (status_order < 15) : toujours autorisé.
* À partir de 'Pris en charge' : autorisé seulement si aucun changement de statut
n'a eu lieu depuis l'upload du document (i.e. le statut n'a pas bougé depuis
que le document a été ajouté).
"""
if not user.is_authenticated or user_config is None:
return False
if user_config.roles.filter(name='admin').exists():
return True
if user_config.is_intern and user_config.roles.filter(name='manager').exists():
return True
intervention = getattr(document, 'intervention', None)
thematic_code = getattr(getattr(intervention, 'thematic', None), 'code', None)
# Thématique 'structures'
if thematic_code == 'structures':
# Auteur OU rôle manager/external_manager
if document.uploaded_by_id == user.id:
return True
if user_config.roles.filter(name__in=['manager', 'external_manager']).exists():
return True
return False
if document.uploaded_by_id != user.id:
return False
if intervention is None:
return False
# Avant 'Pris en charge' : toujours supprimable par l'auteur
if intervention.status_order < STATUS_ORDERS.get('assigned', 15):
return True
# À partir de 'Pris en charge' : supprimable uniquement si aucun changement de statut
# n'a eu lieu depuis que le document a été uploadé.
upload_date = getattr(document, 'upload_date', None)
if upload_date is None:
return False
return not InterventionTimeLine.objects.filter(
intervention=intervention,
event_type='status_change',
event_time__gt=upload_date,
).exists()
# ============================================================================
# Guards pour les fichiers privés (private_media)
# ============================================================================
def guard_intervention_document_file(obj, user):
"""
Guard pour InterventionDocument.file et thumbnail.
L'utilisateur doit pouvoir voir l'intervention associée,
ou au minimum avoir accès au résumé par thématique.
"""
if not user.is_authenticated:
return False
# Peut voir l'intervention en accès complet
if can_view_intervention(user, obj.intervention):
return True
# Ou a accès au résumé par thématique
return can_view_intervention_summary(user, obj.intervention)
def guard_intervention_sheet_file(obj, user):
"""
Guard pour Intervention.intervention_sheet.
L'utilisateur doit pouvoir voir l'intervention.
"""
if not user.is_authenticated:
return False
return can_view_intervention(user, obj)
def guard_operation_photo_file(obj, user):
"""
Guard pour OperationPhoto.photo et thumbnail.
L'utilisateur doit pouvoir voir l'intervention associée à l'opération.
"""
if not user.is_authenticated:
return False
return can_view_intervention(user, obj.operation.intervention)
def guard_operation_photo_file(obj, user):
"""
Guard pour OperationPhoto.photo et thumbnail.
L'utilisateur doit pouvoir voir l'intervention associée à l'opération.
"""
if not user.is_authenticated:
return False
return can_view_intervention(user, obj.operation.intervention)
def guard_guided_operation_media_file(obj, user):
"""
Guard pour GuidedOperationMedia.file.
L'utilisateur doit pouvoir voir l'intervention associée.
"""
if not user.is_authenticated:
return False
return can_view_intervention(user, obj.guided_data.intervention)
def guard_symptom_recommendation_document(obj, user):
"""
Guard pour SymptomRecommendation.document.
Accessible à tout utilisateur authentifié (données de référence).
"""
return user.is_authenticated
# ============================================================================
# Fonctions pour les interventions créées par les prestataires
# ============================================================================
def should_show_created_by_provider_badge(user, intervention):
"""
Vérifie si le badge 'créé par le prestataire' doit être affiché.
Le badge est visible pour les managers et controllers tant que l'intervention
n'a pas été validée.
"""
if not intervention.created_by_provider:
return False
# Le badge n'est plus visible après validation
if intervention.status_order >= STATUS_ORDERS.get('validated', 51):
return False
try:
user_config = UserConfig.objects.get(user=user)
except UserConfig.DoesNotExist:
return False
# Visible pour admin, manager, controller et top_manager
return user_config.roles.filter(name__in=['admin', 'manager', 'controller', 'top_manager']).exists()
def is_external_manager_with_contract_creation_rights(user, contract=None):
"""
Vérifie si l'utilisateur est un external_manager avec le droit de créer des interventions.
"""
try:
user_config = UserConfig.objects.get(user=user)
except UserConfig.DoesNotExist:
return False
if not user_config.roles.filter(name='external_manager').exists():
return False
if contract:
return UserContractAccess.objects.filter(
user_config=user_config,
contract=contract,
can_create_interventions=True
).exists()
return UserContractAccess.objects.filter(
user_config=user_config,
can_create_interventions=True
).exists()
def can_view_correction_messages(user):
"""
Vérifie si l'utilisateur a le droit de voir les messages de correction.
Rôles autorisés: admin, manager, controller, external_manager
"""
try:
user_config = UserConfig.objects.get(user=user)
except UserConfig.DoesNotExist:
return False
allowed_roles = ['admin', 'manager', 'controller', 'external_manager']
return user_config.roles.filter(name__in=allowed_roles).exists()
def can_user_create_inspection_for_contract(user, contract):
"""
Vérifie si l'utilisateur détient les droits de changement de statut vers l'un des statuts :
in_preparation, to_be_approved, to_be_planned, assigned, ou in_progress
pour le contrat spécifié. Les administrateurs contournent toutes les restrictions.
"""
try:
user_config = UserConfig.objects.get(user=user)
except UserConfig.DoesNotExist:
return False
if user_config.roles.filter(name='admin').exists():
return True
uca = UserContractAccess.objects.filter(
user_config=user_config,
contract=contract,
).first()
if not uca:
return False
target_statuses = ['in_preparation', 'to_be_approved', 'to_be_planned', 'assigned', 'in_progress']
return UserContractStatusPermission.objects.filter(
user_contract=uca,
status__in=target_statuses,
can_change_status_to=True,
).exists()