endoreg-db 0.4.5__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/admin.py +90 -1
- endoreg_db/api_urls.py +4 -0
- endoreg_db/apps.py +12 -0
- endoreg_db/assets/dummy_model.ckpt +1 -0
- endoreg_db/codemods/readme.md +88 -0
- endoreg_db/codemods/rename_datetime_fields.py +92 -0
- endoreg_db/config/env.py +101 -0
- endoreg_db/data/__init__.py +76 -4
- endoreg_db/data/ai_model/data.yaml +7 -0
- endoreg_db/data/{label → ai_model_label}/label/data.yaml +27 -1
- endoreg_db/data/ai_model_label/label/polyp_classification.yaml +52 -0
- endoreg_db/data/ai_model_label/label-set/data.yaml +40 -0
- endoreg_db/data/ai_model_label/label-set/polyp_classifications.yaml +25 -0
- endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +27 -0
- endoreg_db/data/ai_model_video_segmentation_label/base_segmentation.yaml +176 -0
- endoreg_db/data/ai_model_video_segmentation_labelset/data.yaml +20 -0
- endoreg_db/data/center/data.yaml +40 -9
- endoreg_db/data/center_shift/ukw.yaml +9 -0
- endoreg_db/data/contraindication/bleeding.yaml +11 -0
- endoreg_db/data/db_summary.csv +58 -0
- endoreg_db/data/db_summary.xlsx +0 -0
- endoreg_db/data/disease/misc.yaml +1 -2
- endoreg_db/data/disease_classification/chronic_kidney_disease.yaml +2 -2
- endoreg_db/data/disease_classification_choice/chronic_kidney_disease.yaml +6 -6
- endoreg_db/data/distribution/numeric/data.yaml +14 -0
- endoreg_db/data/endoscope/data.yaml +93 -0
- endoreg_db/data/endoscopy_processor/data.yaml +3 -0
- endoreg_db/data/event/cardiology.yaml +0 -13
- endoreg_db/data/examination/examinations/data.yaml +34 -28
- endoreg_db/data/examination/type/data.yaml +12 -0
- endoreg_db/data/examination_indication/endoscopy.yaml +424 -0
- endoreg_db/data/examination_indication_classification/endoscopy.yaml +160 -0
- endoreg_db/data/examination_indication_classification_choice/endoscopy.yaml +101 -0
- endoreg_db/data/examination_requirement_set/colonoscopy.yaml +15 -0
- endoreg_db/data/finding/anatomy_colon.yaml +128 -0
- endoreg_db/data/finding/colonoscopy.yaml +40 -0
- endoreg_db/data/finding/colonoscopy_bowel_prep.yaml +56 -0
- endoreg_db/data/finding/complication.yaml +16 -0
- endoreg_db/data/finding/data.yaml +105 -0
- endoreg_db/data/finding/examination_setting.yaml +16 -0
- endoreg_db/data/finding/medication_related.yaml +18 -0
- endoreg_db/data/finding/outcome.yaml +12 -0
- endoreg_db/data/finding_classification/colonoscopy_bowel_preparation.yaml +95 -0
- endoreg_db/data/finding_classification/colonoscopy_jnet.yaml +22 -0
- endoreg_db/data/finding_classification/colonoscopy_kudo.yaml +25 -0
- endoreg_db/data/finding_classification/colonoscopy_lesion_circularity.yaml +20 -0
- endoreg_db/data/finding_classification/colonoscopy_lesion_planarity.yaml +24 -0
- endoreg_db/data/finding_classification/colonoscopy_lesion_size.yaml +68 -0
- endoreg_db/data/finding_classification/colonoscopy_lesion_surface.yaml +20 -0
- endoreg_db/data/finding_classification/colonoscopy_location.yaml +80 -0
- endoreg_db/data/finding_classification/colonoscopy_lst.yaml +21 -0
- endoreg_db/data/finding_classification/colonoscopy_nice.yaml +20 -0
- endoreg_db/data/finding_classification/colonoscopy_paris.yaml +26 -0
- endoreg_db/data/finding_classification/colonoscopy_sano.yaml +22 -0
- endoreg_db/data/finding_classification/colonoscopy_summary.yaml +53 -0
- endoreg_db/data/finding_classification/complication_generic.yaml +25 -0
- endoreg_db/data/finding_classification/examination_setting_generic.yaml +40 -0
- endoreg_db/data/finding_classification/histology_colo.yaml +51 -0
- endoreg_db/data/finding_classification/intervention_required.yaml +26 -0
- endoreg_db/data/finding_classification/medication_related.yaml +23 -0
- endoreg_db/data/finding_classification/visualized.yaml +33 -0
- endoreg_db/data/finding_classification_choice/bowel_preparation.yaml +78 -0
- endoreg_db/data/finding_classification_choice/colon_lesion_circularity_default.yaml +32 -0
- endoreg_db/data/finding_classification_choice/colon_lesion_jnet.yaml +15 -0
- endoreg_db/data/finding_classification_choice/colon_lesion_kudo.yaml +23 -0
- endoreg_db/data/finding_classification_choice/colon_lesion_lst.yaml +15 -0
- endoreg_db/data/finding_classification_choice/colon_lesion_nice.yaml +17 -0
- endoreg_db/data/finding_classification_choice/colon_lesion_paris.yaml +57 -0
- endoreg_db/data/finding_classification_choice/colon_lesion_planarity_default.yaml +49 -0
- endoreg_db/data/finding_classification_choice/colon_lesion_sano.yaml +14 -0
- endoreg_db/data/finding_classification_choice/colon_lesion_surface_intact_default.yaml +36 -0
- endoreg_db/data/finding_classification_choice/colonoscopy_location.yaml +229 -0
- endoreg_db/data/finding_classification_choice/colonoscopy_not_complete_reason.yaml +19 -0
- endoreg_db/data/finding_classification_choice/colonoscopy_size.yaml +82 -0
- endoreg_db/data/finding_classification_choice/colonoscopy_summary_worst_finding.yaml +15 -0
- endoreg_db/data/finding_classification_choice/complication_generic_types.yaml +15 -0
- endoreg_db/data/finding_classification_choice/examination_setting_generic_types.yaml +15 -0
- endoreg_db/data/finding_classification_choice/histology.yaml +24 -0
- endoreg_db/data/finding_classification_choice/histology_polyp.yaml +20 -0
- endoreg_db/data/finding_classification_choice/outcome.yaml +19 -0
- endoreg_db/data/finding_classification_choice/yes_no_na.yaml +11 -0
- endoreg_db/data/finding_classification_type/colonoscopy_basic.yaml +48 -0
- endoreg_db/data/finding_intervention/endoscopy.yaml +43 -0
- endoreg_db/data/finding_intervention/endoscopy_colonoscopy.yaml +168 -0
- endoreg_db/data/finding_intervention/endoscopy_egd.yaml +128 -0
- endoreg_db/data/finding_intervention/endoscopy_ercp.yaml +32 -0
- endoreg_db/data/finding_intervention/endoscopy_eus_lower.yaml +9 -0
- endoreg_db/data/finding_intervention/endoscopy_eus_upper.yaml +36 -0
- endoreg_db/data/finding_intervention_type/endoscopy.yaml +15 -0
- endoreg_db/data/finding_morphology_classification_type/colonoscopy.yaml +79 -0
- endoreg_db/data/finding_type/data.yaml +43 -0
- endoreg_db/data/gender/data.yaml +24 -0
- endoreg_db/data/information_source/annotation.yaml +6 -0
- endoreg_db/data/information_source/endoscopy_guidelines.yaml +7 -0
- endoreg_db/data/information_source/prediction.yaml +7 -0
- endoreg_db/data/information_source_type/data.yaml +8 -0
- endoreg_db/data/lab_value/cardiac_enzymes.yaml +7 -1
- endoreg_db/data/lab_value/coagulation.yaml +6 -1
- endoreg_db/data/lab_value/electrolytes.yaml +39 -1
- endoreg_db/data/lab_value/gastrointestinal_function.yaml +12 -0
- endoreg_db/data/lab_value/hematology.yaml +17 -2
- endoreg_db/data/lab_value/hormones.yaml +6 -0
- endoreg_db/data/lab_value/lipids.yaml +12 -3
- endoreg_db/data/lab_value/misc.yaml +48 -2
- endoreg_db/data/lab_value/renal_function.yaml +2 -1
- endoreg_db/data/lx_client_tag/base.yaml +54 -0
- endoreg_db/data/lx_client_type/base.yaml +30 -0
- endoreg_db/data/lx_permission/base.yaml +24 -0
- endoreg_db/data/lx_permission/endoreg.yaml +52 -0
- endoreg_db/data/medication/anticoagulation.yaml +5 -5
- endoreg_db/data/medication/tah.yaml +5 -5
- endoreg_db/data/medication_indication/anticoagulation.yaml +48 -53
- endoreg_db/data/medication_intake_time/base.yaml +4 -4
- endoreg_db/data/names_first/first_names.yaml +54 -0
- endoreg_db/data/names_last/last_names.yaml +51 -0
- endoreg_db/data/network_device/data.yaml +30 -0
- endoreg_db/data/organ/data.yaml +29 -0
- endoreg_db/data/pdf_type/data.yaml +27 -9
- endoreg_db/data/qualification/endoscopy.yaml +36 -0
- endoreg_db/data/qualification/m2.yaml +39 -0
- endoreg_db/data/qualification/outpatient_clinic.yaml +35 -0
- endoreg_db/data/qualification/sonography.yaml +36 -0
- endoreg_db/data/qualification_type/base.yaml +29 -0
- endoreg_db/data/report_reader_flag/rkh-histology-generic.yaml +10 -0
- endoreg_db/data/report_reader_flag/ukw-examination-generic.yaml +4 -0
- endoreg_db/data/report_reader_flag/ukw-histology-generic.yaml +5 -0
- endoreg_db/data/requirement/age.yaml +26 -0
- endoreg_db/data/requirement/colonoscopy_baseline_austria.yaml +45 -0
- endoreg_db/data/requirement/disease_cardiovascular.yaml +79 -0
- endoreg_db/data/requirement/disease_classification_choice_cardiovascular.yaml +41 -0
- endoreg_db/data/requirement/disease_hepatology.yaml +12 -0
- endoreg_db/data/requirement/disease_misc.yaml +12 -0
- endoreg_db/data/requirement/disease_renal.yaml +96 -0
- endoreg_db/data/requirement/endoscopy_bleeding_risk.yaml +59 -0
- endoreg_db/data/requirement/event_cardiology.yaml +251 -0
- endoreg_db/data/requirement/event_requirements.yaml +145 -0
- endoreg_db/data/requirement/finding_colon_polyp.yaml +50 -0
- endoreg_db/data/requirement/gender.yaml +25 -0
- endoreg_db/data/requirement/lab_value.yaml +441 -0
- endoreg_db/data/requirement/medication.yaml +93 -0
- endoreg_db/data/requirement_operator/age.yaml +13 -0
- endoreg_db/data/requirement_operator/lab_operators.yaml +129 -0
- endoreg_db/data/requirement_operator/model_operators.yaml +96 -0
- endoreg_db/data/requirement_set/01_endoscopy_generic.yaml +48 -0
- endoreg_db/data/requirement_set/colonoscopy_austria_screening.yaml +57 -0
- endoreg_db/data/requirement_set/endoscopy_bleeding_risk.yaml +52 -0
- endoreg_db/data/requirement_set_type/data.yaml +20 -0
- endoreg_db/data/requirement_type/requirement_types.yaml +165 -0
- endoreg_db/data/risk/bleeding.yaml +26 -0
- endoreg_db/data/risk/thrombosis.yaml +37 -0
- endoreg_db/data/risk_type/data.yaml +27 -0
- endoreg_db/data/setup_config.yaml +38 -0
- endoreg_db/data/shift/endoscopy.yaml +21 -0
- endoreg_db/data/shift_type/base.yaml +35 -0
- endoreg_db/data/tag/requirement_set_tags.yaml +11 -0
- endoreg_db/data/unit/concentration.yaml +23 -0
- endoreg_db/data/unit/time.yaml +36 -1
- endoreg_db/exceptions.py +19 -0
- endoreg_db/forms/__init__.py +3 -1
- endoreg_db/forms/examination_form.py +11 -0
- endoreg_db/forms/patient_finding_intervention_form.py +18 -0
- endoreg_db/forms/patient_form.py +27 -0
- endoreg_db/forms/questionnaires/__init__.py +1 -1
- endoreg_db/forms/questionnaires/tto_questionnaire.py +19 -19
- endoreg_db/helpers/count_db.py +45 -0
- endoreg_db/helpers/data_loader.py +208 -0
- endoreg_db/helpers/default_objects.py +378 -0
- endoreg_db/helpers/download_segmentation_model.py +31 -0
- endoreg_db/helpers/interact.py +6 -0
- endoreg_db/helpers/test_video_helper.py +119 -0
- endoreg_db/logger_conf.py +140 -0
- endoreg_db/management/__init__.py +1 -0
- endoreg_db/management/commands/__init__.py +1 -0
- endoreg_db/management/commands/anonymize_video.py +0 -0
- endoreg_db/management/commands/check_auth.py +125 -0
- endoreg_db/management/commands/create_model_meta_from_huggingface.py +115 -0
- endoreg_db/management/commands/create_multilabel_model_meta.py +214 -0
- endoreg_db/management/commands/fix_missing_patient_data.py +172 -0
- endoreg_db/management/commands/fix_video_paths.py +165 -0
- endoreg_db/management/commands/import_fallback_video.py +203 -0
- endoreg_db/management/commands/import_report.py +298 -0
- endoreg_db/management/commands/import_video.py +423 -0
- endoreg_db/management/commands/import_video_with_classification.py +367 -0
- endoreg_db/management/commands/init_default_ai_model.py +112 -0
- endoreg_db/management/commands/load_ai_model_data.py +58 -26
- endoreg_db/management/commands/load_ai_model_label_data.py +59 -0
- endoreg_db/management/commands/load_base_db_data.py +174 -118
- endoreg_db/management/commands/load_center_data.py +46 -21
- endoreg_db/management/commands/{load_logging_data.py → load_contraindication_data.py} +4 -2
- endoreg_db/management/commands/load_disease_data.py +29 -7
- endoreg_db/management/commands/{load_endoscope_type_data.py → load_endoscope_data.py} +30 -7
- endoreg_db/management/commands/load_examination_indication_data.py +86 -0
- endoreg_db/management/commands/load_finding_data.py +128 -0
- endoreg_db/management/commands/load_green_endoscopy_wuerzburg_data.py +0 -1
- endoreg_db/management/commands/load_information_source.py +13 -7
- endoreg_db/management/commands/load_lab_value_data.py +3 -3
- endoreg_db/management/commands/load_medication_data.py +83 -21
- endoreg_db/management/commands/load_name_data.py +37 -0
- endoreg_db/management/commands/{load_medication_intake_time_data.py → load_organ_data.py} +7 -5
- endoreg_db/management/commands/load_qualification_data.py +59 -0
- endoreg_db/management/commands/load_requirement_data.py +180 -0
- endoreg_db/management/commands/load_risk_data.py +56 -0
- endoreg_db/management/commands/load_shift_data.py +60 -0
- endoreg_db/management/commands/load_tag_data.py +57 -0
- endoreg_db/management/commands/register_ai_model.py +1 -1
- endoreg_db/management/commands/setup_endoreg_db.py +381 -0
- endoreg_db/management/commands/start_filewatcher.py +106 -0
- endoreg_db/management/commands/storage_management.py +548 -0
- endoreg_db/management/commands/summarize_db_content.py +189 -0
- endoreg_db/management/commands/validate_video.py +204 -0
- endoreg_db/management/commands/validate_video_files.py +161 -0
- endoreg_db/management/commands/video_validation.py +22 -0
- endoreg_db/mermaid/Overall_flow_patient_finding_intervention.md +10 -0
- endoreg_db/mermaid/anonymized_image_annotation.md +20 -0
- endoreg_db/mermaid/binary_classification_annotation.md +50 -0
- endoreg_db/mermaid/classification.md +8 -0
- endoreg_db/mermaid/examination.md +8 -0
- endoreg_db/mermaid/findings.md +7 -0
- endoreg_db/mermaid/image_classification.md +28 -0
- endoreg_db/mermaid/interventions.md +8 -0
- endoreg_db/mermaid/morphology.md +8 -0
- endoreg_db/mermaid/patient_creation.md +14 -0
- endoreg_db/mermaid/video_segmentation_annotation.md +17 -0
- endoreg_db/migrations/0001_initial.py +1234 -944
- endoreg_db/migrations/0002_add_video_correction_models.py +52 -0
- endoreg_db/migrations/0003_add_center_display_name.py +30 -0
- endoreg_db/models/__init__.py +339 -53
- endoreg_db/models/administration/__init__.py +116 -0
- endoreg_db/models/administration/ai/__init__.py +9 -0
- endoreg_db/models/administration/ai/active_model.py +35 -0
- endoreg_db/models/administration/ai/ai_model.py +156 -0
- endoreg_db/models/{ai_model → administration/ai}/model_type.py +19 -4
- endoreg_db/models/administration/case/__init__.py +19 -0
- endoreg_db/models/administration/case/case.py +114 -0
- endoreg_db/models/{case_template → administration/case/case_template}/__init__.py +10 -1
- endoreg_db/models/{case_template → administration/case/case_template}/case_template.py +60 -16
- endoreg_db/models/{case_template → administration/case/case_template}/case_template_rule.py +6 -13
- endoreg_db/models/{case_template → administration/case/case_template}/case_template_rule_value.py +21 -8
- endoreg_db/models/{case_template → administration/case/case_template}/case_template_type.py +1 -3
- endoreg_db/models/{center → administration/center}/__init__.py +9 -0
- endoreg_db/models/administration/center/center.py +67 -0
- endoreg_db/models/administration/center/center_product.py +64 -0
- endoreg_db/models/administration/center/center_resource.py +49 -0
- endoreg_db/models/administration/center/center_shift.py +88 -0
- endoreg_db/models/administration/center/center_waste.py +30 -0
- endoreg_db/models/administration/permissions/__init__.py +44 -0
- endoreg_db/models/administration/person/__init__.py +24 -0
- endoreg_db/models/administration/person/employee/__init__.py +3 -0
- endoreg_db/models/administration/person/employee/employee.py +35 -0
- endoreg_db/models/administration/person/employee/employee_qualification.py +39 -0
- endoreg_db/models/administration/person/employee/employee_type.py +42 -0
- endoreg_db/models/administration/person/examiner/__init__.py +4 -0
- endoreg_db/models/administration/person/examiner/examiner.py +54 -0
- endoreg_db/models/administration/person/names/__init__.py +0 -0
- endoreg_db/models/{persons → administration/person/names}/first_name.py +1 -1
- endoreg_db/models/{persons → administration/person/names}/last_name.py +2 -3
- endoreg_db/models/administration/person/patient/__init__.py +5 -0
- endoreg_db/models/administration/person/patient/patient.py +460 -0
- endoreg_db/models/administration/person/profession/__init__.py +24 -0
- endoreg_db/models/administration/person/user/__init__.py +5 -0
- endoreg_db/models/administration/person/user/portal_user_information.py +37 -0
- endoreg_db/models/administration/product/__init__.py +14 -0
- endoreg_db/models/administration/product/product.py +97 -0
- endoreg_db/models/administration/product/product_group.py +39 -0
- endoreg_db/models/administration/product/product_material.py +54 -0
- endoreg_db/models/{product → administration/product}/product_weight.py +21 -0
- endoreg_db/models/{product → administration/product}/reference_product.py +44 -13
- endoreg_db/models/administration/qualification/__init__.py +7 -0
- endoreg_db/models/administration/qualification/qualification.py +37 -0
- endoreg_db/models/administration/qualification/qualification_type.py +35 -0
- endoreg_db/models/administration/shift/__init__.py +9 -0
- endoreg_db/models/administration/shift/scheduled_days.py +69 -0
- endoreg_db/models/administration/shift/shift.py +51 -0
- endoreg_db/models/administration/shift/shift_type.py +108 -0
- endoreg_db/models/label/__init__.py +24 -1
- endoreg_db/models/label/annotation/__init__.py +12 -0
- endoreg_db/models/label/annotation/image_classification.py +84 -0
- endoreg_db/models/label/annotation/video_segmentation_annotation.py +66 -0
- endoreg_db/models/label/label.py +53 -54
- endoreg_db/models/label/label_set.py +53 -0
- endoreg_db/models/label/label_type.py +29 -0
- endoreg_db/models/label/label_video_segment/__init__.py +3 -0
- endoreg_db/models/label/label_video_segment/_create_from_video.py +41 -0
- endoreg_db/models/label/label_video_segment/label_video_segment.py +511 -0
- endoreg_db/models/label/video_segmentation_label.py +31 -0
- endoreg_db/models/label/video_segmentation_labelset.py +27 -0
- endoreg_db/models/media/__init__.py +16 -0
- endoreg_db/models/media/frame/__init__.py +3 -0
- endoreg_db/models/media/frame/frame.py +111 -0
- endoreg_db/models/media/pdf/__init__.py +11 -0
- endoreg_db/models/media/pdf/raw_pdf.py +757 -0
- endoreg_db/models/media/pdf/report_file.py +162 -0
- endoreg_db/models/media/pdf/report_reader/__init__.py +7 -0
- endoreg_db/models/media/pdf/report_reader/report_reader_config.py +77 -0
- endoreg_db/models/media/video/__init__.py +8 -0
- endoreg_db/models/media/video/create_from_file.py +358 -0
- endoreg_db/models/media/video/pipe_1.py +213 -0
- endoreg_db/models/media/video/pipe_2.py +105 -0
- endoreg_db/models/media/video/refactor_plan.md +0 -0
- endoreg_db/models/media/video/video_file.py +825 -0
- endoreg_db/models/media/video/video_file_ai.py +443 -0
- endoreg_db/models/media/video/video_file_anonymize.py +349 -0
- endoreg_db/models/media/video/video_file_frames/__init__.py +47 -0
- endoreg_db/models/media/video/video_file_frames/_bulk_create_frames.py +22 -0
- endoreg_db/models/media/video/video_file_frames/_create_frame_object.py +23 -0
- endoreg_db/models/media/video/video_file_frames/_delete_frames.py +104 -0
- endoreg_db/models/media/video/video_file_frames/_extract_frames.py +174 -0
- endoreg_db/models/media/video/video_file_frames/_get_frame.py +28 -0
- endoreg_db/models/media/video/video_file_frames/_get_frame_number.py +27 -0
- endoreg_db/models/media/video/video_file_frames/_get_frame_path.py +20 -0
- endoreg_db/models/media/video/video_file_frames/_get_frame_paths.py +27 -0
- endoreg_db/models/media/video/video_file_frames/_get_frame_range.py +34 -0
- endoreg_db/models/media/video/video_file_frames/_get_frames.py +27 -0
- endoreg_db/models/media/video/video_file_frames/_initialize_frames.py +129 -0
- endoreg_db/models/media/video/video_file_frames/_manage_frame_range.py +141 -0
- endoreg_db/models/media/video/video_file_frames/_mark_frames_extracted_status.py +65 -0
- endoreg_db/models/media/video/video_file_frames.py +0 -0
- endoreg_db/models/media/video/video_file_io.py +168 -0
- endoreg_db/models/media/video/video_file_meta/__init__.py +22 -0
- endoreg_db/models/media/video/video_file_meta/get_crop_template.py +45 -0
- endoreg_db/models/media/video/video_file_meta/get_endo_roi.py +39 -0
- endoreg_db/models/media/video/video_file_meta/get_fps.py +147 -0
- endoreg_db/models/media/video/video_file_meta/initialize_video_specs.py +143 -0
- endoreg_db/models/media/video/video_file_meta/text_meta.py +134 -0
- endoreg_db/models/media/video/video_file_meta/video_meta.py +70 -0
- endoreg_db/models/media/video/video_file_segments.py +209 -0
- endoreg_db/models/media/video/video_metadata.py +65 -0
- endoreg_db/models/media/video/video_processing.py +152 -0
- endoreg_db/models/medical/__init__.py +146 -0
- endoreg_db/models/medical/contraindication/__init__.py +17 -0
- endoreg_db/models/medical/disease.py +156 -0
- endoreg_db/models/medical/event.py +137 -0
- endoreg_db/models/medical/examination/__init__.py +9 -0
- endoreg_db/models/medical/examination/examination.py +148 -0
- endoreg_db/models/medical/examination/examination_indication.py +278 -0
- endoreg_db/models/medical/examination/examination_time.py +49 -0
- endoreg_db/models/medical/examination/examination_time_type.py +41 -0
- endoreg_db/models/medical/examination/examination_type.py +48 -0
- endoreg_db/models/medical/finding/__init__.py +18 -0
- endoreg_db/models/medical/finding/finding.py +96 -0
- endoreg_db/models/medical/finding/finding_classification.py +142 -0
- endoreg_db/models/medical/finding/finding_intervention.py +52 -0
- endoreg_db/models/medical/finding/finding_type.py +35 -0
- endoreg_db/models/medical/hardware/__init__.py +8 -0
- endoreg_db/models/medical/hardware/endoscope.py +65 -0
- endoreg_db/models/{hardware → medical/hardware}/endoscopy_processor.py +68 -29
- endoreg_db/models/medical/laboratory/__init__.py +5 -0
- endoreg_db/models/medical/laboratory/lab_value.py +419 -0
- endoreg_db/models/medical/medication/__init__.py +19 -0
- endoreg_db/models/medical/medication/medication.py +31 -0
- endoreg_db/models/medical/medication/medication_indication.py +50 -0
- endoreg_db/models/medical/medication/medication_indication_type.py +39 -0
- endoreg_db/models/medical/medication/medication_intake_time.py +44 -0
- endoreg_db/models/medical/medication/medication_schedule.py +45 -0
- endoreg_db/models/medical/organ/__init__.py +35 -0
- endoreg_db/models/medical/patient/__init__.py +56 -0
- endoreg_db/models/medical/patient/medication_examples.py +38 -0
- endoreg_db/models/medical/patient/patient_disease.py +63 -0
- endoreg_db/models/medical/patient/patient_event.py +75 -0
- endoreg_db/models/medical/patient/patient_examination.py +249 -0
- endoreg_db/models/medical/patient/patient_examination_indication.py +44 -0
- endoreg_db/models/medical/patient/patient_finding.py +357 -0
- endoreg_db/models/medical/patient/patient_finding_classification.py +207 -0
- endoreg_db/models/medical/patient/patient_finding_intervention.py +40 -0
- endoreg_db/models/medical/patient/patient_lab_sample.py +148 -0
- endoreg_db/models/{persons → medical}/patient/patient_lab_value.py +68 -22
- endoreg_db/models/medical/patient/patient_medication.py +104 -0
- endoreg_db/models/medical/patient/patient_medication_schedule.py +136 -0
- endoreg_db/models/medical/risk/__init__.py +7 -0
- endoreg_db/models/medical/risk/risk.py +72 -0
- endoreg_db/models/medical/risk/risk_type.py +51 -0
- endoreg_db/models/metadata/__init__.py +19 -0
- endoreg_db/models/metadata/frame_ocr_result.py +0 -0
- endoreg_db/models/metadata/model_meta.py +206 -0
- endoreg_db/models/metadata/model_meta_logic.py +343 -0
- endoreg_db/models/{data_file/metadata → metadata}/pdf_meta.py +32 -13
- endoreg_db/models/metadata/sensitive_meta.py +288 -0
- endoreg_db/models/metadata/sensitive_meta_logic.py +1048 -0
- endoreg_db/models/metadata/video_meta.py +332 -0
- endoreg_db/models/metadata/video_prediction_logic.py +190 -0
- endoreg_db/models/metadata/video_prediction_meta.py +270 -0
- endoreg_db/models/other/__init__.py +36 -1
- endoreg_db/models/other/distribution/__init__.py +44 -0
- endoreg_db/models/other/distribution/base_value_distribution.py +20 -0
- endoreg_db/models/other/distribution/date_value_distribution.py +89 -0
- endoreg_db/models/other/distribution/multiple_categorical_value_distribution.py +32 -0
- endoreg_db/models/other/distribution/numeric_value_distribution.py +125 -0
- endoreg_db/models/other/distribution/single_categorical_value_distribution.py +22 -0
- endoreg_db/models/other/emission/__init__.py +5 -0
- endoreg_db/models/other/emission/emission_factor.py +94 -0
- endoreg_db/models/{persons → other}/gender.py +8 -3
- endoreg_db/models/other/information_source.py +159 -0
- endoreg_db/models/other/material.py +14 -2
- endoreg_db/models/other/resource.py +6 -2
- endoreg_db/models/other/tag.py +27 -0
- endoreg_db/models/other/transport_route.py +15 -3
- endoreg_db/models/{unit.py → other/unit.py} +16 -6
- endoreg_db/models/other/waste.py +10 -3
- endoreg_db/models/requirement/__init__.py +11 -0
- endoreg_db/models/requirement/requirement.py +767 -0
- endoreg_db/models/requirement/requirement_evaluation/__init__.py +6 -0
- endoreg_db/models/requirement/requirement_evaluation/get_values.py +40 -0
- endoreg_db/models/requirement/requirement_evaluation/operator_evaluation_models.py +9 -0
- endoreg_db/models/requirement/requirement_evaluation/requirement_type_parser.py +95 -0
- endoreg_db/models/requirement/requirement_operator.py +176 -0
- endoreg_db/models/requirement/requirement_set.py +287 -0
- endoreg_db/models/rule/__init__.py +13 -0
- endoreg_db/models/{rules → rule}/rule.py +6 -3
- endoreg_db/models/{rules → rule}/rule_attribute_dtype.py +0 -2
- endoreg_db/models/{rules → rule}/rule_type.py +0 -2
- endoreg_db/models/{rules → rule}/ruleset.py +0 -2
- endoreg_db/models/state/__init__.py +12 -0
- endoreg_db/models/state/abstract.py +11 -0
- endoreg_db/models/state/audit_ledger.py +150 -0
- endoreg_db/models/state/label_video_segment.py +22 -0
- endoreg_db/models/state/raw_pdf.py +187 -0
- endoreg_db/models/state/sensitive_meta.py +46 -0
- endoreg_db/models/state/video.py +232 -0
- endoreg_db/models/upload_job.py +99 -0
- endoreg_db/models/utils.py +135 -0
- endoreg_db/renames.yml +8 -0
- endoreg_db/root_urls.py +9 -0
- endoreg_db/schemas/__init__.py +0 -0
- endoreg_db/schemas/examination_evaluation.py +27 -0
- endoreg_db/serializers/Frames_NICE_and_PARIS_classifications.py +775 -0
- endoreg_db/serializers/__init__.py +118 -10
- endoreg_db/serializers/_old/raw_pdf_meta_validation.py +223 -0
- endoreg_db/serializers/_old/raw_video_meta_validation.py +179 -0
- endoreg_db/serializers/_old/video.py +71 -0
- endoreg_db/serializers/administration/__init__.py +14 -0
- endoreg_db/serializers/administration/ai/__init__.py +10 -0
- endoreg_db/serializers/administration/ai/active_model.py +10 -0
- endoreg_db/serializers/administration/ai/ai_model.py +18 -0
- endoreg_db/serializers/administration/ai/model_type.py +10 -0
- endoreg_db/serializers/administration/center.py +9 -0
- endoreg_db/serializers/administration/gender.py +9 -0
- endoreg_db/serializers/anonymization.py +69 -0
- endoreg_db/serializers/evaluation/examination_evaluation.py +1 -0
- endoreg_db/serializers/examination/__init__.py +10 -0
- endoreg_db/serializers/examination/base.py +46 -0
- endoreg_db/serializers/examination/dropdown.py +21 -0
- endoreg_db/serializers/examination_serializer.py +12 -0
- endoreg_db/serializers/finding/__init__.py +5 -0
- endoreg_db/serializers/finding/finding.py +54 -0
- endoreg_db/serializers/finding_classification/__init__.py +7 -0
- endoreg_db/serializers/finding_classification/choice.py +19 -0
- endoreg_db/serializers/finding_classification/classification.py +13 -0
- endoreg_db/serializers/label/__init__.py +7 -0
- endoreg_db/serializers/label/image_classification_annotation.py +62 -0
- endoreg_db/serializers/label/label.py +15 -0
- endoreg_db/serializers/label_video_segment/__init__.py +7 -0
- endoreg_db/serializers/label_video_segment/_lvs_create.py +149 -0
- endoreg_db/serializers/label_video_segment/_lvs_update.py +138 -0
- endoreg_db/serializers/label_video_segment/_lvs_validate.py +149 -0
- endoreg_db/serializers/label_video_segment/label_video_segment.py +344 -0
- endoreg_db/serializers/label_video_segment/label_video_segment_annotation.py +99 -0
- endoreg_db/serializers/label_video_segment/label_video_segment_update.py +163 -0
- endoreg_db/serializers/meta/__init__.py +19 -0
- endoreg_db/serializers/meta/pdf_file_meta_extraction.py +115 -0
- endoreg_db/serializers/meta/report_meta.py +53 -0
- endoreg_db/serializers/meta/sensitive_meta_detail.py +162 -0
- endoreg_db/serializers/meta/sensitive_meta_update.py +148 -0
- endoreg_db/serializers/meta/sensitive_meta_verification.py +59 -0
- endoreg_db/serializers/meta/video_meta.py +39 -0
- endoreg_db/serializers/misc/__init__.py +14 -0
- endoreg_db/serializers/misc/file_overview.py +182 -0
- endoreg_db/serializers/misc/sensitive_patient_data.py +120 -0
- endoreg_db/serializers/misc/stats.py +33 -0
- endoreg_db/serializers/misc/translatable_field_mix_in.py +44 -0
- endoreg_db/serializers/misc/upload_job.py +71 -0
- endoreg_db/serializers/patient/__init__.py +11 -0
- endoreg_db/serializers/patient/patient.py +86 -0
- endoreg_db/serializers/patient/patient_dropdown.py +27 -0
- endoreg_db/serializers/patient_examination/__init__.py +7 -0
- endoreg_db/serializers/patient_examination/patient_examination.py +141 -0
- endoreg_db/serializers/patient_finding/__init__.py +15 -0
- endoreg_db/serializers/patient_finding/patient_finding.py +31 -0
- endoreg_db/serializers/patient_finding/patient_finding_classification.py +39 -0
- endoreg_db/serializers/patient_finding/patient_finding_detail.py +53 -0
- endoreg_db/serializers/patient_finding/patient_finding_intervention.py +26 -0
- endoreg_db/serializers/patient_finding/patient_finding_list.py +41 -0
- endoreg_db/serializers/patient_finding/patient_finding_write.py +126 -0
- endoreg_db/serializers/pdf/__init__.py +5 -0
- endoreg_db/serializers/pdf/anony_text_validation.py +85 -0
- endoreg_db/serializers/report/__init__.py +9 -0
- endoreg_db/serializers/report/mixins.py +45 -0
- endoreg_db/serializers/report/report.py +105 -0
- endoreg_db/serializers/report/report_list.py +22 -0
- endoreg_db/serializers/report/secure_file_url.py +26 -0
- endoreg_db/serializers/requirements/requirement_schema.py +25 -0
- endoreg_db/serializers/requirements/requirement_sets.py +29 -0
- endoreg_db/serializers/sensitive_meta_serializer.py +282 -0
- endoreg_db/serializers/video/__init__.py +7 -0
- endoreg_db/serializers/video/segmentation.py +263 -0
- endoreg_db/serializers/video/video_file_brief.py +10 -0
- endoreg_db/serializers/video/video_file_detail.py +83 -0
- endoreg_db/serializers/video/video_file_list.py +67 -0
- endoreg_db/serializers/video/video_metadata.py +105 -0
- endoreg_db/serializers/video/video_processing_history.py +153 -0
- endoreg_db/serializers/video_examination.py +198 -0
- endoreg_db/services/__init__.py +5 -0
- endoreg_db/services/anonymization.py +223 -0
- endoreg_db/services/examination_evaluation.py +149 -0
- endoreg_db/services/finding_description_service.py +0 -0
- endoreg_db/services/lookup_service.py +411 -0
- endoreg_db/services/lookup_store.py +266 -0
- endoreg_db/services/pdf_import.py +1382 -0
- endoreg_db/services/polling_coordinator.py +288 -0
- endoreg_db/services/pseudonym_service.py +89 -0
- endoreg_db/services/requirements_object.py +147 -0
- endoreg_db/services/segment_sync.py +155 -0
- endoreg_db/services/storage_aware_video_processor.py +344 -0
- endoreg_db/services/video_import.py +1259 -0
- endoreg_db/tasks/upload_tasks.py +207 -0
- endoreg_db/tasks/video_ingest.py +157 -0
- endoreg_db/tasks/video_processing_tasks.py +327 -0
- endoreg_db/templates/admin/patient_finding_intervention.html +253 -0
- endoreg_db/templates/admin/start_examination.html +12 -0
- endoreg_db/templates/timeline.html +176 -0
- endoreg_db/urls/__init__.py +83 -0
- endoreg_db/urls/anonymization.py +32 -0
- endoreg_db/urls/auth.py +16 -0
- endoreg_db/urls/classification.py +39 -0
- endoreg_db/urls/examination.py +54 -0
- endoreg_db/urls/files.py +6 -0
- endoreg_db/urls/label_video_segment_validate.py +33 -0
- endoreg_db/urls/label_video_segments.py +46 -0
- endoreg_db/urls/media.py +227 -0
- endoreg_db/urls/patient.py +19 -0
- endoreg_db/urls/report.py +48 -0
- endoreg_db/urls/requirements.py +13 -0
- endoreg_db/urls/sensitive_meta.py +0 -0
- endoreg_db/urls/stats.py +46 -0
- endoreg_db/urls/upload.py +20 -0
- endoreg_db/urls/video.py +61 -0
- endoreg_db/urls.py +9 -0
- endoreg_db/utils/__init__.py +88 -1
- endoreg_db/utils/ai/__init__.py +9 -0
- endoreg_db/{models/ai_model/utils.py → utils/ai/get.py} +1 -4
- endoreg_db/utils/ai/inference_dataset.py +52 -0
- endoreg_db/utils/ai/multilabel_classification_net.py +159 -0
- endoreg_db/utils/ai/postprocess.py +63 -0
- endoreg_db/utils/ai/predict.py +291 -0
- endoreg_db/utils/ai/preprocess.py +68 -0
- endoreg_db/utils/calc_duration_seconds.py +24 -0
- endoreg_db/utils/case_generator/__init__.py +0 -0
- endoreg_db/utils/case_generator/case_generator.py +159 -0
- endoreg_db/utils/case_generator/lab_sample_factory.py +33 -0
- endoreg_db/utils/case_generator/utils.py +30 -0
- endoreg_db/utils/check_video_files.py +148 -0
- endoreg_db/utils/dataloader.py +118 -35
- endoreg_db/utils/dates.py +60 -0
- endoreg_db/utils/env.py +33 -0
- endoreg_db/utils/extract_specific_frames.py +72 -0
- endoreg_db/utils/file_operations.py +29 -1
- endoreg_db/utils/fix_video_path_direct.py +141 -0
- endoreg_db/utils/frame_anonymization_utils.py +463 -0
- endoreg_db/utils/hashs.py +123 -4
- endoreg_db/utils/links/__init__.py +0 -0
- endoreg_db/utils/links/requirement_link.py +193 -0
- endoreg_db/utils/mime_types.py +0 -0
- endoreg_db/utils/names.py +76 -0
- endoreg_db/utils/parse_and_generate_yaml.py +46 -0
- endoreg_db/utils/paths.py +95 -0
- endoreg_db/utils/permissions.py +143 -0
- endoreg_db/utils/pipelines/Readme.md +235 -0
- endoreg_db/utils/pipelines/__init__.py +0 -0
- endoreg_db/utils/pipelines/process_video_dir.py +120 -0
- endoreg_db/utils/product/__init__.py +0 -0
- endoreg_db/utils/product/sum_emissions.py +20 -0
- endoreg_db/utils/product/sum_weights.py +18 -0
- endoreg_db/utils/pydantic_models/__init__.py +6 -0
- endoreg_db/utils/pydantic_models/db_config.py +57 -0
- endoreg_db/utils/requirement_helpers.py +0 -0
- endoreg_db/utils/requirement_operator_logic/__init__.py +0 -0
- endoreg_db/utils/requirement_operator_logic/lab_value_operators.py +578 -0
- endoreg_db/utils/requirement_operator_logic/model_evaluators.py +368 -0
- endoreg_db/utils/setup_config.py +177 -0
- endoreg_db/utils/translation.py +27 -0
- endoreg_db/utils/validate_endo_roi.py +19 -0
- endoreg_db/utils/validate_subcategory_dict.py +91 -0
- endoreg_db/utils/validate_video_detailed.py +357 -0
- endoreg_db/utils/video/__init__.py +26 -0
- endoreg_db/utils/video/extract_frames.py +88 -0
- endoreg_db/utils/video/ffmpeg_wrapper.py +835 -0
- endoreg_db/utils/video/names.py +42 -0
- endoreg_db/utils/video/streaming_processor.py +312 -0
- endoreg_db/utils/video/video_splitter.py +94 -0
- endoreg_db/views/Frames_NICE_and_PARIS_classifications_views.py +238 -0
- endoreg_db/views/__init__.py +274 -0
- endoreg_db/views/anonymization/__init__.py +27 -0
- endoreg_db/views/anonymization/media_management.py +454 -0
- endoreg_db/views/anonymization/overview.py +216 -0
- endoreg_db/views/anonymization/validate.py +107 -0
- endoreg_db/views/auth/__init__.py +13 -0
- endoreg_db/views/auth/keycloak.py +113 -0
- endoreg_db/views/examination/__init__.py +33 -0
- endoreg_db/views/examination/examination.py +37 -0
- endoreg_db/views/examination/examination_manifest_cache.py +26 -0
- endoreg_db/views/examination/get_finding_classification_choices.py +59 -0
- endoreg_db/views/examination/get_finding_classifications.py +36 -0
- endoreg_db/views/examination/get_findings.py +41 -0
- endoreg_db/views/examination/get_instruments.py +18 -0
- endoreg_db/views/examination/get_interventions.py +14 -0
- endoreg_db/views/finding/__init__.py +9 -0
- endoreg_db/views/finding/finding.py +112 -0
- endoreg_db/views/finding/get_classifications.py +14 -0
- endoreg_db/views/finding/get_interventions.py +17 -0
- endoreg_db/views/finding_classification/__init__.py +13 -0
- endoreg_db/views/finding_classification/base.py +0 -0
- endoreg_db/views/finding_classification/finding_classification.py +42 -0
- endoreg_db/views/finding_classification/get_classification_choices.py +55 -0
- endoreg_db/views/label/__init__.py +5 -0
- endoreg_db/views/label/label.py +15 -0
- endoreg_db/views/label_video_segment/__init__.py +16 -0
- endoreg_db/views/label_video_segment/create_lvs_from_annotation.py +44 -0
- endoreg_db/views/label_video_segment/get_lvs_by_name_and_video.py +50 -0
- endoreg_db/views/label_video_segment/label_video_segment.py +77 -0
- endoreg_db/views/label_video_segment/label_video_segment_by_label.py +174 -0
- endoreg_db/views/label_video_segment/label_video_segment_detail.py +73 -0
- endoreg_db/views/label_video_segment/update_lvs_from_annotation.py +46 -0
- endoreg_db/views/label_video_segment/validate.py +226 -0
- endoreg_db/views/media/__init__.py +45 -0
- endoreg_db/views/media/pdf_media.py +388 -0
- endoreg_db/views/media/segments.py +71 -0
- endoreg_db/views/media/sensitive_metadata.py +314 -0
- endoreg_db/views/media/video_media.py +272 -0
- endoreg_db/views/media/video_segments.py +524 -0
- endoreg_db/views/meta/__init__.py +15 -0
- endoreg_db/views/meta/available_files_list.py +146 -0
- endoreg_db/views/meta/report_meta.py +53 -0
- endoreg_db/views/meta/sensitive_meta_detail.py +148 -0
- endoreg_db/views/meta/sensitive_meta_list.py +104 -0
- endoreg_db/views/meta/sensitive_meta_verification.py +71 -0
- endoreg_db/views/misc/__init__.py +63 -0
- endoreg_db/views/misc/center.py +13 -0
- endoreg_db/views/misc/csrf.py +7 -0
- endoreg_db/views/misc/gender.py +14 -0
- endoreg_db/views/misc/secure_file_serving_view.py +80 -0
- endoreg_db/views/misc/secure_file_url_view.py +84 -0
- endoreg_db/views/misc/secure_url_validate.py +79 -0
- endoreg_db/views/misc/stats.py +220 -0
- endoreg_db/views/misc/translation.py +182 -0
- endoreg_db/views/misc/upload_views.py +240 -0
- endoreg_db/views/patient/__init__.py +5 -0
- endoreg_db/views/patient/patient.py +210 -0
- endoreg_db/views/patient_examination/DEPRECATED_video_backup.py +164 -0
- endoreg_db/views/patient_examination/__init__.py +11 -0
- endoreg_db/views/patient_examination/patient_examination.py +140 -0
- endoreg_db/views/patient_examination/patient_examination_create.py +63 -0
- endoreg_db/views/patient_examination/patient_examination_detail.py +66 -0
- endoreg_db/views/patient_examination/patient_examination_list.py +68 -0
- endoreg_db/views/patient_examination/video.py +194 -0
- endoreg_db/views/patient_finding/__init__.py +7 -0
- endoreg_db/views/patient_finding/base.py +0 -0
- endoreg_db/views/patient_finding/patient_finding.py +64 -0
- endoreg_db/views/patient_finding/patient_finding_optimized.py +259 -0
- endoreg_db/views/patient_finding_classification/__init__.py +5 -0
- endoreg_db/views/patient_finding_classification/pfc_create.py +67 -0
- endoreg_db/views/patient_finding_location/__init__.py +5 -0
- endoreg_db/views/patient_finding_location/pfl_create.py +70 -0
- endoreg_db/views/patient_finding_morphology/__init__.py +5 -0
- endoreg_db/views/patient_finding_morphology/pfm_create.py +70 -0
- endoreg_db/views/pdf/__init__.py +8 -0
- endoreg_db/views/pdf/pdf_stream.py +187 -0
- endoreg_db/views/pdf/reimport.py +177 -0
- endoreg_db/views/report/__init__.py +9 -0
- endoreg_db/views/report/report_list.py +112 -0
- endoreg_db/views/report/report_with_secure_url.py +28 -0
- endoreg_db/views/report/start_examination.py +7 -0
- endoreg_db/views/requirement/__init__.py +10 -0
- endoreg_db/views/requirement/evaluate.py +279 -0
- endoreg_db/views/requirement/lookup.py +367 -0
- endoreg_db/views/requirement/lookup_store.py +252 -0
- endoreg_db/views/requirement_lookup/lookup.py +0 -0
- endoreg_db/views/requirement_lookup/lookup_store.py +0 -0
- endoreg_db/views/stats/__init__.py +13 -0
- endoreg_db/views/stats/stats_views.py +229 -0
- endoreg_db/views/video/__init__.py +59 -0
- endoreg_db/views/video/correction.py +530 -0
- endoreg_db/views/video/reimport.py +195 -0
- endoreg_db/views/video/segmentation.py +274 -0
- endoreg_db/views/video/task_status.py +49 -0
- endoreg_db/views/video/timeline.py +46 -0
- endoreg_db/views/video/video_analyze.py +52 -0
- endoreg_db/views/video/video_apply_mask.py +48 -0
- endoreg_db/views/video/video_correction.py +21 -0
- endoreg_db/views/video/video_download_processed.py +58 -0
- endoreg_db/views/video/video_examination_viewset.py +242 -0
- endoreg_db/views/video/video_meta.py +29 -0
- endoreg_db/views/video/video_processing_history.py +24 -0
- endoreg_db/views/video/video_remove_frames.py +48 -0
- endoreg_db/views/video/video_stream.py +306 -0
- endoreg_db/views.py +0 -3
- endoreg_db-0.8.6.3.dist-info/METADATA +383 -0
- endoreg_db-0.8.6.3.dist-info/RECORD +793 -0
- {endoreg_db-0.4.5.dist-info → endoreg_db-0.8.6.3.dist-info}/WHEEL +1 -1
- endoreg_db/data/active_model/data.yaml +0 -3
- endoreg_db/data/agl_service/data.yaml +0 -19
- endoreg_db/data/label/label-set/data.yaml +0 -18
- endoreg_db/management/commands/_load_model_template.py +0 -41
- endoreg_db/management/commands/delete_all.py +0 -18
- endoreg_db/management/commands/delete_legacy_images.py +0 -19
- endoreg_db/management/commands/delete_legacy_videos.py +0 -17
- endoreg_db/management/commands/extract_legacy_video_frames.py +0 -18
- endoreg_db/management/commands/fetch_legacy_image_dataset.py +0 -32
- endoreg_db/management/commands/fix_auth_permission.py +0 -20
- endoreg_db/management/commands/import_legacy_images.py +0 -94
- endoreg_db/management/commands/import_legacy_videos.py +0 -76
- endoreg_db/management/commands/load_active_model_data.py +0 -45
- endoreg_db/management/commands/load_endoscopy_processor_data.py +0 -45
- endoreg_db/management/commands/load_g_play_data.py +0 -113
- endoreg_db/management/commands/load_label_data.py +0 -67
- endoreg_db/management/commands/load_medication_indication_data.py +0 -63
- endoreg_db/management/commands/load_medication_indication_type_data.py +0 -41
- endoreg_db/management/commands/load_medication_schedule_data.py +0 -55
- endoreg_db/management/commands/load_network_data.py +0 -57
- endoreg_db/migrations/0002_anonymizedimagelabel_anonymousimageannotation_and_more.py +0 -55
- endoreg_db/migrations/0003_anonymousimageannotation_original_image_url_and_more.py +0 -39
- endoreg_db/migrations/0004_alter_rawpdffile_file.py +0 -20
- endoreg_db/migrations/0005_uploadedfile_alter_rawpdffile_file_anonymizedfile.py +0 -40
- endoreg_db/migrations/0006_alter_rawpdffile_file.py +0 -20
- endoreg_db/migrations/0007_networkdevicelogentry_datetime_and_more.py +0 -43
- endoreg_db/models/ai_model/__init__.py +0 -3
- endoreg_db/models/ai_model/active_model.py +0 -9
- endoreg_db/models/ai_model/model_meta.py +0 -24
- endoreg_db/models/annotation/__init__.py +0 -3
- endoreg_db/models/annotation/anonymized_image_annotation.py +0 -60
- endoreg_db/models/annotation/binary_classification_annotation_task.py +0 -80
- endoreg_db/models/annotation/image_classification.py +0 -27
- endoreg_db/models/center/center.py +0 -25
- endoreg_db/models/center/center_product.py +0 -34
- endoreg_db/models/center/center_resource.py +0 -19
- endoreg_db/models/center/center_waste.py +0 -11
- endoreg_db/models/data_file/__init__.py +0 -6
- endoreg_db/models/data_file/base_classes/__init__.py +0 -2
- endoreg_db/models/data_file/base_classes/abstract_frame.py +0 -51
- endoreg_db/models/data_file/base_classes/abstract_video.py +0 -201
- endoreg_db/models/data_file/frame.py +0 -45
- endoreg_db/models/data_file/import_classes/__init__.py +0 -32
- endoreg_db/models/data_file/import_classes/processing_functions/__init__.py +0 -35
- endoreg_db/models/data_file/import_classes/processing_functions/pdf.py +0 -28
- endoreg_db/models/data_file/import_classes/processing_functions/video.py +0 -260
- endoreg_db/models/data_file/import_classes/raw_pdf.py +0 -188
- endoreg_db/models/data_file/import_classes/raw_video.py +0 -343
- endoreg_db/models/data_file/metadata/__init__.py +0 -3
- endoreg_db/models/data_file/metadata/sensitive_meta.py +0 -31
- endoreg_db/models/data_file/metadata/video_meta.py +0 -133
- endoreg_db/models/data_file/report_file.py +0 -89
- endoreg_db/models/data_file/video/__init__.py +0 -7
- endoreg_db/models/data_file/video/import_meta.py +0 -25
- endoreg_db/models/data_file/video/video.py +0 -25
- endoreg_db/models/data_file/video_segment.py +0 -107
- endoreg_db/models/disease.py +0 -56
- endoreg_db/models/emission/__init__.py +0 -1
- endoreg_db/models/emission/emission_factor.py +0 -20
- endoreg_db/models/event.py +0 -22
- endoreg_db/models/examination/__init__.py +0 -4
- endoreg_db/models/examination/examination.py +0 -26
- endoreg_db/models/examination/examination_time.py +0 -27
- endoreg_db/models/examination/examination_time_type.py +0 -24
- endoreg_db/models/examination/examination_type.py +0 -18
- endoreg_db/models/hardware/__init__.py +0 -2
- endoreg_db/models/hardware/endoscope.py +0 -44
- endoreg_db/models/information_source.py +0 -29
- endoreg_db/models/laboratory/__init__.py +0 -1
- endoreg_db/models/laboratory/lab_value.py +0 -102
- endoreg_db/models/legacy_data/__init__.py +0 -3
- endoreg_db/models/legacy_data/image.py +0 -34
- endoreg_db/models/logging/__init__.py +0 -4
- endoreg_db/models/logging/agl_service.py +0 -19
- endoreg_db/models/logging/base.py +0 -22
- endoreg_db/models/logging/log_type.py +0 -23
- endoreg_db/models/logging/network_device.py +0 -24
- endoreg_db/models/medication/__init__.py +0 -1
- endoreg_db/models/medication/medication.py +0 -148
- endoreg_db/models/network/__init__.py +0 -3
- endoreg_db/models/network/agl_service.py +0 -38
- endoreg_db/models/network/network_device.py +0 -53
- endoreg_db/models/network/network_device_type.py +0 -23
- endoreg_db/models/other/distribution.py +0 -215
- endoreg_db/models/patient_examination/__init__.py +0 -35
- endoreg_db/models/permissions/__init__.py +0 -44
- endoreg_db/models/persons/__init__.py +0 -7
- endoreg_db/models/persons/examiner/__init__.py +0 -2
- endoreg_db/models/persons/examiner/examiner.py +0 -16
- endoreg_db/models/persons/examiner/examiner_type.py +0 -2
- endoreg_db/models/persons/patient/__init__.py +0 -8
- endoreg_db/models/persons/patient/case/case.py +0 -30
- endoreg_db/models/persons/patient/patient.py +0 -216
- endoreg_db/models/persons/patient/patient_disease.py +0 -16
- endoreg_db/models/persons/patient/patient_event.py +0 -22
- endoreg_db/models/persons/patient/patient_lab_sample.py +0 -106
- endoreg_db/models/persons/patient/patient_medication.py +0 -44
- endoreg_db/models/persons/patient/patient_medication_schedule.py +0 -28
- endoreg_db/models/persons/portal_user_information.py +0 -27
- endoreg_db/models/prediction/__init__.py +0 -2
- endoreg_db/models/prediction/image_classification.py +0 -37
- endoreg_db/models/prediction/video_prediction_meta.py +0 -244
- endoreg_db/models/product/__init__.py +0 -5
- endoreg_db/models/product/product.py +0 -97
- endoreg_db/models/product/product_group.py +0 -19
- endoreg_db/models/product/product_material.py +0 -24
- endoreg_db/models/questionnaires/__init__.py +0 -114
- endoreg_db/models/quiz/__init__.py +0 -2
- endoreg_db/models/quiz/quiz_answer.py +0 -41
- endoreg_db/models/quiz/quiz_question.py +0 -54
- endoreg_db/models/report_reader/__init__.py +0 -2
- endoreg_db/models/report_reader/report_reader_config.py +0 -53
- endoreg_db/models/rules/__init__.py +0 -5
- endoreg_db/queries/get/__init__.py +0 -6
- endoreg_db/queries/get/center.py +0 -42
- endoreg_db/queries/get/model.py +0 -13
- endoreg_db/queries/get/patient.py +0 -14
- endoreg_db/queries/get/patient_examination.py +0 -20
- endoreg_db/queries/get/report_file.py +0 -33
- endoreg_db/queries/get/video.py +0 -31
- endoreg_db/serializers/ai_model.py +0 -19
- endoreg_db/serializers/annotation.py +0 -17
- endoreg_db/serializers/center.py +0 -11
- endoreg_db/serializers/examination.py +0 -33
- endoreg_db/serializers/frame.py +0 -13
- endoreg_db/serializers/hardware.py +0 -21
- endoreg_db/serializers/label.py +0 -22
- endoreg_db/serializers/patient.py +0 -10
- endoreg_db/serializers/prediction.py +0 -15
- endoreg_db/serializers/report_file.py +0 -7
- endoreg_db/serializers/video.py +0 -27
- endoreg_db/tests.py +0 -3
- endoreg_db/utils/legacy_ocr.py +0 -201
- endoreg_db/utils/video_metadata.py +0 -87
- endoreg_db-0.4.5.dist-info/METADATA +0 -34
- endoreg_db-0.4.5.dist-info/RECORD +0 -316
- /endoreg_db/{data/distribution/numeric/.init → api/serializers/finding_descriptions.py} +0 -0
- /endoreg_db/{models/persons/patient/case/__init__.py → api/views/finding_descriptions.py} +0 -0
- /endoreg_db/{queries/get/annotation.py → config/__init__.py} +0 -0
- /endoreg_db/data/{label → ai_model_label}/label-type/data.yaml +0 -0
- /endoreg_db/data/{model_type → ai_model_type}/data.yaml +0 -0
- /endoreg_db/{queries/get/prediction.py → data/shift/m2.yaml} +0 -0
- /endoreg_db/{queries/get/video_import_meta.py → factories/__init__.py} +0 -0
- /endoreg_db/{queries/get/video_prediction_meta.py → helpers/__init__.py} +0 -0
- /endoreg_db/management/commands/{load_report_reader_flag.py → load_report_reader_flag_data.py} +0 -0
- /endoreg_db/models/{persons → administration/person}/person.py +0 -0
- /endoreg_db/models/{report_reader → media/pdf/report_reader}/report_reader_flag.py +0 -0
- /endoreg_db/models/{rules → rule}/rule_applicator.py +0 -0
- {endoreg_db-0.4.5.dist-info → endoreg_db-0.8.6.3.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,1048 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import random
|
|
4
|
+
import re # Neu hinzugefügt für Regex-Pattern
|
|
5
|
+
from datetime import date, datetime, timedelta
|
|
6
|
+
from hashlib import sha256
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, Type
|
|
8
|
+
|
|
9
|
+
from django.db import transaction
|
|
10
|
+
from django.utils import timezone
|
|
11
|
+
|
|
12
|
+
from endoreg_db.utils import guess_name_gender
|
|
13
|
+
|
|
14
|
+
# Assuming these utils are correctly located
|
|
15
|
+
from endoreg_db.utils.hashs import get_patient_examination_hash, get_patient_hash
|
|
16
|
+
|
|
17
|
+
# Import models needed for logic, use local imports inside functions if needed to break cycles
|
|
18
|
+
from ..administration import Center, Examiner, FirstName, LastName, Patient
|
|
19
|
+
from ..medical import PatientExamination
|
|
20
|
+
from ..other import Gender
|
|
21
|
+
from ..state import SensitiveMetaState
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from .sensitive_meta import SensitiveMeta # Import model for type hinting
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
SECRET_SALT = os.getenv("DJANGO_SALT", "default_salt")
|
|
28
|
+
DEFAULT_UNKNOWN_NAME = "unknown"
|
|
29
|
+
|
|
30
|
+
# Regex-Pattern für verschiedene Datumsformate
|
|
31
|
+
ISO_RX = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
|
32
|
+
DE_RX = re.compile(r"^\d{2}\.\d{2}\.\d{4}$")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_any_date(s: str) -> Optional[date]:
|
|
36
|
+
"""
|
|
37
|
+
Parst Datumsstring mit Priorität auf deutsches Format (DD.MM.YYYY).
|
|
38
|
+
|
|
39
|
+
Unterstützte Formate:
|
|
40
|
+
1. DD.MM.YYYY (Priorität) - deutsches Format
|
|
41
|
+
2. YYYY-MM-DD (Fallback) - ISO-Format
|
|
42
|
+
3. Erweiterte Fallbacks über dateparser
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
s: Datumsstring zum Parsen
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
date-Objekt oder None bei ungültigem/fehlendem Input
|
|
49
|
+
"""
|
|
50
|
+
if not s:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
s = s.strip()
|
|
54
|
+
|
|
55
|
+
# 1. German dd.mm.yyyy (PRIORITÄT)
|
|
56
|
+
if DE_RX.match(s):
|
|
57
|
+
try:
|
|
58
|
+
dd, mm, yyyy = s.split(".")
|
|
59
|
+
return date(int(yyyy), int(mm), int(dd))
|
|
60
|
+
except ValueError as e:
|
|
61
|
+
logger.warning(f"Invalid German date format '{s}': {e}")
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
# 2. ISO yyyy-mm-dd (Fallback für Rückwärtskompatibilität)
|
|
65
|
+
if ISO_RX.match(s):
|
|
66
|
+
try:
|
|
67
|
+
return date.fromisoformat(s)
|
|
68
|
+
except ValueError as e:
|
|
69
|
+
logger.warning(f"Invalid ISO date format '{s}': {e}")
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
# 3. Extended fallbacks
|
|
73
|
+
try:
|
|
74
|
+
# Try standard datetime parsing
|
|
75
|
+
return datetime.fromisoformat(s).date()
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
# Try dateparser with German locale preference
|
|
81
|
+
import dateparser
|
|
82
|
+
|
|
83
|
+
dt = dateparser.parse(
|
|
84
|
+
s, settings={"DATE_ORDER": "DMY", "PREFER_DAY_OF_MONTH": "first"}
|
|
85
|
+
)
|
|
86
|
+
return dt.date() if dt else None
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logger.debug(f"Dateparser fallback failed for '{s}': {e}")
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def format_date_german(d: Optional[date]) -> str:
|
|
93
|
+
"""
|
|
94
|
+
Formatiert date-Objekt als deutsches Datumsformat (DD.MM.YYYY).
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
d: date-Objekt oder None
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Formatiertes Datum als String oder leerer String bei None
|
|
101
|
+
"""
|
|
102
|
+
if not d:
|
|
103
|
+
return ""
|
|
104
|
+
return d.strftime("%d.%m.%Y")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def format_date_iso(d: Optional[date]) -> str:
|
|
108
|
+
"""
|
|
109
|
+
Formatiert date-Objekt als ISO-Format (YYYY-MM-DD).
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
d: date-Objekt oder None
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Formatiertes Datum als String oder leerer String bei None
|
|
116
|
+
"""
|
|
117
|
+
if not d:
|
|
118
|
+
return ""
|
|
119
|
+
return d.isoformat()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def generate_random_dob() -> datetime:
|
|
123
|
+
"""Generates a random timezone-aware datetime between 1920-01-01 and 2000-12-31."""
|
|
124
|
+
start_date = date(1920, 1, 1)
|
|
125
|
+
end_date = date(2000, 12, 31)
|
|
126
|
+
time_between_dates = end_date - start_date
|
|
127
|
+
days_between_dates = time_between_dates.days
|
|
128
|
+
random_number_of_days = random.randrange(days_between_dates)
|
|
129
|
+
random_date = start_date + timedelta(days=random_number_of_days)
|
|
130
|
+
random_datetime = datetime.combine(random_date, datetime.min.time())
|
|
131
|
+
return timezone.make_aware(random_datetime)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def generate_random_examination_date() -> date:
|
|
135
|
+
"""Generates a random date within the last 20 years."""
|
|
136
|
+
today = date.today()
|
|
137
|
+
start_date = today - timedelta(days=20 * 365) # Approximate 20 years back
|
|
138
|
+
time_between_dates = today - start_date
|
|
139
|
+
days_between_dates = time_between_dates.days
|
|
140
|
+
random_number_of_days = random.randrange(days_between_dates)
|
|
141
|
+
random_date = start_date + timedelta(days=random_number_of_days)
|
|
142
|
+
return random_date
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def update_name_db(first_name: Optional[str], last_name: Optional[str]):
|
|
146
|
+
"""Adds first and last names to the respective lookup tables if they don't exist."""
|
|
147
|
+
if first_name:
|
|
148
|
+
FirstName.objects.get_or_create(name=first_name)
|
|
149
|
+
if last_name:
|
|
150
|
+
LastName.objects.get_or_create(name=last_name)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def calculate_patient_hash(instance: "SensitiveMeta", salt: str = SECRET_SALT) -> str:
|
|
154
|
+
"""Calculates the patient hash for the instance."""
|
|
155
|
+
dob = instance.patient_dob
|
|
156
|
+
first_name = instance.patient_first_name
|
|
157
|
+
last_name = instance.patient_last_name
|
|
158
|
+
center = instance.center
|
|
159
|
+
|
|
160
|
+
if not dob:
|
|
161
|
+
raise ValueError("Patient DOB is required to calculate patient hash.")
|
|
162
|
+
if not center:
|
|
163
|
+
raise ValueError("Center is required to calculate patient hash.")
|
|
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
|
+
|
|
168
|
+
hash_str = get_patient_hash(
|
|
169
|
+
first_name=first_name,
|
|
170
|
+
last_name=last_name,
|
|
171
|
+
dob=dob,
|
|
172
|
+
center=center.name, # Use center name
|
|
173
|
+
salt=salt,
|
|
174
|
+
)
|
|
175
|
+
return sha256(hash_str.encode()).hexdigest()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def calculate_examination_hash(
|
|
179
|
+
instance: "SensitiveMeta", salt: str = SECRET_SALT
|
|
180
|
+
) -> str:
|
|
181
|
+
"""Calculates the examination hash for the instance."""
|
|
182
|
+
dob = instance.patient_dob
|
|
183
|
+
first_name = instance.patient_first_name
|
|
184
|
+
last_name = instance.patient_last_name
|
|
185
|
+
examination_date = instance.examination_date
|
|
186
|
+
center = instance.center
|
|
187
|
+
|
|
188
|
+
if not dob:
|
|
189
|
+
raise ValueError("Patient DOB is required to calculate examination hash.")
|
|
190
|
+
if not examination_date:
|
|
191
|
+
raise ValueError("Examination date is required to calculate examination hash.")
|
|
192
|
+
if not center:
|
|
193
|
+
raise ValueError("Center is required to calculate examination hash.")
|
|
194
|
+
|
|
195
|
+
hash_str = get_patient_examination_hash(
|
|
196
|
+
first_name=first_name,
|
|
197
|
+
last_name=last_name,
|
|
198
|
+
dob=dob,
|
|
199
|
+
examination_date=examination_date,
|
|
200
|
+
center=center.name, # Use center name
|
|
201
|
+
salt=salt,
|
|
202
|
+
)
|
|
203
|
+
return sha256(hash_str.encode()).hexdigest()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def create_pseudo_examiner_logic(instance: "SensitiveMeta") -> "Examiner":
|
|
207
|
+
"""Creates or retrieves the pseudo examiner based on instance data."""
|
|
208
|
+
first_name = instance.examiner_first_name
|
|
209
|
+
last_name = instance.examiner_last_name
|
|
210
|
+
center = instance.center # Should be set before calling save
|
|
211
|
+
|
|
212
|
+
if not first_name or not last_name or not center:
|
|
213
|
+
logger.warning(
|
|
214
|
+
f"Incomplete examiner info for SensitiveMeta (pk={instance.pk or 'new'}). Using default examiner."
|
|
215
|
+
)
|
|
216
|
+
# Ensure default center exists or handle appropriately
|
|
217
|
+
try:
|
|
218
|
+
default_center = Center.objects.get(name="endoreg_db_demo")
|
|
219
|
+
except Center.DoesNotExist:
|
|
220
|
+
logger.error(
|
|
221
|
+
"Default center 'endoreg_db_demo' not found. Cannot create default examiner."
|
|
222
|
+
)
|
|
223
|
+
raise ValueError("Default center 'endoreg_db_demo' not found.")
|
|
224
|
+
|
|
225
|
+
examiner, _created = Examiner.custom_get_or_create(
|
|
226
|
+
first_name="Unknown", last_name="Unknown", center=default_center
|
|
227
|
+
)
|
|
228
|
+
else:
|
|
229
|
+
examiner, _created = Examiner.custom_get_or_create(
|
|
230
|
+
first_name=first_name, last_name=last_name, center=center
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return examiner
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def get_or_create_pseudo_patient_logic(instance: "SensitiveMeta") -> "Patient":
|
|
237
|
+
"""Gets or creates the pseudo patient based on instance data."""
|
|
238
|
+
# Ensure necessary fields are set
|
|
239
|
+
if not instance.patient_hash:
|
|
240
|
+
instance.patient_hash = calculate_patient_hash(instance)
|
|
241
|
+
if not instance.center:
|
|
242
|
+
raise ValueError("Center must be set before creating pseudo patient.")
|
|
243
|
+
if not instance.patient_gender:
|
|
244
|
+
raise ValueError("Patient gender must be set before creating pseudo patient.")
|
|
245
|
+
if not instance.patient_dob:
|
|
246
|
+
raise ValueError("Patient DOB must be set before creating pseudo patient.")
|
|
247
|
+
|
|
248
|
+
dob = instance.patient_dob
|
|
249
|
+
year = dob.year
|
|
250
|
+
month = dob.month
|
|
251
|
+
|
|
252
|
+
patient, _created = Patient.get_or_create_pseudo_patient_by_hash(
|
|
253
|
+
patient_hash=instance.patient_hash,
|
|
254
|
+
center=instance.center,
|
|
255
|
+
gender=instance.patient_gender,
|
|
256
|
+
birth_year=year,
|
|
257
|
+
birth_month=month,
|
|
258
|
+
)
|
|
259
|
+
return patient
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def get_or_create_pseudo_patient_examination_logic(
|
|
263
|
+
instance: "SensitiveMeta",
|
|
264
|
+
) -> "PatientExamination":
|
|
265
|
+
"""Gets or creates the pseudo patient examination based on instance data."""
|
|
266
|
+
# Ensure necessary fields are set
|
|
267
|
+
if not instance.patient_hash:
|
|
268
|
+
instance.patient_hash = calculate_patient_hash(instance)
|
|
269
|
+
if not instance.examination_hash:
|
|
270
|
+
instance.examination_hash = calculate_examination_hash(instance)
|
|
271
|
+
|
|
272
|
+
# Ensure the pseudo patient exists first, as PatientExamination might depend on it
|
|
273
|
+
if not instance.pseudo_patient_id:
|
|
274
|
+
pseudo_patient = get_or_create_pseudo_patient_logic(instance)
|
|
275
|
+
instance.pseudo_patient_id = pseudo_patient.pk # Assign FK directly
|
|
276
|
+
|
|
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
|
+
)
|
|
284
|
+
)
|
|
285
|
+
return patient_examination
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@transaction.atomic # Ensure all operations within save succeed or fail together
|
|
289
|
+
def perform_save_logic(instance: "SensitiveMeta") -> "Examiner":
|
|
290
|
+
"""
|
|
291
|
+
Contains the core logic for preparing a SensitiveMeta instance for saving.
|
|
292
|
+
Handles data generation (dates), hash calculation, and linking pseudo-entities.
|
|
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
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
# --- Pre-Save Checks and Data Generation ---
|
|
343
|
+
|
|
344
|
+
# 1. Ensure DOB and Examination Date exist
|
|
345
|
+
if not instance.patient_dob:
|
|
346
|
+
logger.debug(
|
|
347
|
+
f"SensitiveMeta (pk={instance.pk or 'new'}): Patient DOB missing, generating random."
|
|
348
|
+
)
|
|
349
|
+
instance.patient_dob = generate_random_dob()
|
|
350
|
+
if not instance.examination_date:
|
|
351
|
+
logger.debug(
|
|
352
|
+
f"SensitiveMeta (pk={instance.pk or 'new'}): Examination date missing, generating random."
|
|
353
|
+
)
|
|
354
|
+
instance.examination_date = generate_random_examination_date()
|
|
355
|
+
|
|
356
|
+
# 2. Ensure Center exists (should be set before calling save)
|
|
357
|
+
if not instance.center:
|
|
358
|
+
raise ValueError("Center must be set before saving SensitiveMeta.")
|
|
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
|
+
|
|
399
|
+
# 3. Ensure Gender exists (should be set before calling save, e.g., during creation/update)
|
|
400
|
+
if not instance.patient_gender:
|
|
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.")
|
|
414
|
+
|
|
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.
|
|
424
|
+
instance.patient_hash = calculate_patient_hash(instance)
|
|
425
|
+
instance.examination_hash = calculate_examination_hash(instance)
|
|
426
|
+
|
|
427
|
+
# 5. Get or Create Pseudo Patient (depends on hash, center, gender, dob)
|
|
428
|
+
# Assign directly to the FK field to avoid premature saving issues
|
|
429
|
+
pseudo_patient = get_or_create_pseudo_patient_logic(instance)
|
|
430
|
+
instance.pseudo_patient_id = pseudo_patient.pk
|
|
431
|
+
|
|
432
|
+
# 6. Get or Create Pseudo Examination (depends on hashes)
|
|
433
|
+
# Assign directly to the FK field
|
|
434
|
+
pseudo_examination = get_or_create_pseudo_patient_examination_logic(instance)
|
|
435
|
+
instance.pseudo_examination_id = pseudo_examination.pk
|
|
436
|
+
|
|
437
|
+
# 7. Get or Create Pseudo Examiner (depends on names, center)
|
|
438
|
+
# This needs to happen *after* the main instance has a PK for M2M linking.
|
|
439
|
+
# We create/get it here and return it to the main save method.
|
|
440
|
+
examiner_instance = create_pseudo_examiner_logic(instance)
|
|
441
|
+
|
|
442
|
+
# 8. Ensure SensitiveMetaState exists (will be checked/created *after* main save)
|
|
443
|
+
|
|
444
|
+
# Return the examiner instance so the model's save method can handle M2M linking
|
|
445
|
+
return examiner_instance
|
|
446
|
+
|
|
447
|
+
|
|
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
|
|
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
|
+
}
|
|
501
|
+
selected_data = {k: v for k, v in data.items() if k in field_names}
|
|
502
|
+
|
|
503
|
+
# --- Convert patient_dob if it's a date object ---
|
|
504
|
+
dob = selected_data.get("patient_dob")
|
|
505
|
+
if isinstance(dob, date) and not isinstance(dob, datetime):
|
|
506
|
+
# Convert date to datetime at the start of the day and make it timezone-aware
|
|
507
|
+
aware_dob = timezone.make_aware(datetime.combine(dob, datetime.min.time()))
|
|
508
|
+
selected_data["patient_dob"] = aware_dob
|
|
509
|
+
logger.debug("Converted patient_dob from date to aware datetime: %s", aware_dob)
|
|
510
|
+
elif isinstance(dob, str):
|
|
511
|
+
# Handle string DOB - check if it's a field name or actual date
|
|
512
|
+
if dob == "patient_dob" or dob in [
|
|
513
|
+
"patient_first_name",
|
|
514
|
+
"patient_last_name",
|
|
515
|
+
"examination_date",
|
|
516
|
+
]:
|
|
517
|
+
logger.warning(
|
|
518
|
+
"Skipping invalid patient_dob value '%s' - appears to be field name",
|
|
519
|
+
dob,
|
|
520
|
+
)
|
|
521
|
+
selected_data.pop("patient_dob", None) # Remove invalid value
|
|
522
|
+
else:
|
|
523
|
+
# Try to parse as date string
|
|
524
|
+
try:
|
|
525
|
+
import dateparser
|
|
526
|
+
|
|
527
|
+
parsed_dob = dateparser.parse(
|
|
528
|
+
dob, languages=["de"], settings={"DATE_ORDER": "DMY"}
|
|
529
|
+
)
|
|
530
|
+
if parsed_dob:
|
|
531
|
+
aware_dob = timezone.make_aware(
|
|
532
|
+
parsed_dob.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
533
|
+
)
|
|
534
|
+
selected_data["patient_dob"] = aware_dob
|
|
535
|
+
logger.debug(
|
|
536
|
+
"Parsed string patient_dob '%s' to aware datetime: %s",
|
|
537
|
+
dob,
|
|
538
|
+
aware_dob,
|
|
539
|
+
)
|
|
540
|
+
else:
|
|
541
|
+
logger.warning(
|
|
542
|
+
"Could not parse patient_dob string '%s', removing from data",
|
|
543
|
+
dob,
|
|
544
|
+
)
|
|
545
|
+
selected_data.pop("patient_dob", None)
|
|
546
|
+
except Exception as e:
|
|
547
|
+
logger.warning(
|
|
548
|
+
"Error parsing patient_dob string '%s': %s, removing from data",
|
|
549
|
+
dob,
|
|
550
|
+
e,
|
|
551
|
+
)
|
|
552
|
+
selected_data.pop("patient_dob", None)
|
|
553
|
+
# --- End Conversion ---
|
|
554
|
+
|
|
555
|
+
# Similar validation for examination_date
|
|
556
|
+
exam_date = selected_data.get("examination_date")
|
|
557
|
+
if isinstance(exam_date, str):
|
|
558
|
+
if exam_date == "examination_date" or exam_date in [
|
|
559
|
+
"patient_first_name",
|
|
560
|
+
"patient_last_name",
|
|
561
|
+
"patient_dob",
|
|
562
|
+
]:
|
|
563
|
+
logger.warning(
|
|
564
|
+
"Skipping invalid examination_date value '%s' - appears to be field name",
|
|
565
|
+
exam_date,
|
|
566
|
+
)
|
|
567
|
+
selected_data.pop("examination_date", None)
|
|
568
|
+
else:
|
|
569
|
+
# Try to parse as date string
|
|
570
|
+
try:
|
|
571
|
+
# First try simple ISO format for YYYY-MM-DD
|
|
572
|
+
if len(exam_date) == 10 and exam_date.count("-") == 2:
|
|
573
|
+
try:
|
|
574
|
+
from datetime import datetime as dt
|
|
575
|
+
|
|
576
|
+
parsed_date = dt.strptime(exam_date, "%Y-%m-%d").date()
|
|
577
|
+
selected_data["examination_date"] = parsed_date
|
|
578
|
+
logger.debug(
|
|
579
|
+
"Parsed ISO examination_date '%s' to date: %s",
|
|
580
|
+
exam_date,
|
|
581
|
+
parsed_date,
|
|
582
|
+
)
|
|
583
|
+
except ValueError:
|
|
584
|
+
# Fall back to dateparser for complex formats
|
|
585
|
+
import dateparser
|
|
586
|
+
|
|
587
|
+
parsed_date = dateparser.parse(
|
|
588
|
+
exam_date, languages=["de"], settings={"DATE_ORDER": "DMY"}
|
|
589
|
+
)
|
|
590
|
+
if parsed_date:
|
|
591
|
+
selected_data["examination_date"] = parsed_date.date()
|
|
592
|
+
logger.debug(
|
|
593
|
+
"Parsed string examination_date '%s' to date: %s",
|
|
594
|
+
exam_date,
|
|
595
|
+
parsed_date.date(),
|
|
596
|
+
)
|
|
597
|
+
else:
|
|
598
|
+
logger.warning(
|
|
599
|
+
"Could not parse examination_date string '%s', removing from data",
|
|
600
|
+
exam_date,
|
|
601
|
+
)
|
|
602
|
+
selected_data.pop("examination_date", None)
|
|
603
|
+
else:
|
|
604
|
+
# Use dateparser for non-ISO formats
|
|
605
|
+
import dateparser
|
|
606
|
+
|
|
607
|
+
parsed_date = dateparser.parse(
|
|
608
|
+
exam_date, languages=["de"], settings={"DATE_ORDER": "DMY"}
|
|
609
|
+
)
|
|
610
|
+
if parsed_date:
|
|
611
|
+
selected_data["examination_date"] = parsed_date.date()
|
|
612
|
+
logger.debug(
|
|
613
|
+
"Parsed string examination_date '%s' to date: %s",
|
|
614
|
+
exam_date,
|
|
615
|
+
parsed_date.date(),
|
|
616
|
+
)
|
|
617
|
+
else:
|
|
618
|
+
logger.warning(
|
|
619
|
+
"Could not parse examination_date string '%s', removing from data",
|
|
620
|
+
exam_date,
|
|
621
|
+
)
|
|
622
|
+
selected_data.pop("examination_date", None)
|
|
623
|
+
except Exception as e:
|
|
624
|
+
logger.warning(
|
|
625
|
+
"Error parsing examination_date string '%s': %s, removing from data",
|
|
626
|
+
exam_date,
|
|
627
|
+
e,
|
|
628
|
+
)
|
|
629
|
+
selected_data.pop("examination_date", None)
|
|
630
|
+
|
|
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
|
|
635
|
+
center_name = data.get("center_name")
|
|
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)}")
|
|
641
|
+
selected_data["center"] = center
|
|
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
|
+
)
|
|
654
|
+
|
|
655
|
+
# Handle Names and Gender
|
|
656
|
+
first_name = selected_data.get("patient_first_name") or DEFAULT_UNKNOWN_NAME
|
|
657
|
+
last_name = selected_data.get("patient_last_name") or DEFAULT_UNKNOWN_NAME
|
|
658
|
+
selected_data["patient_first_name"] = first_name # Ensure defaults are set
|
|
659
|
+
selected_data["patient_last_name"] = last_name
|
|
660
|
+
|
|
661
|
+
patient_gender_input = selected_data.get("patient_gender")
|
|
662
|
+
|
|
663
|
+
if isinstance(patient_gender_input, Gender):
|
|
664
|
+
# Already a Gender object, nothing to do
|
|
665
|
+
pass
|
|
666
|
+
elif isinstance(patient_gender_input, str):
|
|
667
|
+
# Input is a string (gender name)
|
|
668
|
+
try:
|
|
669
|
+
selected_data["patient_gender"] = Gender.objects.get(
|
|
670
|
+
name=patient_gender_input
|
|
671
|
+
)
|
|
672
|
+
except Gender.DoesNotExist:
|
|
673
|
+
logger.warning(
|
|
674
|
+
f"Gender with name '{patient_gender_input}' provided but not found. Attempting to guess or use default."
|
|
675
|
+
)
|
|
676
|
+
# Fall through to guessing logic if provided string name is invalid
|
|
677
|
+
patient_gender_input = None # Reset to trigger guessing
|
|
678
|
+
|
|
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)
|
|
682
|
+
gender_name_to_use = guess_name_gender(first_name)
|
|
683
|
+
if not gender_name_to_use:
|
|
684
|
+
logger.warning(
|
|
685
|
+
f"Could not guess gender for name '{first_name}'. Setting Gender to unknown."
|
|
686
|
+
)
|
|
687
|
+
gender_name_to_use = "unknown"
|
|
688
|
+
try:
|
|
689
|
+
selected_data["patient_gender"] = Gender.objects.get(
|
|
690
|
+
name=gender_name_to_use
|
|
691
|
+
)
|
|
692
|
+
except Gender.DoesNotExist:
|
|
693
|
+
# This should ideally not happen if "unknown" gender is guaranteed to exist
|
|
694
|
+
raise ValueError(
|
|
695
|
+
f"Default or guessed gender '{gender_name_to_use}' does not exist in Gender table."
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
# Update name DB
|
|
699
|
+
update_name_db(first_name, last_name)
|
|
700
|
+
|
|
701
|
+
# Instantiate without saving yet
|
|
702
|
+
sensitive_meta = cls(**selected_data)
|
|
703
|
+
|
|
704
|
+
# Call save once at the end. This triggers the custom save logic.
|
|
705
|
+
sensitive_meta.save() # This will call perform_save_logic internally
|
|
706
|
+
|
|
707
|
+
return sensitive_meta
|
|
708
|
+
|
|
709
|
+
|
|
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
|
+
}
|
|
762
|
+
# Exclude FKs that should not be updated directly from dict keys (handled separately or via save logic)
|
|
763
|
+
excluded_fields = {"pseudo_patient", "pseudo_examination"}
|
|
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
|
|
770
|
+
|
|
771
|
+
center = data.get("center") # First try direct Center object
|
|
772
|
+
center_name = data.get("center_name")
|
|
773
|
+
|
|
774
|
+
if center is not None:
|
|
775
|
+
# Center object provided directly - validate and update
|
|
776
|
+
if isinstance(center, Center):
|
|
777
|
+
instance.center = center
|
|
778
|
+
logger.debug(f"Updated center from Center object: {center.name}")
|
|
779
|
+
else:
|
|
780
|
+
logger.warning(
|
|
781
|
+
f"Invalid center type {type(center)}, expected Center instance. Ignoring."
|
|
782
|
+
)
|
|
783
|
+
# Remove from selected_data to prevent override
|
|
784
|
+
selected_data.pop("center", None)
|
|
785
|
+
elif center_name:
|
|
786
|
+
# center_name string provided - resolve to Center object
|
|
787
|
+
try:
|
|
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)
|
|
799
|
+
|
|
800
|
+
# Set examiner names if provided, before calling save
|
|
801
|
+
examiner_first_name = data.get("examiner_first_name")
|
|
802
|
+
examiner_last_name = data.get("examiner_last_name")
|
|
803
|
+
if examiner_first_name is not None: # Allow setting empty strings
|
|
804
|
+
instance.examiner_first_name = examiner_first_name
|
|
805
|
+
if examiner_last_name is not None:
|
|
806
|
+
instance.examiner_last_name = examiner_last_name
|
|
807
|
+
|
|
808
|
+
# Handle patient_gender specially with graceful error handling
|
|
809
|
+
patient_gender_input = data.get("patient_gender")
|
|
810
|
+
if patient_gender_input is not None:
|
|
811
|
+
try:
|
|
812
|
+
if isinstance(patient_gender_input, Gender):
|
|
813
|
+
selected_data["patient_gender"] = patient_gender_input
|
|
814
|
+
elif isinstance(patient_gender_input, str):
|
|
815
|
+
gender_input_clean = patient_gender_input.strip()
|
|
816
|
+
# Try direct case-insensitive DB lookup first
|
|
817
|
+
gender_obj = Gender.objects.filter(
|
|
818
|
+
name__iexact=gender_input_clean
|
|
819
|
+
).first()
|
|
820
|
+
if gender_obj:
|
|
821
|
+
selected_data["patient_gender"] = gender_obj
|
|
822
|
+
logger.debug(
|
|
823
|
+
f"Successfully matched gender string '{patient_gender_input}' to Gender object via iexact lookup"
|
|
824
|
+
)
|
|
825
|
+
else:
|
|
826
|
+
# Use mapping helper for fallback
|
|
827
|
+
mapped = _map_gender_string_to_standard(gender_input_clean)
|
|
828
|
+
if mapped:
|
|
829
|
+
gender_obj = Gender.objects.filter(name__iexact=mapped).first()
|
|
830
|
+
if gender_obj:
|
|
831
|
+
selected_data["patient_gender"] = gender_obj
|
|
832
|
+
logger.info(
|
|
833
|
+
f"Mapped gender '{patient_gender_input}' to '{mapped}' via fallback mapping"
|
|
834
|
+
)
|
|
835
|
+
else:
|
|
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()
|
|
842
|
+
if unknown_gender:
|
|
843
|
+
selected_data["patient_gender"] = unknown_gender
|
|
844
|
+
logger.warning(
|
|
845
|
+
f"Using 'unknown' gender as fallback for '{patient_gender_input}'"
|
|
846
|
+
)
|
|
847
|
+
else:
|
|
848
|
+
logger.error(
|
|
849
|
+
f"No 'unknown' gender found in database. Cannot handle gender '{patient_gender_input}'. Skipping gender update."
|
|
850
|
+
)
|
|
851
|
+
selected_data.pop("patient_gender", None)
|
|
852
|
+
else:
|
|
853
|
+
# Last resort: try to get 'unknown' gender
|
|
854
|
+
unknown_gender = Gender.objects.filter(
|
|
855
|
+
name__iexact="unknown"
|
|
856
|
+
).first()
|
|
857
|
+
if unknown_gender:
|
|
858
|
+
selected_data["patient_gender"] = unknown_gender
|
|
859
|
+
logger.warning(
|
|
860
|
+
f"Using 'unknown' gender as fallback for '{patient_gender_input}' (no mapping)"
|
|
861
|
+
)
|
|
862
|
+
else:
|
|
863
|
+
logger.error(
|
|
864
|
+
f"No 'unknown' gender found in database. Cannot handle gender '{patient_gender_input}'. Skipping gender update."
|
|
865
|
+
)
|
|
866
|
+
selected_data.pop("patient_gender", None)
|
|
867
|
+
else:
|
|
868
|
+
logger.warning(
|
|
869
|
+
f"Unexpected patient_gender type {type(patient_gender_input)}: {patient_gender_input}. Skipping gender update."
|
|
870
|
+
)
|
|
871
|
+
selected_data.pop("patient_gender", None)
|
|
872
|
+
except Exception as e:
|
|
873
|
+
logger.exception(
|
|
874
|
+
f"Error handling patient_gender '{patient_gender_input}': {e}. Skipping gender update."
|
|
875
|
+
)
|
|
876
|
+
selected_data.pop("patient_gender", None)
|
|
877
|
+
|
|
878
|
+
# Update other attributes from selected_data
|
|
879
|
+
patient_name_changed = False
|
|
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
|
+
|
|
886
|
+
# Avoid overwriting examiner names if they were just explicitly set
|
|
887
|
+
if (
|
|
888
|
+
k not in ["examiner_first_name", "examiner_last_name"]
|
|
889
|
+
or (k == "examiner_first_name" and examiner_first_name is None)
|
|
890
|
+
or (k == "examiner_last_name" and examiner_last_name is None)
|
|
891
|
+
):
|
|
892
|
+
try:
|
|
893
|
+
# --- Convert patient_dob if it's a date object ---
|
|
894
|
+
value_to_set = v
|
|
895
|
+
if k == "patient_dob":
|
|
896
|
+
if isinstance(v, date) and not isinstance(v, datetime):
|
|
897
|
+
aware_dob = timezone.make_aware(
|
|
898
|
+
datetime.combine(v, datetime.min.time())
|
|
899
|
+
)
|
|
900
|
+
value_to_set = aware_dob
|
|
901
|
+
logger.debug(
|
|
902
|
+
"Converted patient_dob from date to aware datetime during update: %s",
|
|
903
|
+
aware_dob,
|
|
904
|
+
)
|
|
905
|
+
elif isinstance(v, str):
|
|
906
|
+
# Handle string DOB - check if it's a field name or actual date
|
|
907
|
+
if v == "patient_dob" or v in [
|
|
908
|
+
"patient_first_name",
|
|
909
|
+
"patient_last_name",
|
|
910
|
+
"examination_date",
|
|
911
|
+
]:
|
|
912
|
+
logger.warning(
|
|
913
|
+
"Skipping invalid patient_dob value '%s' during update - appears to be field name",
|
|
914
|
+
v,
|
|
915
|
+
)
|
|
916
|
+
continue # Skip this field
|
|
917
|
+
else:
|
|
918
|
+
# Try to parse as date string
|
|
919
|
+
try:
|
|
920
|
+
import dateparser
|
|
921
|
+
|
|
922
|
+
parsed_dob = dateparser.parse(
|
|
923
|
+
v, languages=["de"], settings={"DATE_ORDER": "DMY"}
|
|
924
|
+
)
|
|
925
|
+
if parsed_dob:
|
|
926
|
+
value_to_set = timezone.make_aware(
|
|
927
|
+
parsed_dob.replace(
|
|
928
|
+
hour=0, minute=0, second=0, microsecond=0
|
|
929
|
+
)
|
|
930
|
+
)
|
|
931
|
+
logger.debug(
|
|
932
|
+
"Parsed string patient_dob '%s' during update to aware datetime: %s",
|
|
933
|
+
v,
|
|
934
|
+
value_to_set,
|
|
935
|
+
)
|
|
936
|
+
else:
|
|
937
|
+
logger.warning(
|
|
938
|
+
"Could not parse patient_dob string '%s' during update, skipping",
|
|
939
|
+
v,
|
|
940
|
+
)
|
|
941
|
+
continue
|
|
942
|
+
except Exception as e:
|
|
943
|
+
logger.warning(
|
|
944
|
+
"Error parsing patient_dob string '%s' during update: %s, skipping",
|
|
945
|
+
v,
|
|
946
|
+
e,
|
|
947
|
+
)
|
|
948
|
+
continue
|
|
949
|
+
elif k == "examination_date" and isinstance(v, str):
|
|
950
|
+
if v == "examination_date" or v in [
|
|
951
|
+
"patient_first_name",
|
|
952
|
+
"patient_last_name",
|
|
953
|
+
"patient_dob",
|
|
954
|
+
]:
|
|
955
|
+
logger.warning(
|
|
956
|
+
"Skipping invalid examination_date value '%s' during update - appears to be field name",
|
|
957
|
+
v,
|
|
958
|
+
)
|
|
959
|
+
continue
|
|
960
|
+
else:
|
|
961
|
+
# Try to parse as date string
|
|
962
|
+
try:
|
|
963
|
+
import dateparser
|
|
964
|
+
|
|
965
|
+
parsed_date = dateparser.parse(
|
|
966
|
+
v, languages=["de"], settings={"DATE_ORDER": "DMY"}
|
|
967
|
+
)
|
|
968
|
+
if parsed_date:
|
|
969
|
+
value_to_set = parsed_date.date()
|
|
970
|
+
logger.debug(
|
|
971
|
+
"Parsed string examination_date '%s' during update to date: %s",
|
|
972
|
+
v,
|
|
973
|
+
value_to_set,
|
|
974
|
+
)
|
|
975
|
+
else:
|
|
976
|
+
logger.warning(
|
|
977
|
+
"Could not parse examination_date string '%s' during update, skipping",
|
|
978
|
+
v,
|
|
979
|
+
)
|
|
980
|
+
continue
|
|
981
|
+
except Exception as e:
|
|
982
|
+
logger.warning(
|
|
983
|
+
"Error parsing examination_date string '%s' during update: %s, skipping",
|
|
984
|
+
v,
|
|
985
|
+
e,
|
|
986
|
+
)
|
|
987
|
+
continue
|
|
988
|
+
# --- End Conversion ---
|
|
989
|
+
|
|
990
|
+
# Check if patient name is changing
|
|
991
|
+
if (
|
|
992
|
+
k in ["patient_first_name", "patient_last_name"]
|
|
993
|
+
and getattr(instance, k) != value_to_set
|
|
994
|
+
):
|
|
995
|
+
patient_name_changed = True
|
|
996
|
+
|
|
997
|
+
setattr(instance, k, value_to_set) # Use value_to_set
|
|
998
|
+
|
|
999
|
+
except Exception as e:
|
|
1000
|
+
logger.error(
|
|
1001
|
+
f"Error setting attribute '{k}' to '{v}': {e}. Skipping this field."
|
|
1002
|
+
)
|
|
1003
|
+
continue
|
|
1004
|
+
|
|
1005
|
+
# Update name DB if patient names changed
|
|
1006
|
+
if patient_name_changed:
|
|
1007
|
+
try:
|
|
1008
|
+
update_name_db(instance.patient_first_name, instance.patient_last_name)
|
|
1009
|
+
except Exception as e:
|
|
1010
|
+
logger.warning(f"Error updating name database: {e}")
|
|
1011
|
+
|
|
1012
|
+
# Call save - this will trigger the full save logic including hash recalculation etc.
|
|
1013
|
+
try:
|
|
1014
|
+
instance.save()
|
|
1015
|
+
except Exception as e:
|
|
1016
|
+
logger.error(f"Error saving SensitiveMeta instance: {e}")
|
|
1017
|
+
raise
|
|
1018
|
+
|
|
1019
|
+
return instance
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def update_or_create_sensitive_meta_from_dict(
|
|
1023
|
+
cls: Type["SensitiveMeta"],
|
|
1024
|
+
data: Dict[str, Any],
|
|
1025
|
+
instance: Optional["SensitiveMeta"] = None,
|
|
1026
|
+
) -> "SensitiveMeta":
|
|
1027
|
+
"""Logic to update or create a SensitiveMeta instance from a dictionary."""
|
|
1028
|
+
# Check if the instance already exists based on unique fields
|
|
1029
|
+
if instance:
|
|
1030
|
+
# Update the existing instance
|
|
1031
|
+
return update_sensitive_meta_from_dict(instance, data), False
|
|
1032
|
+
else:
|
|
1033
|
+
# Create a new instance
|
|
1034
|
+
return create_sensitive_meta_from_dict(cls, data), True
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def _map_gender_string_to_standard(gender_str: str) -> Optional[str]:
|
|
1038
|
+
"""Maps various gender string inputs to standard gender names used in the DB."""
|
|
1039
|
+
mapping = {
|
|
1040
|
+
"male": ["male", "m", "männlich", "man"],
|
|
1041
|
+
"female": ["female", "f", "weiblich", "woman"],
|
|
1042
|
+
"unknown": ["unknown", "unbekannt", "other", "diverse", ""],
|
|
1043
|
+
}
|
|
1044
|
+
gender_lower = gender_str.strip().lower()
|
|
1045
|
+
for standard, variants in mapping.items():
|
|
1046
|
+
if gender_lower in variants:
|
|
1047
|
+
return standard
|
|
1048
|
+
return None
|