endoreg-db 0.8.5.1__py3-none-any.whl → 0.8.5.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.
Potentially problematic release.
This version of endoreg-db might be problematic. Click here for more details.
- endoreg_db/models/media/video/video_file.py +255 -148
- endoreg_db/models/metadata/sensitive_meta_logic.py +360 -67
- endoreg_db/services/video_import.py +3 -2
- {endoreg_db-0.8.5.1.dist-info → endoreg_db-0.8.5.3.dist-info}/METADATA +1 -1
- {endoreg_db-0.8.5.1.dist-info → endoreg_db-0.8.5.3.dist-info}/RECORD +7 -7
- {endoreg_db-0.8.5.1.dist-info → endoreg_db-0.8.5.3.dist-info}/WHEEL +0 -0
- {endoreg_db-0.8.5.1.dist-info → endoreg_db-0.8.5.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -80,7 +80,9 @@ def parse_any_date(s: str) -> Optional[date]:
|
|
|
80
80
|
# Try dateparser with German locale preference
|
|
81
81
|
import dateparser
|
|
82
82
|
|
|
83
|
-
dt = dateparser.parse(
|
|
83
|
+
dt = dateparser.parse(
|
|
84
|
+
s, settings={"DATE_ORDER": "DMY", "PREFER_DAY_OF_MONTH": "first"}
|
|
85
|
+
)
|
|
84
86
|
return dt.date() if dt else None
|
|
85
87
|
except Exception as e:
|
|
86
88
|
logger.debug(f"Dateparser fallback failed for '{s}': {e}")
|
|
@@ -162,7 +164,7 @@ def calculate_patient_hash(instance: "SensitiveMeta", salt: str = SECRET_SALT) -
|
|
|
162
164
|
|
|
163
165
|
assert first_name is not None, "First name is required to calculate patient hash."
|
|
164
166
|
assert last_name is not None, "Last name is required to calculate patient hash."
|
|
165
|
-
|
|
167
|
+
|
|
166
168
|
hash_str = get_patient_hash(
|
|
167
169
|
first_name=first_name,
|
|
168
170
|
last_name=last_name,
|
|
@@ -173,7 +175,9 @@ def calculate_patient_hash(instance: "SensitiveMeta", salt: str = SECRET_SALT) -
|
|
|
173
175
|
return sha256(hash_str.encode()).hexdigest()
|
|
174
176
|
|
|
175
177
|
|
|
176
|
-
def calculate_examination_hash(
|
|
178
|
+
def calculate_examination_hash(
|
|
179
|
+
instance: "SensitiveMeta", salt: str = SECRET_SALT
|
|
180
|
+
) -> str:
|
|
177
181
|
"""Calculates the examination hash for the instance."""
|
|
178
182
|
dob = instance.patient_dob
|
|
179
183
|
first_name = instance.patient_first_name
|
|
@@ -206,17 +210,25 @@ def create_pseudo_examiner_logic(instance: "SensitiveMeta") -> "Examiner":
|
|
|
206
210
|
center = instance.center # Should be set before calling save
|
|
207
211
|
|
|
208
212
|
if not first_name or not last_name or not center:
|
|
209
|
-
logger.warning(
|
|
213
|
+
logger.warning(
|
|
214
|
+
f"Incomplete examiner info for SensitiveMeta (pk={instance.pk or 'new'}). Using default examiner."
|
|
215
|
+
)
|
|
210
216
|
# Ensure default center exists or handle appropriately
|
|
211
217
|
try:
|
|
212
|
-
default_center = Center.objects.
|
|
218
|
+
default_center = Center.objects.get(name="endoreg_db_demo")
|
|
213
219
|
except Center.DoesNotExist:
|
|
214
|
-
logger.error(
|
|
220
|
+
logger.error(
|
|
221
|
+
"Default center 'endoreg_db_demo' not found. Cannot create default examiner."
|
|
222
|
+
)
|
|
215
223
|
raise ValueError("Default center 'endoreg_db_demo' not found.")
|
|
216
224
|
|
|
217
|
-
examiner, _created = Examiner.custom_get_or_create(
|
|
225
|
+
examiner, _created = Examiner.custom_get_or_create(
|
|
226
|
+
first_name="Unknown", last_name="Unknown", center=default_center
|
|
227
|
+
)
|
|
218
228
|
else:
|
|
219
|
-
examiner, _created = Examiner.custom_get_or_create(
|
|
229
|
+
examiner, _created = Examiner.custom_get_or_create(
|
|
230
|
+
first_name=first_name, last_name=last_name, center=center
|
|
231
|
+
)
|
|
220
232
|
|
|
221
233
|
return examiner
|
|
222
234
|
|
|
@@ -262,11 +274,13 @@ def get_or_create_pseudo_patient_examination_logic(
|
|
|
262
274
|
pseudo_patient = get_or_create_pseudo_patient_logic(instance)
|
|
263
275
|
instance.pseudo_patient_id = pseudo_patient.pk # Assign FK directly
|
|
264
276
|
|
|
265
|
-
patient_examination, _created =
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
277
|
+
patient_examination, _created = (
|
|
278
|
+
PatientExamination.get_or_create_pseudo_patient_examination_by_hash(
|
|
279
|
+
patient_hash=instance.patient_hash,
|
|
280
|
+
examination_hash=instance.examination_hash,
|
|
281
|
+
# Optionally pass pseudo_patient if the method requires it
|
|
282
|
+
# pseudo_patient=instance.pseudo_patient
|
|
283
|
+
)
|
|
270
284
|
)
|
|
271
285
|
return patient_examination
|
|
272
286
|
|
|
@@ -276,33 +290,137 @@ def perform_save_logic(instance: "SensitiveMeta") -> "Examiner":
|
|
|
276
290
|
"""
|
|
277
291
|
Contains the core logic for preparing a SensitiveMeta instance for saving.
|
|
278
292
|
Handles data generation (dates), hash calculation, and linking pseudo-entities.
|
|
279
|
-
|
|
293
|
+
|
|
294
|
+
This function is called on every save() operation and implements a two-phase approach:
|
|
295
|
+
|
|
296
|
+
**Phase 1: Initial Creation (with defaults)**
|
|
297
|
+
- When a SensitiveMeta is first created (e.g., via get_or_create_sensitive_meta()),
|
|
298
|
+
it may have missing patient data (names, DOB, etc.)
|
|
299
|
+
- Default values are set to prevent hash calculation errors:
|
|
300
|
+
* patient_first_name: "unknown"
|
|
301
|
+
* patient_last_name: "unknown"
|
|
302
|
+
* patient_dob: random date (1920-2000)
|
|
303
|
+
- A temporary hash is calculated using these defaults
|
|
304
|
+
- Temporary pseudo-entities (Patient, Examination) are created
|
|
305
|
+
|
|
306
|
+
**Phase 2: Update (with extracted data)**
|
|
307
|
+
- When real patient data is extracted (e.g., from video OCR via lx_anonymizer),
|
|
308
|
+
update_from_dict() is called with actual values
|
|
309
|
+
- The instance fields are updated with real data (names, DOB, etc.)
|
|
310
|
+
- save() is called again, triggering this function
|
|
311
|
+
- Default-setting logic is skipped (fields are no longer empty)
|
|
312
|
+
- Hash is RECALCULATED with real data
|
|
313
|
+
- New pseudo-entities are created/retrieved based on new hash
|
|
314
|
+
|
|
315
|
+
**Example Flow:**
|
|
316
|
+
```
|
|
317
|
+
# Initial creation
|
|
318
|
+
sm = SensitiveMeta.create_from_dict({"center": center})
|
|
319
|
+
# → patient_first_name = "unknown", patient_last_name = "unknown"
|
|
320
|
+
# → hash = sha256("unknown unknown 1990-01-01 ...")
|
|
321
|
+
# → pseudo_patient_temp created
|
|
322
|
+
|
|
323
|
+
# Later update with extracted data
|
|
324
|
+
sm.update_from_dict({"patient_first_name": "Max", "patient_last_name": "Mustermann"})
|
|
325
|
+
# → patient_first_name = "Max", patient_last_name = "Mustermann" (overwrites)
|
|
326
|
+
# → save() triggered → perform_save_logic() called again
|
|
327
|
+
# → Default-setting skipped (names already exist)
|
|
328
|
+
# → hash = sha256("Max Mustermann 1985-03-15 ...") (RECALCULATED)
|
|
329
|
+
# → pseudo_patient_real created/retrieved with new hash
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
instance: The SensitiveMeta instance being saved
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Examiner: The pseudo examiner instance to be linked via M2M after save
|
|
337
|
+
|
|
338
|
+
Raises:
|
|
339
|
+
ValueError: If required fields (center, gender) cannot be determined
|
|
280
340
|
"""
|
|
281
341
|
|
|
282
342
|
# --- Pre-Save Checks and Data Generation ---
|
|
283
343
|
|
|
284
344
|
# 1. Ensure DOB and Examination Date exist
|
|
285
345
|
if not instance.patient_dob:
|
|
286
|
-
logger.debug(
|
|
346
|
+
logger.debug(
|
|
347
|
+
f"SensitiveMeta (pk={instance.pk or 'new'}): Patient DOB missing, generating random."
|
|
348
|
+
)
|
|
287
349
|
instance.patient_dob = generate_random_dob()
|
|
288
350
|
if not instance.examination_date:
|
|
289
|
-
logger.debug(
|
|
351
|
+
logger.debug(
|
|
352
|
+
f"SensitiveMeta (pk={instance.pk or 'new'}): Examination date missing, generating random."
|
|
353
|
+
)
|
|
290
354
|
instance.examination_date = generate_random_examination_date()
|
|
291
355
|
|
|
292
356
|
# 2. Ensure Center exists (should be set before calling save)
|
|
293
357
|
if not instance.center:
|
|
294
358
|
raise ValueError("Center must be set before saving SensitiveMeta.")
|
|
295
359
|
|
|
360
|
+
# 2.5 CRITICAL: Set default patient names BEFORE hash calculation
|
|
361
|
+
#
|
|
362
|
+
# **Why this is necessary:**
|
|
363
|
+
# Hash calculation (step 4) requires first_name and last_name to be non-None.
|
|
364
|
+
# However, on initial creation (e.g., via get_or_create_sensitive_meta()), these
|
|
365
|
+
# fields may be empty because real patient data hasn't been extracted yet.
|
|
366
|
+
#
|
|
367
|
+
# **Two-phase approach:**
|
|
368
|
+
# - Phase 1 (Initial): Set defaults if names are missing
|
|
369
|
+
# → Allows hash calculation to succeed without errors
|
|
370
|
+
# → Creates temporary pseudo-entities with default hash
|
|
371
|
+
#
|
|
372
|
+
# - Phase 2 (Update): Real data extraction (OCR, manual input)
|
|
373
|
+
# → update_from_dict() sets real names ("Max", "Mustermann")
|
|
374
|
+
# → save() is called again
|
|
375
|
+
# → This block is SKIPPED (names already exist)
|
|
376
|
+
# → Hash is recalculated with real data (step 4)
|
|
377
|
+
# → New pseudo-entities created with correct hash
|
|
378
|
+
#
|
|
379
|
+
# **Example:**
|
|
380
|
+
# Initial: patient_first_name = "unknown" → hash = sha256("unknown unknown...")
|
|
381
|
+
# Updated: patient_first_name = "Max" → hash = sha256("Max Mustermann...")
|
|
382
|
+
#
|
|
383
|
+
if not instance.patient_first_name:
|
|
384
|
+
instance.patient_first_name = DEFAULT_UNKNOWN_NAME
|
|
385
|
+
logger.debug(
|
|
386
|
+
"SensitiveMeta (pk=%s): Patient first name missing, set to default '%s'.",
|
|
387
|
+
instance.pk or "new",
|
|
388
|
+
DEFAULT_UNKNOWN_NAME,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
if not instance.patient_last_name:
|
|
392
|
+
instance.patient_last_name = DEFAULT_UNKNOWN_NAME
|
|
393
|
+
logger.debug(
|
|
394
|
+
"SensitiveMeta (pk=%s): Patient last name missing, set to default '%s'.",
|
|
395
|
+
instance.pk or "new",
|
|
396
|
+
DEFAULT_UNKNOWN_NAME,
|
|
397
|
+
)
|
|
398
|
+
|
|
296
399
|
# 3. Ensure Gender exists (should be set before calling save, e.g., during creation/update)
|
|
297
400
|
if not instance.patient_gender:
|
|
298
|
-
#
|
|
299
|
-
first_name = instance.patient_first_name
|
|
300
|
-
|
|
301
|
-
if not
|
|
302
|
-
raise ValueError(
|
|
303
|
-
|
|
401
|
+
# Use the now-guaranteed first_name for gender guessing
|
|
402
|
+
first_name = instance.patient_first_name
|
|
403
|
+
gender_str = guess_name_gender(first_name)
|
|
404
|
+
if not gender_str:
|
|
405
|
+
raise ValueError(
|
|
406
|
+
"Patient gender could not be determined and must be set before saving."
|
|
407
|
+
)
|
|
408
|
+
# Convert string to Gender object
|
|
409
|
+
try:
|
|
410
|
+
gender_obj = Gender.objects.get(name=gender_str)
|
|
411
|
+
instance.patient_gender = gender_obj
|
|
412
|
+
except Gender.DoesNotExist:
|
|
413
|
+
raise ValueError(f"Gender '{gender_str}' not found in database.")
|
|
304
414
|
|
|
305
415
|
# 4. Calculate Hashes (depends on DOB, Exam Date, Center, Names)
|
|
416
|
+
#
|
|
417
|
+
# **IMPORTANT: Hashes are RECALCULATED on every save!**
|
|
418
|
+
# This enables the two-phase update pattern:
|
|
419
|
+
# - Initial save: Hash based on default "unknown unknown" names
|
|
420
|
+
# - Updated save: Hash based on real extracted names ("Max Mustermann")
|
|
421
|
+
#
|
|
422
|
+
# The new hash will link to different pseudo-entities, ensuring proper
|
|
423
|
+
# anonymization while maintaining referential integrity.
|
|
306
424
|
instance.patient_hash = calculate_patient_hash(instance)
|
|
307
425
|
instance.examination_hash = calculate_examination_hash(instance)
|
|
308
426
|
|
|
@@ -327,10 +445,59 @@ def perform_save_logic(instance: "SensitiveMeta") -> "Examiner":
|
|
|
327
445
|
return examiner_instance
|
|
328
446
|
|
|
329
447
|
|
|
330
|
-
def create_sensitive_meta_from_dict(
|
|
331
|
-
""
|
|
448
|
+
def create_sensitive_meta_from_dict(
|
|
449
|
+
cls: Type["SensitiveMeta"], data: Dict[str, Any]
|
|
450
|
+
) -> "SensitiveMeta":
|
|
451
|
+
"""
|
|
452
|
+
Create a SensitiveMeta instance from a dictionary.
|
|
453
|
+
|
|
454
|
+
**Center handling:**
|
|
455
|
+
This function accepts TWO ways to specify the center:
|
|
456
|
+
1. `center` (Center object) - Directly pass a Center instance
|
|
457
|
+
2. `center_name` (string) - Pass the center name as a string (will be resolved to Center object)
|
|
458
|
+
|
|
459
|
+
At least ONE of these must be provided.
|
|
460
|
+
|
|
461
|
+
**Example usage:**
|
|
462
|
+
```python
|
|
463
|
+
# Option 1: With Center object
|
|
464
|
+
data = {
|
|
465
|
+
"patient_first_name": "Patient",
|
|
466
|
+
"patient_last_name": "Unknown",
|
|
467
|
+
"patient_dob": date(1990, 1, 1),
|
|
468
|
+
"examination_date": date.today(),
|
|
469
|
+
"center": center_obj, # ← Center object
|
|
470
|
+
}
|
|
471
|
+
sm = SensitiveMeta.create_from_dict(data)
|
|
472
|
+
|
|
473
|
+
# Option 2: With center name string
|
|
474
|
+
data = {
|
|
475
|
+
"patient_first_name": "Patient",
|
|
476
|
+
"patient_last_name": "Unknown",
|
|
477
|
+
"patient_dob": date(1990, 1, 1),
|
|
478
|
+
"examination_date": date.today(),
|
|
479
|
+
"center_name": "university_hospital_wuerzburg", # ← String
|
|
480
|
+
}
|
|
481
|
+
sm = SensitiveMeta.create_from_dict(data)
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
cls: The SensitiveMeta class
|
|
486
|
+
data: Dictionary containing field values
|
|
332
487
|
|
|
333
|
-
|
|
488
|
+
Returns:
|
|
489
|
+
SensitiveMeta: The created instance
|
|
490
|
+
|
|
491
|
+
Raises:
|
|
492
|
+
ValueError: If neither center nor center_name is provided
|
|
493
|
+
ValueError: If center_name does not match any Center in database
|
|
494
|
+
"""
|
|
495
|
+
|
|
496
|
+
field_names = {
|
|
497
|
+
f.name
|
|
498
|
+
for f in cls._meta.get_fields()
|
|
499
|
+
if not f.is_relation or f.one_to_one or (f.many_to_one and f.related_model)
|
|
500
|
+
}
|
|
334
501
|
selected_data = {k: v for k, v in data.items() if k in field_names}
|
|
335
502
|
|
|
336
503
|
# --- Convert patient_dob if it's a date object ---
|
|
@@ -357,9 +524,13 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
|
|
|
357
524
|
try:
|
|
358
525
|
import dateparser
|
|
359
526
|
|
|
360
|
-
parsed_dob = dateparser.parse(
|
|
527
|
+
parsed_dob = dateparser.parse(
|
|
528
|
+
dob, languages=["de"], settings={"DATE_ORDER": "DMY"}
|
|
529
|
+
)
|
|
361
530
|
if parsed_dob:
|
|
362
|
-
aware_dob = timezone.make_aware(
|
|
531
|
+
aware_dob = timezone.make_aware(
|
|
532
|
+
parsed_dob.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
533
|
+
)
|
|
363
534
|
selected_data["patient_dob"] = aware_dob
|
|
364
535
|
logger.debug(
|
|
365
536
|
"Parsed string patient_dob '%s' to aware datetime: %s",
|
|
@@ -413,7 +584,9 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
|
|
|
413
584
|
# Fall back to dateparser for complex formats
|
|
414
585
|
import dateparser
|
|
415
586
|
|
|
416
|
-
parsed_date = dateparser.parse(
|
|
587
|
+
parsed_date = dateparser.parse(
|
|
588
|
+
exam_date, languages=["de"], settings={"DATE_ORDER": "DMY"}
|
|
589
|
+
)
|
|
417
590
|
if parsed_date:
|
|
418
591
|
selected_data["examination_date"] = parsed_date.date()
|
|
419
592
|
logger.debug(
|
|
@@ -431,7 +604,9 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
|
|
|
431
604
|
# Use dateparser for non-ISO formats
|
|
432
605
|
import dateparser
|
|
433
606
|
|
|
434
|
-
parsed_date = dateparser.parse(
|
|
607
|
+
parsed_date = dateparser.parse(
|
|
608
|
+
exam_date, languages=["de"], settings={"DATE_ORDER": "DMY"}
|
|
609
|
+
)
|
|
435
610
|
if parsed_date:
|
|
436
611
|
selected_data["examination_date"] = parsed_date.date()
|
|
437
612
|
logger.debug(
|
|
@@ -453,15 +628,29 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
|
|
|
453
628
|
)
|
|
454
629
|
selected_data.pop("examination_date", None)
|
|
455
630
|
|
|
456
|
-
# Handle Center
|
|
631
|
+
# Handle Center - accept both center_name (string) and center (object)
|
|
632
|
+
from ..administration import Center
|
|
633
|
+
|
|
634
|
+
center = data.get("center") # First try direct Center object
|
|
457
635
|
center_name = data.get("center_name")
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
center
|
|
636
|
+
|
|
637
|
+
if center is not None:
|
|
638
|
+
# Center object provided directly - validate it's a Center instance
|
|
639
|
+
if not isinstance(center, Center):
|
|
640
|
+
raise ValueError(f"'center' must be a Center instance, got {type(center)}")
|
|
462
641
|
selected_data["center"] = center
|
|
463
|
-
|
|
464
|
-
|
|
642
|
+
elif center_name:
|
|
643
|
+
# center_name string provided - resolve to Center object
|
|
644
|
+
try:
|
|
645
|
+
center = Center.objects.get(name=center_name)
|
|
646
|
+
selected_data["center"] = center
|
|
647
|
+
except Center.DoesNotExist:
|
|
648
|
+
raise ValueError(f"Center with name '{center_name}' does not exist.")
|
|
649
|
+
else:
|
|
650
|
+
# Neither center nor center_name provided
|
|
651
|
+
raise ValueError(
|
|
652
|
+
"Either 'center' (Center object) or 'center_name' (string) is required in data dictionary."
|
|
653
|
+
)
|
|
465
654
|
|
|
466
655
|
# Handle Names and Gender
|
|
467
656
|
first_name = selected_data.get("patient_first_name") or DEFAULT_UNKNOWN_NAME
|
|
@@ -477,22 +666,34 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
|
|
|
477
666
|
elif isinstance(patient_gender_input, str):
|
|
478
667
|
# Input is a string (gender name)
|
|
479
668
|
try:
|
|
480
|
-
selected_data["patient_gender"] = Gender.objects.get(
|
|
669
|
+
selected_data["patient_gender"] = Gender.objects.get(
|
|
670
|
+
name=patient_gender_input
|
|
671
|
+
)
|
|
481
672
|
except Gender.DoesNotExist:
|
|
482
|
-
logger.warning(
|
|
673
|
+
logger.warning(
|
|
674
|
+
f"Gender with name '{patient_gender_input}' provided but not found. Attempting to guess or use default."
|
|
675
|
+
)
|
|
483
676
|
# Fall through to guessing logic if provided string name is invalid
|
|
484
677
|
patient_gender_input = None # Reset to trigger guessing
|
|
485
678
|
|
|
486
|
-
if not isinstance(
|
|
679
|
+
if not isinstance(
|
|
680
|
+
selected_data.get("patient_gender"), Gender
|
|
681
|
+
): # If not already a Gender object (e.g. was None, or string lookup failed)
|
|
487
682
|
gender_name_to_use = guess_name_gender(first_name)
|
|
488
683
|
if not gender_name_to_use:
|
|
489
|
-
logger.warning(
|
|
684
|
+
logger.warning(
|
|
685
|
+
f"Could not guess gender for name '{first_name}'. Setting Gender to unknown."
|
|
686
|
+
)
|
|
490
687
|
gender_name_to_use = "unknown"
|
|
491
688
|
try:
|
|
492
|
-
selected_data["patient_gender"] = Gender.objects.get(
|
|
689
|
+
selected_data["patient_gender"] = Gender.objects.get(
|
|
690
|
+
name=gender_name_to_use
|
|
691
|
+
)
|
|
493
692
|
except Gender.DoesNotExist:
|
|
494
693
|
# This should ideally not happen if "unknown" gender is guaranteed to exist
|
|
495
|
-
raise ValueError(
|
|
694
|
+
raise ValueError(
|
|
695
|
+
f"Default or guessed gender '{gender_name_to_use}' does not exist in Gender table."
|
|
696
|
+
)
|
|
496
697
|
|
|
497
698
|
# Update name DB
|
|
498
699
|
update_name_db(first_name, last_name)
|
|
@@ -506,21 +707,74 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
|
|
|
506
707
|
return sensitive_meta
|
|
507
708
|
|
|
508
709
|
|
|
509
|
-
def update_sensitive_meta_from_dict(
|
|
510
|
-
""
|
|
511
|
-
|
|
710
|
+
def update_sensitive_meta_from_dict(
|
|
711
|
+
instance: "SensitiveMeta", data: Dict[str, Any]
|
|
712
|
+
) -> "SensitiveMeta":
|
|
713
|
+
"""
|
|
714
|
+
Updates a SensitiveMeta instance from a dictionary of new values.
|
|
715
|
+
|
|
716
|
+
**Integration with two-phase save pattern:**
|
|
717
|
+
This function is typically called after initial SensitiveMeta creation when real
|
|
718
|
+
patient data becomes available (e.g., extracted from video OCR, PDF parsing, or
|
|
719
|
+
manual annotation).
|
|
720
|
+
|
|
721
|
+
**Example workflow:**
|
|
722
|
+
```python
|
|
723
|
+
# Phase 1: Initial creation with defaults
|
|
724
|
+
sm = SensitiveMeta.create_from_dict({"center": center})
|
|
725
|
+
# → patient_first_name = "unknown", hash = sha256("unknown...")
|
|
726
|
+
|
|
727
|
+
# Phase 2: Update with extracted data
|
|
728
|
+
extracted = {
|
|
729
|
+
"patient_first_name": "Max",
|
|
730
|
+
"patient_last_name": "Mustermann",
|
|
731
|
+
"patient_dob": date(1985, 3, 15)
|
|
732
|
+
}
|
|
733
|
+
update_sensitive_meta_from_dict(sm, extracted)
|
|
734
|
+
# → Sets: sm.patient_first_name = "Max", sm.patient_last_name = "Mustermann"
|
|
735
|
+
# → Calls: sm.save()
|
|
736
|
+
# → Triggers: perform_save_logic() again
|
|
737
|
+
# → Result: Hash recalculated with real data, new pseudo-entities created
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
**Key behaviors:**
|
|
741
|
+
- Updates instance attributes from provided dictionary
|
|
742
|
+
- Handles type conversions (date strings → date objects, gender strings → Gender objects)
|
|
743
|
+
- Tracks patient name changes to update name database
|
|
744
|
+
- Calls save() at the end, triggering full save logic including hash recalculation
|
|
745
|
+
- Default-setting in perform_save_logic() is skipped (fields already populated)
|
|
746
|
+
|
|
747
|
+
Args:
|
|
748
|
+
instance: The existing SensitiveMeta instance to update
|
|
749
|
+
data: Dictionary of field names and new values
|
|
750
|
+
|
|
751
|
+
Returns:
|
|
752
|
+
The updated SensitiveMeta instance
|
|
753
|
+
|
|
754
|
+
Raises:
|
|
755
|
+
Exception: If save fails or required conversions fail
|
|
756
|
+
"""
|
|
757
|
+
field_names = {
|
|
758
|
+
f.name
|
|
759
|
+
for f in instance._meta.get_fields()
|
|
760
|
+
if not f.is_relation or f.one_to_one or (f.many_to_one and f.related_model)
|
|
761
|
+
}
|
|
512
762
|
# Exclude FKs that should not be updated directly from dict keys (handled separately or via save logic)
|
|
513
763
|
excluded_fields = {"pseudo_patient", "pseudo_examination"}
|
|
514
|
-
selected_data = {
|
|
764
|
+
selected_data = {
|
|
765
|
+
k: v for k, v in data.items() if k in field_names and k not in excluded_fields
|
|
766
|
+
}
|
|
515
767
|
|
|
516
768
|
# Handle potential Center update
|
|
517
769
|
center_name = data.get("center_name")
|
|
518
770
|
if center_name:
|
|
519
771
|
try:
|
|
520
|
-
center = Center.objects.
|
|
772
|
+
center = Center.objects.get(name=center_name)
|
|
521
773
|
instance.center = center # Update center directly
|
|
522
|
-
except Center.DoesNotExist
|
|
523
|
-
logger.warning(
|
|
774
|
+
except Center.DoesNotExist:
|
|
775
|
+
logger.warning(
|
|
776
|
+
f"Center '{center_name}' not found during update. Keeping existing center."
|
|
777
|
+
)
|
|
524
778
|
selected_data.pop("center", None) # Remove from dict if not found
|
|
525
779
|
|
|
526
780
|
# Set examiner names if provided, before calling save
|
|
@@ -540,10 +794,14 @@ def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, A
|
|
|
540
794
|
elif isinstance(patient_gender_input, str):
|
|
541
795
|
gender_input_clean = patient_gender_input.strip()
|
|
542
796
|
# Try direct case-insensitive DB lookup first
|
|
543
|
-
gender_obj = Gender.objects.filter(
|
|
797
|
+
gender_obj = Gender.objects.filter(
|
|
798
|
+
name__iexact=gender_input_clean
|
|
799
|
+
).first()
|
|
544
800
|
if gender_obj:
|
|
545
801
|
selected_data["patient_gender"] = gender_obj
|
|
546
|
-
logger.debug(
|
|
802
|
+
logger.debug(
|
|
803
|
+
f"Successfully matched gender string '{patient_gender_input}' to Gender object via iexact lookup"
|
|
804
|
+
)
|
|
547
805
|
else:
|
|
548
806
|
# Use mapping helper for fallback
|
|
549
807
|
mapped = _map_gender_string_to_standard(gender_input_clean)
|
|
@@ -551,30 +809,50 @@ def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, A
|
|
|
551
809
|
gender_obj = Gender.objects.filter(name__iexact=mapped).first()
|
|
552
810
|
if gender_obj:
|
|
553
811
|
selected_data["patient_gender"] = gender_obj
|
|
554
|
-
logger.info(
|
|
812
|
+
logger.info(
|
|
813
|
+
f"Mapped gender '{patient_gender_input}' to '{mapped}' via fallback mapping"
|
|
814
|
+
)
|
|
555
815
|
else:
|
|
556
|
-
logger.warning(
|
|
557
|
-
|
|
816
|
+
logger.warning(
|
|
817
|
+
f"Mapped gender '{patient_gender_input}' to '{mapped}', but no such Gender in DB. Trying 'unknown'."
|
|
818
|
+
)
|
|
819
|
+
unknown_gender = Gender.objects.filter(
|
|
820
|
+
name__iexact="unknown"
|
|
821
|
+
).first()
|
|
558
822
|
if unknown_gender:
|
|
559
823
|
selected_data["patient_gender"] = unknown_gender
|
|
560
|
-
logger.warning(
|
|
824
|
+
logger.warning(
|
|
825
|
+
f"Using 'unknown' gender as fallback for '{patient_gender_input}'"
|
|
826
|
+
)
|
|
561
827
|
else:
|
|
562
|
-
logger.error(
|
|
828
|
+
logger.error(
|
|
829
|
+
f"No 'unknown' gender found in database. Cannot handle gender '{patient_gender_input}'. Skipping gender update."
|
|
830
|
+
)
|
|
563
831
|
selected_data.pop("patient_gender", None)
|
|
564
832
|
else:
|
|
565
833
|
# Last resort: try to get 'unknown' gender
|
|
566
|
-
unknown_gender = Gender.objects.filter(
|
|
834
|
+
unknown_gender = Gender.objects.filter(
|
|
835
|
+
name__iexact="unknown"
|
|
836
|
+
).first()
|
|
567
837
|
if unknown_gender:
|
|
568
838
|
selected_data["patient_gender"] = unknown_gender
|
|
569
|
-
logger.warning(
|
|
839
|
+
logger.warning(
|
|
840
|
+
f"Using 'unknown' gender as fallback for '{patient_gender_input}' (no mapping)"
|
|
841
|
+
)
|
|
570
842
|
else:
|
|
571
|
-
logger.error(
|
|
843
|
+
logger.error(
|
|
844
|
+
f"No 'unknown' gender found in database. Cannot handle gender '{patient_gender_input}'. Skipping gender update."
|
|
845
|
+
)
|
|
572
846
|
selected_data.pop("patient_gender", None)
|
|
573
847
|
else:
|
|
574
|
-
logger.warning(
|
|
848
|
+
logger.warning(
|
|
849
|
+
f"Unexpected patient_gender type {type(patient_gender_input)}: {patient_gender_input}. Skipping gender update."
|
|
850
|
+
)
|
|
575
851
|
selected_data.pop("patient_gender", None)
|
|
576
852
|
except Exception as e:
|
|
577
|
-
logger.exception(
|
|
853
|
+
logger.exception(
|
|
854
|
+
f"Error handling patient_gender '{patient_gender_input}': {e}. Skipping gender update."
|
|
855
|
+
)
|
|
578
856
|
selected_data.pop("patient_gender", None)
|
|
579
857
|
|
|
580
858
|
# Update other attributes from selected_data
|
|
@@ -591,7 +869,9 @@ def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, A
|
|
|
591
869
|
value_to_set = v
|
|
592
870
|
if k == "patient_dob":
|
|
593
871
|
if isinstance(v, date) and not isinstance(v, datetime):
|
|
594
|
-
aware_dob = timezone.make_aware(
|
|
872
|
+
aware_dob = timezone.make_aware(
|
|
873
|
+
datetime.combine(v, datetime.min.time())
|
|
874
|
+
)
|
|
595
875
|
value_to_set = aware_dob
|
|
596
876
|
logger.debug(
|
|
597
877
|
"Converted patient_dob from date to aware datetime during update: %s",
|
|
@@ -614,9 +894,15 @@ def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, A
|
|
|
614
894
|
try:
|
|
615
895
|
import dateparser
|
|
616
896
|
|
|
617
|
-
parsed_dob = dateparser.parse(
|
|
897
|
+
parsed_dob = dateparser.parse(
|
|
898
|
+
v, languages=["de"], settings={"DATE_ORDER": "DMY"}
|
|
899
|
+
)
|
|
618
900
|
if parsed_dob:
|
|
619
|
-
value_to_set = timezone.make_aware(
|
|
901
|
+
value_to_set = timezone.make_aware(
|
|
902
|
+
parsed_dob.replace(
|
|
903
|
+
hour=0, minute=0, second=0, microsecond=0
|
|
904
|
+
)
|
|
905
|
+
)
|
|
620
906
|
logger.debug(
|
|
621
907
|
"Parsed string patient_dob '%s' during update to aware datetime: %s",
|
|
622
908
|
v,
|
|
@@ -651,7 +937,9 @@ def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, A
|
|
|
651
937
|
try:
|
|
652
938
|
import dateparser
|
|
653
939
|
|
|
654
|
-
parsed_date = dateparser.parse(
|
|
940
|
+
parsed_date = dateparser.parse(
|
|
941
|
+
v, languages=["de"], settings={"DATE_ORDER": "DMY"}
|
|
942
|
+
)
|
|
655
943
|
if parsed_date:
|
|
656
944
|
value_to_set = parsed_date.date()
|
|
657
945
|
logger.debug(
|
|
@@ -675,13 +963,18 @@ def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, A
|
|
|
675
963
|
# --- End Conversion ---
|
|
676
964
|
|
|
677
965
|
# Check if patient name is changing
|
|
678
|
-
if
|
|
966
|
+
if (
|
|
967
|
+
k in ["patient_first_name", "patient_last_name"]
|
|
968
|
+
and getattr(instance, k) != value_to_set
|
|
969
|
+
):
|
|
679
970
|
patient_name_changed = True
|
|
680
971
|
|
|
681
972
|
setattr(instance, k, value_to_set) # Use value_to_set
|
|
682
973
|
|
|
683
974
|
except Exception as e:
|
|
684
|
-
logger.error(
|
|
975
|
+
logger.error(
|
|
976
|
+
f"Error setting attribute '{k}' to '{v}': {e}. Skipping this field."
|
|
977
|
+
)
|
|
685
978
|
continue
|
|
686
979
|
|
|
687
980
|
# Update name DB if patient names changed
|
|
@@ -409,8 +409,7 @@ class VideoImportService:
|
|
|
409
409
|
# Initialize video specifications
|
|
410
410
|
video.initialize_video_specs()
|
|
411
411
|
|
|
412
|
-
|
|
413
|
-
video.initialize_frames()
|
|
412
|
+
|
|
414
413
|
|
|
415
414
|
# Extract frames BEFORE processing to prevent pipeline 1 conflicts
|
|
416
415
|
self.logger.info("Pre-extracting frames to avoid pipeline conflicts...")
|
|
@@ -419,6 +418,8 @@ class VideoImportService:
|
|
|
419
418
|
if frames_extracted:
|
|
420
419
|
self.processing_context["frames_extracted"] = True
|
|
421
420
|
self.logger.info("Frame extraction completed successfully")
|
|
421
|
+
# Initialize frame objects in database
|
|
422
|
+
video.initialize_frames(video.get_frame_paths())
|
|
422
423
|
|
|
423
424
|
# CRITICAL: Immediately save the frames_extracted state to database
|
|
424
425
|
# to prevent refresh_from_db() in pipeline 1 from overriding it
|