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

# Source Code for Module Gnumed.business.gmClinicalRecord

       1  # -*- coding: utf8 -*-
       2  """GNUmed clinical patient record.
       3
       4  Make sure to call set_func_ask_user() and set_encounter_ttl() early on in
       5  your code (before cClinicalRecord.__init__() is called for the first time).
       6  """
       7  #============================================================
       8  __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
       9  __license__ = "GPL v2 or later"
      10
      11  #===================================================
      12  # TODO
      13  # Basically we'll probably have to:
      14  #
      15  # a) serialize access to re-getting data from the cache so
      16  #   that later-but-concurrent cache accesses spin until
      17  #   the first one completes the refetch from the database
      18  #
      19  # b) serialize access to the cache per-se such that cache
      20  #    flushes vs. cache regets happen atomically (where
      21  #    flushes would abort/restart current regets)
      22  #===================================================
      23
      24  # standard libs
      25  import sys
      26  import logging
      27
      28
      29  if __name__ == '__main__':
      30          sys.path.insert(0, '../../')
      31          from Gnumed.pycommon import gmLog2, gmDateTime, gmI18N
      32          gmI18N.activate_locale()
      33          gmI18N.install_domain()
      34          gmDateTime.init()
      35
      36  from Gnumed.pycommon import gmExceptions
      37  from Gnumed.pycommon import gmPG2
      38  from Gnumed.pycommon import gmDispatcher
      39  from Gnumed.pycommon import gmI18N
      40  from Gnumed.pycommon import gmCfg
      41  from Gnumed.pycommon import gmTools
      42  from Gnumed.pycommon import gmDateTime
      43
      44  from Gnumed.business import gmAllergy
      45  from Gnumed.business import gmPathLab
      46  from Gnumed.business import gmLOINC
      47  from Gnumed.business import gmClinNarrative
      48  from Gnumed.business import gmEMRStructItems
      49  from Gnumed.business import gmMedication
      50  from Gnumed.business import gmVaccination
      51  from Gnumed.business import gmFamilyHistory
      52  from Gnumed.business.gmDemographicRecord import get_occupations
      53
      54
      55  _log = logging.getLogger('gm.emr')
      56
      57  _me = None
      58  _here = None
      59  #============================================================
      60  # helper functions
      61  #------------------------------------------------------------
      62  _func_ask_user = None
      63
      64 -def set_func_ask_user(a_func = None):
      65          if not callable(a_func):
      66                  _log.error('[%] not callable, not setting _func_ask_user', a_func)
      67                  return False
      68
      69          _log.debug('setting _func_ask_user to [%s]', a_func)
      70
      71          global _func_ask_user
      72          _func_ask_user = a_func
      73
      74  #============================================================
      75 -class cClinicalRecord(object):
      76
      77          _clin_root_item_children_union_query = None
      78
      79 -        def __init__(self, aPKey=None, allow_user_interaction=True):
      80                  """Fails if
      81
      82                  - no connection to database possible
      83                  - patient referenced by aPKey does not exist
      84                  """
      85                  self.pk_patient = aPKey                 # == identity.pk == primary key
      86
      87                  # log access to patient record (HIPAA, for example)
      88                  cmd = u'SELECT gm.log_access2emr(%(todo)s)'
      89                  args = {'todo': u'patient [%s]' % aPKey}
      90                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
      91
      92                  from Gnumed.business import gmPraxis, gmStaff
      93                  global _me
      94                  if _me is None:
      95                          _me = gmStaff.gmCurrentProvider()
      96                  global _here
      97                  if _here is None:
      98                          _here = gmPraxis.gmCurrentPraxisBranch()
      99
     100                  # ...........................................
     101                  # this is a hack to speed up get_encounters()
     102                  clin_root_item_children = gmPG2.get_child_tables('clin', 'clin_root_item')
     103                  if cClinicalRecord._clin_root_item_children_union_query is None:
     104                          union_phrase = u"""
     105  SELECT fk_encounter from
     106          %s.%s cn
     107                  inner join
     108          (SELECT pk FROM clin.episode ep WHERE ep.fk_health_issue in %%s) as epi
     109                  on (cn.fk_episode =  epi.pk)
     110  """
     111                          cClinicalRecord._clin_root_item_children_union_query = u'union\n'.join (
     112                                  [ union_phrase % (child[0], child[1]) for child in clin_root_item_children ]
     113                          )
     114                  # ...........................................
     115
     116                  self.__db_cache = {}
     117
     118                  # load current or create new encounter
     119                  if _func_ask_user is None:
     120                          _log.error('[_func_ask_user] is None')
     121                          print "*** GNUmed [%s]: _func_ask_user is not set ***" % self.__class__.__name__
     122                  self.remove_empty_encounters()
     123                  self.__encounter = None
     124                  if not self.__initiate_active_encounter(allow_user_interaction = allow_user_interaction):
     125                          raise gmExceptions.ConstructorError, "cannot activate an encounter for patient [%s]" % aPKey
     126
     127                  gmAllergy.ensure_has_allergy_state(encounter = self.current_encounter['pk_encounter'])
     128
     129                  # register backend notification interests
     130                  # (keep this last so we won't hang on threads when
     131                  #  failing this constructor for other reasons ...)
     132                  if not self._register_interests():
     133                          raise gmExceptions.ConstructorError, "cannot register signal interests"
     134
     135                  _log.debug('Instantiated clinical record for patient [%s].' % self.pk_patient)
     136          #--------------------------------------------------------
     137 -        def __del__(self):
     138                  pass
     139          #--------------------------------------------------------
     140 -        def cleanup(self):
     141                  _log.debug('cleaning up after clinical record for patient [%s]' % self.pk_patient)
     142                  return True
     143          #--------------------------------------------------------
     144          # messaging
     145          #--------------------------------------------------------
     146 -        def _register_interests(self):
     147                  gmDispatcher.connect(signal = u'encounter_mod_db', receiver = self.db_callback_encounter_mod_db)
     148
     149                  return True
     150          #--------------------------------------------------------
     151 -        def db_callback_encounter_mod_db(self, **kwds):
     152
     153                  # get the current encounter as an extra instance
     154                  # from the database to check for changes
     155                  curr_enc_in_db = gmEMRStructItems.cEncounter(aPK_obj = self.current_encounter['pk_encounter'])
     156
     157                  # the encounter just retrieved and the active encounter
     158                  # have got the same transaction ID so there's no change
     159                  # in the database, there could be a local change in
     160                  # the active encounter but that doesn't matter
     161                  # THIS DOES NOT WORK
     162  #               if curr_enc_in_db['xmin_encounter'] == self.current_encounter['xmin_encounter']:
     163  #                       return True
     164
     165                  # there must have been a change to the active encounter
     166                  # committed to the database from elsewhere,
     167                  # we must fail propagating the change, however, if
     168                  # there are local changes
     169                  if self.current_encounter.is_modified():
     170                          _log.debug('unsaved changes in active encounter, cannot switch to another one')
     171                          raise ValueError('unsaved changes in active encounter, cannot switch to another one')
     172
     173                  if self.current_encounter.same_payload(another_object = curr_enc_in_db):
     174                          _log.debug('encounter_mod_db received but no change to active encounter payload')
     175                          return True
     176
     177                  # there was a change in the database from elsewhere,
     178                  # locally, however, we don't have any changes, therefore
     179                  # we can propagate the remote change locally without
     180                  # losing anything
     181                  _log.debug('active encounter modified remotely, reloading and announcing the modification')
     182                  self.current_encounter.refetch_payload()
     183                  gmDispatcher.send(u'current_encounter_modified')
     184
     185                  return True
     186          #--------------------------------------------------------
     187 -        def db_callback_vaccs_modified(self, **kwds):
     188                  return True
     189          #--------------------------------------------------------
     190 -        def _health_issues_modified(self):
     191                  try:
     192                          del self.__db_cache['health issues']
     193                  except KeyError:
     194                          pass
     195                  return 1
     196          #--------------------------------------------------------
     197 -        def _db_callback_episodes_modified(self):
     198  #               try:
     199  #                       del self.__db_cache['episodes']
     200  #               except KeyError:
     201  #                       pass
     202                  return 1
     203          #--------------------------------------------------------
     204 -        def _clin_item_modified(self):
     205                  _log.debug('DB: clin_root_item modification')
     206          #--------------------------------------------------------
     207          # API: family history
     208          #--------------------------------------------------------
     209 -        def get_family_history(self, episodes=None, issues=None, encounters=None):
     210                  fhx = gmFamilyHistory.get_family_history (
     211                          order_by = u'l10n_relation, condition',
     212                          patient = self.pk_patient
     213                  )
     214
     215                  if episodes is not None:
     216                          fhx = filter(lambda f: f['pk_episode'] in episodes, fhx)
     217
     218                  if issues is not None:
     219                          fhx = filter(lambda f: f['pk_health_issue'] in issues, fhx)
     220
     221                  if encounters is not None:
     222                          fhx = filter(lambda f: f['pk_encounter'] in encounters, fhx)
     223
     224                  return fhx
     225          #--------------------------------------------------------
     226 -        def add_family_history(self, episode=None, condition=None, relation=None):
     227                  return gmFamilyHistory.create_family_history (
     228                          encounter = self.current_encounter['pk_encounter'],
     229                          episode = episode,
     230                          condition = condition,
     231                          relation = relation
     232                  )
     233          #--------------------------------------------------------
     234          # API: performed procedures
     235          #--------------------------------------------------------
     236 -        def get_performed_procedures(self, episodes=None, issues=None):
     237
     238                  procs = gmEMRStructItems.get_performed_procedures(patient = self.pk_patient)
     239
     240                  if episodes is not None:
     241                          procs = filter(lambda p: p['pk_episode'] in episodes, procs)
     242
     243                  if issues is not None:
     244                          procs = filter(lambda p: p['pk_health_issue'] in issues, procs)
     245
     246                  return procs
     247
     248          performed_procedures = property(get_performed_procedures, lambda x:x)
     249          #--------------------------------------------------------
     250 -        def get_latest_performed_procedure(self):
     251                  return gmEMRStructItems.get_latest_performed_procedure(patient = self.pk_patient)
     252          #--------------------------------------------------------
     253 -        def add_performed_procedure(self, episode=None, location=None, hospital_stay=None, procedure=None):
     254                  return gmEMRStructItems.create_performed_procedure (
     255                          encounter = self.current_encounter['pk_encounter'],
     256                          episode = episode,
     257                          location = location,
     258                          hospital_stay = hospital_stay,
     259                          procedure = procedure
     260                  )
     261          #--------------------------------------------------------
     262          # API: hospitalizations
     263          #--------------------------------------------------------
     264 -        def get_hospital_stays(self, episodes=None, issues=None, ongoing_only=False):
     265                  stays = gmEMRStructItems.get_patient_hospital_stays(patient = self.pk_patient, ongoing_only = ongoing_only)
     266                  if episodes is not None:
     267                          stays = filter(lambda s: s['pk_episode'] in episodes, stays)
     268                  if issues is not None:
     269                          stays = filter(lambda s: s['pk_health_issue'] in issues, stays)
     270                  return stays
     271
     272          hospital_stays = property(get_hospital_stays, lambda x:x)
     273          #--------------------------------------------------------
     274 -        def get_latest_hospital_stay(self):
     275                  return gmEMRStructItems.get_latest_patient_hospital_stay(patient = self.pk_patient)
     276          #--------------------------------------------------------
     277 -        def add_hospital_stay(self, episode=None):
     278                  return gmEMRStructItems.create_hospital_stay (
     279                          encounter = self.current_encounter['pk_encounter'],
     280                          episode = episode
     281                  )
     282          #--------------------------------------------------------
     283 -        def get_hospital_stay_stats_by_hospital(self, cover_period=None):
     284                  args = {'pat': self.pk_patient, 'range': cover_period}
     285                  where_parts = [u'pk_patient = %(pat)s']
     286                  if cover_period is not None:
     287                          where_parts.append(u'discharge > (now() - %(range)s)')
     288
     289                  cmd = u"""
     290                          SELECT hospital, count(1) AS frequency
     291                          FROM clin.v_pat_hospital_stays
     292                          WHERE
     293                                  %s
     294                          GROUP BY hospital
     295                          ORDER BY frequency DESC
     296                  """ % u' AND '.join(where_parts)
     297
     298                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     299                  return rows
     300          #--------------------------------------------------------
     301          # API: narrative
     302          #--------------------------------------------------------
     303 -        def add_notes(self, notes=None, episode=None, encounter=None):
     304
     305                  enc = gmTools.coalesce (
     306                          encounter,
     307                          self.current_encounter['pk_encounter']
     308                  )
     309
     310                  for note in notes:
     311                          success, data = gmClinNarrative.create_clin_narrative (
     312                                  narrative = note[1],
     313                                  soap_cat = note[0],
     314                                  episode_id = episode,
     315                                  encounter_id = enc
     316                          )
     317
     318                  return True
     319          #--------------------------------------------------------
     320 -        def add_clin_narrative(self, note='', soap_cat='s', episode=None):
     321                  if note.strip() == '':
     322                          _log.info('will not create empty clinical note')
     323                          return None
     324                  if isinstance(episode, gmEMRStructItems.cEpisode):
     325                          episode = episode['pk_episode']
     326                  status, data = gmClinNarrative.create_clin_narrative (
     327                          narrative = note,
     328                          soap_cat = soap_cat,
     329                          episode_id = episode,
     330                          encounter_id = self.current_encounter['pk_encounter']
     331                  )
     332                  if not status:
     333                          _log.error(str(data))
     334                          return None
     335                  return data
     336          #--------------------------------------------------------
     337 -        def get_clin_narrative(self, since=None, until=None, encounters=None, episodes=None, issues=None, soap_cats=None, providers=None):
     338                  """Get SOAP notes pertinent to this encounter.
     339
     340                          since
     341                                  - initial date for narrative items
     342                          until
     343                                  - final date for narrative items
     344                          encounters
     345                                  - list of encounters whose narrative are to be retrieved
     346                          episodes
     347                                  - list of episodes whose narrative are to be retrieved
     348                          issues
     349                                  - list of health issues whose narrative are to be retrieved
     350                          soap_cats
     351                                  - list of SOAP categories of the narrative to be retrieved
     352                  """
     353                  where_parts = [u'pk_patient = %(pat)s']
     354                  args = {u'pat': self.pk_patient}
     355
     356                  if issues is not None:
     357                          where_parts.append(u'pk_health_issue IN %(issues)s')
     358                          args['issues'] = tuple(issues)
     359
     360                  if episodes is not None:
     361                          where_parts.append(u'pk_episode IN %(epis)s')
     362                          args['epis'] = tuple(episodes)
     363
     364                  if encounters is not None:
     365                          where_parts.append(u'pk_encounter IN %(encs)s')
     366                          args['encs'] = tuple(encounters)
     367
     368                  if soap_cats is not None:
     369                          where_parts.append(u'soap_cat IN %(cats)s')
     370                          soap_cats = list(soap_cats)
     371                          args['cats'] = [ cat.lower() for cat in soap_cats if cat is not None ]
     372                          if None in soap_cats:
     373                                  args['cats'].append(None)
     374                          args['cats'] = tuple(args['cats'])
     375
     376                  cmd = u"""
     377                          SELECT
     378                                  c_vpn.*,
     379                                  (SELECT rank FROM clin.soap_cat_ranks WHERE soap_cat = c_vpn.soap_cat) AS soap_rank
     380                          FROM clin.v_pat_narrative c_vpn
     381                          WHERE %s
     382                          ORDER BY date, soap_rank
     383                  """ % u' AND '.join(where_parts)
     384
     385                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     386
     387                  filtered_narrative = [ gmClinNarrative.cNarrative(row = {'pk_field': 'pk_narrative', 'idx': idx, 'data': row}) for row in rows ]
     388
     389                  if since is not None:
     390                          filtered_narrative = filter(lambda narr: narr['date'] >= since, filtered_narrative)
     391
     392                  if until is not None:
     393                          filtered_narrative = filter(lambda narr: narr['date'] < until, filtered_narrative)
     394
     395                  if providers is not None:
     396                          filtered_narrative = filter(lambda narr: narr['provider'] in providers, filtered_narrative)
     397
     398                  return filtered_narrative
     399          #--------------------------------------------------------
     400 -        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):
     401                  return gmClinNarrative.get_as_journal (
     402                          patient = self.pk_patient,
     403                          since = since,
     404                          until = until,
     405                          encounters = encounters,
     406                          episodes = episodes,
     407                          issues = issues,
     408                          soap_cats = soap_cats,
     409                          providers = providers,
     410                          order_by = order_by,
     411                          time_range = time_range
     412                  )
     413          #--------------------------------------------------------
     414 -        def search_narrative_simple(self, search_term=''):
     415
     416                  search_term = search_term.strip()
     417                  if search_term == '':
     418                          return []
     419
     420                  cmd = u"""
     421  SELECT
     422          *,
     423          coalesce((SELECT description FROM clin.episode WHERE pk = vn4s.pk_episode), vn4s.src_table)
     424                  as episode,
     425          coalesce((SELECT description FROM clin.health_issue WHERE pk = vn4s.pk_health_issue), vn4s.src_table)
     426                  as health_issue,
     427          (SELECT started FROM clin.encounter WHERE pk = vn4s.pk_encounter)
     428                  as encounter_started,
     429          (SELECT last_affirmed FROM clin.encounter WHERE pk = vn4s.pk_encounter)
     430                  as encounter_ended,
     431          (SELECT _(description) FROM clin.encounter_type WHERE pk = (SELECT fk_type FROM clin.encounter WHERE pk = vn4s.pk_encounter))
     432                  as encounter_type
     433  from clin.v_narrative4search vn4s
     434  WHERE
     435          pk_patient = %(pat)s and
     436          vn4s.narrative ~ %(term)s
     437  order by
     438          encounter_started
     439  """             # case sensitive
     440                  rows, idx = gmPG2.run_ro_queries(queries = [
     441                          {'cmd': cmd, 'args': {'pat': self.pk_patient, 'term': search_term}}
     442                  ])
     443                  return rows
     444          #--------------------------------------------------------
     445 -        def get_text_dump_old(self):
     446                  # don't know how to invalidate this by means of
     447                  # a notify without catching notifies from *all*
     448                  # child tables, the best solution would be if
     449                  # inserts in child tables would also fire triggers
     450                  # of ancestor tables, but oh well,
     451                  # until then the text dump will not be cached ...
     452                  try:
     453                          return self.__db_cache['text dump old']
     454                  except KeyError:
     455                          pass
     456                  # not cached so go get it
     457                  fields = [
     458                          "to_char(modified_when, 'YYYY-MM-DD @ HH24:MI') as modified_when",
     459                          'modified_by',
     460                          'clin_when',
     461                          "case is_modified when false then '%s' else '%s' end as modified_string" % (_('original entry'), _('modified entry')),
     462                          'pk_item',
     463                          'pk_encounter',
     464                          'pk_episode',
     465                          'pk_health_issue',
     466                          'src_table'
     467                  ]
     468                  cmd = "SELECT %s FROM clin.v_pat_items WHERE pk_patient=%%s order by src_table, clin_when" % ', '.join(fields)
     469                  ro_conn = self._conn_pool.GetConnection('historica')
     470                  curs = ro_conn.cursor()
     471                  if not gmPG2.run_query(curs, None, cmd, self.pk_patient):
     472                          _log.error('cannot load item links for patient [%s]' % self.pk_patient)
     473                          curs.close()
     474                          return None
     475                  rows = curs.fetchall()
     476                  view_col_idx = gmPG2.get_col_indices(curs)
     477
     478                  # aggregate by src_table for item retrieval
     479                  items_by_table = {}
     480                  for item in rows:
     481                          src_table = item[view_col_idx['src_table']]
     482                          pk_item = item[view_col_idx['pk_item']]
     483                          if not items_by_table.has_key(src_table):
     484                                  items_by_table[src_table] = {}
     485                          items_by_table[src_table][pk_item] = item
     486
     487                  # get mapping for issue/episode IDs
     488                  issues = self.get_health_issues()
     489                  issue_map = {}
     490                  for issue in issues:
     491                          issue_map[issue['pk']] = issue['description']
     492                  episodes = self.get_episodes()
     493                  episode_map = {}
     494                  for episode in episodes:
     495                          episode_map[episode['pk_episode']] = episode['description']
     496                  emr_data = {}
     497                  # get item data from all source tables
     498                  for src_table in items_by_table.keys():
     499                          item_ids = items_by_table[src_table].keys()
     500                          # we don't know anything about the columns of
     501                          # the source tables but, hey, this is a dump
     502                          if len(item_ids) == 0:
     503                                  _log.info('no items in table [%s] ?!?' % src_table)
     504                                  continue
     505                          elif len(item_ids) == 1:
     506                                  cmd = "SELECT * FROM %s WHERE pk_item=%%s order by modified_when" % src_table
     507                                  if not gmPG2.run_query(curs, None, cmd, item_ids[0]):
     508                                          _log.error('cannot load items from table [%s]' % src_table)
     509                                          # skip this table
     510                                          continue
     511                          elif len(item_ids) > 1:
     512                                  cmd = "SELECT * FROM %s WHERE pk_item in %%s order by modified_when" % src_table
     513                                  if not gmPG.run_query(curs, None, cmd, (tuple(item_ids),)):
     514                                          _log.error('cannot load items from table [%s]' % src_table)
     515                                          # skip this table
     516                                          continue
     517                          rows = curs.fetchall()
     518                          table_col_idx = gmPG.get_col_indices(curs)
     519                          # format per-table items
     520                          for row in rows:
     521                                  # FIXME: make this get_pkey_name()
     522                                  pk_item = row[table_col_idx['pk_item']]
     523                                  view_row = items_by_table[src_table][pk_item]
     524                                  age = view_row[view_col_idx['age']]
     525                                  # format metadata
     526                                  try:
     527                                          episode_name = episode_map[view_row[view_col_idx['pk_episode']]]
     528                                  except:
     529                                          episode_name = view_row[view_col_idx['pk_episode']]
     530                                  try:
     531                                          issue_name = issue_map[view_row[view_col_idx['pk_health_issue']]]
     532                                  except:
     533                                          issue_name = view_row[view_col_idx['pk_health_issue']]
     534
     535                                  if not emr_data.has_key(age):
     536                                          emr_data[age] = []
     537
     538                                  emr_data[age].append(
     539                                          _('%s: encounter (%s)') % (
     540                                                  view_row[view_col_idx['clin_when']],
     541                                                  view_row[view_col_idx['pk_encounter']]
     542                                          )
     543                                  )
     544                                  emr_data[age].append(_('health issue: %s') % issue_name)
     545                                  emr_data[age].append(_('episode     : %s') % episode_name)
     546                                  # format table specific data columns
     547                                  # - ignore those, they are metadata, some
     548                                  #   are in clin.v_pat_items data already
     549                                  cols2ignore = [
     550                                          'pk_audit', 'row_version', 'modified_when', 'modified_by',
     551                                          'pk_item', 'id', 'fk_encounter', 'fk_episode'
     552                                  ]
     553                                  col_data = []
     554                                  for col_name in table_col_idx.keys():
     555                                          if col_name in cols2ignore:
     556                                                  continue
     557                                          emr_data[age].append("=> %s:" % col_name)
     558                                          emr_data[age].append(row[table_col_idx[col_name]])
     559                                  emr_data[age].append("----------------------------------------------------")
     560                                  emr_data[age].append("-- %s from table %s" % (
     561                                          view_row[view_col_idx['modified_string']],
     562                                          src_table
     563                                  ))
     564                                  emr_data[age].append("-- written %s by %s" % (
     565                                          view_row[view_col_idx['modified_when']],
     566                                          view_row[view_col_idx['modified_by']]
     567                                  ))
     568                                  emr_data[age].append("----------------------------------------------------")
     569                  curs.close()
     570                  self._conn_pool.ReleaseConnection('historica')
     571                  return emr_data
     572          #--------------------------------------------------------
     573 -        def get_text_dump(self, since=None, until=None, encounters=None, episodes=None, issues=None):
     574                  # don't know how to invalidate this by means of
     575                  # a notify without catching notifies from *all*
     576                  # child tables, the best solution would be if
     577                  # inserts in child tables would also fire triggers
     578                  # of ancestor tables, but oh well,
     579                  # until then the text dump will not be cached ...
     580                  try:
     581                          return self.__db_cache['text dump']
     582                  except KeyError:
     583                          pass
     584                  # not cached so go get it
     585                  # -- get the data --
     586                  fields = [
     587                          'age',
     588                          "to_char(modified_when, 'YYYY-MM-DD @ HH24:MI') as modified_when",
     589                          'modified_by',
     590                          'clin_when',
     591                          "case is_modified when false then '%s' else '%s' end as modified_string" % (_('original entry'), _('modified entry')),
     592                          'pk_item',
     593                          'pk_encounter',
     594                          'pk_episode',
     595                          'pk_health_issue',
     596                          'src_table'
     597                  ]
     598                  select_from = "SELECT %s FROM clin.v_pat_items" % ', '.join(fields)
     599                  # handle constraint conditions
     600                  where_snippets = []
     601                  params = {}
     602                  where_snippets.append('pk_patient=%(pat_id)s')
     603                  params['pat_id'] = self.pk_patient
     604                  if not since is None:
     605                          where_snippets.append('clin_when >= %(since)s')
     606                          params['since'] = since
     607                  if not until is None:
     608                          where_snippets.append('clin_when <= %(until)s')
     609                          params['until'] = until
     610                  # FIXME: these are interrelated, eg if we constrain encounter
     611                  # we automatically constrain issue/episode, so handle that,
     612                  # encounters
     613                  if not encounters is None and len(encounters) > 0:
     614                          params['enc'] = encounters
     615                          if len(encounters) > 1:
     616                                  where_snippets.append('fk_encounter in %(enc)s')
     617                          else:
     618                                  where_snippets.append('fk_encounter=%(enc)s')
     619                  # episodes
     620                  if not episodes is None and len(episodes) > 0:
     621                          params['epi'] = episodes
     622                          if len(episodes) > 1:
     623                                  where_snippets.append('fk_episode in %(epi)s')
     624                          else:
     625                                  where_snippets.append('fk_episode=%(epi)s')
     626                  # health issues
     627                  if not issues is None and len(issues) > 0:
     628                          params['issue'] = issues
     629                          if len(issues) > 1:
     630                                  where_snippets.append('fk_health_issue in %(issue)s')
     631                          else:
     632                                  where_snippets.append('fk_health_issue=%(issue)s')
     633
     634                  where_clause = ' and '.join(where_snippets)
     635                  order_by = 'order by src_table, age'
     636                  cmd = "%s WHERE %s %s" % (select_from, where_clause, order_by)
     637
     638                  rows, view_col_idx = gmPG.run_ro_query('historica', cmd, 1, params)
     639                  if rows is None:
     640                          _log.error('cannot load item links for patient [%s]' % self.pk_patient)
     641                          return None
     642
     643                  # -- sort the data --
     644                  # FIXME: by issue/encounter/episode, eg formatting
     645                  # aggregate by src_table for item retrieval
     646                  items_by_table = {}
     647                  for item in rows:
     648                          src_table = item[view_col_idx['src_table']]
     649                          pk_item = item[view_col_idx['pk_item']]
     650                          if not items_by_table.has_key(src_table):
     651                                  items_by_table[src_table] = {}
     652                          items_by_table[src_table][pk_item] = item
     653
     654                  # get mapping for issue/episode IDs
     655                  issues = self.get_health_issues()
     656                  issue_map = {}
     657                  for issue in issues:
     658                          issue_map[issue['pk_health_issue']] = issue['description']
     659                  episodes = self.get_episodes()
     660                  episode_map = {}
     661                  for episode in episodes:
     662                          episode_map[episode['pk_episode']] = episode['description']
     663                  emr_data = {}
     664                  # get item data from all source tables
     665                  ro_conn = self._conn_pool.GetConnection('historica')
     666                  curs = ro_conn.cursor()
     667                  for src_table in items_by_table.keys():
     668                          item_ids = items_by_table[src_table].keys()
     669                          # we don't know anything about the columns of
     670                          # the source tables but, hey, this is a dump
     671                          if len(item_ids) == 0:
     672                                  _log.info('no items in table [%s] ?!?' % src_table)
     673                                  continue
     674                          elif len(item_ids) == 1:
     675                                  cmd = "SELECT * FROM %s WHERE pk_item=%%s order by modified_when" % src_table
     676                                  if not gmPG.run_query(curs, None, cmd, item_ids[0]):
     677                                          _log.error('cannot load items from table [%s]' % src_table)
     678                                          # skip this table
     679                                          continue
     680                          elif len(item_ids) > 1:
     681                                  cmd = "SELECT * FROM %s WHERE pk_item in %%s order by modified_when" % src_table
     682                                  if not gmPG.run_query(curs, None, cmd, (tuple(item_ids),)):
     683                                          _log.error('cannot load items from table [%s]' % src_table)
     684                                          # skip this table
     685                                          continue
     686                          rows = curs.fetchall()
     687                          table_col_idx = gmPG.get_col_indices(curs)
     688                          # format per-table items
     689                          for row in rows:
     690                                  # FIXME: make this get_pkey_name()
     691                                  pk_item = row[table_col_idx['pk_item']]
     692                                  view_row = items_by_table[src_table][pk_item]
     693                                  age = view_row[view_col_idx['age']]
     694                                  # format metadata
     695                                  try:
     696                                          episode_name = episode_map[view_row[view_col_idx['pk_episode']]]
     697                                  except:
     698                                          episode_name = view_row[view_col_idx['pk_episode']]
     699                                  try:
     700                                          issue_name = issue_map[view_row[view_col_idx['pk_health_issue']]]
     701                                  except:
     702                                          issue_name = view_row[view_col_idx['pk_health_issue']]
     703
     704                                  if not emr_data.has_key(age):
     705                                          emr_data[age] = []
     706
     707                                  emr_data[age].append(
     708                                          _('%s: encounter (%s)') % (
     709                                                  view_row[view_col_idx['clin_when']],
     710                                                  view_row[view_col_idx['pk_encounter']]
     711                                          )
     712                                  )
     713                                  emr_data[age].append(_('health issue: %s') % issue_name)
     714                                  emr_data[age].append(_('episode     : %s') % episode_name)
     715                                  # format table specific data columns
     716                                  # - ignore those, they are metadata, some
     717                                  #   are in clin.v_pat_items data already
     718                                  cols2ignore = [
     719                                          'pk_audit', 'row_version', 'modified_when', 'modified_by',
     720                                          'pk_item', 'id', 'fk_encounter', 'fk_episode', 'pk'
     721                                  ]
     722                                  col_data = []
     723                                  for col_name in table_col_idx.keys():
     724                                          if col_name in cols2ignore:
     725                                                  continue
     726                                          emr_data[age].append("=> %s: %s" % (col_name, row[table_col_idx[col_name]]))
     727                                  emr_data[age].append("----------------------------------------------------")
     728                                  emr_data[age].append("-- %s from table %s" % (
     729                                          view_row[view_col_idx['modified_string']],
     730                                          src_table
     731                                  ))
     732                                  emr_data[age].append("-- written %s by %s" % (
     733                                          view_row[view_col_idx['modified_when']],
     734                                          view_row[view_col_idx['modified_by']]
     735                                  ))
     736                                  emr_data[age].append("----------------------------------------------------")
     737                  curs.close()
     738                  return emr_data
     739          #--------------------------------------------------------
     740 -        def get_patient_ID(self):
     741                  return self.pk_patient
     742          #--------------------------------------------------------
     743 -        def get_statistics(self):
     744                  union_query = u'\n      union all\n'.join ([
     745                          u"""
     746                                  SELECT ((
     747                                          -- all relevant health issues + active episodes WITH health issue
     748                                          SELECT COUNT(1)
     749                                          FROM clin.v_problem_list
     750                                          WHERE
     751                                                  pk_patient = %(pat)s
     752                                                          AND
     753                                                  pk_health_issue is not null
     754                                  ) + (
     755                                          -- active episodes WITHOUT health issue
     756                                          SELECT COUNT(1)
     757                                          FROM clin.v_problem_list
     758                                          WHERE
     759                                                  pk_patient = %(pat)s
     760                                                          AND
     761                                                  pk_health_issue is null
     762                                  ))""",
     763                          u'SELECT count(1) FROM clin.encounter WHERE fk_patient = %(pat)s',
     764                          u'SELECT count(1) FROM clin.v_pat_items WHERE pk_patient = %(pat)s',
     765                          u'SELECT count(1) FROM blobs.v_doc_med WHERE pk_patient = %(pat)s',
     766                          u'SELECT count(1) FROM clin.v_test_results WHERE pk_patient = %(pat)s',
     767                          u'SELECT count(1) FROM clin.v_pat_hospital_stays WHERE pk_patient = %(pat)s',
     768                          u'SELECT count(1) FROM clin.v_pat_procedures WHERE pk_patient = %(pat)s',
     769                          # active and approved substances == medication
     770                          u"""
     771                                  SELECT count(1)
     772                                  from clin.v_substance_intakes
     773                                  WHERE
     774                                          pk_patient = %(pat)s
     775                                          and is_currently_active in (null, true)
     776                                          and intake_is_approved_of in (null, true)""",
     777                          u'SELECT count(1) FROM clin.v_pat_vaccinations WHERE pk_patient = %(pat)s'
     778                  ])
     779
     780                  rows, idx = gmPG2.run_ro_queries (
     781                          queries = [{'cmd': union_query, 'args': {'pat': self.pk_patient}}],
     782                          get_col_idx = False
     783                  )
     784
     785                  stats = dict (
     786                          problems = rows[0][0],
     787                          encounters = rows[1][0],
     788                          items = rows[2][0],
     789                          documents = rows[3][0],
     790                          results = rows[4][0],
     791                          stays = rows[5][0],
     792                          procedures = rows[6][0],
     793                          active_drugs = rows[7][0],
     794                          vaccinations = rows[8][0]
     795                  )
     796
     797                  return stats
     798          #--------------------------------------------------------
     799 -        def format_statistics(self):
     800                  return _(
     801                          'Medical problems: %(problems)s\n'
     802                          'Total encounters: %(encounters)s\n'
     803                          'Total EMR entries: %(items)s\n'
     804                          'Active medications: %(active_drugs)s\n'
     805                          'Documents: %(documents)s\n'
     806                          'Test results: %(results)s\n'
     807                          'Hospitalizations: %(stays)s\n'
     808                          'Procedures: %(procedures)s\n'
     809                          'Vaccinations: %(vaccinations)s'
     810                  ) % self.get_statistics()
     811          #--------------------------------------------------------
     812 -        def format_summary(self, dob=None):
     813
     814                  stats = self.get_statistics()
     815                  first = self.get_first_encounter()
     816                  last = self.get_last_encounter()
     817                  probs = self.get_problems()
     818
     819                  txt = u''
     820                  if len(probs) > 0:
     821                          txt += _(' %s known problems, clinically relevant thereof:\n') % stats['problems']
     822                  else:
     823                          txt += _(' %s known problems\n') % stats['problems']
     824                  for prob in probs:
     825                          if not prob['clinically_relevant']:
     826                                  continue
     827                          txt += u'   \u00BB%s\u00AB (%s)\n' % (
     828                                  prob['problem'],
     829                                  gmTools.bool2subst(prob['problem_active'], _('active'), _('inactive'))
     830                          )
     831                  txt += u'\n'
     832                  txt += _(' %s encounters from %s to %s\n') % (
     833                          stats['encounters'],
     834                          gmDateTime.pydt_strftime(first['started'], '%Y %b %d'),
     835                          gmDateTime.pydt_strftime(last['started'], '%Y %b %d')
     836                  )
     837                  txt += _(' %s active medications\n') % stats['active_drugs']
     838                  txt += _(' %s documents\n') % stats['documents']
     839                  txt += _(' %s test results\n') % stats['results']
     840                  txt += _(' %s hospitalizations') % stats['stays']
     841                  if stats['stays'] == 0:
     842                          txt += u'\n'
     843                  else:
     844                          txt += _(', most recently:\n%s\n') % self.get_latest_hospital_stay().format(left_margin = 3)
     845                  # FIXME: perhaps only count "ongoing ones"
     846                  txt += _(' %s performed procedures') % stats['procedures']
     847                  if stats['procedures'] == 0:
     848                          txt += u'\n'
     849                  else:
     850                          txt += _(', most recently:\n%s\n') % self.get_latest_performed_procedure().format(left_margin = 3)
     851
     852                  txt += u'\n'
     853                  txt += _('Allergies and Intolerances\n')
     854
     855                  allg_state = self.allergy_state
     856                  txt += (u' ' + allg_state.state_string)
     857                  if allg_state['last_confirmed'] is not None:
     858                          txt += _(' (last confirmed %s)') % gmDateTime.pydt_strftime(allg_state['last_confirmed'], '%Y %b %d')
     859                  txt += u'\n'
     860                  txt += gmTools.coalesce(allg_state['comment'], u'', u' %s\n')
     861                  for allg in self.get_allergies():
     862                          txt += u' %s: %s\n' % (
     863                                  allg['descriptor'],
     864                                  gmTools.coalesce(allg['reaction'], _('unknown reaction'))
     865                          )
     866
     867                  txt += u'\n'
     868                  txt += _('Family History')
     869                  txt += u'\n'
     870                  fhx = self.get_family_history()
     871                  for f in fhx:
     872                          txt += u'%s\n' % f.format(left_margin = 1)
     873
     874                  txt += u'\n'
     875                  txt += _('Occupations')
     876                  txt += u'\n'
     877                  jobs = get_occupations(pk_identity = self.pk_patient)
     878                  for job in jobs:
     879                          txt += u' %s%s\n' % (
     880                                  job['l10n_occupation'],
     881                                  gmTools.coalesce(job['activities'], u'', u': %s')
     882                          )
     883
     884                  txt += u'\n'
     885                  txt += _('Vaccinations')
     886                  txt += u'\n'
     887                  vaccs = self.get_latest_vaccinations()
     888                  inds = sorted(vaccs.keys())
     889                  for ind in inds:
     890                          ind_count, vacc = vaccs[ind]
     891                          if dob is None:
     892                                  age_given = u''
     893                          else:
     894                                  age_given = u' @ %s' % gmDateTime.format_apparent_age_medically(gmDateTime.calculate_apparent_age (
     895                                          start = dob,
     896                                          end = vacc['date_given']
     897                                  ))
     898                          since = _('%s ago') % gmDateTime.format_interval_medically(vacc['interval_since_given'])
     899                          txt += u' %s (%s%s): %s%s (%s %s%s%s)\n' % (
     900                                  ind,
     901                                  gmTools.u_sum,
     902                                  ind_count,
     903                                  #gmDateTime.pydt_strftime(vacc['date_given'], '%b %Y'),
     904                                  since,
     905                                  age_given,
     906                                  vacc['vaccine'],
     907                                  gmTools.u_left_double_angle_quote,
     908                                  vacc['batch_no'],
     909                                  gmTools.u_right_double_angle_quote
     910                          )
     911
     912                  return txt
     913          #--------------------------------------------------------
     914 -        def format_as_journal(self, left_margin=0, patient=None):
     915                  txt = u''
     916                  for enc in self.get_encounters(skip_empty = True):
     917                          txt += gmTools.u_box_horiz_4dashes * 70 + u'\n'
     918                          txt += enc.format (
     919                                  episodes = None,                        # means: each touched upon
     920                                  left_margin = left_margin,
     921                                  patient = patient,
     922                                  fancy_header = False,
     923                                  with_soap = True,
     924                                  with_docs = True,
     925                                  with_tests = True,
     926                                  with_vaccinations = True,
     927                                  with_co_encountlet_hints = False,               # irrelevant
     928                                  with_rfe_aoe = True,
     929                                  with_family_history = True,
     930                                  by_episode = True
     931                          )
     932
     933                  return txt
     934          #--------------------------------------------------------
     935          # API: allergy
     936          #--------------------------------------------------------
     937 -        def get_allergies(self, remove_sensitivities=False, since=None, until=None, encounters=None, episodes=None, issues=None, ID_list=None):
     938                  """Retrieves patient allergy items.
     939
     940                          remove_sensitivities
     941                                  - retrieve real allergies only, without sensitivities
     942                          since
     943                                  - initial date for allergy items
     944                          until
     945                                  - final date for allergy items
     946                          encounters
     947                                  - list of encounters whose allergies are to be retrieved
     948                          episodes
     949                                  - list of episodes whose allergies are to be retrieved
     950                          issues
     951                                  - list of health issues whose allergies are to be retrieved
     952          """
     953                  cmd = u"SELECT * FROM clin.v_pat_allergies WHERE pk_patient=%s order by descriptor"
     954                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient]}], get_col_idx = True)
     955                  allergies = []
     956                  for r in rows:
     957                          allergies.append(gmAllergy.cAllergy(row = {'data': r, 'idx': idx, 'pk_field': 'pk_allergy'}))
     958
     959                  # ok, let's constrain our list
     960                  filtered_allergies = []
     961                  filtered_allergies.extend(allergies)
     962
     963                  if ID_list is not None:
     964                          filtered_allergies = filter(lambda allg: allg['pk_allergy'] in ID_list, filtered_allergies)
     965                          if len(filtered_allergies) == 0:
     966                                  _log.error('no allergies of list [%s] found for patient [%s]' % (str(ID_list), self.pk_patient))
     967                                  # better fail here contrary to what we do elsewhere
     968                                  return None
     969                          else:
     970                                  return filtered_allergies
     971
     972                  if remove_sensitivities:
     973                          filtered_allergies = filter(lambda allg: allg['type'] == 'allergy', filtered_allergies)
     974                  if since is not None:
     975                          filtered_allergies = filter(lambda allg: allg['date'] >= since, filtered_allergies)
     976                  if until is not None:
     977                          filtered_allergies = filter(lambda allg: allg['date'] < until, filtered_allergies)
     978                  if issues is not None:
     979                          filtered_allergies = filter(lambda allg: allg['pk_health_issue'] in issues, filtered_allergies)
     980                  if episodes is not None:
     981                          filtered_allergies = filter(lambda allg: allg['pk_episode'] in episodes, filtered_allergies)
     982                  if encounters is not None:
     983                          filtered_allergies = filter(lambda allg: allg['pk_encounter'] in encounters, filtered_allergies)
     984
     985                  return filtered_allergies
     986          #--------------------------------------------------------
     987 -        def add_allergy(self, allergene=None, allg_type=None, encounter_id=None, episode_id=None):
     988                  if encounter_id is None:
     989                          encounter_id = self.current_encounter['pk_encounter']
     990
     991                  if episode_id is None:
     992                          issue = self.add_health_issue(issue_name = _('Allergies/Intolerances'))
     993                          epi = self.add_episode(episode_name = _('Allergy detail: %s') % allergene, pk_health_issue = issue['pk_health_issue'])
     994                          episode_id = epi['pk_episode']
     995
     996                  new_allergy = gmAllergy.create_allergy (
     997                          allergene = allergene,
     998                          allg_type = allg_type,
     999                          encounter_id = encounter_id,
    1000                          episode_id = episode_id
    1001                  )
    1002
    1003                  return new_allergy
    1004          #--------------------------------------------------------
    1005 -        def delete_allergy(self, pk_allergy=None):
    1006                  cmd = u'delete FROM clin.allergy WHERE pk=%(pk_allg)s'
    1007                  args = {'pk_allg': pk_allergy}
    1008                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    1009          #--------------------------------------------------------
    1010 -        def is_allergic_to(self, atcs=None, inns=None, brand=None):
    1011                  """Cave: only use with one potential allergic agent
    1012                  otherwise you won't know which of the agents the allergy is to."""
    1013
    1014                  # we don't know the state
    1015                  if self.allergy_state is None:
    1016                          return None
    1017
    1018                  # we know there's no allergies
    1019                  if self.allergy_state == 0:
    1020                          return False
    1021
    1022                  args = {
    1023                          'atcs': atcs,
    1024                          'inns': inns,
    1025                          'brand': brand,
    1026                          'pat': self.pk_patient
    1027                  }
    1028                  allergenes = []
    1029                  where_parts = []
    1030
    1031                  if len(atcs) == 0:
    1032                          atcs = None
    1033                  if atcs is not None:
    1034                          where_parts.append(u'atc_code in %(atcs)s')
    1035                  if len(inns) == 0:
    1036                          inns = None
    1037                  if inns is not None:
    1038                          where_parts.append(u'generics in %(inns)s')
    1039                          allergenes.extend(inns)
    1040                  if brand is not None:
    1041                          where_parts.append(u'substance = %(brand)s')
    1042                          allergenes.append(brand)
    1043
    1044                  if len(allergenes) != 0:
    1045                          where_parts.append(u'allergene in %(allgs)s')
    1046                          args['allgs'] = tuple(allergenes)
    1047
    1048                  cmd = u"""
    1049  SELECT * FROM clin.v_pat_allergies
    1050  WHERE
    1051          pk_patient = %%(pat)s
    1052          AND ( %s )""" % u' OR '.join(where_parts)
    1053
    1054                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1055
    1056                  if len(rows) == 0:
    1057                          return False
    1058
    1059                  return gmAllergy.cAllergy(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_allergy'})
    1060          #--------------------------------------------------------
    1061 -        def _set_allergy_state(self, state):
    1062
    1063                  if state not in gmAllergy.allergy_states:
    1064                          raise ValueError('[%s].__set_allergy_state(): <state> must be one of %s' % (self.__class__.__name__, gmAllergy.allergy_states))
    1065
    1066                  allg_state = gmAllergy.ensure_has_allergy_state(encounter = self.current_encounter['pk_encounter'])
    1067                  allg_state['has_allergy'] = state
    1068                  allg_state.save_payload()
    1069                  return True
    1070
    1071 -        def _get_allergy_state(self):
    1072                  return gmAllergy.ensure_has_allergy_state(encounter = self.current_encounter['pk_encounter'])
    1073
    1074          allergy_state = property(_get_allergy_state, _set_allergy_state)
    1075          #--------------------------------------------------------
    1076          # API: episodes
    1077          #--------------------------------------------------------
    1078 -        def get_episodes(self, id_list=None, issues=None, open_status=None, order_by=None, unlinked_only=False):
    1079                  """Fetches from backend patient episodes.
    1080
    1081                  id_list - Episodes' PKs list
    1082                  issues - Health issues' PKs list to filter episodes by
    1083                  open_status - return all (None) episodes, only open (True) or closed (False) one(s)
    1084                  """
    1085                  if (unlinked_only is True) and (issues is not None):
    1086                          raise ValueError('<unlinked_only> cannot be TRUE if <issues> is not None')
    1087
    1088                  if order_by is None:
    1089                          order_by = u''
    1090                  else:
    1091                          order_by = u'ORDER BY %s' % order_by
    1092
    1093                  args = {
    1094                          'pat': self.pk_patient,
    1095                          'open': open_status
    1096                  }
    1097                  where_parts = [u'pk_patient = %(pat)s']
    1098
    1099                  if open_status is not None:
    1100                          where_parts.append(u'episode_open IS %(open)s')
    1101
    1102                  if unlinked_only:
    1103                          where_parts.append(u'pk_health_issue is NULL')
    1104
    1105                  if issues is not None:
    1106                          where_parts.append(u'pk_health_issue IN %(issues)s')
    1107                          args['issues'] = tuple(issues)
    1108
    1109                  if id_list is not None:
    1110                          where_parts.append(u'pk_episode IN %(epis)s')
    1111                          args['epis'] = tuple(id_list)
    1112
    1113                  cmd = u"SELECT * FROM clin.v_pat_episodes WHERE %s %s" % (
    1114                          u' AND '.join(where_parts),
    1115                          order_by
    1116                  )
    1117                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1118
    1119                  return [ gmEMRStructItems.cEpisode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_episode'}) for r in rows ]
    1120
    1121          episodes = property(get_episodes, lambda x:x)
    1122          #------------------------------------------------------------------
    1123 -        def get_unlinked_episodes(self, open_status=None, order_by=None):
    1124                  return self.get_episodes(open_status = open_status, order_by = order_by, unlinked_only = True)
    1125
    1126          unlinked_episodes = property(get_unlinked_episodes, lambda x:x)
    1127          #------------------------------------------------------------------
    1128 -        def get_episodes_by_encounter(self, pk_encounter=None):
    1129                  cmd = u"""SELECT distinct pk_episode
    1130                                          from clin.v_pat_items
    1131                                          WHERE pk_encounter=%(enc)s and pk_patient=%(pat)s"""
    1132                  args = {
    1133                          'enc': gmTools.coalesce(pk_encounter, self.current_encounter['pk_encounter']),
    1134                          'pat': self.pk_patient
    1135                  }
    1136                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
    1137                  if len(rows) == 0:
    1138                          return []
    1139                  epis = []
    1140                  for row in rows:
    1141                          epis.append(row[0])
    1142                  return self.get_episodes(id_list=epis)
    1143          #------------------------------------------------------------------
    1144 -        def add_episode(self, episode_name=None, pk_health_issue=None, is_open=False):
    1145                  """Add episode 'episode_name' for a patient's health issue.
    1146
    1147                  - silently returns if episode already exists
    1148                  """
    1149                  episode = gmEMRStructItems.create_episode (
    1150                          pk_health_issue = pk_health_issue,
    1151                          episode_name = episode_name,
    1152                          is_open = is_open,
    1153                          encounter = self.current_encounter['pk_encounter']
    1154                  )
    1155                  return episode
    1156          #--------------------------------------------------------
    1157 -        def get_most_recent_episode(self, issue=None):
    1158                  # try to find the episode with the most recently modified clinical item
    1159
    1160                  issue_where = gmTools.coalesce(issue, u'', u'and pk_health_issue = %(issue)s')
    1161
    1162                  cmd = u"""
    1163  SELECT pk
    1164  from clin.episode
    1165  WHERE pk = (
    1166          SELECT distinct on(pk_episode) pk_episode
    1167          from clin.v_pat_items
    1168          WHERE
    1169                  pk_patient = %%(pat)s
    1170                          and
    1171                  modified_when = (
    1172                          SELECT max(vpi.modified_when)
    1173                          from clin.v_pat_items vpi
    1174                          WHERE vpi.pk_patient = %%(pat)s
    1175                  )
    1176                  %s
    1177          -- guard against several episodes created at the same moment of time
    1178          limit 1
    1179          )""" % issue_where
    1180                  rows, idx = gmPG2.run_ro_queries(queries = [
    1181                          {'cmd': cmd, 'args': {'pat': self.pk_patient, 'issue': issue}}
    1182                  ])
    1183                  if len(rows) != 0:
    1184                          return gmEMRStructItems.cEpisode(aPK_obj=rows[0][0])
    1185
    1186                  # no clinical items recorded, so try to find
    1187                  # the youngest episode for this patient
    1188                  cmd = u"""
    1189  SELECT vpe0.pk_episode
    1190  from
    1191          clin.v_pat_episodes vpe0
    1192  WHERE
    1193          vpe0.pk_patient = %%(pat)s
    1194                  and
    1195          vpe0.episode_modified_when = (
    1196                  SELECT max(vpe1.episode_modified_when)
    1197                  from clin.v_pat_episodes vpe1
    1198                  WHERE vpe1.pk_episode = vpe0.pk_episode
    1199          )
    1200          %s""" % issue_where
    1201                  rows, idx = gmPG2.run_ro_queries(queries = [
    1202                          {'cmd': cmd, 'args': {'pat': self.pk_patient, 'issue': issue}}
    1203                  ])
    1204                  if len(rows) != 0:
    1205                          return gmEMRStructItems.cEpisode(aPK_obj=rows[0][0])
    1206
    1207                  return None
    1208          #--------------------------------------------------------
    1209 -        def episode2problem(self, episode=None):
    1210                  return gmEMRStructItems.episode2problem(episode=episode)
    1211          #--------------------------------------------------------
    1212          # API: problems
    1213          #--------------------------------------------------------
    1214 -        def get_problems(self, episodes=None, issues=None, include_closed_episodes=False, include_irrelevant_issues=False):
    1215                  """Retrieve a patient's problems.
    1216
    1217                  "Problems" are the UNION of:
    1218
    1219                          - issues which are .clinically_relevant
    1220                          - episodes which are .is_open
    1221
    1222                  Therefore, both an issue and the open episode
    1223                  thereof can each be listed as a problem.
    1224
    1225                  include_closed_episodes/include_irrelevant_issues will
    1226                  include those -- which departs from the definition of
    1227                  the problem list being "active" items only ...
    1228
    1229                  episodes - episodes' PKs to filter problems by
    1230                  issues - health issues' PKs to filter problems by
    1231                  """
    1232                  # FIXME: this could use a good measure of streamlining, probably
    1233
    1234                  args = {'pat': self.pk_patient}
    1235
    1236                  cmd = u"""SELECT pk_health_issue, pk_episode FROM clin.v_problem_list WHERE pk_patient = %(pat)s ORDER BY problem"""
    1237                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
    1238
    1239                  # Instantiate problem items
    1240                  problems = []
    1241                  for row in rows:
    1242                          pk_args = {
    1243                                  u'pk_patient': self.pk_patient,
    1244                                  u'pk_health_issue': row['pk_health_issue'],
    1245                                  u'pk_episode': row['pk_episode']
    1246                          }
    1247                          problems.append(gmEMRStructItems.cProblem(aPK_obj = pk_args, try_potential_problems = False))
    1248
    1249                  # include non-problems ?
    1250                  other_rows = []
    1251                  if include_closed_episodes:
    1252                          cmd = u"""SELECT pk_health_issue, pk_episode FROM clin.v_potential_problem_list WHERE pk_patient = %(pat)s and type = 'episode'"""
    1253                          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
    1254                          other_rows.extend(rows)
    1255
    1256                  if include_irrelevant_issues:
    1257                          cmd = u"""SELECT pk_health_issue, pk_episode FROM clin.v_potential_problem_list WHERE pk_patient = %(pat)s and type = 'health issue'"""
    1258                          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
    1259                          other_rows.extend(rows)
    1260
    1261                  if len(other_rows) > 0:
    1262                          for row in other_rows:
    1263                                  pk_args = {
    1264                                          u'pk_patient': self.pk_patient,
    1265                                          u'pk_health_issue': row['pk_health_issue'],
    1266                                          u'pk_episode': row['pk_episode']
    1267                                  }
    1268                                  problems.append(gmEMRStructItems.cProblem(aPK_obj = pk_args, try_potential_problems = True))
    1269
    1270                  # filter ?
    1271                  if (episodes is None) and (issues is None):
    1272                          return problems
    1273
    1274                  # filter
    1275                  if issues is not None:
    1276                          problems = filter(lambda epi: epi['pk_health_issue'] in issues, problems)
    1277                  if episodes is not None:
    1278                          problems = filter(lambda epi: epi['pk_episode'] in episodes, problems)
    1279
    1280                  return problems
    1281          #--------------------------------------------------------
    1282 -        def problem2episode(self, problem=None):
    1283                  return gmEMRStructItems.problem2episode(problem = problem)
    1284          #--------------------------------------------------------
    1285 -        def problem2issue(self, problem=None):
    1286                  return gmEMRStructItems.problem2issue(problem = problem)
    1287          #--------------------------------------------------------
    1288 -        def reclass_problem(self, problem):
    1289                  return gmEMRStructItems.reclass_problem(problem = problem)
    1290          #--------------------------------------------------------
    1291          # API: health issues
    1292          #--------------------------------------------------------
    1293 -        def get_health_issues(self, id_list = None):
    1294
    1295                  cmd = u"SELECT *, xmin_health_issue FROM clin.v_health_issues WHERE pk_patient = %(pat)s ORDER BY description"
    1296                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pat': self.pk_patient}}], get_col_idx = True)
    1297                  issues = [ gmEMRStructItems.cHealthIssue(row = {'idx': idx, 'data': r, 'pk_field': 'pk_health_issue'}) for r in rows ]
    1298
    1299                  if id_list is None:
    1300                          return issues
    1301
    1302                  if len(id_list) == 0:
    1303                          raise ValueError('id_list to filter by is empty, most likely a programming error')
    1304
    1305                  filtered_issues = []
    1306                  for issue in issues:
    1307                          if issue['pk_health_issue'] in id_list:
    1308                                  filtered_issues.append(issue)
    1309
    1310                  return filtered_issues
    1311
    1312          health_issues = property(get_health_issues, lambda x:x)
    1313          #------------------------------------------------------------------
    1314 -        def add_health_issue(self, issue_name=None):
    1315                  """Adds patient health issue."""
    1316                  return gmEMRStructItems.create_health_issue (
    1317                          description = issue_name,
    1318                          encounter = self.current_encounter['pk_encounter'],
    1319                          patient = self.pk_patient
    1320                  )
    1321          #--------------------------------------------------------
    1322 -        def health_issue2problem(self, issue=None):
    1323                  return gmEMRStructItems.health_issue2problem(issue = issue)
    1324          #--------------------------------------------------------
    1325          # API: substance intake
    1326          #--------------------------------------------------------
    1327 -        def get_current_substance_intakes(self, include_inactive=True, include_unapproved=False, order_by=None, episodes=None, issues=None):
    1328
    1329                  where_parts = [u'pk_patient = %(pat)s']
    1330                  args = {'pat': self.pk_patient}
    1331
    1332                  if not include_inactive:
    1333                          where_parts.append(u'is_currently_active IN (true, null)')
    1334
    1335                  if not include_unapproved:
    1336                          where_parts.append(u'intake_is_approved_of IN (true, null)')
    1337
    1338                  if order_by is None:
    1339                          order_by = u''
    1340                  else:
    1341                          order_by = u'ORDER BY %s' % order_by
    1342
    1343                  cmd = u"SELECT * FROM clin.v_substance_intakes WHERE %s %s" % (
    1344                          u'\nAND '.join(where_parts),
    1345                          order_by
    1346                  )
    1347                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1348                  meds = [ gmMedication.cSubstanceIntakeEntry(row = {'idx': idx, 'data': r, 'pk_field': 'pk_substance_intake'})  for r in rows ]
    1349
    1350                  if episodes is not None:
    1351                          meds = filter(lambda s: s['pk_episode'] in episodes, meds)
    1352
    1353                  if issues is not None:
    1354                          meds = filter(lambda s: s['pk_health_issue'] in issues, meds)
    1355
    1356                  return meds
    1357          #--------------------------------------------------------
    1358 -        def add_substance_intake(self, pk_substance=None, pk_component=None, episode=None, preparation=None):
    1359                  return gmMedication.create_substance_intake (
    1360                          pk_substance = pk_substance,
    1361                          pk_component = pk_component,
    1362                          encounter = self.current_encounter['pk_encounter'],
    1363                          episode = episode,
    1364                          preparation = preparation
    1365                  )
    1366          #--------------------------------------------------------
    1367 -        def substance_intake_exists(self, pk_component=None, pk_substance=None):
    1368                  return gmMedication.substance_intake_exists (
    1369                          pk_component = pk_component,
    1370                          pk_substance = pk_substance,
    1371                          pk_identity = self.pk_patient
    1372                  )
    1373          #--------------------------------------------------------
    1374          # API: vaccinations
    1375          #--------------------------------------------------------
    1376 -        def add_vaccination(self, episode=None, vaccine=None, batch_no=None):
    1377                  return gmVaccination.create_vaccination (
    1378                          encounter = self.current_encounter['pk_encounter'],
    1379                          episode = episode,
    1380                          vaccine = vaccine,
    1381                          batch_no = batch_no
    1382                  )
    1383          #--------------------------------------------------------
    1384 -        def get_latest_vaccinations(self, episodes=None, issues=None):
    1385                  """Returns latest given vaccination for each vaccinated indication.
    1386
    1387                  as a dict {'l10n_indication': cVaccination instance}
    1388
    1389                  Note that this will produce duplicate vaccination instances on combi-indication vaccines !
    1390                  """
    1391                  # find the PKs
    1392                  args = {'pat': self.pk_patient}
    1393                  where_parts = [u'pk_patient = %(pat)s']
    1394
    1395                  if (episodes is not None) and (len(episodes) > 0):
    1396                          where_parts.append(u'pk_episode IN %(epis)s')
    1397                          args['epis'] = tuple(episodes)
    1398
    1399                  if (issues is not None) and (len(issues) > 0):
    1400                          where_parts.append(u'pk_episode IN (select pk from clin.episode where fk_health_issue IN %(issues)s)')
    1401                          args['issues'] = tuple(issues)
    1402
    1403                  cmd = u'SELECT pk_vaccination, l10n_indication, indication_count FROM clin.v_pat_last_vacc4indication WHERE %s' % u'\nAND '.join(where_parts)
    1404                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
    1405
    1406                  # none found
    1407                  if len(rows) == 0:
    1408                          return {}
    1409
    1410                  vpks = [ ind['pk_vaccination'] for ind in rows ]
    1411                  vinds = [ ind['l10n_indication'] for ind in rows ]
    1412                  ind_counts = [ ind['indication_count'] for ind in rows ]
    1413
    1414                  # turn them into vaccinations
    1415                  cmd = gmVaccination.sql_fetch_vaccination % u'pk_vaccination IN %(pks)s'
    1416                  args = {'pks': tuple(vpks)}
    1417                  rows, row_idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1418
    1419                  vaccs = {}
    1420                  for idx in range(len(vpks)):
    1421                          pk = vpks[idx]
    1422                          ind_count = ind_counts[idx]
    1423                          for r in rows:
    1424                                  if r['pk_vaccination'] == pk:
    1425                                          vaccs[vinds[idx]] = (ind_count, gmVaccination.cVaccination(row = {'idx': row_idx, 'data': r, 'pk_field': 'pk_vaccination'}))
    1426
    1427                  return vaccs
    1428          #--------------------------------------------------------
    1429 -        def get_vaccinations(self, order_by=None, episodes=None, issues=None, encounters=None):
    1430
    1431                  args = {'pat': self.pk_patient}
    1432                  where_parts = [u'pk_patient = %(pat)s']
    1433
    1434                  if order_by is None:
    1435                          order_by = u''
    1436                  else:
    1437                          order_by = u'ORDER BY %s' % order_by
    1438
    1439                  if (episodes is not None) and (len(episodes) > 0):
    1440                          where_parts.append(u'pk_episode IN %(epis)s')
    1441                          args['epis'] = tuple(episodes)
    1442
    1443                  if (issues is not None) and (len(issues) > 0):
    1444                          where_parts.append(u'pk_episode IN (SELECT pk FROM clin.episode WHERE fk_health_issue IN %(issues)s)')
    1445                          args['issues'] = tuple(issues)
    1446
    1447                  if (encounters is not None) and (len(encounters) > 0):
    1448                          where_parts.append(u'pk_encounter IN %(encs)s')
    1449                          args['encs'] = tuple(encounters)
    1450
    1451                  cmd = u'%s %s' % (
    1452                          gmVaccination.sql_fetch_vaccination % u'\nAND '.join(where_parts),
    1453                          order_by
    1454                  )
    1455                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1456                  vaccs = [ gmVaccination.cVaccination(row = {'idx': idx, 'data': r, 'pk_field': 'pk_vaccination'})  for r in rows ]
    1457
    1458                  return vaccs
    1459
    1460          vaccinations = property(get_vaccinations, lambda x:x)
    1461          #--------------------------------------------------------
    1462          # old/obsolete:
    1463          #--------------------------------------------------------
    1464 -        def get_scheduled_vaccination_regimes(self, ID=None, indications=None):
    1465                  """Retrieves vaccination regimes the patient is on.
    1466
    1467                          optional:
    1468                          * ID - PK of the vaccination regime
    1469                          * indications - indications we want to retrieve vaccination
    1470                                  regimes for, must be primary language, not l10n_indication
    1471                  """
    1472                  # FIXME: use course, not regime
    1473                  try:
    1474                          self.__db_cache['vaccinations']['scheduled regimes']
    1475                  except KeyError:
    1476                          # retrieve vaccination regimes definitions
    1477                          self.__db_cache['vaccinations']['scheduled regimes'] = []
    1478                          cmd = """SELECT distinct on(pk_course) pk_course
    1479                                           FROM clin.v_vaccs_scheduled4pat
    1480                                           WHERE pk_patient=%s"""
    1481                          rows = gmPG.run_ro_query('historica', cmd, None, self.pk_patient)
    1482                          if rows is None:
    1483                                  _log.error('cannot retrieve scheduled vaccination courses')
    1484                                  del self.__db_cache['vaccinations']['scheduled regimes']
    1485                                  return None
    1486                          # Instantiate vaccination items and keep cache
    1487                          for row in rows:
    1488                                  self.__db_cache['vaccinations']['scheduled regimes'].append(gmVaccination.cVaccinationCourse(aPK_obj=row[0]))
    1489
    1490                  # ok, let's constrain our list
    1491                  filtered_regimes = []
    1492                  filtered_regimes.extend(self.__db_cache['vaccinations']['scheduled regimes'])
    1493                  if ID is not None:
    1494                          filtered_regimes = filter(lambda regime: regime['pk_course'] == ID, filtered_regimes)
    1495                          if len(filtered_regimes) == 0:
    1496                                  _log.error('no vaccination course [%s] found for patient [%s]' % (ID, self.pk_patient))
    1497                                  return []
    1498                          else:
    1499                                  return filtered_regimes[0]
    1500                  if indications is not None:
    1501                          filtered_regimes = filter(lambda regime: regime['indication'] in indications, filtered_regimes)
    1502
    1503                  return filtered_regimes
    1504          #--------------------------------------------------------
    1505  #       def get_vaccinated_indications(self):
    1506  #               """Retrieves patient vaccinated indications list.
    1507  #
    1508  #               Note that this does NOT rely on the patient being on
    1509  #               some schedule or other but rather works with what the
    1510  #               patient has ACTUALLY been vaccinated against. This is
    1511  #               deliberate !
    1512  #               """
    1513  #               # most likely, vaccinations will be fetched close
    1514  #               # by so it makes sense to count on the cache being
    1515  #               # filled (or fill it for nearby use)
    1516  #               vaccinations = self.get_vaccinations()
    1517  #               if vaccinations is None:
    1518  #                       _log.error('cannot load vaccinated indications for patient [%s]' % self.pk_patient)
    1519  #                       return (False, [[_('ERROR: cannot retrieve vaccinated indications'), _('ERROR: cannot retrieve vaccinated indications')]])
    1520  #               if len(vaccinations) == 0:
    1521  #                       return (True, [[_('no vaccinations recorded'), _('no vaccinations recorded')]])
    1522  #               v_indications = []
    1523  #               for vacc in vaccinations:
    1524  #                       tmp = [vacc['indication'], vacc['l10n_indication']]
    1525  #                       # remove duplicates
    1526  #                       if tmp in v_indications:
    1527  #                               continue
    1528  #                       v_indications.append(tmp)
    1529  #               return (True, v_indications)
    1530          #--------------------------------------------------------
    1531 -        def get_vaccinations_old(self, ID=None, indications=None, since=None, until=None, encounters=None, episodes=None, issues=None):
    1532                  """Retrieves list of vaccinations the patient has received.
    1533
    1534                  optional:
    1535                  * ID - PK of a vaccination
    1536                  * indications - indications we want to retrieve vaccination
    1537                          items for, must be primary language, not l10n_indication
    1538          * since - initial date for allergy items
    1539          * until - final date for allergy items
    1540          * encounters - list of encounters whose allergies are to be retrieved
    1541          * episodes - list of episodes whose allergies are to be retrieved
    1542          * issues - list of health issues whose allergies are to be retrieved
    1543                  """
    1544                  try:
    1545                          self.__db_cache['vaccinations']['vaccinated']
    1546                  except KeyError:
    1547                          self.__db_cache['vaccinations']['vaccinated'] = []
    1548                          # Important fetch ordering by indication, date to know if a vaccination is booster
    1549                          cmd= """SELECT * FROM clin.v_pat_vaccinations4indication
    1550                                          WHERE pk_patient=%s
    1551                                          order by indication, date"""
    1552                          rows, idx  = gmPG.run_ro_query('historica', cmd, True, self.pk_patient)
    1553                          if rows is None:
    1554                                  _log.error('cannot load given vaccinations for patient [%s]' % self.pk_patient)
    1555                                  del self.__db_cache['vaccinations']['vaccinated']
    1556                                  return None
    1557                          # Instantiate vaccination items
    1558                          vaccs_by_ind = {}
    1559                          for row in rows:
    1560                                  vacc_row = {
    1561                                          'pk_field': 'pk_vaccination',
    1562                                          'idx': idx,
    1563                                          'data': row
    1564                                  }
    1565                                  vacc = gmVaccination.cVaccination(row=vacc_row)
    1566                                  self.__db_cache['vaccinations']['vaccinated'].append(vacc)
    1567                                  # keep them, ordered by indication
    1568                                  try:
    1569                                          vaccs_by_ind[vacc['indication']].append(vacc)
    1570                                  except KeyError:
    1571                                          vaccs_by_ind[vacc['indication']] = [vacc]
    1572
    1573                          # calculate sequence number and is_booster
    1574                          for ind in vaccs_by_ind.keys():
    1575                                  vacc_regimes = self.get_scheduled_vaccination_regimes(indications = [ind])
    1576                                  for vacc in vaccs_by_ind[ind]:
    1577                                          # due to the "order by indication, date" the vaccinations are in the
    1578                                          # right temporal order inside the indication-keyed dicts
    1579                                          seq_no = vaccs_by_ind[ind].index(vacc) + 1
    1580                                          vacc['seq_no'] = seq_no
    1581                                          # if no active schedule for indication we cannot
    1582                                          # check for booster status (eg. seq_no > max_shot)
    1583                                          if (vacc_regimes is None) or (len(vacc_regimes) == 0):
    1584                                                  continue
    1585                                          if seq_no > vacc_regimes[0]['shots']:
    1586                                                  vacc['is_booster'] = True
    1587                          del vaccs_by_ind
    1588
    1589                  # ok, let's constrain our list
    1590                  filtered_shots = []
    1591                  filtered_shots.extend(self.__db_cache['vaccinations']['vaccinated'])
    1592                  if ID is not None:
    1593                          filtered_shots = filter(lambda shot: shot['pk_vaccination'] == ID, filtered_shots)
    1594                          if len(filtered_shots) == 0:
    1595                                  _log.error('no vaccination [%s] found for patient [%s]' % (ID, self.pk_patient))
    1596                                  return None
    1597                          else:
    1598                                  return filtered_shots[0]
    1599                  if since is not None:
    1600                          filtered_shots = filter(lambda shot: shot['date'] >= since, filtered_shots)
    1601                  if until is not None:
    1602                          filtered_shots = filter(lambda shot: shot['date'] < until, filtered_shots)
    1603                  if issues is not None:
    1604                          filtered_shots = filter(lambda shot: shot['pk_health_issue'] in issues, filtered_shots)
    1605                  if episodes is not None:
    1606                          filtered_shots = filter(lambda shot: shot['pk_episode'] in episodes, filtered_shots)
    1607                  if encounters is not None:
    1608                          filtered_shots = filter(lambda shot: shot['pk_encounter'] in encounters, filtered_shots)
    1609                  if indications is not None:
    1610                          filtered_shots = filter(lambda shot: shot['indication'] in indications, filtered_shots)
    1611                  return filtered_shots
    1612          #--------------------------------------------------------
    1613 -        def get_scheduled_vaccinations(self, indications=None):
    1614                  """Retrieves vaccinations scheduled for a regime a patient is on.
    1615
    1616                  The regime is referenced by its indication (not l10n)
    1617
    1618                  * indications - List of indications (not l10n) of regimes we want scheduled
    1619                                  vaccinations to be fetched for
    1620                  """
    1621                  try:
    1622                          self.__db_cache['vaccinations']['scheduled']
    1623                  except KeyError:
    1624                          self.__db_cache['vaccinations']['scheduled'] = []
    1625                          cmd = """SELECT * FROM clin.v_vaccs_scheduled4pat WHERE pk_patient=%s"""
    1626                          rows, idx = gmPG.run_ro_query('historica', cmd, True, self.pk_patient)
    1627                          if rows is None:
    1628                                  _log.error('cannot load scheduled vaccinations for patient [%s]' % self.pk_patient)
    1629                                  del self.__db_cache['vaccinations']['scheduled']
    1630                                  return None
    1631                          # Instantiate vaccination items
    1632                          for row in rows:
    1633                                  vacc_row = {
    1634                                          'pk_field': 'pk_vacc_def',
    1635                                          'idx': idx,
    1636                                          'data': row
    1637                                  }
    1638                                  self.__db_cache['vaccinations']['scheduled'].append(gmVaccination.cScheduledVaccination(row = vacc_row))
    1639
    1640                  # ok, let's constrain our list
    1641                  if indications is None:
    1642                          return self.__db_cache['vaccinations']['scheduled']
    1643                  filtered_shots = []
    1644                  filtered_shots.extend(self.__db_cache['vaccinations']['scheduled'])
    1645                  filtered_shots = filter(lambda shot: shot['indication'] in indications, filtered_shots)
    1646                  return filtered_shots
    1647          #--------------------------------------------------------
    1648 -        def get_missing_vaccinations(self, indications=None):
    1649                  try:
    1650                          self.__db_cache['vaccinations']['missing']
    1651                  except KeyError:
    1652                          self.__db_cache['vaccinations']['missing'] = {}
    1653                          # 1) non-booster
    1654                          self.__db_cache['vaccinations']['missing']['due'] = []
    1655                          # get list of (indication, seq_no) tuples
    1656                          cmd = "SELECT indication, seq_no FROM clin.v_pat_missing_vaccs WHERE pk_patient=%s"
    1657                          rows = gmPG.run_ro_query('historica', cmd, None, self.pk_patient)
    1658                          if rows is None:
    1659                                  _log.error('error loading (indication, seq_no) for due/overdue vaccinations for patient [%s]' % self.pk_patient)
    1660                                  return None
    1661                          pk_args = {'pat_id': self.pk_patient}
    1662                          if rows is not None:
    1663                                  for row in rows:
    1664                                          pk_args['indication'] = row[0]
    1665                                          pk_args['seq_no'] = row[1]
    1666                                          self.__db_cache['vaccinations']['missing']['due'].append(gmVaccination.cMissingVaccination(aPK_obj=pk_args))
    1667
    1668                          # 2) boosters
    1669                          self.__db_cache['vaccinations']['missing']['boosters'] = []
    1670                          # get list of indications
    1671                          cmd = "SELECT indication, seq_no FROM clin.v_pat_missing_boosters WHERE pk_patient=%s"
    1672                          rows = gmPG.run_ro_query('historica', cmd, None, self.pk_patient)
    1673                          if rows is None:
    1674                                  _log.error('error loading indications for missing boosters for patient [%s]' % self.pk_patient)
    1675                                  return None
    1676                          pk_args = {'pat_id': self.pk_patient}
    1677                          if rows is not None:
    1678                                  for row in rows:
    1679                                          pk_args['indication'] = row[0]
    1680                                          self.__db_cache['vaccinations']['missing']['boosters'].append(gmVaccination.cMissingBooster(aPK_obj=pk_args))
    1681
    1682                  # if any filters ...
    1683                  if indications is None:
    1684                          return self.__db_cache['vaccinations']['missing']
    1685                  if len(indications) == 0:
    1686                          return self.__db_cache['vaccinations']['missing']
    1687                  # ... apply them
    1688                  filtered_shots = {
    1689                          'due': [],
    1690                          'boosters': []
    1691                  }
    1692                  for due_shot in self.__db_cache['vaccinations']['missing']['due']:
    1693                          if due_shot['indication'] in indications: #and due_shot not in filtered_shots['due']:
    1694                                  filtered_shots['due'].append(due_shot)
    1695                  for due_shot in self.__db_cache['vaccinations']['missing']['boosters']:
    1696                          if due_shot['indication'] in indications: #and due_shot not in filtered_shots['boosters']:
    1697                                  filtered_shots['boosters'].append(due_shot)
    1698                  return filtered_shots
    1699          #------------------------------------------------------------------
    1700          # API: encounters
    1701          #------------------------------------------------------------------
    1702 -        def _get_current_encounter(self):
    1703                  return self.__encounter
    1704
    1705 -        def _set_current_encounter(self, encounter):
    1706
    1707                  # first ever setting ?
    1708                  if self.__encounter is None:
    1709                          _log.debug('first setting of active encounter in this clinical record instance')
    1710                  else:
    1711                          _log.debug('switching of active encounter')
    1712                          # fail if the currently active encounter has unsaved changes
    1713                          if self.__encounter.is_modified():
    1714                                  _log.debug('unsaved changes in active encounter, cannot switch to another one')
    1715                                  raise ValueError('unsaved changes in active encounter, cannot switch to another one')
    1716
    1717                  # set the currently active encounter and announce that change
    1718                  if encounter['started'].strftime('%Y-%m-%d %H:%M') == encounter['last_affirmed'].strftime('%Y-%m-%d %H:%M'):
    1719                          now = gmDateTime.pydt_now_here()
    1720                          if now > encounter['started']:
    1721                                  encounter['last_affirmed'] = now                # this will trigger an "encounter_mod_db"
    1722                                  encounter.save()
    1723                  self.__encounter = encounter
    1724                  gmDispatcher.send(u'current_encounter_switched')
    1725
    1726                  return True
    1727
    1728          current_encounter = property(_get_current_encounter, _set_current_encounter)
    1729          active_encounter = property(_get_current_encounter, _set_current_encounter)
    1730          #------------------------------------------------------------------
    1731 -        def __initiate_active_encounter(self, allow_user_interaction=True):
    1732
    1733                  # 1) "very recent" encounter recorded ?
    1734                  if self.__activate_very_recent_encounter():
    1735                          return True
    1736
    1737                  # 2) "fairly recent" encounter recorded ?
    1738                  if self.__activate_fairly_recent_encounter(allow_user_interaction = allow_user_interaction):
    1739                          return True
    1740
    1741                  # 3) start a completely new encounter
    1742                  self.start_new_encounter()
    1743                  return True
    1744          #------------------------------------------------------------------
    1745 -        def __activate_very_recent_encounter(self):
    1746                  """Try to attach to a "very recent" encounter if there is one.
    1747
    1748                  returns:
    1749                          False: no "very recent" encounter, create new one
    1750                  True: success
    1751                  """
    1752                  cfg_db = gmCfg.cCfgSQL()
    1753                  min_ttl = cfg_db.get2 (
    1754                          option = u'encounter.minimum_ttl',
    1755                          workplace = _here.active_workplace,
    1756                          bias = u'user',
    1757                          default = u'1 hour 30 minutes'
    1758                  )
    1759                  cmd = u"""
    1760                          SELECT pk_encounter
    1761                          FROM clin.v_most_recent_encounters
    1762                          WHERE
    1763                                  pk_patient = %s
    1764                                          and
    1765                                  last_affirmed > (now() - %s::interval)
    1766                          ORDER BY
    1767                                  last_affirmed DESC"""
    1768                  enc_rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient, min_ttl]}])
    1769                  # none found
    1770                  if len(enc_rows) == 0:
    1771                          _log.debug('no <very recent> encounter (younger than [%s]) found' % min_ttl)
    1772                          return False
    1773                  # attach to existing
    1774                  self.current_encounter = gmEMRStructItems.cEncounter(aPK_obj=enc_rows[0][0])
    1775                  _log.debug('"very recent" encounter [%s] found and re-activated' % enc_rows[0][0])
    1776                  return True
    1777          #------------------------------------------------------------------
    1778 -        def __activate_fairly_recent_encounter(self, allow_user_interaction=True):
    1779                  """Try to attach to a "fairly recent" encounter if there is one.
    1780
    1781                  returns:
    1782                          False: no "fairly recent" encounter, create new one
    1783                  True: success
    1784                  """
    1785                  if _func_ask_user is None:
    1786                          _log.debug('cannot ask user for guidance, not looking for fairly recent encounter')
    1787                          return False
    1788
    1789                  if not allow_user_interaction:
    1790                          _log.exception('user interaction not desired, not looking for fairly recent encounter')
    1791                          return False
    1792
    1793                  cfg_db = gmCfg.cCfgSQL()
    1794                  min_ttl = cfg_db.get2 (
    1795                          option = u'encounter.minimum_ttl',
    1796                          workplace = _here.active_workplace,
    1797                          bias = u'user',
    1798                          default = u'1 hour 30 minutes'
    1799                  )
    1800                  max_ttl = cfg_db.get2 (
    1801                          option = u'encounter.maximum_ttl',
    1802                          workplace = _here.active_workplace,
    1803                          bias = u'user',
    1804                          default = u'6 hours'
    1805                  )
    1806                  cmd = u"""
    1807                          SELECT pk_encounter
    1808                          FROM clin.v_most_recent_encounters
    1809                          WHERE
    1810                                  pk_patient=%s
    1811                                          AND
    1812                                  last_affirmed BETWEEN (now() - %s::interval) AND (now() - %s::interval)
    1813                          ORDER BY
    1814                                  last_affirmed DESC"""
    1815                  enc_rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient, max_ttl, min_ttl]}])
    1816                  # none found
    1817                  if len(enc_rows) == 0:
    1818                          _log.debug('no <fairly recent> encounter (between [%s] and [%s] old) found' % (min_ttl, max_ttl))
    1819                          return False
    1820
    1821                  _log.debug('"fairly recent" encounter [%s] found', enc_rows[0][0])
    1822
    1823                  encounter = gmEMRStructItems.cEncounter(aPK_obj=enc_rows[0][0])
    1824                  # ask user whether to attach or not
    1825                  cmd = u"""
    1826                          SELECT title, firstnames, lastnames, gender, dob
    1827                          FROM dem.v_basic_person WHERE pk_identity=%s"""
    1828                  pats, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient]}])
    1829                  pat = pats[0]
    1830                  pat_str = u'%s %s %s (%s), %s  [#%s]' % (
    1831                          gmTools.coalesce(pat[0], u'')[:5],
    1832                          pat[1][:15],
    1833                          pat[2][:15],
    1834                          pat[3],
    1835                          gmDateTime.pydt_strftime(pat[4], '%Y %b %d'),
    1836                          self.pk_patient
    1837                  )
    1838                  msg = _(
    1839                          '%s\n'
    1840                          '\n'
    1841                          "This patient's chart was worked on only recently:\n"
    1842                          '\n'
    1843                          ' %s  %s - %s  (%s)\n'
    1844                          '\n'
    1845                          ' Request: %s\n'
    1846                          ' Outcome: %s\n'
    1847                          '\n'
    1848                          'Do you want to continue that consultation\n'
    1849                          'or do you want to start a new one ?\n'
    1850                  ) % (
    1851                          pat_str,
    1852                          gmDateTime.pydt_strftime(encounter['started'], '%Y %b %d'),
    1853                          gmDateTime.pydt_strftime(encounter['started'], '%H:%M'), gmDateTime.pydt_strftime(encounter['last_affirmed'], '%H:%M'),
    1854                          encounter['l10n_type'],
    1855                          gmTools.coalesce(encounter['reason_for_encounter'], _('none given')),
    1856                          gmTools.coalesce(encounter['assessment_of_encounter'], _('none given')),
    1857                  )
    1858                  attach = False
    1859                  try:
    1860                          attach = _func_ask_user(msg = msg, caption = _('Starting patient encounter'), encounter = encounter)
    1861                  except:
    1862                          _log.exception('cannot ask user for guidance, not attaching to existing encounter')
    1863                          return False
    1864                  if not attach:
    1865                          return False
    1866
    1867                  # attach to existing
    1868                  self.current_encounter = encounter
    1869                  _log.debug('"fairly recent" encounter re-activated')
    1870                  return True
    1871          #------------------------------------------------------------------
    1872 -        def start_new_encounter(self):
    1873                  cfg_db = gmCfg.cCfgSQL()
    1874                  enc_type = cfg_db.get2 (
    1875                          option = u'encounter.default_type',
    1876                          workplace = _here.active_workplace,
    1877                          bias = u'user'
    1878                  )
    1879                  if enc_type is None:
    1880                          enc_type = gmEMRStructItems.get_most_commonly_used_encounter_type()
    1881                  if enc_type is None:
    1882                          enc_type = u'in surgery'
    1883                  enc = gmEMRStructItems.create_encounter(fk_patient = self.pk_patient, enc_type = enc_type)
    1884                  enc['pk_org_unit'] = _here['pk_org_unit']
    1885                  enc.save()
    1886                  self.current_encounter = enc
    1887                  _log.debug('new encounter [%s] initiated' % self.current_encounter['pk_encounter'])
    1888          #------------------------------------------------------------------
    1889 -        def get_encounters(self, since=None, until=None, id_list=None, episodes=None, issues=None, skip_empty=False):
    1890                  """Retrieves patient's encounters.
    1891
    1892                  id_list - PKs of encounters to fetch
    1893                  since - initial date for encounter items, DateTime instance
    1894                  until - final date for encounter items, DateTime instance
    1895                  episodes - PKs of the episodes the encounters belong to (many-to-many relation)
    1896                  issues - PKs of the health issues the encounters belong to (many-to-many relation)
    1897                  skip_empty - do NOT return those which do not have any of documents/clinical items/RFE/AOE
    1898
    1899                  NOTE: if you specify *both* issues and episodes
    1900                  you will get the *aggregate* of all encounters even
    1901                  if the episodes all belong to the health issues listed.
    1902                  IOW, the issues broaden the episode list rather than
    1903                  the episode list narrowing the episodes-from-issues
    1904                  list.
    1905                  Rationale: If it was the other way round it would be
    1906                  redundant to specify the list of issues at all.
    1907                  """
    1908                  where_parts = [u'c_vpe.pk_patient = %(pat)s']
    1909                  args = {'pat': self.pk_patient}
    1910
    1911                  if skip_empty:
    1912                          where_parts.append(u"""NOT (
    1913                                  gm.is_null_or_blank_string(c_vpe.reason_for_encounter)
    1914                                          AND
    1915                                  gm.is_null_or_blank_string(c_vpe.assessment_of_encounter)
    1916                                          AND
    1917                                  NOT EXISTS (
    1918                                          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
    1919                                                  UNION ALL
    1920                                          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
    1921                                  ))""")
    1922
    1923                  if since is not None:
    1924                          where_parts.append(u'c_vpe.started >= %(start)s')
    1925                          args['start'] = since
    1926
    1927                  if until is not None:
    1928                          where_parts.append(u'c_vpe.last_affirmed <= %(end)s')
    1929                          args['end'] = since
    1930
    1931                  cmd = u"""
    1932                          SELECT *
    1933                          FROM clin.v_pat_encounters c_vpe
    1934                          WHERE
    1935                                  %s
    1936                          ORDER BY started
    1937                  """ % u' AND '.join(where_parts)
    1938                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1939                  encounters = [ gmEMRStructItems.cEncounter(row = {'data': r, 'idx': idx, 'pk_field': 'pk_encounter'}) for r in rows ]
    1940
    1941                  # we've got the encounters, start filtering
    1942                  filtered_encounters = []
    1943                  filtered_encounters.extend(encounters)
    1944
    1945                  if id_list is not None:
    1946                          filtered_encounters = filter(lambda enc: enc['pk_encounter'] in id_list, filtered_encounters)
    1947
    1948                  if (issues is not None) and (len(issues) > 0):
    1949                          issues = tuple(issues)
    1950                          # however, this seems like the proper approach:
    1951                          # - find episodes corresponding to the health issues in question
    1952                          cmd = u"SELECT distinct pk FROM clin.episode WHERE fk_health_issue in %(issues)s"
    1953                          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'issues': issues}}])
    1954                          epi_ids = map(lambda x:x[0], rows)
    1955                          if episodes is None:
    1956                                  episodes = []
    1957                          episodes.extend(epi_ids)
    1958
    1959                  if (episodes is not None) and (len(episodes) > 0):
    1960                          episodes = tuple(episodes)
    1961                          # if the episodes to filter by belong to the patient in question so will
    1962                          # the encounters found with them - hence we don't need a WHERE on the patient ...
    1963                          cmd = u"SELECT distinct fk_encounter FROM clin.clin_root_item WHERE fk_episode in %(epis)s"
    1964                          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'epis': episodes}}])
    1965                          enc_ids = map(lambda x:x[0], rows)
    1966                          filtered_encounters = filter(lambda enc: enc['pk_encounter'] in enc_ids, filtered_encounters)
    1967
    1968                  return filtered_encounters
    1969          #--------------------------------------------------------
    1970 -        def get_first_encounter(self, issue_id=None, episode_id=None):
    1971                  """Retrieves first encounter for a particular issue and/or episode.
    1972
    1973                  issue_id - First encounter associated health issue
    1974                  episode - First encounter associated episode
    1975                  """
    1976                  # FIXME: use direct query
    1977                  if issue_id is None:
    1978                          issues = None
    1979                  else:
    1980                          issues = [issue_id]
    1981
    1982                  if episode_id is None:
    1983                          episodes = None
    1984                  else:
    1985                          episodes = [episode_id]
    1986
    1987                  encounters = self.get_encounters(issues=issues, episodes=episodes)
    1988                  if len(encounters) == 0:
    1989                          return None
    1990
    1991                  # FIXME: this does not scale particularly well, I assume
    1992                  encounters.sort(lambda x,y: cmp(x['started'], y['started']))
    1993                  return encounters[0]
    1994          #--------------------------------------------------------
    1995 -        def get_earliest_care_date(self):
    1996                  args = {'pat': self.pk_patient}
    1997                  cmd = u"""
    1998  SELECT MIN(earliest) FROM (
    1999          (
    2000                  SELECT MIN(episode_modified_when) AS earliest FROM clin.v_pat_episodes WHERE pk_patient = %(pat)s
    2001
    2002          ) UNION ALL (
    2003
    2004                  SELECT MIN(modified_when) AS earliest FROM clin.v_health_issues WHERE pk_patient = %(pat)s
    2005
    2006          ) UNION ALL (
    2007
    2008                  SELECT MIN(modified_when) AS earliest FROM clin.encounter WHERE fk_patient = %(pat)s
    2009
    2010          ) UNION ALL (
    2011
    2012                  SELECT MIN(started) AS earliest FROM clin.v_pat_encounters WHERE pk_patient = %(pat)s
    2013
    2014          ) UNION ALL (
    2015
    2016                  SELECT MIN(modified_when) AS earliest FROM clin.v_pat_items WHERE pk_patient = %(pat)s
    2017
    2018          ) UNION ALL (
    2019
    2020                  SELECT MIN(modified_when) AS earliest FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat)s
    2021
    2022          ) UNION ALL (
    2023
    2024                  SELECT MIN(last_confirmed) AS earliest FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat)s
    2025
    2026          )
    2027  ) AS candidates"""
    2028                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2029                  return rows[0][0]
    2030
    2031          earliest_care_date = property(get_earliest_care_date, lambda x:x)
    2032          #--------------------------------------------------------
    2033 -        def get_last_encounter(self, issue_id=None, episode_id=None):
    2034                  """Retrieves last encounter for a concrete issue and/or episode
    2035
    2036                  issue_id - Last encounter associated health issue
    2037                  episode_id - Last encounter associated episode
    2038                  """
    2039                  # FIXME: use direct query
    2040
    2041                  if issue_id is None:
    2042                          issues = None
    2043                  else:
    2044                          issues = [issue_id]
    2045
    2046                  if episode_id is None:
    2047                          episodes = None
    2048                  else:
    2049                          episodes = [episode_id]
    2050
    2051                  encounters = self.get_encounters(issues=issues, episodes=episodes)
    2052                  if len(encounters) == 0:
    2053                          return None
    2054
    2055                  # FIXME: this does not scale particularly well, I assume
    2056                  encounters.sort(lambda x,y: cmp(x['started'], y['started']))
    2057                  return encounters[-1]
    2058
    2059          last_encounter = property(get_last_encounter, lambda x:x)
    2060          #------------------------------------------------------------------
    2061 -        def get_encounter_stats_by_type(self, cover_period=None):
    2062                  args = {'pat': self.pk_patient, 'range': cover_period}
    2063                  where_parts = [u'pk_patient = %(pat)s']
    2064                  if cover_period is not None:
    2065                          where_parts.append(u'last_affirmed > now() - %(range)s')
    2066
    2067                  cmd = u"""
    2068                          SELECT l10n_type, count(1) AS frequency
    2069                          FROM clin.v_pat_encounters
    2070                          WHERE
    2071                                  %s
    2072                          GROUP BY l10n_type
    2073                          ORDER BY frequency DESC
    2074                  """ % u' AND '.join(where_parts)
    2075                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2076                  return rows
    2077          #------------------------------------------------------------------
    2078 -        def get_last_but_one_encounter(self, issue_id=None, episode_id=None):
    2079
    2080                  args = {'pat': self.pk_patient}
    2081
    2082                  if (issue_id is None) and (episode_id is None):
    2083
    2084                          cmd = u"""
    2085  SELECT * FROM clin.v_pat_encounters
    2086  WHERE pk_patient = %(pat)s
    2087  ORDER BY started DESC
    2088  LIMIT 2
    2089  """
    2090                  else:
    2091                          where_parts = []
    2092
    2093                          if issue_id is not None:
    2094                                  where_parts.append(u'pk_health_issue = %(issue)s')
    2095                                  args['issue'] = issue_id
    2096
    2097                          if episode_id is not None:
    2098                                  where_parts.append(u'pk_episode = %(epi)s')
    2099                                  args['epi'] = episode_id
    2100
    2101                          cmd = u"""
    2102  SELECT *
    2103  FROM clin.v_pat_encounters
    2104  WHERE
    2105          pk_patient = %%(pat)s
    2106                  AND
    2107          pk_encounter IN (
    2108                  SELECT distinct pk_encounter
    2109                  FROM clin.v_pat_narrative
    2110                  WHERE
    2111                          %s
    2112          )
    2113  ORDER BY started DESC
    2114  LIMIT 2
    2115  """ % u' AND '.join(where_parts)
    2116
    2117                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2118
    2119                  if len(rows) == 0:
    2120                          return None
    2121
    2122                  # just one encounter within the above limits
    2123                  if len(rows) == 1:
    2124                          # is it the current encounter ?
    2125                          if rows[0]['pk_encounter'] == self.current_encounter['pk_encounter']:
    2126                                  # yes
    2127                                  return None
    2128                          # no
    2129                          return gmEMRStructItems.cEncounter(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_encounter'})
    2130
    2131                  # more than one encounter
    2132                  if rows[0]['pk_encounter'] == self.current_encounter['pk_encounter']:
    2133                          return gmEMRStructItems.cEncounter(row = {'data': rows[1], 'idx': idx, 'pk_field': 'pk_encounter'})
    2134
    2135                  return gmEMRStructItems.cEncounter(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_encounter'})
    2136          #------------------------------------------------------------------
    2137 -        def remove_empty_encounters(self):
    2138                  cfg_db = gmCfg.cCfgSQL()
    2139                  ttl = cfg_db.get2 (
    2140                          option = u'encounter.ttl_if_empty',
    2141                          workplace = _here.active_workplace,
    2142                          bias = u'user',
    2143                          default = u'1 week'
    2144                  )
    2145
    2146  #               # FIXME: this should be done async
    2147                  cmd = u"select clin.remove_old_empty_encounters(%(pat)s::integer, %(ttl)s::interval)"
    2148                  args = {'pat': self.pk_patient, 'ttl': ttl}
    2149                  try:
    2150                          rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    2151                  except:
    2152                          _log.exception('error deleting empty encounters')
    2153
    2154                  return True
    2155          #------------------------------------------------------------------
    2156          # API: measurements / test results
    2157          #------------------------------------------------------------------
    2158 -        def get_most_recent_results(self, test_type=None, loinc=None, no_of_results=1):
    2159                  return gmPathLab.get_most_recent_results (
    2160                          test_type = test_type,
    2161                          loinc = loinc,
    2162                          no_of_results = no_of_results,
    2163                          patient = self.pk_patient
    2164                  )
    2165          #------------------------------------------------------------------
    2166 -        def get_result_at_timestamp(self, timestamp=None, test_type=None, loinc=None, tolerance_interval='12 hours'):
    2167                  return gmPathLab.get_result_at_timestamp (
    2168                          timestamp = timestamp,
    2169                          test_type = test_type,
    2170                          loinc = loinc,
    2171                          tolerance_interval = tolerance_interval,
    2172                          patient = self.pk_patient
    2173                  )
    2174          #------------------------------------------------------------------
    2175 -        def get_unsigned_results(self, order_by=None):
    2176                  if order_by is None:
    2177                          order_by = u''
    2178                  else:
    2179                          order_by = u'ORDER BY %s' % order_by
    2180                  cmd = u"""
    2181                          SELECT * FROM clin.v_test_results
    2182                          WHERE
    2183                                  pk_patient = %%(pat)s
    2184                                          AND
    2185                                  reviewed IS FALSE
    2186                          %s""" % order_by
    2187                  args = {'pat': self.pk_patient}
    2188                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2189                  return [ gmPathLab.cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
    2190          #------------------------------------------------------------------
    2191          # FIXME: use psyopg2 dbapi extension of named cursors - they are *server* side !
    2192 -        def get_test_types_for_results(self):
    2193                  """Retrieve data about test types for which this patient has results."""
    2194
    2195                  cmd = u"""
    2196                  SELECT * FROM (
    2197                          SELECT DISTINCT ON (pk_test_type) pk_test_type, clin_when, unified_name
    2198                          FROM clin.v_test_results
    2199                          WHERE pk_patient = %(pat)s
    2200                  ) AS foo
    2201                  ORDER BY clin_when desc, unified_name
    2202                  """
    2203                  args = {'pat': self.pk_patient}
    2204                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
    2205                  return [ gmPathLab.cMeasurementType(aPK_obj = row['pk_test_type']) for row in rows ]
    2206          #------------------------------------------------------------------
    2207 -        def get_dates_for_results(self, tests=None, reverse_chronological=True):
    2208                  """Get the dates for which we have results."""
    2209                  where_parts = [u'pk_patient = %(pat)s']
    2210                  args = {'pat': self.pk_patient}
    2211
    2212                  if tests is not None:
    2213                          where_parts.append(u'pk_test_type IN %(tests)s')
    2214                          args['tests'] = tuple(tests)
    2215
    2216                  cmd = u"""
    2217                          SELECT distinct on (cwhen) date_trunc('day', clin_when) as cwhen
    2218                          FROM clin.v_test_results
    2219                          WHERE %s
    2220                          ORDER BY cwhen %s
    2221                  """ % (
    2222                          u' AND '.join(where_parts),
    2223                          gmTools.bool2subst(reverse_chronological, u'DESC', u'ASC', u'DESC')
    2224                  )
    2225                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
    2226                  return rows
    2227          #------------------------------------------------------------------
    2228 -        def get_test_results(self, encounters=None, episodes=None, tests=None, order_by=None):
    2229                  return gmPathLab.get_test_results (
    2230                          pk_patient = self.pk_patient,
    2231                          encounters = encounters,
    2232                          episodes = episodes,
    2233                          order_by = order_by
    2234                  )
    2235          #------------------------------------------------------------------
    2236 -        def get_test_results_by_date(self, encounter=None, episodes=None, tests=None, reverse_chronological=True):
    2237
    2238                  where_parts = [u'pk_patient = %(pat)s']
    2239                  args = {'pat': self.pk_patient}
    2240
    2241                  if tests is not None:
    2242                          where_parts.append(u'pk_test_type IN %(tests)s')
    2243                          args['tests'] = tuple(tests)
    2244
    2245                  if encounter is not None:
    2246                          where_parts.append(u'pk_encounter = %(enc)s')
    2247                          args['enc'] = encounter
    2248
    2249                  if episodes is not None:
    2250                          where_parts.append(u'pk_episode IN %(epis)s')
    2251                          args['epis'] = tuple(episodes)
    2252
    2253                  cmd = u"""
    2254                          SELECT * FROM clin.v_test_results
    2255                          WHERE %s
    2256                          ORDER BY clin_when %s, pk_episode, unified_name
    2257                  """ % (
    2258                          u' AND '.join(where_parts),
    2259                          gmTools.bool2subst(reverse_chronological, u'DESC', u'ASC', u'DESC')
    2260                  )
    2261                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2262
    2263                  tests = [ gmPathLab.cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
    2264
    2265                  return tests
    2266          #------------------------------------------------------------------
    2267 -        def add_test_result(self, episode=None, type=None, intended_reviewer=None, val_num=None, val_alpha=None, unit=None):
    2268
    2269                  try:
    2270                          epi = int(episode)
    2271                  except:
    2272                          epi = episode['pk_episode']
    2273
    2274                  try:
    2275                          type = int(type)
    2276                  except:
    2277                          type = type['pk_test_type']
    2278
    2279                  if intended_reviewer is None:
    2280                          intended_reviewer = _me['pk_staff']
    2281
    2282                  tr = gmPathLab.create_test_result (
    2283                          encounter = self.current_encounter['pk_encounter'],
    2284                          episode = epi,
    2285                          type = type,
    2286                          intended_reviewer = intended_reviewer,
    2287                          val_num = val_num,
    2288                          val_alpha = val_alpha,
    2289                          unit = unit
    2290                  )
    2291
    2292                  return tr
    2293          #------------------------------------------------------------------
    2294          #------------------------------------------------------------------
    2295          #------------------------------------------------------------------
    2296          #------------------------------------------------------------------
    2297 -        def get_lab_request(self, pk=None, req_id=None, lab=None):
    2298                  # FIXME: verify that it is our patient ? ...
    2299                  req = gmPathLab.cLabRequest(aPK_obj=pk, req_id=req_id, lab=lab)
    2300                  return req
    2301          #------------------------------------------------------------------
    2302 -        def add_lab_request(self, lab=None, req_id=None, encounter_id=None, episode_id=None):
    2303                  if encounter_id is None:
    2304                          encounter_id = self.current_encounter['pk_encounter']
    2305                  status, data = gmPathLab.create_lab_request(
    2306                          lab=lab,
    2307                          req_id=req_id,
    2308                          pat_id=self.pk_patient,
    2309                          encounter_id=encounter_id,
    2310                          episode_id=episode_id
    2311                  )
    2312                  if not status:
    2313                          _log.error(str(data))
    2314                          return None
    2315                  return data
    2316
    2317  #============================================================
    2318  # main
    2319  #------------------------------------------------------------
    2320  if __name__ == "__main__":
    2321
    2322          if len(sys.argv) == 1:
    2323                  sys.exit()
    2324
    2325          if sys.argv[1] != 'test':
    2326                  sys.exit()
    2327
    2328          from Gnumed.pycommon import gmLog2
    2329          #-----------------------------------------
    2330 -        def test_allergy_state():
    2331                  emr = cClinicalRecord(aPKey=1)
    2332                  state = emr.allergy_state
    2333                  print "allergy state is:", state
    2334
    2335                  print "setting state to 0"
    2336                  emr.allergy_state = 0
    2337
    2338                  print "setting state to None"
    2339                  emr.allergy_state = None
    2340
    2341                  print "setting state to 'abc'"
    2342                  emr.allergy_state = 'abc'
    2343          #-----------------------------------------
    2344 -        def test_get_test_names():
    2345                  emr = cClinicalRecord(aPKey=12)
    2346                  rows = emr.get_test_types_for_results()
    2347                  print "test result names:"
    2348                  for row in rows:
    2349                          print row
    2350          #-----------------------------------------
    2351 -        def test_get_dates_for_results():
    2352                  emr = cClinicalRecord(aPKey=12)
    2353                  rows = emr.get_dates_for_results()
    2354                  print "test result dates:"
    2355                  for row in rows:
    2356                          print row
    2357          #-----------------------------------------
    2358 -        def test_get_measurements():
    2359                  emr = cClinicalRecord(aPKey=12)
    2360                  rows, idx = emr.get_measurements_by_date()
    2361                  print "test results:"
    2362                  for row in rows:
    2363                          print row
    2364          #-----------------------------------------
    2365 -        def test_get_test_results_by_date():
    2366                  emr = cClinicalRecord(aPKey=12)
    2367                  tests = emr.get_test_results_by_date()
    2368                  print "test results:"
    2369                  for test in tests:
    2370                          print test
    2371          #-----------------------------------------
    2372 -        def test_get_statistics():
    2373                  emr = cClinicalRecord(aPKey=12)
    2374                  for key, item in emr.get_statistics().iteritems():
    2375                          print key, ":", item
    2376          #-----------------------------------------
    2377 -        def test_get_problems():
    2378                  emr = cClinicalRecord(aPKey=12)
    2379
    2380                  probs = emr.get_problems()
    2381                  print "normal probs (%s):" % len(probs)
    2382                  for p in probs:
    2383                          print u'%s (%s)' % (p['problem'], p['type'])
    2384
    2385                  probs = emr.get_problems(include_closed_episodes=True)
    2386                  print "probs + closed episodes (%s):" % len(probs)
    2387                  for p in probs:
    2388                          print u'%s (%s)' % (p['problem'], p['type'])
    2389
    2390                  probs = emr.get_problems(include_irrelevant_issues=True)
    2391                  print "probs + issues (%s):" % len(probs)
    2392                  for p in probs:
    2393                          print u'%s (%s)' % (p['problem'], p['type'])
    2394
    2395                  probs = emr.get_problems(include_closed_episodes=True, include_irrelevant_issues=True)
    2396                  print "probs + issues + epis (%s):" % len(probs)
    2397                  for p in probs:
    2398                          print u'%s (%s)' % (p['problem'], p['type'])
    2399          #-----------------------------------------
    2400 -        def test_add_test_result():
    2401                  emr = cClinicalRecord(aPKey=12)
    2402                  tr = emr.add_test_result (
    2403                          episode = 1,
    2404                          intended_reviewer = 1,
    2405                          type = 1,
    2406                          val_num = 75,
    2407                          val_alpha = u'somewhat obese',
    2408                          unit = u'kg'
    2409                  )
    2410                  print tr
    2411          #-----------------------------------------
    2412 -        def test_get_most_recent_episode():
    2413                  emr = cClinicalRecord(aPKey=12)
    2414                  print emr.get_most_recent_episode(issue = 2)
    2415          #-----------------------------------------
    2416 -        def test_get_almost_recent_encounter():
    2417                  emr = cClinicalRecord(aPKey=12)
    2418                  print emr.get_last_encounter(issue_id=2)
    2419                  print emr.get_last_but_one_encounter(issue_id=2)
    2420          #-----------------------------------------
    2421 -        def test_get_meds():
    2422                  emr = cClinicalRecord(aPKey=12)
    2423                  for med in emr.get_current_substance_intakes():
    2424                          print med
    2425          #-----------------------------------------
    2426 -        def test_is_allergic_to():
    2427                  emr = cClinicalRecord(aPKey = 12)
    2428                  print emr.is_allergic_to(atcs = tuple(sys.argv[2:]), inns = tuple(sys.argv[2:]), brand = sys.argv[2])
    2429          #-----------------------------------------
    2430 -        def test_get_as_journal():
    2431                  emr = cClinicalRecord(aPKey = 12)
    2432                  for journal_line in emr.get_as_journal():
    2433                          #print journal_line.keys()
    2434                          print u'%(date)s  %(modified_by)s  %(soap_cat)s  %(narrative)s' % journal_line
    2435                          print ""
    2436          #-----------------------------------------
    2437 -        def test_get_most_recent():
    2438                  emr = cClinicalRecord(aPKey=12)
    2439                  print emr.get_most_recent_results()
    2440          #-----------------------------------------
    2441 -        def test_episodes():
    2442                  emr = cClinicalRecord(aPKey=12)
    2443                  print "episodes:", emr.episodes
    2444                  print "unlinked:", emr.unlinked_episodes
    2445
    2446          #-----------------------------------------
    2447 -        def test_format_as_journal():
    2448                  emr = cClinicalRecord(aPKey=12)
    2449                  from Gnumed.business.gmPerson import cPatient
    2450                  pat = cPatient(aPK_obj = 12)
    2451                  print emr.format_as_journal(left_margin = 1, patient = pat)
    2452          #-----------------------------------------
    2453
    2454          #test_allergy_state()
    2455          #test_is_allergic_to()
    2456
    2457          #test_get_test_names()
    2458          #test_get_dates_for_results()
    2459          #test_get_measurements()
    2460          #test_get_test_results_by_date()
    2461          #test_get_statistics()
    2462          #test_get_problems()
    2463          #test_add_test_result()
    2464          #test_get_most_recent_episode()
    2465          #test_get_almost_recent_encounter()
    2466          #test_get_meds()
    2467          #test_get_as_journal()
    2468          #test_get_most_recent()
    2469          #test_episodes()
    2470          test_format_as_journal()
    2471
    2472  #       emr = cClinicalRecord(aPKey = 12)
    2473
    2474  #       # Vacc regimes
    2475  #       vacc_regimes = emr.get_scheduled_vaccination_regimes(indications = ['tetanus'])
    2476  #       print '\nVaccination regimes: '
    2477  #       for a_regime in vacc_regimes:
    2478  #               pass
    2479  #               #print a_regime
    2480  #       vacc_regime = emr.get_scheduled_vaccination_regimes(ID=10)
    2481  #       #print vacc_regime
    2482
    2483  #       # vaccination regimes and vaccinations for regimes
    2484  #       scheduled_vaccs = emr.get_scheduled_vaccinations(indications = ['tetanus'])
    2485  #       print 'Vaccinations for the regime:'
    2486  #       for a_scheduled_vacc in scheduled_vaccs:
    2487  #               pass
    2488  #               #print '   %s' %(a_scheduled_vacc)
    2489
    2490  #       # vaccination next shot and booster
    2491  #       vaccinations = emr.get_vaccinations()
    2492  #       for a_vacc in vaccinations:
    2493  #               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'])
    2494
    2495  #       # first and last encounters
    2496  #       first_encounter = emr.get_first_encounter(issue_id = 1)
    2497  #       print '\nFirst encounter: ' + str(first_encounter)
    2498  #       last_encounter = emr.get_last_encounter(episode_id = 1)
    2499  #       print '\nLast encounter: ' + str(last_encounter)
    2500  #       print ''
    2501
    2502          #dump = record.get_missing_vaccinations()
    2503          #f = open('vaccs.lst', 'wb')
    2504          #if dump is not None:
    2505          #       print "=== due ==="
    2506          #       f.write("=== due ===\n")
    2507          #       for row in dump['due']:
    2508          #               print row
    2509          #               f.write(repr(row))
    2510          #               f.write('\n')
    2511          #       print "=== overdue ==="
    2512          #       f.write("=== overdue ===\n")
    2513          #       for row in dump['overdue']:
    2514          #               print row
    2515          #               f.write(repr(row))
    2516          #               f.write('\n')
    2517          #f.close()
    2518