Gnumed.business.gmPerson

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 Exception:
     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                  cmd = "SELECT pk_address FROM dem.v_pat_addresses WHERE pk_identity = %(pat)s"
    1600                  args = {'pat': self.pk_obj}
    1601                  if address_type is not None:
    1602                          cmd = cmd + " AND address_type = %(typ)s"
    1603                          args['typ'] = address_type
    1604
    1605                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1606                  return [
    1607                          gmDemographicRecord.cPatientAddress(aPK_obj = {
    1608                                  'pk_adr': r['pk_address'],
    1609                                  'pk_pat': self.pk_obj
    1610                          }) for r in rows
    1611                  ]
    1612
    1613          #--------------------------------------------------------
    1614 -        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):
    1615                  """Link an address with a patient, creating the address if it does not exists.
    1616
    1617                  @param id_type The primary key of the address type.
    1618                  """
    1619                  if address is None:
    1620                          address = gmDemographicRecord.create_address (
    1621                                  country_code = country_code,
    1622                                  region_code = region_code,
    1623                                  urb = urb,
    1624                                  suburb = suburb,
    1625                                  postcode = postcode,
    1626                                  street = street,
    1627                                  number = number,
    1628                                  subunit = subunit
    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                  # no, link to person
    1638                  if not rows:
    1639                          args = {'pk_person': self.pk_obj, 'adr': address['pk_address'], 'type': id_type}
    1640                          cmd = """
    1641                                  INSERT INTO dem.lnk_person_org_address(id_identity, id_address)
    1642                                  VALUES (%(pk_person)s, %(adr)s)
    1643                                  RETURNING id_address"""
    1644                          rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True)
    1645                  pk_data = {
    1646                          'pk_adr': rows[0]['id_address'],
    1647                          'pk_pat': self.pk_obj
    1648                  }
    1649                  linked_adr = gmDemographicRecord.cPatientAddress(aPK_obj = pk_data)
    1650                  # possibly change type
    1651                  if id_type is None:
    1652                          if adr_type is not None:
    1653                                  id_type = gmDemographicRecord.create_address_type(address_type = adr_type)
    1654                  if id_type is not None:
    1655                          linked_adr['pk_address_type'] = id_type
    1656                          linked_adr.save()
    1657                  return linked_adr
    1658
    1659          #----------------------------------------------------------------------
    1660 -        def unlink_address(self, address=None, pk_address=None):
    1661                  """Remove an address from the patient.
    1662
    1663                  The address itself stays in the database.
    1664                  The address can be either cAdress or cPatientAdress.
    1665                  """
    1666                  if pk_address is None:
    1667                          args = {'person': self.pk_obj, 'adr': address['pk_address']}
    1668                  else:
    1669                          args = {'person': self.pk_obj, 'adr': pk_address}
    1670                  cmd = """
    1671                          DELETE FROM dem.lnk_person_org_address
    1672                          WHERE
    1673                                  dem.lnk_person_org_address.id_identity = %(person)s
    1674                                          AND
    1675                                  dem.lnk_person_org_address.id_address = %(adr)s
    1676                                          AND
    1677                                  NOT EXISTS(SELECT 1 FROM bill.bill WHERE fk_receiver_address = dem.lnk_person_org_address.id)
    1678                          """
    1679                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    1680          #----------------------------------------------------------------------
    1681          # bills API
    1682          #----------------------------------------------------------------------
    1683 -        def get_bills(self, order_by=None, pk_patient=None):
    1684                  return gmBilling.get_bills (
    1685                          order_by = order_by,
    1686                          pk_patient = self.pk_obj
    1687                  )
    1688
    1689          bills = property(get_bills, lambda x:x)
    1690          #----------------------------------------------------------------------
    1691          # relatives API
    1692          #----------------------------------------------------------------------
    1693 -        def get_relatives(self):
    1694                  cmd = """
    1695                          SELECT
    1696                                  d_rt.description,
    1697                                  d_vap.*
    1698                          FROM
    1699                                  dem.v_all_persons d_vap,
    1700                                  dem.relation_types d_rt,
    1701                                  dem.lnk_person2relative d_lp2r
    1702                          WHERE
    1703                                  (       d_lp2r.id_identity = %(pk)s
    1704                                                  AND
    1705                                          d_vap.pk_identity = d_lp2r.id_relative
    1706                                                  AND
    1707                                          d_rt.id = d_lp2r.id_relation_type
    1708                                  ) or (
    1709                                          d_lp2r.id_relative = %(pk)s
    1710                                                  AND
    1711                                          d_vap.pk_identity = d_lp2r.id_identity
    1712                                                  AND
    1713                                          d_rt.inverse = d_lp2r.id_relation_type
    1714                                  )"""
    1715                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}])
    1716                  if len(rows) == 0:
    1717                          return []
    1718                  return [(row[0], cPerson(row = {'data': row[1:], 'idx':idx, 'pk_field': 'pk_identity'})) for row in rows]
    1719          #--------------------------------------------------------
    1720 -        def link_new_relative(self, rel_type = 'parent'):
    1721                  # create new relative
    1722                  id_new_relative = create_dummy_identity()
    1723
    1724                  relative = cPerson(aPK_obj=id_new_relative)
    1725                  # pre-fill with data from ourselves
    1726  #               relative.copy_addresses(self)
    1727                  relative.add_name( '**?**', self.get_names()['lastnames'])
    1728                  # and link the two
    1729                  if 'relatives' in self._ext_cache:
    1730                          del self._ext_cache['relatives']
    1731                  cmd = """
    1732                          insert into dem.lnk_person2relative (
    1733                                  id_identity, id_relative, id_relation_type
    1734                          ) values (
    1735                                  %s, %s, (select id from dem.relation_types where description = %s)
    1736                          )"""
    1737                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': [self.ID, id_new_relative, rel_type  ]}])
    1738                  return True
    1739          #----------------------------------------------------------------------
    1740 -        def delete_relative(self, relation):
    1741                  # unlink only, don't delete relative itself
    1742                  self.set_relative(None, relation)
    1743          #--------------------------------------------------------
    1744 -        def _get_emergency_contact_from_database(self):
    1745                  if self._payload[self._idx['pk_emergency_contact']] is None:
    1746                          return None
    1747                  return cPerson(self._payload[self._idx['pk_emergency_contact']])
    1748
    1749          emergency_contact_in_database = property(_get_emergency_contact_from_database, lambda x:x)
    1750
    1751          #----------------------------------------------------------------------
    1752          # age/dob related
    1753          #----------------------------------------------------------------------
    1754 -        def get_formatted_dob(self, format='%Y %b %d', none_string=None, honor_estimation=False):
    1755                  return gmDateTime.format_dob (
    1756                          self._payload[self._idx['dob']],
    1757                          format = format,
    1758                          none_string = none_string,
    1759                          dob_is_estimated = self._payload[self._idx['dob_is_estimated']] and honor_estimation
    1760                  )
    1761
    1762          #----------------------------------------------------------------------
    1763 -        def get_medical_age(self):
    1764                  dob = self['dob']
    1765
    1766                  if dob is None:
    1767                          return '??'
    1768
    1769                  if dob > gmDateTime.pydt_now_here():
    1770                          return _('invalid age: DOB in the future')
    1771
    1772                  death = self['deceased']
    1773
    1774                  if death is None:
    1775                          return '%s%s' % (
    1776                                  gmTools.bool2subst (
    1777                                          self._payload[self._idx['dob_is_estimated']],
    1778                                          gmTools.u_almost_equal_to,
    1779                                          ''
    1780                                  ),
    1781                                  gmDateTime.format_apparent_age_medically (
    1782                                          age = gmDateTime.calculate_apparent_age(start = dob)
    1783                                  )
    1784                          )
    1785
    1786                  if dob > death:
    1787                          return _('invalid age: DOB after death')
    1788
    1789                  return '%s%s%s' % (
    1790                          gmTools.u_latin_cross,
    1791                          gmTools.bool2subst (
    1792                                  self._payload[self._idx['dob_is_estimated']],
    1793                                  gmTools.u_almost_equal_to,
    1794                                  ''
    1795                          ),
    1796                          gmDateTime.format_apparent_age_medically (
    1797                                  age = gmDateTime.calculate_apparent_age (
    1798                                          start = dob,
    1799                                          end = self['deceased']
    1800                                  )
    1801                          )
    1802                  )
    1803
    1804          #----------------------------------------------------------------------
    1805 -        def dob_in_range(self, min_distance='1 week', max_distance='1 week'):
    1806                  if self['dob'] is None:
    1807                          return False
    1808                  cmd = 'select dem.dob_is_in_range(%(dob)s, %(min)s, %(max)s)'
    1809                  rows, idx = gmPG2.run_ro_queries (
    1810                          queries = [{
    1811                                  'cmd': cmd,
    1812                                  'args': {'dob': self['dob'], 'min': min_distance, 'max': max_distance}
    1813                          }]
    1814                  )
    1815                  return rows[0][0]
    1816
    1817          #----------------------------------------------------------------------
    1818 -        def _get_current_birthday_passed(self):
    1819                  if self['dob'] is None:
    1820                          return None
    1821                  now = gmDateTime.pydt_now_here()
    1822                  if now.month < self['dob'].month:
    1823                          return False
    1824                  if now.month > self['dob'].month:
    1825                          return True
    1826                  # -> DOB is this month
    1827                  if now.day < self['dob'].day:
    1828                          return False
    1829                  if now.day > self['dob'].day:
    1830                          return True
    1831                  # -> DOB is today
    1832                  return False
    1833
    1834          current_birthday_passed = property(_get_current_birthday_passed)
    1835
    1836          #----------------------------------------------------------------------
    1837 -        def _get_birthday_this_year(self):
    1838                  if self['dob'] is None:
    1839                          return None
    1840                  now = gmDateTime.pydt_now_here()
    1841                  return gmDateTime.pydt_replace (
    1842                          dt = self['dob'],
    1843                          year = now.year,
    1844                          strict = False
    1845                  )
    1846
    1847          birthday_this_year = property(_get_birthday_this_year)
    1848
    1849          #----------------------------------------------------------------------
    1850 -        def _get_birthday_next_year(self):
    1851                  if self['dob'] is None:
    1852                          return None
    1853                  now = gmDateTime.pydt_now_here()
    1854                  return gmDateTime.pydt_replace (
    1855                          dt = self['dob'],
    1856                          year = now.year + 1,
    1857                          strict = False
    1858                  )
    1859
    1860          birthday_next_year = property(_get_birthday_next_year)
    1861
    1862          #----------------------------------------------------------------------
    1863 -        def _get_birthday_last_year(self):
    1864                  if self['dob'] is None:
    1865                          return None
    1866                  now = gmDateTime.pydt_now_here()
    1867                  return gmDateTime.pydt_replace (
    1868                          dt = self['dob'],
    1869                          year = now.year - 1,
    1870                          strict = False
    1871                  )
    1872
    1873          birthday_last_year = property(_get_birthday_last_year, lambda x:x)
    1874
    1875          #----------------------------------------------------------------------
    1876          # practice related
    1877          #----------------------------------------------------------------------
    1878 -        def get_last_encounter(self):
    1879                  cmd = 'select * from clin.v_most_recent_encounters where pk_patient=%s'
    1880                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self._payload[self._idx['pk_identity']]]}])
    1881                  if len(rows) > 0:
    1882                          return rows[0]
    1883                  else:
    1884                          return None
    1885          #--------------------------------------------------------
    1886 -        def get_messages(self, order_by=None):
    1887                  return gmProviderInbox.get_inbox_messages(pk_patient = self._payload[self._idx['pk_identity']], order_by = order_by)
    1888
    1889          messages = property(get_messages, lambda x:x)
    1890          #--------------------------------------------------------
    1891 -        def _get_overdue_messages(self):
    1892                  return gmProviderInbox.get_overdue_messages(pk_patient = self._payload[self._idx['pk_identity']])
    1893
    1894          overdue_messages = property(_get_overdue_messages, lambda x:x)
    1895
    1896          #--------------------------------------------------------
    1897 -        def delete_message(self, pk=None):
    1898                  return gmProviderInbox.delete_inbox_message(inbox_message = pk)
    1899
    1900          #--------------------------------------------------------
    1901 -        def _get_dynamic_hints(self, pk_encounter=None):
    1902                  return gmAutoHints.get_hints_for_patient (
    1903                          pk_identity = self._payload[self._idx['pk_identity']],
    1904                          pk_encounter = pk_encounter
    1905                  )
    1906
    1907          dynamic_hints = property(_get_dynamic_hints, lambda x:x)
    1908
    1909          #--------------------------------------------------------
    1910 -        def _get_suppressed_hints(self):
    1911                  return gmAutoHints.get_suppressed_hints(pk_identity = self._payload[self._idx['pk_identity']])
    1912
    1913          suppressed_hints = property(_get_suppressed_hints, lambda x:x)
    1914
    1915          #--------------------------------------------------------
    1916 -        def _get_primary_provider_identity(self):
    1917                  if self._payload[self._idx['pk_primary_provider']] is None:
    1918                          return None
    1919                  cmd = "SELECT * FROM dem.v_all_persons WHERE pk_identity = (SELECT pk_identity FROM dem.v_staff WHERE pk_staff = %(pk_staff)s)"
    1920                  args = {'pk_staff': self._payload[self._idx['pk_primary_provider']]}
    1921                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1922                  if len(rows) == 0:
    1923                          return None
    1924                  return cPerson(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_identity'})
    1925
    1926          primary_provider_identity = property(_get_primary_provider_identity, lambda x:x)
    1927
    1928          #--------------------------------------------------------
    1929 -        def _get_primary_provider(self):
    1930                  if self._payload[self._idx['pk_primary_provider']] is None:
    1931                          return None
    1932                  from Gnumed.business import gmStaff
    1933                  return gmStaff.cStaff(aPK_obj = self._payload[self._idx['pk_primary_provider']])
    1934
    1935          primary_provider = property(_get_primary_provider, lambda x:x)
    1936
    1937          #----------------------------------------------------------------------
    1938          # convenience
    1939          #----------------------------------------------------------------------
    1940 -        def get_subdir_name(self):
    1941                  """Format patient demographics into patient specific path name fragment."""
    1942
    1943                  return gmTools.fname_sanitize('%s-%s-%s-ID_%s' % (
    1944                          self._payload[self._idx['lastnames']],
    1945                          self._payload[self._idx['firstnames']],
    1946                          self.get_formatted_dob(format = '%Y-%m-%d'),
    1947                          self._payload[self._idx['pk_identity']]         # make unique across "same" patients
    1948                  ))
    1949                  #).replace (
    1950                  #       u'\u2248', u''                  # "approximately", having been added by dob_is_estimated
    1951                  #)
    1952
    1953          subdir_name = property(get_subdir_name, lambda x:x)
    1954
    1955  #============================================================
    1956 -def identity_is_patient(pk_identity):
    1957          cmd = 'SELECT 1 FROM clin.patient WHERE fk_identity = %(pk_pat)s'
    1958          args = {'pk_pat': pk_identity}
    1959          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
    1960          if len(rows) == 0:
    1961                  return False
    1962          return True
    1963
    1964  #------------------------------------------------------------
    1965 -def turn_identity_into_patient(pk_identity):
    1966          cmd = """
    1967                  INSERT INTO clin.patient (fk_identity)
    1968                  SELECT %(pk_ident)s WHERE NOT EXISTS (
    1969                          SELECT 1 FROM clin.patient c_p WHERE fk_identity = %(pk_ident)s
    1970                  )"""
    1971          args = {'pk_ident': pk_identity}
    1972          queries = [{'cmd': cmd, 'args': args}]
    1973          gmPG2.run_rw_queries(queries = queries)
    1974          return True
    1975
    1976  #============================================================
    1977  # helper functions
    1978  #------------------------------------------------------------
    1979  _yield = lambda *x:None
    1980
    1981 -def set_yielder(yielder):
    1982          if not callable(yielder):
    1983                  raise TypeError('yielder <%s> is not callable' % yielder)
    1984          global _yield
    1985          _yield = yielder
    1986          _log.debug('setting yielder to <%s>', yielder)
    1987
    1988  #============================================================
    1989 -class cPatient(cPerson):
    1990          """Represents a person which is a patient.
    1991
    1992          - a specializing subclass of cPerson turning it into a patient
    1993          - its use is to cache subobjects like EMR and document folder
    1994          """
    1995 -        def __init__(self, aPK_obj=None, row=None):
    1996                  cPerson.__init__(self, aPK_obj = aPK_obj, row = row)
    1997                  self.__emr_access_lock = threading.Lock()
    1998                  self.__emr = None
    1999                  self.__doc_folder = None
    2000
    2001          #--------------------------------------------------------
    2002 -        def cleanup(self):
    2003                  """Do cleanups before dying.
    2004
    2005                  - note that this may be called in a thread
    2006                  """
    2007                  if self.__emr is not None:
    2008                          self.__emr.cleanup()
    2009                  if self.__doc_folder is not None:
    2010                          self.__doc_folder.cleanup()
    2011                  cPerson.cleanup(self)
    2012
    2013          #----------------------------------------------------------------
    2014 -        def ensure_has_allergy_state(self, pk_encounter=None):
    2015                  from Gnumed.business.gmAllergy import ensure_has_allergy_state
    2016                  ensure_has_allergy_state(encounter = pk_encounter)
    2017                  return True
    2018
    2019          #----------------------------------------------------------
    2020 -        def get_emr(self):
    2021                  _log.debug('accessing EMR for identity [%s], thread [%s]', self._payload[self._idx['pk_identity']], threading.get_ident())
    2022
    2023                  # fast path: already set, just return it
    2024                  if self.__emr is not None:
    2025                          return self.__emr
    2026
    2027                  stack_logged = False
    2028                  got_lock = self.__emr_access_lock.acquire(False)
    2029                  if not got_lock:
    2030                          # do some logging as we failed to get the lock
    2031                          call_stack = inspect.stack()
    2032                          call_stack.reverse()
    2033                          for idx in range(1, len(call_stack)):
    2034                                  caller = call_stack[idx]
    2035                                  _log.debug('%s[%s] @ [%s] in [%s]', ' '* idx, caller[3], caller[2], caller[1])
    2036                          del call_stack
    2037                          stack_logged = True
    2038                          # now loop a bit
    2039                          for idx in range(500):
    2040                                  _yield()
    2041                                  time.sleep(0.1)
    2042                                  _yield()
    2043                                  got_lock = self.__emr_access_lock.acquire(False)
    2044                                  if got_lock:
    2045                                          break
    2046                          if not got_lock:
    2047                                  _log.error('still failed to acquire EMR access lock, aborting (thread [%s])', threading.get_ident())
    2048                                  self.__emr_access_lock.release()
    2049                                  raise AttributeError('cannot lock access to EMR for identity [%s]' % self._payload[self._idx['pk_identity']])
    2050
    2051                  _log.debug('pulling chart for identity [%s], thread [%s]', self._payload[self._idx['pk_identity']], threading.get_ident())
    2052                  if not stack_logged:
    2053                          # do some logging as we are pulling the chart for the first time
    2054                          call_stack = inspect.stack()
    2055                          call_stack.reverse()
    2056                          for idx in range(1, len(call_stack)):
    2057                                  caller = call_stack[idx]
    2058                                  _log.debug('%s[%s] @ [%s] in [%s]', ' '* idx, caller[3], caller[2], caller[1])
    2059                          del call_stack
    2060                          stack_logged = True
    2061
    2062                  self.is_patient = True
    2063                  from Gnumed.business import gmClinicalRecord
    2064                  emr = gmClinicalRecord.cClinicalRecord(aPKey = self._payload[self._idx['pk_identity']])
    2065
    2066                  _log.debug('returning EMR for identity [%s], thread [%s]', self._payload[self._idx['pk_identity']], threading.get_ident())
    2067                  self.__emr = emr
    2068                  self.__emr_access_lock.release()
    2069                  return self.__emr
    2070
    2071          emr = property(get_emr, lambda x:x)
    2072
    2073          #----------------------------------------------------------
    2074 -        def get_document_folder(self):
    2075                  if self.__doc_folder is None:
    2076                          self.__doc_folder = cDocumentFolder(aPKey = self._payload[self._idx['pk_identity']])
    2077                  return self.__doc_folder
    2078
    2079          document_folder = property(get_document_folder, lambda x:x)
    2080
    2081  #============================================================
    2082 -class gmCurrentPatient(gmBorg.cBorg):
    2083          """Patient Borg to hold the currently active patient.
    2084
    2085          There may be many instances of this but they all share state.
    2086
    2087          The underlying dem.identity row must have .deleted set to FALSE.
    2088
    2089          The sequence of events when changing the active patient:
    2090
    2091                  1) Registered callbacks are run.
    2092                          Those are run synchronously. If a callback
    2093                          returns False or throws an exception the
    2094                          patient switch is aborted. Callback code
    2095                          can rely on the patient still being active
    2096                          and to not go away until it returns. It
    2097                          is not passed any arguments and must return
    2098                          False or True.
    2099
    2100                  2) Signal "pre_patient_unselection" is sent.
    2101                          This does not wait for nor check results.
    2102                          The keyword pk_identity contains the
    2103                          PK of the person being switched away
    2104                          from.
    2105
    2106                  3) the current patient is unset (gmNull.cNull)
    2107
    2108                  4) Signal "current_patient_unset" is sent
    2109                          At this point resetting GUI fields to
    2110                          empty should be done. The active patient
    2111                          is not there anymore.
    2112
    2113                          This does not wait for nor check results.
    2114
    2115                  5) The current patient is set to the new value.
    2116                          The new patient can also remain gmNull.cNull
    2117                          in case the calling code explicitely unset
    2118                          the current patient.
    2119
    2120                  6) Signal "post_patient_selection" is sent.
    2121                          Code listening to this signal can
    2122                          assume that the new patient is
    2123                          already active.
    2124          """
    2125 -        def __init__(self, patient=None, forced_reload=False):
    2126                  """Change or get currently active patient.
    2127
    2128                  patient:
    2129                  * None: get currently active patient
    2130                  * -1: unset currently active patient
    2131                  * cPatient instance: set active patient if possible
    2132                  """
    2133                  # make sure we do have a patient pointer
    2134                  try:
    2135                          self.patient
    2136                  except AttributeError:
    2137                          self.patient = gmNull.cNull()
    2138                          self.__register_interests()
    2139                          # set initial lock state,
    2140                          # this lock protects against activating another patient
    2141                          # when we are controlled from a remote application
    2142                          self.__lock_depth = 0
    2143                          # initialize callback state
    2144                          self.__callbacks_before_switching_away_from_patient = []
    2145
    2146                  # user wants copy of current patient
    2147                  if patient is None:
    2148                          return None
    2149
    2150                  # do nothing if patient is locked
    2151                  if self.locked:
    2152                          _log.error('patient [%s] is locked, cannot change to [%s]' % (self.patient['pk_identity'], patient))
    2153                          return None
    2154
    2155                  # user wants to explicitly unset current patient
    2156                  if patient == -1:
    2157                          _log.debug('explicitly unsetting current patient')
    2158                          if not self.__run_callbacks_before_switching_away_from_patient():
    2159                                  _log.error('not unsetting current patient, at least one pre-change callback failed')
    2160                                  return None
    2161                          self.__send_pre_unselection_notification()
    2162                          self.patient.cleanup()
    2163                          self.patient = gmNull.cNull()
    2164                          self.__send_unselection_notification()
    2165                          # give it some time
    2166                          time.sleep(0.5)
    2167                          self.__send_selection_notification()
    2168                          return None
    2169
    2170                  # must be cPatient instance, then
    2171                  if not isinstance(patient, cPatient):
    2172                          _log.error('cannot set active patient to [%s], must be either None, -1 or cPatient instance' % str(patient))
    2173                          raise TypeError('gmPerson.gmCurrentPatient.__init__(): <patient> must be None, -1 or cPatient instance but is: %s' % str(patient))
    2174
    2175                  # same ID, no change needed
    2176                  if (self.patient['pk_identity'] == patient['pk_identity']) and not forced_reload:
    2177                          return None
    2178
    2179                  if patient['is_deleted']:
    2180                          _log.error('cannot set active patient to disabled dem.identity row: %s', patient)
    2181                          raise ValueError('gmPerson.gmCurrentPatient.__init__(): <patient> is disabled: %s' % patient)
    2182
    2183                  # user wants different patient
    2184                  _log.info('patient change [%s] -> [%s] requested', self.patient['pk_identity'], patient['pk_identity'])
    2185
    2186                  if not self.__run_callbacks_before_switching_away_from_patient():
    2187                          _log.error('not changing current patient, at least one pre-change callback failed')
    2188                          return None
    2189
    2190                  # everything seems swell
    2191                  self.__send_pre_unselection_notification()
    2192                  self.patient.cleanup()
    2193                  self.patient = gmNull.cNull()
    2194                  self.__send_unselection_notification()
    2195                  # give it some time
    2196                  time.sleep(0.5)
    2197                  self.patient = patient
    2198                  # for good measure ...
    2199                  # however, actually we want to get rid of that
    2200                  self.patient.emr
    2201                  self.__send_selection_notification()
    2202
    2203                  return None
    2204
    2205          #--------------------------------------------------------
    2206 -        def __register_interests(self):
    2207                  gmDispatcher.connect(signal = 'gm_table_mod', receiver = self._on_database_signal)
    2208
    2209          #--------------------------------------------------------
    2210 -        def _on_database_signal(self, **kwds):
    2211                  # we don't have a patient: don't process signals
    2212                  if isinstance(self.patient, gmNull.cNull):
    2213                          return True
    2214
    2215                  # we only care about identity and name changes
    2216                  if kwds['table'] not in ['dem.identity', 'dem.names']:
    2217                          return True
    2218
    2219                  # signal is not about our patient: ignore signal
    2220                  if int(kwds['pk_identity']) != self.patient.ID:
    2221                          return True
    2222
    2223                  if kwds['table'] == 'dem.identity':
    2224                          # we don't care about newly INSERTed or DELETEd patients
    2225                          if kwds['operation'] != 'UPDATE':
    2226                                  return True
    2227
    2228                  self.patient.refetch_payload()
    2229                  return True
    2230
    2231          #--------------------------------------------------------
    2232          # external API
    2233          #--------------------------------------------------------
    2234 -        def register_before_switching_from_patient_callback(self, callback=None):
    2235                  # callbacks are run synchronously before
    2236                  # switching *away* from the current patient,
    2237                  # if a callback returns false the current
    2238                  # patient will not be switched away from,
    2239                  # callbacks will not be passed any arguments
    2240                  if not callable(callback):
    2241                          raise TypeError('callback [%s] not callable' % callback)
    2242
    2243                  self.__callbacks_before_switching_away_from_patient.append(callback)
    2244
    2245          #--------------------------------------------------------
    2246 -        def _get_connected(self):
    2247                  return (not isinstance(self.patient, gmNull.cNull))
    2248
    2249          connected = property(_get_connected, lambda x:x)
    2250
    2251          #--------------------------------------------------------
    2252 -        def _get_locked(self):
    2253                  return (self.__lock_depth > 0)
    2254
    2255 -        def _set_locked(self, locked):
    2256                  if locked:
    2257                          self.__lock_depth = self.__lock_depth + 1
    2258                          gmDispatcher.send(signal = 'patient_locked', sender = self.__class__.__name__)
    2259                  else:
    2260                          if self.__lock_depth == 0:
    2261                                  _log.error('lock/unlock imbalance, tried to refcount lock depth below 0')
    2262                                  return
    2263                          else:
    2264                                  self.__lock_depth = self.__lock_depth - 1
    2265                          gmDispatcher.send(signal = 'patient_unlocked', sender = self.__class__.__name__)
    2266
    2267          locked = property(_get_locked, _set_locked)
    2268
    2269          #--------------------------------------------------------
    2270 -        def force_unlock(self):
    2271                  _log.info('forced patient unlock at lock depth [%s]' % self.__lock_depth)
    2272                  self.__lock_depth = 0
    2273                  gmDispatcher.send(signal = 'patient_unlocked', sender = self.__class__.__name__)
    2274
    2275          #--------------------------------------------------------
    2276          # patient change handling
    2277          #--------------------------------------------------------
    2278 -        def __run_callbacks_before_switching_away_from_patient(self):
    2279                  if isinstance(self.patient, gmNull.cNull):
    2280                          return True
    2281
    2282                  for call_back in self.__callbacks_before_switching_away_from_patient:
    2283                          try:
    2284                                  successful = call_back()
    2285                          except Exception:
    2286                                  _log.exception('callback [%s] failed', call_back)
    2287                                  print("*** pre-change callback failed ***")
    2288                                  print(type(call_back))
    2289                                  print(call_back)
    2290                                  return False
    2291
    2292                          if not successful:
    2293                                  _log.error('callback [%s] returned False', call_back)
    2294                                  return False
    2295
    2296                  return True
    2297
    2298          #--------------------------------------------------------
    2299 -        def __send_pre_unselection_notification(self):
    2300                  """Sends signal when current patient is about to be unset.
    2301
    2302                  This does NOT wait for signal handlers to complete.
    2303                  """
    2304                  kwargs = {
    2305                          'signal': 'pre_patient_unselection',
    2306                          'sender': self.__class__.__name__,
    2307                          'pk_identity': self.patient['pk_identity']
    2308                  }
    2309                  gmDispatcher.send(**kwargs)
    2310
    2311          #--------------------------------------------------------
    2312 -        def __send_unselection_notification(self):
    2313                  """Sends signal when the previously active patient has
    2314                     been unset during a change of active patient.
    2315
    2316                  This is the time to initialize GUI fields to empty values.
    2317
    2318                  This does NOT wait for signal handlers to complete.
    2319                  """
    2320                  kwargs = {
    2321                          'signal': 'current_patient_unset',
    2322                          'sender': self.__class__.__name__
    2323                  }
    2324                  gmDispatcher.send(**kwargs)
    2325
    2326          #--------------------------------------------------------
    2327 -        def __send_selection_notification(self):
    2328                  """Sends signal when another patient has actually been made active."""
    2329                  kwargs = {
    2330                          'signal': 'post_patient_selection',
    2331                          'sender': self.__class__.__name__,
    2332                          'pk_identity': self.patient['pk_identity'],
    2333                          'current_patient': self
    2334                  }
    2335                  gmDispatcher.send(**kwargs)
    2336
    2337          #--------------------------------------------------------
    2338          # __getattr__ handling
    2339          #--------------------------------------------------------
    2340 -        def __getattr__(self, attribute):
    2341                  # override __getattr__ here, not __getattribute__ because
    2342                  # the former is used _after_ ordinary attribute lookup
    2343                  # failed while the latter is applied _before_ ordinary
    2344                  # lookup (and is easy to drive into infinite recursion),
    2345                  # this is also why subsequent access to self.patient
    2346                  # simply returns the .patient member value :-)
    2347                  if attribute == 'patient':
    2348                          raise AttributeError
    2349                  if isinstance(self.patient, gmNull.cNull):
    2350                          _log.error("[%s]: cannot getattr(%s, '%s'), patient attribute not connected to a patient", self, self.patient, attribute)
    2351                          raise AttributeError("[%s]: cannot getattr(%s, '%s'), patient attribute not connected to a patient" % (self, self.patient, attribute))
    2352                  return getattr(self.patient, attribute)
    2353
    2354          #--------------------------------------------------------
    2355          # __get/setitem__ handling
    2356          #--------------------------------------------------------
    2357 -        def __getitem__(self, attribute = None):
    2358                  """Return any attribute if known how to retrieve it by proxy.
    2359                  """
    2360                  return self.patient[attribute]
    2361
    2362          #--------------------------------------------------------
    2363 -        def __setitem__(self, attribute, value):
    2364                  self.patient[attribute] = value
    2365
    2366  #============================================================
    2367  # match providers
    2368  #============================================================
    2369 -class cMatchProvider_Provider(gmMatchProvider.cMatchProvider_SQL2):
    2370 -        def __init__(self):
    2371                  gmMatchProvider.cMatchProvider_SQL2.__init__(
    2372                          self,
    2373                          queries = [
    2374                                  """SELECT
    2375                                                  pk_staff AS data,
    2376                                                  short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS list_label,
    2377                                                  short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS field_label
    2378                                          FROM dem.v_staff
    2379                                          WHERE
    2380                                                  is_active AND (
    2381                                                          short_alias %(fragment_condition)s OR
    2382                                                          firstnames %(fragment_condition)s OR
    2383                                                          lastnames %(fragment_condition)s OR
    2384                                                          db_user %(fragment_condition)s
    2385                                                  )
    2386                                  """
    2387                          ]
    2388                  )
    2389                  self.setThresholds(1, 2, 3)
    2390
    2391  #============================================================
    2392  # convenience functions
    2393  #============================================================
    2394 -def create_name(pk_person, firstnames, lastnames, active=False):
    2395          queries = [{
    2396                  'cmd': "select dem.add_name(%s, %s, %s, %s)",
    2397                  'args': [pk_person, firstnames, lastnames, active]
    2398          }]
    2399          rows, idx = gmPG2.run_rw_queries(queries=queries, return_data=True)
    2400          name = cPersonName(aPK_obj = rows[0][0])
    2401          return name
    2402
    2403  #============================================================
    2404 -def create_identity(gender=None, dob=None, lastnames=None, firstnames=None, comment=None):
    2405
    2406          cmd1 = "INSERT INTO dem.identity (gender, dob, comment) VALUES (%s, %s, %s)"
    2407          cmd2 = """
    2408  INSERT INTO dem.names (
    2409          id_identity, lastnames, firstnames
    2410  ) VALUES (
    2411          currval('dem.identity_pk_seq'), coalesce(%s, 'xxxDEFAULTxxx'), coalesce(%s, 'xxxDEFAULTxxx')
    2412  ) RETURNING id_identity"""
    2413  #       cmd2 = u"select dem.add_name(currval('dem.identity_pk_seq')::integer, coalesce(%s, 'xxxDEFAULTxxx'), coalesce(%s, 'xxxDEFAULTxxx'), True)"
    2414          try:
    2415                  rows, idx = gmPG2.run_rw_queries (
    2416                          queries = [
    2417                                  {'cmd': cmd1, 'args': [gender, dob, comment]},
    2418                                  {'cmd': cmd2, 'args': [lastnames, firstnames]}
    2419                                  #{'cmd': cmd2, 'args': [firstnames, lastnames]}
    2420                          ],
    2421                          return_data = True
    2422                  )
    2423          except Exception:
    2424                  _log.exception('cannot create identity')
    2425                  gmLog2.log_stack_trace()
    2426                  return None
    2427          ident = cPerson(aPK_obj = rows[0][0])
    2428          gmHooks.run_hook_script(hook = 'post_person_creation')
    2429          return ident
    2430
    2431  #============================================================
    2432 -def disable_identity(pk_identity):
    2433          _log.info('disabling identity [%s]', pk_identity)
    2434          cmd = "UPDATE dem.identity SET deleted = true WHERE pk = %(pk)s"
    2435          args = {'pk': pk_identity}
    2436          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    2437          return True
    2438
    2439  #============================================================
    2440 -def create_dummy_identity():
    2441          cmd = "INSERT INTO dem.identity(gender) VALUES (NULL::text) RETURNING pk"
    2442          rows, idx = gmPG2.run_rw_queries (
    2443                  queries = [{'cmd': cmd}],
    2444                  return_data = True
    2445          )
    2446          return gmDemographicRecord.cPerson(aPK_obj = rows[0][0])
    2447
    2448  #============================================================
    2449 -def identity_exists(pk_identity):
    2450          cmd = 'SELECT EXISTS(SELECT 1 FROM dem.identity where pk = %(pk)s)'
    2451          args = {'pk': pk_identity}
    2452          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
    2453          return rows[0][0]
    2454
    2455  #============================================================
    2456 -def set_active_patient(patient=None, forced_reload=False):
    2457          """Set active patient.
    2458
    2459          If patient is -1 the active patient will be UNset.
    2460          """
    2461          if isinstance(patient, gmCurrentPatient):
    2462                  return True
    2463
    2464          if isinstance(patient, cPatient):
    2465                  pat = patient
    2466          elif isinstance(patient, cPerson):
    2467                  pat = pat.as_patient
    2468          elif patient == -1:
    2469                  pat = patient
    2470          else:
    2471                  # maybe integer ?
    2472                  success, pk = gmTools.input2int(initial = patient, minval = 1)
    2473                  if not success:
    2474                          raise ValueError('<patient> must be either -1, >0, or a cPatient, cPerson or gmCurrentPatient instance, is: %s' % patient)
    2475                  # but also valid patient ID ?
    2476                  try:
    2477                          pat = cPatient(aPK_obj = pk)
    2478                  except Exception:
    2479                          _log.exception('identity [%s] not found' % patient)
    2480                          return False
    2481
    2482          # attempt to switch
    2483          try:
    2484                  gmCurrentPatient(patient = pat, forced_reload = forced_reload)
    2485          except Exception:
    2486                  _log.exception('error changing active patient to [%s]' % patient)
    2487                  return False
    2488
    2489          return True
    2490
    2491  #============================================================
    2492  # gender related
    2493  #------------------------------------------------------------
    2494 -def get_gender_list():
    2495          """Retrieves the list of known genders from the database."""
    2496          global __gender_idx
    2497          global __gender_list
    2498
    2499          if __gender_list is None:
    2500                  cmd = "SELECT tag, l10n_tag, label, l10n_label, sort_weight FROM dem.v_gender_labels ORDER BY sort_weight DESC"
    2501                  __gender_list, __gender_idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True)
    2502                  _log.debug('genders in database: %s' % __gender_list)
    2503
    2504          return (__gender_list, __gender_idx)
    2505
    2506  #------------------------------------------------------------
    2507  map_gender2mf = {
    2508          'm': 'm',
    2509          'f': 'f',
    2510          'tf': 'f',
    2511          'tm': 'm',
    2512          'h': 'mf'
    2513  }
    2514
    2515  # https://tools.ietf.org/html/rfc6350#section-6.2.7
    2516  # M F O N U
    2517  map_gender2vcard = {
    2518          'm': 'M',
    2519          'f': 'F',
    2520          'tf': 'F',
    2521          'tm': 'M',
    2522          'h': 'O',
    2523          None: 'U'
    2524  }
    2525
    2526  #------------------------------------------------------------
    2527  # maps GNUmed related i18n-aware gender specifiers to a unicode symbol
    2528  map_gender2symbol = {
    2529          'm': '\u2642',
    2530          'f': '\u2640',
    2531          'tf': '\u26A5\u2640',
    2532  #       'tf': u'\u2642\u2640-\u2640',
    2533          'tm': '\u26A5\u2642',
    2534  #       'tm': u'\u2642\u2640-\u2642',
    2535          'h': '\u26A5',
    2536  #       'h': u'\u2642\u2640',
    2537          None: '?\u26A5?'
    2538  }
    2539  #------------------------------------------------------------
    2540 -def map_gender2string(gender=None):
    2541          """Maps GNUmed related i18n-aware gender specifiers to a human-readable string."""
    2542
    2543          global __gender2string_map
    2544
    2545          if __gender2string_map is None:
    2546                  genders, idx = get_gender_list()
    2547                  __gender2string_map = {
    2548                          'm': _('male'),
    2549                          'f': _('female'),
    2550                          'tf': '',
    2551                          'tm': '',
    2552                          'h': '',
    2553                          None: _('unknown gender')
    2554                  }
    2555                  for g in genders:
    2556                          __gender2string_map[g[idx['l10n_tag']]] = g[idx['l10n_label']]
    2557                          __gender2string_map[g[idx['tag']]] = g[idx['l10n_label']]
    2558                  _log.debug('gender -> string mapping: %s' % __gender2string_map)
    2559
    2560          return __gender2string_map[gender]
    2561  #------------------------------------------------------------
    2562 -def map_gender2salutation(gender=None):
    2563          """Maps GNUmed related i18n-aware gender specifiers to a human-readable salutation."""
    2564
    2565          global __gender2salutation_map
    2566
    2567          if __gender2salutation_map is None:
    2568                  genders, idx = get_gender_list()
    2569                  __gender2salutation_map = {
    2570                          'm': _('Mr'),
    2571                          'f': _('Mrs'),
    2572                          'tf': '',
    2573                          'tm': '',
    2574                          'h': '',
    2575                          None: ''
    2576                  }
    2577                  for g in genders:
    2578                          __gender2salutation_map[g[idx['l10n_tag']]] = __gender2salutation_map[g[idx['tag']]]
    2579                          __gender2salutation_map[g[idx['label']]] = __gender2salutation_map[g[idx['tag']]]
    2580                          __gender2salutation_map[g[idx['l10n_label']]] = __gender2salutation_map[g[idx['tag']]]
    2581                  _log.debug('gender -> salutation mapping: %s' % __gender2salutation_map)
    2582
    2583          return __gender2salutation_map[gender]
    2584  #------------------------------------------------------------
    2585 -def map_firstnames2gender(firstnames=None):
    2586          """Try getting the gender for the given first name."""
    2587
    2588          if firstnames is None:
    2589                  return None
    2590
    2591          rows, idx = gmPG2.run_ro_queries(queries = [{
    2592                  'cmd': "SELECT gender FROM dem.name_gender_map WHERE name ILIKE %(fn)s LIMIT 1",
    2593                  'args': {'fn': firstnames}
    2594          }])
    2595
    2596          if len(rows) == 0:
    2597                  return None
    2598
    2599          return rows[0][0]
    2600  #============================================================
    2601 -def get_person_IDs():
    2602          cmd = 'SELECT pk FROM dem.identity'
    2603          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = False)
    2604          return [ r[0] for r in rows ]
    2605
    2606  #============================================================
    2607 -def get_persons_from_pks(pks=None):
    2608          return [ cPerson(aPK_obj = pk) for pk in pks ]
    2609
    2610  #============================================================
    2611 -def get_person_from_xdt(filename=None, encoding=None, dob_format=None):
    2612          from Gnumed.business import gmXdtObjects
    2613          return gmXdtObjects.read_person_from_xdt(filename=filename, encoding=encoding, dob_format=dob_format)
    2614
    2615  #============================================================
    2616 -def get_persons_from_pracsoft_file(filename=None, encoding='ascii'):
    2617          from Gnumed.business import gmPracSoftAU
    2618          return gmPracSoftAU.read_persons_from_pracsoft_file(filename=filename, encoding=encoding)
    2619
    2620  #============================================================
    2621  # main/testing
    2622  #============================================================
    2623  if __name__ == '__main__':
    2624
    2625          if len(sys.argv) == 1:
    2626                  sys.exit()
    2627
    2628          if sys.argv[1] != 'test':
    2629                  sys.exit()
    2630
    2631          import datetime
    2632
    2633          gmI18N.activate_locale()
    2634          gmI18N.install_domain()
    2635          gmDateTime.init()
    2636
    2637          #--------------------------------------------------------
    2638 -        def test_set_active_pat():
    2639
    2640                  ident = cPerson(1)
    2641                  print("setting active patient with", ident)
    2642                  set_active_patient(patient=ident)
    2643
    2644                  patient = cPatient(12)
    2645                  print("setting active patient with", patient)
    2646                  set_active_patient(patient=patient)
    2647
    2648                  pat = gmCurrentPatient()
    2649                  print(pat['dob'])
    2650                  #pat['dob'] = 'test'
    2651
    2652  #               staff = cStaff()
    2653  #               print("setting active patient with", staff)
    2654  #               set_active_patient(patient=staff)
    2655
    2656                  print("setting active patient with -1")
    2657                  set_active_patient(patient=-1)
    2658          #--------------------------------------------------------
    2659 -        def test_dto_person():
    2660                  dto = cDTO_person()
    2661                  dto.firstnames = 'Sepp'
    2662                  dto.lastnames = 'Herberger'
    2663                  dto.gender = 'male'
    2664                  dto.dob = pyDT.datetime.now(tz=gmDateTime.gmCurrentLocalTimezone)
    2665                  print(dto)
    2666
    2667                  print(dto['firstnames'])
    2668                  print(dto['lastnames'])
    2669                  print(dto['gender'])
    2670                  print(dto['dob'])
    2671
    2672                  for key in dto.keys():
    2673                          print(key)
    2674          #--------------------------------------------------------
    2675 -        def test_identity():
    2676                  # create patient
    2677                  print('\n\nCreating identity...')
    2678                  new_identity = create_identity(gender='m', dob='2005-01-01', lastnames='test lastnames', firstnames='test firstnames')
    2679                  print('Identity created: %s' % new_identity)
    2680
    2681                  print('\nSetting title and gender...')
    2682                  new_identity['title'] = 'test title';
    2683                  new_identity['gender'] = 'f';
    2684                  new_identity.save_payload()
    2685                  print('Refetching identity from db: %s' % cPerson(aPK_obj=new_identity['pk_identity']))
    2686
    2687                  print('\nGetting all names...')
    2688                  for a_name in new_identity.get_names():
    2689                          print(a_name)
    2690                  print('Active name: %s' % (new_identity.get_active_name()))
    2691                  print('Setting nickname...')
    2692                  new_identity.set_nickname(nickname='test nickname')
    2693                  print('Refetching all names...')
    2694                  for a_name in new_identity.get_names():
    2695                          print(a_name)
    2696                  print('Active name: %s' % (new_identity.get_active_name()))
    2697
    2698                  print('\nIdentity occupations: %s' % new_identity['occupations'])
    2699                  print('Creating identity occupation...')
    2700                  new_identity.link_occupation('test occupation')
    2701                  print('Identity occupations: %s' % new_identity['occupations'])
    2702
    2703                  print('\nIdentity addresses: %s' % new_identity.get_addresses())
    2704                  print('Creating identity address...')
    2705                  # make sure the state exists in the backend
    2706                  new_identity.link_address (
    2707                          number = 'test 1234',
    2708                          street = 'test street',
    2709                          postcode = 'test postcode',
    2710                          urb = 'test urb',
    2711                          region_code = 'SN',
    2712                          country_code = 'DE'
    2713                  )
    2714                  print('Identity addresses: %s' % new_identity.get_addresses())
    2715
    2716                  print('\nIdentity communications: %s' % new_identity.get_comm_channels())
    2717                  print('Creating identity communication...')
    2718                  new_identity.link_comm_channel('homephone', '1234566')
    2719                  print('Identity communications: %s' % new_identity.get_comm_channels())
    2720          #--------------------------------------------------------
    2721 -        def test_name():
    2722                  for pk in range(1,16):
    2723                          name = cPersonName(aPK_obj=pk)
    2724                          print(name.description)
    2725                          print('  ', name)
    2726          #--------------------------------------------------------
    2727 -        def test_gender_list():
    2728                  genders, idx = get_gender_list()
    2729                  print("\n\nRetrieving gender enum (tag, label, weight):")
    2730                  for gender in genders:
    2731                          print("%s, %s, %s" % (gender[idx['tag']], gender[idx['l10n_label']], gender[idx['sort_weight']]))
    2732          #--------------------------------------------------------
    2733 -        def test_export_area():
    2734                  person = cPerson(aPK_obj = 12)
    2735                  print(person)
    2736                  print(person.export_area)
    2737                  print(person.export_area.items)
    2738          #--------------------------------------------------------
    2739 -        def test_ext_id():
    2740                  person = cPerson(aPK_obj = 9)
    2741                  print(person.get_external_ids(id_type='Fachgebiet', issuer='Ärztekammer'))
    2742                  #print(person.get_external_ids()
    2743          #--------------------------------------------------------
    2744 -        def test_vcf():
    2745                  person = cPerson(aPK_obj = 12)
    2746                  print(person.export_as_vcard())
    2747
    2748          #--------------------------------------------------------
    2749 -        def test_mecard():
    2750                  person = cPerson(aPK_obj = 12)
    2751                  print(person.MECARD)
    2752                  mcf = person.export_as_mecard()
    2753                  print(mcf)
    2754                  #print(gmTools.create_qrcode(filename = mcf, qr_filename = None, verbose = True)
    2755                  print(gmTools.create_qrcode(text = person.MECARD, qr_filename = None, verbose = True))
    2756
    2757          #--------------------------------------------------------
    2758 -        def test_current_patient():
    2759                  pat = gmCurrentPatient()
    2760                  print("pat.emr", pat.emr)
    2761
    2762          #--------------------------------------------------------
    2763 -        def test_ext_id():
    2764                  person = cPerson(aPK_obj = 12)
    2765                  print(person.suggest_external_id(target = 'Orthanc'))
    2766
    2767          #--------------------------------------------------------
    2768 -        def test_assimilate_identity():
    2769                  patient = cPatient(12)
    2770                  set_active_patient(patient = patient)
    2771                  curr_pat = gmCurrentPatient()
    2772                  other_pat = cPerson(1111111)
    2773                  curr_pat.assimilate_identity(other_identity=None)
    2774
    2775          #--------------------------------------------------------
    2776          #test_dto_person()
    2777          #test_identity()
    2778          #test_set_active_pat()
    2779          #test_search_by_dto()
    2780          #test_name()
    2781          #test_gender_list()
    2782
    2783          #map_gender2salutation('m')
    2784          # module functions
    2785
    2786          #comms = get_comm_list()
    2787          #print("\n\nRetrieving communication media enum (id, description): %s" % comms)
    2788          #test_export_area()
    2789          #test_ext_id()
    2790          #test_vcf()
    2791          test_mecard()
    2792          #test_ext_id()
    2793          #test_current_patient()
    2794          #test_assimilate_identity()
    2795
    2796  #============================================================
    2797