[med-svn] [Git][med-team/gnumed-server][upstream] New upstream version 22.30

Emmanuel Arias (@eamanu) gitlab at salsa.debian.org
Tue Apr 1 21:27:26 BST 2025



Emmanuel Arias pushed to branch upstream at Debian Med / gnumed-server


Commits:
2a7ad897 by Emmanuel Arias at 2025-04-01T17:16:28-03:00
New upstream version 22.30
- - - - -


15 changed files:

- server/bootstrap/fixup_db-v22.conf
- server/bootstrap/update_db-v21_v22.conf
- server/doc/schema/gnumed-entire_schema.html
- server/pycommon/gmCfg2.py
- server/pycommon/gmConnectionPool.py
- server/pycommon/gmCrypto.py
- server/pycommon/gmDateTime.py
- server/pycommon/gmI18N.py
- server/pycommon/gmPG2.py
- server/pycommon/gmTools.py
- + server/sql/v21-v22/fixups/v22-dem-org-permission-fixups.sql
- + server/sql/v21-v22/fixups/v22-dem-unique_named_identity-fixup.sql
- server/sql/v21-v22/fixups/v22-dem-v_message_inbox-fixup.sql
- + server/sql/v21-v22/fixups/v22-dem-v_pat_addresses-fixups.sql
- server/sql/v21-v22/fixups/v22-release_notes-fixup.sql


Changes:

=====================================
server/bootstrap/fixup_db-v22.conf
=====================================
@@ -42,6 +42,9 @@ v22-blobs-reviewed_doc_objs-idx.sql
 v22-blobs-v_reviewed_doc_objects-fixup.sql
 v22-dem-identity-idx.sql
 v22-dem-names-idx.sql
+v22-dem-unique_named_identity-fixup.sql
+v22-dem-v_pat_addresses-fixups.sql
+v22-dem-org-permission-fixups.sql
 v22-release_notes-fixup.sql
 $schema$
 


=====================================
server/bootstrap/update_db-v21_v22.conf
=====================================
@@ -174,6 +174,9 @@ v22-blobs-reviewed_doc_objs-idx.sql
 v22-blobs-v_reviewed_doc_objects-fixup.sql
 v22-dem-identity-idx.sql
 v22-dem-names-idx.sql
+v22-dem-unique_named_identity-fixup.sql
+v22-dem-v_pat_addresses-fixups.sql
+v22-dem-org-permission-fixups.sql
 v22-release_notes-fixup.sql
 $schema$
 


=====================================
server/doc/schema/gnumed-entire_schema.html
=====================================
@@ -112,7 +112,7 @@
 <body>
 
 <!-- Primary Index -->
-<p><br><br>Dumped on 2024-01-28</p>
+<p><br><br>Dumped on 2025-03-30</p>
 <h1><a name="index">Index of database - gnumed_v22</a></h1>
 <ul>
   
@@ -143,7 +143,7 @@
   
   <li><a name="clin.schema">clin</a></li><ul>
     <li><a href="gnumed-entire_schema.html#clin.table.-enum-allergy-type">_enum_allergy_type</a></li><li><a href="gnumed-entire_schema.html#clin.view.-view-emr-journal-without-suppressed-hints">_view_emr_journal_without_suppressed_hints</a></li><li><a href="gnumed-entire_schema.html#clin.table.allergy">allergy</a></li><li><a href="gnumed-entire_schema.html#clin.table.allergy-state">allergy_state</a></li><li><a href="gnumed-entire_schema.html#clin.table.clin-aux-note">clin_aux_note</a></li><li><a href="gnumed-entire_schema.html#clin.table.clin-diag">clin_diag</a></li><li><a href="gnumed-entire_schema.html#clin.table.clin-item-type">clin_item_type</a></li><li><a href="gnumed-entire_schema.html#clin.table.clin-narrative">clin_narrative</a></li><li><a href="gnumed-entire_schema.html#clin.table.clin-root-item">clin_root_item</a></li><li><a href="gnumed-entire_schema.html#clin.table.encounter">encounter</a></li><li><a href="gnumed-entire_schema.html#clin.table.encounter-type">encounter_type</a></li><li><a href="gnumed-entire_schema.html#clin.table.episode">episode</a></li><li><a href="gnumed-entire_schema.html#clin.table.export-item">export_item</a></li><li><a href="gnumed-entire_schema.html#clin.table.external-care">external_care</a></li><li><a href="gnumed-entire_schema.html#clin.table.family-history">family_history</a></li><li><a href="gnumed-entire_schema.html#clin.table.fhx-relation-type">fhx_relation_type</a></li><li><a href="gnumed-entire_schema.html#clin.table.form-data">form_data</a></li><li><a href="gnumed-entire_schema.html#clin.table.form-instances">form_instances</a></li><li><a href="gnumed-entire_schema.html#clin.table.health-issue">health_issue</a></li><li><a href="gnumed-entire_schema.html#clin.table.hospital-stay">hospital_stay</a></li><li><a href="gnumed-entire_schema.html#clin.table.incoming-data-unmatchable">incoming_data_unmatchable</a></li><li><a href="gnumed-entire_schema.html#clin.table.incoming-data-unmatched">incoming_data_unmatched</a></li><li><a href="gnumed-entire_schema.html#clin.table.lab-request">lab_request</a></li><li><a href="gnumed-entire_schema.html#clin.table.lnk-code2aoe">lnk_code2aoe</a></li><li><a href="gnumed-entire_schema.html#clin.table.lnk-code2episode">lnk_code2episode</a></li><li><a href="gnumed-entire_schema.html#clin.table.lnk-code2fhx">lnk_code2fhx</a></li><li><a href="gnumed-entire_schema.html#clin.table.lnk-code2h-issue">lnk_code2h_issue</a></li><li><a href="gnumed-entire_schema.html#clin.table.lnk-code2item-root">lnk_code2item_root</a></li><li><a href="gnumed-entire_schema.html#clin.table.lnk-code2narrative">lnk_code2narrative</a></li><li><a href="gnumed-entire_schema.html#clin.table.lnk-code2procedure">lnk_code2procedure</a></li><li><a href="gnumed-entire_schema.html#clin.table.lnk-code2rfe">lnk_code2rfe</a></li><li><a href="gnumed-entire_schema.html#clin.table.lnk-code2tst-pnl">lnk_code2tst_pnl</a></li><li><a href="gnumed-entire_schema.html#clin.table.lnk-constraint2vacc-course">lnk_constraint2vacc_course</a></li><li><a href="gnumed-entire_schema.html#clin.table.lnk-loinc2test-panel">lnk_loinc2test_panel</a></li><li><a href="gnumed-entire_schema.html#clin.table.lnk-pat2vaccination-course">lnk_pat2vaccination_course</a></li><li><a href="gnumed-entire_schema.html#clin.table.lnk-substance2episode">lnk_substance2episode</a></li><li><a href="gnumed-entire_schema.html#clin.table.lnk-tst2norm">lnk_tst2norm</a></li><li><a href="gnumed-entire_schema.html#clin.table.lnk-type2item">lnk_type2item</a></li><li><a href="gnumed-entire_schema.html#clin.table.lnk-vaccination-course2schedule">lnk_vaccination_course2schedule</a></li><li><a href="gnumed-entire_schema.html#clin.table.meta-test-type">meta_test_type</a></li><li><a href="gnumed-entire_schema.html#clin.table.patient">patient</a></li><li><a href="gnumed-entire_schema.html#clin.table.procedure">procedure</a></li><li><a href="gnumed-entire_schema.html#clin.table.review-root">review_root</a></li><li><a href="gnumed-entire_schema.html#clin.table.reviewed-test-results">reviewed_test_results</a></li><li><a href="gnumed-entire_schema.html#clin.table.soap-cat-ranks">soap_cat_ranks</a></li><li><a href="gnumed-entire_schema.html#clin.table.substance-intake">substance_intake</a></li><li><a href="gnumed-entire_schema.html#clin.table.suppressed-hint">suppressed_hint</a></li><li><a href="gnumed-entire_schema.html#clin.table.test-org">test_org</a></li><li><a href="gnumed-entire_schema.html#clin.table.test-panel">test_panel</a></li><li><a href="gnumed-entire_schema.html#clin.table.test-result">test_result</a></li><li><a href="gnumed-entire_schema.html#clin.table.test-type">test_type</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-candidate-diagnoses">v_candidate_diagnoses</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-edc-journal">v_edc_journal</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-emr-journal">v_emr_journal</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-export-items">v_export_items</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-external-care">v_external_care</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-external-care-journal">v_external_care_journal</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-family-history">v_family_history</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-family-history-journal">v_family_history_journal</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-health-issues">v_health_issues</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-health-issues-journal">v_health_issues_journal</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-hospital-stays">v_hospital_stays</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-hospital-stays-journal">v_hospital_stays_journal</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-hospital-stays-journal-multi-day-adm">v_hospital_stays_journal_multi_day_adm</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-hospital-stays-journal-multi-day-dis">v_hospital_stays_journal_multi_day_dis</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-hospital-stays-journal-no-discharge">v_hospital_stays_journal_no_discharge</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-hospital-stays-journal-one-day">v_hospital_stays_journal_one_day</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-incoming-data-unmatched">v_incoming_data_unmatched</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-linked-codes">v_linked_codes</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-most-recent-encounters">v_most_recent_encounters</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-narrative">v_narrative</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-pat-allergies">v_pat_allergies</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-pat-allergies-journal">v_pat_allergies_journal</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-pat-allergy-state">v_pat_allergy_state</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-pat-allergy-state-journal">v_pat_allergy_state_journal</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-pat-encounters">v_pat_encounters</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-pat-encounters-journal">v_pat_encounters_journal</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-pat-episodes">v_pat_episodes</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-pat-episodes-journal">v_pat_episodes_journal</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-pat-items">v_pat_items</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-pat-last-vacc4indication">v_pat_last_vacc4indication</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-pat-narrative-journal">v_pat_narrative_journal</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-pat-vaccs4indication">v_pat_vaccs4indication</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-potential-problem-list">v_potential_problem_list</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-problem-list">v_problem_list</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-procedures">v_procedures</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-procedures-at-hospital">v_procedures_at_hospital</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-procedures-at-hospital-journal">v_procedures_at_hospital_journal</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-procedures-journal">v_procedures_journal</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-procedures-not-at-hospital">v_procedures_not_at_hospital</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-procedures-not-at-hospital-journal">v_procedures_not_at_hospital_journal</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-reminders-journal">v_reminders_journal</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-reviewed-items">v_reviewed_items</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-subst-intake4narr-search">v_subst_intake4narr_search</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-substance-intake-journal">v_substance_intake_journal</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-substance-intakes">v_substance_intakes</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-suppressed-hints">v_suppressed_hints</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-suppressed-hints-journal">v_suppressed_hints_journal</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-test-orgs">v_test_orgs</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-test-panels">v_test_panels</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-test-results">v_test_results</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-test-results-journal">v_test_results_journal</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-test-types">v_test_types</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-test-types4test-panel">v_test_types4test_panel</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-vaccination-courses-in-schedule">v_vaccination_courses_in_schedule</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-vaccinations">v_vaccinations</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-vaccinations-journal">v_vaccinations_journal</a></li><li><a href="gnumed-entire_schema.html#clin.view.v-waiting-list">v_waiting_list</a></li><li><a href="gnumed-entire_schema.html#clin.table.vaccination">vaccination</a></li><li><a href="gnumed-entire_schema.html#clin.table.vaccination-course">vaccination_course</a></li><li><a href="gnumed-entire_schema.html#clin.table.vaccination-course-constraint">vaccination_course_constraint</a></li><li><a href="gnumed-entire_schema.html#clin.table.vaccination-definition">vaccination_definition</a></li><li><a href="gnumed-entire_schema.html#clin.table.vaccination-schedule">vaccination_schedule</a></li><li><a href="gnumed-entire_schema.html#clin.table.waiting-list">waiting_list</a></li>
-    <li><a href="gnumed-entire_schema.html#clin.function.-get-recommendation-for-patient-hint-text-integer">_get_recommendation_for_patient_hint(text, integer)</a></li><li><a href="gnumed-entire_schema.html#clin.function.add-coded-phrase-text-text-text">add_coded_phrase(text, text, text)</a></li><li><a href="gnumed-entire_schema.html#clin.function.f-del-booster-must-have-base-immunity">f_del_booster_must_have_base_immunity()</a></li><li><a href="gnumed-entire_schema.html#clin.function.f-fk-reviewer-default">f_fk_reviewer_default()</a></li><li><a href="gnumed-entire_schema.html#clin.function.f-ins-booster-must-have-base-immunity">f_ins_booster_must_have_base_immunity()</a></li><li><a href="gnumed-entire_schema.html#clin.function.f-protect-clin-root-item">f_protect_clin_root_item()</a></li><li><a href="gnumed-entire_schema.html#clin.function.f-upd-booster-must-have-base-immunity">f_upd_booster_must_have_base_immunity()</a></li><li><a href="gnumed-entire_schema.html#clin.function.get-dob-integer">get_dob(integer)</a></li><li><a href="gnumed-entire_schema.html#clin.function.get-dod-integer">get_dod(integer)</a></li><li><a href="gnumed-entire_schema.html#clin.function.get-hints-for-patient-pk-identity-integer">get_hints_for_patient(_pk_identity integer)</a></li><li><a href="gnumed-entire_schema.html#clin.function.hint-suppression-exists-pk-identity-integer-pk-hint-integer">hint_suppression_exists(_pk_identity integer, _pk_hint integer)</a></li><li><a href="gnumed-entire_schema.html#clin.function.move-waiting-list-entry-integer-integer">move_waiting_list_entry(integer, integer)</a></li><li><a href="gnumed-entire_schema.html#clin.function.remove-old-empty-encounters-integer">remove_old_empty_encounters(integer)</a></li><li><a href="gnumed-entire_schema.html#clin.function.remove-old-empty-encounters-integer-interval">remove_old_empty_encounters(integer, interval)</a></li><li><a href="gnumed-entire_schema.html#clin.function.run-hint-query-title-text-query-text">run_hint_query(_title text, _query text)</a></li><li><a href="gnumed-entire_schema.html#clin.function.transfer-all-encounter-data-pk-source-encounter-integer-pk-target-encounter-integer">transfer_all_encounter_data(_pk_source_encounter integer, _pk_target_encounter integer)</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-activate-issue-on-opening-episode">trf_activate_issue_on_opening_episode()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-announce-active-substance-mod-no-pk">trf_announce_active_substance_mod_no_pk()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-announce-consumed-substance-mod-no-pk">trf_announce_consumed_substance_mod_no_pk()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-check-ext-care-uniq-issue-per-enc-and-unit-ins-upd">trf_check_ext_care_uniq_issue_per_enc_and_unit_ins_upd()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-del-intake-document-deleted">trf_del_intake_document_deleted()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-del-intake-must-unlink-all-drug-components">trf_del_intake_must_unlink_all_drug_components()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-ensure-one-allergy-state-per-patient">trf_ensure_one_allergy_state_per_patient()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-ins-intake-prevent-duplicate-component-links">trf_ins_intake_prevent_duplicate_component_links()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-ins-lc2sth-fk-generic-code">trf_ins_lc2sth_fk_generic_code()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-ins-upd-export-item-normalize-fk-identity">trf_ins_upd_export_item_normalize_fk_identity()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-insert-intake-links-all-drug-components">trf_insert_intake_links_all_drug_components()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-invalidate-review-on-result-change">trf_invalidate_review_on_result_change()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-normalize-proc-is-ongoing">trf_normalize_proc_is_ongoing()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-notify-reviewer-of-review-change">trf_notify_reviewer_of_review_change()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-sanity-check-enc-epi-ins-upd">trf_sanity_check_enc_epi_ins_upd()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-sanity-check-enc-issue-ins-upd">trf_sanity_check_enc_issue_ins_upd()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-sanity-check-enc-vs-issue-on-epi">trf_sanity_check_enc_vs_issue_on_epi()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-sanity-check-procedure-episode">trf_sanity_check_procedure_episode()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-sanity-check-uniq-hint-per-pat-ins-upd">trf_sanity_check_uniq_hint_per_pat_ins_upd()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-sync-allergic-state-on-allergies-modified">trf_sync_allergic_state_on_allergies_modified()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-undiscontinue-unsets-reason">trf_undiscontinue_unsets_reason()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-unique-indication-in-schedule">trf_unique_indication_in_schedule()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-upd-intake-must-link-all-drug-components">trf_upd_intake_must_link_all_drug_components()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-upd-intake-prevent-duplicate-component-links">trf_upd_intake_prevent_duplicate_component_links()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-upd-intake-updates-all-drug-components">trf_upd_intake_updates_all_drug_components()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-upd-lc2sth-fk-generic-code">trf_upd_lc2sth_fk_generic_code()</a></li>
+    <li><a href="gnumed-entire_schema.html#clin.function.-get-recommendation-for-patient-hint-text-integer">_get_recommendation_for_patient_hint(text, integer)</a></li><li><a href="gnumed-entire_schema.html#clin.function.add-coded-phrase-text-text-text">add_coded_phrase(text, text, text)</a></li><li><a href="gnumed-entire_schema.html#clin.function.f-del-booster-must-have-base-immunity">f_del_booster_must_have_base_immunity()</a></li><li><a href="gnumed-entire_schema.html#clin.function.f-fk-reviewer-default">f_fk_reviewer_default()</a></li><li><a href="gnumed-entire_schema.html#clin.function.f-ins-booster-must-have-base-immunity">f_ins_booster_must_have_base_immunity()</a></li><li><a href="gnumed-entire_schema.html#clin.function.f-protect-clin-root-item">f_protect_clin_root_item()</a></li><li><a href="gnumed-entire_schema.html#clin.function.f-upd-booster-must-have-base-immunity">f_upd_booster_must_have_base_immunity()</a></li><li><a href="gnumed-entire_schema.html#clin.function.get-dob-integer">get_dob(integer)</a></li><li><a href="gnumed-entire_schema.html#clin.function.get-dod-integer">get_dod(integer)</a></li><li><a href="gnumed-entire_schema.html#clin.function.get-hints-for-patient-pk-identity-integer">get_hints_for_patient(_pk_identity integer)</a></li><li><a href="gnumed-entire_schema.html#clin.function.hint-suppression-exists-pk-identity-integer-pk-hint-integer">hint_suppression_exists(_pk_identity integer, _pk_hint integer)</a></li><li><a href="gnumed-entire_schema.html#clin.function.move-waiting-list-entry-wl-pos-src-integer-wl-pos-dest-integer">move_waiting_list_entry(_wl_pos_src integer, _wl_pos_dest integer)</a></li><li><a href="gnumed-entire_schema.html#clin.function.remove-old-empty-encounters-integer">remove_old_empty_encounters(integer)</a></li><li><a href="gnumed-entire_schema.html#clin.function.remove-old-empty-encounters-integer-interval">remove_old_empty_encounters(integer, interval)</a></li><li><a href="gnumed-entire_schema.html#clin.function.run-hint-query-title-text-query-text">run_hint_query(_title text, _query text)</a></li><li><a href="gnumed-entire_schema.html#clin.function.transfer-all-encounter-data-pk-source-encounter-integer-pk-target-encounter-integer">transfer_all_encounter_data(_pk_source_encounter integer, _pk_target_encounter integer)</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-activate-issue-on-opening-episode">trf_activate_issue_on_opening_episode()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-announce-active-substance-mod-no-pk">trf_announce_active_substance_mod_no_pk()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-announce-consumed-substance-mod-no-pk">trf_announce_consumed_substance_mod_no_pk()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-check-ext-care-uniq-issue-per-enc-and-unit-ins-upd">trf_check_ext_care_uniq_issue_per_enc_and_unit_ins_upd()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-del-intake-document-deleted">trf_del_intake_document_deleted()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-del-intake-must-unlink-all-drug-components">trf_del_intake_must_unlink_all_drug_components()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-ensure-one-allergy-state-per-patient">trf_ensure_one_allergy_state_per_patient()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-ins-intake-prevent-duplicate-component-links">trf_ins_intake_prevent_duplicate_component_links()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-ins-lc2sth-fk-generic-code">trf_ins_lc2sth_fk_generic_code()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-ins-upd-export-item-normalize-fk-identity">trf_ins_upd_export_item_normalize_fk_identity()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-insert-intake-links-all-drug-components">trf_insert_intake_links_all_drug_components()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-invalidate-review-on-result-change">trf_invalidate_review_on_result_change()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-normalize-proc-is-ongoing">trf_normalize_proc_is_ongoing()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-notify-reviewer-of-review-change">trf_notify_reviewer_of_review_change()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-sanity-check-enc-epi-ins-upd">trf_sanity_check_enc_epi_ins_upd()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-sanity-check-enc-issue-ins-upd">trf_sanity_check_enc_issue_ins_upd()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-sanity-check-enc-vs-issue-on-epi">trf_sanity_check_enc_vs_issue_on_epi()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-sanity-check-procedure-episode">trf_sanity_check_procedure_episode()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-sanity-check-uniq-hint-per-pat-ins-upd">trf_sanity_check_uniq_hint_per_pat_ins_upd()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-sync-allergic-state-on-allergies-modified">trf_sync_allergic_state_on_allergies_modified()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-undiscontinue-unsets-reason">trf_undiscontinue_unsets_reason()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-unique-indication-in-schedule">trf_unique_indication_in_schedule()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-upd-intake-must-link-all-drug-components">trf_upd_intake_must_link_all_drug_components()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-upd-intake-prevent-duplicate-component-links">trf_upd_intake_prevent_duplicate_component_links()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-upd-intake-updates-all-drug-components">trf_upd_intake_updates_all_drug_components()</a></li><li><a href="gnumed-entire_schema.html#clin.function.trf-upd-lc2sth-fk-generic-code">trf_upd_lc2sth_fk_generic_code()</a></li>
   </ul>
   
   <li><a name="de-de.schema">de_de</a></li><ul>
@@ -50900,6 +50900,16 @@ there will always be one "sender".
 <ul class="indexes">
 <!-- Indexes -->
 
+  <li><b>idx_doc_med_fk_encounter</b> fk_encounter</li>
+
+  <li><b>idx_doc_med_fk_episode</b> fk_episode</li>
+
+  <li><b>idx_doc_med_fk_hospital_stay</b> fk_hospital_stay</li>
+
+  <li><b>idx_doc_med_fk_org_unit</b> fk_org_unit</li>
+
+  <li><b>idx_doc_med_fk_type</b> fk_type</li>
+
 </ul>
 <!-- View Definition -->
 
@@ -51175,6 +51185,10 @@ impossible to secure, etc.
 <ul class="indexes">
 <!-- Indexes -->
 
+  <li><b>idx_doc_obj_fk_doc</b> fk_doc</li>
+
+  <li><b>idx_doc_obj_fk_intended_reviewer</b> fk_intended_reviewer</li>
+
 </ul>
 <!-- View Definition -->
 
@@ -51782,6 +51796,10 @@ impossible to secure, etc.
 <ul class="indexes">
 <!-- Indexes -->
 
+  <li><b>idx_rev_doc_objs_fk_reviewed_row</b> fk_reviewed_row</li>
+
+  <li><b>idx_rev_doc_objs_fk_reviewer</b> fk_reviewer</li>
+
 </ul>
 <!-- View Definition -->
 
@@ -54476,103 +54494,86 @@ WHERE (b_vdm.pk_doc = b_do.fk_doc);</pre>
 <!-- View Definition -->
 
 <pre>
-SELECT rdo.fk_reviewed_row AS pk_doc_obj
+SELECT b_rdo.fk_reviewed_row AS pk_doc_obj
 ,
     COALESCE
-(
-     (
-      SELECT staff.short_alias
-           
-        FROM dem.staff
-          
-       WHERE (staff.pk = rdo.fk_reviewer)
-     )
+(d_s.short_alias
      , (
-           ('<#'::text || rdo.fk_reviewer) || '>'::text
+           ('<#'::text || b_rdo.fk_reviewer) || '>'::text
      )
 ) AS reviewer
 ,
-    rdo.is_technically_abnormal
+    b_rdo.is_technically_abnormal
 ,
-    rdo.clinically_relevant
+    b_rdo.clinically_relevant
 ,
     
-(EXISTS 
-     (
-      SELECT 1
-           
-        FROM blobs.doc_obj
-          
-       WHERE (
-                 (doc_obj.pk = rdo.fk_reviewed_row)
-               AND (doc_obj.fk_intended_reviewer = rdo.fk_reviewer)
-           )
-     )
-) AS is_review_by_responsible_reviewer
+(b_rdo.fk_reviewer = b_do.fk_intended_reviewer) AS is_review_by_responsible_reviewer
 ,
     
-(EXISTS 
+(b_rdo.fk_reviewer = 
      (
-      SELECT 1
+      SELECT staff.pk
            
         FROM dem.staff
           
-       WHERE (
-                 (staff.pk = rdo.fk_reviewer)
-               AND (staff.db_user = "current_user"
-                       ()
-                 )
-           )
+       WHERE (staff.db_user = CURRENT_USER)
      )
 ) AS is_your_review
 ,
-    rdo.comment
+    b_rdo.comment
 ,
-    rdo.modified_when AS reviewed_when
+    b_rdo.modified_when AS reviewed_when
 ,
-    rdo.modified_by
+    b_rdo.modified_by
 ,
-    rdo.pk AS pk_review_root
+    b_rdo.pk AS pk_review_root
 ,
-    rdo.fk_reviewer AS pk_reviewer
+    b_rdo.fk_reviewer AS pk_reviewer
 ,
-    
-(
-SELECT v_obj4doc_no_data.pk_patient
-           
-  FROM blobs.v_obj4doc_no_data
-          
- WHERE (v_obj4doc_no_data.pk_obj = rdo.fk_reviewed_row)
-) AS pk_patient
+    c_enc.fk_patient AS pk_patient
 ,
-    
-(
-SELECT v_obj4doc_no_data.pk_encounter
-           
-  FROM blobs.v_obj4doc_no_data
-          
- WHERE (v_obj4doc_no_data.pk_obj = rdo.fk_reviewed_row)
-) AS pk_encounter
+    b_dm.fk_encounter AS pk_encounter
 ,
-    
-(
-SELECT v_obj4doc_no_data.pk_episode
-           
-  FROM blobs.v_obj4doc_no_data
-          
- WHERE (v_obj4doc_no_data.pk_obj = rdo.fk_reviewed_row)
-) AS pk_episode
+    b_dm.fk_episode AS pk_episode
 ,
-    
-(
-SELECT v_obj4doc_no_data.pk_health_issue
-           
-  FROM blobs.v_obj4doc_no_data
-          
- WHERE (v_obj4doc_no_data.pk_obj = rdo.fk_reviewed_row)
-) AS pk_health_issue
+    c_epi.fk_health_issue AS pk_health_issue
    
-FROM blobs.reviewed_doc_objs rdo;</pre>
+FROM (
+     (
+           (
+                 (
+                       (blobs.reviewed_doc_objs b_rdo
+     
+                     LEFT JOIN dem.staff d_s 
+                            ON (
+                                   (d_s.pk = b_rdo.fk_reviewer)
+                             )
+                       )
+     
+               LEFT JOIN blobs.doc_obj b_do 
+                      ON (
+                             (b_do.pk = b_rdo.fk_reviewed_row)
+                       )
+                 )
+     
+         LEFT JOIN blobs.doc_med b_dm 
+                ON (
+                       (b_do.fk_doc = b_dm.pk)
+                 )
+           )
+     
+   LEFT JOIN clin.episode c_epi 
+          ON (
+                 (b_dm.fk_episode = c_epi.pk)
+           )
+     )
+     
+LEFT JOIN clin.encounter c_enc 
+    ON (
+           (c_epi.fk_encounter = c_enc.pk)
+     )
+);</pre>
 
 
 <!-- List off permissions -->
@@ -106865,7 +106866,7 @@ END;</pre>
 
 <hr>
 <h2>Function:
-  <a href="gnumed-entire_schema.html#clin.schema">clin</a>.<a name="clin.function.move-waiting-list-entry-integer-integer">move_waiting_list_entry(integer, integer)</a>
+  <a href="gnumed-entire_schema.html#clin.schema">clin</a>.<a name="clin.function.move-waiting-list-entry-wl-pos-src-integer-wl-pos-dest-integer">move_waiting_list_entry(_wl_pos_src integer, _wl_pos_dest integer)</a>
 </h2>
 <h3>Returns: boolean</h3>
 <h3>Language: PLPGSQL</h3>
@@ -106874,8 +106875,6 @@ END;</pre>
  Fails if there is no row with position $1.</p>
 <pre>
 DECLARE
-	_wl_pos_src alias for $1;
-	_wl_pos_dest alias for $2;
 	_tmp_pos integer;
 	_curr_max_pos integer;
 BEGIN
@@ -106897,7 +106896,7 @@ BEGIN
 	-- does the source row exist ?
 	perform 1 from clin.waiting_list where list_position = _wl_pos_src;
 	if not found then
-		raise notice 'clin.move_waiting_list_entry(): Cannot move entry [%] to [%]. Entry does not exist.', _wl_pos_src, wl_pos_dest ;
+		raise notice 'clin.move_waiting_list_entry(): Cannot move entry [%] to [%]. Entry does not exist.', _wl_pos_src, _wl_pos_dest ;
 		return false;
 	end if;
 	-- load destination row
@@ -111144,6 +111143,12 @@ not-quite-so-bad: occupation
 <ul class="indexes">
 <!-- Indexes -->
 
+  <li><b>idx_dem_identity_fk_emergency_contact</b> fk_emergency_contact</li>
+
+  <li><b>idx_dem_identity_fk_marital_status</b> fk_marital_status</li>
+
+  <li><b>idx_dem_identity_fk_primary_provider</b> fk_primary_provider</li>
+
   <li><b>idx_identity_dob_ymd</b> dem.date_trunc_utc('day'::text, dob)</li>
 
 </ul>
@@ -114636,7 +114641,21 @@ FROM dem.lnk_person_org_address;</pre>
 <ul class="indexes">
 <!-- Indexes -->
 
-  <li><b>idx_names_firstnames</b> firstnames</li>
+  <li><b>idx_dem_names_active_names</b> id_identity, active) WHERE (active IS TRUE</li>
+
+  <li><b>idx_dem_names_firstnames</b> firstnames</li>
+
+  <li><b>idx_dem_names_firstnames_lower</b> lower(firstnames)</li>
+
+  <li><b>idx_dem_names_firstnames_trgm</b> firstnames pgtrgm.gin_trgm_ops</li>
+
+  <li><b>idx_dem_names_id_identity</b> id_identity</li>
+
+  <li><b>idx_dem_names_lastnames</b> lastnames</li>
+
+  <li><b>idx_dem_names_lastnames_lower</b> lower(lastnames)</li>
+
+  <li><b>idx_dem_names_lastnames_trgm</b> lastnames pgtrgm.gin_trgm_ops</li>
 
   <li><b>idx_names_last_first</b> lastnames, firstnames</li>
 
@@ -129076,10 +129095,10 @@ begin
 				(select
 					tbl.contype,
 					'CONSTRAINT:type='
-						|| tbl.contype || ':'
+						|| tbl.contype::TEXT || ':'
 						|| replace(pg_catalog.pg_get_constraintdef(tbl.oid, true), ' ', '_')
 						|| '::active='
-						|| tbl.convalidated
+						|| tbl.convalidated::TEXT
 					 as condef
 				from pg_catalog.pg_constraint tbl
 				where


=====================================
server/pycommon/gmCfg2.py
=====================================
@@ -37,13 +37,13 @@ def __set_opt_in_INI_file(src=None, sink=None, group=None, option=None, value=No
 			continue
 
 		# start of list ?
-		if regex.match('(?P<list_name>.+)(\s|\t)*=(\s|\t)*\$(?P=list_name)\$', line) is not None:
+		if regex.match(r'(?P<list_name>.+)(\s|\t)*=(\s|\t)*\$(?P=list_name)\$', line) is not None:
 			in_list = True
 			sink.write(line)
 			continue
 
 		# end of list ?
-		if regex.match('\$.+\$.*', line) is not None:
+		if regex.match(r'\$.+\$.*', line) is not None:
 			in_list = False
 			sink.write(line)
 			continue
@@ -55,7 +55,7 @@ def __set_opt_in_INI_file(src=None, sink=None, group=None, option=None, value=No
 			continue
 
 		# another group ?
-		if regex.match('\[.+\].*', line) is not None:
+		if regex.match(r'\[.+\].*', line) is not None:
 			# next group but option not seen yet ?
 			if group_seen and not option_seen:
 				sink.write('%s = %s\n\n\n' % (option, value))
@@ -64,7 +64,7 @@ def __set_opt_in_INI_file(src=None, sink=None, group=None, option=None, value=No
 			continue
 
 		# our option ?
-		if regex.match('%s(\s|\t)*=' % option, line) is not None:
+		if regex.match(r'%s(\s|\t)*=' % option, line) is not None:
 			if group_seen:
 				sink.write('%s = %s\n' % (option, value))
 				option_seen = True
@@ -103,7 +103,7 @@ def __set_list_in_INI_file(src=None, sink=None, group=None, option=None, value=N
 		if inside_our_list:			# can only be true if already inside our group
 			# new list has been written already
 			# so now at end of our (old) list ?
-			if regex.match('\$%s\$' % option, line.strip()) is not None:
+			if regex.match(r'\$%s\$' % option, line.strip()) is not None:
 				inside_our_list = False
 				continue
 			# skip old list entries
@@ -111,7 +111,7 @@ def __set_list_in_INI_file(src=None, sink=None, group=None, option=None, value=N
 
 		if inside_our_group:
 			# our option ?
-			if regex.match('%s(\s|\t)*=(\s|\t)*\$%s\$' % (option, option), line.strip()) is not None:
+			if regex.match(r'%s(\s|\t)*=(\s|\t)*\$%s\$' % (option, option), line.strip()) is not None:
 				sink.write(line)										# list header
 				sink.write('\n'.join(value))
 				sink.write('\n')
@@ -121,7 +121,7 @@ def __set_list_in_INI_file(src=None, sink=None, group=None, option=None, value=N
 				continue
 
 			# next group (= end of our group) ?
-			if regex.match('\[.+\]', line.strip()) is not None:
+			if regex.match(r'\[.+\]', line.strip()) is not None:
 				# our list already handled ?  (if so must already be finished)
 				if not our_list_seen:
 					# no, so need to add our list to the group before ...
@@ -175,7 +175,7 @@ def __set_list_in_INI_file_old(src=None, sink=None, group=None, option=None, val
 		# found option but still in (old) list ?
 		if option_seen and in_list:
 			# end of (old) list ?
-			if regex.match('\$.+\$.*', line) is not None:
+			if regex.match(r'\$.+\$.*', line) is not None:
 				in_list = False
 				sink.write(line)
 				continue
@@ -187,7 +187,7 @@ def __set_list_in_INI_file_old(src=None, sink=None, group=None, option=None, val
 			continue
 
 		# at start of a list ?
-		match = regex.match('(?P<list_name>.+)(\s|\t)*=(\s|\t)*\$(?P=list_name)\$', line)
+		match = regex.match(r'(?P<list_name>.+)(\s|\t)*=(\s|\t)*\$(?P=list_name)\$', line)
 		if match is not None:
 			in_list = True
 			# our list ?
@@ -201,7 +201,7 @@ def __set_list_in_INI_file_old(src=None, sink=None, group=None, option=None, val
 			continue
 
 		# at end of a list ?
-		if regex.match('\$.+\$.*', line) is not None:
+		if regex.match(r'\$.+\$.*', line) is not None:
 			in_list = False
 			sink.write(line)
 			continue
@@ -213,7 +213,7 @@ def __set_list_in_INI_file_old(src=None, sink=None, group=None, option=None, val
 			continue
 
 		# another group ?
-		if regex.match('\[%s\].*' % group, line) is not None:
+		if regex.match(r'\[%s\].*' % group, line) is not None:
 			# next group but option not seen yet ?
 			if our_group_seen and not option_seen:
 				option_seen = True
@@ -322,7 +322,7 @@ def parse_INI_stream(stream=None, encoding=None):
 			_log.error(line)
 			continue
 
-		name, remainder = regex.split('\s*[=:]\s*', line, maxsplit = 1)
+		name, remainder = regex.split(r'\s*[=:]\s*', line, maxsplit = 1)
 		if name == '':
 			_log.error('option name empty, aborting')
 			_log.error(line)


=====================================
server/pycommon/gmConnectionPool.py
=====================================
@@ -631,19 +631,19 @@ class gmConnectionPool(gmBorg.cBorg):
 		if 'fe_sendauth' in msg:
 			return True
 
-		if regex.search('user ".*" does not exist', msg) is not None:
+		if regex.search(r'user ".*" does not exist', msg) is not None:
 			return True
 
 		if 'uthenti' in msg:
 			return True
 
 		if ((
-				(regex.search('user ".*"', msg) is not None)
+				(regex.search(r'user ".*"', msg) is not None)
 					or
-				(regex.search('(R|r)ol{1,2}e', msg) is not None)
+				(regex.search(r'(R|r)ol{1,2}e', msg) is not None)
 			)
 			and ('exist' in msg)
-			and (regex.search('n(o|ich)t', msg) is not None)
+			and (regex.search(r'n(o|ich)t', msg) is not None)
 		):
 			return True
 


=====================================
server/pycommon/gmCrypto.py
=====================================
@@ -17,6 +17,7 @@ import sys
 import os
 import logging
 import tempfile
+import shutil
 
 
 # GNUmed libs
@@ -69,8 +70,8 @@ def create_encrypted_zip_archive_from_dir(source_dir, comment=None, overwrite=Tr
 	if len(passphrase) < 5:
 		_log.error('<passphrase> must be at least 5 characters/signs/digits')
 		return None
-	gmLog2.add_word2hide(passphrase)
 
+	gmLog2.add_word2hide(passphrase)
 	source_dir = os.path.abspath(source_dir)
 	if not os.path.isdir(source_dir):
 		_log.error('<source_dir> does not exist or is not a directory: %s', source_dir)
@@ -84,32 +85,42 @@ def create_encrypted_zip_archive_from_dir(source_dir, comment=None, overwrite=Tr
 		_log.warning('no 7z binary found')
 		return None
 
-	sandbox_dir = gmTools.mk_sandbox_dir()
-	archive_path_inner = os.path.join(sandbox_dir, 'data')
-	if not gmTools.mkdir(archive_path_inner):
-		_log.error('cannot create scratch space for inner achive: %s', archive_path_inner)
-	archive_fname_inner = 'data.zip'
-	archive_name_inner = os.path.join(archive_path_inner, archive_fname_inner)
-	archive_path_outer = gmTools.gmPaths().tmp_dir
-	archive_fname_outer = 'datawrapper.zip'
-	archive_name_outer = os.path.join(archive_path_outer, archive_fname_outer)
+	_log.debug('source directory: %s', source_dir)
+	archive_sandbox = gmTools.mk_sandbox_dir()
+	_log.debug('archive creation sandbox: %s', archive_sandbox)
+	inner_archive_dir = os.path.join(archive_sandbox, 'inner')
+	if not gmTools.mkdir(inner_archive_dir):
+		_log.error('cannot create scratch space for inner archive: %s', inner_archive_dir)
+		return None
+
+	outer_archive_dir = os.path.join(archive_sandbox, 'outer')
+	if not gmTools.mkdir(outer_archive_dir):
+		_log.error('cannot create scratch space for outer archive: %s', outer_archive_dir)
+		return None
+
+	inner_archive_name = os.path.join(inner_archive_dir, 'data.zip')
+	outer_archive_name = os.path.join(outer_archive_dir, 'datawrapper.zip')
 	# remove existing archives so they don't get *updated* rather than newly created
 	if overwrite:
-		if not gmTools.remove_file(archive_name_inner, force = True):
-			_log.error('cannot remove existing archive [%s]', archive_name_inner)
+		if not gmTools.remove_file(inner_archive_name, force = True):
+			_log.error('cannot remove existing archive [%s]', inner_archive_name)
 			return False
 
-		if not gmTools.remove_file(archive_name_outer, force = True):
-			_log.error('cannot remove existing archive [%s]', archive_name_outer)
+		if not gmTools.remove_file(outer_archive_name, force = True):
+			_log.error('cannot remove existing archive [%s]', outer_archive_name)
 			return False
 
+	tmp = gmTools.copy_tree_content(source_dir, inner_archive_dir)
+	if not tmp:
+		_log.error('cannot move source data to inner archive creation scratch dir')
+		return False
+
 	# 7z does not support ZIP comments so create a text file holding the comment
 	if comment is not None:
 		tmp, fname = os.path.split(source_dir.rstrip(os.sep))
-		comment_filename = os.path.join(sandbox_dir, '000-%s-comment.txt' % fname)
+		comment_filename = os.path.join(inner_archive_dir, '000-%s-comment.txt' % fname)
 		with open(comment_filename, mode = 'wt', encoding = 'utf8', errors = 'replace') as comment_file:
 			comment_file.write(comment)
-
 	# create inner (data) archive: uncompressed, unencrypted, similar to a tar archive
 	args = [
 		binary,
@@ -118,7 +129,8 @@ def create_encrypted_zip_archive_from_dir(source_dir, comment=None, overwrite=Tr
 		'-bd',				# no progress indicator
 		'-mx0',				# no compression (only store files)
 		'-mcu=on',			# UTF8 filenames
-		'-l',				# store content of links, not links
+		# now the default and switch not supported anymore
+		#'-l',				# store content of links, not links
 		'-scsUTF-8',		# console charset
 		'-tzip'				# force ZIP format
 	]
@@ -127,19 +139,41 @@ def create_encrypted_zip_archive_from_dir(source_dir, comment=None, overwrite=Tr
 		args.append('-bt')
 	else:
 		args.append('-bb1')
-	args.append(archive_name_inner)
-	args.append(source_dir)
-	if comment is not None:
-		args.append(comment_filename)
+	args.append(inner_archive_name)
+	args.append(os.path.join(inner_archive_dir, '.'))
+	_log.debug(args)
+	old_cwd = os.getcwd()
+	os.chdir(inner_archive_dir)
 	success, exit_code, stdout = gmShellAPI.run_process(cmd_line = args, encoding = 'utf8', verbose = verbose)
+	os.chdir(old_cwd)
 	if not success:
 		_log.error('cannot create inner archive')
 		return None
 
+	# test
+	args = [
+		binary,
+		't',				# test archive
+		'-bd',				# no progress indicator
+		'-scsUTF-8'			# console charset
+	]
+	if verbose:
+		args.append('-bb3')
+		args.append('-bt')
+	else:
+		args.append('-bb1')
+	args.append(inner_archive_name)
+	_log.debug(args)
+	success, exit_code, stdout = gmShellAPI.run_process(cmd_line = args, encoding = 'utf8', verbose = verbose)
+	if not success:
+		_log.error('cannot verify integrity of inner zip archive')
+		return None
+
+	# FIXME: needs error handling
+	shutil.move(inner_archive_name, outer_archive_dir)
 	# create "decompress instructions" file
-	instructions_filename = os.path.join(archive_path_inner, '000-on_Windows-open_with-WinZip_or_7z_tools')
+	instructions_filename = os.path.join(outer_archive_dir, '000-on_Windows-open_with-WinZip_or_7z_tools')
 	open(instructions_filename, mode = 'wt').close()
-
 	# create outer (wrapper) archive: compressed, encrypted
 	args = [
 		binary,
@@ -148,7 +182,8 @@ def create_encrypted_zip_archive_from_dir(source_dir, comment=None, overwrite=Tr
 		'-bd',					# no progress indicator
 		'-mx9',					# best available zip compression ratio
 		'-mcu=on',				# UTF8 filenames
-		'-l',					# store content of links, not links
+		# now the default and switch not supported anymore
+		#'-l',					# store content of links, not links
 		'-scsUTF-8',			# console charset
 		'-tzip',				# force ZIP format
 		'-mem=AES256',			# force useful encryption
@@ -159,13 +194,38 @@ def create_encrypted_zip_archive_from_dir(source_dir, comment=None, overwrite=Tr
 		args.append('-bt')
 	else:
 		args.append('-bb1')
-	args.append(archive_name_outer)
-	args.append(archive_path_inner)
+	args.append(outer_archive_name)
+	args.append(os.path.join(outer_archive_dir, '.'))
+	_log.debug(args)
+	old_cwd = os.getcwd()
+	os.chdir(outer_archive_dir)
 	success, exit_code, stdout = gmShellAPI.run_process(cmd_line = args, encoding = 'utf8', verbose = verbose)
-	if success:
-		return archive_name_outer
-	_log.error('cannot create outer archive')
-	return None
+	os.chdir(old_cwd)
+	if not success:
+		_log.error('cannot create wrapper archive')
+		return None
+
+	# test
+	args = [
+		binary,
+		't',					# test archive
+		'-bd',					# no progress indicator
+		'-scsUTF-8',			# console charset
+		'-p%s' % passphrase		# set passphrase
+	]
+	if verbose:
+		args.append('-bb3')
+		args.append('-bt')
+	else:
+		args.append('-bb1')
+	args.append(outer_archive_name)
+	_log.debug(args)
+	success, exit_code, stdout = gmShellAPI.run_process(cmd_line = args, encoding = 'utf8', verbose = verbose)
+	if not success:
+		_log.error('cannot verify integrity of outer zip archive')
+		return None
+
+	return outer_archive_name
 
 #---------------------------------------------------------------------------
 def create_zip_archive_from_dir(source_dir, archive_name=None, comment=None, overwrite=True, verbose=False):
@@ -205,7 +265,6 @@ def create_zip_archive_from_dir(source_dir, archive_name=None, comment=None, ove
 		else:
 			_log.error('cannot remove existing archive comment file [%s]', comment_filename)
 			comment = None
-
 	# compress
 	args = [
 		binary,
@@ -214,7 +273,8 @@ def create_zip_archive_from_dir(source_dir, archive_name=None, comment=None, ove
 		'-bd',				# no progress indicator
 		'-mx9',				# best available zip compression ratio
 		'-mcu=on',			# UTF8 filenames
-		'-l',				# store content of links, not links
+		# now the default and switch not supported anymore:
+		#'-l',				# store content of links, not links
 		'-scsUTF-8',		# console charset
 		'-tzip'				# force ZIP format
 	]
@@ -227,13 +287,34 @@ def create_zip_archive_from_dir(source_dir, archive_name=None, comment=None, ove
 	args.append(source_dir)
 	if comment is not None:
 		args.append(comment_filename)
+	_log.debug(args)
 	success, exit_code, stdout = gmShellAPI.run_process(cmd_line = args, encoding = 'utf8', verbose = verbose)
 	if comment is not None:
 		gmTools.remove_file(comment_filename)
-	if success:
-		return archive_name
+	if not success:
+		_log.error('cannot create zip archive')
+		return None
 
-	return None
+	# test
+	args = [
+		binary,
+		't',				# test archive
+		'-bd',				# no progress indicator
+		'-scsUTF-8'			# console charset
+	]
+	if verbose:
+		args.append('-bb3')
+		args.append('-bt')
+	else:
+		args.append('-bb1')
+	args.append(archive_name)
+	_log.debug(args)
+	success, exit_code, stdout = gmShellAPI.run_process(cmd_line = args, encoding = 'utf8', verbose = verbose)
+	if not success:
+		_log.error('cannot verify zip archive integrity')
+		return None
+
+	return archive_name
 
 #===========================================================================
 # file decryption methods
@@ -261,7 +342,6 @@ def gpg_decrypt_file(filename=None, passphrase=None, verbose=False, target_ext=N
 		'--enable-progress-filter',
 		'--decrypt',
 		'--output', filename_decrypted
-		##'--use-embedded-filename'				# not all encrypted files carry a filename
 	]
 	if verbose:
 		args.extend ([
@@ -419,6 +499,7 @@ def encrypt_pdf(filename=None, passphrase=None, verbose=False, remove_unencrypte
 		filename,
 		filename_encrypted
 	]
+	_log.debug(args)
 	success, exit_code, stdout = gmShellAPI.run_process(cmd_line = args, encoding = 'utf8', verbose = verbose)
 	if not success:
 		return None
@@ -564,6 +645,8 @@ if __name__ == '__main__':
 	gmI18N.activate_locale()
 	gmI18N.install_domain()
 
+	gmLog2.print_logfile_name()
+
 	#-----------------------------------------------------------------------
 	def test_gpg_decrypt():
 		print(gpg_decrypt_file(filename = sys.argv[2], verbose = True))
@@ -579,6 +662,7 @@ if __name__ == '__main__':
 	#-----------------------------------------------------------------------
 	def test_encrypt_pdf():
 		print(encrypt_pdf(filename = sys.argv[2], passphrase = sys.argv[3], verbose = True))
+		#print(encrypt_pdf(filename = sys.argv[2], passphrase = 'PW mit Leerzeichen', verbose = True))
 
 	#-----------------------------------------------------------------------
 	def test_encrypt_file():
@@ -601,6 +685,7 @@ if __name__ == '__main__':
 			comment = 'GNUmed test archive',
 			overwrite = True,
 			passphrase = sys.argv[3],
+#			passphrase = 'PW mit Leerzeichen',
 			verbose = True
 		))
 


=====================================
server/pycommon/gmDateTime.py
=====================================
@@ -977,62 +977,62 @@ def str2interval(str_interval=None):
 
 	# "(~)35(yY)"	- at age 35 years
 	keys = '|'.join(list(unit_keys['year'].replace('_keys_year', '')))
-	if regex.match('^~*(\s|\t)*\d+(%s)*$' % keys, str_interval, flags = regex.UNICODE):
-		return pyDT.timedelta(days = (int(regex.findall('\d+', str_interval, flags = regex.UNICODE)[0]) * avg_days_per_gregorian_year))
+	if regex.match(r'^~*(\s|\t)*\d+(%s)*$' % keys, str_interval, flags = regex.UNICODE):
+		return pyDT.timedelta(days = (int(regex.findall(r'\d+', str_interval, flags = regex.UNICODE)[0]) * avg_days_per_gregorian_year))
 
 	# "(~)12mM" - at age 12 months
 	keys = '|'.join(list(unit_keys['month'].replace('_keys_month', '')))
-	if regex.match('^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.UNICODE):
+	if regex.match(r'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.UNICODE):
 		years, months = divmod (
-			int(regex.findall('\d+', str_interval, flags = regex.UNICODE)[0]),
+			int(regex.findall(r'\d+', str_interval, flags = regex.UNICODE)[0]),
 			12
 		)
 		return pyDT.timedelta(days = ((years * avg_days_per_gregorian_year) + (months * avg_days_per_gregorian_month)))
 
 	# weeks
 	keys = '|'.join(list(unit_keys['week'].replace('_keys_week', '')))
-	if regex.match('^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.UNICODE):
-		return pyDT.timedelta(weeks = int(regex.findall('\d+', str_interval, flags = regex.UNICODE)[0]))
+	if regex.match(r'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.UNICODE):
+		return pyDT.timedelta(weeks = int(regex.findall(r'\d+', str_interval, flags = regex.UNICODE)[0]))
 
 	# days
 	keys = '|'.join(list(unit_keys['day'].replace('_keys_day', '')))
-	if regex.match('^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.UNICODE):
-		return pyDT.timedelta(days = int(regex.findall('\d+', str_interval, flags = regex.UNICODE)[0]))
+	if regex.match(r'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.UNICODE):
+		return pyDT.timedelta(days = int(regex.findall(r'\d+', str_interval, flags = regex.UNICODE)[0]))
 
 	# hours
 	keys = '|'.join(list(unit_keys['hour'].replace('_keys_hour', '')))
-	if regex.match('^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.UNICODE):
-		return pyDT.timedelta(hours = int(regex.findall('\d+', str_interval, flags = regex.UNICODE)[0]))
+	if regex.match(r'^~*(\s|\t)*\d+(\s|\t)*(%s)+$' % keys, str_interval, flags = regex.UNICODE):
+		return pyDT.timedelta(hours = int(regex.findall(r'\d+', str_interval, flags = regex.UNICODE)[0]))
 
 	# x/12 - months
-	if regex.match('^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*12$', str_interval, flags = regex.UNICODE):
+	if regex.match(r'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*12$', str_interval, flags = regex.UNICODE):
 		years, months = divmod (
-			int(regex.findall('\d+', str_interval, flags = regex.UNICODE)[0]),
+			int(regex.findall(r'\d+', str_interval, flags = regex.UNICODE)[0]),
 			12
 		)
 		return pyDT.timedelta(days = ((years * avg_days_per_gregorian_year) + (months * avg_days_per_gregorian_month)))
 
 	# x/52 - weeks
-	if regex.match('^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*52$', str_interval, flags = regex.UNICODE):
-		return pyDT.timedelta(weeks = int(regex.findall('\d+', str_interval, flags = regex.UNICODE)[0]))
+	if regex.match(r'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*52$', str_interval, flags = regex.UNICODE):
+		return pyDT.timedelta(weeks = int(regex.findall(r'\d+', str_interval, flags = regex.UNICODE)[0]))
 
 	# x/7 - days
-	if regex.match('^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*7$', str_interval, flags = regex.UNICODE):
-		return pyDT.timedelta(days = int(regex.findall('\d+', str_interval, flags = regex.UNICODE)[0]))
+	if regex.match(r'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*7$', str_interval, flags = regex.UNICODE):
+		return pyDT.timedelta(days = int(regex.findall(r'\d+', str_interval, flags = regex.UNICODE)[0]))
 
 	# x/24 - hours
-	if regex.match('^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*24$', str_interval, flags = regex.UNICODE):
-		return pyDT.timedelta(hours = int(regex.findall('\d+', str_interval, flags = regex.UNICODE)[0]))
+	if regex.match(r'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*24$', str_interval, flags = regex.UNICODE):
+		return pyDT.timedelta(hours = int(regex.findall(r'\d+', str_interval, flags = regex.UNICODE)[0]))
 
 	# x/60 - minutes
-	if regex.match('^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*60$', str_interval, flags = regex.UNICODE):
-		return pyDT.timedelta(minutes = int(regex.findall('\d+', str_interval, flags = regex.UNICODE)[0]))
+	if regex.match(r'^~*(\s|\t)*\d+(\s|\t)*/(\s|\t)*60$', str_interval, flags = regex.UNICODE):
+		return pyDT.timedelta(minutes = int(regex.findall(r'\d+', str_interval, flags = regex.UNICODE)[0]))
 
 	# nYnM - years, months
 	keys_year = '|'.join(list(unit_keys['year'].replace('_keys_year', '')))
 	keys_month = '|'.join(list(unit_keys['month'].replace('_keys_month', '')))
-	if regex.match('^~*(\s|\t)*\d+(%s|\s|\t)+\d+(\s|\t)*(%s)+$' % (keys_year, keys_month), str_interval, flags = regex.UNICODE):
-		parts = regex.findall('\d+', str_interval, flags = regex.UNICODE)
+	if regex.match(r'^~*(\s|\t)*\d+(%s|\s|\t)+\d+(\s|\t)*(%s)+$' % (keys_year, keys_month), str_interval, flags = regex.UNICODE):
+		parts = regex.findall(r'\d+', str_interval, flags = regex.UNICODE)
 		years, months = divmod(int(parts[1]), 12)
 		years += int(parts[0])
 		return pyDT.timedelta(days = ((years * avg_days_per_gregorian_year) + (months * avg_days_per_gregorian_month)))
@@ -1040,8 +1040,8 @@ def str2interval(str_interval=None):
 	# nMnW - months, weeks
 	keys_month = '|'.join(list(unit_keys['month'].replace('_keys_month', '')))
 	keys_week = '|'.join(list(unit_keys['week'].replace('_keys_week', '')))
-	if regex.match('^~*(\s|\t)*\d+(%s|\s|\t)+\d+(\s|\t)*(%s)+$' % (keys_month, keys_week), str_interval, flags = regex.UNICODE):
-		parts = regex.findall('\d+', str_interval, flags = regex.UNICODE)
+	if regex.match(r'^~*(\s|\t)*\d+(%s|\s|\t)+\d+(\s|\t)*(%s)+$' % (keys_month, keys_week), str_interval, flags = regex.UNICODE):
+		parts = regex.findall(r'\d+', str_interval, flags = regex.UNICODE)
 		months, weeks = divmod(int(parts[1]), 4)
 		months += int(parts[0])
 		return pyDT.timedelta(days = ((months * avg_days_per_gregorian_month) + (weeks * days_per_week)))
@@ -1615,8 +1615,8 @@ def __single_slash(str2parse):
 	matches = []
 	now = pydt_now_here()
 	# "xx/yyyy"
-	if regex.match("^(\s|\t)*\d{1,2}(\s|\t)*/+(\s|\t)*\d{4}(\s|\t)*$", str2parse, flags = regex.UNICODE):
-		parts = regex.findall('\d+', str2parse, flags = regex.UNICODE)
+	if regex.match(r"^(\s|\t)*\d{1,2}(\s|\t)*/+(\s|\t)*\d{4}(\s|\t)*$", str2parse, flags = regex.UNICODE):
+		parts = regex.findall(r'\d+', str2parse, flags = regex.UNICODE)
 		month = int(parts[0])
 		if month in range(1, 13):
 			fts = cFuzzyTimestamp (
@@ -1628,8 +1628,8 @@ def __single_slash(str2parse):
 				'label': fts.format_accurately()
 			})
 	# "xx/"
-	elif regex.match("^(\s|\t)*\d{1,2}(\s|\t)*/+(\s|\t)*$", str2parse, flags = regex.UNICODE):
-		val = int(regex.findall('\d+', str2parse, flags = regex.UNICODE)[0])
+	elif regex.match(r"^(\s|\t)*\d{1,2}(\s|\t)*/+(\s|\t)*$", str2parse, flags = regex.UNICODE):
+		val = int(regex.findall(r'\d+', str2parse, flags = regex.UNICODE)[0])
 
 		if val < 100 and val >= 0:
 			matches.append ({
@@ -1697,10 +1697,10 @@ def __numbers_only(str2parse):
 
 	Spaces or tabs are discarded.
 	"""
-	if not regex.match("^(\s|\t)*\d{1,4}(\s|\t)*$", str2parse, flags = regex.UNICODE):
+	if not regex.match(r"^(\s|\t)*\d{1,4}(\s|\t)*$", str2parse, flags = regex.UNICODE):
 		return []
 
-	val = int(regex.findall('\d{1,4}', str2parse, flags = regex.UNICODE)[0])
+	val = int(regex.findall(r'\d{1,4}', str2parse, flags = regex.UNICODE)[0])
 	if val == 0:
 		return []
 


=====================================
server/pycommon/gmI18N.py
=====================================
@@ -98,9 +98,9 @@ def __split_locale_into_levels():
 	global system_locale_level
 	system_locale_level['full'] = system_locale
 	# trim '@<variant>' part
-	system_locale_level['country'] = regex.split('@|:|\.', system_locale, 1)[0]
+	system_locale_level['country'] = regex.split(r'@|:|\.', system_locale, 1)[0]
 	# trim '_<COUNTRY>@<variant>' part
-	system_locale_level['language'] = system_locale.split('_', 1)[0]
+	system_locale_level['language'] = system_locale.split(r'_', 1)[0]
 
 	_log.debug('system locale levels: %s', system_locale_level)
 


=====================================
server/pycommon/gmPG2.py
=====================================
@@ -696,9 +696,19 @@ def get_db_fingerprint(conn=None, fname=None, with_dump=False, eol=None):
 	except KeyError:
 		lines.append('%20s: %s' % ('Schema hash', md5_sum))
 	for cmd, label in queries:
-		curs.execute(cmd)
-		rows = curs.fetchall()
-		lines.append('%20s: %s' % (label, rows[0][0]))
+		try:
+			curs.execute(cmd)
+			rows = curs.fetchall()
+			val = rows[0][0]
+		except PG_ERROR_EXCEPTION as pg_exc:
+			if pg_exc.pgcode != sql_error_codes.INSUFFICIENT_PRIVILEGE:
+				raise
+
+			if pg_exc.pgerror is None:
+				val = '[%s]: insufficient privileges' % pg_exc.pgcode
+			else:
+				val = '[%s]: %s' % (pg_exc.pgcode, pg_exc.pgerror)
+		lines.append('%20s: %s' % (label, val))
 	if with_dump:
 		lines.append('')
 		lines.append(str(get_schema_structure(link_obj = curs)))


=====================================
server/pycommon/gmTools.py
=====================================
@@ -726,14 +726,14 @@ def fname_sanitize(filename):
 	name_part = unicodedata.normalize('NFKD', name_part)
 	# remove everything not in group []
 	name_part = regex.sub (
-		'[^.\w\s[\]()%§+-]',
+		r'[^.\w\s[\]()%§+-]',
 		'',
 		name_part,
 		flags = regex.UNICODE
 	).strip()
 	# translate whitespace to underscore
 	name_part = regex.sub (
-		'\s+',
+		r'\s+',
 		'_',
 		name_part,
 		flags = regex.UNICODE
@@ -1288,7 +1288,7 @@ def shorten_words_in_line(text=None, max_length=None, min_word_length=None, igno
 	else:
 		if len(text) <= max_length:
 			return text
-	old_words = regex.split('\s+', text, flags = regex.UNICODE)
+	old_words = regex.split(r'\s+', text, flags = regex.UNICODE)
 	no_old_words = len(old_words)
 	max_word_length = max(min_word_length, (max_length // no_old_words))
 	words = []


=====================================
server/sql/v21-v22/fixups/v22-dem-org-permission-fixups.sql
=====================================
@@ -0,0 +1,46 @@
+-- ==============================================================
+-- GNUmed database schema change script
+--
+-- License: GPL v2 or later
+-- Author: Karsten Hilbert
+-- 
+-- ==============================================================
+\set ON_ERROR_STOP 1
+--set default_transaction_read_only to off;
+
+-- --------------------------------------------------------------
+revoke all on dem.org from public;
+revoke all on dem.org from "gm-public";
+grant select on dem.org to group "gm-public";
+revoke all on dem.org from "gm-staff";
+grant select, insert, update, delete on dem.org to group "gm-staff";
+revoke all on dem.org from "gm-doctors";
+grant select, insert, update, delete on dem.org to group "gm-doctors";
+
+
+revoke all on dem.org_unit from public;
+revoke all on dem.org_unit from "gm-public";
+grant select on dem.org_unit to group "gm-public";
+revoke all on dem.org_unit from "gm-staff";
+grant select, insert, update, delete on dem.org_unit to group "gm-staff";
+revoke all on dem.org_unit from "gm-doctors";
+grant select, insert, update, delete on dem.org_unit to group "gm-doctors";
+
+
+revoke all on dem.org_category from public;
+revoke all on dem.org_category from "gm-public";
+grant select on dem.org_category to group "gm-public";
+revoke all on dem.org_category from "gm-staff";
+grant select, insert, update, delete on dem.org_category to group "gm-staff";
+revoke all on dem.org_category from "gm-doctors";
+grant select, insert, update, delete on dem.org_category to group "gm-doctors";
+
+
+revoke all on dem.v_org_unit_comms from public;
+revoke all on dem.v_org_unit_comms from "gm-public";
+revoke all on dem.v_org_unit_comms from "gm-staff";
+revoke all on dem.v_org_unit_comms from "gm-doctors";
+grant select on dem.v_org_unit_comms to group "gm-public";
+
+-- --------------------------------------------------------------
+select gm.log_script_insertion('v22-dem-org-permission-fixups.sql', '22.30');


=====================================
server/sql/v21-v22/fixups/v22-dem-unique_named_identity-fixup.sql
=====================================
@@ -0,0 +1,129 @@
+-- ==============================================================
+-- GNUmed database schema change script
+--
+-- License: GPL v2 or later
+-- Author: karsten.hilbert at gmx.net
+--
+-- ==============================================================
+\set ON_ERROR_STOP 1
+--set default_transaction_read_only to off;
+set check_function_bodies to on;
+
+-- --------------------------------------------------------------
+drop function if exists dem.assert_unique_named_identity() cascade;
+
+
+-- create function and trigger
+create function dem.assert_unique_named_identity()
+	returns trigger
+	language 'plpgsql'
+	as '
+DECLARE
+	_identity_row record;
+	_names_row record;
+	_names_pks integer[];
+	_other_identities integer[];
+BEGIN
+	-- working on dem.identity
+	if TG_TABLE_NAME = ''identity'' then
+		_identity_row := NEW;
+		select array_agg(id) into _names_pks from dem.names where id_identity = NEW.pk;
+	-- working on dem.names
+	else
+		select * into _identity_row from dem.identity where pk = NEW.id_identity;
+		select ARRAY[NEW.id] into _names_pks;
+	end if;
+
+	-- loop over names rows belonging to identity
+	FOR _names_row IN
+		SELECT * FROM dem.names
+		WHERE id = ANY(_names_pks)
+	LOOP
+		-- there must not be any combination of identical
+		-- (dob, firstname, lastname, identity.comment)
+		-- so, look for clashing rows
+		SELECT array_agg(pk_identity) INTO _other_identities FROM
+			dem.v_person_names d_vpn
+				join dem.identity d_i on (d_i.pk = d_vpn.pk_identity)
+		WHERE
+			-- same firstname
+			d_vpn.firstnames = _names_row.firstnames
+				AND
+			-- same lastname
+			d_vpn.lastnames = _names_row.lastnames
+				AND
+			-- same gender
+			d_i.gender is not distinct from _identity_row.gender
+				AND
+			-- same dob (day)
+			date_trunc(''day'', d_i.dob) is not distinct from date_trunc(''day'', _identity_row.dob)
+				AND
+			-- same discriminator
+			d_i.comment is not distinct from _identity_row.comment
+				AND
+			-- but not the currently updated or inserted row
+			d_i.pk != _identity_row.pk
+		;
+
+		if coalesce(array_length(_other_identities, 1), 0) > 0 then
+			RAISE EXCEPTION
+				''[dem.assert_unique_named_identity] % on %.%: More than one person with (firstnames=%), (lastnames=%), (dob=%), (comment=%): % & %'',
+					TG_OP,
+					TG_TABLE_SCHEMA,
+					TG_TABLE_NAME,
+					_names_row.firstnames,
+					_names_row.lastnames,
+					_identity_row.dob,
+					_identity_row.comment,
+					_identity_row.pk,
+					_other_identities
+				USING ERRCODE = ''unique_violation''
+			;
+			RETURN NULL;
+		end if;
+	END LOOP;
+	return NEW;
+END;';
+
+
+comment on function dem.assert_unique_named_identity() is
+	'Ensures unique(identity.dob, names.firstnames, names.lastnames, identity.comment)';
+
+
+-- attach to dem.identity
+create constraint trigger tr_ins_d_i_assert_unique_named_identity
+	after insert on
+		dem.identity
+	deferrable
+		initially deferred
+	for
+		each row
+	execute procedure
+		dem.assert_unique_named_identity();
+
+create constraint trigger tr_upd_d_i_assert_unique_named_identity
+	after update on
+		dem.identity
+	deferrable
+		initially deferred
+	for
+		each row
+	when
+		(NEW.comment IS DISTINCT FROM OLD.comment)
+	execute procedure
+		dem.assert_unique_named_identity();
+
+
+-- attach to dem.names
+create constraint trigger tr_d_n_assert_unique_named_identity
+	after insert or update on
+		dem.names
+	deferrable
+		initially deferred
+	for
+		each row
+	execute procedure
+		dem.assert_unique_named_identity();
+
+-- --------------------------------------------------------------
+select gm.log_script_insertion('v22-dem-unique_named_identity-fixup.sql', '22.30');


=====================================
server/sql/v21-v22/fixups/v22-dem-v_message_inbox-fixup.sql
=====================================
@@ -248,6 +248,7 @@ Using UNION makes sure we get the right level of uniqueness.';
 
 
 grant select on dem.v_message_inbox to group "gm-doctors";
+grant select on dem.v_message_inbox to group "gm-staff";
 
 -- ==============================================================
-select gm.log_script_insertion('v22-dem-v_message_inbox-fixup.sql', '22.5');
+select gm.log_script_insertion('v22-dem-v_message_inbox-fixup.sql', '22.30');


=====================================
server/sql/v21-v22/fixups/v22-dem-v_pat_addresses-fixups.sql
=====================================
@@ -0,0 +1,17 @@
+-- ==============================================================
+-- GNUmed database schema change script
+--
+-- License: GPL v2 or later
+-- Author: Karsten Hilbert
+-- 
+-- ==============================================================
+\set ON_ERROR_STOP 1
+--set default_transaction_read_only to off;
+
+-- --------------------------------------------------------------
+revoke all on dem.v_pat_addresses from public;
+grant select on dem.v_pat_addresses to group "gm-doctors";
+grant select on dem.v_pat_addresses to group "gm-staff";
+
+-- --------------------------------------------------------------
+select gm.log_script_insertion('v22-dem-v_pat_addresses-fixups.sql', '22.30');


=====================================
server/sql/v21-v22/fixups/v22-release_notes-fixup.sql
=====================================
@@ -17,21 +17,21 @@ INSERT INTO dem.message_inbox (
 ) VALUES (
 	(select pk from dem.staff where db_user = 'any-doc'),
 	(select pk_type from dem.v_inbox_item_type where type = 'memo' and category = 'administrative'),
-	'Release Notes for GNUmed 1.8.18 (database v22.28)',
-	'GNUmed 1.8.18 Release Notes:
+	'Release Notes for GNUmed 1.8.20 (database v22.30)',
+	'GNUmed 1.8.20 Release Notes:
 
-	1.8.18
+	1.8.20
 
-IMPROVED: new-patient: dialog layout
-IMPROVED: phrasewheels: in-focus signalling
-IMPROVED: less diagnostic GTK output on console
+FIX: startup: crash on fingerprinting episodes in DB if gm-staff [thanks Maria]
+FIX: patient search: gm-staff shall not ensure patient-ness [thanks Maria]
 
-	22.28
+	22.30
 
-FIX: concatenation of the database schema structure
-
-IMPROVED: documentation for backup systemd .service file
+FIX: unique constraint on identity+name with multiple names per identity [thanks Maria]
+FIX: gm-staff permissions on dem.v_pat_addresses [thanks Maria]
+FIX: gm-staff permissions on dem.v_message_inbox [thanks Maria]
+FIX: permissions on org/unit tables/views
 ');
 
 -- --------------------------------------------------------------
-select gm.log_script_insertion('v22-release_notes-fixup.sql', '22.28 at 1.8.18');
+select gm.log_script_insertion('v22-release_notes-fixup.sql', '22.30 at 1.8.20');



View it on GitLab: https://salsa.debian.org/med-team/gnumed-server/-/commit/2a7ad89760fdd3fbc74f009bea9328a781b1bec6

-- 
View it on GitLab: https://salsa.debian.org/med-team/gnumed-server/-/commit/2a7ad89760fdd3fbc74f009bea9328a781b1bec6
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/debian-med-commit/attachments/20250401/d539e94b/attachment-0001.htm>


More information about the debian-med-commit mailing list