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

# Source Code for Module Gnumed.business.gmPerson

       1  # -*- coding: utf8 -*-
       2  """GNUmed patient objects.
       3
       4  This is a patient object intended to let a useful client-side
       5  API crystallize from actual use in true XP fashion.
       6  """
       7  #============================================================
       8  __version__ = "$Revision: 1.198 $"
       9  __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
      10  __license__ = "GPL"
      11
      12  # std lib
      13  import sys, os.path, time, re as regex, string, types, datetime as pyDT, codecs, threading, logging
      14
      15
      16  # GNUmed
      17  if __name__ == '__main__':
      18          sys.path.insert(0, '../../')
      19  from Gnumed.pycommon import gmExceptions, gmDispatcher, gmBorg, gmI18N, gmNull, gmBusinessDBObject, gmTools
      20  from Gnumed.pycommon import gmPG2
      21  from Gnumed.pycommon import gmDateTime
      22  from Gnumed.pycommon import gmMatchProvider
      23  from Gnumed.pycommon import gmLog2
      24  from Gnumed.pycommon import gmHooks
      25
      26  from Gnumed.business import gmDemographicRecord
      27  from Gnumed.business import gmClinicalRecord
      28  from Gnumed.business import gmXdtMappings
      29  from Gnumed.business import gmProviderInbox
      30  from Gnumed.business.gmDocuments import cDocumentFolder
      31
      32
      33  _log = logging.getLogger('gm.person')
      34  _log.info(__version__)
      35
      36  __gender_list = None
      37  __gender_idx = None
      38
      39  __gender2salutation_map = None
      40
      41  #============================================================
      42  # FIXME: make this work as a mapping type, too
      43 -class cDTO_person(object):
      44
      45 -        def __init__(self):
      46                  self.identity = None
      47                  self.external_ids = []
      48                  self.comm_channels = []
      49                  self.addresses = []
      50          #--------------------------------------------------------
      51          # external API
      52          #--------------------------------------------------------
      53 -        def keys(self):
      54                  return 'firstnames lastnames dob gender'.split()
      55          #--------------------------------------------------------
      56 -        def delete_from_source(self):
      57                  pass
      58          #--------------------------------------------------------
      59 -        def get_candidate_identities(self, can_create=False):
      60                  """Generate generic queries.
      61
      62                  - not locale dependant
      63                  - data -> firstnames, lastnames, dob, gender
      64
      65                  shall we mogrify name parts ? probably not as external
      66                  sources should know what they do
      67
      68                  finds by inactive name, too, but then shows
      69                  the corresponding active name ;-)
      70
      71                  Returns list of matching identities (may be empty)
      72                  or None if it was told to create an identity but couldn't.
      73                  """
      74                  where_snippets = []
      75                  args = {}
      76
      77                  where_snippets.append(u'firstnames = %(first)s')
      78                  args['first'] = self.firstnames
      79
      80                  where_snippets.append(u'lastnames = %(last)s')
      81                  args['last'] = self.lastnames
      82
      83                  if self.dob is not None:
      84                          where_snippets.append(u"dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %(dob)s)")
      85                          args['dob'] = self.dob.replace(hour = 23, minute = 59, second = 59)
      86
      87                  if self.gender is not None:
      88                          where_snippets.append('gender = %(sex)s')
      89                          args['sex'] = self.gender
      90
      91                  cmd = u"""
      92  SELECT *, '%s' AS match_type
      93  FROM dem.v_basic_person
      94  WHERE
      95          pk_identity IN (
      96                  SELECT pk_identity FROM dem.v_person_names WHERE %s
      97          )
      98  ORDER BY lastnames, firstnames, dob""" % (
      99                  _('external patient source (name, gender, date of birth)'),
     100                  ' AND '.join(where_snippets)
     101                  )
     102
     103                  try:
     104                          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx=True)
     105                  except:
     106                          _log.error(u'cannot get candidate identities for dto "%s"' % self)
     107                          _log.exception('query %s' % cmd)
     108                          rows = []
     109
     110                  if len(rows) == 0:
     111                          _log.debug('no candidate identity matches found')
     112                          if not can_create:
     113                                  return []
     114                          ident = self.import_into_database()
     115                          if ident is None:
     116                                  return None
     117                          identities = [ident]
     118                  else:
     119                          identities = [ cIdentity(row = {'pk_field': 'pk_identity', 'data': row, 'idx': idx}) for row in rows ]
     120
     121                  return identities
     122          #--------------------------------------------------------
     123 -        def import_into_database(self):
     124                  """Imports self into the database."""
     125
     126                  self.identity = create_identity (
     127                          firstnames = self.firstnames,
     128                          lastnames = self.lastnames,
     129                          gender = self.gender,
     130                          dob = self.dob
     131                  )
     132
     133                  if self.identity is None:
     134                          return None
     135
     136                  for ext_id in self.external_ids:
     137                          try:
     138                                  self.identity.add_external_id (
     139                                          type_name = ext_id['name'],
     140                                          value = ext_id['value'],
     141                                          issuer = ext_id['issuer'],
     142                                          comment = ext_id['comment']
     143                                  )
     144                          except StandardError:
     145                                  _log.exception('cannot import <external ID> from external data source')
     146                                  _log.log_stack_trace()
     147
     148                  for comm in self.comm_channels:
     149                          try:
     150                                  self.identity.link_comm_channel (
     151                                          comm_medium = comm['channel'],
     152                                          url = comm['url']
     153                                  )
     154                          except StandardError:
     155                                  _log.exception('cannot import <comm channel> from external data source')
     156                                  _log.log_stack_trace()
     157
     158                  for adr in self.addresses:
     159                          try:
     160                                  self.identity.link_address (
     161                                          number = adr['number'],
     162                                          street = adr['street'],
     163                                          postcode = adr['zip'],
     164                                          urb = adr['urb'],
     165                                          state = adr['region'],
     166                                          country = adr['country']
     167                                  )
     168                          except StandardError:
     169                                  _log.exception('cannot import <address> from external data source')
     170                                  _log.log_stack_trace()
     171
     172                  return self.identity
     173          #--------------------------------------------------------
     174 -        def import_extra_data(self, *args, **kwargs):
     175                  pass
     176          #--------------------------------------------------------
     177 -        def remember_external_id(self, name=None, value=None, issuer=None, comment=None):
     178                  value = value.strip()
     179                  if value == u'':
     180                          return
     181                  name = name.strip()
     182                  if name == u'':
     183                          raise ValueError(_('<name> cannot be empty'))
     184                  issuer = issuer.strip()
     185                  if issuer == u'':
     186                          raise ValueError(_('<issuer> cannot be empty'))
     187                  self.external_ids.append({'name': name, 'value': value, 'issuer': issuer, 'comment': comment})
     188          #--------------------------------------------------------
     189 -        def remember_comm_channel(self, channel=None, url=None):
     190                  url = url.strip()
     191                  if url == u'':
     192                          return
     193                  channel = channel.strip()
     194                  if channel == u'':
     195                          raise ValueError(_('<channel> cannot be empty'))
     196                  self.comm_channels.append({'channel': channel, 'url': url})
     197          #--------------------------------------------------------
     198 -        def remember_address(self, number=None, street=None, urb=None, region=None, zip=None, country=None):
     199                  number = number.strip()
     200                  if number == u'':
     201                          raise ValueError(_('<number> cannot be empty'))
     202                  street = street.strip()
     203                  if street == u'':
     204                          raise ValueError(_('<street> cannot be empty'))
     205                  urb = urb.strip()
     206                  if urb == u'':
     207                          raise ValueError(_('<urb> cannot be empty'))
     208                  zip = zip.strip()
     209                  if zip == u'':
     210                          raise ValueError(_('<zip> cannot be empty'))
     211                  country = country.strip()
     212                  if country == u'':
     213                          raise ValueError(_('<country> cannot be empty'))
     214                  region = region.strip()
     215                  if region == u'':
     216                          region = u'??'
     217                  self.addresses.append ({
     218                          u'number': number,
     219                          u'street': street,
     220                          u'zip': zip,
     221                          u'urb': urb,
     222                          u'region': region,
     223                          u'country': country
     224                  })
     225          #--------------------------------------------------------
     226          # customizing behaviour
     227          #--------------------------------------------------------
     228 -        def __str__(self):
     229                  return u'<%s @ %s: %s %s (%s) %s>' % (
     230                          self.__class__.__name__,
     231                          id(self),
     232                          self.firstnames,
     233                          self.lastnames,
     234                          self.gender,
     235                          self.dob
     236                  )
     237          #--------------------------------------------------------
     238 -        def __setattr__(self, attr, val):
     239                  """Do some sanity checks on self.* access."""
     240
     241                  if attr == 'gender':
     242                          glist, idx = get_gender_list()
     243                          for gender in glist:
     244                                  if str(val) in [gender[0], gender[1], gender[2], gender[3]]:
     245                                          val = gender[idx['tag']]
     246                                          object.__setattr__(self, attr, val)
     247                                          return
     248                          raise ValueError('invalid gender: [%s]' % val)
     249
     250                  if attr == 'dob':
     251                          if val is not None:
     252                                  if not isinstance(val, pyDT.datetime):
     253                                          raise TypeError('invalid type for DOB (must be datetime.datetime): %s [%s]' % (type(val), val))
     254                                  if val.tzinfo is None:
     255                                          raise ValueError('datetime.datetime instance is lacking a time zone: [%s]' % val.isoformat())
     256
     257                  object.__setattr__(self, attr, val)
     258                  return
     259          #--------------------------------------------------------
     260 -        def __getitem__(self, attr):
     261                  return getattr(self, attr)
     262  #============================================================
     263 -class cPersonName(gmBusinessDBObject.cBusinessDBObject):
     264          _cmd_fetch_payload = u"SELECT * FROM dem.v_person_names WHERE pk_name = %s"
     265          _cmds_store_payload = [
     266                  u"""UPDATE dem.names SET
     267                          active = FALSE
     268                  WHERE
     269                          %(active_name)s IS TRUE                         -- act only when needed and only
     270                                  AND
     271                          id_identity = %(pk_identity)s           -- on names of this identity
     272                                  AND
     273                          active IS TRUE                                          -- which are active
     274                                  AND
     275                          id != %(pk_name)s                                       -- but NOT *this* name
     276                          """,
     277                  u"""update dem.names set
     278                          active = %(active_name)s,
     279                          preferred = %(preferred)s,
     280                          comment = %(comment)s
     281                  where
     282                          id = %(pk_name)s and
     283                          id_identity = %(pk_identity)s and       -- belt and suspenders
     284                          xmin = %(xmin_name)s""",
     285                  u"""select xmin as xmin_name from dem.names where id = %(pk_name)s"""
     286          ]
     287          _updatable_fields = ['active_name', 'preferred', 'comment']
     288          #--------------------------------------------------------
     289 -        def __setitem__(self, attribute, value):
     290                  if attribute == 'active_name':
     291                          # cannot *directly* deactivate a name, only indirectly
     292                          # by activating another one
     293                          # FIXME: should be done at DB level
     294                          if self._payload[self._idx['active_name']] is True:
     295                                  return
     296                  gmBusinessDBObject.cBusinessDBObject.__setitem__(self, attribute, value)
     297          #--------------------------------------------------------
     298 -        def _get_description(self):
     299                  return '%(last)s, %(title)s %(first)s%(nick)s' % {
     300                          'last': self._payload[self._idx['lastnames']],
     301                          'title': gmTools.coalesce (
     302                                  self._payload[self._idx['title']],
     303                                  map_gender2salutation(self._payload[self._idx['gender']])
     304                          ),
     305                          'first': self._payload[self._idx['firstnames']],
     306                          'nick': gmTools.coalesce(self._payload[self._idx['preferred']], u'', u" '%s'", u'%s')
     307                  }
     308
     309          description = property(_get_description, lambda x:x)
     310  #============================================================
     311 -class cIdentity(gmBusinessDBObject.cBusinessDBObject):
     312          _cmd_fetch_payload = u"SELECT * FROM dem.v_basic_person WHERE pk_identity = %s"
     313          _cmds_store_payload = [
     314                  u"""UPDATE dem.identity SET
     315                                  gender = %(gender)s,
     316                                  dob = %(dob)s,
     317                                  tob = %(tob)s,
     318                                  cob = gm.nullify_empty_string(%(cob)s),
     319                                  title = gm.nullify_empty_string(%(title)s),
     320                                  fk_marital_status = %(pk_marital_status)s,
     321                                  karyotype = gm.nullify_empty_string(%(karyotype)s),
     322                                  pupic = gm.nullify_empty_string(%(pupic)s),
     323                                  deceased = %(deceased)s,
     324                                  emergency_contact = gm.nullify_empty_string(%(emergency_contact)s),
     325                                  fk_emergency_contact = %(pk_emergency_contact)s,
     326                                  fk_primary_provider = %(pk_primary_provider)s,
     327                                  comment = gm.nullify_empty_string(%(comment)s)
     328                          WHERE
     329                                  pk = %(pk_identity)s and
     330                                  xmin = %(xmin_identity)s
     331                          RETURNING
     332                                  xmin AS xmin_identity"""
     333          ]
     334          _updatable_fields = [
     335                  "title",
     336                  "dob",
     337                  "tob",
     338                  "cob",
     339                  "gender",
     340                  "pk_marital_status",
     341                  "karyotype",
     342                  "pupic",
     343                  'deceased',
     344                  'emergency_contact',
     345                  'pk_emergency_contact',
     346                  'pk_primary_provider',
     347                  'comment'
     348          ]
     349          #--------------------------------------------------------
     350 -        def _get_ID(self):
     351                  return self._payload[self._idx['pk_identity']]
     352 -        def _set_ID(self, value):
     353                  raise AttributeError('setting ID of identity is not allowed')
     354          ID = property(_get_ID, _set_ID)
     355          #--------------------------------------------------------
     356 -        def __setitem__(self, attribute, value):
     357
     358                  if attribute == 'dob':
     359                          if value is not None:
     360
     361                                  if isinstance(value, pyDT.datetime):
     362                                          if value.tzinfo is None:
     363                                                  raise ValueError('datetime.datetime instance is lacking a time zone: [%s]' % dt.isoformat())
     364                                  else:
     365                                          raise TypeError, '[%s]: type [%s] (%s) invalid for attribute [dob], must be datetime.datetime or None' % (self.__class__.__name__, type(value), value)
     366
     367                                  # compare DOB at seconds level
     368                                  if self._payload[self._idx['dob']] is not None:
     369                                          old_dob = gmDateTime.pydt_strftime (
     370                                                  self._payload[self._idx['dob']],
     371                                                  format = '%Y %m %d %H %M %S',
     372                                                  accuracy = gmDateTime.acc_seconds
     373                                          )
     374                                          new_dob = gmDateTime.pydt_strftime (
     375                                                  value,
     376                                                  format = '%Y %m %d %H %M %S',
     377                                                  accuracy = gmDateTime.acc_seconds
     378                                          )
     379                                          if new_dob == old_dob:
     380                                                  return
     381
     382                  gmBusinessDBObject.cBusinessDBObject.__setitem__(self, attribute, value)
     383          #--------------------------------------------------------
     384 -        def cleanup(self):
     385                  pass
     386          #--------------------------------------------------------
     387 -        def _get_is_patient(self):
     388                  cmd = u"""
     389                          SELECT EXISTS (
     390                                  SELECT 1
     391                                  FROM clin.v_emr_journal
     392                                  WHERE
     393                                          pk_patient = %(pat)s
     394                                                  AND
     395                                          soap_cat IS NOT NULL
     396                  )"""
     397                  args = {'pat': self._payload[self._idx['pk_identity']]}
     398                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     399                  return rows[0][0]
     400
     401 -        def _set_is_patient(self, value):
     402                  raise AttributeError('setting is_patient status of identity is not allowed')
     403
     404          is_patient = property(_get_is_patient, _set_is_patient)
     405          #--------------------------------------------------------
     406 -        def _get_staff_id(self):
     407                  cmd = u"SELECT pk FROM dem.staff WHERE fk_identity = %(pk)s"
     408                  args = {'pk': self._payload[self._idx['pk_identity']]}
     409                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     410                  if len(rows) == 0:
     411                          return None
     412                  return rows[0][0]
     413
     414          staff_id = property(_get_staff_id, lambda x:x)
     415          #--------------------------------------------------------
     416          # identity API
     417          #--------------------------------------------------------
     418 -        def _get_gender_symbol(self):
     419                  return map_gender2symbol[self._payload[self._idx['gender']]]
     420
     421          gender_symbol = property(_get_gender_symbol, lambda x:x)
     422          #--------------------------------------------------------
     423 -        def get_active_name(self):
     424                  names = self.get_names(active_only = True)
     425                  if len(names) == 0:
     426                          _log.error('cannot retrieve active name for patient [%s]', self._payload[self._idx['pk_identity']])
     427                          return None
     428                  return names[0]
     429
     430          active_name = property(get_active_name, lambda x:x)
     431          #--------------------------------------------------------
     432 -        def get_names(self, active_only=False, exclude_active=False):
     433
     434                  args = {'pk_pat': self._payload[self._idx['pk_identity']]}
     435                  where_parts = [u'pk_identity = %(pk_pat)s']
     436                  if active_only:
     437                          where_parts.append(u'active_name is True')
     438                  if exclude_active:
     439                          where_parts.append(u'active_name is False')
     440                  cmd = u"""
     441                          SELECT *
     442                          FROM dem.v_person_names
     443                          WHERE %s
     444                          ORDER BY active_name DESC, lastnames, firstnames
     445                  """ % u' AND '.join(where_parts)
     446                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     447
     448                  if len(rows) == 0:
     449                          # no names registered for patient
     450                          return []
     451
     452                  names = [ cPersonName(row = {'idx': idx, 'data': r, 'pk_field': 'pk_name'}) for r in rows ]
     453                  return names
     454          #--------------------------------------------------------
     455 -        def get_formatted_dob(self, format='%x', encoding=None, none_string=None):
     456                  return gmDateTime.format_dob (
     457                          self._payload[self._idx['dob']],
     458                          format = format,
     459                          encoding = encoding,
     460                          none_string = none_string
     461                  )
     462          #--------------------------------------------------------
     463 -        def get_description_gender(self):
     464                  return _(u'%(last)s,%(title)s %(first)s%(nick)s (%(sex)s)') % {
     465                          'last': self._payload[self._idx['lastnames']],
     466                          'title': gmTools.coalesce(self._payload[self._idx['title']], u'', u' %s'),
     467                          'first': self._payload[self._idx['firstnames']],
     468                          'nick': gmTools.coalesce(self._payload[self._idx['preferred']], u'', u" '%s'"),
     469                          'sex': self.gender_symbol
     470                  }
     471          #--------------------------------------------------------
     472 -        def get_description(self):
     473                  return _(u'%(last)s,%(title)s %(first)s%(nick)s') % {
     474                          'last': self._payload[self._idx['lastnames']],
     475                          'title': gmTools.coalesce(self._payload[self._idx['title']], u'', u' %s'),
     476                          'first': self._payload[self._idx['firstnames']],
     477                          'nick': gmTools.coalesce(self._payload[self._idx['preferred']], u'', u" '%s'")
     478                  }
     479          #--------------------------------------------------------
     480 -        def add_name(self, firstnames, lastnames, active=True):
     481                  """Add a name.
     482
     483                  @param firstnames The first names.
     484                  @param lastnames The last names.
     485                  @param active When True, the new name will become the active one (hence setting other names to inactive)
     486                  @type active A types.BooleanType instance
     487                  """
     488                  name = create_name(self.ID, firstnames, lastnames, active)
     489                  if active:
     490                          self.refetch_payload()
     491                  return name
     492          #--------------------------------------------------------
     493 -        def delete_name(self, name=None):
     494                  cmd = u"delete from dem.names where id = %(name)s and id_identity = %(pat)s"
     495                  args = {'name': name['pk_name'], 'pat': self.ID}
     496                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     497                  # can't have been the active name as that would raise an
     498                  # exception (since no active name would be left) so no
     499                  # data refetch needed
     500          #--------------------------------------------------------
     501 -        def set_nickname(self, nickname=None):
     502                  """
     503                  Set the nickname. Setting the nickname only makes sense for the currently
     504                  active name.
     505                  @param nickname The preferred/nick/warrior name to set.
     506                  """
     507                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': u"select dem.set_nickname(%s, %s)", 'args': [self.ID, nickname]}])
     508                  self.refetch_payload()
     509                  return True
     510          #--------------------------------------------------------
     511 -        def get_tags(self, order_by=None):
     512                  if order_by is None:
     513                          order_by = u''
     514                  else:
     515                          order_by = u'ORDER BY %s' % order_by
     516
     517                  cmd = gmDemographicRecord._SQL_get_identity_tags % (u'pk_identity = %%(pat)s %s' % order_by)
     518                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {u'pat': self.ID}}], get_col_idx = True)
     519
     520                  return [ gmDemographicRecord.cIdentityTag(row = {'data': r, 'idx': idx, 'pk_field': 'pk_identity_tag'}) for r in rows ]
     521          #--------------------------------------------------------
     522 -        def add_tag(self, tag):
     523                  args = {
     524                          u'tag': tag,
     525                          u'identity': self.ID
     526                  }
     527
     528                  # already exists ?
     529                  cmd = u"SELECT pk FROM dem.identity_tag WHERE fk_tag = %(tag)s AND fk_identity = %(identity)s"
     530                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     531                  if len(rows) > 0:
     532                          return gmDemographicRecord.cIdentityTag(aPK_obj = rows[0]['pk'])
     533
     534                  # no, add
     535                  cmd = u"""
     536                          INSERT INTO dem.identity_tag (
     537                                  fk_tag,
     538                                  fk_identity
     539                          ) VALUES (
     540                                  %(tag)s,
     541                                  %(identity)s
     542                          )
     543                          RETURNING pk
     544                  """
     545                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
     546                  return gmDemographicRecord.cIdentityTag(aPK_obj = rows[0]['pk'])
     547          #--------------------------------------------------------
     548 -        def remove_tag(self, tag):
     549                  cmd = u"DELETE FROM dem.identity_tag WHERE pk = %(pk)s"
     550                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': tag}}])
     551          #--------------------------------------------------------
     552          # external ID API
     553          #
     554          # since external IDs are not treated as first class
     555          # citizens (classes in their own right, that is), we
     556          # handle them *entirely* within cIdentity, also they
     557          # only make sense with one single person (like names)
     558          # and are not reused (like addresses), so they are
     559          # truly added/deleted, not just linked/unlinked
     560          #--------------------------------------------------------
     561 -        def add_external_id(self, type_name=None, value=None, issuer=None, comment=None, pk_type=None):
     562                  """Adds an external ID to the patient.
     563
     564                  creates ID type if necessary
     565                  """
     566
     567                  # check for existing ID
     568                  if pk_type is not None:
     569                          cmd = u"""
     570                                  select * from dem.v_external_ids4identity where
     571                                  pk_identity = %(pat)s and
     572                                  pk_type = %(pk_type)s and
     573                                  value = %(val)s"""
     574                  else:
     575                          # by type/value/issuer
     576                          if issuer is None:
     577                                  cmd = u"""
     578                                          select * from dem.v_external_ids4identity where
     579                                          pk_identity = %(pat)s and
     580                                          name = %(name)s and
     581                                          value = %(val)s"""
     582                          else:
     583                                  cmd = u"""
     584                                          select * from dem.v_external_ids4identity where
     585                                                  pk_identity = %(pat)s and
     586                                                  name = %(name)s and
     587                                                  value = %(val)s and
     588                                                  issuer = %(issuer)s"""
     589                  args = {
     590                          'pat': self.ID,
     591                          'name': type_name,
     592                          'val': value,
     593                          'issuer': issuer,
     594                          'pk_type': pk_type
     595                  }
     596                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
     597
     598                  # create new ID if not found
     599                  if len(rows) == 0:
     600
     601                          args = {
     602                                  'pat': self.ID,
     603                                  'val': value,
     604                                  'type_name': type_name,
     605                                  'pk_type': pk_type,
     606                                  'issuer': issuer,
     607                                  'comment': comment
     608                          }
     609
     610                          if pk_type is None:
     611                                  cmd = u"""insert into dem.lnk_identity2ext_id (external_id, fk_origin, comment, id_identity) values (
     612                                          %(val)s,
     613                                          (select dem.add_external_id_type(%(type_name)s, %(issuer)s)),
     614                                          %(comment)s,
     615                                          %(pat)s
     616                                  )"""
     617                          else:
     618                                  cmd = u"""insert into dem.lnk_identity2ext_id (external_id, fk_origin, comment, id_identity) values (
     619                                          %(val)s,
     620                                          %(pk_type)s,
     621                                          %(comment)s,
     622                                          %(pat)s
     623                                  )"""
     624
     625                          rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     626
     627                  # or update comment of existing ID
     628                  else:
     629                          row = rows[0]
     630                          if comment is not None:
     631                                  # comment not already there ?
     632                                  if gmTools.coalesce(row['comment'], '').find(comment.strip()) == -1:
     633                                          comment = '%s%s' % (gmTools.coalesce(row['comment'], '', '%s // '), comment.strip)
     634                                          cmd = u"update dem.lnk_identity2ext_id set comment = %(comment)s where id=%(pk)s"
     635                                          args = {'comment': comment, 'pk': row['pk_id']}
     636                                          rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     637          #--------------------------------------------------------
     638 -        def update_external_id(self, pk_id=None, type=None, value=None, issuer=None, comment=None):
     639                  """Edits an existing external ID.
     640
     641                  Creates ID type if necessary.
     642                  """
     643                  cmd = u"""
     644                          UPDATE dem.lnk_identity2ext_id SET
     645                                  fk_origin = (SELECT dem.add_external_id_type(%(type)s, %(issuer)s)),
     646                                  external_id = %(value)s,
     647                                  comment = gm.nullify_empty_string(%(comment)s)
     648                          WHERE
     649                                  id = %(pk)s
     650                  """
     651                  args = {'pk': pk_id, 'value': value, 'type': type, 'issuer': issuer, 'comment': comment}
     652                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     653          #--------------------------------------------------------
     654 -        def get_external_ids(self, id_type=None, issuer=None):
     655                  where_parts = ['pk_identity = %(pat)s']
     656                  args = {'pat': self.ID}
     657
     658                  if id_type is not None:
     659                          where_parts.append(u'name = %(name)s')
     660                          args['name'] = id_type.strip()
     661
     662                  if issuer is not None:
     663                          where_parts.append(u'issuer = %(issuer)s')
     664                          args['issuer'] = issuer.strip()
     665
     666                  cmd = u"SELECT * FROM dem.v_external_ids4identity WHERE %s" % ' AND '.join(where_parts)
     667                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
     668
     669                  return rows
     670
     671          external_ids = property(get_external_ids, lambda x:x)
     672          #--------------------------------------------------------
     673 -        def delete_external_id(self, pk_ext_id=None):
     674                  cmd = u"""
     675  delete from dem.lnk_identity2ext_id
     676  where id_identity = %(pat)s and id = %(pk)s"""
     677                  args = {'pat': self.ID, 'pk': pk_ext_id}
     678                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     679          #--------------------------------------------------------
     680 -        def assimilate_identity(self, other_identity=None, link_obj=None):
     681                  """Merge another identity into this one.
     682
     683                  Keep this one. Delete other one."""
     684
     685                  if other_identity.ID == self.ID:
     686                          return True, None
     687
     688                  curr_pat = gmCurrentPatient()
     689                  if curr_pat.connected:
     690                          if other_identity.ID == curr_pat.ID:
     691                                  return False, _('Cannot merge active patient into another patient.')
     692
     693                  queries = []
     694                  args = {'old_pat': other_identity.ID, 'new_pat': self.ID}
     695
     696                  # delete old allergy state
     697                  queries.append ({
     698                          'cmd': u'delete from clin.allergy_state where pk = (select pk_allergy_state from clin.v_pat_allergy_state where pk_patient = %(old_pat)s)',
     699                          'args': args
     700                  })
     701                  # FIXME: adjust allergy_state in kept patient
     702
     703                  # deactivate all names of old patient
     704                  queries.append ({
     705                          'cmd': u'update dem.names set active = False where id_identity = %(old_pat)s',
     706                          'args': args
     707                  })
     708
     709                  # find FKs pointing to identity
     710                  FKs = gmPG2.get_foreign_keys2column (
     711                          schema = u'dem',
     712                          table = u'identity',
     713                          column = u'pk'
     714                  )
     715
     716                  # generate UPDATEs
     717                  cmd_template = u'update %s set %s = %%(new_pat)s where %s = %%(old_pat)s'
     718                  for FK in FKs:
     719                          queries.append ({
     720                                  'cmd': cmd_template % (FK['referencing_table'], FK['referencing_column'], FK['referencing_column']),
     721                                  'args': args
     722                          })
     723
     724                  # remove old identity entry
     725                  queries.append ({
     726                          'cmd': u'delete from dem.identity where pk = %(old_pat)s',
     727                          'args': args
     728                  })
     729
     730                  _log.warning('identity [%s] is about to assimilate identity [%s]', self.ID, other_identity.ID)
     731
     732                  gmPG2.run_rw_queries(link_obj = link_obj, queries = queries, end_tx = True)
     733
     734                  self.add_external_id (
     735                          type_name = u'merged GNUmed identity primary key',
     736                          value = u'GNUmed::pk::%s' % other_identity.ID,
     737                          issuer = u'GNUmed'
     738                  )
     739
     740                  return True, None
     741          #--------------------------------------------------------
     742          #--------------------------------------------------------
     743 -        def put_on_waiting_list(self, urgency=0, comment=None, zone=None):
     744                  cmd = u"""
     745                          insert into clin.waiting_list (fk_patient, urgency, comment, area, list_position)
     746                          values (
     747                                  %(pat)s,
     748                                  %(urg)s,
     749                                  %(cmt)s,
     750                                  %(area)s,
     751                                  (select coalesce((max(list_position) + 1), 1) from clin.waiting_list)
     752                          )"""
     753                  args = {'pat': self.ID, 'urg': urgency, 'cmt': comment, 'area': zone}
     754                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], verbose = True)
     755          #--------------------------------------------------------
     756 -        def get_waiting_list_entry(self):
     757                  cmd = u"""SELECT * FROM clin.v_waiting_list WHERE pk_identity = %(pat)s"""
     758                  args = {'pat': self.ID}
     759                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
     760                  return rows
     761          #--------------------------------------------------------
     762 -        def export_as_gdt(self, filename=None, encoding='iso-8859-15', external_id_type=None):
     763
     764                  template = u'%s%s%s\r\n'
     765
     766                  file = codecs.open (
     767                          filename = filename,
     768                          mode = 'wb',
     769                          encoding = encoding,
     770                          errors = 'strict'
     771                  )
     772
     773                  file.write(template % (u'013', u'8000', u'6301'))
     774                  file.write(template % (u'013', u'9218', u'2.10'))
     775                  if external_id_type is None:
     776                          file.write(template % (u'%03d' % (9 + len(str(self.ID))), u'3000', self.ID))
     777                  else:
     778                          ext_ids = self.get_external_ids(id_type = external_id_type)
     779                          if len(ext_ids) > 0:
     780                                  file.write(template % (u'%03d' % (9 + len(ext_ids[0]['value'])), u'3000', ext_ids[0]['value']))
     781                  file.write(template % (u'%03d' % (9 + len(self._payload[self._idx['lastnames']])), u'3101', self._payload[self._idx['lastnames']]))
     782                  file.write(template % (u'%03d' % (9 + len(self._payload[self._idx['firstnames']])), u'3102', self._payload[self._idx['firstnames']]))
     783                  file.write(template % (u'%03d' % (9 + len(self._payload[self._idx['dob']].strftime('%d%m%Y'))), u'3103', self._payload[self._idx['dob']].strftime('%d%m%Y')))
     784                  file.write(template % (u'010', u'3110', gmXdtMappings.map_gender_gm2xdt[self._payload[self._idx['gender']]]))
     785                  file.write(template % (u'025', u'6330', 'GNUmed::9206::encoding'))
     786                  file.write(template % (u'%03d' % (9 + len(encoding)), u'6331', encoding))
     787                  if external_id_type is None:
     788                          file.write(template % (u'029', u'6332', u'GNUmed::3000::source'))
     789                          file.write(template % (u'017', u'6333', u'internal'))
     790                  else:
     791                          if len(ext_ids) > 0:
     792                                  file.write(template % (u'029', u'6332', u'GNUmed::3000::source'))
     793                                  file.write(template % (u'%03d' % (9 + len(external_id_type)), u'6333', external_id_type))
     794
     795                  file.close()
     796          #--------------------------------------------------------
     797          # occupations API
     798          #--------------------------------------------------------
     799 -        def get_occupations(self):
     800                  return gmDemographicRecord.get_occupations(pk_identity = self.pk_obj)
     801          #--------------------------------------------------------
     802 -        def link_occupation(self, occupation=None, activities=None):
     803                  """Link an occupation with a patient, creating the occupation if it does not exists.
     804
     805                          @param occupation The name of the occupation to link the patient to.
     806                  """
     807                  if (activities is None) and (occupation is None):
     808                          return True
     809
     810                  occupation = occupation.strip()
     811                  if len(occupation) == 0:
     812                          return True
     813
     814                  if activities is not None:
     815                          activities = activities.strip()
     816
     817                  args = {'act': activities, 'pat_id': self.pk_obj, 'job': occupation}
     818
     819                  cmd = u"select activities from dem.v_person_jobs where pk_identity = %(pat_id)s and l10n_occupation = _(%(job)s)"
     820                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
     821
     822                  queries = []
     823                  if len(rows) == 0:
     824                          queries.append ({
     825                                  'cmd': u"INSERT INTO dem.lnk_job2person (fk_identity, fk_occupation, activities) VALUES (%(pat_id)s, dem.create_occupation(%(job)s), %(act)s)",
     826                                  'args': args
     827                          })
     828                  else:
     829                          if rows[0]['activities'] != activities:
     830                                  queries.append ({
     831                                          'cmd': u"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))",
     832                                          'args': args
     833                                  })
     834
     835                  rows, idx = gmPG2.run_rw_queries(queries = queries)
     836
     837                  return True
     838          #--------------------------------------------------------
     839 -        def unlink_occupation(self, occupation=None):
     840                  if occupation is None:
     841                          return True
     842                  occupation = occupation.strip()
     843                  cmd = u"delete from dem.lnk_job2person where fk_identity=%(pk)s and fk_occupation in (select id from dem.occupation where _(name) = _(%(job)s))"
     844                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj, 'job': occupation}}])
     845                  return True
     846          #--------------------------------------------------------
     847          # comms API
     848          #--------------------------------------------------------
     849 -        def get_comm_channels(self, comm_medium=None):
     850                  cmd = u"select * from dem.v_person_comms where pk_identity = %s"
     851                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}], get_col_idx = True)
     852
     853                  filtered = rows
     854
     855                  if comm_medium is not None:
     856                          filtered = []
     857                          for row in rows:
     858                                  if row['comm_type'] == comm_medium:
     859                                          filtered.append(row)
     860
     861                  return [ gmDemographicRecord.cCommChannel(row = {
     862                                          'pk_field': 'pk_lnk_identity2comm',
     863                                          'data': r,
     864                                          'idx': idx
     865                                  }) for r in filtered
     866                          ]
     867          #--------------------------------------------------------
     868 -        def link_comm_channel(self, comm_medium=None, url=None, is_confidential=False, pk_channel_type=None):
     869                  """Link a communication medium with a patient.
     870
     871                  @param comm_medium The name of the communication medium.
     872                  @param url The communication resource locator.
     873                  @type url A types.StringType instance.
     874                  @param is_confidential Wether the data must be treated as confidential.
     875                  @type is_confidential A types.BooleanType instance.
     876                  """
     877                  comm_channel = gmDemographicRecord.create_comm_channel (
     878                          comm_medium = comm_medium,
     879                          url = url,
     880                          is_confidential = is_confidential,
     881                          pk_channel_type = pk_channel_type,
     882                          pk_identity = self.pk_obj
     883                  )
     884                  return comm_channel
     885          #--------------------------------------------------------
     886 -        def unlink_comm_channel(self, comm_channel=None):
     887                  gmDemographicRecord.delete_comm_channel (
     888                          pk = comm_channel['pk_lnk_identity2comm'],
     889                          pk_patient = self.pk_obj
     890                  )
     891          #--------------------------------------------------------
     892          # contacts API
     893          #--------------------------------------------------------
     894 -        def get_addresses(self, address_type=None):
     895                  cmd = u"select * from dem.v_pat_addresses where pk_identity=%s"
     896                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}], get_col_idx=True)
     897                  addresses = []
     898                  for r in rows:
     899                          addresses.append(gmDemographicRecord.cPatientAddress(row={'idx': idx, 'data': r, 'pk_field': 'pk_address'}))
     900
     901                  filtered = addresses
     902
     903                  if address_type is not None:
     904                          filtered = []
     905                          for adr in addresses:
     906                                  if adr['address_type'] == address_type:
     907                                          filtered.append(adr)
     908
     909                  return filtered
     910          #--------------------------------------------------------
     911 -        def link_address(self, number=None, street=None, postcode=None, urb=None, state=None, country=None, subunit=None, suburb=None, id_type=None, address=None):
     912                  """Link an address with a patient, creating the address if it does not exists.
     913
     914                  @param number The number of the address.
     915                  @param street The name of the street.
     916                  @param postcode The postal code of the address.
     917                  @param urb The name of town/city/etc.
     918                  @param state The code of the state.
     919                  @param country The code of the country.
     920                  @param id_type The primary key of the address type.
     921                  """
     922                  if address is None:
     923                          # create/get address
     924                          address = gmDemographicRecord.create_address (
     925                                  country = country,
     926                                  state = state,
     927                                  urb = urb,
     928                                  suburb = suburb,
     929                                  postcode = postcode,
     930                                  street = street,
     931                                  number = number,
     932                                  subunit = subunit
     933                          )
     934
     935                  if address is None:
     936                          return None
     937
     938                  # already linked ?
     939                  cmd = u"SELECT * FROM dem.lnk_person_org_address WHERE id_identity = %(pat)s AND id_address = %(adr)s"
     940                  args = {'pat': self.pk_obj, 'adr': address['pk_address']}
     941                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
     942
     943                  # no, link to person
     944                  if len(rows) == 0:
     945                          args = {'id': self.pk_obj, 'adr': address['pk_address'], 'type': id_type}
     946                          cmd = u"""
     947                                  INSERT INTO dem.lnk_person_org_address(id_identity, id_address)
     948                                  VALUES (%(id)s, %(adr)s)
     949                                  RETURNING *"""
     950                          rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True)
     951
     952                  # already or now linked - needs to change type ?
     953                  if id_type is not None:
     954                          r = rows[0]
     955                          if r['id_type'] != id_type:
     956                                  cmd = "UPDATE dem.lnk_person_org_address SET id_type = %(type)s WHERE id = %(id)s"
     957                                  args = {'type': id_type, 'id': r['id']}
     958                                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     959
     960                  return address
     961          #----------------------------------------------------------------------
     962 -        def unlink_address(self, address=None, pk_address=None):
     963                  """Remove an address from the patient.
     964
     965                  The address itself stays in the database.
     966                  The address can be either cAdress or cPatientAdress.
     967                  """
     968                  if pk_address is None:
     969                          args = {'person': self.pk_obj, 'adr': address['pk_address']}
     970                  else:
     971                          args = {'person': self.pk_obj, 'adr': pk_address}
     972                  cmd = u"DELETE FROM dem.lnk_person_org_address WHERE id_identity = %(person)s AND id_address = %(adr)s"
     973                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     974          #----------------------------------------------------------------------
     975          # relatives API
     976          #----------------------------------------------------------------------
     977 -        def get_relatives(self):
     978                  cmd = u"""
     979                          select
     980                                  t.description,
     981                                  vbp.pk_identity as id,
     982                                  title,
     983                                  firstnames,
     984                                  lastnames,
     985                                  dob,
     986                                  cob,
     987                                  gender,
     988                                  karyotype,
     989                                  pupic,
     990                                  pk_marital_status,
     991                                  marital_status,
     992                                  xmin_identity,
     993                                  preferred
     994                          from
     995                                  dem.v_basic_person vbp, dem.relation_types t, dem.lnk_person2relative l
     996                          where
     997                                  (
     998                                          l.id_identity = %(pk)s and
     999                                          vbp.pk_identity = l.id_relative and
    1000                                          t.id = l.id_relation_type
    1001                                  ) or (
    1002                                          l.id_relative = %(pk)s and
    1003                                          vbp.pk_identity = l.id_identity and
    1004                                          t.inverse = l.id_relation_type
    1005                                  )"""
    1006                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}])
    1007                  if len(rows) == 0:
    1008                          return []
    1009                  return [(row[0], cIdentity(row = {'data': row[1:], 'idx':idx, 'pk_field': 'pk'})) for row in rows]
    1010          #--------------------------------------------------------
    1011 -        def link_new_relative(self, rel_type = 'parent'):
    1012                  # create new relative
    1013                  id_new_relative = create_dummy_identity()
    1014
    1015                  relative = cIdentity(aPK_obj=id_new_relative)
    1016                  # pre-fill with data from ourselves
    1017  #               relative.copy_addresses(self)
    1018                  relative.add_name( '**?**', self.get_names()['lastnames'])
    1019                  # and link the two
    1020                  if self._ext_cache.has_key('relatives'):
    1021                          del self._ext_cache['relatives']
    1022                  cmd = u"""
    1023                          insert into dem.lnk_person2relative (
    1024                                  id_identity, id_relative, id_relation_type
    1025                          ) values (
    1026                                  %s, %s, (select id from dem.relation_types where description = %s)
    1027                          )"""
    1028                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': [self.ID, id_new_relative, rel_type  ]}])
    1029                  return True
    1030          #----------------------------------------------------------------------
    1031 -        def delete_relative(self, relation):
    1032                  # unlink only, don't delete relative itself
    1033                  self.set_relative(None, relation)
    1034          #--------------------------------------------------------
    1035 -        def _get_emergency_contact_from_database(self):
    1036                  if self._payload[self._idx['pk_emergency_contact']] is None:
    1037                          return None
    1038                  return cIdentity(aPK_obj = self._payload[self._idx['pk_emergency_contact']])
    1039
    1040          emergency_contact_in_database = property(_get_emergency_contact_from_database, lambda x:x)
    1041          #----------------------------------------------------------------------
    1042          # age/dob related
    1043          #----------------------------------------------------------------------
    1044 -        def get_medical_age(self):
    1045                  dob = self['dob']
    1046
    1047                  if dob is None:
    1048                          return u'??'
    1049
    1050                  if dob > gmDateTime.pydt_now_here():
    1051                          return _('invalid age: DOB in the future')
    1052
    1053                  death = self['deceased']
    1054
    1055                  if death is None:
    1056                          return gmDateTime.format_apparent_age_medically (
    1057                                  age = gmDateTime.calculate_apparent_age(start = dob)
    1058                          )
    1059
    1060                  if dob > death:
    1061                          return _('invalid age: DOB after death')
    1062
    1063                  return u'%s%s' % (
    1064                          gmTools.u_latin_cross,
    1065                          gmDateTime.format_apparent_age_medically (
    1066                                  age = gmDateTime.calculate_apparent_age (
    1067                                          start = dob,
    1068                                          end = self['deceased']
    1069                                  )
    1070                          )
    1071                  )
    1072          #----------------------------------------------------------------------
    1073 -        def dob_in_range(self, min_distance=u'1 week', max_distance=u'1 week'):
    1074                  cmd = u'select dem.dob_is_in_range(%(dob)s, %(min)s, %(max)s)'
    1075                  rows, idx = gmPG2.run_ro_queries (
    1076                          queries = [{
    1077                                  'cmd': cmd,
    1078                                  'args': {'dob': self['dob'], 'min': min_distance, 'max': max_distance}
    1079                          }]
    1080                  )
    1081                  return rows[0][0]
    1082          #----------------------------------------------------------------------
    1083          # practice related
    1084          #----------------------------------------------------------------------
    1085 -        def get_last_encounter(self):
    1086                  cmd = u'select * from clin.v_most_recent_encounters where pk_patient=%s'
    1087                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self._payload[self._idx['pk_identity']]]}])
    1088                  if len(rows) > 0:
    1089                          return rows[0]
    1090                  else:
    1091                          return None
    1092          #--------------------------------------------------------
    1093 -        def _get_messages(self):
    1094                  return gmProviderInbox.get_inbox_messages(pk_patient = self._payload[self._idx['pk_identity']])
    1095
    1096 -        def _set_messages(self, messages):
    1097                  return
    1098
    1099          messages = property(_get_messages, _set_messages)
    1100          #--------------------------------------------------------
    1101 -        def delete_message(self, pk=None):
    1102                  return gmProviderInbox.delete_inbox_message(inbox_message = pk)
    1103          #--------------------------------------------------------
    1104 -        def _get_primary_provider(self):
    1105                  if self._payload[self._idx['pk_primary_provider']] is None:
    1106                          return None
    1107                  from Gnumed.business import gmStaff
    1108                  return gmStaff.cStaff(aPK_obj = self._payload[self._idx['pk_primary_provider']])
    1109
    1110          primary_provider = property(_get_primary_provider, lambda x:x)
    1111          #----------------------------------------------------------------------
    1112          # convenience
    1113          #----------------------------------------------------------------------
    1114 -        def get_dirname(self):
    1115                  """Format patient demographics into patient specific path name fragment."""
    1116                  return '%s-%s%s-%s' % (
    1117                          self._payload[self._idx['lastnames']].replace(u' ', u'_'),
    1118                          self._payload[self._idx['firstnames']].replace(u' ', u'_'),
    1119                          gmTools.coalesce(self._payload[self._idx['preferred']], u'', template_initial = u'-(%s)'),
    1120                          self.get_formatted_dob(format = '%Y-%m-%d', encoding = gmI18N.get_encoding())
    1121                  )
    1122  #============================================================
    1123 -class cPatient(cIdentity):
    1124          """Represents a person which is a patient.
    1125
    1126          - a specializing subclass of cIdentity turning it into a patient
    1127          - its use is to cache subobjects like EMR and document folder
    1128          """
    1129 -        def __init__(self, aPK_obj=None, row=None):
    1130                  cIdentity.__init__(self, aPK_obj=aPK_obj, row=row)
    1131                  self.__db_cache = {}
    1132                  self.__emr_access_lock = threading.Lock()
    1133          #--------------------------------------------------------
    1134 -        def cleanup(self):
    1135                  """Do cleanups before dying.
    1136
    1137                  - note that this may be called in a thread
    1138                  """
    1139                  if self.__db_cache.has_key('clinical record'):
    1140                          self.__db_cache['clinical record'].cleanup()
    1141                  if self.__db_cache.has_key('document folder'):
    1142                          self.__db_cache['document folder'].cleanup()
    1143                  cIdentity.cleanup(self)
    1144          #----------------------------------------------------------
    1145 -        def get_emr(self):
    1146  #               attempt = 1
    1147  #               got_lock = self.__emr_access_lock.acquire(False)
    1148  #               while not got_lock:
    1149  #                       if attempt == 100:                      # 100 x 500ms -> 50 seconds timeout
    1150  #                               raise AttributeError('cannot access EMR')
    1151  #                       attempt += 1
    1152  #                       time.sleep(0.5)                         # 500ms
    1153  #                       got_lock = self.__emr_access_lock.acquire(False)
    1154                  if not self.__emr_access_lock.acquire(False):
    1155                          raise AttributeError('cannot access EMR')
    1156                  try:
    1157                          emr = self.__db_cache['clinical record']
    1158                          self.__emr_access_lock.release()
    1159                          return emr
    1160                  except KeyError:
    1161                          pass
    1162
    1163                  self.__db_cache['clinical record'] = gmClinicalRecord.cClinicalRecord(aPKey = self._payload[self._idx['pk_identity']])
    1164                  self.__emr_access_lock.release()
    1165                  return self.__db_cache['clinical record']
    1166
    1167          emr = property(get_emr, lambda x:x)
    1168          #--------------------------------------------------------
    1169 -        def get_document_folder(self):
    1170                  try:
    1171                          return self.__db_cache['document folder']
    1172                  except KeyError:
    1173                          pass
    1174
    1175                  self.__db_cache['document folder'] = cDocumentFolder(aPKey = self._payload[self._idx['pk_identity']])
    1176                  return self.__db_cache['document folder']
    1177
    1178          document_folder = property(get_document_folder, lambda x:x)
    1179  #============================================================
    1180 -class gmCurrentPatient(gmBorg.cBorg):
    1181          """Patient Borg to hold currently active patient.
    1182
    1183          There may be many instances of this but they all share state.
    1184          """
    1185 -        def __init__(self, patient=None, forced_reload=False):
    1186                  """Change or get currently active patient.
    1187
    1188                  patient:
    1189                  * None: get currently active patient
    1190                  * -1: unset currently active patient
    1191                  * cPatient instance: set active patient if possible
    1192                  """
    1193                  # make sure we do have a patient pointer
    1194                  try:
    1195                          tmp = self.patient
    1196                  except AttributeError:
    1197                          self.patient = gmNull.cNull()
    1198                          self.__register_interests()
    1199                          # set initial lock state,
    1200                          # this lock protects against activating another patient
    1201                          # when we are controlled from a remote application
    1202                          self.__lock_depth = 0
    1203                          # initialize callback state
    1204                          self.__pre_selection_callbacks = []
    1205
    1206                  # user wants copy of current patient
    1207                  if patient is None:
    1208                          return None
    1209
    1210                  # do nothing if patient is locked
    1211                  if self.locked:
    1212                          _log.error('patient [%s] is locked, cannot change to [%s]' % (self.patient['pk_identity'], patient))
    1213                          return None
    1214
    1215                  # user wants to explicitly unset current patient
    1216                  if patient == -1:
    1217                          _log.debug('explicitly unsetting current patient')
    1218                          if not self.__run_pre_selection_callbacks():
    1219                                  _log.debug('not unsetting current patient')
    1220                                  return None
    1221                          self.__send_pre_selection_notification()
    1222                          self.patient.cleanup()
    1223                          self.patient = gmNull.cNull()
    1224                          self.__send_selection_notification()
    1225                          return None
    1226
    1227                  # must be cPatient instance, then
    1228                  if not isinstance(patient, cPatient):
    1229                          _log.error('cannot set active patient to [%s], must be either None, -1 or cPatient instance' % str(patient))
    1230                          raise TypeError, 'gmPerson.gmCurrentPatient.__init__(): <patient> must be None, -1 or cPatient instance but is: %s' % str(patient)
    1231
    1232                  # same ID, no change needed
    1233                  if (self.patient['pk_identity'] == patient['pk_identity']) and not forced_reload:
    1234                          return None
    1235
    1236                  # user wants different patient
    1237                  _log.debug('patient change [%s] -> [%s] requested', self.patient['pk_identity'], patient['pk_identity'])
    1238
    1239                  # everything seems swell
    1240                  if not self.__run_pre_selection_callbacks():
    1241                          _log.debug('not changing current patient')
    1242                          return None
    1243                  self.__send_pre_selection_notification()
    1244                  self.patient.cleanup()
    1245                  self.patient = patient
    1246                  self.patient.get_emr()
    1247                  self.__send_selection_notification()
    1248
    1249                  return None
    1250          #--------------------------------------------------------
    1251 -        def __register_interests(self):
    1252                  gmDispatcher.connect(signal = u'identity_mod_db', receiver = self._on_identity_change)
    1253                  gmDispatcher.connect(signal = u'name_mod_db', receiver = self._on_identity_change)
    1254          #--------------------------------------------------------
    1255 -        def _on_identity_change(self):
    1256                  """Listen for patient *data* change."""
    1257                  self.patient.refetch_payload()
    1258          #--------------------------------------------------------
    1259          # external API
    1260          #--------------------------------------------------------
    1261 -        def register_pre_selection_callback(self, callback=None):
    1262                  if not callable(callback):
    1263                          raise TypeError(u'callback [%s] not callable' % callback)
    1264
    1265                  self.__pre_selection_callbacks.append(callback)
    1266          #--------------------------------------------------------
    1267 -        def _get_connected(self):
    1268                  return (not isinstance(self.patient, gmNull.cNull))
    1269
    1270 -        def _set_connected(self):
    1271                  raise AttributeError(u'invalid to set <connected> state')
    1272
    1273          connected = property(_get_connected, _set_connected)
    1274          #--------------------------------------------------------
    1275 -        def _get_locked(self):
    1276                  return (self.__lock_depth > 0)
    1277
    1278 -        def _set_locked(self, locked):
    1279                  if locked:
    1280                          self.__lock_depth = self.__lock_depth + 1
    1281                          gmDispatcher.send(signal='patient_locked')
    1282                  else:
    1283                          if self.__lock_depth == 0:
    1284                                  _log.error('lock/unlock imbalance, trying to refcount lock depth below 0')
    1285                                  return
    1286                          else:
    1287                                  self.__lock_depth = self.__lock_depth - 1
    1288                          gmDispatcher.send(signal='patient_unlocked')
    1289
    1290          locked = property(_get_locked, _set_locked)
    1291          #--------------------------------------------------------
    1292 -        def force_unlock(self):
    1293                  _log.info('forced patient unlock at lock depth [%s]' % self.__lock_depth)
    1294                  self.__lock_depth = 0
    1295                  gmDispatcher.send(signal='patient_unlocked')
    1296          #--------------------------------------------------------
    1297          # patient change handling
    1298          #--------------------------------------------------------
    1299 -        def __run_pre_selection_callbacks(self):
    1300                  if isinstance(self.patient, gmNull.cNull):
    1301                          return True
    1302
    1303                  for call_back in self.__pre_selection_callbacks:
    1304                          try:
    1305                                  successful = call_back()
    1306                          except:
    1307                                  _log.exception('callback [%s] failed', call_back)
    1308                                  print "*** pre-selection callback failed ***"
    1309                                  print type(call_back)
    1310                                  print call_back
    1311                                  return False
    1312
    1313                          if not successful:
    1314                                  _log.debug('callback [%s] returned False', call_back)
    1315                                  return False
    1316
    1317                  return True
    1318          #--------------------------------------------------------
    1319 -        def __send_pre_selection_notification(self):
    1320                  """Sends signal when another patient is about to become active.
    1321
    1322                  This does NOT wait for signal handlers to complete.
    1323                  """
    1324                  kwargs = {
    1325                          'signal': u'pre_patient_selection',
    1326                          'sender': id(self.__class__),
    1327                          'pk_identity': self.patient['pk_identity']
    1328                  }
    1329                  gmDispatcher.send(**kwargs)
    1330          #--------------------------------------------------------
    1331 -        def __send_selection_notification(self):
    1332                  """Sends signal when another patient has actually been made active."""
    1333                  kwargs = {
    1334                          'signal': u'post_patient_selection',
    1335                          'sender': id(self.__class__),
    1336                          'pk_identity': self.patient['pk_identity']
    1337                  }
    1338                  gmDispatcher.send(**kwargs)
    1339          #--------------------------------------------------------
    1340          # __getattr__ handling
    1341          #--------------------------------------------------------
    1342 -        def __getattr__(self, attribute):
    1343                  if attribute == 'patient':
    1344                          raise AttributeError
    1345                  if not isinstance(self.patient, gmNull.cNull):
    1346                          return getattr(self.patient, attribute)
    1347          #--------------------------------------------------------
    1348          # __get/setitem__ handling
    1349          #--------------------------------------------------------
    1350 -        def __getitem__(self, attribute = None):
    1351                  """Return any attribute if known how to retrieve it by proxy.
    1352                  """
    1353                  return self.patient[attribute]
    1354          #--------------------------------------------------------
    1355 -        def __setitem__(self, attribute, value):
    1356                  self.patient[attribute] = value
    1357  #============================================================
    1358  # match providers
    1359  #============================================================
    1360 -class cMatchProvider_Provider(gmMatchProvider.cMatchProvider_SQL2):
    1361 -        def __init__(self):
    1362                  gmMatchProvider.cMatchProvider_SQL2.__init__(
    1363                          self,
    1364                          queries = [
    1365                                  u"""SELECT
    1366                                                  pk_staff AS data,
    1367                                                  short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS list_label,
    1368                                                  short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS field_label
    1369                                          FROM dem.v_staff
    1370                                          WHERE
    1371                                                  is_active AND (
    1372                                                          short_alias %(fragment_condition)s OR
    1373                                                          firstnames %(fragment_condition)s OR
    1374                                                          lastnames %(fragment_condition)s OR
    1375                                                          db_user %(fragment_condition)s
    1376                                                  )
    1377                                  """
    1378                          ]
    1379                  )
    1380                  self.setThresholds(1, 2, 3)
    1381  #============================================================
    1382  # convenience functions
    1383  #============================================================
    1384 -def create_name(pk_person, firstnames, lastnames, active=False):
    1385          queries = [{
    1386                  'cmd': u"select dem.add_name(%s, %s, %s, %s)",
    1387                  'args': [pk_person, firstnames, lastnames, active]
    1388          }]
    1389          rows, idx = gmPG2.run_rw_queries(queries=queries, return_data=True)
    1390          name = cPersonName(aPK_obj = rows[0][0])
    1391          return name
    1392  #============================================================
    1393 -def create_identity(gender=None, dob=None, lastnames=None, firstnames=None):
    1394
    1395          cmd1 = u"""INSERT INTO dem.identity (gender, dob) VALUES (%s, %s)"""
    1396          cmd2 = u"""
    1397  INSERT INTO dem.names (
    1398          id_identity, lastnames, firstnames
    1399  ) VALUES (
    1400          currval('dem.identity_pk_seq'), coalesce(%s, 'xxxDEFAULTxxx'), coalesce(%s, 'xxxDEFAULTxxx')
    1401  ) RETURNING id_identity"""
    1402          rows, idx = gmPG2.run_rw_queries (
    1403                  queries = [
    1404                          {'cmd': cmd1, 'args': [gender, dob]},
    1405                          {'cmd': cmd2, 'args': [lastnames, firstnames]}
    1406                  ],
    1407                  return_data = True
    1408          )
    1409          ident = cIdentity(aPK_obj=rows[0][0])
    1410          gmHooks.run_hook_script(hook = u'post_person_creation')
    1411          return ident
    1412  #============================================================
    1413 -def create_dummy_identity():
    1414          cmd = u"INSERT INTO dem.identity(gender) VALUES ('xxxDEFAULTxxx') RETURNING pk"
    1415          rows, idx = gmPG2.run_rw_queries (
    1416                  queries = [{'cmd': cmd}],
    1417                  return_data = True
    1418          )
    1419          return gmDemographicRecord.cIdentity(aPK_obj = rows[0][0])
    1420  #============================================================
    1421 -def set_active_patient(patient=None, forced_reload=False):
    1422          """Set active patient.
    1423
    1424          If patient is -1 the active patient will be UNset.
    1425          """
    1426          if isinstance(patient, cPatient):
    1427                  pat = patient
    1428          elif isinstance(patient, cIdentity):
    1429                  pat = cPatient(aPK_obj=patient['pk_identity'])
    1430  #       elif isinstance(patient, cStaff):
    1431  #               pat = cPatient(aPK_obj=patient['pk_identity'])
    1432          elif isinstance(patient, gmCurrentPatient):
    1433                  pat = patient.patient
    1434          elif patient == -1:
    1435                  pat = patient
    1436          else:
    1437                  raise ValueError('<patient> must be either -1, cPatient, cIdentity or gmCurrentPatient instance, is: %s' % patient)
    1438
    1439          # attempt to switch
    1440          try:
    1441                  gmCurrentPatient(patient = pat, forced_reload = forced_reload)
    1442          except:
    1443                  _log.exception('error changing active patient to [%s]' % patient)
    1444                  return False
    1445
    1446          return True
    1447  #============================================================
    1448  # gender related
    1449  #------------------------------------------------------------
    1450 -def get_gender_list():
    1451          """Retrieves the list of known genders from the database."""
    1452          global __gender_idx
    1453          global __gender_list
    1454
    1455          if __gender_list is None:
    1456                  cmd = u"select tag, l10n_tag, label, l10n_label, sort_weight from dem.v_gender_labels order by sort_weight desc"
    1457                  __gender_list, __gender_idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True)
    1458
    1459          return (__gender_list, __gender_idx)
    1460  #------------------------------------------------------------
    1461  map_gender2mf = {
    1462          'm': u'm',
    1463          'f': u'f',
    1464          'tf': u'f',
    1465          'tm': u'm',
    1466          'h': u'mf'
    1467  }
    1468  #------------------------------------------------------------
    1469  # Maps GNUmed related i18n-aware gender specifiers to a unicode symbol.
    1470  map_gender2symbol = {
    1471          'm': u'\u2642',
    1472          'f': u'\u2640',
    1473          'tf': u'\u26A5\u2640',
    1474          'tm': u'\u26A5\u2642',
    1475          'h': u'\u26A5'
    1476  #       'tf': u'\u2642\u2640-\u2640',
    1477  #       'tm': u'\u2642\u2640-\u2642',
    1478  #       'h': u'\u2642\u2640'
    1479  }
    1480  #------------------------------------------------------------
    1481 -def map_gender2salutation(gender=None):
    1482          """Maps GNUmed related i18n-aware gender specifiers to a human-readable salutation."""
    1483
    1484          global __gender2salutation_map
    1485
    1486          if __gender2salutation_map is None:
    1487                  genders, idx = get_gender_list()
    1488                  __gender2salutation_map = {
    1489                          'm': _('Mr'),
    1490                          'f': _('Mrs'),
    1491                          'tf': u'',
    1492                          'tm': u'',
    1493                          'h': u''
    1494                  }
    1495                  for g in genders:
    1496                          __gender2salutation_map[g[idx['l10n_tag']]] = __gender2salutation_map[g[idx['tag']]]
    1497                          __gender2salutation_map[g[idx['label']]] = __gender2salutation_map[g[idx['tag']]]
    1498                          __gender2salutation_map[g[idx['l10n_label']]] = __gender2salutation_map[g[idx['tag']]]
    1499
    1500          return __gender2salutation_map[gender]
    1501  #------------------------------------------------------------
    1502 -def map_firstnames2gender(firstnames=None):
    1503          """Try getting the gender for the given first name."""
    1504
    1505          if firstnames is None:
    1506                  return None
    1507
    1508          rows, idx = gmPG2.run_ro_queries(queries = [{
    1509                  'cmd': u"select gender from dem.name_gender_map where name ilike %(fn)s limit 1",
    1510                  'args': {'fn': firstnames}
    1511          }])
    1512
    1513          if len(rows) == 0:
    1514                  return None
    1515
    1516          return rows[0][0]
    1517  #============================================================
    1518 -def get_persons_from_pks(pks=None):
    1519          return [ cIdentity(aPK_obj = pk) for pk in pks ]
    1520  #============================================================
    1521 -def get_person_from_xdt(filename=None, encoding=None, dob_format=None):
    1522          from Gnumed.business import gmXdtObjects
    1523          return gmXdtObjects.read_person_from_xdt(filename=filename, encoding=encoding, dob_format=dob_format)
    1524  #============================================================
    1525 -def get_persons_from_pracsoft_file(filename=None, encoding='ascii'):
    1526          from Gnumed.business import gmPracSoftAU
    1527          return gmPracSoftAU.read_persons_from_pracsoft_file(filename=filename, encoding=encoding)
    1528  #============================================================
    1529  # main/testing
    1530  #============================================================
    1531  if __name__ == '__main__':
    1532
    1533          if len(sys.argv) == 1:
    1534                  sys.exit()
    1535
    1536          if sys.argv[1] != 'test':
    1537                  sys.exit()
    1538
    1539          import datetime
    1540
    1541          gmI18N.activate_locale()
    1542          gmI18N.install_domain()
    1543          gmDateTime.init()
    1544
    1545          #--------------------------------------------------------
    1546 -        def test_set_active_pat():
    1547
    1548                  ident = cIdentity(1)
    1549                  print "setting active patient with", ident
    1550                  set_active_patient(patient=ident)
    1551
    1552                  patient = cPatient(12)
    1553                  print "setting active patient with", patient
    1554                  set_active_patient(patient=patient)
    1555
    1556                  pat = gmCurrentPatient()
    1557                  print pat['dob']
    1558                  #pat['dob'] = 'test'
    1559
    1560  #               staff = cStaff()
    1561  #               print "setting active patient with", staff
    1562  #               set_active_patient(patient=staff)
    1563
    1564                  print "setting active patient with -1"
    1565                  set_active_patient(patient=-1)
    1566          #--------------------------------------------------------
    1567 -        def test_dto_person():
    1568                  dto = cDTO_person()
    1569                  dto.firstnames = 'Sepp'
    1570                  dto.lastnames = 'Herberger'
    1571                  dto.gender = 'male'
    1572                  dto.dob = pyDT.datetime.now(tz=gmDateTime.gmCurrentLocalTimezone)
    1573                  print dto
    1574
    1575                  print dto['firstnames']
    1576                  print dto['lastnames']
    1577                  print dto['gender']
    1578                  print dto['dob']
    1579
    1580                  for key in dto.keys():
    1581                          print key
    1582          #--------------------------------------------------------
    1583 -        def test_identity():
    1584                  # create patient
    1585                  print '\n\nCreating identity...'
    1586                  new_identity = create_identity(gender='m', dob='2005-01-01', lastnames='test lastnames', firstnames='test firstnames')
    1587                  print 'Identity created: %s' % new_identity
    1588
    1589                  print '\nSetting title and gender...'
    1590                  new_identity['title'] = 'test title';
    1591                  new_identity['gender'] = 'f';
    1592                  new_identity.save_payload()
    1593                  print 'Refetching identity from db: %s' % cIdentity(aPK_obj=new_identity['pk_identity'])
    1594
    1595                  print '\nGetting all names...'
    1596                  for a_name in new_identity.get_names():
    1597                          print a_name
    1598                  print 'Active name: %s' % (new_identity.get_active_name())
    1599                  print 'Setting nickname...'
    1600                  new_identity.set_nickname(nickname='test nickname')
    1601                  print 'Refetching all names...'
    1602                  for a_name in new_identity.get_names():
    1603                          print a_name
    1604                  print 'Active name: %s' % (new_identity.get_active_name())
    1605
    1606                  print '\nIdentity occupations: %s' % new_identity['occupations']
    1607                  print 'Creating identity occupation...'
    1608                  new_identity.link_occupation('test occupation')
    1609                  print 'Identity occupations: %s' % new_identity['occupations']
    1610
    1611                  print '\nIdentity addresses: %s' % new_identity.get_addresses()
    1612                  print 'Creating identity address...'
    1613                  # make sure the state exists in the backend
    1614                  new_identity.link_address (
    1615                          number = 'test 1234',
    1616                          street = 'test street',
    1617                          postcode = 'test postcode',
    1618                          urb = 'test urb',
    1619                          state = 'SN',
    1620                          country = 'DE'
    1621                  )
    1622                  print 'Identity addresses: %s' % new_identity.get_addresses()
    1623
    1624                  print '\nIdentity communications: %s' % new_identity.get_comm_channels()
    1625                  print 'Creating identity communication...'
    1626                  new_identity.link_comm_channel('homephone', '1234566')
    1627                  print 'Identity communications: %s' % new_identity.get_comm_channels()
    1628          #--------------------------------------------------------
    1629 -        def test_name():
    1630                  for pk in range(1,16):
    1631                          name = cPersonName(aPK_obj=pk)
    1632                          print name.description
    1633                          print '  ', name
    1634          #--------------------------------------------------------
    1635          #test_dto_person()
    1636          #test_identity()
    1637          #test_set_active_pat()
    1638          #test_search_by_dto()
    1639          test_name()
    1640
    1641          #map_gender2salutation('m')
    1642          # module functions
    1643          #genders, idx = get_gender_list()
    1644          #print "\n\nRetrieving gender enum (tag, label, weight):"
    1645          #for gender in genders:
    1646          #       print "%s, %s, %s" % (gender[idx['tag']], gender[idx['l10n_label']], gender[idx['sort_weight']])
    1647
    1648          #comms = get_comm_list()
    1649          #print "\n\nRetrieving communication media enum (id, description): %s" % comms
    1650
    1651  #============================================================
    1652