[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