endoreg-db 0.8.9.32__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 (787) hide show
  1. endoreg_db/__init__.py +0 -0
  2. endoreg_db/_version.py +34 -0
  3. endoreg_db/admin.py +97 -0
  4. endoreg_db/api/serializers/finding_descriptions.py +0 -0
  5. endoreg_db/api/views/finding_descriptions.py +0 -0
  6. endoreg_db/api_urls.py +4 -0
  7. endoreg_db/apps.py +17 -0
  8. endoreg_db/assets/dummy_model.ckpt +1 -0
  9. endoreg_db/authz/auth.py +78 -0
  10. endoreg_db/authz/backends.py +168 -0
  11. endoreg_db/authz/management/commands/list_routes.py +20 -0
  12. endoreg_db/authz/middleware.py +84 -0
  13. endoreg_db/authz/permissions.py +138 -0
  14. endoreg_db/authz/policy.py +224 -0
  15. endoreg_db/authz/settings.py +64 -0
  16. endoreg_db/authz/views_auth.py +70 -0
  17. endoreg_db/codemods/readme.md +88 -0
  18. endoreg_db/codemods/rename_datetime_fields.py +99 -0
  19. endoreg_db/config/__init__.py +0 -0
  20. endoreg_db/config/env.py +106 -0
  21. endoreg_db/config/settings/__init__.py +6 -0
  22. endoreg_db/config/settings/base.py +148 -0
  23. endoreg_db/config/settings/case_gen.py +32 -0
  24. endoreg_db/config/settings/dev.py +108 -0
  25. endoreg_db/config/settings/keycloak.py +177 -0
  26. endoreg_db/config/settings/prod.py +66 -0
  27. endoreg_db/config/settings/test.py +72 -0
  28. endoreg_db/data/__init__.py +135 -0
  29. endoreg_db/data/ai_model/data.yaml +7 -0
  30. endoreg_db/data/ai_model_label/label/data.yaml +88 -0
  31. endoreg_db/data/ai_model_label/label/polyp_classification.yaml +52 -0
  32. endoreg_db/data/ai_model_label/label-set/data.yaml +40 -0
  33. endoreg_db/data/ai_model_label/label-set/polyp_classifications.yaml +25 -0
  34. endoreg_db/data/ai_model_label/label-type/data.yaml +7 -0
  35. endoreg_db/data/ai_model_meta/default_multilabel_classification.yaml +27 -0
  36. endoreg_db/data/ai_model_type/data.yaml +7 -0
  37. endoreg_db/data/ai_model_video_segmentation_label/base_segmentation.yaml +176 -0
  38. endoreg_db/data/ai_model_video_segmentation_labelset/data.yaml +20 -0
  39. endoreg_db/data/case_template/rule/00_patient_lab_sample_add_default_value.yaml +167 -0
  40. endoreg_db/data/case_template/rule/01_patient-set-age.yaml +8 -0
  41. endoreg_db/data/case_template/rule/01_patient-set-gender.yaml +9 -0
  42. endoreg_db/data/case_template/rule/11_create_patient_lab_sample.yaml +23 -0
  43. endoreg_db/data/case_template/rule/12_create-patient_medication-anticoagulation.yaml +19 -0
  44. endoreg_db/data/case_template/rule/13_create-patient_medication_schedule-anticoagulation.yaml +19 -0
  45. endoreg_db/data/case_template/rule/19_create_patient.yaml +17 -0
  46. endoreg_db/data/case_template/rule_type/base_types.yaml +35 -0
  47. endoreg_db/data/case_template/rule_value/.init +0 -0
  48. endoreg_db/data/case_template/rule_value_type/base_types.yaml +59 -0
  49. endoreg_db/data/case_template/template/base.yaml +8 -0
  50. endoreg_db/data/case_template/template_type/pre_endoscopy.yaml +3 -0
  51. endoreg_db/data/case_template/tmp/_rule_value +13 -0
  52. endoreg_db/data/case_template/tmp/rule/01_atrial_fibrillation.yaml +21 -0
  53. endoreg_db/data/case_template/tmp/rule/02_create_object.yaml +10 -0
  54. endoreg_db/data/case_template/tmp/template/atrial_fibrillation_low_risk.yaml +7 -0
  55. endoreg_db/data/center/data.yaml +99 -0
  56. endoreg_db/data/center_resource/green_endoscopy_dashboard_CenterResource.yaml +144 -0
  57. endoreg_db/data/center_shift/ukw.yaml +9 -0
  58. endoreg_db/data/center_waste/green_endoscopy_dashboard_CenterWaste.yaml +48 -0
  59. endoreg_db/data/contraindication/bleeding.yaml +11 -0
  60. endoreg_db/data/db_summary.csv +58 -0
  61. endoreg_db/data/db_summary.xlsx +0 -0
  62. endoreg_db/data/disease/cardiovascular.yaml +37 -0
  63. endoreg_db/data/disease/hepatology.yaml +5 -0
  64. endoreg_db/data/disease/misc.yaml +5 -0
  65. endoreg_db/data/disease/renal.yaml +5 -0
  66. endoreg_db/data/disease_classification/chronic_kidney_disease.yaml +6 -0
  67. endoreg_db/data/disease_classification/coronary_vessel_disease.yaml +6 -0
  68. endoreg_db/data/disease_classification_choice/chronic_kidney_disease.yaml +41 -0
  69. endoreg_db/data/disease_classification_choice/coronary_vessel_disease.yaml +20 -0
  70. endoreg_db/data/distribution/date/patient.yaml +7 -0
  71. endoreg_db/data/distribution/multiple_categorical/.init +0 -0
  72. endoreg_db/data/distribution/numeric/data.yaml +14 -0
  73. endoreg_db/data/distribution/single_categorical/patient.yaml +7 -0
  74. endoreg_db/data/emission_factor/green_endoscopy_dashboard_EmissionFactor.yaml +132 -0
  75. endoreg_db/data/endoscope/data.yaml +93 -0
  76. endoreg_db/data/endoscope_type/data.yaml +11 -0
  77. endoreg_db/data/endoscopy_processor/data.yaml +50 -0
  78. endoreg_db/data/event/cardiology.yaml +15 -0
  79. endoreg_db/data/event/neurology.yaml +14 -0
  80. endoreg_db/data/event/surgery.yaml +13 -0
  81. endoreg_db/data/event/thrombembolism.yaml +20 -0
  82. endoreg_db/data/event_classification/data.yaml +4 -0
  83. endoreg_db/data/event_classification_choice/data.yaml +9 -0
  84. endoreg_db/data/examination/examinations/data.yaml +172 -0
  85. endoreg_db/data/examination/time/data.yaml +48 -0
  86. endoreg_db/data/examination/time-type/data.yaml +5 -0
  87. endoreg_db/data/examination/type/data.yaml +17 -0
  88. endoreg_db/data/examination_indication/endoscopy.yaml +359 -0
  89. endoreg_db/data/examination_indication_classification/endoscopy.yaml +90 -0
  90. endoreg_db/data/examination_indication_classification_choice/endoscopy.yaml +97 -0
  91. endoreg_db/data/examination_requirement_set/colonoscopy.yaml +15 -0
  92. endoreg_db/data/finding/00_generic.yaml +35 -0
  93. endoreg_db/data/finding/00_generic_complication.yaml +9 -0
  94. endoreg_db/data/finding/01_gastroscopy_baseline.yaml +88 -0
  95. endoreg_db/data/finding/01_gastroscopy_observation.yaml +113 -0
  96. endoreg_db/data/finding/02_colonoscopy_baseline.yaml +53 -0
  97. endoreg_db/data/finding/02_colonoscopy_hidden.yaml +119 -0
  98. endoreg_db/data/finding/02_colonoscopy_observation.yaml +152 -0
  99. endoreg_db/data/finding_classification/00_generic.yaml +44 -0
  100. endoreg_db/data/finding_classification/00_generic_histology.yaml +28 -0
  101. endoreg_db/data/finding_classification/00_generic_lesion.yaml +52 -0
  102. endoreg_db/data/finding_classification/02_colonoscopy_baseline.yaml +83 -0
  103. endoreg_db/data/finding_classification/02_colonoscopy_histology.yaml +13 -0
  104. endoreg_db/data/finding_classification/02_colonoscopy_other.yaml +12 -0
  105. endoreg_db/data/finding_classification/02_colonoscopy_polyp.yaml +101 -0
  106. endoreg_db/data/finding_classification_choice/00_generic.yaml +15 -0
  107. endoreg_db/data/finding_classification_choice/00_generic_baseline.yaml +23 -0
  108. endoreg_db/data/finding_classification_choice/00_generic_complication.yaml +15 -0
  109. endoreg_db/data/finding_classification_choice/00_generic_histology.yaml +21 -0
  110. endoreg_db/data/finding_classification_choice/00_generic_lesion.yaml +158 -0
  111. endoreg_db/data/finding_classification_choice/02_colonoscopy_bowel_preparation.yaml +49 -0
  112. endoreg_db/data/finding_classification_choice/02_colonoscopy_generic.yaml +19 -0
  113. endoreg_db/data/finding_classification_choice/02_colonoscopy_histology.yaml +20 -0
  114. endoreg_db/data/finding_classification_choice/02_colonoscopy_location.yaml +248 -0
  115. endoreg_db/data/finding_classification_choice/02_colonoscopy_other.yaml +34 -0
  116. endoreg_db/data/finding_classification_choice/02_colonoscopy_polyp_advanced_imaging.yaml +76 -0
  117. endoreg_db/data/finding_classification_choice/02_colonoscopy_polyp_morphology.yaml +75 -0
  118. endoreg_db/data/finding_classification_choice/02_colonoscopy_size.yaml +27 -0
  119. endoreg_db/data/finding_classification_type/00_generic.yaml +53 -0
  120. endoreg_db/data/finding_classification_type/02_colonoscopy.yaml +9 -0
  121. endoreg_db/data/finding_intervention/00_generic_endoscopy.yaml +59 -0
  122. endoreg_db/data/finding_intervention/00_generic_endoscopy_ablation.yaml +44 -0
  123. endoreg_db/data/finding_intervention/00_generic_endoscopy_bleeding.yaml +55 -0
  124. endoreg_db/data/finding_intervention/00_generic_endoscopy_resection.yaml +85 -0
  125. endoreg_db/data/finding_intervention/00_generic_endoscopy_stenosis.yaml +17 -0
  126. endoreg_db/data/finding_intervention/00_generic_endoscopy_stent.yaml +9 -0
  127. endoreg_db/data/finding_intervention/01_gastroscopy.yaml +19 -0
  128. endoreg_db/data/finding_intervention/04_eus.yaml +39 -0
  129. endoreg_db/data/finding_intervention/05_ercp.yaml +3 -0
  130. endoreg_db/data/finding_intervention_type/endoscopy.yaml +15 -0
  131. endoreg_db/data/finding_type/data.yaml +39 -0
  132. endoreg_db/data/gender/data.yaml +42 -0
  133. endoreg_db/data/information_source/annotation.yaml +6 -0
  134. endoreg_db/data/information_source/data.yaml +30 -0
  135. endoreg_db/data/information_source/endoscopy_guidelines.yaml +7 -0
  136. endoreg_db/data/information_source/medication.yaml +6 -0
  137. endoreg_db/data/information_source/prediction.yaml +7 -0
  138. endoreg_db/data/information_source_type/data.yaml +8 -0
  139. endoreg_db/data/lab_value/cardiac_enzymes.yaml +37 -0
  140. endoreg_db/data/lab_value/coagulation.yaml +54 -0
  141. endoreg_db/data/lab_value/electrolytes.yaml +228 -0
  142. endoreg_db/data/lab_value/gastrointestinal_function.yaml +133 -0
  143. endoreg_db/data/lab_value/hematology.yaml +184 -0
  144. endoreg_db/data/lab_value/hormones.yaml +59 -0
  145. endoreg_db/data/lab_value/lipids.yaml +53 -0
  146. endoreg_db/data/lab_value/misc.yaml +76 -0
  147. endoreg_db/data/lab_value/renal_function.yaml +12 -0
  148. endoreg_db/data/log_type/data.yaml +57 -0
  149. endoreg_db/data/lx_client_tag/base.yaml +54 -0
  150. endoreg_db/data/lx_client_type/base.yaml +30 -0
  151. endoreg_db/data/lx_permission/base.yaml +24 -0
  152. endoreg_db/data/lx_permission/endoreg.yaml +52 -0
  153. endoreg_db/data/material/material.yaml +91 -0
  154. endoreg_db/data/medication/anticoagulation.yaml +65 -0
  155. endoreg_db/data/medication/tah.yaml +70 -0
  156. endoreg_db/data/medication_indication/anticoagulation.yaml +115 -0
  157. endoreg_db/data/medication_indication_type/data.yaml +11 -0
  158. endoreg_db/data/medication_indication_type/thrombembolism.yaml +41 -0
  159. endoreg_db/data/medication_intake_time/base.yaml +31 -0
  160. endoreg_db/data/medication_schedule/apixaban.yaml +95 -0
  161. endoreg_db/data/medication_schedule/ass.yaml +12 -0
  162. endoreg_db/data/medication_schedule/enoxaparin.yaml +26 -0
  163. endoreg_db/data/names_first/first_names.yaml +54 -0
  164. endoreg_db/data/names_last/last_names.yaml +51 -0
  165. endoreg_db/data/network_device/data.yaml +59 -0
  166. endoreg_db/data/network_device_type/data.yaml +12 -0
  167. endoreg_db/data/organ/data.yaml +29 -0
  168. endoreg_db/data/patient_lab_sample_type/generic.yaml +6 -0
  169. endoreg_db/data/pdf_type/data.yaml +46 -0
  170. endoreg_db/data/product/green_endoscopy_dashboard_Product.yaml +66 -0
  171. endoreg_db/data/product_group/green_endoscopy_dashboard_ProductGroup.yaml +33 -0
  172. endoreg_db/data/product_material/green_endoscopy_dashboard_ProductMaterial.yaml +308 -0
  173. endoreg_db/data/product_weight/green_endoscopy_dashboard_ProductWeight.yaml +88 -0
  174. endoreg_db/data/profession/data.yaml +70 -0
  175. endoreg_db/data/qualification/endoscopy.yaml +36 -0
  176. endoreg_db/data/qualification/m2.yaml +39 -0
  177. endoreg_db/data/qualification/outpatient_clinic.yaml +35 -0
  178. endoreg_db/data/qualification/sonography.yaml +36 -0
  179. endoreg_db/data/qualification_type/base.yaml +29 -0
  180. endoreg_db/data/reference_product/green_endoscopy_dashboard_ReferenceProduct.yaml +55 -0
  181. endoreg_db/data/report_reader_flag/rkh-histology-generic.yaml +10 -0
  182. endoreg_db/data/report_reader_flag/ukw-examination-generic.yaml +30 -0
  183. endoreg_db/data/report_reader_flag/ukw-histology-generic.yaml +24 -0
  184. endoreg_db/data/requirement/01_patient_data.yaml +93 -0
  185. endoreg_db/data/requirement/old/colon_polyp_intervention.yaml +49 -0
  186. endoreg_db/data/requirement/old/colonoscopy_baseline_austria.yaml +45 -0
  187. endoreg_db/data/requirement/old/coloreg_colon_polyp.yaml +49 -0
  188. endoreg_db/data/requirement/old/disease_cardiovascular.yaml +79 -0
  189. endoreg_db/data/requirement/old/disease_classification_choice_cardiovascular.yaml +41 -0
  190. endoreg_db/data/requirement/old/disease_hepatology.yaml +12 -0
  191. endoreg_db/data/requirement/old/disease_misc.yaml +12 -0
  192. endoreg_db/data/requirement/old/disease_renal.yaml +96 -0
  193. endoreg_db/data/requirement/old/endoscopy_bleeding_risk.yaml +59 -0
  194. endoreg_db/data/requirement/old/event_cardiology.yaml +251 -0
  195. endoreg_db/data/requirement/old/event_requirements.yaml +145 -0
  196. endoreg_db/data/requirement/old/finding_colon_polyp.yaml +50 -0
  197. endoreg_db/data/requirement/old/gender.yaml +0 -0
  198. endoreg_db/data/requirement/old/lab_value.yaml +441 -0
  199. endoreg_db/data/requirement/old/medication.yaml +93 -0
  200. endoreg_db/data/requirement_operator/_old/age.yaml +13 -0
  201. endoreg_db/data/requirement_operator/_old/lab_operators.yaml +129 -0
  202. endoreg_db/data/requirement_operator/_old/model_operators.yaml +96 -0
  203. endoreg_db/data/requirement_operator/new_operators.yaml +36 -0
  204. endoreg_db/data/requirement_set/01_endoscopy_generic.yaml +65 -0
  205. endoreg_db/data/requirement_set/01_laboratory.yaml +13 -0
  206. endoreg_db/data/requirement_set/02_endoscopy_bleeding_risk.yaml +46 -0
  207. endoreg_db/data/requirement_set/90_coloreg.yaml +190 -0
  208. endoreg_db/data/requirement_set/_old_ +109 -0
  209. endoreg_db/data/requirement_set/colonoscopy_austria_screening.yaml +57 -0
  210. endoreg_db/data/requirement_set_type/data.yaml +41 -0
  211. endoreg_db/data/requirement_type/requirement_types.yaml +165 -0
  212. endoreg_db/data/resource/green_endoscopy_dashboard_Resource.yaml +15 -0
  213. endoreg_db/data/risk/bleeding.yaml +26 -0
  214. endoreg_db/data/risk/thrombosis.yaml +37 -0
  215. endoreg_db/data/risk_type/data.yaml +27 -0
  216. endoreg_db/data/setup_config.yaml +38 -0
  217. endoreg_db/data/shift/endoscopy.yaml +21 -0
  218. endoreg_db/data/shift/m2.yaml +0 -0
  219. endoreg_db/data/shift_type/base.yaml +35 -0
  220. endoreg_db/data/tag/requirement_set_tags.yaml +32 -0
  221. endoreg_db/data/tmp/chronic_kidney_disease.yaml +0 -0
  222. endoreg_db/data/tmp/congestive_heart_failure.yaml +0 -0
  223. endoreg_db/data/transport_route/green_endoscopy_dashboard_TransportRoute.yaml +12 -0
  224. endoreg_db/data/unit/concentration.yaml +115 -0
  225. endoreg_db/data/unit/data.yaml +17 -0
  226. endoreg_db/data/unit/length.yaml +31 -0
  227. endoreg_db/data/unit/misc.yaml +20 -0
  228. endoreg_db/data/unit/rate.yaml +6 -0
  229. endoreg_db/data/unit/time.yaml +48 -0
  230. endoreg_db/data/unit/volume.yaml +35 -0
  231. endoreg_db/data/unit/weight.yaml +38 -0
  232. endoreg_db/data/waste/data.yaml +12 -0
  233. endoreg_db/exceptions.py +24 -0
  234. endoreg_db/export/frames/export.py +6 -0
  235. endoreg_db/export/frames/export_frames_with_labels.py +616 -0
  236. endoreg_db/factories/__init__.py +0 -0
  237. endoreg_db/forms/__init__.py +4 -0
  238. endoreg_db/forms/examination_form.py +12 -0
  239. endoreg_db/forms/patient_finding_intervention_form.py +40 -0
  240. endoreg_db/forms/patient_form.py +23 -0
  241. endoreg_db/forms/questionnaires/__init__.py +1 -0
  242. endoreg_db/forms/questionnaires/tto_questionnaire.py +23 -0
  243. endoreg_db/forms/settings/__init__.py +11 -0
  244. endoreg_db/forms/unit.py +7 -0
  245. endoreg_db/helpers/__init__.py +0 -0
  246. endoreg_db/helpers/count_db.py +48 -0
  247. endoreg_db/helpers/data_loader.py +280 -0
  248. endoreg_db/helpers/default_objects.py +414 -0
  249. endoreg_db/helpers/download_segmentation_model.py +32 -0
  250. endoreg_db/helpers/interact.py +1 -0
  251. endoreg_db/helpers/test_video_helper.py +127 -0
  252. endoreg_db/import_files/__init__.py +27 -0
  253. endoreg_db/import_files/context/__init__.py +7 -0
  254. endoreg_db/import_files/context/default_sensitive_meta.py +83 -0
  255. endoreg_db/import_files/context/ensure_center.py +17 -0
  256. endoreg_db/import_files/context/file_lock.py +66 -0
  257. endoreg_db/import_files/context/import_context.py +42 -0
  258. endoreg_db/import_files/context/validate_directories.py +57 -0
  259. endoreg_db/import_files/file_storage/__init__.py +15 -0
  260. endoreg_db/import_files/file_storage/create_report_file.py +99 -0
  261. endoreg_db/import_files/file_storage/create_video_file.py +104 -0
  262. endoreg_db/import_files/file_storage/sensitive_meta_storage.py +42 -0
  263. endoreg_db/import_files/file_storage/state_management.py +463 -0
  264. endoreg_db/import_files/file_storage/storage.py +42 -0
  265. endoreg_db/import_files/import_service.md +26 -0
  266. endoreg_db/import_files/processing/__init__.py +11 -0
  267. endoreg_db/import_files/processing/report_processing/report_anonymization.py +99 -0
  268. endoreg_db/import_files/processing/sensitive_meta_adapter.py +51 -0
  269. endoreg_db/import_files/processing/video_processing/video_anonymization.py +107 -0
  270. endoreg_db/import_files/pseudonymization/__init__.py +0 -0
  271. endoreg_db/import_files/pseudonymization/fake.py +52 -0
  272. endoreg_db/import_files/pseudonymization/k_anonymity.py +181 -0
  273. endoreg_db/import_files/pseudonymization/k_pseudonymity.py +139 -0
  274. endoreg_db/import_files/pseudonymization/pseudonymize.py +0 -0
  275. endoreg_db/import_files/report_import_service.py +147 -0
  276. endoreg_db/import_files/video_import_service.py +154 -0
  277. endoreg_db/logger_conf.py +156 -0
  278. endoreg_db/management/__init__.py +1 -0
  279. endoreg_db/management/commands/__init__.py +1 -0
  280. endoreg_db/management/commands/anonymize_video.py +0 -0
  281. endoreg_db/management/commands/check_auth.py +132 -0
  282. endoreg_db/management/commands/create_model_meta_from_huggingface.py +177 -0
  283. endoreg_db/management/commands/create_multilabel_model_meta.py +419 -0
  284. endoreg_db/management/commands/export_frame_annot.py +196 -0
  285. endoreg_db/management/commands/fix_missing_patient_data.py +206 -0
  286. endoreg_db/management/commands/fix_video_paths.py +186 -0
  287. endoreg_db/management/commands/import_report.py +361 -0
  288. endoreg_db/management/commands/list_routes.py +20 -0
  289. endoreg_db/management/commands/load_ai_model_data.py +83 -0
  290. endoreg_db/management/commands/load_ai_model_label_data.py +60 -0
  291. endoreg_db/management/commands/load_base_db_data.py +63 -0
  292. endoreg_db/management/commands/load_center_data.py +68 -0
  293. endoreg_db/management/commands/load_contraindication_data.py +39 -0
  294. endoreg_db/management/commands/load_disease_classification_choices_data.py +38 -0
  295. endoreg_db/management/commands/load_disease_classification_data.py +38 -0
  296. endoreg_db/management/commands/load_disease_data.py +59 -0
  297. endoreg_db/management/commands/load_distribution_data.py +63 -0
  298. endoreg_db/management/commands/load_endoscope_data.py +58 -0
  299. endoreg_db/management/commands/load_event_data.py +39 -0
  300. endoreg_db/management/commands/load_examination_data.py +78 -0
  301. endoreg_db/management/commands/load_examination_indication_data.py +85 -0
  302. endoreg_db/management/commands/load_finding_data.py +115 -0
  303. endoreg_db/management/commands/load_gender_data.py +37 -0
  304. endoreg_db/management/commands/load_green_endoscopy_wuerzburg_data.py +142 -0
  305. endoreg_db/management/commands/load_information_source.py +46 -0
  306. endoreg_db/management/commands/load_lab_value_data.py +52 -0
  307. endoreg_db/management/commands/load_legacy_data.py +303 -0
  308. endoreg_db/management/commands/load_medication_data.py +104 -0
  309. endoreg_db/management/commands/load_name_data.py +36 -0
  310. endoreg_db/management/commands/load_organ_data.py +39 -0
  311. endoreg_db/management/commands/load_pdf_type_data.py +58 -0
  312. endoreg_db/management/commands/load_profession_data.py +40 -0
  313. endoreg_db/management/commands/load_qualification_data.py +56 -0
  314. endoreg_db/management/commands/load_report_reader_flag_data.py +40 -0
  315. endoreg_db/management/commands/load_requirement_data.py +207 -0
  316. endoreg_db/management/commands/load_requirement_set_tags.py +95 -0
  317. endoreg_db/management/commands/load_risk_data.py +57 -0
  318. endoreg_db/management/commands/load_shift_data.py +57 -0
  319. endoreg_db/management/commands/load_tag_data.py +54 -0
  320. endoreg_db/management/commands/load_unit_data.py +40 -0
  321. endoreg_db/management/commands/load_user_groups.py +26 -0
  322. endoreg_db/management/commands/model_input.py +169 -0
  323. endoreg_db/management/commands/register_ai_model.py +70 -0
  324. endoreg_db/management/commands/setup_endoreg_db.py +459 -0
  325. endoreg_db/management/commands/start_filewatcher.py +115 -0
  326. endoreg_db/management/commands/storage_management.py +622 -0
  327. endoreg_db/management/commands/summarize_db_content.py +280 -0
  328. endoreg_db/management/commands/train_image_multilabel_model.py +144 -0
  329. endoreg_db/management/commands/validate_video_files.py +189 -0
  330. endoreg_db/management/commands/video_validation.py +20 -0
  331. endoreg_db/mermaid/Overall_flow_patient_finding_intervention.md +10 -0
  332. endoreg_db/mermaid/anonymized_image_annotation.md +20 -0
  333. endoreg_db/mermaid/binary_classification_annotation.md +50 -0
  334. endoreg_db/mermaid/classification.md +8 -0
  335. endoreg_db/mermaid/examination.md +8 -0
  336. endoreg_db/mermaid/findings.md +7 -0
  337. endoreg_db/mermaid/image_classification.md +28 -0
  338. endoreg_db/mermaid/interventions.md +8 -0
  339. endoreg_db/mermaid/morphology.md +8 -0
  340. endoreg_db/mermaid/patient_creation.md +14 -0
  341. endoreg_db/mermaid/video_segmentation_annotation.md +17 -0
  342. endoreg_db/migrations/0001_initial.py +1953 -0
  343. endoreg_db/migrations/__init__.py +0 -0
  344. endoreg_db/models/__init__.py +322 -0
  345. endoreg_db/models/administration/__init__.py +95 -0
  346. endoreg_db/models/administration/ai/__init__.py +9 -0
  347. endoreg_db/models/administration/ai/active_model.py +35 -0
  348. endoreg_db/models/administration/ai/ai_model.py +180 -0
  349. endoreg_db/models/administration/ai/model_type.py +42 -0
  350. endoreg_db/models/administration/case/__init__.py +5 -0
  351. endoreg_db/models/administration/case/case.py +114 -0
  352. endoreg_db/models/administration/case/case_template/__init__.py +3 -0
  353. endoreg_db/models/administration/case/case_template/case_template.py +3 -0
  354. endoreg_db/models/administration/case/case_template/case_template_rule.py +3 -0
  355. endoreg_db/models/administration/case/case_template/case_template_rule_value.py +3 -0
  356. endoreg_db/models/administration/case/case_template/case_template_type.py +3 -0
  357. endoreg_db/models/administration/center/__init__.py +13 -0
  358. endoreg_db/models/administration/center/center.py +85 -0
  359. endoreg_db/models/administration/center/center_product.py +67 -0
  360. endoreg_db/models/administration/center/center_resource.py +69 -0
  361. endoreg_db/models/administration/center/center_shift.py +94 -0
  362. endoreg_db/models/administration/center/center_waste.py +42 -0
  363. endoreg_db/models/administration/person/__init__.py +26 -0
  364. endoreg_db/models/administration/person/employee/__init__.py +3 -0
  365. endoreg_db/models/administration/person/employee/employee.py +40 -0
  366. endoreg_db/models/administration/person/employee/employee_qualification.py +44 -0
  367. endoreg_db/models/administration/person/employee/employee_type.py +50 -0
  368. endoreg_db/models/administration/person/examiner/__init__.py +4 -0
  369. endoreg_db/models/administration/person/examiner/examiner.py +64 -0
  370. endoreg_db/models/administration/person/names/__init__.py +0 -0
  371. endoreg_db/models/administration/person/names/first_name.py +20 -0
  372. endoreg_db/models/administration/person/names/last_name.py +20 -0
  373. endoreg_db/models/administration/person/patient/__init__.py +7 -0
  374. endoreg_db/models/administration/person/patient/patient.py +488 -0
  375. endoreg_db/models/administration/person/patient/patient_external_id.py +36 -0
  376. endoreg_db/models/administration/person/person.py +35 -0
  377. endoreg_db/models/administration/person/profession/__init__.py +28 -0
  378. endoreg_db/models/administration/person/user/__init__.py +5 -0
  379. endoreg_db/models/administration/person/user/portal_user_information.py +41 -0
  380. endoreg_db/models/administration/product/__init__.py +15 -0
  381. endoreg_db/models/administration/product/product.py +106 -0
  382. endoreg_db/models/administration/product/product_group.py +41 -0
  383. endoreg_db/models/administration/product/product_material.py +60 -0
  384. endoreg_db/models/administration/product/product_weight.py +51 -0
  385. endoreg_db/models/administration/product/reference_product.py +147 -0
  386. endoreg_db/models/administration/qualification/__init__.py +7 -0
  387. endoreg_db/models/administration/qualification/qualification.py +43 -0
  388. endoreg_db/models/administration/qualification/qualification_type.py +39 -0
  389. endoreg_db/models/administration/shift/__init__.py +9 -0
  390. endoreg_db/models/administration/shift/scheduled_days.py +72 -0
  391. endoreg_db/models/administration/shift/shift.py +57 -0
  392. endoreg_db/models/administration/shift/shift_type.py +108 -0
  393. endoreg_db/models/aidataset/__init__.py +5 -0
  394. endoreg_db/models/aidataset/aidataset.py +193 -0
  395. endoreg_db/models/label/__init__.py +23 -0
  396. endoreg_db/models/label/annotation/__init__.py +12 -0
  397. endoreg_db/models/label/annotation/image_classification.py +85 -0
  398. endoreg_db/models/label/annotation/video_segmentation_annotation.py +61 -0
  399. endoreg_db/models/label/label.py +91 -0
  400. endoreg_db/models/label/label_set.py +68 -0
  401. endoreg_db/models/label/label_type.py +29 -0
  402. endoreg_db/models/label/label_video_segment/__init__.py +3 -0
  403. endoreg_db/models/label/label_video_segment/_create_from_video.py +42 -0
  404. endoreg_db/models/label/label_video_segment/label_video_segment.py +611 -0
  405. endoreg_db/models/label/video_segmentation_label.py +35 -0
  406. endoreg_db/models/label/video_segmentation_labelset.py +28 -0
  407. endoreg_db/models/media/__init__.py +23 -0
  408. endoreg_db/models/media/frame/__init__.py +3 -0
  409. endoreg_db/models/media/frame/frame.py +137 -0
  410. endoreg_db/models/media/pdf/__init__.py +12 -0
  411. endoreg_db/models/media/pdf/raw_pdf.py +764 -0
  412. endoreg_db/models/media/pdf/report_file.py +162 -0
  413. endoreg_db/models/media/pdf/report_reader/__init__.py +7 -0
  414. endoreg_db/models/media/pdf/report_reader/report_reader_config.py +85 -0
  415. endoreg_db/models/media/pdf/report_reader/report_reader_flag.py +46 -0
  416. endoreg_db/models/media/video/__init__.py +9 -0
  417. endoreg_db/models/media/video/create_from_file.py +402 -0
  418. endoreg_db/models/media/video/pipe_1.py +258 -0
  419. endoreg_db/models/media/video/pipe_2.py +129 -0
  420. endoreg_db/models/media/video/video_file.py +907 -0
  421. endoreg_db/models/media/video/video_file_ai.py +828 -0
  422. endoreg_db/models/media/video/video_file_anonymize.py +524 -0
  423. endoreg_db/models/media/video/video_file_frames/__init__.py +49 -0
  424. endoreg_db/models/media/video/video_file_frames/_bulk_create_frames.py +25 -0
  425. endoreg_db/models/media/video/video_file_frames/_create_frame_object.py +23 -0
  426. endoreg_db/models/media/video/video_file_frames/_delete_frames.py +126 -0
  427. endoreg_db/models/media/video/video_file_frames/_extract_frames.py +233 -0
  428. endoreg_db/models/media/video/video_file_frames/_get_frame.py +36 -0
  429. endoreg_db/models/media/video/video_file_frames/_get_frame_number.py +13 -0
  430. endoreg_db/models/media/video/video_file_frames/_get_frame_path.py +24 -0
  431. endoreg_db/models/media/video/video_file_frames/_get_frame_paths.py +40 -0
  432. endoreg_db/models/media/video/video_file_frames/_get_frame_range.py +44 -0
  433. endoreg_db/models/media/video/video_file_frames/_get_frames.py +30 -0
  434. endoreg_db/models/media/video/video_file_frames/_initialize_frames.py +205 -0
  435. endoreg_db/models/media/video/video_file_frames/_manage_frame_range.py +228 -0
  436. endoreg_db/models/media/video/video_file_frames/_mark_frames_extracted_status.py +107 -0
  437. endoreg_db/models/media/video/video_file_io.py +272 -0
  438. endoreg_db/models/media/video/video_file_meta/__init__.py +22 -0
  439. endoreg_db/models/media/video/video_file_meta/get_crop_template.py +58 -0
  440. endoreg_db/models/media/video/video_file_meta/get_endo_roi.py +62 -0
  441. endoreg_db/models/media/video/video_file_meta/get_fps.py +183 -0
  442. endoreg_db/models/media/video/video_file_meta/initialize_video_specs.py +198 -0
  443. endoreg_db/models/media/video/video_file_meta/text_meta.py +178 -0
  444. endoreg_db/models/media/video/video_file_meta/video_meta.py +105 -0
  445. endoreg_db/models/media/video/video_file_segments.py +317 -0
  446. endoreg_db/models/media/video/video_metadata.py +67 -0
  447. endoreg_db/models/media/video/video_processing.py +192 -0
  448. endoreg_db/models/medical/__init__.py +136 -0
  449. endoreg_db/models/medical/contraindication/README.md +1 -0
  450. endoreg_db/models/medical/contraindication/__init__.py +29 -0
  451. endoreg_db/models/medical/disease.py +174 -0
  452. endoreg_db/models/medical/event.py +154 -0
  453. endoreg_db/models/medical/examination/__init__.py +20 -0
  454. endoreg_db/models/medical/examination/examination.py +183 -0
  455. endoreg_db/models/medical/examination/examination_indication.py +229 -0
  456. endoreg_db/models/medical/examination/examination_time.py +68 -0
  457. endoreg_db/models/medical/examination/examination_time_type.py +44 -0
  458. endoreg_db/models/medical/examination/examination_type.py +47 -0
  459. endoreg_db/models/medical/finding/__init__.py +20 -0
  460. endoreg_db/models/medical/finding/finding.py +113 -0
  461. endoreg_db/models/medical/finding/finding_classification.py +131 -0
  462. endoreg_db/models/medical/finding/finding_intervention.py +68 -0
  463. endoreg_db/models/medical/finding/finding_type.py +38 -0
  464. endoreg_db/models/medical/hardware/__init__.py +8 -0
  465. endoreg_db/models/medical/hardware/endoscope.py +77 -0
  466. endoreg_db/models/medical/hardware/endoscopy_processor.py +182 -0
  467. endoreg_db/models/medical/laboratory/__init__.py +5 -0
  468. endoreg_db/models/medical/laboratory/lab_value.py +490 -0
  469. endoreg_db/models/medical/medication/__init__.py +23 -0
  470. endoreg_db/models/medical/medication/medication.py +45 -0
  471. endoreg_db/models/medical/medication/medication_indication.py +78 -0
  472. endoreg_db/models/medical/medication/medication_indication_type.py +58 -0
  473. endoreg_db/models/medical/medication/medication_intake_time.py +58 -0
  474. endoreg_db/models/medical/medication/medication_schedule.py +58 -0
  475. endoreg_db/models/medical/organ/__init__.py +38 -0
  476. endoreg_db/models/medical/patient/__init__.py +48 -0
  477. endoreg_db/models/medical/patient/medication_examples.py +56 -0
  478. endoreg_db/models/medical/patient/patient_disease.py +72 -0
  479. endoreg_db/models/medical/patient/patient_event.py +80 -0
  480. endoreg_db/models/medical/patient/patient_examination.py +280 -0
  481. endoreg_db/models/medical/patient/patient_examination_indication.py +57 -0
  482. endoreg_db/models/medical/patient/patient_finding.py +416 -0
  483. endoreg_db/models/medical/patient/patient_finding_classification.py +231 -0
  484. endoreg_db/models/medical/patient/patient_finding_intervention.py +37 -0
  485. endoreg_db/models/medical/patient/patient_lab_sample.py +157 -0
  486. endoreg_db/models/medical/patient/patient_lab_value.py +247 -0
  487. endoreg_db/models/medical/patient/patient_medication.py +111 -0
  488. endoreg_db/models/medical/patient/patient_medication_schedule.py +152 -0
  489. endoreg_db/models/medical/risk/__init__.py +7 -0
  490. endoreg_db/models/medical/risk/risk.py +73 -0
  491. endoreg_db/models/medical/risk/risk_type.py +54 -0
  492. endoreg_db/models/metadata/__init__.py +19 -0
  493. endoreg_db/models/metadata/model_meta.py +266 -0
  494. endoreg_db/models/metadata/model_meta_logic.py +485 -0
  495. endoreg_db/models/metadata/pdf_meta.py +96 -0
  496. endoreg_db/models/metadata/sensitive_meta.py +345 -0
  497. endoreg_db/models/metadata/sensitive_meta_logic.py +1161 -0
  498. endoreg_db/models/metadata/video_meta.py +459 -0
  499. endoreg_db/models/metadata/video_prediction_logic.py +232 -0
  500. endoreg_db/models/metadata/video_prediction_meta.py +319 -0
  501. endoreg_db/models/operation_log.py +63 -0
  502. endoreg_db/models/other/__init__.py +40 -0
  503. endoreg_db/models/other/distribution/__init__.py +46 -0
  504. endoreg_db/models/other/distribution/base_value_distribution.py +22 -0
  505. endoreg_db/models/other/distribution/date_value_distribution.py +163 -0
  506. endoreg_db/models/other/distribution/multiple_categorical_value_distribution.py +50 -0
  507. endoreg_db/models/other/distribution/numeric_value_distribution.py +211 -0
  508. endoreg_db/models/other/distribution/single_categorical_value_distribution.py +23 -0
  509. endoreg_db/models/other/emission/__init__.py +5 -0
  510. endoreg_db/models/other/emission/emission_factor.py +110 -0
  511. endoreg_db/models/other/gender.py +32 -0
  512. endoreg_db/models/other/information_source.py +190 -0
  513. endoreg_db/models/other/material.py +34 -0
  514. endoreg_db/models/other/resource.py +24 -0
  515. endoreg_db/models/other/tag.py +32 -0
  516. endoreg_db/models/other/transport_route.py +40 -0
  517. endoreg_db/models/other/unit.py +40 -0
  518. endoreg_db/models/other/waste.py +28 -0
  519. endoreg_db/models/report/__init__.py +0 -0
  520. endoreg_db/models/report/images.py +0 -0
  521. endoreg_db/models/report/report.py +5 -0
  522. endoreg_db/models/requirement/__init__.py +11 -0
  523. endoreg_db/models/requirement/requirement.py +792 -0
  524. endoreg_db/models/requirement/requirement_error.py +84 -0
  525. endoreg_db/models/requirement/requirement_evaluation/__init__.py +6 -0
  526. endoreg_db/models/requirement/requirement_evaluation/evaluate_with_dependencies.py +268 -0
  527. endoreg_db/models/requirement/requirement_evaluation/get_values.py +40 -0
  528. endoreg_db/models/requirement/requirement_evaluation/operator_evaluation_models.py +6 -0
  529. endoreg_db/models/requirement/requirement_evaluation/requirement_type_parser.py +137 -0
  530. endoreg_db/models/requirement/requirement_operator.py +187 -0
  531. endoreg_db/models/requirement/requirement_set.py +327 -0
  532. endoreg_db/models/state/__init__.py +13 -0
  533. endoreg_db/models/state/abstract.py +11 -0
  534. endoreg_db/models/state/anonymization.py +30 -0
  535. endoreg_db/models/state/audit_ledger.py +155 -0
  536. endoreg_db/models/state/label_video_segment.py +31 -0
  537. endoreg_db/models/state/processing_history/__init__.py +3 -0
  538. endoreg_db/models/state/processing_history/processing_history.py +136 -0
  539. endoreg_db/models/state/raw_pdf.py +219 -0
  540. endoreg_db/models/state/sensitive_meta.py +50 -0
  541. endoreg_db/models/state/video.py +251 -0
  542. endoreg_db/models/upload_job.py +100 -0
  543. endoreg_db/models/utils.py +138 -0
  544. endoreg_db/queries/__init__.py +3 -0
  545. endoreg_db/queries/annotations/__init__.py +1 -0
  546. endoreg_db/queries/annotations/legacy.py +169 -0
  547. endoreg_db/queries/sanity/__init_.py +0 -0
  548. endoreg_db/root_urls.py +27 -0
  549. endoreg_db/schemas/__init__.py +0 -0
  550. endoreg_db/schemas/examination_evaluation.py +30 -0
  551. endoreg_db/serializers/Frames_NICE_and_PARIS_classifications.py +861 -0
  552. endoreg_db/serializers/__init__.py +104 -0
  553. endoreg_db/serializers/administration/__init__.py +13 -0
  554. endoreg_db/serializers/administration/ai/__init__.py +9 -0
  555. endoreg_db/serializers/administration/ai/active_model.py +12 -0
  556. endoreg_db/serializers/administration/ai/ai_model.py +20 -0
  557. endoreg_db/serializers/administration/ai/model_type.py +12 -0
  558. endoreg_db/serializers/administration/center.py +14 -0
  559. endoreg_db/serializers/administration/gender.py +11 -0
  560. endoreg_db/serializers/anonymization.py +77 -0
  561. endoreg_db/serializers/evaluation/examination_evaluation.py +0 -0
  562. endoreg_db/serializers/examination/__init__.py +10 -0
  563. endoreg_db/serializers/examination/base.py +45 -0
  564. endoreg_db/serializers/examination/dropdown.py +20 -0
  565. endoreg_db/serializers/examination_serializer.py +9 -0
  566. endoreg_db/serializers/finding/__init__.py +5 -0
  567. endoreg_db/serializers/finding/finding.py +61 -0
  568. endoreg_db/serializers/finding_classification/__init__.py +7 -0
  569. endoreg_db/serializers/finding_classification/choice.py +19 -0
  570. endoreg_db/serializers/finding_classification/classification.py +11 -0
  571. endoreg_db/serializers/label_video_segment/__init__.py +9 -0
  572. endoreg_db/serializers/label_video_segment/image_classification_annotation.py +62 -0
  573. endoreg_db/serializers/label_video_segment/label/__init__.py +6 -0
  574. endoreg_db/serializers/label_video_segment/label/label.py +15 -0
  575. endoreg_db/serializers/label_video_segment/label_video_segment.py +427 -0
  576. endoreg_db/serializers/meta/__init__.py +13 -0
  577. endoreg_db/serializers/meta/sensitive_meta_detail.py +122 -0
  578. endoreg_db/serializers/meta/sensitive_meta_update.py +153 -0
  579. endoreg_db/serializers/meta/sensitive_meta_verification.py +62 -0
  580. endoreg_db/serializers/meta/video_meta.py +39 -0
  581. endoreg_db/serializers/misc/__init__.py +14 -0
  582. endoreg_db/serializers/misc/file_overview.py +72 -0
  583. endoreg_db/serializers/misc/sensitive_patient_data.py +144 -0
  584. endoreg_db/serializers/misc/stats.py +35 -0
  585. endoreg_db/serializers/misc/translatable_field_mix_in.py +44 -0
  586. endoreg_db/serializers/misc/upload_job.py +74 -0
  587. endoreg_db/serializers/patient/__init__.py +12 -0
  588. endoreg_db/serializers/patient/patient.py +103 -0
  589. endoreg_db/serializers/patient/patient_dropdown.py +35 -0
  590. endoreg_db/serializers/patient_examination/__init__.py +7 -0
  591. endoreg_db/serializers/patient_examination/patient_examination.py +168 -0
  592. endoreg_db/serializers/patient_finding/__init__.py +15 -0
  593. endoreg_db/serializers/patient_finding/patient_finding.py +32 -0
  594. endoreg_db/serializers/patient_finding/patient_finding_classification.py +47 -0
  595. endoreg_db/serializers/patient_finding/patient_finding_detail.py +62 -0
  596. endoreg_db/serializers/patient_finding/patient_finding_intervention.py +28 -0
  597. endoreg_db/serializers/patient_finding/patient_finding_list.py +40 -0
  598. endoreg_db/serializers/patient_finding/patient_finding_write.py +135 -0
  599. endoreg_db/serializers/pdf/__init__.py +3 -0
  600. endoreg_db/serializers/pdf/anony_text_validation.py +101 -0
  601. endoreg_db/serializers/requirements/requirement_schema.py +20 -0
  602. endoreg_db/serializers/requirements/requirement_sets.py +99 -0
  603. endoreg_db/serializers/sensitive_meta_serializer.py +301 -0
  604. endoreg_db/serializers/video/__init__.py +7 -0
  605. endoreg_db/serializers/video/video_file.py +283 -0
  606. endoreg_db/serializers/video/video_file_brief.py +14 -0
  607. endoreg_db/serializers/video/video_file_detail.py +96 -0
  608. endoreg_db/serializers/video/video_file_list.py +100 -0
  609. endoreg_db/serializers/video/video_processing_history.py +172 -0
  610. endoreg_db/serializers/video_examination.py +198 -0
  611. endoreg_db/services/__init__.py +5 -0
  612. endoreg_db/services/anonymization.py +274 -0
  613. endoreg_db/services/examination_evaluation.py +172 -0
  614. endoreg_db/services/finding_description_service.py +0 -0
  615. endoreg_db/services/lookup_service.py +424 -0
  616. endoreg_db/services/lookup_store.py +266 -0
  617. endoreg_db/services/model_meta_from_hf.py +76 -0
  618. endoreg_db/services/pdf_import.py +0 -0
  619. endoreg_db/services/polling_coordinator.py +319 -0
  620. endoreg_db/services/pseudonym_service.py +94 -0
  621. endoreg_db/services/report_import.py +13 -0
  622. endoreg_db/services/segment_sync.py +171 -0
  623. endoreg_db/services/video_import.py +9 -0
  624. endoreg_db/templates/admin/patient_finding_intervention.html +253 -0
  625. endoreg_db/templates/admin/start_examination.html +12 -0
  626. endoreg_db/templates/timeline.html +176 -0
  627. endoreg_db/urls/__init__.py +56 -0
  628. endoreg_db/urls/ai.py +14 -0
  629. endoreg_db/urls/anonymization.py +78 -0
  630. endoreg_db/urls/auth.py +16 -0
  631. endoreg_db/urls/classification.py +34 -0
  632. endoreg_db/urls/examination.py +63 -0
  633. endoreg_db/urls/media.py +251 -0
  634. endoreg_db/urls/patient.py +23 -0
  635. endoreg_db/urls/requirements.py +15 -0
  636. endoreg_db/urls/root_urls.py +28 -0
  637. endoreg_db/urls/stats.py +54 -0
  638. endoreg_db/urls/upload.py +12 -0
  639. endoreg_db/urls.py +9 -0
  640. endoreg_db/utils/__init__.py +97 -0
  641. endoreg_db/utils/ai/__init__.py +9 -0
  642. endoreg_db/utils/ai/data_loader_for_model_input.py +262 -0
  643. endoreg_db/utils/ai/data_loader_for_model_training.py +262 -0
  644. endoreg_db/utils/ai/get.py +6 -0
  645. endoreg_db/utils/ai/inference_dataset.py +51 -0
  646. endoreg_db/utils/ai/model_training/config.py +117 -0
  647. endoreg_db/utils/ai/model_training/dataset.py +74 -0
  648. endoreg_db/utils/ai/model_training/losses.py +68 -0
  649. endoreg_db/utils/ai/model_training/metrics.py +78 -0
  650. endoreg_db/utils/ai/model_training/model_backbones.py +155 -0
  651. endoreg_db/utils/ai/model_training/model_gastronet_resnet.py +118 -0
  652. endoreg_db/utils/ai/model_training/trainer_gastronet_multilabel.py +771 -0
  653. endoreg_db/utils/ai/multilabel_classification_net.py +270 -0
  654. endoreg_db/utils/ai/postprocess.py +63 -0
  655. endoreg_db/utils/ai/predict.py +293 -0
  656. endoreg_db/utils/ai/preprocess.py +76 -0
  657. endoreg_db/utils/calc_duration_seconds.py +24 -0
  658. endoreg_db/utils/case_generator/__init__.py +3 -0
  659. endoreg_db/utils/case_generator/lab_sample_factory.py +32 -0
  660. endoreg_db/utils/check_video_files.py +175 -0
  661. endoreg_db/utils/cropping.py +30 -0
  662. endoreg_db/utils/dataloader.py +285 -0
  663. endoreg_db/utils/dates.py +59 -0
  664. endoreg_db/utils/defaults/set_default_center.py +33 -0
  665. endoreg_db/utils/env.py +37 -0
  666. endoreg_db/utils/extract_specific_frames.py +87 -0
  667. endoreg_db/utils/file_operations.py +70 -0
  668. endoreg_db/utils/fix_video_path_direct.py +157 -0
  669. endoreg_db/utils/frame_anonymization_utils.py +463 -0
  670. endoreg_db/utils/hashs.py +138 -0
  671. endoreg_db/utils/links/__init__.py +0 -0
  672. endoreg_db/utils/links/requirement_link.py +237 -0
  673. endoreg_db/utils/mime_types.py +0 -0
  674. endoreg_db/utils/names.py +82 -0
  675. endoreg_db/utils/ocr.py +195 -0
  676. endoreg_db/utils/operation_log.py +87 -0
  677. endoreg_db/utils/parse_and_generate_yaml.py +45 -0
  678. endoreg_db/utils/paths.py +159 -0
  679. endoreg_db/utils/permissions.py +160 -0
  680. endoreg_db/utils/pipelines/Readme.md +235 -0
  681. endoreg_db/utils/pipelines/__init__.py +0 -0
  682. endoreg_db/utils/pipelines/process_video_dir.py +144 -0
  683. endoreg_db/utils/product/__init__.py +0 -0
  684. endoreg_db/utils/product/sum_emissions.py +22 -0
  685. endoreg_db/utils/product/sum_weights.py +20 -0
  686. endoreg_db/utils/pydantic_models/__init__.py +5 -0
  687. endoreg_db/utils/pydantic_models/db_config.py +57 -0
  688. endoreg_db/utils/requirement_helpers.py +0 -0
  689. endoreg_db/utils/requirement_operator_logic/__init__.py +0 -0
  690. endoreg_db/utils/requirement_operator_logic/_old/lab_value_operators.py +678 -0
  691. endoreg_db/utils/requirement_operator_logic/_old/model_evaluators.py +842 -0
  692. endoreg_db/utils/requirement_operator_logic/new_operator_logic.py +114 -0
  693. endoreg_db/utils/setup_config.py +196 -0
  694. endoreg_db/utils/storage.py +117 -0
  695. endoreg_db/utils/translation.py +31 -0
  696. endoreg_db/utils/uuid.py +5 -0
  697. endoreg_db/utils/validate_endo_roi.py +33 -0
  698. endoreg_db/utils/validate_subcategory_dict.py +93 -0
  699. endoreg_db/utils/validate_video_detailed.py +415 -0
  700. endoreg_db/utils/video/__init__.py +30 -0
  701. endoreg_db/utils/video/extract_frames.py +100 -0
  702. endoreg_db/utils/video/ffmpeg_wrapper.py +996 -0
  703. endoreg_db/utils/video/names.py +47 -0
  704. endoreg_db/utils/video/streaming_processor.py +386 -0
  705. endoreg_db/utils/video/video_splitter.py +105 -0
  706. endoreg_db/versioning.md +79 -0
  707. endoreg_db/views/Frames_NICE_and_PARIS_classifications_views.py +247 -0
  708. endoreg_db/views/__init__.py +157 -0
  709. endoreg_db/views/anonymization/__init__.py +31 -0
  710. endoreg_db/views/anonymization/media_management.py +486 -0
  711. endoreg_db/views/anonymization/overview.py +307 -0
  712. endoreg_db/views/anonymization/validate.py +310 -0
  713. endoreg_db/views/auth/__init__.py +13 -0
  714. endoreg_db/views/auth/keycloak.py +146 -0
  715. endoreg_db/views/examination/__init__.py +30 -0
  716. endoreg_db/views/examination/examination.py +37 -0
  717. endoreg_db/views/examination/examination_manifest_cache.py +26 -0
  718. endoreg_db/views/examination/get_finding_classification_choices.py +62 -0
  719. endoreg_db/views/examination/get_finding_classifications.py +38 -0
  720. endoreg_db/views/examination/get_findings.py +39 -0
  721. endoreg_db/views/examination/get_instruments.py +19 -0
  722. endoreg_db/views/examination/get_interventions.py +14 -0
  723. endoreg_db/views/finding/__init__.py +9 -0
  724. endoreg_db/views/finding/finding.py +116 -0
  725. endoreg_db/views/finding/get_classifications.py +14 -0
  726. endoreg_db/views/finding/get_interventions.py +17 -0
  727. endoreg_db/views/finding_classification/__init__.py +13 -0
  728. endoreg_db/views/finding_classification/base.py +0 -0
  729. endoreg_db/views/finding_classification/finding_classification.py +41 -0
  730. endoreg_db/views/finding_classification/get_classification_choices.py +54 -0
  731. endoreg_db/views/media/__init__.py +32 -0
  732. endoreg_db/views/media/pdf_media.py +411 -0
  733. endoreg_db/views/media/sensitive_metadata.py +372 -0
  734. endoreg_db/views/media/video_media.py +275 -0
  735. endoreg_db/views/meta/__init__.py +7 -0
  736. endoreg_db/views/meta/sensitive_meta_list.py +102 -0
  737. endoreg_db/views/meta/sensitive_meta_verification.py +74 -0
  738. endoreg_db/views/misc/__init__.py +29 -0
  739. endoreg_db/views/misc/center.py +14 -0
  740. endoreg_db/views/misc/csrf.py +8 -0
  741. endoreg_db/views/misc/gender.py +15 -0
  742. endoreg_db/views/misc/stats.py +255 -0
  743. endoreg_db/views/misc/upload_views.py +241 -0
  744. endoreg_db/views/patient/__init__.py +3 -0
  745. endoreg_db/views/patient/patient.py +253 -0
  746. endoreg_db/views/patient_examination/__init__.py +11 -0
  747. endoreg_db/views/patient_examination/patient_examination.py +141 -0
  748. endoreg_db/views/patient_examination/patient_examination_create.py +58 -0
  749. endoreg_db/views/patient_examination/patient_examination_detail.py +63 -0
  750. endoreg_db/views/patient_examination/patient_examination_list.py +72 -0
  751. endoreg_db/views/patient_examination/video.py +228 -0
  752. endoreg_db/views/patient_finding/__init__.py +7 -0
  753. endoreg_db/views/patient_finding/base.py +0 -0
  754. endoreg_db/views/patient_finding/patient_finding.py +71 -0
  755. endoreg_db/views/patient_finding/patient_finding_optimized.py +291 -0
  756. endoreg_db/views/patient_finding_classification/__init__.py +5 -0
  757. endoreg_db/views/patient_finding_classification/pfc_create.py +75 -0
  758. endoreg_db/views/report/__init__.py +7 -0
  759. endoreg_db/views/report/reimport.py +177 -0
  760. endoreg_db/views/report/report_stream.py +191 -0
  761. endoreg_db/views/requirement/__init__.py +11 -0
  762. endoreg_db/views/requirement/evaluate.py +278 -0
  763. endoreg_db/views/requirement/lookup.py +380 -0
  764. endoreg_db/views/requirement/lookup_store.py +183 -0
  765. endoreg_db/views/requirement/requirement_utils.py +87 -0
  766. endoreg_db/views/requirement_lookup/lookup.py +0 -0
  767. endoreg_db/views/requirement_lookup/lookup_store.py +0 -0
  768. endoreg_db/views/stats/__init__.py +13 -0
  769. endoreg_db/views/stats/stats_views.py +266 -0
  770. endoreg_db/views/video/__init__.py +49 -0
  771. endoreg_db/views/video/ai/__init__.py +8 -0
  772. endoreg_db/views/video/ai/label.py +159 -0
  773. endoreg_db/views/video/correction.py +529 -0
  774. endoreg_db/views/video/reimport.py +230 -0
  775. endoreg_db/views/video/segments_crud.py +709 -0
  776. endoreg_db/views/video/video_apply_mask.py +49 -0
  777. endoreg_db/views/video/video_correction.py +22 -0
  778. endoreg_db/views/video/video_download_processed.py +58 -0
  779. endoreg_db/views/video/video_examination_viewset.py +242 -0
  780. endoreg_db/views/video/video_metadata.py +101 -0
  781. endoreg_db/views/video/video_processing_history.py +25 -0
  782. endoreg_db/views/video/video_remove_frames.py +49 -0
  783. endoreg_db/views/video/video_stream.py +334 -0
  784. endoreg_db-0.8.9.32.dist-info/METADATA +404 -0
  785. endoreg_db-0.8.9.32.dist-info/RECORD +787 -0
  786. endoreg_db-0.8.9.32.dist-info/WHEEL +4 -0
  787. endoreg_db-0.8.9.32.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,1161 @@
1
+ import logging
2
+ import os
3
+ import random
4
+ import re # Neu hinzugefügt für Regex-Pattern
5
+ from datetime import date, datetime, timedelta
6
+ from hashlib import sha256
7
+ from typing import TYPE_CHECKING, Any, Dict, Optional, Type
8
+
9
+ from django.db import transaction
10
+ from django.utils import timezone
11
+
12
+ from endoreg_db.utils import guess_name_gender
13
+
14
+ # Assuming these utils are correctly located
15
+ from endoreg_db.utils.hashs import get_patient_examination_hash, get_patient_hash
16
+
17
+ # Import models needed for logic, use local imports inside functions if needed to break cycles
18
+ from ..administration import Center, Examiner, FirstName, LastName, Patient
19
+ from ..medical import PatientExamination
20
+ from ..other import Gender
21
+
22
+ if TYPE_CHECKING:
23
+ from .sensitive_meta import SensitiveMeta # Import model for type hinting
24
+
25
+ logger = logging.getLogger(__name__)
26
+ SECRET_SALT = os.getenv("DJANGO_SALT", "default_salt")
27
+ DEFAULT_UNKNOWN = "unknown"
28
+
29
+
30
+ # Regex-Pattern für verschiedene Datumsformate
31
+ ISO_RX = re.compile(r"^\d{4}-\d{2}-\d{2}$")
32
+ DE_RX = re.compile(r"^\d{2}\.\d{2}\.\d{4}$")
33
+
34
+
35
+ def parse_any_date(s: str) -> Optional[date]:
36
+ """
37
+ Parst Datumsstring mit Priorität auf deutsches Format (DD.MM.YYYY).
38
+
39
+ Unterstützte Formate:
40
+ 1. DD.MM.YYYY (Priorität) - deutsches Format
41
+ 2. YYYY-MM-DD (Fallback) - ISO-Format
42
+ 3. Erweiterte Fallbacks über dateparser
43
+
44
+ Args:
45
+ s: Datumsstring zum Parsen
46
+
47
+ Returns:
48
+ date-Objekt oder None bei ungültigem/fehlendem Input
49
+ """
50
+ if not s:
51
+ return None
52
+
53
+ s = s.strip()
54
+
55
+ # 1. German dd.mm.yyyy (PRIORITÄT)
56
+ if DE_RX.match(s):
57
+ try:
58
+ dd, mm, yyyy = s.split(".")
59
+ return date(int(yyyy), int(mm), int(dd))
60
+ except ValueError as e:
61
+ logger.warning(f"Invalid German date format '{s}': {e}")
62
+ return None
63
+
64
+ # 2. ISO yyyy-mm-dd (Fallback für Rückwärtskompatibilität)
65
+ if ISO_RX.match(s):
66
+ try:
67
+ return date.fromisoformat(s)
68
+ except ValueError as e:
69
+ logger.warning(f"Invalid ISO date format '{s}': {e}")
70
+ return None
71
+
72
+ # 3. Extended fallbacks
73
+ try:
74
+ # Try standard datetime parsing
75
+ return datetime.fromisoformat(s).date()
76
+ except Exception:
77
+ pass
78
+
79
+ try:
80
+ # Try dateparser with German locale preference
81
+ import dateparser
82
+
83
+ dt = dateparser.parse(
84
+ s, settings={"DATE_ORDER": "DMY", "PREFER_DAY_OF_MONTH": "first"}
85
+ )
86
+ return dt.date() if dt else None
87
+ except Exception as e:
88
+ logger.debug(f"Dateparser fallback failed for '{s}': {e}")
89
+ return None
90
+
91
+
92
+ def format_date_german(d: Optional[date]) -> str:
93
+ """
94
+ Formatiert date-Objekt als deutsches Datumsformat (DD.MM.YYYY).
95
+
96
+ Args:
97
+ d: date-Objekt oder None
98
+
99
+ Returns:
100
+ Formatiertes Datum als String oder leerer String bei None
101
+ """
102
+ if not d:
103
+ return ""
104
+ return d.strftime("%d.%m.%Y")
105
+
106
+
107
+ def format_date_iso(d: Optional[date]) -> str:
108
+ """
109
+ Formatiert date-Objekt als ISO-Format (YYYY-MM-DD).
110
+
111
+ Args:
112
+ d: date-Objekt oder None
113
+
114
+ Returns:
115
+ Formatiertes Datum als String oder leerer String bei None
116
+ """
117
+ if not d:
118
+ return ""
119
+ return d.isoformat()
120
+
121
+
122
+ def generate_random_dob() -> datetime:
123
+ """Generates a random timezone-aware datetime between 1920-01-01 and 2000-12-31."""
124
+ start_date = date(1920, 1, 1)
125
+ end_date = date(2000, 12, 31)
126
+ time_between_dates = end_date - start_date
127
+ days_between_dates = time_between_dates.days
128
+ random_number_of_days = random.randrange(days_between_dates)
129
+ random_date = start_date + timedelta(days=random_number_of_days)
130
+ random_datetime = datetime.combine(random_date, datetime.min.time())
131
+ return timezone.make_aware(random_datetime)
132
+
133
+
134
+ def generate_random_examination_date() -> date:
135
+ """Generates a random date within the last 20 years."""
136
+ today = date.today()
137
+ start_date = today - timedelta(days=20 * 365) # Approximate 20 years back
138
+ time_between_dates = today - start_date
139
+ days_between_dates = time_between_dates.days
140
+ random_number_of_days = random.randrange(days_between_dates)
141
+ random_date = start_date + timedelta(days=random_number_of_days)
142
+ return random_date
143
+
144
+
145
+ def update_name_db(first_name: Optional[str], last_name: Optional[str]):
146
+ """Adds first and last names to the respective lookup tables if they don't exist."""
147
+ if first_name:
148
+ FirstName.objects.get_or_create(name=first_name)
149
+ if last_name:
150
+ LastName.objects.get_or_create(name=last_name)
151
+
152
+
153
+ def calculate_patient_hash(instance: "SensitiveMeta", salt: str = SECRET_SALT) -> str:
154
+ """Calculates the patient hash for the instance."""
155
+ dob = instance.patient_dob
156
+ first_name = instance.patient_first_name
157
+ last_name = instance.patient_last_name
158
+ center = instance.center
159
+
160
+ if not dob:
161
+ raise ValueError("Patient DOB is required to calculate patient hash.")
162
+ if not center:
163
+ raise ValueError("Center is required to calculate patient hash.")
164
+
165
+ assert first_name is not None, "First name is required to calculate patient hash."
166
+ assert last_name is not None, "Last name is required to calculate patient hash."
167
+
168
+ hash_str = get_patient_hash(
169
+ first_name=first_name,
170
+ last_name=last_name,
171
+ dob=dob,
172
+ center=center.name, # Use center name
173
+ salt=salt,
174
+ )
175
+ return sha256(hash_str.encode()).hexdigest()
176
+
177
+
178
+ def calculate_examination_hash(
179
+ instance: "SensitiveMeta", salt: str = SECRET_SALT
180
+ ) -> str:
181
+ """Calculates the examination hash for the instance."""
182
+ dob = instance.patient_dob
183
+ first_name = instance.patient_first_name
184
+ last_name = instance.patient_last_name
185
+ examination_date = instance.examination_date
186
+ center = instance.center
187
+
188
+ if not dob:
189
+ raise ValueError("Patient DOB is required to calculate examination hash.")
190
+ if not examination_date:
191
+ raise ValueError("Examination date is required to calculate examination hash.")
192
+ if not center:
193
+ raise ValueError("Center is required to calculate examination hash.")
194
+
195
+ if not first_name:
196
+ raise ValueError("First name is required to calculate examination hash.")
197
+ if not last_name:
198
+ raise ValueError("Last name is required to calculate examination hash.")
199
+
200
+ hash_str = get_patient_examination_hash(
201
+ first_name=first_name,
202
+ last_name=last_name,
203
+ dob=dob,
204
+ examination_date=examination_date,
205
+ center=center.name, # Use center name
206
+ salt=salt,
207
+ )
208
+ return sha256(hash_str.encode()).hexdigest()
209
+
210
+
211
+ def create_pseudo_examiner_logic(instance: "SensitiveMeta") -> "Examiner":
212
+ """Creates or retrieves the pseudo examiner based on instance data."""
213
+ first_name = instance.examiner_first_name
214
+ last_name = instance.examiner_last_name
215
+ center = instance.center # Should be set before calling save
216
+
217
+ if not first_name or not last_name or not center:
218
+ logger.warning(
219
+ f"Incomplete examiner info for SensitiveMeta (pk={instance.pk or 'new'}). Using default examiner."
220
+ )
221
+ # Ensure default center exists or handle appropriately
222
+ try:
223
+ default_center = Center.objects.get(name="endoreg_db_demo")
224
+ except Center.DoesNotExist:
225
+ logger.error(
226
+ "Default center 'endoreg_db_demo' not found. Cannot create default examiner."
227
+ )
228
+ raise ValueError("Default center 'endoreg_db_demo' not found.")
229
+
230
+ examiner, _created = Examiner.custom_get_or_create(
231
+ first_name="Unknown", last_name="Unknown", center=default_center
232
+ )
233
+ else:
234
+ examiner, _created = Examiner.custom_get_or_create(
235
+ first_name=first_name, last_name=last_name, center=center
236
+ )
237
+
238
+ return examiner
239
+
240
+
241
+ def get_or_create_pseudo_patient_logic(instance: "SensitiveMeta"):
242
+ """Gets or creates the pseudo patient based on instance data."""
243
+ # Ensure necessary fields are set
244
+ if not instance.patient_hash:
245
+ instance.patient_hash = calculate_patient_hash(instance)
246
+ if not instance.center:
247
+ raise ValueError("Center must be set before creating pseudo patient.")
248
+ if not instance.patient_gender:
249
+ raise ValueError("Patient gender must be set before creating pseudo patient.")
250
+ if not instance.patient_dob:
251
+ raise ValueError("Patient DOB must be set before creating pseudo patient.")
252
+
253
+ dob = instance.patient_dob
254
+ year = dob.year
255
+ month = dob.month
256
+
257
+ patient, _created = Patient.get_or_create_pseudo_patient_by_hash(
258
+ patient_hash=instance.patient_hash,
259
+ center=instance.center,
260
+ gender=instance.patient_gender,
261
+ birth_year=year,
262
+ birth_month=month,
263
+ )
264
+ return patient, _created
265
+
266
+
267
+ def get_or_create_pseudo_patient_examination_logic(
268
+ instance: "SensitiveMeta",
269
+ ):
270
+ """Gets or creates the pseudo patient examination based on instance data."""
271
+ # Ensure necessary fields are set
272
+ if not instance.patient_hash:
273
+ instance.patient_hash = calculate_patient_hash(instance)
274
+ if not instance.examination_hash:
275
+ instance.examination_hash = calculate_examination_hash(instance)
276
+
277
+ # Ensure the pseudo patient exists first, as PatientExamination might depend on it
278
+ if not instance.pseudo_patient:
279
+ pseudo_patient, _created = get_or_create_pseudo_patient_logic(instance)
280
+ instance.pseudo_patient = pseudo_patient # Assign FK directly
281
+
282
+ patient_examination, _created = (
283
+ PatientExamination.get_or_create_pseudo_patient_examination_by_hash(
284
+ patient_hash=instance.patient_hash,
285
+ examination_hash=instance.examination_hash,
286
+ # Optionally pass pseudo_patient if the method requires it
287
+ # pseudo_patient=instance.pseudo_patient
288
+ )
289
+ )
290
+ return patient_examination, _created
291
+
292
+
293
+ @transaction.atomic # Ensure all operations within save succeed or fail together
294
+ def perform_save_logic(instance: "SensitiveMeta") -> "Examiner":
295
+ """
296
+ Contains the core logic for preparing a SensitiveMeta instance for saving.
297
+ Handles data generation (dates), hash calculation, and linking pseudo-entities.
298
+
299
+ This function is called on every save() operation and implements a two-phase approach:
300
+
301
+ **Phase 1: Initial Creation (with defaults)**
302
+ - When a SensitiveMeta is first created (e.g., via create_from_dict),
303
+ it may have missing patient data (names, DOB, etc.)
304
+ - Default values are set to prevent hash calculation errors:
305
+ * patient_first_name: "unknown"
306
+ * patient_last_name: "unknown"
307
+ * patient_dob: random date (1920-2000)
308
+ - A temporary hash is calculated using these defaults
309
+ - Temporary pseudo-entities (Patient, Examination) are created
310
+
311
+ **Phase 2: Update (with extracted data)**
312
+ - When real patient data is extracted (e.g., from video OCR via lx_anonymizer),
313
+ update_from_dict() is called with actual values
314
+ - The instance fields are updated with real data (names, DOB, etc.)
315
+ - save() is called again, triggering this function
316
+ - Default-setting logic is skipped (fields are no longer empty)
317
+ - Hash is RECALCULATED with real data
318
+ - New pseudo-entities are created/retrieved based on new hash
319
+
320
+ **Example Flow:**
321
+ ```
322
+ # Initial creation
323
+ sm = SensitiveMeta.create_from_dict({"center": center})
324
+ # → patient_first_name = "unknown", patient_last_name = "unknown"
325
+ # → hash = sha256("unknown unknown 1990-01-01 ...")
326
+ # → pseudo_patient_temp created
327
+
328
+ # Later update with extracted data
329
+ sm.update_from_dict({"patient_first_name": "Max", "patient_last_name": "Mustermann"})
330
+ # → patient_first_name = "Max", patient_last_name = "Mustermann" (overwrites)
331
+ # → save() triggered → perform_save_logic() called again
332
+ # → Default-setting skipped (names already exist)
333
+ # → hash = sha256("Max Mustermann 1985-03-15 ...") (RECALCULATED)
334
+ # → pseudo_patient_real created/retrieved with new hash
335
+ ```
336
+
337
+ Args:
338
+ instance: The SensitiveMeta instance being saved
339
+
340
+ Returns:
341
+ Examiner: The pseudo examiner instance to be linked via M2M after save
342
+
343
+ Raises:
344
+ ValueError: If required fields (center, gender) cannot be determined
345
+ """
346
+
347
+ # --- Pre-Save Checks and Data Generation ---
348
+
349
+ # 1. Ensure DOB and Examination Date exist
350
+ if not instance.patient_dob:
351
+ logger.debug(
352
+ f"SensitiveMeta (pk={instance.pk or 'new'}): Patient DOB missing, generating random."
353
+ )
354
+ instance.patient_dob = generate_random_dob()
355
+ if not instance.examination_date:
356
+ logger.debug(
357
+ f"SensitiveMeta (pk={instance.pk or 'new'}): Examination date missing, generating random."
358
+ )
359
+ instance.examination_date = generate_random_examination_date()
360
+
361
+ # 2. Ensure Center exists (should be set before calling save)
362
+ if not instance.center:
363
+ raise ValueError("Center must be set before saving SensitiveMeta.")
364
+
365
+ # 2.5 CRITICAL: Set default patient names BEFORE hash calculation
366
+ #
367
+ # **Why this is necessary:**
368
+ # Hash calculation (step 4) requires first_name and last_name to be non-None.
369
+ # However, on initial creation (e.g., via get_or_create_sensitive_meta()), these
370
+ # fields may be empty because real patient data hasn't been extracted yet.
371
+ #
372
+ # **Two-phase approach:**
373
+ # - Phase 1 (Initial): Set defaults if names are missing
374
+ # → Allows hash calculation to succeed without errors
375
+ # → Creates temporary pseudo-entities with default hash
376
+ #
377
+ # - Phase 2 (Update): Real data extraction (OCR, manual input)
378
+ # → update_from_dict() sets real names ("Max", "Mustermann")
379
+ # → save() is called again
380
+ # → This block is SKIPPED (names already exist)
381
+ # → Hash is recalculated with real data (step 4)
382
+ # → New pseudo-entities created with correct hash
383
+ #
384
+ # **Example:**
385
+ # Initial: patient_first_name = "unknown" → hash = sha256("unknown unknown...")
386
+ # Updated: patient_first_name = "Max" → hash = sha256("Max Mustermann...")
387
+ #
388
+ if not instance.patient_first_name:
389
+ instance.patient_first_name = DEFAULT_UNKNOWN
390
+ logger.debug(
391
+ "SensitiveMeta (pk=%s): Patient first name missing, set to default '%s'.",
392
+ instance.pk or "new",
393
+ DEFAULT_UNKNOWN,
394
+ )
395
+
396
+ if not instance.patient_last_name:
397
+ instance.patient_last_name = DEFAULT_UNKNOWN
398
+ logger.debug(
399
+ "SensitiveMeta (pk=%s): Patient last name missing, set to default '%s'.",
400
+ instance.pk or "new",
401
+ DEFAULT_UNKNOWN,
402
+ )
403
+
404
+ # 3. Ensure Gender exists (should be set before calling save, e.g., during creation/update)
405
+ if not instance.patient_gender:
406
+ # Use the now-guaranteed first_name for gender guessing
407
+ first_name = instance.patient_first_name
408
+ gender_str = guess_name_gender(first_name)
409
+ if not gender_str:
410
+ raise ValueError(
411
+ "Patient gender could not be determined and must be set before saving."
412
+ )
413
+ # Convert string to Gender object
414
+ try:
415
+ gender_obj = Gender.objects.get(name=gender_str)
416
+ instance.patient_gender = gender_obj
417
+ except Gender.DoesNotExist:
418
+ # If the gender is 'unknown' (likely because name was DEFAULT_UNKNOWN),
419
+ # we should auto-create it rather than crashing.
420
+ if gender_str == "unknown" or instance.patient_first_name == DEFAULT_UNKNOWN:
421
+ logger.warning(
422
+ f"Gender '{gender_str}' not found in DB. Auto-creating default entry."
423
+ )
424
+ gender_obj, _ = Gender.objects.get_or_create(
425
+ name="unknown",
426
+ defaults={
427
+ "abbreviation": "?",
428
+ "description": "Auto-created default gender"
429
+ }
430
+ )
431
+ instance.patient_gender = gender_obj
432
+ else:
433
+ # If it's a specific gender (e.g., 'male') that is missing,
434
+ # that is a configuration error we should raise.
435
+ raise ValueError(f"Gender '{gender_str}' not found in database.")
436
+ # 4. Calculate Hashes (depends on DOB, Exam Date, Center, Names)
437
+ #
438
+ # **IMPORTANT: Hashes are RECALCULATED on every save!**
439
+ # This enables the two-phase update pattern:
440
+ # - Initial save: Hash based on default "unknown unknown" names
441
+ # - Updated save: Hash based on real extracted names ("Max Mustermann")
442
+ #
443
+ # The new hash will link to different pseudo-entities, ensuring proper
444
+ # anonymization while maintaining referential integrity.
445
+ instance.patient_hash = calculate_patient_hash(instance)
446
+ instance.examination_hash = calculate_examination_hash(instance)
447
+
448
+ # 5. Get or Create Pseudo Patient (depends on hash, center, gender, dob)
449
+ # Assign directly to the FK field to avoid premature saving issues
450
+ pseudo_patient, _created = get_or_create_pseudo_patient_logic(instance)
451
+ instance.pseudo_patient = pseudo_patient
452
+
453
+ # 6. Get or Create Pseudo Examination (depends on hashes)
454
+ # Assign directly to the FK field
455
+ pseudo_examination, _created = get_or_create_pseudo_patient_examination_logic(
456
+ instance
457
+ )
458
+ instance.pseudo_examination = pseudo_examination
459
+
460
+ # 7. Get or Create Pseudo Examiner (depends on names, center)
461
+ # This needs to happen *after* the main instance has a PK for M2M linking.
462
+ # We create/get it here and return it to the main save method.
463
+ examiner_instance = create_pseudo_examiner_logic(instance)
464
+
465
+ # 8. Ensure SensitiveMetaState exists (will be checked/created *after* main save)
466
+
467
+ # Return the examiner instance so the model's save method can handle M2M linking
468
+ return examiner_instance
469
+
470
+
471
+ def create_sensitive_meta_from_dict(
472
+ cls: Type["SensitiveMeta"], data: Dict[str, Any]
473
+ ) -> "SensitiveMeta":
474
+ """
475
+ Create a SensitiveMeta instance from a dictionary.
476
+
477
+ **Center handling:**
478
+ This function accepts TWO ways to specify the center:
479
+ 1. `center` (Center object) - Directly pass a Center instance
480
+ 2. `center_name` (string) - Pass the center name as a string (will be resolved to Center object)
481
+
482
+ At least ONE of these must be provided.
483
+
484
+ **Example usage:**
485
+ ```python
486
+ # Option 1: With Center object
487
+ data = {
488
+ "patient_first_name": "Patient",
489
+ "patient_last_name": "Unknown",
490
+ "patient_dob": date(1990, 1, 1),
491
+ "examination_date": date.today(),
492
+ "center": center_obj, # ← Center object
493
+ "text": text #from extraction
494
+
495
+ }
496
+ sm = SensitiveMeta.create_from_dict(data)
497
+
498
+ # Option 2: With center name string
499
+ data = {
500
+ "patient_first_name": "Patient",
501
+ "patient_last_name": "Unknown",
502
+ "patient_dob": date(1990, 1, 1),
503
+ "examination_date": date.today(),
504
+ "center_name": "university_hospital_wuerzburg", # ← String
505
+ "anonymized_text": "anonymized text"
506
+ }
507
+ sm = SensitiveMeta.create_from_dict(data)
508
+ ```
509
+
510
+ Args:
511
+ cls: The SensitiveMeta class
512
+ data: Dictionary containing field values
513
+
514
+ Returns:
515
+ SensitiveMeta: The created instance
516
+
517
+ Raises:
518
+ ValueError: If neither center nor center_name is provided
519
+ ValueError: If center_name does not match any Center in database
520
+ """
521
+
522
+ field_names = {
523
+ f.name
524
+ for f in cls._meta.get_fields()
525
+ if not f.is_relation or f.one_to_one or (f.many_to_one and f.related_model)
526
+ }
527
+ selected_data = {k: v for k, v in data.items() if k in field_names}
528
+
529
+ # --- Convert patient_dob if it's a date object ---
530
+ dob = selected_data.get("patient_dob")
531
+ if isinstance(dob, date) and not isinstance(dob, datetime):
532
+ # Convert date to datetime at the start of the day and make it timezone-aware
533
+ aware_dob = timezone.make_aware(datetime.combine(dob, datetime.min.time()))
534
+ selected_data["patient_dob"] = aware_dob
535
+ logger.debug("Converted patient_dob from date to aware datetime: %s", aware_dob)
536
+ elif isinstance(dob, str):
537
+ # Handle string DOB - check if it's a field name or actual date
538
+ if dob == "patient_dob" or dob in [
539
+ "patient_first_name",
540
+ "patient_last_name",
541
+ "examination_date",
542
+ ]:
543
+ logger.warning(
544
+ "Skipping invalid patient_dob value '%s' - appears to be field name",
545
+ dob,
546
+ )
547
+ selected_data.pop("patient_dob", None) # Remove invalid value
548
+ else:
549
+ # Try to parse as date string
550
+ try:
551
+ import dateparser
552
+
553
+ parsed_dob = dateparser.parse(
554
+ dob, languages=["de"], settings={"DATE_ORDER": "DMY"}
555
+ )
556
+ if parsed_dob:
557
+ aware_dob = timezone.make_aware(
558
+ parsed_dob.replace(hour=0, minute=0, second=0, microsecond=0)
559
+ )
560
+ selected_data["patient_dob"] = aware_dob
561
+ logger.debug(
562
+ "Parsed string patient_dob '%s' to aware datetime: %s",
563
+ dob,
564
+ aware_dob,
565
+ )
566
+ else:
567
+ logger.warning(
568
+ "Could not parse patient_dob string '%s', removing from data",
569
+ dob,
570
+ )
571
+ selected_data.pop("patient_dob", None)
572
+ except Exception as e:
573
+ logger.warning(
574
+ "Error parsing patient_dob string '%s': %s, removing from data",
575
+ dob,
576
+ e,
577
+ )
578
+ selected_data.pop("patient_dob", None)
579
+ # --- End Conversion ---
580
+
581
+ # Similar validation for examination_date
582
+ exam_date = selected_data.get("examination_date")
583
+ if isinstance(exam_date, str):
584
+ if exam_date == "examination_date" or exam_date in [
585
+ "patient_first_name",
586
+ "patient_last_name",
587
+ "patient_dob",
588
+ ]:
589
+ logger.warning(
590
+ "Skipping invalid examination_date value '%s' - appears to be field name",
591
+ exam_date,
592
+ )
593
+ selected_data.pop("examination_date", None)
594
+ else:
595
+ # Try to parse as date string
596
+ try:
597
+ # First try simple ISO format for YYYY-MM-DD
598
+ if len(exam_date) == 10 and exam_date.count("-") == 2:
599
+ try:
600
+ from datetime import datetime as dt
601
+
602
+ parsed_date = dt.strptime(exam_date, "%Y-%m-%d").date()
603
+ selected_data["examination_date"] = parsed_date
604
+ logger.debug(
605
+ "Parsed ISO examination_date '%s' to date: %s",
606
+ exam_date,
607
+ parsed_date,
608
+ )
609
+ except ValueError:
610
+ # Fall back to dateparser for complex formats
611
+ import dateparser
612
+
613
+ parsed_date = dateparser.parse(
614
+ exam_date, languages=["de"], settings={"DATE_ORDER": "DMY"}
615
+ )
616
+ if parsed_date:
617
+ selected_data["examination_date"] = parsed_date.date()
618
+ logger.debug(
619
+ "Parsed string examination_date '%s' to date: %s",
620
+ exam_date,
621
+ parsed_date.date(),
622
+ )
623
+ else:
624
+ logger.warning(
625
+ "Could not parse examination_date string '%s', removing from data",
626
+ exam_date,
627
+ )
628
+ selected_data.pop("examination_date", None)
629
+ else:
630
+ # Use dateparser for non-ISO formats
631
+ import dateparser
632
+
633
+ parsed_date = dateparser.parse(
634
+ exam_date, languages=["de"], settings={"DATE_ORDER": "DMY"}
635
+ )
636
+ if parsed_date:
637
+ selected_data["examination_date"] = parsed_date.date()
638
+ logger.debug(
639
+ "Parsed string examination_date '%s' to date: %s",
640
+ exam_date,
641
+ parsed_date.date(),
642
+ )
643
+ else:
644
+ logger.warning(
645
+ "Could not parse examination_date string '%s', removing from data",
646
+ exam_date,
647
+ )
648
+ selected_data.pop("examination_date", None)
649
+
650
+ except Exception as e:
651
+ logger.warning(
652
+ "Error parsing examination_date string '%s': %s, removing from data",
653
+ exam_date,
654
+ e,
655
+ )
656
+ selected_data.pop("examination_date", None)
657
+
658
+ # Handle Center - accept both center_name (string) and center (object)
659
+ from ..administration import Center
660
+
661
+ center = data.get("center") # First try direct Center object
662
+ center_name = data.get("center_name")
663
+
664
+ if center is not None:
665
+ # Center object provided directly - validate it's a Center instance
666
+ if not isinstance(center, Center):
667
+ raise ValueError(f"'center' must be a Center instance, got {type(center)}")
668
+ selected_data["center"] = center
669
+ elif center_name:
670
+ # center_name string provided - resolve to Center object
671
+ try:
672
+ center = Center.objects.get(name=center_name)
673
+ selected_data["center"] = center
674
+ except Center.DoesNotExist:
675
+ raise ValueError(f"Center with name '{center_name}' does not exist.")
676
+ else:
677
+ # Neither center nor center_name provided
678
+ raise ValueError(
679
+ "Either 'center' (Center object) or 'center_name' (string) is required in data dictionary."
680
+ )
681
+
682
+ # Handle Names and Gender
683
+ first_name = selected_data.get("patient_first_name") or DEFAULT_UNKNOWN
684
+ last_name = selected_data.get("patient_last_name") or DEFAULT_UNKNOWN
685
+ selected_data["patient_first_name"] = first_name # Ensure defaults are set
686
+ selected_data["patient_last_name"] = last_name
687
+
688
+ patient_gender_input = selected_data.get("patient_gender")
689
+
690
+ if isinstance(patient_gender_input, Gender):
691
+ # Already a Gender object, nothing to do
692
+ pass
693
+ elif isinstance(patient_gender_input, str):
694
+ # Input is a string (gender name)
695
+ try:
696
+ selected_data["patient_gender"] = Gender.objects.get(
697
+ name=patient_gender_input
698
+ )
699
+ except Gender.DoesNotExist:
700
+ logger.warning(
701
+ f"Gender with name '{patient_gender_input}' provided but not found. Attempting to guess or use default."
702
+ )
703
+ # Fall through to guessing logic if provided string name is invalid
704
+ normalized = (patient_gender_input or "").lower()
705
+ if normalized in {"male", "female", "unknown"}:
706
+ gender_obj, _ = Gender.objects.get_or_create(
707
+ name=normalized,
708
+ defaults={
709
+ "abbreviation": normalized[:1].upper() or None,
710
+ "description": "Auto-created default gender entry",
711
+ },
712
+ )
713
+ selected_data["patient_gender"] = gender_obj
714
+ else:
715
+ patient_gender_input = None # Reset to trigger guessing
716
+
717
+ if not isinstance(
718
+ selected_data.get("patient_gender"), Gender
719
+ ): # If not already a Gender object (e.g. was None, or string lookup failed)
720
+ gender_name_to_use = guess_name_gender(first_name)
721
+ if not gender_name_to_use:
722
+ logger.warning(
723
+ f"Could not guess gender for name '{first_name}'. Setting Gender to unknown."
724
+ )
725
+ gender_name_to_use = "unknown"
726
+ try:
727
+ selected_data["patient_gender"] = Gender.objects.get(
728
+ name=gender_name_to_use
729
+ )
730
+ except Gender.DoesNotExist:
731
+ gender_obj, _ = Gender.objects.get_or_create(
732
+ name=gender_name_to_use,
733
+ defaults={
734
+ "abbreviation": gender_name_to_use[:1].upper() or None,
735
+ "description": "Auto-created default gender entry",
736
+ },
737
+ )
738
+ selected_data["patient_gender"] = gender_obj
739
+
740
+ # Handle Text
741
+ selected_data["text"] = data.get("text") or DEFAULT_UNKNOWN
742
+
743
+ # --- Add missing optional fields safely ---
744
+ file_path = data.get("file_path")
745
+ if file_path:
746
+ selected_data["file_path"] = str(file_path)
747
+ logger.debug(f"Set file_path: {file_path}")
748
+
749
+ casenumber = data.get("casenumber")
750
+ if casenumber:
751
+ selected_data["casenumber"] = str(casenumber).strip()
752
+ logger.debug(f"Set casenumber: {casenumber}")
753
+
754
+ exam_time = data.get("examination_time")
755
+ if exam_time:
756
+ try:
757
+ from datetime import time as dt_time
758
+
759
+ # Accepts strings like "14:35" or full datetime
760
+ if isinstance(exam_time, str):
761
+ h, m = exam_time.strip().split(":")[:2]
762
+ selected_data["examination_time"] = dt_time(int(h), int(m))
763
+ elif isinstance(exam_time, datetime):
764
+ selected_data["examination_time"] = exam_time.time()
765
+ elif isinstance(exam_time, date):
766
+ # no time info — ignore
767
+ logger.debug(
768
+ f"examination_time value {exam_time} has no time component; skipping"
769
+ )
770
+ else:
771
+ selected_data["examination_time"] = exam_time
772
+ except Exception as e:
773
+ logger.warning(f"Invalid examination_time '{exam_time}': {e}")
774
+
775
+ anonymized_text = data.get("anonymized_text") or data.get("anonym_text")
776
+ if anonymized_text:
777
+ if isinstance(anonymized_text, (str, bytes)):
778
+ selected_data["anonymized_text"] = (
779
+ anonymized_text.decode()
780
+ if isinstance(anonymized_text, bytes)
781
+ else anonymized_text
782
+ )
783
+ else:
784
+ selected_data["anonymized_text"] = str(anonymized_text)
785
+ logger.debug(
786
+ "Set anonymized_text (length=%d)", len(selected_data["anonymized_text"])
787
+ )
788
+
789
+ # Update name DB
790
+ update_name_db(first_name, last_name)
791
+
792
+ # Instantiate without saving yet
793
+ sensitive_meta = cls(**selected_data)
794
+
795
+ # Call save once at the end. This triggers the custom save logic.
796
+ sensitive_meta.save() # This will call perform_save_logic internally
797
+
798
+ return sensitive_meta
799
+
800
+
801
+ def update_sensitive_meta_from_dict(
802
+ instance: "SensitiveMeta", data: Dict[str, Any]
803
+ ) -> "SensitiveMeta":
804
+ """
805
+ Updates a SensitiveMeta instance from a dictionary of new values.
806
+
807
+ **Integration with two-phase save pattern:**
808
+ This function is typically called after initial SensitiveMeta creation when real
809
+ patient data becomes available (e.g., extracted from video OCR, report parsing, or
810
+ manual annotation).
811
+
812
+ **Example workflow:**
813
+ ```python
814
+ # Phase 1: Initial creation with defaults
815
+ sm = SensitiveMeta.create_from_dict({"center": center})
816
+ # → patient_first_name = "unknown", hash = sha256("unknown...")
817
+
818
+ # Phase 2: Update with extracted data
819
+ extracted = {
820
+ "patient_first_name": "Max",
821
+ "patient_last_name": "Mustermann",
822
+ "patient_dob": date(1985, 3, 15)
823
+ }
824
+ update_sensitive_meta_from_dict(sm, extracted)
825
+ # → Sets: sm.patient_first_name = "Max", sm.patient_last_name = "Mustermann"
826
+ # → Calls: sm.save()
827
+ # → Triggers: perform_save_logic() again
828
+ # → Result: Hash recalculated with real data, new pseudo-entities created
829
+ ```
830
+
831
+ **Key behaviors:**
832
+ - Updates instance attributes from provided dictionary
833
+ - Handles type conversions (date strings → date objects, gender strings → Gender objects)
834
+ - Tracks patient name changes to update name database
835
+ - Calls save() at the end, triggering full save logic including hash recalculation
836
+ - Default-setting in perform_save_logic() is skipped (fields already populated)
837
+
838
+ Args:
839
+ instance: The existing SensitiveMeta instance to update
840
+ data: Dictionary of field names and new values
841
+
842
+ Returns:
843
+ The updated SensitiveMeta instance
844
+
845
+ Raises:
846
+ Exception: If save fails or required conversions fail
847
+ """
848
+ field_names = {
849
+ f.name
850
+ for f in instance._meta.get_fields()
851
+ if not f.is_relation or f.one_to_one or (f.many_to_one and f.related_model)
852
+ }
853
+ # Exclude FKs that should not be updated directly from dict keys (handled separately or via save logic)
854
+ excluded_fields = {"pseudo_patient", "pseudo_examination"}
855
+ selected_data = {
856
+ k: v for k, v in data.items() if k in field_names and k not in excluded_fields
857
+ }
858
+
859
+ # Handle potential Center update - accept both center_name (string) and center (object)
860
+ from ..administration import Center
861
+
862
+ center = data.get("center") # First try direct Center object
863
+ center_name = data.get("center_name")
864
+
865
+ if center is not None:
866
+ # Center object provided directly - validate and update
867
+ if isinstance(center, Center):
868
+ instance.center = center
869
+ logger.debug(f"Updated center from Center object: {center.name}")
870
+ else:
871
+ logger.warning(
872
+ f"Invalid center type {type(center)}, expected Center instance. Ignoring."
873
+ )
874
+ # Remove from selected_data to prevent override
875
+ selected_data.pop("center", None)
876
+ elif center_name:
877
+ # center_name string provided - resolve to Center object
878
+ try:
879
+ center_obj = Center.objects.get(name=center_name)
880
+ instance.center = center_obj
881
+ logger.debug(f"Updated center from center_name string: {center_name}")
882
+ except Center.DoesNotExist:
883
+ logger.warning(
884
+ f"Center '{center_name}' not found during update. Keeping existing center."
885
+ )
886
+ else:
887
+ # Both are None/missing - remove 'center' from selected_data to preserve existing value
888
+ selected_data.pop("center", None)
889
+ # If both are None/missing, keep existing center (no update needed)
890
+
891
+ # Set examiner names if provided, before calling save
892
+ examiner_first_name = data.get("examiner_first_name")
893
+ examiner_last_name = data.get("examiner_last_name")
894
+ if examiner_first_name is not None: # Allow setting empty strings
895
+ instance.examiner_first_name = examiner_first_name
896
+ if examiner_last_name is not None:
897
+ instance.examiner_last_name = examiner_last_name
898
+
899
+ # Handle patient_gender specially with graceful error handling
900
+ patient_gender_input = data.get("patient_gender")
901
+ if patient_gender_input is not None:
902
+ try:
903
+ if isinstance(patient_gender_input, Gender):
904
+ selected_data["patient_gender"] = patient_gender_input
905
+ elif isinstance(patient_gender_input, str):
906
+ gender_input_clean = patient_gender_input.strip()
907
+ # Try direct case-insensitive DB lookup first
908
+ gender_obj = Gender.objects.filter(
909
+ name__iexact=gender_input_clean
910
+ ).first()
911
+ if gender_obj:
912
+ selected_data["patient_gender"] = gender_obj
913
+ logger.debug(
914
+ f"Successfully matched gender string '{patient_gender_input}' to Gender object via iexact lookup"
915
+ )
916
+ else:
917
+ # Use mapping helper for fallback
918
+ mapped = _map_gender_string_to_standard(gender_input_clean)
919
+ if mapped:
920
+ gender_obj = Gender.objects.filter(name__iexact=mapped).first()
921
+ if gender_obj:
922
+ selected_data["patient_gender"] = gender_obj
923
+ logger.info(
924
+ f"Mapped gender '{patient_gender_input}' to '{mapped}' via fallback mapping"
925
+ )
926
+ else:
927
+ logger.warning(
928
+ f"Mapped gender '{patient_gender_input}' to '{mapped}', but no such Gender in DB. Trying 'unknown'."
929
+ )
930
+ unknown_gender = Gender.objects.filter(
931
+ name__iexact="unknown"
932
+ ).first()
933
+ if unknown_gender:
934
+ selected_data["patient_gender"] = unknown_gender
935
+ logger.warning(
936
+ f"Using 'unknown' gender as fallback for '{patient_gender_input}'"
937
+ )
938
+ else:
939
+ logger.error(
940
+ f"No 'unknown' gender found in database. Cannot handle gender '{patient_gender_input}'. Skipping gender update."
941
+ )
942
+ selected_data.pop("patient_gender", None)
943
+ else:
944
+ # Last resort: try to get 'unknown' gender
945
+ unknown_gender = Gender.objects.filter(
946
+ name__iexact="unknown"
947
+ ).first()
948
+ if unknown_gender:
949
+ selected_data["patient_gender"] = unknown_gender
950
+ logger.warning(
951
+ f"Using 'unknown' gender as fallback for '{patient_gender_input}' (no mapping)"
952
+ )
953
+ else:
954
+ logger.error(
955
+ f"No 'unknown' gender found in database. Cannot handle gender '{patient_gender_input}'. Skipping gender update."
956
+ )
957
+ selected_data.pop("patient_gender", None)
958
+ else:
959
+ logger.warning(
960
+ f"Unexpected patient_gender type {type(patient_gender_input)}: {patient_gender_input}. Skipping gender update."
961
+ )
962
+ selected_data.pop("patient_gender", None)
963
+ except Exception as e:
964
+ logger.exception(
965
+ f"Error handling patient_gender '{patient_gender_input}': {e}. Skipping gender update."
966
+ )
967
+ selected_data.pop("patient_gender", None)
968
+
969
+ # TODO Review: Handle new optional fields on update
970
+ for key in (
971
+ "file_path",
972
+ "casenumber",
973
+ "examination_time",
974
+ "anonymized_text",
975
+ "anonym_text",
976
+ ):
977
+ if key in data and data[key] is not None:
978
+ val = data[key]
979
+ if key in ("file_path", "casenumber"):
980
+ setattr(instance, key, str(val))
981
+ elif key in ("anonymized_text", "anonym_text"):
982
+ setattr(
983
+ instance,
984
+ "anonymized_text",
985
+ val if isinstance(val, str) else str(val),
986
+ )
987
+ elif key == "examination_time":
988
+ try:
989
+ from datetime import time as dt_time
990
+
991
+ if isinstance(val, str) and ":" in val:
992
+ h, m = val.strip().split(":")[:2]
993
+ setattr(instance, "examination_time", dt_time(int(h), int(m)))
994
+ elif isinstance(val, datetime):
995
+ setattr(instance, "examination_time", val.time())
996
+ except Exception as e:
997
+ logger.warning(f"Skipping invalid examination_time '{val}': {e}")
998
+
999
+ # Update other attributes from selected_data
1000
+ patient_name_changed = False
1001
+ for k, v in selected_data.items():
1002
+ # Skip None values to avoid overwriting existing data
1003
+ if v is None:
1004
+ logger.debug(f"Skipping field '{k}' during update because value is None")
1005
+ continue
1006
+
1007
+ # Avoid overwriting examiner names if they were just explicitly set
1008
+ if (
1009
+ k not in ["examiner_first_name", "examiner_last_name"]
1010
+ or (k == "examiner_first_name" and examiner_first_name is None)
1011
+ or (k == "examiner_last_name" and examiner_last_name is None)
1012
+ ):
1013
+ try:
1014
+ # --- Convert patient_dob if it's a date object ---
1015
+ value_to_set = v
1016
+ if k == "patient_dob":
1017
+ if isinstance(v, date) and not isinstance(v, datetime):
1018
+ aware_dob = timezone.make_aware(
1019
+ datetime.combine(v, datetime.min.time())
1020
+ )
1021
+ value_to_set = aware_dob
1022
+ logger.debug(
1023
+ "Converted patient_dob from date to aware datetime during update: %s",
1024
+ aware_dob,
1025
+ )
1026
+ elif isinstance(v, str):
1027
+ parsed = parse_any_date(v)
1028
+ if parsed:
1029
+ aware_dob = timezone.make_aware(
1030
+ datetime.combine(parsed, datetime.min.time())
1031
+ )
1032
+ value_to_set = aware_dob
1033
+ logger.debug(
1034
+ "Parsed string patient_dob '%s' during update to aware datetime: %s",
1035
+ v,
1036
+ aware_dob,
1037
+ )
1038
+ else:
1039
+ logger.warning(
1040
+ "Could not parse patient_dob string '%s' during update, skipping",
1041
+ v,
1042
+ )
1043
+ continue
1044
+ elif k == "examination_date":
1045
+ if isinstance(v, str):
1046
+ parsed = parse_any_date(v)
1047
+ if parsed:
1048
+ value_to_set = (
1049
+ parsed # field is DateField, so keep it as date
1050
+ )
1051
+ logger.debug(
1052
+ "Parsed string examination_date '%s' during update to date: %s",
1053
+ v,
1054
+ value_to_set,
1055
+ )
1056
+ else:
1057
+ logger.warning(
1058
+ "Could not parse examination_date string '%s' during update, skipping",
1059
+ v,
1060
+ )
1061
+ continue
1062
+ elif isinstance(v, date):
1063
+ value_to_set = v
1064
+
1065
+ # --- End Conversion ---
1066
+
1067
+ # Check if patient name is changing
1068
+ if (
1069
+ k in ["patient_first_name", "patient_last_name"]
1070
+ and getattr(instance, k) != value_to_set
1071
+ ):
1072
+ patient_name_changed = True
1073
+
1074
+ setattr(instance, k, value_to_set) # Use value_to_set
1075
+
1076
+ except Exception as e:
1077
+ logger.error(
1078
+ f"Error setting attribute '{k}' to '{v}': {e}. Skipping this field."
1079
+ )
1080
+ continue
1081
+
1082
+ # Update name DB if patient names changed
1083
+ if patient_name_changed:
1084
+ try:
1085
+ update_name_db(instance.patient_first_name, instance.patient_last_name)
1086
+ except Exception as e:
1087
+ logger.warning(f"Error updating name database: {e}")
1088
+
1089
+ # Call save - this will trigger the full save logic including hash recalculation etc.
1090
+ try:
1091
+ instance.save()
1092
+ except Exception as e:
1093
+ logger.error(f"Error saving SensitiveMeta instance: {e}")
1094
+ raise
1095
+
1096
+ return instance
1097
+
1098
+
1099
+ def update_or_create_sensitive_meta_from_dict(
1100
+ cls: Type["SensitiveMeta"],
1101
+ data: Dict[str, Any],
1102
+ instance: Optional["SensitiveMeta"] = None,
1103
+ ):
1104
+ """Logic to update or create a SensitiveMeta instance from a dictionary."""
1105
+ # Check if the instance already exists based on unique fields
1106
+ sensitive_meta: "SensitiveMeta"
1107
+ _created: bool
1108
+ if instance:
1109
+ # Update the existing instance
1110
+ sensitive_meta = update_sensitive_meta_from_dict(instance, data)
1111
+ _created = False
1112
+
1113
+ else:
1114
+ # Create a new instance
1115
+ sensitive_meta = create_sensitive_meta_from_dict(cls, data)
1116
+ _created = True
1117
+ return sensitive_meta, _created
1118
+
1119
+
1120
+ def _map_gender_string_to_standard(gender_str: str) -> Optional[str]:
1121
+ """Maps various gender string inputs to standard gender names used in the DB."""
1122
+ mapping = {
1123
+ "male": ["male", "m", "männlich", "man"],
1124
+ "female": ["female", "f", "weiblich", "woman"],
1125
+ "unknown": ["unknown", "unbekannt", "other", "diverse", ""],
1126
+ }
1127
+ gender_lower = gender_str.strip().lower()
1128
+ for standard, variants in mapping.items():
1129
+ if gender_lower in variants:
1130
+ return standard
1131
+ return None
1132
+
1133
+
1134
+ def _create_anonymized_record(
1135
+ instance: "SensitiveMeta",
1136
+ DEFAULT_ANONYMIZED=None,
1137
+ DEFAULT_ANONYMIZED_DATE=timezone.make_aware(datetime(1900, 1, 1)),
1138
+ ) -> None:
1139
+ """
1140
+ Create a SensitiveMeta instance with all sensitive fields set to anonymized defaults.
1141
+ This is only called after anonymization and will delete all data that can identify a patient from the database.
1142
+ What is left will only be the patient hash.
1143
+
1144
+ Args:
1145
+ instance: The existing SensitiveMeta instance to anonymize
1146
+ DEFAULT_ANONYMIZED: Usually None, The default string to use for anonymized fields (e.g., "anonymized,")
1147
+ """
1148
+
1149
+ instance.refresh_from_db()
1150
+ instance.get_patient_hash()
1151
+ instance.get_patient_examination_hash()
1152
+
1153
+ anonymized_data = {
1154
+ "patient_first_name": DEFAULT_ANONYMIZED,
1155
+ "patient_last_name": DEFAULT_ANONYMIZED,
1156
+ "patient_dob": DEFAULT_ANONYMIZED_DATE,
1157
+ "examination_date": DEFAULT_ANONYMIZED_DATE,
1158
+ }
1159
+ sensitive_meta = update_sensitive_meta_from_dict(instance, anonymized_data)
1160
+
1161
+ sensitive_meta.save()