diff --git a/Mapapi/permissions.py b/Mapapi/permissions.py index 6b36f0ab..3b645865 100644 --- a/Mapapi/permissions.py +++ b/Mapapi/permissions.py @@ -18,6 +18,11 @@ ) +def _is_super(request): + """True si l'utilisateur est un super admin authentifié (accès total, sans exception).""" + return bool(request.user and request.user.is_authenticated and request.user.is_superuser) + + def _get_incident_from_view(view, request): """Récupère l'incident ciblé par la vue. @@ -46,6 +51,8 @@ class IsIncidentLeader(BasePermission): def has_permission(self, request, view): if not request.user or not request.user.is_authenticated: return False + if _is_super(request): + return True incident = _get_incident_from_view(view, request) if incident is None: # si pas d'incident dans l'URL, on délègue à has_object_permission @@ -60,6 +67,8 @@ def has_permission(self, request, view): return incident.taken_by_id == request.user.id def has_object_permission(self, request, view, obj): + if _is_super(request): + return True incident = getattr(obj, 'incident', obj if isinstance(obj, Incident) else None) if incident is None: return False @@ -81,6 +90,8 @@ class IsIncidentCollaborator(BasePermission): def has_permission(self, request, view): if not request.user or not request.user.is_authenticated: return False + if _is_super(request): + return True incident = _get_incident_from_view(view, request) if incident is None: return True @@ -91,6 +102,8 @@ def has_permission(self, request, view): ).exists() def has_object_permission(self, request, view, obj): + if _is_super(request): + return True incident = getattr(obj, 'incident', obj if isinstance(obj, Incident) else None) if incident is None: return False @@ -113,6 +126,8 @@ class IsIncidentContributor(BasePermission): def has_permission(self, request, view): if not request.user or not request.user.is_authenticated: return False + if _is_super(request): + return True # Les méthodes de lecture restent autorisées pour tous les collaborateurs if request.method in SAFE_METHODS: return IsIncidentCollaborator().has_permission(request, view) @@ -139,6 +154,8 @@ class IsIncidentLeaderOrContributor(BasePermission): def has_permission(self, request, view): if not request.user or not request.user.is_authenticated: return False + if _is_super(request): + return True if request.method in SAFE_METHODS: return IsIncidentCollaborator().has_permission(request, view) incident = _get_incident_from_view(view, request) diff --git a/Mapapi/serializer.py b/Mapapi/serializer.py index 3ecd4725..0c469a8d 100644 --- a/Mapapi/serializer.py +++ b/Mapapi/serializer.py @@ -360,6 +360,28 @@ class Meta: 'error_message', 'created_at', 'updated_at', ) +class OtherCollaboratorSerializer(serializers.ModelSerializer): + organisation_name = serializers.CharField( + source='user.organisation_member.name', read_only=True, default=None + ) + organisation_id = serializers.IntegerField( + source='user.organisation_member_id', read_only=True, default=None + ) + user_full_name = serializers.SerializerMethodField() + user_email = serializers.EmailField(source='user.email', read_only=True) + + class Meta: + model = Collaboration + fields = ( + 'id', 'user', 'user_full_name', 'user_email', + 'organisation_id', 'organisation_name', 'role', 'status', 'end_date' + ) + + def get_user_full_name(self, obj): + if obj.user: + return f"{obj.user.first_name or ''} {obj.user.last_name or ''}".strip() or obj.user.email + return None + class CollaborationSerializer(ModelSerializer): # Nom de l'organisation du collaborateur (lecture seule) organisation_name = serializers.CharField( @@ -373,6 +395,7 @@ class CollaborationSerializer(ModelSerializer): incident_title = serializers.CharField(source='incident.title', read_only=True) incident_details = IncidentSerializer(source='incident', read_only=True) prediction_details = PredictionSerializer(source='incident.prediction', read_only=True) + other_collaborators = serializers.SerializerMethodField() class Meta: model = Collaboration @@ -388,6 +411,10 @@ def get_user_full_name(self, obj): return f"{obj.user.first_name or ''} {obj.user.last_name or ''}".strip() or obj.user.email return None + def get_other_collaborators(self, obj): + qs = Collaboration.objects.filter(incident=obj.incident).exclude(id=obj.id).select_related('user', 'user__organisation_member') + return OtherCollaboratorSerializer(qs, many=True).data + def validate_role(self, value): """Un utilisateur ne peut pas se déclarer leader lui-même. diff --git a/Mapapi/views/collaboration.py b/Mapapi/views/collaboration.py index 295e4ece..4299c359 100644 --- a/Mapapi/views/collaboration.py +++ b/Mapapi/views/collaboration.py @@ -37,9 +37,13 @@ class CollaborationDashboardView(generics.ListAPIView): def get_queryset(self): user = self.request.user - qs = Collaboration.objects.filter( - Q(user=user) | Q(incident__taken_by=user) - ).select_related( + if user.is_superuser: + qs = Collaboration.objects.all() + else: + qs = Collaboration.objects.filter( + Q(user=user) | Q(incident__taken_by=user) + ) + qs = qs.select_related( 'incident', 'user', 'user__organisation_member' ).order_by('-created_at') @@ -95,9 +99,13 @@ class CollaborationView(generics.CreateAPIView, generics.ListAPIView): def get_queryset(self): user = self.request.user - qs = Collaboration.objects.filter( - Q(user=user) | Q(incident__taken_by=user) - ).select_related( + if user.is_superuser: + qs = Collaboration.objects.all() + else: + qs = Collaboration.objects.filter( + Q(user=user) | Q(incident__taken_by=user) + ) + qs = qs.select_related( 'incident', 'user', 'user__organisation_member' ).order_by('-id') @@ -236,8 +244,9 @@ def post(self, request, collaboration_id, action, format=None): if action not in ["accept", "reject"]: return Response({"error": "Invalid action"}, status=status.HTTP_400_BAD_REQUEST) - # Seul le leader de l'incident peut accepter/rejeter + # Seul le leader de l'incident (ou un super admin) peut accepter/rejeter is_leader = ( + request.user.is_superuser or collaboration.incident.taken_by == request.user or Collaboration.objects.filter( incident=collaboration.incident, @@ -261,6 +270,16 @@ def post(self, request, collaboration_id, action, format=None): if incident.take_in_charge_mode == 'internal': incident.take_in_charge_mode = 'collaborative' incident.save(update_fields=['take_in_charge_mode']) + if incident.taken_by: + collab_leader, created = Collaboration.objects.get_or_create( + incident=incident, + user=incident.taken_by, + defaults={'role': COLLAB_ROLE_LEADER, 'status': 'accepted'} + ) + if not created: + collab_leader.role = COLLAB_ROLE_LEADER + collab_leader.status = 'accepted' + collab_leader.save(update_fields=['role', 'status']) return Response({"status": "Collaboration accepted"}, status=status.HTTP_200_OK) elif action == "reject": collaboration.status = 'declined' @@ -276,8 +295,9 @@ def post(self, request, *args, **kwargs): collaboration_id = request.data.get('collaboration_id') collaboration = Collaboration.objects.get(id=collaboration_id) - # Seul le leader de l'incident peut décliner + # Seul le leader de l'incident (ou un super admin) peut décliner is_leader = ( + request.user.is_superuser or collaboration.incident.taken_by == request.user or Collaboration.objects.filter( incident=collaboration.incident, @@ -337,8 +357,9 @@ def post(self, request, *args, **kwargs): collaboration = Collaboration.objects.get(id=collaboration_id) - # Seul le leader de l'incident peut accepter + # Seul le leader de l'incident (ou un super admin) peut accepter is_leader = ( + request.user.is_superuser or collaboration.incident.taken_by == request.user or Collaboration.objects.filter( incident=collaboration.incident, @@ -375,6 +396,16 @@ def post(self, request, *args, **kwargs): if incident.take_in_charge_mode == 'internal': incident.take_in_charge_mode = 'collaborative' incident.save(update_fields=['take_in_charge_mode']) + if incident.taken_by: + collab_leader, created = Collaboration.objects.get_or_create( + incident=incident, + user=incident.taken_by, + defaults={'role': COLLAB_ROLE_LEADER, 'status': 'accepted'} + ) + if not created: + collab_leader.role = COLLAB_ROLE_LEADER + collab_leader.status = 'accepted' + collab_leader.save(update_fields=['role', 'status']) return Response( {"message": "Collaboration acceptée avec succès"}, diff --git a/Mapapi/views/incident.py b/Mapapi/views/incident.py index 07f7984f..87b6f375 100644 --- a/Mapapi/views/incident.py +++ b/Mapapi/views/incident.py @@ -297,6 +297,9 @@ class OrgIncidentsView(generics.ListAPIView): def get_queryset(self): user = self.request.user + if user.is_superuser: + return Incident.objects.all().select_related('user_id', 'category_id').order_by('-created_at') + org = user.organisation_member if not org: @@ -481,6 +484,8 @@ class AgentAssignedIncidentsView(generics.ListAPIView): def get_queryset(self): user = self.request.user + if user.is_superuser: + return IncidentAssignment.objects.all().select_related('incident', 'agent', 'assigned_by') if user.org_role != ORG_ROLE_FIELD: return IncidentAssignment.objects.none() return IncidentAssignment.objects.filter(agent=user).select_related('incident', 'agent', 'assigned_by') @@ -492,14 +497,54 @@ class FieldReportListCreateView(generics.ListCreateAPIView): def get_queryset(self): user = self.request.user - qs = FieldReport.objects.select_related('incident', 'agent').order_by('-visited_at') - if user.is_staff or user.is_superuser: - return qs - if user.org_role == ORG_ROLE_FIELD: - return qs.filter(agent=user) - if user.organisation_member: - return qs.filter(agent__organisation_member=user.organisation_member) - return FieldReport.objects.none() + qs = FieldReport.objects.select_related( + 'incident', 'agent', 'agent__organisation_member' + ).order_by('-visited_at') + + # Super admin / staff : accès total + if user.is_superuser or user.is_staff: + pass + elif user.org_role == ORG_ROLE_FIELD: + # Agent de terrain : ses propres rapports uniquement + qs = qs.filter(agent=user) + elif user.organisation_member and user.org_role in [ORG_ROLE_ADMIN, ORG_ROLE_BUREAU]: + # Agent de bureau / admin d'organisation + incident_id = self.request.query_params.get('incident') or self.request.query_params.get('incident_id') + if incident_id: + try: + incident = Incident.objects.get(pk=incident_id) + # L'org a-t-elle un lien avec l'incident ? + is_owner = incident.user_id and incident.user_id.organisation_member == user.organisation_member + is_taker = incident.taken_by and incident.taken_by.organisation_member == user.organisation_member + has_collab = Collaboration.objects.filter( + incident=incident, + user__organisation_member=user.organisation_member, + status='accepted' + ).exists() + + if is_owner or is_taker or has_collab: + qs = qs.filter(incident=incident) + else: + return FieldReport.objects.none() + except (Incident.DoesNotExist, ValueError): + return FieldReport.objects.none() + else: + # Liste générale : rapports des agents de son org ou sur des incidents liés à son org + qs = qs.filter( + Q(agent__organisation_member=user.organisation_member) | + Q(incident__user_id__organisation_member=user.organisation_member) | + Q(incident__taken_by__organisation_member=user.organisation_member) | + Q(incident__collaboration__user__organisation_member=user.organisation_member, incident__collaboration__status='accepted') + ).distinct() + else: + return FieldReport.objects.none() + + # Filtre optionnel par incident (appliqué à tous les rôles restants qui ont pu passer) + incident_id = self.request.query_params.get('incident') or self.request.query_params.get('incident_id') + if incident_id and (user.is_superuser or user.is_staff or user.org_role == ORG_ROLE_FIELD): + qs = qs.filter(incident_id=incident_id) + + return qs def create(self, request, *args, **kwargs): incident_id = request.data.get('incident') or request.data.get('incident_id') @@ -591,7 +636,7 @@ def post(self, request, incident_id): {"error": "Seul un admin ou agent de bureau peut modifier la visibilité."}, status=status.HTTP_403_FORBIDDEN, ) - elif not user.is_staff: + elif not (user.is_staff or user.is_superuser): return Response( {"error": "Vous n'avez pas les droits sur cet incident."}, status=status.HTTP_403_FORBIDDEN, @@ -1016,11 +1061,15 @@ def post(self, request, incident_id): incident.take_in_charge_mode = 'internal' incident.save(update_fields=['taken_by', 'etat', 'take_in_charge_mode']) - collaboration, _ = Collaboration.objects.get_or_create( + collaboration, created = Collaboration.objects.get_or_create( incident=incident, user=request.user, defaults={'role': COLLAB_ROLE_LEADER, 'status': 'accepted'}, ) + if not created: + collaboration.role = COLLAB_ROLE_LEADER + collaboration.status = 'accepted' + collaboration.save(update_fields=['role', 'status']) action_message = f"took incident {incident_id} into account in internal mode" UserAction.objects.create(user=request.user, action=action_message) diff --git a/Mapapi/views/organisation.py b/Mapapi/views/organisation.py index dbe7e362..352c9f26 100644 --- a/Mapapi/views/organisation.py +++ b/Mapapi/views/organisation.py @@ -118,7 +118,7 @@ def list(self, request, *args, **kwargs): # Vérifier que l'utilisateur est admin ou agent de bureau de cette org org_id = self.kwargs['pk'] user = request.user - if not (user.is_staff or ( + if not (user.is_staff or user.is_superuser or ( user.organisation_member_id == org_id and user.org_role in [ORG_ROLE_ADMIN, ORG_ROLE_BUREAU] )): @@ -141,7 +141,7 @@ class OrganisationMemberCreateView(APIView): def post(self, request, pk): user = request.user # Seul un admin org ou agent de bureau de cette org peut ajouter - if not (user.is_staff or ( + if not (user.is_staff or user.is_superuser or ( user.organisation_member_id == pk and user.org_role in [ORG_ROLE_ADMIN, ORG_ROLE_BUREAU] )): @@ -259,7 +259,7 @@ def post(self, request, pk): user = request.user # Permissions : org_admin/bureau_agent de cette org ou superadmin - if not (user.is_staff or ( + if not (user.is_staff or user.is_superuser or ( user.organisation_member_id == pk and user.org_role in [ORG_ROLE_ADMIN, ORG_ROLE_BUREAU] )): @@ -388,7 +388,7 @@ class OrganisationMemberDetailView(APIView): def _check_permission(self, request, pk): user = request.user - if user.is_staff: + if user.is_staff or user.is_superuser: return True return ( user.organisation_member_id == pk @@ -505,7 +505,7 @@ def post(self, request, pk): user = request.user # Permissions : admin de l'org ou superadmin (un bureau_agent ne crée pas d'admin) - if not (user.is_staff or ( + if not (user.is_staff or user.is_superuser or ( user.organisation_member_id == pk and user.org_role == ORG_ROLE_ADMIN )): diff --git a/Mapapi/views/task.py b/Mapapi/views/task.py index 13eeb1ba..cfc3a885 100644 --- a/Mapapi/views/task.py +++ b/Mapapi/views/task.py @@ -20,12 +20,9 @@ class IncidentTaskListCreateView(generics.ListCreateAPIView): permission_classes = [IsAuthenticated, IsIncidentLeaderOrContributor] def get_queryset(self): - incident_id = self.kwargs['incident_id'] - qs = IncidentTask.objects.filter(incident_id=incident_id).order_by('start_date', 'id') - print(f"DEBUG: incident_id={incident_id}, tasks count={qs.count()}") - if qs.count() > 0: - print(f"DEBUG: task IDs = {list(qs.values_list('id', flat=True))}") - return qs + return IncidentTask.objects.filter( + incident_id=self.kwargs['incident_id'] + ).order_by('start_date', 'id') def perform_create(self, serializer): incident = Incident.objects.get(pk=self.kwargs['incident_id'])