[med-svn] [Git][med-team/gnumed-client][upstream] New upstream version 1.8.21+dfsg

Andreas Tille (@tille) gitlab at salsa.debian.org
Sun Apr 27 18:54:55 BST 2025



Andreas Tille pushed to branch upstream at Debian Med / gnumed-client


Commits:
78f55775 by Andreas Tille at 2025-04-27T19:22:35+02:00
New upstream version 1.8.21+dfsg
- - - - -


15 changed files:

- client/CHANGELOG
- client/doc/api/gmAutoFileImport.html
- client/doc/api/gmClinNarrative.html
- client/doc/api/gmConnectionPool.html
- client/doc/api/gmDemographicRecord.html
- client/doc/api/gmPerson.html
- client/doc/api/gmPraxis.html
- client/doc/api/gmProviderInbox.html
- client/doc/api/gnumed.html
- client/doc/gnumed.conf.example
- client/doc/schema/gnumed-entire_schema.html
- client/etc/gnumed/gnumed-client.conf.example
- client/gm-from-vcs.conf
- client/gnumed.py
- client/pycommon/gmPG2.py


Changes:

=====================================
client/CHANGELOG
=====================================
@@ -6,6 +6,10 @@
 # rel-1-8-patches
 ------------------------------------------------
 
+	1.8.21
+
+FIX: startup: crash on fingerprinting v15+ servers [thanks gm-dbo]
+
 	1.8.20
 
 FIX: startup: crash on fingerprinting episodes in DB if gm-staff [thanks Maria]
@@ -2268,6 +2272,10 @@ FIX: missing cast to ::text in dem.date_trunc_utc() calls
 # gnumed_v22
 ------------------------------------------------
 
+	22.31
+
+FIX: crash on fingerprinting v15+ servers [thanks gm-dbo]
+
 	22.30
 
 FIX: unique constraint on identity+name with multiple names per identity [thanks Maria]


=====================================
client/doc/api/gmAutoFileImport.html
=====================================
@@ -57,39 +57,23 @@ from Gnumed.business import gmIncomingData
 
 _log = logging.getLogger('gm.autoimport')
 
-#============================================================
-def _worker__auto_import_files():
-        """Import files. Will run in a thread."""
-        cAutoImportDir().import_files()
 
-#============================================================
-__default_dirs_created = False
+AUTOIMPORT_DIR_README = """GNUmed Electronic Medical Record
 
-def setup_default_import_dirs() -> bool:
-        global __default_dirs_created
-        if __default_dirs_created:
-                return True
+Files dropped into the following directories (and their
+subdirectories) will be auto-imported into the GNUmed
+incoming area.
 
-        _log.debug('setting up default auto-import directories')
-        paths = [
-                # aka "~/gnumed/"
-                os.path.join(gmTools.gmPaths().user_work_dir, 'auto-import'),
-                # aka ".local/gnumed/"
-                os.path.join(gmTools.gmPaths().user_appdata_dir, 'auto-import')
-        ]
-        README = """GNUmed Electronic Medical Record
+        %s/
+                (for user interaction)
 
-        for user interaction:
-                %s/
+        %s/
+                (for programmatic interaction)
 
-        for programmatic interaction:
-                %s/
+Subdirectories can be links.
 
-Files dropped into these directories and their subdirectories
-will be auto-imported into the GNUmed incoming area. Sub-
-directories can also be links.
 
-Rules:
+File import rules:
 
         - inaccessible files will be ignored
 
@@ -102,18 +86,46 @@ Rules:
           ".FILENAME.CURRENT_TIMESTAMP.imported"
 
         - files already existing in the database (based on
-          MD5 of the file content) will be removed
+          MD5 of the file content) will be removed from
+          the directory
 
         - one level of subdirectories is scanned for files
 
         - subdirectories will not be removed, even if empty
 
-How to safely drop files into this directory:
 
-        Copy in the file with a filename ending in ".new". When
-        done copying rename it by removing the ".new" suffix. Renaming
-        in-place is expected to be a safe (atomic) operation.
-""" % tuple(paths)
+How to safely drop files into these directories:
+
+        Copy the file with a filename ending in ".new" into the
+        desired directory.
+
+        When done copying rename it by removing the ".new" suffix.
+        Renaming in-place must be an atomic operation. Check your
+        filesystem's documentation.
+"""
+
+#============================================================
+def _worker__auto_import_files():
+        """Import files. Will run in a thread."""
+        cAutoImportDir().import_files()
+
+#============================================================
+__default_dirs_created = False
+
+def setup_default_import_dirs() -> bool:
+        global __default_dirs_created
+        if __default_dirs_created:
+                return True
+
+        _log.debug('setting up default auto-import directories')
+        paths = [
+                # aka "~/gnumed/"
+                os.path.join(gmTools.gmPaths().user_work_dir, 'auto-import'),
+                # aka ".local/gnumed/"
+                os.path.join(gmTools.gmPaths().user_appdata_dir, 'auto-import')
+        ]
+
+        README = AUTOIMPORT_DIR_README % tuple(paths)
         for path in paths:
                 _log.debug(path)
                 if gmTools.mkdir(directory = path):
@@ -153,7 +165,7 @@ class cAutoImportDir:
                 self.__paths = [
                         # aka "~/gnumed/"
                         os.path.join(gmTools.gmPaths().user_work_dir, 'auto-import'),
-                        # aka ".gnumed/"
+                        # aka ".local/gnumed/"
                         os.path.join(gmTools.gmPaths().user_appdata_dir, 'auto-import')
                 ]
                 _log.info(self.__paths)
@@ -301,43 +313,8 @@ if __name__ == "__main__":
                 # aka ".local/gnumed/"
                 os.path.join(gmTools.gmPaths().user_appdata_dir, 'auto-import')
         ]
-        README = """GNUmed Electronic Medical Record
-
-        for user interaction:
-                %s/
-
-        for programmatic interaction:
-                %s/
-
-Files dropped into these directories and their subdirectories
-will be auto-imported into the GNUmed incoming area. Sub-
-directories can also be links.
-
-Rules:
-
-        - inaccessible files will be ignored
-
-        - filenames ending in ".imported" will be ignored
-
-        - filenames ending in ".new" will be ignored, unless
-          the file was last modified more than 24 hours ago
-
-        - successfully imported files will be renamed to
-          ".FILENAME.CURRENT_TIMESTAMP.imported"
-
-        - files already existing in the database (based on
-          MD5 of the file content) will be removed
-
-        - one level of subdirectories is scanned for files
-
-        - subdirectories will not be removed, even if empty
-
-How to safely drop files into this directory:
 
-        Copy in the file with a filename ending in ".new". When
-        done copying rename it by removing the ".new" suffix. Renaming
-        in-place is expected to be a safe (atomic) operation.
-""" % tuple(paths)
+        README = AUTOIMPORT_DIR_README % tuple(paths)
         for path in paths:
                 _log.debug(path)
                 if gmTools.mkdir(directory = path):
@@ -401,7 +378,7 @@ How to safely drop files into this directory:
                 self.__paths = [
                         # aka "~/gnumed/"
                         os.path.join(gmTools.gmPaths().user_work_dir, 'auto-import'),
-                        # aka ".gnumed/"
+                        # aka ".local/gnumed/"
                         os.path.join(gmTools.gmPaths().user_appdata_dir, 'auto-import')
                 ]
                 _log.info(self.__paths)


=====================================
client/doc/api/gmClinNarrative.html
=====================================
@@ -409,7 +409,7 @@ def get_as_journal (
         patient=None,
         active_encounter=None,
         types=None
-) -> list[str]:
+) -> list:
 
         if (patient is None) and (episodes is None) and (issues is None) and (encounters is None):
                 raise ValueError('at least one of <patient>, <episodes>, <issues>, <encounters> must not be None')
@@ -840,7 +840,7 @@ None=admin) and the values being text (possibly multi-line)</p>
 </details>
 </dd>
 <dt id="Gnumed.business.gmClinNarrative.get_as_journal"><code class="name flex">
-<span>def <span class="ident">get_as_journal</span></span>(<span>since=None, until=None, encounters=None, episodes=None, issues=None, soap_cats=None, providers=None, order_by=None, time_range=None, patient=None, active_encounter=None, types=None) ‑> list[str]</span>
+<span>def <span class="ident">get_as_journal</span></span>(<span>since=None, until=None, encounters=None, episodes=None, issues=None, soap_cats=None, providers=None, order_by=None, time_range=None, patient=None, active_encounter=None, types=None) ‑> list</span>
 </code></dt>
 <dd>
 <div class="desc"></div>
@@ -861,7 +861,7 @@ None=admin) and the values being text (possibly multi-line)</p>
         patient=None,
         active_encounter=None,
         types=None
-) -> list[str]:
+) -> list:
 
         if (patient is None) and (episodes is None) and (issues is None) and (encounters is None):
                 raise ValueError('at least one of <patient>, <episodes>, <issues>, <encounters> must not be None')


=====================================
client/doc/api/gmConnectionPool.html
=====================================
@@ -148,6 +148,558 @@ _connection_loss_markers = [
         'terminating connection due to administrator command'
 ]
 
+# %(role_name)s
+SQL__get_permissions_for_role_name = """
+-- Cluster permissions not "on" anything else
+SELECT
+  'cluster' AS object_type,
+  NULL AS name_1,
+  NULL AS name_2,
+  NULL AS name_3,
+  unnest(
+    CASE WHEN rolcanlogin THEN ARRAY['LOGIN'] ELSE ARRAY[]::text[] END
+    || CASE WHEN rolsuper THEN ARRAY['SUPERUSER'] ELSE ARRAY[]::text[] END
+    || CASE WHEN rolcreaterole THEN ARRAY['CREATE ROLE'] ELSE ARRAY[]::text[] END
+    || CASE WHEN rolcreatedb THEN ARRAY['CREATE DATABASE'] ELSE ARRAY[]::text[] END
+  ) AS privilege_type
+FROM pg_roles
+WHERE oid = quote_ident('%(role_name)s')::regrole
+
+UNION ALL
+
+-- Direct role memberships
+SELECT 'role' AS object_type, groups.rolname AS name_1, NULL AS name_2, NULL AS name_3, 'MEMBER' AS privilege_type
+FROM pg_auth_members mg
+INNER JOIN pg_roles groups ON groups.oid = mg.roleid
+INNER JOIN pg_roles members ON members.oid = mg.member
+WHERE members.rolname = '%(role_name)s'
+
+-- Direct ACL or ownerships
+UNION ALL (
+  -- ACL or owned-by dependencies of the role - global or in the currently connected database
+  WITH owned_or_acl AS (
+    SELECT
+      refobjid,  -- The referenced object: the role in this case
+      classid,   -- The pg_class oid that the dependant object is in
+      objid,     -- The oid of the dependant object in the table specified by classid
+      deptype,   -- The dependency type: o==is owner, and might have acl, a==has acl and not owner
+      objsubid   -- The 1-indexed column index for table column permissions. 0 otherwise.
+    FROM pg_shdepend
+    WHERE refobjid = quote_ident('%(role_name)s')::regrole
+    AND refclassid='pg_catalog.pg_authid'::regclass
+    AND deptype IN ('a', 'o')
+    AND (dbid = 0 OR dbid = (SELECT oid FROM pg_database WHERE datname = current_database()))
+  ),
+
+  relkind_mapping(relkind, type) AS (
+    VALUES
+      ('r', 'table'),
+      ('v', 'view'),
+      ('m', 'materialized view'),
+      ('f', 'foreign table'),
+      ('p', 'partitioned table'),
+      ('S', 'sequence')
+  ),
+
+  prokind_mapping(prokind, type) AS (
+    VALUES
+      ('f', 'function'),
+      ('p', 'procedure'),
+      ('a', 'aggregate function'),
+      ('w', 'window function')
+  ),
+
+  typtype_mapping(typtype, type) AS (
+    VALUES
+      ('b', 'base type'),
+      ('c', 'composite type'),
+      ('e', 'enum type'),
+      ('p', 'pseudo type'),
+      ('r', 'range type'),
+      ('m', 'multirange type'),
+      ('d', 'domain')
+  )
+
+  -- Database ownership
+  SELECT 'database' as object_type, datname AS name_1, NULL AS name_2, NULL AS name_3, 'OWNER' AS privilege_type
+  FROM pg_database d
+  INNER JOIN owned_or_acl a ON a.objid = d.oid
+  WHERE classid = 'pg_database'::regclass AND deptype = 'o'
+
+  UNION ALL
+
+  -- Database privileges
+  SELECT 'database' as object_type, datname AS name_1, NULL AS name_2, NULL AS name_3, privilege_type
+  FROM pg_database d
+  INNER JOIN owned_or_acl a ON a.objid = d.oid
+  CROSS JOIN aclexplode(COALESCE(d.datacl, acldefault('d', d.datdba)))
+  WHERE classid = 'pg_database'::regclass AND grantee = refobjid
+
+  UNION ALL
+
+  -- Schema ownership
+  SELECT 'schema' as object_type, nspname AS name_1, NULL AS name_2, NULL AS name_3, 'OWNER' AS privilege_type
+  FROM pg_namespace n
+  INNER JOIN owned_or_acl a ON a.objid = n.oid
+  WHERE classid = 'pg_namespace'::regclass AND deptype = 'o'
+
+  UNION ALL
+
+  -- Schema privileges
+  SELECT 'schema' as object_type, nspname AS name_1, NULL AS name_2, NULL AS name_3, privilege_type
+  FROM pg_namespace n
+  INNER JOIN owned_or_acl a ON a.objid = n.oid
+  CROSS JOIN aclexplode(COALESCE(n.nspacl, acldefault('n', n.nspowner)))
+  WHERE classid = 'pg_namespace'::regclass AND grantee = refobjid
+
+  UNION ALL
+
+  -- Table(-like) ownership
+  SELECT r.type as object_type, nspname AS name_1, relname AS name_2, NULL AS name_3, 'OWNER' AS privilege_type
+  FROM pg_class c
+  INNER JOIN pg_namespace n ON n.oid = c.relnamespace
+  INNER JOIN owned_or_acl a ON a.objid = c.oid
+  INNER JOIN relkind_mapping r ON r.relkind = c.relkind
+  WHERE classid = 'pg_class'::regclass AND deptype = 'o' AND objsubid = 0
+
+  UNION ALL
+
+  -- Table(-like) privileges
+  SELECT r.type as object_type, nspname AS name_1, relname AS name_2, NULL AS name_3, privilege_type
+  FROM pg_class c
+  INNER JOIN pg_namespace n ON n.oid = c.relnamespace
+  INNER JOIN owned_or_acl a ON a.objid = c.oid
+  CROSS JOIN aclexplode(COALESCE(c.relacl, acldefault('r', c.relowner)))
+  INNER JOIN relkind_mapping r ON r.relkind = c.relkind
+  WHERE classid = 'pg_class'::regclass AND grantee = refobjid AND objsubid = 0
+
+  UNION ALL
+
+  -- Column privileges
+  SELECT 'table column', nspname AS name_1, relname AS name_2, attname AS name_3, privilege_type
+  FROM pg_attribute t
+  INNER JOIN pg_class c ON c.oid = t.attrelid
+  INNER JOIN pg_namespace n ON n.oid = c.relnamespace
+  INNER JOIN owned_or_acl a ON a.objid = t.attrelid
+  CROSS JOIN aclexplode(COALESCE(t.attacl, acldefault('c', c.relowner)))
+  WHERE classid = 'pg_class'::regclass AND grantee = refobjid AND objsubid != 0
+
+  UNION ALL
+
+  -- Function and procdedure ownership
+  SELECT m.type as object_type, nspname AS name_1, proname AS name_2, p.oid::text AS name_3, 'OWNER' AS privilege_type
+  FROM pg_proc p
+  INNER JOIN pg_namespace n ON n.oid = p.pronamespace
+  INNER JOIN owned_or_acl a ON a.objid = p.oid
+  INNER JOIN prokind_mapping m ON m.prokind = p.prokind
+  WHERE classid = 'pg_proc'::regclass AND deptype = 'o'
+
+  UNION ALL
+
+  -- Function and procedure privileges
+  SELECT m.type as object_type, nspname AS name_1, proname AS name_2, p.oid::text AS name_3, privilege_type
+  FROM pg_proc p
+  INNER JOIN pg_namespace n ON n.oid = p.pronamespace
+  INNER JOIN owned_or_acl a ON a.objid = p.oid
+  CROSS JOIN aclexplode(COALESCE(p.proacl, acldefault('f', p.proowner)))
+  INNER JOIN prokind_mapping m ON m.prokind = p.prokind
+  WHERE classid = 'pg_proc'::regclass AND grantee = refobjid
+
+  UNION ALL
+
+  -- Large object ownership
+  SELECT 'large object' as object_type, l.oid::text AS name_1, NULL AS name_2, NULL AS name_3, 'OWNER' AS privilege_type
+  FROM pg_largeobject_metadata l
+  INNER JOIN owned_or_acl a ON a.objid = l.oid
+  WHERE classid = 'pg_largeobject'::regclass AND deptype = 'o'
+
+  UNION ALL
+
+  -- Large object privileges
+  SELECT 'large object' as object_type, l.oid::text AS name_1, NULL AS name_2, NULL AS name_3, privilege_type
+  FROM pg_largeobject_metadata l
+  INNER JOIN owned_or_acl a ON a.objid = l.oid
+  CROSS JOIN aclexplode(COALESCE(l.lomacl, acldefault('L', l.lomowner)))
+  WHERE classid = 'pg_largeobject'::regclass AND grantee = refobjid
+
+  UNION ALL
+
+  -- Type ownership
+  SELECT m.type, nspname AS name_1, typname AS name_2, NULL AS name_3, 'OWNER' AS privilege_type
+  FROM pg_type t
+  INNER JOIN pg_namespace n ON n.oid = t.typnamespace
+  INNER JOIN owned_or_acl a ON a.objid = t.oid
+  INNER JOIN typtype_mapping m ON m.typtype = t.typtype
+  WHERE classid = 'pg_type'::regclass AND deptype = 'o'
+
+  UNION ALL
+
+  -- Type privileges
+  SELECT m.type, nspname AS name_1, typname AS name_2, NULL AS name_3, privilege_type
+  FROM pg_type t
+  INNER JOIN pg_namespace n ON n.oid = t.typnamespace
+  INNER JOIN owned_or_acl a ON a.objid = t.oid
+  CROSS JOIN aclexplode(COALESCE(t.typacl, acldefault('T', t.typowner)))
+  INNER JOIN typtype_mapping m ON m.typtype = t.typtype
+  WHERE classid = 'pg_type'::regclass AND grantee = refobjid
+
+  UNION ALL
+
+  -- Language ownership
+  SELECT 'language' as object_type, l.lanname AS name_1, NULL AS name_2, NULL AS name_3, 'OWNER' AS privilege_type
+  FROM pg_language l
+  INNER JOIN owned_or_acl a ON a.objid = l.oid
+  WHERE classid = 'pg_language'::regclass AND deptype = 'o'
+
+  UNION ALL
+
+  -- Language privileges
+  SELECT 'language' as object_type, l.lanname AS name_1, NULL AS name_2, NULL AS name_3, privilege_type
+  FROM pg_language l
+  INNER JOIN owned_or_acl a ON a.objid = l.oid
+  CROSS JOIN aclexplode(COALESCE(l.lanacl, acldefault('l', l.lanowner)))
+  WHERE classid = 'pg_language'::regclass AND grantee = refobjid
+
+  UNION ALL
+
+  -- Tablespace ownership
+  SELECT 'tablespace' as object_type, t.spcname AS name_1, NULL AS name_2, NULL AS name_3, 'OWNER' AS privilege_type
+  FROM pg_tablespace t
+  INNER JOIN owned_or_acl a ON a.objid = t.oid
+  WHERE classid = 'pg_tablespace'::regclass AND deptype = 'o'
+
+  UNION ALL
+
+  -- Tablespace privileges
+  SELECT 'tablespace' as object_type, t.spcname AS name_1, NULL AS name_2, NULL AS name_3, privilege_type
+  FROM pg_tablespace t
+  INNER JOIN owned_or_acl a ON a.objid = t.oid
+  CROSS JOIN aclexplode(COALESCE(t.spcacl, acldefault('t', t.spcowner)))
+  WHERE classid = 'pg_tablespace'::regclass AND grantee = refobjid
+
+  UNION ALL
+
+  -- Foreign data wrapper ownership
+  SELECT 'foreign-data wrapper' as object_type, f.fdwname AS name_1, NULL AS name_2, NULL AS name_3, 'OWNER' AS privilege_type
+  FROM pg_foreign_data_wrapper f
+  INNER JOIN owned_or_acl a ON a.objid = f.oid
+  WHERE classid = 'pg_foreign_data_wrapper'::regclass AND deptype = 'o'
+
+  UNION ALL
+
+  -- Foreign data wrapper privileges
+  SELECT 'foreign-data wrapper' as object_type, f.fdwname AS name_1, NULL AS name_2, NULL AS name_3, privilege_type
+  FROM pg_foreign_data_wrapper f
+  INNER JOIN owned_or_acl a ON a.objid = f.oid
+  CROSS JOIN aclexplode(COALESCE(f.fdwacl, acldefault('F', f.fdwowner)))
+  WHERE classid = 'pg_foreign_data_wrapper'::regclass AND grantee = refobjid
+
+  UNION ALL
+
+  -- Foreign server ownership
+  SELECT 'foreign server' as object_type, f.srvname AS name_1, NULL AS name_2, NULL AS name_3, 'OWNER' AS privilege_type
+  FROM pg_foreign_server f
+  INNER JOIN owned_or_acl a ON a.objid = f.oid
+  WHERE classid = 'pg_foreign_server'::regclass AND deptype = 'o'
+
+  UNION ALL
+
+  -- Foreign server privileges
+  SELECT 'foreign server' as object_type, f.srvname AS name_1, NULL AS name_2, NULL AS name_3, privilege_type
+  FROM pg_foreign_server f
+  INNER JOIN owned_or_acl a ON a.objid = f.oid
+  CROSS JOIN aclexplode(COALESCE(f.srvacl, acldefault('S', f.srvowner)))
+  WHERE classid = 'pg_foreign_server'::regclass AND grantee = refobjid
+
+  UNION ALL
+
+  -- Parameter privileges
+  SELECT 'parameter' as object_type, p.parname AS name_1, NULL AS name_2, NULL AS name_3, privilege_type
+  FROM pg_parameter_acl p
+  INNER JOIN owned_or_acl a ON a.objid = p.oid
+  CROSS JOIN aclexplode(p.paracl)
+  WHERE classid = 'pg_parameter_acl'::regclass AND grantee = refobjid
+)
+
+order by name_1, name_2, name_3
+;"""
+
+SQL__get_permissions_for_current_role = """
+-- Cluster permissions not "on" anything else
+SELECT
+  'cluster' as object_type,
+  NULL AS name_1,
+  NULL AS name_2,
+  NULL AS name_3,
+  unnest(
+    CASE WHEN rolcanlogin THEN ARRAY['LOGIN'] ELSE ARRAY[]::text[] END
+    || CASE WHEN rolsuper THEN ARRAY['SUPERUSER'] ELSE ARRAY[]::text[] END
+    || CASE WHEN rolcreaterole THEN ARRAY['CREATE ROLE'] ELSE ARRAY[]::text[] END
+    || CASE WHEN rolcreatedb THEN ARRAY['CREATE DATABASE'] ELSE ARRAY[]::text[] END
+  ) AS privilege_type
+FROM pg_roles
+WHERE oid = quote_ident(current_user)::regrole
+
+UNION ALL
+
+-- Direct role memberships
+SELECT 'role' as object_type, groups.rolname AS name_1, NULL AS name_2, NULL AS name_3, 'MEMBER' AS privilege_type
+FROM pg_auth_members mg
+INNER JOIN pg_roles groups ON groups.oid = mg.roleid
+INNER JOIN pg_roles members ON members.oid = mg.member
+WHERE members.rolname = current_user
+
+-- Direct ACL or ownerships
+UNION ALL (
+  -- ACL or owned-by dependencies of the role - global or in the currently connected database
+  WITH owned_or_acl AS (
+    SELECT
+      refobjid,  -- The referenced object: the role in this case
+      classid,   -- The pg_class oid that the dependant object is in
+      objid,     -- The oid of the dependant object in the table specified by classid
+      deptype,   -- The dependency type: o==is owner, and might have acl, a==has acl and not owner
+      objsubid   -- The 1-indexed column index for table column permissions. 0 otherwise.
+    FROM pg_shdepend
+    WHERE refobjid = quote_ident(current_user)::regrole
+    AND refclassid='pg_catalog.pg_authid'::regclass
+    AND deptype IN ('a', 'o')
+    AND (dbid = 0 OR dbid = (SELECT oid FROM pg_database WHERE datname = current_database()))
+  ),
+
+  relkind_mapping(relkind, type) AS (
+    VALUES
+      ('r', 'table'),
+      ('v', 'view'),
+      ('m', 'materialized view'),
+      ('f', 'foreign table'),
+      ('p', 'partitioned table'),
+      ('S', 'sequence')
+  ),
+
+  prokind_mapping(prokind, type) AS (
+    VALUES
+      ('f', 'function'),
+      ('p', 'procedure'),
+      ('a', 'aggregate function'),
+      ('w', 'window function')
+  ),
+
+  typtype_mapping(typtype, type) AS (
+    VALUES
+      ('b', 'base type'),
+      ('c', 'composite type'),
+      ('e', 'enum type'),
+      ('p', 'pseudo type'),
+      ('r', 'range type'),
+      ('m', 'multirange type'),
+      ('d', 'domain')
+  )
+
+  -- Database ownership
+  SELECT 'database' as object_type, datname AS name_1, NULL AS name_2, NULL AS name_3, 'OWNER' AS privilege_type
+  FROM pg_database d
+  INNER JOIN owned_or_acl a ON a.objid = d.oid
+  WHERE classid = 'pg_database'::regclass AND deptype = 'o'
+
+  UNION ALL
+
+  -- Database privileges
+  SELECT 'database' as object_type, datname AS name_1, NULL AS name_2, NULL AS name_3, privilege_type
+  FROM pg_database d
+  INNER JOIN owned_or_acl a ON a.objid = d.oid
+  CROSS JOIN aclexplode(COALESCE(d.datacl, acldefault('d', d.datdba)))
+  WHERE classid = 'pg_database'::regclass AND grantee = refobjid
+
+  UNION ALL
+
+  -- Schema ownership
+  SELECT 'schema' as object_type, nspname AS name_1, NULL AS name_2, NULL AS name_3, 'OWNER' AS privilege_type
+  FROM pg_namespace n
+  INNER JOIN owned_or_acl a ON a.objid = n.oid
+  WHERE classid = 'pg_namespace'::regclass AND deptype = 'o'
+
+  UNION ALL
+
+  -- Schema privileges
+  SELECT 'schema' as object_type, nspname AS name_1, NULL AS name_2, NULL AS name_3, privilege_type
+  FROM pg_namespace n
+  INNER JOIN owned_or_acl a ON a.objid = n.oid
+  CROSS JOIN aclexplode(COALESCE(n.nspacl, acldefault('n', n.nspowner)))
+  WHERE classid = 'pg_namespace'::regclass AND grantee = refobjid
+
+  UNION ALL
+
+  -- Table(-like) ownership
+  SELECT r.type as object_type, nspname AS name_1, relname AS name_2, NULL AS name_3, 'OWNER' AS privilege_type
+  FROM pg_class c
+  INNER JOIN pg_namespace n ON n.oid = c.relnamespace
+  INNER JOIN owned_or_acl a ON a.objid = c.oid
+  INNER JOIN relkind_mapping r ON r.relkind = c.relkind
+  WHERE classid = 'pg_class'::regclass AND deptype = 'o' AND objsubid = 0
+
+  UNION ALL
+
+  -- Table(-like) privileges
+  SELECT r.type as object_type, nspname AS name_1, relname AS name_2, NULL AS name_3, privilege_type
+  FROM pg_class c
+  INNER JOIN pg_namespace n ON n.oid = c.relnamespace
+  INNER JOIN owned_or_acl a ON a.objid = c.oid
+  CROSS JOIN aclexplode(COALESCE(c.relacl, acldefault('r', c.relowner)))
+  INNER JOIN relkind_mapping r ON r.relkind = c.relkind
+  WHERE classid = 'pg_class'::regclass AND grantee = refobjid AND objsubid = 0
+
+  UNION ALL
+
+  -- Column privileges
+  SELECT 'table column', nspname AS name_1, relname AS name_2, attname AS name_3, privilege_type
+  FROM pg_attribute t
+  INNER JOIN pg_class c ON c.oid = t.attrelid
+  INNER JOIN pg_namespace n ON n.oid = c.relnamespace
+  INNER JOIN owned_or_acl a ON a.objid = t.attrelid
+  CROSS JOIN aclexplode(COALESCE(t.attacl, acldefault('c', c.relowner)))
+  WHERE classid = 'pg_class'::regclass AND grantee = refobjid AND objsubid != 0
+
+  UNION ALL
+
+  -- Function and procdedure ownership
+  SELECT m.type as object_type, nspname AS name_1, proname AS name_2, p.oid::text AS name_3, 'OWNER' AS privilege_type
+  FROM pg_proc p
+  INNER JOIN pg_namespace n ON n.oid = p.pronamespace
+  INNER JOIN owned_or_acl a ON a.objid = p.oid
+  INNER JOIN prokind_mapping m ON m.prokind = p.prokind
+  WHERE classid = 'pg_proc'::regclass AND deptype = 'o'
+
+  UNION ALL
+
+  -- Function and procedure privileges
+  SELECT m.type as object_type, nspname AS name_1, proname AS name_2, p.oid::text AS name_3, privilege_type
+  FROM pg_proc p
+  INNER JOIN pg_namespace n ON n.oid = p.pronamespace
+  INNER JOIN owned_or_acl a ON a.objid = p.oid
+  CROSS JOIN aclexplode(COALESCE(p.proacl, acldefault('f', p.proowner)))
+  INNER JOIN prokind_mapping m ON m.prokind = p.prokind
+  WHERE classid = 'pg_proc'::regclass AND grantee = refobjid
+
+  UNION ALL
+
+  -- Large object ownership
+  SELECT 'large object' as object_type, l.oid::text AS name_1, NULL AS name_2, NULL AS name_3, 'OWNER' AS privilege_type
+  FROM pg_largeobject_metadata l
+  INNER JOIN owned_or_acl a ON a.objid = l.oid
+  WHERE classid = 'pg_largeobject'::regclass AND deptype = 'o'
+
+  UNION ALL
+
+  -- Large object privileges
+  SELECT 'large object' as object_type, l.oid::text AS name_1, NULL AS name_2, NULL AS name_3, privilege_type
+  FROM pg_largeobject_metadata l
+  INNER JOIN owned_or_acl a ON a.objid = l.oid
+  CROSS JOIN aclexplode(COALESCE(l.lomacl, acldefault('L', l.lomowner)))
+  WHERE classid = 'pg_largeobject'::regclass AND grantee = refobjid
+
+  UNION ALL
+
+  -- Type ownership
+  SELECT m.type, nspname AS name_1, typname AS name_2, NULL AS name_3, 'OWNER' AS privilege_type
+  FROM pg_type t
+  INNER JOIN pg_namespace n ON n.oid = t.typnamespace
+  INNER JOIN owned_or_acl a ON a.objid = t.oid
+  INNER JOIN typtype_mapping m ON m.typtype = t.typtype
+  WHERE classid = 'pg_type'::regclass AND deptype = 'o'
+
+  UNION ALL
+
+  -- Type privileges
+  SELECT m.type, nspname AS name_1, typname AS name_2, NULL AS name_3, privilege_type
+  FROM pg_type t
+  INNER JOIN pg_namespace n ON n.oid = t.typnamespace
+  INNER JOIN owned_or_acl a ON a.objid = t.oid
+  CROSS JOIN aclexplode(COALESCE(t.typacl, acldefault('T', t.typowner)))
+  INNER JOIN typtype_mapping m ON m.typtype = t.typtype
+  WHERE classid = 'pg_type'::regclass AND grantee = refobjid
+
+  UNION ALL
+
+  -- Language ownership
+  SELECT 'language' as object_type, l.lanname AS name_1, NULL AS name_2, NULL AS name_3, 'OWNER' AS privilege_type
+  FROM pg_language l
+  INNER JOIN owned_or_acl a ON a.objid = l.oid
+  WHERE classid = 'pg_language'::regclass AND deptype = 'o'
+
+  UNION ALL
+
+  -- Language privileges
+  SELECT 'language' as object_type, l.lanname AS name_1, NULL AS name_2, NULL AS name_3, privilege_type
+  FROM pg_language l
+  INNER JOIN owned_or_acl a ON a.objid = l.oid
+  CROSS JOIN aclexplode(COALESCE(l.lanacl, acldefault('l', l.lanowner)))
+  WHERE classid = 'pg_language'::regclass AND grantee = refobjid
+
+  UNION ALL
+
+  -- Tablespace ownership
+  SELECT 'tablespace' as object_type, t.spcname AS name_1, NULL AS name_2, NULL AS name_3, 'OWNER' AS privilege_type
+  FROM pg_tablespace t
+  INNER JOIN owned_or_acl a ON a.objid = t.oid
+  WHERE classid = 'pg_tablespace'::regclass AND deptype = 'o'
+
+  UNION ALL
+
+  -- Tablespace privileges
+  SELECT 'tablespace' as object_type, t.spcname AS name_1, NULL AS name_2, NULL AS name_3, privilege_type
+  FROM pg_tablespace t
+  INNER JOIN owned_or_acl a ON a.objid = t.oid
+  CROSS JOIN aclexplode(COALESCE(t.spcacl, acldefault('t', t.spcowner)))
+  WHERE classid = 'pg_tablespace'::regclass AND grantee = refobjid
+
+  UNION ALL
+
+  -- Foreign data wrapper ownership
+  SELECT 'foreign-data wrapper' as object_type, f.fdwname AS name_1, NULL AS name_2, NULL AS name_3, 'OWNER' AS privilege_type
+  FROM pg_foreign_data_wrapper f
+  INNER JOIN owned_or_acl a ON a.objid = f.oid
+  WHERE classid = 'pg_foreign_data_wrapper'::regclass AND deptype = 'o'
+
+  UNION ALL
+
+  -- Foreign data wrapper privileges
+  SELECT 'foreign-data wrapper' as object_type, f.fdwname AS name_1, NULL AS name_2, NULL AS name_3, privilege_type
+  FROM pg_foreign_data_wrapper f
+  INNER JOIN owned_or_acl a ON a.objid = f.oid
+  CROSS JOIN aclexplode(COALESCE(f.fdwacl, acldefault('F', f.fdwowner)))
+  WHERE classid = 'pg_foreign_data_wrapper'::regclass AND grantee = refobjid
+
+  UNION ALL
+
+  -- Foreign server ownership
+  SELECT 'foreign server' as object_type, f.srvname AS name_1, NULL AS name_2, NULL AS name_3, 'OWNER' AS privilege_type
+  FROM pg_foreign_server f
+  INNER JOIN owned_or_acl a ON a.objid = f.oid
+  WHERE classid = 'pg_foreign_server'::regclass AND deptype = 'o'
+
+  UNION ALL
+
+  -- Foreign server privileges
+  SELECT 'foreign server' as object_type, f.srvname AS name_1, NULL AS name_2, NULL AS name_3, privilege_type
+  FROM pg_foreign_server f
+  INNER JOIN owned_or_acl a ON a.objid = f.oid
+  CROSS JOIN aclexplode(COALESCE(f.srvacl, acldefault('S', f.srvowner)))
+  WHERE classid = 'pg_foreign_server'::regclass AND grantee = refobjid
+
+  UNION ALL
+
+  -- Parameter privileges
+  SELECT 'parameter' as object_type, p.parname AS name_1, NULL AS name_2, NULL AS name_3, privilege_type
+  FROM pg_parameter_acl p
+  INNER JOIN owned_or_acl a ON a.objid = p.oid
+  CROSS JOIN aclexplode(p.paracl)
+  WHERE classid = 'pg_parameter_acl'::regclass AND grantee = refobjid
+)
+
+order by name_1, name_2, name_3
+;"""
+
+
 #============================================================
 class cPGCredentials:
         """Holds PostgreSQL credentials"""
@@ -529,6 +1081,7 @@ class gmConnectionPool(gmBorg.cBorg):
                         conn.commit()
                 curs = conn.cursor()
                 log_pg_settings(curs = curs)
+                log_role_permissions(curs)
                 curs.close()
                 conn.commit()
                 _log.debug('done')
@@ -963,6 +1516,33 @@ def log_conn_state(conn:psycopg2.extras.DictConnection) -> None:
         for key in d:
                 _log.debug('%s: %s', key, d[key])
 
+#------------------------------------------------------------
+def log_role_permissions(curs, role:str=None):
+        """Log permissions for role."""
+
+        if role:
+                SQL = SQL__get_permissions_for_role_name % {'role_name': role}
+                msg = 'permissions for role [%s]:' % role
+        else:
+                SQL = SQL__get_permissions_for_current_role
+                msg = 'permissions for role [current_user]:'
+        try:
+                curs.execute(SQL)
+        except psycopg2.Error:
+                _log.exception('cannot retrieve permissions')
+                return
+
+        perms = curs.fetchall()
+        if not perms:
+                _log.debug('no permissions')
+                return
+
+        gmLog2.log_multiline (
+                message = msg,
+                line_prefix = ' ',
+                text = [ '%(privilege_type)10s  ON  %(name_1)s.%(name_2)s.%(name_3)s (%(object_type)s)' % p for p in perms ]
+        )
+
 #------------------------------------------------------------
 def _safe_transaction_rollback(self) -> bool:
         """Make connection.rollback() somewhat fault tolerant.
@@ -1038,6 +1618,8 @@ if __name__ == "__main__":
         if sys.argv[1] != 'test':
                 sys.exit()
 
+        gmLog2.print_logfile_name()
+
         #--------------------------------------------------------------------
         def test_exceptions():
                 print("testing exceptions")
@@ -1201,12 +1783,29 @@ if __name__ == "__main__":
                 pool.credentials = creds
                 conn = pool.get_connection()
 
+        #--------------------------------------------------------------------
+        def test_log_role_permissions():
+                creds = cPGCredentials()
+                creds.database = 'gnumed_v23'
+                creds.user = 'any-doc'
+                creds.host = 'localhost'
+                pool = gmConnectionPool()
+                pool.credentials = creds
+                pool.get_connection()
+                #curs = conn.cursor()
+                #log_role_permissions(curs)
+                #log_role_permissions(curs, role = 'any-staff')
+                #log_role_permissions(curs, role = 'any-doc')
+                #log_role_permissions(curs, role = 'gm-staff')
+                #log_role_permissions(curs, role = 'gm-dbo')
+
         #--------------------------------------------------------------------
         #test_credentials()
         #test_exceptions()
         #test_get_connection()
-        test_verbose_get_connection()
-        #test_change_creds()</code></pre>
+        #test_verbose_get_connection()
+        #test_change_creds()
+        test_log_role_permissions()</code></pre>
 </details>
 </section>
 <section>
@@ -1516,6 +2115,42 @@ Query
         return True</code></pre>
 </details>
 </dd>
+<dt id="Gnumed.pycommon.gmConnectionPool.log_role_permissions"><code class="name flex">
+<span>def <span class="ident">log_role_permissions</span></span>(<span>curs, role: str = None)</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Log permissions for role.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def log_role_permissions(curs, role:str=None):
+        """Log permissions for role."""
+
+        if role:
+                SQL = SQL__get_permissions_for_role_name % {'role_name': role}
+                msg = 'permissions for role [%s]:' % role
+        else:
+                SQL = SQL__get_permissions_for_current_role
+                msg = 'permissions for role [current_user]:'
+        try:
+                curs.execute(SQL)
+        except psycopg2.Error:
+                _log.exception('cannot retrieve permissions')
+                return
+
+        perms = curs.fetchall()
+        if not perms:
+                _log.debug('no permissions')
+                return
+
+        gmLog2.log_multiline (
+                message = msg,
+                line_prefix = ' ',
+                text = [ '%(privilege_type)10s  ON  %(name_1)s.%(name_2)s.%(name_3)s (%(object_type)s)' % p for p in perms ]
+        )</code></pre>
+</details>
+</dd>
 </dl>
 </section>
 <section>
@@ -2132,6 +2767,7 @@ via .credentials = <cPGCredentials>.</p></div>
                         conn.commit()
                 curs = conn.cursor()
                 log_pg_settings(curs = curs)
+                log_role_permissions(curs)
                 curs.close()
                 conn.commit()
                 _log.debug('done')
@@ -2659,6 +3295,7 @@ timezone, or datestyle, hence it can be used for
 <li><code><a title="Gnumed.pycommon.gmConnectionPool.log_cursor_state" href="gmConnectionPool.html#Gnumed.pycommon.gmConnectionPool.log_cursor_state">log_cursor_state</a></code></li>
 <li><code><a title="Gnumed.pycommon.gmConnectionPool.log_pg_exception_details" href="gmConnectionPool.html#Gnumed.pycommon.gmConnectionPool.log_pg_exception_details">log_pg_exception_details</a></code></li>
 <li><code><a title="Gnumed.pycommon.gmConnectionPool.log_pg_settings" href="gmConnectionPool.html#Gnumed.pycommon.gmConnectionPool.log_pg_settings">log_pg_settings</a></code></li>
+<li><code><a title="Gnumed.pycommon.gmConnectionPool.log_role_permissions" href="gmConnectionPool.html#Gnumed.pycommon.gmConnectionPool.log_role_permissions">log_role_permissions</a></code></li>
 </ul>
 </li>
 <li><h3><a href="gmConnectionPool.html#header-classes">Classes</a></h3>


=====================================
client/doc/api/gmDemographicRecord.html
=====================================
@@ -1136,7 +1136,6 @@ if __name__ == "__main__":
 
         import random
 
-        from Gnumed.pycommon import gmConnectionPool
         #--------------------------------------------------------
         def test_address_exists():
 
@@ -1254,12 +1253,14 @@ if __name__ == "__main__":
         #--------------------------------------------------------
         def test_get_billing_address():
                 print(get_patient_address_by_type(pk_patient = 12, adr_type = 'billing'))
+
         #--------------------------------------------------------
         def test_map_urb_zip_region2country():
                 print(map_urb_zip_region2country(urb = 'Kassel', zip = '34119', region = 'Hessen'))
                 print(map_urb_zip_region2country(urb = 'Kassel', zip = None, region = 'Hessen'))
                 print(map_urb_zip_region2country(urb = None, zip = '34119', region = 'Hessen'))
                 print(map_urb_zip_region2country(urb = 'Kassel', zip = '34119', region = None))
+
         #--------------------------------------------------------
         def test_map_urb_zip_country2region():
                 print(map_urb_zip_country2region(urb = 'Kassel', zip = '34119', country = 'Germany', country_code = 'DE'))
@@ -1276,16 +1277,15 @@ if __name__ == "__main__":
 
         #--------------------------------------------------------
         #gmPG2.get_connection()
-        l, creds = gmPG2.request_login_params()
-        gmConnectionPool.gmConnectionPool().credentials = creds
+        gmPG2.request_login_params(setup_pool = True)
 
-        test_address_exists()
+        #test_address_exists()
         #test_create_address()
         #test_get_countries()
         #test_get_country_for_region()
         #test_delete_tag()
         #test_tag_images()
-        test_get_billing_address()
+        #test_get_billing_address()
         #test_map_urb_zip_region2country()
         #test_map_urb_zip_country2region()</code></pre>
 </details>


=====================================
client/doc/api/gmPerson.html
=====================================
@@ -85,6 +85,7 @@ __gender_list = None
 
 __gender2salutation_map = None
 __gender2string_map = None
+__gender2symbol_map = None
 
 #============================================================
 _MERGE_SCRIPT_HEADER = """-- GNUmed patient merge script
@@ -655,7 +656,6 @@ class cPerson(gmBusinessDBObject.cBusinessDBObject):
 
         #--------------------------------------------------------
         def _get_as_patient(self) -> 'cPatient':
-                self.is_patient = True
                 return cPatient(self._payload['pk_identity'])
 
         as_patient = property(_get_as_patient)
@@ -675,7 +675,7 @@ class cPerson(gmBusinessDBObject.cBusinessDBObject):
         # identity API
         #--------------------------------------------------------
         def _get_gender_symbol(self) -> str:
-                return map_gender2symbol[self._payload['gender']]
+                return map_gender2symbol(self._payload['gender'])
 
         gender_symbol = property(_get_gender_symbol)
 
@@ -1944,6 +1944,18 @@ class cPerson(gmBusinessDBObject.cBusinessDBObject):
 
         #----------------------------------------------------------------------
         # practice related
+        #----------------------------------------------------------------------
+        def get_last_contact(self):
+                SQL = 'select pk_encounter, last_affirmed, l10n_type from clin.v_most_recent_encounters where pk_patient = %(pat)s'
+                args = {'pat': self._payload['pk_identity']}
+                rows = gmPG2.run_ro_queries(queries = [{'cmd': SQL, 'args': args}])
+                if rows:
+                        return rows[0]
+
+                return None
+
+        last_contact = property(get_last_contact)
+
         #----------------------------------------------------------------------
         def get_last_encounter(self):
                 cmd = 'select * from clin.v_most_recent_encounters where pk_patient=%s'
@@ -2039,7 +2051,7 @@ def identity_is_patient(pk_identity:int) -> bool | None:
         args = {'pk_pat': pk_identity}
         status = False
         try:
-                rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': SQL, 'args': args}])
+                rows = gmPG2.run_ro_queries(queries = [{'cmd': SQL, 'args': args}])
                 if rows:
                         status = True
         except gmExceptions.AccessDenied:
@@ -2086,6 +2098,9 @@ class cPatient(cPerson):
         #----------------------------------------------------------
         def get_emr(self):
                 _log.debug('accessing EMR for identity [%s], thread [%s]', self._payload['pk_identity'], threading.get_native_id())
+                if self.is_patient is None:
+                        _log.error('trying to access EMR without required permissions')
+                        return None
 
                 # fast path: already set, just return it
                 if self.__emr is not None:
@@ -2138,6 +2153,10 @@ class cPatient(cPerson):
 
         #----------------------------------------------------------
         def get_document_folder(self):
+                if self.is_patient is None:
+                        _log.error('trying to access EMR without required permissions')
+                        return None
+
                 if self.__doc_folder is None:
                         self.__doc_folder = cDocumentFolder(aPKey = self._payload['pk_identity'])
                 return self.__doc_folder
@@ -2193,9 +2212,9 @@ class gmCurrentPatient(gmBorg.cBorg):
                 Args:
                         patient:
 
-                        * None: get currently active patient
+                        * None: return currently active patient
                         * -1: unset currently active patient
-                        * cPatient instance: set active patient if possible
+                        * cPatient/cPerson instance: set active patient if possible
                 """
                 try:
                         self.patient
@@ -2236,20 +2255,22 @@ class gmCurrentPatient(gmBorg.cBorg):
                         return None
 
                 # must be cPatient instance, then
-                if not isinstance(patient, cPatient):
-                        _log.error('cannot set active patient to [%s], must be either None, -1 or cPatient instance' % str(patient))
-                        raise TypeError('gmPerson.gmCurrentPatient.__init__(): <patient> must be None, -1 or cPatient instance but is: %s' % str(patient))
+                if not isinstance(patient, (cPatient, cPerson)):
+                        _log.error('cannot set active patient to [%s], must be either None, -1, cPatient or cPerson instance' % str(patient))
+                        raise TypeError('gmPerson.gmCurrentPatient.__init__(): <patient> must be None, -1, cPerson or cPatient instance but is: %s' % str(patient))
 
-                _log.info('patient switch [%s] -> [%s] requested', self.patient['pk_identity'], patient['pk_identity'])
+                #_log.info('patient switch [%s] -> [%s] requested', self.patient['pk_identity'], patient['pk_identity'])
+                _log.info('patient switch [%s] -> [%s] requested', self.patient.ID, patient.ID)
 
                 # same ID, no change needed
-                if (self.patient['pk_identity'] == patient['pk_identity']) and not forced_reload:
+                #if (self.patient['pk_identity'] == patient['pk_identity']) and not forced_reload:
+                if (self.patient.ID == patient.ID) and not forced_reload:
                         return None
 
                 # do not access "deleted" patients
                 if patient['is_deleted']:
                         _log.error('cannot set active patient to disabled dem.identity row: %s', patient)
-                        raise ValueError('gmPerson.gmCurrentPatient.__init__(): <patient> is disabled: %s' % patient)
+                        raise ValueError('gmPerson.gmCurrentPatient.__init__(): <person> is disabled: %s' % patient)
 
                 # this blocks
                 if not self.__run_callbacks_before_switching_away_from_patient():
@@ -2568,12 +2589,12 @@ def set_active_patient(patient=None, forced_reload=False):
 def get_gender_list() -> list:
         """Retrieves the list of known genders from the database."""
         global __gender_list
+        if __gender_list:
+                return __gender_list
 
-        if __gender_list is None:
-                cmd = "SELECT tag, l10n_tag, label, l10n_label, sort_weight FROM dem.v_gender_labels ORDER BY sort_weight DESC"
-                __gender_list = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
-                _log.debug('genders in database: %s' % __gender_list)
-
+        cmd = "SELECT tag, l10n_tag, label, l10n_label, symbol, l10n_symbol FROM dem.v_gender_labels ORDER BY l10n_label"
+        __gender_list = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
+        _log.debug('genders in database: %s' % __gender_list)
         return __gender_list
 
 #------------------------------------------------------------
@@ -2595,28 +2616,11 @@ map_gender2vcard = {
         'h': 'O',
         None: 'U'
 }
-
 #------------------------------------------------------------
-# maps GNUmed related i18n-aware gender specifiers to a unicode symbol
-map_gender2symbol = {
-        'm': '\u2642',
-        'f': '\u2640',
-        'tf': '\u26A5\u2640',
-#       'tf': u'\u2642\u2640-\u2640',
-        'tm': '\u26A5\u2642',
-#       'tm': u'\u2642\u2640-\u2642',
-        'h': '\u26A5',
-#       'h': u'\u2642\u2640',
-        None: '?\u26A5?'
-}
-#------------------------------------------------------------
-def map_gender2string(gender=None):
+def map_gender2string(gender:str=None) -> str:
         """Maps GNUmed related i18n-aware gender specifiers to a human-readable string."""
-
         global __gender2string_map
-
-        if __gender2string_map is None:
-                genders = get_gender_list()
+        if not __gender2string_map:
                 __gender2string_map = {
                         'm': _('male'),
                         'f': _('female'),
@@ -2625,20 +2629,52 @@ def map_gender2string(gender=None):
                         'h': '',
                         None: _('unknown gender')
                 }
-                for g in genders:
-                        __gender2string_map[g['l10n_tag']] = g['l10n_label']
-                        __gender2string_map[g['tag']] = g['l10n_label']
+                for g in get_gender_list():
+                        if g['l10n_label']:
+                                __gender2string_map[g['l10n_tag']] = g['l10n_label']
+                                __gender2string_map[g['tag']] = g['l10n_label']
                 _log.debug('gender -> string mapping: %s' % __gender2string_map)
+        try:
+                return __gender2string_map[gender]
+
+        except KeyError:
+                return '?%s?' % gender
+
+#------------------------------------------------------------
+def map_gender2symbol(gender:str=None) -> str:
+        """Maps GNUmed related i18n-aware gender specifiers to a unicode symbol."""
+        global __gender2symbol_map
+        if not __gender2symbol_map:
+                # built-in defaults
+                __gender2symbol_map = {
+                        'm': '\u2642',
+                        'f': '\u2640',
+                        'tf': '\u26A5\u2640',
+                        #'tf': u'\u2642\u2640-\u2640',
+                        'tm': '\u26A5\u2642',
+                        #'tm': u'\u2642\u2640-\u2642',
+                        'h': '\u26A5',
+                        #'h': u'\u2642\u2640',
+                        None: '?\u26A5?'
+                }
+                # update from database, possibly adding more genders
+                for g in get_gender_list():
+                        if g['l10n_symbol']:
+                                __gender2symbol_map[g['l10n_tag']] = g['l10n_symbol']
+                                __gender2symbol_map[g['tag']] = g['l10n_symbol']
+                _log.debug('gender -> symbol mapping: %s' % __gender2symbol_map)
+        try:
+                return __gender2symbol_map[gender]
+
+        except KeyError:
+                return '?%s?' % gender
 
-        return __gender2string_map[gender]
 #------------------------------------------------------------
 def map_gender2salutation(gender=None):
         """Maps GNUmed related i18n-aware gender specifiers to a human-readable salutation."""
 
         global __gender2salutation_map
-
-        if __gender2salutation_map is None:
-                genders = get_gender_list()
+        if not __gender2salutation_map:
                 __gender2salutation_map = {
                         'm': _('Mr'),
                         'f': _('Mrs'),
@@ -2647,13 +2683,17 @@ def map_gender2salutation(gender=None):
                         'h': '',
                         None: ''
                 }
-                for g in genders:
-                        __gender2salutation_map[g['l10n_tag']] = __gender2salutation_map[g['tag']]
-                        __gender2salutation_map[g['label']] = __gender2salutation_map[g['tag']]
-                        __gender2salutation_map[g['l10n_label']] = __gender2salutation_map[g['tag']]
+        #       for g in get_gender_list():
+        #               __gender2salutation_map[g['l10n_tag']] = __gender2salutation_map[g['tag']]
+        #               __gender2salutation_map[g['label']] = __gender2salutation_map[g['tag']]
+        #               __gender2salutation_map[g['l10n_label']] = __gender2salutation_map[g['tag']]
                 _log.debug('gender -> salutation mapping: %s' % __gender2salutation_map)
+        try:
+                return __gender2salutation_map[gender]
+
+        except KeyError:
+                return ''
 
-        return __gender2salutation_map[gender]
 #------------------------------------------------------------
 def map_firstnames2gender(firstnames=None):
         """Try getting the gender for the given first name."""
@@ -2670,6 +2710,7 @@ def map_firstnames2gender(firstnames=None):
                 return None
 
         return rows[0][0]
+
 #============================================================
 def get_person_IDs():
         cmd = 'SELECT pk FROM dem.identity'
@@ -2705,6 +2746,8 @@ if __name__ == '__main__':
         if sys.argv[1] != 'test':
                 sys.exit()
 
+        gmLog2.print_logfile_name()
+
         gmDateTime.init()
 
         #--------------------------------------------------------
@@ -2713,24 +2756,31 @@ if __name__ == '__main__':
                 ident = cPerson(1)
                 print("setting active patient with", ident)
                 print(ident.description)
+                print(ident.last_contact)
                 set_active_patient(patient=ident)
+                input()
 
                 patient = cPatient(12)
                 print("setting active patient with", patient)
                 print(patient.description)
+                print(patient.last_contact)
                 set_active_patient(patient=patient)
+                input()
 
                 pat = gmCurrentPatient()
                 print(pat['dob'])
                 print(pat.description)
+                print(pat.last_contact)
                 #pat['dob'] = 'test'
 
 #               staff = cStaff()
 #               print("setting active patient with", staff)
 #               set_active_patient(patient=staff)
 
+                input()
                 print("setting active patient with -1")
                 set_active_patient(patient=-1)
+
         #--------------------------------------------------------
         def test_dto_person():
                 dto = cDTO_person()
@@ -2802,9 +2852,9 @@ if __name__ == '__main__':
         #--------------------------------------------------------
         def test_gender_list():
                 genders = get_gender_list()
-                print("\n\nRetrieving gender enum (tag, label, sort_weight):")
+                print("\n\nRetrieving gender enum (tag, label, symbol):")
                 for gender in genders:
-                        print("%s, %s, %s" % (gender['tag'], gender['l10n_label'], gender['sort_weight']))
+                        print("%s, %s, %s, %s" % (gender['tag'], gender['l10n_label'], gender['l10n_symbol'], map_gender2symbol(gender['tag'])))
 
         #--------------------------------------------------------
         def test_export_area():
@@ -3079,12 +3129,12 @@ INSERT INTO dem.names (
 <pre><code class="python">def get_gender_list() -> list:
         """Retrieves the list of known genders from the database."""
         global __gender_list
+        if __gender_list:
+                return __gender_list
 
-        if __gender_list is None:
-                cmd = "SELECT tag, l10n_tag, label, l10n_label, sort_weight FROM dem.v_gender_labels ORDER BY sort_weight DESC"
-                __gender_list = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
-                _log.debug('genders in database: %s' % __gender_list)
-
+        cmd = "SELECT tag, l10n_tag, label, l10n_label, symbol, l10n_symbol FROM dem.v_gender_labels ORDER BY l10n_label"
+        __gender_list = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
+        _log.debug('genders in database: %s' % __gender_list)
         return __gender_list</code></pre>
 </details>
 </dd>
@@ -3317,7 +3367,7 @@ INSERT INTO dem.names (
         args = {'pk_pat': pk_identity}
         status = False
         try:
-                rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': SQL, 'args': args}])
+                rows = gmPG2.run_ro_queries(queries = [{'cmd': SQL, 'args': args}])
                 if rows:
                         status = True
         except gmExceptions.AccessDenied:
@@ -3364,9 +3414,7 @@ INSERT INTO dem.names (
         """Maps GNUmed related i18n-aware gender specifiers to a human-readable salutation."""
 
         global __gender2salutation_map
-
-        if __gender2salutation_map is None:
-                genders = get_gender_list()
+        if not __gender2salutation_map:
                 __gender2salutation_map = {
                         'm': _('Mr'),
                         'f': _('Mrs'),
@@ -3375,17 +3423,20 @@ INSERT INTO dem.names (
                         'h': '',
                         None: ''
                 }
-                for g in genders:
-                        __gender2salutation_map[g['l10n_tag']] = __gender2salutation_map[g['tag']]
-                        __gender2salutation_map[g['label']] = __gender2salutation_map[g['tag']]
-                        __gender2salutation_map[g['l10n_label']] = __gender2salutation_map[g['tag']]
+        #       for g in get_gender_list():
+        #               __gender2salutation_map[g['l10n_tag']] = __gender2salutation_map[g['tag']]
+        #               __gender2salutation_map[g['label']] = __gender2salutation_map[g['tag']]
+        #               __gender2salutation_map[g['l10n_label']] = __gender2salutation_map[g['tag']]
                 _log.debug('gender -> salutation mapping: %s' % __gender2salutation_map)
+        try:
+                return __gender2salutation_map[gender]
 
-        return __gender2salutation_map[gender]</code></pre>
+        except KeyError:
+                return ''</code></pre>
 </details>
 </dd>
 <dt id="Gnumed.business.gmPerson.map_gender2string"><code class="name flex">
-<span>def <span class="ident">map_gender2string</span></span>(<span>gender=None)</span>
+<span>def <span class="ident">map_gender2string</span></span>(<span>gender: str = None) ‑> str</span>
 </code></dt>
 <dd>
 <div class="desc"><p>Maps GNUmed related i18n-aware gender specifiers to a human-readable string.</p></div>
@@ -3393,13 +3444,10 @@ INSERT INTO dem.names (
 <summary>
 <span>Expand source code</span>
 </summary>
-<pre><code class="python">def map_gender2string(gender=None):
+<pre><code class="python">def map_gender2string(gender:str=None) -> str:
         """Maps GNUmed related i18n-aware gender specifiers to a human-readable string."""
-
         global __gender2string_map
-
-        if __gender2string_map is None:
-                genders = get_gender_list()
+        if not __gender2string_map:
                 __gender2string_map = {
                         'm': _('male'),
                         'f': _('female'),
@@ -3408,12 +3456,54 @@ INSERT INTO dem.names (
                         'h': '',
                         None: _('unknown gender')
                 }
-                for g in genders:
-                        __gender2string_map[g['l10n_tag']] = g['l10n_label']
-                        __gender2string_map[g['tag']] = g['l10n_label']
+                for g in get_gender_list():
+                        if g['l10n_label']:
+                                __gender2string_map[g['l10n_tag']] = g['l10n_label']
+                                __gender2string_map[g['tag']] = g['l10n_label']
                 _log.debug('gender -> string mapping: %s' % __gender2string_map)
+        try:
+                return __gender2string_map[gender]
 
-        return __gender2string_map[gender]</code></pre>
+        except KeyError:
+                return '?%s?' % gender</code></pre>
+</details>
+</dd>
+<dt id="Gnumed.business.gmPerson.map_gender2symbol"><code class="name flex">
+<span>def <span class="ident">map_gender2symbol</span></span>(<span>gender: str = None) ‑> str</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Maps GNUmed related i18n-aware gender specifiers to a unicode symbol.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def map_gender2symbol(gender:str=None) -> str:
+        """Maps GNUmed related i18n-aware gender specifiers to a unicode symbol."""
+        global __gender2symbol_map
+        if not __gender2symbol_map:
+                # built-in defaults
+                __gender2symbol_map = {
+                        'm': '\u2642',
+                        'f': '\u2640',
+                        'tf': '\u26A5\u2640',
+                        #'tf': u'\u2642\u2640-\u2640',
+                        'tm': '\u26A5\u2642',
+                        #'tm': u'\u2642\u2640-\u2642',
+                        'h': '\u26A5',
+                        #'h': u'\u2642\u2640',
+                        None: '?\u26A5?'
+                }
+                # update from database, possibly adding more genders
+                for g in get_gender_list():
+                        if g['l10n_symbol']:
+                                __gender2symbol_map[g['l10n_tag']] = g['l10n_symbol']
+                                __gender2symbol_map[g['tag']] = g['l10n_symbol']
+                _log.debug('gender -> symbol mapping: %s' % __gender2symbol_map)
+        try:
+                return __gender2symbol_map[gender]
+
+        except KeyError:
+                return '?%s?' % gender</code></pre>
 </details>
 </dd>
 <dt id="Gnumed.business.gmPerson.set_active_patient"><code class="name flex">
@@ -4327,6 +4417,9 @@ objects = [ cChildClass(row = {'data': r, 'pk_field': 'the PK column name'}) for
         #----------------------------------------------------------
         def get_emr(self):
                 _log.debug('accessing EMR for identity [%s], thread [%s]', self._payload['pk_identity'], threading.get_native_id())
+                if self.is_patient is None:
+                        _log.error('trying to access EMR without required permissions')
+                        return None
 
                 # fast path: already set, just return it
                 if self.__emr is not None:
@@ -4379,6 +4472,10 @@ objects = [ cChildClass(row = {'data': r, 'pk_field': 'the PK column name'}) for
 
         #----------------------------------------------------------
         def get_document_folder(self):
+                if self.is_patient is None:
+                        _log.error('trying to access EMR without required permissions')
+                        return None
+
                 if self.__doc_folder is None:
                         self.__doc_folder = cDocumentFolder(aPKey = self._payload['pk_identity'])
                 return self.__doc_folder
@@ -4400,6 +4497,10 @@ objects = [ cChildClass(row = {'data': r, 'pk_field': 'the PK column name'}) for
 <span>Expand source code</span>
 </summary>
 <pre><code class="python">def get_document_folder(self):
+        if self.is_patient is None:
+                _log.error('trying to access EMR without required permissions')
+                return None
+
         if self.__doc_folder is None:
                 self.__doc_folder = cDocumentFolder(aPKey = self._payload['pk_identity'])
         return self.__doc_folder</code></pre>
@@ -4414,6 +4515,9 @@ objects = [ cChildClass(row = {'data': r, 'pk_field': 'the PK column name'}) for
 </summary>
 <pre><code class="python">def get_emr(self):
         _log.debug('accessing EMR for identity [%s], thread [%s]', self._payload['pk_identity'], threading.get_native_id())
+        if self.is_patient is None:
+                _log.error('trying to access EMR without required permissions')
+                return None
 
         # fast path: already set, just return it
         if self.__emr is not None:
@@ -4500,6 +4604,10 @@ objects = [ cChildClass(row = {'data': r, 'pk_field': 'the PK column name'}) for
 <span>Expand source code</span>
 </summary>
 <pre><code class="python">def get_document_folder(self):
+        if self.is_patient is None:
+                _log.error('trying to access EMR without required permissions')
+                return None
+
         if self.__doc_folder is None:
                 self.__doc_folder = cDocumentFolder(aPKey = self._payload['pk_identity'])
         return self.__doc_folder</code></pre>
@@ -4516,6 +4624,9 @@ objects = [ cChildClass(row = {'data': r, 'pk_field': 'the PK column name'}) for
 </summary>
 <pre><code class="python">def get_emr(self):
         _log.debug('accessing EMR for identity [%s], thread [%s]', self._payload['pk_identity'], threading.get_native_id())
+        if self.is_patient is None:
+                _log.error('trying to access EMR without required permissions')
+                return None
 
         # fast path: already set, just return it
         if self.__emr is not None:
@@ -4779,7 +4890,6 @@ objects = [ cChildClass(row = {'data': r, 'pk_field': 'the PK column name'}) for
 
         #--------------------------------------------------------
         def _get_as_patient(self) -> 'cPatient':
-                self.is_patient = True
                 return cPatient(self._payload['pk_identity'])
 
         as_patient = property(_get_as_patient)
@@ -4799,7 +4909,7 @@ objects = [ cChildClass(row = {'data': r, 'pk_field': 'the PK column name'}) for
         # identity API
         #--------------------------------------------------------
         def _get_gender_symbol(self) -> str:
-                return map_gender2symbol[self._payload['gender']]
+                return map_gender2symbol(self._payload['gender'])
 
         gender_symbol = property(_get_gender_symbol)
 
@@ -6068,6 +6178,18 @@ objects = [ cChildClass(row = {'data': r, 'pk_field': 'the PK column name'}) for
 
         #----------------------------------------------------------------------
         # practice related
+        #----------------------------------------------------------------------
+        def get_last_contact(self):
+                SQL = 'select pk_encounter, last_affirmed, l10n_type from clin.v_most_recent_encounters where pk_patient = %(pat)s'
+                args = {'pat': self._payload['pk_identity']}
+                rows = gmPG2.run_ro_queries(queries = [{'cmd': SQL, 'args': args}])
+                if rows:
+                        return rows[0]
+
+                return None
+
+        last_contact = property(get_last_contact)
+
         #----------------------------------------------------------------------
         def get_last_encounter(self):
                 cmd = 'select * from clin.v_most_recent_encounters where pk_patient=%s'
@@ -6245,7 +6367,6 @@ MECARD:N:$<lastname::::>$,$<firstname::::>$;BDAY:$<date_of_birth::%Y%m%d::>$;ADR
 <span>Expand source code</span>
 </summary>
 <pre><code class="python">def _get_as_patient(self) -> 'cPatient':
-        self.is_patient = True
         return cPatient(self._payload['pk_identity'])</code></pre>
 </details>
 </dd>
@@ -6532,7 +6653,7 @@ MECARD:N:$<lastname::::>$,$<firstname::::>$;BDAY:$<date_of_birth::%Y%m%d::>$;ADR
 <span>Expand source code</span>
 </summary>
 <pre><code class="python">def _get_gender_symbol(self) -> str:
-        return map_gender2symbol[self._payload['gender']]</code></pre>
+        return map_gender2symbol(self._payload['gender'])</code></pre>
 </details>
 </dd>
 <dt id="Gnumed.business.gmPerson.cPerson.is_patient"><code class="name">var <span class="ident">is_patient</span> : bool</code></dt>
@@ -6546,6 +6667,23 @@ MECARD:N:$<lastname::::>$,$<firstname::::>$;BDAY:$<date_of_birth::%Y%m%d::>$;ADR
         return identity_is_patient(self._payload['pk_identity'])</code></pre>
 </details>
 </dd>
+<dt id="Gnumed.business.gmPerson.cPerson.last_contact"><code class="name">var <span class="ident">last_contact</span></code></dt>
+<dd>
+<div class="desc"></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def get_last_contact(self):
+        SQL = 'select pk_encounter, last_affirmed, l10n_type from clin.v_most_recent_encounters where pk_patient = %(pat)s'
+        args = {'pat': self._payload['pk_identity']}
+        rows = gmPG2.run_ro_queries(queries = [{'cmd': SQL, 'args': args}])
+        if rows:
+                return rows[0]
+
+        return None</code></pre>
+</details>
+</dd>
 <dt id="Gnumed.business.gmPerson.cPerson.medical_age"><code class="name">var <span class="ident">medical_age</span></code></dt>
 <dd>
 <div class="desc"></div>
@@ -7659,6 +7797,25 @@ MECARD:N:$<lastname::::>$,$<firstname::::>$;BDAY:$<date_of_birth::%Y%m%d::>$;ADR
         )</code></pre>
 </details>
 </dd>
+<dt id="Gnumed.business.gmPerson.cPerson.get_last_contact"><code class="name flex">
+<span>def <span class="ident">get_last_contact</span></span>(<span>self)</span>
+</code></dt>
+<dd>
+<div class="desc"></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def get_last_contact(self):
+        SQL = 'select pk_encounter, last_affirmed, l10n_type from clin.v_most_recent_encounters where pk_patient = %(pat)s'
+        args = {'pat': self._payload['pk_identity']}
+        rows = gmPG2.run_ro_queries(queries = [{'cmd': SQL, 'args': args}])
+        if rows:
+                return rows[0]
+
+        return None</code></pre>
+</details>
+</dd>
 <dt id="Gnumed.business.gmPerson.cPerson.get_last_encounter"><code class="name flex">
 <span>def <span class="ident">get_last_encounter</span></span>(<span>self)</span>
 </code></dt>
@@ -8528,9 +8685,9 @@ objects = [ cChildClass(row = {'data': r, 'pk_field': 'the PK column name'}) for
 <h2 id="args">Args</h2>
 <p>patient:</p>
 <ul>
-<li>None: get currently active patient</li>
+<li>None: return currently active patient</li>
 <li>-1: unset currently active patient</li>
-<li>cPatient instance: set active patient if possible</li>
+<li>cPatient/cPerson instance: set active patient if possible</li>
 </ul></div>
 <details class="source">
 <summary>
@@ -8584,9 +8741,9 @@ objects = [ cChildClass(row = {'data': r, 'pk_field': 'the PK column name'}) for
                 Args:
                         patient:
 
-                        * None: get currently active patient
+                        * None: return currently active patient
                         * -1: unset currently active patient
-                        * cPatient instance: set active patient if possible
+                        * cPatient/cPerson instance: set active patient if possible
                 """
                 try:
                         self.patient
@@ -8627,20 +8784,22 @@ objects = [ cChildClass(row = {'data': r, 'pk_field': 'the PK column name'}) for
                         return None
 
                 # must be cPatient instance, then
-                if not isinstance(patient, cPatient):
-                        _log.error('cannot set active patient to [%s], must be either None, -1 or cPatient instance' % str(patient))
-                        raise TypeError('gmPerson.gmCurrentPatient.__init__(): <patient> must be None, -1 or cPatient instance but is: %s' % str(patient))
+                if not isinstance(patient, (cPatient, cPerson)):
+                        _log.error('cannot set active patient to [%s], must be either None, -1, cPatient or cPerson instance' % str(patient))
+                        raise TypeError('gmPerson.gmCurrentPatient.__init__(): <patient> must be None, -1, cPerson or cPatient instance but is: %s' % str(patient))
 
-                _log.info('patient switch [%s] -> [%s] requested', self.patient['pk_identity'], patient['pk_identity'])
+                #_log.info('patient switch [%s] -> [%s] requested', self.patient['pk_identity'], patient['pk_identity'])
+                _log.info('patient switch [%s] -> [%s] requested', self.patient.ID, patient.ID)
 
                 # same ID, no change needed
-                if (self.patient['pk_identity'] == patient['pk_identity']) and not forced_reload:
+                #if (self.patient['pk_identity'] == patient['pk_identity']) and not forced_reload:
+                if (self.patient.ID == patient.ID) and not forced_reload:
                         return None
 
                 # do not access "deleted" patients
                 if patient['is_deleted']:
                         _log.error('cannot set active patient to disabled dem.identity row: %s', patient)
-                        raise ValueError('gmPerson.gmCurrentPatient.__init__(): <patient> is disabled: %s' % patient)
+                        raise ValueError('gmPerson.gmCurrentPatient.__init__(): <person> is disabled: %s' % patient)
 
                 # this blocks
                 if not self.__run_callbacks_before_switching_away_from_patient():
@@ -8925,6 +9084,7 @@ objects = [ cChildClass(row = {'data': r, 'pk_field': 'the PK column name'}) for
 <li><code><a title="Gnumed.business.gmPerson.map_firstnames2gender" href="gmPerson.html#Gnumed.business.gmPerson.map_firstnames2gender">map_firstnames2gender</a></code></li>
 <li><code><a title="Gnumed.business.gmPerson.map_gender2salutation" href="gmPerson.html#Gnumed.business.gmPerson.map_gender2salutation">map_gender2salutation</a></code></li>
 <li><code><a title="Gnumed.business.gmPerson.map_gender2string" href="gmPerson.html#Gnumed.business.gmPerson.map_gender2string">map_gender2string</a></code></li>
+<li><code><a title="Gnumed.business.gmPerson.map_gender2symbol" href="gmPerson.html#Gnumed.business.gmPerson.map_gender2symbol">map_gender2symbol</a></code></li>
 <li><code><a title="Gnumed.business.gmPerson.set_active_patient" href="gmPerson.html#Gnumed.business.gmPerson.set_active_patient">set_active_patient</a></code></li>
 <li><code><a title="Gnumed.business.gmPerson.set_yielder" href="gmPerson.html#Gnumed.business.gmPerson.set_yielder">set_yielder</a></code></li>
 </ul>
@@ -9004,6 +9164,7 @@ objects = [ cChildClass(row = {'data': r, 'pk_field': 'the PK column name'}) for
 <li><code><a title="Gnumed.business.gmPerson.cPerson.get_description_gender" href="gmPerson.html#Gnumed.business.gmPerson.cPerson.get_description_gender">get_description_gender</a></code></li>
 <li><code><a title="Gnumed.business.gmPerson.cPerson.get_external_ids" href="gmPerson.html#Gnumed.business.gmPerson.cPerson.get_external_ids">get_external_ids</a></code></li>
 <li><code><a title="Gnumed.business.gmPerson.cPerson.get_formatted_dob" href="gmPerson.html#Gnumed.business.gmPerson.cPerson.get_formatted_dob">get_formatted_dob</a></code></li>
+<li><code><a title="Gnumed.business.gmPerson.cPerson.get_last_contact" href="gmPerson.html#Gnumed.business.gmPerson.cPerson.get_last_contact">get_last_contact</a></code></li>
 <li><code><a title="Gnumed.business.gmPerson.cPerson.get_last_encounter" href="gmPerson.html#Gnumed.business.gmPerson.cPerson.get_last_encounter">get_last_encounter</a></code></li>
 <li><code><a title="Gnumed.business.gmPerson.cPerson.get_medical_age" href="gmPerson.html#Gnumed.business.gmPerson.cPerson.get_medical_age">get_medical_age</a></code></li>
 <li><code><a title="Gnumed.business.gmPerson.cPerson.get_messages" href="gmPerson.html#Gnumed.business.gmPerson.cPerson.get_messages">get_messages</a></code></li>
@@ -9014,6 +9175,7 @@ objects = [ cChildClass(row = {'data': r, 'pk_field': 'the PK column name'}) for
 <li><code><a title="Gnumed.business.gmPerson.cPerson.get_tags" href="gmPerson.html#Gnumed.business.gmPerson.cPerson.get_tags">get_tags</a></code></li>
 <li><code><a title="Gnumed.business.gmPerson.cPerson.get_waiting_list_entry" href="gmPerson.html#Gnumed.business.gmPerson.cPerson.get_waiting_list_entry">get_waiting_list_entry</a></code></li>
 <li><code><a title="Gnumed.business.gmPerson.cPerson.is_patient" href="gmPerson.html#Gnumed.business.gmPerson.cPerson.is_patient">is_patient</a></code></li>
+<li><code><a title="Gnumed.business.gmPerson.cPerson.last_contact" href="gmPerson.html#Gnumed.business.gmPerson.cPerson.last_contact">last_contact</a></code></li>
 <li><code><a title="Gnumed.business.gmPerson.cPerson.link_address" href="gmPerson.html#Gnumed.business.gmPerson.cPerson.link_address">link_address</a></code></li>
 <li><code><a title="Gnumed.business.gmPerson.cPerson.link_comm_channel" href="gmPerson.html#Gnumed.business.gmPerson.cPerson.link_comm_channel">link_comm_channel</a></code></li>
 <li><code><a title="Gnumed.business.gmPerson.cPerson.link_new_relative" href="gmPerson.html#Gnumed.business.gmPerson.cPerson.link_new_relative">link_new_relative</a></code></li>


=====================================
client/doc/api/gmPraxis.html
=====================================
@@ -686,11 +686,11 @@ if __name__ == '__main__':
         #--------------------------------------------------------
         def test_mecard():
                 for b in get_praxis_branches():
+                        print(gmTools.create_qrcode(text = b.MECARD, qr_filename = None, verbose = True))
                         print(b.MECARD)
                         mcf = b.export_as_mecard()
                         print(mcf)
-                        #print(gmTools.create_qrcode(filename = mcf, qr_filename = None, verbose = True)
-                        print(gmTools.create_qrcode(text = b.MECARD, qr_filename = None, verbose = True))
+                        print(gmTools.create_qrcode(filename = mcf, qr_filename = None, verbose = True))
                         input()
 
         #--------------------------------------------------------


=====================================
client/doc/api/gmProviderInbox.html
=====================================
@@ -428,6 +428,11 @@ if __name__ == '__main__':
         gmI18N.activate_locale()
         gmI18N.install_domain()
 
+        from Gnumed.pycommon import gmLog2
+        gmLog2.print_logfile_name()
+
+        gmPG2.request_login_params(setup_pool = True)
+
         #---------------------------------------
         def test_inbox():
                 gmStaff.gmCurrentProvider(provider = gmStaff.cStaff())
@@ -449,7 +454,7 @@ if __name__ == '__main__':
         #test_inbox()
         #test_msg()
         #test_create_type()
-        #test_due()
+        test_due()
 
 #============================================================</code></pre>
 </details>


=====================================
client/doc/api/gnumed.html
=====================================
@@ -225,7 +225,15 @@ The default log file.
 Integration with systemd-tmpfiles(8).
 .TP
 .B gnumed-completion.bash
-Integration with BASH completions.</p>
+Integration with BASH completions.
+.TP
+.B ~/gnumed/auto-incoming/
+Directory from which files are auto-imported for archival as
+patient documents. This directory is meant for the user to
+manually drop files into.</p>
+<p>It will also contain a README listing another auto-import
+directory meant for programmatic dropping of files. Under
+Linux this path will likely be ~/.local/gnumed/auto-incoming/</p>
 <p>.SH SEE ALSO
 .PP
 .TP
@@ -509,6 +517,15 @@ Integration with systemd-tmpfiles(8).
 .TP
 .B gnumed-completion.bash
 Integration with BASH completions.
+.TP
+.B ~/gnumed/auto-incoming/
+Directory from which files are auto-imported for archival as
+patient documents. This directory is meant for the user to
+manually drop files into.
+
+It will also contain a README listing another auto-import
+directory meant for programmatic dropping of files. Under
+Linux this path will likely be ~/.local/gnumed/auto-incoming/
 
 
 .SH SEE ALSO


=====================================
client/doc/gnumed.conf.example
=====================================
@@ -20,7 +20,7 @@
 
 # -------------------------------------------------------------
 [preferences]
-profile = GNUmed database at publicdb.gnumed.de (PUBLIC) (gnumed_v22 at publicdb.gnumed.de)
+profile = GNUmed database on this machine ("local": Linux/Mac) (gnumed_v22@)
 login = any-doc
 
 


=====================================
client/doc/schema/gnumed-entire_schema.html
=====================================
@@ -112,7 +112,7 @@
 <body>
 
 <!-- Primary Index -->
-<p><br><br>Dumped on 2025-03-30</p>
+<p><br><br>Dumped on 2025-04-20</p>
 <h1><a name="index">Index of database - gnumed_v22</a></h1>
 <ul>
   
@@ -127481,58 +127481,65 @@ END;</pre>
 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 * into _names_row from dem.names where id_identity = NEW.pk;
+		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;
-		_names_row := NEW;
+		select ARRAY[NEW.id] into _names_pks;
 	end if;
-	-- there cannot 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'
+	-- 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
 		;
-		RETURN NULL;
-	end if;
+		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;</pre>
 


=====================================
client/etc/gnumed/gnumed-client.conf.example
=====================================
@@ -20,7 +20,7 @@
 
 # -------------------------------------------------------------
 [preferences]
-profile = GNUmed database at publicdb.gnumed.de (PUBLIC) (gnumed_v22 at publicdb.gnumed.de)
+profile = GNUmed database on this machine ("local": Linux/Mac) (gnumed_v22@)
 login = any-doc
 
 


=====================================
client/gm-from-vcs.conf
=====================================
@@ -20,7 +20,7 @@
 
 # -------------------------------------------------------------
 [preferences]
-profile = GNUmed database at publicdb.gnumed.de (PUBLIC) (gnumed_v22 at publicdb.gnumed.de)
+profile = GNUmed database on this machine ("local": Linux/Mac) (gnumed_v22@)
 login = any-doc
 
 


=====================================
client/gnumed.py
=====================================
@@ -97,7 +97,7 @@ against. Please run GNUmed as a non-root user.
 	sys.exit(1)
 
 #----------------------------------------------------------
-current_client_version = '1.8.20'
+current_client_version = '1.8.21'
 current_client_branch = '1.8'
 
 _log = None


=====================================
client/pycommon/gmPG2.py
=====================================
@@ -699,7 +699,10 @@ def get_db_fingerprint(conn=None, fname=None, with_dump=False, eol=None):
 		try:
 			curs.execute(cmd)
 			rows = curs.fetchall()
-			val = rows[0][0]
+			if rows:
+				val = rows[0][0]
+			else:
+				val = '<not found>'
 		except PG_ERROR_EXCEPTION as pg_exc:
 			if pg_exc.pgcode != sql_error_codes.INSUFFICIENT_PRIVILEGE:
 				raise
@@ -2887,10 +2890,10 @@ SELECT to_timestamp (foofoo,'YYMMDD.HH24MI') FROM (
 	#test_row_locks()
 	#test_faulty_SQL()
 	#test_log_settings()
-	#test_get_db_fingerprint()
+	test_get_db_fingerprint()
 	#test_schema_compatible()
 	#test_get_schema_structure()
-	test___get_schema_structure()
+	#test___get_schema_structure()
 	#test_pg_temp_concat()
 
 # ======================================================================



View it on GitLab: https://salsa.debian.org/med-team/gnumed-client/-/commit/78f55775de908b66919d4bbba864888ef5d4ae45

-- 
View it on GitLab: https://salsa.debian.org/med-team/gnumed-client/-/commit/78f55775de908b66919d4bbba864888ef5d4ae45
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/20250427/f260d46d/attachment-0001.htm>


More information about the debian-med-commit mailing list