endoreg-db 0.8.6.1__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_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 +3 -3
- 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 +110 -182
- 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_2.py +8 -9
- endoreg_db/models/media/video/video_file.py +150 -108
- 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 +125 -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 +192 -173
- 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 +0 -2
- 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/file_overview.py +11 -99
- 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/services/anonymization.py +75 -73
- endoreg_db/services/lookup_service.py +37 -24
- endoreg_db/services/pdf_import.py +166 -68
- endoreg_db/services/storage_aware_video_processor.py +140 -114
- endoreg_db/services/video_import.py +193 -283
- endoreg_db/urls/__init__.py +7 -20
- endoreg_db/urls/media.py +108 -67
- 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 +0 -10
- 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 -152
- 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 +2 -3
- endoreg_db/views/meta/sensitive_meta_detail.py +0 -63
- endoreg_db/views/patient/patient.py +5 -4
- endoreg_db/views/pdf/pdf_stream.py +20 -21
- endoreg_db/views/pdf/reimport.py +11 -32
- endoreg_db/views/requirement/evaluate.py +188 -187
- endoreg_db/views/requirement/lookup.py +17 -3
- endoreg_db/views/requirement/requirement_utils.py +89 -0
- endoreg_db/views/video/__init__.py +0 -2
- endoreg_db/views/video/correction.py +2 -2
- {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.0.dist-info}/METADATA +7 -3
- {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.0.dist-info}/RECORD +341 -245
- endoreg_db/models/administration/permissions/__init__.py +0 -44
- 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/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.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/{models/media/video/refactor_plan.md → views/pdf/pdf_stream_views.py} +0 -0
- {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.0.dist-info}/WHEEL +0 -0
- {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,16 +1,32 @@
|
|
|
1
|
-
from django.db import models
|
|
2
|
-
from typing import TYPE_CHECKING, Dict, List, Union
|
|
3
|
-
from endoreg_db.utils.links.requirement_link import RequirementLinks
|
|
4
1
|
import logging
|
|
5
2
|
from subprocess import run
|
|
3
|
+
from typing import TYPE_CHECKING, Dict, List, Union, cast, Tuple
|
|
4
|
+
|
|
5
|
+
from django.db import models
|
|
6
|
+
|
|
7
|
+
from endoreg_db.utils.links.requirement_link import RequirementLinks
|
|
8
|
+
|
|
6
9
|
|
|
7
10
|
logger = logging.getLogger(__name__)
|
|
8
11
|
|
|
9
12
|
|
|
13
|
+
def _validate_requirement_configuration(instance: "Requirement") -> bool:
|
|
14
|
+
"""Ensures requirement fixtures declare both requirement_types and operators."""
|
|
15
|
+
if not instance.requirement_types.exists():
|
|
16
|
+
raise ValueError(
|
|
17
|
+
f"Requirement '{instance.name}' must be associated with at least one RequirementType."
|
|
18
|
+
)
|
|
19
|
+
if not instance.operators.exists():
|
|
20
|
+
raise ValueError(
|
|
21
|
+
f"Requirement '{instance.name}' must be associated with at least one RequirementOperator."
|
|
22
|
+
)
|
|
23
|
+
return True
|
|
24
|
+
|
|
25
|
+
|
|
10
26
|
QuerySet = models.QuerySet
|
|
11
27
|
|
|
12
28
|
if TYPE_CHECKING:
|
|
13
|
-
from endoreg_db.models import (
|
|
29
|
+
from endoreg_db.models import ( # RequirementSet,
|
|
14
30
|
Disease,
|
|
15
31
|
DiseaseClassificationChoice,
|
|
16
32
|
Event,
|
|
@@ -19,27 +35,27 @@ if TYPE_CHECKING:
|
|
|
19
35
|
Examination,
|
|
20
36
|
ExaminationIndication,
|
|
21
37
|
Finding,
|
|
22
|
-
FindingIntervention,
|
|
23
38
|
FindingClassification,
|
|
24
39
|
FindingClassificationChoice,
|
|
25
40
|
FindingClassificationType,
|
|
41
|
+
FindingIntervention,
|
|
42
|
+
Gender,
|
|
26
43
|
LabValue,
|
|
27
44
|
Medication,
|
|
28
45
|
MedicationIndication,
|
|
29
|
-
MedicationIntakeTime,
|
|
46
|
+
MedicationIntakeTime, # Added MedicationIntakeTime
|
|
30
47
|
MedicationSchedule,
|
|
31
48
|
PatientDisease,
|
|
32
49
|
PatientEvent,
|
|
33
50
|
PatientExamination,
|
|
34
51
|
PatientFinding,
|
|
35
|
-
PatientFindingIntervention,
|
|
36
52
|
PatientFindingClassification,
|
|
53
|
+
PatientFindingIntervention,
|
|
37
54
|
PatientLabValue,
|
|
38
|
-
PatientMedicationSchedule,
|
|
55
|
+
PatientMedicationSchedule, # Added PatientMedicationSchedule
|
|
39
56
|
RequirementOperator,
|
|
40
|
-
RequirementSet,
|
|
41
|
-
Gender
|
|
42
57
|
)
|
|
58
|
+
|
|
43
59
|
# from endoreg_db.utils.links.requirement_link import RequirementLinks # Already imported above
|
|
44
60
|
|
|
45
61
|
|
|
@@ -115,12 +131,12 @@ class Requirement(models.Model):
|
|
|
115
131
|
name = models.CharField(max_length=100, unique=True)
|
|
116
132
|
description = models.TextField(blank=True, null=True)
|
|
117
133
|
|
|
134
|
+
|
|
118
135
|
numeric_value = models.FloatField(
|
|
119
136
|
blank=True,
|
|
120
137
|
null=True,
|
|
121
138
|
help_text="Numeric value for the requirement. If not set, the requirement is not used in calculations.",
|
|
122
139
|
)
|
|
123
|
-
|
|
124
140
|
numeric_value_min = models.FloatField(
|
|
125
141
|
blank=True,
|
|
126
142
|
null=True,
|
|
@@ -131,29 +147,26 @@ class Requirement(models.Model):
|
|
|
131
147
|
null=True,
|
|
132
148
|
help_text="Maximum numeric value for the requirement. If not set, the requirement is not used in calculations.",
|
|
133
149
|
)
|
|
134
|
-
|
|
135
150
|
string_value = models.CharField(
|
|
136
151
|
max_length=100,
|
|
137
152
|
blank=True,
|
|
138
153
|
null=True,
|
|
139
154
|
help_text="String value for the requirement. If not set, the requirement is not used in calculations.",
|
|
140
155
|
)
|
|
141
|
-
|
|
142
156
|
string_values = models.TextField(
|
|
143
157
|
blank=True,
|
|
144
158
|
null=True,
|
|
145
159
|
help_text=" ','-separated list of string values for the requirement.If not set, the requirement is not used in calculations.",
|
|
146
160
|
)
|
|
147
|
-
|
|
148
161
|
objects = RequirementManager()
|
|
149
162
|
|
|
150
|
-
requirement_types = models.ManyToManyField(
|
|
163
|
+
requirement_types = models.ManyToManyField(
|
|
151
164
|
"RequirementType",
|
|
152
165
|
blank=True,
|
|
153
166
|
related_name="linked_requirements",
|
|
154
167
|
)
|
|
155
168
|
|
|
156
|
-
operators = models.ManyToManyField(
|
|
169
|
+
operators = models.ManyToManyField(
|
|
157
170
|
"RequirementOperator",
|
|
158
171
|
blank=True,
|
|
159
172
|
related_name="required_in",
|
|
@@ -167,120 +180,146 @@ class Requirement(models.Model):
|
|
|
167
180
|
null=True,
|
|
168
181
|
)
|
|
169
182
|
|
|
170
|
-
examinations = models.ManyToManyField(
|
|
183
|
+
examinations = models.ManyToManyField(
|
|
171
184
|
"Examination",
|
|
172
185
|
blank=True,
|
|
173
186
|
related_name="required_in",
|
|
174
187
|
)
|
|
175
188
|
|
|
176
|
-
examination_indications = models.ManyToManyField(
|
|
189
|
+
examination_indications = models.ManyToManyField(
|
|
177
190
|
"ExaminationIndication",
|
|
178
191
|
blank=True,
|
|
179
192
|
related_name="required_in",
|
|
180
193
|
)
|
|
181
194
|
|
|
182
|
-
diseases = models.ManyToManyField(
|
|
195
|
+
diseases = models.ManyToManyField(
|
|
183
196
|
"Disease",
|
|
184
197
|
blank=True,
|
|
185
198
|
related_name="required_in",
|
|
186
199
|
)
|
|
187
200
|
|
|
188
|
-
disease_classification_choices = models.ManyToManyField(
|
|
201
|
+
disease_classification_choices = models.ManyToManyField(
|
|
189
202
|
"DiseaseClassificationChoice",
|
|
190
203
|
blank=True,
|
|
191
204
|
related_name="required_in",
|
|
192
205
|
)
|
|
193
206
|
|
|
194
|
-
events = models.ManyToManyField(
|
|
207
|
+
events = models.ManyToManyField(
|
|
195
208
|
"Event",
|
|
196
209
|
blank=True,
|
|
197
210
|
related_name="required_in",
|
|
198
211
|
)
|
|
199
212
|
|
|
200
|
-
lab_values = models.ManyToManyField(
|
|
213
|
+
lab_values = models.ManyToManyField(
|
|
201
214
|
"LabValue",
|
|
202
215
|
blank=True,
|
|
203
216
|
related_name="required_in",
|
|
204
217
|
)
|
|
205
218
|
|
|
206
|
-
findings = models.ManyToManyField(
|
|
219
|
+
findings = models.ManyToManyField(
|
|
207
220
|
"Finding",
|
|
208
221
|
blank=True,
|
|
209
222
|
related_name="required_in",
|
|
210
223
|
)
|
|
211
224
|
|
|
212
|
-
finding_classifications = models.ManyToManyField(
|
|
225
|
+
finding_classifications = models.ManyToManyField(
|
|
213
226
|
"FindingClassification",
|
|
214
227
|
blank=True,
|
|
215
228
|
related_name="required_in",
|
|
216
229
|
)
|
|
217
230
|
|
|
218
|
-
finding_classification_choices = models.ManyToManyField(
|
|
231
|
+
finding_classification_choices = models.ManyToManyField(
|
|
219
232
|
"FindingClassificationChoice",
|
|
220
233
|
blank=True,
|
|
221
234
|
related_name="required_in",
|
|
222
235
|
)
|
|
223
236
|
|
|
224
|
-
finding_interventions = models.ManyToManyField(
|
|
237
|
+
finding_interventions = models.ManyToManyField(
|
|
225
238
|
"FindingIntervention",
|
|
226
239
|
blank=True,
|
|
227
240
|
related_name="required_in",
|
|
228
241
|
)
|
|
229
242
|
|
|
230
|
-
medications = models.ManyToManyField(
|
|
243
|
+
medications = models.ManyToManyField(
|
|
231
244
|
"Medication",
|
|
232
245
|
blank=True,
|
|
233
246
|
related_name="required_in",
|
|
234
247
|
)
|
|
235
248
|
|
|
236
|
-
medication_indications = models.ManyToManyField(
|
|
249
|
+
medication_indications = models.ManyToManyField(
|
|
237
250
|
"MedicationIndication",
|
|
238
251
|
blank=True,
|
|
239
252
|
related_name="required_in",
|
|
240
253
|
)
|
|
241
254
|
|
|
242
|
-
medication_intake_times = models.ManyToManyField(
|
|
255
|
+
medication_intake_times = models.ManyToManyField(
|
|
243
256
|
"MedicationIntakeTime",
|
|
244
257
|
blank=True,
|
|
245
258
|
related_name="required_in",
|
|
246
259
|
)
|
|
247
260
|
|
|
248
|
-
medication_schedules = models.ManyToManyField(
|
|
261
|
+
medication_schedules = models.ManyToManyField(
|
|
249
262
|
"MedicationSchedule",
|
|
250
263
|
blank=True,
|
|
251
264
|
related_name="required_in",
|
|
252
265
|
)
|
|
253
266
|
|
|
254
|
-
genders = models.ManyToManyField(
|
|
267
|
+
genders = models.ManyToManyField(
|
|
255
268
|
"Gender",
|
|
256
269
|
blank=True,
|
|
257
270
|
related_name="required_in",
|
|
258
271
|
)
|
|
259
272
|
|
|
260
273
|
if TYPE_CHECKING:
|
|
261
|
-
requirement_types
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
274
|
+
requirement_types = cast(
|
|
275
|
+
models.manager.RelatedManager["RequirementType"], requirement_types
|
|
276
|
+
)
|
|
277
|
+
operators = cast(
|
|
278
|
+
models.manager.RelatedManager["RequirementOperator"], operators
|
|
279
|
+
)
|
|
280
|
+
# requirement_sets = cast(models.manager.RelatedManager["RequirementSet"], requirement_sets)
|
|
281
|
+
examinations = cast(models.manager.RelatedManager["Examination"], examinations)
|
|
282
|
+
examination_indications = cast(
|
|
283
|
+
models.manager.RelatedManager["ExaminationIndication"],
|
|
284
|
+
examination_indications,
|
|
285
|
+
)
|
|
286
|
+
lab_values = cast(models.manager.RelatedManager["LabValue"], lab_values)
|
|
287
|
+
diseases = cast(models.manager.RelatedManager["Disease"], diseases)
|
|
288
|
+
disease_classification_choices = cast(
|
|
289
|
+
models.manager.RelatedManager["DiseaseClassificationChoice"],
|
|
290
|
+
disease_classification_choices,
|
|
291
|
+
)
|
|
292
|
+
events = cast(models.manager.RelatedManager["Event"], events)
|
|
293
|
+
findings = cast(models.manager.RelatedManager["Finding"], findings)
|
|
294
|
+
finding_classifications = cast(
|
|
295
|
+
models.manager.RelatedManager["FindingClassification"],
|
|
296
|
+
finding_classifications,
|
|
297
|
+
)
|
|
298
|
+
finding_classification_choices = cast(
|
|
299
|
+
models.manager.RelatedManager["FindingClassificationChoice"],
|
|
300
|
+
finding_classification_choices,
|
|
301
|
+
)
|
|
302
|
+
finding_interventions = cast(
|
|
303
|
+
models.manager.RelatedManager["FindingIntervention"], finding_interventions
|
|
304
|
+
)
|
|
305
|
+
medications = cast(models.manager.RelatedManager["Medication"], medications)
|
|
306
|
+
medication_indications = cast(
|
|
307
|
+
models.manager.RelatedManager["MedicationIndication"],
|
|
308
|
+
medication_indications,
|
|
309
|
+
)
|
|
310
|
+
medication_intake_times = cast(
|
|
311
|
+
models.manager.RelatedManager["MedicationIntakeTime"],
|
|
312
|
+
medication_intake_times,
|
|
313
|
+
)
|
|
314
|
+
medication_schedules = cast(
|
|
315
|
+
models.manager.RelatedManager["MedicationSchedule"], medication_schedules
|
|
316
|
+
)
|
|
317
|
+
genders = cast(models.manager.RelatedManager["Gender"], genders)
|
|
279
318
|
|
|
280
319
|
def natural_key(self):
|
|
281
320
|
"""
|
|
282
321
|
Returns a tuple containing the instance's name as its natural key.
|
|
283
|
-
|
|
322
|
+
|
|
284
323
|
This tuple provides a unique identifier for serialization purposes.
|
|
285
324
|
"""
|
|
286
325
|
return (self.name,)
|
|
@@ -289,36 +328,47 @@ class Requirement(models.Model):
|
|
|
289
328
|
"""Returns the name of the requirement as its string representation."""
|
|
290
329
|
return str(self.name)
|
|
291
330
|
|
|
331
|
+
# override save method to add a validation step; requirements need at least one operator and at least one requirement type
|
|
332
|
+
# def save(self, *args, **kwargs):
|
|
333
|
+
# _valid = _validate_requirement_configuration(
|
|
334
|
+
# self,
|
|
335
|
+
# )
|
|
336
|
+
# super().save(*args, **kwargs)
|
|
337
|
+
|
|
292
338
|
@property
|
|
293
|
-
def expected_models(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
339
|
+
def expected_models(
|
|
340
|
+
self,
|
|
341
|
+
) -> List[
|
|
342
|
+
Union[
|
|
343
|
+
"Disease",
|
|
344
|
+
"DiseaseClassificationChoice",
|
|
345
|
+
"Event",
|
|
346
|
+
"EventClassification",
|
|
347
|
+
"EventClassificationChoice",
|
|
348
|
+
"Examination",
|
|
349
|
+
"ExaminationIndication",
|
|
350
|
+
"Finding",
|
|
351
|
+
"FindingIntervention",
|
|
352
|
+
"FindingClassification",
|
|
353
|
+
"FindingClassificationChoice",
|
|
354
|
+
"FindingClassificationType",
|
|
355
|
+
"LabValue",
|
|
356
|
+
"Medication",
|
|
357
|
+
"MedicationIndication",
|
|
358
|
+
"MedicationIntakeTime", # Added MedicationIntakeTime
|
|
359
|
+
"PatientDisease",
|
|
360
|
+
"PatientEvent",
|
|
361
|
+
"PatientExamination",
|
|
362
|
+
"PatientFinding",
|
|
363
|
+
"PatientFindingIntervention",
|
|
364
|
+
"PatientFindingClassification",
|
|
365
|
+
"PatientLabValue",
|
|
366
|
+
"PatientMedicationSchedule", # Added PatientMedicationSchedule
|
|
367
|
+
]
|
|
368
|
+
]:
|
|
319
369
|
"""
|
|
320
370
|
Return the list of model classes that are expected as input for evaluating this requirement.
|
|
321
|
-
|
|
371
|
+
|
|
322
372
|
The returned models correspond to the requirement types linked to this requirement, mapped via the internal data model dictionary.
|
|
323
373
|
"""
|
|
324
374
|
req_types = self.requirement_types.all()
|
|
@@ -332,66 +382,141 @@ class Requirement(models.Model):
|
|
|
332
382
|
def links(self) -> "RequirementLinks":
|
|
333
383
|
"""
|
|
334
384
|
Return a RequirementLinks object containing all non-null related model instances for this requirement.
|
|
335
|
-
|
|
385
|
+
|
|
336
386
|
The returned object provides structured access to all associated entities, such as examinations, diseases, findings, classifications, interventions, medications, and related choices, aggregated from the requirement's many-to-many fields.
|
|
337
387
|
"""
|
|
338
388
|
# requirement_sets is not part of RequirementLinks (avoids circular import); collect other related models
|
|
339
389
|
models_dict = RequirementLinks(
|
|
340
390
|
examinations=[_ for _ in self.examinations.all() if _ is not None],
|
|
341
|
-
examination_indications=[
|
|
391
|
+
examination_indications=[
|
|
392
|
+
_ for _ in self.examination_indications.all() if _ is not None
|
|
393
|
+
],
|
|
342
394
|
lab_values=[_ for _ in self.lab_values.all() if _ is not None],
|
|
343
395
|
diseases=[_ for _ in self.diseases.all() if _ is not None],
|
|
344
|
-
disease_classification_choices=[
|
|
396
|
+
disease_classification_choices=[
|
|
397
|
+
_ for _ in self.disease_classification_choices.all() if _ is not None
|
|
398
|
+
],
|
|
345
399
|
events=[_ for _ in self.events.all() if _ is not None],
|
|
346
400
|
findings=[_ for _ in self.findings.all() if _ is not None],
|
|
347
|
-
finding_classifications=[
|
|
348
|
-
|
|
349
|
-
|
|
401
|
+
finding_classifications=[
|
|
402
|
+
_ for _ in self.finding_classifications.all() if _ is not None
|
|
403
|
+
],
|
|
404
|
+
finding_classification_choices=[
|
|
405
|
+
_ for _ in self.finding_classification_choices.all() if _ is not None
|
|
406
|
+
],
|
|
407
|
+
finding_interventions=[
|
|
408
|
+
_ for _ in self.finding_interventions.all() if _ is not None
|
|
409
|
+
],
|
|
350
410
|
medications=[_ for _ in self.medications.all() if _ is not None],
|
|
351
|
-
medication_indications=[
|
|
352
|
-
|
|
411
|
+
medication_indications=[
|
|
412
|
+
_ for _ in self.medication_indications.all() if _ is not None
|
|
413
|
+
],
|
|
414
|
+
medication_intake_times=[
|
|
415
|
+
_ for _ in self.medication_intake_times.all() if _ is not None
|
|
416
|
+
],
|
|
353
417
|
)
|
|
354
418
|
return models_dict
|
|
355
|
-
|
|
419
|
+
|
|
356
420
|
@property
|
|
357
421
|
def data_model_dict(self) -> dict:
|
|
358
422
|
"""
|
|
359
423
|
Provides a mapping from requirement type names to their corresponding model classes.
|
|
360
|
-
|
|
424
|
+
|
|
361
425
|
Returns:
|
|
362
426
|
A dictionary where keys are requirement type names and values are model classes used for requirement evaluation.
|
|
363
427
|
"""
|
|
364
428
|
from .requirement_evaluation.requirement_type_parser import data_model_dict
|
|
429
|
+
|
|
365
430
|
return data_model_dict
|
|
366
|
-
|
|
431
|
+
|
|
367
432
|
@property
|
|
368
433
|
def active_links(self) -> Dict[str, List]:
|
|
369
434
|
"""Returns a dictionary of linked models containing only non-empty entries.
|
|
370
|
-
|
|
435
|
+
|
|
371
436
|
The returned dictionary includes only those related model lists that have at least one linked instance.
|
|
372
437
|
"""
|
|
373
438
|
return self.links.active()
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
439
|
+
|
|
440
|
+
def _parse_string_values(self) -> Dict[str, str]:
|
|
441
|
+
"""Parses the optional ``string_values`` field into a dictionary.
|
|
442
|
+
|
|
443
|
+
Values follow a simple ``key=value`` syntax separated by commas. Entries
|
|
444
|
+
without an equals sign are treated as boolean flags (value ``"true"``).
|
|
445
|
+
"""
|
|
446
|
+
if not self.string_values:
|
|
447
|
+
return {}
|
|
448
|
+
|
|
449
|
+
parsed: Dict[str, str] = {}
|
|
450
|
+
for raw_entry in self.string_values.split(","):
|
|
451
|
+
entry = raw_entry.strip()
|
|
452
|
+
if not entry:
|
|
453
|
+
continue
|
|
454
|
+
if "=" in entry:
|
|
455
|
+
key, value = entry.split("=", 1)
|
|
456
|
+
parsed[key.strip()] = value.strip()
|
|
457
|
+
else:
|
|
458
|
+
parsed[entry] = "true"
|
|
459
|
+
return parsed
|
|
460
|
+
|
|
461
|
+
def _resolve_queryset_config(self, kwargs: Dict) -> tuple[str, int | None]:
|
|
462
|
+
"""Derives queryset evaluation settings from kwargs or ``string_values``.
|
|
463
|
+
|
|
464
|
+
Supported modes:
|
|
465
|
+
- ``any`` (default): at least one item may satisfy the requirement.
|
|
466
|
+
- ``all``: every item in the queryset must satisfy the requirement.
|
|
467
|
+
- ``min_count``/``at_least``/``min``: at least *n* items must satisfy.
|
|
468
|
+
"""
|
|
469
|
+
settings = self._parse_string_values()
|
|
470
|
+
|
|
471
|
+
mode_raw = kwargs.get("queryset_mode") or settings.get("qs_mode") or "any"
|
|
472
|
+
mode = str(mode_raw).strip().lower()
|
|
473
|
+
mode_aliases = {
|
|
474
|
+
"min": "min_count",
|
|
475
|
+
"at_least": "min_count",
|
|
476
|
+
"minimum": "min_count",
|
|
477
|
+
}
|
|
478
|
+
mode = mode_aliases.get(mode, mode)
|
|
479
|
+
if mode not in {"any", "all", "min_count"}:
|
|
480
|
+
mode = "any"
|
|
481
|
+
|
|
482
|
+
min_count_raw = kwargs.get("queryset_min_count")
|
|
483
|
+
if min_count_raw is None:
|
|
484
|
+
for candidate_key in ("qs_min_count", "qs_min", "qs_count", "qs_required"):
|
|
485
|
+
if candidate_key in settings:
|
|
486
|
+
min_count_raw = settings[candidate_key]
|
|
487
|
+
break
|
|
488
|
+
|
|
489
|
+
try:
|
|
490
|
+
min_count = int(min_count_raw) if min_count_raw is not None else None
|
|
491
|
+
except (TypeError, ValueError):
|
|
492
|
+
min_count = None
|
|
493
|
+
|
|
494
|
+
return mode, min_count
|
|
495
|
+
|
|
496
|
+
def evaluate(self, *args, mode: str, **kwargs):
|
|
377
497
|
"""
|
|
378
498
|
Evaluates whether the requirement is satisfied for the given input models using linked operators and gender constraints.
|
|
379
|
-
|
|
499
|
+
|
|
380
500
|
Args:
|
|
381
501
|
*args: Instances or QuerySets of expected model classes to be evaluated. Each must have a `.links` property returning a `RequirementLinks` object.
|
|
382
502
|
mode: Evaluation mode; "strict" requires all operators to pass, "loose" requires any operator to pass.
|
|
383
503
|
**kwargs: Additional keyword arguments passed to operator evaluations.
|
|
384
|
-
|
|
504
|
+
|
|
385
505
|
Returns:
|
|
386
506
|
True if the requirement is satisfied according to the specified mode, linked operators, and gender restrictions; otherwise, False.
|
|
387
|
-
|
|
507
|
+
|
|
388
508
|
Raises:
|
|
389
509
|
ValueError: If an invalid mode is provided.
|
|
390
510
|
TypeError: If an input is not an instance or QuerySet of expected models, or lacks a valid `.links` attribute.
|
|
391
|
-
|
|
511
|
+
|
|
392
512
|
If the requirement specifies genders, only input containing a patient with a matching gender will be considered valid for evaluation.
|
|
393
513
|
"""
|
|
394
|
-
|
|
514
|
+
|
|
515
|
+
try:
|
|
516
|
+
_validate_requirement_configuration(self)
|
|
517
|
+
except Exception as e:
|
|
518
|
+
logger.warning(str(e))
|
|
519
|
+
# TODO Review, Optimize or remove
|
|
395
520
|
if mode not in ["strict", "loose"]:
|
|
396
521
|
raise ValueError(f"Invalid mode: {mode}. Use 'strict' or 'loose'.")
|
|
397
522
|
|
|
@@ -400,6 +525,11 @@ class Requirement(models.Model):
|
|
|
400
525
|
requirement_req_links = self.links
|
|
401
526
|
expected_models = self.expected_models
|
|
402
527
|
|
|
528
|
+
operators = list(self.operators.all())
|
|
529
|
+
has_operators = bool(operators)
|
|
530
|
+
requirement_has_conditions = bool(requirement_req_links.active())
|
|
531
|
+
queryset_mode, queryset_min_count = self._resolve_queryset_config(kwargs)
|
|
532
|
+
|
|
403
533
|
# helpers to avoid passing a complex tuple to isinstance/issubclass which confuses type checkers
|
|
404
534
|
def _is_expected_instance(obj) -> bool:
|
|
405
535
|
for cls in expected_models:
|
|
@@ -413,7 +543,7 @@ class Requirement(models.Model):
|
|
|
413
543
|
return False
|
|
414
544
|
|
|
415
545
|
def _is_queryset_of_expected(qs) -> bool:
|
|
416
|
-
if not isinstance(qs, models.QuerySet) or not hasattr(qs,
|
|
546
|
+
if not isinstance(qs, models.QuerySet) or not hasattr(qs, "model"):
|
|
417
547
|
return False
|
|
418
548
|
for cls in expected_models:
|
|
419
549
|
if isinstance(cls, type):
|
|
@@ -433,76 +563,122 @@ class Requirement(models.Model):
|
|
|
433
563
|
if not _is_expected_instance(_input):
|
|
434
564
|
# Allow QuerySets of expected models
|
|
435
565
|
if _is_queryset_of_expected(_input):
|
|
436
|
-
|
|
437
|
-
|
|
566
|
+
if not _input.exists():
|
|
567
|
+
# Empty queryset: enforce stricter modes immediately
|
|
568
|
+
if queryset_mode == "all":
|
|
569
|
+
return False
|
|
570
|
+
if queryset_mode == "min_count":
|
|
571
|
+
required = (
|
|
572
|
+
queryset_min_count
|
|
573
|
+
if queryset_min_count is not None
|
|
574
|
+
else 1
|
|
575
|
+
)
|
|
576
|
+
if required > 0:
|
|
577
|
+
return False
|
|
438
578
|
continue
|
|
439
|
-
|
|
440
|
-
queryset_results = []
|
|
579
|
+
|
|
580
|
+
queryset_results: List[bool] = []
|
|
581
|
+
queryset_true_count = 0
|
|
582
|
+
queryset_item_count = 0
|
|
583
|
+
|
|
441
584
|
for item in _input:
|
|
442
|
-
if not hasattr(item,
|
|
585
|
+
if not hasattr(item, "links") or not isinstance(
|
|
586
|
+
item.links, RequirementLinks
|
|
587
|
+
):
|
|
443
588
|
raise TypeError(
|
|
444
589
|
f"Item {item} of type {type(item)} in QuerySet does not have a valid .links attribute of type RequirementLinks."
|
|
445
590
|
)
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
item_input_links = RequirementLinks(**
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
591
|
+
|
|
592
|
+
item_active_links = item.links.active()
|
|
593
|
+
item_input_links = RequirementLinks(**item_active_links)
|
|
594
|
+
|
|
595
|
+
for link_key, link_list in item_active_links.items():
|
|
596
|
+
if link_key not in aggregated_input_links_data:
|
|
597
|
+
aggregated_input_links_data[link_key] = []
|
|
598
|
+
aggregated_input_links_data[link_key].extend(link_list)
|
|
599
|
+
|
|
600
|
+
per_item_args = tuple(
|
|
601
|
+
item if arg is _input else arg for arg in args
|
|
602
|
+
)
|
|
603
|
+
op_kwargs = kwargs.copy()
|
|
604
|
+
op_kwargs["requirement"] = self
|
|
605
|
+
op_kwargs["original_input_args"] = per_item_args
|
|
606
|
+
|
|
607
|
+
if has_operators:
|
|
608
|
+
item_operator_results: List[bool] = []
|
|
609
|
+
for operator in operators:
|
|
610
|
+
try:
|
|
611
|
+
operator_result = operator.evaluate(
|
|
612
|
+
requirement_links=requirement_req_links,
|
|
613
|
+
input_links=item_input_links,
|
|
614
|
+
**op_kwargs,
|
|
615
|
+
)
|
|
616
|
+
item_operator_results.append(operator_result)
|
|
617
|
+
except Exception as exc:
|
|
618
|
+
logger.debug(
|
|
619
|
+
f"Operator {operator.name} evaluation failed for item {item}: {exc}"
|
|
620
|
+
)
|
|
621
|
+
item_operator_results.append(False)
|
|
622
|
+
item_result = (
|
|
623
|
+
evaluate_result_list_func(item_operator_results)
|
|
624
|
+
if item_operator_results
|
|
625
|
+
else True
|
|
626
|
+
)
|
|
627
|
+
else:
|
|
628
|
+
item_result = not requirement_has_conditions
|
|
629
|
+
|
|
468
630
|
queryset_results.append(item_result)
|
|
631
|
+
if item_result:
|
|
632
|
+
queryset_true_count += 1
|
|
633
|
+
queryset_item_count += 1
|
|
469
634
|
processed_inputs_count += 1
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
635
|
+
|
|
636
|
+
if queryset_mode == "all":
|
|
637
|
+
if queryset_item_count == 0 or not all(queryset_results):
|
|
638
|
+
return False
|
|
639
|
+
elif queryset_mode == "min_count":
|
|
640
|
+
required = (
|
|
641
|
+
queryset_min_count if queryset_min_count is not None else 1
|
|
642
|
+
)
|
|
643
|
+
if queryset_true_count < max(required, 0):
|
|
644
|
+
return False
|
|
645
|
+
# queryset_mode == "any" imposes no extra constraint here
|
|
646
|
+
continue # Move to the next arg after processing queryset
|
|
475
647
|
else:
|
|
476
648
|
raise TypeError(
|
|
477
|
-
f"Input type {type(_input)} is not among expected models: {self.expected_models} "
|
|
478
|
-
f"nor a QuerySet of expected models."
|
|
649
|
+
f"Input type {type(_input)} is not among expected models: {self.expected_models} nor a QuerySet of expected models."
|
|
479
650
|
)
|
|
480
651
|
|
|
481
652
|
# Process single model instance
|
|
482
|
-
if not hasattr(_input,
|
|
653
|
+
if not hasattr(_input, "links") or not isinstance(
|
|
654
|
+
_input.links, RequirementLinks
|
|
655
|
+
):
|
|
483
656
|
raise TypeError(
|
|
484
657
|
f"Input {_input} of type {type(_input)} does not have a valid .links attribute of type RequirementLinks."
|
|
485
658
|
)
|
|
486
|
-
|
|
487
|
-
active_input_links = _input.links.active()
|
|
659
|
+
|
|
660
|
+
active_input_links = _input.links.active() # Get dict of non-empty lists
|
|
488
661
|
for link_key, link_list in active_input_links.items():
|
|
489
662
|
if link_key not in aggregated_input_links_data:
|
|
490
663
|
aggregated_input_links_data[link_key] = []
|
|
491
664
|
aggregated_input_links_data[link_key].extend(link_list)
|
|
492
665
|
processed_inputs_count += 1
|
|
493
666
|
|
|
494
|
-
if
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
667
|
+
if (
|
|
668
|
+
not processed_inputs_count and args
|
|
669
|
+
): # If args were provided but none were processable (e.g. all empty querysets)
|
|
670
|
+
# This situation implies no relevant data was provided for evaluation against the requirement.
|
|
671
|
+
# Depending on operator logic (e.g., "requires at least one matching item"), this might lead to False.
|
|
672
|
+
# For "models_match_any", an empty input_links will likely result in False if requirement_req_links is not empty.
|
|
673
|
+
pass
|
|
500
674
|
|
|
501
675
|
# Deduplicate items within each list after aggregation
|
|
502
676
|
for key in aggregated_input_links_data:
|
|
503
677
|
try:
|
|
504
678
|
# Using dict.fromkeys to preserve order and remove duplicates for hashable items
|
|
505
|
-
aggregated_input_links_data[key] = list(
|
|
679
|
+
aggregated_input_links_data[key] = list(
|
|
680
|
+
dict.fromkeys(aggregated_input_links_data[key])
|
|
681
|
+
)
|
|
506
682
|
except TypeError:
|
|
507
683
|
# Fallback for non-hashable items (though Django models are hashable)
|
|
508
684
|
temp_list = []
|
|
@@ -510,14 +686,15 @@ class Requirement(models.Model):
|
|
|
510
686
|
if item not in temp_list:
|
|
511
687
|
temp_list.append(item)
|
|
512
688
|
aggregated_input_links_data[key] = temp_list
|
|
513
|
-
|
|
689
|
+
|
|
514
690
|
final_input_links = RequirementLinks(**aggregated_input_links_data)
|
|
515
|
-
|
|
691
|
+
|
|
516
692
|
# Gender strict check: if this requirement has genders, only pass if patient.gender is in the set
|
|
517
693
|
genders_exist = self.genders.exists()
|
|
518
694
|
if genders_exist:
|
|
519
695
|
# Import here to avoid circular import
|
|
520
696
|
from endoreg_db.models.administration.person.patient import Patient
|
|
697
|
+
|
|
521
698
|
patient = None
|
|
522
699
|
for arg in args:
|
|
523
700
|
if isinstance(arg, Patient):
|
|
@@ -528,62 +705,81 @@ class Requirement(models.Model):
|
|
|
528
705
|
if not self.genders.filter(pk=patient.gender.pk).exists():
|
|
529
706
|
return False
|
|
530
707
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
#
|
|
537
|
-
# This behavior might need further refinement based on business logic.
|
|
538
|
-
if not requirement_req_links.active(): # No conditions in requirement
|
|
539
|
-
return True # Vacuously true if requirement itself is empty
|
|
540
|
-
return False # Cannot be satisfied if requirement has conditions but no operators to check them
|
|
541
|
-
|
|
708
|
+
if (
|
|
709
|
+
not has_operators
|
|
710
|
+
): # If a requirement has no operators, its evaluation is ambiguous.
|
|
711
|
+
if not requirement_has_conditions: # No conditions in requirement
|
|
712
|
+
return True # Vacuously true if requirement itself is empty
|
|
713
|
+
return False # Cannot be satisfied if requirement has conditions but no operators to check them
|
|
542
714
|
|
|
543
715
|
operator_results = []
|
|
544
716
|
for operator in operators:
|
|
545
717
|
# Prepare kwargs for the operator, including the current Requirement instance
|
|
546
|
-
op_kwargs =
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
718
|
+
op_kwargs = (
|
|
719
|
+
kwargs.copy()
|
|
720
|
+
) # Start with kwargs passed to Requirement.evaluate
|
|
721
|
+
op_kwargs["requirement"] = self # Add the Requirement instance itself
|
|
722
|
+
op_kwargs["original_input_args"] = (
|
|
723
|
+
args # Add the original input arguments for operators that need them (e.g., age operators)
|
|
724
|
+
)
|
|
725
|
+
operator_results.append(
|
|
726
|
+
operator.evaluate(
|
|
727
|
+
requirement_links=requirement_req_links,
|
|
728
|
+
input_links=final_input_links,
|
|
729
|
+
**op_kwargs,
|
|
730
|
+
)
|
|
731
|
+
)
|
|
554
732
|
|
|
555
733
|
is_valid = evaluate_result_list_func(operator_results)
|
|
556
734
|
|
|
557
735
|
return is_valid
|
|
558
736
|
|
|
559
|
-
|
|
737
|
+
|
|
738
|
+
def evaluate_with_details(self, *args, mode: str, **kwargs) -> Tuple[bool, str]:
|
|
560
739
|
"""
|
|
561
740
|
Evaluates whether the requirement is satisfied for the given input models using linked operators and gender constraints.
|
|
562
|
-
|
|
741
|
+
|
|
563
742
|
Args:
|
|
564
743
|
*args: Instances or QuerySets of expected model classes to be evaluated. Each must have a `.links` property returning a `RequirementLinks` object.
|
|
565
744
|
mode: Evaluation mode; "strict" requires all operators to pass, "loose" requires any operator to pass.
|
|
566
745
|
**kwargs: Additional keyword arguments passed to operator evaluations.
|
|
567
|
-
|
|
746
|
+
|
|
568
747
|
Returns:
|
|
569
|
-
|
|
570
|
-
|
|
748
|
+
(met, details):
|
|
749
|
+
met: True/False, ob die Voraussetzung erfüllt ist
|
|
750
|
+
details: menschenlesbare Erklärung (für UI geeignet)
|
|
751
|
+
|
|
571
752
|
Raises:
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
If the requirement specifies genders, only input containing a patient with a matching gender will be considered valid for evaluation.
|
|
753
|
+
RequirementEvaluationError:
|
|
754
|
+
- bei ungültigem Modus
|
|
755
|
+
- bei komplett falschen Input-Typen / fehlender .links-Struktur
|
|
576
756
|
"""
|
|
577
|
-
|
|
757
|
+
from endoreg_db.models.requirement.requirement_error import RequirementEvaluationError
|
|
758
|
+
|
|
759
|
+
# --- Mode validieren -------------------------------------------------
|
|
578
760
|
if mode not in ["strict", "loose"]:
|
|
579
|
-
raise
|
|
761
|
+
raise RequirementEvaluationError(
|
|
762
|
+
requirement=self,
|
|
763
|
+
code="INVALID_MODE",
|
|
764
|
+
technical_message=f"Invalid mode: {mode}. Use 'strict' or 'loose'.",
|
|
765
|
+
user_message=(
|
|
766
|
+
"Diese Voraussetzung ist intern mit einem ungültigen "
|
|
767
|
+
"Bewertungsmodus konfiguriert und kann aktuell nicht "
|
|
768
|
+
"korrekt geprüft werden."
|
|
769
|
+
),
|
|
770
|
+
)
|
|
580
771
|
|
|
581
772
|
evaluate_result_list_func = all if mode == "strict" else any
|
|
582
773
|
|
|
583
774
|
requirement_req_links = self.links
|
|
584
775
|
expected_models = self.expected_models
|
|
585
776
|
|
|
586
|
-
|
|
777
|
+
operators = list(self.operators.all())
|
|
778
|
+
has_operators = bool(operators)
|
|
779
|
+
requirement_has_conditions = bool(requirement_req_links.active())
|
|
780
|
+
queryset_mode, queryset_min_count = self._resolve_queryset_config(kwargs)
|
|
781
|
+
|
|
782
|
+
# --- Helper für Typprüfung ------------------------------------------
|
|
587
783
|
def _is_expected_instance(obj) -> bool:
|
|
588
784
|
for cls in expected_models:
|
|
589
785
|
if isinstance(cls, type):
|
|
@@ -591,12 +787,12 @@ class Requirement(models.Model):
|
|
|
591
787
|
if isinstance(obj, cls):
|
|
592
788
|
return True
|
|
593
789
|
except Exception:
|
|
594
|
-
# cls might
|
|
790
|
+
# cls might nicht runtime-kompatibel sein
|
|
595
791
|
continue
|
|
596
792
|
return False
|
|
597
793
|
|
|
598
794
|
def _is_queryset_of_expected(qs) -> bool:
|
|
599
|
-
if not isinstance(qs, models.QuerySet) or not hasattr(qs,
|
|
795
|
+
if not isinstance(qs, models.QuerySet) or not hasattr(qs, "model"):
|
|
600
796
|
return False
|
|
601
797
|
for cls in expected_models:
|
|
602
798
|
if isinstance(cls, type):
|
|
@@ -607,161 +803,273 @@ class Requirement(models.Model):
|
|
|
607
803
|
continue
|
|
608
804
|
return False
|
|
609
805
|
|
|
610
|
-
#
|
|
611
|
-
aggregated_input_links_data = {}
|
|
806
|
+
# --- RequirementLinks aus allen Inputs aggregieren -------------------
|
|
807
|
+
aggregated_input_links_data: dict = {}
|
|
612
808
|
processed_inputs_count = 0
|
|
613
809
|
|
|
614
810
|
for _input in args:
|
|
615
|
-
# Check if the input is an instance of any of the expected model types
|
|
616
811
|
if not _is_expected_instance(_input):
|
|
617
|
-
#
|
|
812
|
+
# QuerySet von erwarteten Typen erlauben
|
|
618
813
|
if _is_queryset_of_expected(_input):
|
|
619
|
-
|
|
620
|
-
|
|
814
|
+
if not _input.exists():
|
|
815
|
+
# leeres QS -> je nach QS-Mode sofort nicht erfüllt
|
|
816
|
+
if queryset_mode == "all":
|
|
817
|
+
return (
|
|
818
|
+
False,
|
|
819
|
+
"Für diese Voraussetzung müssen alle passenden Einträge vorliegen, "
|
|
820
|
+
"aber es wurden keine entsprechenden Datensätze gefunden.",
|
|
821
|
+
)
|
|
822
|
+
if queryset_mode == "min_count":
|
|
823
|
+
required = (
|
|
824
|
+
queryset_min_count
|
|
825
|
+
if queryset_min_count is not None
|
|
826
|
+
else 1
|
|
827
|
+
)
|
|
828
|
+
if required > 0:
|
|
829
|
+
return (
|
|
830
|
+
False,
|
|
831
|
+
f"Für diese Voraussetzung werden mindestens {required} passende "
|
|
832
|
+
"Einträge benötigt, es wurden jedoch keine gefunden.",
|
|
833
|
+
)
|
|
834
|
+
# queryset_mode == "any" bei leerem QS -> neutral (keine zusätzliche Einschränkung)
|
|
621
835
|
continue
|
|
622
|
-
|
|
623
|
-
queryset_results = []
|
|
836
|
+
|
|
837
|
+
queryset_results: List[bool] = []
|
|
838
|
+
queryset_true_count = 0
|
|
839
|
+
queryset_item_count = 0
|
|
840
|
+
|
|
624
841
|
for item in _input:
|
|
625
|
-
if not hasattr(item,
|
|
626
|
-
|
|
627
|
-
|
|
842
|
+
if not hasattr(item, "links") or not isinstance(
|
|
843
|
+
item.links, RequirementLinks
|
|
844
|
+
):
|
|
845
|
+
raise RequirementEvaluationError(
|
|
846
|
+
requirement=self,
|
|
847
|
+
code="MISSING_LINKS_ATTR",
|
|
848
|
+
technical_message=(
|
|
849
|
+
f"Item {item} of type {type(item)} in QuerySet does not "
|
|
850
|
+
f"have a valid .links attribute of type RequirementLinks."
|
|
851
|
+
),
|
|
852
|
+
user_message=(
|
|
853
|
+
"Für einen Datensatz fehlen die intern benötigten Verknüpfungen, "
|
|
854
|
+
"sodass diese Voraussetzung nicht korrekt geprüft werden kann."
|
|
855
|
+
),
|
|
856
|
+
meta={"item_type": str(type(item))},
|
|
628
857
|
)
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
item_input_links = RequirementLinks(**
|
|
632
|
-
|
|
633
|
-
#
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
858
|
+
|
|
859
|
+
item_active_links = item.links.active()
|
|
860
|
+
item_input_links = RequirementLinks(**item_active_links)
|
|
861
|
+
|
|
862
|
+
# Links sammeln
|
|
863
|
+
for link_key, link_list in item_active_links.items():
|
|
864
|
+
if link_key not in aggregated_input_links_data:
|
|
865
|
+
aggregated_input_links_data[link_key] = []
|
|
866
|
+
aggregated_input_links_data[link_key].extend(link_list)
|
|
867
|
+
|
|
868
|
+
per_item_args = tuple(
|
|
869
|
+
item if arg is _input else arg for arg in args
|
|
870
|
+
)
|
|
871
|
+
op_kwargs = kwargs.copy()
|
|
872
|
+
op_kwargs["requirement"] = self
|
|
873
|
+
op_kwargs["original_input_args"] = per_item_args
|
|
874
|
+
|
|
875
|
+
if has_operators:
|
|
876
|
+
item_operator_results: List[bool] = []
|
|
877
|
+
for operator in operators:
|
|
878
|
+
try:
|
|
879
|
+
operator_result = operator.evaluate(
|
|
880
|
+
requirement_links=requirement_req_links,
|
|
881
|
+
input_links=item_input_links,
|
|
882
|
+
**op_kwargs,
|
|
883
|
+
)
|
|
884
|
+
item_operator_results.append(operator_result)
|
|
885
|
+
except Exception as exc:
|
|
886
|
+
logger.debug(
|
|
887
|
+
"Operator %s evaluation failed for item %s: %s",
|
|
888
|
+
getattr(operator, "name", "unknown"),
|
|
889
|
+
item,
|
|
890
|
+
exc,
|
|
891
|
+
)
|
|
892
|
+
item_operator_results.append(False)
|
|
893
|
+
item_result = (
|
|
894
|
+
evaluate_result_list_func(item_operator_results)
|
|
895
|
+
if item_operator_results
|
|
896
|
+
else True
|
|
897
|
+
)
|
|
898
|
+
else:
|
|
899
|
+
# keine Operatoren -> Bedingung erfüllt, wenn Requirement selbst keine Bedingungen hat
|
|
900
|
+
item_result = not requirement_has_conditions
|
|
901
|
+
|
|
651
902
|
queryset_results.append(item_result)
|
|
903
|
+
if item_result:
|
|
904
|
+
queryset_true_count += 1
|
|
905
|
+
queryset_item_count += 1
|
|
652
906
|
processed_inputs_count += 1
|
|
653
|
-
|
|
654
|
-
# If any item in the QuerySet matches, return True for the whole QuerySet evaluation
|
|
655
|
-
if any(queryset_results):
|
|
656
|
-
return True
|
|
657
|
-
continue # Move to the next arg after processing queryset
|
|
658
|
-
else:
|
|
659
|
-
raise TypeError(
|
|
660
|
-
f"Input type {type(_input)} is not among expected models: {self.expected_models} "
|
|
661
|
-
f"nor a QuerySet of expected models."
|
|
662
|
-
)
|
|
663
907
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
908
|
+
# QS-Modus nach Auswertung anwenden
|
|
909
|
+
if queryset_mode == "all":
|
|
910
|
+
if queryset_item_count == 0 or not all(queryset_results):
|
|
911
|
+
return (
|
|
912
|
+
False,
|
|
913
|
+
"Für diese Voraussetzung müssen alle relevanten Einträge die Bedingung erfüllen.",
|
|
914
|
+
)
|
|
915
|
+
elif queryset_mode == "min_count":
|
|
916
|
+
required = (
|
|
917
|
+
queryset_min_count if queryset_min_count is not None else 1
|
|
918
|
+
)
|
|
919
|
+
if queryset_true_count < max(required, 0):
|
|
920
|
+
return (
|
|
921
|
+
False,
|
|
922
|
+
f"Für diese Voraussetzung werden mindestens {max(required, 0)} "
|
|
923
|
+
f"passende Einträge benötigt (gefunden: {queryset_true_count}).",
|
|
924
|
+
)
|
|
925
|
+
# queryset_mode == "any": keine zusätzliche Einschränkung
|
|
926
|
+
continue
|
|
927
|
+
|
|
928
|
+
# Weder Instanz noch QS eines erwarteten Modells -> Konfig-/Aufruf-Fehler
|
|
929
|
+
raise RequirementEvaluationError(
|
|
930
|
+
requirement=self,
|
|
931
|
+
code="INVALID_INPUT_TYPE",
|
|
932
|
+
technical_message=(
|
|
933
|
+
f"Input type {type(_input)} is not among expected models: "
|
|
934
|
+
f"{self.expected_models} nor a QuerySet of expected models."
|
|
935
|
+
),
|
|
936
|
+
user_message=(
|
|
937
|
+
"Diese Voraussetzung wurde mit einem nicht passenden Datentyp "
|
|
938
|
+
"aufgerufen und kann aktuell nicht korrekt geprüft werden."
|
|
939
|
+
),
|
|
940
|
+
meta={"input_type": str(type(_input))},
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
# Einzelinstanz erwarteten Typs
|
|
944
|
+
if not hasattr(_input, "links") or not isinstance(
|
|
945
|
+
_input.links, RequirementLinks
|
|
946
|
+
):
|
|
947
|
+
raise RequirementEvaluationError(
|
|
948
|
+
requirement=self,
|
|
949
|
+
code="MISSING_LINKS_ATTR",
|
|
950
|
+
technical_message=(
|
|
951
|
+
f"Input {_input} of type {type(_input)} does not have a valid "
|
|
952
|
+
f".links attribute of type RequirementLinks."
|
|
953
|
+
),
|
|
954
|
+
user_message=(
|
|
955
|
+
"Für die Auswertung dieser Voraussetzung fehlen die intern "
|
|
956
|
+
"benötigten Verknüpfungsinformationen."
|
|
957
|
+
),
|
|
958
|
+
meta={"input_type": str(type(_input))},
|
|
668
959
|
)
|
|
669
|
-
|
|
670
|
-
active_input_links = _input.links.active()
|
|
960
|
+
|
|
961
|
+
active_input_links = _input.links.active()
|
|
671
962
|
for link_key, link_list in active_input_links.items():
|
|
672
963
|
if link_key not in aggregated_input_links_data:
|
|
673
964
|
aggregated_input_links_data[link_key] = []
|
|
674
965
|
aggregated_input_links_data[link_key].extend(link_list)
|
|
675
966
|
processed_inputs_count += 1
|
|
676
967
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
# Depending on operator logic (e.g., "requires at least one matching item"), this might lead to False.
|
|
680
|
-
# For "models_match_any", an empty input_links will likely result in False if requirement_req_links is not empty.
|
|
681
|
-
pass
|
|
682
|
-
|
|
968
|
+
# Wenn es zwar *args gibt, aber alles leer/irrelevant war, lassen wir das weiterlaufen.
|
|
969
|
+
# Operatoren sehen dann ggf. ein leeres final_input_links.
|
|
683
970
|
|
|
684
|
-
#
|
|
971
|
+
# Deduplizieren der aggregierten Links
|
|
685
972
|
for key in aggregated_input_links_data:
|
|
686
973
|
try:
|
|
687
|
-
|
|
688
|
-
|
|
974
|
+
aggregated_input_links_data[key] = list(
|
|
975
|
+
dict.fromkeys(aggregated_input_links_data[key])
|
|
976
|
+
)
|
|
689
977
|
except TypeError:
|
|
690
|
-
# Fallback
|
|
691
|
-
|
|
978
|
+
# Fallback für nicht-hashbare Items
|
|
979
|
+
tmp: list = []
|
|
692
980
|
for item in aggregated_input_links_data[key]:
|
|
693
|
-
if item not in
|
|
694
|
-
|
|
695
|
-
aggregated_input_links_data[key] =
|
|
696
|
-
|
|
981
|
+
if item not in tmp:
|
|
982
|
+
tmp.append(item)
|
|
983
|
+
aggregated_input_links_data[key] = tmp
|
|
984
|
+
|
|
697
985
|
final_input_links = RequirementLinks(**aggregated_input_links_data)
|
|
698
|
-
|
|
699
|
-
# Gender
|
|
986
|
+
|
|
987
|
+
# --- Gender-Check ----------------------------------------------------
|
|
700
988
|
genders_exist = self.genders.exists()
|
|
701
989
|
if genders_exist:
|
|
702
|
-
# Import here to avoid circular import
|
|
703
990
|
from endoreg_db.models.administration.person.patient import Patient
|
|
991
|
+
|
|
704
992
|
patient = None
|
|
705
993
|
for arg in args:
|
|
706
994
|
if isinstance(arg, Patient):
|
|
707
995
|
patient = arg
|
|
708
996
|
break
|
|
997
|
+
|
|
709
998
|
if patient is None or patient.gender is None:
|
|
710
|
-
return
|
|
999
|
+
return (
|
|
1000
|
+
False,
|
|
1001
|
+
"Für diese Voraussetzung ist ein hinterlegtes Geschlecht des Patienten erforderlich.",
|
|
1002
|
+
)
|
|
1003
|
+
|
|
711
1004
|
if not self.genders.filter(pk=patient.gender.pk).exists():
|
|
712
|
-
return
|
|
1005
|
+
return (
|
|
1006
|
+
False,
|
|
1007
|
+
"Diese Voraussetzung gilt nur für bestimmte Geschlechter und ist "
|
|
1008
|
+
"für diesen Patienten nicht erfüllt.",
|
|
1009
|
+
)
|
|
713
1010
|
|
|
714
|
-
|
|
715
|
-
if not
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
return True # Vacuously true if requirement itself is empty
|
|
723
|
-
return False # Cannot be satisfied if requirement has conditions but no operators to check them
|
|
1011
|
+
# --- Fall: keine Operatoren -----------------------------------------
|
|
1012
|
+
if not has_operators:
|
|
1013
|
+
if not requirement_has_conditions:
|
|
1014
|
+
return True, "Keine Operatoren für die Bewertung erforderlich."
|
|
1015
|
+
return (
|
|
1016
|
+
False,
|
|
1017
|
+
"Die Voraussetzung besitzt Bedingungen, aber keinen Operator zur Auswertung.",
|
|
1018
|
+
)
|
|
724
1019
|
|
|
1020
|
+
# --- Operatoren anwenden --------------------------------------------
|
|
1021
|
+
operator_results: List[bool] = []
|
|
1022
|
+
operator_details: List[str] = []
|
|
725
1023
|
|
|
726
|
-
operator_results = []
|
|
727
|
-
operator_details = []
|
|
728
1024
|
for operator in operators:
|
|
729
|
-
|
|
730
|
-
op_kwargs =
|
|
731
|
-
op_kwargs[
|
|
732
|
-
|
|
1025
|
+
op_kwargs = kwargs.copy()
|
|
1026
|
+
op_kwargs["requirement"] = self
|
|
1027
|
+
op_kwargs["original_input_args"] = args
|
|
1028
|
+
|
|
733
1029
|
try:
|
|
734
1030
|
operator_result = operator.evaluate(
|
|
735
1031
|
requirement_links=requirement_req_links,
|
|
736
1032
|
input_links=final_input_links,
|
|
737
|
-
**op_kwargs
|
|
1033
|
+
**op_kwargs,
|
|
738
1034
|
)
|
|
739
1035
|
operator_results.append(operator_result)
|
|
740
|
-
operator_details.append(
|
|
1036
|
+
operator_details.append(
|
|
1037
|
+
f"{operator.name}: {'erfüllt' if operator_result else 'nicht erfüllt'}"
|
|
1038
|
+
)
|
|
741
1039
|
except Exception as e:
|
|
742
1040
|
operator_results.append(False)
|
|
743
|
-
operator_details.append(f"{operator.name}: {
|
|
1041
|
+
operator_details.append(f"{operator.name}: technischer Fehler ({e})")
|
|
1042
|
+
logger.debug(
|
|
1043
|
+
"Operator %s evaluation failed for requirement %s: %s",
|
|
1044
|
+
getattr(operator, "name", "unknown"),
|
|
1045
|
+
getattr(self, "name", "unknown"),
|
|
1046
|
+
e,
|
|
1047
|
+
)
|
|
744
1048
|
|
|
745
1049
|
is_valid = evaluate_result_list_func(operator_results)
|
|
746
1050
|
|
|
747
|
-
#
|
|
1051
|
+
# --- Detailtext bauen -----------------------------------------------
|
|
748
1052
|
if not operator_results:
|
|
749
|
-
details = "Keine Operatoren für die Bewertung verfügbar"
|
|
1053
|
+
details = "Keine Operatoren für die Bewertung verfügbar."
|
|
750
1054
|
elif len(operator_results) == 1:
|
|
751
1055
|
details = operator_details[0]
|
|
752
1056
|
else:
|
|
753
|
-
failed_details = [
|
|
1057
|
+
failed_details = [
|
|
1058
|
+
detail
|
|
1059
|
+
for detail, result in zip(operator_details, operator_results)
|
|
1060
|
+
if not result
|
|
1061
|
+
]
|
|
754
1062
|
if failed_details:
|
|
755
1063
|
details = "; ".join(failed_details)
|
|
756
1064
|
else:
|
|
757
|
-
details = "Alle
|
|
1065
|
+
details = "Alle verknüpften Bedingungen sind erfüllt."
|
|
758
1066
|
|
|
759
|
-
#
|
|
1067
|
+
# Arbeitsverzeichnis als Debug-Helfer anhängen (optional)
|
|
760
1068
|
try:
|
|
761
1069
|
cwd = run("pwd", capture_output=True, text=True).stdout.strip()
|
|
762
1070
|
details = f"{details}\ncwd: {cwd}"
|
|
763
1071
|
except Exception:
|
|
764
|
-
#
|
|
1072
|
+
# nicht kritisch
|
|
765
1073
|
pass
|
|
766
1074
|
|
|
767
|
-
return is_valid, details
|
|
1075
|
+
return bool(is_valid), details
|