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

# Source Code for Module Gnumed.business.gmClinicalRecord

       1  # -*- coding: utf-8 -*-
       2  """GNUmed clinical patient record."""
       3  #============================================================
       4  __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
       5  __license__ = "GPL v2 or later"
       6
       7  # standard libs
       8  import sys
       9  import logging
      10  import threading
      11  import datetime as pydt
      12
      13  if __name__ == '__main__':
      14          sys.path.insert(0, '../../')
      15
      16  from Gnumed.pycommon import gmI18N
      17  from Gnumed.pycommon import gmDateTime
      18
      19  if __name__ == '__main__':
      20          from Gnumed.pycommon import gmLog2
      21          gmI18N.activate_locale()
      22          gmI18N.install_domain()
      23          gmDateTime.init()
      24
      25  from Gnumed.pycommon import gmExceptions
      26  from Gnumed.pycommon import gmPG2
      27  from Gnumed.pycommon import gmDispatcher
      28  from Gnumed.pycommon import gmCfg
      29  from Gnumed.pycommon import gmTools
      30
      31  from Gnumed.business import gmGenericEMRItem
      32  from Gnumed.business import gmAllergy
      33  from Gnumed.business import gmPathLab
      34  from Gnumed.business import gmLOINC
      35  from Gnumed.business import gmClinNarrative
      36  from Gnumed.business import gmSoapDefs
      37  from Gnumed.business import gmEMRStructItems
      38  from Gnumed.business import gmMedication
      39  from Gnumed.business import gmVaccination
      40  from Gnumed.business import gmFamilyHistory
      41  from Gnumed.business import gmExternalCare
      42  from Gnumed.business import gmOrganization
      43  from Gnumed.business import gmAutoHints
      44  from Gnumed.business.gmDemographicRecord import get_occupations
      45
      46
      47  _log = logging.getLogger('gm.emr')
      48
      49  _here = None
      50  #============================================================
      51  # helper functions
      52  #------------------------------------------------------------
      53  #_func_ask_user = None
      54  #
      55  #def set_func_ask_user(a_func = None):
      56  #       if not callable(a_func):
      57  #               _log.error('[%] not callable, not setting _func_ask_user', a_func)
      58  #               return False
      59  #
      60  #       _log.debug('setting _func_ask_user to [%s]', a_func)
      61  #
      62  #       global _func_ask_user
      63  #       _func_ask_user = a_func
      64
      65  #============================================================
      66  from Gnumed.business.gmDocuments import cDocument
      67  from Gnumed.business.gmProviderInbox import cInboxMessage
      68
      69  _map_table2class = {
      70          'clin.encounter': gmEMRStructItems.cEncounter,
      71          'clin.episode': gmEMRStructItems.cEpisode,
      72          'clin.health_issue': gmEMRStructItems.cHealthIssue,
      73          'clin.external_care': gmExternalCare.cExternalCareItem,
      74          'clin.vaccination': gmVaccination.cVaccination,
      75          'clin.clin_narrative': gmClinNarrative.cNarrative,
      76          'clin.test_result': gmPathLab.cTestResult,
      77          'clin.substance_intake': gmMedication.cSubstanceIntakeEntry,
      78          'clin.hospital_stay': gmEMRStructItems.cHospitalStay,
      79          'clin.procedure': gmEMRStructItems.cPerformedProcedure,
      80          'clin.allergy': gmAllergy.cAllergy,
      81          'clin.allergy_state': gmAllergy.cAllergyState,
      82          'clin.family_history': gmFamilyHistory.cFamilyHistory,
      83          'clin.suppressed_hint': gmAutoHints.cSuppressedHint,
      84          'blobs.doc_med': cDocument,
      85          'dem.message_inbox': cInboxMessage,
      86          'ref.auto_hint': gmAutoHints.cDynamicHint
      87  }
      88
      89 -def instantiate_clin_root_item(table, pk):
      90          try:
      91                  item_class = _map_table2class[table]
      92          except KeyError:
      93                  _log.error('unmapped clin_root_item entry [%s], cannot instantiate', table)
      94                  return None
      95
      96          return item_class(aPK_obj = pk)
      97
      98  #------------------------------------------------------------
      99 -def format_clin_root_item(table, pk, patient=None):
     100
     101          instance = instantiate_clin_root_item(table, pk)
     102          if instance is None:
     103                  return _('cannot instantiate clinical root item <%s(%s)>' % (table, pk))
     104
     105  #       if patient is not None:
     106  #               if patient.ID != instance['pk_patient']:
     107  #                       raise ValueError(u'patient passed in: [%s], but instance is: [%s:%s:%s]' % (patient.ID, table, pk, instance['pk_patient']))
     108
     109          if hasattr(instance, 'format_maximum_information'):
     110                  return '\n'.join(instance.format_maximum_information(patient = patient))
     111
     112          if hasattr(instance, 'format'):
     113                  try:
     114                          formatted = instance.format(patient = patient)
     115                  except TypeError:
     116                          formatted = instance.format()
     117                  if type(formatted) == type([]):
     118                          return '\n'.join(formatted)
     119                  return formatted
     120
     121          d = instance.fields_as_dict (
     122                  date_format = '%Y %b %d  %H:%M',
     123                  none_string = gmTools.u_diameter,
     124                  escape_style = None,
     125                  bool_strings = [_('True'), _('False')]
     126          )
     127          return gmTools.format_dict_like(d, tabular = True, value_delimiters = None)
     128
     129  #============================================================
     130 -def __noop_delayed_execute(*args, **kwargs):
     131          pass
     132
     133
     134  _delayed_execute = __noop_delayed_execute
     135
     136
     137 -def set_delayed_executor(executor):
     138          if not callable(executor):
     139                  raise TypeError('executor <%s> is not callable' % executor)
     140          global _delayed_execute
     141          _delayed_execute = executor
     142          _log.debug('setting delayed executor to <%s>', executor)
     143
     144  #------------------------------------------------------------
     145 -class cClinicalRecord(object):
     146
     147 -        def __init__(self, aPKey=None):#, allow_user_interaction=True, encounter=None):
     148                  """Fails if
     149
     150                  - no connection to database possible
     151                  - patient referenced by aPKey does not exist
     152                  """
     153                  self.pk_patient = aPKey                 # == identity.pk == primary key
     154                  self.gender = None
     155                  self.dob = None
     156
     157                  from Gnumed.business import gmPraxis
     158                  global _here
     159                  if _here is None:
     160                          _here = gmPraxis.gmCurrentPraxisBranch()
     161
     162                  self.__encounter = None
     163                  self.__setup_active_encounter()
     164
     165                  # register backend notification interests
     166                  # (keep this last so we won't hang on threads when
     167                  #  failing this constructor for other reasons ...)
     168                  if not self._register_interests():
     169                          raise gmExceptions.ConstructorError("cannot register signal interests")
     170
     171                  gmAllergy.ensure_has_allergy_state(encounter = self.current_encounter['pk_encounter'])
     172                  #_delayed_execute(gmAllergy.ensure_has_allergy_state, encounter = self.current_encounter['pk_encounter'])
     173
     174                  self.__calculator = None
     175
     176                  _log.debug('Instantiated clinical record for patient [%s].' % self.pk_patient)
     177
     178  #       #--------------------------------------------------------
     179  #       def __old_style_init(self, allow_user_interaction=True):
     180  #
     181  #               _log.error('%s.__old_style_init() used', self.__class__.__name__)
     182  #               print u'*** GNUmed [%s]: __old_style_init() used ***' % self.__class__.__name__
     183  #
     184  #               # FIXME: delegate to worker thread
     185  #               # log access to patient record (HIPAA, for example)
     186  #               cmd = u'SELECT gm.log_access2emr(%(todo)s)'
     187  #               args = {'todo': u'patient [%s]' % self.pk_patient}
     188  #               gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     189  #
     190  #               # load current or create new encounter
     191  #               if _func_ask_user is None:
     192  #                       _log.error('[_func_ask_user] is None')
     193  #                       print "*** GNUmed [%s]: _func_ask_user is not set ***" % self.__class__.__name__
     194  #
     195  ##              # FIXME: delegate to worker thread ?
     196  #               self.remove_empty_encounters()
     197  #
     198  #               self.__encounter = None
     199  #               if not self.__initiate_active_encounter(allow_user_interaction = allow_user_interaction):
     200  #                       raise gmExceptions.ConstructorError("cannot activate an encounter for patient [%s]" % self.pk_patient)
     201  #
     202  ##              # FIXME: delegate to worker thread
     203  #               gmAllergy.ensure_has_allergy_state(encounter = self.current_encounter['pk_encounter'])
     204
     205          #--------------------------------------------------------
     206 -        def cleanup(self):
     207                  _log.debug('cleaning up after clinical record for patient [%s]' % self.pk_patient)
     208                  if self.__encounter is not None:
     209                          self.__encounter.unlock(exclusive = False)
     210                  return True
     211
     212          #--------------------------------------------------------
     213 -        def log_access(self, action=None):
     214                  if action is None:
     215                          action = 'EMR access for pk_identity [%s]' % self.pk_patient
     216                  args = {'action': action}
     217                  cmd = 'SELECT gm.log_access2emr(%(action)s)'
     218                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     219
     220          #--------------------------------------------------------
     221 -        def _get_calculator(self):
     222                  if self.__calculator is None:
     223                          from Gnumed.business.gmClinicalCalculator import cClinicalCalculator
     224                          self.__calculator = cClinicalCalculator()
     225                          from Gnumed.business.gmPerson import gmCurrentPatient
     226                          curr_pat = gmCurrentPatient()
     227                          if curr_pat.ID == self.pk_patient:
     228                                  self.__calculator.patient = curr_pat
     229                          else:
     230                                  from Gnumed.business.gmPerson import cPatient
     231                                  self.__calculator.patient = cPatient(self.pk_patient)
     232                  return self.__calculator
     233
     234          calculator = property(_get_calculator, lambda x:x)
     235
     236          #--------------------------------------------------------
     237          # messaging
     238          #--------------------------------------------------------
     239 -        def _register_interests(self):
     240                  #gmDispatcher.connect(signal = u'clin.encounter_mod_db', receiver = self.db_callback_encounter_mod_db)
     241                  gmDispatcher.connect(signal = 'gm_table_mod', receiver = self.db_modification_callback)
     242
     243                  return True
     244
     245          #--------------------------------------------------------
     246 -        def db_modification_callback(self, **kwds):
     247
     248                  if kwds['table'] != 'clin.encounter':
     249                          return True
     250                  if self.current_encounter is None:
     251                          _log.debug('no local current-encounter, ignoring encounter modification signal')
     252                          return True
     253                  if int(kwds['pk_of_row']) != self.current_encounter['pk_encounter']:
     254                          _log.debug('modified encounter [%s] != local encounter [%s], ignoring signal', kwds['pk_of_row'], self.current_encounter['pk_encounter'])
     255                          return True
     256
     257                  _log.debug('modification of our encounter (%s) signalled (%s)', self.current_encounter['pk_encounter'], kwds['pk_of_row'])
     258
     259                  # get the current encounter as an extra instance
     260                  # from the database to check for changes
     261                  curr_enc_in_db = gmEMRStructItems.cEncounter(aPK_obj = self.current_encounter['pk_encounter'])
     262
     263                  # the encounter just retrieved and the active encounter
     264                  # have got the same transaction ID so there's no change
     265                  # in the database, there could be a local change in
     266                  # the active encounter but that doesn't matter because
     267                  # no one else can have written to the DB so far
     268                  if curr_enc_in_db['xmin_encounter'] == self.current_encounter['xmin_encounter']:
     269                          _log.debug('same XMIN, no difference between DB and in-client instance of current encounter expected')
     270                          if self.current_encounter.is_modified():
     271                                  _log.error('encounter modification signal from DB with same XMIN as in local in-client instance of encounter BUT local instance ALSO has .is_modified()=True')
     272                                  _log.error('this hints at an error in .is_modified handling')
     273                                  gmTools.compare_dict_likes(self.current_encounter.fields_as_dict(), curr_enc_in_db.fields_as_dict(), 'modified enc in client', 'enc loaded from DB')
     274                          return True
     275
     276                  # there must have been a change to the active encounter
     277                  # committed to the database from elsewhere,
     278                  # we must fail propagating the change, however, if
     279                  # there are local changes pending
     280                  if self.current_encounter.is_modified():
     281                          gmTools.compare_dict_likes(self.current_encounter.fields_as_dict(), curr_enc_in_db.fields_as_dict(), 'modified enc in client', 'signalled enc loaded from DB')
     282                          raise ValueError('unsaved changes in locally active encounter [%s], cannot switch to DB state of encounter [%s]' % (
     283                                  self.current_encounter['pk_encounter'],
     284                                  curr_enc_in_db['pk_encounter']
     285                          ))
     286
     287                  # don't do this: same_payload() does not compare _all_ fields
     288                  # so we can get into a reality disconnect if we don't
     289                  # announce the mod
     290  #               if self.current_encounter.same_payload(another_object = curr_enc_in_db):
     291  #                       _log.debug('clin.encounter_mod_db received but no change to active encounter payload')
     292  #                       return True
     293
     294                  # there was a change in the database from elsewhere,
     295                  # locally, however, we don't have any pending changes,
     296                  # therefore we can propagate the remote change locally
     297                  # without losing anything
     298                  # this really should be the standard case
     299                  gmTools.compare_dict_likes(self.current_encounter.fields_as_dict(), curr_enc_in_db.fields_as_dict(), 'modified enc in client', 'enc loaded from DB')
     300                  _log.debug('active encounter modified remotely, no locally pending changes, reloading from DB and locally announcing the remote modification')
     301                  self.current_encounter.refetch_payload()
     302                  gmDispatcher.send('current_encounter_modified')
     303
     304                  return True
     305
     306          #--------------------------------------------------------
     307 -        def db_callback_encounter_mod_db(self, **kwds):
     308
     309                  # get the current encounter as an extra instance
     310                  # from the database to check for changes
     311                  curr_enc_in_db = gmEMRStructItems.cEncounter(aPK_obj = self.current_encounter['pk_encounter'])
     312
     313                  # the encounter just retrieved and the active encounter
     314                  # have got the same transaction ID so there's no change
     315                  # in the database, there could be a local change in
     316                  # the active encounter but that doesn't matter because
     317                  # no one else can have written to the DB so far
     318                  # THIS DOES NOT WORK
     319  #               if curr_enc_in_db['xmin_encounter'] == self.current_encounter['xmin_encounter']:
     320  #                       return True
     321
     322                  # there must have been a change to the active encounter
     323                  # committed to the database from elsewhere,
     324                  # we must fail propagating the change, however, if
     325                  # there are local changes
     326                  if self.current_encounter.is_modified():
     327                          gmTools.compare_dict_likes(self.current_encounter.fields_as_dict(), curr_enc_in_db.fields_as_dict(), 'modified enc in client', 'enc loaded from DB')
     328                          _log.error('current in client: %s', self.current_encounter)
     329                          raise ValueError('unsaved changes in active encounter [%s], cannot switch [%s]' % (
     330                                  self.current_encounter['pk_encounter'],
     331                                  curr_enc_in_db['pk_encounter']
     332                          ))
     333
     334                  if self.current_encounter.same_payload(another_object = curr_enc_in_db):
     335                          _log.debug('clin.encounter_mod_db received but no change to active encounter payload')
     336                          return True
     337
     338                  # there was a change in the database from elsewhere,
     339                  # locally, however, we don't have any changes, therefore
     340                  # we can propagate the remote change locally without
     341                  # losing anything
     342                  gmTools.compare_dict_likes(self.current_encounter.fields_as_dict(), curr_enc_in_db.fields_as_dict(), 'modified enc in client', 'enc loaded from DB')
     343                  _log.debug('active encounter modified remotely, reloading from DB and locally announcing the modification')
     344                  self.current_encounter.refetch_payload()
     345                  gmDispatcher.send('current_encounter_modified')
     346
     347                  return True
     348
     349          #--------------------------------------------------------
     350          # API: family history
     351          #--------------------------------------------------------
     352 -        def get_family_history(self, episodes=None, issues=None, encounters=None):
     353                  fhx = gmFamilyHistory.get_family_history (
     354                          order_by = 'l10n_relation, condition',
     355                          patient = self.pk_patient
     356                  )
     357
     358                  if episodes is not None:
     359                          fhx = [ f for f in fhx if f['pk_episode'] in episodes ]
     360
     361                  if issues is not None:
     362                          fhx = [ f for f in fhx if f['pk_health_issue'] in issues ]
     363
     364                  if encounters is not None:
     365                          fhx = [ f for f in fhx if f['pk_encounter'] in encounters ]
     366
     367                  return fhx
     368
     369          #--------------------------------------------------------
     370 -        def add_family_history(self, episode=None, condition=None, relation=None):
     371                  return gmFamilyHistory.create_family_history (
     372                          encounter = self.current_encounter['pk_encounter'],
     373                          episode = episode,
     374                          condition = condition,
     375                          relation = relation
     376                  )
     377
     378          #--------------------------------------------------------
     379          # API: pregnancy
     380          #--------------------------------------------------------
     381 -        def _get_gender(self):
     382                  if self.__gender is not None:
     383                          return self.__gender
     384                  cmd = 'SELECT gender, dob FROM dem.v_all_persons WHERE pk_identity = %(pat)s'
     385                  args = {'pat': self.pk_patient}
     386                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     387                  self.__gender = rows[0]['gender']
     388                  self.__dob = rows[0]['dob']
     389
     390 -        def _set_gender(self, gender):
     391                  self.__gender = gender
     392
     393          gender = property(_get_gender, _set_gender)
     394
     395          #--------------------------------------------------------
     396 -        def _get_dob(self):
     397                  if self.__dob is not None:
     398                          return self.__dob
     399                  cmd = 'SELECT gender, dob FROM dem.v_all_persons WHERE pk_identity = %(pat)s'
     400                  args = {'pat': self.pk_patient}
     401                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     402                  self.__gender = rows[0]['gender']
     403                  self.__dob = rows[0]['dob']
     404
     405 -        def _set_dob(self, dob):
     406                  self.__dob = dob
     407
     408          dob = property(_get_dob, _set_dob)
     409
     410          #--------------------------------------------------------
     411 -        def _get_EDC(self):
     412                  cmd = 'SELECT edc FROM clin.patient WHERE fk_identity = %(pat)s'
     413                  args = {'pat': self.pk_patient}
     414                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     415                  if len(rows) == 0:
     416                          return None
     417                  return rows[0]['edc']
     418
     419 -        def _set_EDC(self, edc):
     420                  cmd = """
     421                          INSERT INTO clin.patient (fk_identity, edc) SELECT
     422                                  %(pat)s,
     423                                  %(edc)s
     424                          WHERE NOT EXISTS (
     425                                  SELECT 1 FROM clin.patient WHERE fk_identity = %(pat)s
     426                          )
     427                          RETURNING pk"""
     428                  args = {'pat': self.pk_patient, 'edc': edc}
     429                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False, return_data = True)
     430                  if len(rows) == 0:
     431                          cmd = 'UPDATE clin.patient SET edc = %(edc)s WHERE fk_identity = %(pat)s'
     432                          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     433
     434          EDC = property(_get_EDC, _set_EDC)
     435
     436          #--------------------------------------------------------
     437 -        def _EDC_is_fishy(self):
     438                  edc = self.EDC
     439                  if edc is None:
     440                          return False
     441                  if self.gender != 'f':
     442                          return True
     443                  now = gmDateTime.pydt_now_here()
     444                  # mother too young
     445                  if (self.dob + pydt.timedelta(weeks = 5 * 52)) > now:
     446                          return True
     447                  # mother too old
     448                  if (self.dob + pydt.timedelta(weeks = 55 * 52)) < now:
     449                          return True
     450                  # Beulah Hunter, 375 days (http://www.reference.com/motif/health/longest-human-pregnancy-on-record)
     451                  # EDC too far in the future
     452                  if (edc - pydt.timedelta(days = 380)) > now:
     453                          return True
     454                  # even if the pregnancy would have *started* when it
     455                  # was documented to *end* it would be over by now by
     456                  # all accounts
     457                  # EDC too far in the past
     458                  if edc < (now - pydt.timedelta(days = 380)):
     459                          return True
     460
     461          EDC_is_fishy = property(_EDC_is_fishy, lambda x:x)
     462
     463          #--------------------------------------------------------
     464 -        def __normalize_smoking_details(self, details):
     465                  try:
     466                          details['quit_when']
     467                  except KeyError:
     468                          details['quit_when'] = None
     469
     470                  try:
     471                          details['last_confirmed']
     472                          if details['last_confirmed'] is None:
     473                                  details['last_confirmed'] = gmDateTime.pydt_now_here()
     474                  except KeyError:
     475                          details['last_confirmed'] = gmDateTime.pydt_now_here()
     476
     477                  try:
     478                          details['comment']
     479                          if details['comment'].strip() == '':
     480                                  details['comment'] = None
     481                  except KeyError:
     482                          details['comment'] = None
     483
     484                  return details
     485
     486          #--------------------------------------------------------
     487 -        def _get_smoking_status(self):
     488                  use = self.harmful_substance_use
     489                  if use is None:
     490                          return None
     491                  return use['tobacco']
     492
     493 -        def _set_smoking_status(self, status):
     494                  # valid ?
     495                  status_flag, details = status
     496                  self.__harmful_substance_use = None
     497                  args = {
     498                          'pat': self.pk_patient,
     499                          'status': status_flag
     500                  }
     501                  if status_flag is None:
     502                          cmd = 'UPDATE clin.patient SET smoking_status = %(status)s, smoking_details = NULL WHERE fk_identity = %(pat)s'
     503                  elif status_flag == 0:
     504                          details['quit_when'] = None
     505                          args['details'] = gmTools.dict2json(self.__normalize_smoking_details(details))
     506                          cmd = 'UPDATE clin.patient SET smoking_status = %(status)s, smoking_details = %(details)s WHERE fk_identity = %(pat)s'
     507                  else:
     508                          args['details'] = gmTools.dict2json(self.__normalize_smoking_details(details))
     509                          cmd = 'UPDATE clin.patient SET smoking_status = %(status)s, smoking_details = %(details)s WHERE fk_identity = %(pat)s'
     510                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     511
     512          smoking_status = property(_get_smoking_status, _set_smoking_status)
     513
     514          #--------------------------------------------------------
     515 -        def _get_alcohol_status(self):
     516                  use = self.harmful_substance_use
     517                  if use is None:
     518                          return None
     519                  return use['alcohol']
     520
     521 -        def _set_alcohol_status(self, status):
     522                  # valid ?
     523                  harmful, details = status
     524                  self.__harmful_substance_use = None
     525                  args = {'pat': self.pk_patient}
     526                  if harmful is None:
     527                          cmd = 'UPDATE clin.patient SET c2_currently_harmful_use = NULL, c2_details = NULL WHERE fk_identity = %(pat)s'
     528                  elif harmful is False:
     529                          cmd = 'UPDATE clin.patient SET c2_currently_harmful_use = FALSE, c2_details = gm.nullify_empty_string(%(details)s) WHERE fk_identity = %(pat)s'
     530                  else:
     531                          cmd = 'UPDATE clin.patient SET c2_currently_harmful_use = TRUE, c2_details = gm.nullify_empty_string(%(details)s) WHERE fk_identity = %(pat)s'
     532                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     533
     534          alcohol_status = property(_get_alcohol_status, _set_alcohol_status)
     535
     536          #--------------------------------------------------------
     537 -        def _get_drugs_status(self):
     538                  use = self.harmful_substance_use
     539                  if use is None:
     540                          return None
     541                  return use['drugs']
     542
     543 -        def _set_drugs_status(self, status):
     544                  # valid ?
     545                  harmful, details = status
     546                  self.__harmful_substance_use = None
     547                  args = {'pat': self.pk_patient}
     548                  if harmful is None:
     549                          cmd = 'UPDATE clin.patient SET drugs_currently_harmful_use = NULL, drugs_details = NULL WHERE fk_identity = %(pat)s'
     550                  elif harmful is False:
     551                          cmd = 'UPDATE clin.patient SET drugs_currently_harmful_use = FALSE, drugs_details = gm.nullify_empty_string(%(details)s) WHERE fk_identity = %(pat)s'
     552                  else:
     553                          cmd = 'UPDATE clin.patient SET drugs_currently_harmful_use = TRUE, drugs_details = gm.nullify_empty_string(%(details)s) WHERE fk_identity = %(pat)s'
     554                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     555
     556          drugs_status = property(_get_drugs_status, _set_drugs_status)
     557
     558          #--------------------------------------------------------
     559 -        def _get_harmful_substance_use(self):
     560                  # caching does not take into account status changes from elsewhere
     561                  try:
     562                          self.__harmful_substance_use
     563                  except AttributeError:
     564                          self.__harmful_substance_use = None
     565
     566                  if self.__harmful_substance_use is not None:
     567                          return self.__harmful_substance_use
     568
     569                  args = {'pat': self.pk_patient}
     570                  cmd = """
     571                          SELECT
     572                                  -- tobacco use
     573                                  smoking_status,
     574                                  smoking_details,
     575                                  (smoking_details->>'last_confirmed')::timestamp with time zone
     576                                          AS ts_last,
     577                                  (smoking_details->>'quit_when')::timestamp with time zone
     578                                          AS ts_quit,
     579                                  -- c2 use
     580                                  c2_currently_harmful_use,
     581                                  c2_details,
     582                                  -- other drugs use
     583                                  drugs_currently_harmful_use,
     584                                  drugs_details
     585                          FROM clin.patient
     586                          WHERE fk_identity = %(pat)s
     587                  """
     588                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     589                  if len(rows) == 0:
     590                          return None
     591                  # disentangle smoking
     592                  status = rows[0]['smoking_status']
     593                  details = rows[0]['smoking_details']
     594                  if status is not None:
     595                          details['last_confirmed'] = rows[0]['ts_last']
     596                          details['quit_when'] = rows[0]['ts_quit']
     597                  # set fields
     598                  self.__harmful_substance_use = {
     599                          'tobacco':  (status, details),
     600                          'alcohol': (rows[0]['c2_currently_harmful_use'], rows[0]['c2_details']),
     601                          'drugs': (rows[0]['drugs_currently_harmful_use'], rows[0]['drugs_details'])
     602                  }
     603
     604                  return self.__harmful_substance_use
     605
     606
     607 -        def _get_harmful_substance_use2(self):
     608                  cmd = 'SELECT * FROM clin.v_substance_intakes WHERE harmful_use_type  = %s'
     609
     610          harmful_substance_use = property(_get_harmful_substance_use, lambda x:x)
     611
     612          #--------------------------------------------------------
     613 -        def format_harmful_substance_use(self, include_tobacco=True, include_alcohol=True, include_drugs=True, include_nonuse=True, include_unknown=True):
     614                  use = self.harmful_substance_use
     615                  if use is None:
     616                          return []
     617
     618                  lines = []
     619
     620                  if include_tobacco:
     621                          status, details = use['tobacco']
     622                          add_details = False
     623                          if status is None:
     624                                  if include_unknown:
     625                                          lines.append(_('unknown smoking status'))
     626                          elif status == 0:
     627                                  if include_nonuse:
     628                                          lines.append('%s (%s)' % (_('non-smoker'), gmDateTime.pydt_strftime(details['last_confirmed'], '%Y %b %d')))
     629                                          add_details = True
     630                          elif status == 1:               # now or previous
     631                                  if details['quit_when'] is None:
     632                                          lines.append('%s (%s)' % (_('current smoker'), gmDateTime.pydt_strftime(details['last_confirmed'], '%Y %b %d')))
     633                                          add_details = True
     634                                  elif details['quit_when'] < gmDateTime.pydt_now_here():
     635                                          if include_nonuse:
     636                                                  lines.append('%s (%s)' % (_('ex-smoker'), gmDateTime.pydt_strftime(details['last_confirmed'], '%Y %b %d')))
     637                                                  add_details = True
     638                                  else:
     639                                          lines.append('%s (%s)' % (_('current smoker'), gmDateTime.pydt_strftime(details['last_confirmed'], '%Y %b %d')))
     640                                          add_details = True
     641                          elif status == 2:               # addicted
     642                                  lines.append('%s (%s)' % (_('tobacco addiction'), gmDateTime.pydt_strftime(details['last_confirmed'], '%Y %b %d')))
     643                                  add_details = True
     644                          if add_details:
     645                                  if details['quit_when'] is not None:
     646                                          lines.append(' %s: %s' % (_('Quit date'), gmDateTime.pydt_strftime(details['quit_when'], '%Y %b %d')))
     647                                  if details['comment'] is not None:
     648                                          lines.append(' %s' % details['comment'])
     649
     650                  if include_alcohol:
     651                          status, details = use['alcohol']
     652                          if status is False:
     653                                  if include_nonuse:
     654                                          if len(lines) > 0:
     655                                                  lines.append('')
     656                                          lines.append(_('no or non-harmful alcohol use'))
     657                                          lines.append(' %s' % details)
     658                          elif status is True:
     659                                  if len(lines) > 0:
     660                                          lines.append('')
     661                                  lines.append(_('harmful alcohol use'))
     662                                  lines.append(' %s' % details)
     663                          else:
     664                                  if include_unknown:
     665                                          if len(lines) > 0:
     666                                                  lines.append('')
     667                                          lines.append(_('unknown alcohol use'))
     668                                          lines.append(' %s' % details)
     669
     670                  if include_drugs:
     671                          status, details = use['drugs']
     672                          if status is False:
     673                                  if include_nonuse:
     674                                          if len(lines) > 0:
     675                                                  lines.append('')
     676                                          lines.append(_('no or non-harmful drug use'))
     677                                          lines.append(' %s' % details)
     678                          elif status is True:
     679                                  if len(lines) > 0:
     680                                          lines.append('')
     681                                  lines.append(_('harmful drug use'))
     682                                  lines.append(' %s' % details)
     683                          else:
     684                                  if include_unknown:
     685                                          if len(lines) > 0:
     686                                                  lines.append('')
     687                                          lines.append(_('unknown drug use'))
     688                                          lines.append(' %s' % details)
     689
     690                  return lines
     691
     692          #--------------------------------------------------------
     693 -        def _get_currently_abuses_substances(self):
     694                  # returns True / False / None (= unknown)
     695
     696                  use = self.harmful_substance_use
     697                  # we know that at least one group is used:
     698                  if use['alcohol'][0] is True:
     699                          return True
     700                  if use['drugs'][0] is True:
     701                          return True
     702                  if use['tobacco'][0] > 0:
     703                  # is True:
     704                          if use['tobacco'][1]['quit_when'] is None:
     705                                  return True
     706                  # at this point no group is currently used for sure
     707                  # we don't know about some of the groups so we can NOT say: no abuse at all:
     708                  if use['alcohol'][0] is None:
     709                          return None
     710                  if use['drugs'][0] is None:
     711                          return None
     712                  if use['tobacco'][0] is None:
     713                          return None
     714                  # at this point all groups must be FALSE, except for
     715                  # tobacco which can also be TRUE _but_, if so, a quit
     716                  # date has been set, which is considered non-abuse
     717                  return False
     718
     719          currently_abuses_substances = property(_get_currently_abuses_substances, lambda x:x)
     720
     721          #--------------------------------------------------------
     722          # API: performed procedures
     723          #--------------------------------------------------------
     724 -        def get_performed_procedures(self, episodes=None, issues=None):
     725
     726                  procs = gmEMRStructItems.get_performed_procedures(patient = self.pk_patient)
     727
     728                  if episodes is not None:
     729                          procs = [ p for p in procs if p['pk_episode'] in episodes ]
     730
     731                  if issues is not None:
     732                          procs = [ p for p in procs if p['pk_health_issue'] in issues ]
     733
     734                  return procs
     735
     736          performed_procedures = property(get_performed_procedures, lambda x:x)
     737          #--------------------------------------------------------
     738 -        def get_latest_performed_procedure(self):
     739                  return gmEMRStructItems.get_latest_performed_procedure(patient = self.pk_patient)
     740          #--------------------------------------------------------
     741 -        def add_performed_procedure(self, episode=None, location=None, hospital_stay=None, procedure=None):
     742                  return gmEMRStructItems.create_performed_procedure (
     743                          encounter = self.current_encounter['pk_encounter'],
     744                          episode = episode,
     745                          location = location,
     746                          hospital_stay = hospital_stay,
     747                          procedure = procedure
     748                  )
     749          #--------------------------------------------------------
     750 -        def get_procedure_locations_as_org_units(self):
     751                  where = 'pk_org_unit IN (SELECT DISTINCT pk_org_unit FROM clin.v_procedures_not_at_hospital WHERE pk_patient = %(pat)s)'
     752                  args = {'pat': self.pk_patient}
     753                  cmd = gmOrganization._SQL_get_org_unit % where
     754                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     755                  return [ gmOrganization.cOrgUnit(row = {'pk_field': 'pk_org_unit', 'data': r, 'idx': idx}) for r in rows ]
     756
     757          #--------------------------------------------------------
     758          # API: hospitalizations
     759          #--------------------------------------------------------
     760 -        def get_hospital_stays(self, episodes=None, issues=None, ongoing_only=False):
     761                  stays = gmEMRStructItems.get_patient_hospital_stays(patient = self.pk_patient, ongoing_only = ongoing_only)
     762                  if episodes is not None:
     763                          stays = [ s for s in stays if s['pk_episode'] in episodes ]
     764                  if issues is not None:
     765                          stays = [ s for s in stays if s['pk_health_issue'] in issues ]
     766                  return stays
     767
     768          hospital_stays = property(get_hospital_stays, lambda x:x)
     769          #--------------------------------------------------------
     770 -        def get_latest_hospital_stay(self):
     771                  return gmEMRStructItems.get_latest_patient_hospital_stay(patient = self.pk_patient)
     772          #--------------------------------------------------------
     773 -        def add_hospital_stay(self, episode=None, fk_org_unit=None):
     774                  return gmEMRStructItems.create_hospital_stay (
     775                          encounter = self.current_encounter['pk_encounter'],
     776                          episode = episode,
     777                          fk_org_unit = fk_org_unit
     778                  )
     779          #--------------------------------------------------------
     780 -        def get_hospital_stay_stats_by_hospital(self, cover_period=None):
     781                  args = {'pat': self.pk_patient, 'range': cover_period}
     782                  where_parts = ['pk_patient = %(pat)s']
     783                  if cover_period is not None:
     784                          where_parts.append('discharge > (now() - %(range)s)')
     785
     786                  cmd = """
     787                          SELECT hospital, count(1) AS frequency
     788                          FROM clin.v_hospital_stays
     789                          WHERE
     790                                  %s
     791                          GROUP BY hospital
     792                          ORDER BY frequency DESC
     793                  """ % ' AND '.join(where_parts)
     794
     795                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     796                  return rows
     797          #--------------------------------------------------------
     798 -        def get_attended_hospitals_as_org_units(self):
     799                  where = 'pk_org_unit IN (SELECT DISTINCT pk_org_unit FROM clin.v_hospital_stays WHERE pk_patient = %(pat)s)'
     800                  args = {'pat': self.pk_patient}
     801                  cmd = gmOrganization._SQL_get_org_unit % where
     802                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     803                  return [ gmOrganization.cOrgUnit(row = {'pk_field': 'pk_org_unit', 'data': r, 'idx': idx}) for r in rows ]
     804
     805          #--------------------------------------------------------
     806          # API: narrative
     807          #--------------------------------------------------------
     808 -        def add_notes(self, notes=None, episode=None, encounter=None):
     809                  enc = gmTools.coalesce (
     810                          encounter,
     811                          self.current_encounter['pk_encounter']
     812                  )
     813                  for note in notes:
     814                          gmClinNarrative.create_narrative_item (
     815                                  narrative = note[1],
     816                                  soap_cat = note[0],
     817                                  episode_id = episode,
     818                                  encounter_id = enc
     819                          )
     820                  return True
     821
     822          #--------------------------------------------------------
     823 -        def add_clin_narrative(self, note='', soap_cat='s', episode=None, link_obj=None):
     824                  if note.strip() == '':
     825                          _log.info('will not create empty clinical note')
     826                          return None
     827                  if isinstance(episode, gmEMRStructItems.cEpisode):
     828                          episode = episode['pk_episode']
     829                  instance = gmClinNarrative.create_narrative_item (
     830                          link_obj = link_obj,
     831                          narrative = note,
     832                          soap_cat = soap_cat,
     833                          episode_id = episode,
     834                          encounter_id = self.current_encounter['pk_encounter']
     835                  )
     836                  return instance
     837
     838          #--------------------------------------------------------
     839 -        def get_clin_narrative(self, encounters=None, episodes=None, issues=None, soap_cats=None, providers=None):
     840                  """Get SOAP notes pertinent to this encounter.
     841
     842                          encounters
     843                                  - list of encounters the narrative of which are to be retrieved
     844                          episodes
     845                                  - list of episodes the narrative of which are to be retrieved
     846                          issues
     847                                  - list of health issues the narrative of which are to be retrieved
     848                          soap_cats
     849                                  - list of SOAP categories of the narrative to be retrieved
     850                  """
     851                  where_parts = ['pk_patient = %(pat)s']
     852                  args = {'pat': self.pk_patient}
     853
     854                  if issues is not None:
     855                          where_parts.append('pk_health_issue IN %(issues)s')
     856                          if len(issues) == 0:
     857                                  args['issues'] = tuple()
     858                          else:
     859                                  if isinstance(issues[0], gmEMRStructItems.cHealthIssue):
     860                                          args['issues'] = tuple([ i['pk_health_issue'] for i in issues ])
     861                                  elif isinstance(issues[0], int):
     862                                          args['issues'] = tuple(issues)
     863                                  else:
     864                                          raise ValueError('<issues> must be list of type int (=pk) or cHealthIssue, but 1st issue is: %s' % issues[0])
     865
     866                  if episodes is not None:
     867                          where_parts.append('pk_episode IN %(epis)s')
     868                          if len(episodes) == 0:
     869                                  args['epis'] = tuple()
     870                          else:
     871                                  if isinstance(episodes[0], gmEMRStructItems.cEpisode):
     872                                          args['epis'] = tuple([ e['pk_episode'] for e in episodes ])
     873                                  elif isinstance(episodes[0], int):
     874                                          args['epis'] = tuple(episodes)
     875                                  else:
     876                                          raise ValueError('<episodes> must be list of type int (=pk) or cEpisode, but 1st episode is: %s' % episodes[0])
     877
     878                  if encounters is not None:
     879                          where_parts.append('pk_encounter IN %(encs)s')
     880                          if len(encounters) == 0:
     881                                  args['encs'] = tuple()
     882                          else:
     883                                  if isinstance(encounters[0], gmEMRStructItems.cEncounter):
     884                                          args['encs'] = tuple([ e['pk_encounter'] for e in encounters ])
     885                                  elif isinstance(encounters[0], int):
     886                                          args['encs'] = tuple(encounters)
     887                                  else:
     888                                          raise ValueError('<encounters> must be list of type int (=pk) or cEncounter, but 1st encounter is: %s' % encounters[0])
     889
     890                  if soap_cats is not None:
     891                          where_parts.append('c_vn.soap_cat IN %(cats)s')
     892                          args['cats'] = tuple(gmSoapDefs.soap_cats2list(soap_cats))
     893
     894                  if providers is not None:
     895                          where_parts.append('c_vn.modified_by IN %(docs)s')
     896                          args['docs'] = tuple(providers)
     897
     898                  cmd = """
     899                          SELECT
     900                                  c_vn.*,
     901                                  c_scr.rank AS soap_rank
     902                          FROM
     903                                  clin.v_narrative c_vn
     904                                          LEFT JOIN clin.soap_cat_ranks c_scr on c_vn.soap_cat = c_scr.soap_cat
     905                          WHERE %s
     906                          ORDER BY date, soap_rank
     907                  """ % ' AND '.join(where_parts)
     908
     909                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     910                  return [ gmClinNarrative.cNarrative(row = {'pk_field': 'pk_narrative', 'idx': idx, 'data': row}) for row in rows ]
     911
     912          #--------------------------------------------------------
     913 -        def search_narrative_simple(self, search_term=''):
     914
     915                  search_term = search_term.strip()
     916                  if search_term == '':
     917                          return []
     918
     919                  cmd = """
     920                          SELECT
     921                                  *,
     922                                  coalesce((SELECT description FROM clin.episode WHERE pk = vn4s.pk_episode), vn4s.src_table)
     923                                          as episode,
     924                                  coalesce((SELECT description FROM clin.health_issue WHERE pk = vn4s.pk_health_issue), vn4s.src_table)
     925                                          as health_issue,
     926                                  (SELECT started FROM clin.encounter WHERE pk = vn4s.pk_encounter)
     927                                          as encounter_started,
     928                                  (SELECT last_affirmed FROM clin.encounter WHERE pk = vn4s.pk_encounter)
     929                                          as encounter_ended,
     930                                  (SELECT _(description) FROM clin.encounter_type WHERE pk = (SELECT fk_type FROM clin.encounter WHERE pk = vn4s.pk_encounter))
     931                                          as encounter_type
     932                          from clin.v_narrative4search vn4s
     933                          WHERE
     934                                  pk_patient = %(pat)s and
     935                                  vn4s.narrative ~ %(term)s
     936                          order by
     937                                  encounter_started
     938                  """ # case sensitive
     939                  rows, idx = gmPG2.run_ro_queries(queries = [
     940                          {'cmd': cmd, 'args': {'pat': self.pk_patient, 'term': search_term}}
     941                  ])
     942                  return rows
     943          #--------------------------------------------------------
     944 -        def get_text_dump(self, since=None, until=None, encounters=None, episodes=None, issues=None):
     945                  fields = [
     946                          'age',
     947                          "to_char(modified_when, 'YYYY-MM-DD @ HH24:MI') as modified_when",
     948                          'modified_by',
     949                          'clin_when',
     950                          "case is_modified when false then '%s' else '%s' end as modified_string" % (_('original entry'), _('modified entry')),
     951                          'pk_item',
     952                          'pk_encounter',
     953                          'pk_episode',
     954                          'pk_health_issue',
     955                          'src_table'
     956                  ]
     957                  select_from = "SELECT %s FROM clin.v_pat_items" % ', '.join(fields)
     958                  # handle constraint conditions
     959                  where_snippets = []
     960                  params = {}
     961                  where_snippets.append('pk_patient=%(pat_id)s')
     962                  params['pat_id'] = self.pk_patient
     963                  if not since is None:
     964                          where_snippets.append('clin_when >= %(since)s')
     965                          params['since'] = since
     966                  if not until is None:
     967                          where_snippets.append('clin_when <= %(until)s')
     968                          params['until'] = until
     969                  # FIXME: these are interrelated, eg if we constrain encounter
     970                  # we automatically constrain issue/episode, so handle that,
     971                  # encounters
     972                  if not encounters is None and len(encounters) > 0:
     973                          params['enc'] = encounters
     974                          if len(encounters) > 1:
     975                                  where_snippets.append('fk_encounter in %(enc)s')
     976                          else:
     977                                  where_snippets.append('fk_encounter=%(enc)s')
     978                  # episodes
     979                  if not episodes is None and len(episodes) > 0:
     980                          params['epi'] = episodes
     981                          if len(episodes) > 1:
     982                                  where_snippets.append('fk_episode in %(epi)s')
     983                          else:
     984                                  where_snippets.append('fk_episode=%(epi)s')
     985                  # health issues
     986                  if not issues is None and len(issues) > 0:
     987                          params['issue'] = issues
     988                          if len(issues) > 1:
     989                                  where_snippets.append('fk_health_issue in %(issue)s')
     990                          else:
     991                                  where_snippets.append('fk_health_issue=%(issue)s')
     992
     993                  where_clause = ' and '.join(where_snippets)
     994                  order_by = 'order by src_table, age'
     995                  cmd = "%s WHERE %s %s" % (select_from, where_clause, order_by)
     996
     997                  rows, view_col_idx = gmPG.run_ro_query('historica', cmd, 1, params)
     998                  if rows is None:
     999                          _log.error('cannot load item links for patient [%s]' % self.pk_patient)
    1000                          return None
    1001
    1002                  # -- sort the data --
    1003                  # FIXME: by issue/encounter/episode, eg formatting
    1004                  # aggregate by src_table for item retrieval
    1005                  items_by_table = {}
    1006                  for item in rows:
    1007                          src_table = item[view_col_idx['src_table']]
    1008                          pk_item = item[view_col_idx['pk_item']]
    1009                          if src_table not in items_by_table:
    1010                                  items_by_table[src_table] = {}
    1011                          items_by_table[src_table][pk_item] = item
    1012
    1013                  # get mapping for issue/episode IDs
    1014                  issues = self.get_health_issues()
    1015                  issue_map = {}
    1016                  for issue in issues:
    1017                          issue_map[issue['pk_health_issue']] = issue['description']
    1018                  episodes = self.get_episodes()
    1019                  episode_map = {}
    1020                  for episode in episodes:
    1021                          episode_map[episode['pk_episode']] = episode['description']
    1022                  emr_data = {}
    1023                  # get item data from all source tables
    1024                  ro_conn = self._conn_pool.GetConnection('historica')
    1025                  curs = ro_conn.cursor()
    1026                  for src_table in items_by_table.keys():
    1027                          item_ids = items_by_table[src_table].keys()
    1028                          # we don't know anything about the columns of
    1029                          # the source tables but, hey, this is a dump
    1030                          if len(item_ids) == 0:
    1031                                  _log.info('no items in table [%s] ?!?' % src_table)
    1032                                  continue
    1033                          elif len(item_ids) == 1:
    1034                                  cmd = "SELECT * FROM %s WHERE pk_item=%%s order by modified_when" % src_table
    1035                                  if not gmPG.run_query(curs, None, cmd, item_ids[0]):
    1036                                          _log.error('cannot load items from table [%s]' % src_table)
    1037                                          # skip this table
    1038                                          continue
    1039                          elif len(item_ids) > 1:
    1040                                  cmd = "SELECT * FROM %s WHERE pk_item in %%s order by modified_when" % src_table
    1041                                  if not gmPG.run_query(curs, None, cmd, (tuple(item_ids),)):
    1042                                          _log.error('cannot load items from table [%s]' % src_table)
    1043                                          # skip this table
    1044                                          continue
    1045                          rows = curs.fetchall()
    1046                          table_col_idx = gmPG.get_col_indices(curs)
    1047                          # format per-table items
    1048                          for row in rows:
    1049                                  # FIXME: make this get_pkey_name()
    1050                                  pk_item = row[table_col_idx['pk_item']]
    1051                                  view_row = items_by_table[src_table][pk_item]
    1052                                  age = view_row[view_col_idx['age']]
    1053                                  # format metadata
    1054                                  try:
    1055                                          episode_name = episode_map[view_row[view_col_idx['pk_episode']]]
    1056                                  except:
    1057                                          episode_name = view_row[view_col_idx['pk_episode']]
    1058                                  try:
    1059                                          issue_name = issue_map[view_row[view_col_idx['pk_health_issue']]]
    1060                                  except:
    1061                                          issue_name = view_row[view_col_idx['pk_health_issue']]
    1062
    1063                                  if age not in emr_data:
    1064                                          emr_data[age] = []
    1065
    1066                                  emr_data[age].append(
    1067                                          _('%s: encounter (%s)') % (
    1068                                                  view_row[view_col_idx['clin_when']],
    1069                                                  view_row[view_col_idx['pk_encounter']]
    1070                                          )
    1071                                  )
    1072                                  emr_data[age].append(_('health issue: %s') % issue_name)
    1073                                  emr_data[age].append(_('episode     : %s') % episode_name)
    1074                                  # format table specific data columns
    1075                                  # - ignore those, they are metadata, some
    1076                                  #   are in clin.v_pat_items data already
    1077                                  cols2ignore = [
    1078                                          'pk_audit', 'row_version', 'modified_when', 'modified_by',
    1079                                          'pk_item', 'id', 'fk_encounter', 'fk_episode', 'pk'
    1080                                  ]
    1081                                  col_data = []
    1082                                  for col_name in table_col_idx.keys():
    1083                                          if col_name in cols2ignore:
    1084                                                  continue
    1085                                          emr_data[age].append("=> %s: %s" % (col_name, row[table_col_idx[col_name]]))
    1086                                  emr_data[age].append("----------------------------------------------------")
    1087                                  emr_data[age].append("-- %s from table %s" % (
    1088                                          view_row[view_col_idx['modified_string']],
    1089                                          src_table
    1090                                  ))
    1091                                  emr_data[age].append("-- written %s by %s" % (
    1092                                          view_row[view_col_idx['modified_when']],
    1093                                          view_row[view_col_idx['modified_by']]
    1094                                  ))
    1095                                  emr_data[age].append("----------------------------------------------------")
    1096                  curs.close()
    1097                  return emr_data
    1098          #--------------------------------------------------------
    1099 -        def get_patient_ID(self):
    1100                  return self.pk_patient
    1101          #--------------------------------------------------------
    1102 -        def get_statistics(self):
    1103                  union_query = '\n       union all\n'.join ([
    1104                          """
    1105                                  SELECT ((
    1106                                          -- all relevant health issues + active episodes WITH health issue
    1107                                          SELECT COUNT(1)
    1108                                          FROM clin.v_problem_list
    1109                                          WHERE
    1110                                                  pk_patient = %(pat)s
    1111                                                          AND
    1112                                                  pk_health_issue is not null
    1113                                  ) + (
    1114                                          -- active episodes WITHOUT health issue
    1115                                          SELECT COUNT(1)
    1116                                          FROM clin.v_problem_list
    1117                                          WHERE
    1118                                                  pk_patient = %(pat)s
    1119                                                          AND
    1120                                                  pk_health_issue is null
    1121                                  ))""",
    1122                          'SELECT count(1) FROM clin.encounter WHERE fk_patient = %(pat)s',
    1123                          'SELECT count(1) FROM clin.v_pat_items WHERE pk_patient = %(pat)s',
    1124                          'SELECT count(1) FROM blobs.v_doc_med WHERE pk_patient = %(pat)s',
    1125                          'SELECT count(1) FROM clin.v_test_results WHERE pk_patient = %(pat)s',
    1126                          'SELECT count(1) FROM clin.v_hospital_stays WHERE pk_patient = %(pat)s',
    1127                          'SELECT count(1) FROM clin.v_procedures WHERE pk_patient = %(pat)s',
    1128                          # active and approved substances == medication
    1129                          """
    1130                                  SELECT count(1)
    1131                                  FROM clin.v_substance_intakes
    1132                                  WHERE
    1133                                          pk_patient = %(pat)s
    1134                                                  AND
    1135                                          is_currently_active IN (null, true)
    1136                                                  AND
    1137                                          intake_is_approved_of IN (null, true)""",
    1138                          'SELECT count(1) FROM clin.v_vaccinations WHERE pk_patient = %(pat)s'
    1139                  ])
    1140
    1141                  rows, idx = gmPG2.run_ro_queries (
    1142                          queries = [{'cmd': union_query, 'args': {'pat': self.pk_patient}}],
    1143                          get_col_idx = False
    1144                  )
    1145
    1146                  stats = dict (
    1147                          problems = rows[0][0],
    1148                          encounters = rows[1][0],
    1149                          items = rows[2][0],
    1150                          documents = rows[3][0],
    1151                          results = rows[4][0],
    1152                          stays = rows[5][0],
    1153                          procedures = rows[6][0],
    1154                          active_drugs = rows[7][0],
    1155                          vaccinations = rows[8][0]
    1156                  )
    1157
    1158                  return stats
    1159          #--------------------------------------------------------
    1160 -        def format_statistics(self):
    1161                  return _(
    1162                          'Medical problems: %(problems)s\n'
    1163                          'Total encounters: %(encounters)s\n'
    1164                          'Total EMR entries: %(items)s\n'
    1165                          'Active medications: %(active_drugs)s\n'
    1166                          'Documents: %(documents)s\n'
    1167                          'Test results: %(results)s\n'
    1168                          'Hospitalizations: %(stays)s\n'
    1169                          'Procedures: %(procedures)s\n'
    1170                          'Vaccinations: %(vaccinations)s'
    1171                  ) % self.get_statistics()
    1172          #--------------------------------------------------------
    1173 -        def format_summary(self):
    1174
    1175                  cmd = "SELECT dob FROM dem.v_all_persons WHERE pk_identity = %(pk)s"
    1176                  args = {'pk': self.pk_patient}
    1177                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
    1178                  dob = rows[0]['dob']
    1179
    1180                  stats = self.get_statistics()
    1181                  first = self.get_first_encounter()
    1182                  last = self.get_last_encounter()
    1183                  probs = self.get_problems()
    1184
    1185                  txt = ''
    1186                  if len(probs) > 0:
    1187                          txt += _(' %s known problems, clinically relevant thereof:\n') % stats['problems']
    1188                  else:
    1189                          txt += _(' %s known problems\n') % stats['problems']
    1190                  for prob in probs:
    1191                          if not prob['clinically_relevant']:
    1192                                  continue
    1193                          txt += '   \u00BB%s\u00AB (%s)\n' % (
    1194                                  prob['problem'],
    1195                                  gmTools.bool2subst(prob['problem_active'], _('active'), _('inactive'))
    1196                          )
    1197                  txt += '\n'
    1198                  txt += _(' %s encounters from %s to %s\n') % (
    1199                          stats['encounters'],
    1200                          gmDateTime.pydt_strftime(first['started'], '%Y %b %d'),
    1201                          gmDateTime.pydt_strftime(last['started'], '%Y %b %d')
    1202                  )
    1203                  txt += _(' %s active medications\n') % stats['active_drugs']
    1204                  txt += _(' %s documents\n') % stats['documents']
    1205                  txt += _(' %s test results\n') % stats['results']
    1206                  txt += _(' %s hospitalizations') % stats['stays']
    1207                  if stats['stays'] == 0:
    1208                          txt += '\n'
    1209                  else:
    1210                          txt += _(', most recently:\n%s\n') % self.get_latest_hospital_stay().format(left_margin = 3)
    1211                  # FIXME: perhaps only count "ongoing ones"
    1212                  txt += _(' %s performed procedures') % stats['procedures']
    1213                  if stats['procedures'] == 0:
    1214                          txt += '\n'
    1215                  else:
    1216                          txt += _(', most recently:\n%s\n') % self.get_latest_performed_procedure().format(left_margin = 3)
    1217
    1218                  txt += '\n'
    1219                  txt += _('Allergies and Intolerances\n')
    1220
    1221                  allg_state = self.allergy_state
    1222                  txt += (' ' + allg_state.state_string)
    1223                  if allg_state['last_confirmed'] is not None:
    1224                          txt += _(' (last confirmed %s)') % gmDateTime.pydt_strftime(allg_state['last_confirmed'], '%Y %b %d')
    1225                  txt += '\n'
    1226                  txt += gmTools.coalesce(allg_state['comment'], '', ' %s\n')
    1227                  for allg in self.get_allergies():
    1228                          txt += ' %s: %s\n' % (
    1229                                  allg['descriptor'],
    1230                                  gmTools.coalesce(allg['reaction'], _('unknown reaction'))
    1231                          )
    1232
    1233                  meds = self.get_current_medications(order_by = 'intake_is_approved_of DESC, substance')
    1234                  if len(meds) > 0:
    1235                          txt += '\n'
    1236                          txt += _('Medications and Substances')
    1237                          txt += '\n'
    1238                  for m in meds:
    1239                          txt += '%s\n' % m.format_as_single_line(left_margin = 1)
    1240
    1241                  fhx = self.get_family_history()
    1242                  if len(fhx) > 0:
    1243                          txt += '\n'
    1244                          txt += _('Family History')
    1245                          txt += '\n'
    1246                  for f in fhx:
    1247                          txt += '%s\n' % f.format(left_margin = 1)
    1248
    1249                  jobs = get_occupations(pk_identity = self.pk_patient)
    1250                  if len(jobs) > 0:
    1251                          txt += '\n'
    1252                          txt += _('Occupations')
    1253                          txt += '\n'
    1254                  for job in jobs:
    1255                          txt += ' %s%s\n' % (
    1256                                  job['l10n_occupation'],
    1257                                  gmTools.coalesce(job['activities'], '', ': %s')
    1258                          )
    1259
    1260                  vaccs = self.get_latest_vaccinations()
    1261                  if len(vaccs) > 0:
    1262                          txt += '\n'
    1263                          txt += _('Vaccinations')
    1264                          txt += '\n'
    1265                  inds = sorted(vaccs.keys())
    1266                  for ind in inds:
    1267                          ind_count, vacc = vaccs[ind]
    1268                          if dob is None:
    1269                                  age_given = ''
    1270                          else:
    1271                                  age_given = ' @ %s' % gmDateTime.format_apparent_age_medically(gmDateTime.calculate_apparent_age (
    1272                                          start = dob,
    1273                                          end = vacc['date_given']
    1274                                  ))
    1275                          since = _('%s ago') % gmDateTime.format_interval_medically(vacc['interval_since_given'])
    1276                          txt += ' %s (%s%s): %s%s (%s %s%s%s)\n' % (
    1277                                  ind,
    1278                                  gmTools.u_sum,
    1279                                  ind_count,
    1280                                  #gmDateTime.pydt_strftime(vacc['date_given'], '%b %Y'),
    1281                                  since,
    1282                                  age_given,
    1283                                  vacc['vaccine'],
    1284                                  gmTools.u_left_double_angle_quote,
    1285                                  vacc['batch_no'],
    1286                                  gmTools.u_right_double_angle_quote
    1287                          )
    1288
    1289                  care = self.get_external_care_items(order_by = 'issue, organization, unit, provider', exclude_inactive = True)
    1290                  if len(care) > 0:
    1291                          txt += '\n'
    1292                          txt += _('External care')
    1293                          txt += '\n'
    1294                  for item in care:
    1295                          txt += ' %s: %s\n' % (
    1296                                  item['issue'],
    1297                                  gmTools.coalesce (
    1298                                          item['provider'],
    1299                                          '%s@%s' % (item['unit'], item['organization']),
    1300                                          '%%s (%s@%s)' % (item['unit'], item['organization'])
    1301                                  )
    1302                          )
    1303
    1304                  return txt
    1305
    1306          #--------------------------------------------------------
    1307 -        def format_as_journal(self, left_margin=0, patient=None):
    1308                  txt = ''
    1309                  for enc in self.get_encounters(skip_empty = True):
    1310                          txt += gmTools.u_box_horiz_4dashes * 70 + '\n'
    1311                          txt += enc.format (
    1312                                  episodes = None,                        # means: each touched upon
    1313                                  left_margin = left_margin,
    1314                                  patient = patient,
    1315                                  fancy_header = False,
    1316                                  with_soap = True,
    1317                                  with_docs = True,
    1318                                  with_tests = True,
    1319                                  with_vaccinations = True,
    1320                                  with_co_encountlet_hints = False,               # irrelevant
    1321                                  with_rfe_aoe = True,
    1322                                  with_family_history = True,
    1323                                  by_episode = True
    1324                          )
    1325
    1326                  return txt
    1327
    1328          #--------------------------------------------------------
    1329 -        def get_as_journal(self, since=None, until=None, encounters=None, episodes=None, issues=None, soap_cats=None, providers=None, order_by=None, time_range=None):
    1330                  return gmClinNarrative.get_as_journal (
    1331                          patient = self.pk_patient,
    1332                          since = since,
    1333                          until = until,
    1334                          encounters = encounters,
    1335                          episodes = episodes,
    1336                          issues = issues,
    1337                          soap_cats = soap_cats,
    1338                          providers = providers,
    1339                          order_by = order_by,
    1340                          time_range = time_range,
    1341                          active_encounter = self.active_encounter
    1342                  )
    1343
    1344          #------------------------------------------------------------------
    1345 -        def get_generic_emr_items(self, pk_encounters=None, pk_episodes=None, pk_health_issues=None, use_active_encounter=False, order_by=None):
    1346                  if use_active_encounter:
    1347                          active_encounter = self.active_encounter
    1348                  else:
    1349                          active_encounter = None
    1350                  return gmGenericEMRItem.get_generic_emr_items (
    1351                          patient = self.pk_patient,
    1352                          encounters = pk_encounters,
    1353                          episodes = pk_episodes,
    1354                          issues = pk_health_issues,
    1355                          active_encounter = active_encounter,
    1356                          order_by = order_by
    1357                  )
    1358
    1359          #--------------------------------------------------------
    1360          # API: allergy
    1361          #--------------------------------------------------------
    1362 -        def get_allergies(self, remove_sensitivities=False, since=None, until=None, encounters=None, episodes=None, issues=None, ID_list=None):
    1363                  """Retrieves patient allergy items.
    1364
    1365                          remove_sensitivities
    1366                                  - retrieve real allergies only, without sensitivities
    1367                          since
    1368                                  - initial date for allergy items
    1369                          until
    1370                                  - final date for allergy items
    1371                          encounters
    1372                                  - list of encounters whose allergies are to be retrieved
    1373                          episodes
    1374                                  - list of episodes whose allergies are to be retrieved
    1375                          issues
    1376                                  - list of health issues whose allergies are to be retrieved
    1377          """
    1378                  cmd = "SELECT * FROM clin.v_pat_allergies WHERE pk_patient=%s order by descriptor"
    1379                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient]}], get_col_idx = True)
    1380                  filtered_allergies = []
    1381                  for r in rows:
    1382                          filtered_allergies.append(gmAllergy.cAllergy(row = {'data': r, 'idx': idx, 'pk_field': 'pk_allergy'}))
    1383
    1384                  # ok, let's constrain our list
    1385                  if ID_list is not None:
    1386                          filtered_allergies = [ allg for allg in filtered_allergies if allg['pk_allergy'] in ID_list ]
    1387                          if len(filtered_allergies) == 0:
    1388                                  _log.error('no allergies of list [%s] found for patient [%s]' % (str(ID_list), self.pk_patient))
    1389                                  # better fail here contrary to what we do elsewhere
    1390                                  return None
    1391                          else:
    1392                                  return filtered_allergies
    1393
    1394                  if remove_sensitivities:
    1395                          filtered_allergies = [ allg for allg in filtered_allergies if allg['type'] == 'allergy' ]
    1396                  if since is not None:
    1397                          filtered_allergies = [ allg for allg in filtered_allergies if allg['date'] >= since ]
    1398                  if until is not None:
    1399                          filtered_allergies = [ allg for allg in filtered_allergies if allg['date'] < until ]
    1400                  if issues is not None:
    1401                          filtered_allergies = [ allg for allg in filtered_allergies if allg['pk_health_issue'] in issues ]
    1402                  if episodes is not None:
    1403                          filtered_allergies = [ allg for allg in filtered_allergies if allg['pk_episode'] in episodes ]
    1404                  if encounters is not None:
    1405                          filtered_allergies = [ allg for allg in filtered_allergies if allg['pk_encounter'] in encounters ]
    1406
    1407                  return filtered_allergies
    1408          #--------------------------------------------------------
    1409 -        def add_allergy(self, allergene=None, allg_type=None, encounter_id=None, episode_id=None):
    1410                  if encounter_id is None:
    1411                          encounter_id = self.current_encounter['pk_encounter']
    1412
    1413                  if episode_id is None:
    1414                          issue = self.add_health_issue(issue_name = _('Allergies/Intolerances'))
    1415                          epi = self.add_episode(episode_name = _('Allergy detail: %s') % allergene, pk_health_issue = issue['pk_health_issue'])
    1416                          episode_id = epi['pk_episode']
    1417
    1418                  new_allergy = gmAllergy.create_allergy (
    1419                          allergene = allergene,
    1420                          allg_type = allg_type,
    1421                          encounter_id = encounter_id,
    1422                          episode_id = episode_id
    1423                  )
    1424
    1425                  return new_allergy
    1426          #--------------------------------------------------------
    1427 -        def delete_allergy(self, pk_allergy=None):
    1428                  cmd = 'delete FROM clin.allergy WHERE pk=%(pk_allg)s'
    1429                  args = {'pk_allg': pk_allergy}
    1430                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    1431
    1432          #--------------------------------------------------------
    1433 -        def is_allergic_to(self, atcs=None, inns=None, product_name=None):
    1434                  """Cave: only use with one potential allergic agent
    1435                  otherwise you won't know which of the agents the allergy is to."""
    1436
    1437                  # we don't know the state
    1438                  if self.allergy_state is None:
    1439                          return None
    1440
    1441                  # we know there's no allergies
    1442                  if self.allergy_state == 0:
    1443                          return False
    1444
    1445                  args = {
    1446                          'atcs': atcs,
    1447                          'inns': inns,
    1448                          'prod_name': product_name,
    1449                          'pat': self.pk_patient
    1450                  }
    1451                  allergenes = []
    1452                  where_parts = []
    1453
    1454                  if len(atcs) == 0:
    1455                          atcs = None
    1456                  if atcs is not None:
    1457                          where_parts.append('atc_code in %(atcs)s')
    1458                  if len(inns) == 0:
    1459                          inns = None
    1460                  if inns is not None:
    1461                          where_parts.append('generics in %(inns)s')
    1462                          allergenes.extend(inns)
    1463                  if product_name is not None:
    1464                          where_parts.append('substance = %(prod_name)s')
    1465                          allergenes.append(product_name)
    1466
    1467                  if len(allergenes) != 0:
    1468                          where_parts.append('allergene in %(allgs)s')
    1469                          args['allgs'] = tuple(allergenes)
    1470
    1471                  cmd = """
    1472  SELECT * FROM clin.v_pat_allergies
    1473  WHERE
    1474          pk_patient = %%(pat)s
    1475          AND ( %s )""" % ' OR '.join(where_parts)
    1476
    1477                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1478
    1479                  if len(rows) == 0:
    1480                          return False
    1481
    1482                  return gmAllergy.cAllergy(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_allergy'})
    1483          #--------------------------------------------------------
    1484 -        def _set_allergy_state(self, state):
    1485
    1486                  if state not in gmAllergy.allergy_states:
    1487                          raise ValueError('[%s].__set_allergy_state(): <state> must be one of %s' % (self.__class__.__name__, gmAllergy.allergy_states))
    1488
    1489                  allg_state = gmAllergy.ensure_has_allergy_state(encounter = self.current_encounter['pk_encounter'])
    1490                  allg_state['has_allergy'] = state
    1491                  allg_state.save_payload()
    1492                  return True
    1493
    1494 -        def _get_allergy_state(self):
    1495                  return gmAllergy.ensure_has_allergy_state(encounter = self.current_encounter['pk_encounter'])
    1496
    1497          allergy_state = property(_get_allergy_state, _set_allergy_state)
    1498          #--------------------------------------------------------
    1499          # API: external care
    1500          #--------------------------------------------------------
    1501 -        def get_external_care_items(self, order_by=None, exclude_inactive=False):
    1502                  return gmExternalCare.get_external_care_items (
    1503                          pk_identity = self.pk_patient,
    1504                          order_by = order_by,
    1505                          exclude_inactive = exclude_inactive
    1506                  )
    1507
    1508          external_care_items = property(get_external_care_items, lambda x:x)
    1509
    1510          #--------------------------------------------------------
    1511          # API: episodes
    1512          #--------------------------------------------------------
    1513 -        def get_episodes(self, id_list=None, issues=None, open_status=None, order_by=None, unlinked_only=False):
    1514                  """Fetches from backend patient episodes.
    1515
    1516                  id_list - Episodes' PKs list
    1517                  issues - Health issues' PKs list to filter episodes by
    1518                  open_status - return all (None) episodes, only open (True) or closed (False) one(s)
    1519                  """
    1520                  if (unlinked_only is True) and (issues is not None):
    1521                          raise ValueError('<unlinked_only> cannot be TRUE if <issues> is not None')
    1522
    1523                  if order_by is None:
    1524                          order_by = ''
    1525                  else:
    1526                          order_by = 'ORDER BY %s' % order_by
    1527
    1528                  args = {
    1529                          'pat': self.pk_patient,
    1530                          'open': open_status
    1531                  }
    1532                  where_parts = ['pk_patient = %(pat)s']
    1533
    1534                  if open_status is not None:
    1535                          where_parts.append('episode_open IS %(open)s')
    1536
    1537                  if unlinked_only:
    1538                          where_parts.append('pk_health_issue is NULL')
    1539
    1540                  if issues is not None:
    1541                          where_parts.append('pk_health_issue IN %(issues)s')
    1542                          args['issues'] = tuple(issues)
    1543
    1544                  if id_list is not None:
    1545                          where_parts.append('pk_episode IN %(epis)s')
    1546                          args['epis'] = tuple(id_list)
    1547
    1548                  cmd = "SELECT * FROM clin.v_pat_episodes WHERE %s %s" % (
    1549                          ' AND '.join(where_parts),
    1550                          order_by
    1551                  )
    1552                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1553
    1554                  return [ gmEMRStructItems.cEpisode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_episode'}) for r in rows ]
    1555
    1556          episodes = property(get_episodes, lambda x:x)
    1557          #------------------------------------------------------------------
    1558 -        def get_unlinked_episodes(self, open_status=None, order_by=None):
    1559                  return self.get_episodes(open_status = open_status, order_by = order_by, unlinked_only = True)
    1560
    1561          unlinked_episodes = property(get_unlinked_episodes, lambda x:x)
    1562          #------------------------------------------------------------------
    1563 -        def get_episodes_by_encounter(self, pk_encounter=None):
    1564                  cmd = """SELECT distinct pk_episode
    1565                                          from clin.v_pat_items
    1566                                          WHERE pk_encounter=%(enc)s and pk_patient=%(pat)s"""
    1567                  args = {
    1568                          'enc': gmTools.coalesce(pk_encounter, self.current_encounter['pk_encounter']),
    1569                          'pat': self.pk_patient
    1570                  }
    1571                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
    1572                  if len(rows) == 0:
    1573                          return []
    1574                  epis = []
    1575                  for row in rows:
    1576                          epis.append(row[0])
    1577                  return self.get_episodes(id_list=epis)
    1578          #------------------------------------------------------------------
    1579 -        def add_episode(self, episode_name=None, pk_health_issue=None, is_open=False, allow_dupes=False, link_obj=None):
    1580                  """Add episode 'episode_name' for a patient's health issue.
    1581
    1582                  - silently returns if episode already exists
    1583                  """
    1584                  episode = gmEMRStructItems.create_episode (
    1585                          link_obj = link_obj,
    1586                          pk_health_issue = pk_health_issue,
    1587                          episode_name = episode_name,
    1588                          is_open = is_open,
    1589                          encounter = self.current_encounter['pk_encounter'],
    1590                          allow_dupes = allow_dupes
    1591                  )
    1592                  return episode
    1593          #--------------------------------------------------------
    1594 -        def get_most_recent_episode(self, issue=None):
    1595                  # try to find the episode with the most recently modified clinical item
    1596
    1597                  issue_where = gmTools.coalesce(issue, '', 'and pk_health_issue = %(issue)s')
    1598
    1599                  cmd = """
    1600  SELECT pk
    1601  from clin.episode
    1602  WHERE pk = (
    1603          SELECT distinct on(pk_episode) pk_episode
    1604          from clin.v_pat_items
    1605          WHERE
    1606                  pk_patient = %%(pat)s
    1607                          and
    1608                  modified_when = (
    1609                          SELECT max(vpi.modified_when)
    1610                          from clin.v_pat_items vpi
    1611                          WHERE vpi.pk_patient = %%(pat)s
    1612                  )
    1613                  %s
    1614          -- guard against several episodes created at the same moment of time
    1615          limit 1
    1616          )""" % issue_where
    1617                  rows, idx = gmPG2.run_ro_queries(queries = [
    1618                          {'cmd': cmd, 'args': {'pat': self.pk_patient, 'issue': issue}}
    1619                  ])
    1620                  if len(rows) != 0:
    1621                          return gmEMRStructItems.cEpisode(aPK_obj=rows[0][0])
    1622
    1623                  # no clinical items recorded, so try to find
    1624                  # the youngest episode for this patient
    1625                  cmd = """
    1626  SELECT vpe0.pk_episode
    1627  from
    1628          clin.v_pat_episodes vpe0
    1629  WHERE
    1630          vpe0.pk_patient = %%(pat)s
    1631                  and
    1632          vpe0.episode_modified_when = (
    1633                  SELECT max(vpe1.episode_modified_when)
    1634                  from clin.v_pat_episodes vpe1
    1635                  WHERE vpe1.pk_episode = vpe0.pk_episode
    1636          )
    1637          %s""" % issue_where
    1638                  rows, idx = gmPG2.run_ro_queries(queries = [
    1639                          {'cmd': cmd, 'args': {'pat': self.pk_patient, 'issue': issue}}
    1640                  ])
    1641                  if len(rows) != 0:
    1642                          return gmEMRStructItems.cEpisode(aPK_obj=rows[0][0])
    1643
    1644                  return None
    1645          #--------------------------------------------------------
    1646 -        def episode2problem(self, episode=None):
    1647                  return gmEMRStructItems.episode2problem(episode=episode)
    1648          #--------------------------------------------------------
    1649          # API: problems
    1650          #--------------------------------------------------------
    1651 -        def get_problems(self, episodes=None, issues=None, include_closed_episodes=False, include_irrelevant_issues=False):
    1652                  """Retrieve a patient's problems.
    1653
    1654                  "Problems" are the UNION of:
    1655
    1656                          - issues which are .clinically_relevant
    1657                          - episodes which are .is_open
    1658
    1659                  Therefore, both an issue and the open episode
    1660                  thereof can each be listed as a problem.
    1661
    1662                  include_closed_episodes/include_irrelevant_issues will
    1663                  include those -- which departs from the definition of
    1664                  the problem list being "active" items only ...
    1665
    1666                  episodes - episodes' PKs to filter problems by
    1667                  issues - health issues' PKs to filter problems by
    1668                  """
    1669                  # FIXME: this could use a good measure of streamlining, probably
    1670
    1671                  args = {'pat': self.pk_patient}
    1672
    1673                  cmd = """SELECT pk_health_issue, pk_episode FROM clin.v_problem_list WHERE pk_patient = %(pat)s ORDER BY problem"""
    1674                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
    1675
    1676                  # Instantiate problem items
    1677                  problems = []
    1678                  for row in rows:
    1679                          pk_args = {
    1680                                  'pk_patient': self.pk_patient,
    1681                                  'pk_health_issue': row['pk_health_issue'],
    1682                                  'pk_episode': row['pk_episode']
    1683                          }
    1684                          problems.append(gmEMRStructItems.cProblem(aPK_obj = pk_args, try_potential_problems = False))
    1685
    1686                  # include non-problems ?
    1687                  other_rows = []
    1688                  if include_closed_episodes:
    1689                          cmd = """SELECT pk_health_issue, pk_episode FROM clin.v_potential_problem_list WHERE pk_patient = %(pat)s and type = 'episode'"""
    1690                          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
    1691                          other_rows.extend(rows)
    1692
    1693                  if include_irrelevant_issues:
    1694                          cmd = """SELECT pk_health_issue, pk_episode FROM clin.v_potential_problem_list WHERE pk_patient = %(pat)s and type = 'health issue'"""
    1695                          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
    1696                          other_rows.extend(rows)
    1697
    1698                  if len(other_rows) > 0:
    1699                          for row in other_rows:
    1700                                  pk_args = {
    1701                                          'pk_patient': self.pk_patient,
    1702                                          'pk_health_issue': row['pk_health_issue'],
    1703                                          'pk_episode': row['pk_episode']
    1704                                  }
    1705                                  problems.append(gmEMRStructItems.cProblem(aPK_obj = pk_args, try_potential_problems = True))
    1706
    1707                  # filter
    1708                  if issues is not None:
    1709                          problems = [ p for p in problems if p['pk_health_issue'] in issues ]
    1710                  if episodes is not None:
    1711                          problems = [ p for p in problems if p['pk_episode'] in episodes ]
    1712
    1713                  return problems
    1714
    1715          #--------------------------------------------------------
    1716 -        def problem2episode(self, problem=None):
    1717                  return gmEMRStructItems.problem2episode(problem = problem)
    1718
    1719          #--------------------------------------------------------
    1720 -        def problem2issue(self, problem=None):
    1721                  return gmEMRStructItems.problem2issue(problem = problem)
    1722
    1723          #--------------------------------------------------------
    1724 -        def reclass_problem(self, problem):
    1725                  return gmEMRStructItems.reclass_problem(problem = problem)
    1726
    1727          #--------------------------------------------------------
    1728 -        def get_candidate_diagnoses(self):
    1729                  cmd = "SELECT * FROM clin.v_candidate_diagnoses WHERE pk_patient = %(pat)s"
    1730                  rows, idx = gmPG2.run_ro_queries (
    1731                          queries = [{'cmd': cmd, 'args': {'pat': self.pk_patient}}],
    1732                          get_col_idx = False
    1733                  )
    1734                  return rows
    1735
    1736          candidate_diagnoses = property(get_candidate_diagnoses)
    1737
    1738          #--------------------------------------------------------
    1739          # API: health issues
    1740          #--------------------------------------------------------
    1741 -        def get_health_issues(self, id_list = None):
    1742
    1743                  cmd = "SELECT *, xmin_health_issue FROM clin.v_health_issues WHERE pk_patient = %(pat)s ORDER BY description"
    1744                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pat': self.pk_patient}}], get_col_idx = True)
    1745                  issues = [ gmEMRStructItems.cHealthIssue(row = {'idx': idx, 'data': r, 'pk_field': 'pk_health_issue'}) for r in rows ]
    1746
    1747                  if id_list is None:
    1748                          return issues
    1749
    1750                  if len(id_list) == 0:
    1751                          raise ValueError('id_list to filter by is empty, most likely a programming error')
    1752
    1753                  filtered_issues = []
    1754                  for issue in issues:
    1755                          if issue['pk_health_issue'] in id_list:
    1756                                  filtered_issues.append(issue)
    1757
    1758                  return filtered_issues
    1759
    1760          health_issues = property(get_health_issues, lambda x:x)
    1761
    1762          #------------------------------------------------------------------
    1763 -        def add_health_issue(self, issue_name=None):
    1764                  """Adds patient health issue."""
    1765                  return gmEMRStructItems.create_health_issue (
    1766                          description = issue_name,
    1767                          encounter = self.current_encounter['pk_encounter'],
    1768                          patient = self.pk_patient
    1769                  )
    1770          #--------------------------------------------------------
    1771 -        def health_issue2problem(self, issue=None):
    1772                  return gmEMRStructItems.health_issue2problem(issue = issue)
    1773          #--------------------------------------------------------
    1774          # API: substance intake
    1775          #--------------------------------------------------------
    1776 -        def get_current_medications(self, include_inactive=True, include_unapproved=False, order_by=None, episodes=None, issues=None):
    1777                  return self._get_current_substance_intakes (
    1778                          include_inactive = include_inactive,
    1779                          include_unapproved = include_unapproved,
    1780                          order_by = order_by,
    1781                          episodes = episodes,
    1782                          issues = issues,
    1783                          exclude_medications = False,
    1784                          exclude_potential_abuses = True
    1785                  )
    1786
    1787          #--------------------------------------------------------
    1788 -        def _get_abused_substances(self, order_by=None):
    1789                  return self._get_current_substance_intakes (
    1790                          include_inactive = True,
    1791                          include_unapproved = True,
    1792                          order_by = order_by,
    1793                          episodes = None,
    1794                          issues = None,
    1795                          exclude_medications = True,
    1796                          exclude_potential_abuses = False
    1797                  )
    1798
    1799          abused_substances = property(_get_abused_substances, lambda x:x)
    1800
    1801          #--------------------------------------------------------
    1802 -        def _get_current_substance_intakes(self, include_inactive=True, include_unapproved=False, order_by=None, episodes=None, issues=None, exclude_potential_abuses=False, exclude_medications=False):
    1803
    1804                  where_parts = ['pk_patient = %(pat)s']
    1805                  args = {'pat': self.pk_patient}
    1806
    1807                  if not include_inactive:
    1808                          where_parts.append('is_currently_active IN (TRUE, NULL)')
    1809
    1810                  if not include_unapproved:
    1811                          where_parts.append('intake_is_approved_of IN (TRUE, NULL)')
    1812
    1813                  if exclude_potential_abuses:
    1814                          where_parts.append('harmful_use_type IS NULL')
    1815
    1816                  if exclude_medications:
    1817                          where_parts.append('harmful_use_type IS NOT NULL')
    1818
    1819                  if order_by is None:
    1820                          order_by = ''
    1821                  else:
    1822                          order_by = 'ORDER BY %s' % order_by
    1823
    1824                  cmd = "SELECT * FROM clin.v_substance_intakes WHERE %s %s" % (
    1825                          '\nAND '.join(where_parts),
    1826                          order_by
    1827                  )
    1828                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1829                  intakes = [ gmMedication.cSubstanceIntakeEntry(row = {'idx': idx, 'data': r, 'pk_field': 'pk_substance_intake'})  for r in rows ]
    1830
    1831                  if episodes is not None:
    1832                          intakes = [ i for i in intakes if i['pk_episode'] in episodes  ]
    1833
    1834                  if issues is not None:
    1835                          intakes = [ i for i in intakes if i ['pk_health_issue'] in issues ]
    1836
    1837                  return intakes
    1838
    1839          #--------------------------------------------------------
    1840 -        def add_substance_intake(self, pk_component=None, pk_episode=None, pk_drug_product=None, pk_health_issue=None):
    1841                  pk_enc = self.current_encounter['pk_encounter']
    1842                  if pk_episode is None:
    1843                          pk_episode = gmMedication.create_default_medication_history_episode (
    1844                                  pk_health_issue = pk_health_issue,
    1845                                  encounter = pk_enc
    1846                          )
    1847                  return gmMedication.create_substance_intake (
    1848                          pk_component = pk_component,
    1849                          pk_encounter = pk_enc,
    1850                          pk_episode = pk_episode,
    1851                          pk_drug_product = pk_drug_product
    1852                  )
    1853
    1854          #--------------------------------------------------------
    1855 -        def substance_intake_exists(self, pk_component=None, pk_substance=None, pk_drug_product=None):
    1856                  return gmMedication.substance_intake_exists (
    1857                          pk_component = pk_component,
    1858                          pk_substance = pk_substance,
    1859                          pk_identity = self.pk_patient,
    1860                          pk_drug_product = pk_drug_product
    1861                  )
    1862
    1863          #--------------------------------------------------------
    1864          # API: vaccinations
    1865          #--------------------------------------------------------
    1866 -        def add_vaccination(self, episode=None, vaccine=None, batch_no=None):
    1867                  return gmVaccination.create_vaccination (
    1868                          encounter = self.current_encounter['pk_encounter'],
    1869                          episode = episode,
    1870                          vaccine = vaccine,
    1871                          batch_no = batch_no
    1872                  )
    1873
    1874          #--------------------------------------------------------
    1875 -        def get_latest_vaccinations(self, episodes=None, issues=None, atc_indications=None):
    1876                  """Returns latest given vaccination for each vaccinated indication.
    1877
    1878                  as a dict {'l10n_indication': cVaccination instance}
    1879
    1880                  Note that this will produce duplicate vaccination instances on combi-indication vaccines !
    1881                  """
    1882                  args = {'pat': self.pk_patient}
    1883                  where_parts = ['c_v_shots.pk_patient = %(pat)s']
    1884
    1885                  if (episodes is not None) and (len(episodes) > 0):
    1886                          where_parts.append('c_v_shots.pk_episode IN %(epis)s')
    1887                          args['epis'] = tuple(episodes)
    1888
    1889                  if (issues is not None) and (len(issues) > 0):
    1890                          where_parts.append('c_v_shots.pk_episode IN (select pk from clin.episode where fk_health_issue IN %(issues)s)')
    1891                          args['issues'] = tuple(issues)
    1892
    1893                  if (atc_indications is not None) and (len(atc_indications) > 0):
    1894                          where_parts.append('c_v_plv4i.atc_indication IN %(atc_inds)s')
    1895                          args['atc_inds'] = tuple(atc_indications)
    1896
    1897                  # find the shots
    1898                  cmd = """
    1899                          SELECT
    1900                                  c_v_shots.*,
    1901                                  c_v_plv4i.l10n_indication,
    1902                                  c_v_plv4i.no_of_shots
    1903                          FROM
    1904                                  clin.v_vaccinations c_v_shots
    1905                                          JOIN clin.v_pat_last_vacc4indication c_v_plv4i ON (c_v_shots.pk_vaccination = c_v_plv4i.pk_vaccination)
    1906                          WHERE %s
    1907                  """ % '\nAND '.join(where_parts)
    1908                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1909
    1910                  # none found
    1911                  if len(rows) == 0:
    1912                          return {}
    1913
    1914                  # turn them into vaccinations
    1915                  # (idx is constant)
    1916                  vaccs = {}
    1917                  for shot_row in rows:
    1918                          vaccs[shot_row['l10n_indication']] = (
    1919                                  shot_row['no_of_shots'],
    1920                                  gmVaccination.cVaccination(row = {'idx': idx, 'data': shot_row, 'pk_field': 'pk_vaccination'})
    1921                          )
    1922
    1923                  return vaccs
    1924
    1925          #--------------------------------------------------------
    1926 -        def get_vaccinations(self, order_by=None, episodes=None, issues=None, encounters=None):
    1927                  return gmVaccination.get_vaccinations (
    1928                          pk_identity = self.pk_patient,
    1929                          pk_episodes = episodes,
    1930                          pk_health_issues = issues,
    1931                          pk_encounters = encounters,
    1932                          order_by = order_by,
    1933                          return_pks = False
    1934                  )
    1935
    1936          vaccinations = property(get_vaccinations, lambda x:x)
    1937
    1938          #--------------------------------------------------------
    1939          # old/obsolete:
    1940          #--------------------------------------------------------
    1941 -        def get_scheduled_vaccination_regimes(self, ID=None, indications=None):
    1942                  """Retrieves vaccination regimes the patient is on.
    1943
    1944                          optional:
    1945                          * ID - PK of the vaccination regime
    1946                          * indications - indications we want to retrieve vaccination
    1947                                  regimes for, must be primary language, not l10n_indication
    1948                  """
    1949                  # FIXME: use course, not regime
    1950                  # retrieve vaccination regimes definitions
    1951                  cmd = """SELECT distinct on(pk_course) pk_course
    1952                                   FROM clin.v_vaccs_scheduled4pat
    1953                                   WHERE pk_patient=%s"""
    1954                  rows = gmPG.run_ro_query('historica', cmd, None, self.pk_patient)
    1955                  if rows is None:
    1956                          _log.error('cannot retrieve scheduled vaccination courses')
    1957                          return None
    1958                  # Instantiate vaccination items and keep cache
    1959                  for row in rows:
    1960                          self.__db_cache['vaccinations']['scheduled regimes'].append(gmVaccination.cVaccinationCourse(aPK_obj=row[0]))
    1961
    1962                  # ok, let's constrain our list
    1963                  filtered_regimes = []
    1964                  filtered_regimes.extend(self.__db_cache['vaccinations']['scheduled regimes'])
    1965                  if ID is not None:
    1966                          filtered_regimes = [ r for r in filtered_regimes if r['pk_course'] == ID ]
    1967                          if len(filtered_regimes) == 0:
    1968                                  _log.error('no vaccination course [%s] found for patient [%s]' % (ID, self.pk_patient))
    1969                                  return []
    1970                          else:
    1971                                  return filtered_regimes[0]
    1972                  if indications is not None:
    1973                          filtered_regimes = [ r for r in filtered_regimes if r['indication'] in indications ]
    1974
    1975                  return filtered_regimes
    1976          #--------------------------------------------------------
    1977  #       def get_vaccinated_indications(self):
    1978  #               """Retrieves patient vaccinated indications list.
    1979  #
    1980  #               Note that this does NOT rely on the patient being on
    1981  #               some schedule or other but rather works with what the
    1982  #               patient has ACTUALLY been vaccinated against. This is
    1983  #               deliberate !
    1984  #               """
    1985  #               # most likely, vaccinations will be fetched close
    1986  #               # by so it makes sense to count on the cache being
    1987  #               # filled (or fill it for nearby use)
    1988  #               vaccinations = self.get_vaccinations()
    1989  #               if vaccinations is None:
    1990  #                       _log.error('cannot load vaccinated indications for patient [%s]' % self.pk_patient)
    1991  #                       return (False, [[_('ERROR: cannot retrieve vaccinated indications'), _('ERROR: cannot retrieve vaccinated indications')]])
    1992  #               if len(vaccinations) == 0:
    1993  #                       return (True, [[_('no vaccinations recorded'), _('no vaccinations recorded')]])
    1994  #               v_indications = []
    1995  #               for vacc in vaccinations:
    1996  #                       tmp = [vacc['indication'], vacc['l10n_indication']]
    1997  #                       # remove duplicates
    1998  #                       if tmp in v_indications:
    1999  #                               continue
    2000  #                       v_indications.append(tmp)
    2001  #               return (True, v_indications)
    2002          #--------------------------------------------------------
    2003 -        def get_vaccinations_old(self, ID=None, indications=None, since=None, until=None, encounters=None, episodes=None, issues=None):
    2004                  """Retrieves list of vaccinations the patient has received.
    2005
    2006                  optional:
    2007                  * ID - PK of a vaccination
    2008                  * indications - indications we want to retrieve vaccination
    2009                          items for, must be primary language, not l10n_indication
    2010          * since - initial date for allergy items
    2011          * until - final date for allergy items
    2012          * encounters - list of encounters whose allergies are to be retrieved
    2013          * episodes - list of episodes whose allergies are to be retrieved
    2014          * issues - list of health issues whose allergies are to be retrieved
    2015                  """
    2016                  try:
    2017                          self.__db_cache['vaccinations']['vaccinated']
    2018                  except KeyError:
    2019                          self.__db_cache['vaccinations']['vaccinated'] = []
    2020                          # Important fetch ordering by indication, date to know if a vaccination is booster
    2021                          cmd= """SELECT * FROM clin.v_pat_vaccinations4indication
    2022                                          WHERE pk_patient=%s
    2023                                          order by indication, date"""
    2024                          rows, idx  = gmPG.run_ro_query('historica', cmd, True, self.pk_patient)
    2025                          if rows is None:
    2026                                  _log.error('cannot load given vaccinations for patient [%s]' % self.pk_patient)
    2027                                  del self.__db_cache['vaccinations']['vaccinated']
    2028                                  return None
    2029                          # Instantiate vaccination items
    2030                          vaccs_by_ind = {}
    2031                          for row in rows:
    2032                                  vacc_row = {
    2033                                          'pk_field': 'pk_vaccination',
    2034                                          'idx': idx,
    2035                                          'data': row
    2036                                  }
    2037                                  vacc = gmVaccination.cVaccination(row=vacc_row)
    2038                                  self.__db_cache['vaccinations']['vaccinated'].append(vacc)
    2039                                  # keep them, ordered by indication
    2040                                  try:
    2041                                          vaccs_by_ind[vacc['indication']].append(vacc)
    2042                                  except KeyError:
    2043                                          vaccs_by_ind[vacc['indication']] = [vacc]
    2044
    2045                          # calculate sequence number and is_booster
    2046                          for ind in vaccs_by_ind.keys():
    2047                                  vacc_regimes = self.get_scheduled_vaccination_regimes(indications = [ind])
    2048                                  for vacc in vaccs_by_ind[ind]:
    2049                                          # due to the "order by indication, date" the vaccinations are in the
    2050                                          # right temporal order inside the indication-keyed dicts
    2051                                          seq_no = vaccs_by_ind[ind].index(vacc) + 1
    2052                                          vacc['seq_no'] = seq_no
    2053                                          # if no active schedule for indication we cannot
    2054                                          # check for booster status (eg. seq_no > max_shot)
    2055                                          if (vacc_regimes is None) or (len(vacc_regimes) == 0):
    2056                                                  continue
    2057                                          if seq_no > vacc_regimes[0]['shots']:
    2058                                                  vacc['is_booster'] = True
    2059                          del vaccs_by_ind
    2060
    2061                  # ok, let's constrain our list
    2062                  filtered_shots = []
    2063                  filtered_shots.extend(self.__db_cache['vaccinations']['vaccinated'])
    2064                  if ID is not None:
    2065                          filtered_shots = filter(lambda shot: shot['pk_vaccination'] == ID, filtered_shots)
    2066                          if len(filtered_shots) == 0:
    2067                                  _log.error('no vaccination [%s] found for patient [%s]' % (ID, self.pk_patient))
    2068                                  return None
    2069                          else:
    2070                                  return filtered_shots[0]
    2071                  if since is not None:
    2072                          filtered_shots = filter(lambda shot: shot['date'] >= since, filtered_shots)
    2073                  if until is not None:
    2074                          filtered_shots = filter(lambda shot: shot['date'] < until, filtered_shots)
    2075                  if issues is not None:
    2076                          filtered_shots = filter(lambda shot: shot['pk_health_issue'] in issues, filtered_shots)
    2077                  if episodes is not None:
    2078                          filtered_shots = filter(lambda shot: shot['pk_episode'] in episodes, filtered_shots)
    2079                  if encounters is not None:
    2080                          filtered_shots = filter(lambda shot: shot['pk_encounter'] in encounters, filtered_shots)
    2081                  if indications is not None:
    2082                          filtered_shots = filter(lambda shot: shot['indication'] in indications, filtered_shots)
    2083                  return filtered_shots
    2084          #--------------------------------------------------------
    2085 -        def get_scheduled_vaccinations(self, indications=None):
    2086                  """Retrieves vaccinations scheduled for a regime a patient is on.
    2087
    2088                  The regime is referenced by its indication (not l10n)
    2089
    2090                  * indications - List of indications (not l10n) of regimes we want scheduled
    2091                                  vaccinations to be fetched for
    2092                  """
    2093                  try:
    2094                          self.__db_cache['vaccinations']['scheduled']
    2095                  except KeyError:
    2096                          self.__db_cache['vaccinations']['scheduled'] = []
    2097                          cmd = """SELECT * FROM clin.v_vaccs_scheduled4pat WHERE pk_patient=%s"""
    2098                          rows, idx = gmPG.run_ro_query('historica', cmd, True, self.pk_patient)
    2099                          if rows is None:
    2100                                  _log.error('cannot load scheduled vaccinations for patient [%s]' % self.pk_patient)
    2101                                  del self.__db_cache['vaccinations']['scheduled']
    2102                                  return None
    2103                          # Instantiate vaccination items
    2104                          for row in rows:
    2105                                  vacc_row = {
    2106                                          'pk_field': 'pk_vacc_def',
    2107                                          'idx': idx,
    2108                                          'data': row
    2109                                  }
    2110                                  self.__db_cache['vaccinations']['scheduled'].append(gmVaccination.cScheduledVaccination(row = vacc_row))
    2111
    2112                  # ok, let's constrain our list
    2113                  if indications is None:
    2114                          return self.__db_cache['vaccinations']['scheduled']
    2115                  filtered_shots = []
    2116                  filtered_shots.extend(self.__db_cache['vaccinations']['scheduled'])
    2117                  filtered_shots = filter(lambda shot: shot['indication'] in indications, filtered_shots)
    2118                  return filtered_shots
    2119          #--------------------------------------------------------
    2120 -        def get_missing_vaccinations(self, indications=None):
    2121                  try:
    2122                          self.__db_cache['vaccinations']['missing']
    2123                  except KeyError:
    2124                          self.__db_cache['vaccinations']['missing'] = {}
    2125                          # 1) non-booster
    2126                          self.__db_cache['vaccinations']['missing']['due'] = []
    2127                          # get list of (indication, seq_no) tuples
    2128                          cmd = "SELECT indication, seq_no FROM clin.v_pat_missing_vaccs WHERE pk_patient=%s"
    2129                          rows = gmPG.run_ro_query('historica', cmd, None, self.pk_patient)
    2130                          if rows is None:
    2131                                  _log.error('error loading (indication, seq_no) for due/overdue vaccinations for patient [%s]' % self.pk_patient)
    2132                                  return None
    2133                          pk_args = {'pat_id': self.pk_patient}
    2134                          if rows is not None:
    2135                                  for row in rows:
    2136                                          pk_args['indication'] = row[0]
    2137                                          pk_args['seq_no'] = row[1]
    2138                                          self.__db_cache['vaccinations']['missing']['due'].append(gmVaccination.cMissingVaccination(aPK_obj=pk_args))
    2139
    2140                          # 2) boosters
    2141                          self.__db_cache['vaccinations']['missing']['boosters'] = []
    2142                          # get list of indications
    2143                          cmd = "SELECT indication, seq_no FROM clin.v_pat_missing_boosters WHERE pk_patient=%s"
    2144                          rows = gmPG.run_ro_query('historica', cmd, None, self.pk_patient)
    2145                          if rows is None:
    2146                                  _log.error('error loading indications for missing boosters for patient [%s]' % self.pk_patient)
    2147                                  return None
    2148                          pk_args = {'pat_id': self.pk_patient}
    2149                          if rows is not None:
    2150                                  for row in rows:
    2151                                          pk_args['indication'] = row[0]
    2152                                          self.__db_cache['vaccinations']['missing']['boosters'].append(gmVaccination.cMissingBooster(aPK_obj=pk_args))
    2153
    2154                  # if any filters ...
    2155                  if indications is None:
    2156                          return self.__db_cache['vaccinations']['missing']
    2157                  if len(indications) == 0:
    2158                          return self.__db_cache['vaccinations']['missing']
    2159                  # ... apply them
    2160                  filtered_shots = {
    2161                          'due': [],
    2162                          'boosters': []
    2163                  }
    2164                  for due_shot in self.__db_cache['vaccinations']['missing']['due']:
    2165                          if due_shot['indication'] in indications: #and due_shot not in filtered_shots['due']:
    2166                                  filtered_shots['due'].append(due_shot)
    2167                  for due_shot in self.__db_cache['vaccinations']['missing']['boosters']:
    2168                          if due_shot['indication'] in indications: #and due_shot not in filtered_shots['boosters']:
    2169                                  filtered_shots['boosters'].append(due_shot)
    2170                  return filtered_shots
    2171
    2172          #------------------------------------------------------------------
    2173          # API: encounters
    2174          #------------------------------------------------------------------
    2175 -        def _get_current_encounter(self):
    2176                  return self.__encounter
    2177
    2178 -        def _set_current_encounter(self, encounter):
    2179                  # first ever setting ? -> fast path
    2180                  if self.__encounter is None:
    2181                          _log.debug('first setting of active encounter in this clinical record instance')
    2182                          encounter.lock(exclusive = False)               # lock new
    2183                          self.__encounter = encounter
    2184                          gmDispatcher.send('current_encounter_switched')
    2185                          return True
    2186
    2187                  # real switch -> slow path
    2188                  _log.debug('switching of active encounter')
    2189                  # fail if the currently active encounter has unsaved changes
    2190                  if self.__encounter.is_modified():
    2191                          gmTools.compare_dict_likes(self.__encounter, encounter, 'modified enc in client', 'enc to switch to')
    2192                          _log.error('current in client: %s', self.__encounter)
    2193                          raise ValueError('unsaved changes in active encounter [%s], cannot switch to another one [%s]' % (
    2194                                  self.__encounter['pk_encounter'],
    2195                                  encounter['pk_encounter']
    2196                          ))
    2197
    2198                  prev_enc = self.__encounter
    2199                  encounter.lock(exclusive = False)               # lock new
    2200                  self.__encounter = encounter
    2201                  prev_enc.unlock(exclusive = False)              # unlock old
    2202                  gmDispatcher.send('current_encounter_switched')
    2203
    2204                  return True
    2205
    2206          current_encounter = property(_get_current_encounter, _set_current_encounter)
    2207          active_encounter = property(_get_current_encounter, _set_current_encounter)
    2208
    2209          #--------------------------------------------------------
    2210 -        def __setup_active_encounter(self):
    2211                  _log.debug('setting up active encounter for identity [%s]', self.pk_patient)
    2212
    2213                  # log access to patient record (HIPAA, for example)
    2214                  _delayed_execute(self.log_access, action = 'pulling chart for identity [%s]' % self.pk_patient)
    2215
    2216                  # cleanup (not async, because we don't want recent encounters
    2217                  # to become the active one just because they are recent)
    2218                  self.remove_empty_encounters()
    2219
    2220                  # activate very recent encounter if available
    2221                  if self.__activate_very_recent_encounter():
    2222                          return
    2223
    2224                  fairly_recent_enc = self.__get_fairly_recent_encounter()
    2225
    2226                  # create new encounter for the time being
    2227                  self.start_new_encounter()
    2228
    2229                  if fairly_recent_enc is None:
    2230                          return
    2231
    2232                  # but check whether user wants to continue a "fairly recent" one
    2233                  gmDispatcher.send (
    2234                          signal = 'ask_for_encounter_continuation',
    2235                          new_encounter = self.__encounter,
    2236                          fairly_recent_encounter = fairly_recent_enc
    2237                  )
    2238
    2239          #------------------------------------------------------------------
    2240 -        def __activate_very_recent_encounter(self):
    2241                  """Try to attach to a "very recent" encounter if there is one.
    2242
    2243                  returns:
    2244                          False: no "very recent" encounter
    2245                  True: success
    2246                  """
    2247                  cfg_db = gmCfg.cCfgSQL()
    2248                  min_ttl = cfg_db.get2 (
    2249                          option = 'encounter.minimum_ttl',
    2250                          workplace = _here.active_workplace,
    2251                          bias = 'user',
    2252                          default = '1 hour 30 minutes'
    2253                  )
    2254                  cmd = gmEMRStructItems.SQL_get_encounters % """pk_encounter = (
    2255                          SELECT pk_encounter
    2256                          FROM clin.v_most_recent_encounters
    2257                          WHERE
    2258                                  pk_patient = %s
    2259                                          and
    2260                                  last_affirmed > (now() - %s::interval)
    2261                          ORDER BY
    2262                                  last_affirmed DESC
    2263                          LIMIT 1
    2264                  )"""
    2265                  enc_rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient, min_ttl]}], get_col_idx = True)
    2266
    2267                  # none found
    2268                  if len(enc_rows) == 0:
    2269                          _log.debug('no <very recent> encounter (younger than [%s]) found' % min_ttl)
    2270                          return False
    2271
    2272                  _log.debug('"very recent" encounter [%s] found and re-activated' % enc_rows[0]['pk_encounter'])
    2273
    2274                  # attach to existing
    2275                  self.current_encounter = gmEMRStructItems.cEncounter(row = {'data': enc_rows[0], 'idx': idx, 'pk_field': 'pk_encounter'})
    2276                  return True
    2277
    2278          #------------------------------------------------------------------
    2279 -        def __get_fairly_recent_encounter(self):
    2280                  cfg_db = gmCfg.cCfgSQL()
    2281                  min_ttl = cfg_db.get2 (
    2282                          option = 'encounter.minimum_ttl',
    2283                          workplace = _here.active_workplace,
    2284                          bias = 'user',
    2285                          default = '1 hour 30 minutes'
    2286                  )
    2287                  max_ttl = cfg_db.get2 (
    2288                          option = 'encounter.maximum_ttl',
    2289                          workplace = _here.active_workplace,
    2290                          bias = 'user',
    2291                          default = '6 hours'
    2292                  )
    2293
    2294                  # do we happen to have a "fairly recent" candidate ?
    2295                  cmd = gmEMRStructItems.SQL_get_encounters % """pk_encounter = (
    2296                          SELECT pk_encounter
    2297                          FROM clin.v_most_recent_encounters
    2298                          WHERE
    2299                                  pk_patient=%s
    2300                                          AND
    2301                                  last_affirmed BETWEEN (now() - %s::interval) AND (now() - %s::interval)
    2302                          ORDER BY
    2303                                  last_affirmed DESC
    2304                          LIMIT 1
    2305                  )"""
    2306                  enc_rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient, max_ttl, min_ttl]}], get_col_idx = True)
    2307
    2308                  # none found
    2309                  if len(enc_rows) == 0:
    2310                          _log.debug('no <fairly recent> encounter (between [%s] and [%s] old) found' % (min_ttl, max_ttl))
    2311                          return None
    2312
    2313                  _log.debug('"fairly recent" encounter [%s] found', enc_rows[0]['pk_encounter'])
    2314                  return gmEMRStructItems.cEncounter(row = {'data': enc_rows[0], 'idx': idx, 'pk_field': 'pk_encounter'})
    2315
    2316  #       #------------------------------------------------------------------
    2317  #       def __check_for_fairly_recent_encounter(self):
    2318  #
    2319  #               cfg_db = gmCfg.cCfgSQL()
    2320  #               min_ttl = cfg_db.get2 (
    2321  #                       option = u'encounter.minimum_ttl',
    2322  #                       workplace = _here.active_workplace,
    2323  #                       bias = u'user',
    2324  #                       default = u'1 hour 30 minutes'
    2325  #               )
    2326  #               max_ttl = cfg_db.get2 (
    2327  #                       option = u'encounter.maximum_ttl',
    2328  #                       workplace = _here.active_workplace,
    2329  #                       bias = u'user',
    2330  #                       default = u'6 hours'
    2331  #               )
    2332  #
    2333  #               # do we happen to have a "fairly recent" candidate ?
    2334  #               cmd = gmEMRStructItems.SQL_get_encounters % u"""pk_encounter = (
    2335  #                       SELECT pk_encounter
    2336  #                       FROM clin.v_most_recent_encounters
    2337  #                       WHERE
    2338  #                               pk_patient=%s
    2339  #                                       AND
    2340  #                               last_affirmed BETWEEN (now() - %s::interval) AND (now() - %s::interval)
    2341  #                       ORDER BY
    2342  #                               last_affirmed DESC
    2343  #                       LIMIT 1
    2344  #               )"""
    2345  #               enc_rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient, max_ttl, min_ttl]}], get_col_idx = True)
    2346  #
    2347  #               # none found
    2348  #               if len(enc_rows) == 0:
    2349  #                       _log.debug('no <fairly recent> encounter (between [%s] and [%s] old) found' % (min_ttl, max_ttl))
    2350  #                       return
    2351  #
    2352  #               _log.debug('"fairly recent" encounter [%s] found', enc_rows[0]['pk_encounter'])
    2353  #               fairly_recent_enc = gmEMRStructItems.cEncounter(row = {'data': enc_rows[0], 'idx': idx, 'pk_field': 'pk_encounter'})
    2354  #               gmDispatcher.send(u'ask_for_encounter_continuation', current = self.__encounter, fairly_recent_encounter = fairly_recent_enc)
    2355
    2356  #       #------------------------------------------------------------------
    2357  #       def __activate_fairly_recent_encounter(self, allow_user_interaction=True):
    2358  #               """Try to attach to a "fairly recent" encounter if there is one.
    2359  #
    2360  #               returns:
    2361  #                       False: no "fairly recent" encounter, create new one
    2362  #               True: success
    2363  #               """
    2364  #               if _func_ask_user is None:
    2365  #                       _log.debug('cannot ask user for guidance, not looking for fairly recent encounter')
    2366  #                       return False
    2367  #
    2368  #               if not allow_user_interaction:
    2369  #                       _log.exception('user interaction not desired, not looking for fairly recent encounter')
    2370  #                       return False
    2371  #
    2372  #               cfg_db = gmCfg.cCfgSQL()
    2373  #               min_ttl = cfg_db.get2 (
    2374  #                       option = u'encounter.minimum_ttl',
    2375  #                       workplace = _here.active_workplace,
    2376  #                       bias = u'user',
    2377  #                       default = u'1 hour 30 minutes'
    2378  #               )
    2379  #               max_ttl = cfg_db.get2 (
    2380  #                       option = u'encounter.maximum_ttl',
    2381  #                       workplace = _here.active_workplace,
    2382  #                       bias = u'user',
    2383  #                       default = u'6 hours'
    2384  #               )
    2385  #               cmd = u"""
    2386  #                       SELECT pk_encounter
    2387  #                       FROM clin.v_most_recent_encounters
    2388  #                       WHERE
    2389  #                               pk_patient=%s
    2390  #                                       AND
    2391  #                               last_affirmed BETWEEN (now() - %s::interval) AND (now() - %s::interval)
    2392  #                       ORDER BY
    2393  #                               last_affirmed DESC"""
    2394  #               enc_rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient, max_ttl, min_ttl]}])
    2395  #               # none found
    2396  #               if len(enc_rows) == 0:
    2397  #                       _log.debug('no <fairly recent> encounter (between [%s] and [%s] old) found' % (min_ttl, max_ttl))
    2398  #                       return False
    2399  #
    2400  #               _log.debug('"fairly recent" encounter [%s] found', enc_rows[0][0])
    2401  #
    2402  #               encounter = gmEMRStructItems.cEncounter(aPK_obj=enc_rows[0][0])
    2403  #               # ask user whether to attach or not
    2404  #               cmd = u"""
    2405  #                       SELECT title, firstnames, lastnames, gender, dob
    2406  #                       FROM dem.v_all_persons WHERE pk_identity=%s"""
    2407  #               pats, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient]}])
    2408  #               pat = pats[0]
    2409  #               pat_str = u'%s %s %s (%s), %s  [#%s]' % (
    2410  #                       gmTools.coalesce(pat[0], u'')[:5],
    2411  #                       pat[1][:15],
    2412  #                       pat[2][:15],
    2413  #                       pat[3],
    2414  #                       gmDateTime.pydt_strftime(pat[4], '%Y %b %d'),
    2415  #                       self.pk_patient
    2416  #               )
    2417  #               msg = _(
    2418  #                       '%s\n'
    2419  #                       '\n'
    2420  #                       "This patient's chart was worked on only recently:\n"
    2421  #                       '\n'
    2422  #                       ' %s  %s - %s  (%s)\n'
    2423  #                       '\n'
    2424  #                       ' Reason for Encounter:\n'
    2425  #                       '  %s\n'
    2426  #                       ' Assessment of Encounter:\n'
    2427  #                       '  %s\n'
    2428  #                       '\n'
    2429  #                       'Do you want to continue that consultation\n'
    2430  #                       'or do you want to start a new one ?\n'
    2431  #               ) % (
    2432  #                       pat_str,
    2433  #                       gmDateTime.pydt_strftime(encounter['started'], '%Y %b %d'),
    2434  #                       gmDateTime.pydt_strftime(encounter['started'], '%H:%M'), gmDateTime.pydt_strftime(encounter['last_affirmed'], '%H:%M'),
    2435  #                       encounter['l10n_type'],
    2436  #                       gmTools.coalesce(encounter['reason_for_encounter'], _('none given')),
    2437  #                       gmTools.coalesce(encounter['assessment_of_encounter'], _('none given')),
    2438  #               )
    2439  #               attach = False
    2440  #               try:
    2441  #                       attach = _func_ask_user(msg = msg, caption = _('Starting patient encounter'), encounter = encounter)
    2442  #               except:
    2443  #                       _log.exception('cannot ask user for guidance, not attaching to existing encounter')
    2444  #                       return False
    2445  #               if not attach:
    2446  #                       return False
    2447  #
    2448  #               # attach to existing
    2449  #               self.current_encounter = encounter
    2450  #               _log.debug('"fairly recent" encounter re-activated')
    2451  #               return True
    2452
    2453          #------------------------------------------------------------------
    2454 -        def start_new_encounter(self):
    2455                  cfg_db = gmCfg.cCfgSQL()
    2456                  enc_type = cfg_db.get2 (
    2457                          option = 'encounter.default_type',
    2458                          workplace = _here.active_workplace,
    2459                          bias = 'user'
    2460                  )
    2461                  if enc_type is None:
    2462                          enc_type = gmEMRStructItems.get_most_commonly_used_encounter_type()
    2463                  if enc_type is None:
    2464                          enc_type = 'in surgery'
    2465                  enc = gmEMRStructItems.create_encounter(fk_patient = self.pk_patient, enc_type = enc_type)
    2466                  enc['pk_org_unit'] = _here['pk_org_unit']
    2467                  enc.save()
    2468                  self.current_encounter = enc
    2469                  _log.debug('new encounter [%s] activated', enc['pk_encounter'])
    2470
    2471          #------------------------------------------------------------------
    2472 -        def get_encounters(self, since=None, until=None, id_list=None, episodes=None, issues=None, skip_empty=False, order_by=None, max_encounters=None):
    2473                  """Retrieves patient's encounters.
    2474
    2475                  id_list - PKs of encounters to fetch
    2476                  since - initial date for encounter items, DateTime instance
    2477                  until - final date for encounter items, DateTime instance
    2478                  episodes - PKs of the episodes the encounters belong to (many-to-many relation)
    2479                  issues - PKs of the health issues the encounters belong to (many-to-many relation)
    2480                  skip_empty - do NOT return those which do not have any of documents/clinical items/RFE/AOE
    2481
    2482                  NOTE: if you specify *both* issues and episodes
    2483                  you will get the *aggregate* of all encounters even
    2484                  if the episodes all belong to the health issues listed.
    2485                  IOW, the issues broaden the episode list rather than
    2486                  the episode list narrowing the episodes-from-issues
    2487                  list.
    2488                  Rationale: If it was the other way round it would be
    2489                  redundant to specify the list of issues at all.
    2490                  """
    2491                  # if issues are given, translate them to their episodes
    2492                  if (issues is not None) and (len(issues) > 0):
    2493                          # - find episodes corresponding to the health issues in question
    2494                          cmd = "SELECT distinct pk_episode FROM clin.v_pat_episodes WHERE pk_health_issue in %(issue_pks)s AND pk_patient = %(pat)s"
    2495                          args = {'issue_pks': tuple(issues), 'pat': self.pk_patient}
    2496                          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
    2497                          epis4issues_pks = [ r['pk_episode']  for r in rows ]
    2498                          if episodes is None:
    2499                                  episodes = []
    2500                          episodes.extend(epis4issues_pks)
    2501
    2502                  if (episodes is not None) and (len(episodes) > 0):
    2503                          # since the episodes to filter by belong to the patient in question so will
    2504                          # the encounters found with them - hence we don't need a WHERE on the patient ...
    2505                          # but, better safe than sorry ...
    2506                          args = {'epi_pks': tuple(episodes), 'pat': self.pk_patient}
    2507                          cmd = "SELECT distinct fk_encounter FROM clin.clin_root_item WHERE fk_episode IN %(epi_pks)s AND fk_encounter IN (SELECT pk FROM clin.encounter WHERE fk_patient = %(pat)s)"
    2508                          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
    2509                          encs4epis_pks = [ r['fk_encounter'] for r in rows ]
    2510                          if id_list is None:
    2511                                  id_list = []
    2512                          id_list.extend(encs4epis_pks)
    2513
    2514                  where_parts = ['c_vpe.pk_patient = %(pat)s']
    2515                  args = {'pat': self.pk_patient}
    2516
    2517                  if skip_empty:
    2518                          where_parts.append("""NOT (
    2519                                  gm.is_null_or_blank_string(c_vpe.reason_for_encounter)
    2520                                          AND
    2521                                  gm.is_null_or_blank_string(c_vpe.assessment_of_encounter)
    2522                                          AND
    2523                                  NOT EXISTS (
    2524                                          SELECT 1 FROM clin.v_pat_items c_vpi WHERE c_vpi.pk_patient = %(pat)s AND c_vpi.pk_encounter = c_vpe.pk_encounter
    2525                                                  UNION ALL
    2526                                          SELECT 1 FROM blobs.v_doc_med b_vdm WHERE b_vdm.pk_patient = %(pat)s AND b_vdm.pk_encounter = c_vpe.pk_encounter
    2527                                  ))""")
    2528
    2529                  if since is not None:
    2530                          where_parts.append('c_vpe.started >= %(start)s')
    2531                          args['start'] = since
    2532
    2533                  if until is not None:
    2534                          where_parts.append('c_vpe.last_affirmed <= %(end)s')
    2535                          args['end'] = since
    2536
    2537                  if (id_list is not None) and (len(id_list) > 0):
    2538                          where_parts.append('c_vpe.pk_encounter IN %(enc_pks)s')
    2539                          args['enc_pks'] = tuple(id_list)
    2540
    2541                  if order_by is None:
    2542                          order_by = 'c_vpe.started'
    2543
    2544                  if max_encounters is None:
    2545                          limit = ''
    2546                  else:
    2547                          limit = 'LIMIT %s' % max_encounters
    2548
    2549                  cmd = """
    2550                          SELECT * FROM clin.v_pat_encounters c_vpe
    2551                          WHERE
    2552                                  %s
    2553                          ORDER BY %s %s
    2554                  """ % (
    2555                          ' AND '.join(where_parts),
    2556                          order_by,
    2557                          limit
    2558                  )
    2559                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2560                  encounters = [ gmEMRStructItems.cEncounter(row = {'data': r, 'idx': idx, 'pk_field': 'pk_encounter'}) for r in rows ]
    2561
    2562                  # we've got the encounters, start filtering
    2563                  filtered_encounters = []
    2564                  filtered_encounters.extend(encounters)
    2565
    2566                  if (episodes is not None) and (len(episodes) > 0):
    2567                          # since the episodes to filter by belong to the patient in question so will
    2568                          # the encounters found with them - hence we don't need a WHERE on the patient ...
    2569                          # but, better safe than sorry ...
    2570                          args = {'epi_pks': tuple(episodes), 'pat': self.pk_patient}
    2571                          cmd = "SELECT distinct fk_encounter FROM clin.clin_root_item WHERE fk_episode IN %(epi_pks)s AND fk_encounter IN (SELECT pk FROM clin.encounter WHERE fk_patient = %(pat)s)"
    2572                          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
    2573                          encs4epis_pks = [ r['fk_encounter'] for r in rows ]
    2574                          filtered_encounters = [ enc for enc in filtered_encounters if enc['pk_encounter'] in encs4epis_pks ]
    2575
    2576                  return filtered_encounters
    2577
    2578          #--------------------------------------------------------
    2579 -        def get_first_encounter(self, issue_id=None, episode_id=None):
    2580                  """Retrieves first encounter for a particular issue and/or episode.
    2581
    2582                  issue_id - First encounter associated health issue
    2583                  episode - First encounter associated episode
    2584                  """
    2585                  if issue_id is None:
    2586                          issues = None
    2587                  else:
    2588                          issues = [issue_id]
    2589
    2590                  if episode_id is None:
    2591                          episodes = None
    2592                  else:
    2593                          episodes = [episode_id]
    2594
    2595                  encounters = self.get_encounters(issues = issues, episodes = episodes, order_by = 'started', max_encounters = 1)
    2596                  if len(encounters) == 0:
    2597                          return None
    2598
    2599                  return encounters[0]
    2600
    2601          first_encounter = property(get_first_encounter, lambda x:x)
    2602
    2603          #--------------------------------------------------------
    2604 -        def get_earliest_care_date(self):
    2605                  args = {'pat': self.pk_patient}
    2606                  cmd = """
    2607  SELECT MIN(earliest) FROM (
    2608          (
    2609                  SELECT MIN(episode_modified_when) AS earliest FROM clin.v_pat_episodes WHERE pk_patient = %(pat)s
    2610
    2611          ) UNION ALL (
    2612
    2613                  SELECT MIN(modified_when) AS earliest FROM clin.v_health_issues WHERE pk_patient = %(pat)s
    2614
    2615          ) UNION ALL (
    2616
    2617                  SELECT MIN(modified_when) AS earliest FROM clin.encounter WHERE fk_patient = %(pat)s
    2618
    2619          ) UNION ALL (
    2620
    2621                  SELECT MIN(started) AS earliest FROM clin.v_pat_encounters WHERE pk_patient = %(pat)s
    2622
    2623          ) UNION ALL (
    2624
    2625                  SELECT MIN(modified_when) AS earliest FROM clin.v_pat_items WHERE pk_patient = %(pat)s
    2626
    2627          ) UNION ALL (
    2628
    2629                  SELECT MIN(modified_when) AS earliest FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat)s
    2630
    2631          ) UNION ALL (
    2632
    2633                  SELECT MIN(last_confirmed) AS earliest FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat)s
    2634
    2635          )
    2636  ) AS candidates"""
    2637                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2638                  return rows[0][0]
    2639
    2640          earliest_care_date = property(get_earliest_care_date, lambda x:x)
    2641
    2642          #--------------------------------------------------------
    2643 -        def get_most_recent_care_date(self):
    2644                  encounters = self.get_encounters(order_by = 'started DESC', max_encounters = 1)
    2645                  if len(encounters) == 0:
    2646                          return None
    2647                  return encounters[0]['last_affirmed']
    2648
    2649          most_recent_care_date = property(get_most_recent_care_date)
    2650
    2651          #--------------------------------------------------------
    2652 -        def get_last_encounter(self, issue_id=None, episode_id=None):
    2653                  """Retrieves last encounter for a concrete issue and/or episode
    2654
    2655                  issue_id - Last encounter associated health issue
    2656                  episode_id - Last encounter associated episode
    2657                  """
    2658                  if issue_id is None:
    2659                          issues = None
    2660                  else:
    2661                          issues = [issue_id]
    2662
    2663                  if episode_id is None:
    2664                          episodes = None
    2665                  else:
    2666                          episodes = [episode_id]
    2667
    2668                  encounters = self.get_encounters(issues = issues, episodes = episodes, order_by = 'started DESC', max_encounters = 1)
    2669                  if len(encounters) == 0:
    2670                          return None
    2671
    2672                  return encounters[0]
    2673
    2674          last_encounter = property(get_last_encounter, lambda x:x)
    2675
    2676          #------------------------------------------------------------------
    2677 -        def get_encounter_stats_by_type(self, cover_period=None):
    2678                  args = {'pat': self.pk_patient, 'range': cover_period}
    2679                  where_parts = ['pk_patient = %(pat)s']
    2680                  if cover_period is not None:
    2681                          where_parts.append('last_affirmed > now() - %(range)s')
    2682
    2683                  cmd = """
    2684                          SELECT l10n_type, count(1) AS frequency
    2685                          FROM clin.v_pat_encounters
    2686                          WHERE
    2687                                  %s
    2688                          GROUP BY l10n_type
    2689                          ORDER BY frequency DESC
    2690                  """ % ' AND '.join(where_parts)
    2691                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2692                  return rows
    2693
    2694          #------------------------------------------------------------------
    2695 -        def get_last_but_one_encounter(self, issue_id=None, episode_id=None):
    2696
    2697                  args = {'pat': self.pk_patient}
    2698
    2699                  if (issue_id is None) and (episode_id is None):
    2700                          cmd = """
    2701                                  SELECT * FROM clin.v_pat_encounters
    2702                                  WHERE pk_patient = %(pat)s
    2703                                  ORDER BY started DESC
    2704                                  LIMIT 2
    2705                          """
    2706                  else:
    2707                          where_parts = []
    2708
    2709                          if issue_id is not None:
    2710                                  where_parts.append('pk_health_issue = %(issue)s')
    2711                                  args['issue'] = issue_id
    2712
    2713                          if episode_id is not None:
    2714                                  where_parts.append('pk_episode = %(epi)s')
    2715                                  args['epi'] = episode_id
    2716
    2717                          cmd = """
    2718                                  SELECT *
    2719                                  FROM clin.v_pat_encounters
    2720                                  WHERE
    2721                                          pk_patient = %%(pat)s
    2722                                                  AND
    2723                                          pk_encounter IN (
    2724                                                  SELECT distinct pk_encounter
    2725                                                  FROM clin.v_narrative
    2726                                                  WHERE
    2727                                                          %s
    2728                                          )
    2729                                  ORDER BY started DESC
    2730                                  LIMIT 2
    2731                          """ % ' AND '.join(where_parts)
    2732
    2733                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2734
    2735                  if len(rows) == 0:
    2736                          return None
    2737
    2738                  # just one encounter within the above limits
    2739                  if len(rows) == 1:
    2740                          # is it the current encounter ?
    2741                          if rows[0]['pk_encounter'] == self.current_encounter['pk_encounter']:
    2742                                  # yes
    2743                                  return None
    2744                          # no
    2745                          return gmEMRStructItems.cEncounter(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_encounter'})
    2746
    2747                  # more than one encounter
    2748                  if rows[0]['pk_encounter'] == self.current_encounter['pk_encounter']:
    2749                          return gmEMRStructItems.cEncounter(row = {'data': rows[1], 'idx': idx, 'pk_field': 'pk_encounter'})
    2750
    2751                  return gmEMRStructItems.cEncounter(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_encounter'})
    2752
    2753          last_but_one_encounter = property(get_last_but_one_encounter, lambda x:x)
    2754
    2755          #------------------------------------------------------------------
    2756 -        def remove_empty_encounters(self):
    2757                  _log.debug('removing empty encounters for pk_identity [%s]', self.pk_patient)
    2758                  cfg_db = gmCfg.cCfgSQL()
    2759                  ttl = cfg_db.get2 (
    2760                          option = 'encounter.ttl_if_empty',
    2761                          workplace = _here.active_workplace,
    2762                          bias = 'user',
    2763                          default = '1 week'
    2764                  )
    2765  #               # FIXME: this should be done async
    2766                  cmd = "SELECT clin.remove_old_empty_encounters(%(pat)s::INTEGER, %(ttl)s::INTERVAL)"
    2767                  args = {'pat': self.pk_patient, 'ttl': ttl}
    2768                  try:
    2769                          rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True)
    2770                  except:
    2771                          _log.exception('error deleting empty encounters')
    2772                          return False
    2773
    2774                  if not rows[0][0]:
    2775                          _log.debug('no encounters deleted (less than 2 exist)')
    2776
    2777                  return True
    2778
    2779          #------------------------------------------------------------------
    2780          # API: measurements / test results
    2781          #------------------------------------------------------------------
    2782 -        def get_most_recent_results_for_patient(self, no_of_results=1):
    2783                  return gmPathLab.get_most_recent_results_for_patient (
    2784                          no_of_results = no_of_results,
    2785                          patient = self.pk_patient
    2786                  )
    2787
    2788          #------------------------------------------------------------------
    2789 -        def get_most_recent_results_in_loinc_group(self, loincs=None, no_of_results=1, consider_meta_type=False):
    2790                  return gmPathLab.get_most_recent_results_in_loinc_group (
    2791                          loincs = loincs,
    2792                          no_of_results = no_of_results,
    2793                          consider_meta_type = consider_meta_type,
    2794                          patient = self.pk_patient
    2795                  )
    2796
    2797          #------------------------------------------------------------------
    2798 -        def get_most_recent_results_for_test_type(self, test_type=None, no_of_results=1):
    2799                  return gmPathLab.get_most_recent_results_for_test_type (
    2800                          test_type = test_type,
    2801                          no_of_results = no_of_results,
    2802                          patient = self.pk_patient
    2803                  )
    2804
    2805          #------------------------------------------------------------------
    2806 -        def get_most_recent_result_for_test_types(self, pk_test_types=None):
    2807                  return gmPathLab.get_most_recent_result_for_test_types (
    2808                          pk_test_types = pk_test_types,
    2809                          pk_patient = self.pk_patient
    2810                  )
    2811
    2812          #------------------------------------------------------------------
    2813 -        def get_result_at_timestamp(self, timestamp=None, test_type=None, loinc=None, tolerance_interval='12 hours'):
    2814                  return gmPathLab.get_result_at_timestamp (
    2815                          timestamp = timestamp,
    2816                          test_type = test_type,
    2817                          loinc = loinc,
    2818                          tolerance_interval = tolerance_interval,
    2819                          patient = self.pk_patient
    2820                  )
    2821
    2822          #------------------------------------------------------------------
    2823 -        def get_results_for_day(self, timestamp=None, order_by=None):
    2824                  return gmPathLab.get_results_for_day (
    2825                          timestamp = timestamp,
    2826                          patient = self.pk_patient,
    2827                          order_by = order_by
    2828                  )
    2829
    2830          #------------------------------------------------------------------
    2831 -        def get_results_for_issue(self, pk_health_issue=None, order_by=None):
    2832                  return gmPathLab.get_results_for_issue (
    2833                          pk_health_issue = pk_health_issue,
    2834                          order_by = order_by
    2835                  )
    2836
    2837          #------------------------------------------------------------------
    2838 -        def get_results_for_episode(self, pk_episode=None):
    2839                  return gmPathLab.get_results_for_episode(pk_episode = pk_episode)
    2840
    2841          #------------------------------------------------------------------
    2842 -        def get_unsigned_results(self, order_by=None):
    2843                  if order_by is None:
    2844                          order_by = ''
    2845                  else:
    2846                          order_by = 'ORDER BY %s' % order_by
    2847                  cmd = """
    2848                          SELECT * FROM clin.v_test_results
    2849                          WHERE
    2850                                  pk_patient = %%(pat)s
    2851                                          AND
    2852                                  reviewed IS FALSE
    2853                          %s""" % order_by
    2854                  args = {'pat': self.pk_patient}
    2855                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2856                  return [ gmPathLab.cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
    2857
    2858          #------------------------------------------------------------------
    2859          # FIXME: use psyopg2 dbapi extension of named cursors - they are *server* side !
    2860 -        def get_test_types_for_results(self, order_by=None, unique_meta_types=False):
    2861                  """Retrieve data about test types for which this patient has results."""
    2862                  if order_by is None:
    2863                          order_by = ''
    2864                  else:
    2865                          order_by = 'ORDER BY %s' % order_by
    2866
    2867                  if unique_meta_types:
    2868                          cmd = """
    2869                                  SELECT * FROM clin.v_test_types c_vtt
    2870                                  WHERE c_vtt.pk_test_type IN (
    2871                                                  SELECT DISTINCT ON (c_vtr1.pk_meta_test_type) c_vtr1.pk_test_type
    2872                                                  FROM clin.v_test_results c_vtr1
    2873                                                  WHERE
    2874                                                          c_vtr1.pk_patient = %%(pat)s
    2875                                                                  AND
    2876                                                          c_vtr1.pk_meta_test_type IS NOT NULL
    2877                                          UNION ALL
    2878                                                  SELECT DISTINCT ON (c_vtr2.pk_test_type) c_vtr2.pk_test_type
    2879                                                  FROM clin.v_test_results c_vtr2
    2880                                                  WHERE
    2881                                                          c_vtr2.pk_patient = %%(pat)s
    2882                                                                  AND
    2883                                                          c_vtr2.pk_meta_test_type IS NULL
    2884                                  )
    2885                                  %s""" % order_by
    2886                  else:
    2887                          cmd = """
    2888                                  SELECT * FROM clin.v_test_types c_vtt
    2889                                  WHERE c_vtt.pk_test_type IN (
    2890                                          SELECT DISTINCT ON (c_vtr.pk_test_type) c_vtr.pk_test_type
    2891                                          FROM clin.v_test_results c_vtr
    2892                                          WHERE c_vtr.pk_patient = %%(pat)s
    2893                                  )
    2894                                  %s""" % order_by
    2895
    2896                  args = {'pat': self.pk_patient}
    2897                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2898                  return [ gmPathLab.cMeasurementType(row = {'pk_field': 'pk_test_type', 'idx': idx, 'data': r}) for r in rows ]
    2899
    2900          #------------------------------------------------------------------
    2901 -        def get_dates_for_results(self, tests=None, reverse_chronological=True):
    2902                  """Get the dates for which we have results."""
    2903                  where_parts = ['pk_patient = %(pat)s']
    2904                  args = {'pat': self.pk_patient}
    2905
    2906                  if tests is not None:
    2907                          where_parts.append('pk_test_type IN %(tests)s')
    2908                          args['tests'] = tuple(tests)
    2909
    2910                  cmd = """
    2911                          SELECT DISTINCT ON (clin_when_day)
    2912                                  clin_when_day,
    2913                                  is_reviewed
    2914                          FROM (
    2915                                  SELECT
    2916                                          date_trunc('day', clin_when)
    2917                                                  AS clin_when_day,
    2918                                          bool_and(reviewed)
    2919                                                  AS is_reviewed
    2920                                  FROM (
    2921                                          SELECT
    2922                                                  clin_when,
    2923                                                  reviewed,
    2924                                                  pk_patient,
    2925                                                  pk_test_result
    2926                                          FROM clin.v_test_results
    2927                                          WHERE %s
    2928                                  )
    2929                                          AS patient_tests
    2930                                  GROUP BY clin_when_day
    2931                          )
    2932                                  AS grouped_days
    2933                          ORDER BY clin_when_day %s
    2934                  """ % (
    2935                          ' AND '.join(where_parts),
    2936                          gmTools.bool2subst(reverse_chronological, 'DESC', 'ASC', 'DESC')
    2937                  )
    2938                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
    2939                  return rows
    2940
    2941          #------------------------------------------------------------------
    2942 -        def get_issues_or_episodes_for_results(self, tests=None):
    2943                  """Get the issues/episodes for which we have results."""
    2944                  where_parts = ['pk_patient = %(pat)s']
    2945                  args = {'pat': self.pk_patient}
    2946
    2947                  if tests is not None:
    2948                          where_parts.append('pk_test_type IN %(tests)s')
    2949                          args['tests'] = tuple(tests)
    2950                  where = ' AND '.join(where_parts)
    2951                  cmd = """
    2952                  SELECT * FROM ((
    2953                          -- issues, each including all it"s episodes
    2954                          SELECT
    2955                                  health_issue AS problem,
    2956                                  pk_health_issue,
    2957                                  NULL::integer AS pk_episode,
    2958                                  1 AS rank
    2959                          FROM clin.v_test_results
    2960                          WHERE pk_health_issue IS NOT NULL AND %s
    2961                          GROUP BY pk_health_issue, problem
    2962                  ) UNION ALL (
    2963                          -- episodes w/o issue
    2964                          SELECT
    2965                                  episode AS problem,
    2966                                  NULL::integer AS pk_health_issue,
    2967                                  pk_episode,
    2968                                  2 AS rank
    2969                          FROM clin.v_test_results
    2970                          WHERE pk_health_issue IS NULL AND %s
    2971                          GROUP BY pk_episode, problem
    2972                  )) AS grouped_union
    2973                  ORDER BY rank, problem
    2974                  """ % (where, where)
    2975                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
    2976                  return rows
    2977
    2978          #------------------------------------------------------------------
    2979 -        def get_test_results(self, encounters=None, episodes=None, tests=None, order_by=None):
    2980                  return gmPathLab.get_test_results (
    2981                          pk_patient = self.pk_patient,
    2982                          encounters = encounters,
    2983                          episodes = episodes,
    2984                          order_by = order_by
    2985                  )
    2986          #------------------------------------------------------------------
    2987 -        def get_test_results_by_date(self, encounter=None, episodes=None, tests=None, reverse_chronological=True):
    2988
    2989                  where_parts = ['pk_patient = %(pat)s']
    2990                  args = {'pat': self.pk_patient}
    2991
    2992                  if tests is not None:
    2993                          where_parts.append('pk_test_type IN %(tests)s')
    2994                          args['tests'] = tuple(tests)
    2995
    2996                  if encounter is not None:
    2997                          where_parts.append('pk_encounter = %(enc)s')
    2998                          args['enc'] = encounter
    2999
    3000                  if episodes is not None:
    3001                          where_parts.append('pk_episode IN %(epis)s')
    3002                          args['epis'] = tuple(episodes)
    3003
    3004                  cmd = """
    3005                          SELECT * FROM clin.v_test_results
    3006                          WHERE %s
    3007                          ORDER BY clin_when %s, pk_episode, unified_name
    3008                  """ % (
    3009                          ' AND '.join(where_parts),
    3010                          gmTools.bool2subst(reverse_chronological, 'DESC', 'ASC', 'DESC')
    3011                  )
    3012                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    3013
    3014                  tests = [ gmPathLab.cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
    3015
    3016                  return tests
    3017          #------------------------------------------------------------------
    3018 -        def add_test_result(self, episode=None, type=None, intended_reviewer=None, val_num=None, val_alpha=None, unit=None, link_obj=None):
    3019
    3020                  try:
    3021                          epi = int(episode)
    3022                  except:
    3023                          epi = episode['pk_episode']
    3024
    3025                  try:
    3026                          type = int(type)
    3027                  except:
    3028                          type = type['pk_test_type']
    3029
    3030                  tr = gmPathLab.create_test_result (
    3031                          link_obj = link_obj,
    3032                          encounter = self.current_encounter['pk_encounter'],
    3033                          episode = epi,
    3034                          type = type,
    3035                          intended_reviewer = intended_reviewer,
    3036                          val_num = val_num,
    3037                          val_alpha = val_alpha,
    3038                          unit = unit
    3039                  )
    3040
    3041                  return tr
    3042
    3043          #------------------------------------------------------------------
    3044 -        def get_labs_as_org_units(self):
    3045                  where = 'pk_org_unit IN (%s)' % """
    3046                          SELECT DISTINCT fk_org_unit FROM clin.test_org WHERE pk IN (
    3047                                  SELECT DISTINCT pk_test_org FROM clin.v_test_results where pk_patient = %(pat)s
    3048                          )"""
    3049                  args = {'pat': self.pk_patient}
    3050                  cmd = gmOrganization._SQL_get_org_unit % where
    3051                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    3052                  return [ gmOrganization.cOrgUnit(row = {'pk_field': 'pk_org_unit', 'data': r, 'idx': idx}) for r in rows ]
    3053
    3054          #------------------------------------------------------------------
    3055 -        def _get_best_gfr_or_crea(self):
    3056
    3057                  measured_gfr = self.get_most_recent_results_in_loinc_group(loincs = gmLOINC.LOINC_gfr_quantity, no_of_results = 1)
    3058                  crea = self.get_most_recent_results_in_loinc_group(loincs = gmLOINC.LOINC_creatinine_quantity, no_of_results = 1)
    3059
    3060                  if (measured_gfr is None) and (crea is None):
    3061                          return None
    3062
    3063                  if (measured_gfr is not None) and (crea is None):
    3064                          return measured_gfr
    3065
    3066                  # from here, Crea cannot be None anymore
    3067                  if measured_gfr is None:
    3068                          eGFR = self.calculator.eGFR
    3069                          if eGFR.numeric_value is None:
    3070                                  return crea
    3071                          return eGFR
    3072
    3073                  # from here, measured_gfr cannot be None anymore, either
    3074                  two_weeks = pydt.timedelta(weeks = 2)
    3075                  gfr_too_old = (crea['clin_when'] - measured_gfr['clin_when']) > two_weeks
    3076                  if not gfr_too_old:
    3077                          return measured_gfr
    3078
    3079                  # from here, measured_gfr is considered too
    3080                  # old, so attempt a more timely estimate
    3081                  eGFR = self.calculator.eGFR
    3082                  if eGFR.numeric_value is None:
    3083                          # return crea since we cannot get a
    3084                          # better estimate for some reason
    3085                          return crea
    3086
    3087                  return eGFR
    3088
    3089          best_gfr_or_crea = property(_get_best_gfr_or_crea, lambda x:x)
    3090
    3091          #------------------------------------------------------------------
    3092 -        def _get_bmi(self):
    3093                  return self.calculator.bmi
    3094
    3095          bmi = property(_get_bmi, lambda x:x)
    3096
    3097          #------------------------------------------------------------------
    3098 -        def _get_dynamic_hints(self):
    3099                  return gmAutoHints.get_hints_for_patient(pk_identity = self.pk_patient, pk_encounter = self.current_encounter['pk_encounter'])
    3100
    3101          dynamic_hints = property(_get_dynamic_hints, lambda x:x)
    3102
    3103          #------------------------------------------------------------------
    3104          #------------------------------------------------------------------
    3105          #------------------------------------------------------------------
    3106 -        def get_lab_request(self, pk=None, req_id=None, lab=None):
    3107                  # FIXME: verify that it is our patient ? ...
    3108                  req = gmPathLab.cLabRequest(aPK_obj=pk, req_id=req_id, lab=lab)
    3109                  return req
    3110          #------------------------------------------------------------------
    3111 -        def add_lab_request(self, lab=None, req_id=None, encounter_id=None, episode_id=None):
    3112                  if encounter_id is None:
    3113                          encounter_id = self.current_encounter['pk_encounter']
    3114                  status, data = gmPathLab.create_lab_request(
    3115                          lab=lab,
    3116                          req_id=req_id,
    3117                          pat_id=self.pk_patient,
    3118                          encounter_id=encounter_id,
    3119                          episode_id=episode_id
    3120                  )
    3121                  if not status:
    3122                          _log.error(str(data))
    3123                          return None
    3124                  return data
    3125
    3126  #============================================================
    3127  # main
    3128  #------------------------------------------------------------
    3129  if __name__ == "__main__":
    3130
    3131          if len(sys.argv) == 1:
    3132                  sys.exit()
    3133
    3134          if sys.argv[1] != 'test':
    3135                  sys.exit()
    3136
    3137          from Gnumed.pycommon import gmLog2
    3138
    3139          from Gnumed.business import gmPraxis
    3140          branches = gmPraxis.get_praxis_branches()
    3141          praxis = gmPraxis.gmCurrentPraxisBranch(branches[0])
    3142
    3143 -        def _do_delayed(*args, **kwargs):
    3144                  print(args)
    3145                  print(kwargs)
    3146                  args[0](*args[1:], **kwargs)
    3147
    3148          set_delayed_executor(_do_delayed)
    3149
    3150          #-----------------------------------------
    3151 -        def test_allergy_state():
    3152                  emr = cClinicalRecord(aPKey=1)
    3153                  state = emr.allergy_state
    3154                  print("allergy state is:", state)
    3155
    3156                  print("setting state to 0")
    3157                  emr.allergy_state = 0
    3158
    3159                  print("setting state to None")
    3160                  emr.allergy_state = None
    3161
    3162                  print("setting state to 'abc'")
    3163                  emr.allergy_state = 'abc'
    3164
    3165          #-----------------------------------------
    3166 -        def test_get_test_names():
    3167                  emr = cClinicalRecord(aPKey = 6)
    3168                  rows = emr.get_test_types_for_results(unique_meta_types = True)
    3169                  print("test result names:", len(rows))
    3170  #               for row in rows:
    3171  #                       print row
    3172
    3173          #-----------------------------------------
    3174 -        def test_get_dates_for_results():
    3175                  emr = cClinicalRecord(aPKey=12)
    3176                  rows = emr.get_dates_for_results()
    3177                  print("test result dates:")
    3178                  for row in rows:
    3179                          print(row)
    3180
    3181          #-----------------------------------------
    3182 -        def test_get_measurements():
    3183                  emr = cClinicalRecord(aPKey=12)
    3184                  rows, idx = emr.get_measurements_by_date()
    3185                  print("test results:")
    3186                  for row in rows:
    3187                          print(row)
    3188
    3189          #-----------------------------------------
    3190 -        def test_get_test_results_by_date():
    3191                  emr = cClinicalRecord(aPKey=12)
    3192                  tests = emr.get_test_results_by_date()
    3193                  print("test results:")
    3194                  for test in tests:
    3195                          print(test)
    3196
    3197          #-----------------------------------------
    3198 -        def test_get_statistics():
    3199                  emr = cClinicalRecord(aPKey=12)
    3200                  for key, item in emr.get_statistics().items():
    3201                          print(key, ":", item)
    3202
    3203          #-----------------------------------------
    3204 -        def test_get_problems():
    3205                  emr = cClinicalRecord(aPKey=12)
    3206
    3207                  probs = emr.get_problems()
    3208                  print("normal probs (%s):" % len(probs))
    3209                  for p in probs:
    3210                          print('%s (%s)' % (p['problem'], p['type']))
    3211
    3212                  probs = emr.get_problems(include_closed_episodes=True)
    3213                  print("probs + closed episodes (%s):" % len(probs))
    3214                  for p in probs:
    3215                          print('%s (%s)' % (p['problem'], p['type']))
    3216
    3217                  probs = emr.get_problems(include_irrelevant_issues=True)
    3218                  print("probs + issues (%s):" % len(probs))
    3219                  for p in probs:
    3220                          print('%s (%s)' % (p['problem'], p['type']))
    3221
    3222                  probs = emr.get_problems(include_closed_episodes=True, include_irrelevant_issues=True)
    3223                  print("probs + issues + epis (%s):" % len(probs))
    3224                  for p in probs:
    3225                          print('%s (%s)' % (p['problem'], p['type']))
    3226
    3227          #-----------------------------------------
    3228 -        def test_add_test_result():
    3229                  emr = cClinicalRecord(aPKey=12)
    3230                  tr = emr.add_test_result (
    3231                          episode = 1,
    3232                          intended_reviewer = 1,
    3233                          type = 1,
    3234                          val_num = 75,
    3235                          val_alpha = 'somewhat obese',
    3236                          unit = 'kg'
    3237                  )
    3238                  print(tr)
    3239
    3240          #-----------------------------------------
    3241 -        def test_get_most_recent_episode():
    3242                  emr = cClinicalRecord(aPKey=12)
    3243                  print(emr.get_most_recent_episode(issue = 2))
    3244
    3245          #-----------------------------------------
    3246 -        def test_get_almost_recent_encounter():
    3247                  emr = cClinicalRecord(aPKey=12)
    3248                  print(emr.get_last_encounter(issue_id=2))
    3249                  print(emr.get_last_but_one_encounter(issue_id=2))
    3250
    3251          #-----------------------------------------
    3252 -        def test_get_encounters():
    3253                  emr = cClinicalRecord(aPKey = 5)
    3254                  print(emr.get_first_encounter(episode_id = 1638))
    3255                  print(emr.get_last_encounter(episode_id = 1638))
    3256
    3257          #-----------------------------------------
    3258 -        def test_get_issues():
    3259                  emr = cClinicalRecord(aPKey = 12)
    3260                  for issue in emr.health_issues:
    3261                          print(issue['description'])
    3262
    3263          #-----------------------------------------
    3264 -        def test_get_dx():
    3265                  emr = cClinicalRecord(aPKey = 12)
    3266                  for dx in emr.candidate_diagnoses:
    3267                          print(dx)
    3268
    3269          #-----------------------------------------
    3270 -        def test_get_meds():
    3271                  emr = cClinicalRecord(aPKey=12)
    3272                  for med in emr.get_current_medications():
    3273                          print(med)
    3274
    3275          #-----------------------------------------
    3276 -        def test_get_abuses():
    3277                  emr = cClinicalRecord(aPKey=12)
    3278                  for med in emr.abused_substances:
    3279                          print(med.format(single_line = True))
    3280
    3281          #-----------------------------------------
    3282 -        def test_is_allergic_to():
    3283                  emr = cClinicalRecord(aPKey = 12)
    3284                  print(emr.is_allergic_to(atcs = tuple(sys.argv[2:]), inns = tuple(sys.argv[2:]), product_name = sys.argv[2]))
    3285
    3286          #-----------------------------------------
    3287 -        def test_get_as_journal():
    3288                  emr = cClinicalRecord(aPKey = 12)
    3289                  for journal_line in emr.get_as_journal():
    3290                          #print journal_line.keys()
    3291                          print('%(date)s  %(modified_by)s  %(soap_cat)s  %(narrative)s' % journal_line)
    3292                          print("")
    3293
    3294          #-----------------------------------------
    3295 -        def test_get_most_recent():
    3296                  emr = cClinicalRecord(aPKey=12)
    3297                  print(emr.get_most_recent_results_for_test_type())
    3298
    3299          #-----------------------------------------
    3300 -        def test_episodes():
    3301                  emr = cClinicalRecord(aPKey=12)
    3302                  print("episodes:", emr.episodes)
    3303                  print("unlinked:", emr.unlinked_episodes)
    3304
    3305          #-----------------------------------------
    3306 -        def test_format_as_journal():
    3307                  emr = cClinicalRecord(aPKey=12)
    3308                  from Gnumed.business.gmPerson import cPatient
    3309                  pat = cPatient(aPK_obj = 12)
    3310                  print(emr.format_as_journal(left_margin = 1, patient = pat))
    3311
    3312          #-----------------------------------------
    3313 -        def test_smoking():
    3314                  emr = cClinicalRecord(aPKey=12)
    3315                  #print emr.is_or_was_smoker
    3316                  smoking, details = emr.smoking_status
    3317                  print('status:', smoking)
    3318                  print('details:')
    3319                  print(details)
    3320                  emr.smoking_status = (True, {'comment': '2', 'last_confirmed': gmDateTime.pydt_now_here()})
    3321                  print(emr.smoking_status)
    3322                  print(emr.alcohol_status)
    3323                  print(emr.drugs_status)
    3324
    3325          #-----------------------------------------
    3326
    3327          #test_allergy_state()
    3328          #test_is_allergic_to()
    3329
    3330          #test_get_test_names()
    3331          #test_get_dates_for_results()
    3332          #test_get_measurements()
    3333          #test_get_test_results_by_date()
    3334          #test_get_statistics()
    3335          #test_get_problems()
    3336          #test_add_test_result()
    3337          #test_get_most_recent_episode()
    3338          #test_get_almost_recent_encounter()
    3339          #test_get_meds()
    3340          #test_get_as_journal()
    3341          #test_get_most_recent()
    3342          #test_episodes()
    3343          #test_format_as_journal()
    3344          #test_smoking()
    3345          #test_get_abuses()
    3346          #test_get_encounters()
    3347          #test_get_issues()
    3348          #test_get_dx()
    3349
    3350          emr = cClinicalRecord(aPKey = 12)
    3351
    3352  #       # Vacc regimes
    3353  #       vacc_regimes = emr.get_scheduled_vaccination_regimes(indications = ['tetanus'])
    3354  #       print '\nVaccination regimes: '
    3355  #       for a_regime in vacc_regimes:
    3356  #               pass
    3357  #               #print a_regime
    3358  #       vacc_regime = emr.get_scheduled_vaccination_regimes(ID=10)
    3359  #       #print vacc_regime
    3360
    3361  #       # vaccination regimes and vaccinations for regimes
    3362  #       scheduled_vaccs = emr.get_scheduled_vaccinations(indications = ['tetanus'])
    3363  #       print 'Vaccinations for the regime:'
    3364  #       for a_scheduled_vacc in scheduled_vaccs:
    3365  #               pass
    3366  #               #print '   %s' %(a_scheduled_vacc)
    3367
    3368  #       # vaccination next shot and booster
    3369          v1 = emr.vaccinations
    3370          print(v1)
    3371          v2 = gmVaccination.get_vaccinations(pk_identity = 12, return_pks = True)
    3372          print(v2)
    3373          for v in v1:
    3374                  if v['pk_vaccination'] not in v2:
    3375                          print('ERROR')
    3376
    3377  #       for a_vacc in vaccinations:
    3378  #               print '\nVaccination %s , date: %s, booster: %s, seq no: %s' %(a_vacc['batch_no'], a_vacc['date'].strftime('%Y-%m-%d'), a_vacc['is_booster'], a_vacc['seq_no'])
    3379
    3380  #       # first and last encounters
    3381  #       first_encounter = emr.get_first_encounter(issue_id = 1)
    3382  #       print '\nFirst encounter: ' + str(first_encounter)
    3383  #       last_encounter = emr.get_last_encounter(episode_id = 1)
    3384  #       print '\nLast encounter: ' + str(last_encounter)
    3385  #       print ''
    3386
    3387          #dump = record.get_missing_vaccinations()
    3388          #f = io.open('vaccs.lst', 'wb')
    3389          #if dump is not None:
    3390          #       print "=== due ==="
    3391          #       f.write(u"=== due ===\n")
    3392          #       for row in dump['due']:
    3393          #               print row
    3394          #               f.write(repr(row))
    3395          #               f.write(u'\n')
    3396          #       print "=== overdue ==="
    3397          #       f.write(u"=== overdue ===\n")
    3398          #       for row in dump['overdue']:
    3399          #               print row
    3400          #               f.write(repr(row))
    3401          #               f.write(u'\n')
    3402          #f.close()
    3403