Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Mapapi/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down
27 changes: 27 additions & 0 deletions Mapapi/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -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.

Expand Down
49 changes: 40 additions & 9 deletions Mapapi/views/collaboration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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')

Expand Down Expand Up @@ -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,
Expand All @@ -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'
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"},
Expand Down
69 changes: 59 additions & 10 deletions Mapapi/views/incident.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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')
Expand All @@ -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')
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 5 additions & 5 deletions Mapapi/views/organisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
)):
Expand All @@ -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]
)):
Expand Down Expand Up @@ -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]
)):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
)):
Expand Down
9 changes: 3 additions & 6 deletions Mapapi/views/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
Expand Down