[med-svn] [gnumed-client] 01/10: Imported Upstream version 1.5.8+dfsg

Andreas Tille tille at debian.org
Wed Dec 16 15:56:19 UTC 2015


This is an automated email from the git hooks/post-receive script.

tille pushed a commit to branch master
in repository gnumed-client.

commit d28963f95234074971ab819ad8ace2c7e0db062f
Author: Andreas Tille <tille at debian.org>
Date:   Tue Nov 10 08:34:38 2015 +0100

    Imported Upstream version 1.5.8+dfsg
---
 client/CHANGELOG                            |  20 ++
 client/business/gmClinicalRecord.py         |  96 +++++-
 client/business/gmEMRStructItems.py         |   4 +-
 client/business/gmPerson.py                 |  97 ++++--
 client/business/gmStaff.py                  |   9 +-
 client/doc/schema/gnumed-entire_schema.html |   2 +-
 client/gm-from-vcs.bat                      |  21 +-
 client/gnumed.py                            |  66 ++++-
 client/pycommon/gmBackendListener.py        |   4 +-
 client/pycommon/gmBorg.py                   |  43 +--
 client/pycommon/gmBusinessDBObject.py       |  27 +-
 client/pycommon/gmNull.py                   | 108 +++----
 client/pycommon/gmPG2.py                    |   7 +-
 client/pycommon/gmTools.py                  | 116 +++++++-
 client/wxpython/gmGuiMain.py                |  63 +++-
 client/wxpython/gmListWidgets.py            |   4 +-
 client/wxpython/gmNarrativeWidgets.py       |   9 +-
 client/wxpython/gmPatOverviewWidgets.py     |  66 +++--
 client/wxpython/gmPersonCreationWidgets.py  |   7 +-
 client/wxpython/gmPregWidgets.py            |   2 +-
 client/wxpython/gmSubstanceMgmtWidgets.py   | 442 ----------------------------
 client/wxpython/gmTopPanel.py               |  60 +++-
 22 files changed, 586 insertions(+), 687 deletions(-)

diff --git a/client/CHANGELOG b/client/CHANGELOG
index 3f03667..afa8beb 100644
--- a/client/CHANGELOG
+++ b/client/CHANGELOG
@@ -6,6 +6,26 @@
 # rel-1-5-patches
 ------------------------------------------------
 
+	1.5.8
+
+FIX: SQL formatting when retrieving clinical narrative [thanks Marc]
+FIX: strange case of "curr_pat is None" in top panel [thanks Marc]
+
+	1.5.7
+
+FIX: one more nonissue-problem tooltip exception in SOAP editor [thanks Marc]
+FIX: encounter change exception on patient change w/ multiple clients [thanks Marc]
+FIX: patient overview tooltip exception on patient change [thanks Marc]
+FIX: mysterious non-problem with missing "Gnumed." in import [thanks Basti]
+FIX: symlink creation on Windows
+
+IMPROVED: logging of payload changes in case of conflict
+IMPROVED: early startup logging
+IMPROVED: show low file location during startup
+IMPROVED: windows startup batch file
+IMPROVED: redirect wxPython log to python logging
+IMPROVED: set wxPython AssertMode appropriately
+
 	1.5.6
 
 FIX: exception on removing temporary config file [thanks Vaibhav]
diff --git a/client/business/gmClinicalRecord.py b/client/business/gmClinicalRecord.py
index 97d5b68..013ffc5 100644
--- a/client/business/gmClinicalRecord.py
+++ b/client/business/gmClinicalRecord.py
@@ -138,10 +138,70 @@ class cClinicalRecord(object):
 	# messaging
 	#--------------------------------------------------------
 	def _register_interests(self):
-		gmDispatcher.connect(signal = u'clin.encounter_mod_db', receiver = self.db_callback_encounter_mod_db)
+		#gmDispatcher.connect(signal = u'clin.encounter_mod_db', receiver = self.db_callback_encounter_mod_db)
+		gmDispatcher.connect(signal = u'gm_table_mod', receiver = self.db_modification_callback)
 
 		return True
 	#--------------------------------------------------------
+	def db_modification_callback(self, **kwds):
+
+		if kwds['table'] != u'clin.encounter':
+			return True
+		if self.current_encounter is None:
+			_log.debug('no local current-encounter, ignoring encounter modification signal')
+			return True
+		if int(kwds['pk_row']) <> self.current_encounter['pk_encounter']:
+			_log.debug('modified encounter [%s] != local encounter [%s], ignoring signal', kwds['pk_row'], self.current_encounter['pk_encounter'])
+			return True
+
+		# get the current encounter as an extra instance
+		# from the database to check for changes
+		curr_enc_in_db = gmEMRStructItems.cEncounter(aPK_obj = self.current_encounter['pk_encounter'])
+
+		# the encounter just retrieved and the active encounter
+		# have got the same transaction ID so there's no change
+		# in the database, there could be a local change in
+		# the active encounter but that doesn't matter because
+		# no one else can have written to the DB so far
+		if curr_enc_in_db['xmin_encounter'] == self.current_encounter['xmin_encounter']:
+			_log.debug('same XMIN, there really should not be any difference between DB and in-client instance of current encounter')
+			if self.current_encounter.is_modified():
+				_log.error('encounter modification signal from DB with same XMIN as in local in-client instance of encounter BUT local instance ALSO has .is_modified()=True')
+				_log.error('this hints at an error in .is_modified handling')
+				gmTools.compare_dict_likes(self.current_encounter.fields_as_dict(), curr_enc_in_db.fields_as_dict(), 'modified enc in client', 'enc loaded from DB')
+			return True
+
+		# there must have been a change to the active encounter
+		# committed to the database from elsewhere,
+		# we must fail propagating the change, however, if
+		# there are local changes pending
+		if self.current_encounter.is_modified():
+			gmTools.compare_dict_likes(self.current_encounter.fields_as_dict(), curr_enc_in_db.fields_as_dict(), 'modified enc in client', 'enc loaded from DB')
+			raise ValueError('unsaved changes in locally active encounter [%s], cannot switch to DB state of encounter [%s]' % (
+				self.current_encounter['pk_encounter'],
+				curr_enc_in_db['pk_encounter']
+			))
+
+		# don't do this: same_payload() does not compare _all_ fields
+		# so we can get into a reality disconnect if we don't
+		# announce the mod
+#		if self.current_encounter.same_payload(another_object = curr_enc_in_db):
+#			_log.debug('clin.encounter_mod_db received but no change to active encounter payload')
+#			return True
+
+		# there was a change in the database from elsewhere,
+		# locally, however, we don't have any pending changes,
+		# therefore we can propagate the remote change locally
+		# without losing anything
+		# this really should be the standard case
+		gmTools.compare_dict_likes(self.current_encounter.fields_as_dict(), curr_enc_in_db.fields_as_dict(), 'modified enc in client', 'enc loaded from DB')
+		_log.debug('active encounter modified remotely, no locally pending changes, reloading from DB and locally announcing the remote modification')
+		self.current_encounter.refetch_payload()
+		gmDispatcher.send(u'current_encounter_modified')
+
+		return True
+
+	#--------------------------------------------------------
 	def db_callback_encounter_mod_db(self, **kwds):
 
 		# get the current encounter as an extra instance
@@ -151,7 +211,8 @@ class cClinicalRecord(object):
 		# the encounter just retrieved and the active encounter
 		# have got the same transaction ID so there's no change
 		# in the database, there could be a local change in
-		# the active encounter but that doesn't matter
+		# the active encounter but that doesn't matter because
+		# no one else can have written to the DB so far
 		# THIS DOES NOT WORK
 #		if curr_enc_in_db['xmin_encounter'] == self.current_encounter['xmin_encounter']:
 #			return True
@@ -161,8 +222,9 @@ class cClinicalRecord(object):
 		# we must fail propagating the change, however, if
 		# there are local changes
 		if self.current_encounter.is_modified():
+			gmTools.compare_dict_likes(self.current_encounter.fields_as_dict(), curr_enc_in_db.fields_as_dict(), 'modified enc in client', 'enc loaded from DB')
 			_log.error('current in client: %s', self.current_encounter)
-			raise ValueError('unsaved changes in active encounter [%s], cannot switch to another one [%s]' % (
+			raise ValueError('unsaved changes in active encounter [%s], cannot switch [%s]' % (
 				self.current_encounter['pk_encounter'],
 				curr_enc_in_db['pk_encounter']
 			))
@@ -175,7 +237,8 @@ class cClinicalRecord(object):
 		# locally, however, we don't have any changes, therefore
 		# we can propagate the remote change locally without
 		# losing anything
-		_log.debug('active encounter modified remotely, reloading and announcing the modification')
+		gmTools.compare_dict_likes(self.current_encounter.fields_as_dict(), curr_enc_in_db.fields_as_dict(), 'modified enc in client', 'enc loaded from DB')
+		_log.debug('active encounter modified remotely, reloading from DB and locally announcing the modification')
 		self.current_encounter.refetch_payload()
 		gmDispatcher.send(u'current_encounter_modified')
 
@@ -409,16 +472,17 @@ class cClinicalRecord(object):
 			_log.error(str(data))
 			return None
 		return data
+
 	#--------------------------------------------------------
 	def get_clin_narrative(self, encounters=None, episodes=None, issues=None, soap_cats=None, providers=None):
 		"""Get SOAP notes pertinent to this encounter.
 
 			encounters
-				- list of encounters whose narrative are to be retrieved
+				- list of encounters the narrative of which are to be retrieved
 			episodes
-				- list of episodes whose narrative are to be retrieved
+				- list of episodes the narrative of which are to be retrieved
 			issues
-				- list of health issues whose narrative are to be retrieved
+				- list of health issues the narrative of which are to be retrieved
 			soap_cats
 				- list of SOAP categories of the narrative to be retrieved
 		"""
@@ -435,7 +499,7 @@ class cClinicalRecord(object):
 				elif isinstance(issues[0], int):
 					args['issues'] = tuple(issues)
 				else:
-					raise ValueError('<issues> must of of type int (=pk) or cHealthIssue, but 1st issue is: %s' % issues[0])
+					raise ValueError('<issues> must be list of type int (=pk) or cHealthIssue, but 1st issue is: %s' % issues[0])
 
 		if episodes is not None:
 			where_parts.append(u'pk_episode IN %(epis)s')
@@ -447,7 +511,7 @@ class cClinicalRecord(object):
 				elif isinstance(episodes[0], int):
 					args['epis'] = tuple(episodes)
 				else:
-					raise ValueError('<episodes> must of of type int (=pk) or cEpisode, but 1st episode is: %s' % episodes[0])
+					raise ValueError('<episodes> must be list of type int (=pk) or cEpisode, but 1st episode is: %s' % episodes[0])
 
 		if encounters is not None:
 			where_parts.append(u'pk_encounter IN %(encs)s')
@@ -459,7 +523,7 @@ class cClinicalRecord(object):
 				elif isinstance(encounters[0], int):
 					args['encs'] = tuple(encounters)
 				else:
-					raise ValueError('<encounters> must of of type int (=pk) or cEncounter, but 1st encounter is: %s' % encounters[0])
+					raise ValueError('<encounters> must be list of type int (=pk) or cEncounter, but 1st encounter is: %s' % encounters[0])
 
 		if soap_cats is not None:
 			where_parts.append(u'c_vn.soap_cat IN %(cats)s')
@@ -469,6 +533,10 @@ class cClinicalRecord(object):
 				args['cats'].append(None)
 			args['cats'] = tuple(args['cats'])
 
+		if providers is not None:
+			where_parts.append(u'c_vn.modified_by IN %(docs)s')
+			args['docs'] = tuple(providers)
+
 		cmd = u"""
 			SELECT
 				c_vn.*,
@@ -481,13 +549,8 @@ class cClinicalRecord(object):
 		""" % u' AND '.join(where_parts)
 
 		rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
+		return [ gmClinNarrative.cNarrative(row = {'pk_field': 'pk_narrative', 'idx': idx, 'data': row}) for row in rows ]
 
-		filtered_narrative = [ gmClinNarrative.cNarrative(row = {'pk_field': 'pk_narrative', 'idx': idx, 'data': row}) for row in rows ]
-
-		if providers is not None:
-			filtered_narrative = filter(lambda narr: narr['modified_by'] in providers, filtered_narrative)
-
-		return filtered_narrative
 	#--------------------------------------------------------
 	def get_as_journal(self, since=None, until=None, encounters=None, episodes=None, issues=None, soap_cats=None, providers=None, order_by=None, time_range=None):
 		return gmClinNarrative.get_as_journal (
@@ -1698,6 +1761,7 @@ WHERE
 			_log.debug('switching of active encounter')
 			# fail if the currently active encounter has unsaved changes
 			if self.__encounter.is_modified():
+				gmTools.compare_dict_likes(self.__encounter, encounter, 'modified enc in client', 'enc to switch to')
 				_log.error('current in client: %s', self.__encounter)
 				raise ValueError('unsaved changes in active encounter [%s], cannot switch to another one [%s]' % (
 					self.__encounter['pk_encounter'],
diff --git a/client/business/gmEMRStructItems.py b/client/business/gmEMRStructItems.py
index 5538f6a..7cfd862 100644
--- a/client/business/gmEMRStructItems.py
+++ b/client/business/gmEMRStructItems.py
@@ -1625,7 +1625,7 @@ def episode2problem(episode=None, allow_closed=False):
 class cEncounter(gmBusinessDBObject.cBusinessDBObject):
 	"""Represents one encounter."""
 
-	_cmd_fetch_payload = u"select * from clin.v_pat_encounters where pk_encounter = %s"
+	_cmd_fetch_payload = u"SELECT * FROM clin.v_pat_encounters WHERE pk_encounter = %s"
 	_cmds_store_payload = [
 		u"""UPDATE clin.encounter SET
 				started = %(started)s,
@@ -1639,7 +1639,7 @@ class cEncounter(gmBusinessDBObject.cBusinessDBObject):
 				xmin = %(xmin_encounter)s
 			""",
 		# need to return all fields so we can survive in-place upgrades
-		u"""select * from clin.v_pat_encounters where pk_encounter = %(pk_encounter)s"""
+		u"SELECT * FROM clin.v_pat_encounters WHERE pk_encounter = %(pk_encounter)s"
 	]
 	_updatable_fields = [
 		'started',
diff --git a/client/business/gmPerson.py b/client/business/gmPerson.py
index 32b318c..84ac92b 100644
--- a/client/business/gmPerson.py
+++ b/client/business/gmPerson.py
@@ -23,6 +23,7 @@ from xml.etree import ElementTree as etree
 
 # GNUmed
 if __name__ == '__main__':
+	logging.basicConfig(level = logging.DEBUG)
 	sys.path.insert(0, '../../')
 from Gnumed.pycommon import gmExceptions
 from Gnumed.pycommon import gmDispatcher
@@ -620,8 +621,15 @@ class cIdentity(gmBusinessDBObject.cBusinessDBObject):
 		active name.
 		@param nickname The preferred/nick/warrior name to set.
 		"""
-		rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': u"select dem.set_nickname(%s, %s)", 'args': [self.ID, nickname]}])
-		self.refetch_payload()
+		if self._payload[self._idx['preferred']] == nickname:
+			return True
+		rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': u"SELECT dem.set_nickname(%s, %s)", 'args': [self.ID, nickname]}])
+		# setting nickname doesn't change dem.identity, so other fields
+		# of dem.v_basic_person do not get changed as a consequence of
+		# setting the nickname, hence locally setting nickname matches
+		# in-database reality
+		self._payload[self._idx['preferred']] = nickname
+		#self.refetch_payload()
 		return True
 	#--------------------------------------------------------
 	def get_tags(self, order_by=None):
@@ -1732,7 +1740,7 @@ class cPatient(cIdentity):
 #		return self.__emr
 
 	def get_emr(self, allow_user_interaction=True):
-		_log.debug('accessing EMR (thread %s)', thread.get_ident())
+		_log.debug('accessing EMR for identity [%s] (thread %s)', self._payload[self._idx['pk_identity']], thread.get_ident())
 		if not self.__emr_access_lock.acquire(False):
 			got_lock = False
 			for idx in range(100):
@@ -1744,7 +1752,7 @@ class cPatient(cIdentity):
 					break
 			if not got_lock:
 				_log.error('still failed to acquire EMR access lock, aborting (thread %s)', thread.get_ident())
-				raise AttributeError('cannot lock access to EMR')
+				raise AttributeError('cannot lock access to EMR for identity [%s]', self._payload[self._idx['pk_identity']])
 
 #			# maybe something slow is happening on the machine
 #			_log.debug('failed to acquire EMR access lock, sleeping for 500ms (thread %s)', thread.get_ident())
@@ -1754,14 +1762,14 @@ class cPatient(cIdentity):
 #				raise AttributeError('cannot lock access to EMR')
 
 		if self.__emr is None:
-			_log.debug('pulling chart (thread %s)', thread.get_ident())
+			_log.debug('pulling chart for identity [%s] (thread %s)', self._payload[self._idx['pk_identity']], thread.get_ident())
 			#emr = _pull_chart(self._payload[self._idx['pk_identity']])
 			emr = _pull_chart(self)
 			if emr is None:		# user aborted pulling chart
 				return None
 			self.__emr = emr
 
-		_log.debug('returning EMR (thread %s)', thread.get_ident())
+		_log.debug('returning EMR for identity [%s] (thread %s)', self._payload[self._idx['pk_identity']], thread.get_ident())
 		self.__emr_access_lock.release()
 		return self.__emr
 
@@ -1823,7 +1831,7 @@ class gmCurrentPatient(gmBorg.cBorg):
 		"""
 		# make sure we do have a patient pointer
 		try:
-			tmp = self.patient
+			self.patient
 		except AttributeError:
 			self.patient = gmNull.cNull()
 			self.__register_interests()
@@ -1847,7 +1855,7 @@ class gmCurrentPatient(gmBorg.cBorg):
 		if patient == -1:
 			_log.debug('explicitly unsetting current patient')
 			if not self.__run_callbacks_before_switching_away_from_patient():
-				_log.debug('not unsetting current patient')
+				_log.error('not unsetting current patient, at least one pre-change callback failed')
 				return None
 			self.__send_pre_unselection_notification()
 			self.patient.cleanup()
@@ -1868,10 +1876,10 @@ class gmCurrentPatient(gmBorg.cBorg):
 			return None
 
 		# user wants different patient
-		_log.debug('patient change [%s] -> [%s] requested', self.patient['pk_identity'], patient['pk_identity'])
+		_log.info('patient change [%s] -> [%s] requested', self.patient['pk_identity'], patient['pk_identity'])
 
 		if not self.__run_callbacks_before_switching_away_from_patient():
-			_log.debug('not changing current patient')
+			_log.error('not changing current patient, at least one pre-change callback failed')
 			return None
 
 		# everything seems swell
@@ -1890,12 +1898,30 @@ class gmCurrentPatient(gmBorg.cBorg):
 		return None
 	#--------------------------------------------------------
 	def __register_interests(self):
-		gmDispatcher.connect(signal = u'dem.identity_mod_db', receiver = self._on_identity_change)
-		gmDispatcher.connect(signal = u'dem.names_mod_db', receiver = self._on_identity_change)
+		gmDispatcher.connect(signal = u'gm_table_mod', receiver = self._on_database_signal)
+
 	#--------------------------------------------------------
-	def _on_identity_change(self):
-		"""Listen for patient *data* change."""
+	def _on_database_signal(self, **kwds):
+		# we don't have a patient: don't process signals
+		if isinstance(self.patient, gmNull.cNull):
+			return True
+
+		# we only care about identity and name changes
+		if kwds['table'] not in [u'dem.identity', u'dem.names']:
+			return True
+
+		# signal is not about our patient: ignore signal
+		if int(kwds['pk_identity']) != self.patient.ID:
+			return True
+
+		if kwds['table'] == u'dem.identity':
+			# we don't care about newly INSERTed or DELETEd patients
+			if kwds['operation'] != 'UPDATE':
+				return True
+
 		self.patient.refetch_payload()
+		return True
+
 	#--------------------------------------------------------
 	# external API
 	#--------------------------------------------------------
@@ -1909,14 +1935,13 @@ class gmCurrentPatient(gmBorg.cBorg):
 			raise TypeError(u'callback [%s] not callable' % callback)
 
 		self.__callbacks_before_switching_away_from_patient.append(callback)
+
 	#--------------------------------------------------------
 	def _get_connected(self):
 		return (not isinstance(self.patient, gmNull.cNull))
 
-	def _set_connected(self):
-		raise AttributeError(u'invalid to set <connected> state')
+	connected = property(_get_connected, lambda x:x)
 
-	connected = property(_get_connected, _set_connected)
 	#--------------------------------------------------------
 	def _get_locked(self):
 		return (self.__lock_depth > 0)
@@ -1927,18 +1952,20 @@ class gmCurrentPatient(gmBorg.cBorg):
 			gmDispatcher.send(signal = 'patient_locked', sender = self.__class__.__name__)
 		else:
 			if self.__lock_depth == 0:
-				_log.error('lock/unlock imbalance, trying to refcount lock depth below 0')
+				_log.error('lock/unlock imbalance, tried to refcount lock depth below 0')
 				return
 			else:
 				self.__lock_depth = self.__lock_depth - 1
 			gmDispatcher.send(signal = 'patient_unlocked', sender = self.__class__.__name__)
 
 	locked = property(_get_locked, _set_locked)
+
 	#--------------------------------------------------------
 	def force_unlock(self):
 		_log.info('forced patient unlock at lock depth [%s]' % self.__lock_depth)
 		self.__lock_depth = 0
 		gmDispatcher.send(signal = 'patient_unlocked', sender = self.__class__.__name__)
+
 	#--------------------------------------------------------
 	# patient change handling
 	#--------------------------------------------------------
@@ -1957,10 +1984,11 @@ class gmCurrentPatient(gmBorg.cBorg):
 				return False
 
 			if not successful:
-				_log.debug('callback [%s] returned False', call_back)
+				_log.error('callback [%s] returned False', call_back)
 				return False
 
 		return True
+
 	#--------------------------------------------------------
 	def __send_pre_unselection_notification(self):
 		"""Sends signal when current patient is about to be unset.
@@ -1973,6 +2001,7 @@ class gmCurrentPatient(gmBorg.cBorg):
 			'pk_identity': self.patient['pk_identity']
 		}
 		gmDispatcher.send(**kwargs)
+
 	#--------------------------------------------------------
 	def __send_unselection_notification(self):
 		"""Sends signal when the previously active patient has
@@ -1987,6 +2016,7 @@ class gmCurrentPatient(gmBorg.cBorg):
 			'sender': self.__class__.__name__
 		}
 		gmDispatcher.send(**kwargs)
+
 	#--------------------------------------------------------
 	def __send_selection_notification(self):
 		"""Sends signal when another patient has actually been made active."""
@@ -1996,14 +2026,24 @@ class gmCurrentPatient(gmBorg.cBorg):
 			'pk_identity': self.patient['pk_identity']
 		}
 		gmDispatcher.send(**kwargs)
+
 	#--------------------------------------------------------
 	# __getattr__ handling
 	#--------------------------------------------------------
 	def __getattr__(self, attribute):
+		# override __getattr__ here, not __getattribute__ because
+		# the former is used _after_ ordinary attribute lookup
+		# failed while the latter is applied _before_ ordinary
+		# lookup (and is easy to drive into infinite recursion),
+		# this is also why subsequent access to self.patient
+		# simply returns the .patient member value :-)
 		if attribute == 'patient':
 			raise AttributeError
-		if not isinstance(self.patient, gmNull.cNull):
-			return getattr(self.patient, attribute)
+		if isinstance(self.patient, gmNull.cNull):
+			_log.error("[%s]: cannot getattr(%s, '%s'), patient attribute not connected to a patient", self, self.patient, attribute)
+			raise AttributeError("[%s]: cannot getattr(%s, '%s'), patient attribute not connected to a patient" % (self, self.patient, attribute))
+		return getattr(self.patient, attribute)
+
 	#--------------------------------------------------------
 	# __get/setitem__ handling
 	#--------------------------------------------------------
@@ -2011,9 +2051,11 @@ class gmCurrentPatient(gmBorg.cBorg):
 		"""Return any attribute if known how to retrieve it by proxy.
 		"""
 		return self.patient[attribute]
+
 	#--------------------------------------------------------
 	def __setitem__(self, attribute, value):
 		self.patient[attribute] = value
+
 #============================================================
 # match providers
 #============================================================
@@ -2060,14 +2102,16 @@ INSERT INTO dem.names (
 ) VALUES (
 	currval('dem.identity_pk_seq'), coalesce(%s, 'xxxDEFAULTxxx'), coalesce(%s, 'xxxDEFAULTxxx')
 ) RETURNING id_identity"""
+#	cmd2 = u"select dem.add_name(currval('dem.identity_pk_seq')::integer, coalesce(%s, 'xxxDEFAULTxxx'), coalesce(%s, 'xxxDEFAULTxxx'), True)"
 	rows, idx = gmPG2.run_rw_queries (
 		queries = [
 			{'cmd': cmd1, 'args': [gender, dob]},
 			{'cmd': cmd2, 'args': [lastnames, firstnames]}
+			#{'cmd': cmd2, 'args': [firstnames, lastnames]}
 		],
 		return_data = True
 	)
-	ident = cIdentity(aPK_obj=rows[0][0])
+	ident = cIdentity(aPK_obj = rows[0][0])
 	gmHooks.run_hook_script(hook = u'post_person_creation')
 	return ident
 
@@ -2376,6 +2420,12 @@ if __name__ == '__main__':
 	def test_vcf():
 		person = cIdentity(aPK_obj = 12)
 		print person.export_as_vcard()
+
+	#--------------------------------------------------------
+	def test_current_patient():
+		pat = gmCurrentPatient()
+		print "pat.get_emr()", pat.get_emr()
+
 	#--------------------------------------------------------
 	#test_dto_person()
 	#test_identity()
@@ -2391,6 +2441,7 @@ if __name__ == '__main__':
 	#print "\n\nRetrieving communication media enum (id, description): %s" % comms
 	#test_export_area()
 	#test_ext_id()
-	test_vcf()
+	#test_vcf()
+	test_current_patient()
 
 #============================================================
diff --git a/client/business/gmStaff.py b/client/business/gmStaff.py
index 802bd12..bf5391e 100644
--- a/client/business/gmStaff.py
+++ b/client/business/gmStaff.py
@@ -244,6 +244,7 @@ def deactivate_staff(conn=None, pk_staff=None):
 #============================================================
 def set_current_provider_to_logged_on_user():
 	gmCurrentProvider(provider = cStaff())
+
 #============================================================
 class gmCurrentProvider(gmBorg.cBorg):
 	"""Staff member Borg to hold currently logged on provider.
@@ -271,15 +272,15 @@ class gmCurrentProvider(gmBorg.cBorg):
 		if not isinstance(provider, cStaff):
 			raise ValueError, 'cannot set logged on provider to [%s], must be either None or cStaff instance' % str(provider)
 
-		# same ID, no change needed
-		if self.provider['pk_staff'] == provider['pk_staff']:
-			return None
-
 		# first invocation
 		if isinstance(self.provider, gmNull.cNull):
 			self.provider = provider
 			return None
 
+		# same ID, no change needed
+		if self.provider['pk_staff'] == provider['pk_staff']:
+			return None
+
 		# user wants different provider
 		raise ValueError, 'provider change [%s] -> [%s] not yet supported' % (self.provider['pk_staff'], provider['pk_staff'])
 
diff --git a/client/doc/schema/gnumed-entire_schema.html b/client/doc/schema/gnumed-entire_schema.html
index 3e7c538..ac003b1 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 2015-07-08</p>
+	<p><br><br>Dumped on 2015-10-11</p>
 <h1><a name="index">Index of database - gnumed_v20</a></h1>
 <ul>
     
diff --git a/client/gm-from-vcs.bat b/client/gm-from-vcs.bat
index e84baec..215e867 100755
--- a/client/gm-from-vcs.bat
+++ b/client/gm-from-vcs.bat
@@ -1,4 +1,19 @@
-mklink /J ..\Gnumed ..\client
-set PYTHONPATH=..\;%PYTHONPATH%
-Python gnumed.py --log-file=gm-from-vcs.log --conf-file=gm-from-vcs.conf --debug
+REM # GNUmed tarball startup batch file
+
+REM # normally we would like to use a link but Python
+REM # on Windows seems to have problems importing
+REM # modules from directory links
+REM mklink /J ..\Gnumed ..\client
+
+REM # hence we use xcopy: http://commandwindows.com/xcopy.htm
+REM # but need to remove old link first (if any)
+fsutil reparsepoint delete ..\Gnumed
+REM # if it still exists it shouldn't be a link, so remove the directory now
+rmdir ..\Gnumed /s
+xcopy ..\client ..\Gnumed /E /I /F /H /O /Y
+
+set PYTHONPATH=..;%PYTHONPATH%
+
+REM echo Log file: ./gm-from-vcs.log
+Python gnumed.py --log-file=gm-from-vcs.log --conf-file=gm-from-vcs.conf --local-import --debug
 
diff --git a/client/gnumed.py b/client/gnumed.py
index f75f658..1f077e1 100644
--- a/client/gnumed.py
+++ b/client/gnumed.py
@@ -86,10 +86,11 @@ against. Please run GNUmed as a non-root user.
 	sys.exit(1)
 
 #----------------------------------------------------------
-current_client_version = u'1.5.6'
+current_client_version = u'1.5.8'
 current_client_branch = u'1.5'
 
 _log = None
+_pre_log_buffer = []
 _cfg = None
 _old_sig_term = None
 _known_short_options = u'h?V'
@@ -167,28 +168,55 @@ Cannot run GNUmed without any of them.
 #==========================================================
 # convenience functions
 #==========================================================
+def _symlink_windows(source, link_name):
+	import ctypes
+	csl = ctypes.windll.kernel32.CreateSymbolicLinkW
+	csl.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32)
+	csl.restype = ctypes.c_ubyte
+	if os.path.isdir(source):
+		flags = 1
+	else:
+		flags = 0
+	ret_code = csl(link_name, source.replace('/', '\\'), flags)
+	if ret_code == 0:
+		raise ctypes.WinError()
+	return ret_code
+
+#==========================================================
 def setup_python_path():
 
 	if not u'--local-import' in sys.argv:
+		_pre_log_buffer.append('running against systemwide install')
 		return
 
-	local_python_base_dir = os.path.dirname (
+	local_python_import_dir = os.path.dirname (
 		os.path.abspath(os.path.join(sys.argv[0], '..'))
 	)
-	print "Running from local source tree (%s) ..." % local_python_base_dir
+	print "Running from local source tree (%s) ..." % local_python_import_dir
+	_pre_log_buffer.append("running from local source tree: %s" % local_python_import_dir)
 
 	# does the path exist at all, physically ?
 	# (*broken* links are reported as False)
-	link_name = os.path.join(local_python_base_dir, 'Gnumed')
-	if not os.path.exists(link_name):
-		real_dir = os.path.join(local_python_base_dir, 'client')
-		print "Creating module import symlink ..."
+	link_name = os.path.join(local_python_import_dir, 'Gnumed')
+	if os.path.exists(link_name):
+		_pre_log_buffer.append('local module import dir symlink exists: %s' % link_name)
+	else:
+		real_dir = os.path.join(local_python_import_dir, 'client')
+		print "Creating local module import symlink ..."
 		print ' real dir:', real_dir
 		print '     link:', link_name
-		os.symlink(real_dir, link_name)
+		try:
+			os.symlink(real_dir, link_name)
+		except AttributeError:
+			_pre_log_buffer.append('Windows does not have os.symlink(), resorting to ctypes')
+			result = _symlink_windows(real_dir, link_name)
+			_pre_log_buffer.append('ctypes.windll.kernel32.CreateSymbolicLinkW() exit code: %s', result)
+		_pre_log_buffer.append('created local module import dir symlink: link [%s] => dir [%s]' % (link_name, real_dir))
 
 	print "Adjusting PYTHONPATH ..."
-	sys.path.insert(0, local_python_base_dir)
+	sys.path.insert(0, local_python_import_dir)
+	_pre_log_buffer.append('sys.path with local module import base dir prepended: %s' % sys.path)
+
 #==========================================================
 def setup_local_repo_path():
 
@@ -252,10 +280,13 @@ def setup_fault_handler(target=None):
 		import faulthandler
 	except ImportError:
 		print "Faulthandler not available ..."
+		_pre_log_buffer.append('<faulthandler> not available')
 		return
 	if target is None:
 		faulthandler.enable()
+		_pre_log_buffer.append('<faulthandler> enabled, target = [console]: %s (%s)' % (faulthandler, faulthandler.__version__))
 		return
+	_pre_log_buffer.append('<faulthandler> enabled, target = [%s]: %s (%s)' % (target, faulthandler, faulthandler.__version__))
 	faulthandler.enable(file = target)
 
 #==========================================================
@@ -265,6 +296,7 @@ def setup_logging():
 	except ImportError:
 		sys.exit(import_error_sermon % '\n '.join(sys.path))
 
+	print "Log file: %s" % _gmLog2._logfile.name
 	setup_fault_handler(target = _gmLog2._logfile)
 
 	global gmLog2
@@ -272,27 +304,37 @@ def setup_logging():
 
 	global _log
 	_log = logging.getLogger('gm.launcher')
+
 #==========================================================
 def log_startup_info():
+	global _pre_log_buffer
+	if len(_pre_log_buffer) > 0:
+		_log.info('early startup log buffer:')
+	for line in _pre_log_buffer:
+		_log.info(u' ' + line)
+	del _pre_log_buffer
 	_log.info(u'GNUmed client version [%s] on branch [%s]', current_client_version, current_client_branch)
 	_log.info(u'Platform: %s', platform.uname())
-	_log.info(u'Python %s on %s (%s)', sys.version, sys.platform, os.name)
+	_log.info((u'Python %s on %s (%s)' % (sys.version, sys.platform, os.name)).replace(u'\n', u'<\\n>'))
 	try:
 		import lsb_release
-		_log.info(u'%s' % lsb_release.get_distro_information())
+		_log.info(u'lsb_release: %s', lsb_release.get_distro_information())
 	except ImportError:
 		pass
+	_log.info('os.getcwd(): [%s]', os.getcwd())
 	_log.info('process environment:')
 	for key, val in os.environ.items():
 		_log.info(u' %s: %s' % (
 			(u'${%s}' % key).rjust(30),
 			unicode(val, encoding = sys.getfilesystemencoding(), errors = 'replace')
 		))
+
 #==========================================================
 def setup_console_exception_handler():
 	from Gnumed.pycommon.gmTools import handle_uncaught_exception_console
 
 	sys.excepthook = handle_uncaught_exception_console
+
 #==========================================================
 def setup_cli():
 	from Gnumed.pycommon import gmCfg2
@@ -368,10 +410,12 @@ def handle_sig_term(signum, frame):
 		sys.exit(signal.SIGTERM)
 	else:
 		_old_sig_term(signum, frame)
+
 #----------------------------------------------------------
 def setup_signal_handlers():
 	global _old_sig_term
 	old_sig_term = signal.signal(signal.SIGTERM, handle_sig_term)
+
 #==========================================================
 def setup_locale():
 	gmI18N.activate_locale()
diff --git a/client/pycommon/gmBackendListener.py b/client/pycommon/gmBackendListener.py
index ca9fac5..665f029 100644
--- a/client/pycommon/gmBackendListener.py
+++ b/client/pycommon/gmBackendListener.py
@@ -57,7 +57,7 @@ class gmBackendListener(gmBorg.cBorg):
 
 		self._conn = conn
 		self.backend_pid = self._conn.get_backend_pid()
-		_log.debug('connection has backend PID [%s]', self.backend_pid)
+		_log.debug('notification listener connection has backend PID [%s]', self.backend_pid)
 		self._conn.set_isolation_level(0)		# autocommit mode = psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT
 		self._cursor = self._conn.cursor()
 		try:
@@ -217,7 +217,7 @@ class gmBackendListener(gmBorg.cBorg):
 				self.__notifications_received += 1
 				if self.debug:
 					print notification
-				_log.debug('#%s: %s', self.__notifications_received, notification)
+				_log.debug('#%s: %s (first param is PID of sending backend)', self.__notifications_received, notification)
 				# decode payload
 				payload = notification.payload.split(u'::')
 				operation = None
diff --git a/client/pycommon/gmBorg.py b/client/pycommon/gmBorg.py
index 37132c2..76864b2 100644
--- a/client/pycommon/gmBorg.py
+++ b/client/pycommon/gmBorg.py
@@ -1,10 +1,8 @@
 #===================================================
 # Thanks to Python Patterns !
 # ---------------------------
-# $Id: gmBorg.py,v 1.7 2009-05-08 07:58:35 ncq Exp $
-__version__ = "$Revision: 1.7 $"
 __author__ = "Karsten.Hilbert at gmx.net"
-__license__ = "GPL"
+__license__ = "GPL v2 or later"
 
 #===================================================
 class cBorg(object):
@@ -12,7 +10,7 @@ class cBorg(object):
 
 	- mixin this class with your class' ancestors to borg it
 
-	- there may be many instances of this - PER CHILD CLASS - but they all share state
+	- there may be many _instances_ of this - PER CHILD CLASS - but they all share _state_
 	"""
 	_instances = {}
 
@@ -22,6 +20,7 @@ class cBorg(object):
 			#cBorg._instances[cls] = object.__new__(cls, *args, **kargs)
 			cBorg._instances[cls] = object.__new__(cls)
 		return cBorg._instances[cls]
+
 #===================================================
 if __name__ == '__main__':
 
@@ -57,39 +56,3 @@ if __name__ == '__main__':
 	print c3.x
 
 #===================================================
-# $Log: gmBorg.py,v $
-# Revision 1.7  2009-05-08 07:58:35  ncq
-# - __new__ doesn't take args anymore
-#
-# Revision 1.6  2008/05/21 13:57:57  ncq
-# - remove old borg
-#
-# Revision 1.5  2007/10/23 21:23:30  ncq
-# - cleanup
-#
-# Revision 1.4  2007/09/24 22:05:23  ncq
-# - improved docs
-#
-# Revision 1.3  2007/05/11 14:14:59  ncq
-# - make borg per-sublcass
-#
-# Revision 1.2  2007/05/07 12:30:05  ncq
-# - make cBorg an object child so properties work on it
-#
-# Revision 1.1  2004/02/25 09:30:13  ncq
-# - moved here from python-common
-#
-# Revision 1.3  2003/12/29 16:21:51  uid66147
-# - spelling fix
-#
-# Revision 1.2  2003/11/17 10:56:35  sjtan
-#
-# synced and commiting.
-#
-# Revision 1.1  2003/10/23 06:02:38  sjtan
-#
-# manual edit areas modelled after r.terry's specs.
-#
-# Revision 1.1  2003/04/02 16:07:55  ncq
-# - first version
-#
diff --git a/client/pycommon/gmBusinessDBObject.py b/client/pycommon/gmBusinessDBObject.py
index 7aadbde..19589f9 100644
--- a/client/pycommon/gmBusinessDBObject.py
+++ b/client/pycommon/gmBusinessDBObject.py
@@ -141,7 +141,9 @@ if __name__ == '__main__':
 from Gnumed.pycommon import gmExceptions
 from Gnumed.pycommon import gmPG2
 from Gnumed.pycommon.gmDateTime import pydt_strftime
-from Gnumed.pycommon.gmTools import tex_escape_string, xetex_escape_string
+from Gnumed.pycommon.gmTools import tex_escape_string, xetex_escape_string, compare_dict_likes
+from Gnumed.pycommon.gmTools import xetex_escape_string
+from Gnumed.pycommon.gmTools import compare_dict_likes
 
 
 _log = logging.getLogger('gm.db')
@@ -483,7 +485,7 @@ def delete_xxx(pk_XXX=None):
 	#--------------------------------------------------------
 	def fields_as_dict(self, date_format='%Y %b %d  %H:%M', none_string=u'', escape_style=None, bool_strings=None):
 		if bool_strings is None:
-			bools = {True: u'true', False: u'false'}
+			bools = {True: u'True', False: u'False'}
 		else:
 			bools = {True: bool_strings[0], False: bool_strings[1]}
 		data = {}
@@ -501,6 +503,9 @@ def delete_xxx(pk_XXX=None):
 				continue
 
 			if isinstance(val, datetime.datetime):
+				if date_format is None:
+					data[field] = val
+					continue
 				data[field] = pydt_strftime(val, format = date_format, encoding = 'utf8')
 				if escape_style in [u'latex', u'tex']:
 					data[field] = tex_escape_string(data[field])
@@ -534,10 +539,11 @@ def delete_xxx(pk_XXX=None):
 		"""Fetch field values from backend.
 		"""
 		if self._is_modified:
+			compare_dict_likes(self.original_payload, self.fields_as_dict(date_format = None, none_string = None), u'original payload', u'modified payload')
 			if ignore_changes:
 				_log.critical('[%s:%s]: loosing payload changes' % (self.__class__.__name__, self.pk_obj))
-				_log.debug('original: %s' % self.original_payload)
-				_log.debug('modified: %s' % self._payload)
+				#_log.debug('original: %s' % self.original_payload)
+				#_log.debug('modified: %s' % self._payload)
 			else:
 				_log.critical('[%s:%s]: cannot reload, payload changed' % (self.__class__.__name__, self.pk_obj))
 				return False
@@ -604,10 +610,11 @@ def delete_xxx(pk_XXX=None):
 		if len(rows) == 0:
 			return (False, (u'cannot update row', _('[%s:%s]: row not updated (nothing returned), row in use ?') % (self.__class__.__name__, self.pk_obj)))
 
-		# update cached values from should-be-first-and-only result
-		# row of last query,
+		# update cached values from should-be-first-and-only
+		# result row of last query,
 		# update all fields returned such that computed
-		# columns see their new values
+		# columns see their new values (given they are
+		# returned by the query)
 		row = rows[0]
 		for key in idx:
 			try:
@@ -621,10 +628,14 @@ def delete_xxx(pk_XXX=None):
 				_log.error(args)
 				raise
 
+		# only at conn.commit() time will data actually
+		# get committed (and thusly trigger based notifications
+		# be sent out), so reset the local modification flag
+		# right before that
+		self._is_modified = False
 		conn.commit()
 		close_conn()
 
-		self._is_modified = False
 		# update to new "original" payload
 		self.original_payload = {}
 		for field in self._idx.keys():
diff --git a/client/pycommon/gmNull.py b/client/pycommon/gmNull.py
index 1ac21e9..2bc48a3 100644
--- a/client/pycommon/gmNull.py
+++ b/client/pycommon/gmNull.py
@@ -35,19 +35,19 @@ combinations of these words: Null, object, design and pattern.
 Dinu C. Gherman,
 August 2001
 
-For modifications see CVS changelog below.
-
 Karsten Hilbert
 July 2004
 """
 #==============================================================
-# $Source: /home/ncq/Projekte/cvs2git/vcs-mirror/gnumed/gnumed/client/pycommon/gmNull.py,v $
-__version__ = "$Revision: 1.6 $"
-__author__ = "Dinu C. Gherman"
+__author__ = "Dinu C. Gherman, Karsten Hilbert"
 __license__ = "GPL v2 or later (details at http://www.gnu.org)"
 
+import logging
+
+_log = logging.getLogger('cNull')
+
 #==============================================================
-class cNull:
+class cNull(object):
 	"""A class for implementing Null objects.
 
 	This class ignores all parameters passed when constructing or 
@@ -61,69 +61,67 @@ class cNull:
 	on the environment and, hence, these special methods are not
 	provided here.
 	"""
-	_warn = 0
-
 	# object constructing
-	
 	def __init__(self, *args, **kwargs):
 		"Ignore parameters."
-		try:
-			cNull._warn = kwargs['warn']
-		except KeyError:
-			pass
-		return None
+		_log.debug(u'args: %s', args)
+		_log.debug(u'kwargs: %s', kwargs)
 
 	# object calling
-
 	def __call__(self, *args, **kwargs):
 		"Ignore method calls."
-		if cNull._warn:
-			print "cNull.__call__()"
+		_log.debug(u'args: %s', args)
+		_log.debug(u'kwargs: %s', kwargs)
 		return self
 
 	# attribute handling
-
-	def __getattr__(self, mname):
+	def __getattr__(self, attribute):
 		"Ignore attribute requests."
-		if cNull._warn:
-			print "cNull.__getattr__()"
+		_log.debug(u'%s.%s', self, attribute)
 		return self
 
-	def __setattr__(self, name, value):
+	def __setattr__(self, attribute, value):
 		"Ignore attribute setting."
-		if cNull._warn:
-			print "cNull.__setattr__()"
+		_log.debug(u'%s.%s = %s', self, attribute, value)
 		return self
 
-	def __delattr__(self, name):
+	def __delattr__(self, attribute):
 		"Ignore deleting attributes."
-		if cNull._warn:
-			print "cNull.__delattr__()"
+		_log.debug(u'%s.%s', self, attribute)
 		return self
 
-	# misc.
+	# item handling
+	def __getitem__(self, item):
+		"Ignore item requests."
+		_log.debug(u'%s[%s]', self, item)
+		return self
+
+	def __setitem__(self, item, value):
+		"Ignore item setting."
+		_log.debug(u'%s[%s] = %s', self, item, value)
+		return self
 
+	def __delitem__(self, item):
+		"Ignore deleting items."
+		_log.debug(u'%s[%s]', self, item)
+		return self
+
+	# misc.
 	def __repr__(self):
 		"Return a string representation."
-		if cNull._warn:
-			print "cNull.__repr__()"
 		return "<cNull instance @ %s>" % id(self)
 
 	def __str__(self):
 		"Convert to a string and return it."
-		if cNull._warn:
-			print "cNull.__str__()"
-		return "cNull instance"
+		return 'cNull instance'
 
 	def __nonzero__(self):
-		if cNull._warn:
-			print "cNull.__nonzero__()"
+		_log.debug(u'returns 0')
 		return 0
-        
+
 	def __len__(self):
-		if cNull._warn:
-			print "cNull.__len__()"
-		return 0        
+		_log.debug(u'0')
+		return 0
 
 #==============================================================
 def test():
@@ -133,7 +131,7 @@ def test():
 
 	n = cNull()
 	n = cNull('value')
-	n = cNull('value', param='value', warn=1)
+	n = cNull('value', param='value')
 
 	n()
 	n('value')
@@ -154,6 +152,10 @@ def test():
 	n.attr1 = 'value'
 	n.attr1.attr2 = 'value'
 
+	n['1']
+	n['2'] = '123'
+	del n['3']
+
 	del n.attr1
 	del n.attr1.attr2.attr3
 
@@ -167,29 +169,7 @@ def test():
 		print "Null object == 1"
 	else:
 		print "Null object != 1"
-#--------------------------------------------------------------
-if __name__ == '__main__':
-	test()
 
 #==============================================================
-# $Log: gmNull.py,v $
-# Revision 1.6  2005-06-28 14:12:55  cfmoro
-# Integration in space fixes
-#
-# Revision 1.5  2004/12/22 08:40:01  ncq
-# - make output more obvious
-#
-# Revision 1.4  2004/11/24 15:49:11  ncq
-# - use 0/1 not False/True so we can run on older pythons
-#
-# Revision 1.3  2004/08/20 08:38:47  ncq
-# - robustify while working on allowing inactive patient after search
-#
-# Revision 1.2  2004/07/21 07:51:47  ncq
-# - tabified
-# - __nonzero__ added
-# - if keyword argument 'warn' is True: warn on use of Null class
-#
-# Revision 1.1	2004/07/06 00:08:31	 ncq
-# - null design pattern from python cookbook
-#
+if __name__ == '__main__':
+	test()
diff --git a/client/pycommon/gmPG2.py b/client/pycommon/gmPG2.py
index da71465..4e9317b 100644
--- a/client/pycommon/gmPG2.py
+++ b/client/pycommon/gmPG2.py
@@ -353,7 +353,7 @@ where
 		curs.execute(cmd, args)
 		rows = curs.fetchall()
 		if len(rows) > 0:
-			result = rows[0][0]
+			result = rows[0]['name']
 			_log.debug(u'[%s] maps to [%s]', timezone, result)
 	except:
 		_log.exception(u'cannot expand timezone abbreviation [%s]', timezone)
@@ -1603,6 +1603,7 @@ def get_raw_connection(dsn=None, verbose=False, readonly=True):
 
 	try:
 		conn = dbapi.connect(dsn=dsn, connection_factory=psycopg2.extras.DictConnection)
+		#conn = dbapi.connect(dsn=dsn, cursor_factory=psycopg2.extras.RealDictCursor)
 	except dbapi.OperationalError, e:
 
 		t, v, tb = sys.exc_info()
@@ -1930,7 +1931,7 @@ def _log_PG_settings(curs=None):
 		_log.error(u'cannot log PG settings (>>>show all<<< did not return rows)')
 		return False
 	for setting in settings:
-		_log.debug(u'PG option [%s]: %s', setting[0], setting[1])
+		_log.debug(u'PG option [%s]: %s', setting['name'], setting['setting'])
 
 	try:
 		curs.execute(u'select pg_available_extensions()')
@@ -1942,7 +1943,7 @@ def _log_PG_settings(curs=None):
 		_log.error(u'no PG extensions available')
 		return False
 	for ext in extensions:
-		_log.debug(u'PG extension: %s', ext[0])
+		_log.debug(u'PG extension: %s', ext['pg_available_extensions'])
 
 	return True
 #========================================================================
diff --git a/client/pycommon/gmTools.py b/client/pycommon/gmTools.py
index 263330a..de090ee 100644
--- a/client/pycommon/gmTools.py
+++ b/client/pycommon/gmTools.py
@@ -6,13 +6,21 @@ __author__ = "K. Hilbert <Karsten.Hilbert at gmx.net>"
 __license__ = "GPL v2 or later (details at http://www.gnu.org)"
 
 # std libs
-import re as regex, sys, os, os.path, csv, tempfile, logging, hashlib
+import sys
+import os
+import os.path
+import csv
+import tempfile
+import logging
+import hashlib
 import platform
 import subprocess
 import decimal
 import getpass
-import cPickle, zlib
+import re as regex
 import xml.sax.saxutils as xml_tools
+# old:
+import cPickle, zlib
 
 
 # GNUmed libs
@@ -95,6 +103,12 @@ u_kanji_yen = u'\u5186'						# Yen kanji
 u_replacement_character = u'\ufffd'
 u_link_symbol = u'\u1f517'
 
+_kB = 1024
+_MB = 1024 * _kB
+_GB = 1024 * _MB
+_TB = 1024 * _GB
+_PB = 1024 * _TB
+
 #===========================================================================
 def handle_uncaught_exception_console(t, v, tb):
 
@@ -105,6 +119,7 @@ def handle_uncaught_exception_console(t, v, tb):
 	print "`========================================================"
 	_log.critical('unhandled exception caught', exc_info = (t,v,tb))
 	sys.__excepthook__(t,v,tb)
+
 #===========================================================================
 # path level operations
 #---------------------------------------------------------------------------
@@ -358,6 +373,7 @@ class gmPaths(gmBorg.cBorg):
 		return self.__tmp_dir
 
 	tmp_dir = property(_get_tmp_dir, _set_tmp_dir)
+
 #===========================================================================
 # file related tools
 #---------------------------------------------------------------------------
@@ -396,6 +412,7 @@ def gpg_decrypt_file(filename=None, passphrase=None):
 		return None
 
 	return filename_decrypted
+
 #---------------------------------------------------------------------------
 def file2md5(filename=None, return_hex=True):
 	blocksize = 2**10 * 128			# 128k, since md5 uses 128 byte blocks
@@ -415,6 +432,26 @@ def file2md5(filename=None, return_hex=True):
 	if return_hex:
 		return md5.hexdigest()
 	return md5.digest()
+
+#---------------------------------------------------------------------------
+def file2chunked_md5(filename=None, chunk_size=500*_MB):
+	_log.debug('chunked_md5(%s, chunk_size=%s bytes)', filename, chunk_size)
+	md5_concat = u''
+	f = open(filename, 'rb')
+	while True:
+		md5 = hashlib.md5()
+		data = f.read(chunk_size)
+		if not data:
+			break
+		md5.update(data)
+		md5_concat += md5.hexdigest()
+	f.close()
+	md5 = hashlib.md5()
+	md5.update(md5_concat)
+	hex_digest = md5.hexdigest()
+	_log.debug('md5("%s"): %s', md5_concat, hex_digest)
+	return hex_digest
+
 #---------------------------------------------------------------------------
 def unicode2charset_encoder(unicode_csv_data, encoding='utf-8'):
 	for line in unicode_csv_data:
@@ -498,6 +535,7 @@ def get_unique_filename(prefix=None, suffix=None, tmp_dir=None):
 	f.close()
 
 	return filename
+
 #===========================================================================
 def import_module_from_directory(module_path=None, module_name=None, always_remove_path=False):
 	"""Import a module from any location."""
@@ -529,15 +567,10 @@ def import_module_from_directory(module_path=None, module_name=None, always_remo
 			sys.path.remove(module_path)
 
 	return module
+
 #===========================================================================
 # text related tools
 #---------------------------------------------------------------------------
-_kB = 1024
-_MB = 1024 * _kB
-_GB = 1024 * _MB
-_TB = 1024 * _GB
-_PB = 1024 * _TB
-#---------------------------------------------------------------------------
 def size2str(size=0, template=u'%s'):
 	if size == 1:
 		return template % _('1 Byte')
@@ -856,6 +889,44 @@ def html_escape_string(text=None):
 	return "".join(__html_escape_table.get(char, char) for char in text)
 
 #---------------------------------------------------------------------------
+#---------------------------------------------------------------------------
+def compare_dict_likes(d1, d2, title1=None, title2=None):
+	_log.info('comparing dict-likes: %s[%s] vs %s[%s]', coalesce(title1, u'', u'"%s" '), type(d1), coalesce(title2, u'', u'"%s" '), type(d2))
+	k1 = frozenset(d1)
+	k2 = frozenset(d2)
+	different = False
+	if len(k1) != len(k2):
+		_log.info('different number of keys: %s vs %s', len(k1), len(k2))
+		different = True
+	for key in k1:
+		if key in k2:
+			if type(d1[key]) != type(d2[key]):
+				_log.info(u'%25.25s: type(dict1) = %s = >>>%s<<<' % (key, type(d1[key]), d1[key]))
+				_log.info(u'%25.25s  type(dict2) = %s = >>>%s<<<' % (u'', type(d2[key]), d2[key]))
+				different = True
+				continue
+			if d1[key] == d2[key]:
+				_log.info(u'%25.25s:  both = >>>%s<<<' % (key, d1[key]))
+			else:
+				_log.info(u'%25.25s: dict1 = >>>%s<<<' % (key, d1[key]))
+				_log.info(u'%25.25s  dict2 = >>>%s<<<' % (u'', d2[key]))
+				different = True
+		else:
+			_log.info(u'%25.25s: %50.50s | <MISSING>' % (key, u'>>>%s<<<' % d1[key]))
+			different = True
+	for key in k2:
+		if key in k1:
+			continue
+		_log.info(u'%25.25s: %50.50s | %.50s' % (key, u'<MISSING>', u'>>>%s<<<' % d2[key]))
+		different = True
+	if different:
+		_log.info('dict-likes appear to be different from each other')
+		return False
+	_log.info('dict-likes appear equal to each other')
+	return True
+
+#---------------------------------------------------------------------------
+#---------------------------------------------------------------------------
 def prompted_input(prompt=None, default=None):
 	"""Obtains entry from standard input.
 
@@ -1200,7 +1271,8 @@ second line\n
 		print wrap(test, 7, u'   ', u' ')
 	#-----------------------------------------------------------------------
 	def test_md5():
-		print '%s: %s' % (sys.argv[2], file2md5(sys.argv[2]))
+		print 'md5 %s: %s' % (sys.argv[2], file2md5(sys.argv[2]))
+		print 'chunked md5 %s: %s' % (sys.argv[2], file2chunked_md5(sys.argv[2]))
 	#-----------------------------------------------------------------------
 	def test_unicode():
 		print u_link_symbol * 10
@@ -1259,6 +1331,27 @@ second line\n
 	def test_dir_is_empty():
 		print sys.argv[2], 'empty:', dir_is_empty(sys.argv[2])
 	#-----------------------------------------------------------------------
+	def test_compare_dicts():
+		d1 = {}
+		d2 = {}
+		d1[1] = 1
+		d1[2] = 2
+		d1[3] = 3
+		# 4
+		d1[5] = 5
+
+		d2[1] = 1
+		d2[2] = None
+		# 3
+		d2[4] = 4
+
+		compare_dict_likes(d1, d2)
+
+		d1 = {1: 1, 2: 2}
+		d2 = {1: 1, 2: 2}
+
+		compare_dict_likes(d1, d2, 'same1', 'same2')
+	#-----------------------------------------------------------------------
 	#test_coalesce()
 	#test_capitalize()
 	#test_import_module()
@@ -1273,13 +1366,14 @@ second line\n
 	#test_input2decimal()
 	#test_input2int()
 	#test_unwrap()
-	#test_md5()
+	test_md5()
 	#test_unicode()
 	#test_xml_escape()
 	#test_gpg_decrypt()
 	#test_strip_trailing_empty_lines()
 	#test_fname_stem()
-	test_tex_escape()
+	#test_tex_escape()
 	#test_dir_is_empty()
+	#test_compare_dicts()
 
 #===========================================================================
diff --git a/client/wxpython/gmGuiMain.py b/client/wxpython/gmGuiMain.py
index a65455a..4578730 100644
--- a/client/wxpython/gmGuiMain.py
+++ b/client/wxpython/gmGuiMain.py
@@ -141,6 +141,17 @@ _scripting_listener = None
 _original_wxEndBusyCursor = None
 
 #==============================================================================
+class cLog_wx2gm(wx.PyLog):
+	# redirect wx.LogXXX() calls to python logging log
+	def DoLogTextAtLevel(self, level, msg):
+		_log.log(level, msg)
+
+__wxlog = cLog_wx2gm()
+_log.info('redirecting wx.Log to [%s]', __wxlog)
+wx.Log_SetActiveTarget(__wxlog)
+#wx.LogDebug('test message')
+
+#==============================================================================
 class gmTopLevelFrame(wx.Frame):
 	"""GNUmed client's main windows frame.
 
@@ -237,6 +248,7 @@ class gmTopLevelFrame(wx.Frame):
 			_log.error('cannot switch font from [%s] (%s) to [%s]', font.GetNativeFontInfoUserDesc(), font.GetNativeFontInfoDesc(), font_face)
 
 		return
+
 	#----------------------------------------------
 	def __set_GUI_size(self):
 		"""Try to get previous window size from backend."""
@@ -988,18 +1000,51 @@ class gmTopLevelFrame(wx.Frame):
 		wx.EVT_END_SESSION(self, self._on_end_session)
 
 		gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
-		gmDispatcher.connect(signal = u'dem.names_mod_db', receiver = self._on_pat_name_changed)
-		gmDispatcher.connect(signal = u'dem.identity_mod_db', receiver = self._on_pat_name_changed)
-		gmDispatcher.connect(signal = u'dem.praxis_branch_mod_db', receiver = self._on_pat_name_changed)
 		gmDispatcher.connect(signal = u'statustext', receiver = self._on_set_statustext)
 		gmDispatcher.connect(signal = u'request_user_attention', receiver = self._on_request_user_attention)
-		# FIXME: xxxxxxx signal
-		gmDispatcher.connect(signal = u'db_maintenance_warning', receiver = self._on_db_maintenance_warning)
 		gmDispatcher.connect(signal = u'register_pre_exit_callback', receiver = self._register_pre_exit_callback)
 		gmDispatcher.connect(signal = u'plugin_loaded', receiver = self._on_plugin_loaded)
 
+		gmDispatcher.connect(signal = u'db_maintenance_warning', receiver = self._on_db_maintenance_warning)
+		gmDispatcher.connect(signal = u'gm_table_mod', receiver = self._on_database_signal)
+
+		# FIXME: xxxxxxx signal
+
 		gmPerson.gmCurrentPatient().register_before_switching_from_patient_callback(callback = self._before_switching_from_patient_callback)
+
 	#----------------------------------------------
+	def _on_database_signal(self, **kwds):
+
+		if kwds['table'] == u'dem.praxis_branch':
+			if kwds['operation'] != u'UPDATE':
+				return True
+			branch = gmPraxis.gmCurrentPraxisBranch()
+			if branch['pk_praxis_branch'] != kwds['pk_row']:
+				return True
+			self.__update_window_title()
+			return True
+
+		if kwds['table'] == u'dem.names':
+			pat = gmPerson.gmCurrentPatient()
+			if pat.connected:
+				if pat.ID != kwds['pk_identity']:
+					return True
+			self.__update_window_title()
+			return True
+
+		if kwds['table'] == u'dem.identity':
+			if kwds['operation'] != u'UPDATE':
+				return True
+			pat = gmPerson.gmCurrentPatient()
+			if pat.connected:
+				if pat.ID != kwds['pk_identity']:
+					return True
+			self.__update_window_title()
+			return True
+
+		return True
+
+	#-----------------------------------------------
 	def _on_plugin_loaded(self, plugin_name=None, class_name=None, menu_name=None, menu_item_name=None, menu_help_string=None):
 
 		_log.debug('registering plugin with menu system')
@@ -1130,9 +1175,6 @@ class gmTopLevelFrame(wx.Frame):
 
 		gmHooks.run_hook_script(hook = u'request_user_attention')
 	#-----------------------------------------------
-	def _on_pat_name_changed(self):
-		self.__update_window_title()
-	#-----------------------------------------------
 	def _on_post_patient_selection(self, **kwargs):
 		self.__update_window_title()
 		gmDispatcher.send(signal = 'statustext', msg = u'')
@@ -3142,6 +3184,11 @@ class gmApp(wx.App):
 
 	def OnInit(self):
 
+		if _cfg.get(option = 'debug'):
+			self.SetAssertMode(wx.PYAPP_ASSERT_LOG)
+		else:
+			self.SetAssertMode(wx.PYAPP_ASSERT_SUPPRESS)
+
 		self.__starting_up = True
 
 		gmExceptionHandlingWidgets.install_wx_exception_handler()
diff --git a/client/wxpython/gmListWidgets.py b/client/wxpython/gmListWidgets.py
index 29ed1f5..8ee6a3f 100644
--- a/client/wxpython/gmListWidgets.py
+++ b/client/wxpython/gmListWidgets.py
@@ -1041,7 +1041,7 @@ A discontinuous selection may depend on your holding down a platform-dependent m
 			if self.debug is not None:
 				_log.debug('[round %s] <%s>.GetItemCount() before DeleteAllItems(): %s (thread [%s])', tries, self.debug, self.GetItemCount(), thread.get_ident())
 			if not self.DeleteAllItems():
-				_log.debug('<%s>.DeleteAllItems() failed', self.debug)
+				_log.error('<%s>.DeleteAllItems() failed', self.debug)
 			item_count = self.GetItemCount()
 			if item_count == 0:
 				return True
@@ -1070,7 +1070,7 @@ A discontinuous selection may depend on your holding down a platform-dependent m
 				topmost_visible = self.TopItem
 
 		if not self.remove_items_safely(max_tries = 3):
-			_log.debug(", continuing and hoping for the best")
+			_log.error("cannot remove items (?), continuing and hoping for the best")
 
 		if items is None:
 			self.data = None
diff --git a/client/wxpython/gmNarrativeWidgets.py b/client/wxpython/gmNarrativeWidgets.py
index ac6f83a..2494064 100644
--- a/client/wxpython/gmNarrativeWidgets.py
+++ b/client/wxpython/gmNarrativeWidgets.py
@@ -1105,11 +1105,10 @@ class cSoapPluginPnl(wxgSoapPluginPnl.wxgSoapPluginPnl, gmRegetMixin.cRegetOnPai
 						with_rfe_aoe = True
 					)
 
-		tmp = emr.active_encounter.format_soap (
-			soap_cats = 'soapu',
-			emr = emr,
-			issues = [ problem['pk_health_issue'] ],
-		)
+		if problem['pk_health_issue'] is None:
+			tmp = emr.active_encounter.format_soap(soap_cats = 'soapu', emr = emr)
+		else:
+			tmp = emr.active_encounter.format_soap(soap_cats = 'soapu', emr = emr, issues = [problem['pk_health_issue']])
 		if len(tmp) > 0:
 			soap += _('Current encounter:') + u'\n'
 			soap += u'\n'.join(tmp) + u'\n'
diff --git a/client/wxpython/gmPatOverviewWidgets.py b/client/wxpython/gmPatOverviewWidgets.py
index 3ac783c..2140e84 100644
--- a/client/wxpython/gmPatOverviewWidgets.py
+++ b/client/wxpython/gmPatOverviewWidgets.py
@@ -123,34 +123,17 @@ class cPatientOverviewPnl(wxgPatientOverviewPnl.wxgPatientOverviewPnl, gmRegetMi
 		gmDispatcher.connect(signal = u'pre_patient_unselection', receiver = self._on_pre_patient_unselection)
 		gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
 
+		# generic database change signal
+		gmDispatcher.connect(signal = u'gm_table_mod', receiver = self._on_database_signal)
+
 		# database change signals
-		gmDispatcher.connect(signal = u'dem.identity_mod_db', receiver = self._on_post_patient_selection)
-		gmDispatcher.connect(signal = u'dem.names_mod_db', receiver = self._on_post_patient_selection)
-		gmDispatcher.connect(signal = u'dem.comm_channel_mod_db', receiver = self._on_post_patient_selection)
-		gmDispatcher.connect(signal = u'dem.job_mod_db', receiver = self._on_post_patient_selection)
 		# no signal for external IDs yet
 		# no signal for address yet
-		#gmDispatcher.connect(signal = u'current_encounter_modified', receiver = self._on_current_encounter_modified)
-		#gmDispatcher.connect(signal = u'current_encounter_switched', receiver = self._on_current_encounter_switched)
-
-		gmDispatcher.connect(signal = u'clin.episode_mod_db', receiver = self._on_episode_issue_mod_db)
-		gmDispatcher.connect(signal = u'clin.health_issue_mod_db', receiver = self._on_episode_issue_mod_db)
-
-		gmDispatcher.connect(signal = u'clin.substance_intake_mod_db', receiver = self._on_post_patient_selection)
-
-		gmDispatcher.connect(signal = u'clin.hospital_stay_mod_db', receiver = self._on_post_patient_selection)
-		gmDispatcher.connect(signal = u'clin.family_history_mod_db', receiver = self._on_post_patient_selection)
-		gmDispatcher.connect(signal = u'clin.procedure_mod_db', receiver = self._on_post_patient_selection)
-		gmDispatcher.connect(signal = u'clin.vaccination_mod_db', receiver = self._on_post_patient_selection)
-		#gmDispatcher.connect(signal = u'clin.external_care_mod_db', receiver = self._on_post_patient_selection)
+		##gmDispatcher.connect(signal = u'current_encounter_modified', receiver = self._on_current_encounter_modified)
+		##gmDispatcher.connect(signal = u'current_encounter_switched', receiver = self._on_current_encounter_switched)
 
-		gmDispatcher.connect(signal = u'dem.message_inbox_mod_db', receiver = self._on_post_patient_selection)
-		gmDispatcher.connect(signal = u'clin.test_result_mod_db', receiver = self._on_post_patient_selection)
+		# doesn't have pk_identity:
 		gmDispatcher.connect(signal = u'clin.reviewed_test_results_mod_db', receiver = self._on_post_patient_selection)
-		gmDispatcher.connect(signal = u'blobs.doc_med_mod_db', receiver = self._on_post_patient_selection)
-
-		# generic signal
-		gmDispatcher.connect(signal = u'gm_table_mod', receiver = self._on_post_patient_selection)
 
 		# synchronous signals
 #		self.__pat.register_before_switching_from_patient_callback(callback = self._before_switching_from_patient_callback)
@@ -170,8 +153,41 @@ class cPatientOverviewPnl(wxgPatientOverviewPnl.wxgPatientOverviewPnl, gmRegetMi
 	def _on_post_patient_selection(self):
 		self._schedule_data_reget()
 	#--------------------------------------------------------
-	def _on_episode_issue_mod_db(self):
-		self._schedule_data_reget()
+	def _on_database_signal(self, **kwds):
+
+		pat = gmPerson.gmCurrentPatient()
+		if not pat.connected:
+			# probably not needed:
+			#self._schedule_data_reget()
+			return True
+
+		if kwds['pk_identity'] != pat.ID:
+			return True
+
+		if kwds['table'] == u'dem.identity':
+			if kwds['operation'] != u'UPDATE':
+				return True
+
+		if kwds['table'] in [
+			u'dem.identity',
+			u'dem.names',
+			u'dem.lnk_identity2comm',
+			u'dem.lnk_job2person',
+			u'clin.substance_intake',
+			u'clin.hospital_stay',
+			u'clin.procedure',
+			u'clin.vaccination',
+			u'clin.family_history',
+			u'clin.test_result',
+			u'blobs.doc_med',
+			u'dem.message_inbox',
+			u'clin.episode',
+			u'clin.health_issue'
+		]:
+			self._schedule_data_reget()
+			return True
+
+		return True
 	#-----------------------------------------------------
 	# reget-on-paint mixin API
 	#-----------------------------------------------------
diff --git a/client/wxpython/gmPersonCreationWidgets.py b/client/wxpython/gmPersonCreationWidgets.py
index dd4f3ad..3ded688 100644
--- a/client/wxpython/gmPersonCreationWidgets.py
+++ b/client/wxpython/gmPersonCreationWidgets.py
@@ -474,20 +474,23 @@ class cNewPatientEAPnl(wxgNewPatientEAPnl.wxgNewPatientEAPnl, gmEditArea.cGeneri
 			lastnames = self._PRW_lastname.GetValue().strip(),
 			firstnames = self._PRW_firstnames.GetValue().strip()
 		)
-		_log.debug('identity created: %s' % new_identity)
+		_log.info('identity created: %s' % new_identity)
 
 		new_identity['dob_is_estimated'] = self._CHBOX_estimated_dob.GetValue()
 		val = self._TCTRL_tob.GetValue().strip()
 		if val != u'':
 			new_identity['tob'] = pydt.time(int(val[:2]), int(val[3:5]))
 		new_identity['title'] = gmTools.none_if(self._PRW_title.GetValue().strip())
-		new_identity.set_nickname(nickname = gmTools.none_if(self._PRW_nickname.GetValue().strip(), u''))
 
 		prov = self._PRW_primary_provider.GetData()
 		if prov is not None:
 			new_identity['pk_primary_provider'] = prov
 		new_identity['comment'] = gmTools.none_if(self._TCTRL_comment.GetValue().strip(), u'')
 		new_identity.save()
+		_log.info('new identity updated: %s' % new_identity)
+
+		new_identity.set_nickname(nickname = gmTools.none_if(self._PRW_nickname.GetValue().strip(), u''))
+		_log.info('nickname set on new identity: %s' % new_identity)
 
 		# address
 		# if we reach this the address cannot be completely empty
diff --git a/client/wxpython/gmPregWidgets.py b/client/wxpython/gmPregWidgets.py
index eca1f25..a7a77e6 100644
--- a/client/wxpython/gmPregWidgets.py
+++ b/client/wxpython/gmPregWidgets.py
@@ -42,7 +42,7 @@ def calculate_edc(parent=None, patient=None):
 	patient.emr.EDC = edc
 
 #====================================================================
-from wxGladeWidgets import wxgEdcCalculatorDlg
+from Gnumed.wxGladeWidgets import wxgEdcCalculatorDlg
 
 class cEdcCalculatorDlg(wxgEdcCalculatorDlg.wxgEdcCalculatorDlg):
 
diff --git a/client/wxpython/gmSubstanceMgmtWidgets.py b/client/wxpython/gmSubstanceMgmtWidgets.py
deleted file mode 100644
index 6dc81f5..0000000
--- a/client/wxpython/gmSubstanceMgmtWidgets.py
+++ /dev/null
@@ -1,442 +0,0 @@
-# -*- coding: utf-8 -*-
-
-#from __future__ import print_function
-
-__doc__ = """GNUmed drug / substance reference widgets."""
-
-#================================================================
-__author__ = "Karsten Hilbert <Karsten.Hilbert at gmx.net>"
-__license__ = "GPL v2 or later"
-
-import logging
-import sys
-import os.path
-#import io
-#import csv
-#import decimal
-#import datetime as pydt
-
-
-import wx
-
-
-if __name__ == '__main__':
-	sys.path.insert(0, '../../')
-	from Gnumed.pycommon import gmI18N
-	gmI18N.activate_locale()
-	gmI18N.install_domain(domain = 'gnumed')
-
-from Gnumed.pycommon import gmDispatcher
-from Gnumed.pycommon import gmCfg
-from Gnumed.pycommon import gmShellAPI
-from Gnumed.pycommon import gmTools
-#from Gnumed.pycommon import gmDateTime
-from Gnumed.pycommon import gmMatchProvider
-#from Gnumed.pycommon import gmI18N
-#from Gnumed.pycommon import gmPrinting
-#from Gnumed.pycommon import gmCfg2
-#from Gnumed.pycommon import gmNetworkTools
-
-from Gnumed.business import gmPerson
-from Gnumed.business import gmATC
-from Gnumed.business import gmPraxis
-from Gnumed.business import gmMedication
-#from Gnumed.business import gmForms
-#from Gnumed.business import gmStaff
-#from Gnumed.business import gmDocuments
-#from Gnumed.business import gmLOINC
-#from Gnumed.business import gmClinicalRecord
-#from Gnumed.business import gmClinicalCalculator
-#from Gnumed.business import gmPathLab
-
-from Gnumed.wxpython import gmGuiHelpers
-#from Gnumed.wxpython import gmRegetMixin
-from Gnumed.wxpython import gmAuthWidgets
-from Gnumed.wxpython import gmEditArea
-#from Gnumed.wxpython import gmMacro
-from Gnumed.wxpython import gmCfgWidgets
-from Gnumed.wxpython import gmListWidgets
-from Gnumed.wxpython import gmPhraseWheel
-
-
-_log = logging.getLogger('gm.ui')
-
-#============================================================
-# generic drug database access
-#------------------------------------------------------------
-def configure_drug_data_source(parent=None):
-	gmCfgWidgets.configure_string_from_list_option (
-		parent = parent,
-		message = _(
-			'\n'
-			'Please select the default drug data source from the list below.\n'
-			'\n'
-			'Note that to actually use it you need to have the database installed, too.'
-		),
-		option = 'external.drug_data.default_source',
-		bias = 'user',
-		default_value = None,
-		choices = gmMedication.drug_data_source_interfaces.keys(),
-		columns = [_('Drug data source')],
-		data = gmMedication.drug_data_source_interfaces.keys(),
-		caption = _('Configuring default drug data source')
-	)
-
-#============================================================
-def get_drug_database(parent=None, patient=None):
-	dbcfg = gmCfg.cCfgSQL()
-
-	# load from option
-	default_db = dbcfg.get2 (
-		option = 'external.drug_data.default_source',
-		workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
-		bias = 'workplace'
-	)
-
-	# not configured -> try to configure
-	if default_db is None:
-		gmDispatcher.send('statustext', msg = _('No default drug database configured.'), beep = True)
-		configure_drug_data_source(parent = parent)
-		default_db = dbcfg.get2 (
-			option = 'external.drug_data.default_source',
-			workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
-			bias = 'workplace'
-		)
-		# still not configured -> return
-		if default_db is None:
-			gmGuiHelpers.gm_show_error (
-				aMessage = _('There is no default drug database configured.'),
-				aTitle = _('Jumping to drug database')
-			)
-			return None
-
-	# now it MUST be configured (either newly or previously)
-	# but also *validly* ?
-	try:
-		drug_db = gmMedication.drug_data_source_interfaces[default_db]()
-	except KeyError:
-		# not valid
-		_log.error('faulty default drug data source configuration: %s', default_db)
-		# try to configure
-		configure_drug_data_source(parent = parent)
-		default_db = dbcfg.get2 (
-			option = 'external.drug_data.default_source',
-			workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
-			bias = 'workplace'
-		)
-		# deconfigured or aborted (and thusly still misconfigured) ?
-		try:
-			drug_db = gmMedication.drug_data_source_interfaces[default_db]()
-		except KeyError:
-			_log.error('still faulty default drug data source configuration: %s', default_db)
-			return None
-
-	if patient is not None:
-		drug_db.patient = pat
-
-	return drug_db
-
-#============================================================
-def jump_to_drug_database(patient=None):
-	drug_db = get_drug_database(patient = patient)
-	if drug_db is None:
-		return
-	drug_db.switch_to_frontend(blocking = False)
-
-#============================================================
-def jump_to_ifap_deprecated(import_drugs=False, emr=None):
-
-	if import_drugs and (emr is None):
-		gmDispatcher.send('statustext', msg = _('Cannot import drugs from IFAP into chart without chart.'))
-		return False
-
-	dbcfg = gmCfg.cCfgSQL()
-
-	ifap_cmd = dbcfg.get2 (
-		option = 'external.ifap-win.shell_command',
-		workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
-		bias = 'workplace',
-		default = 'wine "C:\Ifapwin\WIAMDB.EXE"'
-	)
-	found, binary = gmShellAPI.detect_external_binary(ifap_cmd)
-	if not found:
-		gmDispatcher.send('statustext', msg = _('Cannot call IFAP via [%s].') % ifap_cmd)
-		return False
-	ifap_cmd = binary
-
-	if import_drugs:
-		transfer_file = os.path.expanduser(dbcfg.get2 (
-			option = 'external.ifap-win.transfer_file',
-			workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
-			bias = 'workplace',
-			default = '~/.wine/drive_c/Ifapwin/ifap2gnumed.csv'
-		))
-		# file must exist for Ifap to write into it
-		try:
-			f = io.open(transfer_file, mode = 'wt').close()
-		except IOError:
-			_log.exception('Cannot create IFAP <-> GNUmed transfer file [%s]', transfer_file)
-			gmDispatcher.send('statustext', msg = _('Cannot create IFAP <-> GNUmed transfer file [%s].') % transfer_file)
-			return False
-
-	wx.BeginBusyCursor()
-	gmShellAPI.run_command_in_shell(command = ifap_cmd, blocking = import_drugs)
-	wx.EndBusyCursor()
-
-	if import_drugs:
-		# COMMENT: this file must exist PRIOR to invoking IFAP
-		# COMMENT: or else IFAP will not write data into it ...
-		try:
-			csv_file = io.open(transfer_file, mode = 'rt', encoding = 'latin1')						# FIXME: encoding unknown
-		except:
-			_log.exception('cannot access [%s]', fname)
-			csv_file = None
-
-		if csv_file is not None:
-			import csv
-			csv_lines = csv.DictReader (
-				csv_file,
-				fieldnames = u'PZN Handelsname Form Abpackungsmenge Einheit Preis1 Hersteller Preis2 rezeptpflichtig Festbetrag Packungszahl Packungsgr\xf6\xdfe'.split(),
-				delimiter = ';'
-			)
-			# dummy episode for now
-			epi = emr.add_episode(episode_name = _('Current medication'))
-			for line in csv_lines:
-				narr = u'%sx %s %s %s (\u2258 %s %s) von %s (%s)' % (
-					line['Packungszahl'].strip(),
-					line['Handelsname'].strip(),
-					line['Form'].strip(),
-					line[u'Packungsgr\xf6\xdfe'].strip(),
-					line['Abpackungsmenge'].strip(),
-					line['Einheit'].strip(),
-					line['Hersteller'].strip(),
-					line['PZN'].strip()
-				)
-				emr.add_clin_narrative(note = narr, soap_cat = 's', episode = epi)
-			csv_file.close()
-
-	return True
-
-#============================================================
-# ATC related widgets
-#------------------------------------------------------------
-def browse_atc_reference_deprecated(parent=None):
-
-	if parent is None:
-		parent = wx.GetApp().GetTopWindow()
-	#------------------------------------------------------------
-	def refresh(lctrl):
-		atcs = gmATC.get_reference_atcs()
-
-		items = [ [
-			a['atc'],
-			a['term'],
-			gmTools.coalesce(a['unit'], u''),
-			gmTools.coalesce(a['administrative_route'], u''),
-			gmTools.coalesce(a['comment'], u''),
-			a['version'],
-			a['lang']
-		] for a in atcs ]
-		lctrl.set_string_items(items)
-		lctrl.set_data(atcs)
-	#------------------------------------------------------------
-	gmListWidgets.get_choices_from_list (
-		parent = parent,
-		msg = _('\nThe ATC codes as known to GNUmed.\n'),
-		caption = _('Showing ATC codes.'),
-		columns = [ u'ATC', _('Term'), _('Unit'), _(u'Route'), _('Comment'), _('Version'), _('Language') ],
-		single_selection = True,
-		refresh_callback = refresh
-	)
-
-#============================================================
-def update_atc_reference_data():
-
-	dlg = wx.FileDialog (
-		parent = None,
-		message = _('Choose an ATC import config file'),
-		defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')),
-		defaultFile = '',
-		wildcard = "%s (*.conf)|*.conf|%s (*)|*" % (_('config files'), _('all files')),
-		style = wx.OPEN | wx.FILE_MUST_EXIST
-	)
-
-	result = dlg.ShowModal()
-	if result == wx.ID_CANCEL:
-		return
-
-	cfg_file = dlg.GetPath()
-	dlg.Destroy()
-
-	conn = gmAuthWidgets.get_dbowner_connection(procedure = _('importing ATC reference data'))
-	if conn is None:
-		return False
-
-	wx.BeginBusyCursor()
-
-	if gmATC.atc_import(cfg_fname = cfg_file, conn = conn):
-		gmDispatcher.send(signal = 'statustext', msg = _('Successfully imported ATC reference data.'))
-	else:
-		gmDispatcher.send(signal = 'statustext', msg = _('Importing ATC reference data failed.'), beep = True)
-
-	wx.EndBusyCursor()
-	return True
-
-#============================================================
-class cATCPhraseWheel(gmPhraseWheel.cPhraseWheel):
-
-	def __init__(self, *args, **kwargs):
-
-		gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
-		query = u"""
-
-			SELECT DISTINCT ON (label)
-				atc_code,
-				label
-			FROM (
-
-				SELECT
-					code as atc_code,
-					(code || ': ' || term)
-						AS label
-				FROM ref.atc
-				WHERE
-					term %(fragment_condition)s
-						OR
-					code %(fragment_condition)s
-
-				UNION ALL
-
-				SELECT
-					atc_code,
-					(atc_code || ': ' || description)
-						AS label
-				FROM ref.consumable_substance
-				WHERE
-					description %(fragment_condition)s
-						OR
-					atc_code %(fragment_condition)s
-
-				UNION ALL
-
-				SELECT
-					atc_code,
-					(atc_code || ': ' || description || ' (' || preparation || ')')
-						AS label
-				FROM ref.branded_drug
-				WHERE
-					description %(fragment_condition)s
-						OR
-					atc_code %(fragment_condition)s
-
-				-- it would be nice to be able to include clin.vacc_indication but that's hard to do in SQL
-
-			) AS candidates
-			WHERE atc_code IS NOT NULL
-			ORDER BY label
-			LIMIT 50"""
-
-		mp = gmMatchProvider.cMatchProvider_SQL2(queries = query)
-		mp.setThresholds(1, 2, 4)
-#		mp.word_separators = '[ \t=+&:@]+'
-		self.SetToolTipString(_('Select an ATC (Anatomical-Therapeutic-Chemical) code.'))
-		self.matcher = mp
-		self.selection_only = True
-
-#============================================================
-# consumable substances widgets
-#------------------------------------------------------------
-def edit_consumable_substance(parent=None, substance=None, single_entry=False):
-
-	if substance is not None:
-		if substance.is_in_use_by_patients:
-			gmDispatcher.send(signal = 'statustext', msg = _('Cannot edit this substance. It is in use.'), beep = True)
-			return False
-
-	ea = cConsumableSubstanceEAPnl(parent = parent, id = -1)
-	ea.data = substance
-	ea.mode = gmTools.coalesce(substance, 'new', 'edit')
-	dlg = gmEditArea.cGenericEditAreaDlg2(parent = parent, id = -1, edit_area = ea, single_entry = single_entry)
-	dlg.SetTitle(gmTools.coalesce(substance, _('Adding new consumable substance'), _('Editing consumable substance')))
-	if dlg.ShowModal() == wx.ID_OK:
-		dlg.Destroy()
-		return True
-	dlg.Destroy()
-	return False
-
-#------------------------------------------------------------
-def manage_consumable_substances(parent=None):
-
-	if parent is None:
-		parent = wx.GetApp().GetTopWindow()
-	#------------------------------------------------------------
-	def add_from_db(substance):
-		drug_db = get_drug_database(parent = parent)
-		if drug_db is None:
-			return False
-		drug_db.import_drugs()
-		return True
-	#------------------------------------------------------------
-	def edit(substance=None):
-		return edit_consumable_substance(parent = parent, substance = substance, single_entry = (substance is not None))
-	#------------------------------------------------------------
-	def delete(substance):
-		if substance.is_in_use_by_patients:
-			gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete this substance. It is in use.'), beep = True)
-			return False
-
-		return gmMedication.delete_consumable_substance(substance = substance['pk'])
-	#------------------------------------------------------------
-	def refresh(lctrl):
-		substs = gmMedication.get_consumable_substances(order_by = 'description')
-		items = [ [
-			s['description'],
-			s['amount'],
-			s['unit'],
-			gmTools.coalesce(s['atc_code'], u''),
-			s['pk']
-		] for s in substs ]
-		lctrl.set_string_items(items)
-		lctrl.set_data(substs)
-	#------------------------------------------------------------
-	msg = _('\nThese are the consumable substances registered with GNUmed.\n')
-
-	gmListWidgets.get_choices_from_list (
-		parent = parent,
-		msg = msg,
-		caption = _('Showing consumable substances.'),
-		columns = [_('Substance'), _('Amount'), _('Unit'), 'ATC', u'#'],
-		single_selection = True,
-		new_callback = edit,
-		edit_callback = edit,
-		delete_callback = delete,
-		refresh_callback = refresh,
-		left_extra_button = (_('Import'), _('Import consumable substances from a drug database.'), add_from_db)
-	)
-
-
-#============================================================
-# main
-#------------------------------------------------------------
-if __name__ == '__main__':
-
-	if len(sys.argv) < 2:
-		sys.exit()
-
-	if sys.argv[1] != 'test':
-		sys.exit()
-
-	from Gnumed.business import gmPersonSearch
-
-	pat = gmPersonSearch.ask_for_patient()
-	if pat is None:
-		sys.exit()
-	gmPerson.set_active_patient(patient = pat)
-
-	#----------------------------------------
-	app = wx.PyWidgetTester(size = (600, 300))
-#	#app.SetWidget(cATCPhraseWheel, -1)
-	#app.SetWidget(cSubstancePhraseWheel, -1)
-	app.SetWidget(cBrandOrSubstancePhraseWheel, -1)
-	app.MainLoop()
-	#manage_substance_intakes()
diff --git a/client/wxpython/gmTopPanel.py b/client/wxpython/gmTopPanel.py
index 827fcfd..eca0e6b 100644
--- a/client/wxpython/gmTopPanel.py
+++ b/client/wxpython/gmTopPanel.py
@@ -71,13 +71,10 @@ class cTopPnl(wxgTopPnl.wxgTopPnl):
 
 		# client internal signals
 		gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
-		gmDispatcher.connect(signal = u'clin.allgergy_mod_db', receiver = self._on_allergies_change)
-		gmDispatcher.connect(signal = u'clin.allergy_state_mod_db', receiver = self._on_allergies_change)
-		gmDispatcher.connect(signal = u'dem.names_mod_db', receiver = self._on_name_identity_change)
-		gmDispatcher.connect(signal = u'dem.identity_mod_db', receiver = self._on_name_identity_change)
-		gmDispatcher.connect(signal = u'dem.identity_tag_mod_db', receiver = self._on_tag_change)
-
 		gmDispatcher.connect(signal = u'focus_patient_search', receiver = self._on_focus_patient_search)
+
+		gmDispatcher.connect(signal = u'gm_table_mod', receiver = self._on_database_signal)
+
 	#----------------------------------------------
 	# event handling
 	#----------------------------------------------
@@ -89,19 +86,47 @@ class cTopPnl(wxgTopPnl.wxgTopPnl):
 		dlg.ShowModal()
 		return
 	#----------------------------------------------
-	def _on_tag_change(self):
-		self.__update_tags()
-	#----------------------------------------------
-	def _on_name_identity_change(self):
-		self.__update_age_label()
+	def _on_database_signal(self, **kwds):
+
+		if kwds['table'] not in [u'dem.identity', u'dem.names', u'dem.identity_tag', u'clin.allergy', u'clin.allergy_state']:
+			return True
+
+		if self.curr_pat.connected:
+			# signal is not about our patient: ignore signal
+			if int(kwds['pk_identity']) != self.curr_pat.ID:
+				return True
+
+		if kwds['table'] == u'dem.identity':
+			# we don't care about newly INSERTed or DELETEd patients
+			if kwds['operation'] != 'UPDATE':
+				return True
+			self.__update_age_label()
+			return True
+
+		if kwds['table'] == u'dem.names':
+			self.__update_age_label()
+			return True
+
+		if kwds['table'] == u'dem.identity_tag':
+			self.__update_tags()
+			return True
+
+		if kwds['table'] == u'clin.allergy':
+			self.__update_allergies()
+			return True
+
+		if kwds['table'] == u'clin.allergy_state':
+			self.__update_allergies()
+			return True
+
+		return True
+
 	#----------------------------------------------
 	def _on_post_patient_selection(self, **kwargs):
 		self.__update_age_label()
 		self.__update_allergies()
 		self.__update_tags()
-	#-------------------------------------------------------
-	def _on_allergies_change(self, **kwargs):
-		self.__update_allergies()
+
 	#-------------------------------------------------------
 	def _on_focus_patient_search(self, **kwargs):
 		wx.CallAfter(self._TCTRL_patient_selector.SetFocus)
@@ -199,6 +224,13 @@ class cTopPnl(wxgTopPnl.wxgTopPnl):
 	#-------------------------------------------------------
 	def __update_allergies(self, **kwargs):
 
+		if not self.curr_pat.connected:
+			self._LBL_allergies.SetForegroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOWTEXT))
+			self._TCTRL_allergies.SetForegroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOWTEXT))
+			self._TCTRL_allergies.SetValue(u'')
+			self._TCTRL_allergies.SetToolTipString(u'')
+			return
+
 		show_red = True
 
 		emr = self.curr_pat.get_emr()

-- 
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