[med-svn] [gnumed-client] 01/02: New upstream version 1.6.13+dfsg
Andreas Tille
tille at debian.org
Sat May 20 18:40:30 UTC 2017
This is an automated email from the git hooks/post-receive script.
tille pushed a commit to branch master
in repository gnumed-client.
commit 061d57b715b3cbbf1f9389f3663cb66257390505
Author: Andreas Tille <tille at debian.org>
Date: Sat May 20 20:12:58 2017 +0200
New upstream version 1.6.13+dfsg
---
client/CHANGELOG | 30 +-
client/business/gmClinNarrative.py | 13 +-
client/business/gmClinicalRecord.py | 21 -
client/business/gmDICOM.py | 46 +-
client/business/gmExportArea.py | 19 +-
client/business/gmPerson.py | 50 +-
client/business/gmSoapDefs.py | 56 +-
client/doc/schema/gnumed-entire_schema.html | 887 ++++++++++++++++++++++------
client/gm-from-vcs.sh | 3 +-
client/gnumed.py | 5 +-
client/pycommon/gmBackendListener.py | 6 +-
client/pycommon/gmDateTime.py | 15 +-
client/pycommon/gmDispatcher.py | 8 +-
client/pycommon/gmLog2.py | 58 +-
client/pycommon/gmPG2.py | 6 +-
client/wxpython/gmAuthWidgets.py | 55 ++
client/wxpython/gmDemographicsWidgets.py | 6 +-
client/wxpython/gmEMRStructWidgets.py | 24 +-
client/wxpython/gmEditArea.py | 3 +
client/wxpython/gmExportAreaWidgets.py | 16 +-
client/wxpython/gmGuiMain.py | 20 +-
client/wxpython/gmHorstSpace.py | 88 ++-
client/wxpython/gmMeasurementWidgets.py | 6 +-
client/wxpython/gmMedicationWidgets.py | 5 +-
client/wxpython/gmNarrativeWidgets.py | 2 +
client/wxpython/gmProviderInboxWidgets.py | 2 +-
client/wxpython/gmSOAPWidgets.py | 1 +
client/wxpython/gmSubstanceMgmtWidgets.py | 27 +-
client/wxpython/gmTextCtrl.py | 26 +-
client/wxpython/gmVaccWidgets.py | 64 +-
30 files changed, 1229 insertions(+), 339 deletions(-)
diff --git a/client/CHANGELOG b/client/CHANGELOG
index dd89b01..66b7f05 100644
--- a/client/CHANGELOG
+++ b/client/CHANGELOG
@@ -6,6 +6,27 @@
# rel-1-6-patches
------------------------------------------------
+ 1.6.13
+
+FIX: editing of drug products
+FIX: formatting of intervals with seconds [thanks Rickard]
+FIX: robustify backend listener against change notification trigger errors
+FIX: backport once-only detection of unicode char selector
+FIX: improper handling of notebook page change events
+FIX: error handling on uploading DICOM to Orthanc
+
+IMPROVED: more fully prevent logfile based password leaks
+IMPROVED: add listing of latest vaccination per indication
+IMPROVED: export area change listening and sortability
+IMPROVED: episode edit area behaviour
+IMPROVED: add measurement by clicking empty cell in grid
+
+NEW: add Constans algorithm for upper extremity DVT
+
+ 1.6.12
+
+FIX: patient merging [thanks Marc]
+
1.6.11
IMPROVED: edit area refresh on first setting data
@@ -17,11 +38,11 @@ IMPROVED: use of pdfinfo in gm-describe_file
FIX: stall of gm-create_datamatrix in swap storm
FIX: BMP creation without substance intakes
FIX: missing quotes in BMP datafile [thanks Moritz]
-FIX: failure to sometimes store progress notes [thanks Marc]
FIX: exception on double-clicking document tree label node
FIX: exception on switching to drug database frontend [thanks a sk_SK]
FIX: exception on saving hospital stay [thanks a sk_SK]
FIX: exception on checking for upgrade [thanks Philipp]
+FIX: force soap cat to lower case on creating progress notes
1.6.10
@@ -1834,11 +1855,18 @@ FIX: missing cast to ::text in dem.date_trunc_utc() calls
# gnumed_v21
------------------------------------------------
+ 21.12
+
+IMPROVED: logging on dem.identity/dem.names uniqueness violation
+
+FIX: remove no longer needed public.array_agg
+
21.11
IMPROVED: backup scripts error checking
FIX: serialization failures due to table mod announcement triggers
+FIX: reset upper case soap cats to lower case
21.10
diff --git a/client/business/gmClinNarrative.py b/client/business/gmClinNarrative.py
index 008afbb..4ae98c3 100644
--- a/client/business/gmClinNarrative.py
+++ b/client/business/gmClinNarrative.py
@@ -181,6 +181,9 @@ def create_progress_note(soap=None, episode_id=None, encounter_id=None, link_obj
if soap is None:
return True
+ if not gmSoapDefs.are_valid_soap_cats(soap.keys(), allow_upper = True):
+ raise ValueError(u'invalid SOAP category in <soap> dictionary: %s', soap)
+
if link_obj is None:
link_obj = gmPG2.get_connection(readonly = False)
conn_rollback = link_obj.rollback
@@ -193,10 +196,6 @@ def create_progress_note(soap=None, episode_id=None, encounter_id=None, link_obj
instances = {}
for cat in soap:
- if cat not in gmSoapDefs.KNOWN_SOAP_CATS:
- conn_rollback()
- conn_close()
- raise ValueError(u'invalid SOAP category [%s] in <soap> dictionary: %s', cat, soap)
val = soap[cat]
if val is None:
continue
@@ -243,7 +242,7 @@ def create_narrative_item(narrative=None, soap_cat=None, episode_id=None, encoun
INSERT INTO clin.clin_narrative
(fk_encounter, fk_episode, narrative, soap_cat)
SELECT
- %(enc)s, %(epi)s, %(narr)s, %(soap)s
+ %(enc)s, %(epi)s, %(narr)s, lower(%(soap)s)
WHERE NOT EXISTS (
SELECT 1 FROM clin.v_narrative
WHERE
@@ -251,7 +250,7 @@ def create_narrative_item(narrative=None, soap_cat=None, episode_id=None, encoun
AND
pk_episode = %(epi)s
AND
- soap_cat = %(soap)s
+ soap_cat = lower(%(soap)s)
AND
narrative = %(narr)s
)
@@ -272,7 +271,7 @@ def create_narrative_item(narrative=None, soap_cat=None, episode_id=None, encoun
AND
pk_episode = %(epi)s
AND
- soap_cat = %(soap)s
+ soap_cat = lower(%(soap)s)
AND
narrative = %(narr)s
"""
diff --git a/client/business/gmClinicalRecord.py b/client/business/gmClinicalRecord.py
index ca47211..a1a13b9 100644
--- a/client/business/gmClinicalRecord.py
+++ b/client/business/gmClinicalRecord.py
@@ -1857,10 +1857,6 @@ WHERE
where_parts.append(u'c_v_pv.pk_episode IN (select pk from clin.episode where fk_health_issue IN %(issues)s)')
args['issues'] = tuple(issues)
- ## find the PKs
- #cmd = u'SELECT pk_vaccination, l10n_indication, no_of_shots FROM clin.v_pat_last_vacc4indication WHERE %s' % u'\nAND '.join(where_parts)
- #rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
-
# find the shots
cmd = u"""
SELECT
@@ -1888,23 +1884,6 @@ WHERE
gmVaccination.cVaccination(row = {'idx': idx, 'data': shot_row, 'pk_field': 'pk_vaccination'})
)
- #shot_pks = [ shot_for_ind['pk_vaccination'] for shot_for_ind in rows ]
- #shot_inds = [ shot_for_ind['l10n_indication'] for shot_for_ind in rows ]
- #counts_of_shots = [ shot_for_ind['no_of_shots'] for shot_for_ind in rows ]
-
- ## turn them into vaccinations
- #cmd = gmVaccination.sql_fetch_vaccination % u'pk_vaccination IN %(pks)s'
- #args = {'pks': tuple(shot_pks)}
- #rows, row_idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
-
- #vaccs = {}
- #for shot_num in range(len(shot_pks)):
- # pk = shot_pks[shot_num]
- # count_of_shots = counts_of_shots[shot_num]
- # for r in rows:
- # if r['pk_vaccination'] == pk:
- # vaccs[shot_inds[shot_num]] = (count_of_shots, gmVaccination.cVaccination(row = {'idx': row_idx, 'data': r, 'pk_field': 'pk_vaccination'}))
-
return vaccs
#--------------------------------------------------------
diff --git a/client/business/gmDICOM.py b/client/business/gmDICOM.py
index 5177399..ce4c926 100644
--- a/client/business/gmDICOM.py
+++ b/client/business/gmDICOM.py
@@ -530,7 +530,7 @@ class cOrthancServer:
#--------------------------------------------------------
def upload_dicom_file(self, filename, check_mime_type=False):
if gmTools.fname_stem(filename) == u'DICOMDIR':
- _log.debug('ignoring [%s], no use uploading DICOMDIR files to Orthanc', filename)
+ _log.debug(u'ignoring [%s], no use uploading DICOMDIR files to Orthanc', filename)
return True
if check_mime_type:
@@ -544,6 +544,7 @@ class cOrthancServer:
return False
dcm_data = f.read()
f.close()
+ _log.debug(u'uploading [%s]', filename)
upload_url = '%s/instances' % self.__server_url
uploaded = self.__run_POST(upload_url, data = dcm_data, content_type = 'application/dicom')
if uploaded is False:
@@ -580,7 +581,7 @@ class cOrthancServer:
not_uploaded.append(filename)
if len(not_uploaded) > 0:
- _log.error('not all files uploaded')
+ _log.error(u'not all files uploaded')
return (uploaded, not_uploaded)
#--------------------------------------------------------
@@ -592,21 +593,25 @@ class cOrthancServer:
_log.error(exc)
#--------------------
- _log.debug('uploading DICOM files from [%s]', directory)
+ _log.debug(u'uploading DICOM files from [%s]', directory)
if not recursive:
files2try = os.listdir(directory)
+ _log.debug(u'found %s files', len(files2try))
if ignore_other_files:
files2try = [ f for f in files2try if gmMimeLib.guess_mimetype(f) == u'application/dicom' ]
+ _log.debug(u'DICOM files therein: %s', len(files2try))
return self.upload_dicom_files(files = files2try, check_mime_type = check_mime_type)
- _log.debug('recursing for DICOM files')
+ _log.debug(u'recursing for DICOM files')
uploaded = []
not_uploaded = []
for curr_root, curr_root_subdirs, curr_root_files in os.walk(directory, onerror = _on_error):
- _log.debug('recursing into [%s]', curr_root)
+ _log.debug(u'recursing into [%s]', curr_root)
files2try = [ os.path.join(curr_root, f) for f in curr_root_files ]
+ _log.debug(u'found %s files', len(files2try))
if ignore_other_files:
files2try = [ f for f in files2try if gmMimeLib.guess_mimetype(f) == u'application/dicom' ]
+ _log.debug(u'DICOM files therein: %s', len(files2try))
up, not_up = self.upload_dicom_files (
files = files2try,
check_mime_type = check_mime_type
@@ -816,6 +821,12 @@ class cOrthancServer:
except httplib.ResponseNotReady:
_log.exception('cannot GET: %s', full_url)
return False
+ except httplib.InvalidURL:
+ _log.exception('cannot GET: %s', full_url)
+ return False
+ except httplib2.ServerNotFoundError:
+ _log.exception('cannot GET: %s', full_url)
+ return False
except socket.error:
_log.exception('cannot GET: %s', full_url)
return False
@@ -843,24 +854,31 @@ class cOrthancServer:
else:
body = json.dumps(data)
headers = { 'content-type' : 'application/json' }
+
try:
- response, content = self.__conn.request(url, 'POST', body = body, headers = headers)
- except httplib.ResponseNotReady:
- _log.exception('cannot POST: %s', url)
- return False
+ try:
+ response, content = self.__conn.request(url, 'POST', body = body, headers = headers)
+ except socket.error as exc:
+ if exc.errno != 32:
+ raise
+ _log.exception(u'retrying POST [%s] after: %s', url, exc)
+ response, content = self.__conn.request(url, 'POST', body = body, headers = headers)
except socket.error:
- _log.exception('cannot POST: %s', url)
+ _log.exception(u'cannot POST: %s', url)
+ return False
+ except httplib.ResponseNotReady:
+ _log.exception(u'cannot POST: %s', url)
return False
except OverflowError:
- _log.exception('cannot POST: %s', url)
+ _log.exception(u'cannot POST: %s', url)
return False
if response.status == 404:
- _log.debug('no data, response: %s', response)
+ _log.debug(u'no data, response: %s', response)
return []
if not (response.status in [ 200, 302 ]):
- _log.error('cannot POST: %s', url)
- _log.error('response: %s', response)
+ _log.error(u'cannot POST: %s', url)
+ _log.error(u'response: %s', response)
return False
try:
return json.loads(content)
diff --git a/client/business/gmExportArea.py b/client/business/gmExportArea.py
index c89acf2..ecd01f7 100644
--- a/client/business/gmExportArea.py
+++ b/client/business/gmExportArea.py
@@ -430,13 +430,30 @@ class cExportArea(object):
#--------------------------------------------------------
def add_documents(self, documents=None):
for doc in documents:
+ doc_tag = _(u'%s (%s)%s') % (
+ doc['l10n_type'],
+ gmDateTime.pydt_strftime(doc['clin_when'], '%Y %b %d'),
+ gmTools.coalesce(doc['comment'], u'', u' "%s"')
+ )
for obj in doc.parts:
if self.document_part_item_exists(pk_part = obj['pk_obj']):
continue
+ f_ext = u''
+ if obj['filename'] is not None:
+ f_ext = os.path.splitext(ob['filename'])[1].strip('.').strip()
+ if f_ext != u'':
+ f_ext = u' .' + f_ext.upper()
+ obj_tag = _(u'part %s (%s%s)%s') % (
+ obj['seq_idx'],
+ gmTools.size2str(obj['size']),
+ f_ext,
+ gmTools.coalesce(obj['obj_comment'], u'', u' "%s"')
+ )
create_export_item (
- description = _('doc: %s') % obj.format_single_line(),
+ description = u'%s - %s' % (doc_tag, obj_tag),
pk_doc_obj = obj['pk_obj']
)
+
#--------------------------------------------------------
def document_part_item_exists(self, pk_part=None):
cmd = u"SELECT EXISTS (SELECT 1 FROM clin.export_item WHERE fk_doc_obj = %(pk_obj)s)"
diff --git a/client/business/gmPerson.py b/client/business/gmPerson.py
index 603c198..f3f9c39 100644
--- a/client/business/gmPerson.py
+++ b/client/business/gmPerson.py
@@ -921,6 +921,9 @@ class cPerson(gmBusinessDBObject.cBusinessDBObject):
if other_identity.ID == curr_pat.ID:
return False, _('Cannot merge active patient into another patient.')
+ now_here = gmDateTime.pydt_strftime(gmDateTime.pydt_now_here())
+ distinguisher = _(u'merge of #%s into #%s @ %s') % (other_identity.ID, self.ID, now_here)
+
queries = []
args = {'pat2del': other_identity.ID, 'pat2keep': self.ID}
@@ -944,7 +947,7 @@ class cPerson(gmBusinessDBObject.cBusinessDBObject):
})
# delete old allergy state
queries.append ({
- 'cmd': u'delete from clin.allergy_state where pk = (select pk_allergy_state from clin.v_pat_allergy_state where pk_patient = %(pat2del)s)',
+ 'cmd': u'DELETE FROM clin.allergy_state WHERE pk = (SELECT pk_allergy_state FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2del)s)',
'args': args
})
@@ -963,11 +966,23 @@ class cPerson(gmBusinessDBObject.cBusinessDBObject):
})
# transfer names
- # 1) disambiguate names in old pat
+ # 0) force pat2del to be a unique identity from pat2keep
+ # should identical names happen to occur
+ queries.append ({
+ 'cmd': u"""
+ UPDATE dem.identity SET
+ comment = coalesce(comment || ' ', '') || '(%s)'
+ WHERE
+ pk = %%(pat2del)s
+ """ % distinguisher,
+ 'args': args
+ })
+
+ # 1) disambiguate names in old patient
queries.append ({
'cmd': u"""
UPDATE dem.names d_n1 SET
- lastnames = lastnames || ' (%s %s)'
+ lastnames = lastnames || ' (%s)'
WHERE
d_n1.id_identity = %%(pat2del)s
AND
@@ -979,10 +994,10 @@ class cPerson(gmBusinessDBObject.cBusinessDBObject):
d_n2.lastnames = d_n1.lastnames
AND
d_n2.firstnames = d_n1.firstnames
- )""" % (_('assimilated'), gmDateTime.pydt_strftime(gmDateTime.pydt_now_here())),
+ )""" % distinguisher,
'args': args
})
- # 2) move inactive ones (but beware of dupes)
+ # 2) move inactive ones (dupes are expected to have been eliminated in step 1 above)
queries.append ({
'cmd': u"""
UPDATE dem.names SET
@@ -990,16 +1005,25 @@ class cPerson(gmBusinessDBObject.cBusinessDBObject):
WHERE id_identity = %(pat2del)s AND active IS false""",
'args': args
})
- # 3) copy active ones
+ # 3) copy active name (because each identity MUST have at least one
+ # *active* name so we can't just UPDATE over to pat2keep),
+ # also, needs de-duplication or else it would conflict with *itself*
+ # on pat2keep, said de-duplication happened in step 0 above
+ # whereby pat2del is made unique by means of adding a pseudo-random
+ # dem.identity.comment
queries.append ({
'cmd': u"""
INSERT INTO dem.names (
id_identity, active, lastnames, firstnames, preferred, comment
)
SELECT
- %(pat2keep)s, false, lastnames, firstnames, preferred, comment
+ %%(pat2keep)s, false, lastnames, firstnames, preferred, comment || ' (%s)'
FROM dem.names d_n
- WHERE d_n.id_identity = %(pat2del)s AND d_n.active IS true""",
+ WHERE
+ d_n.id_identity = %%(pat2del)s
+ AND
+ d_n.active IS true
+ """ % distinguisher,
'args': args
})
@@ -1008,7 +1032,7 @@ class cPerson(gmBusinessDBObject.cBusinessDBObject):
queries.append ({
'cmd': u"""
UPDATE dem.lnk_identity2comm
- SET url = url || ' (%s %s)'
+ SET url = url || ' (%s)'
WHERE
fk_identity = %%(pat2del)s
AND
@@ -1016,14 +1040,14 @@ class cPerson(gmBusinessDBObject.cBusinessDBObject):
SELECT 1 FROM dem.lnk_identity2comm d_li2c
WHERE d_li2c.fk_identity = %%(pat2keep)s AND d_li2c.url = url
)
- """ % (_('merged'), gmDateTime.pydt_strftime(gmDateTime.pydt_now_here())),
+ """ % distinguisher,
'args': args
})
# - same-value external IDs
queries.append ({
'cmd': u"""
UPDATE dem.lnk_identity2ext_id
- SET external_id = external_id || ' (%s %s)'
+ SET external_id = external_id || ' (%s)'
WHERE
id_identity = %%(pat2del)s
AND
@@ -1036,7 +1060,7 @@ class cPerson(gmBusinessDBObject.cBusinessDBObject):
AND
d_li2e.fk_origin = fk_origin
)
- """ % (_('merged'), gmDateTime.pydt_strftime(gmDateTime.pydt_now_here())),
+ """ % distinguisher,
'args': args
})
# - same addresses
@@ -1096,7 +1120,7 @@ class cPerson(gmBusinessDBObject.cBusinessDBObject):
args['date'] = gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), '%Y %B %d %H:%M')
script.write(_MERGE_SCRIPT_HEADER % args)
for query in queries:
- script.write((query['cmd'].lstrip()) % args)
+ script.write(query['cmd'] % args)
script.write(u';\n')
script.write(u'\nROLLBACK;\n')
script.write(u'--COMMIT;\n')
diff --git a/client/business/gmSoapDefs.py b/client/business/gmSoapDefs.py
index 97774ef..7c3cdaa 100644
--- a/client/business/gmSoapDefs.py
+++ b/client/business/gmSoapDefs.py
@@ -2,11 +2,7 @@
__author__ = "Karsten Hilbert <Karsten.Hilbert at gmx.net>"
__license__ = 'GPL v2 or later (for details see http://gnu.org)'
-
#============================================================
-import logging
-
-
try:
_('dummy-no-need-to-translate-but-make-epydoc-happy')
except NameError:
@@ -81,6 +77,38 @@ def soap_cats2list(soap_cats):
return normalized_cats
#============================================================
+def are_valid_soap_cats(soap_cats, allow_upper=True):
+
+ for cat in KNOWN_SOAP_CATS:
+ try:
+ while True: soap_cats.remove(cat)
+ except ValueError:
+ pass
+
+ if allow_upper:
+ for cat in KNOWN_SOAP_CATS:
+ if cat is None:
+ continue
+ try:
+ while True: soap_cats.remove(cat.upper())
+ except ValueError:
+ pass
+
+ if len(soap_cats) == 0:
+ return True
+
+ return False
+
+#============================================================
+def normalize_soap_cat(soap_cat):
+ if soap_cat in KNOWN_SOAP_CATS:
+ return soap_cat
+ soap_cat = soap_cat.lower()
+ if soap_cat in KNOWN_SOAP_CATS:
+ return soap_cat
+ return False
+
+#============================================================
if __name__ == '__main__':
import sys
@@ -104,4 +132,24 @@ if __name__ == '__main__':
print c, soap_cat2l10n[c], soap_cat2l10n_str[c]
#--------------------------------------------------------
+ def test_are_valid_cats():
+ cats = [
+ list(u'soap'),
+ list(u'soapSOAP'),
+ list(u'soapx'),
+ list(u'soapX'),
+ list(u'soapSOAPx'),
+ [None],
+ ['s', None],
+ ['s', None, 'O'],
+ ['s', None, 'x'],
+ ['s', None, 'X'],
+ ]
+ for cat_list in cats:
+ print cat_list
+ print ' valid (plain):', are_valid_soap_cats(cat_list, False)
+ print ' valid (w/ upper):', are_valid_soap_cats(cat_list, True)
+
+ #--------------------------------------------------------
test_translation()
+ test_are_valid_cats()
diff --git a/client/doc/schema/gnumed-entire_schema.html b/client/doc/schema/gnumed-entire_schema.html
index 26ae867..581eaa2 100644
--- a/client/doc/schema/gnumed-entire_schema.html
+++ b/client/doc/schema/gnumed-entire_schema.html
@@ -112,7 +112,7 @@
<body>
<!-- Primary Index -->
- <p><br><br>Dumped on 2016-12-19</p>
+ <p><br><br>Dumped on 2017-05-14</p>
<h1><a name="index">Index of database - gnumed_v21</a></h1>
<ul>
@@ -143,7 +143,7 @@
<li><a name="clin.schema">clin</a></li><ul>
<li><a href="gnumed-entire_schema.html#clin.table.-enum-allergy-type">_enum_allergy_type</a></li><li><a href="gnumed-entire_schema.html#clin.table.allergy">allergy</a></li><li><a href="gnumed-entire_schema.html#clin.table.allergy-state">allergy_state</a></li><li><a href="gnumed-entire_schema.html#clin.table.clin-aux-note">clin_aux_note</a></li><li><a href="gnumed-entire_schema.html#clin.table.clin-diag">clin_diag</a></li><li><a href="gnumed-entire_schema.html#clin.table.clin-item-ty [...]
- <li><a href="gnumed-entire_schema.html#clin.function.-get-recommendation-for-patient-hint-text-integer">_get_recommendation_for_patient_hint(text, integer)</a></li><li><a href="gnumed-entire_schema.html#clin.function.add-coded-phrase-text-text-text">add_coded_phrase(text, text, text)</a></li><li><a href="gnumed-entire_schema.html#clin.function.f-del-booster-must-have-base-immunity">f_del_booster_must_have_base_immunity()</a></li><li><a href="gnumed-entire_schema.html#clin.function.f-f [...]
+ <li><a href="gnumed-entire_schema.html#clin.function.-get-recommendation-for-patient-hint-text-integer">_get_recommendation_for_patient_hint(text, integer)</a></li><li><a href="gnumed-entire_schema.html#clin.function.add-coded-phrase-text-text-text">add_coded_phrase(text, text, text)</a></li><li><a href="gnumed-entire_schema.html#clin.function.f-del-booster-must-have-base-immunity">f_del_booster_must_have_base_immunity()</a></li><li><a href="gnumed-entire_schema.html#clin.function.f-f [...]
</ul>
<li><a name="de-de.schema">de_de</a></li><ul>
@@ -153,7 +153,7 @@
<li><a name="dem.schema">dem</a></li><ul>
<li><a href="gnumed-entire_schema.html#dem.table.address">address</a></li><li><a href="gnumed-entire_schema.html#dem.table.address-type">address_type</a></li><li><a href="gnumed-entire_schema.html#dem.table.country">country</a></li><li><a href="gnumed-entire_schema.html#dem.table.enum-comm-types">enum_comm_types</a></li><li><a href="gnumed-entire_schema.html#dem.table.enum-ext-id-types">enum_ext_id_types</a></li><li><a href="gnumed-entire_schema.html#dem.table.gender-label">gender_l [...]
- <li><a href="gnumed-entire_schema.html#dem.function.add-external-id-type-text-text">add_external_id_type(text, text)</a></li><li><a href="gnumed-entire_schema.html#dem.function.add-name-integer-text-text-boolean">add_name(integer, text, text, boolean)</a></li><li><a href="gnumed-entire_schema.html#dem.function.address-exists-text-text-text-text-text-text-text">address_exists(text, text, text, text, text, text, text)</a></li><li><a href="gnumed-entire_schema.html#dem.function.create-ad [...]
+ <li><a href="gnumed-entire_schema.html#dem.function.add-external-id-type-text-text">add_external_id_type(text, text)</a></li><li><a href="gnumed-entire_schema.html#dem.function.add-name-integer-text-text-boolean">add_name(integer, text, text, boolean)</a></li><li><a href="gnumed-entire_schema.html#dem.function.address-exists-text-text-text-text-text-text-text">address_exists(text, text, text, text, text, text, text)</a></li><li><a href="gnumed-entire_schema.html#dem.function.create-ad [...]
</ul>
<li><a name="gm.schema">gm</a></li><ul>
@@ -182,7 +182,7 @@
</ul>
<li><a name="staging.schema">staging</a></li><ul>
- <li><a href="gnumed-entire_schema.html#staging.table.lab-request">lab_request</a></li><li><a href="gnumed-entire_schema.html#staging.table.loinc-staging">loinc_staging</a></li><li><a href="gnumed-entire_schema.html#staging.table.test-result">test_result</a></li>
+ <li><a href="gnumed-entire_schema.html#staging.view.journal-without-suppressed-hints">journal_without_suppressed_hints</a></li><li><a href="gnumed-entire_schema.html#staging.table.lab-request">lab_request</a></li><li><a href="gnumed-entire_schema.html#staging.table.loinc-staging">loinc_staging</a></li><li><a href="gnumed-entire_schema.html#staging.table.test-result">test_result</a></li>
</ul>
@@ -105194,21 +105194,21 @@ END;</pre>
<hr>
<h2>Function:
- <a href="gnumed-entire_schema.html#clin.schema">clin</a>.<a name="clin.function.get-hints-for-patient-integer">get_hints_for_patient(integer)</a>
+ <a href="gnumed-entire_schema.html#clin.schema">clin</a>.<a name="clin.function.get-hints-for-patient-pk-identity-integer">get_hints_for_patient(_pk_identity integer)</a>
</h2>
<h3>Returns: SET OF v_auto_hints</h3>
<h3>Language: PLPGSQL</h3>
<pre>
DECLARE
- _pk_identity ALIAS FOR $1;
_hint ref.v_auto_hints%rowtype;
_query text;
- _md5_suppressed text;
- _rationale4suppression text;
_suppression_exists boolean; -- does not mean that the suppression applies
+ _md5_at_suppression text;
+ _old_rationale4suppression text;
_hint_currently_applies boolean; -- regardless of whether suppressed or not
_hint_recommendation text;
+ _title text;
-- _exc_state text;
-- _exc_msg text;
-- _exc_detail text;
@@ -105217,133 +105217,138 @@ DECLARE
BEGIN
-- loop over all defined hints
FOR _hint IN SELECT * FROM ref.v_auto_hints WHERE is_active LOOP
+ --raise NOTICE 'checking hint for patient %: %', _pk_identity, _hint.title;
-- is the hint suppressed ?
- SELECT
- md5_sum,
- rationale
- INTO
- _md5_suppressed,
- _rationale4suppression
- FROM clin.suppressed_hint WHERE
- fk_hint = _hint.pk_auto_hint
- AND
- fk_encounter IN (
- SELECT pk FROM clin.encounter WHERE fk_patient = _pk_identity
- );
- IF FOUND THEN
- _suppression_exists := TRUE;
- ELSE
- _suppression_exists := FALSE;
- END IF;
- -- does the hint currently apply ?
+ SELECT (clin.hint_suppression_exists(_pk_identity, _hint.pk_auto_hint)).*
+ INTO STRICT _suppression_exists, _md5_at_suppression, _old_rationale4suppression;
+ -- does the hint currently apply ? (regardless of whether it is suppressed)
_query := replace(_hint.query, 'ID_ACTIVE_PATIENT', _pk_identity::text);
- BEGIN
- EXECUTE _query INTO STRICT _hint_currently_applies;
- EXCEPTION
- --WHEN insufficient_privilege THEN RAISE WARNING 'auto hint query failed: %', _query;
- WHEN others THEN
- RAISE WARNING 'auto hint query failed: %', _query;
- -- only available starting with PG 9.2:
- --GET STACKED DIAGNOSTICS
- -- _exc_state = RETURNED_SQLSTATE,
- -- _exc_msg = MESSAGE_TEXT,
- -- _exc_detail = PG_EXCEPTION_DETAIL,
- -- _exc_hint = PG_EXCEPTION_HINT,
- -- _exc_context = PG_EXCEPTION_CONTEXT;
- --RAISE WARNING 'SQL STATE: %', _exc_state;
- --RAISE WARNING 'MESSAGE: %', _exc_msg;
- --RAISE WARNING 'DETAIL: %', _exc_detail;
- --RAISE WARNING 'HINT: %', _exc_hint;
- --RAISE WARNING 'CONTEXT: %', _exc_context;
- -- workaround for 9.1:
- RAISE WARNING 'SQL STATE: %', SQLSTATE;
- RAISE WARNING 'MESSAGE: %', SQLERRM;
- _hint.title := 'ERROR checking for [' || _hint.title || '] !';
- _hint.hint := _query;
- RETURN NEXT _hint;
- -- process next hint
- CONTINUE;
- END;
- IF _suppression_exists THEN
- -- is the hint definition still the same as at the time of suppression ?
- IF _md5_suppressed = _hint.md5_sum THEN
- -- yes, but does this hint currently apply ?
- IF _hint_currently_applies THEN
- -- suppressed, suppression valid, and hint applies: skip this hint
- CONTINUE;
- END IF;
- -- suppressed, suppression valid, hint does NOT apply:
- -- skip but invalidate suppression, because:
- -- * previously the hint applied and the user suppressed it,
- -- * then the patient changed such that the hint does not
- -- apply anymore (but the suppression is still valid),
- -- * when the patient changes again, the hint might apply again
- -- * HOWEVER - since the suppression would still be valid - the
- -- hint would magically get suppressed again (which is
- -- medically unsafe) ...
- -- after invalidation, the hint will no longer be suppressed,
- -- however - since it does not currently apply it - it will
- -- still not be returned until it applies again ...
- --
- -- -----------------------------------------------------------------------
- -- UNFORTUNATELY, the following is currently not _possible_ because
- -- we are running inside a READONLY transaction (due to inherent
- -- security risks when running arbitrary user queries [IOW the hint
- -- SQL] against the database) and we cannot execute a
- -- sub-transaction as READWRITE :-/
- --
- --UPDATE clin.suppressed_hint
- --SET md5_sum = 'invalidated'::text -- will not ever match any md5 sum
- --WHERE
- -- fk_encounter IN (
- -- SELECT pk FROM clin.encounter WHERE fk_patient = _pk_identity
- -- )
- -- AND
- -- fk_hint = _hint.pk_auto_hint;
- -- -----------------------------------------------------------------------
- --
- -- hence our our workaround is to, indeed, return the hint but
- -- tag it with a magic rationale, by means of which the client
- -- can detect it to be in need of invalidation:
- _hint.title := 'HINT DOES NOT APPLY BUT NEEDS INVALIDATION OF EXISTING SUPPRESSION [' || _hint.title || '].';
- _hint.rationale4suppression := 'magic_tag::please_invalidate_suppression';
- RETURN NEXT _hint;
- CONTINUE;
- END IF;
- -- suppression exists but hint definition must have changed
- -- does the new hint apply ?
- IF _hint_currently_applies THEN
- -- yes: ignore the suppression but provide previous
- -- rationale for suppression to the user
- _hint.rationale4suppression := _rationale4suppression;
- -- retrieve recommendation
- SELECT clin._get_recommendation_for_patient_hint(_hint.recommendation_query, _pk_identity) INTO STRICT _hint_recommendation;
- _hint.recommendation := _hint_recommendation;
- RETURN NEXT _hint;
+ _query := replace(_query, 'clin.v_emr_journal', 'staging.journal_without_suppressed_hints');
+ SELECT (clin.run_hint_query(_hint.title, _query)).*
+ INTO STRICT _hint_currently_applies, _title;
+ -- error ?
+ IF _hint_currently_applies IS NULL THEN
+ --raise NOTICE ' error -> return';
+ _hint.title := _title;
+ _hint.hint := _query;
+ RETURN NEXT _hint;
+ -- process next hint
+ CONTINUE;
+ END IF;
+ -- hint does not apply
+ IF _hint_currently_applies IS FALSE THEN
+ -- does a (previously stored) suppression exist ?
+ IF _suppression_exists IS FALSE THEN
+ -- no, so skip this hint
+ --raise NOTICE ' does not apply -> skip';
CONTINUE;
END IF;
- -- no, new hint does not apply, so ask for
- -- invalidation of suppression (see above)
+ --raise NOTICE ' does not apply but suppression invalid -> return for invalidation';
+ -- hint suppressed but does NOT apply:
+ -- skip hint but invalidate suppression, because:
+ -- * previously the hint must have applied and the user suppressed it,
+ -- * then patient data (or hint definition) changed such that
+ -- the hint does not apply anymore (but the suppression is
+ -- still valid),
+ -- * when patient data changes again, the hint might apply again
+ -- * HOWEVER - since the suppression would still be valid - the
+ -- hint would magically get suppressed again (which is
+ -- medically unsafe) ...
+ -- after invalidation, the hint will no longer be suppressed,
+ -- however - since it does not currently apply - it will
+ -- still not be returned and shown until it applies again ...
+ --
+ -- -----------------------------------------------------------------------
+ -- UNFORTUNATELY, the following is currently not _possible_ because
+ -- we are running inside a READONLY transaction (due to inherent
+ -- security risks when running arbitrary user queries [IOW the hint
+ -- SQL] against the database) and we cannot execute a
+ -- sub-transaction as READWRITE :-/
+ --
+ --UPDATE clin.suppressed_hint
+ --SET md5_sum = 'invalidated'::text -- will not ever match any md5 sum
+ --WHERE
+ -- fk_encounter IN (
+ -- SELECT pk FROM clin.encounter WHERE fk_patient = _pk_identity
+ -- )
+ -- AND
+ -- fk_hint = _hint.pk_auto_hint;
+ -- -----------------------------------------------------------------------
+ --
+ -- hence our our workaround is to, indeed, return the hint but
+ -- tag it with a magic rationale, by means of which the client
+ -- can detect it to be in need of invalidation:
_hint.title := 'HINT DOES NOT APPLY BUT NEEDS INVALIDATION OF EXISTING SUPPRESSION [' || _hint.title || '].';
- _hint.rationale4suppression := 'please_invalidate_suppression';
+ _hint.rationale4suppression := 'magic_tag::does_not_apply::suppression_needs_invalidation';
RETURN NEXT _hint;
CONTINUE;
END IF;
- -- hint is not suppressed
- -- does the hint currently apply ?
- IF _hint_currently_applies THEN
- -- yes: retrieve recommendation
+ --raise NOTICE ' applies';
+ -- but is there a suppression ?
+ IF _suppression_exists IS FALSE THEN
+ --raise NOTICE ' return';
+ -- no: retrieve recommendation
SELECT clin._get_recommendation_for_patient_hint(_hint.recommendation_query, _pk_identity) INTO STRICT _hint_recommendation;
_hint.recommendation := _hint_recommendation;
+ -- return hint
RETURN NEXT _hint;
+ CONTINUE;
END IF;
- -- no: ignore it and process next hint in LOOP
+ -- yes, is suppressed
+ --raise NOTICE ' is suppressed';
+ -- is the suppression still valid ?
+ -- -> yes, suppression valid
+ IF _md5_at_suppression = _hint.md5_sum THEN
+ --raise NOTICE '-> suppression valid, ignoring hint';
+ -- hint applies, suppressed, suppression valid: skip this hint
+ CONTINUE;
+ END IF;
+ -- -> no, suppression not valid
+ -- hint definition must have changed so ignore the suppression but
+ -- provide previous rationale for suppression to the user
+ _hint.rationale4suppression := _old_rationale4suppression;
+ -- retrieve recommendation
+ SELECT clin._get_recommendation_for_patient_hint(_hint.recommendation_query, _pk_identity) INTO STRICT _hint_recommendation;
+ _hint.recommendation := _hint_recommendation;
+ RETURN NEXT _hint;
+ CONTINUE;
END LOOP;
RETURN;
END;</pre>
<hr>
<h2>Function:
+ <a href="gnumed-entire_schema.html#clin.schema">clin</a>.<a name="clin.function.hint-suppression-exists-o-rationale-integer-o-md5-integer">hint_suppression_exists(_o_rationale integer, _o_md5 integer)</a>
+ </h2>
+<h3>Returns: record</h3>
+<h3>Language: PLPGSQL</h3>
+
+ <pre>
+--DECLARE
+-- _md5_suppressed text;
+-- _old_rationale4suppression text;
+BEGIN
+ SELECT
+ md5_sum,
+ rationale
+ INTO
+ _o_md5,
+ _o_rationale
+ FROM clin.suppressed_hint WHERE
+ fk_hint = _pk_hint
+ AND
+ fk_encounter IN (
+ SELECT pk FROM clin.encounter WHERE fk_patient = _pk_identity
+ );
+ IF FOUND THEN
+ _o_exists := TRUE;
+ ELSE
+ _o_exists := FALSE;
+ END IF;
+END;</pre>
+
+ <hr>
+ <h2>Function:
<a href="gnumed-entire_schema.html#clin.schema">clin</a>.<a name="clin.function.move-waiting-list-entry-integer-integer">move_waiting_list_entry(integer, integer)</a>
</h2>
<h3>Returns: boolean</h3>
@@ -105478,6 +105483,43 @@ END;</pre>
<hr>
<h2>Function:
+ <a href="gnumed-entire_schema.html#clin.schema">clin</a>.<a name="clin.function.run-hint-query-o-title-text-o-applies-text">run_hint_query(_o_title text, _o_applies text)</a>
+ </h2>
+<h3>Returns: record</h3>
+<h3>Language: PLPGSQL</h3>
+
+ <pre>
+BEGIN
+ BEGIN
+ EXECUTE _query INTO STRICT _o_applies;
+ EXCEPTION
+ --WHEN insufficient_privilege THEN RAISE WARNING 'auto hint query failed: %', _query;
+ WHEN others THEN
+ RAISE WARNING 'auto hint query failed: %', _query;
+ -- only available starting with PG 9.2:
+ --GET STACKED DIAGNOSTICS
+ -- _exc_state = RETURNED_SQLSTATE,
+ -- _exc_msg = MESSAGE_TEXT,
+ -- _exc_detail = PG_EXCEPTION_DETAIL,
+ -- _exc_hint = PG_EXCEPTION_HINT,
+ -- _exc_context = PG_EXCEPTION_CONTEXT;
+ --RAISE WARNING 'SQL STATE: %', _exc_state;
+ --RAISE WARNING 'MESSAGE: %', _exc_msg;
+ --RAISE WARNING 'DETAIL: %', _exc_detail;
+ --RAISE WARNING 'HINT: %', _exc_hint;
+ --RAISE WARNING 'CONTEXT: %', _exc_context;
+ -- workaround for 9.1:
+ RAISE WARNING 'SQL STATE: %', SQLSTATE;
+ RAISE WARNING 'MESSAGE: %', SQLERRM;
+ _o_applies := NULL;
+ _o_title := ('ERROR checking for [' || _title || '] !')::TEXT;
+ RETURN;
+ END;
+ _o_title := _title;
+END;</pre>
+
+ <hr>
+ <h2>Function:
<a href="gnumed-entire_schema.html#clin.schema">clin</a>.<a name="clin.function.trf-activate-issue-on-opening-episode">trf_activate_issue_on_opening_episode()</a>
</h2>
<h3>Returns: trigger</h3>
@@ -106123,69 +106165,6 @@ end;</pre>
<hr>
<h2>Function:
- <a href="gnumed-entire_schema.html#clin.schema">clin</a>.<a name="clin.function.trf-sane-identity-comment">trf_sane_identity_comment()</a>
- </h2>
-<h3>Returns: trigger</h3>
-<h3>Language: PLPGSQL</h3>
- <p>Ensures unique(identity.dob, names.firstnames, names.lastnames, identity.comment)</p>
- <pre>
-DECLARE
- _identity_row record;
- _names_row record;
-BEGIN
- if TG_TABLE_NAME = 'identity' then
- if TG_OP = 'UPDATE' then
- if NEW.comment IS NOT DISTINCT FROM OLD.comment then
- return NEW;
- end if;
- end if;
- _identity_row := NEW;
- select * into _names_row from dem.names where id_identity = NEW.pk;
- else
- select * into _identity_row from dem.identity where pk = NEW.id_identity;
- _names_row := NEW;
- end if;
- -- any row with
- PERFORM 1 FROM
- dem.v_all_persons
- WHERE
- -- same firstname
- firstnames = _names_row.firstnames
- and
- -- same lastname
- lastnames = _names_row.lastnames
- and
- -- same gender
- gender is not distinct from _identity_row.gender
- and
- -- same dob (day)
- dob_only is not distinct from _identity_row.dob
- and
- -- same discriminator
- comment is not distinct from _identity_row.comment
- and
- -- but not the currently updated or inserted row
- pk_identity != _identity_row.pk
- ;
- if FOUND then
- RAISE EXCEPTION
- '% 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
- USING ERRCODE = 'unique_violation'
- ;
- RETURN NULL;
- end if;
- return NEW;
-END;</pre>
-
- <hr>
- <h2>Function:
<a href="gnumed-entire_schema.html#clin.schema">clin</a>.<a name="clin.function.trf-sanity-check-enc-epi-ins-upd">trf_sanity_check_enc_epi_ins_upd()</a>
</h2>
<h3>Returns: trigger</h3>
@@ -125689,6 +125668,81 @@ BEGIN
return OLD;
END;</pre>
+ <hr>
+ <h2>Function:
+ <a href="gnumed-entire_schema.html#dem.schema">dem</a>.<a name="dem.function.trf-sane-identity-comment">trf_sane_identity_comment()</a>
+ </h2>
+<h3>Returns: trigger</h3>
+<h3>Language: PLPGSQL</h3>
+ <p>Ensures unique(identity.dob, names.firstnames, names.lastnames, identity.comment)</p>
+ <pre>
+DECLARE
+ _identity_row record;
+ _names_row record;
+ _other_identities integer[];
+BEGIN
+ -- working on dem.identity
+ if TG_TABLE_NAME = 'identity' then
+ -- UPDATEs ...
+ if TG_OP = 'UPDATE' then
+ -- ... which do NOT change .comment ...
+ if NEW.comment IS NOT DISTINCT FROM OLD.comment then
+ -- ... are safe because they were successfully INSERTed before
+ return NEW;
+ end if;
+ end if;
+ -- but INSERTs need checking
+ _identity_row := NEW;
+ select * into _names_row 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;
+ 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
+ '% 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;
+ return NEW;
+END;</pre>
+
<!-- gmgm -->
@@ -127353,7 +127407,10 @@ BEGIN
_cmd := 'create constraint trigger zzz_tr_announce_' || _schema_name || '_' || _table_name || '_ins_upd';
_cmd := _cmd || ' after insert or update';
_cmd := _cmd || ' on ' || _qualified_table;
- _cmd := _cmd || ' deferrable';
+ -- needed so a SELECT inside, say, _identity_accessor_SQL running
+ -- concurrently to a "lengthy" TX does not create a serialization
+ -- failure by being a rw-dependancy pivot
+ _cmd := _cmd || ' deferrable initially deferred';
_cmd := _cmd || ' for each row';
_cmd := _cmd || ' execute procedure gm.trf_announce_table_ins_upd(''' || _payload || ''', ''' || _pk_accessor_SQL || ''', ''' || _identity_accessor_SQL || ''');';
execute _cmd;
@@ -127362,7 +127419,10 @@ BEGIN
_cmd := 'create constraint trigger zzz_tr_announce_' || _schema_name || '_' || _table_name || '_del';
_cmd := _cmd || ' after delete';
_cmd := _cmd || ' on ' || _qualified_table;
- _cmd := _cmd || ' deferrable';
+ -- needed so a SELECT inside, say, _identity_accessor_SQL running
+ -- concurrently to a "lengthy" TX does not create a serialization
+ -- failure by being a rw-dependancy pivot
+ _cmd := _cmd || ' deferrable initially deferred';
_cmd := _cmd || ' for each row';
_cmd := _cmd || ' execute procedure gm.trf_announce_table_del(''' || _payload || ''', ''' || _pk_accessor_SQL || ''', ''' || _identity_accessor_SQL || ''');';
execute _cmd;
@@ -141113,6 +141173,469 @@ END;</pre>
<hr>
+ <h2>View:
+
+ <a href="gnumed-entire_schema.html#staging.schema">staging</a>.<a name="staging.view.journal-without-suppressed-hints">journal_without_suppressed_hints</a>
+ </h2>
+
+
+
+ <table width="100%" cellspacing="0" cellpadding="3">
+ <caption>staging.journal_without_suppressed_hints Structure</caption>
+ <tr>
+ <th>F-Key</th>
+ <th>Name</th>
+ <th>Type</th>
+ <th>Description</th>
+ </tr>
+
+ <tr class="tr0">
+ <td>
+
+ </td>
+ <td>pk_patient</td>
+ <td>integer</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr1">
+ <td>
+
+ </td>
+ <td>modified_when</td>
+ <td>timestamp with time zone</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr0">
+ <td>
+
+ </td>
+ <td>clin_when</td>
+ <td>timestamp with time zone</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr1">
+ <td>
+
+ </td>
+ <td>modified_by</td>
+ <td>text</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr0">
+ <td>
+
+ </td>
+ <td>soap_cat</td>
+ <td>text</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr1">
+ <td>
+
+ </td>
+ <td>narrative</td>
+ <td>text</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr0">
+ <td>
+
+ </td>
+ <td>pk_encounter</td>
+ <td>integer</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr1">
+ <td>
+
+ </td>
+ <td>pk_episode</td>
+ <td>integer</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr0">
+ <td>
+
+ </td>
+ <td>pk_health_issue</td>
+ <td>integer</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr1">
+ <td>
+
+ </td>
+ <td>src_pk</td>
+ <td>integer</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr0">
+ <td>
+
+ </td>
+ <td>src_table</td>
+ <td>text</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr1">
+ <td>
+
+ </td>
+ <td>row_version</td>
+ <td>integer</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr0">
+ <td>
+
+ </td>
+ <td>health_issue</td>
+ <td>text</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr1">
+ <td>
+
+ </td>
+ <td>issue_laterality</td>
+ <td>character varying</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr0">
+ <td>
+
+ </td>
+ <td>issue_active</td>
+ <td>boolean</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr1">
+ <td>
+
+ </td>
+ <td>issue_clinically_relevant</td>
+ <td>boolean</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr0">
+ <td>
+
+ </td>
+ <td>issue_confidential</td>
+ <td>boolean</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr1">
+ <td>
+
+ </td>
+ <td>episode</td>
+ <td>text</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr0">
+ <td>
+
+ </td>
+ <td>episode_open</td>
+ <td>boolean</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr1">
+ <td>
+
+ </td>
+ <td>encounter_started</td>
+ <td>timestamp with time zone</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr0">
+ <td>
+
+ </td>
+ <td>encounter_last_affirmed</td>
+ <td>timestamp with time zone</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr1">
+ <td>
+
+ </td>
+ <td>encounter_type</td>
+ <td>text</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ <tr class="tr0">
+ <td>
+
+ </td>
+ <td>encounter_l10n_type</td>
+ <td>text</td>
+ <td><i>
+
+
+
+
+ </i>
+
+ </td>
+ </tr>
+
+ </table>
+
+ <!-- Inherits -->
+
+
+
+
+ <!-- Constraint List -->
+
+
+ <!-- Foreign Key Discovery -->
+
+
+ <!-- Indexes -->
+
+
+ <!-- View Definition -->
+
+ <pre>
+SELECT v_emr_journal.pk_patient
+,
+ v_emr_journal.modified_when
+,
+ v_emr_journal.clin_when
+,
+ v_emr_journal.modified_by
+,
+ v_emr_journal.soap_cat
+,
+ v_emr_journal.narrative
+,
+ v_emr_journal.pk_encounter
+,
+ v_emr_journal.pk_episode
+,
+ v_emr_journal.pk_health_issue
+,
+ v_emr_journal.src_pk
+,
+ v_emr_journal.src_table
+,
+ v_emr_journal.row_version
+,
+ v_emr_journal.health_issue
+,
+ v_emr_journal.issue_laterality
+,
+ v_emr_journal.issue_active
+,
+ v_emr_journal.issue_clinically_relevant
+,
+ v_emr_journal.issue_confidential
+,
+ v_emr_journal.episode
+,
+ v_emr_journal.episode_open
+,
+ v_emr_journal.encounter_started
+,
+ v_emr_journal.encounter_last_affirmed
+,
+ v_emr_journal.encounter_type
+,
+ v_emr_journal.encounter_l10n_type
+
+FROM clin.v_emr_journal
+
+WHERE (v_emr_journal.src_table <> 'clin.suppressed_hint'::text);</pre>
+
+
+ <!-- List off permissions -->
+
+
+ <p>
+ <a href="gnumed-entire_schema.html#index">Index</a> -
+ <a href="gnumed-entire_schema.html#staging.schema">Schema staging</a>
+ </p>
+
+ <hr>
<h2>Table:
<a href="gnumed-entire_schema.html#staging.schema">staging</a>.<a name="staging.table.lab-request">lab_request</a>
diff --git a/client/gm-from-vcs.sh b/client/gm-from-vcs.sh
index a33d8ca..38615fd 100755
--- a/client/gm-from-vcs.sh
+++ b/client/gm-from-vcs.sh
@@ -49,13 +49,14 @@ echo "-------------------------------------------------"
echo "Running from Git branch: "`git branch | grep \*`
echo "-------------------------------------------------"
echo "config file: ${CONF}"
-echo "options: ${DEV_OPTS}"
##python -Q warn -3 gnumed.py ${CONF} ${DEV_OPTS} $@
##python -Q warn -3 gnumed.py ${CONF} ${DEV_OPTS} $@ 2> >(grep -v wx > gm-vcs-py2to3_warnings.log) # 1>&2)
+#echo "options: ${DEV_OPTS}"
#python -Q warn -3 gnumed.py ${CONF} ${DEV_OPTS} $@ |& tee gm-vcs-py2to3_warnings.log
# - *released* tarball version:
+echo "options: ${TARBALL_OPTS}"
python gnumed.py ${CONF} ${TARBALL_OPTS} $@
# - production version (does not use tarball files !):
diff --git a/client/gnumed.py b/client/gnumed.py
index 12b57af..7117b13 100644
--- a/client/gnumed.py
+++ b/client/gnumed.py
@@ -91,7 +91,7 @@ against. Please run GNUmed as a non-root user.
sys.exit(1)
#----------------------------------------------------------
-current_client_version = u'1.6.11'
+current_client_version = u'1.6.13'
current_client_branch = u'1.6'
_log = None
@@ -374,7 +374,8 @@ def setup_cli():
val = False
_cfg.set_option (
option = u'skip-update-check',
- value = val
+ value = True
+# value = val
)
val = _cfg.get(option = '--hipaa', source_order = [('cli', 'return')])
diff --git a/client/pycommon/gmBackendListener.py b/client/pycommon/gmBackendListener.py
index d9345ec..df0eaac 100644
--- a/client/pycommon/gmBackendListener.py
+++ b/client/pycommon/gmBackendListener.py
@@ -238,7 +238,11 @@ class gmBackendListener(gmBorg.cBorg):
if item.startswith(u'row PK='):
pk_row = int(item.split(u'=')[1])
if item.startswith(u'person PK='):
- pk_identity = int(item.split(u'=')[1])
+ try:
+ pk_identity = int(item.split(u'=')[1])
+ except ValueError:
+ _log.exception(u'error in change notification trigger')
+ pk_identity = -1
# try sending intra-client signals:
# 1) generic signal
self.__messages_sent += 1
diff --git a/client/pycommon/gmDateTime.py b/client/pycommon/gmDateTime.py
index 191c3c4..44e0286 100644
--- a/client/pycommon/gmDateTime.py
+++ b/client/pycommon/gmDateTime.py
@@ -540,7 +540,7 @@ def format_interval(interval=None, accuracy_wanted=None, none_string=None, verbo
tag = u' ' + _('second')
else:
tag = u's'
- tmp += u' %s%s' % (int(mins), tag)
+ tmp += u' %s%s' % (int(secs), tag)
if tmp == u'':
if verbose:
@@ -2235,10 +2235,19 @@ if __name__ == '__main__':
]
#-----------------------------------------------------------------------
def test_format_interval():
+ intv = pyDT.timedelta(minutes=1, seconds=2)
+ for acc in _accuracy_strings.keys():
+ print ('[%s]: "%s" -> "%s"' % (acc, intv, format_interval(intv, acc)))
+ return
+
for tmp in intervals_as_str:
intv = str2interval(str_interval = tmp)
+ if intv is None:
+ print(tmp, '->', intv)
+ continue
for acc in _accuracy_strings.keys():
print ('[%s]: "%s" -> "%s"' % (acc, tmp, format_interval(intv, acc)))
+
#-----------------------------------------------------------------------
def test_format_interval_medically():
@@ -2471,11 +2480,11 @@ if __name__ == '__main__':
#test_cFuzzyTimeStamp()
#test_get_pydt()
#test_str2interval()
- #test_format_interval()
+ test_format_interval()
#test_format_interval_medically()
#test_str2pydt()
#test_pydt_strftime()
#test_calculate_apparent_age()
- test_is_leap_year()
+ #test_is_leap_year()
#===========================================================================
diff --git a/client/pycommon/gmDispatcher.py b/client/pycommon/gmDispatcher.py
index f0dd6af..8a32d2b 100644
--- a/client/pycommon/gmDispatcher.py
+++ b/client/pycommon/gmDispatcher.py
@@ -102,7 +102,7 @@ def connect(receiver=None, signal=Any, sender=Any, weak=1):
raise ValueError('gmDispatcher.connect(): must define <receiver>')
if signal not in known_signals:
- _log.error('unknown signal [%(sig)s]', {'sig': signal})
+ _log.warning('unknown signal [%(sig)s]', {'sig': signal})
if signal is not Any:
signal = str(signal)
@@ -141,7 +141,7 @@ def disconnect(receiver, signal=Any, sender=Any, weak=1):
Disconnecting is not required. The use of disconnect is the same as for
connect, only in reverse. Think of it as undoing a previous connection."""
if signal not in known_signals:
- _log.error('unknown signal [%(sig)s]', {'sig': signal})
+ _log.warning('unknown signal [%(sig)s]', {'sig': signal})
if signal is not Any:
signal = str(signal)
@@ -150,13 +150,13 @@ def disconnect(receiver, signal=Any, sender=Any, weak=1):
try:
receivers = connections[senderkey][signal]
except KeyError:
- _log.error('no receivers for signal %(sig)s from sender %(sender)s', {'sig': repr(signal), 'sender': sender})
+ _log.warning('no receivers for signal %(sig)s from sender %(sender)s', {'sig': repr(signal), 'sender': sender})
print('DISPATCHER ERROR: no receivers for signal %s from sender %s' % (repr(signal), sender))
return
try:
receivers.remove(receiver)
except ValueError:
- _log.error('receiver [%(rx)s] not connected to signal [%(sig)s] from [%(sender)s]', {'rx': receiver, 'sig': repr(signal), 'sender': sender})
+ _log.warning('receiver [%(rx)s] not connected to signal [%(sig)s] from [%(sender)s]', {'rx': receiver, 'sig': repr(signal), 'sender': sender})
print("DISPATCHER ERROR: receiver [%s] not connected to signal [%s] from [%s]" % (receiver, repr(signal), sender))
_cleanupConnections(senderkey, signal)
#---------------------------------------------------------------------
diff --git a/client/pycommon/gmLog2.py b/client/pycommon/gmLog2.py
index 0697db6..f804126 100644
--- a/client/pycommon/gmLog2.py
+++ b/client/pycommon/gmLog2.py
@@ -51,6 +51,9 @@ import io
import codecs
import locale
import datetime as pydt
+import random
+import time
+import calendar
_logfile_name = None
@@ -139,6 +142,13 @@ def flush():
# logger.debug(' %s: %s', attr, getattr(v, attr))
#===============================================================
+def log_instance_state(instance):
+ logger = logging.getLogger('gm.logging')
+ logger.debug('state of %s', instance)
+ for attr in [ a for a in dir(instance) if not a.startswith('__') ]:
+ logger.debug(' %s: %s', attr, getattr(instance, attr))
+
+#===============================================================
def log_stack_trace(message=None, t=None, v=None, tb=None):
logger = logging.getLogger('gm.logging')
@@ -238,9 +248,35 @@ def set_string_encoding(encoding=None):
_string_encoding = locale.getpreferredencoding(do_setlocale=False)
logger.info(u'setting python.str -> python.unicode encoding to <%s> (locale.getpreferredencoding)', _string_encoding)
return True
+
#===============================================================
# internal API
#===============================================================
+__words2hide = []
+
+def add_word2hide(word):
+ if word not in __words2hide:
+ __words2hide.append(word)
+
+#---------------------------------------------------------------
+__original_logger_write_func = None
+
+def __safe_logger_write_func(s):
+ for word in __words2hide:
+ # random is seeded from system time at import,
+ # jump ahead, scrambled by whatever is "now"
+ random.jumpahead(calendar.timegm(time.gmtime()))
+ # from that generate a replacement string valid for
+ # *this* round of replacements of *this* word,
+ # this approach won't mitigate guessing trivial passwords
+ # from replacements of known data (a known-plaintext attack)
+ # but will make automated searching for replaced strings
+ # in the log more difficult
+ bummer = hex(random.randint(0, sys.maxint)).lstrip(u'0x')
+ s = s.replace(word, bummer)
+ __original_logger_write_func(s)
+
+#---------------------------------------------------------------
def __setup_logging():
set_string_encoding()
@@ -259,6 +295,10 @@ def __setup_logging():
_logfile = io.open(_logfile_name, mode = 'wt', encoding = 'utf8', errors = 'replace')
+ global __original_logger_write_func
+ __original_logger_write_func = _logfile.write
+ _logfile.write = __safe_logger_write_func
+
logging.basicConfig (
format = fmt,
datefmt = '%Y-%m-%d %H:%M:%S',
@@ -266,12 +306,16 @@ def __setup_logging():
stream = _logfile
)
+ logging.captureWarnings(True)
+
logger = logging.getLogger('gm.logging')
logger.critical(u'-------- start of logging ------------------------------')
logger.info(u'log file is <%s>', _logfile_name)
logger.info(u'log level is [%s]', logging.getLevelName(logger.getEffectiveLevel()))
logger.info(u'log file encoding is <utf8>')
logger.info(u'initial python.str -> python.unicode encoding is <%s>', _string_encoding)
+ logger.debug(u'log file .write() patched from original %s to patched %s', __original_logger_write_func, __safe_logger_write_func)
+
#---------------------------------------------------------------
def __get_logfile_name():
@@ -309,6 +353,7 @@ def __get_logfile_name():
_logfile_name = os.path.join(dir_name, default_logfile_name)
return True
+
#===============================================================
# main
#---------------------------------------------------------------
@@ -316,10 +361,19 @@ __setup_logging()
if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ sys.exit()
+
+ if sys.argv[1] != u'test':
+ sys.exit()
+
#-----------------------------------------------------------
def test():
logger = logging.getLogger('gmLog2.test')
logger.error("I expected to see %s::test()" % __file__)
+ add_word2hide(u'super secret passphrase')
+ logger.debug('credentials: super secret passphrase')
+
try:
int(None)
except:
@@ -327,6 +381,4 @@ if __name__ == '__main__':
log_stack_trace()
flush()
#-----------------------------------------------------------
- if len(sys.argv) > 1 and sys.argv[1] == u'test':
- test()
-#===============================================================
+ test()
diff --git a/client/pycommon/gmPG2.py b/client/pycommon/gmPG2.py
index 38990dc..4eb2bd6 100644
--- a/client/pycommon/gmPG2.py
+++ b/client/pycommon/gmPG2.py
@@ -434,6 +434,7 @@ def __request_login_params_tui():
login.user = prompted_input(prompt = "user name", default = '')
tmp = 'password for "%s" (not shown): ' % login.user
login.password = getpass.getpass(tmp)
+ gmLog2.add_word2hide(login.password)
login.port = prompted_input(prompt = "port", default = 5432)
except KeyboardInterrupt:
_log.warning("user cancelled text mode login dialog")
@@ -465,7 +466,10 @@ def __request_login_params_gui_wx():
if login is None:
raise gmExceptions.ConnectionError(_("Can't connect to database without login information!"))
+ gmLog2.add_word2hide(login.password)
+
return login
+
#---------------------------------------------------
def request_login_params():
"""Request login parameters for database connection."""
@@ -1540,7 +1544,7 @@ Query
conn.status,
conn.isexecuting(),
- cursor.query,
+ unicode(cursor.query, 'utf8', 'replace'),
)
return txt
diff --git a/client/wxpython/gmAuthWidgets.py b/client/wxpython/gmAuthWidgets.py
index e96cefd..40fe57b 100644
--- a/client/wxpython/gmAuthWidgets.py
+++ b/client/wxpython/gmAuthWidgets.py
@@ -29,6 +29,7 @@ from Gnumed.pycommon import gmBackendListener
from Gnumed.pycommon import gmTools
from Gnumed.pycommon import gmCfg2
from Gnumed.pycommon import gmI18N
+from Gnumed.pycommon import gmLog2
from Gnumed.business import gmPraxis
@@ -135,6 +136,8 @@ def connect_to_database(max_attempts=3, expected_version=None, require_version=T
_log.info("user cancelled login dialog")
break
+ gmLog2.add_word2hide(login.password)
+
# try getting a connection to verify the DSN works
dsn = gmPG2.make_psycopg2_dsn (
database = login.database,
@@ -287,6 +290,7 @@ def connect_to_database(max_attempts=3, expected_version=None, require_version=T
dlg.Destroy()
return connected
+
#================================================================
def get_dbowner_connection(procedure=None, dbo_password=None, dbo_account=u'gm-dbo'):
if procedure is None:
@@ -310,6 +314,8 @@ Please enter the current password for <%s>:""") % (
if dbo_password == '':
return None
+ gmLog2.add_word2hide(dbo_password)
+
# 2) connect as gm-dbo
login = gmPG2.get_default_login()
dsn = gmPG2.make_psycopg2_dsn (
@@ -336,6 +342,7 @@ Please enter the current password for <%s>:""") % (
return None
return conn
+
#================================================================
def change_gmdbowner_password():
@@ -364,6 +371,8 @@ def change_gmdbowner_password():
if dbo_pwd_new_1.strip() == u'':
return False
+ gmLog2.add_word2hide(dbo_pwd_new_1)
+
dbo_pwd_new_2 = wx.GetPasswordFromUser (
message = _(u"""Enter the NEW password for the GNUmed database owner, again.
@@ -377,6 +386,49 @@ def change_gmdbowner_password():
if dbo_pwd_new_1 != dbo_pwd_new_2:
return False
+ # pwd2 == pwd1 at this point so no need to hide (again)
+
+ """ On Mon, Mar 13, 2017 at 12:19:22PM -0400, Tom Lane wrote:
+ > Date: Mon, 13 Mar 2017 12:19:22 -0400
+ > From: Tom Lane <tgl at sss.pgh.pa.us>
+ > To: Adrian Klaver <adrian.klaver at aklaver.com>
+ > cc: Schmid Andreas <Andreas.Schmid at bd.so.ch>,
+ > "'pgsql-general at postgresql.org'" <pgsql-general at postgresql.org>
+ > Subject: Re: [GENERAL] createuser: How to specify a database to connect to
+ >
+ > Adrian Klaver <adrian.klaver at aklaver.com> writes:
+ > > On 03/13/2017 08:52 AM, Tom Lane wrote:
+ > >> If by "history" you're worried about the server-side statement log, this
+ > >> is merest fantasy: the createuser program is not magic, it just constructs
+ > >> and sends a CREATE USER command for you. You'd actually be more secure
+ > >> using psql, where (if you're superuser) you could shut off log_statement
+ > >> for your session first.
+ >
+ > > There is a difference though:
+ >
+ > > psql> CREATE USER:
+ >
+ > > postgres-2017-03-13 09:03:27.147 PDT-0LOG: statement: create user
+ > > dummy_user with login password '1234';
+ >
+ > Well, what you're supposed to do is
+ >
+ > postgres=# create user dummy_user;
+ > postgres=# \password dummy_user
+ > Enter new password:
+ > Enter it again:
+ > postgres=#
+ >
+ > which will result in sending something like
+ >
+ > ALTER USER dummy_user PASSWORD 'md5c5e9567bc40082671d02c654260e0e09'
+ >
+ > You can additionally protect that by wrapping it into one transaction
+ > (if you have a setup where the momentary existence of the role without a
+ > password would be problematic) and/or shutting off logging beforehand.
+ """
+
+ # this REALLY should be prefixed with md5 and the md5sum sent rather than the pwd
cmd = u"""ALTER ROLE "%s" ENCRYPTED PASSWORD '%s';""" % (
dbo_account,
dbo_pwd_new_2
@@ -384,9 +436,11 @@ def change_gmdbowner_password():
gmPG2.run_rw_queries(link_obj = dbo_conn, queries = [{'cmd': cmd}], end_tx = True)
return True
+
#================================================================
class cBackendProfile:
pass
+
#================================================================
class cLoginDialog(wx.Dialog):
"""cLoginDialog - window holding cLoginPanel"""
@@ -398,6 +452,7 @@ class cLoginDialog(wx.Dialog):
self.Centre()
self.SetIcon(gmTools.get_icon(wx = wx))
+
#================================================================
class cLoginPanel(wx.Panel):
"""GUI panel class that interactively gets Postgres login parameters.
diff --git a/client/wxpython/gmDemographicsWidgets.py b/client/wxpython/gmDemographicsWidgets.py
index 50310f8..7d00178 100644
--- a/client/wxpython/gmDemographicsWidgets.py
+++ b/client/wxpython/gmDemographicsWidgets.py
@@ -1128,7 +1128,7 @@ class cPersonNameEAPnl(wxgPersonNameEAPnl.wxgPersonNameEAPnl, gmEditArea.cGeneri
data = self.__identity.add_name(first, last, active)
except gmPG2.dbapi.IntegrityError as exc:
_log.exception('cannot save new name')
- exc = make_pg_exception_fields_unicode(exc)
+ exc = gmPG2.make_pg_exception_fields_unicode(exc)
gmGuiHelpers.gm_show_error (
aTitle = _('Adding name'),
aMessage = _(
@@ -1136,7 +1136,6 @@ class cPersonNameEAPnl(wxgPersonNameEAPnl.wxgPersonNameEAPnl, gmEditArea.cGeneri
'\n'
' %s'
) % exc.u_pgerror
-# ) % str(exc)
)
return False
@@ -1176,7 +1175,7 @@ class cPersonNameEAPnl(wxgPersonNameEAPnl.wxgPersonNameEAPnl, gmEditArea.cGeneri
name = self.__identity.add_name(first, last, active)
except gmPG2.dbapi.IntegrityError as exc:
_log.exception('cannot clone name when editing existing name')
- exc = make_pg_exception_fields_unicode(exc)
+ exc = gmPG2.make_pg_exception_fields_unicode(exc)
gmGuiHelpers.gm_show_error (
aTitle = _('Editing name'),
aMessage = _(
@@ -1613,6 +1612,7 @@ class cPersonDemographicsEditorNb(wx.Notebook):
style = wx.NB_TOP | wx.NB_MULTILINE | wx.NO_BORDER,
name = self.__class__.__name__
)
+ _log.debug('created wx.Notebook: %s with ID %s', self.__class__.__name__, self.Id)
self.__identity = None
self.__do_layout()
diff --git a/client/wxpython/gmEMRStructWidgets.py b/client/wxpython/gmEMRStructWidgets.py
index 97ad08e..a423209 100644
--- a/client/wxpython/gmEMRStructWidgets.py
+++ b/client/wxpython/gmEMRStructWidgets.py
@@ -1067,6 +1067,7 @@ def move_episode_to_issue(episode=None, target_issue=None, save_to_backend=False
if save_to_backend:
episode.save_payload()
return True
+
#----------------------------------------------------------------
class cEpisodeListSelectorDlg(gmListWidgets.cGenericListSelectorDlg):
@@ -1302,6 +1303,7 @@ class cEpisodeEditAreaPnl(gmEditArea.cGenericEditAreaMixin, wxgEpisodeEditAreaPn
gmEditArea.cGenericEditAreaMixin.__init__(self)
self.data = episode
+
#----------------------------------------------------------------
# generic Edit Area mixin API
#----------------------------------------------------------------
@@ -1318,6 +1320,7 @@ class cEpisodeEditAreaPnl(gmEditArea.cGenericEditAreaMixin, wxgEpisodeEditAreaPn
self._PRW_description.Refresh()
return not errors
+
#----------------------------------------------------------------
def _save_as_new(self):
@@ -1351,6 +1354,7 @@ class cEpisodeEditAreaPnl(gmEditArea.cGenericEditAreaMixin, wxgEpisodeEditAreaPn
self.data = epi
return True
+
#----------------------------------------------------------------
def _save_as_update(self):
@@ -1380,6 +1384,7 @@ class cEpisodeEditAreaPnl(gmEditArea.cGenericEditAreaMixin, wxgEpisodeEditAreaPn
self.data.generic_codes = [ c['data'] for c in self._PRW_codes.GetData() ]
return True
+
#----------------------------------------------------------------
def _refresh_as_new(self):
if self.data is None:
@@ -1393,15 +1398,24 @@ class cEpisodeEditAreaPnl(gmEditArea.cGenericEditAreaMixin, wxgEpisodeEditAreaPn
self._PRW_certainty.SetText()
self._CHBOX_closed.SetValue(False)
self._PRW_codes.SetText()
+
+ self._PRW_issue.SetFocus()
+
#----------------------------------------------------------------
def _refresh_from_existing(self):
ident = gmPerson.cPerson(aPK_obj = self.data['pk_patient'])
self._TCTRL_patient.SetValue(ident.get_description_gender())
if self.data['pk_health_issue'] is not None:
- self._PRW_issue.SetText(self.data['health_issue'], data=self.data['pk_health_issue'])
+ self._PRW_issue.SetText (
+ self.data['health_issue'],
+ data = self.data['pk_health_issue']
+ )
- self._PRW_description.SetText(self.data['description'], data=self.data['description'])
+ self._PRW_description.SetText (
+ self.data['description'],
+ data = self.data['description']
+ )
self._TCTRL_status.SetValue(gmTools.coalesce(self.data['summary'], u''))
@@ -1412,6 +1426,12 @@ class cEpisodeEditAreaPnl(gmEditArea.cGenericEditAreaMixin, wxgEpisodeEditAreaPn
val, data = self._PRW_codes.generic_linked_codes2item_dict(self.data.generic_codes)
self._PRW_codes.SetText(val, data)
+
+ if self.data['pk_health_issue'] is None:
+ self._PRW_issue.SetFocus()
+ else:
+ self._PRW_description.SetFocus()
+
#----------------------------------------------------------------
def _refresh_as_new_from_existing(self):
self._refresh_as_new()
diff --git a/client/wxpython/gmEditArea.py b/client/wxpython/gmEditArea.py
index 04f9d2b..08801b6 100644
--- a/client/wxpython/gmEditArea.py
+++ b/client/wxpython/gmEditArea.py
@@ -159,6 +159,7 @@ class cXxxEAPnl(wxgXxxEAPnl.wxgXxxEAPnl, gmEditArea.cGenericEditAreaMixin):
self.refresh()
mode = property(_get_mode, _set_mode)
+
#----------------------------------------------------------------
def _get_data(self):
return self.__data
@@ -171,11 +172,13 @@ class cXxxEAPnl(wxgXxxEAPnl.wxgXxxEAPnl, gmEditArea.cGenericEditAreaMixin):
self.refresh()
data = property(_get_data, _set_data)
+
#----------------------------------------------------------------
def show_msg(self, msg):
gmDispatcher.send(signal = 'statustext', msg = msg)
status_message = property(lambda x:x, show_msg)
+
#----------------------------------------------------------------
# generic edit area dialog API
#----------------------------------------------------------------
diff --git a/client/wxpython/gmExportAreaWidgets.py b/client/wxpython/gmExportAreaWidgets.py
index f48076b..1870f4e 100644
--- a/client/wxpython/gmExportAreaWidgets.py
+++ b/client/wxpython/gmExportAreaWidgets.py
@@ -63,15 +63,20 @@ class cExportAreaPluginPnl(wxgExportAreaPluginPnl.wxgExportAreaPluginPnl, gmRege
def _on_table_mod(self, *args, **kwargs):
if kwargs['table'] != 'clin.export_item':
return
- pat = gmPerson.gmCurrentPatient()
- if not pat.connected:
- return
- if kwargs['pk_identity'] != pat.ID:
- return
+ # work around problem in v21 change notification trigger on clin.export_item
+ # properly fixed in v22
+ if kwargs['pk_identity'] != -1:
+ pat = gmPerson.gmCurrentPatient()
+ if not pat.connected:
+ return
+ if kwargs['pk_identity'] != pat.ID:
+ return
self._schedule_data_reget()
+
#--------------------------------------------------------
def _on_list_item_selected(self, event):
event.Skip()
+
#--------------------------------------------------------
def _on_show_item_button_pressed(self, event):
event.Skip()
@@ -79,6 +84,7 @@ class cExportAreaPluginPnl(wxgExportAreaPluginPnl.wxgExportAreaPluginPnl, gmRege
if item is None:
return
item.display_via_mime(block = False)
+
#--------------------------------------------------------
def _on_add_items_button_pressed(self, event):
event.Skip()
diff --git a/client/wxpython/gmGuiMain.py b/client/wxpython/gmGuiMain.py
index 1cfdec9..4ba935b 100644
--- a/client/wxpython/gmGuiMain.py
+++ b/client/wxpython/gmGuiMain.py
@@ -723,9 +723,12 @@ class gmTopLevelFrame(wx.Frame):
item = menu_emr_edit.Append(-1, _('&Measurements'), _('Manage measurement results for the current patient.'))
self.Bind(wx.EVT_MENU, self.__on_manage_measurements, item)
- item = menu_emr_edit.Append(-1, _('&Vaccinations'), _('Manage vaccinations for the current patient.'))
+ item = menu_emr_edit.Append(-1, _('&Vaccination history'), _('Manage vaccinations for the current patient.'))
self.Bind(wx.EVT_MENU, self.__on_add_vaccination, item)
+ item = menu_emr_edit.Append(-1, _('&Vaccinations (latest)'), _('List latest vaccinations for the current patient.'))
+ self.Bind(wx.EVT_MENU, self.__on_show_latest_vaccinations, item)
+
item = menu_emr_edit.Append(-1, _('&Family history (FHx)'), _('Manage family history.'))
self.Bind(wx.EVT_MENU, self.__on_manage_fhx, item)
@@ -2798,6 +2801,7 @@ class gmTopLevelFrame(wx.Frame):
return False
gmDemographicsWidgets.edit_occupation()
evt.Skip()
+
#----------------------------------------------
@gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage vaccinations'))
def __on_add_vaccination(self, evt):
@@ -2806,8 +2810,20 @@ class gmTopLevelFrame(wx.Frame):
gmDispatcher.send(signal = 'statustext', msg = _('Cannot add vaccinations. No active patient.'))
return False
- gmVaccWidgets.manage_vaccinations(parent = self)
+ gmVaccWidgets.manage_vaccinations(parent = self, latest_only = False)
evt.Skip()
+
+ #----------------------------------------------
+ @gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage vaccinations'))
+ def __on_show_latest_vaccinations(self, evt):
+ pat = gmPerson.gmCurrentPatient()
+ if not pat.connected:
+ gmDispatcher.send(signal = 'statustext', msg = _('Cannot manage vaccinations. No active patient.'))
+ return False
+
+ gmVaccWidgets.manage_vaccinations(parent = self, latest_only = True)
+ evt.Skip()
+
#----------------------------------------------
@gmAccessPermissionWidgets.verify_minimum_required_role('full clinical access', activity = _('manage family history'))
def __on_manage_fhx(self, evt):
diff --git a/client/wxpython/gmHorstSpace.py b/client/wxpython/gmHorstSpace.py
index 089e147..f1c2e36 100644
--- a/client/wxpython/gmHorstSpace.py
+++ b/client/wxpython/gmHorstSpace.py
@@ -18,7 +18,7 @@ import os.path, os, sys, logging
import wx
-from Gnumed.pycommon import gmGuiBroker, gmI18N, gmDispatcher, gmCfg
+from Gnumed.pycommon import gmGuiBroker, gmI18N, gmDispatcher, gmCfg, gmLog2
from Gnumed.wxpython import gmPlugin, gmTopPanel, gmGuiHelpers
from Gnumed.business import gmPerson, gmPraxis
@@ -51,6 +51,7 @@ class cHorstSpaceLayoutMgr(wx.Panel):
size = wx.Size(320,240),
style = wx.NB_BOTTOM
)
+ _log.debug('created wx.Notebook: %s with ID %s', self.__class__.__name__, self.nb.Id)
# plugins
self.__gb = gmGuiBroker.GuiBroker()
self.__gb['horstspace.notebook'] = self.nb # FIXME: remove per Ian's API suggestion
@@ -78,10 +79,15 @@ class cHorstSpaceLayoutMgr(wx.Panel):
# internal API
#----------------------------------------------
def __register_events(self):
+ # because of
+ # https://www.wiki.wxpython.org/self.Bind%20vs.%20self.button.Bind
+ # do self.Bind() rather than self.nb.Bind()
# - notebook page is about to change
- self.nb.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGING, self._on_notebook_page_changing)
+ #self.nb.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGING, self._on_notebook_page_changing)
+ self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGING, self._on_notebook_page_changing, self.nb)
# - notebook page has been changed
- self.nb.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self._on_notebook_page_changed)
+ #self.nb.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self._on_notebook_page_changed)
+ self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self._on_notebook_page_changed, self.nb)
# - popup menu on right click in notebook
wx.EVT_RIGHT_UP(self.nb, self._on_right_click)
@@ -149,24 +155,31 @@ class cHorstSpaceLayoutMgr(wx.Panel):
default = u'gmPatientOverviewPlugin'
)
gmDispatcher.send(signal = 'display_widget', name = default_plugin)
+
#----------------------------------------------
def _on_notebook_page_changing(self, event):
"""Called before notebook page change is processed."""
_log.debug('just before switching notebook tabs')
- self.__new_page_already_checked = False
+ _log.debug('id: %s', event.Id)
+ _log.debug('event object (= source notebook): %s = %s', event.EventObject.Id, event.EventObject)
+ _log.debug('this notebook (= event receiver): %s = %s', self.nb.Id, self.nb)
+ if event.EventObject.Id != self.nb.Id:
+ _log.error('this event came from another notebook')
+
+ self.__target_page_already_checked = False
self.__id_nb_page_before_switch = self.nb.GetSelection()
self.__id_evt_page_before_switch = event.GetOldSelection()
__id_evt_page_after_switch = event.GetSelection()
- _log.debug('event.GetOldSelection()=%s* -> event.GetSelection()=%s', self.__id_evt_page_before_switch, __id_evt_page_after_switch)
-
+ _log.debug(u'source/target page state in EVT_NOTEBOOK_PAGE_CHANGING:')
+ _log.debug(u' #1 - notebook current page: %s (= notebook.GetSelection())', self.__id_nb_page_before_switch)
+ _log.debug(u' #2 - event source page: %s (= page event says it is coming from, event.GetOldSelection())', self.__id_evt_page_before_switch)
+ _log.debug(u' #3 - event target page: %s (= page event wants to go to, event.GetSelection())', __id_evt_page_after_switch)
if self.__id_evt_page_before_switch != self.__id_nb_page_before_switch:
- _log.debug('the following two should match but do not:')
- _log.debug(' event.GetOldSelection(): %s', self.__id_evt_page_before_switch)
- _log.debug(' notebook.GetSelection(): %s', self.__id_nb_page_before_switch)
+ _log.warning(' problem: #1 and #2 really should match but do not')
# can we check the target page ?
if __id_evt_page_after_switch == self.__id_evt_page_before_switch:
@@ -179,7 +192,7 @@ class cHorstSpaceLayoutMgr(wx.Panel):
_log.debug('current notebook page : %s', self.__id_nb_page_before_switch)
_log.debug('source page from event: %s', self.__id_evt_page_before_switch)
_log.debug('target page from event: %s', __id_evt_page_after_switch)
- _log.info('cannot check whether notebook page change needs to be vetoed')
+ _log.warning('cannot check whether notebook page change needs to be vetoed')
# but let's do a basic check anyways
pat = gmPerson.gmCurrentPatient()
if not pat.connected:
@@ -192,61 +205,76 @@ class cHorstSpaceLayoutMgr(wx.Panel):
return
# check target page
- new_page = self.__gb['horstspace.notebook.pages'][__id_evt_page_after_switch]
- if not new_page.can_receive_focus():
- _log.debug('veto()ing page change')
+ target_page = self.__gb['horstspace.notebook.pages'][__id_evt_page_after_switch]
+ _log.debug('checking event target page for focussability: %s', target_page)
+ if not target_page.can_receive_focus():
+ _log.warning('veto()ing page change')
event.Veto()
return
# everything seems fine so switch
- self.__new_page_already_checked = True
+ _log.debug('event target page seems focussable')
+ self.__target_page_already_checked = True
event.Allow() # redundant ?
event.Skip()
return
+
#----------------------------------------------
def _on_notebook_page_changed(self, event):
"""Called when notebook page changes."""
_log.debug('just after switching notebook tabs')
+
+ _log.debug('id: %s', event.Id)
+ _log.debug('event object (= source notebook): %s = %s', event.EventObject.Id, event.EventObject)
+ _log.debug('this notebook (= event receiver): %s = %s', self.nb.Id, self.nb)
+ if event.EventObject.Id != self.nb.Id:
+ _log.error('this event came from another notebook')
+
event.Skip()
+ id_nb_page_after_switch = self.nb.GetSelection()
id_evt_page_before_switch = event.GetOldSelection()
id_evt_page_after_switch = event.GetSelection()
- id_nb_page_after_switch = self.nb.GetSelection()
- _log.debug('event.GetOldSelection()=%s -> event.GetSelection()=%s*', id_evt_page_before_switch, id_evt_page_after_switch)
+ _log.debug(u'source/target page state in EVT_NOTEBOOK_PAGE_CHANGED:')
+ _log.debug(u' #1 - current notebook page: %s (notebook.GetSelection())', id_nb_page_after_switch)
+ _log.debug(u' #2 - event source page: %s (= page event says it is coming from, event.GetOldSelection())', id_evt_page_before_switch)
+ _log.debug(u' #3 - event target page: %s (= page event wants to go to, event.GetSelection())', id_evt_page_after_switch)
if self.__id_nb_page_before_switch != id_evt_page_before_switch:
- _log.debug('those two really *should* match:')
- _log.debug(' wx.Notebook.GetSelection() (before switch) : %s' % self.__id_nb_page_before_switch)
- _log.debug(' EVT_NOTEBOOK_PAGE_CHANGED.GetOldSelection(): %s' % id_evt_page_before_switch)
+ _log.warning('those two really *should* match:')
+ _log.warning(' wx.Notebook.GetSelection(): %s (notebook current page before switch) ', self.__id_nb_page_before_switch)
+ _log.warning(' EVT_NOTEBOOK_PAGE_CHANGED.GetOldSelection(): %s (event source page)' % id_evt_page_before_switch)
- new_page = self.__gb['horstspace.notebook.pages'][id_evt_page_after_switch]
+ target_page = self.__gb['horstspace.notebook.pages'][id_evt_page_after_switch]
# well-behaving wxPython port ?
- if self.__new_page_already_checked:
- new_page.receive_focus()
+ if self.__target_page_already_checked:
+ _log.debug('target page (evt=%s, nb=%s) claims to have been checked for focussability already: %s', id_evt_page_after_switch, id_nb_page_after_switch, target_page)
+ target_page.receive_focus()
# activate toolbar of new page
-# self.__gb['horstspace.top_panel'].ShowBar(new_page.__class__.__name__)
- self.__new_page_already_checked = False
+ #self.__gb['horstspace.top_panel'].ShowBar(target_page.__class__.__name__)
+ self.__target_page_already_checked = False
return
# no, complain
- _log.debug('target page not checked for focussability yet')
+ _log.debug('target page not checked for focussability yet: %s', target_page)
_log.debug('EVT_NOTEBOOK_PAGE_CHANGED.GetOldSelection(): %s' % id_evt_page_before_switch)
_log.debug('EVT_NOTEBOOK_PAGE_CHANGED.GetSelection() : %s' % id_evt_page_after_switch)
_log.debug('wx.Notebook.GetSelection() (after switch) : %s' % id_nb_page_after_switch)
# check the new page just for good measure
- if new_page.can_receive_focus():
- _log.debug('we are lucky: new page *can* receive focus')
- new_page.receive_focus()
+ if target_page.can_receive_focus():
+ _log.debug('we are lucky: target page *can* receive focus anyway')
+ target_page.receive_focus()
# activate toolbar of new page
-# self.__gb['horstspace.top_panel'].ShowBar(new_page.__class__.__name__)
+# self.__gb['horstspace.top_panel'].ShowBar(target_page.__class__.__name__)
return
- _log.warning('new page cannot receive focus but too late for veto')
+ _log.error('target page cannot receive focus but too late for veto')
return
+
#----------------------------------------------
def _on_right_click(self, evt):
evt.Skip()
diff --git a/client/wxpython/gmMeasurementWidgets.py b/client/wxpython/gmMeasurementWidgets.py
index 2edbf03..cb861c8 100644
--- a/client/wxpython/gmMeasurementWidgets.py
+++ b/client/wxpython/gmMeasurementWidgets.py
@@ -1192,6 +1192,7 @@ class cMeasurementsNb(wx.Notebook, gmPlugin.cPatientChange_PluginMixin):
style = wx.NB_TOP | wx.NB_MULTILINE | wx.NO_BORDER,
name = self.__class__.__name__
)
+ _log.debug('created wx.Notebook: %s with ID %s', self.__class__.__name__, self.Id)
gmPlugin.cPatientChange_PluginMixin.__init__(self)
self.__patient = gmPerson.gmCurrentPatient()
self.__init_ui()
@@ -1293,7 +1294,6 @@ class cMeasurementsGrid(wx.grid.Grid):
- thereby it can display any patient at any time
"""
# FIXME: sort-by-battery
- # FIXME: filter-by-battery
# FIXME: filter out empty
# FIXME: filter by tests of a selected date
# FIXME: dates DESC/ASC by cfg
@@ -1828,8 +1828,8 @@ class cMeasurementsGrid(wx.grid.Grid):
try:
self.__cell_data[col][row]
except KeyError:
- # FIXME: invoke editor for adding value for day of that column
- # FIMXE: and test of that row
+ # FIXME: preset date/test type from cell location, preset episode/med context from other tests on this date
+ edit_measurement(parent = self, measurement = None, single_entry = True)
return
if len(self.__cell_data[col][row]) > 1:
diff --git a/client/wxpython/gmMedicationWidgets.py b/client/wxpython/gmMedicationWidgets.py
index 68ca1d1..ba58577 100644
--- a/client/wxpython/gmMedicationWidgets.py
+++ b/client/wxpython/gmMedicationWidgets.py
@@ -335,6 +335,7 @@ class cSubstanceIntakeEAPnl(wxgCurrentMedicationEAPnl.wxgCurrentMedicationEAPnl,
self.mode = 'edit'
self.__init_ui()
+
#----------------------------------------------------------------
def __init_ui(self):
@@ -493,8 +494,8 @@ class cSubstanceIntakeEAPnl(wxgCurrentMedicationEAPnl.wxgCurrentMedicationEAPnl,
self._PRW_drug.display_as_valid(True)
- # we aren't editing
- if self.mode != 'new':
+ # if we are editing the drug SHOULD exist so don't error
+ if self.mode == 'edit':
return True
selected_drug = self._PRW_drug.GetData(as_instance = True)
diff --git a/client/wxpython/gmNarrativeWidgets.py b/client/wxpython/gmNarrativeWidgets.py
index 77b4f6d..56e01cd 100644
--- a/client/wxpython/gmNarrativeWidgets.py
+++ b/client/wxpython/gmNarrativeWidgets.py
@@ -778,6 +778,8 @@ class cSoapNoteInputNotebook(wx.Notebook):
kwargs['style'] = wx.NB_TOP | wx.NB_MULTILINE | wx.NO_BORDER
wx.Notebook.__init__(self, *args, **kwargs)
+
+ _log.debug('created wx.Notebook: %s with ID %s', self.__class__.__name__, self.Id)
#--------------------------------------------------------
# public API
#--------------------------------------------------------
diff --git a/client/wxpython/gmProviderInboxWidgets.py b/client/wxpython/gmProviderInboxWidgets.py
index 093bac0..f766e5e 100644
--- a/client/wxpython/gmProviderInboxWidgets.py
+++ b/client/wxpython/gmProviderInboxWidgets.py
@@ -765,7 +765,7 @@ class cProviderInboxPnl(wxgProviderInboxPnl.wxgProviderInboxPnl, gmRegetMixin.cR
epi = emr.add_episode(episode_name = 'administrative', is_open = False)
soap_cat = gmTools.bool2subst (
(self.__focussed_msg['category'] == u'clinical'),
- u'U',
+ u'u',
None
)
narr = _('Deleted inbox message:\n%s') % self.__focussed_msg.format(with_patient = False)
diff --git a/client/wxpython/gmSOAPWidgets.py b/client/wxpython/gmSOAPWidgets.py
index ebcee91..1d6a1d2 100644
--- a/client/wxpython/gmSOAPWidgets.py
+++ b/client/wxpython/gmSOAPWidgets.py
@@ -120,6 +120,7 @@ class cProgressNoteInputNotebook(wx.Notebook, gmRegetMixin.cRegetOnPaintMixin):
style = wx.NB_TOP | wx.NB_MULTILINE | wx.NO_BORDER,
name = self.__class__.__name__
)
+ _log.debug('created wx.Notebook: %s with ID %s', self.__class__.__name__, self.Id)
gmRegetMixin.cRegetOnPaintMixin.__init__(self)
self.__pat = gmPerson.gmCurrentPatient()
self.__do_layout()
diff --git a/client/wxpython/gmSubstanceMgmtWidgets.py b/client/wxpython/gmSubstanceMgmtWidgets.py
index fe09265..90348b2 100644
--- a/client/wxpython/gmSubstanceMgmtWidgets.py
+++ b/client/wxpython/gmSubstanceMgmtWidgets.py
@@ -992,19 +992,20 @@ class cBrandedDrugEAPnl(wxgBrandedDrugEAPnl.wxgBrandedDrugEAPnl, gmEditArea.cGen
# dupe ?
drug = gmMedication.get_drug_by_brand(brand_name = brand_name, preparation = preparation)
if drug is not None:
- validity = False
- self._PRW_brand.display_as_valid(False)
- self._PRW_preparation.display_as_valid(False)
- gmGuiHelpers.gm_show_error (
- title = _('Checking brand data'),
- error = _(
- 'The brand information you entered:\n'
- '\n'
- ' [%s %s]\n'
- '\n'
- 'already exists as a drug product.'
- ) % (brand_name, preparation)
- )
+ if self.mode != 'edit':
+ validity = False
+ self._PRW_brand.display_as_valid(False)
+ self._PRW_preparation.display_as_valid(False)
+ gmGuiHelpers.gm_show_error (
+ title = _('Checking brand data'),
+ error = _(
+ 'The brand information you entered:\n'
+ '\n'
+ ' [%s %s]\n'
+ '\n'
+ 'already exists as a drug product.'
+ ) % (brand_name, preparation)
+ )
else:
# lacking components ?
diff --git a/client/wxpython/gmTextCtrl.py b/client/wxpython/gmTextCtrl.py
index de9c2e2..e9d550e 100644
--- a/client/wxpython/gmTextCtrl.py
+++ b/client/wxpython/gmTextCtrl.py
@@ -98,20 +98,34 @@ class cColoredStatus_TextCtrlMixin():
self.Refresh()
#============================================================
+_KNOWN_UNICODE_SELECTORS = [
+ 'kcharselect', # KDE
+ 'gucharmap', # GNOME
+ 'BabelMap.exe', # Windows, supposed to be better than charmap.exe
+ 'charmap.exe', # Microsoft utility
+ 'gm-unicode2clipboard' # generic GNUmed workaround
+ # Mac OSX supposedly features built-in support
+]
+
class cUnicodeInsertion_TextCtrlMixin():
"""Mixin for inserting unicode characters via selection tool."""
+
+ _unicode_selector = None
+
def __init__(self, *args, **kwargs):
if not isinstance(self, (wx.TextCtrl, wx.stc.StyledTextCtrl)):
raise TypeError('[%s]: can only be applied to wx.TextCtrl or wx.stc.StyledTextCtrl, not [%s]' % (cUnicodeInsertion_TextCtrlMixin, self.__class__.__name__))
- found, self.__unicode_selector = gmShellAPI.find_first_binary(binaries = ['kcharselect', 'gucharmap', 'charmap.exe', 'gm-unicode2clipboard'])
- if not found:
- _log.error('no unicode character selection tool found')
- return
+ if cUnicodeInsertion_TextCtrlMixin._unicode_selector is None:
+ found, cUnicodeInsertion_TextCtrlMixin._unicode_selector = gmShellAPI.find_first_binary(binaries = _KNOWN_UNICODE_SELECTORS)
+ if found:
+ _log.debug('found [%s] for unicode character selection', cUnicodeInsertion_TextCtrlMixin._unicode_selector)
+ else:
+ _log.error('no unicode character selection tool found')
#--------------------------------------------------------
def mixin_insert_unicode_character(self):
- if self.__unicode_selector is None:
+ if cUnicodeInsertion_TextCtrlMixin._unicode_selector is None:
return False
# read clipboard
@@ -128,7 +142,7 @@ class cUnicodeInsertion_TextCtrlMixin():
prev_clip = data_obj.Text
# run selector
- if not gmShellAPI.run_command_in_shell(command = self.__unicode_selector, blocking = True):
+ if not gmShellAPI.run_command_in_shell(command = cUnicodeInsertion_TextCtrlMixin._unicode_selector, blocking = True):
wx.TheClipboard.Close()
return False
diff --git a/client/wxpython/gmVaccWidgets.py b/client/wxpython/gmVaccWidgets.py
index c2984fd..beb3e59 100644
--- a/client/wxpython/gmVaccWidgets.py
+++ b/client/wxpython/gmVaccWidgets.py
@@ -611,7 +611,7 @@ def edit_vaccination(parent=None, vaccination=None, single_entry=True):
return False
#----------------------------------------------------------------------
-def manage_vaccinations(parent=None):
+def manage_vaccinations(parent=None, latest_only=False):
pat = gmPerson.gmCurrentPatient()
emr = pat.get_emr()
@@ -665,6 +665,7 @@ def manage_vaccinations(parent=None):
)
return False
+
#------------------------------------------------------------
def get_tooltip(vaccination):
if vaccination is None:
@@ -675,36 +676,70 @@ def manage_vaccinations(parent=None):
with_reaction = True,
date_format = '%Y %b %d'
))
+
#------------------------------------------------------------
def edit(vaccination=None):
return edit_vaccination(parent = parent, vaccination = vaccination, single_entry = (vaccination is not None))
+
#------------------------------------------------------------
def delete(vaccination=None):
gmVaccination.delete_vaccination(vaccination = vaccination['pk_vaccination'])
return True
+
#------------------------------------------------------------
def refresh(lctrl):
- vaccs = emr.get_vaccinations(order_by = 'date_given DESC, pk_vaccination')
-
- items = [ [
- gmDateTime.pydt_strftime(v['date_given'], '%Y %b %d'),
- v['vaccine'],
- u', '.join(v['l10n_indications']),
- v['batch_no'],
- gmTools.coalesce(v['site'], u''),
- gmTools.coalesce(v['reaction'], u''),
- gmTools.coalesce(v['comment'], u'')
- ] for v in vaccs ]
+ if latest_only:
+ items = []
+ vaccs = []
+ latest_vaccs = emr.get_latest_vaccinations()
+ for indication in sorted(latest_vaccs.keys()):
+ no_shots4ind, latest_vacc4ind = latest_vaccs[indication]
+ #for indication, no_shots_and_latest_shot in latest_vaccs.items():
+ #no_shots4ind, latest_vacc4ind = no_shots_and_latest_shot
+ items.append ([
+ indication,
+ _(u'%s (latest of %s: %s ago)') % (
+ gmDateTime.pydt_strftime(latest_vacc4ind['date_given'], format = '%Y %b'),
+ no_shots4ind,
+ gmDateTime.format_interval_medically(gmDateTime.pydt_now_here() - latest_vacc4ind['date_given'])
+ ),
+ latest_vacc4ind['vaccine'],
+ latest_vacc4ind['batch_no'],
+ gmTools.coalesce(latest_vacc4ind['site'], u''),
+ gmTools.coalesce(latest_vacc4ind['reaction'], u''),
+ gmTools.coalesce(latest_vacc4ind['comment'], u'')
+ ])
+ vaccs.append(latest_vacc4ind)
+ else:
+ vaccs = emr.get_vaccinations(order_by = 'date_given DESC, pk_vaccination')
+ items = [ [
+ gmDateTime.pydt_strftime(v['date_given'], '%Y %b %d'),
+ v['vaccine'],
+ u', '.join(v['l10n_indications']),
+ v['batch_no'],
+ gmTools.coalesce(v['site'], u''),
+ gmTools.coalesce(v['reaction'], u''),
+ gmTools.coalesce(v['comment'], u'')
+ ] for v in vaccs ]
lctrl.set_string_items(items)
lctrl.set_data(vaccs)
+
#------------------------------------------------------------
gmListWidgets.get_choices_from_list (
parent = parent,
- msg = _('\nComplete vaccination history for this patient.\n'),
+ msg = gmTools.bool2subst (
+ latest_only,
+ _(u'Most recent vaccination for each indication.\n'),
+ _(u'Complete vaccination history.\n')
+ ),
caption = _('Showing vaccinations.'),
- columns = [ _('Date'), _('Vaccine'), _(u'Intended to protect from'), _('Batch'), _('Site'), _('Reaction'), _('Comment') ],
+ columns = gmTools.bool2subst (
+ latest_only,
+ [ _('Indication'), _('Date'), _('Vaccine'), _('Batch'), _('Site'), _('Reaction'), _('Comment') ],
+ [ _('Date'), _('Vaccine'), _(u'Intended to protect from'), _('Batch'), _('Site'), _('Reaction'), _('Comment') ]
+ ),
single_selection = True,
refresh_callback = refresh,
new_callback = edit,
@@ -715,6 +750,7 @@ def manage_vaccinations(parent=None):
middle_extra_button = (_('Recall'), _('Add a recall for a vaccination'), add_recall),
right_extra_button = (_('Vx schedules'), _('Open a browser showing vaccination schedules.'), browse2schedules)
)
+
#----------------------------------------------------------------------
from Gnumed.wxGladeWidgets import wxgVaccinationEAPnl
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-med/gnumed-client.git
More information about the debian-med-commit
mailing list