endoreg-db 0.8.4.4__py3-none-any.whl → 0.8.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of endoreg-db might be problematic. Click here for more details.
- endoreg_db/management/commands/load_ai_model_data.py +2 -1
- endoreg_db/management/commands/setup_endoreg_db.py +11 -7
- 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/metadata/model_meta_logic.py +15 -1
- endoreg_db/models/metadata/sensitive_meta_logic.py +391 -70
- 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 +340 -101
- 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/views/__init__.py +6 -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.4.4.dist-info → endoreg_db-0.8.6.1.dist-info}/METADATA +1 -1
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.6.1.dist-info}/RECORD +33 -34
- 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.4.4.dist-info → endoreg_db-0.8.6.1.dist-info}/WHEEL +0 -0
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.6.1.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}")
|
|
@@ -160,6 +162,9 @@ def calculate_patient_hash(instance: "SensitiveMeta", salt: str = SECRET_SALT) -
|
|
|
160
162
|
if not center:
|
|
161
163
|
raise ValueError("Center is required to calculate patient hash.")
|
|
162
164
|
|
|
165
|
+
assert first_name is not None, "First name is required to calculate patient hash."
|
|
166
|
+
assert last_name is not None, "Last name is required to calculate patient hash."
|
|
167
|
+
|
|
163
168
|
hash_str = get_patient_hash(
|
|
164
169
|
first_name=first_name,
|
|
165
170
|
last_name=last_name,
|
|
@@ -170,7 +175,9 @@ def calculate_patient_hash(instance: "SensitiveMeta", salt: str = SECRET_SALT) -
|
|
|
170
175
|
return sha256(hash_str.encode()).hexdigest()
|
|
171
176
|
|
|
172
177
|
|
|
173
|
-
def calculate_examination_hash(
|
|
178
|
+
def calculate_examination_hash(
|
|
179
|
+
instance: "SensitiveMeta", salt: str = SECRET_SALT
|
|
180
|
+
) -> str:
|
|
174
181
|
"""Calculates the examination hash for the instance."""
|
|
175
182
|
dob = instance.patient_dob
|
|
176
183
|
first_name = instance.patient_first_name
|
|
@@ -203,17 +210,25 @@ def create_pseudo_examiner_logic(instance: "SensitiveMeta") -> "Examiner":
|
|
|
203
210
|
center = instance.center # Should be set before calling save
|
|
204
211
|
|
|
205
212
|
if not first_name or not last_name or not center:
|
|
206
|
-
logger.warning(
|
|
213
|
+
logger.warning(
|
|
214
|
+
f"Incomplete examiner info for SensitiveMeta (pk={instance.pk or 'new'}). Using default examiner."
|
|
215
|
+
)
|
|
207
216
|
# Ensure default center exists or handle appropriately
|
|
208
217
|
try:
|
|
209
|
-
default_center = Center.objects.
|
|
218
|
+
default_center = Center.objects.get(name="endoreg_db_demo")
|
|
210
219
|
except Center.DoesNotExist:
|
|
211
|
-
logger.error(
|
|
220
|
+
logger.error(
|
|
221
|
+
"Default center 'endoreg_db_demo' not found. Cannot create default examiner."
|
|
222
|
+
)
|
|
212
223
|
raise ValueError("Default center 'endoreg_db_demo' not found.")
|
|
213
224
|
|
|
214
|
-
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
|
+
)
|
|
215
228
|
else:
|
|
216
|
-
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
|
+
)
|
|
217
232
|
|
|
218
233
|
return examiner
|
|
219
234
|
|
|
@@ -259,11 +274,13 @@ def get_or_create_pseudo_patient_examination_logic(
|
|
|
259
274
|
pseudo_patient = get_or_create_pseudo_patient_logic(instance)
|
|
260
275
|
instance.pseudo_patient_id = pseudo_patient.pk # Assign FK directly
|
|
261
276
|
|
|
262
|
-
patient_examination, _created =
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
+
)
|
|
267
284
|
)
|
|
268
285
|
return patient_examination
|
|
269
286
|
|
|
@@ -273,33 +290,137 @@ def perform_save_logic(instance: "SensitiveMeta") -> "Examiner":
|
|
|
273
290
|
"""
|
|
274
291
|
Contains the core logic for preparing a SensitiveMeta instance for saving.
|
|
275
292
|
Handles data generation (dates), hash calculation, and linking pseudo-entities.
|
|
276
|
-
|
|
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
|
|
277
340
|
"""
|
|
278
341
|
|
|
279
342
|
# --- Pre-Save Checks and Data Generation ---
|
|
280
343
|
|
|
281
344
|
# 1. Ensure DOB and Examination Date exist
|
|
282
345
|
if not instance.patient_dob:
|
|
283
|
-
logger.debug(
|
|
346
|
+
logger.debug(
|
|
347
|
+
f"SensitiveMeta (pk={instance.pk or 'new'}): Patient DOB missing, generating random."
|
|
348
|
+
)
|
|
284
349
|
instance.patient_dob = generate_random_dob()
|
|
285
350
|
if not instance.examination_date:
|
|
286
|
-
logger.debug(
|
|
351
|
+
logger.debug(
|
|
352
|
+
f"SensitiveMeta (pk={instance.pk or 'new'}): Examination date missing, generating random."
|
|
353
|
+
)
|
|
287
354
|
instance.examination_date = generate_random_examination_date()
|
|
288
355
|
|
|
289
356
|
# 2. Ensure Center exists (should be set before calling save)
|
|
290
357
|
if not instance.center:
|
|
291
358
|
raise ValueError("Center must be set before saving SensitiveMeta.")
|
|
292
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
|
+
|
|
293
399
|
# 3. Ensure Gender exists (should be set before calling save, e.g., during creation/update)
|
|
294
400
|
if not instance.patient_gender:
|
|
295
|
-
#
|
|
296
|
-
first_name = instance.patient_first_name
|
|
297
|
-
|
|
298
|
-
if not
|
|
299
|
-
raise ValueError(
|
|
300
|
-
|
|
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.")
|
|
301
414
|
|
|
302
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.
|
|
303
424
|
instance.patient_hash = calculate_patient_hash(instance)
|
|
304
425
|
instance.examination_hash = calculate_examination_hash(instance)
|
|
305
426
|
|
|
@@ -324,10 +445,59 @@ def perform_save_logic(instance: "SensitiveMeta") -> "Examiner":
|
|
|
324
445
|
return examiner_instance
|
|
325
446
|
|
|
326
447
|
|
|
327
|
-
def create_sensitive_meta_from_dict(
|
|
328
|
-
""
|
|
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
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
SensitiveMeta: The created instance
|
|
329
490
|
|
|
330
|
-
|
|
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
|
+
}
|
|
331
501
|
selected_data = {k: v for k, v in data.items() if k in field_names}
|
|
332
502
|
|
|
333
503
|
# --- Convert patient_dob if it's a date object ---
|
|
@@ -354,9 +524,13 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
|
|
|
354
524
|
try:
|
|
355
525
|
import dateparser
|
|
356
526
|
|
|
357
|
-
parsed_dob = dateparser.parse(
|
|
527
|
+
parsed_dob = dateparser.parse(
|
|
528
|
+
dob, languages=["de"], settings={"DATE_ORDER": "DMY"}
|
|
529
|
+
)
|
|
358
530
|
if parsed_dob:
|
|
359
|
-
aware_dob = timezone.make_aware(
|
|
531
|
+
aware_dob = timezone.make_aware(
|
|
532
|
+
parsed_dob.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
533
|
+
)
|
|
360
534
|
selected_data["patient_dob"] = aware_dob
|
|
361
535
|
logger.debug(
|
|
362
536
|
"Parsed string patient_dob '%s' to aware datetime: %s",
|
|
@@ -410,7 +584,9 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
|
|
|
410
584
|
# Fall back to dateparser for complex formats
|
|
411
585
|
import dateparser
|
|
412
586
|
|
|
413
|
-
parsed_date = dateparser.parse(
|
|
587
|
+
parsed_date = dateparser.parse(
|
|
588
|
+
exam_date, languages=["de"], settings={"DATE_ORDER": "DMY"}
|
|
589
|
+
)
|
|
414
590
|
if parsed_date:
|
|
415
591
|
selected_data["examination_date"] = parsed_date.date()
|
|
416
592
|
logger.debug(
|
|
@@ -428,7 +604,9 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
|
|
|
428
604
|
# Use dateparser for non-ISO formats
|
|
429
605
|
import dateparser
|
|
430
606
|
|
|
431
|
-
parsed_date = dateparser.parse(
|
|
607
|
+
parsed_date = dateparser.parse(
|
|
608
|
+
exam_date, languages=["de"], settings={"DATE_ORDER": "DMY"}
|
|
609
|
+
)
|
|
432
610
|
if parsed_date:
|
|
433
611
|
selected_data["examination_date"] = parsed_date.date()
|
|
434
612
|
logger.debug(
|
|
@@ -450,15 +628,29 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
|
|
|
450
628
|
)
|
|
451
629
|
selected_data.pop("examination_date", None)
|
|
452
630
|
|
|
453
|
-
# 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
|
|
454
635
|
center_name = data.get("center_name")
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
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)}")
|
|
459
641
|
selected_data["center"] = center
|
|
460
|
-
|
|
461
|
-
|
|
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
|
+
)
|
|
462
654
|
|
|
463
655
|
# Handle Names and Gender
|
|
464
656
|
first_name = selected_data.get("patient_first_name") or DEFAULT_UNKNOWN_NAME
|
|
@@ -474,22 +666,34 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
|
|
|
474
666
|
elif isinstance(patient_gender_input, str):
|
|
475
667
|
# Input is a string (gender name)
|
|
476
668
|
try:
|
|
477
|
-
selected_data["patient_gender"] = Gender.objects.get(
|
|
669
|
+
selected_data["patient_gender"] = Gender.objects.get(
|
|
670
|
+
name=patient_gender_input
|
|
671
|
+
)
|
|
478
672
|
except Gender.DoesNotExist:
|
|
479
|
-
logger.warning(
|
|
673
|
+
logger.warning(
|
|
674
|
+
f"Gender with name '{patient_gender_input}' provided but not found. Attempting to guess or use default."
|
|
675
|
+
)
|
|
480
676
|
# Fall through to guessing logic if provided string name is invalid
|
|
481
677
|
patient_gender_input = None # Reset to trigger guessing
|
|
482
678
|
|
|
483
|
-
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)
|
|
484
682
|
gender_name_to_use = guess_name_gender(first_name)
|
|
485
683
|
if not gender_name_to_use:
|
|
486
|
-
logger.warning(
|
|
684
|
+
logger.warning(
|
|
685
|
+
f"Could not guess gender for name '{first_name}'. Setting Gender to unknown."
|
|
686
|
+
)
|
|
487
687
|
gender_name_to_use = "unknown"
|
|
488
688
|
try:
|
|
489
|
-
selected_data["patient_gender"] = Gender.objects.get(
|
|
689
|
+
selected_data["patient_gender"] = Gender.objects.get(
|
|
690
|
+
name=gender_name_to_use
|
|
691
|
+
)
|
|
490
692
|
except Gender.DoesNotExist:
|
|
491
693
|
# This should ideally not happen if "unknown" gender is guaranteed to exist
|
|
492
|
-
raise ValueError(
|
|
694
|
+
raise ValueError(
|
|
695
|
+
f"Default or guessed gender '{gender_name_to_use}' does not exist in Gender table."
|
|
696
|
+
)
|
|
493
697
|
|
|
494
698
|
# Update name DB
|
|
495
699
|
update_name_db(first_name, last_name)
|
|
@@ -503,22 +707,95 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
|
|
|
503
707
|
return sensitive_meta
|
|
504
708
|
|
|
505
709
|
|
|
506
|
-
def update_sensitive_meta_from_dict(
|
|
507
|
-
""
|
|
508
|
-
|
|
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
|
+
}
|
|
509
762
|
# Exclude FKs that should not be updated directly from dict keys (handled separately or via save logic)
|
|
510
763
|
excluded_fields = {"pseudo_patient", "pseudo_examination"}
|
|
511
|
-
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
|
+
}
|
|
767
|
+
|
|
768
|
+
# Handle potential Center update - accept both center_name (string) and center (object)
|
|
769
|
+
from ..administration import Center
|
|
512
770
|
|
|
513
|
-
#
|
|
771
|
+
center = data.get("center") # First try direct Center object
|
|
514
772
|
center_name = data.get("center_name")
|
|
515
|
-
|
|
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
|
|
516
787
|
try:
|
|
517
|
-
|
|
518
|
-
instance.center =
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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}")
|
|
791
|
+
except Center.DoesNotExist:
|
|
792
|
+
logger.warning(
|
|
793
|
+
f"Center '{center_name}' not found during update. Keeping existing center."
|
|
794
|
+
)
|
|
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)
|
|
522
799
|
|
|
523
800
|
# Set examiner names if provided, before calling save
|
|
524
801
|
examiner_first_name = data.get("examiner_first_name")
|
|
@@ -537,10 +814,14 @@ def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, A
|
|
|
537
814
|
elif isinstance(patient_gender_input, str):
|
|
538
815
|
gender_input_clean = patient_gender_input.strip()
|
|
539
816
|
# Try direct case-insensitive DB lookup first
|
|
540
|
-
gender_obj = Gender.objects.filter(
|
|
817
|
+
gender_obj = Gender.objects.filter(
|
|
818
|
+
name__iexact=gender_input_clean
|
|
819
|
+
).first()
|
|
541
820
|
if gender_obj:
|
|
542
821
|
selected_data["patient_gender"] = gender_obj
|
|
543
|
-
logger.debug(
|
|
822
|
+
logger.debug(
|
|
823
|
+
f"Successfully matched gender string '{patient_gender_input}' to Gender object via iexact lookup"
|
|
824
|
+
)
|
|
544
825
|
else:
|
|
545
826
|
# Use mapping helper for fallback
|
|
546
827
|
mapped = _map_gender_string_to_standard(gender_input_clean)
|
|
@@ -548,35 +829,60 @@ def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, A
|
|
|
548
829
|
gender_obj = Gender.objects.filter(name__iexact=mapped).first()
|
|
549
830
|
if gender_obj:
|
|
550
831
|
selected_data["patient_gender"] = gender_obj
|
|
551
|
-
logger.info(
|
|
832
|
+
logger.info(
|
|
833
|
+
f"Mapped gender '{patient_gender_input}' to '{mapped}' via fallback mapping"
|
|
834
|
+
)
|
|
552
835
|
else:
|
|
553
|
-
logger.warning(
|
|
554
|
-
|
|
836
|
+
logger.warning(
|
|
837
|
+
f"Mapped gender '{patient_gender_input}' to '{mapped}', but no such Gender in DB. Trying 'unknown'."
|
|
838
|
+
)
|
|
839
|
+
unknown_gender = Gender.objects.filter(
|
|
840
|
+
name__iexact="unknown"
|
|
841
|
+
).first()
|
|
555
842
|
if unknown_gender:
|
|
556
843
|
selected_data["patient_gender"] = unknown_gender
|
|
557
|
-
logger.warning(
|
|
844
|
+
logger.warning(
|
|
845
|
+
f"Using 'unknown' gender as fallback for '{patient_gender_input}'"
|
|
846
|
+
)
|
|
558
847
|
else:
|
|
559
|
-
logger.error(
|
|
848
|
+
logger.error(
|
|
849
|
+
f"No 'unknown' gender found in database. Cannot handle gender '{patient_gender_input}'. Skipping gender update."
|
|
850
|
+
)
|
|
560
851
|
selected_data.pop("patient_gender", None)
|
|
561
852
|
else:
|
|
562
853
|
# Last resort: try to get 'unknown' gender
|
|
563
|
-
unknown_gender = Gender.objects.filter(
|
|
854
|
+
unknown_gender = Gender.objects.filter(
|
|
855
|
+
name__iexact="unknown"
|
|
856
|
+
).first()
|
|
564
857
|
if unknown_gender:
|
|
565
858
|
selected_data["patient_gender"] = unknown_gender
|
|
566
|
-
logger.warning(
|
|
859
|
+
logger.warning(
|
|
860
|
+
f"Using 'unknown' gender as fallback for '{patient_gender_input}' (no mapping)"
|
|
861
|
+
)
|
|
567
862
|
else:
|
|
568
|
-
logger.error(
|
|
863
|
+
logger.error(
|
|
864
|
+
f"No 'unknown' gender found in database. Cannot handle gender '{patient_gender_input}'. Skipping gender update."
|
|
865
|
+
)
|
|
569
866
|
selected_data.pop("patient_gender", None)
|
|
570
867
|
else:
|
|
571
|
-
logger.warning(
|
|
868
|
+
logger.warning(
|
|
869
|
+
f"Unexpected patient_gender type {type(patient_gender_input)}: {patient_gender_input}. Skipping gender update."
|
|
870
|
+
)
|
|
572
871
|
selected_data.pop("patient_gender", None)
|
|
573
872
|
except Exception as e:
|
|
574
|
-
logger.exception(
|
|
873
|
+
logger.exception(
|
|
874
|
+
f"Error handling patient_gender '{patient_gender_input}': {e}. Skipping gender update."
|
|
875
|
+
)
|
|
575
876
|
selected_data.pop("patient_gender", None)
|
|
576
877
|
|
|
577
878
|
# Update other attributes from selected_data
|
|
578
879
|
patient_name_changed = False
|
|
579
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
|
+
|
|
580
886
|
# Avoid overwriting examiner names if they were just explicitly set
|
|
581
887
|
if (
|
|
582
888
|
k not in ["examiner_first_name", "examiner_last_name"]
|
|
@@ -588,7 +894,9 @@ def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, A
|
|
|
588
894
|
value_to_set = v
|
|
589
895
|
if k == "patient_dob":
|
|
590
896
|
if isinstance(v, date) and not isinstance(v, datetime):
|
|
591
|
-
aware_dob = timezone.make_aware(
|
|
897
|
+
aware_dob = timezone.make_aware(
|
|
898
|
+
datetime.combine(v, datetime.min.time())
|
|
899
|
+
)
|
|
592
900
|
value_to_set = aware_dob
|
|
593
901
|
logger.debug(
|
|
594
902
|
"Converted patient_dob from date to aware datetime during update: %s",
|
|
@@ -611,9 +919,15 @@ def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, A
|
|
|
611
919
|
try:
|
|
612
920
|
import dateparser
|
|
613
921
|
|
|
614
|
-
parsed_dob = dateparser.parse(
|
|
922
|
+
parsed_dob = dateparser.parse(
|
|
923
|
+
v, languages=["de"], settings={"DATE_ORDER": "DMY"}
|
|
924
|
+
)
|
|
615
925
|
if parsed_dob:
|
|
616
|
-
value_to_set = timezone.make_aware(
|
|
926
|
+
value_to_set = timezone.make_aware(
|
|
927
|
+
parsed_dob.replace(
|
|
928
|
+
hour=0, minute=0, second=0, microsecond=0
|
|
929
|
+
)
|
|
930
|
+
)
|
|
617
931
|
logger.debug(
|
|
618
932
|
"Parsed string patient_dob '%s' during update to aware datetime: %s",
|
|
619
933
|
v,
|
|
@@ -648,7 +962,9 @@ def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, A
|
|
|
648
962
|
try:
|
|
649
963
|
import dateparser
|
|
650
964
|
|
|
651
|
-
parsed_date = dateparser.parse(
|
|
965
|
+
parsed_date = dateparser.parse(
|
|
966
|
+
v, languages=["de"], settings={"DATE_ORDER": "DMY"}
|
|
967
|
+
)
|
|
652
968
|
if parsed_date:
|
|
653
969
|
value_to_set = parsed_date.date()
|
|
654
970
|
logger.debug(
|
|
@@ -672,13 +988,18 @@ def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, A
|
|
|
672
988
|
# --- End Conversion ---
|
|
673
989
|
|
|
674
990
|
# Check if patient name is changing
|
|
675
|
-
if
|
|
991
|
+
if (
|
|
992
|
+
k in ["patient_first_name", "patient_last_name"]
|
|
993
|
+
and getattr(instance, k) != value_to_set
|
|
994
|
+
):
|
|
676
995
|
patient_name_changed = True
|
|
677
996
|
|
|
678
997
|
setattr(instance, k, value_to_set) # Use value_to_set
|
|
679
998
|
|
|
680
999
|
except Exception as e:
|
|
681
|
-
logger.error(
|
|
1000
|
+
logger.error(
|
|
1001
|
+
f"Error setting attribute '{k}' to '{v}': {e}. Skipping this field."
|
|
1002
|
+
)
|
|
682
1003
|
continue
|
|
683
1004
|
|
|
684
1005
|
# Update name DB if patient names changed
|