endoreg-db 0.8.1__py3-none-any.whl → 0.8.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of endoreg-db might be problematic. Click here for more details.

Files changed (48) hide show
  1. endoreg_db/helpers/download_segmentation_model.py +31 -0
  2. endoreg_db/migrations/0003_add_center_display_name.py +30 -0
  3. endoreg_db/models/administration/center/center.py +7 -1
  4. endoreg_db/models/media/pdf/raw_pdf.py +31 -26
  5. endoreg_db/models/media/video/create_from_file.py +26 -4
  6. endoreg_db/models/media/video/pipe_1.py +13 -1
  7. endoreg_db/models/media/video/video_file.py +36 -13
  8. endoreg_db/models/media/video/video_file_anonymize.py +2 -1
  9. endoreg_db/models/media/video/video_file_frames/_manage_frame_range.py +12 -0
  10. endoreg_db/models/media/video/video_file_io.py +4 -2
  11. endoreg_db/models/metadata/video_meta.py +2 -2
  12. endoreg_db/serializers/anonymization.py +3 -0
  13. endoreg_db/services/pdf_import.py +131 -45
  14. endoreg_db/services/video_import.py +427 -128
  15. endoreg_db/urls/__init__.py +0 -2
  16. endoreg_db/urls/media.py +201 -4
  17. endoreg_db/urls/report.py +0 -30
  18. endoreg_db/urls/sensitive_meta.py +0 -36
  19. endoreg_db/urls/video.py +30 -88
  20. endoreg_db/utils/paths.py +2 -10
  21. endoreg_db/utils/video/ffmpeg_wrapper.py +67 -4
  22. endoreg_db/views/anonymization/validate.py +76 -32
  23. endoreg_db/views/media/__init__.py +38 -2
  24. endoreg_db/views/media/pdf_media.py +1 -1
  25. endoreg_db/views/media/segments.py +71 -0
  26. endoreg_db/views/media/sensitive_metadata.py +314 -0
  27. endoreg_db/views/media/video_segments.py +596 -0
  28. endoreg_db/views/pdf/reimport.py +18 -8
  29. endoreg_db/views/video/__init__.py +0 -8
  30. endoreg_db/views/video/correction.py +34 -32
  31. endoreg_db/views/video/reimport.py +15 -12
  32. endoreg_db/views/video/video_stream.py +168 -50
  33. {endoreg_db-0.8.1.dist-info → endoreg_db-0.8.2.1.dist-info}/METADATA +2 -2
  34. {endoreg_db-0.8.1.dist-info → endoreg_db-0.8.2.1.dist-info}/RECORD +47 -43
  35. endoreg_db/views/video/media/__init__.py +0 -23
  36. /endoreg_db/{urls/pdf.py → config/__init__.py} +0 -0
  37. /endoreg_db/views/video/{media/task_status.py → task_status.py} +0 -0
  38. /endoreg_db/views/video/{media/video_analyze.py → video_analyze.py} +0 -0
  39. /endoreg_db/views/video/{media/video_apply_mask.py → video_apply_mask.py} +0 -0
  40. /endoreg_db/views/video/{media/video_correction.py → video_correction.py} +0 -0
  41. /endoreg_db/views/video/{media/video_download_processed.py → video_download_processed.py} +0 -0
  42. /endoreg_db/views/video/{media/video_media.py → video_media.py} +0 -0
  43. /endoreg_db/views/video/{media/video_meta.py → video_meta.py} +0 -0
  44. /endoreg_db/views/video/{media/video_processing_history.py → video_processing_history.py} +0 -0
  45. /endoreg_db/views/video/{media/video_remove_frames.py → video_remove_frames.py} +0 -0
  46. /endoreg_db/views/video/{media/video_reprocess.py → video_reprocess.py} +0 -0
  47. {endoreg_db-0.8.1.dist-info → endoreg_db-0.8.2.1.dist-info}/WHEEL +0 -0
  48. {endoreg_db-0.8.1.dist-info → endoreg_db-0.8.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,11 +1,18 @@
1
+ import logging
2
+ from typing import Any, Dict, cast
3
+
4
+ from django.db import transaction
1
5
  from rest_framework import status
2
6
  from rest_framework.response import Response
3
7
  from rest_framework.views import APIView
4
- from django.db import transaction
5
- from endoreg_db.models import VideoFile, RawPdfFile
8
+
9
+ from endoreg_db.models import RawPdfFile, VideoFile
6
10
  from endoreg_db.serializers.anonymization import SensitiveMetaValidateSerializer
7
11
 
8
12
 
13
+ logger = logging.getLogger(__name__)
14
+
15
+
9
16
  class AnonymizationValidateView(APIView):
10
17
  """
11
18
  POST /api/anonymization/<int:file_id>/validate/
@@ -21,6 +28,7 @@ class AnonymizationValidateView(APIView):
21
28
  "casenumber": "12345",
22
29
  "anonymized_text": "...", // nur für PDFs; Videos ignorieren
23
30
  "is_verified": true // optional; default true
31
+ "file_type": "video" // optional; "video" oder "pdf"; wenn nicht angegeben, wird zuerst Video, dann PDF versucht
24
32
  }
25
33
 
26
34
  Rückwärtskompatibilität: ISO-Format (YYYY-MM-DD) wird ebenfalls akzeptiert.
@@ -31,33 +39,69 @@ class AnonymizationValidateView(APIView):
31
39
  # Serializer-Validierung mit deutscher Datums-Priorität
32
40
  serializer = SensitiveMetaValidateSerializer(data=request.data or {})
33
41
  serializer.is_valid(raise_exception=True)
34
- payload = serializer.validated_data
35
- payload.setdefault("is_verified", True)
36
-
37
- # Try Video first
38
- video = VideoFile.objects.filter(pk=file_id).first()
39
- if video:
40
- # Ensure center_name is in payload for hash calculation
41
- if video.center and not payload.get("center_name"):
42
- payload["center_name"] = video.center.name
43
-
44
- ok = video.validate_metadata_annotation(payload)
45
- #if ok:
46
- # video._cleanup_raw_assets()
47
- if not ok:
48
- return Response({"error": "Video validation failed."}, status=status.HTTP_400_BAD_REQUEST)
49
- return Response({"message": "Video validated."}, status=status.HTTP_200_OK)
50
-
51
- # Then PDF
52
- pdf = RawPdfFile.objects.filter(pk=file_id).first()
53
- if pdf:
54
- # Ensure center_name is in payload for hash calculation
55
- if pdf.center and not payload.get("center_name"):
56
- payload["center_name"] = pdf.center.name
57
-
58
- ok = pdf.validate_metadata_annotation(payload)
59
- if not ok:
60
- return Response({"error": "PDF validation failed."}, status=status.HTTP_400_BAD_REQUEST)
61
- return Response({"message": "PDF validated."}, status=status.HTTP_200_OK)
62
-
63
- return Response({"error": f"Item {file_id} not found as video or pdf."}, status=status.HTTP_404_NOT_FOUND)
42
+ validated_data = cast(Dict[str, Any], serializer.validated_data)
43
+ payload: Dict[str, Any] = dict(validated_data)
44
+ if "is_verified" not in payload:
45
+ payload["is_verified"] = True
46
+
47
+ file_type = payload.get("file_type")
48
+
49
+ # Try Video first (unless explicitly requesting PDF)
50
+ if file_type in (None, "video"):
51
+ video = VideoFile.objects.select_related("center").filter(pk=file_id).first()
52
+ if video is not None:
53
+ prepared_payload = self._prepare_payload(payload, video)
54
+ try:
55
+ ok = video.validate_metadata_annotation(prepared_payload)
56
+ except Exception: # pragma: no cover - defensive safety net
57
+ logger.exception("Video validation crashed for id=%s", file_id)
58
+ return Response(
59
+ {"error": "Video validation encountered an unexpected error."},
60
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR,
61
+ )
62
+
63
+ if not ok:
64
+ return Response({"error": "Video validation failed."}, status=status.HTTP_400_BAD_REQUEST)
65
+
66
+ return Response({"message": "Video validated."}, status=status.HTTP_200_OK)
67
+
68
+ if file_type == "video":
69
+ return Response({"error": f"Video {file_id} not found."}, status=status.HTTP_404_NOT_FOUND)
70
+
71
+ # Then PDF (unless explicitly requesting Video)
72
+ if file_type in (None, "pdf"):
73
+ pdf = RawPdfFile.objects.select_related("center").filter(pk=file_id).first()
74
+ if pdf is not None:
75
+ prepared_payload = self._prepare_payload(payload, pdf)
76
+ try:
77
+ ok = pdf.validate_metadata_annotation(prepared_payload)
78
+ except Exception: # pragma: no cover - defensive safety net
79
+ logger.exception("PDF validation crashed for id=%s", file_id)
80
+ return Response(
81
+ {"error": "PDF validation encountered an unexpected error."},
82
+ status=status.HTTP_500_INTERNAL_SERVER_ERROR,
83
+ )
84
+
85
+ if not ok:
86
+ return Response({"error": "PDF validation failed."}, status=status.HTTP_400_BAD_REQUEST)
87
+
88
+ return Response({"message": "PDF validated."}, status=status.HTTP_200_OK)
89
+
90
+ if file_type == "pdf":
91
+ return Response({"error": f"PDF {file_id} not found."}, status=status.HTTP_404_NOT_FOUND)
92
+
93
+ return Response({"error": f"Item {file_id} not found as video or pdf."}, status=status.HTTP_404_NOT_FOUND)
94
+
95
+ @staticmethod
96
+ def _prepare_payload(base_payload: Dict[str, Any], file_obj: Any) -> Dict[str, Any]:
97
+ """Return a fresh payload tailored for the given file object."""
98
+
99
+ prepared = dict(base_payload)
100
+ prepared.pop("file_type", None)
101
+
102
+ center = getattr(file_obj, "center", None)
103
+ center_name = getattr(center, "name", None)
104
+ if center_name and not prepared.get("center_name"):
105
+ prepared["center_name"] = center_name
106
+
107
+ return prepared
@@ -1,9 +1,45 @@
1
1
  # Media Management Views (Phase 1.2)
2
2
 
3
3
  from .video_media import VideoMediaView
4
- from .pdf_media import PDFMediaManagementView
4
+ from .pdf_media import PdfMediaView
5
+ from ..video.reimport import VideoReimportView
6
+ from ..pdf.reimport import PdfReimportView
7
+ from .segments import video_segments_by_pk
8
+ from .video_segments import (
9
+ video_segments_collection,
10
+ video_segments_by_video,
11
+ video_segment_detail,
12
+ video_segments_stats,
13
+ video_segment_validate,
14
+ video_segments_validate_bulk,
15
+ video_segments_validation_status,
16
+ )
17
+ from .sensitive_metadata import (
18
+ video_sensitive_metadata,
19
+ video_sensitive_metadata_verify,
20
+ pdf_sensitive_metadata,
21
+ pdf_sensitive_metadata_verify,
22
+ sensitive_metadata_list,
23
+ pdf_sensitive_metadata_list,
24
+ )
5
25
 
6
26
  __all__ = [
7
27
  'VideoMediaView',
8
- 'PDFMediaManagementView',
28
+ 'PdfMediaView',
29
+ 'VideoReimportView',
30
+ 'PdfReimportView',
31
+ 'video_segments_by_pk',
32
+ 'video_segments_collection',
33
+ 'video_segments_by_video',
34
+ 'video_segment_detail',
35
+ 'video_segments_stats',
36
+ 'video_segment_validate',
37
+ 'video_segments_validate_bulk',
38
+ 'video_segments_validation_status',
39
+ 'video_sensitive_metadata',
40
+ 'video_sensitive_metadata_verify',
41
+ 'pdf_sensitive_metadata',
42
+ 'pdf_sensitive_metadata_verify',
43
+ 'sensitive_metadata_list',
44
+ 'pdf_sensitive_metadata_list',
9
45
  ]
@@ -21,7 +21,7 @@ from endoreg_db.utils.permissions import EnvironmentAwarePermission
21
21
  logger = logging.getLogger(__name__)
22
22
 
23
23
 
24
- class PDFMediaManagementView(APIView):
24
+ class PdfMediaView(APIView):
25
25
  """
26
26
  PDF Media Management API for CRUD operations on PDF files.
27
27
 
@@ -0,0 +1,71 @@
1
+ """
2
+ Modern Media Framework - Video Segment API Views
3
+ October 14, 2025 - Migration to unified /api/media/videos/<pk>/segments/ pattern
4
+
5
+ This module provides modern framework views for video segment management,
6
+ wrapping legacy segment views with pk-based parameter handling.
7
+ """
8
+ from endoreg_db.models import Label, LabelVideoSegment, VideoFile
9
+ from endoreg_db.serializers.label_video_segment.label_video_segment import LabelVideoSegmentSerializer
10
+
11
+ from django.db import transaction
12
+ from rest_framework import status
13
+ from rest_framework.decorators import api_view, permission_classes
14
+ from rest_framework.response import Response
15
+
16
+ from endoreg_db.utils.permissions import EnvironmentAwarePermission
17
+ import logging
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @api_view(['GET'])
23
+ @permission_classes([EnvironmentAwarePermission])
24
+ def video_segments_by_pk(request, pk):
25
+ """
26
+ Modern media framework endpoint for retrieving video segments.
27
+
28
+ GET /api/media/videos/<int:pk>/segments/?label=<label_name>
29
+
30
+ Returns all segments for a video, optionally filtered by label name.
31
+ This is the modern replacement for /api/video/<id>/segments/
32
+
33
+ Query Parameters:
34
+ label (str, optional): Filter segments by label name (e.g., 'outside')
35
+
36
+ Returns:
37
+ 200: List of video segments
38
+ 404: Video not found
39
+ """
40
+ try:
41
+ video = VideoFile.objects.get(id=pk)
42
+ except VideoFile.DoesNotExist:
43
+ logger.warning(f"Video with pk {pk} not found")
44
+ return Response(
45
+ {'error': f'Video with id {pk} not found'},
46
+ status=status.HTTP_404_NOT_FOUND
47
+ )
48
+
49
+ # Start with all segments for this video
50
+ queryset = LabelVideoSegment.objects.filter(video_file=video)
51
+
52
+ # Optional filtering by label name
53
+ label_name = request.GET.get('label')
54
+ if label_name:
55
+ try:
56
+ label = Label.objects.get(name=label_name)
57
+ queryset = queryset.filter(label=label)
58
+ logger.info(f"Filtering segments for video {pk} by label '{label_name}'")
59
+ except Label.DoesNotExist:
60
+ logger.warning(f"Label '{label_name}' not found, returning empty result")
61
+ return Response(
62
+ {'error': f"Label '{label_name}' not found"},
63
+ status=status.HTTP_404_NOT_FOUND
64
+ )
65
+
66
+ # Order by start time for consistent results
67
+ segments = queryset.order_by('start_frame_number')
68
+ serializer = LabelVideoSegmentSerializer(segments, many=True)
69
+
70
+ logger.info(f"Returning {len(segments)} segments for video {pk}")
71
+ return Response(serializer.data)
@@ -0,0 +1,314 @@
1
+ # Modern Media Framework: Sensitive Metadata Management
2
+ from rest_framework.decorators import api_view, permission_classes
3
+ from rest_framework.response import Response
4
+ from rest_framework import status
5
+ from django.db import transaction
6
+ from django.db.models import Q
7
+ from django.shortcuts import get_object_or_404
8
+ from endoreg_db.utils.permissions import EnvironmentAwarePermission
9
+ from endoreg_db.models import VideoFile, RawPdfFile, SensitiveMeta
10
+ from endoreg_db.serializers.meta import (
11
+ SensitiveMetaDetailSerializer,
12
+ SensitiveMetaUpdateSerializer,
13
+ )
14
+
15
+ # === VIDEO SENSITIVE METADATA ===
16
+
17
+ @api_view(['GET', 'PATCH'])
18
+ @permission_classes([EnvironmentAwarePermission])
19
+ def video_sensitive_metadata(request, pk):
20
+ """
21
+ GET /api/media/videos/<pk>/sensitive-metadata/
22
+ PATCH /api/media/videos/<pk>/sensitive-metadata/
23
+
24
+ Get or update sensitive metadata for a video.
25
+ Video-scoped: Uses video ID to locate related sensitive metadata.
26
+ """
27
+ video = get_object_or_404(VideoFile, pk=pk)
28
+
29
+ # Get related sensitive metadata
30
+ if not video.sensitive_meta:
31
+ return Response(
32
+ {"error": f"No sensitive metadata found for video {pk}"},
33
+ status=status.HTTP_404_NOT_FOUND
34
+ )
35
+
36
+ sensitive_meta = video.sensitive_meta
37
+
38
+ if request.method == 'GET':
39
+ serializer = SensitiveMetaDetailSerializer(sensitive_meta)
40
+ return Response(serializer.data, status=status.HTTP_200_OK)
41
+
42
+ elif request.method == 'PATCH':
43
+ serializer = SensitiveMetaUpdateSerializer(
44
+ sensitive_meta,
45
+ data=request.data,
46
+ partial=True
47
+ )
48
+
49
+ if serializer.is_valid():
50
+ updated_instance = serializer.save()
51
+ response_serializer = SensitiveMetaDetailSerializer(updated_instance)
52
+
53
+ return Response({
54
+ "message": "Sensitive metadata updated successfully",
55
+ "sensitive_meta": response_serializer.data,
56
+ "video_id": pk
57
+ }, status=status.HTTP_200_OK)
58
+
59
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
60
+
61
+
62
+ @api_view(['POST'])
63
+ @permission_classes([EnvironmentAwarePermission])
64
+ @transaction.atomic
65
+ def video_sensitive_metadata_verify(request, pk):
66
+ """
67
+ POST /api/media/videos/<pk>/sensitive-metadata/verify/
68
+
69
+ Update verification state for video sensitive metadata.
70
+
71
+ Expected payload:
72
+ {
73
+ "dob_verified": true,
74
+ "names_verified": true
75
+ }
76
+ """
77
+ video = get_object_or_404(VideoFile, pk=pk)
78
+
79
+ if not video.sensitive_meta:
80
+ return Response(
81
+ {"error": f"No sensitive metadata found for video {pk}"},
82
+ status=status.HTTP_404_NOT_FOUND
83
+ )
84
+
85
+ sensitive_meta = video.sensitive_meta
86
+
87
+ dob_verified = request.data.get('dob_verified')
88
+ names_verified = request.data.get('names_verified')
89
+
90
+ if dob_verified is None and names_verified is None:
91
+ return Response(
92
+ {"error": "At least one of dob_verified or names_verified must be provided"},
93
+ status=status.HTTP_400_BAD_REQUEST
94
+ )
95
+
96
+ state = sensitive_meta.get_or_create_state()
97
+
98
+ if dob_verified is not None:
99
+ state.dob_verified = dob_verified
100
+ if names_verified is not None:
101
+ state.names_verified = names_verified
102
+
103
+ state.save()
104
+
105
+ response_serializer = SensitiveMetaDetailSerializer(sensitive_meta)
106
+ return Response({
107
+ "message": "Verification state updated successfully",
108
+ "sensitive_meta": response_serializer.data,
109
+ "video_id": pk,
110
+ "state_verified": state.is_verified
111
+ }, status=status.HTTP_200_OK)
112
+
113
+
114
+ # === PDF SENSITIVE METADATA ===
115
+
116
+ @api_view(['GET', 'PATCH'])
117
+ @permission_classes([EnvironmentAwarePermission])
118
+ def pdf_sensitive_metadata(request, pk):
119
+ """
120
+ GET /api/media/pdfs/<pk>/sensitive-metadata/
121
+ PATCH /api/media/pdfs/<pk>/sensitive-metadata/
122
+
123
+ Get or update sensitive metadata for a PDF.
124
+ PDF-scoped: Uses PDF ID to locate related sensitive metadata.
125
+ """
126
+ pdf = get_object_or_404(RawPdfFile, pk=pk)
127
+
128
+ # Get related sensitive metadata
129
+ if not pdf.sensitive_meta:
130
+ return Response(
131
+ {"error": f"No sensitive metadata found for PDF {pk}"},
132
+ status=status.HTTP_404_NOT_FOUND
133
+ )
134
+
135
+ sensitive_meta = pdf.sensitive_meta
136
+
137
+ if request.method == 'GET':
138
+ serializer = SensitiveMetaDetailSerializer(sensitive_meta)
139
+ return Response(serializer.data, status=status.HTTP_200_OK)
140
+
141
+ elif request.method == 'PATCH':
142
+ serializer = SensitiveMetaUpdateSerializer(
143
+ sensitive_meta,
144
+ data=request.data,
145
+ partial=True
146
+ )
147
+
148
+ if serializer.is_valid():
149
+ updated_instance = serializer.save()
150
+ response_serializer = SensitiveMetaDetailSerializer(updated_instance)
151
+
152
+ return Response({
153
+ "message": "Sensitive metadata updated successfully",
154
+ "sensitive_meta": response_serializer.data,
155
+ "pdf_id": pk
156
+ }, status=status.HTTP_200_OK)
157
+
158
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
159
+
160
+
161
+ @api_view(['POST'])
162
+ @permission_classes([EnvironmentAwarePermission])
163
+ @transaction.atomic
164
+ def pdf_sensitive_metadata_verify(request, pk):
165
+ """
166
+ POST /api/media/pdfs/<pk>/sensitive-metadata/verify/
167
+
168
+ Update verification state for PDF sensitive metadata.
169
+
170
+ Expected payload:
171
+ {
172
+ "dob_verified": true,
173
+ "names_verified": true
174
+ }
175
+ """
176
+ pdf = get_object_or_404(RawPdfFile, pk=pk)
177
+
178
+ if not pdf.sensitive_meta:
179
+ return Response(
180
+ {"error": f"No sensitive metadata found for PDF {pk}"},
181
+ status=status.HTTP_404_NOT_FOUND
182
+ )
183
+
184
+ sensitive_meta = pdf.sensitive_meta
185
+
186
+ dob_verified = request.data.get('dob_verified')
187
+ names_verified = request.data.get('names_verified')
188
+
189
+ if dob_verified is None and names_verified is None:
190
+ return Response(
191
+ {"error": "At least one of dob_verified or names_verified must be provided"},
192
+ status=status.HTTP_400_BAD_REQUEST
193
+ )
194
+
195
+ state = sensitive_meta.get_or_create_state()
196
+
197
+ if dob_verified is not None:
198
+ state.dob_verified = dob_verified
199
+ if names_verified is not None:
200
+ state.names_verified = names_verified
201
+
202
+ state.save()
203
+
204
+ response_serializer = SensitiveMetaDetailSerializer(sensitive_meta)
205
+ return Response({
206
+ "message": "Verification state updated successfully",
207
+ "sensitive_meta": response_serializer.data,
208
+ "pdf_id": pk,
209
+ "state_verified": state.is_verified
210
+ }, status=status.HTTP_200_OK)
211
+
212
+
213
+ # === LIST ENDPOINTS (Collection-Level) ===
214
+
215
+ @api_view(['GET'])
216
+ @permission_classes([EnvironmentAwarePermission])
217
+ def sensitive_metadata_list(request):
218
+ """
219
+ GET /api/media/sensitive-metadata/
220
+
221
+ List all sensitive metadata (combined PDFs and Videos).
222
+ Supports filtering by content_type, status, etc.
223
+
224
+ Query parameters:
225
+ - content_type: 'pdf' | 'video' (optional)
226
+ - verified: Filter by verification status
227
+ - ordering: Sort field
228
+ - search: Search in patient names
229
+ """
230
+ from endoreg_db.serializers.meta import SensitiveMetaDetailSerializer
231
+
232
+ # Get all sensitive metadata
233
+ queryset = SensitiveMeta.objects.select_related('state').all()
234
+
235
+ # Filter by content type
236
+ content_type = request.query_params.get('content_type')
237
+ if content_type == 'pdf':
238
+ # Only PDFs - filter by existence of related PDFs
239
+ queryset = queryset.filter(raw_pdf_files__isnull=False).distinct()
240
+ elif content_type == 'video':
241
+ # Only Videos - filter by existence of related video
242
+ queryset = queryset.filter(video_file__isnull=False).distinct()
243
+
244
+ # Filter by verification status
245
+ verified = request.query_params.get('verified')
246
+ if verified is not None:
247
+ verified_bool = verified.lower() in ('true', '1', 'yes')
248
+ queryset = queryset.filter(state__is_verified=verified_bool)
249
+
250
+ # Search in patient names
251
+ search = request.query_params.get('search')
252
+ if search:
253
+ queryset = queryset.filter(
254
+ Q(patient_first_name__icontains=search) |
255
+ Q(patient_last_name__icontains=search)
256
+ )
257
+
258
+ # Ordering
259
+ ordering = request.query_params.get('ordering', '-id')
260
+ queryset = queryset.order_by(ordering)
261
+
262
+ # Pagination
263
+ from rest_framework.pagination import PageNumberPagination
264
+ paginator = PageNumberPagination()
265
+ paginator.page_size = 20
266
+ page = paginator.paginate_queryset(queryset, request)
267
+
268
+ if page is not None:
269
+ serializer = SensitiveMetaDetailSerializer(page, many=True)
270
+ return paginator.get_paginated_response(serializer.data)
271
+
272
+ serializer = SensitiveMetaDetailSerializer(queryset, many=True)
273
+ return Response(serializer.data, status=status.HTTP_200_OK)
274
+
275
+
276
+ @api_view(['GET'])
277
+ @permission_classes([EnvironmentAwarePermission])
278
+ def pdf_sensitive_metadata_list(request):
279
+ """
280
+ GET /api/media/pdfs/sensitive-metadata/
281
+
282
+ List sensitive metadata for PDFs only.
283
+ Replaces legacy /api/pdf/sensitivemeta/list/
284
+ """
285
+ from endoreg_db.serializers.meta import SensitiveMetaDetailSerializer
286
+
287
+ # Get all PDFs with sensitive metadata
288
+ queryset = SensitiveMeta.objects.select_related('state').filter(
289
+ raw_pdf_files__isnull=False
290
+ ).distinct()
291
+
292
+ # Apply filters
293
+ search = request.query_params.get('search')
294
+ if search:
295
+ queryset = queryset.filter(
296
+ Q(patient_first_name__icontains=search) |
297
+ Q(patient_last_name__icontains=search)
298
+ )
299
+
300
+ ordering = request.query_params.get('ordering', '-id')
301
+ queryset = queryset.order_by(ordering)
302
+
303
+ # Pagination
304
+ from rest_framework.pagination import PageNumberPagination
305
+ paginator = PageNumberPagination()
306
+ paginator.page_size = 20
307
+ page = paginator.paginate_queryset(queryset, request)
308
+
309
+ if page is not None:
310
+ serializer = SensitiveMetaDetailSerializer(page, many=True)
311
+ return paginator.get_paginated_response(serializer.data)
312
+
313
+ serializer = SensitiveMetaDetailSerializer(queryset, many=True)
314
+ return Response(serializer.data, status=status.HTTP_200_OK)