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.

Files changed (360) 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 +8 -31
  10. endoreg_db/data/_examples/disease.yaml +55 -0
  11. endoreg_db/data/_examples/disease_classification.yaml +13 -0
  12. endoreg_db/data/_examples/disease_classification_choice.yaml +62 -0
  13. endoreg_db/data/_examples/event.yaml +64 -0
  14. endoreg_db/data/_examples/examination.yaml +72 -0
  15. endoreg_db/data/_examples/finding/anatomy_colon.yaml +128 -0
  16. endoreg_db/data/_examples/finding/colonoscopy.yaml +40 -0
  17. endoreg_db/data/_examples/finding/colonoscopy_bowel_prep.yaml +56 -0
  18. endoreg_db/data/_examples/finding/complication.yaml +16 -0
  19. endoreg_db/data/_examples/finding/data.yaml +105 -0
  20. endoreg_db/data/_examples/finding/examination_setting.yaml +16 -0
  21. endoreg_db/data/_examples/finding/medication_related.yaml +18 -0
  22. endoreg_db/data/_examples/finding/outcome.yaml +12 -0
  23. endoreg_db/data/_examples/finding_classification/colonoscopy_bowel_preparation.yaml +68 -0
  24. endoreg_db/data/_examples/finding_classification/colonoscopy_jnet.yaml +22 -0
  25. endoreg_db/data/_examples/finding_classification/colonoscopy_kudo.yaml +25 -0
  26. endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_circularity.yaml +20 -0
  27. endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_planarity.yaml +24 -0
  28. endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_size.yaml +68 -0
  29. endoreg_db/data/_examples/finding_classification/colonoscopy_lesion_surface.yaml +20 -0
  30. endoreg_db/data/_examples/finding_classification/colonoscopy_location.yaml +80 -0
  31. endoreg_db/data/_examples/finding_classification/colonoscopy_lst.yaml +21 -0
  32. endoreg_db/data/_examples/finding_classification/colonoscopy_nice.yaml +20 -0
  33. endoreg_db/data/_examples/finding_classification/colonoscopy_paris.yaml +26 -0
  34. endoreg_db/data/_examples/finding_classification/colonoscopy_sano.yaml +22 -0
  35. endoreg_db/data/_examples/finding_classification/colonoscopy_summary.yaml +53 -0
  36. endoreg_db/data/_examples/finding_classification/complication_generic.yaml +25 -0
  37. endoreg_db/data/_examples/finding_classification/examination_setting_generic.yaml +40 -0
  38. endoreg_db/data/_examples/finding_classification/histology_colo.yaml +51 -0
  39. endoreg_db/data/_examples/finding_classification/intervention_required.yaml +26 -0
  40. endoreg_db/data/_examples/finding_classification/medication_related.yaml +23 -0
  41. endoreg_db/data/_examples/finding_classification/visualized.yaml +33 -0
  42. endoreg_db/data/_examples/finding_classification_choice/bowel_preparation.yaml +78 -0
  43. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_circularity_default.yaml +32 -0
  44. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_jnet.yaml +15 -0
  45. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_kudo.yaml +23 -0
  46. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_lst.yaml +15 -0
  47. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_nice.yaml +17 -0
  48. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_paris.yaml +57 -0
  49. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_planarity_default.yaml +49 -0
  50. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_sano.yaml +14 -0
  51. endoreg_db/data/_examples/finding_classification_choice/colon_lesion_surface_intact_default.yaml +36 -0
  52. endoreg_db/data/_examples/finding_classification_choice/colonoscopy_location.yaml +229 -0
  53. endoreg_db/data/_examples/finding_classification_choice/colonoscopy_not_complete_reason.yaml +19 -0
  54. endoreg_db/data/_examples/finding_classification_choice/colonoscopy_size.yaml +82 -0
  55. endoreg_db/data/_examples/finding_classification_choice/colonoscopy_summary_worst_finding.yaml +15 -0
  56. endoreg_db/data/_examples/finding_classification_choice/complication_generic_types.yaml +15 -0
  57. endoreg_db/data/_examples/finding_classification_choice/examination_setting_generic_types.yaml +15 -0
  58. endoreg_db/data/_examples/finding_classification_choice/histology.yaml +24 -0
  59. endoreg_db/data/_examples/finding_classification_choice/histology_polyp.yaml +20 -0
  60. endoreg_db/data/_examples/finding_classification_choice/outcome.yaml +19 -0
  61. endoreg_db/data/_examples/finding_classification_choice/yes_no_na.yaml +11 -0
  62. endoreg_db/data/_examples/finding_classification_type/colonoscopy_basic.yaml +48 -0
  63. endoreg_db/data/_examples/finding_intervention/endoscopy.yaml +43 -0
  64. endoreg_db/data/_examples/finding_intervention/endoscopy_colonoscopy.yaml +168 -0
  65. endoreg_db/data/_examples/finding_intervention/endoscopy_egd.yaml +128 -0
  66. endoreg_db/data/_examples/finding_intervention/endoscopy_ercp.yaml +32 -0
  67. endoreg_db/data/_examples/finding_intervention/endoscopy_eus_lower.yaml +9 -0
  68. endoreg_db/data/_examples/finding_intervention/endoscopy_eus_upper.yaml +36 -0
  69. endoreg_db/data/_examples/finding_intervention_type/endoscopy.yaml +15 -0
  70. endoreg_db/data/_examples/finding_type/data.yaml +43 -0
  71. endoreg_db/data/_examples/requirement/age.yaml +26 -0
  72. endoreg_db/data/_examples/requirement/colonoscopy_baseline_austria.yaml +45 -0
  73. endoreg_db/data/_examples/requirement/disease_cardiovascular.yaml +79 -0
  74. endoreg_db/data/_examples/requirement/disease_classification_choice_cardiovascular.yaml +41 -0
  75. endoreg_db/data/_examples/requirement/disease_hepatology.yaml +12 -0
  76. endoreg_db/data/_examples/requirement/disease_misc.yaml +12 -0
  77. endoreg_db/data/_examples/requirement/disease_renal.yaml +96 -0
  78. endoreg_db/data/_examples/requirement/endoscopy_bleeding_risk.yaml +59 -0
  79. endoreg_db/data/_examples/requirement/event_cardiology.yaml +251 -0
  80. endoreg_db/data/_examples/requirement/event_requirements.yaml +145 -0
  81. endoreg_db/data/_examples/requirement/finding_colon_polyp.yaml +50 -0
  82. endoreg_db/data/_examples/requirement/gender.yaml +25 -0
  83. endoreg_db/data/_examples/requirement/lab_value.yaml +441 -0
  84. endoreg_db/data/_examples/requirement/medication.yaml +93 -0
  85. endoreg_db/data/_examples/requirement_operator/age.yaml +13 -0
  86. endoreg_db/data/_examples/requirement_operator/lab_operators.yaml +129 -0
  87. endoreg_db/data/_examples/requirement_operator/model_operators.yaml +96 -0
  88. endoreg_db/data/_examples/requirement_set/01_endoscopy_generic.yaml +48 -0
  89. endoreg_db/data/_examples/requirement_set/colonoscopy_austria_screening.yaml +57 -0
  90. endoreg_db/data/_examples/yaml_examples.xlsx +0 -0
  91. endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +4 -3
  92. endoreg_db/data/event_classification/data.yaml +4 -0
  93. endoreg_db/data/event_classification_choice/data.yaml +9 -0
  94. endoreg_db/data/finding_classification/colonoscopy_bowel_preparation.yaml +43 -70
  95. endoreg_db/data/finding_classification/colonoscopy_lesion_size.yaml +22 -52
  96. endoreg_db/data/finding_classification/colonoscopy_location.yaml +31 -62
  97. endoreg_db/data/finding_classification/histology_colo.yaml +28 -36
  98. endoreg_db/data/requirement/colon_polyp_intervention.yaml +49 -0
  99. endoreg_db/data/requirement/coloreg_colon_polyp.yaml +49 -0
  100. endoreg_db/data/requirement_set/01_endoscopy_generic.yaml +31 -12
  101. endoreg_db/data/requirement_set/01_laboratory.yaml +13 -0
  102. endoreg_db/data/requirement_set/02_endoscopy_bleeding_risk.yaml +46 -0
  103. endoreg_db/data/requirement_set/90_coloreg.yaml +178 -0
  104. endoreg_db/data/requirement_set/_old_ +109 -0
  105. endoreg_db/data/requirement_set_type/data.yaml +21 -0
  106. endoreg_db/data/setup_config.yaml +4 -4
  107. endoreg_db/data/tag/requirement_set_tags.yaml +21 -0
  108. endoreg_db/exceptions.py +5 -2
  109. endoreg_db/helpers/data_loader.py +1 -1
  110. endoreg_db/management/commands/create_model_meta_from_huggingface.py +21 -10
  111. endoreg_db/management/commands/create_multilabel_model_meta.py +299 -129
  112. endoreg_db/management/commands/import_video.py +9 -10
  113. endoreg_db/management/commands/import_video_with_classification.py +1 -1
  114. endoreg_db/management/commands/init_default_ai_model.py +1 -1
  115. endoreg_db/management/commands/list_routes.py +18 -0
  116. endoreg_db/management/commands/load_center_data.py +12 -12
  117. endoreg_db/management/commands/load_requirement_data.py +60 -31
  118. endoreg_db/management/commands/load_requirement_set_tags.py +95 -0
  119. endoreg_db/management/commands/setup_endoreg_db.py +3 -3
  120. endoreg_db/management/commands/storage_management.py +271 -203
  121. endoreg_db/migrations/0001_initial.py +1799 -1300
  122. endoreg_db/migrations/0002_requirementset_depends_on.py +18 -0
  123. endoreg_db/migrations/_old/0001_initial.py +1857 -0
  124. endoreg_db/migrations/_old/0004_employee_city_employee_post_code_employee_street_and_more.py +68 -0
  125. endoreg_db/migrations/_old/0004_remove_casetemplate_rules_and_more.py +77 -0
  126. endoreg_db/migrations/_old/0005_merge_20251111_1003.py +14 -0
  127. endoreg_db/migrations/_old/0006_sensitivemeta_anonymized_text_and_more.py +68 -0
  128. endoreg_db/migrations/_old/0007_remove_rule_attribute_dtype_remove_rule_rule_type_and_more.py +89 -0
  129. endoreg_db/migrations/_old/0008_remove_event_event_classification_and_more.py +27 -0
  130. endoreg_db/migrations/_old/0009_alter_modelmeta_options_and_more.py +21 -0
  131. endoreg_db/models/__init__.py +78 -123
  132. endoreg_db/models/administration/__init__.py +21 -42
  133. endoreg_db/models/administration/ai/active_model.py +2 -2
  134. endoreg_db/models/administration/ai/ai_model.py +7 -6
  135. endoreg_db/models/administration/case/__init__.py +1 -15
  136. endoreg_db/models/administration/case/case.py +3 -3
  137. endoreg_db/models/administration/case/case_template/__init__.py +2 -14
  138. endoreg_db/models/administration/case/case_template/case_template.py +2 -124
  139. endoreg_db/models/administration/case/case_template/case_template_rule.py +2 -268
  140. endoreg_db/models/administration/case/case_template/case_template_rule_value.py +2 -85
  141. endoreg_db/models/administration/case/case_template/case_template_type.py +2 -25
  142. endoreg_db/models/administration/center/center.py +33 -19
  143. endoreg_db/models/administration/center/center_product.py +12 -9
  144. endoreg_db/models/administration/center/center_resource.py +25 -19
  145. endoreg_db/models/administration/center/center_shift.py +21 -17
  146. endoreg_db/models/administration/center/center_waste.py +16 -8
  147. endoreg_db/models/administration/person/__init__.py +2 -0
  148. endoreg_db/models/administration/person/employee/employee.py +10 -5
  149. endoreg_db/models/administration/person/employee/employee_qualification.py +9 -4
  150. endoreg_db/models/administration/person/employee/employee_type.py +12 -6
  151. endoreg_db/models/administration/person/examiner/examiner.py +13 -11
  152. endoreg_db/models/administration/person/patient/__init__.py +2 -0
  153. endoreg_db/models/administration/person/patient/patient.py +103 -100
  154. endoreg_db/models/administration/person/patient/patient_external_id.py +37 -0
  155. endoreg_db/models/administration/person/person.py +4 -0
  156. endoreg_db/models/administration/person/profession/__init__.py +8 -4
  157. endoreg_db/models/administration/person/user/portal_user_information.py +11 -7
  158. endoreg_db/models/administration/product/product.py +20 -15
  159. endoreg_db/models/administration/product/product_material.py +17 -18
  160. endoreg_db/models/administration/product/product_weight.py +12 -8
  161. endoreg_db/models/administration/product/reference_product.py +23 -55
  162. endoreg_db/models/administration/qualification/qualification.py +7 -3
  163. endoreg_db/models/administration/qualification/qualification_type.py +7 -3
  164. endoreg_db/models/administration/shift/scheduled_days.py +8 -5
  165. endoreg_db/models/administration/shift/shift.py +16 -12
  166. endoreg_db/models/administration/shift/shift_type.py +23 -31
  167. endoreg_db/models/label/__init__.py +7 -8
  168. endoreg_db/models/label/annotation/image_classification.py +10 -9
  169. endoreg_db/models/label/annotation/video_segmentation_annotation.py +8 -5
  170. endoreg_db/models/label/label.py +15 -15
  171. endoreg_db/models/label/label_set.py +19 -6
  172. endoreg_db/models/label/label_type.py +1 -1
  173. endoreg_db/models/label/label_video_segment/_create_from_video.py +5 -8
  174. endoreg_db/models/label/label_video_segment/label_video_segment.py +76 -102
  175. endoreg_db/models/label/video_segmentation_label.py +4 -0
  176. endoreg_db/models/label/video_segmentation_labelset.py +4 -3
  177. endoreg_db/models/media/frame/frame.py +22 -22
  178. endoreg_db/models/media/pdf/raw_pdf.py +110 -182
  179. endoreg_db/models/media/pdf/report_file.py +25 -29
  180. endoreg_db/models/media/pdf/report_reader/report_reader_config.py +30 -46
  181. endoreg_db/models/media/pdf/report_reader/report_reader_flag.py +23 -7
  182. endoreg_db/models/media/video/__init__.py +1 -0
  183. endoreg_db/models/media/video/create_from_file.py +48 -56
  184. endoreg_db/models/media/video/pipe_2.py +8 -9
  185. endoreg_db/models/media/video/video_file.py +150 -108
  186. endoreg_db/models/media/video/video_file_ai.py +288 -74
  187. endoreg_db/models/media/video/video_file_anonymize.py +38 -38
  188. endoreg_db/models/media/video/video_file_frames/__init__.py +3 -1
  189. endoreg_db/models/media/video/video_file_frames/_bulk_create_frames.py +6 -8
  190. endoreg_db/models/media/video/video_file_frames/_create_frame_object.py +7 -9
  191. endoreg_db/models/media/video/video_file_frames/_delete_frames.py +9 -8
  192. endoreg_db/models/media/video/video_file_frames/_extract_frames.py +38 -45
  193. endoreg_db/models/media/video/video_file_frames/_get_frame.py +6 -8
  194. endoreg_db/models/media/video/video_file_frames/_get_frame_number.py +4 -18
  195. endoreg_db/models/media/video/video_file_frames/_get_frame_path.py +4 -3
  196. endoreg_db/models/media/video/video_file_frames/_get_frame_paths.py +7 -6
  197. endoreg_db/models/media/video/video_file_frames/_get_frame_range.py +6 -8
  198. endoreg_db/models/media/video/video_file_frames/_get_frames.py +6 -8
  199. endoreg_db/models/media/video/video_file_frames/_initialize_frames.py +15 -25
  200. endoreg_db/models/media/video/video_file_frames/_manage_frame_range.py +26 -23
  201. endoreg_db/models/media/video/video_file_frames/_mark_frames_extracted_status.py +23 -14
  202. endoreg_db/models/media/video/video_file_io.py +109 -62
  203. endoreg_db/models/media/video/video_file_meta/get_crop_template.py +3 -3
  204. endoreg_db/models/media/video/video_file_meta/get_endo_roi.py +5 -3
  205. endoreg_db/models/media/video/video_file_meta/get_fps.py +37 -34
  206. endoreg_db/models/media/video/video_file_meta/initialize_video_specs.py +19 -25
  207. endoreg_db/models/media/video/video_file_meta/text_meta.py +41 -38
  208. endoreg_db/models/media/video/video_file_meta/video_meta.py +14 -7
  209. endoreg_db/models/media/video/video_file_segments.py +24 -17
  210. endoreg_db/models/media/video/video_metadata.py +19 -35
  211. endoreg_db/models/media/video/video_processing.py +96 -95
  212. endoreg_db/models/medical/contraindication/__init__.py +13 -3
  213. endoreg_db/models/medical/disease.py +22 -16
  214. endoreg_db/models/medical/event.py +31 -18
  215. endoreg_db/models/medical/examination/__init__.py +13 -6
  216. endoreg_db/models/medical/examination/examination.py +17 -18
  217. endoreg_db/models/medical/examination/examination_indication.py +26 -25
  218. endoreg_db/models/medical/examination/examination_time.py +16 -6
  219. endoreg_db/models/medical/examination/examination_time_type.py +9 -6
  220. endoreg_db/models/medical/examination/examination_type.py +3 -4
  221. endoreg_db/models/medical/finding/finding.py +38 -39
  222. endoreg_db/models/medical/finding/finding_classification.py +37 -48
  223. endoreg_db/models/medical/finding/finding_intervention.py +27 -22
  224. endoreg_db/models/medical/finding/finding_type.py +13 -12
  225. endoreg_db/models/medical/hardware/endoscope.py +20 -26
  226. endoreg_db/models/medical/hardware/endoscopy_processor.py +2 -2
  227. endoreg_db/models/medical/laboratory/lab_value.py +62 -91
  228. endoreg_db/models/medical/medication/medication.py +22 -10
  229. endoreg_db/models/medical/medication/medication_indication.py +29 -3
  230. endoreg_db/models/medical/medication/medication_indication_type.py +25 -14
  231. endoreg_db/models/medical/medication/medication_intake_time.py +31 -19
  232. endoreg_db/models/medical/medication/medication_schedule.py +27 -16
  233. endoreg_db/models/medical/organ/__init__.py +15 -12
  234. endoreg_db/models/medical/patient/medication_examples.py +1 -5
  235. endoreg_db/models/medical/patient/patient_disease.py +20 -23
  236. endoreg_db/models/medical/patient/patient_event.py +19 -22
  237. endoreg_db/models/medical/patient/patient_examination.py +48 -54
  238. endoreg_db/models/medical/patient/patient_examination_indication.py +16 -14
  239. endoreg_db/models/medical/patient/patient_finding.py +122 -139
  240. endoreg_db/models/medical/patient/patient_finding_classification.py +44 -49
  241. endoreg_db/models/medical/patient/patient_finding_intervention.py +8 -19
  242. endoreg_db/models/medical/patient/patient_lab_sample.py +28 -23
  243. endoreg_db/models/medical/patient/patient_lab_value.py +82 -89
  244. endoreg_db/models/medical/patient/patient_medication.py +27 -38
  245. endoreg_db/models/medical/patient/patient_medication_schedule.py +28 -36
  246. endoreg_db/models/medical/risk/risk.py +7 -6
  247. endoreg_db/models/medical/risk/risk_type.py +8 -5
  248. endoreg_db/models/metadata/model_meta.py +60 -29
  249. endoreg_db/models/metadata/model_meta_logic.py +125 -18
  250. endoreg_db/models/metadata/pdf_meta.py +19 -24
  251. endoreg_db/models/metadata/sensitive_meta.py +102 -85
  252. endoreg_db/models/metadata/sensitive_meta_logic.py +192 -173
  253. endoreg_db/models/metadata/video_meta.py +51 -31
  254. endoreg_db/models/metadata/video_prediction_logic.py +16 -23
  255. endoreg_db/models/metadata/video_prediction_meta.py +29 -33
  256. endoreg_db/models/other/distribution/date_value_distribution.py +89 -29
  257. endoreg_db/models/other/distribution/multiple_categorical_value_distribution.py +21 -5
  258. endoreg_db/models/other/distribution/numeric_value_distribution.py +114 -53
  259. endoreg_db/models/other/distribution/single_categorical_value_distribution.py +4 -3
  260. endoreg_db/models/other/emission/emission_factor.py +18 -8
  261. endoreg_db/models/other/gender.py +10 -5
  262. endoreg_db/models/other/information_source.py +25 -25
  263. endoreg_db/models/other/material.py +9 -5
  264. endoreg_db/models/other/resource.py +6 -4
  265. endoreg_db/models/other/tag.py +10 -5
  266. endoreg_db/models/other/transport_route.py +13 -8
  267. endoreg_db/models/other/unit.py +10 -6
  268. endoreg_db/models/other/waste.py +6 -5
  269. endoreg_db/models/requirement/requirement.py +580 -272
  270. endoreg_db/models/requirement/requirement_error.py +85 -0
  271. endoreg_db/models/requirement/requirement_evaluation/evaluate_with_dependencies.py +268 -0
  272. endoreg_db/models/requirement/requirement_evaluation/operator_evaluation_models.py +3 -6
  273. endoreg_db/models/requirement/requirement_evaluation/requirement_type_parser.py +90 -64
  274. endoreg_db/models/requirement/requirement_operator.py +36 -33
  275. endoreg_db/models/requirement/requirement_set.py +74 -57
  276. endoreg_db/models/state/__init__.py +4 -4
  277. endoreg_db/models/state/abstract.py +2 -2
  278. endoreg_db/models/state/anonymization.py +12 -0
  279. endoreg_db/models/state/audit_ledger.py +46 -47
  280. endoreg_db/models/state/label_video_segment.py +9 -0
  281. endoreg_db/models/state/raw_pdf.py +40 -46
  282. endoreg_db/models/state/sensitive_meta.py +6 -2
  283. endoreg_db/models/state/video.py +58 -53
  284. endoreg_db/models/upload_job.py +32 -55
  285. endoreg_db/models/utils.py +1 -2
  286. endoreg_db/root_urls.py +21 -2
  287. endoreg_db/serializers/__init__.py +0 -2
  288. endoreg_db/serializers/anonymization.py +18 -10
  289. endoreg_db/serializers/meta/report_meta.py +1 -1
  290. endoreg_db/serializers/meta/sensitive_meta_detail.py +63 -118
  291. endoreg_db/serializers/misc/file_overview.py +11 -99
  292. endoreg_db/serializers/requirements/requirement_sets.py +92 -22
  293. endoreg_db/serializers/video/segmentation.py +2 -1
  294. endoreg_db/serializers/video/video_processing_history.py +20 -5
  295. endoreg_db/services/anonymization.py +75 -73
  296. endoreg_db/services/lookup_service.py +37 -24
  297. endoreg_db/services/pdf_import.py +166 -68
  298. endoreg_db/services/storage_aware_video_processor.py +140 -114
  299. endoreg_db/services/video_import.py +193 -283
  300. endoreg_db/urls/__init__.py +7 -20
  301. endoreg_db/urls/media.py +108 -67
  302. endoreg_db/urls/root_urls.py +29 -0
  303. endoreg_db/utils/__init__.py +15 -5
  304. endoreg_db/utils/ai/multilabel_classification_net.py +116 -20
  305. endoreg_db/utils/case_generator/__init__.py +3 -0
  306. endoreg_db/utils/dataloader.py +88 -16
  307. endoreg_db/utils/defaults/set_default_center.py +32 -0
  308. endoreg_db/utils/names.py +22 -16
  309. endoreg_db/utils/permissions.py +2 -1
  310. endoreg_db/utils/pipelines/process_video_dir.py +1 -1
  311. endoreg_db/utils/requirement_operator_logic/model_evaluators.py +414 -127
  312. endoreg_db/utils/setup_config.py +8 -5
  313. endoreg_db/utils/storage.py +115 -0
  314. endoreg_db/utils/validate_endo_roi.py +8 -2
  315. endoreg_db/utils/video/ffmpeg_wrapper.py +184 -188
  316. endoreg_db/views/__init__.py +0 -10
  317. endoreg_db/views/anonymization/media_management.py +198 -163
  318. endoreg_db/views/anonymization/overview.py +4 -1
  319. endoreg_db/views/anonymization/validate.py +174 -40
  320. endoreg_db/views/media/__init__.py +2 -0
  321. endoreg_db/views/media/pdf_media.py +131 -152
  322. endoreg_db/views/media/sensitive_metadata.py +46 -6
  323. endoreg_db/views/media/video_media.py +89 -82
  324. endoreg_db/views/media/video_segments.py +2 -3
  325. endoreg_db/views/meta/sensitive_meta_detail.py +0 -63
  326. endoreg_db/views/patient/patient.py +5 -4
  327. endoreg_db/views/pdf/pdf_stream.py +20 -21
  328. endoreg_db/views/pdf/reimport.py +11 -32
  329. endoreg_db/views/requirement/evaluate.py +188 -187
  330. endoreg_db/views/requirement/lookup.py +17 -3
  331. endoreg_db/views/requirement/requirement_utils.py +89 -0
  332. endoreg_db/views/video/__init__.py +0 -2
  333. endoreg_db/views/video/correction.py +2 -2
  334. {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.0.dist-info}/METADATA +7 -3
  335. {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.0.dist-info}/RECORD +341 -245
  336. endoreg_db/models/administration/permissions/__init__.py +0 -44
  337. endoreg_db/models/media/video/video_file_frames.py +0 -0
  338. endoreg_db/models/metadata/frame_ocr_result.py +0 -0
  339. endoreg_db/models/rule/__init__.py +0 -13
  340. endoreg_db/models/rule/rule.py +0 -27
  341. endoreg_db/models/rule/rule_applicator.py +0 -224
  342. endoreg_db/models/rule/rule_attribute_dtype.py +0 -17
  343. endoreg_db/models/rule/rule_type.py +0 -20
  344. endoreg_db/models/rule/ruleset.py +0 -17
  345. endoreg_db/serializers/video/video_metadata.py +0 -105
  346. endoreg_db/urls/report.py +0 -48
  347. endoreg_db/urls/video.py +0 -61
  348. endoreg_db/utils/case_generator/case_generator.py +0 -159
  349. endoreg_db/utils/case_generator/utils.py +0 -30
  350. endoreg_db/views/report/__init__.py +0 -9
  351. endoreg_db/views/report/report_list.py +0 -112
  352. endoreg_db/views/report/report_with_secure_url.py +0 -28
  353. endoreg_db/views/report/start_examination.py +0 -7
  354. endoreg_db/views.py +0 -0
  355. /endoreg_db/data/{requirement_set → _examples/requirement_set}/endoscopy_bleeding_risk.yaml +0 -0
  356. /endoreg_db/migrations/{0002_add_video_correction_models.py → _old/0002_add_video_correction_models.py} +0 -0
  357. /endoreg_db/migrations/{0003_add_center_display_name.py → _old/0003_add_center_display_name.py} +0 -0
  358. /endoreg_db/{models/media/video/refactor_plan.md → views/pdf/pdf_stream_views.py} +0 -0
  359. {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.0.dist-info}/WHEEL +0 -0
  360. {endoreg_db-0.8.6.1.dist-info → endoreg_db-0.8.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -18,14 +18,14 @@ from endoreg_db.utils.hashs import get_patient_examination_hash, get_patient_has
18
18
  from ..administration import Center, Examiner, FirstName, LastName, Patient
19
19
  from ..medical import PatientExamination
20
20
  from ..other import Gender
21
- from ..state import SensitiveMetaState
22
21
 
23
22
  if TYPE_CHECKING:
24
23
  from .sensitive_meta import SensitiveMeta # Import model for type hinting
25
24
 
26
25
  logger = logging.getLogger(__name__)
27
26
  SECRET_SALT = os.getenv("DJANGO_SALT", "default_salt")
28
- DEFAULT_UNKNOWN_NAME = "unknown"
27
+ DEFAULT_UNKNOWN = "unknown"
28
+
29
29
 
30
30
  # Regex-Pattern für verschiedene Datumsformate
31
31
  ISO_RX = re.compile(r"^\d{4}-\d{2}-\d{2}$")
@@ -80,9 +80,7 @@ def parse_any_date(s: str) -> Optional[date]:
80
80
  # Try dateparser with German locale preference
81
81
  import dateparser
82
82
 
83
- dt = dateparser.parse(
84
- s, settings={"DATE_ORDER": "DMY", "PREFER_DAY_OF_MONTH": "first"}
85
- )
83
+ dt = dateparser.parse(s, settings={"DATE_ORDER": "DMY", "PREFER_DAY_OF_MONTH": "first"})
86
84
  return dt.date() if dt else None
87
85
  except Exception as e:
88
86
  logger.debug(f"Dateparser fallback failed for '{s}': {e}")
@@ -175,9 +173,7 @@ def calculate_patient_hash(instance: "SensitiveMeta", salt: str = SECRET_SALT) -
175
173
  return sha256(hash_str.encode()).hexdigest()
176
174
 
177
175
 
178
- def calculate_examination_hash(
179
- instance: "SensitiveMeta", salt: str = SECRET_SALT
180
- ) -> str:
176
+ def calculate_examination_hash(instance: "SensitiveMeta", salt: str = SECRET_SALT) -> str:
181
177
  """Calculates the examination hash for the instance."""
182
178
  dob = instance.patient_dob
183
179
  first_name = instance.patient_first_name
@@ -192,6 +188,11 @@ def calculate_examination_hash(
192
188
  if not center:
193
189
  raise ValueError("Center is required to calculate examination hash.")
194
190
 
191
+ if not first_name:
192
+ raise ValueError("First name is required to calculate examination hash.")
193
+ if not last_name:
194
+ raise ValueError("Last name is required to calculate examination hash.")
195
+
195
196
  hash_str = get_patient_examination_hash(
196
197
  first_name=first_name,
197
198
  last_name=last_name,
@@ -210,30 +211,22 @@ def create_pseudo_examiner_logic(instance: "SensitiveMeta") -> "Examiner":
210
211
  center = instance.center # Should be set before calling save
211
212
 
212
213
  if not first_name or not last_name or not center:
213
- logger.warning(
214
- f"Incomplete examiner info for SensitiveMeta (pk={instance.pk or 'new'}). Using default examiner."
215
- )
214
+ logger.warning(f"Incomplete examiner info for SensitiveMeta (pk={instance.pk or 'new'}). Using default examiner.")
216
215
  # Ensure default center exists or handle appropriately
217
216
  try:
218
217
  default_center = Center.objects.get(name="endoreg_db_demo")
219
218
  except Center.DoesNotExist:
220
- logger.error(
221
- "Default center 'endoreg_db_demo' not found. Cannot create default examiner."
222
- )
219
+ logger.error("Default center 'endoreg_db_demo' not found. Cannot create default examiner.")
223
220
  raise ValueError("Default center 'endoreg_db_demo' not found.")
224
221
 
225
- examiner, _created = Examiner.custom_get_or_create(
226
- first_name="Unknown", last_name="Unknown", center=default_center
227
- )
222
+ examiner, _created = Examiner.custom_get_or_create(first_name="Unknown", last_name="Unknown", center=default_center)
228
223
  else:
229
- examiner, _created = Examiner.custom_get_or_create(
230
- first_name=first_name, last_name=last_name, center=center
231
- )
224
+ examiner, _created = Examiner.custom_get_or_create(first_name=first_name, last_name=last_name, center=center)
232
225
 
233
226
  return examiner
234
227
 
235
228
 
236
- def get_or_create_pseudo_patient_logic(instance: "SensitiveMeta") -> "Patient":
229
+ def get_or_create_pseudo_patient_logic(instance: "SensitiveMeta"):
237
230
  """Gets or creates the pseudo patient based on instance data."""
238
231
  # Ensure necessary fields are set
239
232
  if not instance.patient_hash:
@@ -256,12 +249,12 @@ def get_or_create_pseudo_patient_logic(instance: "SensitiveMeta") -> "Patient":
256
249
  birth_year=year,
257
250
  birth_month=month,
258
251
  )
259
- return patient
252
+ return patient, _created
260
253
 
261
254
 
262
255
  def get_or_create_pseudo_patient_examination_logic(
263
256
  instance: "SensitiveMeta",
264
- ) -> "PatientExamination":
257
+ ):
265
258
  """Gets or creates the pseudo patient examination based on instance data."""
266
259
  # Ensure necessary fields are set
267
260
  if not instance.patient_hash:
@@ -270,19 +263,17 @@ def get_or_create_pseudo_patient_examination_logic(
270
263
  instance.examination_hash = calculate_examination_hash(instance)
271
264
 
272
265
  # Ensure the pseudo patient exists first, as PatientExamination might depend on it
273
- if not instance.pseudo_patient_id:
274
- pseudo_patient = get_or_create_pseudo_patient_logic(instance)
275
- instance.pseudo_patient_id = pseudo_patient.pk # Assign FK directly
276
-
277
- patient_examination, _created = (
278
- PatientExamination.get_or_create_pseudo_patient_examination_by_hash(
279
- patient_hash=instance.patient_hash,
280
- examination_hash=instance.examination_hash,
281
- # Optionally pass pseudo_patient if the method requires it
282
- # pseudo_patient=instance.pseudo_patient
283
- )
266
+ if not instance.pseudo_patient:
267
+ pseudo_patient, _created = get_or_create_pseudo_patient_logic(instance)
268
+ instance.pseudo_patient = pseudo_patient # Assign FK directly
269
+
270
+ patient_examination, _created = PatientExamination.get_or_create_pseudo_patient_examination_by_hash(
271
+ patient_hash=instance.patient_hash,
272
+ examination_hash=instance.examination_hash,
273
+ # Optionally pass pseudo_patient if the method requires it
274
+ # pseudo_patient=instance.pseudo_patient
284
275
  )
285
- return patient_examination
276
+ return patient_examination, _created
286
277
 
287
278
 
288
279
  @transaction.atomic # Ensure all operations within save succeed or fail together
@@ -343,14 +334,10 @@ def perform_save_logic(instance: "SensitiveMeta") -> "Examiner":
343
334
 
344
335
  # 1. Ensure DOB and Examination Date exist
345
336
  if not instance.patient_dob:
346
- logger.debug(
347
- f"SensitiveMeta (pk={instance.pk or 'new'}): Patient DOB missing, generating random."
348
- )
337
+ logger.debug(f"SensitiveMeta (pk={instance.pk or 'new'}): Patient DOB missing, generating random.")
349
338
  instance.patient_dob = generate_random_dob()
350
339
  if not instance.examination_date:
351
- logger.debug(
352
- f"SensitiveMeta (pk={instance.pk or 'new'}): Examination date missing, generating random."
353
- )
340
+ logger.debug(f"SensitiveMeta (pk={instance.pk or 'new'}): Examination date missing, generating random.")
354
341
  instance.examination_date = generate_random_examination_date()
355
342
 
356
343
  # 2. Ensure Center exists (should be set before calling save)
@@ -381,19 +368,19 @@ def perform_save_logic(instance: "SensitiveMeta") -> "Examiner":
381
368
  # Updated: patient_first_name = "Max" → hash = sha256("Max Mustermann...")
382
369
  #
383
370
  if not instance.patient_first_name:
384
- instance.patient_first_name = DEFAULT_UNKNOWN_NAME
371
+ instance.patient_first_name = DEFAULT_UNKNOWN
385
372
  logger.debug(
386
373
  "SensitiveMeta (pk=%s): Patient first name missing, set to default '%s'.",
387
374
  instance.pk or "new",
388
- DEFAULT_UNKNOWN_NAME,
375
+ DEFAULT_UNKNOWN,
389
376
  )
390
377
 
391
378
  if not instance.patient_last_name:
392
- instance.patient_last_name = DEFAULT_UNKNOWN_NAME
379
+ instance.patient_last_name = DEFAULT_UNKNOWN
393
380
  logger.debug(
394
381
  "SensitiveMeta (pk=%s): Patient last name missing, set to default '%s'.",
395
382
  instance.pk or "new",
396
- DEFAULT_UNKNOWN_NAME,
383
+ DEFAULT_UNKNOWN,
397
384
  )
398
385
 
399
386
  # 3. Ensure Gender exists (should be set before calling save, e.g., during creation/update)
@@ -402,9 +389,7 @@ def perform_save_logic(instance: "SensitiveMeta") -> "Examiner":
402
389
  first_name = instance.patient_first_name
403
390
  gender_str = guess_name_gender(first_name)
404
391
  if not gender_str:
405
- raise ValueError(
406
- "Patient gender could not be determined and must be set before saving."
407
- )
392
+ raise ValueError("Patient gender could not be determined and must be set before saving.")
408
393
  # Convert string to Gender object
409
394
  try:
410
395
  gender_obj = Gender.objects.get(name=gender_str)
@@ -426,13 +411,13 @@ def perform_save_logic(instance: "SensitiveMeta") -> "Examiner":
426
411
 
427
412
  # 5. Get or Create Pseudo Patient (depends on hash, center, gender, dob)
428
413
  # Assign directly to the FK field to avoid premature saving issues
429
- pseudo_patient = get_or_create_pseudo_patient_logic(instance)
430
- instance.pseudo_patient_id = pseudo_patient.pk
414
+ pseudo_patient, _created = get_or_create_pseudo_patient_logic(instance)
415
+ instance.pseudo_patient = pseudo_patient
431
416
 
432
417
  # 6. Get or Create Pseudo Examination (depends on hashes)
433
418
  # Assign directly to the FK field
434
- pseudo_examination = get_or_create_pseudo_patient_examination_logic(instance)
435
- instance.pseudo_examination_id = pseudo_examination.pk
419
+ pseudo_examination, _created = get_or_create_pseudo_patient_examination_logic(instance)
420
+ instance.pseudo_examination = pseudo_examination
436
421
 
437
422
  # 7. Get or Create Pseudo Examiner (depends on names, center)
438
423
  # This needs to happen *after* the main instance has a PK for M2M linking.
@@ -445,9 +430,7 @@ def perform_save_logic(instance: "SensitiveMeta") -> "Examiner":
445
430
  return examiner_instance
446
431
 
447
432
 
448
- def create_sensitive_meta_from_dict(
449
- cls: Type["SensitiveMeta"], data: Dict[str, Any]
450
- ) -> "SensitiveMeta":
433
+ def create_sensitive_meta_from_dict(cls: Type["SensitiveMeta"], data: Dict[str, Any]) -> "SensitiveMeta":
451
434
  """
452
435
  Create a SensitiveMeta instance from a dictionary.
453
436
 
@@ -467,6 +450,8 @@ def create_sensitive_meta_from_dict(
467
450
  "patient_dob": date(1990, 1, 1),
468
451
  "examination_date": date.today(),
469
452
  "center": center_obj, # ← Center object
453
+ "text": text #from extraction
454
+
470
455
  }
471
456
  sm = SensitiveMeta.create_from_dict(data)
472
457
 
@@ -477,6 +462,7 @@ def create_sensitive_meta_from_dict(
477
462
  "patient_dob": date(1990, 1, 1),
478
463
  "examination_date": date.today(),
479
464
  "center_name": "university_hospital_wuerzburg", # ← String
465
+ "anonymized_text": "anonymized text"
480
466
  }
481
467
  sm = SensitiveMeta.create_from_dict(data)
482
468
  ```
@@ -493,11 +479,7 @@ def create_sensitive_meta_from_dict(
493
479
  ValueError: If center_name does not match any Center in database
494
480
  """
495
481
 
496
- field_names = {
497
- f.name
498
- for f in cls._meta.get_fields()
499
- if not f.is_relation or f.one_to_one or (f.many_to_one and f.related_model)
500
- }
482
+ field_names = {f.name for f in cls._meta.get_fields() if not f.is_relation or f.one_to_one or (f.many_to_one and f.related_model)}
501
483
  selected_data = {k: v for k, v in data.items() if k in field_names}
502
484
 
503
485
  # --- Convert patient_dob if it's a date object ---
@@ -524,13 +506,9 @@ def create_sensitive_meta_from_dict(
524
506
  try:
525
507
  import dateparser
526
508
 
527
- parsed_dob = dateparser.parse(
528
- dob, languages=["de"], settings={"DATE_ORDER": "DMY"}
529
- )
509
+ parsed_dob = dateparser.parse(dob, languages=["de"], settings={"DATE_ORDER": "DMY"})
530
510
  if parsed_dob:
531
- aware_dob = timezone.make_aware(
532
- parsed_dob.replace(hour=0, minute=0, second=0, microsecond=0)
533
- )
511
+ aware_dob = timezone.make_aware(parsed_dob.replace(hour=0, minute=0, second=0, microsecond=0))
534
512
  selected_data["patient_dob"] = aware_dob
535
513
  logger.debug(
536
514
  "Parsed string patient_dob '%s' to aware datetime: %s",
@@ -584,9 +562,7 @@ def create_sensitive_meta_from_dict(
584
562
  # Fall back to dateparser for complex formats
585
563
  import dateparser
586
564
 
587
- parsed_date = dateparser.parse(
588
- exam_date, languages=["de"], settings={"DATE_ORDER": "DMY"}
589
- )
565
+ parsed_date = dateparser.parse(exam_date, languages=["de"], settings={"DATE_ORDER": "DMY"})
590
566
  if parsed_date:
591
567
  selected_data["examination_date"] = parsed_date.date()
592
568
  logger.debug(
@@ -604,9 +580,7 @@ def create_sensitive_meta_from_dict(
604
580
  # Use dateparser for non-ISO formats
605
581
  import dateparser
606
582
 
607
- parsed_date = dateparser.parse(
608
- exam_date, languages=["de"], settings={"DATE_ORDER": "DMY"}
609
- )
583
+ parsed_date = dateparser.parse(exam_date, languages=["de"], settings={"DATE_ORDER": "DMY"})
610
584
  if parsed_date:
611
585
  selected_data["examination_date"] = parsed_date.date()
612
586
  logger.debug(
@@ -620,6 +594,7 @@ def create_sensitive_meta_from_dict(
620
594
  exam_date,
621
595
  )
622
596
  selected_data.pop("examination_date", None)
597
+
623
598
  except Exception as e:
624
599
  logger.warning(
625
600
  "Error parsing examination_date string '%s': %s, removing from data",
@@ -648,13 +623,11 @@ def create_sensitive_meta_from_dict(
648
623
  raise ValueError(f"Center with name '{center_name}' does not exist.")
649
624
  else:
650
625
  # Neither center nor center_name provided
651
- raise ValueError(
652
- "Either 'center' (Center object) or 'center_name' (string) is required in data dictionary."
653
- )
626
+ raise ValueError("Either 'center' (Center object) or 'center_name' (string) is required in data dictionary.")
654
627
 
655
628
  # Handle Names and Gender
656
- first_name = selected_data.get("patient_first_name") or DEFAULT_UNKNOWN_NAME
657
- last_name = selected_data.get("patient_last_name") or DEFAULT_UNKNOWN_NAME
629
+ first_name = selected_data.get("patient_first_name") or DEFAULT_UNKNOWN
630
+ last_name = selected_data.get("patient_last_name") or DEFAULT_UNKNOWN
658
631
  selected_data["patient_first_name"] = first_name # Ensure defaults are set
659
632
  selected_data["patient_last_name"] = last_name
660
633
 
@@ -666,34 +639,80 @@ def create_sensitive_meta_from_dict(
666
639
  elif isinstance(patient_gender_input, str):
667
640
  # Input is a string (gender name)
668
641
  try:
669
- selected_data["patient_gender"] = Gender.objects.get(
670
- name=patient_gender_input
671
- )
642
+ selected_data["patient_gender"] = Gender.objects.get(name=patient_gender_input)
672
643
  except Gender.DoesNotExist:
673
- logger.warning(
674
- f"Gender with name '{patient_gender_input}' provided but not found. Attempting to guess or use default."
675
- )
644
+ logger.warning(f"Gender with name '{patient_gender_input}' provided but not found. Attempting to guess or use default.")
676
645
  # Fall through to guessing logic if provided string name is invalid
677
- patient_gender_input = None # Reset to trigger guessing
646
+ normalized = (patient_gender_input or "").lower()
647
+ if normalized in {"male", "female", "unknown"}:
648
+ gender_obj, _ = Gender.objects.get_or_create(
649
+ name=normalized,
650
+ defaults={
651
+ "abbreviation": normalized[:1].upper() or None,
652
+ "description": "Auto-created default gender entry",
653
+ },
654
+ )
655
+ selected_data["patient_gender"] = gender_obj
656
+ else:
657
+ patient_gender_input = None # Reset to trigger guessing
678
658
 
679
- if not isinstance(
680
- selected_data.get("patient_gender"), Gender
681
- ): # If not already a Gender object (e.g. was None, or string lookup failed)
659
+ if not isinstance(selected_data.get("patient_gender"), Gender): # If not already a Gender object (e.g. was None, or string lookup failed)
682
660
  gender_name_to_use = guess_name_gender(first_name)
683
661
  if not gender_name_to_use:
684
- logger.warning(
685
- f"Could not guess gender for name '{first_name}'. Setting Gender to unknown."
686
- )
662
+ logger.warning(f"Could not guess gender for name '{first_name}'. Setting Gender to unknown.")
687
663
  gender_name_to_use = "unknown"
688
664
  try:
689
- selected_data["patient_gender"] = Gender.objects.get(
690
- name=gender_name_to_use
691
- )
665
+ selected_data["patient_gender"] = Gender.objects.get(name=gender_name_to_use)
692
666
  except Gender.DoesNotExist:
693
- # This should ideally not happen if "unknown" gender is guaranteed to exist
694
- raise ValueError(
695
- f"Default or guessed gender '{gender_name_to_use}' does not exist in Gender table."
667
+ gender_obj, _ = Gender.objects.get_or_create(
668
+ name=gender_name_to_use,
669
+ defaults={
670
+ "abbreviation": gender_name_to_use[:1].upper() or None,
671
+ "description": "Auto-created default gender entry",
672
+ },
696
673
  )
674
+ selected_data["patient_gender"] = gender_obj
675
+
676
+ # Handle Text
677
+ selected_data["text"] = data.get("text") or DEFAULT_UNKNOWN
678
+
679
+ # --- Add missing optional fields safely ---
680
+ file_path = data.get("file_path")
681
+ if file_path:
682
+ selected_data["file_path"] = str(file_path)
683
+ logger.debug(f"Set file_path: {file_path}")
684
+
685
+ casenumber = data.get("casenumber")
686
+ if casenumber:
687
+ selected_data["casenumber"] = str(casenumber).strip()
688
+ logger.debug(f"Set casenumber: {casenumber}")
689
+
690
+ exam_time = data.get("examination_time")
691
+ if exam_time:
692
+ try:
693
+ from datetime import time as dt_time
694
+
695
+ # Accepts strings like "14:35" or full datetime
696
+ if isinstance(exam_time, str):
697
+ h, m = exam_time.strip().split(":")[:2]
698
+ selected_data["examination_time"] = dt_time(int(h), int(m))
699
+ elif isinstance(exam_time, datetime):
700
+ selected_data["examination_time"] = exam_time.time()
701
+ elif isinstance(exam_time, date):
702
+ # no time info — ignore
703
+ logger.debug(f"examination_time value {exam_time} has no time component; skipping")
704
+ else:
705
+ selected_data["examination_time"] = exam_time
706
+ except Exception as e:
707
+ logger.warning(f"Invalid examination_time '{exam_time}': {e}")
708
+
709
+ anonymized_text = data.get("anonymized_text") or data.get("anonym_text")
710
+ if anonymized_text:
711
+ if isinstance(anonymized_text, (str, bytes)):
712
+ selected_data["anonymized_text"] = anonymized_text.decode() if isinstance(anonymized_text, bytes) else anonymized_text
713
+ else:
714
+ selected_data["anonymized_text"] = str(anonymized_text)
715
+ logger.debug("Set anonymized_text (length=%d)", len(selected_data["anonymized_text"]))
697
716
 
698
717
  # Update name DB
699
718
  update_name_db(first_name, last_name)
@@ -707,9 +726,7 @@ def create_sensitive_meta_from_dict(
707
726
  return sensitive_meta
708
727
 
709
728
 
710
- def update_sensitive_meta_from_dict(
711
- instance: "SensitiveMeta", data: Dict[str, Any]
712
- ) -> "SensitiveMeta":
729
+ def update_sensitive_meta_from_dict(instance: "SensitiveMeta", data: Dict[str, Any]) -> "SensitiveMeta":
713
730
  """
714
731
  Updates a SensitiveMeta instance from a dictionary of new values.
715
732
 
@@ -754,16 +771,10 @@ def update_sensitive_meta_from_dict(
754
771
  Raises:
755
772
  Exception: If save fails or required conversions fail
756
773
  """
757
- field_names = {
758
- f.name
759
- for f in instance._meta.get_fields()
760
- if not f.is_relation or f.one_to_one or (f.many_to_one and f.related_model)
761
- }
774
+ field_names = {f.name for f in instance._meta.get_fields() if not f.is_relation or f.one_to_one or (f.many_to_one and f.related_model)}
762
775
  # Exclude FKs that should not be updated directly from dict keys (handled separately or via save logic)
763
776
  excluded_fields = {"pseudo_patient", "pseudo_examination"}
764
- selected_data = {
765
- k: v for k, v in data.items() if k in field_names and k not in excluded_fields
766
- }
777
+ selected_data = {k: v for k, v in data.items() if k in field_names and k not in excluded_fields}
767
778
 
768
779
  # Handle potential Center update - accept both center_name (string) and center (object)
769
780
  from ..administration import Center
@@ -777,9 +788,7 @@ def update_sensitive_meta_from_dict(
777
788
  instance.center = center
778
789
  logger.debug(f"Updated center from Center object: {center.name}")
779
790
  else:
780
- logger.warning(
781
- f"Invalid center type {type(center)}, expected Center instance. Ignoring."
782
- )
791
+ logger.warning(f"Invalid center type {type(center)}, expected Center instance. Ignoring.")
783
792
  # Remove from selected_data to prevent override
784
793
  selected_data.pop("center", None)
785
794
  elif center_name:
@@ -789,9 +798,7 @@ def update_sensitive_meta_from_dict(
789
798
  instance.center = center_obj
790
799
  logger.debug(f"Updated center from center_name string: {center_name}")
791
800
  except Center.DoesNotExist:
792
- logger.warning(
793
- f"Center '{center_name}' not found during update. Keeping existing center."
794
- )
801
+ logger.warning(f"Center '{center_name}' not found during update. Keeping existing center.")
795
802
  else:
796
803
  # Both are None/missing - remove 'center' from selected_data to preserve existing value
797
804
  selected_data.pop("center", None)
@@ -814,14 +821,10 @@ def update_sensitive_meta_from_dict(
814
821
  elif isinstance(patient_gender_input, str):
815
822
  gender_input_clean = patient_gender_input.strip()
816
823
  # Try direct case-insensitive DB lookup first
817
- gender_obj = Gender.objects.filter(
818
- name__iexact=gender_input_clean
819
- ).first()
824
+ gender_obj = Gender.objects.filter(name__iexact=gender_input_clean).first()
820
825
  if gender_obj:
821
826
  selected_data["patient_gender"] = gender_obj
822
- logger.debug(
823
- f"Successfully matched gender string '{patient_gender_input}' to Gender object via iexact lookup"
824
- )
827
+ logger.debug(f"Successfully matched gender string '{patient_gender_input}' to Gender object via iexact lookup")
825
828
  else:
826
829
  # Use mapping helper for fallback
827
830
  mapped = _map_gender_string_to_standard(gender_input_clean)
@@ -829,52 +832,52 @@ def update_sensitive_meta_from_dict(
829
832
  gender_obj = Gender.objects.filter(name__iexact=mapped).first()
830
833
  if gender_obj:
831
834
  selected_data["patient_gender"] = gender_obj
832
- logger.info(
833
- f"Mapped gender '{patient_gender_input}' to '{mapped}' via fallback mapping"
834
- )
835
+ logger.info(f"Mapped gender '{patient_gender_input}' to '{mapped}' via fallback mapping")
835
836
  else:
836
- logger.warning(
837
- f"Mapped gender '{patient_gender_input}' to '{mapped}', but no such Gender in DB. Trying 'unknown'."
838
- )
839
- unknown_gender = Gender.objects.filter(
840
- name__iexact="unknown"
841
- ).first()
837
+ logger.warning(f"Mapped gender '{patient_gender_input}' to '{mapped}', but no such Gender in DB. Trying 'unknown'.")
838
+ unknown_gender = Gender.objects.filter(name__iexact="unknown").first()
842
839
  if unknown_gender:
843
840
  selected_data["patient_gender"] = unknown_gender
844
- logger.warning(
845
- f"Using 'unknown' gender as fallback for '{patient_gender_input}'"
846
- )
841
+ logger.warning(f"Using 'unknown' gender as fallback for '{patient_gender_input}'")
847
842
  else:
848
- logger.error(
849
- f"No 'unknown' gender found in database. Cannot handle gender '{patient_gender_input}'. Skipping gender update."
850
- )
843
+ logger.error(f"No 'unknown' gender found in database. Cannot handle gender '{patient_gender_input}'. Skipping gender update.")
851
844
  selected_data.pop("patient_gender", None)
852
845
  else:
853
846
  # Last resort: try to get 'unknown' gender
854
- unknown_gender = Gender.objects.filter(
855
- name__iexact="unknown"
856
- ).first()
847
+ unknown_gender = Gender.objects.filter(name__iexact="unknown").first()
857
848
  if unknown_gender:
858
849
  selected_data["patient_gender"] = unknown_gender
859
- logger.warning(
860
- f"Using 'unknown' gender as fallback for '{patient_gender_input}' (no mapping)"
861
- )
850
+ logger.warning(f"Using 'unknown' gender as fallback for '{patient_gender_input}' (no mapping)")
862
851
  else:
863
- logger.error(
864
- f"No 'unknown' gender found in database. Cannot handle gender '{patient_gender_input}'. Skipping gender update."
865
- )
852
+ logger.error(f"No 'unknown' gender found in database. Cannot handle gender '{patient_gender_input}'. Skipping gender update.")
866
853
  selected_data.pop("patient_gender", None)
867
854
  else:
868
- logger.warning(
869
- f"Unexpected patient_gender type {type(patient_gender_input)}: {patient_gender_input}. Skipping gender update."
870
- )
855
+ logger.warning(f"Unexpected patient_gender type {type(patient_gender_input)}: {patient_gender_input}. Skipping gender update.")
871
856
  selected_data.pop("patient_gender", None)
872
857
  except Exception as e:
873
- logger.exception(
874
- f"Error handling patient_gender '{patient_gender_input}': {e}. Skipping gender update."
875
- )
858
+ logger.exception(f"Error handling patient_gender '{patient_gender_input}': {e}. Skipping gender update.")
876
859
  selected_data.pop("patient_gender", None)
877
860
 
861
+ # TODO Review: Handle new optional fields on update
862
+ for key in ("file_path", "casenumber", "examination_time", "anonymized_text", "anonym_text"):
863
+ if key in data and data[key] is not None:
864
+ val = data[key]
865
+ if key in ("file_path", "casenumber"):
866
+ setattr(instance, key, str(val))
867
+ elif key in ("anonymized_text", "anonym_text"):
868
+ setattr(instance, "anonymized_text", val if isinstance(val, str) else str(val))
869
+ elif key == "examination_time":
870
+ try:
871
+ from datetime import time as dt_time
872
+
873
+ if isinstance(val, str) and ":" in val:
874
+ h, m = val.strip().split(":")[:2]
875
+ setattr(instance, "examination_time", dt_time(int(h), int(m)))
876
+ elif isinstance(val, datetime):
877
+ setattr(instance, "examination_time", val.time())
878
+ except Exception as e:
879
+ logger.warning(f"Skipping invalid examination_time '{val}': {e}")
880
+
878
881
  # Update other attributes from selected_data
879
882
  patient_name_changed = False
880
883
  for k, v in selected_data.items():
@@ -894,9 +897,7 @@ def update_sensitive_meta_from_dict(
894
897
  value_to_set = v
895
898
  if k == "patient_dob":
896
899
  if isinstance(v, date) and not isinstance(v, datetime):
897
- aware_dob = timezone.make_aware(
898
- datetime.combine(v, datetime.min.time())
899
- )
900
+ aware_dob = timezone.make_aware(datetime.combine(v, datetime.min.time()))
900
901
  value_to_set = aware_dob
901
902
  logger.debug(
902
903
  "Converted patient_dob from date to aware datetime during update: %s",
@@ -919,15 +920,9 @@ def update_sensitive_meta_from_dict(
919
920
  try:
920
921
  import dateparser
921
922
 
922
- parsed_dob = dateparser.parse(
923
- v, languages=["de"], settings={"DATE_ORDER": "DMY"}
924
- )
923
+ parsed_dob = dateparser.parse(v, languages=["de"], settings={"DATE_ORDER": "DMY"})
925
924
  if parsed_dob:
926
- value_to_set = timezone.make_aware(
927
- parsed_dob.replace(
928
- hour=0, minute=0, second=0, microsecond=0
929
- )
930
- )
925
+ value_to_set = timezone.make_aware(parsed_dob.replace(hour=0, minute=0, second=0, microsecond=0))
931
926
  logger.debug(
932
927
  "Parsed string patient_dob '%s' during update to aware datetime: %s",
933
928
  v,
@@ -962,9 +957,7 @@ def update_sensitive_meta_from_dict(
962
957
  try:
963
958
  import dateparser
964
959
 
965
- parsed_date = dateparser.parse(
966
- v, languages=["de"], settings={"DATE_ORDER": "DMY"}
967
- )
960
+ parsed_date = dateparser.parse(v, languages=["de"], settings={"DATE_ORDER": "DMY"})
968
961
  if parsed_date:
969
962
  value_to_set = parsed_date.date()
970
963
  logger.debug(
@@ -988,18 +981,13 @@ def update_sensitive_meta_from_dict(
988
981
  # --- End Conversion ---
989
982
 
990
983
  # Check if patient name is changing
991
- if (
992
- k in ["patient_first_name", "patient_last_name"]
993
- and getattr(instance, k) != value_to_set
994
- ):
984
+ if k in ["patient_first_name", "patient_last_name"] and getattr(instance, k) != value_to_set:
995
985
  patient_name_changed = True
996
986
 
997
987
  setattr(instance, k, value_to_set) # Use value_to_set
998
988
 
999
989
  except Exception as e:
1000
- logger.error(
1001
- f"Error setting attribute '{k}' to '{v}': {e}. Skipping this field."
1002
- )
990
+ logger.error(f"Error setting attribute '{k}' to '{v}': {e}. Skipping this field.")
1003
991
  continue
1004
992
 
1005
993
  # Update name DB if patient names changed
@@ -1023,15 +1011,21 @@ def update_or_create_sensitive_meta_from_dict(
1023
1011
  cls: Type["SensitiveMeta"],
1024
1012
  data: Dict[str, Any],
1025
1013
  instance: Optional["SensitiveMeta"] = None,
1026
- ) -> "SensitiveMeta":
1014
+ ):
1027
1015
  """Logic to update or create a SensitiveMeta instance from a dictionary."""
1028
1016
  # Check if the instance already exists based on unique fields
1017
+ sensitive_meta: "SensitiveMeta"
1018
+ _created: bool
1029
1019
  if instance:
1030
1020
  # Update the existing instance
1031
- return update_sensitive_meta_from_dict(instance, data), False
1021
+ sensitive_meta = update_sensitive_meta_from_dict(instance, data)
1022
+ _created = False
1023
+
1032
1024
  else:
1033
1025
  # Create a new instance
1034
- return create_sensitive_meta_from_dict(cls, data), True
1026
+ sensitive_meta = create_sensitive_meta_from_dict(cls, data)
1027
+ _created = True
1028
+ return sensitive_meta, _created
1035
1029
 
1036
1030
 
1037
1031
  def _map_gender_string_to_standard(gender_str: str) -> Optional[str]:
@@ -1046,3 +1040,28 @@ def _map_gender_string_to_standard(gender_str: str) -> Optional[str]:
1046
1040
  if gender_lower in variants:
1047
1041
  return standard
1048
1042
  return None
1043
+
1044
+ def _create_anonymized_record(instance: "SensitiveMeta", DEFAULT_ANONYMIZED=None, DEFAULT_ANONYMIZED_DATE=timezone.make_aware(datetime(1900, 1, 1))) -> None:
1045
+ """
1046
+ Create a SensitiveMeta instance with all sensitive fields set to anonymized defaults.
1047
+ This is only called after anonymization and will delete all data that can identify a patient from the database.
1048
+ What is left will only be the patient hash.
1049
+
1050
+ Args:
1051
+ instance: The existing SensitiveMeta instance to anonymize
1052
+ DEFAULT_ANONYMIZED: Usually None, The default string to use for anonymized fields (e.g., "anonymized,")
1053
+ """
1054
+
1055
+ instance.refresh_from_db()
1056
+ instance.get_patient_hash()
1057
+ instance.get_patient_examination_hash()
1058
+
1059
+ anonymized_data = {
1060
+ "patient_first_name": DEFAULT_ANONYMIZED,
1061
+ "patient_last_name": DEFAULT_ANONYMIZED,
1062
+ "patient_dob": DEFAULT_ANONYMIZED_DATE,
1063
+ "examination_date": DEFAULT_ANONYMIZED_DATE,
1064
+ }
1065
+ sensitive_meta = update_sensitive_meta_from_dict(instance, anonymized_data)
1066
+
1067
+ sensitive_meta.save()