endoreg-db 0.8.5.3__py3-none-any.whl → 0.8.5.5__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.

@@ -523,8 +523,13 @@ class VideoFile(models.Model):
523
523
  """
524
524
  Validate the metadata of the VideoFile instance.
525
525
 
526
- Called after annotation in the frontend, this method deletes the associated active file, updates the sensitive meta data with the user annotated data.
527
- It also ensures the video file is properly saved after the metadata update.
526
+ Called after annotation in the frontend, this method:
527
+ 1. Updates sensitive metadata with user-annotated data
528
+ 2. Deletes the RAW video file (keeping only the anonymized version)
529
+ 3. Marks the video as validated
530
+
531
+ **IMPORTANT:** Only the raw video is deleted. The processed (anonymized)
532
+ video is preserved as the final validated output.
528
533
  """
529
534
  from datetime import date as dt_date
530
535
 
@@ -541,9 +546,22 @@ class VideoFile(models.Model):
541
546
  }
542
547
  self.sensitive_meta = SensitiveMeta.create_from_dict(default_data)
543
548
 
544
- # Delete the active file to ensure it is reprocessed with the new metadata
545
- if self.active_file_path.exists():
546
- self.active_file_path.unlink(missing_ok=True)
549
+ # CRITICAL FIX: Delete RAW video file, not the processed (anonymized) one
550
+ # After validation, only the anonymized video should remain
551
+ from .video_file_io import _get_raw_file_path
552
+
553
+ raw_path = _get_raw_file_path(self)
554
+ if raw_path and raw_path.exists():
555
+ logger.info(f"Deleting raw video file after validation: {raw_path}")
556
+ raw_path.unlink(missing_ok=True)
557
+ # Clear the raw_file field in database (use delete() to avoid save issues)
558
+ if self.raw_file:
559
+ self.raw_file.delete(save=False)
560
+ logger.info(
561
+ f"Raw video deleted for {self.uuid}. Anonymized video preserved."
562
+ )
563
+ else:
564
+ logger.warning(f"Raw video file not found for deletion: {self.uuid}")
547
565
 
548
566
  # Update sensitive metadata with user annotations
549
567
  sensitive_meta = _update_text_metadata(
@@ -765,17 +765,37 @@ def update_sensitive_meta_from_dict(
765
765
  k: v for k, v in data.items() if k in field_names and k not in excluded_fields
766
766
  }
767
767
 
768
- # Handle potential Center update
768
+ # Handle potential Center update - accept both center_name (string) and center (object)
769
+ from ..administration import Center
770
+
771
+ center = data.get("center") # First try direct Center object
769
772
  center_name = data.get("center_name")
770
- if center_name:
773
+
774
+ if center is not None:
775
+ # Center object provided directly - validate and update
776
+ if isinstance(center, Center):
777
+ instance.center = center
778
+ logger.debug(f"Updated center from Center object: {center.name}")
779
+ else:
780
+ logger.warning(
781
+ f"Invalid center type {type(center)}, expected Center instance. Ignoring."
782
+ )
783
+ # Remove from selected_data to prevent override
784
+ selected_data.pop("center", None)
785
+ elif center_name:
786
+ # center_name string provided - resolve to Center object
771
787
  try:
772
- center = Center.objects.get(name=center_name)
773
- instance.center = center # Update center directly
788
+ center_obj = Center.objects.get(name=center_name)
789
+ instance.center = center_obj
790
+ logger.debug(f"Updated center from center_name string: {center_name}")
774
791
  except Center.DoesNotExist:
775
792
  logger.warning(
776
793
  f"Center '{center_name}' not found during update. Keeping existing center."
777
794
  )
778
- selected_data.pop("center", None) # Remove from dict if not found
795
+ else:
796
+ # Both are None/missing - remove 'center' from selected_data to preserve existing value
797
+ selected_data.pop("center", None)
798
+ # If both are None/missing, keep existing center (no update needed)
779
799
 
780
800
  # Set examiner names if provided, before calling save
781
801
  examiner_first_name = data.get("examiner_first_name")
@@ -858,6 +878,11 @@ def update_sensitive_meta_from_dict(
858
878
  # Update other attributes from selected_data
859
879
  patient_name_changed = False
860
880
  for k, v in selected_data.items():
881
+ # Skip None values to avoid overwriting existing data
882
+ if v is None:
883
+ logger.debug(f"Skipping field '{k}' during update because value is None")
884
+ continue
885
+
861
886
  # Avoid overwriting examiner names if they were just explicitly set
862
887
  if (
863
888
  k not in ["examiner_first_name", "examiner_last_name"]
@@ -1,81 +1,60 @@
1
1
  from .administration import (
2
+ ActiveModelSerializer,
3
+ AiModelSerializer,
2
4
  CenterSerializer,
3
5
  GenderSerializer,
4
- ActiveModelSerializer,
5
6
  ModelTypeSerializer,
6
- AiModelSerializer
7
7
  )
8
-
9
8
  from .examination import (
9
+ ExaminationDropdownSerializer,
10
10
  ExaminationSerializer,
11
11
  ExaminationTypeSerializer,
12
- ExaminationDropdownSerializer
13
12
  )
14
-
15
13
  from .finding import FindingSerializer
16
-
17
14
  from .finding_classification import (
18
15
  FindingClassificationChoiceSerializer,
19
- FindingClassificationSerializer
16
+ FindingClassificationSerializer,
20
17
  )
21
-
22
- from .label import (
23
- LabelSerializer,
24
- ImageClassificationAnnotationSerializer
25
- )
26
-
18
+ from .label import ImageClassificationAnnotationSerializer, LabelSerializer
27
19
  from .label_video_segment import (
28
- LabelVideoSegmentSerializer,
29
20
  LabelVideoSegmentAnnotationSerializer,
21
+ LabelVideoSegmentSerializer,
30
22
  )
31
-
32
23
  from .meta import (
33
- ReportMetaSerializer,
34
24
  PDFFileForMetaSerializer,
25
+ ReportMetaSerializer,
35
26
  SensitiveMetaDetailSerializer,
36
27
  SensitiveMetaUpdateSerializer,
37
28
  SensitiveMetaVerificationSerializer,
38
29
  VideoMetaSerializer,
39
30
  )
40
-
41
31
  from .misc import (
42
32
  FileOverviewSerializer,
43
- VoPPatientDataSerializer,
44
33
  StatsSerializer,
45
- UploadJobStatusSerializer,
34
+ TranslatableFieldMixin,
46
35
  UploadCreateResponseSerializer,
47
- TranslatableFieldMixin
48
- )
49
-
50
- from .patient import (
51
- PatientSerializer,
52
- PatientDropdownSerializer,
53
- )
54
-
55
- from .patient_examination import (
56
- PatientExaminationSerializer,
36
+ UploadJobStatusSerializer,
37
+ VoPPatientDataSerializer,
57
38
  )
58
-
39
+ from .patient import PatientDropdownSerializer, PatientSerializer
40
+ from .patient_examination import PatientExaminationSerializer
59
41
  from .patient_finding import (
60
- PatientFindingSerializer,
61
42
  PatientFindingClassificationSerializer,
62
43
  PatientFindingDetailSerializer,
63
44
  PatientFindingInterventionSerializer,
64
45
  PatientFindingListSerializer,
46
+ PatientFindingSerializer,
65
47
  PatientFindingWriteSerializer,
66
48
  )
67
-
68
- from .pdf import (
69
- RawPdfAnonyTextSerializer
70
- )
71
- from .report import (
72
- ReportListSerializer,
73
- ReportDataSerializer,
74
- SecureFileUrlSerializer
75
- )
76
-
49
+ from .pdf import RawPdfAnonyTextSerializer
50
+ from .report import ReportDataSerializer, ReportListSerializer, SecureFileUrlSerializer
77
51
  from .video.video_metadata import VideoMetadataSerializer
78
52
  from .video.video_processing_history import VideoProcessingHistorySerializer
53
+ from .video_examination import (
54
+ VideoExaminationCreateSerializer,
55
+ VideoExaminationSerializer,
56
+ VideoExaminationUpdateSerializer,
57
+ )
79
58
 
80
59
  __all__ = [
81
60
  # Administration
@@ -84,24 +63,19 @@ __all__ = [
84
63
  "ActiveModelSerializer",
85
64
  "ModelTypeSerializer",
86
65
  "AiModelSerializer",
87
-
88
66
  # Examination
89
67
  "ExaminationSerializer",
90
68
  "ExaminationTypeSerializer",
91
69
  "ExaminationDropdownSerializer",
92
-
93
70
  # Finding
94
- 'FindingSerializer',
95
- 'FindingClassificationSerializer',
71
+ "FindingSerializer",
72
+ "FindingClassificationSerializer",
96
73
  "FindingClassificationChoiceSerializer",
97
-
98
74
  "LabelSerializer",
99
75
  "ImageClassificationAnnotationSerializer",
100
-
101
76
  # LabelVideoSegment
102
77
  "LabelVideoSegmentSerializer",
103
78
  "LabelVideoSegmentAnnotationSerializer",
104
-
105
79
  # Meta
106
80
  "PDFFileForMetaSerializer",
107
81
  "ReportMetaSerializer",
@@ -109,7 +83,6 @@ __all__ = [
109
83
  "SensitiveMetaUpdateSerializer",
110
84
  "SensitiveMetaVerificationSerializer",
111
85
  "VideoMetaSerializer",
112
-
113
86
  # Misc
114
87
  "FileOverviewSerializer",
115
88
  "VoPPatientDataSerializer",
@@ -117,14 +90,11 @@ __all__ = [
117
90
  "UploadJobStatusSerializer",
118
91
  "UploadCreateResponseSerializer",
119
92
  "TranslatableFieldMixin",
120
-
121
93
  # Patient
122
94
  "PatientSerializer",
123
95
  "PatientDropdownSerializer",
124
-
125
96
  # Patient Examination
126
97
  "PatientExaminationSerializer",
127
-
128
98
  # Patient Finding
129
99
  "PatientFindingSerializer",
130
100
  "PatientFindingClassificationSerializer",
@@ -132,16 +102,17 @@ __all__ = [
132
102
  "PatientFindingInterventionSerializer",
133
103
  "PatientFindingListSerializer",
134
104
  "PatientFindingWriteSerializer",
135
-
136
105
  # PDF
137
106
  "RawPdfAnonyTextSerializer",
138
-
139
107
  # Report
140
108
  "ReportListSerializer",
141
109
  "ReportDataSerializer",
142
110
  "SecureFileUrlSerializer",
143
-
144
111
  # Video Correction (Phase 1.1)
145
112
  "VideoMetadataSerializer",
146
113
  "VideoProcessingHistorySerializer",
114
+ # Video Examination
115
+ "VideoExaminationSerializer",
116
+ "VideoExaminationCreateSerializer",
117
+ "VideoExaminationUpdateSerializer",
147
118
  ]
@@ -0,0 +1,198 @@
1
+ """
2
+ Video Examination Serializer
3
+
4
+ Serializes PatientExamination instances that are associated with VideoFile records.
5
+ This allows frontend components like VideoExaminationAnnotation.vue to display
6
+ and manage examinations within the video annotation workflow.
7
+ """
8
+
9
+ from rest_framework import serializers
10
+
11
+ from ..models import Examination, PatientExamination, VideoFile
12
+
13
+
14
+ class VideoExaminationSerializer(serializers.ModelSerializer):
15
+ """
16
+ Serializer for video-based patient examinations.
17
+
18
+ Exposes examination data within the context of video annotation:
19
+ - Basic examination metadata (type, date, hash)
20
+ - Related patient information (anonymized)
21
+ - Video reference
22
+ - Associated findings
23
+ """
24
+
25
+ # Custom fields for frontend compatibility
26
+ examination_name = serializers.CharField(source="examination.name", read_only=True)
27
+ examination_id = serializers.IntegerField(source="examination.id", read_only=True)
28
+ video_id = serializers.IntegerField(source="video.id", read_only=True)
29
+ patient_hash = serializers.CharField(source="patient.patient_hash", read_only=True)
30
+
31
+ # Nested findings data
32
+ findings = serializers.SerializerMethodField()
33
+
34
+ class Meta:
35
+ model = PatientExamination
36
+ fields = [
37
+ "id",
38
+ "hash",
39
+ "examination_id",
40
+ "examination_name",
41
+ "video_id",
42
+ "patient_hash",
43
+ "date_start",
44
+ "date_end",
45
+ "findings",
46
+ ]
47
+ read_only_fields = ["hash", "patient_hash"]
48
+
49
+ def get_findings(self, obj):
50
+ """
51
+ Return serialized findings associated with this examination.
52
+
53
+ Args:
54
+ obj: PatientExamination instance
55
+
56
+ Returns:
57
+ List of finding dictionaries with basic metadata
58
+ """
59
+ patient_findings = obj.patient_findings.all()
60
+ return [
61
+ {
62
+ "id": pf.id,
63
+ "finding_id": pf.finding.id if pf.finding else None,
64
+ "finding_name": pf.finding.name if pf.finding else None,
65
+ "created_at": pf.created_at if hasattr(pf, "created_at") else None,
66
+ }
67
+ for pf in patient_findings
68
+ ]
69
+
70
+
71
+ class VideoExaminationCreateSerializer(serializers.Serializer):
72
+ """
73
+ Serializer for creating video examinations via API.
74
+
75
+ Handles the complex creation logic required to link:
76
+ - VideoFile (must exist)
77
+ - Examination type (must exist)
78
+ - Patient (derived from video's SensitiveMeta)
79
+ - New PatientExamination record
80
+ """
81
+
82
+ video_id = serializers.IntegerField(required=True)
83
+ examination_id = serializers.IntegerField(required=True)
84
+ date_start = serializers.DateField(required=False, allow_null=True)
85
+ date_end = serializers.DateField(required=False, allow_null=True)
86
+
87
+ def validate_video_id(self, value):
88
+ """Ensure video exists"""
89
+ if not VideoFile.objects.filter(id=value).exists():
90
+ raise serializers.ValidationError(f"Video with id {value} does not exist")
91
+ return value
92
+
93
+ def validate_examination_id(self, value):
94
+ """Ensure examination type exists"""
95
+ if not Examination.objects.filter(id=value).exists():
96
+ raise serializers.ValidationError(
97
+ f"Examination with id {value} does not exist"
98
+ )
99
+ return value
100
+
101
+ def create(self, validated_data):
102
+ """
103
+ Create PatientExamination record.
104
+
105
+ Links video to examination through patient relationship:
106
+ 1. Get video and extract patient from SensitiveMeta
107
+ 2. Get examination type
108
+ 3. Create PatientExamination linking patient, examination, video
109
+
110
+ Raises:
111
+ ValidationError: If video has no patient or sensitive_meta
112
+ """
113
+ video = VideoFile.objects.get(id=validated_data["video_id"])
114
+ examination = Examination.objects.get(id=validated_data["examination_id"])
115
+
116
+ # Get patient from video's sensitive metadata
117
+ if not hasattr(video, "sensitive_meta") or not video.sensitive_meta:
118
+ raise serializers.ValidationError(
119
+ "Video must have sensitive metadata with patient information"
120
+ )
121
+
122
+ sensitive_meta = video.sensitive_meta
123
+ if not sensitive_meta.pseudo_patient:
124
+ raise serializers.ValidationError(
125
+ "Video's sensitive metadata must have an associated pseudo patient"
126
+ )
127
+
128
+ patient = sensitive_meta.pseudo_patient
129
+
130
+ # Check if PatientExamination already exists for this video
131
+ existing_exam = PatientExamination.objects.filter(video=video).first()
132
+ if existing_exam:
133
+ # Update existing
134
+ patient_exam = existing_exam
135
+ patient_exam.examination = examination
136
+ if "date_start" in validated_data:
137
+ patient_exam.date_start = validated_data["date_start"]
138
+ if "date_end" in validated_data:
139
+ patient_exam.date_end = validated_data["date_end"]
140
+ patient_exam.save()
141
+ else:
142
+ # Create new
143
+ patient_exam = PatientExamination.objects.create(
144
+ patient=patient,
145
+ examination=examination,
146
+ video=video,
147
+ date_start=validated_data.get("date_start"),
148
+ date_end=validated_data.get("date_end"),
149
+ )
150
+
151
+ return patient_exam
152
+
153
+
154
+ class VideoExaminationUpdateSerializer(serializers.Serializer):
155
+ """
156
+ Serializer for updating video examinations.
157
+
158
+ Allows modification of:
159
+ - Examination type
160
+ - Date range
161
+ - Associated findings (via separate endpoint)
162
+ """
163
+
164
+ examination_id = serializers.IntegerField(required=False)
165
+ date_start = serializers.DateField(required=False, allow_null=True)
166
+ date_end = serializers.DateField(required=False, allow_null=True)
167
+
168
+ def validate_examination_id(self, value):
169
+ """Ensure examination type exists if provided"""
170
+ if value is not None and not Examination.objects.filter(id=value).exists():
171
+ raise serializers.ValidationError(
172
+ f"Examination with id {value} does not exist"
173
+ )
174
+ return value
175
+
176
+ def update(self, instance, validated_data):
177
+ """
178
+ Update PatientExamination fields.
179
+
180
+ Args:
181
+ instance: Existing PatientExamination
182
+ validated_data: Validated update data
183
+
184
+ Returns:
185
+ Updated PatientExamination instance
186
+ """
187
+ if "examination_id" in validated_data:
188
+ examination = Examination.objects.get(id=validated_data["examination_id"])
189
+ instance.examination = examination
190
+
191
+ if "date_start" in validated_data:
192
+ instance.date_start = validated_data["date_start"]
193
+
194
+ if "date_end" in validated_data:
195
+ instance.date_end = validated_data["date_end"]
196
+
197
+ instance.save()
198
+ return instance
@@ -1,36 +1,39 @@
1
- from django.urls import path, include
2
1
  from django.conf import settings
3
2
  from django.conf.urls.static import static
3
+ from django.urls import include, path
4
4
  from rest_framework.routers import DefaultRouter
5
5
 
6
- # Phase 1.2: Media Management URLs ✅ IMPLEMENTED
7
- from .media import urlpatterns as media_url_patterns
8
-
9
6
  from endoreg_db.views import (
10
- VideoViewSet,
11
7
  ExaminationViewSet,
12
- VideoExaminationViewSet,
8
+ FindingClassificationViewSet,
13
9
  FindingViewSet,
14
- FindingClassificationViewSet,
10
+ PatientExaminationViewSet,
15
11
  PatientFindingViewSet,
16
- PatientExaminationViewSet
12
+ VideoExaminationViewSet,
13
+ VideoViewSet,
17
14
  )
18
15
 
19
16
  from .anonymization import url_patterns as anonymization_url_patterns
20
- from .classification import url_patterns as classification_url_patterns
21
17
  from .auth import urlpatterns as auth_url_patterns
18
+ from .classification import url_patterns as classification_url_patterns
22
19
  from .examination import urlpatterns as examination_url_patterns
23
20
  from .files import urlpatterns as files_url_patterns
21
+ from .label_video_segment_validate import (
22
+ url_patterns as label_video_segment_validate_url_patterns,
23
+ )
24
24
  from .label_video_segments import url_patterns as label_video_segments_url_patterns
25
- from .label_video_segment_validate import url_patterns as label_video_segment_validate_url_patterns
25
+
26
+ # Phase 1.2: Media Management URLs ✅ IMPLEMENTED
27
+ from .media import urlpatterns as media_url_patterns
28
+ from .patient import urlpatterns as patient_url_patterns
29
+
26
30
  # TODO Phase 1.2: Implement VideoMediaView and PDFMediaView before enabling
27
31
  # from .media import urlpatterns as media_url_patterns
28
32
  from .report import url_patterns as report_url_patterns
29
- from .upload import urlpatterns as upload_url_patterns
30
- from .video import url_patterns as video_url_patterns
31
33
  from .requirements import urlpatterns as requirements_url_patterns
32
- from .patient import urlpatterns as patient_url_patterns
33
34
  from .stats import url_patterns as stats_url_patterns
35
+ from .upload import urlpatterns as upload_url_patterns
36
+ from .video import url_patterns as video_url_patterns
34
37
 
35
38
  api_urls = []
36
39
  api_urls += classification_url_patterns
@@ -50,21 +53,31 @@ api_urls += patient_url_patterns
50
53
  api_urls += stats_url_patterns
51
54
 
52
55
  router = DefaultRouter()
53
- router.register(r'videos', VideoViewSet, basename='videos')
54
- router.register(r'examinations', ExaminationViewSet)
55
- router.register(r'video-examinations', VideoExaminationViewSet, basename='video-examinations')
56
- router.register(r'findings', FindingViewSet)
57
- router.register(r'classifications', FindingClassificationViewSet)
58
- router.register(r'patient-findings', PatientFindingViewSet)
59
- router.register(r'patient-examinations', PatientExaminationViewSet)
56
+ router.register(r"videos", VideoViewSet, basename="videos")
57
+ router.register(r"examinations", ExaminationViewSet)
58
+ router.register(
59
+ r"video-examinations", VideoExaminationViewSet, basename="video-examinations"
60
+ )
61
+ router.register(r"findings", FindingViewSet)
62
+ router.register(r"classifications", FindingClassificationViewSet)
63
+ router.register(r"patient-findings", PatientFindingViewSet)
64
+ router.register(r"patient-examinations", PatientExaminationViewSet)
65
+
66
+ # Additional custom video examination routes
67
+ # Frontend expects: GET /api/video/{id}/examinations/
68
+ video_examinations_list = VideoExaminationViewSet.as_view({"get": "by_video"})
60
69
 
61
70
  # Export raw API urlpatterns (no prefix). The project-level endoreg_db/urls.py mounts these under /api/.
62
71
  urlpatterns = [
63
- path('', include(api_urls)), # Specific routes first
64
- path('', include(router.urls)), # Generic router routes second
72
+ path(
73
+ "video/<int:video_id>/examinations/",
74
+ video_examinations_list,
75
+ name="video-examinations-by-video",
76
+ ),
77
+ path("", include(api_urls)), # Specific routes first
78
+ path("", include(router.urls)), # Generic router routes second
65
79
  ]
66
80
 
67
81
  if settings.DEBUG:
68
82
  urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
69
83
  urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
70
-