endoreg-db 0.8.6.1__py3-none-any.whl → 0.8.8.9__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.

Files changed (503) hide show
  1. endoreg_db/authz/auth.py +74 -0
  2. endoreg_db/authz/backends.py +168 -0
  3. endoreg_db/authz/management/commands/list_routes.py +18 -0
  4. endoreg_db/authz/middleware.py +83 -0
  5. endoreg_db/authz/permissions.py +127 -0
  6. endoreg_db/authz/policy.py +218 -0
  7. endoreg_db/authz/views_auth.py +66 -0
  8. endoreg_db/config/env.py +13 -8
  9. endoreg_db/data/__init__.py +2 -11
  10. endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +3 -3
  11. endoreg_db/data/event_classification/data.yaml +4 -0
  12. endoreg_db/data/event_classification_choice/data.yaml +9 -0
  13. endoreg_db/data/examination/examinations/data.yaml +114 -14
  14. endoreg_db/data/examination/time-type/data.yaml +0 -3
  15. endoreg_db/data/examination_indication/endoscopy.yaml +108 -173
  16. endoreg_db/data/examination_indication_classification/endoscopy.yaml +0 -70
  17. endoreg_db/data/examination_indication_classification_choice/endoscopy.yaml +33 -37
  18. endoreg_db/data/finding/00_generic.yaml +35 -0
  19. endoreg_db/data/finding/00_generic_complication.yaml +9 -0
  20. endoreg_db/data/finding/01_gastroscopy_baseline.yaml +88 -0
  21. endoreg_db/data/finding/01_gastroscopy_observation.yaml +113 -0
  22. endoreg_db/data/finding/02_colonoscopy_baseline.yaml +53 -0
  23. endoreg_db/data/finding/02_colonoscopy_hidden.yaml +119 -0
  24. endoreg_db/data/finding/02_colonoscopy_observation.yaml +152 -0
  25. endoreg_db/data/finding_classification/00_generic.yaml +44 -0
  26. endoreg_db/data/finding_classification/00_generic_histology.yaml +28 -0
  27. endoreg_db/data/finding_classification/00_generic_lesion.yaml +52 -0
  28. endoreg_db/data/finding_classification/02_colonoscopy_baseline.yaml +83 -0
  29. endoreg_db/data/finding_classification/02_colonoscopy_histology.yaml +13 -0
  30. endoreg_db/data/finding_classification/02_colonoscopy_other.yaml +12 -0
  31. endoreg_db/data/finding_classification/02_colonoscopy_polyp.yaml +101 -0
  32. endoreg_db/data/finding_classification_choice/{yes_no_na.yaml → 00_generic.yaml} +5 -1
  33. endoreg_db/data/finding_classification_choice/{examination_setting_generic_types.yaml → 00_generic_baseline.yaml} +10 -2
  34. endoreg_db/data/finding_classification_choice/{complication_generic_types.yaml → 00_generic_complication.yaml} +1 -1
  35. endoreg_db/data/finding_classification_choice/{histology.yaml → 00_generic_histology.yaml} +1 -4
  36. endoreg_db/data/finding_classification_choice/00_generic_lesion.yaml +158 -0
  37. endoreg_db/data/finding_classification_choice/{bowel_preparation.yaml → 02_colonoscopy_bowel_preparation.yaml} +1 -30
  38. endoreg_db/data/finding_classification_choice/{colonoscopy_not_complete_reason.yaml → 02_colonoscopy_generic.yaml} +1 -1
  39. endoreg_db/data/finding_classification_choice/{histology_polyp.yaml → 02_colonoscopy_histology.yaml} +1 -1
  40. endoreg_db/data/finding_classification_choice/{colonoscopy_location.yaml → 02_colonoscopy_location.yaml} +23 -4
  41. endoreg_db/data/finding_classification_choice/02_colonoscopy_other.yaml +34 -0
  42. endoreg_db/data/finding_classification_choice/02_colonoscopy_polyp_advanced_imaging.yaml +76 -0
  43. endoreg_db/data/finding_classification_choice/{colon_lesion_paris.yaml → 02_colonoscopy_polyp_morphology.yaml} +26 -8
  44. endoreg_db/data/finding_classification_choice/02_colonoscopy_size.yaml +27 -0
  45. endoreg_db/data/finding_classification_type/{colonoscopy_basic.yaml → 00_generic.yaml} +18 -13
  46. endoreg_db/data/finding_classification_type/02_colonoscopy.yaml +9 -0
  47. endoreg_db/data/finding_intervention/00_generic_endoscopy.yaml +59 -0
  48. endoreg_db/data/finding_intervention/00_generic_endoscopy_ablation.yaml +44 -0
  49. endoreg_db/data/finding_intervention/00_generic_endoscopy_bleeding.yaml +55 -0
  50. endoreg_db/data/finding_intervention/00_generic_endoscopy_resection.yaml +85 -0
  51. endoreg_db/data/finding_intervention/00_generic_endoscopy_stenosis.yaml +17 -0
  52. endoreg_db/data/finding_intervention/00_generic_endoscopy_stent.yaml +9 -0
  53. endoreg_db/data/finding_intervention/01_gastroscopy.yaml +19 -0
  54. endoreg_db/data/finding_intervention/04_eus.yaml +39 -0
  55. endoreg_db/data/finding_intervention/05_ercp.yaml +3 -0
  56. endoreg_db/data/finding_type/data.yaml +8 -12
  57. endoreg_db/data/requirement/01_patient_data.yaml +93 -0
  58. endoreg_db/data/requirement/old/colon_polyp_intervention.yaml +49 -0
  59. endoreg_db/data/requirement/old/coloreg_colon_polyp.yaml +49 -0
  60. endoreg_db/data/requirement_operator/new_operators.yaml +36 -0
  61. endoreg_db/data/requirement_set/01_endoscopy_generic.yaml +29 -12
  62. endoreg_db/data/requirement_set/01_laboratory.yaml +13 -0
  63. endoreg_db/data/requirement_set/{endoscopy_bleeding_risk.yaml → 02_endoscopy_bleeding_risk.yaml} +0 -6
  64. endoreg_db/data/requirement_set/90_coloreg.yaml +190 -0
  65. endoreg_db/data/requirement_set/_old_ +109 -0
  66. endoreg_db/data/requirement_set_type/data.yaml +21 -0
  67. endoreg_db/data/setup_config.yaml +4 -4
  68. endoreg_db/data/tag/requirement_set_tags.yaml +21 -0
  69. endoreg_db/exceptions.py +4 -2
  70. endoreg_db/forms/examination_form.py +1 -1
  71. endoreg_db/helpers/data_loader.py +125 -53
  72. endoreg_db/helpers/default_objects.py +116 -81
  73. endoreg_db/import_files/__init__.py +27 -0
  74. endoreg_db/import_files/context/__init__.py +7 -0
  75. endoreg_db/import_files/context/default_sensitive_meta.py +81 -0
  76. endoreg_db/import_files/context/ensure_center.py +17 -0
  77. endoreg_db/import_files/context/file_lock.py +66 -0
  78. endoreg_db/import_files/context/import_context.py +43 -0
  79. endoreg_db/import_files/context/validate_directories.py +56 -0
  80. endoreg_db/import_files/file_storage/__init__.py +15 -0
  81. endoreg_db/import_files/file_storage/create_report_file.py +76 -0
  82. endoreg_db/import_files/file_storage/create_video_file.py +75 -0
  83. endoreg_db/import_files/file_storage/sensitive_meta_storage.py +39 -0
  84. endoreg_db/import_files/file_storage/state_management.py +400 -0
  85. endoreg_db/import_files/file_storage/storage.py +36 -0
  86. endoreg_db/import_files/import_service.md +26 -0
  87. endoreg_db/import_files/processing/__init__.py +11 -0
  88. endoreg_db/import_files/processing/report_processing/report_anonymization.py +94 -0
  89. endoreg_db/import_files/processing/sensitive_meta_adapter.py +51 -0
  90. endoreg_db/import_files/processing/video_processing/video_anonymization.py +107 -0
  91. endoreg_db/import_files/processing/video_processing/video_cleanup_on_error.py +119 -0
  92. endoreg_db/import_files/pseudonymization/fake.py +52 -0
  93. endoreg_db/import_files/pseudonymization/k_anonymity.py +182 -0
  94. endoreg_db/import_files/pseudonymization/k_pseudonymity.py +128 -0
  95. endoreg_db/import_files/report_import_service.py +141 -0
  96. endoreg_db/import_files/video_import_service.py +150 -0
  97. endoreg_db/management/commands/create_model_meta_from_huggingface.py +21 -10
  98. endoreg_db/management/commands/create_multilabel_model_meta.py +299 -129
  99. endoreg_db/management/commands/import_report.py +130 -65
  100. endoreg_db/management/commands/import_video.py +9 -10
  101. endoreg_db/management/commands/import_video_with_classification.py +2 -2
  102. endoreg_db/management/commands/list_routes.py +18 -0
  103. endoreg_db/management/commands/load_ai_model_data.py +5 -5
  104. endoreg_db/management/commands/load_ai_model_label_data.py +9 -7
  105. endoreg_db/management/commands/load_base_db_data.py +5 -134
  106. endoreg_db/management/commands/load_center_data.py +12 -12
  107. endoreg_db/management/commands/load_contraindication_data.py +14 -16
  108. endoreg_db/management/commands/load_disease_classification_choices_data.py +15 -18
  109. endoreg_db/management/commands/load_disease_classification_data.py +15 -18
  110. endoreg_db/management/commands/load_disease_data.py +25 -28
  111. endoreg_db/management/commands/load_endoscope_data.py +20 -27
  112. endoreg_db/management/commands/load_event_data.py +14 -16
  113. endoreg_db/management/commands/load_examination_data.py +31 -44
  114. endoreg_db/management/commands/load_examination_indication_data.py +20 -21
  115. endoreg_db/management/commands/load_finding_data.py +52 -80
  116. endoreg_db/management/commands/load_information_source.py +21 -23
  117. endoreg_db/management/commands/load_lab_value_data.py +17 -26
  118. endoreg_db/management/commands/load_medication_data.py +13 -12
  119. endoreg_db/management/commands/load_organ_data.py +15 -19
  120. endoreg_db/management/commands/load_pdf_type_data.py +19 -18
  121. endoreg_db/management/commands/load_profession_data.py +14 -17
  122. endoreg_db/management/commands/load_qualification_data.py +20 -23
  123. endoreg_db/management/commands/load_report_reader_flag_data.py +17 -19
  124. endoreg_db/management/commands/load_requirement_data.py +62 -39
  125. endoreg_db/management/commands/load_requirement_set_tags.py +95 -0
  126. endoreg_db/management/commands/load_risk_data.py +7 -6
  127. endoreg_db/management/commands/load_shift_data.py +20 -23
  128. endoreg_db/management/commands/load_tag_data.py +8 -11
  129. endoreg_db/management/commands/load_unit_data.py +17 -19
  130. endoreg_db/management/commands/setup_endoreg_db.py +3 -3
  131. endoreg_db/management/commands/start_filewatcher.py +46 -37
  132. endoreg_db/management/commands/storage_management.py +271 -203
  133. endoreg_db/management/commands/validate_video_files.py +1 -5
  134. endoreg_db/migrations/0001_initial.py +297 -250
  135. endoreg_db/models/__init__.py +78 -123
  136. endoreg_db/models/administration/__init__.py +21 -42
  137. endoreg_db/models/administration/ai/active_model.py +2 -2
  138. endoreg_db/models/administration/ai/ai_model.py +7 -6
  139. endoreg_db/models/administration/case/__init__.py +1 -15
  140. endoreg_db/models/administration/case/case.py +3 -3
  141. endoreg_db/models/administration/case/case_template/__init__.py +2 -14
  142. endoreg_db/models/administration/case/case_template/case_template.py +2 -124
  143. endoreg_db/models/administration/case/case_template/case_template_rule.py +2 -268
  144. endoreg_db/models/administration/case/case_template/case_template_rule_value.py +2 -85
  145. endoreg_db/models/administration/case/case_template/case_template_type.py +2 -25
  146. endoreg_db/models/administration/center/center.py +33 -19
  147. endoreg_db/models/administration/center/center_product.py +12 -9
  148. endoreg_db/models/administration/center/center_resource.py +25 -19
  149. endoreg_db/models/administration/center/center_shift.py +21 -17
  150. endoreg_db/models/administration/center/center_waste.py +16 -8
  151. endoreg_db/models/administration/person/__init__.py +2 -0
  152. endoreg_db/models/administration/person/employee/employee.py +10 -5
  153. endoreg_db/models/administration/person/employee/employee_qualification.py +9 -4
  154. endoreg_db/models/administration/person/employee/employee_type.py +12 -6
  155. endoreg_db/models/administration/person/examiner/examiner.py +13 -11
  156. endoreg_db/models/administration/person/patient/__init__.py +2 -0
  157. endoreg_db/models/administration/person/patient/patient.py +129 -100
  158. endoreg_db/models/administration/person/patient/patient_external_id.py +37 -0
  159. endoreg_db/models/administration/person/person.py +4 -0
  160. endoreg_db/models/administration/person/profession/__init__.py +8 -4
  161. endoreg_db/models/administration/person/user/portal_user_information.py +11 -7
  162. endoreg_db/models/administration/product/product.py +20 -15
  163. endoreg_db/models/administration/product/product_material.py +17 -18
  164. endoreg_db/models/administration/product/product_weight.py +12 -8
  165. endoreg_db/models/administration/product/reference_product.py +23 -55
  166. endoreg_db/models/administration/qualification/qualification.py +7 -3
  167. endoreg_db/models/administration/qualification/qualification_type.py +7 -3
  168. endoreg_db/models/administration/shift/scheduled_days.py +8 -5
  169. endoreg_db/models/administration/shift/shift.py +16 -12
  170. endoreg_db/models/administration/shift/shift_type.py +23 -31
  171. endoreg_db/models/label/__init__.py +8 -9
  172. endoreg_db/models/label/annotation/image_classification.py +10 -9
  173. endoreg_db/models/label/annotation/video_segmentation_annotation.py +23 -28
  174. endoreg_db/models/label/label.py +15 -15
  175. endoreg_db/models/label/label_set.py +19 -6
  176. endoreg_db/models/label/label_type.py +1 -1
  177. endoreg_db/models/label/label_video_segment/_create_from_video.py +5 -8
  178. endoreg_db/models/label/label_video_segment/label_video_segment.py +98 -102
  179. endoreg_db/models/label/video_segmentation_label.py +4 -0
  180. endoreg_db/models/label/video_segmentation_labelset.py +4 -3
  181. endoreg_db/models/media/frame/frame.py +22 -22
  182. endoreg_db/models/media/pdf/raw_pdf.py +194 -194
  183. endoreg_db/models/media/pdf/report_file.py +25 -29
  184. endoreg_db/models/media/pdf/report_reader/report_reader_config.py +55 -47
  185. endoreg_db/models/media/pdf/report_reader/report_reader_flag.py +23 -7
  186. endoreg_db/models/media/processing_history/__init__.py +5 -0
  187. endoreg_db/models/media/processing_history/processing_history.py +96 -0
  188. endoreg_db/models/media/video/__init__.py +1 -0
  189. endoreg_db/models/media/video/create_from_file.py +139 -77
  190. endoreg_db/models/media/video/pipe_2.py +8 -9
  191. endoreg_db/models/media/video/video_file.py +174 -112
  192. endoreg_db/models/media/video/video_file_ai.py +288 -74
  193. endoreg_db/models/media/video/video_file_anonymize.py +38 -38
  194. endoreg_db/models/media/video/video_file_frames/__init__.py +3 -1
  195. endoreg_db/models/media/video/video_file_frames/_bulk_create_frames.py +6 -8
  196. endoreg_db/models/media/video/video_file_frames/_create_frame_object.py +7 -9
  197. endoreg_db/models/media/video/video_file_frames/_delete_frames.py +9 -8
  198. endoreg_db/models/media/video/video_file_frames/_extract_frames.py +38 -45
  199. endoreg_db/models/media/video/video_file_frames/_get_frame.py +6 -8
  200. endoreg_db/models/media/video/video_file_frames/_get_frame_number.py +4 -18
  201. endoreg_db/models/media/video/video_file_frames/_get_frame_path.py +4 -3
  202. endoreg_db/models/media/video/video_file_frames/_get_frame_paths.py +7 -6
  203. endoreg_db/models/media/video/video_file_frames/_get_frame_range.py +6 -8
  204. endoreg_db/models/media/video/video_file_frames/_get_frames.py +6 -8
  205. endoreg_db/models/media/video/video_file_frames/_initialize_frames.py +15 -25
  206. endoreg_db/models/media/video/video_file_frames/_manage_frame_range.py +26 -23
  207. endoreg_db/models/media/video/video_file_frames/_mark_frames_extracted_status.py +23 -14
  208. endoreg_db/models/media/video/video_file_io.py +113 -61
  209. endoreg_db/models/media/video/video_file_meta/get_crop_template.py +3 -3
  210. endoreg_db/models/media/video/video_file_meta/get_endo_roi.py +5 -3
  211. endoreg_db/models/media/video/video_file_meta/get_fps.py +37 -34
  212. endoreg_db/models/media/video/video_file_meta/initialize_video_specs.py +19 -25
  213. endoreg_db/models/media/video/video_file_meta/text_meta.py +41 -38
  214. endoreg_db/models/media/video/video_file_meta/video_meta.py +14 -7
  215. endoreg_db/models/media/video/video_file_segments.py +24 -17
  216. endoreg_db/models/media/video/video_metadata.py +19 -35
  217. endoreg_db/models/media/video/video_processing.py +96 -95
  218. endoreg_db/models/medical/contraindication/README.md +1 -0
  219. endoreg_db/models/medical/contraindication/__init__.py +13 -3
  220. endoreg_db/models/medical/disease.py +22 -16
  221. endoreg_db/models/medical/event.py +31 -18
  222. endoreg_db/models/medical/examination/__init__.py +13 -6
  223. endoreg_db/models/medical/examination/examination.py +39 -20
  224. endoreg_db/models/medical/examination/examination_indication.py +30 -95
  225. endoreg_db/models/medical/examination/examination_time.py +23 -8
  226. endoreg_db/models/medical/examination/examination_time_type.py +9 -6
  227. endoreg_db/models/medical/examination/examination_type.py +3 -4
  228. endoreg_db/models/medical/finding/finding.py +32 -40
  229. endoreg_db/models/medical/finding/finding_classification.py +42 -72
  230. endoreg_db/models/medical/finding/finding_intervention.py +25 -22
  231. endoreg_db/models/medical/finding/finding_type.py +13 -12
  232. endoreg_db/models/medical/hardware/endoscope.py +26 -26
  233. endoreg_db/models/medical/hardware/endoscopy_processor.py +2 -2
  234. endoreg_db/models/medical/laboratory/lab_value.py +62 -91
  235. endoreg_db/models/medical/medication/medication.py +22 -10
  236. endoreg_db/models/medical/medication/medication_indication.py +29 -3
  237. endoreg_db/models/medical/medication/medication_indication_type.py +25 -14
  238. endoreg_db/models/medical/medication/medication_intake_time.py +31 -19
  239. endoreg_db/models/medical/medication/medication_schedule.py +27 -16
  240. endoreg_db/models/medical/organ/__init__.py +15 -12
  241. endoreg_db/models/medical/patient/medication_examples.py +6 -6
  242. endoreg_db/models/medical/patient/patient_disease.py +20 -23
  243. endoreg_db/models/medical/patient/patient_event.py +19 -22
  244. endoreg_db/models/medical/patient/patient_examination.py +48 -54
  245. endoreg_db/models/medical/patient/patient_examination_indication.py +16 -14
  246. endoreg_db/models/medical/patient/patient_finding.py +122 -139
  247. endoreg_db/models/medical/patient/patient_finding_classification.py +44 -49
  248. endoreg_db/models/medical/patient/patient_finding_intervention.py +8 -19
  249. endoreg_db/models/medical/patient/patient_lab_sample.py +28 -23
  250. endoreg_db/models/medical/patient/patient_lab_value.py +82 -89
  251. endoreg_db/models/medical/patient/patient_medication.py +27 -38
  252. endoreg_db/models/medical/patient/patient_medication_schedule.py +28 -36
  253. endoreg_db/models/medical/risk/risk.py +7 -6
  254. endoreg_db/models/medical/risk/risk_type.py +8 -5
  255. endoreg_db/models/metadata/model_meta.py +60 -29
  256. endoreg_db/models/metadata/model_meta_logic.py +125 -18
  257. endoreg_db/models/metadata/pdf_meta.py +31 -24
  258. endoreg_db/models/metadata/sensitive_meta.py +105 -85
  259. endoreg_db/models/metadata/sensitive_meta_logic.py +198 -103
  260. endoreg_db/models/metadata/video_meta.py +51 -31
  261. endoreg_db/models/metadata/video_prediction_logic.py +16 -23
  262. endoreg_db/models/metadata/video_prediction_meta.py +29 -33
  263. endoreg_db/models/other/distribution/date_value_distribution.py +89 -29
  264. endoreg_db/models/other/distribution/multiple_categorical_value_distribution.py +21 -5
  265. endoreg_db/models/other/distribution/numeric_value_distribution.py +114 -53
  266. endoreg_db/models/other/distribution/single_categorical_value_distribution.py +4 -3
  267. endoreg_db/models/other/emission/emission_factor.py +18 -8
  268. endoreg_db/models/other/gender.py +10 -5
  269. endoreg_db/models/other/information_source.py +50 -29
  270. endoreg_db/models/other/material.py +9 -5
  271. endoreg_db/models/other/resource.py +6 -4
  272. endoreg_db/models/other/tag.py +10 -5
  273. endoreg_db/models/other/transport_route.py +13 -8
  274. endoreg_db/models/other/unit.py +10 -6
  275. endoreg_db/models/other/waste.py +6 -5
  276. endoreg_db/models/report/report.py +6 -0
  277. endoreg_db/models/requirement/requirement.py +329 -361
  278. endoreg_db/models/requirement/requirement_error.py +85 -0
  279. endoreg_db/models/requirement/requirement_evaluation/evaluate_with_dependencies.py +268 -0
  280. endoreg_db/models/requirement/requirement_evaluation/operator_evaluation_models.py +3 -6
  281. endoreg_db/models/requirement/requirement_evaluation/requirement_type_parser.py +90 -64
  282. endoreg_db/models/requirement/requirement_operator.py +103 -112
  283. endoreg_db/models/requirement/requirement_set.py +74 -57
  284. endoreg_db/models/state/__init__.py +4 -4
  285. endoreg_db/models/state/abstract.py +2 -2
  286. endoreg_db/models/state/anonymization.py +12 -0
  287. endoreg_db/models/state/audit_ledger.py +49 -51
  288. endoreg_db/models/state/label_video_segment.py +9 -0
  289. endoreg_db/models/state/raw_pdf.py +101 -68
  290. endoreg_db/models/state/sensitive_meta.py +6 -2
  291. endoreg_db/models/state/video.py +110 -90
  292. endoreg_db/models/upload_job.py +35 -34
  293. endoreg_db/models/utils.py +28 -25
  294. endoreg_db/queries/__init__.py +3 -1
  295. endoreg_db/root_urls.py +21 -2
  296. endoreg_db/schemas/examination_evaluation.py +1 -1
  297. endoreg_db/serializers/__init__.py +2 -10
  298. endoreg_db/serializers/anonymization.py +18 -10
  299. endoreg_db/serializers/label_video_segment/label_video_segment.py +2 -29
  300. endoreg_db/serializers/meta/__init__.py +1 -6
  301. endoreg_db/serializers/meta/sensitive_meta_detail.py +63 -118
  302. endoreg_db/serializers/misc/file_overview.py +11 -99
  303. endoreg_db/serializers/misc/sensitive_patient_data.py +50 -26
  304. endoreg_db/serializers/patient_examination/patient_examination.py +3 -3
  305. endoreg_db/serializers/pdf/anony_text_validation.py +39 -23
  306. endoreg_db/serializers/requirements/requirement_sets.py +92 -22
  307. endoreg_db/serializers/video/segmentation.py +2 -1
  308. endoreg_db/serializers/video/video_file_list.py +65 -34
  309. endoreg_db/serializers/video/video_processing_history.py +20 -5
  310. endoreg_db/services/__old/pdf_import.py +1487 -0
  311. endoreg_db/services/__old/video_import.py +1306 -0
  312. endoreg_db/services/anonymization.py +128 -89
  313. endoreg_db/services/lookup_service.py +65 -52
  314. endoreg_db/services/lookup_store.py +2 -2
  315. endoreg_db/services/pdf_import.py +0 -1382
  316. endoreg_db/services/report_import.py +10 -0
  317. endoreg_db/services/video_import.py +6 -1255
  318. endoreg_db/tasks/upload_tasks.py +79 -70
  319. endoreg_db/tasks/video_ingest.py +8 -4
  320. endoreg_db/urls/__init__.py +5 -32
  321. endoreg_db/urls/ai.py +32 -0
  322. endoreg_db/urls/media.py +121 -83
  323. endoreg_db/urls/root_urls.py +29 -0
  324. endoreg_db/utils/__init__.py +15 -5
  325. endoreg_db/utils/ai/multilabel_classification_net.py +116 -20
  326. endoreg_db/utils/case_generator/__init__.py +3 -0
  327. endoreg_db/utils/dataloader.py +142 -40
  328. endoreg_db/utils/defaults/set_default_center.py +32 -0
  329. endoreg_db/utils/names.py +22 -16
  330. endoreg_db/utils/paths.py +110 -46
  331. endoreg_db/utils/permissions.py +2 -1
  332. endoreg_db/utils/pipelines/Readme.md +1 -1
  333. endoreg_db/utils/pipelines/process_video_dir.py +1 -1
  334. endoreg_db/utils/requirement_operator_logic/_old/model_evaluators.py +655 -0
  335. endoreg_db/utils/requirement_operator_logic/new_operator_logic.py +97 -0
  336. endoreg_db/utils/setup_config.py +8 -5
  337. endoreg_db/utils/storage.py +115 -0
  338. endoreg_db/utils/validate_endo_roi.py +8 -2
  339. endoreg_db/utils/video/ffmpeg_wrapper.py +184 -188
  340. endoreg_db/views/__init__.py +85 -183
  341. endoreg_db/views/ai/__init__.py +8 -0
  342. endoreg_db/views/ai/label.py +155 -0
  343. endoreg_db/views/anonymization/media_management.py +202 -166
  344. endoreg_db/views/anonymization/overview.py +99 -67
  345. endoreg_db/views/anonymization/validate.py +182 -44
  346. endoreg_db/views/media/__init__.py +7 -20
  347. endoreg_db/views/media/pdf_media.py +197 -174
  348. endoreg_db/views/media/sensitive_metadata.py +193 -138
  349. endoreg_db/views/media/video_media.py +89 -82
  350. endoreg_db/views/meta/__init__.py +0 -8
  351. endoreg_db/views/misc/__init__.py +1 -7
  352. endoreg_db/views/misc/upload_views.py +94 -93
  353. endoreg_db/views/patient/patient.py +5 -4
  354. endoreg_db/views/report/__init__.py +5 -7
  355. endoreg_db/views/{pdf → report}/reimport.py +22 -22
  356. endoreg_db/views/{pdf/pdf_stream.py → report/report_stream.py} +46 -39
  357. endoreg_db/views/requirement/evaluate.py +188 -187
  358. endoreg_db/views/requirement/lookup.py +17 -3
  359. endoreg_db/views/requirement/lookup_store.py +22 -90
  360. endoreg_db/views/requirement/requirement_utils.py +89 -0
  361. endoreg_db/views/video/__init__.py +23 -24
  362. endoreg_db/views/video/correction.py +201 -172
  363. endoreg_db/views/video/reimport.py +1 -1
  364. endoreg_db/views/{media/video_segments.py → video/segments_crud.py} +77 -40
  365. endoreg_db/views/video/{video_meta.py → video_meta_stats.py} +2 -2
  366. endoreg_db/views/video/video_stream.py +7 -8
  367. {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.9.dist-info}/METADATA +7 -3
  368. {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.9.dist-info}/RECORD +391 -413
  369. {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.9.dist-info}/WHEEL +1 -1
  370. endoreg_db/data/finding/anatomy_colon.yaml +0 -128
  371. endoreg_db/data/finding/colonoscopy.yaml +0 -40
  372. endoreg_db/data/finding/colonoscopy_bowel_prep.yaml +0 -56
  373. endoreg_db/data/finding/complication.yaml +0 -16
  374. endoreg_db/data/finding/data.yaml +0 -105
  375. endoreg_db/data/finding/examination_setting.yaml +0 -16
  376. endoreg_db/data/finding/medication_related.yaml +0 -18
  377. endoreg_db/data/finding/outcome.yaml +0 -12
  378. endoreg_db/data/finding_classification/colonoscopy_bowel_preparation.yaml +0 -95
  379. endoreg_db/data/finding_classification/colonoscopy_jnet.yaml +0 -22
  380. endoreg_db/data/finding_classification/colonoscopy_kudo.yaml +0 -25
  381. endoreg_db/data/finding_classification/colonoscopy_lesion_circularity.yaml +0 -20
  382. endoreg_db/data/finding_classification/colonoscopy_lesion_planarity.yaml +0 -24
  383. endoreg_db/data/finding_classification/colonoscopy_lesion_size.yaml +0 -68
  384. endoreg_db/data/finding_classification/colonoscopy_lesion_surface.yaml +0 -20
  385. endoreg_db/data/finding_classification/colonoscopy_location.yaml +0 -80
  386. endoreg_db/data/finding_classification/colonoscopy_lst.yaml +0 -21
  387. endoreg_db/data/finding_classification/colonoscopy_nice.yaml +0 -20
  388. endoreg_db/data/finding_classification/colonoscopy_paris.yaml +0 -26
  389. endoreg_db/data/finding_classification/colonoscopy_sano.yaml +0 -22
  390. endoreg_db/data/finding_classification/colonoscopy_summary.yaml +0 -53
  391. endoreg_db/data/finding_classification/complication_generic.yaml +0 -25
  392. endoreg_db/data/finding_classification/examination_setting_generic.yaml +0 -40
  393. endoreg_db/data/finding_classification/histology_colo.yaml +0 -51
  394. endoreg_db/data/finding_classification/intervention_required.yaml +0 -26
  395. endoreg_db/data/finding_classification/medication_related.yaml +0 -23
  396. endoreg_db/data/finding_classification/visualized.yaml +0 -33
  397. endoreg_db/data/finding_classification_choice/colon_lesion_circularity_default.yaml +0 -32
  398. endoreg_db/data/finding_classification_choice/colon_lesion_jnet.yaml +0 -15
  399. endoreg_db/data/finding_classification_choice/colon_lesion_kudo.yaml +0 -23
  400. endoreg_db/data/finding_classification_choice/colon_lesion_lst.yaml +0 -15
  401. endoreg_db/data/finding_classification_choice/colon_lesion_nice.yaml +0 -17
  402. endoreg_db/data/finding_classification_choice/colon_lesion_planarity_default.yaml +0 -49
  403. endoreg_db/data/finding_classification_choice/colon_lesion_sano.yaml +0 -14
  404. endoreg_db/data/finding_classification_choice/colon_lesion_surface_intact_default.yaml +0 -36
  405. endoreg_db/data/finding_classification_choice/colonoscopy_size.yaml +0 -82
  406. endoreg_db/data/finding_classification_choice/colonoscopy_summary_worst_finding.yaml +0 -15
  407. endoreg_db/data/finding_classification_choice/outcome.yaml +0 -19
  408. endoreg_db/data/finding_intervention/endoscopy.yaml +0 -43
  409. endoreg_db/data/finding_intervention/endoscopy_colonoscopy.yaml +0 -168
  410. endoreg_db/data/finding_intervention/endoscopy_egd.yaml +0 -128
  411. endoreg_db/data/finding_intervention/endoscopy_ercp.yaml +0 -32
  412. endoreg_db/data/finding_intervention/endoscopy_eus_lower.yaml +0 -9
  413. endoreg_db/data/finding_intervention/endoscopy_eus_upper.yaml +0 -36
  414. endoreg_db/data/finding_morphology_classification_type/colonoscopy.yaml +0 -79
  415. endoreg_db/data/requirement/age.yaml +0 -26
  416. endoreg_db/data/requirement/gender.yaml +0 -25
  417. endoreg_db/management/commands/init_default_ai_model.py +0 -112
  418. endoreg_db/management/commands/reset_celery_schedule.py +0 -9
  419. endoreg_db/management/commands/validate_video.py +0 -204
  420. endoreg_db/migrations/0002_add_video_correction_models.py +0 -52
  421. endoreg_db/migrations/0003_add_center_display_name.py +0 -30
  422. endoreg_db/models/administration/permissions/__init__.py +0 -44
  423. endoreg_db/models/rule/__init__.py +0 -13
  424. endoreg_db/models/rule/rule.py +0 -27
  425. endoreg_db/models/rule/rule_applicator.py +0 -224
  426. endoreg_db/models/rule/rule_attribute_dtype.py +0 -17
  427. endoreg_db/models/rule/rule_type.py +0 -20
  428. endoreg_db/models/rule/ruleset.py +0 -17
  429. endoreg_db/renames.yml +0 -8
  430. endoreg_db/serializers/_old/raw_pdf_meta_validation.py +0 -223
  431. endoreg_db/serializers/_old/raw_video_meta_validation.py +0 -179
  432. endoreg_db/serializers/_old/video.py +0 -71
  433. endoreg_db/serializers/meta/pdf_file_meta_extraction.py +0 -115
  434. endoreg_db/serializers/meta/report_meta.py +0 -53
  435. endoreg_db/serializers/report/__init__.py +0 -9
  436. endoreg_db/serializers/report/mixins.py +0 -45
  437. endoreg_db/serializers/report/report.py +0 -105
  438. endoreg_db/serializers/report/report_list.py +0 -22
  439. endoreg_db/serializers/report/secure_file_url.py +0 -26
  440. endoreg_db/serializers/video/video_metadata.py +0 -105
  441. endoreg_db/services/requirements_object.py +0 -147
  442. endoreg_db/services/storage_aware_video_processor.py +0 -344
  443. endoreg_db/urls/files.py +0 -6
  444. endoreg_db/urls/label_video_segment_validate.py +0 -33
  445. endoreg_db/urls/label_video_segments.py +0 -46
  446. endoreg_db/urls/report.py +0 -48
  447. endoreg_db/urls/video.py +0 -61
  448. endoreg_db/utils/case_generator/case_generator.py +0 -159
  449. endoreg_db/utils/case_generator/utils.py +0 -30
  450. endoreg_db/utils/requirement_operator_logic/model_evaluators.py +0 -368
  451. endoreg_db/views/label/__init__.py +0 -5
  452. endoreg_db/views/label/label.py +0 -15
  453. endoreg_db/views/label_video_segment/__init__.py +0 -16
  454. endoreg_db/views/label_video_segment/create_lvs_from_annotation.py +0 -44
  455. endoreg_db/views/label_video_segment/get_lvs_by_name_and_video.py +0 -50
  456. endoreg_db/views/label_video_segment/label_video_segment.py +0 -77
  457. endoreg_db/views/label_video_segment/label_video_segment_by_label.py +0 -174
  458. endoreg_db/views/label_video_segment/label_video_segment_detail.py +0 -73
  459. endoreg_db/views/label_video_segment/update_lvs_from_annotation.py +0 -46
  460. endoreg_db/views/label_video_segment/validate.py +0 -226
  461. endoreg_db/views/media/segments.py +0 -71
  462. endoreg_db/views/meta/available_files_list.py +0 -146
  463. endoreg_db/views/meta/report_meta.py +0 -53
  464. endoreg_db/views/meta/sensitive_meta_detail.py +0 -148
  465. endoreg_db/views/misc/secure_file_serving_view.py +0 -80
  466. endoreg_db/views/misc/secure_file_url_view.py +0 -84
  467. endoreg_db/views/misc/secure_url_validate.py +0 -79
  468. endoreg_db/views/patient_examination/DEPRECATED_video_backup.py +0 -164
  469. endoreg_db/views/patient_finding_location/__init__.py +0 -5
  470. endoreg_db/views/patient_finding_location/pfl_create.py +0 -70
  471. endoreg_db/views/patient_finding_morphology/__init__.py +0 -5
  472. endoreg_db/views/patient_finding_morphology/pfm_create.py +0 -70
  473. endoreg_db/views/pdf/__init__.py +0 -8
  474. endoreg_db/views/report/report_list.py +0 -112
  475. endoreg_db/views/report/report_with_secure_url.py +0 -28
  476. endoreg_db/views/report/start_examination.py +0 -7
  477. endoreg_db/views/video/segmentation.py +0 -274
  478. endoreg_db/views/video/task_status.py +0 -49
  479. endoreg_db/views/video/timeline.py +0 -46
  480. endoreg_db/views/video/video_analyze.py +0 -52
  481. endoreg_db/views.py +0 -0
  482. /endoreg_db/data/requirement/{colonoscopy_baseline_austria.yaml → old/colonoscopy_baseline_austria.yaml} +0 -0
  483. /endoreg_db/data/requirement/{disease_cardiovascular.yaml → old/disease_cardiovascular.yaml} +0 -0
  484. /endoreg_db/data/requirement/{disease_classification_choice_cardiovascular.yaml → old/disease_classification_choice_cardiovascular.yaml} +0 -0
  485. /endoreg_db/data/requirement/{disease_hepatology.yaml → old/disease_hepatology.yaml} +0 -0
  486. /endoreg_db/data/requirement/{disease_misc.yaml → old/disease_misc.yaml} +0 -0
  487. /endoreg_db/data/requirement/{disease_renal.yaml → old/disease_renal.yaml} +0 -0
  488. /endoreg_db/data/requirement/{endoscopy_bleeding_risk.yaml → old/endoscopy_bleeding_risk.yaml} +0 -0
  489. /endoreg_db/data/requirement/{event_cardiology.yaml → old/event_cardiology.yaml} +0 -0
  490. /endoreg_db/data/requirement/{event_requirements.yaml → old/event_requirements.yaml} +0 -0
  491. /endoreg_db/data/requirement/{finding_colon_polyp.yaml → old/finding_colon_polyp.yaml} +0 -0
  492. /endoreg_db/{migrations/__init__.py → data/requirement/old/gender.yaml} +0 -0
  493. /endoreg_db/data/requirement/{lab_value.yaml → old/lab_value.yaml} +0 -0
  494. /endoreg_db/data/requirement/{medication.yaml → old/medication.yaml} +0 -0
  495. /endoreg_db/data/requirement_operator/{age.yaml → _old/age.yaml} +0 -0
  496. /endoreg_db/data/requirement_operator/{lab_operators.yaml → _old/lab_operators.yaml} +0 -0
  497. /endoreg_db/data/requirement_operator/{model_operators.yaml → _old/model_operators.yaml} +0 -0
  498. /endoreg_db/{models/media/video/refactor_plan.md → import_files/pseudonymization/__init__.py} +0 -0
  499. /endoreg_db/{models/media/video/video_file_frames.py → import_files/pseudonymization/pseudonymize.py} +0 -0
  500. /endoreg_db/models/{metadata/frame_ocr_result.py → report/__init__.py} +0 -0
  501. /endoreg_db/{urls/sensitive_meta.py → models/report/images.py} +0 -0
  502. /endoreg_db/utils/requirement_operator_logic/{lab_value_operators.py → _old/lab_value_operators.py} +0 -0
  503. {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.9.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,11 @@
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, Tuple, Union, cast
4
+
5
+ from django.db import models
6
+ from pydantic import BaseModel
7
+
8
+ from endoreg_db.utils.links.requirement_link import RequirementLinks
6
9
 
7
10
  logger = logging.getLogger(__name__)
8
11
 
@@ -10,7 +13,7 @@ logger = logging.getLogger(__name__)
10
13
  QuerySet = models.QuerySet
11
14
 
12
15
  if TYPE_CHECKING:
13
- from endoreg_db.models import (
16
+ from endoreg_db.models import ( # RequirementSet,
14
17
  Disease,
15
18
  DiseaseClassificationChoice,
16
19
  Event,
@@ -19,27 +22,27 @@ if TYPE_CHECKING:
19
22
  Examination,
20
23
  ExaminationIndication,
21
24
  Finding,
22
- FindingIntervention,
23
25
  FindingClassification,
24
26
  FindingClassificationChoice,
25
27
  FindingClassificationType,
28
+ FindingIntervention,
29
+ Gender,
26
30
  LabValue,
27
31
  Medication,
28
32
  MedicationIndication,
29
- MedicationIntakeTime, # Added MedicationIntakeTime
33
+ MedicationIntakeTime, # Added MedicationIntakeTime
30
34
  MedicationSchedule,
31
35
  PatientDisease,
32
36
  PatientEvent,
33
37
  PatientExamination,
34
38
  PatientFinding,
35
- PatientFindingIntervention,
36
39
  PatientFindingClassification,
40
+ PatientFindingIntervention,
37
41
  PatientLabValue,
38
- PatientMedicationSchedule, # Added PatientMedicationSchedule
42
+ PatientMedicationSchedule, # Added PatientMedicationSchedule
39
43
  RequirementOperator,
40
- RequirementSet,
41
- Gender
42
44
  )
45
+
43
46
  # from endoreg_db.utils.links.requirement_link import RequirementLinks # Already imported above
44
47
 
45
48
 
@@ -114,48 +117,55 @@ class Requirement(models.Model):
114
117
 
115
118
  name = models.CharField(max_length=100, unique=True)
116
119
  description = models.TextField(blank=True, null=True)
117
-
120
+
121
+ operator_instructions = models.TextField(
122
+ help_text="semicolon-separated list of target attributes for the requirement",
123
+ )
124
+
125
+ @property
126
+ def operator_instructions_parsed(self):
127
+ from endoreg_db.models.requirement.requirement_operator import RequirementOperator
128
+
129
+ instructions = RequirementOperator.parse_instructions(self.operator_instructions)
130
+ return instructions
131
+
118
132
  numeric_value = models.FloatField(
119
133
  blank=True,
120
134
  null=True,
121
- help_text="Numeric value for the requirement. If not set, the requirement is not used in calculations.",
135
+ help_text="Numeric value for the requirement. ons.",
122
136
  )
123
-
124
137
  numeric_value_min = models.FloatField(
125
138
  blank=True,
126
139
  null=True,
127
- help_text="Minimum numeric value for the requirement. If not set, the requirement is not used in calculations.",
140
+ help_text="Minimum numeric value for the requirement. ons.",
128
141
  )
129
142
  numeric_value_max = models.FloatField(
130
143
  blank=True,
131
144
  null=True,
132
- help_text="Maximum numeric value for the requirement. If not set, the requirement is not used in calculations.",
145
+ help_text="Maximum numeric value for the requirement. ons.",
133
146
  )
134
-
135
147
  string_value = models.CharField(
136
148
  max_length=100,
137
149
  blank=True,
138
150
  null=True,
139
- help_text="String value for the requirement. If not set, the requirement is not used in calculations.",
151
+ help_text="String value for the requirement. ons.",
140
152
  )
141
-
142
153
  string_values = models.TextField(
143
154
  blank=True,
144
155
  null=True,
145
- help_text=" ','-separated list of string values for the requirement.If not set, the requirement is not used in calculations.",
156
+ help_text=" ','-separated list of string values for the requirement.ons.",
146
157
  )
147
-
148
158
  objects = RequirementManager()
149
159
 
150
- requirement_types = models.ManyToManyField( # type: ignore[assignment]
160
+ requirement_types = models.ManyToManyField(
151
161
  "RequirementType",
152
162
  blank=True,
153
163
  related_name="linked_requirements",
154
164
  )
155
165
 
156
- operators = models.ManyToManyField( # type: ignore[assignment]
166
+ operator = models.ForeignKey(
157
167
  "RequirementOperator",
158
- blank=True,
168
+ on_delete=models.CASCADE,
159
169
  related_name="required_in",
160
170
  )
161
171
 
@@ -167,120 +177,138 @@ class Requirement(models.Model):
167
177
  null=True,
168
178
  )
169
179
 
170
- examinations = models.ManyToManyField( # type: ignore[assignment]
180
+ examinations = models.ManyToManyField(
171
181
  "Examination",
172
182
  blank=True,
173
183
  related_name="required_in",
174
184
  )
175
185
 
176
- examination_indications = models.ManyToManyField( # type: ignore[assignment]
186
+ examination_indications = models.ManyToManyField(
177
187
  "ExaminationIndication",
178
188
  blank=True,
179
189
  related_name="required_in",
180
190
  )
181
191
 
182
- diseases = models.ManyToManyField( # type: ignore[assignment]
192
+ diseases = models.ManyToManyField(
183
193
  "Disease",
184
194
  blank=True,
185
195
  related_name="required_in",
186
196
  )
187
197
 
188
- disease_classification_choices = models.ManyToManyField( # type: ignore[assignment]
198
+ disease_classification_choices = models.ManyToManyField(
189
199
  "DiseaseClassificationChoice",
190
200
  blank=True,
191
201
  related_name="required_in",
192
202
  )
193
203
 
194
- events = models.ManyToManyField( # type: ignore[assignment]
204
+ events = models.ManyToManyField(
195
205
  "Event",
196
206
  blank=True,
197
207
  related_name="required_in",
198
208
  )
199
209
 
200
- lab_values = models.ManyToManyField( # type: ignore[assignment]
210
+ lab_values = models.ManyToManyField(
201
211
  "LabValue",
202
212
  blank=True,
203
213
  related_name="required_in",
204
214
  )
205
215
 
206
- findings = models.ManyToManyField( # type: ignore[assignment]
216
+ findings = models.ManyToManyField(
207
217
  "Finding",
208
218
  blank=True,
209
219
  related_name="required_in",
210
220
  )
211
221
 
212
- finding_classifications = models.ManyToManyField( # type: ignore[assignment]
222
+ finding_classifications = models.ManyToManyField(
213
223
  "FindingClassification",
214
224
  blank=True,
215
225
  related_name="required_in",
216
226
  )
217
227
 
218
- finding_classification_choices = models.ManyToManyField( # type: ignore[assignment]
228
+ finding_classification_choices = models.ManyToManyField(
219
229
  "FindingClassificationChoice",
220
230
  blank=True,
221
231
  related_name="required_in",
222
232
  )
223
233
 
224
- finding_interventions = models.ManyToManyField( # type: ignore[assignment]
234
+ finding_interventions = models.ManyToManyField(
225
235
  "FindingIntervention",
226
236
  blank=True,
227
237
  related_name="required_in",
228
238
  )
229
239
 
230
- medications = models.ManyToManyField( # type: ignore[assignment]
240
+ medications = models.ManyToManyField(
231
241
  "Medication",
232
242
  blank=True,
233
243
  related_name="required_in",
234
244
  )
235
245
 
236
- medication_indications = models.ManyToManyField( # type: ignore[assignment]
246
+ medication_indications = models.ManyToManyField(
237
247
  "MedicationIndication",
238
248
  blank=True,
239
249
  related_name="required_in",
240
250
  )
241
251
 
242
- medication_intake_times = models.ManyToManyField( # type: ignore[assignment]
252
+ medication_intake_times = models.ManyToManyField(
243
253
  "MedicationIntakeTime",
244
254
  blank=True,
245
255
  related_name="required_in",
246
256
  )
247
257
 
248
- medication_schedules = models.ManyToManyField( # type: ignore[assignment]
258
+ medication_schedules = models.ManyToManyField(
249
259
  "MedicationSchedule",
250
260
  blank=True,
251
261
  related_name="required_in",
252
262
  )
253
263
 
254
- genders = models.ManyToManyField( # type: ignore[assignment]
264
+ genders = models.ManyToManyField(
255
265
  "Gender",
256
266
  blank=True,
257
267
  related_name="required_in",
258
268
  )
259
269
 
260
270
  if TYPE_CHECKING:
261
- requirement_types: models.QuerySet[RequirementType]
262
- operators: models.QuerySet[RequirementOperator]
263
- requirement_sets: models.QuerySet[RequirementSet]
264
- examinations: models.QuerySet[Examination]
265
- examination_indications: models.QuerySet[ExaminationIndication]
266
- lab_values: models.QuerySet[LabValue]
267
- diseases: models.QuerySet[Disease]
268
- disease_classification_choices: models.QuerySet[DiseaseClassificationChoice]
269
- events: models.QuerySet[Event]
270
- findings: models.QuerySet[Finding]
271
- finding_classifications: models.QuerySet[FindingClassification]
272
- finding_classification_choices: models.QuerySet[FindingClassificationChoice]
273
- finding_interventions: models.QuerySet[FindingIntervention]
274
- medications: models.QuerySet[Medication]
275
- medication_indications: models.QuerySet[MedicationIndication]
276
- medication_intake_times: models.QuerySet[MedicationIntakeTime] # Added type hint
277
- medication_schedules: models.QuerySet[MedicationSchedule]
278
- genders: models.QuerySet[Gender]
271
+ requirement_types = cast(models.manager.RelatedManager["RequirementType"], requirement_types)
272
+ operator = models.ForeignKey["RequirementOperator"]
273
+ # requirement_sets = cast(models.manager.RelatedManager["RequirementSet"], requirement_sets)
274
+ examinations = cast(models.manager.RelatedManager["Examination"], examinations)
275
+ examination_indications = cast(
276
+ models.manager.RelatedManager["ExaminationIndication"],
277
+ examination_indications,
278
+ )
279
+ lab_values = cast(models.manager.RelatedManager["LabValue"], lab_values)
280
+ diseases = cast(models.manager.RelatedManager["Disease"], diseases)
281
+ disease_classification_choices = cast(
282
+ models.manager.RelatedManager["DiseaseClassificationChoice"],
283
+ disease_classification_choices,
284
+ )
285
+ events = cast(models.manager.RelatedManager["Event"], events)
286
+ findings = cast(models.manager.RelatedManager["Finding"], findings)
287
+ finding_classifications = cast(
288
+ models.manager.RelatedManager["FindingClassification"],
289
+ finding_classifications,
290
+ )
291
+ finding_classification_choices = cast(
292
+ models.manager.RelatedManager["FindingClassificationChoice"],
293
+ finding_classification_choices,
294
+ )
295
+ finding_interventions = cast(models.manager.RelatedManager["FindingIntervention"], finding_interventions)
296
+ medications = cast(models.manager.RelatedManager["Medication"], medications)
297
+ medication_indications = cast(
298
+ models.manager.RelatedManager["MedicationIndication"],
299
+ medication_indications,
300
+ )
301
+ medication_intake_times = cast(
302
+ models.manager.RelatedManager["MedicationIntakeTime"],
303
+ medication_intake_times,
304
+ )
305
+ medication_schedules = cast(models.manager.RelatedManager["MedicationSchedule"], medication_schedules)
306
+ genders = cast(models.manager.RelatedManager["Gender"], genders)
279
307
 
280
308
  def natural_key(self):
281
309
  """
282
310
  Returns a tuple containing the instance's name as its natural key.
283
-
311
+
284
312
  This tuple provides a unique identifier for serialization purposes.
285
313
  """
286
314
  return (self.name,)
@@ -290,35 +318,39 @@ class Requirement(models.Model):
290
318
  return str(self.name)
291
319
 
292
320
  @property
293
- def expected_models(self) -> List[Union[
294
- "Disease",
295
- "DiseaseClassificationChoice",
296
- "Event",
297
- "EventClassification",
298
- "EventClassificationChoice",
299
- "Examination",
300
- "ExaminationIndication",
301
- "Finding",
302
- "FindingIntervention",
303
- "FindingClassification",
304
- "FindingClassificationChoice",
305
- "FindingClassificationType",
306
- "LabValue",
307
- "Medication",
308
- "MedicationIndication",
309
- "MedicationIntakeTime", # Added MedicationIntakeTime
310
- "PatientDisease",
311
- "PatientEvent",
312
- "PatientExamination",
313
- "PatientFinding",
314
- "PatientFindingIntervention",
315
- "PatientFindingClassification",
316
- "PatientLabValue",
317
- "PatientMedicationSchedule", # Added PatientMedicationSchedule
318
- ]]:
321
+ def expected_models(
322
+ self,
323
+ ) -> List[
324
+ Union[
325
+ "Disease",
326
+ "DiseaseClassificationChoice",
327
+ "Event",
328
+ "EventClassification",
329
+ "EventClassificationChoice",
330
+ "Examination",
331
+ "ExaminationIndication",
332
+ "Finding",
333
+ "FindingIntervention",
334
+ "FindingClassification",
335
+ "FindingClassificationChoice",
336
+ "FindingClassificationType",
337
+ "LabValue",
338
+ "Medication",
339
+ "MedicationIndication",
340
+ "MedicationIntakeTime", # Added MedicationIntakeTime
341
+ "PatientDisease",
342
+ "PatientEvent",
343
+ "PatientExamination",
344
+ "PatientFinding",
345
+ "PatientFindingIntervention",
346
+ "PatientFindingClassification",
347
+ "PatientLabValue",
348
+ "PatientMedicationSchedule", # Added PatientMedicationSchedule
349
+ ]
350
+ ]:
319
351
  """
320
352
  Return the list of model classes that are expected as input for evaluating this requirement.
321
-
353
+
322
354
  The returned models correspond to the requirement types linked to this requirement, mapped via the internal data model dictionary.
323
355
  """
324
356
  req_types = self.requirement_types.all()
@@ -332,7 +364,7 @@ class Requirement(models.Model):
332
364
  def links(self) -> "RequirementLinks":
333
365
  """
334
366
  Return a RequirementLinks object containing all non-null related model instances for this requirement.
335
-
367
+
336
368
  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
369
  """
338
370
  # requirement_sets is not part of RequirementLinks (avoids circular import); collect other related models
@@ -352,238 +384,103 @@ class Requirement(models.Model):
352
384
  medication_intake_times=[_ for _ in self.medication_intake_times.all() if _ is not None],
353
385
  )
354
386
  return models_dict
355
-
387
+
356
388
  @property
357
389
  def data_model_dict(self) -> dict:
358
390
  """
359
391
  Provides a mapping from requirement type names to their corresponding model classes.
360
-
392
+
361
393
  Returns:
362
394
  A dictionary where keys are requirement type names and values are model classes used for requirement evaluation.
363
395
  """
364
396
  from .requirement_evaluation.requirement_type_parser import data_model_dict
397
+
365
398
  return data_model_dict
366
-
399
+
367
400
  @property
368
401
  def active_links(self) -> Dict[str, List]:
369
402
  """Returns a dictionary of linked models containing only non-empty entries.
370
-
403
+
371
404
  The returned dictionary includes only those related model lists that have at least one linked instance.
372
405
  """
373
406
  return self.links.active()
374
-
375
-
376
- def evaluate(self, *args, mode:str, **kwargs):
407
+
408
+ def evaluate(self, input_obj):
377
409
  """
378
410
  Evaluates whether the requirement is satisfied for the given input models using linked operators and gender constraints.
379
-
411
+
380
412
  Args:
381
413
  *args: Instances or QuerySets of expected model classes to be evaluated. Each must have a `.links` property returning a `RequirementLinks` object.
382
414
  mode: Evaluation mode; "strict" requires all operators to pass, "loose" requires any operator to pass.
383
415
  **kwargs: Additional keyword arguments passed to operator evaluations.
384
-
416
+
385
417
  Returns:
386
418
  True if the requirement is satisfied according to the specified mode, linked operators, and gender restrictions; otherwise, False.
387
-
419
+
388
420
  Raises:
389
421
  ValueError: If an invalid mode is provided.
390
422
  TypeError: If an input is not an instance or QuerySet of expected models, or lacks a valid `.links` attribute.
391
-
423
+
392
424
  If the requirement specifies genders, only input containing a patient with a matching gender will be considered valid for evaluation.
393
425
  """
394
- #TODO Review, Optimize or remove
395
- if mode not in ["strict", "loose"]:
396
- raise ValueError(f"Invalid mode: {mode}. Use 'strict' or 'loose'.")
426
+ is_valid: bool = False
397
427
 
398
- evaluate_result_list_func = all if mode == "strict" else any
428
+ requirement_req_links = self.active_links
399
429
 
400
- requirement_req_links = self.links
401
- expected_models = self.expected_models
430
+ # expected_models = self.expected_models
402
431
 
403
- # helpers to avoid passing a complex tuple to isinstance/issubclass which confuses type checkers
404
- def _is_expected_instance(obj) -> bool:
405
- for cls in expected_models:
406
- if isinstance(cls, type):
407
- try:
408
- if isinstance(obj, cls):
409
- return True
410
- except Exception:
411
- # cls might not be a runtime type
412
- continue
413
- return False
432
+ operator = self.operator
433
+ assert isinstance(operator, RequirementOperator)
414
434
 
415
- def _is_queryset_of_expected(qs) -> bool:
416
- if not isinstance(qs, models.QuerySet) or not hasattr(qs, 'model'):
417
- return False
418
- for cls in expected_models:
419
- if isinstance(cls, type):
420
- try:
421
- if issubclass(qs.model, cls):
422
- return True
423
- except Exception:
424
- continue
425
- return False
435
+ operator_instructions = self.operator_instructions_parsed
426
436
 
427
- # Aggregate RequirementLinks from all input arguments
428
- aggregated_input_links_data = {}
429
- processed_inputs_count = 0
430
-
431
- for _input in args:
432
- # Check if the input is an instance of any of the expected model types
433
- if not _is_expected_instance(_input):
434
- # Allow QuerySets of expected models
435
- if _is_queryset_of_expected(_input):
436
- # For QuerySets, evaluate each item individually and return True if any matches
437
- if not _input.exists(): # Skip empty querysets
438
- continue
439
-
440
- queryset_results = []
441
- for item in _input:
442
- if not hasattr(item, 'links') or not isinstance(item.links, RequirementLinks):
443
- raise TypeError(
444
- f"Item {item} of type {type(item)} in QuerySet does not have a valid .links attribute of type RequirementLinks."
445
- )
446
-
447
- # Evaluate this single item against the requirement
448
- item_input_links = RequirementLinks(**item.links.active())
449
-
450
- # Evaluate all operators for this single item
451
- item_operator_results = []
452
- for operator in self.operators.all():
453
- try:
454
- operator_result = operator.evaluate(
455
- requirement_links=requirement_req_links,
456
- input_links=item_input_links,
457
- requirement=self,
458
- original_input_args=args,
459
- **kwargs
460
- )
461
- item_operator_results.append(operator_result)
462
- except Exception as e:
463
- logger.debug(f"Operator {operator.name} evaluation failed for item {item}: {e}")
464
- item_operator_results.append(False)
465
-
466
- # Apply evaluation mode for this single item
467
- item_result = evaluate_result_list_func(item_operator_results) if item_operator_results else True
468
- queryset_results.append(item_result)
469
- processed_inputs_count += 1
470
-
471
- # If any item in the QuerySet matches, return True for the whole QuerySet evaluation
472
- if any(queryset_results):
473
- return True
474
- continue # Move to the next arg after processing queryset
475
- else:
476
- raise TypeError(
477
- f"Input type {type(_input)} is not among expected models: {self.expected_models} "
478
- f"nor a QuerySet of expected models."
479
- )
480
-
481
- # Process single model instance
482
- if not hasattr(_input, 'links') or not isinstance(_input.links, RequirementLinks):
483
- raise TypeError(
484
- f"Input {_input} of type {type(_input)} does not have a valid .links attribute of type RequirementLinks."
485
- )
486
-
487
- active_input_links = _input.links.active() # Get dict of non-empty lists
488
- for link_key, link_list in active_input_links.items():
489
- if link_key not in aggregated_input_links_data:
490
- aggregated_input_links_data[link_key] = []
491
- aggregated_input_links_data[link_key].extend(link_list)
492
- processed_inputs_count += 1
493
-
494
- if not processed_inputs_count and args: # If args were provided but none were processable (e.g. all empty querysets)
495
- # This situation implies no relevant data was provided for evaluation against the requirement.
496
- # Depending on operator logic (e.g., "requires at least one matching item"), this might lead to False.
497
- # For "models_match_any", an empty input_links will likely result in False if requirement_req_links is not empty.
498
- pass
499
-
500
-
501
- # Deduplicate items within each list after aggregation
502
- for key in aggregated_input_links_data:
503
- try:
504
- # Using dict.fromkeys to preserve order and remove duplicates for hashable items
505
- aggregated_input_links_data[key] = list(dict.fromkeys(aggregated_input_links_data[key]))
506
- except TypeError:
507
- # Fallback for non-hashable items (though Django models are hashable)
508
- temp_list = []
509
- for item in aggregated_input_links_data[key]:
510
- if item not in temp_list:
511
- temp_list.append(item)
512
- aggregated_input_links_data[key] = temp_list
513
-
514
- final_input_links = RequirementLinks(**aggregated_input_links_data)
515
-
516
- # Gender strict check: if this requirement has genders, only pass if patient.gender is in the set
517
- genders_exist = self.genders.exists()
518
- if genders_exist:
519
- # Import here to avoid circular import
520
- from endoreg_db.models.administration.person.patient import Patient
521
- patient = None
522
- for arg in args:
523
- if isinstance(arg, Patient):
524
- patient = arg
525
- break
526
- if patient is None or patient.gender is None:
527
- return False
528
- if not self.genders.filter(pk=patient.gender.pk).exists():
529
- return False
530
-
531
- operators = self.operators.all()
532
- if not operators.exists(): # If a requirement has no operators, its evaluation is ambiguous.
533
- # Consider if this should be True, False, or an error.
534
- # For now, if no operators, and mode is strict, it's vacuously true. If loose, vacuously false.
535
- # However, typically a requirement implies some condition.
536
- # Let's assume if no operators, it cannot be satisfied unless it also has no specific links.
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
-
542
-
543
- operator_results = []
544
- for operator in operators:
545
- # Prepare kwargs for the operator, including the current Requirement instance
546
- op_kwargs = kwargs.copy() # Start with kwargs passed to Requirement.evaluate
547
- op_kwargs['requirement'] = self # Add the Requirement instance itself
548
- op_kwargs['original_input_args'] = args # Add the original input arguments for operators that need them (e.g., age operators)
549
- operator_results.append(operator.evaluate(
550
- requirement_links=requirement_req_links,
551
- input_links=final_input_links,
552
- **op_kwargs
553
- ))
554
-
555
- is_valid = evaluate_result_list_func(operator_results)
437
+ is_valid = operator.evaluate(input_links)
556
438
 
557
439
  return is_valid
558
440
 
559
- def evaluate_with_details(self, *args, mode:str, **kwargs):
441
+ def evaluate_with_details(self, *args, mode: str, **kwargs) -> Tuple[bool, str]:
560
442
  """
561
443
  Evaluates whether the requirement is satisfied for the given input models using linked operators and gender constraints.
562
-
444
+
563
445
  Args:
564
446
  *args: Instances or QuerySets of expected model classes to be evaluated. Each must have a `.links` property returning a `RequirementLinks` object.
565
447
  mode: Evaluation mode; "strict" requires all operators to pass, "loose" requires any operator to pass.
566
448
  **kwargs: Additional keyword arguments passed to operator evaluations.
567
-
449
+
568
450
  Returns:
569
- True if the requirement is satisfied according to the specified mode, linked operators, and gender restrictions; otherwise, False.
570
-
451
+ (met, details):
452
+ met: True/False, ob die Voraussetzung erfüllt ist
453
+ details: menschenlesbare Erklärung (für UI geeignet)
454
+
571
455
  Raises:
572
- ValueError: If an invalid mode is provided.
573
- TypeError: If an input is not an instance or QuerySet of expected models, or lacks a valid `.links` attribute.
574
-
575
- If the requirement specifies genders, only input containing a patient with a matching gender will be considered valid for evaluation.
456
+ RequirementEvaluationError:
457
+ - bei ungültigem Modus
458
+ - bei komplett falschen Input-Typen / fehlender .links-Struktur
576
459
  """
577
- #TODO Review, Optimize or remove
460
+ from endoreg_db.models.requirement.requirement_error import RequirementEvaluationError
461
+
462
+ # --- Mode validieren -------------------------------------------------
578
463
  if mode not in ["strict", "loose"]:
579
- raise ValueError(f"Invalid mode: {mode}. Use 'strict' or 'loose'.")
464
+ raise RequirementEvaluationError(
465
+ requirement=self,
466
+ code="INVALID_MODE",
467
+ technical_message=f"Invalid mode: {mode}. Use 'strict' or 'loose'.",
468
+ user_message=(
469
+ "Diese Voraussetzung ist intern mit einem ungültigen Bewertungsmodus konfiguriert und kann aktuell nicht korrekt geprüft werden."
470
+ ),
471
+ )
580
472
 
581
473
  evaluate_result_list_func = all if mode == "strict" else any
582
474
 
583
475
  requirement_req_links = self.links
584
476
  expected_models = self.expected_models
585
477
 
586
- # helpers to avoid passing a complex tuple to isinstance/issubclass which confuses type checkers
478
+ operators = list(self.operators.all())
479
+ has_operators = bool(operators)
480
+ requirement_has_conditions = bool(requirement_req_links.active())
481
+ queryset_mode, queryset_min_count = self._resolve_queryset_config(kwargs)
482
+
483
+ # --- Helper für Typprüfung ------------------------------------------
587
484
  def _is_expected_instance(obj) -> bool:
588
485
  for cls in expected_models:
589
486
  if isinstance(cls, type):
@@ -591,12 +488,12 @@ class Requirement(models.Model):
591
488
  if isinstance(obj, cls):
592
489
  return True
593
490
  except Exception:
594
- # cls might not be a runtime type
491
+ # cls might nicht runtime-kompatibel sein
595
492
  continue
596
493
  return False
597
494
 
598
495
  def _is_queryset_of_expected(qs) -> bool:
599
- if not isinstance(qs, models.QuerySet) or not hasattr(qs, 'model'):
496
+ if not isinstance(qs, models.QuerySet) or not hasattr(qs, "model"):
600
497
  return False
601
498
  for cls in expected_models:
602
499
  if isinstance(cls, type):
@@ -607,146 +504,217 @@ class Requirement(models.Model):
607
504
  continue
608
505
  return False
609
506
 
610
- # Aggregate RequirementLinks from all input arguments
611
- aggregated_input_links_data = {}
507
+ # --- RequirementLinks aus allen Inputs aggregieren -------------------
508
+ aggregated_input_links_data: dict = {}
612
509
  processed_inputs_count = 0
613
510
 
614
511
  for _input in args:
615
- # Check if the input is an instance of any of the expected model types
616
512
  if not _is_expected_instance(_input):
617
- # Allow QuerySets of expected models
513
+ # QuerySet von erwarteten Typen erlauben
618
514
  if _is_queryset_of_expected(_input):
619
- # For QuerySets, evaluate each item individually and return True if any matches
620
- if not _input.exists(): # Skip empty querysets
515
+ if not _input.exists():
516
+ # leeres QS -> je nach QS-Mode sofort nicht erfüllt
517
+ if queryset_mode == "all":
518
+ return (
519
+ False,
520
+ "Für diese Voraussetzung müssen alle passenden Einträge vorliegen, aber es wurden keine entsprechenden Datensätze gefunden.",
521
+ )
522
+ if queryset_mode == "min_count":
523
+ required = queryset_min_count if queryset_min_count is not None else 1
524
+ if required > 0:
525
+ return (
526
+ False,
527
+ f"Für diese Voraussetzung werden mindestens {required} passende Einträge benötigt, es wurden jedoch keine gefunden.",
528
+ )
529
+ # queryset_mode == "any" bei leerem QS -> neutral (keine zusätzliche Einschränkung)
621
530
  continue
622
-
623
- queryset_results = []
531
+
532
+ queryset_results: List[bool] = []
533
+ queryset_true_count = 0
534
+ queryset_item_count = 0
535
+
624
536
  for item in _input:
625
- if not hasattr(item, 'links') or not isinstance(item.links, RequirementLinks):
626
- raise TypeError(
627
- f"Item {item} of type {type(item)} in QuerySet does not have a valid .links attribute of type RequirementLinks."
537
+ if not hasattr(item, "links") or not isinstance(item.links, RequirementLinks):
538
+ raise RequirementEvaluationError(
539
+ requirement=self,
540
+ code="MISSING_LINKS_ATTR",
541
+ technical_message=(
542
+ f"Item {item} of type {type(item)} in QuerySet does not have a valid .links attribute of type RequirementLinks."
543
+ ),
544
+ user_message=(
545
+ "Für einen Datensatz fehlen die intern benötigten Verknüpfungen, "
546
+ "sodass diese Voraussetzung nicht korrekt geprüft werden kann."
547
+ ),
548
+ meta={"item_type": str(type(item))},
628
549
  )
629
-
630
- # Evaluate this single item against the requirement
631
- item_input_links = RequirementLinks(**item.links.active())
632
-
633
- # Evaluate all operators for this single item
634
- item_operator_results = []
635
- for operator in self.operators.all():
636
- try:
637
- operator_result = operator.evaluate(
638
- requirement_links=requirement_req_links,
639
- input_links=item_input_links,
640
- requirement=self,
641
- original_input_args=args,
642
- **kwargs
643
- )
644
- item_operator_results.append(operator_result)
645
- except Exception as e:
646
- logger.debug(f"Operator {operator.name} evaluation failed for item {item}: {e}")
647
- item_operator_results.append(False)
648
-
649
- # Apply evaluation mode for this single item
650
- item_result = evaluate_result_list_func(item_operator_results) if item_operator_results else True
550
+
551
+ item_active_links = item.links.active()
552
+ item_input_links = RequirementLinks(**item_active_links)
553
+
554
+ # Links sammeln
555
+ for link_key, link_list in item_active_links.items():
556
+ if link_key not in aggregated_input_links_data:
557
+ aggregated_input_links_data[link_key] = []
558
+ aggregated_input_links_data[link_key].extend(link_list)
559
+
560
+ per_item_args = tuple(item if arg is _input else arg for arg in args)
561
+ op_kwargs = kwargs.copy()
562
+ op_kwargs["requirement"] = self
563
+ op_kwargs["original_input_args"] = per_item_args
564
+
565
+ if has_operators:
566
+ item_operator_results: List[bool] = []
567
+ for operator in operators:
568
+ try:
569
+ operator_result = operator.evaluate(
570
+ requirement_links=requirement_req_links,
571
+ input_links=item_input_links,
572
+ **op_kwargs,
573
+ )
574
+ item_operator_results.append(operator_result)
575
+ except Exception as exc:
576
+ logger.debug(
577
+ "Operator %s evaluation failed for item %s: %s",
578
+ getattr(operator, "name", "unknown"),
579
+ item,
580
+ exc,
581
+ )
582
+ item_operator_results.append(False)
583
+ item_result = evaluate_result_list_func(item_operator_results) if item_operator_results else True
584
+ else:
585
+ # keine Operatoren -> Bedingung erfüllt, wenn Requirement selbst keine Bedingungen hat
586
+ item_result = not requirement_has_conditions
587
+
651
588
  queryset_results.append(item_result)
589
+ if item_result:
590
+ queryset_true_count += 1
591
+ queryset_item_count += 1
652
592
  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
-
664
- # Process single model instance
665
- if not hasattr(_input, 'links') or not isinstance(_input.links, RequirementLinks):
666
- raise TypeError(
667
- f"Input {_input} of type {type(_input)} does not have a valid .links attribute of type RequirementLinks."
593
+
594
+ # QS-Modus nach Auswertung anwenden
595
+ if queryset_mode == "all":
596
+ if queryset_item_count == 0 or not all(queryset_results):
597
+ return (
598
+ False,
599
+ "Für diese Voraussetzung müssen alle relevanten Einträge die Bedingung erfüllen.",
600
+ )
601
+ elif queryset_mode == "min_count":
602
+ required = queryset_min_count if queryset_min_count is not None else 1
603
+ if queryset_true_count < max(required, 0):
604
+ return (
605
+ False,
606
+ f"Für diese Voraussetzung werden mindestens {max(required, 0)} passende Einträge benötigt (gefunden: {queryset_true_count}).",
607
+ )
608
+ # queryset_mode == "any": keine zusätzliche Einschränkung
609
+ continue
610
+
611
+ # Weder Instanz noch QS eines erwarteten Modells -> Konfig-/Aufruf-Fehler
612
+ raise RequirementEvaluationError(
613
+ requirement=self,
614
+ code="INVALID_INPUT_TYPE",
615
+ technical_message=(f"Input type {type(_input)} is not among expected models: {self.expected_models} nor a QuerySet of expected models."),
616
+ user_message=("Diese Voraussetzung wurde mit einem nicht passenden Datentyp aufgerufen und kann aktuell nicht korrekt geprüft werden."),
617
+ meta={"input_type": str(type(_input))},
618
+ )
619
+
620
+ # Einzelinstanz erwarteten Typs
621
+ if not hasattr(_input, "links") or not isinstance(_input.links, RequirementLinks):
622
+ raise RequirementEvaluationError(
623
+ requirement=self,
624
+ code="MISSING_LINKS_ATTR",
625
+ technical_message=(f"Input {_input} of type {type(_input)} does not have a valid .links attribute of type RequirementLinks."),
626
+ user_message=("Für die Auswertung dieser Voraussetzung fehlen die intern benötigten Verknüpfungsinformationen."),
627
+ meta={"input_type": str(type(_input))},
668
628
  )
669
-
670
- active_input_links = _input.links.active() # Get dict of non-empty lists
629
+
630
+ active_input_links = _input.links.active()
671
631
  for link_key, link_list in active_input_links.items():
672
632
  if link_key not in aggregated_input_links_data:
673
633
  aggregated_input_links_data[link_key] = []
674
634
  aggregated_input_links_data[link_key].extend(link_list)
675
635
  processed_inputs_count += 1
676
636
 
677
- if not processed_inputs_count and args: # If args were provided but none were processable (e.g. all empty querysets)
678
- # This situation implies no relevant data was provided for evaluation against the requirement.
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
-
637
+ # Wenn es zwar *args gibt, aber alles leer/irrelevant war, lassen wir das weiterlaufen.
638
+ # Operatoren sehen dann ggf. ein leeres final_input_links.
683
639
 
684
- # Deduplicate items within each list after aggregation
640
+ # Deduplizieren der aggregierten Links
685
641
  for key in aggregated_input_links_data:
686
642
  try:
687
- # Using dict.fromkeys to preserve order and remove duplicates for hashable items
688
643
  aggregated_input_links_data[key] = list(dict.fromkeys(aggregated_input_links_data[key]))
689
644
  except TypeError:
690
- # Fallback for non-hashable items (though Django models are hashable)
691
- temp_list = []
645
+ # Fallback für nicht-hashbare Items
646
+ tmp: list = []
692
647
  for item in aggregated_input_links_data[key]:
693
- if item not in temp_list:
694
- temp_list.append(item)
695
- aggregated_input_links_data[key] = temp_list
696
-
648
+ if item not in tmp:
649
+ tmp.append(item)
650
+ aggregated_input_links_data[key] = tmp
651
+
697
652
  final_input_links = RequirementLinks(**aggregated_input_links_data)
698
-
699
- # Gender strict check: if this requirement has genders, only pass if patient.gender is in the set
653
+
654
+ # --- Gender-Check ----------------------------------------------------
700
655
  genders_exist = self.genders.exists()
701
656
  if genders_exist:
702
- # Import here to avoid circular import
703
657
  from endoreg_db.models.administration.person.patient import Patient
658
+
704
659
  patient = None
705
660
  for arg in args:
706
661
  if isinstance(arg, Patient):
707
662
  patient = arg
708
663
  break
664
+
709
665
  if patient is None or patient.gender is None:
710
- return False
666
+ return (
667
+ False,
668
+ "Für diese Voraussetzung ist ein hinterlegtes Geschlecht des Patienten erforderlich.",
669
+ )
670
+
711
671
  if not self.genders.filter(pk=patient.gender.pk).exists():
712
- return False
672
+ return (
673
+ False,
674
+ "Diese Voraussetzung gilt nur für bestimmte Geschlechter und ist für diesen Patienten nicht erfüllt.",
675
+ )
713
676
 
714
- operators = self.operators.all()
715
- if not operators.exists(): # If a requirement has no operators, its evaluation is ambiguous.
716
- # Consider if this should be True, False, or an error.
717
- # For now, if no operators, and mode is strict, it's vacuously true. If loose, vacuously false.
718
- # However, typically a requirement implies some condition.
719
- # Let's assume if no operators, it cannot be satisfied unless it also has no specific links.
720
- # This behavior might need further refinement based on business logic.
721
- if not requirement_req_links.active(): # No conditions in requirement
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
677
+ # --- Fall: keine Operatoren -----------------------------------------
678
+ if not has_operators:
679
+ if not requirement_has_conditions:
680
+ return True, "Keine Operatoren für die Bewertung erforderlich."
681
+ return (
682
+ False,
683
+ "Die Voraussetzung besitzt Bedingungen, aber keinen Operator zur Auswertung.",
684
+ )
724
685
 
686
+ # --- Operatoren anwenden --------------------------------------------
687
+ operator_results: List[bool] = []
688
+ operator_details: List[str] = []
725
689
 
726
- operator_results = []
727
- operator_details = []
728
690
  for operator in operators:
729
- # Prepare kwargs for the operator, including the current Requirement instance
730
- op_kwargs = kwargs.copy() # Start with kwargs passed to Requirement.evaluate
731
- op_kwargs['requirement'] = self # Add the Requirement instance itself
732
- op_kwargs['original_input_args'] = args # Add the original input arguments for operators that need them (e.g., age operators)
691
+ op_kwargs = kwargs.copy()
692
+ op_kwargs["requirement"] = self
693
+ op_kwargs["original_input_args"] = args
694
+
733
695
  try:
734
696
  operator_result = operator.evaluate(
735
697
  requirement_links=requirement_req_links,
736
698
  input_links=final_input_links,
737
- **op_kwargs
699
+ **op_kwargs,
738
700
  )
739
701
  operator_results.append(operator_result)
740
- operator_details.append(f"{operator.name}: {'Passed' if operator_result else 'Failed'}")
702
+ operator_details.append(f"{operator.name}: {'erfüllt' if operator_result else 'nicht erfüllt'}")
741
703
  except Exception as e:
742
704
  operator_results.append(False)
743
- operator_details.append(f"{operator.name}: {str(e)}")
705
+ operator_details.append(f"{operator.name}: technischer Fehler ({e})")
706
+ logger.debug(
707
+ "Operator %s evaluation failed for requirement %s: %s",
708
+ getattr(operator, "name", "unknown"),
709
+ getattr(self, "name", "unknown"),
710
+ e,
711
+ )
744
712
 
745
713
  is_valid = evaluate_result_list_func(operator_results)
746
714
 
747
- # Create detailed feedback
715
+ # --- Detailtext bauen -----------------------------------------------
748
716
  if not operator_results:
749
- details = "Keine Operatoren für die Bewertung verfügbar"
717
+ details = "Keine Operatoren für die Bewertung verfügbar."
750
718
  elif len(operator_results) == 1:
751
719
  details = operator_details[0]
752
720
  else:
@@ -754,14 +722,14 @@ class Requirement(models.Model):
754
722
  if failed_details:
755
723
  details = "; ".join(failed_details)
756
724
  else:
757
- details = "Alle Operatoren erfolgreich"
725
+ details = "Alle verknüpften Bedingungen sind erfüllt."
758
726
 
759
- # Append working directory for debugging convenience
727
+ # Arbeitsverzeichnis als Debug-Helfer anhängen (optional)
760
728
  try:
761
729
  cwd = run("pwd", capture_output=True, text=True).stdout.strip()
762
730
  details = f"{details}\ncwd: {cwd}"
763
731
  except Exception:
764
- # non-fatal: ignore if subprocess fails
732
+ # nicht kritisch
765
733
  pass
766
734
 
767
- return is_valid, details
735
+ return bool(is_valid), details