Coverage for backend/django/core/auxiliary/viewsets/FlowsheetViewSet.py: 97%
143 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-05-13 02:47 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-05-13 02:47 +0000
1from rest_framework import serializers, status, viewsets
2from rest_framework.response import Response
3from django.db.models import QuerySet
4from core.auxiliary.models.Flowsheet import Flowsheet
5from core.auxiliary.enums.FlowsheetTemplateType import FlowsheetTemplateType
6from core.auxiliary.serializers.FlowsheetSerializer import FlowsheetSerializer
7from drf_spectacular.utils import extend_schema
8from rest_framework.decorators import action
9from drf_spectacular.types import OpenApiTypes
10from drf_spectacular.utils import OpenApiParameter
11from authentication.user.AccessTable import AccessTable
12from authentication.user.models import User
13import core.auxiliary.enums.ViewType as ViewType
16class ImportFlowsheetSerializer(serializers.Serializer):
17 data = serializers.JSONField()
20class FlowsheetSharingSerializer(serializers.Serializer):
21 flowsheet = serializers.IntegerField()
22 user_email = serializers.EmailField()
23 read_only = serializers.BooleanField(default=False)
26class RemoveSharedUserSerializer(serializers.Serializer):
27 flowsheet = serializers.IntegerField()
28 user_email = serializers.EmailField()
31class UpdateSharedUserAccessSerializer(serializers.Serializer):
32 flowsheet = serializers.IntegerField()
33 user_email = serializers.EmailField()
34 read_only = serializers.BooleanField()
37class SharedUserSerializer(serializers.Serializer):
38 email = serializers.EmailField()
39 read_only = serializers.BooleanField()
42class ListSharedUsersRequestSerializer(serializers.Serializer):
43 flowsheet = serializers.IntegerField()
46class ListSharedUsersSerializer(serializers.Serializer):
47 users = SharedUserSerializer(many=True)
50class FlowsheetViewSet(viewsets.ModelViewSet):
51 """
52 Viewset for the Flowsheet model itself.
54 Flowsheet does not use AccessControlManager, so its access rules stay here
55 and in the serializer rather than in the generic flowsheet-scoped manager.
56 """
57 serializer_class = FlowsheetSerializer
59 @staticmethod
60 def _with_current_user_access(queryset: QuerySet[Flowsheet], user) -> QuerySet[Flowsheet]:
61 return Flowsheet.with_user_access(queryset, user)
63 def get_queryset(self) -> QuerySet[Flowsheet]:
64 current_user = self.request.user
65 queryset = (Flowsheet.objects
66 .filter(owner=current_user, flowsheet_template_type=FlowsheetTemplateType.NotTemplate)
67 .select_related('owner'))
68 return self._with_current_user_access(queryset, current_user)
70 @extend_schema(
71 parameters=[
72 OpenApiParameter(name="type", required=True, type=OpenApiTypes.STR),
73 ]
74 )
75 def list(self, request, *args, **kwargs):
76 type = request.query_params.get('type', ViewType.OWNED)
77 queryset = Flowsheet.get_flowsheets_by_view_type(request.user, type)
78 queryset = (queryset
79 .filter(flowsheet_template_type=FlowsheetTemplateType.NotTemplate)
80 .select_related('owner'))
81 queryset = self._with_current_user_access(queryset, request.user)
82 serializer = self.get_serializer(queryset, many=True)
83 return Response(serializer.data)
85 def retrieve(self, request, *args, **kwargs):
86 try:
87 instance = (
88 self._with_current_user_access(
89 Flowsheet.objects.select_related("owner"),
90 request.user,
91 )
92 .get(id=kwargs["pk"])
93 )
94 except Flowsheet.DoesNotExist:
95 return Response({"error": "Flowsheet not found"}, status=status.HTTP_404_NOT_FOUND)
97 user_access_entries = getattr(instance, "current_user_access_entries", [])
98 has_direct_access = request.user.id == instance.owner_id or bool(user_access_entries)
100 if has_direct_access:
101 # Update the savedDate field
102 instance.set_saved_date()
103 serializer = self.get_serializer(instance)
105 return Response(serializer.data)
106 elif instance.flowsheet_template_type == FlowsheetTemplateType.PublicTemplate and request.user.is_staff:
107 # Allow admins to view contents of public templates
108 serializer = self.get_serializer(instance)
110 return Response(serializer.data)
111 else:
112 return Response({"error": "You do not have access to this flowsheet"}, status=status.HTTP_403_FORBIDDEN)
114 def create(self, request, *args, **kwargs):
115 flowsheet_serializer = FlowsheetSerializer(
116 data=request.data, context={"request": request})
117 flowsheet_serializer.is_valid(raise_exception=True)
118 flowsheet_serializer.save()
120 return Response(flowsheet_serializer.data, status=status.HTTP_201_CREATED)
122 def _is_read_only_sharee(self, user, flowsheet_id: int) -> bool:
123 """
124 Lightweight helper for the flowsheet record itself, which is not filtered
125 through AccessControlManager like flowsheet-owned child models are.
126 """
127 access = AccessTable.objects.filter(
128 user=user,
129 flowsheet_id=flowsheet_id,
130 ).values("read_only").first()
131 return bool(access and access["read_only"])
133 def update(self, request, *args, **kwargs):
134 if self._is_read_only_sharee(request.user, kwargs["pk"]): 134 ↛ 135line 134 didn't jump to line 135 because the condition on line 134 was never true
135 return Response(
136 {"error": "This flowsheet is shared with read-only access."},
137 status=status.HTTP_403_FORBIDDEN,
138 )
139 return super().update(request, *args, **kwargs)
141 def partial_update(self, request, *args, **kwargs):
142 if self._is_read_only_sharee(request.user, kwargs["pk"]):
143 return Response(
144 {"error": "This flowsheet is shared with read-only access."},
145 status=status.HTTP_403_FORBIDDEN,
146 )
147 return super().partial_update(request, *args, **kwargs)
149 def destroy(self, request, *args, **kwargs):
150 if self._is_read_only_sharee(request.user, kwargs["pk"]):
151 return Response(
152 {"error": "This flowsheet is shared with read-only access."},
153 status=status.HTTP_403_FORBIDDEN,
154 )
155 return super().destroy(request, *args, **kwargs)
157 @extend_schema(request=FlowsheetSharingSerializer, responses=None)
158 @action(detail=False, methods=['post'], url_path='share-flowsheet')
159 def share_flowsheet(self, request):
160 serializer = FlowsheetSharingSerializer(data=request.data)
161 serializer.is_valid(raise_exception=True)
162 flowsheet = serializer.validated_data['flowsheet']
163 user_email = serializer.validated_data['user_email']
164 read_only = serializer.validated_data['read_only']
166 try:
167 Flowsheet.share_flowsheet(request.user, flowsheet, user_email, read_only=read_only)
168 return Response({"message": "Flowsheet shared successfully"}, status=status.HTTP_200_OK)
169 except ValueError as exc:
170 return Response({"error": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
171 except PermissionError as exc:
172 return Response({"error": str(exc)}, status=status.HTTP_403_FORBIDDEN)
173 except (Flowsheet.DoesNotExist, User.DoesNotExist):
174 return Response({"error": "Failed to share flowsheet"}, status=status.HTTP_404_NOT_FOUND)
176 @extend_schema(responses=ListSharedUsersSerializer)
177 @action(detail=False, methods=['get'], url_path='list-shared-users')
178 def list_shared_users(self, request, *args, **kwargs):
179 query_serializer = ListSharedUsersRequestSerializer(data=request.query_params)
180 query_serializer.is_valid(raise_exception=True)
181 flowsheet_id = query_serializer.validated_data["flowsheet"]
183 try:
184 users = Flowsheet.get_shared_users(request.user, flowsheet_id)
185 serializer = ListSharedUsersSerializer({"users": users})
186 return Response(serializer.data, status=status.HTTP_200_OK)
187 except Flowsheet.DoesNotExist:
188 return Response({"error": "Failed to retrieve list of user"}, status=status.HTTP_404_NOT_FOUND)
190 @extend_schema(request=RemoveSharedUserSerializer, responses=None)
191 @action(detail=False, methods=['post'], url_path='remove-user')
192 def remove_user(self, request, *args, **kwargs):
193 serializer = RemoveSharedUserSerializer(data=request.data)
194 serializer.is_valid(raise_exception=True)
195 flowsheet_id = serializer.validated_data["flowsheet"]
196 user_email = serializer.validated_data["user_email"]
198 try:
199 Flowsheet.remove_user(request.user, flowsheet_id, user_email)
200 return Response({"message": "User removed successfully"}, status=status.HTTP_200_OK)
201 except ValueError as exc:
202 return Response({"error": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
203 except (Flowsheet.DoesNotExist, User.DoesNotExist, AccessTable.DoesNotExist):
204 return Response({"error": "Failed to remove user"}, status=status.HTTP_404_NOT_FOUND)
206 @extend_schema(request=UpdateSharedUserAccessSerializer, responses=None)
207 @action(detail=False, methods=['post'], url_path='update-shared-user-access')
208 def update_shared_user_access(self, request, *args, **kwargs):
209 serializer = UpdateSharedUserAccessSerializer(data=request.data)
210 serializer.is_valid(raise_exception=True)
212 flowsheet_id = serializer.validated_data["flowsheet"]
213 user_email = serializer.validated_data["user_email"]
214 read_only = serializer.validated_data["read_only"]
216 try:
217 Flowsheet.update_shared_user_access(
218 owner=request.user,
219 flowsheet_id=flowsheet_id,
220 user_email=user_email,
221 read_only=read_only,
222 )
223 return Response({"message": "Shared user access updated successfully"}, status=status.HTTP_200_OK)
224 except ValueError as exc:
225 return Response({"error": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
226 except (Flowsheet.DoesNotExist, User.DoesNotExist, AccessTable.DoesNotExist):
227 return Response({"error": "Failed to update shared user access"}, status=status.HTTP_404_NOT_FOUND)