endoreg-db 0.8.2.3__py3-none-any.whl → 0.8.2.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.
- endoreg_db/helpers/default_objects.py +48 -29
- endoreg_db/management/commands/import_video.py +5 -4
- endoreg_db/models/__init__.py +4 -4
- endoreg_db/models/media/__init__.py +3 -1
- endoreg_db/models/media/video/__init__.py +4 -0
- endoreg_db/models/media/video/video_file.py +53 -44
- endoreg_db/models/{video_metadata.py → media/video/video_metadata.py} +1 -2
- endoreg_db/models/{video_processing.py → media/video/video_processing.py} +1 -2
- endoreg_db/models/medical/hardware/endoscopy_processor.py +28 -10
- endoreg_db/models/metadata/sensitive_meta.py +8 -8
- endoreg_db/serializers/video/video_metadata.py +1 -1
- endoreg_db/services/pseudonym_service.py +1 -1
- endoreg_db/services/video_import.py +275 -410
- endoreg_db/urls/media.py +6 -9
- endoreg_db/utils/paths.py +15 -16
- endoreg_db/views/__init__.py +3 -13
- endoreg_db/views/video/__init__.py +2 -5
- endoreg_db/views/video/correction.py +35 -179
- endoreg_db/views/video/video_correction.py +1 -1
- {endoreg_db-0.8.2.3.dist-info → endoreg_db-0.8.2.5.dist-info}/METADATA +2 -2
- {endoreg_db-0.8.2.3.dist-info → endoreg_db-0.8.2.5.dist-info}/RECORD +23 -26
- endoreg_db/models/media/video/video_file_meta.py +0 -11
- endoreg_db/services/ollama_api_docs.py +0 -1528
- endoreg_db/views/video/video_reprocess.py +0 -40
- {endoreg_db-0.8.2.3.dist-info → endoreg_db-0.8.2.5.dist-info}/WHEEL +0 -0
- {endoreg_db-0.8.2.3.dist-info → endoreg_db-0.8.2.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import random
|
|
2
|
+
from typing import Optional
|
|
2
3
|
from endoreg_db.models import (
|
|
3
4
|
Center,
|
|
4
5
|
Gender,
|
|
@@ -17,6 +18,7 @@ import shutil
|
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from django.conf import settings # Import settings
|
|
19
20
|
from django.core.files.storage import default_storage # Import default storage
|
|
21
|
+
from django.db.models.fields.files import FieldFile
|
|
20
22
|
|
|
21
23
|
from endoreg_db.utils import (
|
|
22
24
|
create_mock_patient_name,
|
|
@@ -44,6 +46,10 @@ DEFAULT_INDICATIONS = [
|
|
|
44
46
|
DEFAULT_SEGMENTATION_MODEL_NAME = "image_multilabel_classification_colonoscopy_default"
|
|
45
47
|
|
|
46
48
|
DEFAULT_GENDER = "unknown"
|
|
49
|
+
DEFAULT_PATIENT_FIRST_NAME = "TestFirst"
|
|
50
|
+
DEFAULT_PATIENT_LAST_NAME = "TestLast"
|
|
51
|
+
DEFAULT_PATIENT_GENDER_NAME = "female"
|
|
52
|
+
DEFAULT_PATIENT_BIRTH_DATE = date(1970, 1, 1)
|
|
47
53
|
|
|
48
54
|
def get_information_source_prediction():
|
|
49
55
|
"""
|
|
@@ -170,33 +176,41 @@ def get_default_center() -> Center:
|
|
|
170
176
|
return center
|
|
171
177
|
|
|
172
178
|
def generate_patient(**kwargs) -> Patient:
|
|
173
|
-
"""
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
# Set default values
|
|
185
|
-
gender = kwargs.get("gender", get_random_gender())
|
|
186
|
-
if not isinstance(gender, Gender):
|
|
179
|
+
"""Create a Patient with deterministic defaults unless ``randomize=True`` is supplied."""
|
|
180
|
+
|
|
181
|
+
randomize = kwargs.pop("randomize", False)
|
|
182
|
+
|
|
183
|
+
gender = kwargs.get("gender")
|
|
184
|
+
if gender is None:
|
|
185
|
+
if randomize:
|
|
186
|
+
gender = get_random_gender()
|
|
187
|
+
else:
|
|
188
|
+
gender = Gender.objects.get(name=DEFAULT_PATIENT_GENDER_NAME)
|
|
189
|
+
elif not isinstance(gender, Gender):
|
|
187
190
|
gender = Gender.objects.get(name=gender)
|
|
188
191
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
first_name
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
192
|
+
first_name = kwargs.get("first_name")
|
|
193
|
+
last_name = kwargs.get("last_name")
|
|
194
|
+
if first_name is None or last_name is None:
|
|
195
|
+
if randomize:
|
|
196
|
+
generated_first, generated_last = create_mock_patient_name(gender=gender.name)
|
|
197
|
+
else:
|
|
198
|
+
generated_first, generated_last = DEFAULT_PATIENT_FIRST_NAME, DEFAULT_PATIENT_LAST_NAME
|
|
199
|
+
first_name = first_name or generated_first
|
|
200
|
+
last_name = last_name or generated_last
|
|
201
|
+
|
|
202
|
+
dob = kwargs.get("dob")
|
|
203
|
+
if dob is None:
|
|
204
|
+
birth_date = kwargs.get("birth_date", DEFAULT_PATIENT_BIRTH_DATE)
|
|
205
|
+
if isinstance(birth_date, date):
|
|
206
|
+
dob = birth_date
|
|
207
|
+
else:
|
|
208
|
+
dob = date.fromisoformat(str(birth_date))
|
|
209
|
+
|
|
210
|
+
center = kwargs.get("center")
|
|
197
211
|
if center is None:
|
|
198
212
|
center = get_default_center()
|
|
199
|
-
|
|
213
|
+
elif not isinstance(center, Center):
|
|
200
214
|
center = Center.objects.get(name=center)
|
|
201
215
|
|
|
202
216
|
patient = Patient(
|
|
@@ -261,6 +275,7 @@ def get_default_egd_pdf():
|
|
|
261
275
|
shutil.copy(egd_path, temp_file_path)
|
|
262
276
|
|
|
263
277
|
pdf_file = None
|
|
278
|
+
file_field: Optional[FieldFile] = None
|
|
264
279
|
try:
|
|
265
280
|
# Create the PDF record using the temporary file.
|
|
266
281
|
# delete_source=True will ensure temp_file_path is deleted by create_from_file
|
|
@@ -275,8 +290,11 @@ def get_default_egd_pdf():
|
|
|
275
290
|
raise RuntimeError("Failed to create PDF file object")
|
|
276
291
|
|
|
277
292
|
# Use storage API to check existence
|
|
278
|
-
|
|
279
|
-
|
|
293
|
+
file_field = pdf_file.file
|
|
294
|
+
if not isinstance(file_field, FieldFile):
|
|
295
|
+
raise RuntimeError("RawPdfFile.file did not return a FieldFile instance")
|
|
296
|
+
if not default_storage.exists(file_field.path):
|
|
297
|
+
raise RuntimeError(f"PDF file does not exist in storage at {file_field.path}")
|
|
280
298
|
|
|
281
299
|
# Check that the source temp file was deleted
|
|
282
300
|
if temp_file_path.exists():
|
|
@@ -308,10 +326,11 @@ def get_default_egd_pdf():
|
|
|
308
326
|
|
|
309
327
|
# pdf_file.file.path might fail if storage doesn't support direct paths (like S3)
|
|
310
328
|
# Prefer using storage API for checks. Logging path if available.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
329
|
+
if file_field is not None:
|
|
330
|
+
try:
|
|
331
|
+
logger.info(f"PDF file created: {file_field.name}, Path: {file_field.path}")
|
|
332
|
+
except NotImplementedError:
|
|
333
|
+
logger.info(f"PDF file created: {file_field.name}, Path: (Not available from storage)")
|
|
315
334
|
|
|
316
335
|
|
|
317
336
|
return pdf_file
|
|
@@ -294,10 +294,11 @@ class Command(BaseCommand):
|
|
|
294
294
|
|
|
295
295
|
# Updated to handle new return signature (path, metadata)
|
|
296
296
|
cleaned_video_path, extracted_metadata = frame_cleaner.clean_video(
|
|
297
|
-
Path(video_file_obj.raw_file.path),
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
297
|
+
video_path=Path(video_file_obj.raw_file.path),
|
|
298
|
+
endoscope_roi=video_file_obj.processor.get_roi_endoscope_image() if video_file_obj.processor else None,
|
|
299
|
+
endoscope_data_roi_nested=video_file_obj.processor.get_rois() if video_file_obj.processor else None,
|
|
300
|
+
output_path=video_file_obj.get_processed_file_path(),
|
|
301
|
+
technique="mask_overlay" # Use mask overlay technique as default, if not set this will be inferred.
|
|
301
302
|
)
|
|
302
303
|
|
|
303
304
|
# Save the cleaned video using Django's FileField
|
endoreg_db/models/__init__.py
CHANGED
|
@@ -60,12 +60,10 @@ from .media import (
|
|
|
60
60
|
AnonymHistologyReport,
|
|
61
61
|
ReportReaderConfig,
|
|
62
62
|
ReportReaderFlag,
|
|
63
|
+
VideoMetadata,
|
|
64
|
+
VideoProcessingHistory,
|
|
63
65
|
)
|
|
64
66
|
|
|
65
|
-
####### Video Correction (Phase 1.1) ########
|
|
66
|
-
from .video_metadata import VideoMetadata
|
|
67
|
-
from .video_processing import VideoProcessingHistory
|
|
68
|
-
|
|
69
67
|
######## Medical ########
|
|
70
68
|
from .medical import (
|
|
71
69
|
Disease,
|
|
@@ -241,6 +239,8 @@ __all__ = [
|
|
|
241
239
|
"AnonymHistologyReport",
|
|
242
240
|
'ReportReaderConfig',
|
|
243
241
|
'ReportReaderFlag',
|
|
242
|
+
'VideoMetadata',
|
|
243
|
+
'VideoProcessingHistory',
|
|
244
244
|
|
|
245
245
|
|
|
246
246
|
######## Medical ########
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from .video import VideoFile
|
|
1
|
+
from .video import VideoFile, VideoMetadata, VideoProcessingHistory
|
|
2
2
|
from .frame import Frame
|
|
3
3
|
from .pdf import RawPdfFile, DocumentType, AnonymExaminationReport, ReportReaderConfig, ReportReaderFlag, AnonymHistologyReport
|
|
4
4
|
|
|
@@ -11,4 +11,6 @@ __all__ = [
|
|
|
11
11
|
"AnonymHistologyReport",
|
|
12
12
|
'ReportReaderConfig',
|
|
13
13
|
'ReportReaderFlag',
|
|
14
|
+
'VideoMetadata',
|
|
15
|
+
'VideoProcessingHistory',
|
|
14
16
|
]
|
|
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Optional, Union, cast
|
|
|
7
7
|
|
|
8
8
|
from django.db import models
|
|
9
9
|
from django.core.files import File
|
|
10
|
+
from django.db.models.fields.files import FieldFile
|
|
10
11
|
from django.core.validators import FileExtensionValidator
|
|
11
12
|
from django.db.models import F
|
|
12
13
|
from endoreg_db.utils.calc_duration_seconds import _calc_duration_vf
|
|
@@ -127,36 +128,36 @@ class VideoFile(models.Model):
|
|
|
127
128
|
sensitive_meta = models.OneToOneField(
|
|
128
129
|
"SensitiveMeta", on_delete=models.SET_NULL, null=True, blank=True, related_name="video_file"
|
|
129
130
|
) # type: ignore
|
|
130
|
-
center = models.ForeignKey("Center", on_delete=models.PROTECT)
|
|
131
|
+
center = models.ForeignKey("Center", on_delete=models.PROTECT) # type: ignore
|
|
131
132
|
processor = models.ForeignKey(
|
|
132
133
|
"EndoscopyProcessor", on_delete=models.PROTECT, blank=True, null=True
|
|
133
|
-
)
|
|
134
|
+
) # type: ignore
|
|
134
135
|
video_meta = models.OneToOneField(
|
|
135
136
|
"VideoMeta", on_delete=models.SET_NULL, null=True, blank=True, related_name="video_file"
|
|
136
|
-
)
|
|
137
|
+
) # type: ignore
|
|
137
138
|
examination = models.ForeignKey(
|
|
138
139
|
"PatientExamination",
|
|
139
140
|
on_delete=models.SET_NULL,
|
|
140
141
|
blank=True,
|
|
141
142
|
null=True,
|
|
142
143
|
related_name="video_files",
|
|
143
|
-
)
|
|
144
|
+
) # type: ignore
|
|
144
145
|
patient = models.ForeignKey(
|
|
145
146
|
"Patient",
|
|
146
147
|
on_delete=models.SET_NULL,
|
|
147
148
|
blank=True,
|
|
148
149
|
null=True,
|
|
149
150
|
related_name="video_files",
|
|
150
|
-
)
|
|
151
|
+
) # type: ignore
|
|
151
152
|
ai_model_meta = models.ForeignKey(
|
|
152
153
|
"ModelMeta", on_delete=models.SET_NULL, blank=True, null=True
|
|
153
|
-
)
|
|
154
|
+
) # type: ignore
|
|
154
155
|
state = models.OneToOneField(
|
|
155
156
|
"VideoState", on_delete=models.SET_NULL, null=True, blank=True, related_name="video_file"
|
|
156
|
-
)
|
|
157
|
+
) # type: ignore
|
|
157
158
|
import_meta = models.OneToOneField(
|
|
158
159
|
"VideoImportMeta", on_delete=models.CASCADE, blank=True, null=True
|
|
159
|
-
)
|
|
160
|
+
) # type: ignore
|
|
160
161
|
|
|
161
162
|
original_file_name = models.CharField(max_length=255, blank=True, null=True)
|
|
162
163
|
uploaded_at = models.DateTimeField(auto_now_add=True)
|
|
@@ -197,7 +198,9 @@ class VideoFile(models.Model):
|
|
|
197
198
|
"""
|
|
198
199
|
from endoreg_db.models import FFMpegMeta
|
|
199
200
|
if self.video_meta is not None:
|
|
200
|
-
|
|
201
|
+
if self.video_meta.ffmpeg_meta is not None:
|
|
202
|
+
return self.video_meta.ffmpeg_meta
|
|
203
|
+
raise AssertionError("Expected FFMpegMeta instance.")
|
|
201
204
|
else:
|
|
202
205
|
self.initialize_video_specs()
|
|
203
206
|
ffmpeg_meta = self.video_meta.ffmpeg_meta if self.video_meta else None
|
|
@@ -216,20 +219,19 @@ class VideoFile(models.Model):
|
|
|
216
219
|
Raises:
|
|
217
220
|
Value Error if no active VideoFile is available.
|
|
218
221
|
"""
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
return url
|
|
222
|
+
active = self.active_file
|
|
223
|
+
if not isinstance(active, FieldFile):
|
|
224
|
+
raise ValueError("Active file is not a stored FieldFile instance.")
|
|
225
|
+
if not active.name:
|
|
226
|
+
raise ValueError("Active file has no associated name.")
|
|
227
|
+
return active.url
|
|
226
228
|
|
|
227
229
|
@property
|
|
228
|
-
def active_raw_file(self) ->
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
230
|
+
def active_raw_file(self) -> FieldFile:
|
|
231
|
+
raw = self.raw_file
|
|
232
|
+
if isinstance(raw, FieldFile) and raw.name:
|
|
233
|
+
return raw
|
|
234
|
+
raise ValueError("No raw file available for this video")
|
|
233
235
|
|
|
234
236
|
@property
|
|
235
237
|
def active_raw_file_url(self)-> str:
|
|
@@ -241,12 +243,10 @@ class VideoFile(models.Model):
|
|
|
241
243
|
|
|
242
244
|
Returns:
|
|
243
245
|
"""
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
url = _file.url
|
|
249
|
-
return url
|
|
246
|
+
raw = self.active_raw_file
|
|
247
|
+
if not raw.name:
|
|
248
|
+
raise ValueError("Active raw file has no associated name.")
|
|
249
|
+
return raw.url
|
|
250
250
|
|
|
251
251
|
|
|
252
252
|
# Pipeline Functions
|
|
@@ -363,7 +363,7 @@ class VideoFile(models.Model):
|
|
|
363
363
|
|
|
364
364
|
|
|
365
365
|
@property
|
|
366
|
-
def active_file(self) ->
|
|
366
|
+
def active_file(self) -> FieldFile:
|
|
367
367
|
"""
|
|
368
368
|
Return the active video file, preferring the processed file if available.
|
|
369
369
|
|
|
@@ -373,12 +373,15 @@ class VideoFile(models.Model):
|
|
|
373
373
|
Raises:
|
|
374
374
|
ValueError: If neither a processed nor a raw file is available.
|
|
375
375
|
"""
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
376
|
+
processed = self.processed_file
|
|
377
|
+
if isinstance(processed, FieldFile) and processed.name:
|
|
378
|
+
return processed
|
|
379
|
+
|
|
380
|
+
raw = self.raw_file
|
|
381
|
+
if isinstance(raw, FieldFile) and raw.name:
|
|
382
|
+
return raw
|
|
383
|
+
|
|
384
|
+
raise ValueError("No active file available. VideoFile has neither raw nor processed file.")
|
|
382
385
|
|
|
383
386
|
|
|
384
387
|
@property
|
|
@@ -393,13 +396,17 @@ class VideoFile(models.Model):
|
|
|
393
396
|
ValueError: If neither a processed nor raw file is present.
|
|
394
397
|
"""
|
|
395
398
|
active = self.active_file
|
|
396
|
-
if active
|
|
397
|
-
|
|
398
|
-
elif active
|
|
399
|
-
|
|
399
|
+
if active is self.processed_file:
|
|
400
|
+
path = _get_processed_file_path(self)
|
|
401
|
+
elif active is self.raw_file:
|
|
402
|
+
path = _get_raw_file_path(self)
|
|
400
403
|
else:
|
|
401
404
|
raise ValueError("No active file path available. VideoFile has neither raw nor processed file.")
|
|
402
405
|
|
|
406
|
+
if path is None:
|
|
407
|
+
raise ValueError("Active file path could not be resolved.")
|
|
408
|
+
return path
|
|
409
|
+
|
|
403
410
|
|
|
404
411
|
@classmethod
|
|
405
412
|
def create_from_file(cls, file_path: Union[str, Path], center_name: str, **kwargs) -> Optional["VideoFile"]:
|
|
@@ -448,13 +455,14 @@ class VideoFile(models.Model):
|
|
|
448
455
|
"""
|
|
449
456
|
# Ensure frames are deleted before the main instance
|
|
450
457
|
_delete_frames(self)
|
|
451
|
-
|
|
458
|
+
|
|
452
459
|
# Call the original delete method to remove the instance from the database
|
|
453
|
-
|
|
454
|
-
|
|
460
|
+
active_path = self.active_file_path
|
|
461
|
+
logger.info(f"Deleting VideoFile: {self.uuid} - {active_path}")
|
|
462
|
+
|
|
455
463
|
# Delete associated files if they exist
|
|
456
|
-
if
|
|
457
|
-
|
|
464
|
+
if active_path.exists():
|
|
465
|
+
active_path.unlink(missing_ok=True)
|
|
458
466
|
|
|
459
467
|
# Delete file storage
|
|
460
468
|
if self.raw_file and self.raw_file.storage.exists(self.raw_file.name):
|
|
@@ -479,8 +487,9 @@ class VideoFile(models.Model):
|
|
|
479
487
|
|
|
480
488
|
try:
|
|
481
489
|
# Call parent delete with proper parameters
|
|
482
|
-
super().delete(using=using, keep_parents=keep_parents)
|
|
490
|
+
result = super().delete(using=using, keep_parents=keep_parents)
|
|
483
491
|
logger.info(f"VideoFile {self.uuid} deleted successfully.")
|
|
492
|
+
return result
|
|
484
493
|
except Exception as e:
|
|
485
494
|
logger.error(f"Error deleting VideoFile {self.uuid}: {e}")
|
|
486
495
|
raise
|
|
@@ -5,8 +5,7 @@ Stores analysis results for videos (sensitive frames, detection statistics).
|
|
|
5
5
|
Created as part of Phase 1.1: Video Correction API Endpoints.
|
|
6
6
|
"""
|
|
7
7
|
from django.db import models
|
|
8
|
-
from
|
|
9
|
-
|
|
8
|
+
from .video_file import VideoFile
|
|
10
9
|
|
|
11
10
|
class VideoMetadata(models.Model):
|
|
12
11
|
"""
|
|
@@ -6,8 +6,7 @@ Created as part of Phase 1.1: Video Correction API Endpoints.
|
|
|
6
6
|
"""
|
|
7
7
|
from django.db import models
|
|
8
8
|
from django.utils import timezone
|
|
9
|
-
from
|
|
10
|
-
|
|
9
|
+
from .video_file import VideoFile
|
|
11
10
|
|
|
12
11
|
class VideoProcessingHistory(models.Model):
|
|
13
12
|
"""
|
|
@@ -75,10 +75,14 @@ class EndoscopyProcessor(models.Model):
|
|
|
75
75
|
def get_by_name(self, name):
|
|
76
76
|
return self.objects.get(name=name)
|
|
77
77
|
|
|
78
|
-
def __str__(self):
|
|
78
|
+
def __str__(self) -> str:
|
|
79
|
+
if self.name is None:
|
|
80
|
+
return ""
|
|
79
81
|
return self.name
|
|
80
82
|
|
|
81
|
-
def get_roi_endoscope_image(self):
|
|
83
|
+
def get_roi_endoscope_image(self) -> dict[str, int | None] | None:
|
|
84
|
+
if self.endoscope_image_x is None:
|
|
85
|
+
return None
|
|
82
86
|
return {
|
|
83
87
|
"x": self.endoscope_image_x,
|
|
84
88
|
"y": self.endoscope_image_y,
|
|
@@ -86,7 +90,9 @@ class EndoscopyProcessor(models.Model):
|
|
|
86
90
|
"height": self.endoscope_image_height,
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
def get_roi_examination_date(self):
|
|
93
|
+
def get_roi_examination_date(self) -> dict[str, int | None] | None:
|
|
94
|
+
if self.examination_date_x is None:
|
|
95
|
+
return None
|
|
90
96
|
return {
|
|
91
97
|
"x": self.examination_date_x,
|
|
92
98
|
"y": self.examination_date_y,
|
|
@@ -94,7 +100,9 @@ class EndoscopyProcessor(models.Model):
|
|
|
94
100
|
"height": self.examination_date_height,
|
|
95
101
|
}
|
|
96
102
|
|
|
97
|
-
def get_roi_examination_time(self):
|
|
103
|
+
def get_roi_examination_time(self) -> dict[str, int | None] | None:
|
|
104
|
+
if self.examination_time_x is None:
|
|
105
|
+
return None
|
|
98
106
|
return {
|
|
99
107
|
"x": self.examination_time_x,
|
|
100
108
|
"y": self.examination_time_y,
|
|
@@ -102,7 +110,9 @@ class EndoscopyProcessor(models.Model):
|
|
|
102
110
|
"height": self.examination_time_height,
|
|
103
111
|
}
|
|
104
112
|
|
|
105
|
-
def get_roi_patient_last_name(self):
|
|
113
|
+
def get_roi_patient_last_name(self) -> dict[str, int | None] | None:
|
|
114
|
+
if self.patient_last_name_x is None:
|
|
115
|
+
return None
|
|
106
116
|
return {
|
|
107
117
|
"x": self.patient_last_name_x,
|
|
108
118
|
"y": self.patient_last_name_y,
|
|
@@ -110,7 +120,9 @@ class EndoscopyProcessor(models.Model):
|
|
|
110
120
|
"height": self.patient_last_name_height,
|
|
111
121
|
}
|
|
112
122
|
|
|
113
|
-
def get_roi_patient_first_name(self):
|
|
123
|
+
def get_roi_patient_first_name(self) -> dict[str, int | None] | None:
|
|
124
|
+
if self.patient_first_name_x is None:
|
|
125
|
+
return None
|
|
114
126
|
return {
|
|
115
127
|
"x": self.patient_first_name_x,
|
|
116
128
|
"y": self.patient_first_name_y,
|
|
@@ -118,7 +130,9 @@ class EndoscopyProcessor(models.Model):
|
|
|
118
130
|
"height": self.patient_first_name_height,
|
|
119
131
|
}
|
|
120
132
|
|
|
121
|
-
def get_roi_patient_dob(self):
|
|
133
|
+
def get_roi_patient_dob(self) -> dict[str, int | None] | None:
|
|
134
|
+
if self.patient_dob_x is None:
|
|
135
|
+
return None
|
|
122
136
|
return {
|
|
123
137
|
"x": self.patient_dob_x,
|
|
124
138
|
"y": self.patient_dob_y,
|
|
@@ -126,7 +140,9 @@ class EndoscopyProcessor(models.Model):
|
|
|
126
140
|
"height": self.patient_dob_height,
|
|
127
141
|
}
|
|
128
142
|
|
|
129
|
-
def get_roi_endoscope_type(self):
|
|
143
|
+
def get_roi_endoscope_type(self) -> dict[str, int | None] | None:
|
|
144
|
+
if self.endoscope_type_x is None:
|
|
145
|
+
return None
|
|
130
146
|
return {
|
|
131
147
|
"x": self.endoscope_type_x,
|
|
132
148
|
"y": self.endoscope_type_y,
|
|
@@ -134,7 +150,9 @@ class EndoscopyProcessor(models.Model):
|
|
|
134
150
|
"height": self.endoscope_type_height,
|
|
135
151
|
}
|
|
136
152
|
|
|
137
|
-
def get_roi_endoscopy_sn(self):
|
|
153
|
+
def get_roi_endoscopy_sn(self) -> dict[str, int | None] | None:
|
|
154
|
+
if self.endoscope_sn_x is None:
|
|
155
|
+
return None
|
|
138
156
|
return {
|
|
139
157
|
"x": self.endoscope_sn_x,
|
|
140
158
|
"y": self.endoscope_sn_y,
|
|
@@ -142,7 +160,7 @@ class EndoscopyProcessor(models.Model):
|
|
|
142
160
|
"height": self.endoscope_sn_height,
|
|
143
161
|
}
|
|
144
162
|
|
|
145
|
-
def get_rois(self):
|
|
163
|
+
def get_rois(self) -> dict[ str, dict[str, int | None] | None]:
|
|
146
164
|
return {
|
|
147
165
|
"endoscope_image": self.get_roi_endoscope_image(),
|
|
148
166
|
"examination_date": self.get_roi_examination_date(),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from django.db import models
|
|
2
2
|
# Removed hash utils, datetime, random, os, timezone, sha256 imports
|
|
3
3
|
# Removed icecream import (was used in old save logic)
|
|
4
|
-
from typing import TYPE_CHECKING, Dict, Any, Type
|
|
4
|
+
from typing import TYPE_CHECKING, Dict, Any, Type, Self
|
|
5
5
|
import logging # Add logging import
|
|
6
6
|
|
|
7
7
|
# Import logic functions
|
|
@@ -37,7 +37,7 @@ class SensitiveMeta(models.Model):
|
|
|
37
37
|
blank=True,
|
|
38
38
|
null=True,
|
|
39
39
|
help_text="FK to the pseudo-anonymized Patient record."
|
|
40
|
-
)
|
|
40
|
+
) # type: ignore
|
|
41
41
|
patient_first_name = models.CharField(max_length=255, blank=True, null=True)
|
|
42
42
|
patient_last_name = models.CharField(max_length=255, blank=True, null=True)
|
|
43
43
|
patient_dob = models.DateTimeField(
|
|
@@ -51,24 +51,24 @@ class SensitiveMeta(models.Model):
|
|
|
51
51
|
blank=True,
|
|
52
52
|
null=True,
|
|
53
53
|
help_text="FK to the pseudo-anonymized PatientExamination record."
|
|
54
|
-
)
|
|
54
|
+
) # type: ignore
|
|
55
55
|
patient_gender = models.ForeignKey(
|
|
56
56
|
"Gender",
|
|
57
57
|
on_delete=models.CASCADE,
|
|
58
58
|
blank=True,
|
|
59
59
|
null=True,
|
|
60
|
-
)
|
|
60
|
+
) # type: ignore
|
|
61
61
|
examiners = models.ManyToManyField(
|
|
62
62
|
"Examiner",
|
|
63
63
|
blank=True,
|
|
64
64
|
help_text="Pseudo-anonymized examiner(s) associated with the examination."
|
|
65
|
-
)
|
|
65
|
+
) # type: ignore
|
|
66
66
|
center = models.ForeignKey(
|
|
67
67
|
"Center",
|
|
68
68
|
on_delete=models.CASCADE,
|
|
69
69
|
blank=True, # Should ideally be False if always required before save
|
|
70
70
|
null=True, # Should ideally be False
|
|
71
|
-
)
|
|
71
|
+
) # type: ignore
|
|
72
72
|
|
|
73
73
|
# Raw examiner names stored temporarily until pseudo-examiner is created/linked
|
|
74
74
|
examiner_first_name = models.CharField(max_length=255, blank=True, null=True, editable=False)
|
|
@@ -116,7 +116,7 @@ class SensitiveMeta(models.Model):
|
|
|
116
116
|
return None # Cannot determine before saving and linking
|
|
117
117
|
|
|
118
118
|
# --- Update method delegates to logic ---
|
|
119
|
-
def update_from_dict(self, data: Dict[str, Any]):
|
|
119
|
+
def update_from_dict(self, data: Dict[str, Any]) -> Self:
|
|
120
120
|
"""Updates the instance from a dictionary using external logic."""
|
|
121
121
|
# Delegate to logic function
|
|
122
122
|
return logic.update_sensitive_meta_from_dict(self, data)
|
|
@@ -258,7 +258,7 @@ class SensitiveMeta(models.Model):
|
|
|
258
258
|
|
|
259
259
|
# 4. Handle ManyToMany linking (examiners) *after* the instance has a PK.
|
|
260
260
|
if examiner_to_link and self.pk and not self.examiners.filter(pk=examiner_to_link.pk).exists():
|
|
261
|
-
self.examiners.add(examiner_to_link)
|
|
261
|
+
self.examiners.add(examiner_to_link) # type: ignore
|
|
262
262
|
# Adding to M2M handles its own DB interaction, no second super().save() needed.
|
|
263
263
|
|
|
264
264
|
def mark_dob_verified(self):
|
|
@@ -5,7 +5,7 @@ Serializes VideoMetadata model for API responses.
|
|
|
5
5
|
Created as part of Phase 1.1: Video Correction API Endpoints.
|
|
6
6
|
"""
|
|
7
7
|
from rest_framework import serializers
|
|
8
|
-
from endoreg_db.models import VideoMetadata
|
|
8
|
+
from endoreg_db.models.media.video import VideoMetadata
|
|
9
9
|
import json
|
|
10
10
|
|
|
11
11
|
|
|
@@ -56,7 +56,7 @@ def generate_patient_pseudonym(patient: Patient) -> Tuple[str, bool]:
|
|
|
56
56
|
patient.patient_hash = patient_hash
|
|
57
57
|
patient.save(update_fields=['patient_hash'])
|
|
58
58
|
|
|
59
|
-
logger.info(f"Generated and persisted pseudonym for patient {patient.id}
|
|
59
|
+
logger.info(f"Generated and persisted pseudonym for patient {patient.id}")
|
|
60
60
|
|
|
61
61
|
return patient_hash, True
|
|
62
62
|
|