Package Gnumed :: Package business :: Module gmPerson |
[frames] | no frames]

# Source Code for Module Gnumed.business.gmPerson

       1  # -*- coding: utf-8 -*-
       2
       3  from __future__ import print_function
       4
       5  """GNUmed patient objects.
       6
       7  This is a patient object intended to let a useful client-side
       8  API crystallize from actual use in true XP fashion.
       9  """
      10  #============================================================
      11  __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
      12  __license__ = "GPL"
      13
      14  # std lib
      15  import sys
      16  import os.path
      17  import time
      18  import re as regex
      19  import datetime as pyDT
      20  import threading
      21  import logging
      22  import io
      23  import inspect
      24  from xml.etree import ElementTree as etree
      25
      26
      27  # GNUmed
      28  if __name__ == '__main__':
      29          logging.basicConfig(level = logging.DEBUG)
      30          sys.path.insert(0, '../../')
      31  from Gnumed.pycommon import gmExceptions
      32  from Gnumed.pycommon import gmDispatcher
      33  from Gnumed.pycommon import gmBorg
      34  from Gnumed.pycommon import gmI18N
      35  if __name__ == '__main__':
      36          gmI18N.activate_locale()
      37          gmI18N.install_domain()
      38  from Gnumed.pycommon import gmNull
      39  from Gnumed.pycommon import gmBusinessDBObject
      40  from Gnumed.pycommon import gmTools
      41  from Gnumed.pycommon import gmPG2
      42  from Gnumed.pycommon import gmDateTime
      43  from Gnumed.pycommon import gmMatchProvider
      44  from Gnumed.pycommon import gmLog2
      45  from Gnumed.pycommon import gmHooks
      46
      47  from Gnumed.business import gmDemographicRecord
      48  from Gnumed.business import gmClinicalRecord
      49  from Gnumed.business import gmXdtMappings
      50  from Gnumed.business import gmProviderInbox
      51  from Gnumed.business import gmExportArea
      52  from Gnumed.business import gmBilling
      53  from Gnumed.business import gmAutoHints
      54  from Gnumed.business.gmDocuments import cDocumentFolder
      55
      56
      57  _log = logging.getLogger('gm.person')
      58
      59  __gender_list = None
      60  __gender_idx = None
      61
      62  __gender2salutation_map = None
      63  __gender2string_map = None
      64
      65  #============================================================
      66  _MERGE_SCRIPT_HEADER = """-- GNUmed patient merge script
      67  -- created: %(date)s
      68  -- patient to keep : #%(pat2keep)s
      69  -- patient to merge: #%(pat2del)s
      70  --
      71  -- You can EASILY cause mangled data by uncritically applying this script, so ...
      72  -- ... BE POSITIVELY SURE YOU UNDERSTAND THE FULL EXTENT OF WHAT IT DOES !
      73
      74
      75  --set default_transaction_read_only to off;
      76
      77  BEGIN;
      78  """
      79
      80  #============================================================
      81 -def external_id_exists(pk_issuer, value):
      82          cmd = 'SELECT COUNT(1) FROM dem.lnk_identity2ext_id WHERE fk_origin = %(issuer)s AND external_id = %(val)s'
      83          args = {'issuer': pk_issuer, 'val': value}
      84          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
      85          return rows[0][0]
      86
      87  #============================================================
      88 -def get_potential_person_dupes(lastnames, dob, firstnames=None, active_only=True):
      89          args = {
      90                  'last': lastnames,
      91                  'dob': dob
      92          }
      93          where_parts = [
      94                  "lastnames = %(last)s",
      95                  "dem.date_trunc_utc('day', dob) = dem.date_trunc_utc('day', %(dob)s)"
      96          ]
      97          if firstnames is not None:
      98                  if firstnames.strip() != '':
      99                          #where_parts.append(u"position(%(first)s in firstnames) = 1")
     100                          where_parts.append("firstnames ~* %(first)s")
     101                          args['first'] = '\\m' + firstnames
     102          if active_only:
     103                  cmd = """SELECT COUNT(1) FROM dem.v_active_persons WHERE %s""" % ' AND '.join(where_parts)
     104          else:
     105                  cmd = """SELECT COUNT(1) FROM dem.v_all_persons WHERE %s""" % ' AND '.join(where_parts)
     106          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     107          return rows[0][0]
     108
     109  #============================================================
     110 -def this_person_exists(lastnames, firstnames, dob, comment):
     111          # backend also looks at gender (IOW, only fails on same-gender dupes)
     112          # implement in plpgsql and re-use in both validation trigger and here
     113          if comment is not None:
     114                  comment = comment.strip()
     115                  if comment == u'':
     116                          comment = None
     117          args = {
     118                  'last': lastnames.strip(),
     119                  'first': firstnames.strip(),
     120                  'dob': dob,
     121                  'cmt': comment
     122          }
     123          where_parts = [
     124                  u'lower(lastnames) = lower(%(last)s)',
     125                  u'lower(firstnames) = lower(%(first)s)',
     126                  u"dem.date_trunc_utc('day', dob) IS NOT DISTINCT FROM dem.date_trunc_utc('day', %(dob)s)",
     127                  u'lower(comment) IS NOT DISTINCT FROM lower(%(cmt)s)'
     128          ]
     129          cmd = u"SELECT COUNT(1) FROM dem.v_persons WHERE %s" % u' AND '.join(where_parts)
     130          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     131          return rows[0][0]
     132
     133  #============================================================
     134  # FIXME: make this work as a mapping type, too
     135 -class cDTO_person(object):
     136
     137 -        def __init__(self):
     138                  self.identity = None
     139                  self.external_ids = []
     140                  self.comm_channels = []
     141                  self.addresses = []
     142
     143                  self.firstnames = None
     144                  self.lastnames = None
     145                  self.title = None
     146                  self.gender = None
     147                  self.dob = None
     148                  self.dob_is_estimated = False
     149                  self.source = self.__class__.__name__
     150
     151                  self.dob_formats = None
     152                  self.dob_tz = None
     153
     154          #--------------------------------------------------------
     155          # external API
     156          #--------------------------------------------------------
     157 -        def keys(self):
     158                  return 'firstnames lastnames dob gender title'.split()
     159          #--------------------------------------------------------
     160 -        def delete_from_source(self):
     161                  pass
     162          #--------------------------------------------------------
     163 -        def _is_unique(self):
     164                  where_snippets = [
     165                          'firstnames = %(first)s',
     166                          'lastnames = %(last)s'
     167                  ]
     168                  args = {
     169                          'first': self.firstnames,
     170                          'last': self.lastnames
     171                  }
     172                  if self.dob is not None:
     173                          where_snippets.append("dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %(dob)s)")
     174                          args['dob'] = self.dob.replace(hour = 23, minute = 59, second = 59)
     175                  if self.gender is not None:
     176                          where_snippets.append('gender = %(sex)s')
     177                          args['sex'] = self.gender
     178                  cmd = 'SELECT count(1) FROM dem.v_person_names WHERE %s' % ' AND '.join(where_snippets)
     179                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     180
     181                  return rows[0][0] == 1
     182
     183          is_unique = property(_is_unique)
     184
     185          #--------------------------------------------------------
     186 -        def _exists(self):
     187                  where_snippets = [
     188                          'firstnames = %(first)s',
     189                          'lastnames = %(last)s'
     190                  ]
     191                  args = {
     192                          'first': self.firstnames,
     193                          'last': self.lastnames
     194                  }
     195                  if self.dob is not None:
     196                          where_snippets.append("dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %(dob)s)")
     197                          args['dob'] = self.dob.replace(hour = 23, minute = 59, second = 59)
     198                  if self.gender is not None:
     199                          where_snippets.append('gender = %(sex)s')
     200                          args['sex'] = self.gender
     201                  cmd = 'SELECT count(1) FROM dem.v_person_names WHERE %s' % ' AND '.join(where_snippets)
     202                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     203
     204                  return rows[0][0] > 0
     205
     206          exists = property(_exists)
     207
     208          #--------------------------------------------------------
     209 -        def get_candidate_identities(self, can_create=False):
     210                  """Generate generic queries.
     211
     212                  - not locale dependant
     213                  - data -> firstnames, lastnames, dob, gender
     214
     215                  shall we mogrify name parts ? probably not as external
     216                  sources should know what they do
     217
     218                  finds by inactive name, too, but then shows
     219                  the corresponding active name ;-)
     220
     221                  Returns list of matching identities (may be empty)
     222                  or None if it was told to create an identity but couldn't.
     223                  """
     224                  where_snippets = []
     225                  args = {}
     226
     227                  where_snippets.append('lower(firstnames) = lower(%(first)s)')
     228                  args['first'] = self.firstnames
     229
     230                  where_snippets.append('lower(lastnames) = lower(%(last)s)')
     231                  args['last'] = self.lastnames
     232
     233                  if self.dob is not None:
     234                          where_snippets.append("dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %(dob)s)")
     235                          args['dob'] = self.dob.replace(hour = 23, minute = 59, second = 59)
     236
     237                  if self.gender is not None:
     238                          where_snippets.append('lower(gender) = lower(%(sex)s)')
     239                          args['sex'] = self.gender
     240
     241                  # FIXME: allow disabled persons ?
     242                  cmd = """
     243                          SELECT *, '%s' AS match_type
     244                          FROM dem.v_active_persons
     245                          WHERE
     246                                  pk_identity IN (
     247                                          SELECT pk_identity FROM dem.v_person_names WHERE %s
     248                                  )
     249                          ORDER BY lastnames, firstnames, dob""" % (
     250                  _('external patient source (name, gender, date of birth)'),
     251                  ' AND '.join(where_snippets)
     252                  )
     253
     254                  try:
     255                          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx=True)
     256                  except:
     257                          _log.error('cannot get candidate identities for dto "%s"' % self)
     258                          _log.exception('query %s' % cmd)
     259                          rows = []
     260
     261                  if len(rows) == 0:
     262                          _log.debug('no candidate identity matches found')
     263                          if not can_create:
     264                                  return []
     265                          ident = self.import_into_database()
     266                          if ident is None:
     267                                  return None
     268                          identities = [ident]
     269                  else:
     270                          identities = [ cPerson(row = {'pk_field': 'pk_identity', 'data': row, 'idx': idx}) for row in rows ]
     271
     272                  return identities
     273          #--------------------------------------------------------
     274 -        def import_into_database(self):
     275                  """Imports self into the database."""
     276
     277                  self.identity = create_identity (
     278                          firstnames = self.firstnames,
     279                          lastnames = self.lastnames,
     280                          gender = self.gender,
     281                          dob = self.dob
     282                  )
     283
     284                  if self.identity is None:
     285                          return None
     286
     287                  self.identity['dob_is_estimated'] = self.dob_is_estimated is True
     288                  if self.title is not None:
     289                          self.identity['title'] = self.title
     290                  self.identity.save()
     291
     292                  for ext_id in self.external_ids:
     293                          try:
     294                                  self.identity.add_external_id (
     295                                          type_name = ext_id['name'],
     296                                          value = ext_id['value'],
     297                                          issuer = ext_id['issuer'],
     298                                          comment = ext_id['comment']
     299                                  )
     300                          except Exception:
     301                                  _log.exception('cannot import <external ID> from external data source')
     302                                  gmLog2.log_stack_trace()
     303
     304                  for comm in self.comm_channels:
     305                          try:
     306                                  self.identity.link_comm_channel (
     307                                          comm_medium = comm['channel'],
     308                                          url = comm['url']
     309                                  )
     310                          except Exception:
     311                                  _log.exception('cannot import <comm channel> from external data source')
     312                                  gmLog2.log_stack_trace()
     313
     314                  for adr in self.addresses:
     315                          try:
     316                                  self.identity.link_address (
     317                                          adr_type = adr['type'],
     318                                          number = adr['number'],
     319                                          subunit = adr['subunit'],
     320                                          street = adr['street'],
     321                                          postcode = adr['zip'],
     322                                          urb = adr['urb'],
     323                                          region_code = adr['region_code'],
     324                                          country_code = adr['country_code']
     325                                  )
     326                          except Exception:
     327                                  _log.exception('cannot import <address> from external data source')
     328                                  gmLog2.log_stack_trace()
     329
     330                  return self.identity
     331          #--------------------------------------------------------
     332 -        def import_extra_data(self, *args, **kwargs):
     333                  pass
     334          #--------------------------------------------------------
     335 -        def remember_external_id(self, name=None, value=None, issuer=None, comment=None):
     336                  value = value.strip()
     337                  if value == '':
     338                          return
     339                  name = name.strip()
     340                  if name == '':
     341                          raise ValueError(_('<name> cannot be empty'))
     342                  issuer = issuer.strip()
     343                  if issuer == '':
     344                          raise ValueError(_('<issuer> cannot be empty'))
     345                  self.external_ids.append({'name': name, 'value': value, 'issuer': issuer, 'comment': comment})
     346          #--------------------------------------------------------
     347 -        def remember_comm_channel(self, channel=None, url=None):
     348                  url = url.strip()
     349                  if url == '':
     350                          return
     351                  channel = channel.strip()
     352                  if channel == '':
     353                          raise ValueError(_('<channel> cannot be empty'))
     354                  self.comm_channels.append({'channel': channel, 'url': url})
     355          #--------------------------------------------------------
     356 -        def remember_address(self, number=None, street=None, urb=None, region_code=None, zip=None, country_code=None, adr_type=None, subunit=None):
     357                  number = number.strip()
     358                  if number == '':
     359                          raise ValueError(_('<number> cannot be empty'))
     360                  street = street.strip()
     361                  if street == '':
     362                          raise ValueError(_('<street> cannot be empty'))
     363                  urb = urb.strip()
     364                  if urb == '':
     365                          raise ValueError(_('<urb> cannot be empty'))
     366                  zip = zip.strip()
     367                  if zip == '':
     368                          raise ValueError(_('<zip> cannot be empty'))
     369                  country_code = country_code.strip()
     370                  if country_code == '':
     371                          raise ValueError(_('<country_code> cannot be empty'))
     372                  if region_code is not None:
     373                          region_code = region_code.strip()
     374                  if region_code in [None, '']:
     375                          region_code = '??'
     376                  self.addresses.append ({
     377                          'type': adr_type,
     378                          'number': number,
     379                          'subunit': subunit,
     380                          'street': street,
     381                          'zip': zip,
     382                          'urb': urb,
     383                          'region_code': region_code,
     384                          'country_code': country_code
     385                  })
     386          #--------------------------------------------------------
     387          # customizing behaviour
     388          #--------------------------------------------------------
     389 -        def __str__(self):
     390                  return '<%s (%s) @ %s: %s %s (%s) %s%s>' % (
     391                          self.__class__.__name__,
     392                          self.source,
     393                          id(self),
     394                          self.lastnames.upper(),
     395                          self.firstnames,
     396                          self.gender,
     397                          gmTools.bool2subst(self.dob_is_estimated, '~', '', ''),
     398                          self.dob
     399                  )
     400          #--------------------------------------------------------
     401 -        def __setattr__(self, attr, val):
     402                  """Do some sanity checks on self.* access."""
     403
     404                  if attr == 'gender':
     405                          if val is None:
     406                                  object.__setattr__(self, attr, val)
     407                                  return
     408                          glist, idx = get_gender_list()
     409                          for gender in glist:
     410                                  if str(val) in [gender[0], gender[1], gender[2], gender[3]]:
     411                                          val = gender[idx['tag']]
     412                                          object.__setattr__(self, attr, val)
     413                                          return
     414                          raise ValueError('invalid gender: [%s]' % val)
     415
     416                  if attr == 'dob':
     417                          if val is not None:
     418                                  if isinstance(val, str):
     419                                          dob = self.__parse_dob_str(val)
     420                                          if dob is None:
     421                                                  raise ValueError('cannot parse DOB [%s]' % val)
     422                                          val = dob
     423                                  if not isinstance(val, pyDT.datetime):
     424                                          raise TypeError('invalid type for DOB (must be datetime.datetime): %s [%s]' % (type(val), val))
     425                                  if val.tzinfo is None:
     426                                          raise ValueError('datetime.datetime instance is lacking a time zone: [%s]' % val.isoformat())
     427
     428                  object.__setattr__(self, attr, val)
     429                  return
     430          #--------------------------------------------------------
     431 -        def __getitem__(self, attr):
     432                  return getattr(self, attr)
     433          #--------------------------------------------------------
     434 -        def __parse_dob_str(self, dob_str):
     435                  if self.dob_formats is None:
     436                          return None
     437                  for dob_format in self.dob_formats:
     438                          try:
     439                                  dob = pyDT.datetime.strptime(dob_str, dob_format)
     440                          except ValueError:
     441                                  _log.exception('cannot parse DOB [%s] with [%s]', dob_str, dob_format)
     442                                  continue
     443                          if self.dob_tz is None:
     444                                  raise ValueError('lacking TZ information in DOB [%s] and/or format [%s]' % (dob_str, self.dob_format))
     445                          dob = dob.replace(tzinfo = self.dob_tz)
     446                          return dob
     447                  return None
     448
     449  #============================================================
     450 -class cPersonName(gmBusinessDBObject.cBusinessDBObject):
     451          _cmd_fetch_payload = "SELECT * FROM dem.v_person_names WHERE pk_name = %s"
     452          _cmds_store_payload = [
     453                  """UPDATE dem.names SET
     454                          active = FALSE
     455                  WHERE
     456                          %(active_name)s IS TRUE                         -- act only when needed and only
     457                                  AND
     458                          id_identity = %(pk_identity)s           -- on names of this identity
     459                                  AND
     460                          active IS TRUE                                          -- which are active
     461                                  AND
     462                          id != %(pk_name)s                                       -- but NOT *this* name
     463                          """,
     464                  """update dem.names set
     465                          active = %(active_name)s,
     466                          preferred = %(preferred)s,
     467                          comment = %(comment)s
     468                  where
     469                          id = %(pk_name)s and
     470                          id_identity = %(pk_identity)s and       -- belt and suspenders
     471                          xmin = %(xmin_name)s""",
     472                  """select xmin as xmin_name from dem.names where id = %(pk_name)s"""
     473          ]
     474          _updatable_fields = ['active_name', 'preferred', 'comment']
     475          #--------------------------------------------------------
     476 -        def __setitem__(self, attribute, value):
     477                  if attribute == 'active_name':
     478                          # cannot *directly* deactivate a name, only indirectly
     479                          # by activating another one
     480                          # FIXME: should be done at DB level
     481                          if self._payload[self._idx['active_name']] is True:
     482                                  return
     483                  gmBusinessDBObject.cBusinessDBObject.__setitem__(self, attribute, value)
     484          #--------------------------------------------------------
     485 -        def _get_description(self):
     486                  return '%(last)s, %(title)s %(first)s%(nick)s' % {
     487                          'last': self._payload[self._idx['lastnames']],
     488                          'title': gmTools.coalesce (
     489                                  self._payload[self._idx['title']],
     490                                  map_gender2salutation(self._payload[self._idx['gender']])
     491                          ),
     492                          'first': self._payload[self._idx['firstnames']],
     493                          'nick': gmTools.coalesce(self._payload[self._idx['preferred']], '', " '%s'", '%s')
     494                  }
     495
     496          description = property(_get_description, lambda x:x)
     497
     498  #============================================================
     499  _SQL_get_active_person = "SELECT * FROM dem.v_active_persons WHERE pk_identity = %s"
     500  _SQL_get_any_person = "SELECT * FROM dem.v_all_persons WHERE pk_identity = %s"
     501
     502 -class cPerson(gmBusinessDBObject.cBusinessDBObject):
     503          _cmd_fetch_payload = _SQL_get_any_person
     504          _cmds_store_payload = [
     505                  """UPDATE dem.identity SET
     506                                  gender = %(gender)s,
     507                                  dob = %(dob)s,
     508                                  dob_is_estimated = %(dob_is_estimated)s,
     509                                  tob = %(tob)s,
     510                                  title = gm.nullify_empty_string(%(title)s),
     511                                  fk_marital_status = %(pk_marital_status)s,
     512                                  deceased = %(deceased)s,
     513                                  emergency_contact = gm.nullify_empty_string(%(emergency_contact)s),
     514                                  fk_emergency_contact = %(pk_emergency_contact)s,
     515                                  fk_primary_provider = %(pk_primary_provider)s,
     516                                  comment = gm.nullify_empty_string(%(comment)s)
     517                          WHERE
     518                                  pk = %(pk_identity)s and
     519                                  xmin = %(xmin_identity)s
     520                          RETURNING
     521                                  xmin AS xmin_identity"""
     522          ]
     523          _updatable_fields = [
     524                  "title",
     525                  "dob",
     526                  "tob",
     527                  "gender",
     528                  "pk_marital_status",
     529                  'deceased',
     530                  'emergency_contact',
     531                  'pk_emergency_contact',
     532                  'pk_primary_provider',
     533                  'comment',
     534                  'dob_is_estimated'
     535          ]
     536          #--------------------------------------------------------
     537 -        def _get_ID(self):
     538                  return self._payload[self._idx['pk_identity']]
     539 -        def _set_ID(self, value):
     540                  raise AttributeError('setting ID of identity is not allowed')
     541
     542          ID = property(_get_ID, _set_ID)
     543
     544          #--------------------------------------------------------
     545 -        def __setitem__(self, attribute, value):
     546
     547                  if attribute == 'dob':
     548                          if value is not None:
     549
     550                                  if isinstance(value, pyDT.datetime):
     551                                          if value.tzinfo is None:
     552                                                  raise ValueError('datetime.datetime instance is lacking a time zone: [%s]' % dt.isoformat())
     553                                  else:
     554                                          raise TypeError('[%s]: type [%s] (%s) invalid for attribute [dob], must be datetime.datetime or None' % (self.__class__.__name__, type(value), value))
     555
     556                                  # compare DOB at seconds level
     557                                  if self._payload[self._idx['dob']] is not None:
     558                                          old_dob = gmDateTime.pydt_strftime (
     559                                                  self._payload[self._idx['dob']],
     560                                                  format = '%Y %m %d %H %M %S',
     561                                                  accuracy = gmDateTime.acc_seconds
     562                                          )
     563                                          new_dob = gmDateTime.pydt_strftime (
     564                                                  value,
     565                                                  format = '%Y %m %d %H %M %S',
     566                                                  accuracy = gmDateTime.acc_seconds
     567                                          )
     568                                          if new_dob == old_dob:
     569                                                  return
     570
     571                  gmBusinessDBObject.cBusinessDBObject.__setitem__(self, attribute, value)
     572
     573          #--------------------------------------------------------
     574 -        def cleanup(self):
     575                  pass
     576
     577          #--------------------------------------------------------
     578 -        def _get_is_patient(self):
     579                  return identity_is_patient(self._payload[self._idx['pk_identity']])
     580
     581 -        def _set_is_patient(self, turn_into_patient):
     582                  if turn_into_patient:
     583                          return turn_identity_into_patient(self._payload[self._idx['pk_identity']])
     584                  return False
     585
     586          is_patient = property(_get_is_patient, _set_is_patient)
     587
     588          #--------------------------------------------------------
     589 -        def _get_as_patient(self):
     590                  return cPatient(self._payload[self._idx['pk_identity']])
     591
     592          as_patient = property(_get_as_patient, lambda x:x)
     593
     594          #--------------------------------------------------------
     595 -        def _get_staff_id(self):
     596                  cmd = "SELECT pk FROM dem.staff WHERE fk_identity = %(pk)s"
     597                  args = {'pk': self._payload[self._idx['pk_identity']]}
     598                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     599                  if len(rows) == 0:
     600                          return None
     601                  return rows[0][0]
     602
     603          staff_id = property(_get_staff_id, lambda x:x)
     604
     605          #--------------------------------------------------------
     606          # identity API
     607          #--------------------------------------------------------
     608 -        def _get_gender_symbol(self):
     609                  return map_gender2symbol[self._payload[self._idx['gender']]]
     610
     611          gender_symbol = property(_get_gender_symbol, lambda x:x)
     612          #--------------------------------------------------------
     613 -        def _get_gender_string(self):
     614                  return map_gender2string(gender = self._payload[self._idx['gender']])
     615
     616          gender_string = property(_get_gender_string, lambda x:x)
     617          #--------------------------------------------------------
     618 -        def _get_gender_list(self):
     619                  gender_list, tmp = get_gender_list()
     620                  return gender_list
     621
     622          gender_list = property(_get_gender_list, lambda x:x)
     623          #--------------------------------------------------------
     624 -        def get_active_name(self):
     625                  names = self.get_names(active_only = True)
     626                  if len(names) == 0:
     627                          _log.error('cannot retrieve active name for patient [%s]', self._payload[self._idx['pk_identity']])
     628                          return None
     629                  return names[0]
     630
     631          active_name = property(get_active_name, lambda x:x)
     632          #--------------------------------------------------------
     633 -        def get_names(self, active_only=False, exclude_active=False):
     634
     635                  args = {'pk_pat': self._payload[self._idx['pk_identity']]}
     636                  where_parts = ['pk_identity = %(pk_pat)s']
     637                  if active_only:
     638                          where_parts.append('active_name is True')
     639                  if exclude_active:
     640                          where_parts.append('active_name is False')
     641                  cmd = """
     642                          SELECT *
     643                          FROM dem.v_person_names
     644                          WHERE %s
     645                          ORDER BY active_name DESC, lastnames, firstnames
     646                  """ % ' AND '.join(where_parts)
     647                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     648
     649                  if len(rows) == 0:
     650                          # no names registered for patient
     651                          return []
     652
     653                  names = [ cPersonName(row = {'idx': idx, 'data': r, 'pk_field': 'pk_name'}) for r in rows ]
     654                  return names
     655          #--------------------------------------------------------
     656 -        def get_description_gender(self, with_nickname=True):
     657                  if with_nickname:
     658                          template = _('%(last)s,%(title)s %(first)s%(nick)s (%(sex)s)')
     659                  else:
     660                          template = _('%(last)s,%(title)s %(first)s (%(sex)s)')
     661                  return template % {
     662                          'last': self._payload[self._idx['lastnames']],
     663                          'title': gmTools.coalesce(self._payload[self._idx['title']], '', ' %s'),
     664                          'first': self._payload[self._idx['firstnames']],
     665                          'nick': gmTools.coalesce(self._payload[self._idx['preferred']], '', " '%s'"),
     666                          'sex': self.gender_symbol
     667                  }
     668
     669          #--------------------------------------------------------
     670 -        def get_description(self, with_nickname=True):
     671                  if with_nickname:
     672                          template = _('%(last)s,%(title)s %(first)s%(nick)s')
     673                  else:
     674                          template = _('%(last)s,%(title)s %(first)s')
     675                  return template % {
     676                          'last': self._payload[self._idx['lastnames']],
     677                          'title': gmTools.coalesce(self._payload[self._idx['title']], '', ' %s'),
     678                          'first': self._payload[self._idx['firstnames']],
     679                          'nick': gmTools.coalesce(self._payload[self._idx['preferred']], '', " '%s'")
     680                  }
     681
     682          #--------------------------------------------------------
     683 -        def add_name(self, firstnames, lastnames, active=True):
     684                  """Add a name.
     685
     686                  @param firstnames The first names.
     687                  @param lastnames The last names.
     688                  @param active When True, the new name will become the active one (hence setting other names to inactive)
     689                  @type active A bool instance
     690                  """
     691                  name = create_name(self.ID, firstnames, lastnames, active)
     692                  if active:
     693                          self.refetch_payload()
     694                  return name
     695
     696          #--------------------------------------------------------
     697 -        def delete_name(self, name=None):
     698                  cmd = "delete from dem.names where id = %(name)s and id_identity = %(pat)s"
     699                  args = {'name': name['pk_name'], 'pat': self.ID}
     700                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     701                  # can't have been the active name as that would raise an
     702                  # exception (since no active name would be left) so no
     703                  # data refetch needed
     704
     705          #--------------------------------------------------------
     706 -        def set_nickname(self, nickname=None):
     707                  """
     708                  Set the nickname. Setting the nickname only makes sense for the currently
     709                  active name.
     710                  @param nickname The preferred/nick/warrior name to set.
     711                  """
     712                  if self._payload[self._idx['preferred']] == nickname:
     713                          return True
     714                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': "SELECT dem.set_nickname(%s, %s)", 'args': [self.ID, nickname]}])
     715                  # setting nickname doesn't change dem.identity, so other fields
     716                  # of dem.v_active_persons do not get changed as a consequence of
     717                  # setting the nickname, hence locally setting nickname matches
     718                  # in-database reality
     719                  self._payload[self._idx['preferred']] = nickname
     720                  #self.refetch_payload()
     721                  return True
     722
     723          #--------------------------------------------------------
     724 -        def get_tags(self, order_by=None):
     725                  if order_by is None:
     726                          order_by = ''
     727                  else:
     728                          order_by = 'ORDER BY %s' % order_by
     729
     730                  cmd = gmDemographicRecord._SQL_get_person_tags % ('pk_identity = %%(pat)s %s' % order_by)
     731                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pat': self.ID}}], get_col_idx = True)
     732
     733                  return [ gmDemographicRecord.cPersonTag(row = {'data': r, 'idx': idx, 'pk_field': 'pk_identity_tag'}) for r in rows ]
     734
     735          tags = property(get_tags, lambda x:x)
     736
     737          #--------------------------------------------------------
     738 -        def add_tag(self, tag):
     739                  args = {
     740                          'tag': tag,
     741                          'identity': self.ID
     742                  }
     743
     744                  # already exists ?
     745                  cmd = "SELECT pk FROM dem.identity_tag WHERE fk_tag = %(tag)s AND fk_identity = %(identity)s"
     746                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     747                  if len(rows) > 0:
     748                          return gmDemographicRecord.cPersonTag(aPK_obj = rows[0]['pk'])
     749
     750                  # no, add
     751                  cmd = """
     752                          INSERT INTO dem.identity_tag (
     753                                  fk_tag,
     754                                  fk_identity
     755                          ) VALUES (
     756                                  %(tag)s,
     757                                  %(identity)s
     758                          )
     759                          RETURNING pk
     760                  """
     761                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
     762                  return gmDemographicRecord.cPersonTag(aPK_obj = rows[0]['pk'])
     763
     764          #--------------------------------------------------------
     765 -        def remove_tag(self, tag):
     766                  cmd = "DELETE FROM dem.identity_tag WHERE pk = %(pk)s"
     767                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': tag}}])
     768
     769          #--------------------------------------------------------
     770          # external ID API
     771          #
     772          # since external IDs are not treated as first class
     773          # citizens (classes in their own right, that is), we
     774          # handle them *entirely* within cPerson, also they
     775          # only make sense with one single person (like names)
     776          # and are not reused (like addresses), so they are
     777          # truly added/deleted, not just linked/unlinked
     778          #--------------------------------------------------------
     779 -        def add_external_id(self, type_name=None, value=None, issuer=None, comment=None, pk_type=None):
     780                  """Adds an external ID to the patient.
     781
     782                  creates ID type if necessary
     783                  """
     784                  # check for existing ID
     785                  if pk_type is not None:
     786                          cmd = """
     787                                  select * from dem.v_external_ids4identity where
     788                                  pk_identity = %(pat)s and
     789                                  pk_type = %(pk_type)s and
     790                                  value = %(val)s"""
     791                  else:
     792                          # by type/value/issuer
     793                          if issuer is None:
     794                                  cmd = """
     795                                          select * from dem.v_external_ids4identity where
     796                                          pk_identity = %(pat)s and
     797                                          name = %(name)s and
     798                                          value = %(val)s"""
     799                          else:
     800                                  cmd = """
     801                                          select * from dem.v_external_ids4identity where
     802                                                  pk_identity = %(pat)s and
     803                                                  name = %(name)s and
     804                                                  value = %(val)s and
     805                                                  issuer = %(issuer)s"""
     806                  args = {
     807                          'pat': self.ID,
     808                          'name': type_name,
     809                          'val': value,
     810                          'issuer': issuer,
     811                          'pk_type': pk_type
     812                  }
     813                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
     814
     815                  # create new ID if not found
     816                  if len(rows) == 0:
     817
     818                          args = {
     819                                  'pat': self.ID,
     820                                  'val': value,
     821                                  'type_name': type_name,
     822                                  'pk_type': pk_type,
     823                                  'issuer': issuer,
     824                                  'comment': comment
     825                          }
     826
     827                          if pk_type is None:
     828                                  cmd = """insert into dem.lnk_identity2ext_id (external_id, fk_origin, comment, id_identity) values (
     829                                          %(val)s,
     830                                          (select dem.add_external_id_type(%(type_name)s, %(issuer)s)),
     831                                          %(comment)s,
     832                                          %(pat)s
     833                                  )"""
     834                          else:
     835                                  cmd = """insert into dem.lnk_identity2ext_id (external_id, fk_origin, comment, id_identity) values (
     836                                          %(val)s,
     837                                          %(pk_type)s,
     838                                          %(comment)s,
     839                                          %(pat)s
     840                                  )"""
     841
     842                          rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     843
     844                  # or update comment of existing ID
     845                  else:
     846                          row = rows[0]
     847                          if comment is not None:
     848                                  # comment not already there ?
     849                                  if gmTools.coalesce(row['comment'], '').find(comment.strip()) == -1:
     850                                          comment = '%s%s' % (gmTools.coalesce(row['comment'], '', '%s // '), comment.strip)
     851                                          cmd = "update dem.lnk_identity2ext_id set comment = %(comment)s where id=%(pk)s"
     852                                          args = {'comment': comment, 'pk': row['pk_id']}
     853                                          rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     854
     855          #--------------------------------------------------------
     856 -        def update_external_id(self, pk_id=None, type=None, value=None, issuer=None, comment=None):
     857                  """Edits an existing external ID.
     858
     859                  Creates ID type if necessary.
     860                  """
     861                  cmd = """
     862                          UPDATE dem.lnk_identity2ext_id SET
     863                                  fk_origin = (SELECT dem.add_external_id_type(%(type)s, %(issuer)s)),
     864                                  external_id = %(value)s,
     865                                  comment = gm.nullify_empty_string(%(comment)s)
     866                          WHERE
     867                                  id = %(pk)s
     868                  """
     869                  args = {'pk': pk_id, 'value': value, 'type': type, 'issuer': issuer, 'comment': comment}
     870                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     871
     872          #--------------------------------------------------------
     873 -        def get_external_ids(self, id_type=None, issuer=None):
     874                  where_parts = ['pk_identity = %(pat)s']
     875                  args = {'pat': self.ID}
     876
     877                  if id_type is not None:
     878                          where_parts.append('name = %(name)s')
     879                          args['name'] = id_type.strip()
     880
     881                  if issuer is not None:
     882                          where_parts.append('issuer = %(issuer)s')
     883                          args['issuer'] = issuer.strip()
     884
     885                  cmd = "SELECT * FROM dem.v_external_ids4identity WHERE %s" % ' AND '.join(where_parts)
     886                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
     887
     888                  return rows
     889
     890          external_ids = property(get_external_ids, lambda x:x)
     891
     892          #--------------------------------------------------------
     893 -        def delete_external_id(self, pk_ext_id=None):
     894                  cmd = """
     895                          DELETE FROM dem.lnk_identity2ext_id
     896                          WHERE id_identity = %(pat)s AND id = %(pk)s"""
     897                  args = {'pat': self.ID, 'pk': pk_ext_id}
     898                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     899
     900          #--------------------------------------------------------
     901 -        def suggest_external_id(self, target=None, encoding=None):
     902                  name = self.active_name
     903                  last = ' '.join(p for p in name['lastnames'].split("-"))
     904                  last = ' '.join(p for p in last.split("."))
     905                  last = ' '.join(p for p in last.split("'"))
     906                  last = ''.join(gmTools.capitalize(text = p, mode = gmTools.CAPS_FIRST_ONLY) for p in last.split(' '))
     907                  first = ' '.join(p for p in name['firstnames'].split("-"))
     908                  first = ' '.join(p for p in first.split("."))
     909                  first = ' '.join(p for p in first.split("'"))
     910                  first = ''.join(gmTools.capitalize(text = p, mode = gmTools.CAPS_FIRST_ONLY) for p in first.split(' '))
     911                  suggestion = 'GMd-%s%s%s%s%s' % (
     912                          gmTools.coalesce(target, '', '%s-'),
     913                          last,
     914                          first,
     915                          self.get_formatted_dob(format = '-%Y%m%d', none_string = ''),
     916                          gmTools.coalesce(self['gender'], '', '-%s')
     917                  )
     918                  try:
     919                          import unidecode
     920                          return unidecode.unidecode(suggestion)
     921                  except ImportError:
     922                          _log.debug('cannot transliterate external ID suggestion, <unidecode> module not installed')
     923                  if encoding is None:
     924                          return suggestion
     925                  return suggestion.encode(encoding)
     926
     927          external_id_suggestion = property(suggest_external_id, lambda x:x)
     928
     929          #--------------------------------------------------------
     930 -        def suggest_external_ids(self, target=None, encoding=None):
     931                  names2use = [self.active_name]
     932                  names2use.extend(self.get_names(active_only = False, exclude_active = True))
     933                  target = gmTools.coalesce(target, '', '%s-')
     934                  dob = self.get_formatted_dob(format = '-%Y%m%d', none_string = '')
     935                  gender = gmTools.coalesce(self['gender'], '', '-%s')
     936                  suggestions = []
     937                  for name in names2use:
     938                          last = ' '.join(p for p in name['lastnames'].split("-"))
     939                          last = ' '.join(p for p in last.split("."))
     940                          last = ' '.join(p for p in last.split("'"))
     941                          last = ''.join(gmTools.capitalize(text = p, mode = gmTools.CAPS_FIRST_ONLY) for p in last.split(' '))
     942                          first = ' '.join(p for p in name['firstnames'].split("-"))
     943                          first = ' '.join(p for p in first.split("."))
     944                          first = ' '.join(p for p in first.split("'"))
     945                          first = ''.join(gmTools.capitalize(text = p, mode = gmTools.CAPS_FIRST_ONLY) for p in first.split(' '))
     946                          suggestion = 'GMd-%s%s%s%s%s' % (target, last, first, dob, gender)
     947                          try:
     948                                  import unidecode
     949                                  suggestions.append(unidecode.unidecode(suggestion))
     950                                  continue
     951                          except ImportError:
     952                                  _log.debug('cannot transliterate external ID suggestion, <unidecode> module not installed')
     953                          if encoding is None:
     954                                  suggestions.append(suggestion)
     955                          else:
     956                                  suggestions.append(suggestion.encode(encoding))
     957                  return suggestions
     958
     959          #--------------------------------------------------------
     960          #--------------------------------------------------------
     961 -        def assimilate_identity(self, other_identity=None, link_obj=None):
     962                  """Merge another identity into this one.
     963
     964                  Keep this one. Delete other one."""
     965
     966                  if other_identity.ID == self.ID:
     967                          return True, None
     968
     969                  curr_pat = gmCurrentPatient()
     970                  if curr_pat.connected:
     971                          if other_identity.ID == curr_pat.ID:
     972                                  return False, _('Cannot merge active patient into another patient.')
     973
     974                  now_here = gmDateTime.pydt_strftime(gmDateTime.pydt_now_here())
     975                  distinguisher = _('merge of #%s into #%s @ %s') % (other_identity.ID, self.ID, now_here)
     976
     977                  queries = []
     978                  args = {'pat2del': other_identity.ID, 'pat2keep': self.ID}
     979
     980                  # merge allergy state
     981                  queries.append ({
     982                          'cmd': """
     983                                  UPDATE clin.allergy_state SET
     984                                          has_allergy = greatest (
     985                                                  (SELECT has_allergy FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2del)s),
     986                                                  (SELECT has_allergy FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2keep)s)
     987                                          ),
     988                                          -- perhaps use least() to play it safe and make it appear longer ago than it might have been, actually ?
     989                                          last_confirmed = greatest (
     990                                                  (SELECT last_confirmed FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2del)s),
     991                                                  (SELECT last_confirmed FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2keep)s)
     992                                          )
     993                                  WHERE
     994                                          pk = (SELECT pk_allergy_state FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2keep)s)
     995                          """,
     996                          'args': args
     997                  })
     998                  # delete old allergy state
     999                  queries.append ({
    1000                          'cmd': 'DELETE FROM clin.allergy_state WHERE pk = (SELECT pk_allergy_state FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2del)s)',
    1001                          'args': args
    1002                  })
    1003
    1004                  # merge patient proxy
    1005                  queries.append ({
    1006                          'cmd': """
    1007                                  UPDATE clin.patient SET
    1008                                          edc = coalesce (
    1009                                                  edc,
    1010                                                  (SELECT edc FROM clin.patient WHERE fk_identity = %(pat2del)s)
    1011                                          )
    1012                                  WHERE
    1013                                          fk_identity = %(pat2keep)s
    1014                          """,
    1015                          'args': args
    1016                  })
    1017
    1018                  # transfer names
    1019                  # 1) hard-disambiguate all inactive names in old patient
    1020                  #    (the active one will be disambiguated upon being moved)
    1021                  queries.append ({
    1022                          'cmd': """
    1023                                  UPDATE dem.names d_n1 SET
    1024                                          comment = coalesce (
    1025                                                  comment, ''
    1026                                          ) || coalesce (
    1027                                                  ' (from identity: "' || (SELECT comment FROM dem.identity WHERE pk = %%(pat2del)s) || '")',
    1028                                                  ''
    1029                                          ) || ' (during: "%s")'
    1030                                  WHERE
    1031                                          d_n1.id_identity = %%(pat2del)s
    1032                                  """ % distinguisher,
    1033                          'args': args
    1034                  })
    1035                  # 2) move inactive ones (dupes are expected to have been eliminated in step 1 above)
    1036                  queries.append ({
    1037                          'cmd': u"""
    1038                                  UPDATE dem.names d_n SET
    1039                                          id_identity = %(pat2keep)s,
    1040                                          lastnames = lastnames || ' [' || random()::TEXT || ']'
    1041                                  WHERE
    1042                                          d_n.id_identity = %(pat2del)s
    1043                                                  AND
    1044                                          d_n.active IS false
    1045                                  """,
    1046                          'args': args
    1047                  })
    1048                  # 3) copy active name into pat2keep as an inactive name,
    1049                  #    because each identity MUST have at LEAST one name,
    1050                  #    we can't simply UPDATE over to pat2keep
    1051                  #    also, needs de-duplication or else it would conflict with
    1052                  #    *itself* on pat2keep
    1053                  queries.append ({
    1054                          'cmd': """
    1055                                  INSERT INTO dem.names (
    1056                                          id_identity, active, firstnames, preferred, comment,
    1057                                          lastnames
    1058                                  )
    1059                                          SELECT
    1060                                                  %(pat2keep)s, false, firstnames, preferred, comment,
    1061                                                  lastnames || ' [' || random()::text || ']'
    1062                                          FROM dem.names d_n
    1063                                          WHERE
    1064                                                  d_n.id_identity = %(pat2del)s
    1065                                                          AND
    1066                                                  d_n.active IS true
    1067                                  """,
    1068                          'args': args
    1069                  })
    1070
    1071                  # disambiguate potential dupes
    1072                  # - same-url comm channels
    1073                  queries.append ({
    1074                          'cmd': """
    1075                                  UPDATE dem.lnk_identity2comm
    1076                                  SET url = url || ' (%s)'
    1077                                  WHERE
    1078                                          fk_identity = %%(pat2del)s
    1079                                                  AND
    1080                                          EXISTS (
    1081                                                  SELECT 1 FROM dem.lnk_identity2comm d_li2c
    1082                                                  WHERE d_li2c.fk_identity = %%(pat2keep)s AND d_li2c.url = url
    1083                                          )
    1084                                  """ % distinguisher,
    1085                          'args': args
    1086                  })
    1087                  # - same-value external IDs
    1088                  queries.append ({
    1089                          'cmd': """
    1090                                  UPDATE dem.lnk_identity2ext_id
    1091                                  SET external_id = external_id || ' (%s)'
    1092                                  WHERE
    1093                                          id_identity = %%(pat2del)s
    1094                                                  AND
    1095                                          EXISTS (
    1096                                                  SELECT 1 FROM dem.lnk_identity2ext_id d_li2e
    1097                                                  WHERE
    1098                                                          d_li2e.id_identity = %%(pat2keep)s
    1099                                                                  AND
    1100                                                          d_li2e.external_id = external_id
    1101                                                                  AND
    1102                                                          d_li2e.fk_origin = fk_origin
    1103                                          )
    1104                                  """ % distinguisher,
    1105                          'args': args
    1106                  })
    1107                  # - same addresses
    1108                  queries.append ({
    1109                          'cmd': """
    1110                                  DELETE FROM dem.lnk_person_org_address
    1111                                  WHERE
    1112                                          id_identity = %(pat2del)s
    1113                                                  AND
    1114                                          id_address IN (
    1115                                                  SELECT id_address FROM dem.lnk_person_org_address d_lpoa
    1116                                                  WHERE d_lpoa.id_identity = %(pat2keep)s
    1117                                          )
    1118                          """,
    1119                          'args': args
    1120                  })
    1121
    1122                  # find FKs pointing to dem.identity.pk
    1123                  FKs = gmPG2.get_foreign_keys2column (
    1124                          schema = 'dem',
    1125                          table = 'identity',
    1126                          column = 'pk'
    1127                  )
    1128                  # find FKs pointing to clin.patient.fk_identity
    1129                  FKs.extend (gmPG2.get_foreign_keys2column (
    1130                          schema = 'clin',
    1131                          table = 'patient',
    1132                          column = 'fk_identity'
    1133                  ))
    1134
    1135                  # generate UPDATEs
    1136                  cmd_template = 'UPDATE %s SET %s = %%(pat2keep)s WHERE %s = %%(pat2del)s'
    1137                  for FK in FKs:
    1138                          if FK['referencing_table'] in ['dem.names', 'clin.patient']:
    1139                                  continue
    1140                          queries.append ({
    1141                                  'cmd': cmd_template % (FK['referencing_table'], FK['referencing_column'], FK['referencing_column']),
    1142                                  'args': args
    1143                          })
    1144
    1145                  # delete old patient proxy
    1146                  queries.append ({
    1147                          'cmd': 'DELETE FROM clin.patient WHERE fk_identity = %(pat2del)s',
    1148                          'args': args
    1149                  })
    1150
    1151                  # remove old identity entry
    1152                  queries.append ({
    1153                          'cmd': 'delete from dem.identity where pk = %(pat2del)s',
    1154                          'args': args
    1155                  })
    1156
    1157                  script_name = gmTools.get_unique_filename(prefix = 'gm-assimilate-%(pat2del)s-into-%(pat2keep)s-' % args, suffix = '.sql')
    1158                  _log.warning('identity [%s] is about to assimilate identity [%s], SQL script [%s]', self.ID, other_identity.ID, script_name)
    1159
    1160                  script = io.open(script_name, 'wt')
    1161                  args['date'] = gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), '%Y %B %d  %H:%M')
    1162                  script.write(_MERGE_SCRIPT_HEADER % args)
    1163                  for query in queries:
    1164                          script.write(query['cmd'] % args)
    1165                          script.write(';\n')
    1166                  script.write('\nROLLBACK;\n')
    1167                  script.write('--COMMIT;\n')
    1168                  script.close()
    1169
    1170                  try:
    1171                          gmPG2.run_rw_queries(link_obj = link_obj, queries = queries, end_tx = True)
    1172                  except Exception:
    1173                          return False, _('The merge failed. Check the log and [%s]') % script_name
    1174
    1175                  self.add_external_id (
    1176                          type_name = 'merged GNUmed identity primary key',
    1177                          value = 'GNUmed::pk::%s' % other_identity.ID,
    1178                          issuer = 'GNUmed'
    1179                  )
    1180
    1181                  return True, None
    1182
    1183          #--------------------------------------------------------
    1184          #--------------------------------------------------------
    1185 -        def put_on_waiting_list(self, urgency=0, comment=None, zone=None):
    1186                  cmd = """
    1187                          insert into clin.waiting_list (fk_patient, urgency, comment, area, list_position)
    1188                          values (
    1189                                  %(pat)s,
    1190                                  %(urg)s,
    1191                                  %(cmt)s,
    1192                                  %(area)s,
    1193                                  (select coalesce((max(list_position) + 1), 1) from clin.waiting_list)
    1194                          )"""
    1195                  args = {'pat': self.ID, 'urg': urgency, 'cmt': comment, 'area': zone}
    1196                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], verbose = True)
    1197                  gmHooks.run_hook_script(hook = 'after_waiting_list_modified')
    1198
    1199          #--------------------------------------------------------
    1200 -        def get_waiting_list_entry(self):
    1201                  cmd = """SELECT * FROM clin.v_waiting_list WHERE pk_identity = %(pat)s"""
    1202                  args = {'pat': self.ID}
    1203                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
    1204                  return rows
    1205
    1206          waiting_list_entries = property(get_waiting_list_entry, lambda x:x)
    1207
    1208          #--------------------------------------------------------
    1209 -        def _get_export_area(self):
    1210                  return gmExportArea.cExportArea(self.ID)
    1211
    1212          export_area = property(_get_export_area, lambda x:x)
    1213          #--------------------------------------------------------
    1214 -        def export_as_gdt(self, filename=None, encoding='iso-8859-15', external_id_type=None):
    1215
    1216                  template = '%s%s%s\r\n'
    1217
    1218                  if filename is None:
    1219                          filename = gmTools.get_unique_filename (
    1220                                  prefix = 'gm-patient-',
    1221                                  suffix = '.gdt'
    1222                          )
    1223
    1224                  gdt_file = io.open(filename, mode = 'wt', encoding = encoding, errors = 'strict')
    1225
    1226                  gdt_file.write(template % ('013', '8000', '6301'))
    1227                  gdt_file.write(template % ('013', '9218', '2.10'))
    1228                  if external_id_type is None:
    1229                          gdt_file.write(template % ('%03d' % (9 + len(str(self.ID))), '3000', self.ID))
    1230                  else:
    1231                          ext_ids = self.get_external_ids(id_type = external_id_type)
    1232                          if len(ext_ids) > 0:
    1233                                  gdt_file.write(template % ('%03d' % (9 + len(ext_ids[0]['value'])), '3000', ext_ids[0]['value']))
    1234                  gdt_file.write(template % ('%03d' % (9 + len(self._payload[self._idx['lastnames']])), '3101', self._payload[self._idx['lastnames']]))
    1235                  gdt_file.write(template % ('%03d' % (9 + len(self._payload[self._idx['firstnames']])), '3102', self._payload[self._idx['firstnames']]))
    1236                  gdt_file.write(template % ('%03d' % (9 + len(self._payload[self._idx['dob']].strftime('%d%m%Y'))), '3103', self._payload[self._idx['dob']].strftime('%d%m%Y')))
    1237                  gdt_file.write(template % ('010', '3110', gmXdtMappings.map_gender_gm2xdt[self._payload[self._idx['gender']]]))
    1238                  gdt_file.write(template % ('025', '6330', 'GNUmed::9206::encoding'))
    1239                  gdt_file.write(template % ('%03d' % (9 + len(encoding)), '6331', encoding))
    1240                  if external_id_type is None:
    1241                          gdt_file.write(template % ('029', '6332', 'GNUmed::3000::source'))
    1242                          gdt_file.write(template % ('017', '6333', 'internal'))
    1243                  else:
    1244                          if len(ext_ids) > 0:
    1245                                  gdt_file.write(template % ('029', '6332', 'GNUmed::3000::source'))
    1246                                  gdt_file.write(template % ('%03d' % (9 + len(external_id_type)), '6333', external_id_type))
    1247
    1248                  gdt_file.close()
    1249
    1250                  return filename
    1251          #--------------------------------------------------------
    1252 -        def export_as_xml_linuxmednews(self, filename=None):
    1253
    1254                  if filename is None:
    1255                          filename = gmTools.get_unique_filename (
    1256                                  prefix = 'gm-LinuxMedNews_demographics-',
    1257                                  suffix = '.xml'
    1258                          )
    1259
    1260                  dob_format = '%Y-%m-%d'
    1261                  pat = etree.Element('patient')
    1262
    1263                  first = etree.SubElement(pat, 'firstname')
    1264                  first.text = gmTools.coalesce(self._payload[self._idx['firstnames']], '')
    1265
    1266                  last = etree.SubElement(pat, 'lastname')
    1267                  last.text = gmTools.coalesce(self._payload[self._idx['lastnames']], '')
    1268
    1269                  # privacy
    1270                  #middle = etree.SubElement(pat, u'middlename')
    1271                  #middle.set(u'comment', _('preferred name/call name/...'))
    1272                  #middle.text = gmTools.coalesce(self._payload[self._idx['preferred']], u'')
    1273
    1274                  pref = etree.SubElement(pat, 'name_prefix')
    1275                  pref.text = gmTools.coalesce(self._payload[self._idx['title']], '')
    1276
    1277                  suff = etree.SubElement(pat, 'name_suffix')
    1278                  suff.text = ''
    1279
    1280                  dob = etree.SubElement(pat, 'DOB')
    1281                  dob.set('format', dob_format)
    1282                  dob.text = gmDateTime.pydt_strftime(self._payload[self._idx['dob']], dob_format, accuracy = gmDateTime.acc_days, none_str = '')
    1283
    1284                  gender = etree.SubElement(pat, 'gender')
    1285                  gender.set('comment', self.gender_string)
    1286                  if self._payload[self._idx['gender']] is None:
    1287                          gender.text = ''
    1288                  else:
    1289                          gender.text = map_gender2mf[self._payload[self._idx['gender']]]
    1290
    1291                  home = etree.SubElement(pat, 'home_address')
    1292                  adrs = self.get_addresses(address_type = 'home')
    1293                  if len(adrs) > 0:
    1294                          adr = adrs[0]
    1295                          city = etree.SubElement(home, 'city')
    1296                          city.set('comment', gmTools.coalesce(adr['suburb'], ''))
    1297                          city.text = gmTools.coalesce(adr['urb'], '')
    1298
    1299                          region = etree.SubElement(home, 'region')
    1300                          region.set('comment', gmTools.coalesce(adr['l10n_region'], ''))
    1301                          region.text = gmTools.coalesce(adr['code_region'], '')
    1302
    1303                          zipcode = etree.SubElement(home, 'postal_code')
    1304                          zipcode.text = gmTools.coalesce(adr['postcode'], '')
    1305
    1306                          street = etree.SubElement(home, 'street')
    1307                          street.set('comment', gmTools.coalesce(adr['notes_street'], ''))
    1308                          street.text = gmTools.coalesce(adr['street'], '')
    1309
    1310                          no = etree.SubElement(home, 'number')
    1311                          no.set('subunit', gmTools.coalesce(adr['subunit'], ''))
    1312                          no.set('comment', gmTools.coalesce(adr['notes_subunit'], ''))
    1313                          no.text = gmTools.coalesce(adr['number'], '')
    1314
    1315                          country = etree.SubElement(home, 'country')
    1316                          country.set('comment', adr['l10n_country'])
    1317                          country.text = gmTools.coalesce(adr['code_country'], '')
    1318
    1319                  phone = etree.SubElement(pat, 'home_phone')
    1320                  rec = self.get_comm_channels(comm_medium = 'homephone')
    1321                  if len(rec) > 0:
    1322                          if not rec[0]['is_confidential']:
    1323                                  phone.set('comment', gmTools.coalesce(rec[0]['comment'], ''))
    1324                                  phone.text = rec[0]['url']
    1325
    1326                  phone = etree.SubElement(pat, 'work_phone')
    1327                  rec = self.get_comm_channels(comm_medium = 'workphone')
    1328                  if len(rec) > 0:
    1329                          if not rec[0]['is_confidential']:
    1330                                  phone.set('comment', gmTools.coalesce(rec[0]['comment'], ''))
    1331                                  phone.text = rec[0]['url']
    1332
    1333                  phone = etree.SubElement(pat, 'cell_phone')
    1334                  rec = self.get_comm_channels(comm_medium = 'mobile')
    1335                  if len(rec) > 0:
    1336                          if not rec[0]['is_confidential']:
    1337                                  phone.set('comment', gmTools.coalesce(rec[0]['comment'], ''))
    1338                                  phone.text = rec[0]['url']
    1339
    1340                  tree = etree.ElementTree(pat)
    1341                  tree.write(filename, encoding = 'UTF-8')
    1342
    1343                  return filename
    1344
    1345          #--------------------------------------------------------
    1346 -        def export_as_vcard(self, filename=None):
    1347                  # http://vobject.skyhouseconsulting.com/usage.html
    1348                  # http://en.wikipedia.org/wiki/VCard
    1349                  # http://svn.osafoundation.org/vobject/trunk/vobject/vcard.py
    1350                  # http://www.ietf.org/rfc/rfc2426.txt
    1351
    1352                  dob_format = '%Y%m%d'
    1353
    1354                  import vobject
    1355
    1356                  vc = vobject.vCard()
    1357                  vc.add('kind')
    1358                  vc.kind.value = 'individual'
    1359
    1360                  vc.add('fn')
    1361                  vc.fn.value = self.get_description(with_nickname = False)       # privacy
    1362                  vc.add('n')
    1363                  vc.n.value = vobject.vcard.Name(family = self._payload[self._idx['lastnames']], given = self._payload[self._idx['firstnames']])
    1364                  # privacy
    1365                  #vc.add(u'nickname')
    1366                  #vc.nickname.value = gmTools.coalesce(self._payload[self._idx['preferred']], u'')
    1367                  vc.add('title')
    1368                  vc.title.value = gmTools.coalesce(self._payload[self._idx['title']], '')
    1369                  vc.add('gender')
    1370                  # FIXME: dont know how to add gender_string after ';'
    1371                  vc.gender.value = map_gender2vcard[self._payload[self._idx['gender']]]#, self.gender_string
    1372                  vc.add('bday')
    1373                  vc.bday.value = gmDateTime.pydt_strftime(self._payload[self._idx['dob']], dob_format, accuracy = gmDateTime.acc_days, none_str = '')
    1374
    1375                  channels = self.get_comm_channels(comm_medium = 'homephone')
    1376                  if len(channels) > 0:
    1377                          if not channels[0]['is_confidential']:
    1378                                  vc.add('tel')
    1379                                  vc.tel.value = channels[0]['url']
    1380                                  vc.tel.type_param = 'HOME'
    1381                  channels = self.get_comm_channels(comm_medium = 'workphone')
    1382                  if len(channels) > 0:
    1383                          if not channels[0]['is_confidential']:
    1384                                  vc.add('tel')
    1385                                  vc.tel.value = channels[0]['url']
    1386                                  vc.tel.type_param = 'WORK'
    1387                  channels = self.get_comm_channels(comm_medium = 'mobile')
    1388                  if len(channels) > 0:
    1389                          if not channels[0]['is_confidential']:
    1390                                  vc.add('tel')
    1391                                  vc.tel.value = channels[0]['url']
    1392                                  vc.tel.type_param = 'CELL'
    1393                  channels = self.get_comm_channels(comm_medium = 'fax')
    1394                  if len(channels) > 0:
    1395                          if not channels[0]['is_confidential']:
    1396                                  vc.add('tel')
    1397                                  vc.tel.value = channels[0]['url']
    1398                                  vc.tel.type_param = 'FAX'
    1399                  channels = self.get_comm_channels(comm_medium = 'email')
    1400                  if len(channels) > 0:
    1401                          if not channels[0]['is_confidential']:
    1402                                  vc.add('email')
    1403                                  vc.tel.value = channels[0]['url']
    1404                                  vc.tel.type_param = 'INTERNET'
    1405                  channels = self.get_comm_channels(comm_medium = 'web')
    1406                  if len(channels) > 0:
    1407                          if not channels[0]['is_confidential']:
    1408                                  vc.add('url')
    1409                                  vc.tel.value = channels[0]['url']
    1410                                  vc.tel.type_param = 'INTERNET'
    1411
    1412                  adrs = self.get_addresses(address_type = 'home')
    1413                  if len(adrs) > 0:
    1414                          home_adr = adrs[0]
    1415                          vc.add('adr')
    1416                          vc.adr.type_param = 'HOME'
    1417                          vc.adr.value = vobject.vcard.Address()
    1418                          vc_adr = vc.adr.value
    1419                          vc_adr.extended = gmTools.coalesce(home_adr['subunit'], '')
    1420                          vc_adr.street = gmTools.coalesce(home_adr['street'], '', '%s ') + gmTools.coalesce(home_adr['number'], '')
    1421                          vc_adr.region = gmTools.coalesce(home_adr['l10n_region'], '')
    1422                          vc_adr.code = gmTools.coalesce(home_adr['postcode'], '')
    1423                          vc_adr.city = gmTools.coalesce(home_adr['urb'], '')
    1424                          vc_adr.country = gmTools.coalesce(home_adr['l10n_country'], '')
    1425
    1426                  #photo (base64)
    1427
    1428                  if filename is None:
    1429                          filename = gmTools.get_unique_filename (
    1430                                  prefix = 'gm-patient-',
    1431                                  suffix = '.vcf'
    1432                          )
    1433                  vcf = io.open(filename, mode = 'wt', encoding = 'utf8')
    1434                  try:
    1435                          vcf.write(vc.serialize())
    1436                  except UnicodeDecodeError:
    1437                          _log.exception('failed to serialize VCF data')
    1438                          vcf.close()
    1439                          return 'cannot-serialize.vcf'
    1440                  vcf.close()
    1441
    1442                  return filename
    1443
    1444          #--------------------------------------------------------
    1445 -        def export_as_mecard(self, filename=None):
    1446                  if filename is None:
    1447                          filename = gmTools.get_unique_filename (
    1448                                  prefix = 'gm-patient-',
    1449                                  suffix = '.mcf'
    1450                          )
    1451                  with io.open(filename, mode = 'wt', encoding = 'utf8') as mecard_file:
    1452                          mecard_file.write(self.MECARD)
    1453                  return filename
    1454
    1455          #--------------------------------------------------------
    1456 -        def _get_mecard(self):
    1457                  """
    1458                  http://blog.thenetimpact.com/2011/07/decoding-qr-codes-how-to-format-data-for-qr-code-generators/
    1459                  https://www.nttdocomo.co.jp/english/service/developer/make/content/barcode/function/application/addressbook/index.html
    1460
    1461                  MECARD:N:NAME;ADR:pobox,subunit,unit,street,ort,region,zip,country;TEL:111111111;FAX:22222222;EMAIL:mail@praxis.org;
    1462
    1463
    1464                  MECARD:N:lastname,firstname;BDAY:YYYYMMDD;ADR:pobox,subunit,number,street,location,region,zip,country;;
    1465                  MECARD:N:$<lastname::::>$,$<firstname::::>$;BDAY:$<date_of_birth::%Y%m%d::>$;ADR:,$<adr_subunit::home::>$,$<adr_number::home::>$,$<adr_street::home::>$,$<adr_location::home::>$,,$<adr_postcode::home::>$,$<adr_country::home::>$;;
    1466                  """
    1467                  MECARD = 'MECARD:N:%s,%s;' % (
    1468                          self._payload[self._idx['lastnames']],
    1469                          self._payload[self._idx['firstnames']]
    1470                  )
    1471                  if self._payload[self._idx['dob']] is not None:
    1472                          MECARD += 'BDAY:%s;' % gmDateTime.pydt_strftime (
    1473                                  self._payload[self._idx['dob']],
    1474                                  '%Y%m%d',
    1475                                  accuracy = gmDateTime.acc_days,
    1476                                  none_str = ''
    1477                          )
    1478                  adrs = self.get_addresses(address_type = 'home')
    1479                  if len(adrs) > 0:
    1480                          MECARD += 'ADR:,%(subunit)s,%(number)s,%(street)s,%(urb)s,,%(postcode)s,%(l10n_country)s;' % adrs[0]
    1481                  comms = self.get_comm_channels(comm_medium = 'homephone')
    1482                  if len(comms) > 0:
    1483                          if not comms[0]['is_confidential']:
    1484                                  MECARD += 'TEL:%s;' % comms[0]['url']
    1485                  comms = self.get_comm_channels(comm_medium = 'fax')
    1486                  if len(comms) > 0:
    1487                          if not comms[0]['is_confidential']:
    1488                                  MECARD += 'FAX:%s;' % comms[0]['url']
    1489                  comms = self.get_comm_channels(comm_medium = 'email')
    1490                  if len(comms) > 0:
    1491                          if not comms[0]['is_confidential']:
    1492                                  MECARD += 'EMAIL:%s;' % comms[0]['url']
    1493                  return MECARD
    1494
    1495          MECARD = property(_get_mecard)
    1496
    1497          #--------------------------------------------------------
    1498          # occupations API
    1499          #--------------------------------------------------------
    1500 -        def get_occupations(self):
    1501                  return gmDemographicRecord.get_occupations(pk_identity = self.pk_obj)
    1502
    1503          #--------------------------------------------------------
    1504 -        def link_occupation(self, occupation=None, activities=None):
    1505                  """Link an occupation with a patient, creating the occupation if it does not exists.
    1506
    1507                          @param occupation The name of the occupation to link the patient to.
    1508                  """
    1509                  if (activities is None) and (occupation is None):
    1510                          return True
    1511
    1512                  occupation = occupation.strip()
    1513                  if len(occupation) == 0:
    1514                          return True
    1515
    1516                  if activities is not None:
    1517                          activities = activities.strip()
    1518
    1519                  args = {'act': activities, 'pat_id': self.pk_obj, 'job': occupation}
    1520
    1521                  cmd = "select activities from dem.v_person_jobs where pk_identity = %(pat_id)s and l10n_occupation = _(%(job)s)"
    1522                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
    1523
    1524                  queries = []
    1525                  if len(rows) == 0:
    1526                          queries.append ({
    1527                                  'cmd': "INSERT INTO dem.lnk_job2person (fk_identity, fk_occupation, activities) VALUES (%(pat_id)s, dem.create_occupation(%(job)s), %(act)s)",
    1528                                  'args': args
    1529                          })
    1530                  else:
    1531                          if rows[0]['activities'] != activities:
    1532                                  queries.append ({
    1533                                          'cmd': "update dem.lnk_job2person set activities=%(act)s where fk_identity=%(pat_id)s and fk_occupation=(select id from dem.occupation where _(name) = _(%(job)s))",
    1534                                          'args': args
    1535                                  })
    1536
    1537                  rows, idx = gmPG2.run_rw_queries(queries = queries)
    1538
    1539                  return True
    1540          #--------------------------------------------------------
    1541 -        def unlink_occupation(self, occupation=None):
    1542                  if occupation is None:
    1543                          return True
    1544                  occupation = occupation.strip()
    1545                  cmd = "delete from dem.lnk_job2person where fk_identity=%(pk)s and fk_occupation in (select id from dem.occupation where _(name) = _(%(job)s))"
    1546                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj, 'job': occupation}}])
    1547                  return True
    1548          #--------------------------------------------------------
    1549          # comms API
    1550          #--------------------------------------------------------
    1551 -        def get_comm_channels(self, comm_medium=None):
    1552                  cmd = "select * from dem.v_person_comms where pk_identity = %s"
    1553                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}], get_col_idx = True)
    1554
    1555                  filtered = rows
    1556
    1557                  if comm_medium is not None:
    1558                          filtered = []
    1559                          for row in rows:
    1560                                  if row['comm_type'] == comm_medium:
    1561                                          filtered.append(row)
    1562
    1563                  return [ gmDemographicRecord.cCommChannel(row = {
    1564                                          'pk_field': 'pk_lnk_identity2comm',
    1565                                          'data': r,
    1566                                          'idx': idx
    1567                                  }) for r in filtered
    1568                          ]
    1569
    1570          comm_channels = property(get_comm_channels, lambda x:x)
    1571          #--------------------------------------------------------
    1572 -        def link_comm_channel(self, comm_medium=None, url=None, is_confidential=False, pk_channel_type=None):
    1573                  """Link a communication medium with a patient.
    1574
    1575                  @param comm_medium The name of the communication medium.
    1576                  @param url The communication resource locator.
    1577                  @type url A str instance.
    1578                  @param is_confidential Wether the data must be treated as confidential.
    1579                  @type is_confidential A bool instance.
    1580                  """
    1581                  comm_channel = gmDemographicRecord.create_comm_channel (
    1582                          comm_medium = comm_medium,
    1583                          url = url,
    1584                          is_confidential = is_confidential,
    1585                          pk_channel_type = pk_channel_type,
    1586                          pk_identity = self.pk_obj
    1587                  )
    1588                  return comm_channel
    1589          #--------------------------------------------------------
    1590 -        def unlink_comm_channel(self, comm_channel=None):
    1591                  gmDemographicRecord.delete_comm_channel (
    1592                          pk = comm_channel['pk_lnk_identity2comm'],
    1593                          pk_patient = self.pk_obj
    1594                  )
    1595          #--------------------------------------------------------
    1596          # contacts API
    1597          #--------------------------------------------------------
    1598 -        def get_addresses(self, address_type=None):
    1599
    1600                  cmd = "SELECT * FROM dem.v_pat_addresses WHERE pk_identity = %(pat)s"
    1601                  args = {'pat': self.pk_obj}
    1602                  if address_type is not None:
    1603                          cmd = cmd + " AND address_type = %(typ)s"
    1604                          args['typ'] = address_type
    1605
    1606                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1607
    1608                  return [
    1609                          gmDemographicRecord.cPatientAddress(row = {'idx': idx, 'data': r, 'pk_field': 'pk_address'})
    1610                          for r in rows
    1611                  ]
    1612          #--------------------------------------------------------
    1613 -        def link_address(self, number=None, street=None, postcode=None, urb=None, region_code=None, country_code=None, subunit=None, suburb=None, id_type=None, address=None, adr_type=None):
    1614                  """Link an address with a patient, creating the address if it does not exists.
    1615
    1616                  @param id_type The primary key of the address type.
    1617                  """
    1618                  if address is None:
    1619                          address = gmDemographicRecord.create_address (
    1620                                  country_code = country_code,
    1621                                  region_code = region_code,
    1622                                  urb = urb,
    1623                                  suburb = suburb,
    1624                                  postcode = postcode,
    1625                                  street = street,
    1626                                  number = number,
    1627                                  subunit = subunit
    1628                          )
    1629
    1630                  if address is None:
    1631                          return None
    1632
    1633                  # already linked ?
    1634                  cmd = "SELECT id_address FROM dem.lnk_person_org_address WHERE id_identity = %(pat)s AND id_address = %(adr)s"
    1635                  args = {'pat': self.pk_obj, 'adr': address['pk_address']}
    1636                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
    1637
    1638                  # no, link to person
    1639                  if len(rows) == 0:
    1640                          args = {'id': self.pk_obj, 'adr': address['pk_address'], 'type': id_type}
    1641                          cmd = """
    1642                                  INSERT INTO dem.lnk_person_org_address(id_identity, id_address)
    1643                                  VALUES (%(id)s, %(adr)s)
    1644                                  RETURNING id_address"""
    1645                          rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True)
    1646
    1647                  linked_adr = gmDemographicRecord.cPatientAddress(aPK_obj = rows[0]['id_address'])
    1648
    1649                  # possibly change type
    1650                  if id_type is None:
    1651                          if adr_type is not None:
    1652                                  id_type = gmDemographicRecord.create_address_type(address_type = adr_type)
    1653                  if id_type is not None:
    1654                          linked_adr['pk_address_type'] = id_type
    1655                          linked_adr.save()
    1656
    1657                  return linked_adr
    1658          #----------------------------------------------------------------------
    1659 -        def unlink_address(self, address=None, pk_address=None):
    1660                  """Remove an address from the patient.
    1661
    1662                  The address itself stays in the database.
    1663                  The address can be either cAdress or cPatientAdress.
    1664                  """
    1665                  if pk_address is None:
    1666                          args = {'person': self.pk_obj, 'adr': address['pk_address']}
    1667                  else:
    1668                          args = {'person': self.pk_obj, 'adr': pk_address}
    1669                  cmd = """
    1670                          DELETE FROM dem.lnk_person_org_address
    1671                          WHERE
    1672                                  dem.lnk_person_org_address.id_identity = %(person)s
    1673                                          AND
    1674                                  dem.lnk_person_org_address.id_address = %(adr)s
    1675                                          AND
    1676                                  NOT EXISTS(SELECT 1 FROM bill.bill WHERE fk_receiver_address = dem.lnk_person_org_address.id)
    1677                          """
    1678                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    1679          #----------------------------------------------------------------------
    1680          # bills API
    1681          #----------------------------------------------------------------------
    1682 -        def get_bills(self, order_by=None, pk_patient=None):
    1683                  return gmBilling.get_bills (
    1684                          order_by = order_by,
    1685                          pk_patient = self.pk_obj
    1686                  )
    1687
    1688          bills = property(get_bills, lambda x:x)
    1689          #----------------------------------------------------------------------
    1690          # relatives API
    1691          #----------------------------------------------------------------------
    1692 -        def get_relatives(self):
    1693                  cmd = """
    1694                          SELECT
    1695                                  d_rt.description,
    1696                                  d_vap.*
    1697                          FROM
    1698                                  dem.v_all_persons d_vap,
    1699                                  dem.relation_types d_rt,
    1700                                  dem.lnk_person2relative d_lp2r
    1701                          WHERE
    1702                                  (       d_lp2r.id_identity = %(pk)s
    1703                                                  AND
    1704                                          d_vap.pk_identity = d_lp2r.id_relative
    1705                                                  AND
    1706                                          d_rt.id = d_lp2r.id_relation_type
    1707                                  ) or (
    1708                                          d_lp2r.id_relative = %(pk)s
    1709                                                  AND
    1710                                          d_vap.pk_identity = d_lp2r.id_identity
    1711                                                  AND
    1712                                          d_rt.inverse = d_lp2r.id_relation_type
    1713                                  )"""
    1714                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}])
    1715                  if len(rows) == 0:
    1716                          return []
    1717                  return [(row[0], cPerson(row = {'data': row[1:], 'idx':idx, 'pk_field': 'pk_identity'})) for row in rows]
    1718          #--------------------------------------------------------
    1719 -        def link_new_relative(self, rel_type = 'parent'):
    1720                  # create new relative
    1721                  id_new_relative = create_dummy_identity()
    1722
    1723                  relative = cPerson(aPK_obj=id_new_relative)
    1724                  # pre-fill with data from ourselves
    1725  #               relative.copy_addresses(self)
    1726                  relative.add_name( '**?**', self.get_names()['lastnames'])
    1727                  # and link the two
    1728                  if 'relatives' in self._ext_cache:
    1729                          del self._ext_cache['relatives']
    1730                  cmd = """
    1731                          insert into dem.lnk_person2relative (
    1732                                  id_identity, id_relative, id_relation_type
    1733                          ) values (
    1734                                  %s, %s, (select id from dem.relation_types where description = %s)
    1735                          )"""
    1736                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': [self.ID, id_new_relative, rel_type  ]}])
    1737                  return True
    1738          #----------------------------------------------------------------------
    1739 -        def delete_relative(self, relation):
    1740                  # unlink only, don't delete relative itself
    1741                  self.set_relative(None, relation)
    1742          #--------------------------------------------------------
    1743 -        def _get_emergency_contact_from_database(self):
    1744                  if self._payload[self._idx['pk_emergency_contact']] is None:
    1745                          return None
    1746                  return cPerson(self._payload[self._idx['pk_emergency_contact']])
    1747
    1748          emergency_contact_in_database = property(_get_emergency_contact_from_database, lambda x:x)
    1749
    1750          #----------------------------------------------------------------------
    1751          # age/dob related
    1752          #----------------------------------------------------------------------
    1753 -        def get_formatted_dob(self, format='%Y %b %d', none_string=None, honor_estimation=False):
    1754                  return gmDateTime.format_dob (
    1755                          self._payload[self._idx['dob']],
    1756                          format = format,
    1757                          none_string = none_string,
    1758                          dob_is_estimated = self._payload[self._idx['dob_is_estimated']] and honor_estimation
    1759                  )
    1760
    1761          #----------------------------------------------------------------------
    1762 -        def get_medical_age(self):
    1763                  dob = self['dob']
    1764
    1765                  if dob is None:
    1766                          return '??'
    1767
    1768                  if dob > gmDateTime.pydt_now_here():
    1769                          return _('invalid age: DOB in the future')
    1770
    1771                  death = self['deceased']
    1772
    1773                  if death is None:
    1774                          return '%s%s' % (
    1775                                  gmTools.bool2subst (
    1776                                          self._payload[self._idx['dob_is_estimated']],
    1777                                          gmTools.u_almost_equal_to,
    1778                                          ''
    1779                                  ),
    1780                                  gmDateTime.format_apparent_age_medically (
    1781                                          age = gmDateTime.calculate_apparent_age(start = dob)
    1782                                  )
    1783                          )
    1784
    1785                  if dob > death:
    1786                          return _('invalid age: DOB after death')
    1787
    1788                  return '%s%s%s' % (
    1789                          gmTools.u_latin_cross,
    1790                          gmTools.bool2subst (
    1791                                  self._payload[self._idx['dob_is_estimated']],
    1792                                  gmTools.u_almost_equal_to,
    1793                                  ''
    1794                          ),
    1795                          gmDateTime.format_apparent_age_medically (
    1796                                  age = gmDateTime.calculate_apparent_age (
    1797                                          start = dob,
    1798                                          end = self['deceased']
    1799                                  )
    1800                          )
    1801                  )
    1802
    1803          #----------------------------------------------------------------------
    1804 -        def dob_in_range(self, min_distance='1 week', max_distance='1 week'):
    1805                  if self['dob'] is None:
    1806                          return False
    1807                  cmd = 'select dem.dob_is_in_range(%(dob)s, %(min)s, %(max)s)'
    1808                  rows, idx = gmPG2.run_ro_queries (
    1809                          queries = [{
    1810                                  'cmd': cmd,
    1811                                  'args': {'dob': self['dob'], 'min': min_distance, 'max': max_distance}
    1812                          }]
    1813                  )
    1814                  return rows[0][0]
    1815
    1816          #----------------------------------------------------------------------
    1817 -        def _get_current_birthday_passed(self):
    1818                  if self['dob'] is None:
    1819                          return None
    1820                  now = gmDateTime.pydt_now_here()
    1821                  if now.month < self['dob'].month:
    1822                          return False
    1823                  if now.month > self['dob'].month:
    1824                          return True
    1825                  # -> DOB is this month
    1826                  if now.day < self['dob'].day:
    1827                          return False
    1828                  if now.day > self['dob'].day:
    1829                          return True
    1830                  # -> DOB is today
    1831                  return False
    1832
    1833          current_birthday_passed = property(_get_current_birthday_passed)
    1834
    1835          #----------------------------------------------------------------------
    1836 -        def _get_birthday_this_year(self):
    1837                  if self['dob'] is None:
    1838                          return None
    1839                  now = gmDateTime.pydt_now_here()
    1840                  return gmDateTime.pydt_replace (
    1841                          dt = self['dob'],
    1842                          year = now.year,
    1843                          strict = False
    1844                  )
    1845
    1846          birthday_this_year = property(_get_birthday_this_year)
    1847
    1848          #----------------------------------------------------------------------
    1849 -        def _get_birthday_next_year(self):
    1850                  if self['dob'] is None:
    1851                          return None
    1852                  now = gmDateTime.pydt_now_here()
    1853                  return gmDateTime.pydt_replace (
    1854                          dt = self['dob'],
    1855                          year = now.year + 1,
    1856                          strict = False
    1857                  )
    1858
    1859          birthday_next_year = property(_get_birthday_next_year)
    1860
    1861          #----------------------------------------------------------------------
    1862 -        def _get_birthday_last_year(self):
    1863                  if self['dob'] is None:
    1864                          return None
    1865                  now = gmDateTime.pydt_now_here()
    1866                  return gmDateTime.pydt_replace (
    1867                          dt = self['dob'],
    1868                          year = now.year - 1,
    1869                          strict = False
    1870                  )
    1871
    1872          birthday_last_year = property(_get_birthday_last_year, lambda x:x)
    1873
    1874          #----------------------------------------------------------------------
    1875          # practice related
    1876          #----------------------------------------------------------------------
    1877 -        def get_last_encounter(self):
    1878                  cmd = 'select * from clin.v_most_recent_encounters where pk_patient=%s'
    1879                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self._payload[self._idx['pk_identity']]]}])
    1880                  if len(rows) > 0:
    1881                          return rows[0]
    1882                  else:
    1883                          return None
    1884          #--------------------------------------------------------
    1885 -        def get_messages(self, order_by=None):
    1886                  return gmProviderInbox.get_inbox_messages(pk_patient = self._payload[self._idx['pk_identity']], order_by = order_by)
    1887
    1888          messages = property(get_messages, lambda x:x)
    1889          #--------------------------------------------------------
    1890 -        def _get_overdue_messages(self):
    1891                  return gmProviderInbox.get_overdue_messages(pk_patient = self._payload[self._idx['pk_identity']])
    1892
    1893          overdue_messages = property(_get_overdue_messages, lambda x:x)
    1894
    1895          #--------------------------------------------------------
    1896 -        def delete_message(self, pk=None):
    1897                  return gmProviderInbox.delete_inbox_message(inbox_message = pk)
    1898
    1899          #--------------------------------------------------------
    1900 -        def _get_dynamic_hints(self, pk_encounter=None):
    1901                  return gmAutoHints.get_hints_for_patient (
    1902                          pk_identity = self._payload[self._idx['pk_identity']],
    1903                          pk_encounter = pk_encounter
    1904                  )
    1905
    1906          dynamic_hints = property(_get_dynamic_hints, lambda x:x)
    1907
    1908          #--------------------------------------------------------
    1909 -        def _get_suppressed_hints(self):
    1910                  return gmAutoHints.get_suppressed_hints(pk_identity = self._payload[self._idx['pk_identity']])
    1911
    1912          suppressed_hints = property(_get_suppressed_hints, lambda x:x)
    1913
    1914          #--------------------------------------------------------
    1915 -        def _get_primary_provider_identity(self):
    1916                  if self._payload[self._idx['pk_primary_provider']] is None:
    1917                          return None
    1918                  cmd = "SELECT * FROM dem.v_all_persons WHERE pk_identity = (SELECT pk_identity FROM dem.v_staff WHERE pk_staff = %(pk_staff)s)"
    1919                  args = {'pk_staff': self._payload[self._idx['pk_primary_provider']]}
    1920                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1921                  if len(rows) == 0:
    1922                          return None
    1923                  return cPerson(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_identity'})
    1924
    1925          primary_provider_identity = property(_get_primary_provider_identity, lambda x:x)
    1926
    1927          #--------------------------------------------------------
    1928 -        def _get_primary_provider(self):
    1929                  if self._payload[self._idx['pk_primary_provider']] is None:
    1930                          return None
    1931                  from Gnumed.business import gmStaff
    1932                  return gmStaff.cStaff(aPK_obj = self._payload[self._idx['pk_primary_provider']])
    1933
    1934          primary_provider = property(_get_primary_provider, lambda x:x)
    1935
    1936          #----------------------------------------------------------------------
    1937          # convenience
    1938          #----------------------------------------------------------------------
    1939 -        def get_subdir_name(self):
    1940                  """Format patient demographics into patient specific path name fragment."""
    1941
    1942                  return gmTools.fname_sanitize('%s-%s-%s-ID_%s' % (
    1943                          self._payload[self._idx['lastnames']],
    1944                          self._payload[self._idx['firstnames']],
    1945                          self.get_formatted_dob(format = '%Y-%m-%d'),
    1946                          self._payload[self._idx['pk_identity']]         # make unique across "same" patients
    1947                  ))
    1948                  #).replace (
    1949                  #       u'\u2248', u''                  # "approximately", having been added by dob_is_estimated
    1950                  #)
    1951
    1952          subdir_name = property(get_subdir_name, lambda x:x)
    1953
    1954  #============================================================
    1955 -def identity_is_patient(pk_identity):
    1956          cmd = 'SELECT 1 FROM clin.patient WHERE fk_identity = %(pk_pat)s'
    1957          args = {'pk_pat': pk_identity}
    1958          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
    1959          if len(rows) == 0:
    1960                  return False
    1961          return True
    1962
    1963  #------------------------------------------------------------
    1964 -def turn_identity_into_patient(pk_identity):
    1965          cmd = """
    1966                  INSERT INTO clin.patient (fk_identity)
    1967                  SELECT %(pk_ident)s WHERE NOT EXISTS (
    1968                          SELECT 1 FROM clin.patient c_p WHERE fk_identity = %(pk_ident)s
    1969                  )"""
    1970          args = {'pk_ident': pk_identity}
    1971          queries = [{'cmd': cmd, 'args': args}]
    1972          gmPG2.run_rw_queries(queries = queries)
    1973          return True
    1974
    1975  #============================================================
    1976  # helper functions
    1977  #------------------------------------------------------------
    1978  _yield = lambda x:x
    1979
    1980 -def set_yielder(yielder):
    1981          if not callable(yielder):
    1982                  raise TypeError('yielder <%s> is not callable' % yielder)
    1983          global _yield
    1984          _yield = yielder
    1985          _log.debug('setting yielder to <%s>', yielder)
    1986
    1987  #============================================================
    1988 -class cPatient(cPerson):
    1989          """Represents a person which is a patient.
    1990
    1991          - a specializing subclass of cPerson turning it into a patient
    1992          - its use is to cache subobjects like EMR and document folder
    1993          """
    1994 -        def __init__(self, aPK_obj=None, row=None):
    1995                  cPerson.__init__(self, aPK_obj = aPK_obj, row = row)
    1996                  self.__emr_access_lock = threading.Lock()
    1997                  self.__emr = None
    1998                  self.__doc_folder = None
    1999
    2000          #--------------------------------------------------------
    2001 -        def cleanup(self):
    2002                  """Do cleanups before dying.
    2003
    2004                  - note that this may be called in a thread
    2005                  """
    2006                  if self.__emr is not None:
    2007                          self.__emr.cleanup()
    2008                  if self.__doc_folder is not None:
    2009                          self.__doc_folder.cleanup()
    2010                  cPerson.cleanup(self)
    2011
    2012          #----------------------------------------------------------------
    2013 -        def ensure_has_allergy_state(self, pk_encounter=None):
    2014                  from Gnumed.business.gmAllergy import ensure_has_allergy_state
    2015                  ensure_has_allergy_state(encounter = pk_encounter)
    2016                  return True
    2017
    2018          #----------------------------------------------------------
    2019 -        def get_emr(self):
    2020                  _log.debug('accessing EMR for identity [%s], thread [%s]', self._payload[self._idx['pk_identity']], threading.get_ident())
    2021
    2022                  # fast path: already set, just return it
    2023                  if self.__emr is not None:
    2024                          return self.__emr
    2025
    2026                  stack_logged = False
    2027                  got_lock = self.__emr_access_lock.acquire(False)
    2028                  if not got_lock:
    2029                          # do some logging as we failed to get the lock
    2030                          call_stack = inspect.stack()
    2031                          call_stack.reverse()
    2032                          for idx in range(1, len(call_stack)):
    2033                                  caller = call_stack[idx]
    2034                                  _log.debug('%s[%s] @ [%s] in [%s]', ' '* idx, caller[3], caller[2], caller[1])
    2035                          del call_stack
    2036                          stack_logged = True
    2037                          # now loop a bit
    2038                          for idx in range(500):
    2039                                  _yield()
    2040                                  time.sleep(0.1)
    2041                                  _yield()
    2042                                  got_lock = self.__emr_access_lock.acquire(False)
    2043                                  if got_lock:
    2044                                          break
    2045                          if not got_lock:
    2046                                  _log.error('still failed to acquire EMR access lock, aborting (thread [%s])', threading.get_ident())
    2047                                  self.__emr_access_lock.release()
    2048                                  raise AttributeError('cannot lock access to EMR for identity [%s]' % self._payload[self._idx['pk_identity']])
    2049
    2050                  _log.debug('pulling chart for identity [%s], thread [%s]', self._payload[self._idx['pk_identity']], threading.get_ident())
    2051                  if not stack_logged:
    2052                          # do some logging as we are pulling the chart for the first time
    2053                          call_stack = inspect.stack()
    2054                          call_stack.reverse()
    2055                          for idx in range(1, len(call_stack)):
    2056                                  caller = call_stack[idx]
    2057                                  _log.debug('%s[%s] @ [%s] in [%s]', ' '* idx, caller[3], caller[2], caller[1])
    2058                          del call_stack
    2059                          stack_logged = True
    2060
    2061                  self.is_patient = True
    2062                  from Gnumed.business import gmClinicalRecord
    2063                  emr = gmClinicalRecord.cClinicalRecord(aPKey = self._payload[self._idx['pk_identity']])
    2064
    2065                  _log.debug('returning EMR for identity [%s], thread [%s]', self._payload[self._idx['pk_identity']], threading.get_ident())
    2066                  self.__emr = emr
    2067                  self.__emr_access_lock.release()
    2068                  return self.__emr
    2069
    2070          emr = property(get_emr, lambda x:x)
    2071
    2072          #----------------------------------------------------------
    2073 -        def get_document_folder(self):
    2074                  if self.__doc_folder is None:
    2075                          self.__doc_folder = cDocumentFolder(aPKey = self._payload[self._idx['pk_identity']])
    2076                  return self.__doc_folder
    2077
    2078          document_folder = property(get_document_folder, lambda x:x)
    2079
    2080  #============================================================
    2081 -class gmCurrentPatient(gmBorg.cBorg):
    2082          """Patient Borg to hold the currently active patient.
    2083
    2084          There may be many instances of this but they all share state.
    2085
    2086          The underlying dem.identity row must have .deleted set to FALSE.
    2087
    2088          The sequence of events when changing the active patient:
    2089
    2090                  1) Registered callbacks are run.
    2091                          Those are run synchronously. If a callback
    2092                          returns False or throws an exception the
    2093                          patient switch is aborted. Callback code
    2094                          can rely on the patient still being active
    2095                          and to not go away until it returns. It
    2096                          is not passed any arguments and must return
    2097                          False or True.
    2098
    2099                  2) Signal "pre_patient_unselection" is sent.
    2100                          This does not wait for nor check results.
    2101                          The keyword pk_identity contains the
    2102                          PK of the person being switched away
    2103                          from.
    2104
    2105                  3) the current patient is unset (gmNull.cNull)
    2106
    2107                  4) Signal "current_patient_unset" is sent
    2108                          At this point resetting GUI fields to
    2109                          empty should be done. The active patient
    2110                          is not there anymore.
    2111
    2112                          This does not wait for nor check results.
    2113
    2114                  5) The current patient is set to the new value.
    2115                          The new patient can also remain gmNull.cNull
    2116                          in case the calling code explicitely unset
    2117                          the current patient.
    2118
    2119                  6) Signal "post_patient_selection" is sent.
    2120                          Code listening to this signal can
    2121                          assume that the new patient is
    2122                          already active.
    2123          """
    2124 -        def __init__(self, patient=None, forced_reload=False):
    2125                  """Change or get currently active patient.
    2126
    2127                  patient:
    2128                  * None: get currently active patient
    2129                  * -1: unset currently active patient
    2130                  * cPatient instance: set active patient if possible
    2131                  """
    2132                  # make sure we do have a patient pointer
    2133                  try:
    2134                          self.patient
    2135                  except AttributeError:
    2136                          self.patient = gmNull.cNull()
    2137                          self.__register_interests()
    2138                          # set initial lock state,
    2139                          # this lock protects against activating another patient
    2140                          # when we are controlled from a remote application
    2141                          self.__lock_depth = 0
    2142                          # initialize callback state
    2143                          self.__callbacks_before_switching_away_from_patient = []
    2144
    2145                  # user wants copy of current patient
    2146                  if patient is None:
    2147                          return None
    2148
    2149                  # do nothing if patient is locked
    2150                  if self.locked:
    2151                          _log.error('patient [%s] is locked, cannot change to [%s]' % (self.patient['pk_identity'], patient))
    2152                          return None
    2153
    2154                  # user wants to explicitly unset current patient
    2155                  if patient == -1:
    2156                          _log.debug('explicitly unsetting current patient')
    2157                          if not self.__run_callbacks_before_switching_away_from_patient():
    2158                                  _log.error('not unsetting current patient, at least one pre-change callback failed')
    2159                                  return None
    2160                          self.__send_pre_unselection_notification()
    2161                          self.patient.cleanup()
    2162                          self.patient = gmNull.cNull()
    2163                          self.__send_unselection_notification()
    2164                          # give it some time
    2165                          time.sleep(0.5)
    2166                          self.__send_selection_notification()
    2167                          return None
    2168
    2169                  # must be cPatient instance, then
    2170                  if not isinstance(patient, cPatient):
    2171                          _log.error('cannot set active patient to [%s], must be either None, -1 or cPatient instance' % str(patient))
    2172                          raise TypeError('gmPerson.gmCurrentPatient.__init__(): <patient> must be None, -1 or cPatient instance but is: %s' % str(patient))
    2173
    2174                  # same ID, no change needed
    2175                  if (self.patient['pk_identity'] == patient['pk_identity']) and not forced_reload:
    2176                          return None
    2177
    2178                  if patient['is_deleted']:
    2179                          _log.error('cannot set active patient to disabled dem.identity row: %s', patient)
    2180                          raise ValueError('gmPerson.gmCurrentPatient.__init__(): <patient> is disabled: %s' % patient)
    2181
    2182                  # user wants different patient
    2183                  _log.info('patient change [%s] -> [%s] requested', self.patient['pk_identity'], patient['pk_identity'])
    2184
    2185                  if not self.__run_callbacks_before_switching_away_from_patient():
    2186                          _log.error('not changing current patient, at least one pre-change callback failed')
    2187                          return None
    2188
    2189                  # everything seems swell
    2190                  self.__send_pre_unselection_notification()
    2191                  self.patient.cleanup()
    2192                  self.patient = gmNull.cNull()
    2193                  self.__send_unselection_notification()
    2194                  # give it some time
    2195                  time.sleep(0.5)
    2196                  self.patient = patient
    2197                  # for good measure ...
    2198                  # however, actually we want to get rid of that
    2199                  self.patient.emr
    2200                  self.__send_selection_notification()
    2201
    2202                  return None
    2203
    2204          #--------------------------------------------------------
    2205 -        def __register_interests(self):
    2206                  gmDispatcher.connect(signal = 'gm_table_mod', receiver = self._on_database_signal)
    2207
    2208          #--------------------------------------------------------
    2209 -        def _on_database_signal(self, **kwds):
    2210                  # we don't have a patient: don't process signals
    2211                  if isinstance(self.patient, gmNull.cNull):
    2212                          return True
    2213
    2214                  # we only care about identity and name changes
    2215                  if kwds['table'] not in ['dem.identity', 'dem.names']:
    2216                          return True
    2217
    2218                  # signal is not about our patient: ignore signal
    2219                  if int(kwds['pk_identity']) != self.patient.ID:
    2220                          return True
    2221
    2222                  if kwds['table'] == 'dem.identity':
    2223                          # we don't care about newly INSERTed or DELETEd patients
    2224                          if kwds['operation'] != 'UPDATE':
    2225                                  return True
    2226
    2227                  self.patient.refetch_payload()
    2228                  return True
    2229
    2230          #--------------------------------------------------------
    2231          # external API
    2232          #--------------------------------------------------------
    2233 -        def register_before_switching_from_patient_callback(self, callback=None):
    2234                  # callbacks are run synchronously before
    2235                  # switching *away* from the current patient,
    2236                  # if a callback returns false the current
    2237                  # patient will not be switched away from,
    2238                  # callbacks will not be passed any arguments
    2239                  if not callable(callback):
    2240                          raise TypeError('callback [%s] not callable' % callback)
    2241
    2242                  self.__callbacks_before_switching_away_from_patient.append(callback)
    2243
    2244          #--------------------------------------------------------
    2245 -        def _get_connected(self):
    2246                  return (not isinstance(self.patient, gmNull.cNull))
    2247
    2248          connected = property(_get_connected, lambda x:x)
    2249
    2250          #--------------------------------------------------------
    2251 -        def _get_locked(self):
    2252                  return (self.__lock_depth > 0)
    2253
    2254 -        def _set_locked(self, locked):
    2255                  if locked:
    2256                          self.__lock_depth = self.__lock_depth + 1
    2257                          gmDispatcher.send(signal = 'patient_locked', sender = self.__class__.__name__)
    2258                  else:
    2259                          if self.__lock_depth == 0:
    2260                                  _log.error('lock/unlock imbalance, tried to refcount lock depth below 0')
    2261                                  return
    2262                          else:
    2263                                  self.__lock_depth = self.__lock_depth - 1
    2264                          gmDispatcher.send(signal = 'patient_unlocked', sender = self.__class__.__name__)
    2265
    2266          locked = property(_get_locked, _set_locked)
    2267
    2268          #--------------------------------------------------------
    2269 -        def force_unlock(self):
    2270                  _log.info('forced patient unlock at lock depth [%s]' % self.__lock_depth)
    2271                  self.__lock_depth = 0
    2272                  gmDispatcher.send(signal = 'patient_unlocked', sender = self.__class__.__name__)
    2273
    2274          #--------------------------------------------------------
    2275          # patient change handling
    2276          #--------------------------------------------------------
    2277 -        def __run_callbacks_before_switching_away_from_patient(self):
    2278                  if isinstance(self.patient, gmNull.cNull):
    2279                          return True
    2280
    2281                  for call_back in self.__callbacks_before_switching_away_from_patient:
    2282                          try:
    2283                                  successful = call_back()
    2284                          except:
    2285                                  _log.exception('callback [%s] failed', call_back)
    2286                                  print("*** pre-change callback failed ***")
    2287                                  print(type(call_back))
    2288                                  print(call_back)
    2289                                  return False
    2290
    2291                          if not successful:
    2292                                  _log.error('callback [%s] returned False', call_back)
    2293                                  return False
    2294
    2295                  return True
    2296
    2297          #--------------------------------------------------------
    2298 -        def __send_pre_unselection_notification(self):
    2299                  """Sends signal when current patient is about to be unset.
    2300
    2301                  This does NOT wait for signal handlers to complete.
    2302                  """
    2303                  kwargs = {
    2304                          'signal': 'pre_patient_unselection',
    2305                          'sender': self.__class__.__name__,
    2306                          'pk_identity': self.patient['pk_identity']
    2307                  }
    2308                  gmDispatcher.send(**kwargs)
    2309
    2310          #--------------------------------------------------------
    2311 -        def __send_unselection_notification(self):
    2312                  """Sends signal when the previously active patient has
    2313                     been unset during a change of active patient.
    2314
    2315                  This is the time to initialize GUI fields to empty values.
    2316
    2317                  This does NOT wait for signal handlers to complete.
    2318                  """
    2319                  kwargs = {
    2320                          'signal': 'current_patient_unset',
    2321                          'sender': self.__class__.__name__
    2322                  }
    2323                  gmDispatcher.send(**kwargs)
    2324
    2325          #--------------------------------------------------------
    2326 -        def __send_selection_notification(self):
    2327                  """Sends signal when another patient has actually been made active."""
    2328                  kwargs = {
    2329                          'signal': 'post_patient_selection',
    2330                          'sender': self.__class__.__name__,
    2331                          'pk_identity': self.patient['pk_identity']
    2332                  }
    2333                  gmDispatcher.send(**kwargs)
    2334
    2335          #--------------------------------------------------------
    2336          # __getattr__ handling
    2337          #--------------------------------------------------------
    2338 -        def __getattr__(self, attribute):
    2339                  # override __getattr__ here, not __getattribute__ because
    2340                  # the former is used _after_ ordinary attribute lookup
    2341                  # failed while the latter is applied _before_ ordinary
    2342                  # lookup (and is easy to drive into infinite recursion),
    2343                  # this is also why subsequent access to self.patient
    2344                  # simply returns the .patient member value :-)
    2345                  if attribute == 'patient':
    2346                          raise AttributeError
    2347                  if isinstance(self.patient, gmNull.cNull):
    2348                          _log.error("[%s]: cannot getattr(%s, '%s'), patient attribute not connected to a patient", self, self.patient, attribute)
    2349                          raise AttributeError("[%s]: cannot getattr(%s, '%s'), patient attribute not connected to a patient" % (self, self.patient, attribute))
    2350                  return getattr(self.patient, attribute)
    2351
    2352          #--------------------------------------------------------
    2353          # __get/setitem__ handling
    2354          #--------------------------------------------------------
    2355 -        def __getitem__(self, attribute = None):
    2356                  """Return any attribute if known how to retrieve it by proxy.
    2357                  """
    2358                  return self.patient[attribute]
    2359
    2360          #--------------------------------------------------------
    2361 -        def __setitem__(self, attribute, value):
    2362                  self.patient[attribute] = value
    2363
    2364  #============================================================
    2365  # match providers
    2366  #============================================================
    2367 -class cMatchProvider_Provider(gmMatchProvider.cMatchProvider_SQL2):
    2368 -        def __init__(self):
    2369                  gmMatchProvider.cMatchProvider_SQL2.__init__(
    2370                          self,
    2371                          queries = [
    2372                                  """SELECT
    2373                                                  pk_staff AS data,
    2374                                                  short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS list_label,
    2375                                                  short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS field_label
    2376                                          FROM dem.v_staff
    2377                                          WHERE
    2378                                                  is_active AND (
    2379                                                          short_alias %(fragment_condition)s OR
    2380                                                          firstnames %(fragment_condition)s OR
    2381                                                          lastnames %(fragment_condition)s OR
    2382                                                          db_user %(fragment_condition)s
    2383                                                  )
    2384                                  """
    2385                          ]
    2386                  )
    2387                  self.setThresholds(1, 2, 3)
    2388
    2389  #============================================================
    2390  # convenience functions
    2391  #============================================================
    2392 -def create_name(pk_person, firstnames, lastnames, active=False):
    2393          queries = [{
    2394                  'cmd': "select dem.add_name(%s, %s, %s, %s)",
    2395                  'args': [pk_person, firstnames, lastnames, active]
    2396          }]
    2397          rows, idx = gmPG2.run_rw_queries(queries=queries, return_data=True)
    2398          name = cPersonName(aPK_obj = rows[0][0])
    2399          return name
    2400
    2401  #============================================================
    2402 -def create_identity(gender=None, dob=None, lastnames=None, firstnames=None, comment=None):
    2403
    2404          cmd1 = "INSERT INTO dem.identity (gender, dob, comment) VALUES (%s, %s, %s)"
    2405          cmd2 = """
    2406  INSERT INTO dem.names (
    2407          id_identity, lastnames, firstnames
    2408  ) VALUES (
    2409          currval('dem.identity_pk_seq'), coalesce(%s, 'xxxDEFAULTxxx'), coalesce(%s, 'xxxDEFAULTxxx')
    2410  ) RETURNING id_identity"""
    2411  #       cmd2 = u"select dem.add_name(currval('dem.identity_pk_seq')::integer, coalesce(%s, 'xxxDEFAULTxxx'), coalesce(%s, 'xxxDEFAULTxxx'), True)"
    2412          try:
    2413                  rows, idx = gmPG2.run_rw_queries (
    2414                          queries = [
    2415                                  {'cmd': cmd1, 'args': [gender, dob, comment]},
    2416                                  {'cmd': cmd2, 'args': [lastnames, firstnames]}
    2417                                  #{'cmd': cmd2, 'args': [firstnames, lastnames]}
    2418                          ],
    2419                          return_data = True
    2420                  )
    2421          except Exception:
    2422                  _log.exception('cannot create identity')
    2423                  gmLog2.log_stack_trace()
    2424                  return None
    2425          ident = cPerson(aPK_obj = rows[0][0])
    2426          gmHooks.run_hook_script(hook = 'post_person_creation')
    2427          return ident
    2428
    2429  #============================================================
    2430 -def disable_identity(pk_identity):
    2431          _log.info('disabling identity [%s]', pk_identity)
    2432          cmd = "UPDATE dem.identity SET deleted = true WHERE pk = %(pk)s"
    2433          args = {'pk': pk_identity}
    2434          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    2435          return True
    2436
    2437  #============================================================
    2438 -def create_dummy_identity():
    2439          cmd = "INSERT INTO dem.identity(gender) VALUES (NULL::text) RETURNING pk"
    2440          rows, idx = gmPG2.run_rw_queries (
    2441                  queries = [{'cmd': cmd}],
    2442                  return_data = True
    2443          )
    2444          return gmDemographicRecord.cPerson(aPK_obj = rows[0][0])
    2445
    2446  #============================================================
    2447 -def identity_exists(pk_identity):
    2448          cmd = 'SELECT EXISTS(SELECT 1 FROM dem.identity where pk = %(pk)s)'
    2449          args = {'pk': pk_identity}
    2450          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
    2451          return rows[0][0]
    2452
    2453  #============================================================
    2454 -def set_active_patient(patient=None, forced_reload=False):
    2455          """Set active patient.
    2456
    2457          If patient is -1 the active patient will be UNset.
    2458          """
    2459          if isinstance(patient, gmCurrentPatient):
    2460                  return True
    2461
    2462          if isinstance(patient, cPatient):
    2463                  pat = patient
    2464          elif isinstance(patient, cPerson):
    2465                  pat = pat.as_patient
    2466          elif patient == -1:
    2467                  pat = patient
    2468          else:
    2469                  # maybe integer ?
    2470                  success, pk = gmTools.input2int(initial = patient, minval = 1)
    2471                  if not success:
    2472                          raise ValueError('<patient> must be either -1, >0, or a cPatient, cPerson or gmCurrentPatient instance, is: %s' % patient)
    2473                  # but also valid patient ID ?
    2474                  try:
    2475                          pat = cPatient(aPK_obj = pk)
    2476                  except:
    2477                          _log.exception('identity [%s] not found' % patient)
    2478                          return False
    2479
    2480          # attempt to switch
    2481          try:
    2482                  gmCurrentPatient(patient = pat, forced_reload = forced_reload)
    2483          except:
    2484                  _log.exception('error changing active patient to [%s]' % patient)
    2485                  return False
    2486
    2487          return True
    2488
    2489  #============================================================
    2490  # gender related
    2491  #------------------------------------------------------------
    2492 -def get_gender_list():
    2493          """Retrieves the list of known genders from the database."""
    2494          global __gender_idx
    2495          global __gender_list
    2496
    2497          if __gender_list is None:
    2498                  cmd = "SELECT tag, l10n_tag, label, l10n_label, sort_weight FROM dem.v_gender_labels ORDER BY sort_weight DESC"
    2499                  __gender_list, __gender_idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True)
    2500                  _log.debug('genders in database: %s' % __gender_list)
    2501
    2502          return (__gender_list, __gender_idx)
    2503
    2504  #------------------------------------------------------------
    2505  map_gender2mf = {
    2506          'm': 'm',
    2507          'f': 'f',
    2508          'tf': 'f',
    2509          'tm': 'm',
    2510          'h': 'mf'
    2511  }
    2512
    2513  # https://tools.ietf.org/html/rfc6350#section-6.2.7
    2514  # M F O N U
    2515  map_gender2vcard = {
    2516          'm': 'M',
    2517          'f': 'F',
    2518          'tf': 'F',
    2519          'tm': 'M',
    2520          'h': 'O',
    2521          None: 'U'
    2522  }
    2523
    2524  #------------------------------------------------------------
    2525  # maps GNUmed related i18n-aware gender specifiers to a unicode symbol
    2526  map_gender2symbol = {
    2527          'm': '\u2642',
    2528          'f': '\u2640',
    2529          'tf': '\u26A5\u2640',
    2530  #       'tf': u'\u2642\u2640-\u2640',
    2531          'tm': '\u26A5\u2642',
    2532  #       'tm': u'\u2642\u2640-\u2642',
    2533          'h': '\u26A5',
    2534  #       'h': u'\u2642\u2640',
    2535          None: '?\u26A5?'
    2536  }
    2537  #------------------------------------------------------------
    2538 -def map_gender2string(gender=None):
    2539          """Maps GNUmed related i18n-aware gender specifiers to a human-readable string."""
    2540
    2541          global __gender2string_map
    2542
    2543          if __gender2string_map is None:
    2544                  genders, idx = get_gender_list()
    2545                  __gender2string_map = {
    2546                          'm': _('male'),
    2547                          'f': _('female'),
    2548                          'tf': '',
    2549                          'tm': '',
    2550                          'h': '',
    2551                          None: _('unknown gender')
    2552                  }
    2553                  for g in genders:
    2554                          __gender2string_map[g[idx['l10n_tag']]] = g[idx['l10n_label']]
    2555                          __gender2string_map[g[idx['tag']]] = g[idx['l10n_label']]
    2556                  _log.debug('gender -> string mapping: %s' % __gender2string_map)
    2557
    2558          return __gender2string_map[gender]
    2559  #------------------------------------------------------------
    2560 -def map_gender2salutation(gender=None):
    2561          """Maps GNUmed related i18n-aware gender specifiers to a human-readable salutation."""
    2562
    2563          global __gender2salutation_map
    2564
    2565          if __gender2salutation_map is None:
    2566                  genders, idx = get_gender_list()
    2567                  __gender2salutation_map = {
    2568                          'm': _('Mr'),
    2569                          'f': _('Mrs'),
    2570                          'tf': '',
    2571                          'tm': '',
    2572                          'h': '',
    2573                          None: ''
    2574                  }
    2575                  for g in genders:
    2576                          __gender2salutation_map[g[idx['l10n_tag']]] = __gender2salutation_map[g[idx['tag']]]
    2577                          __gender2salutation_map[g[idx['label']]] = __gender2salutation_map[g[idx['tag']]]
    2578                          __gender2salutation_map[g[idx['l10n_label']]] = __gender2salutation_map[g[idx['tag']]]
    2579                  _log.debug('gender -> salutation mapping: %s' % __gender2salutation_map)
    2580
    2581          return __gender2salutation_map[gender]
    2582  #------------------------------------------------------------
    2583 -def map_firstnames2gender(firstnames=None):
    2584          """Try getting the gender for the given first name."""
    2585
    2586          if firstnames is None:
    2587                  return None
    2588
    2589          rows, idx = gmPG2.run_ro_queries(queries = [{
    2590                  'cmd': "SELECT gender FROM dem.name_gender_map WHERE name ILIKE %(fn)s LIMIT 1",
    2591                  'args': {'fn': firstnames}
    2592          }])
    2593
    2594          if len(rows) == 0:
    2595                  return None
    2596
    2597          return rows[0][0]
    2598  #============================================================
    2599 -def get_person_IDs():
    2600          cmd = 'SELECT pk FROM dem.identity'
    2601          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = False)
    2602          return [ r[0] for r in rows ]
    2603
    2604  #============================================================
    2605 -def get_persons_from_pks(pks=None):
    2606          return [ cPerson(aPK_obj = pk) for pk in pks ]
    2607
    2608  #============================================================
    2609 -def get_person_from_xdt(filename=None, encoding=None, dob_format=None):
    2610          from Gnumed.business import gmXdtObjects
    2611          return gmXdtObjects.read_person_from_xdt(filename=filename, encoding=encoding, dob_format=dob_format)
    2612
    2613  #============================================================
    2614 -def get_persons_from_pracsoft_file(filename=None, encoding='ascii'):
    2615          from Gnumed.business import gmPracSoftAU
    2616          return gmPracSoftAU.read_persons_from_pracsoft_file(filename=filename, encoding=encoding)
    2617
    2618  #============================================================
    2619  # main/testing
    2620  #============================================================
    2621  if __name__ == '__main__':
    2622
    2623          if len(sys.argv) == 1:
    2624                  sys.exit()
    2625
    2626          if sys.argv[1] != 'test':
    2627                  sys.exit()
    2628
    2629          import datetime
    2630
    2631          gmI18N.activate_locale()
    2632          gmI18N.install_domain()
    2633          gmDateTime.init()
    2634
    2635          #--------------------------------------------------------
    2636 -        def test_set_active_pat():
    2637
    2638                  ident = cPerson(1)
    2639                  print("setting active patient with", ident)
    2640                  set_active_patient(patient=ident)
    2641
    2642                  patient = cPatient(12)
    2643                  print("setting active patient with", patient)
    2644                  set_active_patient(patient=patient)
    2645
    2646                  pat = gmCurrentPatient()
    2647                  print(pat['dob'])
    2648                  #pat['dob'] = 'test'
    2649
    2650  #               staff = cStaff()
    2651  #               print("setting active patient with", staff)
    2652  #               set_active_patient(patient=staff)
    2653
    2654                  print("setting active patient with -1")
    2655                  set_active_patient(patient=-1)
    2656          #--------------------------------------------------------
    2657 -        def test_dto_person():
    2658                  dto = cDTO_person()
    2659                  dto.firstnames = 'Sepp'
    2660                  dto.lastnames = 'Herberger'
    2661                  dto.gender = 'male'
    2662                  dto.dob = pyDT.datetime.now(tz=gmDateTime.gmCurrentLocalTimezone)
    2663                  print(dto)
    2664
    2665                  print(dto['firstnames'])
    2666                  print(dto['lastnames'])
    2667                  print(dto['gender'])
    2668                  print(dto['dob'])
    2669
    2670                  for key in dto.keys():
    2671                          print(key)
    2672          #--------------------------------------------------------
    2673 -        def test_identity():
    2674                  # create patient
    2675                  print('\n\nCreating identity...')
    2676                  new_identity = create_identity(gender='m', dob='2005-01-01', lastnames='test lastnames', firstnames='test firstnames')
    2677                  print('Identity created: %s' % new_identity)
    2678
    2679                  print('\nSetting title and gender...')
    2680                  new_identity['title'] = 'test title';
    2681                  new_identity['gender'] = 'f';
    2682                  new_identity.save_payload()
    2683                  print('Refetching identity from db: %s' % cPerson(aPK_obj=new_identity['pk_identity']))
    2684
    2685                  print('\nGetting all names...')
    2686                  for a_name in new_identity.get_names():
    2687                          print(a_name)
    2688                  print('Active name: %s' % (new_identity.get_active_name()))
    2689                  print('Setting nickname...')
    2690                  new_identity.set_nickname(nickname='test nickname')
    2691                  print('Refetching all names...')
    2692                  for a_name in new_identity.get_names():
    2693                          print(a_name)
    2694                  print('Active name: %s' % (new_identity.get_active_name()))
    2695
    2696                  print('\nIdentity occupations: %s' % new_identity['occupations'])
    2697                  print('Creating identity occupation...')
    2698                  new_identity.link_occupation('test occupation')
    2699                  print('Identity occupations: %s' % new_identity['occupations'])
    2700
    2701                  print('\nIdentity addresses: %s' % new_identity.get_addresses())
    2702                  print('Creating identity address...')
    2703                  # make sure the state exists in the backend
    2704                  new_identity.link_address (
    2705                          number = 'test 1234',
    2706                          street = 'test street',
    2707                          postcode = 'test postcode',
    2708                          urb = 'test urb',
    2709                          region_code = 'SN',
    2710                          country_code = 'DE'
    2711                  )
    2712                  print('Identity addresses: %s' % new_identity.get_addresses())
    2713
    2714                  print('\nIdentity communications: %s' % new_identity.get_comm_channels())
    2715                  print('Creating identity communication...')
    2716                  new_identity.link_comm_channel('homephone', '1234566')
    2717                  print('Identity communications: %s' % new_identity.get_comm_channels())
    2718          #--------------------------------------------------------
    2719 -        def test_name():
    2720                  for pk in range(1,16):
    2721                          name = cPersonName(aPK_obj=pk)
    2722                          print(name.description)
    2723                          print('  ', name)
    2724          #--------------------------------------------------------
    2725 -        def test_gender_list():
    2726                  genders, idx = get_gender_list()
    2727                  print("\n\nRetrieving gender enum (tag, label, weight):")
    2728                  for gender in genders:
    2729                          print("%s, %s, %s" % (gender[idx['tag']], gender[idx['l10n_label']], gender[idx['sort_weight']]))
    2730          #--------------------------------------------------------
    2731 -        def test_export_area():
    2732                  person = cPerson(aPK_obj = 12)
    2733                  print(person)
    2734                  print(person.export_area)
    2735                  print(person.export_area.items)
    2736          #--------------------------------------------------------
    2737 -        def test_ext_id():
    2738                  person = cPerson(aPK_obj = 9)
    2739                  print(person.get_external_ids(id_type='Fachgebiet', issuer='Ärztekammer'))
    2740                  #print(person.get_external_ids()
    2741          #--------------------------------------------------------
    2742 -        def test_vcf():
    2743                  person = cPerson(aPK_obj = 12)
    2744                  print(person.export_as_vcard())
    2745
    2746          #--------------------------------------------------------
    2747 -        def test_mecard():
    2748                  person = cPerson(aPK_obj = 12)
    2749                  print(person.MECARD)
    2750                  mcf = person.export_as_mecard()
    2751                  print(mcf)
    2752                  #print(gmTools.create_qrcode(filename = mcf, qr_filename = None, verbose = True)
    2753                  print(gmTools.create_qrcode(text = person.MECARD, qr_filename = None, verbose = True))
    2754
    2755          #--------------------------------------------------------
    2756 -        def test_current_patient():
    2757                  pat = gmCurrentPatient()
    2758                  print("pat.emr", pat.emr)
    2759
    2760          #--------------------------------------------------------
    2761 -        def test_ext_id():
    2762                  person = cPerson(aPK_obj = 12)
    2763                  print(person.suggest_external_id(target = 'Orthanc'))
    2764
    2765          #--------------------------------------------------------
    2766 -        def test_assimilate_identity():
    2767                  patient = cPatient(12)
    2768                  set_active_patient(patient = patient)
    2769                  curr_pat = gmCurrentPatient()
    2770                  other_pat = cPerson(1111111)
    2771                  curr_pat.assimilate_identity(other_identity=None)
    2772
    2773          #--------------------------------------------------------
    2774          #test_dto_person()
    2775          #test_identity()
    2776          #test_set_active_pat()
    2777          #test_search_by_dto()
    2778          #test_name()
    2779          #test_gender_list()
    2780
    2781          #map_gender2salutation('m')
    2782          # module functions
    2783
    2784          #comms = get_comm_list()
    2785          #print("\n\nRetrieving communication media enum (id, description): %s" % comms)
    2786          #test_export_area()
    2787          #test_ext_id()
    2788          #test_vcf()
    2789          test_mecard()
    2790          #test_ext_id()
    2791          #test_current_patient()
    2792          #test_assimilate_identity()
    2793
    2794  #============================================================
    2795