[med-svn] [Git][med-team/gnumed-client][master] 6 commits: New upstream version 1.8.21+dfsg
Andreas Tille (@tille)
gitlab at salsa.debian.org
Sun Apr 27 18:54:17 BST 2025
Andreas Tille pushed to branch master 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
- - - - -
aa6af924 by Andreas Tille at 2025-04-27T19:22:35+02:00
New upstream version
- - - - -
bcb77d64 by Andreas Tille at 2025-04-27T19:22:43+02:00
Update upstream source from tag 'upstream/1.8.21+dfsg'
Update to upstream version '1.8.21+dfsg'
with Debian dir 93bfa30d63671cecb399392e06c4e928a1f6b9a8
- - - - -
4c41866e by Andreas Tille at 2025-04-27T19:22:43+02:00
Standards-Version: 4.7.2 (routine-update)
- - - - -
3efbda63 by Andreas Tille at 2025-04-27T19:34:15+02:00
Close bug
- - - - -
9fd5d9db by Andreas Tille at 2025-04-27T19:38:48+02:00
Upload to unstable
- - - - -
17 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
- debian/changelog
- debian/control
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()
# ======================================================================
=====================================
debian/changelog
=====================================
@@ -1,3 +1,11 @@
+gnumed-client (1.8.21+dfsg-1) unstable; urgency=medium
+
+ * New upstream version
+ Closes: #1103674
+ * Standards-Version: 4.7.2 (routine-update)
+
+ -- Andreas Tille <tille at debian.org> Sun, 27 Apr 2025 19:34:23 +0200
+
gnumed-client (1.8.20+dfsg-1) unstable; urgency=medium
* New upstream version (Closes: #1101691).
=====================================
debian/control
=====================================
@@ -13,7 +13,7 @@ Build-Depends: debhelper-compat (= 13),
libjs-jquery-livequery,
yui-compressor,
bash-completion
-Standards-Version: 4.7.0
+Standards-Version: 4.7.2
Vcs-Browser: https://salsa.debian.org/med-team/gnumed-client
Vcs-Git: https://salsa.debian.org/med-team/gnumed-client.git
Homepage: https://www.gnumed.de
View it on GitLab: https://salsa.debian.org/med-team/gnumed-client/-/compare/afac7279960ba52f96677d37ea677f9e9e3aff73...9fd5d9db38d769f1aa1f7de9a550ba56492e3948
--
View it on GitLab: https://salsa.debian.org/med-team/gnumed-client/-/compare/afac7279960ba52f96677d37ea677f9e9e3aff73...9fd5d9db38d769f1aa1f7de9a550ba56492e3948
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/581f57ce/attachment-0001.htm>
More information about the debian-med-commit
mailing list