endoreg-db 0.8.4.4__py3-none-any.whl → 0.8.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of endoreg-db might be problematic. Click here for more details.
- endoreg_db/authz/auth.py +74 -0
- endoreg_db/authz/backends.py +168 -0
- endoreg_db/authz/management/commands/list_routes.py +18 -0
- endoreg_db/authz/middleware.py +83 -0
- endoreg_db/authz/permissions.py +127 -0
- endoreg_db/authz/policy.py +218 -0
- endoreg_db/authz/views_auth.py +66 -0
- endoreg_db/config/env.py +13 -8
- endoreg_db/data/__init__.py +8 -31
- endoreg_db/data/_examples/disease.yaml +55 -0
- endoreg_db/data/_examples/disease_classification.yaml +13 -0
- endoreg_db/data/_examples/disease_classification_choice.yaml +62 -0
- endoreg_db/data/_examples/event.yaml +64 -0
- endoreg_db/data/_examples/examination.yaml +72 -0
- endoreg_db/data/_examples/finding/anatomy_colon.yaml +128 -0
- endoreg_db/data/_examples/finding/colonoscopy.yaml +40 -0
- endoreg_db/data/_examples/finding/colonoscopy_bowel_prep.yaml +56 -0
- endoreg_db/data/_examples/finding/complication.yaml +16 -0
- endoreg_db/data/_examples/finding/data.yaml +105 -0
- endoreg_db/data/_examples/finding/examination_setting.yaml +16 -0
- endoreg_db/data/_examples/finding/medication_related.yaml +18 -0
- endoreg_db/data/_examples/finding/outcome.yaml +12 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_bowel_preparation.yaml +68 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_jnet.yaml +22 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_kudo.yaml +25 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_circularity.yaml +20 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_planarity.yaml +24 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_size.yaml +68 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_surface.yaml +20 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_location.yaml +80 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_lst.yaml +21 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_nice.yaml +20 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_paris.yaml +26 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_sano.yaml +22 -0
- endoreg_db/data/_examples/finding_classification/colonoscopy_summary.yaml +53 -0
- endoreg_db/data/_examples/finding_classification/complication_generic.yaml +25 -0
- endoreg_db/data/_examples/finding_classification/examination_setting_generic.yaml +40 -0
- endoreg_db/data/_examples/finding_classification/histology_colo.yaml +51 -0
- endoreg_db/data/_examples/finding_classification/intervention_required.yaml +26 -0
- endoreg_db/data/_examples/finding_classification/medication_related.yaml +23 -0
- endoreg_db/data/_examples/finding_classification/visualized.yaml +33 -0
- endoreg_db/data/_examples/finding_classification_choice/bowel_preparation.yaml +78 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_circularity_default.yaml +32 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_jnet.yaml +15 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_kudo.yaml +23 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_lst.yaml +15 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_nice.yaml +17 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_paris.yaml +57 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_planarity_default.yaml +49 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_sano.yaml +14 -0
- endoreg_db/data/_examples/finding_classification_choice/colon_lesion_surface_intact_default.yaml +36 -0
- endoreg_db/data/_examples/finding_classification_choice/colonoscopy_location.yaml +229 -0
- endoreg_db/data/_examples/finding_classification_choice/colonoscopy_not_complete_reason.yaml +19 -0
- endoreg_db/data/_examples/finding_classification_choice/colonoscopy_size.yaml +82 -0
- endoreg_db/data/_examples/finding_classification_choice/colonoscopy_summary_worst_finding.yaml +15 -0
- endoreg_db/data/_examples/finding_classification_choice/complication_generic_types.yaml +15 -0
- endoreg_db/data/_examples/finding_classification_choice/examination_setting_generic_types.yaml +15 -0
- endoreg_db/data/_examples/finding_classification_choice/histology.yaml +24 -0
- endoreg_db/data/_examples/finding_classification_choice/histology_polyp.yaml +20 -0
- endoreg_db/data/_examples/finding_classification_choice/outcome.yaml +19 -0
- endoreg_db/data/_examples/finding_classification_choice/yes_no_na.yaml +11 -0
- endoreg_db/data/_examples/finding_classification_type/colonoscopy_basic.yaml +48 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy.yaml +43 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy_colonoscopy.yaml +168 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy_egd.yaml +128 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy_ercp.yaml +32 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy_eus_lower.yaml +9 -0
- endoreg_db/data/_examples/finding_intervention/endoscopy_eus_upper.yaml +36 -0
- endoreg_db/data/_examples/finding_intervention_type/endoscopy.yaml +15 -0
- endoreg_db/data/_examples/finding_type/data.yaml +43 -0
- endoreg_db/data/_examples/requirement/age.yaml +26 -0
- endoreg_db/data/_examples/requirement/colonoscopy_baseline_austria.yaml +45 -0
- endoreg_db/data/_examples/requirement/disease_cardiovascular.yaml +79 -0
- endoreg_db/data/_examples/requirement/disease_classification_choice_cardiovascular.yaml +41 -0
- endoreg_db/data/_examples/requirement/disease_hepatology.yaml +12 -0
- endoreg_db/data/_examples/requirement/disease_misc.yaml +12 -0
- endoreg_db/data/_examples/requirement/disease_renal.yaml +96 -0
- endoreg_db/data/_examples/requirement/endoscopy_bleeding_risk.yaml +59 -0
- endoreg_db/data/_examples/requirement/event_cardiology.yaml +251 -0
- endoreg_db/data/_examples/requirement/event_requirements.yaml +145 -0
- endoreg_db/data/_examples/requirement/finding_colon_polyp.yaml +50 -0
- endoreg_db/data/_examples/requirement/gender.yaml +25 -0
- endoreg_db/data/_examples/requirement/lab_value.yaml +441 -0
- endoreg_db/data/_examples/requirement/medication.yaml +93 -0
- endoreg_db/data/_examples/requirement_operator/age.yaml +13 -0
- endoreg_db/data/_examples/requirement_operator/lab_operators.yaml +129 -0
- endoreg_db/data/_examples/requirement_operator/model_operators.yaml +96 -0
- endoreg_db/data/_examples/requirement_set/01_endoscopy_generic.yaml +48 -0
- endoreg_db/data/_examples/requirement_set/colonoscopy_austria_screening.yaml +57 -0
- endoreg_db/data/_examples/yaml_examples.xlsx +0 -0
- endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +4 -3
- endoreg_db/data/event_classification/data.yaml +4 -0
- endoreg_db/data/event_classification_choice/data.yaml +9 -0
- endoreg_db/data/finding_classification/colonoscopy_bowel_preparation.yaml +43 -70
- endoreg_db/data/finding_classification/colonoscopy_lesion_size.yaml +22 -52
- endoreg_db/data/finding_classification/colonoscopy_location.yaml +31 -62
- endoreg_db/data/finding_classification/histology_colo.yaml +28 -36
- endoreg_db/data/requirement/colon_polyp_intervention.yaml +49 -0
- endoreg_db/data/requirement/coloreg_colon_polyp.yaml +49 -0
- endoreg_db/data/requirement_set/01_endoscopy_generic.yaml +31 -12
- endoreg_db/data/requirement_set/01_laboratory.yaml +13 -0
- endoreg_db/data/requirement_set/02_endoscopy_bleeding_risk.yaml +46 -0
- endoreg_db/data/requirement_set/90_coloreg.yaml +178 -0
- endoreg_db/data/requirement_set/_old_ +109 -0
- endoreg_db/data/requirement_set_type/data.yaml +21 -0
- endoreg_db/data/setup_config.yaml +4 -4
- endoreg_db/data/tag/requirement_set_tags.yaml +21 -0
- endoreg_db/exceptions.py +5 -2
- endoreg_db/helpers/data_loader.py +1 -1
- endoreg_db/management/commands/create_model_meta_from_huggingface.py +21 -10
- endoreg_db/management/commands/create_multilabel_model_meta.py +299 -129
- endoreg_db/management/commands/import_video.py +9 -10
- endoreg_db/management/commands/import_video_with_classification.py +1 -1
- endoreg_db/management/commands/init_default_ai_model.py +1 -1
- endoreg_db/management/commands/list_routes.py +18 -0
- endoreg_db/management/commands/load_ai_model_data.py +2 -1
- endoreg_db/management/commands/load_center_data.py +12 -12
- endoreg_db/management/commands/load_requirement_data.py +60 -31
- endoreg_db/management/commands/load_requirement_set_tags.py +95 -0
- endoreg_db/management/commands/setup_endoreg_db.py +14 -10
- endoreg_db/management/commands/storage_management.py +271 -203
- endoreg_db/migrations/0001_initial.py +1799 -1300
- endoreg_db/migrations/0002_requirementset_depends_on.py +18 -0
- endoreg_db/migrations/_old/0001_initial.py +1857 -0
- endoreg_db/migrations/_old/0004_employee_city_employee_post_code_employee_street_and_more.py +68 -0
- endoreg_db/migrations/_old/0004_remove_casetemplate_rules_and_more.py +77 -0
- endoreg_db/migrations/_old/0005_merge_20251111_1003.py +14 -0
- endoreg_db/migrations/_old/0006_sensitivemeta_anonymized_text_and_more.py +68 -0
- endoreg_db/migrations/_old/0007_remove_rule_attribute_dtype_remove_rule_rule_type_and_more.py +89 -0
- endoreg_db/migrations/_old/0008_remove_event_event_classification_and_more.py +27 -0
- endoreg_db/migrations/_old/0009_alter_modelmeta_options_and_more.py +21 -0
- endoreg_db/models/__init__.py +78 -123
- endoreg_db/models/administration/__init__.py +21 -42
- endoreg_db/models/administration/ai/active_model.py +2 -2
- endoreg_db/models/administration/ai/ai_model.py +7 -6
- endoreg_db/models/administration/case/__init__.py +1 -15
- endoreg_db/models/administration/case/case.py +3 -3
- endoreg_db/models/administration/case/case_template/__init__.py +2 -14
- endoreg_db/models/administration/case/case_template/case_template.py +2 -124
- endoreg_db/models/administration/case/case_template/case_template_rule.py +2 -268
- endoreg_db/models/administration/case/case_template/case_template_rule_value.py +2 -85
- endoreg_db/models/administration/case/case_template/case_template_type.py +2 -25
- endoreg_db/models/administration/center/center.py +33 -19
- endoreg_db/models/administration/center/center_product.py +12 -9
- endoreg_db/models/administration/center/center_resource.py +25 -19
- endoreg_db/models/administration/center/center_shift.py +21 -17
- endoreg_db/models/administration/center/center_waste.py +16 -8
- endoreg_db/models/administration/person/__init__.py +2 -0
- endoreg_db/models/administration/person/employee/employee.py +10 -5
- endoreg_db/models/administration/person/employee/employee_qualification.py +9 -4
- endoreg_db/models/administration/person/employee/employee_type.py +12 -6
- endoreg_db/models/administration/person/examiner/examiner.py +13 -11
- endoreg_db/models/administration/person/patient/__init__.py +2 -0
- endoreg_db/models/administration/person/patient/patient.py +103 -100
- endoreg_db/models/administration/person/patient/patient_external_id.py +37 -0
- endoreg_db/models/administration/person/person.py +4 -0
- endoreg_db/models/administration/person/profession/__init__.py +8 -4
- endoreg_db/models/administration/person/user/portal_user_information.py +11 -7
- endoreg_db/models/administration/product/product.py +20 -15
- endoreg_db/models/administration/product/product_material.py +17 -18
- endoreg_db/models/administration/product/product_weight.py +12 -8
- endoreg_db/models/administration/product/reference_product.py +23 -55
- endoreg_db/models/administration/qualification/qualification.py +7 -3
- endoreg_db/models/administration/qualification/qualification_type.py +7 -3
- endoreg_db/models/administration/shift/scheduled_days.py +8 -5
- endoreg_db/models/administration/shift/shift.py +16 -12
- endoreg_db/models/administration/shift/shift_type.py +23 -31
- endoreg_db/models/label/__init__.py +7 -8
- endoreg_db/models/label/annotation/image_classification.py +10 -9
- endoreg_db/models/label/annotation/video_segmentation_annotation.py +8 -5
- endoreg_db/models/label/label.py +15 -15
- endoreg_db/models/label/label_set.py +19 -6
- endoreg_db/models/label/label_type.py +1 -1
- endoreg_db/models/label/label_video_segment/_create_from_video.py +5 -8
- endoreg_db/models/label/label_video_segment/label_video_segment.py +76 -102
- endoreg_db/models/label/video_segmentation_label.py +4 -0
- endoreg_db/models/label/video_segmentation_labelset.py +4 -3
- endoreg_db/models/media/frame/frame.py +22 -22
- endoreg_db/models/media/pdf/raw_pdf.py +249 -177
- endoreg_db/models/media/pdf/report_file.py +25 -29
- endoreg_db/models/media/pdf/report_reader/report_reader_config.py +30 -46
- endoreg_db/models/media/pdf/report_reader/report_reader_flag.py +23 -7
- endoreg_db/models/media/video/__init__.py +1 -0
- endoreg_db/models/media/video/create_from_file.py +48 -56
- endoreg_db/models/media/video/pipe_1.py +30 -33
- endoreg_db/models/media/video/pipe_2.py +8 -9
- endoreg_db/models/media/video/video_file.py +359 -204
- endoreg_db/models/media/video/video_file_ai.py +288 -74
- endoreg_db/models/media/video/video_file_anonymize.py +38 -38
- endoreg_db/models/media/video/video_file_frames/__init__.py +3 -1
- endoreg_db/models/media/video/video_file_frames/_bulk_create_frames.py +6 -8
- endoreg_db/models/media/video/video_file_frames/_create_frame_object.py +7 -9
- endoreg_db/models/media/video/video_file_frames/_delete_frames.py +9 -8
- endoreg_db/models/media/video/video_file_frames/_extract_frames.py +38 -45
- endoreg_db/models/media/video/video_file_frames/_get_frame.py +6 -8
- endoreg_db/models/media/video/video_file_frames/_get_frame_number.py +4 -18
- endoreg_db/models/media/video/video_file_frames/_get_frame_path.py +4 -3
- endoreg_db/models/media/video/video_file_frames/_get_frame_paths.py +7 -6
- endoreg_db/models/media/video/video_file_frames/_get_frame_range.py +6 -8
- endoreg_db/models/media/video/video_file_frames/_get_frames.py +6 -8
- endoreg_db/models/media/video/video_file_frames/_initialize_frames.py +15 -25
- endoreg_db/models/media/video/video_file_frames/_manage_frame_range.py +26 -23
- endoreg_db/models/media/video/video_file_frames/_mark_frames_extracted_status.py +23 -14
- endoreg_db/models/media/video/video_file_io.py +109 -62
- endoreg_db/models/media/video/video_file_meta/get_crop_template.py +3 -3
- endoreg_db/models/media/video/video_file_meta/get_endo_roi.py +5 -3
- endoreg_db/models/media/video/video_file_meta/get_fps.py +37 -34
- endoreg_db/models/media/video/video_file_meta/initialize_video_specs.py +19 -25
- endoreg_db/models/media/video/video_file_meta/text_meta.py +41 -38
- endoreg_db/models/media/video/video_file_meta/video_meta.py +14 -7
- endoreg_db/models/media/video/video_file_segments.py +24 -17
- endoreg_db/models/media/video/video_metadata.py +19 -35
- endoreg_db/models/media/video/video_processing.py +96 -95
- endoreg_db/models/medical/contraindication/__init__.py +13 -3
- endoreg_db/models/medical/disease.py +22 -16
- endoreg_db/models/medical/event.py +31 -18
- endoreg_db/models/medical/examination/__init__.py +13 -6
- endoreg_db/models/medical/examination/examination.py +17 -18
- endoreg_db/models/medical/examination/examination_indication.py +26 -25
- endoreg_db/models/medical/examination/examination_time.py +16 -6
- endoreg_db/models/medical/examination/examination_time_type.py +9 -6
- endoreg_db/models/medical/examination/examination_type.py +3 -4
- endoreg_db/models/medical/finding/finding.py +38 -39
- endoreg_db/models/medical/finding/finding_classification.py +37 -48
- endoreg_db/models/medical/finding/finding_intervention.py +27 -22
- endoreg_db/models/medical/finding/finding_type.py +13 -12
- endoreg_db/models/medical/hardware/endoscope.py +20 -26
- endoreg_db/models/medical/hardware/endoscopy_processor.py +2 -2
- endoreg_db/models/medical/laboratory/lab_value.py +62 -91
- endoreg_db/models/medical/medication/medication.py +22 -10
- endoreg_db/models/medical/medication/medication_indication.py +29 -3
- endoreg_db/models/medical/medication/medication_indication_type.py +25 -14
- endoreg_db/models/medical/medication/medication_intake_time.py +31 -19
- endoreg_db/models/medical/medication/medication_schedule.py +27 -16
- endoreg_db/models/medical/organ/__init__.py +15 -12
- endoreg_db/models/medical/patient/medication_examples.py +1 -5
- endoreg_db/models/medical/patient/patient_disease.py +20 -23
- endoreg_db/models/medical/patient/patient_event.py +19 -22
- endoreg_db/models/medical/patient/patient_examination.py +48 -54
- endoreg_db/models/medical/patient/patient_examination_indication.py +16 -14
- endoreg_db/models/medical/patient/patient_finding.py +122 -139
- endoreg_db/models/medical/patient/patient_finding_classification.py +44 -49
- endoreg_db/models/medical/patient/patient_finding_intervention.py +8 -19
- endoreg_db/models/medical/patient/patient_lab_sample.py +28 -23
- endoreg_db/models/medical/patient/patient_lab_value.py +82 -89
- endoreg_db/models/medical/patient/patient_medication.py +27 -38
- endoreg_db/models/medical/patient/patient_medication_schedule.py +28 -36
- endoreg_db/models/medical/risk/risk.py +7 -6
- endoreg_db/models/medical/risk/risk_type.py +8 -5
- endoreg_db/models/metadata/model_meta.py +60 -29
- endoreg_db/models/metadata/model_meta_logic.py +139 -18
- endoreg_db/models/metadata/pdf_meta.py +19 -24
- endoreg_db/models/metadata/sensitive_meta.py +102 -85
- endoreg_db/models/metadata/sensitive_meta_logic.py +383 -43
- endoreg_db/models/metadata/video_meta.py +51 -31
- endoreg_db/models/metadata/video_prediction_logic.py +16 -23
- endoreg_db/models/metadata/video_prediction_meta.py +29 -33
- endoreg_db/models/other/distribution/date_value_distribution.py +89 -29
- endoreg_db/models/other/distribution/multiple_categorical_value_distribution.py +21 -5
- endoreg_db/models/other/distribution/numeric_value_distribution.py +114 -53
- endoreg_db/models/other/distribution/single_categorical_value_distribution.py +4 -3
- endoreg_db/models/other/emission/emission_factor.py +18 -8
- endoreg_db/models/other/gender.py +10 -5
- endoreg_db/models/other/information_source.py +25 -25
- endoreg_db/models/other/material.py +9 -5
- endoreg_db/models/other/resource.py +6 -4
- endoreg_db/models/other/tag.py +10 -5
- endoreg_db/models/other/transport_route.py +13 -8
- endoreg_db/models/other/unit.py +10 -6
- endoreg_db/models/other/waste.py +6 -5
- endoreg_db/models/requirement/requirement.py +580 -272
- endoreg_db/models/requirement/requirement_error.py +85 -0
- endoreg_db/models/requirement/requirement_evaluation/evaluate_with_dependencies.py +268 -0
- endoreg_db/models/requirement/requirement_evaluation/operator_evaluation_models.py +3 -6
- endoreg_db/models/requirement/requirement_evaluation/requirement_type_parser.py +90 -64
- endoreg_db/models/requirement/requirement_operator.py +36 -33
- endoreg_db/models/requirement/requirement_set.py +74 -57
- endoreg_db/models/state/__init__.py +4 -4
- endoreg_db/models/state/abstract.py +2 -2
- endoreg_db/models/state/anonymization.py +12 -0
- endoreg_db/models/state/audit_ledger.py +46 -47
- endoreg_db/models/state/label_video_segment.py +9 -0
- endoreg_db/models/state/raw_pdf.py +40 -46
- endoreg_db/models/state/sensitive_meta.py +6 -2
- endoreg_db/models/state/video.py +58 -53
- endoreg_db/models/upload_job.py +32 -55
- endoreg_db/models/utils.py +1 -2
- endoreg_db/root_urls.py +21 -2
- endoreg_db/serializers/__init__.py +26 -57
- endoreg_db/serializers/anonymization.py +18 -10
- endoreg_db/serializers/meta/report_meta.py +1 -1
- endoreg_db/serializers/meta/sensitive_meta_detail.py +63 -118
- endoreg_db/serializers/misc/__init__.py +1 -1
- endoreg_db/serializers/misc/file_overview.py +33 -91
- endoreg_db/serializers/misc/{vop_patient_data.py → sensitive_patient_data.py} +1 -1
- endoreg_db/serializers/requirements/requirement_sets.py +92 -22
- endoreg_db/serializers/video/segmentation.py +2 -1
- endoreg_db/serializers/video/video_processing_history.py +20 -5
- endoreg_db/serializers/video_examination.py +198 -0
- endoreg_db/services/anonymization.py +75 -73
- endoreg_db/services/lookup_service.py +256 -73
- endoreg_db/services/lookup_store.py +174 -30
- endoreg_db/services/pdf_import.py +711 -310
- endoreg_db/services/storage_aware_video_processor.py +140 -114
- endoreg_db/services/video_import.py +266 -117
- endoreg_db/urls/__init__.py +27 -27
- endoreg_db/urls/label_video_segments.py +2 -0
- endoreg_db/urls/media.py +108 -66
- endoreg_db/urls/root_urls.py +29 -0
- endoreg_db/utils/__init__.py +15 -5
- endoreg_db/utils/ai/multilabel_classification_net.py +116 -20
- endoreg_db/utils/case_generator/__init__.py +3 -0
- endoreg_db/utils/dataloader.py +88 -16
- endoreg_db/utils/defaults/set_default_center.py +32 -0
- endoreg_db/utils/names.py +22 -16
- endoreg_db/utils/permissions.py +2 -1
- endoreg_db/utils/pipelines/process_video_dir.py +1 -1
- endoreg_db/utils/requirement_operator_logic/model_evaluators.py +414 -127
- endoreg_db/utils/setup_config.py +8 -5
- endoreg_db/utils/storage.py +115 -0
- endoreg_db/utils/validate_endo_roi.py +8 -2
- endoreg_db/utils/video/ffmpeg_wrapper.py +184 -188
- endoreg_db/views/__init__.py +5 -12
- endoreg_db/views/anonymization/media_management.py +198 -163
- endoreg_db/views/anonymization/overview.py +4 -1
- endoreg_db/views/anonymization/validate.py +174 -40
- endoreg_db/views/media/__init__.py +2 -0
- endoreg_db/views/media/pdf_media.py +131 -150
- endoreg_db/views/media/sensitive_metadata.py +46 -6
- endoreg_db/views/media/video_media.py +89 -82
- endoreg_db/views/media/video_segments.py +187 -260
- endoreg_db/views/meta/sensitive_meta_detail.py +0 -63
- endoreg_db/views/patient/patient.py +5 -4
- endoreg_db/views/pdf/__init__.py +5 -8
- endoreg_db/views/pdf/pdf_stream.py +186 -0
- endoreg_db/views/pdf/pdf_stream_views.py +0 -127
- endoreg_db/views/pdf/reimport.py +86 -91
- endoreg_db/views/requirement/evaluate.py +188 -187
- endoreg_db/views/requirement/lookup.py +186 -288
- endoreg_db/views/requirement/requirement_utils.py +89 -0
- endoreg_db/views/video/__init__.py +0 -4
- endoreg_db/views/video/correction.py +2 -2
- endoreg_db/views/video/video_examination_viewset.py +202 -289
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.8.0.dist-info}/METADATA +7 -3
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.8.0.dist-info}/RECORD +350 -255
- endoreg_db/models/administration/permissions/__init__.py +0 -44
- endoreg_db/models/media/video/refactor_plan.md +0 -0
- endoreg_db/models/media/video/video_file_frames.py +0 -0
- endoreg_db/models/metadata/frame_ocr_result.py +0 -0
- endoreg_db/models/rule/__init__.py +0 -13
- endoreg_db/models/rule/rule.py +0 -27
- endoreg_db/models/rule/rule_applicator.py +0 -224
- endoreg_db/models/rule/rule_attribute_dtype.py +0 -17
- endoreg_db/models/rule/rule_type.py +0 -20
- endoreg_db/models/rule/ruleset.py +0 -17
- endoreg_db/serializers/video/video_metadata.py +0 -105
- endoreg_db/urls/report.py +0 -48
- endoreg_db/urls/video.py +0 -61
- endoreg_db/utils/case_generator/case_generator.py +0 -159
- endoreg_db/utils/case_generator/utils.py +0 -30
- endoreg_db/views/pdf/pdf_media.py +0 -239
- endoreg_db/views/report/__init__.py +0 -9
- endoreg_db/views/report/report_list.py +0 -112
- endoreg_db/views/report/report_with_secure_url.py +0 -28
- endoreg_db/views/report/start_examination.py +0 -7
- endoreg_db/views/video/video_media.py +0 -158
- endoreg_db/views.py +0 -0
- /endoreg_db/data/{requirement_set → _examples/requirement_set}/endoscopy_bleeding_risk.yaml +0 -0
- /endoreg_db/migrations/{0002_add_video_correction_models.py → _old/0002_add_video_correction_models.py} +0 -0
- /endoreg_db/migrations/{0003_add_center_display_name.py → _old/0003_add_center_display_name.py} +0 -0
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.8.0.dist-info}/WHEEL +0 -0
- {endoreg_db-0.8.4.4.dist-info → endoreg_db-0.8.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -18,14 +18,14 @@ from endoreg_db.utils.hashs import get_patient_examination_hash, get_patient_has
|
|
|
18
18
|
from ..administration import Center, Examiner, FirstName, LastName, Patient
|
|
19
19
|
from ..medical import PatientExamination
|
|
20
20
|
from ..other import Gender
|
|
21
|
-
from ..state import SensitiveMetaState
|
|
22
21
|
|
|
23
22
|
if TYPE_CHECKING:
|
|
24
23
|
from .sensitive_meta import SensitiveMeta # Import model for type hinting
|
|
25
24
|
|
|
26
25
|
logger = logging.getLogger(__name__)
|
|
27
26
|
SECRET_SALT = os.getenv("DJANGO_SALT", "default_salt")
|
|
28
|
-
|
|
27
|
+
DEFAULT_UNKNOWN = "unknown"
|
|
28
|
+
|
|
29
29
|
|
|
30
30
|
# Regex-Pattern für verschiedene Datumsformate
|
|
31
31
|
ISO_RX = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
|
@@ -160,6 +160,9 @@ def calculate_patient_hash(instance: "SensitiveMeta", salt: str = SECRET_SALT) -
|
|
|
160
160
|
if not center:
|
|
161
161
|
raise ValueError("Center is required to calculate patient hash.")
|
|
162
162
|
|
|
163
|
+
assert first_name is not None, "First name is required to calculate patient hash."
|
|
164
|
+
assert last_name is not None, "Last name is required to calculate patient hash."
|
|
165
|
+
|
|
163
166
|
hash_str = get_patient_hash(
|
|
164
167
|
first_name=first_name,
|
|
165
168
|
last_name=last_name,
|
|
@@ -185,6 +188,11 @@ def calculate_examination_hash(instance: "SensitiveMeta", salt: str = SECRET_SAL
|
|
|
185
188
|
if not center:
|
|
186
189
|
raise ValueError("Center is required to calculate examination hash.")
|
|
187
190
|
|
|
191
|
+
if not first_name:
|
|
192
|
+
raise ValueError("First name is required to calculate examination hash.")
|
|
193
|
+
if not last_name:
|
|
194
|
+
raise ValueError("Last name is required to calculate examination hash.")
|
|
195
|
+
|
|
188
196
|
hash_str = get_patient_examination_hash(
|
|
189
197
|
first_name=first_name,
|
|
190
198
|
last_name=last_name,
|
|
@@ -206,7 +214,7 @@ def create_pseudo_examiner_logic(instance: "SensitiveMeta") -> "Examiner":
|
|
|
206
214
|
logger.warning(f"Incomplete examiner info for SensitiveMeta (pk={instance.pk or 'new'}). Using default examiner.")
|
|
207
215
|
# Ensure default center exists or handle appropriately
|
|
208
216
|
try:
|
|
209
|
-
default_center = Center.objects.
|
|
217
|
+
default_center = Center.objects.get(name="endoreg_db_demo")
|
|
210
218
|
except Center.DoesNotExist:
|
|
211
219
|
logger.error("Default center 'endoreg_db_demo' not found. Cannot create default examiner.")
|
|
212
220
|
raise ValueError("Default center 'endoreg_db_demo' not found.")
|
|
@@ -218,7 +226,7 @@ def create_pseudo_examiner_logic(instance: "SensitiveMeta") -> "Examiner":
|
|
|
218
226
|
return examiner
|
|
219
227
|
|
|
220
228
|
|
|
221
|
-
def get_or_create_pseudo_patient_logic(instance: "SensitiveMeta")
|
|
229
|
+
def get_or_create_pseudo_patient_logic(instance: "SensitiveMeta"):
|
|
222
230
|
"""Gets or creates the pseudo patient based on instance data."""
|
|
223
231
|
# Ensure necessary fields are set
|
|
224
232
|
if not instance.patient_hash:
|
|
@@ -241,12 +249,12 @@ def get_or_create_pseudo_patient_logic(instance: "SensitiveMeta") -> "Patient":
|
|
|
241
249
|
birth_year=year,
|
|
242
250
|
birth_month=month,
|
|
243
251
|
)
|
|
244
|
-
return patient
|
|
252
|
+
return patient, _created
|
|
245
253
|
|
|
246
254
|
|
|
247
255
|
def get_or_create_pseudo_patient_examination_logic(
|
|
248
256
|
instance: "SensitiveMeta",
|
|
249
|
-
)
|
|
257
|
+
):
|
|
250
258
|
"""Gets or creates the pseudo patient examination based on instance data."""
|
|
251
259
|
# Ensure necessary fields are set
|
|
252
260
|
if not instance.patient_hash:
|
|
@@ -255,9 +263,9 @@ def get_or_create_pseudo_patient_examination_logic(
|
|
|
255
263
|
instance.examination_hash = calculate_examination_hash(instance)
|
|
256
264
|
|
|
257
265
|
# Ensure the pseudo patient exists first, as PatientExamination might depend on it
|
|
258
|
-
if not instance.
|
|
259
|
-
pseudo_patient = get_or_create_pseudo_patient_logic(instance)
|
|
260
|
-
instance.
|
|
266
|
+
if not instance.pseudo_patient:
|
|
267
|
+
pseudo_patient, _created = get_or_create_pseudo_patient_logic(instance)
|
|
268
|
+
instance.pseudo_patient = pseudo_patient # Assign FK directly
|
|
261
269
|
|
|
262
270
|
patient_examination, _created = PatientExamination.get_or_create_pseudo_patient_examination_by_hash(
|
|
263
271
|
patient_hash=instance.patient_hash,
|
|
@@ -265,7 +273,7 @@ def get_or_create_pseudo_patient_examination_logic(
|
|
|
265
273
|
# Optionally pass pseudo_patient if the method requires it
|
|
266
274
|
# pseudo_patient=instance.pseudo_patient
|
|
267
275
|
)
|
|
268
|
-
return patient_examination
|
|
276
|
+
return patient_examination, _created
|
|
269
277
|
|
|
270
278
|
|
|
271
279
|
@transaction.atomic # Ensure all operations within save succeed or fail together
|
|
@@ -273,7 +281,53 @@ def perform_save_logic(instance: "SensitiveMeta") -> "Examiner":
|
|
|
273
281
|
"""
|
|
274
282
|
Contains the core logic for preparing a SensitiveMeta instance for saving.
|
|
275
283
|
Handles data generation (dates), hash calculation, and linking pseudo-entities.
|
|
276
|
-
|
|
284
|
+
|
|
285
|
+
This function is called on every save() operation and implements a two-phase approach:
|
|
286
|
+
|
|
287
|
+
**Phase 1: Initial Creation (with defaults)**
|
|
288
|
+
- When a SensitiveMeta is first created (e.g., via get_or_create_sensitive_meta()),
|
|
289
|
+
it may have missing patient data (names, DOB, etc.)
|
|
290
|
+
- Default values are set to prevent hash calculation errors:
|
|
291
|
+
* patient_first_name: "unknown"
|
|
292
|
+
* patient_last_name: "unknown"
|
|
293
|
+
* patient_dob: random date (1920-2000)
|
|
294
|
+
- A temporary hash is calculated using these defaults
|
|
295
|
+
- Temporary pseudo-entities (Patient, Examination) are created
|
|
296
|
+
|
|
297
|
+
**Phase 2: Update (with extracted data)**
|
|
298
|
+
- When real patient data is extracted (e.g., from video OCR via lx_anonymizer),
|
|
299
|
+
update_from_dict() is called with actual values
|
|
300
|
+
- The instance fields are updated with real data (names, DOB, etc.)
|
|
301
|
+
- save() is called again, triggering this function
|
|
302
|
+
- Default-setting logic is skipped (fields are no longer empty)
|
|
303
|
+
- Hash is RECALCULATED with real data
|
|
304
|
+
- New pseudo-entities are created/retrieved based on new hash
|
|
305
|
+
|
|
306
|
+
**Example Flow:**
|
|
307
|
+
```
|
|
308
|
+
# Initial creation
|
|
309
|
+
sm = SensitiveMeta.create_from_dict({"center": center})
|
|
310
|
+
# → patient_first_name = "unknown", patient_last_name = "unknown"
|
|
311
|
+
# → hash = sha256("unknown unknown 1990-01-01 ...")
|
|
312
|
+
# → pseudo_patient_temp created
|
|
313
|
+
|
|
314
|
+
# Later update with extracted data
|
|
315
|
+
sm.update_from_dict({"patient_first_name": "Max", "patient_last_name": "Mustermann"})
|
|
316
|
+
# → patient_first_name = "Max", patient_last_name = "Mustermann" (overwrites)
|
|
317
|
+
# → save() triggered → perform_save_logic() called again
|
|
318
|
+
# → Default-setting skipped (names already exist)
|
|
319
|
+
# → hash = sha256("Max Mustermann 1985-03-15 ...") (RECALCULATED)
|
|
320
|
+
# → pseudo_patient_real created/retrieved with new hash
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
instance: The SensitiveMeta instance being saved
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Examiner: The pseudo examiner instance to be linked via M2M after save
|
|
328
|
+
|
|
329
|
+
Raises:
|
|
330
|
+
ValueError: If required fields (center, gender) cannot be determined
|
|
277
331
|
"""
|
|
278
332
|
|
|
279
333
|
# --- Pre-Save Checks and Data Generation ---
|
|
@@ -290,28 +344,80 @@ def perform_save_logic(instance: "SensitiveMeta") -> "Examiner":
|
|
|
290
344
|
if not instance.center:
|
|
291
345
|
raise ValueError("Center must be set before saving SensitiveMeta.")
|
|
292
346
|
|
|
347
|
+
# 2.5 CRITICAL: Set default patient names BEFORE hash calculation
|
|
348
|
+
#
|
|
349
|
+
# **Why this is necessary:**
|
|
350
|
+
# Hash calculation (step 4) requires first_name and last_name to be non-None.
|
|
351
|
+
# However, on initial creation (e.g., via get_or_create_sensitive_meta()), these
|
|
352
|
+
# fields may be empty because real patient data hasn't been extracted yet.
|
|
353
|
+
#
|
|
354
|
+
# **Two-phase approach:**
|
|
355
|
+
# - Phase 1 (Initial): Set defaults if names are missing
|
|
356
|
+
# → Allows hash calculation to succeed without errors
|
|
357
|
+
# → Creates temporary pseudo-entities with default hash
|
|
358
|
+
#
|
|
359
|
+
# - Phase 2 (Update): Real data extraction (OCR, manual input)
|
|
360
|
+
# → update_from_dict() sets real names ("Max", "Mustermann")
|
|
361
|
+
# → save() is called again
|
|
362
|
+
# → This block is SKIPPED (names already exist)
|
|
363
|
+
# → Hash is recalculated with real data (step 4)
|
|
364
|
+
# → New pseudo-entities created with correct hash
|
|
365
|
+
#
|
|
366
|
+
# **Example:**
|
|
367
|
+
# Initial: patient_first_name = "unknown" → hash = sha256("unknown unknown...")
|
|
368
|
+
# Updated: patient_first_name = "Max" → hash = sha256("Max Mustermann...")
|
|
369
|
+
#
|
|
370
|
+
if not instance.patient_first_name:
|
|
371
|
+
instance.patient_first_name = DEFAULT_UNKNOWN
|
|
372
|
+
logger.debug(
|
|
373
|
+
"SensitiveMeta (pk=%s): Patient first name missing, set to default '%s'.",
|
|
374
|
+
instance.pk or "new",
|
|
375
|
+
DEFAULT_UNKNOWN,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
if not instance.patient_last_name:
|
|
379
|
+
instance.patient_last_name = DEFAULT_UNKNOWN
|
|
380
|
+
logger.debug(
|
|
381
|
+
"SensitiveMeta (pk=%s): Patient last name missing, set to default '%s'.",
|
|
382
|
+
instance.pk or "new",
|
|
383
|
+
DEFAULT_UNKNOWN,
|
|
384
|
+
)
|
|
385
|
+
|
|
293
386
|
# 3. Ensure Gender exists (should be set before calling save, e.g., during creation/update)
|
|
294
387
|
if not instance.patient_gender:
|
|
295
|
-
#
|
|
296
|
-
first_name = instance.patient_first_name
|
|
297
|
-
|
|
298
|
-
if not
|
|
388
|
+
# Use the now-guaranteed first_name for gender guessing
|
|
389
|
+
first_name = instance.patient_first_name
|
|
390
|
+
gender_str = guess_name_gender(first_name)
|
|
391
|
+
if not gender_str:
|
|
299
392
|
raise ValueError("Patient gender could not be determined and must be set before saving.")
|
|
300
|
-
|
|
393
|
+
# Convert string to Gender object
|
|
394
|
+
try:
|
|
395
|
+
gender_obj = Gender.objects.get(name=gender_str)
|
|
396
|
+
instance.patient_gender = gender_obj
|
|
397
|
+
except Gender.DoesNotExist:
|
|
398
|
+
raise ValueError(f"Gender '{gender_str}' not found in database.")
|
|
301
399
|
|
|
302
400
|
# 4. Calculate Hashes (depends on DOB, Exam Date, Center, Names)
|
|
401
|
+
#
|
|
402
|
+
# **IMPORTANT: Hashes are RECALCULATED on every save!**
|
|
403
|
+
# This enables the two-phase update pattern:
|
|
404
|
+
# - Initial save: Hash based on default "unknown unknown" names
|
|
405
|
+
# - Updated save: Hash based on real extracted names ("Max Mustermann")
|
|
406
|
+
#
|
|
407
|
+
# The new hash will link to different pseudo-entities, ensuring proper
|
|
408
|
+
# anonymization while maintaining referential integrity.
|
|
303
409
|
instance.patient_hash = calculate_patient_hash(instance)
|
|
304
410
|
instance.examination_hash = calculate_examination_hash(instance)
|
|
305
411
|
|
|
306
412
|
# 5. Get or Create Pseudo Patient (depends on hash, center, gender, dob)
|
|
307
413
|
# Assign directly to the FK field to avoid premature saving issues
|
|
308
|
-
pseudo_patient = get_or_create_pseudo_patient_logic(instance)
|
|
309
|
-
instance.
|
|
414
|
+
pseudo_patient, _created = get_or_create_pseudo_patient_logic(instance)
|
|
415
|
+
instance.pseudo_patient = pseudo_patient
|
|
310
416
|
|
|
311
417
|
# 6. Get or Create Pseudo Examination (depends on hashes)
|
|
312
418
|
# Assign directly to the FK field
|
|
313
|
-
pseudo_examination = get_or_create_pseudo_patient_examination_logic(instance)
|
|
314
|
-
instance.
|
|
419
|
+
pseudo_examination, _created = get_or_create_pseudo_patient_examination_logic(instance)
|
|
420
|
+
instance.pseudo_examination = pseudo_examination
|
|
315
421
|
|
|
316
422
|
# 7. Get or Create Pseudo Examiner (depends on names, center)
|
|
317
423
|
# This needs to happen *after* the main instance has a PK for M2M linking.
|
|
@@ -325,7 +431,53 @@ def perform_save_logic(instance: "SensitiveMeta") -> "Examiner":
|
|
|
325
431
|
|
|
326
432
|
|
|
327
433
|
def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str, Any]) -> "SensitiveMeta":
|
|
328
|
-
"""
|
|
434
|
+
"""
|
|
435
|
+
Create a SensitiveMeta instance from a dictionary.
|
|
436
|
+
|
|
437
|
+
**Center handling:**
|
|
438
|
+
This function accepts TWO ways to specify the center:
|
|
439
|
+
1. `center` (Center object) - Directly pass a Center instance
|
|
440
|
+
2. `center_name` (string) - Pass the center name as a string (will be resolved to Center object)
|
|
441
|
+
|
|
442
|
+
At least ONE of these must be provided.
|
|
443
|
+
|
|
444
|
+
**Example usage:**
|
|
445
|
+
```python
|
|
446
|
+
# Option 1: With Center object
|
|
447
|
+
data = {
|
|
448
|
+
"patient_first_name": "Patient",
|
|
449
|
+
"patient_last_name": "Unknown",
|
|
450
|
+
"patient_dob": date(1990, 1, 1),
|
|
451
|
+
"examination_date": date.today(),
|
|
452
|
+
"center": center_obj, # ← Center object
|
|
453
|
+
"text": text #from extraction
|
|
454
|
+
|
|
455
|
+
}
|
|
456
|
+
sm = SensitiveMeta.create_from_dict(data)
|
|
457
|
+
|
|
458
|
+
# Option 2: With center name string
|
|
459
|
+
data = {
|
|
460
|
+
"patient_first_name": "Patient",
|
|
461
|
+
"patient_last_name": "Unknown",
|
|
462
|
+
"patient_dob": date(1990, 1, 1),
|
|
463
|
+
"examination_date": date.today(),
|
|
464
|
+
"center_name": "university_hospital_wuerzburg", # ← String
|
|
465
|
+
"anonymized_text": "anonymized text"
|
|
466
|
+
}
|
|
467
|
+
sm = SensitiveMeta.create_from_dict(data)
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
cls: The SensitiveMeta class
|
|
472
|
+
data: Dictionary containing field values
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
SensitiveMeta: The created instance
|
|
476
|
+
|
|
477
|
+
Raises:
|
|
478
|
+
ValueError: If neither center nor center_name is provided
|
|
479
|
+
ValueError: If center_name does not match any Center in database
|
|
480
|
+
"""
|
|
329
481
|
|
|
330
482
|
field_names = {f.name for f in cls._meta.get_fields() if not f.is_relation or f.one_to_one or (f.many_to_one and f.related_model)}
|
|
331
483
|
selected_data = {k: v for k, v in data.items() if k in field_names}
|
|
@@ -442,6 +594,7 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
|
|
|
442
594
|
exam_date,
|
|
443
595
|
)
|
|
444
596
|
selected_data.pop("examination_date", None)
|
|
597
|
+
|
|
445
598
|
except Exception as e:
|
|
446
599
|
logger.warning(
|
|
447
600
|
"Error parsing examination_date string '%s': %s, removing from data",
|
|
@@ -450,19 +603,31 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
|
|
|
450
603
|
)
|
|
451
604
|
selected_data.pop("examination_date", None)
|
|
452
605
|
|
|
453
|
-
# Handle Center
|
|
606
|
+
# Handle Center - accept both center_name (string) and center (object)
|
|
607
|
+
from ..administration import Center
|
|
608
|
+
|
|
609
|
+
center = data.get("center") # First try direct Center object
|
|
454
610
|
center_name = data.get("center_name")
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
center
|
|
611
|
+
|
|
612
|
+
if center is not None:
|
|
613
|
+
# Center object provided directly - validate it's a Center instance
|
|
614
|
+
if not isinstance(center, Center):
|
|
615
|
+
raise ValueError(f"'center' must be a Center instance, got {type(center)}")
|
|
459
616
|
selected_data["center"] = center
|
|
460
|
-
|
|
461
|
-
|
|
617
|
+
elif center_name:
|
|
618
|
+
# center_name string provided - resolve to Center object
|
|
619
|
+
try:
|
|
620
|
+
center = Center.objects.get(name=center_name)
|
|
621
|
+
selected_data["center"] = center
|
|
622
|
+
except Center.DoesNotExist:
|
|
623
|
+
raise ValueError(f"Center with name '{center_name}' does not exist.")
|
|
624
|
+
else:
|
|
625
|
+
# Neither center nor center_name provided
|
|
626
|
+
raise ValueError("Either 'center' (Center object) or 'center_name' (string) is required in data dictionary.")
|
|
462
627
|
|
|
463
628
|
# Handle Names and Gender
|
|
464
|
-
first_name = selected_data.get("patient_first_name") or
|
|
465
|
-
last_name = selected_data.get("patient_last_name") or
|
|
629
|
+
first_name = selected_data.get("patient_first_name") or DEFAULT_UNKNOWN
|
|
630
|
+
last_name = selected_data.get("patient_last_name") or DEFAULT_UNKNOWN
|
|
466
631
|
selected_data["patient_first_name"] = first_name # Ensure defaults are set
|
|
467
632
|
selected_data["patient_last_name"] = last_name
|
|
468
633
|
|
|
@@ -478,7 +643,18 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
|
|
|
478
643
|
except Gender.DoesNotExist:
|
|
479
644
|
logger.warning(f"Gender with name '{patient_gender_input}' provided but not found. Attempting to guess or use default.")
|
|
480
645
|
# Fall through to guessing logic if provided string name is invalid
|
|
481
|
-
|
|
646
|
+
normalized = (patient_gender_input or "").lower()
|
|
647
|
+
if normalized in {"male", "female", "unknown"}:
|
|
648
|
+
gender_obj, _ = Gender.objects.get_or_create(
|
|
649
|
+
name=normalized,
|
|
650
|
+
defaults={
|
|
651
|
+
"abbreviation": normalized[:1].upper() or None,
|
|
652
|
+
"description": "Auto-created default gender entry",
|
|
653
|
+
},
|
|
654
|
+
)
|
|
655
|
+
selected_data["patient_gender"] = gender_obj
|
|
656
|
+
else:
|
|
657
|
+
patient_gender_input = None # Reset to trigger guessing
|
|
482
658
|
|
|
483
659
|
if not isinstance(selected_data.get("patient_gender"), Gender): # If not already a Gender object (e.g. was None, or string lookup failed)
|
|
484
660
|
gender_name_to_use = guess_name_gender(first_name)
|
|
@@ -488,8 +664,55 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
|
|
|
488
664
|
try:
|
|
489
665
|
selected_data["patient_gender"] = Gender.objects.get(name=gender_name_to_use)
|
|
490
666
|
except Gender.DoesNotExist:
|
|
491
|
-
|
|
492
|
-
|
|
667
|
+
gender_obj, _ = Gender.objects.get_or_create(
|
|
668
|
+
name=gender_name_to_use,
|
|
669
|
+
defaults={
|
|
670
|
+
"abbreviation": gender_name_to_use[:1].upper() or None,
|
|
671
|
+
"description": "Auto-created default gender entry",
|
|
672
|
+
},
|
|
673
|
+
)
|
|
674
|
+
selected_data["patient_gender"] = gender_obj
|
|
675
|
+
|
|
676
|
+
# Handle Text
|
|
677
|
+
selected_data["text"] = data.get("text") or DEFAULT_UNKNOWN
|
|
678
|
+
|
|
679
|
+
# --- Add missing optional fields safely ---
|
|
680
|
+
file_path = data.get("file_path")
|
|
681
|
+
if file_path:
|
|
682
|
+
selected_data["file_path"] = str(file_path)
|
|
683
|
+
logger.debug(f"Set file_path: {file_path}")
|
|
684
|
+
|
|
685
|
+
casenumber = data.get("casenumber")
|
|
686
|
+
if casenumber:
|
|
687
|
+
selected_data["casenumber"] = str(casenumber).strip()
|
|
688
|
+
logger.debug(f"Set casenumber: {casenumber}")
|
|
689
|
+
|
|
690
|
+
exam_time = data.get("examination_time")
|
|
691
|
+
if exam_time:
|
|
692
|
+
try:
|
|
693
|
+
from datetime import time as dt_time
|
|
694
|
+
|
|
695
|
+
# Accepts strings like "14:35" or full datetime
|
|
696
|
+
if isinstance(exam_time, str):
|
|
697
|
+
h, m = exam_time.strip().split(":")[:2]
|
|
698
|
+
selected_data["examination_time"] = dt_time(int(h), int(m))
|
|
699
|
+
elif isinstance(exam_time, datetime):
|
|
700
|
+
selected_data["examination_time"] = exam_time.time()
|
|
701
|
+
elif isinstance(exam_time, date):
|
|
702
|
+
# no time info — ignore
|
|
703
|
+
logger.debug(f"examination_time value {exam_time} has no time component; skipping")
|
|
704
|
+
else:
|
|
705
|
+
selected_data["examination_time"] = exam_time
|
|
706
|
+
except Exception as e:
|
|
707
|
+
logger.warning(f"Invalid examination_time '{exam_time}': {e}")
|
|
708
|
+
|
|
709
|
+
anonymized_text = data.get("anonymized_text") or data.get("anonym_text")
|
|
710
|
+
if anonymized_text:
|
|
711
|
+
if isinstance(anonymized_text, (str, bytes)):
|
|
712
|
+
selected_data["anonymized_text"] = anonymized_text.decode() if isinstance(anonymized_text, bytes) else anonymized_text
|
|
713
|
+
else:
|
|
714
|
+
selected_data["anonymized_text"] = str(anonymized_text)
|
|
715
|
+
logger.debug("Set anonymized_text (length=%d)", len(selected_data["anonymized_text"]))
|
|
493
716
|
|
|
494
717
|
# Update name DB
|
|
495
718
|
update_name_db(first_name, last_name)
|
|
@@ -504,21 +727,82 @@ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str,
|
|
|
504
727
|
|
|
505
728
|
|
|
506
729
|
def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, Any]) -> "SensitiveMeta":
|
|
507
|
-
"""
|
|
730
|
+
"""
|
|
731
|
+
Updates a SensitiveMeta instance from a dictionary of new values.
|
|
732
|
+
|
|
733
|
+
**Integration with two-phase save pattern:**
|
|
734
|
+
This function is typically called after initial SensitiveMeta creation when real
|
|
735
|
+
patient data becomes available (e.g., extracted from video OCR, PDF parsing, or
|
|
736
|
+
manual annotation).
|
|
737
|
+
|
|
738
|
+
**Example workflow:**
|
|
739
|
+
```python
|
|
740
|
+
# Phase 1: Initial creation with defaults
|
|
741
|
+
sm = SensitiveMeta.create_from_dict({"center": center})
|
|
742
|
+
# → patient_first_name = "unknown", hash = sha256("unknown...")
|
|
743
|
+
|
|
744
|
+
# Phase 2: Update with extracted data
|
|
745
|
+
extracted = {
|
|
746
|
+
"patient_first_name": "Max",
|
|
747
|
+
"patient_last_name": "Mustermann",
|
|
748
|
+
"patient_dob": date(1985, 3, 15)
|
|
749
|
+
}
|
|
750
|
+
update_sensitive_meta_from_dict(sm, extracted)
|
|
751
|
+
# → Sets: sm.patient_first_name = "Max", sm.patient_last_name = "Mustermann"
|
|
752
|
+
# → Calls: sm.save()
|
|
753
|
+
# → Triggers: perform_save_logic() again
|
|
754
|
+
# → Result: Hash recalculated with real data, new pseudo-entities created
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
**Key behaviors:**
|
|
758
|
+
- Updates instance attributes from provided dictionary
|
|
759
|
+
- Handles type conversions (date strings → date objects, gender strings → Gender objects)
|
|
760
|
+
- Tracks patient name changes to update name database
|
|
761
|
+
- Calls save() at the end, triggering full save logic including hash recalculation
|
|
762
|
+
- Default-setting in perform_save_logic() is skipped (fields already populated)
|
|
763
|
+
|
|
764
|
+
Args:
|
|
765
|
+
instance: The existing SensitiveMeta instance to update
|
|
766
|
+
data: Dictionary of field names and new values
|
|
767
|
+
|
|
768
|
+
Returns:
|
|
769
|
+
The updated SensitiveMeta instance
|
|
770
|
+
|
|
771
|
+
Raises:
|
|
772
|
+
Exception: If save fails or required conversions fail
|
|
773
|
+
"""
|
|
508
774
|
field_names = {f.name for f in instance._meta.get_fields() if not f.is_relation or f.one_to_one or (f.many_to_one and f.related_model)}
|
|
509
775
|
# Exclude FKs that should not be updated directly from dict keys (handled separately or via save logic)
|
|
510
776
|
excluded_fields = {"pseudo_patient", "pseudo_examination"}
|
|
511
777
|
selected_data = {k: v for k, v in data.items() if k in field_names and k not in excluded_fields}
|
|
512
778
|
|
|
513
|
-
# Handle potential Center update
|
|
779
|
+
# Handle potential Center update - accept both center_name (string) and center (object)
|
|
780
|
+
from ..administration import Center
|
|
781
|
+
|
|
782
|
+
center = data.get("center") # First try direct Center object
|
|
514
783
|
center_name = data.get("center_name")
|
|
515
|
-
|
|
784
|
+
|
|
785
|
+
if center is not None:
|
|
786
|
+
# Center object provided directly - validate and update
|
|
787
|
+
if isinstance(center, Center):
|
|
788
|
+
instance.center = center
|
|
789
|
+
logger.debug(f"Updated center from Center object: {center.name}")
|
|
790
|
+
else:
|
|
791
|
+
logger.warning(f"Invalid center type {type(center)}, expected Center instance. Ignoring.")
|
|
792
|
+
# Remove from selected_data to prevent override
|
|
793
|
+
selected_data.pop("center", None)
|
|
794
|
+
elif center_name:
|
|
795
|
+
# center_name string provided - resolve to Center object
|
|
516
796
|
try:
|
|
517
|
-
|
|
518
|
-
instance.center =
|
|
519
|
-
|
|
797
|
+
center_obj = Center.objects.get(name=center_name)
|
|
798
|
+
instance.center = center_obj
|
|
799
|
+
logger.debug(f"Updated center from center_name string: {center_name}")
|
|
800
|
+
except Center.DoesNotExist:
|
|
520
801
|
logger.warning(f"Center '{center_name}' not found during update. Keeping existing center.")
|
|
521
|
-
|
|
802
|
+
else:
|
|
803
|
+
# Both are None/missing - remove 'center' from selected_data to preserve existing value
|
|
804
|
+
selected_data.pop("center", None)
|
|
805
|
+
# If both are None/missing, keep existing center (no update needed)
|
|
522
806
|
|
|
523
807
|
# Set examiner names if provided, before calling save
|
|
524
808
|
examiner_first_name = data.get("examiner_first_name")
|
|
@@ -574,9 +858,34 @@ def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, A
|
|
|
574
858
|
logger.exception(f"Error handling patient_gender '{patient_gender_input}': {e}. Skipping gender update.")
|
|
575
859
|
selected_data.pop("patient_gender", None)
|
|
576
860
|
|
|
861
|
+
# TODO Review: Handle new optional fields on update
|
|
862
|
+
for key in ("file_path", "casenumber", "examination_time", "anonymized_text", "anonym_text"):
|
|
863
|
+
if key in data and data[key] is not None:
|
|
864
|
+
val = data[key]
|
|
865
|
+
if key in ("file_path", "casenumber"):
|
|
866
|
+
setattr(instance, key, str(val))
|
|
867
|
+
elif key in ("anonymized_text", "anonym_text"):
|
|
868
|
+
setattr(instance, "anonymized_text", val if isinstance(val, str) else str(val))
|
|
869
|
+
elif key == "examination_time":
|
|
870
|
+
try:
|
|
871
|
+
from datetime import time as dt_time
|
|
872
|
+
|
|
873
|
+
if isinstance(val, str) and ":" in val:
|
|
874
|
+
h, m = val.strip().split(":")[:2]
|
|
875
|
+
setattr(instance, "examination_time", dt_time(int(h), int(m)))
|
|
876
|
+
elif isinstance(val, datetime):
|
|
877
|
+
setattr(instance, "examination_time", val.time())
|
|
878
|
+
except Exception as e:
|
|
879
|
+
logger.warning(f"Skipping invalid examination_time '{val}': {e}")
|
|
880
|
+
|
|
577
881
|
# Update other attributes from selected_data
|
|
578
882
|
patient_name_changed = False
|
|
579
883
|
for k, v in selected_data.items():
|
|
884
|
+
# Skip None values to avoid overwriting existing data
|
|
885
|
+
if v is None:
|
|
886
|
+
logger.debug(f"Skipping field '{k}' during update because value is None")
|
|
887
|
+
continue
|
|
888
|
+
|
|
580
889
|
# Avoid overwriting examiner names if they were just explicitly set
|
|
581
890
|
if (
|
|
582
891
|
k not in ["examiner_first_name", "examiner_last_name"]
|
|
@@ -702,15 +1011,21 @@ def update_or_create_sensitive_meta_from_dict(
|
|
|
702
1011
|
cls: Type["SensitiveMeta"],
|
|
703
1012
|
data: Dict[str, Any],
|
|
704
1013
|
instance: Optional["SensitiveMeta"] = None,
|
|
705
|
-
)
|
|
1014
|
+
):
|
|
706
1015
|
"""Logic to update or create a SensitiveMeta instance from a dictionary."""
|
|
707
1016
|
# Check if the instance already exists based on unique fields
|
|
1017
|
+
sensitive_meta: "SensitiveMeta"
|
|
1018
|
+
_created: bool
|
|
708
1019
|
if instance:
|
|
709
1020
|
# Update the existing instance
|
|
710
|
-
|
|
1021
|
+
sensitive_meta = update_sensitive_meta_from_dict(instance, data)
|
|
1022
|
+
_created = False
|
|
1023
|
+
|
|
711
1024
|
else:
|
|
712
1025
|
# Create a new instance
|
|
713
|
-
|
|
1026
|
+
sensitive_meta = create_sensitive_meta_from_dict(cls, data)
|
|
1027
|
+
_created = True
|
|
1028
|
+
return sensitive_meta, _created
|
|
714
1029
|
|
|
715
1030
|
|
|
716
1031
|
def _map_gender_string_to_standard(gender_str: str) -> Optional[str]:
|
|
@@ -725,3 +1040,28 @@ def _map_gender_string_to_standard(gender_str: str) -> Optional[str]:
|
|
|
725
1040
|
if gender_lower in variants:
|
|
726
1041
|
return standard
|
|
727
1042
|
return None
|
|
1043
|
+
|
|
1044
|
+
def _create_anonymized_record(instance: "SensitiveMeta", DEFAULT_ANONYMIZED=None, DEFAULT_ANONYMIZED_DATE=timezone.make_aware(datetime(1900, 1, 1))) -> None:
|
|
1045
|
+
"""
|
|
1046
|
+
Create a SensitiveMeta instance with all sensitive fields set to anonymized defaults.
|
|
1047
|
+
This is only called after anonymization and will delete all data that can identify a patient from the database.
|
|
1048
|
+
What is left will only be the patient hash.
|
|
1049
|
+
|
|
1050
|
+
Args:
|
|
1051
|
+
instance: The existing SensitiveMeta instance to anonymize
|
|
1052
|
+
DEFAULT_ANONYMIZED: Usually None, The default string to use for anonymized fields (e.g., "anonymized,")
|
|
1053
|
+
"""
|
|
1054
|
+
|
|
1055
|
+
instance.refresh_from_db()
|
|
1056
|
+
instance.get_patient_hash()
|
|
1057
|
+
instance.get_patient_examination_hash()
|
|
1058
|
+
|
|
1059
|
+
anonymized_data = {
|
|
1060
|
+
"patient_first_name": DEFAULT_ANONYMIZED,
|
|
1061
|
+
"patient_last_name": DEFAULT_ANONYMIZED,
|
|
1062
|
+
"patient_dob": DEFAULT_ANONYMIZED_DATE,
|
|
1063
|
+
"examination_date": DEFAULT_ANONYMIZED_DATE,
|
|
1064
|
+
}
|
|
1065
|
+
sensitive_meta = update_sensitive_meta_from_dict(instance, anonymized_data)
|
|
1066
|
+
|
|
1067
|
+
sensitive_meta.save()
|