endoreg-db 0.8.4.4__py3-none-any.whl → 0.8.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of endoreg-db might be problematic. Click here for more details.

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