endoreg-db 0.8.3.7__py3-none-any.whl → 0.8.6.3__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.
- endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +23 -1
- endoreg_db/data/setup_config.yaml +38 -0
- endoreg_db/management/commands/create_model_meta_from_huggingface.py +19 -5
- endoreg_db/management/commands/load_ai_model_data.py +18 -15
- endoreg_db/management/commands/setup_endoreg_db.py +218 -33
- endoreg_db/models/media/pdf/raw_pdf.py +241 -97
- endoreg_db/models/media/video/pipe_1.py +30 -33
- endoreg_db/models/media/video/video_file.py +300 -187
- endoreg_db/models/medical/hardware/endoscopy_processor.py +10 -1
- endoreg_db/models/metadata/model_meta_logic.py +63 -43
- endoreg_db/models/metadata/sensitive_meta_logic.py +251 -25
- endoreg_db/serializers/__init__.py +26 -55
- endoreg_db/serializers/misc/__init__.py +1 -1
- endoreg_db/serializers/misc/file_overview.py +65 -35
- endoreg_db/serializers/misc/{vop_patient_data.py → sensitive_patient_data.py} +1 -1
- endoreg_db/serializers/video_examination.py +198 -0
- endoreg_db/services/lookup_service.py +228 -58
- endoreg_db/services/lookup_store.py +174 -30
- endoreg_db/services/pdf_import.py +585 -282
- endoreg_db/services/video_import.py +485 -242
- endoreg_db/urls/__init__.py +36 -23
- endoreg_db/urls/label_video_segments.py +2 -0
- endoreg_db/urls/media.py +3 -2
- endoreg_db/utils/setup_config.py +177 -0
- endoreg_db/views/__init__.py +5 -3
- endoreg_db/views/media/pdf_media.py +3 -1
- endoreg_db/views/media/video_media.py +1 -1
- endoreg_db/views/media/video_segments.py +187 -259
- endoreg_db/views/pdf/__init__.py +5 -8
- endoreg_db/views/pdf/pdf_stream.py +187 -0
- endoreg_db/views/pdf/reimport.py +110 -94
- endoreg_db/views/requirement/lookup.py +171 -287
- endoreg_db/views/video/__init__.py +0 -2
- endoreg_db/views/video/video_examination_viewset.py +202 -289
- {endoreg_db-0.8.3.7.dist-info → endoreg_db-0.8.6.3.dist-info}/METADATA +1 -2
- {endoreg_db-0.8.3.7.dist-info → endoreg_db-0.8.6.3.dist-info}/RECORD +38 -37
- endoreg_db/views/pdf/pdf_media.py +0 -239
- endoreg_db/views/pdf/pdf_stream_views.py +0 -127
- endoreg_db/views/video/video_media.py +0 -158
- {endoreg_db-0.8.3.7.dist-info → endoreg_db-0.8.6.3.dist-info}/WHEEL +0 -0
- {endoreg_db-0.8.3.7.dist-info → endoreg_db-0.8.6.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
-
|
|
34
|
+
TranslatableFieldMixin,
|
|
46
35
|
UploadCreateResponseSerializer,
|
|
47
|
-
|
|
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 .
|
|
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
|
-
|
|
95
|
-
|
|
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
|
]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from .file_overview import FileOverviewSerializer
|
|
2
|
-
from .
|
|
2
|
+
from .sensitive_patient_data import VoPPatientDataSerializer
|
|
3
3
|
from .stats import StatsSerializer
|
|
4
4
|
from .upload_job import UploadJobStatusSerializer, UploadCreateResponseSerializer
|
|
5
5
|
from .translatable_field_mix_in import TranslatableFieldMixin
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
from rest_framework import serializers
|
|
2
1
|
from typing import TYPE_CHECKING
|
|
3
|
-
|
|
4
|
-
from
|
|
5
|
-
|
|
2
|
+
|
|
3
|
+
from rest_framework import serializers
|
|
4
|
+
|
|
5
|
+
from endoreg_db.models.media import RawPdfFile, VideoFile
|
|
6
|
+
from endoreg_db.models.state.raw_pdf import (
|
|
7
|
+
AnonymizationStatus as PdfAnonymizationStatus,
|
|
8
|
+
)
|
|
9
|
+
from endoreg_db.models.state.video import (
|
|
10
|
+
AnonymizationStatus as VideoAnonymizationStatus,
|
|
11
|
+
)
|
|
6
12
|
|
|
7
13
|
if TYPE_CHECKING:
|
|
8
14
|
pass
|
|
9
15
|
|
|
16
|
+
|
|
10
17
|
class FileOverviewSerializer(serializers.Serializer):
|
|
11
18
|
"""
|
|
12
19
|
Polymorphic "union" serializer – we normalise both model types
|
|
@@ -22,75 +29,81 @@ class FileOverviewSerializer(serializers.Serializer):
|
|
|
22
29
|
annotationStatus = serializers.CharField(read_only=True)
|
|
23
30
|
createdAt = serializers.DateTimeField(read_only=True)
|
|
24
31
|
text = serializers.CharField(required=False, allow_blank=True, read_only=True)
|
|
25
|
-
anonymizedText = serializers.CharField(
|
|
32
|
+
anonymizedText = serializers.CharField(
|
|
33
|
+
required=False, allow_blank=True, read_only=True
|
|
34
|
+
)
|
|
26
35
|
|
|
27
36
|
# --- converting DB objects to that shape -----------------------
|
|
28
37
|
def to_representation(self, instance):
|
|
29
38
|
"""
|
|
30
39
|
Return a unified dictionary representation of either a VideoFile or RawPdfFile instance for front-end use.
|
|
31
|
-
|
|
40
|
+
|
|
32
41
|
For VideoFile instances, extracts and structures metadata such as patient, examination, equipment, and examiner information, and generates an anonymized version of the text by replacing sensitive fields with placeholders. For RawPdfFile instances, extracts text and anonymized text directly and determines statuses based on available fields.
|
|
33
|
-
|
|
42
|
+
|
|
34
43
|
Parameters:
|
|
35
44
|
instance: A VideoFile or RawPdfFile object to be serialized.
|
|
36
|
-
|
|
45
|
+
|
|
37
46
|
Returns:
|
|
38
47
|
dict: A normalized dictionary containing id, filename, mediaType, anonymizationStatus, annotationStatus, createdAt, text, and anonymizedText fields.
|
|
39
|
-
|
|
48
|
+
|
|
40
49
|
Raises:
|
|
41
50
|
TypeError: If the instance is not a VideoFile or RawPdfFile.
|
|
42
51
|
"""
|
|
43
52
|
text = ""
|
|
44
53
|
anonym_text = ""
|
|
45
|
-
|
|
54
|
+
|
|
46
55
|
if isinstance(instance, VideoFile):
|
|
47
56
|
media_type = "video"
|
|
48
57
|
created_at = instance.uploaded_at
|
|
49
58
|
filename = instance.original_file_name or (
|
|
50
|
-
instance.raw_file.name.split("/")[-1]
|
|
59
|
+
instance.raw_file.name.split("/")[-1]
|
|
60
|
+
if instance.raw_file
|
|
61
|
+
else "unknown"
|
|
51
62
|
)
|
|
52
|
-
|
|
63
|
+
|
|
53
64
|
# ------- anonymization status using VideoState model
|
|
54
65
|
vs = instance.state
|
|
55
|
-
anonym_status =
|
|
56
|
-
|
|
66
|
+
anonym_status = (
|
|
67
|
+
vs.anonymization_status if vs else VideoAnonymizationStatus.NOT_STARTED
|
|
68
|
+
)
|
|
69
|
+
|
|
57
70
|
# ------- annotation status (validated label segments)
|
|
58
71
|
if instance.label_video_segments.filter(state__is_validated=True).exists():
|
|
59
72
|
annot_status = "done"
|
|
60
73
|
else:
|
|
61
74
|
annot_status = "not_started"
|
|
62
|
-
|
|
75
|
+
|
|
63
76
|
# ------- Extract text from sensitive_meta for videos
|
|
64
77
|
if instance.sensitive_meta:
|
|
65
78
|
sm = instance.sensitive_meta
|
|
66
79
|
# Create a structured text representation from sensitive meta
|
|
67
80
|
text_parts = []
|
|
68
|
-
|
|
81
|
+
|
|
69
82
|
# Patient information
|
|
70
83
|
if sm.patient_first_name or sm.patient_last_name:
|
|
71
84
|
patient_name = f"{sm.patient_first_name or ''} {sm.patient_last_name or ''}".strip()
|
|
72
85
|
text_parts.append(f"Patient: {patient_name}")
|
|
73
|
-
|
|
86
|
+
|
|
74
87
|
if sm.patient_dob:
|
|
75
88
|
text_parts.append(f"Date of Birth: {sm.patient_dob.date()}")
|
|
76
|
-
|
|
89
|
+
|
|
77
90
|
if sm.patient_gender:
|
|
78
91
|
text_parts.append(f"Gender: {sm.patient_gender}")
|
|
79
|
-
|
|
92
|
+
|
|
80
93
|
# Examination information
|
|
81
94
|
if sm.examination_date:
|
|
82
95
|
text_parts.append(f"Examination Date: {sm.examination_date}")
|
|
83
|
-
|
|
96
|
+
|
|
84
97
|
if sm.center:
|
|
85
98
|
text_parts.append(f"Center: {sm.center.name}")
|
|
86
|
-
|
|
99
|
+
|
|
87
100
|
# Equipment information
|
|
88
101
|
if sm.endoscope_type:
|
|
89
102
|
text_parts.append(f"Endoscope Type: {sm.endoscope_type}")
|
|
90
|
-
|
|
103
|
+
|
|
91
104
|
if sm.endoscope_sn:
|
|
92
105
|
text_parts.append(f"Endoscope SN: {sm.endoscope_sn}")
|
|
93
|
-
|
|
106
|
+
|
|
94
107
|
# Examiner information
|
|
95
108
|
if sm.pk: # Only if saved
|
|
96
109
|
try:
|
|
@@ -100,39 +113,56 @@ class FileOverviewSerializer(serializers.Serializer):
|
|
|
100
113
|
text_parts.append(f"Examiners: {examiner_names}")
|
|
101
114
|
except Exception:
|
|
102
115
|
pass # Ignore examiner lookup errors
|
|
103
|
-
|
|
116
|
+
|
|
104
117
|
text = "\n".join(text_parts)
|
|
105
|
-
|
|
118
|
+
|
|
106
119
|
# Create anonymized version by replacing sensitive data
|
|
107
120
|
anonym_text = text
|
|
108
121
|
if sm.patient_first_name:
|
|
109
|
-
anonym_text = anonym_text.replace(
|
|
122
|
+
anonym_text = anonym_text.replace(
|
|
123
|
+
sm.patient_first_name, "[FIRST_NAME]"
|
|
124
|
+
)
|
|
110
125
|
if sm.patient_last_name:
|
|
111
|
-
anonym_text = anonym_text.replace(
|
|
126
|
+
anonym_text = anonym_text.replace(
|
|
127
|
+
sm.patient_last_name, "[LAST_NAME]"
|
|
128
|
+
)
|
|
112
129
|
if sm.patient_dob:
|
|
113
|
-
anonym_text = anonym_text.replace(
|
|
130
|
+
anonym_text = anonym_text.replace(
|
|
131
|
+
str(sm.patient_dob.date()), "[DOB]"
|
|
132
|
+
)
|
|
114
133
|
if sm.endoscope_sn:
|
|
115
134
|
anonym_text = anonym_text.replace(sm.endoscope_sn, "[ENDOSCOPE_SN]")
|
|
116
|
-
|
|
135
|
+
|
|
117
136
|
# Replace examiner names if available
|
|
118
137
|
if sm.pk:
|
|
119
138
|
try:
|
|
120
139
|
examiners = list(sm.examiners.all())
|
|
121
140
|
for examiner in examiners:
|
|
122
|
-
anonym_text = anonym_text.replace(
|
|
141
|
+
anonym_text = anonym_text.replace(
|
|
142
|
+
str(examiner), "[EXAMINER]"
|
|
143
|
+
)
|
|
123
144
|
except Exception:
|
|
124
145
|
pass
|
|
125
146
|
|
|
126
147
|
elif isinstance(instance, RawPdfFile):
|
|
127
|
-
instance:RawPdfFile
|
|
148
|
+
instance: RawPdfFile
|
|
128
149
|
media_type = "pdf"
|
|
129
150
|
created_at = instance.date_created
|
|
130
151
|
filename = instance.file.name.split("/")[-1] if instance.file else "unknown"
|
|
131
|
-
|
|
132
|
-
|
|
152
|
+
|
|
153
|
+
# ------- anonymization status using RawPdfState model (like VideoFile)
|
|
154
|
+
ps = instance.state
|
|
155
|
+
anonym_status = (
|
|
156
|
+
ps.anonymization_status if ps else PdfAnonymizationStatus.NOT_STARTED
|
|
157
|
+
)
|
|
158
|
+
|
|
133
159
|
# PDF annotation == "sensitive meta validated"
|
|
134
|
-
annot_status =
|
|
135
|
-
|
|
160
|
+
annot_status = (
|
|
161
|
+
"done"
|
|
162
|
+
if getattr(instance.sensitive_meta, "is_verified", False)
|
|
163
|
+
else "not_started"
|
|
164
|
+
)
|
|
165
|
+
|
|
136
166
|
# Extract text content from PDF
|
|
137
167
|
text = instance.text or ""
|
|
138
168
|
anonym_text = instance.anonymized_text or ""
|
|
@@ -80,7 +80,7 @@ class VoPPatientDataSerializer(serializers.Serializer):
|
|
|
80
80
|
|
|
81
81
|
elif isinstance(instance, RawPdfFile):
|
|
82
82
|
# Generate PDF streaming URL using pdf_id (RawPdfFile.id)
|
|
83
|
-
pdf_stream_url = f"/api/
|
|
83
|
+
pdf_stream_url = f"/api/media/pdfs/{instance.pk}/stream/"
|
|
84
84
|
|
|
85
85
|
return {
|
|
86
86
|
"id": instance.pk,
|
|
@@ -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
|