Gnumed.business.gmEMRStructItems

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

# Source Code for Module Gnumed.business.gmEMRStructItems

       1  # -*- coding: utf8 -*-
       2  """GNUmed health related business object.
       3
       4  license: GPL v2 or later
       5  """
       6  #============================================================
       7  __author__ = "Carlos Moro <cfmoro1976@yahoo.es>, <karsten.hilbert@gmx.net>"
       8
       9  import types, sys, string, datetime, logging, time
      10
      11
      12  if __name__ == '__main__':
      13          sys.path.insert(0, '../../')
      14  from Gnumed.pycommon import gmPG2
      15  from Gnumed.pycommon import gmI18N
      16  from Gnumed.pycommon import gmTools
      17  from Gnumed.pycommon import gmDateTime
      18  from Gnumed.pycommon import gmBusinessDBObject
      19  from Gnumed.pycommon import gmNull
      20  from Gnumed.pycommon import gmExceptions
      21
      22  from Gnumed.business import gmClinNarrative
      23  from Gnumed.business import gmCoding
      24  from Gnumed.business import gmPraxis
      25  from Gnumed.business import gmOrganization
      26
      27
      28  _log = logging.getLogger('gm.emr')
      29
      30  try: _
      31  except NameError: _ = lambda x:x
      32  #============================================================
      33  # diagnostic certainty classification
      34  #============================================================
      35  __diagnostic_certainty_classification_map = None
      36
      37 -def diagnostic_certainty_classification2str(classification):
      38
      39          global __diagnostic_certainty_classification_map
      40
      41          if __diagnostic_certainty_classification_map is None:
      42                  __diagnostic_certainty_classification_map = {
      43                          None: u'',
      44                          u'A': _(u'A: Sign'),
      45                          u'B': _(u'B: Cluster of signs'),
      46                          u'C': _(u'C: Syndromic diagnosis'),
      47                          u'D': _(u'D: Scientific diagnosis')
      48                  }
      49
      50          try:
      51                  return __diagnostic_certainty_classification_map[classification]
      52          except KeyError:
      53                  return _(u'<%s>: unknown diagnostic certainty classification') % classification
      54  #============================================================
      55  # Health Issues API
      56  #============================================================
      57  laterality2str = {
      58          None: u'?',
      59          u'na': u'',
      60          u'sd': _('bilateral'),
      61          u'ds': _('bilateral'),
      62          u's': _('left'),
      63          u'd': _('right')
      64  }
      65
      66  #============================================================
      67 -class cHealthIssue(gmBusinessDBObject.cBusinessDBObject):
      68          """Represents one health issue."""
      69
      70          _cmd_fetch_payload = u"select *, xmin_health_issue from clin.v_health_issues where pk_health_issue=%s"
      71          _cmds_store_payload = [
      72                  u"""update clin.health_issue set
      73                                  description = %(description)s,
      74                                  summary = gm.nullify_empty_string(%(summary)s),
      75                                  age_noted = %(age_noted)s,
      76                                  laterality = gm.nullify_empty_string(%(laterality)s),
      77                                  grouping = gm.nullify_empty_string(%(grouping)s),
      78                                  diagnostic_certainty_classification = gm.nullify_empty_string(%(diagnostic_certainty_classification)s),
      79                                  is_active = %(is_active)s,
      80                                  clinically_relevant = %(clinically_relevant)s,
      81                                  is_confidential = %(is_confidential)s,
      82                                  is_cause_of_death = %(is_cause_of_death)s
      83                          where
      84                                  pk = %(pk_health_issue)s and
      85                                  xmin = %(xmin_health_issue)s""",
      86                  u"select xmin as xmin_health_issue from clin.health_issue where pk = %(pk_health_issue)s"
      87          ]
      88          _updatable_fields = [
      89                  'description',
      90                  'summary',
      91                  'grouping',
      92                  'age_noted',
      93                  'laterality',
      94                  'is_active',
      95                  'clinically_relevant',
      96                  'is_confidential',
      97                  'is_cause_of_death',
      98                  'diagnostic_certainty_classification'
      99          ]
     100          #--------------------------------------------------------
     101 -        def __init__(self, aPK_obj=None, encounter=None, name='xxxDEFAULTxxx', patient=None, row=None):
     102                  pk = aPK_obj
     103
     104                  if (pk is not None) or (row is not None):
     105                          gmBusinessDBObject.cBusinessDBObject.__init__(self, aPK_obj=pk, row=row)
     106                          return
     107
     108                  if patient is None:
     109                          cmd = u"""select *, xmin_health_issue from clin.v_health_issues
     110                                          where
     111                                                  description = %(desc)s
     112                                                          and
     113                                                  pk_patient = (select fk_patient from clin.encounter where pk = %(enc)s)"""
     114                  else:
     115                          cmd = u"""select *, xmin_health_issue from clin.v_health_issues
     116                                          where
     117                                                  description = %(desc)s
     118                                                          and
     119                                                  pk_patient = %(pat)s"""
     120
     121                  queries = [{'cmd': cmd, 'args': {'enc': encounter, 'desc': name, 'pat': patient}}]
     122                  rows, idx = gmPG2.run_ro_queries(queries = queries,     get_col_idx = True)
     123
     124                  if len(rows) == 0:
     125                          raise gmExceptions.NoSuchBusinessObjectError, 'no health issue for [enc:%s::desc:%s::pat:%s]' % (encounter, name, patient)
     126
     127                  pk = rows[0][0]
     128                  r = {'idx': idx, 'data': rows[0], 'pk_field': 'pk_health_issue'}
     129
     130                  gmBusinessDBObject.cBusinessDBObject.__init__(self, row=r)
     131          #--------------------------------------------------------
     132          # external API
     133          #--------------------------------------------------------
     134 -        def rename(self, description=None):
     135                  """Method for issue renaming.
     136
     137                  @param description
     138                          - the new descriptive name for the issue
     139                  @type description
     140                          - a string instance
     141                  """
     142                  # sanity check
     143                  if not type(description) in [str, unicode] or description.strip() == '':
     144                          _log.error('<description> must be a non-empty string')
     145                          return False
     146                  # update the issue description
     147                  old_description = self._payload[self._idx['description']]
     148                  self._payload[self._idx['description']] = description.strip()
     149                  self._is_modified = True
     150                  successful, data = self.save_payload()
     151                  if not successful:
     152                          _log.error('cannot rename health issue [%s] with [%s]' % (self, description))
     153                          self._payload[self._idx['description']] = old_description
     154                          return False
     155                  return True
     156          #--------------------------------------------------------
     157 -        def get_episodes(self):
     158                  cmd = u"SELECT * FROM clin.v_pat_episodes WHERE pk_health_issue = %(pk)s"
     159                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}], get_col_idx = True)
     160                  return [ cEpisode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_episode'})  for r in rows ]
     161          #--------------------------------------------------------
     162 -        def close_expired_episode(self, ttl=180):
     163                  """ttl in days"""
     164                  open_episode = self.get_open_episode()
     165                  if open_episode is None:
     166                          return True
     167                  latest = open_episode.latest_access_date
     168                  ttl = datetime.timedelta(ttl)
     169                  now = datetime.datetime.now(tz=latest.tzinfo)
     170                  if (latest + ttl) > now:
     171                          return False
     172                  open_episode['episode_open'] = False
     173                  success, data = open_episode.save_payload()
     174                  if success:
     175                          return True
     176                  return False            # should be an exception
     177          #--------------------------------------------------------
     178 -        def close_episode(self):
     179                  open_episode = self.get_open_episode()
     180                  open_episode['episode_open'] = False
     181                  success, data = open_episode.save_payload()
     182                  if success:
     183                          return True
     184                  return False
     185          #--------------------------------------------------------
     186 -        def has_open_episode(self):
     187                  cmd = u"select exists (select 1 from clin.episode where fk_health_issue = %s and is_open is True)"
     188                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}])
     189                  return rows[0][0]
     190          #--------------------------------------------------------
     191 -        def get_open_episode(self):
     192                  cmd = u"select pk from clin.episode where fk_health_issue = %s and is_open is True"
     193                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}])
     194                  if len(rows) == 0:
     195                          return None
     196                  return cEpisode(aPK_obj=rows[0][0])
     197          #--------------------------------------------------------
     198 -        def age_noted_human_readable(self):
     199                  if self._payload[self._idx['age_noted']] is None:
     200                          return u'<???>'
     201
     202                  # since we've already got an interval we are bound to use it,
     203                  # further transformation will only introduce more errors,
     204                  # later we can improve this deeper inside
     205                  return gmDateTime.format_interval_medically(self._payload[self._idx['age_noted']])
     206          #--------------------------------------------------------
     207 -        def add_code(self, pk_code=None):
     208                  """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
     209                  cmd = u"INSERT INTO clin.lnk_code2h_issue (fk_item, fk_generic_code) values (%(item)s, %(code)s)"
     210                  args = {
     211                          'item': self._payload[self._idx['pk_health_issue']],
     212                          'code': pk_code
     213                  }
     214                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     215                  return True
     216          #--------------------------------------------------------
     217 -        def remove_code(self, pk_code=None):
     218                  """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
     219                  cmd = u"DELETE FROM clin.lnk_code2h_issue WHERE fk_item = %(item)s AND fk_generic_code = %(code)s"
     220                  args = {
     221                          'item': self._payload[self._idx['pk_health_issue']],
     222                          'code': pk_code
     223                  }
     224                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     225                  return True
     226          #--------------------------------------------------------
     227 -        def format_as_journal(self, left_margin=0, date_format='%a, %b %d %Y'):
     228                  rows = gmClinNarrative.get_as_journal (
     229                          issues = (self.pk_obj,),
     230                          order_by = u'pk_episode, pk_encounter, clin_when, scr, src_table'
     231                  )
     232
     233                  if len(rows) == 0:
     234                          return u''
     235
     236                  left_margin = u' ' * left_margin
     237
     238                  lines = []
     239                  lines.append(_('Clinical data generated during encounters under this health issue:'))
     240
     241                  prev_epi = None
     242                  for row in rows:
     243                          if row['pk_episode'] != prev_epi:
     244                                  lines.append(u'')
     245                                  prev_epi = row['pk_episode']
     246
     247                          when = row['clin_when'].strftime(date_format).decode(gmI18N.get_encoding())
     248                          top_row = u'%s%s %s (%s) %s' % (
     249                                  gmTools.u_box_top_left_arc,
     250                                  gmTools.u_box_horiz_single,
     251                                  gmClinNarrative.soap_cat2l10n_str[row['real_soap_cat']],
     252                                  when,
     253                                  gmTools.u_box_horiz_single * 5
     254                          )
     255                          soap = gmTools.wrap (
     256                                  text = row['narrative'],
     257                                  width = 60,
     258                                  initial_indent = u'  ',
     259                                  subsequent_indent = u'  ' + left_margin
     260                          )
     261                          row_ver = u''
     262                          if row['row_version'] > 0:
     263                                  row_ver = u'v%s: ' % row['row_version']
     264                          bottom_row = u'%s%s %s, %s%s %s' % (
     265                                  u' ' * 40,
     266                                  gmTools.u_box_horiz_light_heavy,
     267                                  row['modified_by'],
     268                                  row_ver,
     269                                  row['date_modified'],
     270                                  gmTools.u_box_horiz_heavy_light
     271                          )
     272
     273                          lines.append(top_row)
     274                          lines.append(soap)
     275                          lines.append(bottom_row)
     276
     277                  eol_w_margin = u'\n%s' % left_margin
     278                  return left_margin + eol_w_margin.join(lines) + u'\n'
     279          #--------------------------------------------------------
     280 -        def format (self, left_margin=0, patient=None,
     281                  with_summary=True,
     282                  with_codes=True,
     283                  with_episodes=True,
     284                  with_encounters=True,
     285                  with_medications=True,
     286                  with_hospital_stays=True,
     287                  with_procedures=True,
     288                  with_family_history=True,
     289                  with_documents=True,
     290                  with_tests=True,
     291                  with_vaccinations=True
     292          ):
     293
     294                  if patient.ID != self._payload[self._idx['pk_patient']]:
     295                          msg = '<patient>.ID = %s but health issue %s belongs to patient %s' % (
     296                                  patient.ID,
     297                                  self._payload[self._idx['pk_health_issue']],
     298                                  self._payload[self._idx['pk_patient']]
     299                          )
     300                          raise ValueError(msg)
     301
     302                  lines = []
     303
     304                  lines.append(_('Health Issue %s%s%s%s   [#%s]') % (
     305                          u'\u00BB',
     306                          self._payload[self._idx['description']],
     307                          u'\u00AB',
     308                          gmTools.coalesce (
     309                                  initial = self.laterality_description,
     310                                  instead = u'',
     311                                  template_initial = u' (%s)',
     312                                  none_equivalents = [None, u'', u'?']
     313                          ),
     314                          self._payload[self._idx['pk_health_issue']]
     315                  ))
     316
     317                  if self._payload[self._idx['is_confidential']]:
     318                          lines.append('')
     319                          lines.append(_(' ***** CONFIDENTIAL *****'))
     320                          lines.append('')
     321
     322                  if self._payload[self._idx['is_cause_of_death']]:
     323                          lines.append('')
     324                          lines.append(_(' contributed to death of patient'))
     325                          lines.append('')
     326
     327                  enc = cEncounter(aPK_obj = self._payload[self._idx['pk_encounter']])
     328                  lines.append (_(' Created during encounter: %s (%s - %s)   [#%s]') % (
     329                          enc['l10n_type'],
     330                          enc['started_original_tz'].strftime('%Y-%m-%d %H:%M'),
     331                          enc['last_affirmed_original_tz'].strftime('%H:%M'),
     332                          self._payload[self._idx['pk_encounter']]
     333                  ))
     334
     335                  if self._payload[self._idx['age_noted']] is not None:
     336                          lines.append(_(' Noted at age: %s') % self.age_noted_human_readable())
     337
     338                  lines.append(u' ' + _('Status') + u': %s, %s%s' % (
     339                          gmTools.bool2subst(self._payload[self._idx['is_active']], _('active'), _('inactive')),
     340                          gmTools.bool2subst(self._payload[self._idx['clinically_relevant']], _('clinically relevant'), _('not clinically relevant')),
     341                          gmTools.coalesce (
     342                                  initial = diagnostic_certainty_classification2str(self._payload[self._idx['diagnostic_certainty_classification']]),
     343                                  instead = u'',
     344                                  template_initial = u', %s',
     345                                  none_equivalents = [None, u'']
     346                          )
     347                  ))
     348
     349                  if with_summary:
     350                          if self._payload[self._idx['summary']] is not None:
     351                                  lines.append(u' %s:' % _('Synopsis'))
     352                                  lines.append(gmTools.wrap (
     353                                          text = self._payload[self._idx['summary']],
     354                                          width = 60,
     355                                          initial_indent = u'  ',
     356                                          subsequent_indent = u'  '
     357                                  ))
     358
     359                  # codes ?
     360                  if with_codes:
     361                          codes = self.generic_codes
     362                          if len(codes) > 0:
     363                                  lines.append(u'')
     364                          for c in codes:
     365                                  lines.append(u' %s: %s (%s - %s)' % (
     366                                          c['code'],
     367                                          c['term'],
     368                                          c['name_short'],
     369                                          c['version']
     370                                  ))
     371                          del codes
     372
     373                  lines.append(u'')
     374
     375                  emr = patient.get_emr()
     376
     377                  # episodes
     378                  if with_episodes:
     379                          epis = emr.get_episodes(issues = [self._payload[self._idx['pk_health_issue']]])
     380                          if epis is None:
     381                                  lines.append(_('Error retrieving episodes for this health issue.'))
     382                          elif len(epis) == 0:
     383                                  lines.append(_('There are no episodes for this health issue.'))
     384                          else:
     385                                  lines.append (
     386                                          _('Episodes: %s (most recent: %s%s%s)') % (
     387                                                  len(epis),
     388                                                  gmTools.u_left_double_angle_quote,
     389                                                  emr.get_most_recent_episode(issue = self._payload[self._idx['pk_health_issue']])['description'],
     390                                                  gmTools.u_right_double_angle_quote
     391                                          )
     392                                  )
     393                                  for epi in epis:
     394                                          lines.append(u' \u00BB%s\u00AB (%s)' % (
     395                                                  epi['description'],
     396                                                  gmTools.bool2subst(epi['episode_open'], _('ongoing'), _('closed'))
     397                                          ))
     398                          lines.append('')
     399
     400                  # encounters
     401                  if with_encounters:
     402                          first_encounter = emr.get_first_encounter(issue_id = self._payload[self._idx['pk_health_issue']])
     403                          last_encounter = emr.get_last_encounter(issue_id = self._payload[self._idx['pk_health_issue']])
     404
     405                          if first_encounter is None or last_encounter is None:
     406                                  lines.append(_('No encounters found for this health issue.'))
     407                          else:
     408                                  encs = emr.get_encounters(issues = [self._payload[self._idx['pk_health_issue']]])
     409                                  lines.append(_('Encounters: %s (%s - %s):') % (
     410                                          len(encs),
     411                                          first_encounter['started_original_tz'].strftime('%m/%Y'),
     412                                          last_encounter['last_affirmed_original_tz'].strftime('%m/%Y')
     413                                  ))
     414                                  lines.append(_(' Most recent: %s - %s') % (
     415                                          last_encounter['started_original_tz'].strftime('%Y-%m-%d %H:%M'),
     416                                          last_encounter['last_affirmed_original_tz'].strftime('%H:%M')
     417                                  ))
     418
     419                  # medications
     420                  if with_medications:
     421                          meds = emr.get_current_substance_intakes (
     422                                  issues = [ self._payload[self._idx['pk_health_issue']] ],
     423                                  order_by = u'is_currently_active DESC, started, substance'
     424                          )
     425                          if len(meds) > 0:
     426                                  lines.append(u'')
     427                                  lines.append(_('Medications and Substances'))
     428                          for m in meds:
     429                                  lines.append(m.format(left_margin = (left_margin + 1)))
     430                          del meds
     431
     432                  # hospitalizations
     433                  if with_hospital_stays:
     434                          stays = emr.get_hospital_stays (
     435                                  issues = [ self._payload[self._idx['pk_health_issue']] ]
     436                          )
     437                          if len(stays) > 0:
     438                                  lines.append(u'')
     439                                  lines.append(_('Hospitalizations: %s') % len(stays))
     440                          for s in stays:
     441                                  lines.append(s.format(left_margin = (left_margin + 1)))
     442                          del stays
     443
     444                  # procedures
     445                  if with_procedures:
     446                          procs = emr.get_performed_procedures (
     447                                  issues = [ self._payload[self._idx['pk_health_issue']] ]
     448                          )
     449                          if len(procs) > 0:
     450                                  lines.append(u'')
     451                                  lines.append(_('Procedures performed: %s') % len(procs))
     452                          for p in procs:
     453                                  lines.append(p.format(left_margin = (left_margin + 1)))
     454                          del procs
     455
     456                  # family history
     457                  if with_family_history:
     458                          fhx = emr.get_family_history(issues = [ self._payload[self._idx['pk_health_issue']] ])
     459                          if len(fhx) > 0:
     460                                  lines.append(u'')
     461                                  lines.append(_('Family History: %s') % len(fhx))
     462                          for f in fhx:
     463                                  lines.append(f.format (
     464                                          left_margin = (left_margin + 1),
     465                                          include_episode = True,
     466                                          include_comment = True,
     467                                          include_codes = False
     468                                  ))
     469                          del fhx
     470
     471                  epis = self.get_episodes()
     472                  if len(epis) > 0:
     473                          epi_pks = [ e['pk_episode'] for e in epis ]
     474
     475                          # documents
     476                          if with_documents:
     477                                  doc_folder = patient.get_document_folder()
     478                                  docs = doc_folder.get_documents(episodes = epi_pks)
     479                                  if len(docs) > 0:
     480                                          lines.append(u'')
     481                                          lines.append(_('Documents: %s') % len(docs))
     482                                  del docs
     483
     484                          # test results
     485                          if with_tests:
     486                                  tests = emr.get_test_results_by_date(episodes = epi_pks)
     487                                  if len(tests) > 0:
     488                                          lines.append(u'')
     489                                          lines.append(_('Measurements and Results: %s') % len(tests))
     490                                  del tests
     491
     492                          # vaccinations
     493                          if with_vaccinations:
     494                                  vaccs = emr.get_vaccinations(episodes = epi_pks, order_by = u'date_given, vaccine')
     495                                  if len(vaccs) > 0:
     496                                          lines.append(u'')
     497                                          lines.append(_('Vaccinations:'))
     498                                  for vacc in vaccs:
     499                                          lines.extend(vacc.format(with_reaction = True))
     500                                  del vaccs
     501
     502                  del epis
     503
     504                  left_margin = u' ' * left_margin
     505                  eol_w_margin = u'\n%s' % left_margin
     506                  lines = gmTools.strip_trailing_empty_lines(lines = lines, eol = u'\n')
     507                  return left_margin + eol_w_margin.join(lines) + u'\n'
     508          #--------------------------------------------------------
     509          # properties
     510          #--------------------------------------------------------
     511          episodes = property(get_episodes, lambda x:x)
     512          #--------------------------------------------------------
     513          open_episode = property(get_open_episode, lambda x:x)
     514          #--------------------------------------------------------
     515          has_open_episode = property(has_open_episode, lambda x:x)
     516          #--------------------------------------------------------
     517 -        def _get_first_episode(self):
     518                  cmd = u"""SELECT pk_episode FROM clin.v_pat_episodes WHERE pk_health_issue = %(issue)s ORDER BY started_first limit 1"""
     519                  args = {'issue': self.pk_obj}
     520                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     521                  if len(rows) == 0:
     522                          return None
     523                  return cEpisode(aPK_obj = rows[0][0])
     524
     525          first_episode = property(_get_first_episode, lambda x:x)
     526          #--------------------------------------------------------
     527 -        def _get_latest_episode(self):
     528                  cmd = u"""SELECT
     529                          coalesce (
     530                                  (SELECT pk FROM clin.episode WHERE fk_health_issue = %(issue)s AND is_open IS TRUE),
     531                                  (SELECT pk_episode AS pk FROM clin.v_pat_episodes WHERE pk_health_issue = %(issue)s ORDER BY last_affirmed DESC limit 1)
     532                  )"""
     533                  args = {'issue': self.pk_obj}
     534                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     535                  if len(rows) == 0:
     536                          return None
     537                  if rows[0][0] is None:
     538                          return None
     539                  return cEpisode(aPK_obj = rows[0][0])
     540
     541          latest_episode = property(_get_latest_episode, lambda x:x)
     542          #--------------------------------------------------------
     543          # Steffi suggested we divide into safe and assumed start dates
     544 -        def _get_safe_start_date(self):
     545                  """This returns the date when we can assume to safely
     546                     KNOW the health issue existed (because
     547                     the provider said so)."""
     548                  args = {
     549                          'enc': self._payload[self._idx['pk_encounter']],
     550                          'pk': self._payload[self._idx['pk_health_issue']]
     551                  }
     552                  cmd = u"""
     553  SELECT COALESCE (
     554          -- this one must override all:
     555          -- .age_noted if not null and DOB is known
     556          (CASE
     557                  WHEN c_hi.age_noted IS NULL
     558                  THEN NULL::timestamp with time zone
     559                  WHEN
     560                          (SELECT d_i.dob FROM dem.identity d_i WHERE d_i.pk = (
     561                                  SELECT c_enc.fk_patient FROM clin.encounter c_enc WHERE c_enc.pk = %(enc)s
     562                          )) IS NULL
     563                  THEN NULL::timestamp with time zone
     564                  ELSE
     565                          c_hi.age_noted + (
     566                                  SELECT d_i.dob FROM dem.identity d_i WHERE d_i.pk = (
     567                                          SELECT c_enc.fk_patient FROM clin.encounter c_enc WHERE c_enc.pk = %(enc)s
     568                                  )
     569                          )
     570          END),
     571
     572          -- start of encounter in which created, earliest = explicitely set
     573          (SELECT c_enc.started AS earliest FROM clin.encounter c_enc WHERE c_enc.pk = (
     574                          c_hi.fk_encounter
     575                          --SELECT fk_encounter FROM clin.health_issue WHERE clin.health_issue.pk = %(pk)s
     576          ))
     577  )
     578  FROM clin.health_issue c_hi
     579  WHERE c_hi.pk = %(pk)s"""
     580                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
     581                  return rows[0][0]
     582
     583          safe_start_date = property(_get_safe_start_date, lambda x:x)
     584          #--------------------------------------------------------
     585 -        def _get_possible_start_date(self):
     586                  args = {'pk': self._payload[self._idx['pk_health_issue']]}
     587                  cmd = u"""
     588  SELECT MIN(earliest) FROM (
     589          -- last modification, earliest = when created in/changed to the current state
     590          (SELECT modified_when AS earliest FROM clin.health_issue WHERE pk = %(pk)s)
     591
     592  UNION ALL
     593          -- last modification of encounter in which created, earliest = initial creation of that encounter
     594          (SELECT c_enc.modified_when AS earliest FROM clin.encounter c_enc WHERE c_enc.pk = (
     595                  SELECT c_hi.fk_encounter FROM clin.health_issue c_hi WHERE c_hi.pk = %(pk)s
     596          ))
     597
     598  UNION ALL
     599          -- earliest explicit .clin_when of clinical items linked to this health_issue
     600          (SELECT MIN(c_vpi.clin_when) AS earliest FROM clin.v_pat_items c_vpi WHERE c_vpi.pk_health_issue = %(pk)s)
     601
     602  UNION ALL
     603          -- earliest modification time of clinical items linked to this health issue
     604          -- this CAN be used since if an item is linked to a health issue it can be
     605          -- assumed the health issue (should have) existed at the time of creation
     606          (SELECT MIN(c_vpi.modified_when) AS earliest FROM clin.v_pat_items c_vpi WHERE c_vpi.pk_health_issue = %(pk)s)
     607
     608  UNION ALL
     609          -- earliest start of encounters of clinical items linked to this episode
     610          (SELECT MIN(c_enc.started) AS earliest FROM clin.encounter c_enc WHERE c_enc.pk IN (
     611                  SELECT c_vpi.pk_encounter FROM clin.v_pat_items c_vpi WHERE c_vpi.pk_health_issue = %(pk)s
     612          ))
     613
     614  -- here we should be looking at
     615  -- .best_guess_start_date of all episodes linked to this encounter
     616
     617  ) AS candidates"""
     618
     619                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
     620                  return rows[0][0]
     621
     622          possible_start_date = property(_get_possible_start_date)
     623          #--------------------------------------------------------
     624 -        def _get_end_date(self):
     625                  if self._payload[self._idx['is_active']]:
     626                          return gmDateTime.pydt_now_here()
     627                  if self.has_open_episode:
     628                          return gmDateTime.pydt_now_here()
     629                  return self.latest_access_date
     630
     631          end_date = property(_get_end_date)
     632          #--------------------------------------------------------
     633 -        def _get_latest_access_date(self):
     634                  args = {
     635                          'enc': self._payload[self._idx['pk_encounter']],
     636                          'pk': self._payload[self._idx['pk_health_issue']]
     637                  }
     638                  cmd = u"""
     639  SELECT
     640          MAX(latest)
     641  FROM (
     642          -- last modification, latest = when last changed to the current state
     643          -- DO NOT USE: database upgrades may change this field
     644          (SELECT modified_when AS latest FROM clin.health_issue WHERE pk = %(pk)s)
     645
     646          --UNION ALL
     647          -- last modification of encounter in which created, latest = initial creation of that encounter
     648          -- DO NOT USE: just because one corrects a typo does not mean the issue took any longer
     649          --(SELECT c_enc.modified_when AS latest FROM clin.encounter c_enc WHERE c_enc.pk = (
     650          --      SELECT fk_encounter FROM clin.episode WHERE pk = %(pk)s
     651          -- )
     652          --)
     653
     654          --UNION ALL
     655          -- end of encounter in which created, latest = explicitely set
     656          -- DO NOT USE: we can retrospectively create issues which
     657          -- DO NOT USE: are long since finished
     658          --(SELECT c_enc.last_affirmed AS latest FROM clin.encounter c_enc WHERE c_enc.pk = (
     659          --      SELECT fk_encounter FROM clin.episode WHERE pk = %(pk)s
     660          -- )
     661          --)
     662
     663          UNION ALL
     664          -- latest end of encounters of clinical items linked to this issue
     665          (SELECT
     666                  MAX(last_affirmed) AS latest
     667           FROM clin.encounter
     668           WHERE pk IN (
     669                  SELECT pk_encounter FROM clin.v_pat_items WHERE pk_health_issue = %(pk)s
     670           )
     671          )
     672
     673          UNION ALL
     674          -- latest explicit .clin_when of clinical items linked to this issue
     675          (SELECT
     676                  MAX(clin_when) AS latest
     677           FROM clin.v_pat_items
     678           WHERE pk_health_issue = %(pk)s
     679          )
     680
     681          -- latest modification time of clinical items linked to this issue
     682          -- this CAN be used since if an item is linked to an issue it can be
     683          -- assumed the issue (should have) existed at the time of modification
     684          -- DO NOT USE, because typo fixes should not extend the issue
     685          --(SELECT MIN(modified_when) AS latest FROM clin.clin_root_item WHERE fk_episode = %(pk)s)
     686
     687  ) AS candidates"""
     688                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}])
     689                  return rows[0][0]
     690
     691          latest_access_date = property(_get_latest_access_date)
     692          #--------------------------------------------------------
     693 -        def _get_laterality_description(self):
     694                  try:
     695                          return laterality2str[self._payload[self._idx['laterality']]]
     696                  except KeyError:
     697                          return u'<???>'
     698
     699          laterality_description = property(_get_laterality_description, lambda x:x)
     700          #--------------------------------------------------------
     701 -        def _get_diagnostic_certainty_description(self):
     702                  return diagnostic_certainty_classification2str(self._payload[self._idx['diagnostic_certainty_classification']])
     703
     704          diagnostic_certainty_description = property(_get_diagnostic_certainty_description, lambda x:x)
     705          #--------------------------------------------------------
     706 -        def _get_generic_codes(self):
     707                  if len(self._payload[self._idx['pk_generic_codes']]) == 0:
     708                          return []
     709
     710                  cmd = gmCoding._SQL_get_generic_linked_codes % u'pk_generic_code IN %(pks)s'
     711                  args = {'pks': tuple(self._payload[self._idx['pk_generic_codes']])}
     712                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     713                  return [ gmCoding.cGenericLinkedCode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_lnk_code2item'}) for r in rows ]
     714
     715 -        def _set_generic_codes(self, pk_codes):
     716                  queries = []
     717                  # remove all codes
     718                  if len(self._payload[self._idx['pk_generic_codes']]) > 0:
     719                          queries.append ({
     720                                  'cmd': u'DELETE FROM clin.lnk_code2h_issue WHERE fk_item = %(issue)s AND fk_generic_code IN %(codes)s',
     721                                  'args': {
     722                                          'issue': self._payload[self._idx['pk_health_issue']],
     723                                          'codes': tuple(self._payload[self._idx['pk_generic_codes']])
     724                                  }
     725                          })
     726                  # add new codes
     727                  for pk_code in pk_codes:
     728                          queries.append ({
     729                                  'cmd': u'INSERT INTO clin.lnk_code2h_issue (fk_item, fk_generic_code) VALUES (%(issue)s, %(pk_code)s)',
     730                                  'args': {
     731                                          'issue': self._payload[self._idx['pk_health_issue']],
     732                                          'pk_code': pk_code
     733                                  }
     734                          })
     735                  if len(queries) == 0:
     736                          return
     737                  # run it all in one transaction
     738                  rows, idx = gmPG2.run_rw_queries(queries = queries)
     739                  return
     740
     741          generic_codes = property(_get_generic_codes, _set_generic_codes)
     742  #============================================================
     743 -def create_health_issue(description=None, encounter=None, patient=None):
     744          """Creates a new health issue for a given patient.
     745
     746          description - health issue name
     747          """
     748          try:
     749                  h_issue = cHealthIssue(name = description, encounter = encounter, patient = patient)
     750                  return h_issue
     751          except gmExceptions.NoSuchBusinessObjectError:
     752                  pass
     753
     754          queries = []
     755          cmd = u"insert into clin.health_issue (description, fk_encounter) values (%(desc)s, %(enc)s)"
     756          queries.append({'cmd': cmd, 'args': {'desc': description, 'enc': encounter}})
     757
     758          cmd = u"select currval('clin.health_issue_pk_seq')"
     759          queries.append({'cmd': cmd})
     760
     761          rows, idx = gmPG2.run_rw_queries(queries = queries, return_data = True)
     762          h_issue = cHealthIssue(aPK_obj = rows[0][0])
     763
     764          return h_issue
     765  #-----------------------------------------------------------
     766 -def delete_health_issue(health_issue=None):
     767          if isinstance(health_issue, cHealthIssue):
     768                  pk = health_issue['pk_health_issue']
     769          else:
     770                  pk = int(health_issue)
     771
     772          try:
     773                  gmPG2.run_rw_queries(queries = [{'cmd': u'delete from clin.health_issue where pk=%(pk)s', 'args': {'pk': pk}}])
     774          except gmPG2.dbapi.IntegrityError:
     775                  # should be parsing pgcode/and or error message
     776                  _log.exception('cannot delete health issue')
     777                  raise gmExceptions.DatabaseObjectInUseError('cannot delete health issue, it is in use')
     778  #------------------------------------------------------------
     779  # use as dummy for unassociated episodes
     780 -def get_dummy_health_issue():
     781          issue = {
     782                  'pk_health_issue': None,
     783                  'description': _('Unattributed episodes'),
     784                  'age_noted': None,
     785                  'laterality': u'na',
     786                  'is_active': True,
     787                  'clinically_relevant': True,
     788                  'is_confidential': None,
     789                  'is_cause_of_death': False,
     790                  'is_dummy': True,
     791                  'grouping': None
     792          }
     793          return issue
     794  #-----------------------------------------------------------
     795 -def health_issue2problem(health_issue=None, allow_irrelevant=False):
     796          return cProblem (
     797                  aPK_obj = {
     798                          'pk_patient': health_issue['pk_patient'],
     799                          'pk_health_issue': health_issue['pk_health_issue'],
     800                          'pk_episode': None
     801                  },
     802                  try_potential_problems = allow_irrelevant
     803          )
     804  #============================================================
     805  # episodes API
     806  #============================================================
     807 -class cEpisode(gmBusinessDBObject.cBusinessDBObject):
     808          """Represents one clinical episode.
     809          """
     810          _cmd_fetch_payload = u"select * from clin.v_pat_episodes where pk_episode=%s"
     811          _cmds_store_payload = [
     812                  u"""update clin.episode set
     813                                  fk_health_issue = %(pk_health_issue)s,
     814                                  is_open = %(episode_open)s::boolean,
     815                                  description = %(description)s,
     816                                  summary = gm.nullify_empty_string(%(summary)s),
     817                                  diagnostic_certainty_classification = gm.nullify_empty_string(%(diagnostic_certainty_classification)s)
     818                          where
     819                                  pk = %(pk_episode)s and
     820                                  xmin = %(xmin_episode)s""",
     821                  u"""select xmin_episode from clin.v_pat_episodes where pk_episode = %(pk_episode)s"""
     822          ]
     823          _updatable_fields = [
     824                  'pk_health_issue',
     825                  'episode_open',
     826                  'description',
     827                  'summary',
     828                  'diagnostic_certainty_classification'
     829          ]
     830          #--------------------------------------------------------
     831 -        def __init__(self, aPK_obj=None, id_patient=None, name='xxxDEFAULTxxx', health_issue=None, row=None, encounter=None):
     832                  pk = aPK_obj
     833                  if pk is None and row is None:
     834
     835                          where_parts = [u'description = %(desc)s']
     836
     837                          if id_patient is not None:
     838                                  where_parts.append(u'pk_patient = %(pat)s')
     839
     840                          if health_issue is not None:
     841                                  where_parts.append(u'pk_health_issue = %(issue)s')
     842
     843                          if encounter is not None:
     844                                  where_parts.append(u'pk_patient = (select fk_patient from clin.encounter where pk = %(enc)s)')
     845
     846                          args = {
     847                                  'pat': id_patient,
     848                                  'issue': health_issue,
     849                                  'enc': encounter,
     850                                  'desc': name
     851                          }
     852
     853                          cmd = u"select * from clin.v_pat_episodes where %s" % u' and '.join(where_parts)
     854
     855                          rows, idx = gmPG2.run_ro_queries(
     856                                  queries = [{'cmd': cmd, 'args': args}],
     857                                  get_col_idx=True
     858                          )
     859
     860                          if len(rows) == 0:
     861                                  raise gmExceptions.NoSuchBusinessObjectError, 'no episode for [%s:%s:%s:%s]' % (id_patient, name, health_issue, encounter)
     862
     863                          r = {'idx': idx, 'data': rows[0], 'pk_field': 'pk_episode'}
     864                          gmBusinessDBObject.cBusinessDBObject.__init__(self, row=r)
     865
     866                  else:
     867                          gmBusinessDBObject.cBusinessDBObject.__init__(self, aPK_obj=pk, row=row)
     868          #--------------------------------------------------------
     869          # external API
     870          #--------------------------------------------------------
     871 -        def get_access_range(self):
     872                  """Get earliest and latest access to this episode.
     873
     874                  Returns a tuple(earliest, latest).
     875                  """
     876                  return (self.best_guess_start_date, self.latest_access_date)
     877          #--------------------------------------------------------
     878 -        def get_patient(self):
     879                  return self._payload[self._idx['pk_patient']]
     880          #--------------------------------------------------------
     881 -        def get_narrative(self, soap_cats=None, encounters=None, order_by = None):
     882                  return gmClinNarrative.get_narrative (
     883                          soap_cats = soap_cats,
     884                          encounters = encounters,
     885                          episodes = [self.pk_obj],
     886                          order_by = order_by
     887                  )
     888          #--------------------------------------------------------
     889 -        def rename(self, description=None):
     890                  """Method for episode editing, that is, episode renaming.
     891
     892                  @param description
     893                          - the new descriptive name for the encounter
     894                  @type description
     895                          - a string instance
     896                  """
     897                  # sanity check
     898                  if description.strip() == '':
     899                          _log.error('<description> must be a non-empty string instance')
     900                          return False
     901                  # update the episode description
     902                  old_description = self._payload[self._idx['description']]
     903                  self._payload[self._idx['description']] = description.strip()
     904                  self._is_modified = True
     905                  successful, data = self.save_payload()
     906                  if not successful:
     907                          _log.error('cannot rename episode [%s] to [%s]' % (self, description))
     908                          self._payload[self._idx['description']] = old_description
     909                          return False
     910                  return True
     911          #--------------------------------------------------------
     912 -        def add_code(self, pk_code=None):
     913                  """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
     914
     915                  if pk_code in self._payload[self._idx['pk_generic_codes']]:
     916                          return
     917
     918                  cmd = u"""
     919                          INSERT INTO clin.lnk_code2episode
     920                                  (fk_item, fk_generic_code)
     921                          SELECT
     922                                  %(item)s,
     923                                  %(code)s
     924                          WHERE NOT EXISTS (
     925                                  SELECT 1 FROM clin.lnk_code2episode
     926                                  WHERE
     927                                          fk_item = %(item)s
     928                                                  AND
     929                                          fk_generic_code = %(code)s
     930                          )"""
     931                  args = {
     932                          'item': self._payload[self._idx['pk_episode']],
     933                          'code': pk_code
     934                  }
     935                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     936                  return
     937          #--------------------------------------------------------
     938 -        def remove_code(self, pk_code=None):
     939                  """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
     940                  cmd = u"DELETE FROM clin.lnk_code2episode WHERE fk_item = %(item)s AND fk_generic_code = %(code)s"
     941                  args = {
     942                          'item': self._payload[self._idx['pk_episode']],
     943                          'code': pk_code
     944                  }
     945                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     946                  return True
     947          #--------------------------------------------------------
     948 -        def format_as_journal(self, left_margin=0, date_format='%a, %b %d %Y'):
     949                  rows = gmClinNarrative.get_as_journal (
     950                          episodes = (self.pk_obj,),
     951                          order_by = u'pk_encounter, clin_when, scr, src_table'
     952                          #order_by = u'pk_encounter, scr, clin_when, src_table'
     953                  )
     954
     955                  if len(rows) == 0:
     956                          return u''
     957
     958                  lines = []
     959
     960                  lines.append(_('Clinical data generated during encounters within this episode:'))
     961
     962                  left_margin = u' ' * left_margin
     963
     964                  prev_enc = None
     965                  for row in rows:
     966                          if row['pk_encounter'] != prev_enc:
     967                                  lines.append(u'')
     968                                  prev_enc = row['pk_encounter']
     969
     970                          when = row['clin_when'].strftime(date_format).decode(gmI18N.get_encoding())
     971                          top_row = u'%s%s %s (%s) %s' % (
     972                                  gmTools.u_box_top_left_arc,
     973                                  gmTools.u_box_horiz_single,
     974                                  gmClinNarrative.soap_cat2l10n_str[row['real_soap_cat']],
     975                                  when,
     976                                  gmTools.u_box_horiz_single * 5
     977                          )
     978                          soap = gmTools.wrap (
     979                                  text = row['narrative'],
     980                                  width = 60,
     981                                  initial_indent = u'  ',
     982                                  subsequent_indent = u'  ' + left_margin
     983                          )
     984                          row_ver = u''
     985                          if row['row_version'] > 0:
     986                                  row_ver = u'v%s: ' % row['row_version']
     987                          bottom_row = u'%s%s %s, %s%s %s' % (
     988                                  u' ' * 40,
     989                                  gmTools.u_box_horiz_light_heavy,
     990                                  row['modified_by'],
     991                                  row_ver,
     992                                  row['date_modified'],
     993                                  gmTools.u_box_horiz_heavy_light
     994                          )
     995
     996                          lines.append(top_row)
     997                          lines.append(soap)
     998                          lines.append(bottom_row)
     999
    1000                  eol_w_margin = u'\n%s' % left_margin
    1001                  return left_margin + eol_w_margin.join(lines) + u'\n'
    1002          #--------------------------------------------------------
    1003 -        def format(self, left_margin=0, patient=None,
    1004                  with_summary=True,
    1005                  with_codes=True,
    1006                  with_encounters=True,
    1007                  with_documents=True,
    1008                  with_hospital_stays=True,
    1009                  with_procedures=True,
    1010                  with_family_history=True,
    1011                  with_tests=True,
    1012                  with_vaccinations=True,
    1013                  with_health_issue=False
    1014          ):
    1015
    1016                  if patient.ID != self._payload[self._idx['pk_patient']]:
    1017                          msg = '<patient>.ID = %s but episode %s belongs to patient %s' % (
    1018                                  patient.ID,
    1019                                  self._payload[self._idx['pk_episode']],
    1020                                  self._payload[self._idx['pk_patient']]
    1021                          )
    1022                          raise ValueError(msg)
    1023
    1024                  lines = []
    1025
    1026                  # episode details
    1027                  lines.append (_('Episode %s%s%s   [#%s]') % (
    1028                          gmTools.u_left_double_angle_quote,
    1029                          self._payload[self._idx['description']],
    1030                          gmTools.u_right_double_angle_quote,
    1031                          self._payload[self._idx['pk_episode']]
    1032                  ))
    1033
    1034                  enc = cEncounter(aPK_obj = self._payload[self._idx['pk_encounter']])
    1035                  lines.append (u' ' + _('Created during encounter: %s (%s - %s)   [#%s]') % (
    1036                          enc['l10n_type'],
    1037                          enc['started_original_tz'].strftime('%Y-%m-%d %H:%M'),
    1038                          enc['last_affirmed_original_tz'].strftime('%H:%M'),
    1039                          self._payload[self._idx['pk_encounter']]
    1040                  ))
    1041
    1042                  emr = patient.get_emr()
    1043                  encs = emr.get_encounters(episodes = [self._payload[self._idx['pk_episode']]])
    1044                  first_encounter = None
    1045                  last_encounter = None
    1046                  if (encs is not None) and (len(encs) > 0):
    1047                          first_encounter = emr.get_first_encounter(episode_id = self._payload[self._idx['pk_episode']])
    1048                          last_encounter = emr.get_last_encounter(episode_id = self._payload[self._idx['pk_episode']])
    1049                          if self._payload[self._idx['episode_open']]:
    1050                                  end = gmDateTime.pydt_now_here()
    1051                                  end_str = gmTools.u_ellipsis
    1052                          else:
    1053                                  end = last_encounter['last_affirmed']
    1054                                  end_str = last_encounter['last_affirmed'].strftime('%m/%Y')
    1055                          age = gmDateTime.format_interval_medically(end - first_encounter['started'])
    1056                          lines.append(_(' Duration: %s (%s - %s)') % (
    1057                                  age,
    1058                                  first_encounter['started'].strftime('%m/%Y'),
    1059                                  end_str
    1060                          ))
    1061
    1062                  lines.append(u' ' + _('Status') + u': %s%s' % (
    1063                          gmTools.bool2subst(self._payload[self._idx['episode_open']], _('active'), _('finished')),
    1064                          gmTools.coalesce (
    1065                                  initial = diagnostic_certainty_classification2str(self._payload[self._idx['diagnostic_certainty_classification']]),
    1066                                  instead = u'',
    1067                                  template_initial = u', %s',
    1068                                  none_equivalents = [None, u'']
    1069                          )
    1070                  ))
    1071
    1072                  if with_health_issue:
    1073                          lines.append(u' ' + _('Health issue') + u': %s' % gmTools.coalesce (
    1074                                  self._payload[self._idx['health_issue']],
    1075                                  _('none associated')
    1076                          ))
    1077
    1078                  if with_summary:
    1079                          if self._payload[self._idx['summary']] is not None:
    1080                                  lines.append(u' %s:' % _('Synopsis'))
    1081                                  lines.append(gmTools.wrap (
    1082                                                  text = self._payload[self._idx['summary']],
    1083                                                  width = 60,
    1084                                                  initial_indent = u'  ',
    1085                                                  subsequent_indent = u'  '
    1086                                          )
    1087                                  )
    1088
    1089                  # codes
    1090                  if with_codes:
    1091                          codes = self.generic_codes
    1092                          if len(codes) > 0:
    1093                                  lines.append(u'')
    1094                          for c in codes:
    1095                                  lines.append(u' %s: %s (%s - %s)' % (
    1096                                          c['code'],
    1097                                          c['term'],
    1098                                          c['name_short'],
    1099                                          c['version']
    1100                                  ))
    1101                          del codes
    1102
    1103                  lines.append(u'')
    1104
    1105                  # encounters
    1106                  if with_encounters:
    1107                          if encs is None:
    1108                                  lines.append(_('Error retrieving encounters for this health issue.'))
    1109                          elif len(encs) == 0:
    1110                                  #lines.append(_('There are no encounters for this issue.'))
    1111                                  pass
    1112                          else:
    1113                                  lines.append(_('Last worked on: %s\n') % last_encounter['last_affirmed_original_tz'].strftime('%Y-%m-%d %H:%M'))
    1114
    1115                                  if len(encs) < 4:
    1116                                          line = _('%s encounter(s) (%s - %s):')
    1117                                  else:
    1118                                          line = _('1st and (up to 3) most recent (of %s) encounters (%s - %s):')
    1119                                  lines.append(line % (
    1120                                          len(encs),
    1121                                          first_encounter['started'].strftime('%m/%Y'),
    1122                                          last_encounter['last_affirmed'].strftime('%m/%Y')
    1123                                  ))
    1124
    1125                                  lines.append(u' %s - %s (%s):%s' % (
    1126                                          first_encounter['started_original_tz'].strftime('%Y-%m-%d %H:%M'),
    1127                                          first_encounter['last_affirmed_original_tz'].strftime('%H:%M'),
    1128                                          first_encounter['l10n_type'],
    1129                                          gmTools.coalesce (
    1130                                                  first_encounter['assessment_of_encounter'],
    1131                                                  gmTools.coalesce (
    1132                                                          first_encounter['reason_for_encounter'],
    1133                                                          u'',
    1134                                                          u' \u00BB%s\u00AB' + (u' (%s)' % _('RFE'))
    1135                                                  ),
    1136                                                  u' \u00BB%s\u00AB' + (u' (%s)' % _('AOE'))
    1137                                          )
    1138                                  ))
    1139
    1140                                  if len(encs) > 4:
    1141                                          lines.append(_(' ... %s skipped ...') % (len(encs) - 4))
    1142
    1143                                  for enc in encs[1:][-3:]:
    1144                                          lines.append(u' %s - %s (%s):%s' % (
    1145                                                  enc['started_original_tz'].strftime('%Y-%m-%d %H:%M'),
    1146                                                  enc['last_affirmed_original_tz'].strftime('%H:%M'),
    1147                                                  enc['l10n_type'],
    1148                                                  gmTools.coalesce (
    1149                                                          enc['assessment_of_encounter'],
    1150                                                          gmTools.coalesce (
    1151                                                                  enc['reason_for_encounter'],
    1152                                                                  u'',
    1153                                                                  u' \u00BB%s\u00AB' + (u' (%s)' % _('RFE'))
    1154                                                          ),
    1155                                                          u' \u00BB%s\u00AB' + (u' (%s)' % _('AOE'))
    1156                                                  )
    1157                                          ))
    1158                                  del encs
    1159
    1160                          # spell out last encounter
    1161                          if last_encounter is not None:
    1162                                  lines.append('')
    1163                                  lines.append(_('Progress notes in most recent encounter:'))
    1164                                  lines.extend(last_encounter.format_soap (
    1165                                          episodes = [ self._payload[self._idx['pk_episode']] ],
    1166                                          left_margin = left_margin,
    1167                                          soap_cats = 'soapu',
    1168                                          emr = emr
    1169                                  ))
    1170
    1171                  # documents
    1172                  if with_documents:
    1173                          doc_folder = patient.get_document_folder()
    1174                          docs = doc_folder.get_documents (
    1175                                  episodes = [ self._payload[self._idx['pk_episode']] ]
    1176                          )
    1177                          if len(docs) > 0:
    1178                                  lines.append('')
    1179                                  lines.append(_('Documents: %s') % len(docs))
    1180                          for d in docs:
    1181                                  lines.append(u' %s %s:%s%s' % (
    1182                                          d['clin_when'].strftime('%Y-%m-%d'),
    1183                                          d['l10n_type'],
    1184                                          gmTools.coalesce(d['comment'], u'', u' "%s"'),
    1185                                          gmTools.coalesce(d['ext_ref'], u'', u' (%s)')
    1186                                  ))
    1187                          del docs
    1188
    1189                  # hospitalizations
    1190                  if with_hospital_stays:
    1191                          stays = emr.get_hospital_stays(episodes = [ self._payload[self._idx['pk_episode']] ])
    1192                          if len(stays) > 0:
    1193                                  lines.append('')
    1194                                  lines.append(_('Hospitalizations: %s') % len(stays))
    1195                          for s in stays:
    1196                                  lines.append(s.format(left_margin = (left_margin + 1)))
    1197                          del stays
    1198
    1199                  # procedures
    1200                  if with_procedures:
    1201                          procs = emr.get_performed_procedures(episodes = [ self._payload[self._idx['pk_episode']] ])
    1202                          if len(procs) > 0:
    1203                                  lines.append(u'')
    1204                                  lines.append(_('Procedures performed: %s') % len(procs))
    1205                          for p in procs:
    1206                                  lines.append(p.format (
    1207                                          left_margin = (left_margin + 1),
    1208                                          include_episode = False,
    1209                                          include_codes = True
    1210                                  ))
    1211                          del procs
    1212
    1213                  # family history
    1214                  if with_family_history:
    1215                          fhx = emr.get_family_history(episodes = [ self._payload[self._idx['pk_episode']] ])
    1216                          if len(fhx) > 0:
    1217                                  lines.append(u'')
    1218                                  lines.append(_('Family History: %s') % len(fhx))
    1219                          for f in fhx:
    1220                                  lines.append(f.format (
    1221                                          left_margin = (left_margin + 1),
    1222                                          include_episode = False,
    1223                                          include_comment = True,
    1224                                          include_codes = True
    1225                                  ))
    1226                          del fhx
    1227
    1228                  # test results
    1229                  if with_tests:
    1230                          tests = emr.get_test_results_by_date(episodes = [ self._payload[self._idx['pk_episode']] ])
    1231                          if len(tests) > 0:
    1232                                  lines.append('')
    1233                                  lines.append(_('Measurements and Results:'))
    1234                          for t in tests:
    1235                                  lines.append(t.format (
    1236                                          with_review = False,
    1237                                          with_ranges = False,
    1238                                          with_evaluation = False,
    1239                                          with_episode = False,
    1240                                          with_type_details = False,
    1241                                          date_format = '%Y %b %d'
    1242                                  ))
    1243                          del tests
    1244
    1245                  # vaccinations
    1246                  if with_vaccinations:
    1247                          vaccs = emr.get_vaccinations (
    1248                                  episodes = [ self._payload[self._idx['pk_episode']] ],
    1249                                  order_by = u'date_given DESC, vaccine'
    1250                          )
    1251                          if len(vaccs) > 0:
    1252                                  lines.append(u'')
    1253                                  lines.append(_('Vaccinations:'))
    1254                          for vacc in vaccs:
    1255                                  lines.extend(vacc.format (
    1256                                          with_indications = True,
    1257                                          with_comment = True,
    1258                                          with_reaction = True,
    1259                                          date_format = '%Y-%m-%d'
    1260                                  ))
    1261                          del vaccs
    1262
    1263                  left_margin = u' ' * left_margin
    1264                  eol_w_margin = u'\n%s' % left_margin
    1265                  lines = gmTools.strip_trailing_empty_lines(lines = lines, eol = u'\n')
    1266                  return left_margin + eol_w_margin.join(lines) + u'\n'
    1267          #--------------------------------------------------------
    1268          # properties
    1269          #--------------------------------------------------------
    1270 -        def _get_best_guess_start_date(self):
    1271                  cmd = u"""
    1272  SELECT
    1273          MIN(earliest)
    1274  FROM (
    1275          -- last modification, earliest = when created in/changed to the current state
    1276          (SELECT c_epi.modified_when AS earliest FROM clin.episode c_epi WHERE c_epi.pk = %(pk)s)
    1277
    1278                  UNION ALL
    1279
    1280          -- last modification of encounter in which created, earliest = initial creation of that encounter
    1281          (SELECT c_enc.modified_when AS earliest FROM clin.encounter c_enc WHERE c_enc.pk = (
    1282                  SELECT fk_encounter FROM clin.episode WHERE pk = %(pk)s
    1283           )
    1284          )
    1285                  UNION ALL
    1286
    1287          -- start of encounter in which created, earliest = explicitely set
    1288          (SELECT c_enc.started AS earliest FROM clin.encounter c_enc WHERE c_enc.pk = (
    1289                  SELECT fk_encounter FROM clin.episode WHERE pk = %(pk)s
    1290           )
    1291          )
    1292                  UNION ALL
    1293
    1294          -- earliest start of encounters of clinical items linked to this episode
    1295          (SELECT MIN(started) AS earliest FROM clin.encounter WHERE pk IN (
    1296                  SELECT fk_encounter FROM clin.clin_root_item WHERE fk_episode = %(pk)s
    1297           )
    1298          )
    1299                  UNION ALL
    1300
    1301          -- earliest explicit .clin_when of clinical items linked to this episode
    1302          (SELECT MIN(clin_when) AS earliest FROM clin.clin_root_item WHERE fk_episode = %(pk)s)
    1303
    1304                  UNION ALL
    1305
    1306          -- earliest modification time of clinical items linked to this episode
    1307          -- this CAN be used since if an item is linked to an episode it can be
    1308          -- assumed the episode (should have) existed at the time of creation
    1309          (SELECT MIN(modified_when) AS earliest FROM clin.clin_root_item WHERE fk_episode = %(pk)s)
    1310
    1311          -- not sure about this one:
    1312          -- .pk -> clin.clin_root_item.fk_encounter.modified_when
    1313
    1314  ) AS candidates"""
    1315                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}])
    1316                  return rows[0][0]
    1317
    1318          best_guess_start_date = property(_get_best_guess_start_date)
    1319          #--------------------------------------------------------
    1320 -        def _get_latest_access_date(self):
    1321                  cmd = u"""
    1322  SELECT
    1323          MAX(latest)
    1324  FROM (
    1325          -- last modification, latest = when last changed to the current state
    1326          (SELECT c_epi.modified_when AS latest, 'clin.episode.modified_when'::text AS candidate FROM clin.episode c_epi WHERE c_epi.pk = %(pk)s)
    1327
    1328                  UNION ALL
    1329
    1330          -- last modification of encounter in which created, latest = initial creation of that encounter
    1331          -- DO NOT USE: just because one corrects a typo does not mean the episode took longer
    1332          --(SELECT c_enc.modified_when AS latest FROM clin.encounter c_enc WHERE c_enc.pk = (
    1333          --      SELECT fk_encounter FROM clin.episode WHERE pk = %(pk)s
    1334          -- )
    1335          --)
    1336
    1337          -- end of encounter in which created, latest = explicitely set
    1338          -- DO NOT USE: we can retrospectively create episodes which
    1339          -- DO NOT USE: are long since finished
    1340          --(SELECT c_enc.last_affirmed AS latest FROM clin.encounter c_enc WHERE c_enc.pk = (
    1341          --      SELECT fk_encounter FROM clin.episode WHERE pk = %(pk)s
    1342          -- )
    1343          --)
    1344
    1345          -- latest end of encounters of clinical items linked to this episode
    1346          (SELECT
    1347                  MAX(last_affirmed) AS latest,
    1348                  'clin.episode.pk = clin.clin_root_item,fk_episode -> .fk_encounter.last_affirmed'::text AS candidate
    1349           FROM clin.encounter
    1350           WHERE pk IN (
    1351                  SELECT fk_encounter FROM clin.clin_root_item WHERE fk_episode = %(pk)s
    1352           )
    1353          )
    1354                  UNION ALL
    1355
    1356          -- latest explicit .clin_when of clinical items linked to this episode
    1357          (SELECT
    1358                  MAX(clin_when) AS latest,
    1359                  'clin.episode.pk = clin.clin_root_item,fk_episode -> .clin_when'::text AS candidate
    1360           FROM clin.clin_root_item
    1361           WHERE fk_episode = %(pk)s
    1362          )
    1363
    1364          -- latest modification time of clinical items linked to this episode
    1365          -- this CAN be used since if an item is linked to an episode it can be
    1366          -- assumed the episode (should have) existed at the time of creation
    1367          -- DO NOT USE, because typo fixes should not extend the episode
    1368          --(SELECT MIN(modified_when) AS latest FROM clin.clin_root_item WHERE fk_episode = %(pk)s)
    1369
    1370          -- not sure about this one:
    1371          -- .pk -> clin.clin_root_item.fk_encounter.modified_when
    1372
    1373  ) AS candidates"""
    1374                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}])
    1375                  #_log.debug('last episode access: %s (%s)', rows[0][0], rows[0][1])
    1376                  return rows[0][0]
    1377
    1378          latest_access_date = property(_get_latest_access_date)
    1379          #--------------------------------------------------------
    1380 -        def _get_diagnostic_certainty_description(self):
    1381                  return diagnostic_certainty_classification2str(self._payload[self._idx['diagnostic_certainty_classification']])
    1382
    1383          diagnostic_certainty_description = property(_get_diagnostic_certainty_description, lambda x:x)
    1384          #--------------------------------------------------------
    1385 -        def _get_generic_codes(self):
    1386                  if len(self._payload[self._idx['pk_generic_codes']]) == 0:
    1387                          return []
    1388
    1389                  cmd = gmCoding._SQL_get_generic_linked_codes % u'pk_generic_code IN %(pks)s'
    1390                  args = {'pks': tuple(self._payload[self._idx['pk_generic_codes']])}
    1391                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1392                  return [ gmCoding.cGenericLinkedCode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_lnk_code2item'}) for r in rows ]
    1393
    1394 -        def _set_generic_codes(self, pk_codes):
    1395                  queries = []
    1396                  # remove all codes
    1397                  if len(self._payload[self._idx['pk_generic_codes']]) > 0:
    1398                          queries.append ({
    1399                                  'cmd': u'DELETE FROM clin.lnk_code2episode WHERE fk_item = %(epi)s AND fk_generic_code IN %(codes)s',
    1400                                  'args': {
    1401                                          'epi': self._payload[self._idx['pk_episode']],
    1402                                          'codes': tuple(self._payload[self._idx['pk_generic_codes']])
    1403                                  }
    1404                          })
    1405                  # add new codes
    1406                  for pk_code in pk_codes:
    1407                          queries.append ({
    1408                                  'cmd': u'INSERT INTO clin.lnk_code2episode (fk_item, fk_generic_code) VALUES (%(epi)s, %(pk_code)s)',
    1409                                  'args': {
    1410                                          'epi': self._payload[self._idx['pk_episode']],
    1411                                          'pk_code': pk_code
    1412                                  }
    1413                          })
    1414                  if len(queries) == 0:
    1415                          return
    1416                  # run it all in one transaction
    1417                  rows, idx = gmPG2.run_rw_queries(queries = queries)
    1418                  return
    1419
    1420          generic_codes = property(_get_generic_codes, _set_generic_codes)
    1421          #--------------------------------------------------------
    1422 -        def _get_has_narrative(self):
    1423                  cmd = u"""SELECT EXISTS (
    1424                          SELECT 1 FROM clin.clin_narrative
    1425                          WHERE
    1426                                  fk_episode = %(epi)s
    1427                                          AND
    1428                                  fk_encounter IN (
    1429                                          SELECT pk FROM clin.encounter WHERE fk_patient = %(pat)s
    1430                                  )
    1431                  )"""
    1432                  args = {
    1433                          u'pat': self._payload[self._idx['pk_patient']],
    1434                          u'epi': self._payload[self._idx['pk_episode']]
    1435                  }
    1436                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
    1437                  return rows[0][0]
    1438
    1439          has_narrative = property(_get_has_narrative, lambda x:x)
    1440  #============================================================
    1441 -def create_episode(pk_health_issue=None, episode_name=None, is_open=False, allow_dupes=False, encounter=None):
    1442          """Creates a new episode for a given patient's health issue.
    1443
    1444          pk_health_issue - given health issue PK
    1445          episode_name - name of episode
    1446          """
    1447          if not allow_dupes:
    1448                  try:
    1449                          episode = cEpisode(name = episode_name, health_issue = pk_health_issue, encounter = encounter)
    1450                          if episode['episode_open'] != is_open:
    1451                                  episode['episode_open'] = is_open
    1452                                  episode.save_payload()
    1453                          return episode
    1454                  except gmExceptions.ConstructorError:
    1455                          pass
    1456
    1457          queries = []
    1458          cmd = u"INSERT INTO clin.episode (fk_health_issue, description, is_open, fk_encounter) VALUES (%s, %s, %s::boolean, %s)"
    1459          queries.append({'cmd': cmd, 'args': [pk_health_issue, episode_name, is_open, encounter]})
    1460          queries.append({'cmd': cEpisode._cmd_fetch_payload % u"currval('clin.episode_pk_seq')"})
    1461          rows, idx = gmPG2.run_rw_queries(queries = queries, return_data=True, get_col_idx=True)
    1462
    1463          episode = cEpisode(row={'data': rows[0], 'idx': idx, 'pk_field': 'pk_episode'})
    1464          return episode
    1465  #-----------------------------------------------------------
    1466 -def delete_episode(episode=None):
    1467          if isinstance(episode, cEpisode):
    1468                  pk = episode['pk_episode']
    1469          else:
    1470                  pk = int(episode)
    1471
    1472          cmd = u'DELETE FROM clin.episode WHERE pk = %(pk)s'
    1473
    1474          try:
    1475                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': pk}}])
    1476          except gmPG2.dbapi.IntegrityError:
    1477                  # should be parsing pgcode/and or error message
    1478                  _log.exception('cannot delete episode, it is in use')
    1479                  return False
    1480
    1481          return True
    1482  #-----------------------------------------------------------
    1483 -def episode2problem(episode=None, allow_closed=False):
    1484          return cProblem (
    1485                  aPK_obj = {
    1486                          'pk_patient': episode['pk_patient'],
    1487                          'pk_episode': episode['pk_episode'],
    1488                          'pk_health_issue': episode['pk_health_issue']
    1489                  },
    1490                  try_potential_problems = allow_closed
    1491          )
    1492  #============================================================
    1493  # encounter API
    1494  #============================================================
    1495 -class cEncounter(gmBusinessDBObject.cBusinessDBObject):
    1496          """Represents one encounter."""
    1497
    1498          _cmd_fetch_payload = u"select * from clin.v_pat_encounters where pk_encounter = %s"
    1499          _cmds_store_payload = [
    1500                  u"""UPDATE clin.encounter SET
    1501                                  started = %(started)s,
    1502                                  last_affirmed = %(last_affirmed)s,
    1503                                  fk_location = %(pk_org_unit)s,
    1504                                  fk_type = %(pk_type)s,
    1505                                  reason_for_encounter = gm.nullify_empty_string(%(reason_for_encounter)s),
    1506                                  assessment_of_encounter = gm.nullify_empty_string(%(assessment_of_encounter)s)
    1507                          WHERE
    1508                                  pk = %(pk_encounter)s AND
    1509                                  xmin = %(xmin_encounter)s
    1510                          """,
    1511                  # need to return all fields so we can survive in-place upgrades
    1512                  u"""select * from clin.v_pat_encounters where pk_encounter = %(pk_encounter)s"""
    1513          ]
    1514          _updatable_fields = [
    1515                  'started',
    1516                  'last_affirmed',
    1517                  'pk_org_unit',
    1518                  'pk_type',
    1519                  'reason_for_encounter',
    1520                  'assessment_of_encounter'
    1521          ]
    1522          #--------------------------------------------------------
    1523 -        def set_active(self):
    1524                  """Set the encounter as the active one.
    1525
    1526                  "Setting active" means making sure the encounter
    1527                  row has the youngest "last_affirmed" timestamp of
    1528                  all encounter rows for this patient.
    1529                  """
    1530                  self['last_affirmed'] = gmDateTime.pydt_now_here()
    1531                  self.save()
    1532          #--------------------------------------------------------
    1533 -        def transfer_clinical_data(self, source_episode=None, target_episode=None):
    1534                  """
    1535                  Moves every element currently linked to the current encounter
    1536                  and the source_episode onto target_episode.
    1537
    1538                  @param source_episode The episode the elements are currently linked to.
    1539                  @type target_episode A cEpisode intance.
    1540                  @param target_episode The episode the elements will be relinked to.
    1541                  @type target_episode A cEpisode intance.
    1542                  """
    1543                  if source_episode['pk_episode'] == target_episode['pk_episode']:
    1544                          return True
    1545
    1546                  queries = []
    1547                  cmd = u"""
    1548                          UPDATE clin.clin_root_item
    1549                          SET fk_episode = %(trg)s
    1550                          WHERE
    1551                                  fk_encounter = %(enc)s AND
    1552                                  fk_episode = %(src)s
    1553                          """
    1554                  rows, idx = gmPG2.run_rw_queries(queries = [{
    1555                          'cmd': cmd,
    1556                          'args': {
    1557                                  'trg': target_episode['pk_episode'],
    1558                                  'enc': self.pk_obj,
    1559                                  'src': source_episode['pk_episode']
    1560                          }
    1561                  }])
    1562                  self.refetch_payload()
    1563                  return True
    1564          #--------------------------------------------------------
    1565 -        def same_payload(self, another_object=None):
    1566
    1567                  relevant_fields = [
    1568                          'pk_org_unit',
    1569                          'pk_type',
    1570                          'pk_patient',
    1571                          'reason_for_encounter',
    1572                          'assessment_of_encounter'
    1573                  ]
    1574                  for field in relevant_fields:
    1575                          if self._payload[self._idx[field]] != another_object[field]:
    1576                                  _log.debug('mismatch on [%s]: "%s" vs. "%s"', field, self._payload[self._idx[field]], another_object[field])
    1577                                  return False
    1578
    1579                  relevant_fields = [
    1580                          'started',
    1581                          'last_affirmed',
    1582                  ]
    1583                  for field in relevant_fields:
    1584                          if self._payload[self._idx[field]] is None:
    1585                                  if another_object[field] is None:
    1586                                          continue
    1587                                  _log.debug('mismatch on [%s]: here="%s", other="%s"', field, self._payload[self._idx[field]], another_object[field])
    1588                                  return False
    1589
    1590                          if another_object[field] is None:
    1591                                  return False
    1592
    1593                          # compares at seconds granularity
    1594                          if self._payload[self._idx[field]].strftime('%Y-%m-%d %H:%M:%S') != another_object[field].strftime('%Y-%m-%d %H:%M:%S'):
    1595                                  _log.debug('mismatch on [%s]: here="%s", other="%s"', field, self._payload[self._idx[field]], another_object[field])
    1596                                  return False
    1597
    1598                  # compare codes
    1599                  # 1) RFE
    1600                  if another_object['pk_generic_codes_rfe'] is None:
    1601                          if self._payload[self._idx['pk_generic_codes_rfe']] is not None:
    1602                                  return False
    1603                  if another_object['pk_generic_codes_rfe'] is not None:
    1604                          if self._payload[self._idx['pk_generic_codes_rfe']] is None:
    1605                                  return False
    1606                  if (
    1607                          (another_object['pk_generic_codes_rfe'] is None)
    1608                                  and
    1609                          (self._payload[self._idx['pk_generic_codes_rfe']] is None)
    1610                  ) is False:
    1611                          if set(another_object['pk_generic_codes_rfe']) != set(self._payload[self._idx['pk_generic_codes_rfe']]):
    1612                                  return False
    1613                  # 2) AOE
    1614                  if another_object['pk_generic_codes_aoe'] is None:
    1615                          if self._payload[self._idx['pk_generic_codes_aoe']] is not None:
    1616                                  return False
    1617                  if another_object['pk_generic_codes_aoe'] is not None:
    1618                          if self._payload[self._idx['pk_generic_codes_aoe']] is None:
    1619                                  return False
    1620                  if (
    1621                          (another_object['pk_generic_codes_aoe'] is None)
    1622                                  and
    1623                          (self._payload[self._idx['pk_generic_codes_aoe']] is None)
    1624                  ) is False:
    1625                          if set(another_object['pk_generic_codes_aoe']) != set(self._payload[self._idx['pk_generic_codes_aoe']]):
    1626                                  return False
    1627
    1628                  return True
    1629          #--------------------------------------------------------
    1630 -        def has_clinical_data(self):
    1631                  cmd = u"""
    1632  select exists (
    1633          select 1 from clin.v_pat_items where pk_patient = %(pat)s and pk_encounter = %(enc)s
    1634                  union all
    1635          select 1 from blobs.v_doc_med where pk_patient = %(pat)s and pk_encounter = %(enc)s
    1636  )"""
    1637                  args = {
    1638                          'pat': self._payload[self._idx['pk_patient']],
    1639                          'enc': self.pk_obj
    1640                  }
    1641                  rows, idx = gmPG2.run_ro_queries (
    1642                          queries = [{
    1643                                  'cmd': cmd,
    1644                                  'args': args
    1645                          }]
    1646                  )
    1647                  return rows[0][0]
    1648          #--------------------------------------------------------
    1649 -        def has_narrative(self):
    1650                  cmd = u"""
    1651  select exists (
    1652          select 1 from clin.v_pat_items where pk_patient=%(pat)s and pk_encounter=%(enc)s
    1653  )"""
    1654                  args = {
    1655                          'pat': self._payload[self._idx['pk_patient']],
    1656                          'enc': self.pk_obj
    1657                  }
    1658                  rows, idx = gmPG2.run_ro_queries (
    1659                          queries = [{
    1660                                  'cmd': cmd,
    1661                                  'args': args
    1662                          }]
    1663                  )
    1664                  return rows[0][0]
    1665          #--------------------------------------------------------
    1666 -        def has_soap_narrative(self, soap_cats=None):
    1667                  """soap_cats: <space> = admin category"""
    1668
    1669                  if soap_cats is None:
    1670                          soap_cats = u'soap '
    1671                  else:
    1672                          soap_cats = soap_cats.lower()
    1673
    1674                  cats = []
    1675                  for cat in soap_cats:
    1676                          if cat in u'soapu':
    1677                                  cats.append(cat)
    1678                                  continue
    1679                          if cat == u' ':
    1680                                  cats.append(None)
    1681
    1682                  cmd = u"""
    1683                          SELECT EXISTS (
    1684                                  SELECT 1 FROM clin.clin_narrative
    1685                                  WHERE
    1686                                          fk_encounter = %(enc)s
    1687                                                  AND
    1688                                          soap_cat IN %(cats)s
    1689                                  LIMIT 1
    1690                          )
    1691                  """
    1692                  args = {'enc': self._payload[self._idx['pk_encounter']], 'cats': tuple(cats)}
    1693                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd,'args': args}])
    1694                  return rows[0][0]
    1695          #--------------------------------------------------------
    1696 -        def has_documents(self):
    1697                  cmd = u"""
    1698  select exists (
    1699          select 1 from blobs.v_doc_med where pk_patient = %(pat)s and pk_encounter = %(enc)s
    1700  )"""
    1701                  args = {
    1702                          'pat': self._payload[self._idx['pk_patient']],
    1703                          'enc': self.pk_obj
    1704                  }
    1705                  rows, idx = gmPG2.run_ro_queries (
    1706                          queries = [{
    1707                                  'cmd': cmd,
    1708                                  'args': args
    1709                          }]
    1710                  )
    1711                  return rows[0][0]
    1712          #--------------------------------------------------------
    1713 -        def get_latest_soap(self, soap_cat=None, episode=None):
    1714
    1715                  if soap_cat is not None:
    1716                          soap_cat = soap_cat.lower()
    1717
    1718                  if episode is None:
    1719                          epi_part = u'fk_episode is null'
    1720                  else:
    1721                          epi_part = u'fk_episode = %(epi)s'
    1722
    1723                  cmd = u"""
    1724  select narrative
    1725  from clin.clin_narrative
    1726  where
    1727          fk_encounter = %%(enc)s
    1728                  and
    1729          soap_cat = %%(cat)s
    1730                  and
    1731          %s
    1732  order by clin_when desc
    1733  limit 1
    1734                  """ % epi_part
    1735
    1736                  args = {'enc': self.pk_obj, 'cat': soap_cat, 'epi': episode}
    1737
    1738                  rows, idx = gmPG2.run_ro_queries (
    1739                          queries = [{
    1740                                  'cmd': cmd,
    1741                                  'args': args
    1742                          }]
    1743                  )
    1744                  if len(rows) == 0:
    1745                          return None
    1746
    1747                  return rows[0][0]
    1748          #--------------------------------------------------------
    1749 -        def get_episodes(self, exclude=None):
    1750                  cmd = u"""
    1751                          SELECT * FROM clin.v_pat_episodes
    1752                          WHERE pk_episode IN (
    1753                                          SELECT DISTINCT fk_episode
    1754                                          FROM clin.clin_root_item
    1755                                          WHERE fk_encounter = %%(enc)s
    1756
    1757                                                  UNION
    1758
    1759                                          SELECT DISTINCT fk_episode
    1760                                          FROM blobs.doc_med
    1761                                          WHERE fk_encounter = %%(enc)s
    1762                          ) %s"""
    1763                  args = {'enc': self.pk_obj}
    1764                  if exclude is not None:
    1765                          cmd = cmd % u'AND pk_episode NOT IN %(excluded)s'
    1766                          args['excluded'] = tuple(exclude)
    1767                  else:
    1768                          cmd = cmd % u''
    1769
    1770                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1771
    1772                  return [ cEpisode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_episode'})  for r in rows ]
    1773
    1774          episodes = property(get_episodes, lambda x:x)
    1775          #--------------------------------------------------------
    1776 -        def add_code(self, pk_code=None, field=None):
    1777                  """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
    1778                  if field == u'rfe':
    1779                          cmd = u"INSERT INTO clin.lnk_code2rfe (fk_item, fk_generic_code) values (%(item)s, %(code)s)"
    1780                  elif field == u'aoe':
    1781                          cmd = u"INSERT INTO clin.lnk_code2aoe (fk_item, fk_generic_code) values (%(item)s, %(code)s)"
    1782                  else:
    1783                          raise ValueError('<field> must be one of "rfe" or "aoe", not "%s"', field)
    1784                  args = {
    1785                          'item': self._payload[self._idx['pk_encounter']],
    1786                          'code': pk_code
    1787                  }
    1788                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    1789                  return True
    1790          #--------------------------------------------------------
    1791 -        def remove_code(self, pk_code=None, field=None):
    1792                  """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
    1793                  if field == u'rfe':
    1794                          cmd = u"DELETE FROM clin.lnk_code2rfe WHERE fk_item = %(item)s AND fk_generic_code = %(code)s"
    1795                  elif field == u'aoe':
    1796                          cmd = u"DELETE FROM clin.lnk_code2aoe WHERE fk_item = %(item)s AND fk_generic_code = %(code)s"
    1797                  else:
    1798                          raise ValueError('<field> must be one of "rfe" or "aoe", not "%s"', field)
    1799                  args = {
    1800                          'item': self._payload[self._idx['pk_encounter']],
    1801                          'code': pk_code
    1802                  }
    1803                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    1804                  return True
    1805          #--------------------------------------------------------
    1806 -        def format_soap(self, episodes=None, left_margin=0, soap_cats='soapu', emr=None, issues=None):
    1807
    1808                  lines = []
    1809                  for soap_cat in soap_cats:
    1810                          soap_cat_narratives = emr.get_clin_narrative (
    1811                                  episodes = episodes,
    1812                                  issues = issues,
    1813                                  encounters = [self._payload[self._idx['pk_encounter']]],
    1814                                  soap_cats = [soap_cat]
    1815                          )
    1816                          if soap_cat_narratives is None:
    1817                                  continue
    1818                          if len(soap_cat_narratives) == 0:
    1819                                  continue
    1820
    1821                          lines.append(u'%s%s %s %s' % (
    1822                                  gmTools.u_box_top_left_arc,
    1823                                  gmTools.u_box_horiz_single,
    1824                                  gmClinNarrative.soap_cat2l10n_str[soap_cat],
    1825                                  gmTools.u_box_horiz_single * 5
    1826                          ))
    1827                          for soap_entry in soap_cat_narratives:
    1828                                  txt = gmTools.wrap (
    1829                                          text = soap_entry['narrative'],
    1830                                          width = 75,
    1831                                          initial_indent = u'',
    1832                                          subsequent_indent = (u' ' * left_margin)
    1833                                  )
    1834                                  lines.append(txt)
    1835                                  when = gmDateTime.pydt_strftime (
    1836                                          soap_entry['date'],
    1837                                          format = '%Y-%m-%d %H:%M',
    1838                                          accuracy = gmDateTime.acc_minutes
    1839                                  )
    1840                                  txt = u'%s%s %.8s, %s %s' % (
    1841                                          u' ' * 40,
    1842                                          gmTools.u_box_horiz_light_heavy,
    1843                                          soap_entry['modified_by'],
    1844                                          when,
    1845                                          gmTools.u_box_horiz_heavy_light
    1846                                  )
    1847                                  lines.append(txt)
    1848                                  lines.append('')
    1849
    1850                  return lines
    1851          #--------------------------------------------------------
    1852 -        def format_latex(self, date_format=None, soap_cats=None, soap_order=None):
    1853
    1854                  nothing2format = (
    1855                          (self._payload[self._idx['reason_for_encounter']] is None)
    1856                                  and
    1857                          (self._payload[self._idx['assessment_of_encounter']] is None)
    1858                                  and
    1859                          (self.has_soap_narrative(soap_cats = u'soapu') is False)
    1860                  )
    1861                  if nothing2format:
    1862                          return u''
    1863
    1864                  if date_format is None:
    1865                          date_format = '%A, %b %d %Y'
    1866
    1867                  tex = u'\\multicolumn{2}{l}{%s: %s ({\\footnotesize %s - %s})} \\tabularnewline \n' % (
    1868                          gmTools.tex_escape_string(self._payload[self._idx['l10n_type']]),
    1869                          self._payload[self._idx['started']].strftime(date_format).decode(gmI18N.get_encoding()),
    1870                          self._payload[self._idx['started']].strftime('%H:%M'),
    1871                          self._payload[self._idx['last_affirmed']].strftime('%H:%M')
    1872                  )
    1873                  tex += u'\\hline \\tabularnewline \n'
    1874
    1875                  for epi in self.get_episodes():
    1876                          soaps = epi.get_narrative(soap_cats = soap_cats, encounters = [self.pk_obj], order_by = soap_order)
    1877                          if len(soaps) == 0:
    1878                                  continue
    1879                          tex += u'\\multicolumn{2}{l}{\\emph{%s: %s%s}} \\tabularnewline \n' % (
    1880                                  gmTools.tex_escape_string(_('Problem')),
    1881                                  gmTools.tex_escape_string(epi['description']),
    1882                                  gmTools.coalesce (
    1883                                          initial = diagnostic_certainty_classification2str(epi['diagnostic_certainty_classification']),
    1884                                          instead = u'',
    1885                                          template_initial = u' {\\footnotesize [%s]}',
    1886                                          none_equivalents = [None, u'']
    1887                                  )
    1888                          )
    1889                          if epi['pk_health_issue'] is not None:
    1890                                  tex += u'\\multicolumn{2}{l}{\\emph{%s: %s%s}} \\tabularnewline \n' % (
    1891                                          gmTools.tex_escape_string(_('Health issue')),
    1892                                          gmTools.tex_escape_string(epi['health_issue']),
    1893                                          gmTools.coalesce (
    1894                                                  initial = diagnostic_certainty_classification2str(epi['diagnostic_certainty_classification_issue']),
    1895                                                  instead = u'',
    1896                                                  template_initial = u' {\\footnotesize [%s]}',
    1897                                                  none_equivalents = [None, u'']
    1898                                          )
    1899                                  )
    1900                          for soap in soaps:
    1901                                  tex += u'{\\small %s} & %s \\tabularnewline \n' % (
    1902                                          gmClinNarrative.soap_cat2l10n[soap['soap_cat']],
    1903                                          gmTools.tex_escape_string(soap['narrative'].strip(u'\n'))
    1904                                  )
    1905                          tex += u' & \\tabularnewline \n'
    1906
    1907                  if self._payload[self._idx['reason_for_encounter']] is not None:
    1908                          tex += u'%s & %s \\tabularnewline \n' % (
    1909                                  gmTools.tex_escape_string(_('RFE')),
    1910                                  gmTools.tex_escape_string(self._payload[self._idx['reason_for_encounter']])
    1911                          )
    1912                  if self._payload[self._idx['assessment_of_encounter']] is not None:
    1913                          tex += u'%s & %s \\tabularnewline \n' % (
    1914                                  gmTools.tex_escape_string(_('AOE')),
    1915                                  gmTools.tex_escape_string(self._payload[self._idx['assessment_of_encounter']])
    1916                          )
    1917
    1918                  tex += u'\\hline \\tabularnewline \n'
    1919                  tex += u' & \\tabularnewline \n'
    1920
    1921                  return tex
    1922          #--------------------------------------------------------
    1923 -        def __format_header_fancy(self, left_margin=0):
    1924                  lines = []
    1925
    1926                  lines.append(u'%s%s: %s - %s (@%s)%s [#%s]' % (
    1927                          u' ' * left_margin,
    1928                          self._payload[self._idx['l10n_type']],
    1929                          self._payload[self._idx['started_original_tz']].strftime('%Y-%m-%d %H:%M'),
    1930                          self._payload[self._idx['last_affirmed_original_tz']].strftime('%H:%M'),
    1931                          self._payload[self._idx['source_time_zone']],
    1932                          gmTools.coalesce (
    1933                                  self._payload[self._idx['assessment_of_encounter']],
    1934                                  u'',
    1935                                  u' %s%%s%s' % (gmTools.u_left_double_angle_quote, gmTools.u_right_double_angle_quote)
    1936                          ),
    1937                          self._payload[self._idx['pk_encounter']]
    1938                  ))
    1939
    1940                  lines.append(_('  your time: %s - %s  (@%s = %s%s)\n') % (
    1941                          self._payload[self._idx['started']].strftime('%Y-%m-%d %H:%M'),
    1942                          self._payload[self._idx['last_affirmed']].strftime('%H:%M'),
    1943                          gmDateTime.current_local_iso_numeric_timezone_string,
    1944                          gmTools.bool2subst (
    1945                                  gmDateTime.dst_currently_in_effect,
    1946                                  gmDateTime.py_dst_timezone_name,
    1947                                  gmDateTime.py_timezone_name
    1948                          ),
    1949                          gmTools.bool2subst(gmDateTime.dst_currently_in_effect, u' - ' + _('daylight savings time in effect'), u'')
    1950                  ))
    1951
    1952                  if self._payload[self._idx['praxis_branch']] is not None:
    1953                          lines.append(_('Location: %s (%s)') % (self._payload[self._idx['praxis_branch']], self._payload[self._idx['praxis']]))
    1954
    1955                  if self._payload[self._idx['reason_for_encounter']] is not None:
    1956                          lines.append(u'%s: %s' % (_('RFE'), self._payload[self._idx['reason_for_encounter']]))
    1957                          codes = self.generic_codes_rfe
    1958                          for c in codes:
    1959                                  lines.append(u' %s: %s (%s - %s)' % (
    1960                                          c['code'],
    1961                                          c['term'],
    1962                                          c['name_short'],
    1963                                          c['version']
    1964                                  ))
    1965                          if len(codes) > 0:
    1966                                  lines.append(u'')
    1967
    1968                  if self._payload[self._idx['assessment_of_encounter']] is not None:
    1969                          lines.append(u'%s: %s' % (_('AOE'), self._payload[self._idx['assessment_of_encounter']]))
    1970                          codes = self.generic_codes_aoe
    1971                          for c in codes:
    1972                                  lines.append(u' %s: %s (%s - %s)' % (
    1973                                          c['code'],
    1974                                          c['term'],
    1975                                          c['name_short'],
    1976                                          c['version']
    1977                                  ))
    1978                          if len(codes) > 0:
    1979                                  lines.append(u'')
    1980                          del codes
    1981                  return lines
    1982
    1983          #--------------------------------------------------------
    1984 -        def format_header(self, fancy_header=True, left_margin=0, with_rfe_aoe=False):
    1985                  lines = []
    1986
    1987                  if fancy_header:
    1988                          return self.__format_header_fancy(left_margin = left_margin)
    1989
    1990                  now = gmDateTime.pydt_now_here()
    1991                  if now.strftime('%Y-%m-%d') == self._payload[self._idx['started_original_tz']].strftime('%Y-%m-%d'):
    1992                          start = u'%s %s' % (
    1993                                  _('today'),
    1994                                  self._payload[self._idx['started_original_tz']].strftime('%H:%M')
    1995                          )
    1996                  else:
    1997                          start = self._payload[self._idx['started_original_tz']].strftime('%Y-%m-%d %H:%M')
    1998                  lines.append(u'%s%s: %s - %s%s%s' % (
    1999                          u' ' * left_margin,
    2000                          self._payload[self._idx['l10n_type']],
    2001                          start,
    2002                          self._payload[self._idx['last_affirmed_original_tz']].strftime('%H:%M'),
    2003                          gmTools.coalesce(self._payload[self._idx['assessment_of_encounter']], u'', u' \u00BB%s\u00AB'),
    2004                          gmTools.coalesce(self._payload[self._idx['praxis_branch']], u'', u' @%s')
    2005                  ))
    2006                  if with_rfe_aoe:
    2007                          if self._payload[self._idx['reason_for_encounter']] is not None:
    2008                                  lines.append(u'%s: %s' % (_('RFE'), self._payload[self._idx['reason_for_encounter']]))
    2009                          codes = self.generic_codes_rfe
    2010                          for c in codes:
    2011                                  lines.append(u' %s: %s (%s - %s)' % (
    2012                                          c['code'],
    2013                                          c['term'],
    2014                                          c['name_short'],
    2015                                          c['version']
    2016                                  ))
    2017                          if len(codes) > 0:
    2018                                  lines.append(u'')
    2019                          if self._payload[self._idx['assessment_of_encounter']] is not None:
    2020                                  lines.append(u'%s: %s' % (_('AOE'), self._payload[self._idx['assessment_of_encounter']]))
    2021                          codes = self.generic_codes_aoe
    2022                          if len(codes) > 0:
    2023                                  lines.append(u'')
    2024                          for c in codes:
    2025                                  lines.append(u' %s: %s (%s - %s)' % (
    2026                                          c['code'],
    2027                                          c['term'],
    2028                                          c['name_short'],
    2029                                          c['version']
    2030                                  ))
    2031                          if len(codes) > 0:
    2032                                  lines.append(u'')
    2033                          del codes
    2034
    2035                  return lines
    2036          #--------------------------------------------------------
    2037 -        def format_by_episode(self, episodes=None, issues=None, left_margin=0, patient=None, with_soap=False, with_tests=True, with_docs=True, with_vaccinations=True, with_family_history=True):
    2038
    2039                  lines = []
    2040                  emr = patient.emr
    2041                  if episodes is None:
    2042                          episodes = [ e['pk_episode'] for e in self.episodes ]
    2043
    2044                  for pk in episodes:
    2045                          epi = cEpisode(aPK_obj = pk)
    2046                          lines.append(_('\nEpisode %s%s%s%s:') % (
    2047                                  gmTools.u_left_double_angle_quote,
    2048                                  epi['description'],
    2049                                  gmTools.u_right_double_angle_quote,
    2050                                  gmTools.coalesce(epi['health_issue'], u'', u' (%s)')
    2051                          ))
    2052
    2053                          # soap
    2054                          if with_soap:
    2055                                  if patient.ID != self._payload[self._idx['pk_patient']]:
    2056                                          msg = '<patient>.ID = %s but encounter %s belongs to patient %s' % (
    2057                                                  patient.ID,
    2058                                                  self._payload[self._idx['pk_encounter']],
    2059                                                  self._payload[self._idx['pk_patient']]
    2060                                          )
    2061                                          raise ValueError(msg)
    2062
    2063                                  lines.extend(self.format_soap (
    2064                                          episodes = [pk],
    2065                                          left_margin = left_margin,
    2066                                          soap_cats = 'soapu',
    2067                                          emr = emr,
    2068                                          issues = issues
    2069                                  ))
    2070
    2071                          # test results
    2072                          if with_tests:
    2073                                  tests = emr.get_test_results_by_date (
    2074                                          episodes = [pk],
    2075                                          encounter = self._payload[self._idx['pk_encounter']]
    2076                                  )
    2077                                  if len(tests) > 0:
    2078                                          lines.append('')
    2079                                          lines.append(_('Measurements and Results:'))
    2080
    2081                                  for t in tests:
    2082                                          lines.append(t.format())
    2083
    2084                                  del tests
    2085
    2086                          # vaccinations
    2087                          if with_vaccinations:
    2088                                  vaccs = emr.get_vaccinations (
    2089                                          episodes = [pk],
    2090                                          encounters = [ self._payload[self._idx['pk_encounter']] ],
    2091                                          order_by = u'date_given DESC, vaccine'
    2092                                  )
    2093                                  if len(vaccs) > 0:
    2094                                          lines.append(u'')
    2095                                          lines.append(_('Vaccinations:'))
    2096                                  for vacc in vaccs:
    2097                                          lines.extend(vacc.format (
    2098                                                  with_indications = True,
    2099                                                  with_comment = True,
    2100                                                  with_reaction = True,
    2101                                                  date_format = '%Y-%m-%d'
    2102                                          ))
    2103                                  del vaccs
    2104
    2105                          # family history
    2106                          if with_family_history:
    2107                                  fhx = emr.get_family_history(episodes = [pk])
    2108                                  if len(fhx) > 0:
    2109                                          lines.append(u'')
    2110                                          lines.append(_('Family History: %s') % len(fhx))
    2111                                  for f in fhx:
    2112                                          lines.append(f.format (
    2113                                                  left_margin = (left_margin + 1),
    2114                                                  include_episode = False,
    2115                                                  include_comment = True
    2116                                          ))
    2117                                  del fhx
    2118
    2119                          # documents
    2120                          if with_docs:
    2121                                  doc_folder = patient.get_document_folder()
    2122                                  docs = doc_folder.get_documents (
    2123                                          episodes = [pk],
    2124                                          encounter = self._payload[self._idx['pk_encounter']]
    2125                                  )
    2126                                  if len(docs) > 0:
    2127                                          lines.append(u'')
    2128                                          lines.append(_('Documents:'))
    2129                                  for d in docs:
    2130                                          lines.append(u' %s %s:%s%s' % (
    2131                                                  d['clin_when'].strftime('%Y-%m-%d'),
    2132                                                  d['l10n_type'],
    2133                                                  gmTools.coalesce(d['comment'], u'', u' "%s"'),
    2134                                                  gmTools.coalesce(d['ext_ref'], u'', u' (%s)')
    2135                                          ))
    2136
    2137                                  del docs
    2138
    2139                  return lines
    2140          #--------------------------------------------------------
    2141 -        def format(self, episodes=None, with_soap=False, left_margin=0, patient=None, issues=None, with_docs=True, with_tests=True, fancy_header=True, with_vaccinations=True, with_co_encountlet_hints=False, with_rfe_aoe=False, with_family_history=True, by_episode=False):
    2142                  """Format an encounter.
    2143
    2144                  with_co_encountlet_hints:
    2145                          - whether to include which *other* episodes were discussed during this encounter
    2146                          - (only makes sense if episodes != None)
    2147                  """
    2148                  lines = self.format_header (
    2149                          fancy_header = fancy_header,
    2150                          left_margin = left_margin,
    2151                          with_rfe_aoe = with_rfe_aoe
    2152                  )
    2153
    2154                  if by_episode:
    2155                          lines.extend(self.format_by_episode (
    2156                                  episodes = episodes,
    2157                                  issues = issues,
    2158                                  left_margin = left_margin,
    2159                                  patient = patient,
    2160                                  with_soap = with_soap,
    2161                                  with_tests = with_tests,
    2162                                  with_docs = with_docs,
    2163                                  with_vaccinations = with_vaccinations,
    2164                                  with_family_history = with_family_history
    2165                          ))
    2166
    2167                  else:
    2168                          if with_soap:
    2169                                  lines.append(u'')
    2170
    2171                                  if patient.ID != self._payload[self._idx['pk_patient']]:
    2172                                          msg = '<patient>.ID = %s but encounter %s belongs to patient %s' % (
    2173                                                  patient.ID,
    2174                                                  self._payload[self._idx['pk_encounter']],
    2175                                                  self._payload[self._idx['pk_patient']]
    2176                                          )
    2177                                          raise ValueError(msg)
    2178
    2179                                  emr = patient.get_emr()
    2180
    2181                                  lines.extend(self.format_soap (
    2182                                          episodes = episodes,
    2183                                          left_margin = left_margin,
    2184                                          soap_cats = 'soapu',
    2185                                          emr = emr,
    2186                                          issues = issues
    2187                                  ))
    2188
    2189          #               # family history
    2190          #               if with_family_history:
    2191          #                       if episodes is not None:
    2192          #                               fhx = emr.get_family_history(episodes = episodes)
    2193          #                               if len(fhx) > 0:
    2194          #                                       lines.append(u'')
    2195          #                                       lines.append(_('Family History: %s') % len(fhx))
    2196          #                               for f in fhx:
    2197          #                                       lines.append(f.format (
    2198          #                                               left_margin = (left_margin + 1),
    2199          #                                               include_episode = False,
    2200          #                                               include_comment = True
    2201          #                                       ))
    2202          #                               del fhx
    2203
    2204                          # test results
    2205                          if with_tests:
    2206                                  emr = patient.get_emr()
    2207                                  tests = emr.get_test_results_by_date (
    2208                                          episodes = episodes,
    2209                                          encounter = self._payload[self._idx['pk_encounter']]
    2210                                  )
    2211                                  if len(tests) > 0:
    2212                                          lines.append('')
    2213                                          lines.append(_('Measurements and Results:'))
    2214
    2215                                  for t in tests:
    2216                                          lines.append(t.format())
    2217
    2218                                  del tests
    2219
    2220                          # vaccinations
    2221                          if with_vaccinations:
    2222                                  emr = patient.get_emr()
    2223                                  vaccs = emr.get_vaccinations (
    2224                                          episodes = episodes,
    2225                                          encounters = [ self._payload[self._idx['pk_encounter']] ],
    2226                                          order_by = u'date_given DESC, vaccine'
    2227                                  )
    2228
    2229                                  if len(vaccs) > 0:
    2230                                          lines.append(u'')
    2231                                          lines.append(_('Vaccinations:'))
    2232
    2233                                  for vacc in vaccs:
    2234                                          lines.extend(vacc.format (
    2235                                                  with_indications = True,
    2236                                                  with_comment = True,
    2237                                                  with_reaction = True,
    2238                                                  date_format = '%Y-%m-%d'
    2239                                          ))
    2240                                  del vaccs
    2241
    2242                          # documents
    2243                          if with_docs:
    2244                                  doc_folder = patient.get_document_folder()
    2245                                  docs = doc_folder.get_documents (
    2246                                          episodes = episodes,
    2247                                          encounter = self._payload[self._idx['pk_encounter']]
    2248                                  )
    2249
    2250                                  if len(docs) > 0:
    2251                                          lines.append(u'')
    2252                                          lines.append(_('Documents:'))
    2253
    2254                                  for d in docs:
    2255                                          lines.append(u' %s %s:%s%s' % (
    2256                                                  d['clin_when'].strftime('%Y-%m-%d'),
    2257                                                  d['l10n_type'],
    2258                                                  gmTools.coalesce(d['comment'], u'', u' "%s"'),
    2259                                                  gmTools.coalesce(d['ext_ref'], u'', u' (%s)')
    2260                                          ))
    2261
    2262                                  del docs
    2263
    2264                          # co-encountlets
    2265                          if with_co_encountlet_hints:
    2266                                  if episodes is not None:
    2267                                          other_epis = self.get_episodes(exclude = episodes)
    2268                                          if len(other_epis) > 0:
    2269                                                  lines.append(u'')
    2270                                                  lines.append(_('%s other episodes touched upon during this encounter:') % len(other_epis))
    2271                                          for epi in other_epis:
    2272                                                  lines.append(u' %s%s%s%s' % (
    2273                                                          gmTools.u_left_double_angle_quote,
    2274                                                          epi['description'],
    2275                                                          gmTools.u_right_double_angle_quote,
    2276                                                          gmTools.coalesce(epi['health_issue'], u'', u' (%s)')
    2277                                                  ))
    2278
    2279                  eol_w_margin = u'\n%s' % (u' ' * left_margin)
    2280                  return u'%s\n' % eol_w_margin.join(lines)
    2281          #--------------------------------------------------------
    2282          # properties
    2283          #--------------------------------------------------------
    2284 -        def _get_generic_codes_rfe(self):
    2285                  if len(self._payload[self._idx['pk_generic_codes_rfe']]) == 0:
    2286                          return []
    2287
    2288                  cmd = gmCoding._SQL_get_generic_linked_codes % u'pk_generic_code IN %(pks)s'
    2289                  args = {'pks': tuple(self._payload[self._idx['pk_generic_codes_rfe']])}
    2290                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2291                  return [ gmCoding.cGenericLinkedCode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_lnk_code2item'}) for r in rows ]
    2292
    2293 -        def _set_generic_codes_rfe(self, pk_codes):
    2294                  queries = []
    2295                  # remove all codes
    2296                  if len(self._payload[self._idx['pk_generic_codes_rfe']]) > 0:
    2297                          queries.append ({
    2298                                  'cmd': u'DELETE FROM clin.lnk_code2rfe WHERE fk_item = %(enc)s AND fk_generic_code IN %(codes)s',
    2299                                  'args': {
    2300                                          'enc': self._payload[self._idx['pk_encounter']],
    2301                                          'codes': tuple(self._payload[self._idx['pk_generic_codes_rfe']])
    2302                                  }
    2303                          })
    2304                  # add new codes
    2305                  for pk_code in pk_codes:
    2306                          queries.append ({
    2307                                  'cmd': u'INSERT INTO clin.lnk_code2rfe (fk_item, fk_generic_code) VALUES (%(enc)s, %(pk_code)s)',
    2308                                  'args': {
    2309                                          'enc': self._payload[self._idx['pk_encounter']],
    2310                                          'pk_code': pk_code
    2311                                  }
    2312                          })
    2313                  if len(queries) == 0:
    2314                          return
    2315                  # run it all in one transaction
    2316                  rows, idx = gmPG2.run_rw_queries(queries = queries)
    2317                  self.refetch_payload()
    2318                  return
    2319
    2320          generic_codes_rfe = property(_get_generic_codes_rfe, _set_generic_codes_rfe)
    2321          #--------------------------------------------------------
    2322 -        def _get_generic_codes_aoe(self):
    2323                  if len(self._payload[self._idx['pk_generic_codes_aoe']]) == 0:
    2324                          return []
    2325
    2326                  cmd = gmCoding._SQL_get_generic_linked_codes % u'pk_generic_code IN %(pks)s'
    2327                  args = {'pks': tuple(self._payload[self._idx['pk_generic_codes_aoe']])}
    2328                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2329                  return [ gmCoding.cGenericLinkedCode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_lnk_code2item'}) for r in rows ]
    2330
    2331 -        def _set_generic_codes_aoe(self, pk_codes):
    2332                  queries = []
    2333                  # remove all codes
    2334                  if len(self._payload[self._idx['pk_generic_codes_aoe']]) > 0:
    2335                          queries.append ({
    2336                                  'cmd': u'DELETE FROM clin.lnk_code2aoe WHERE fk_item = %(enc)s AND fk_generic_code IN %(codes)s',
    2337                                  'args': {
    2338                                          'enc': self._payload[self._idx['pk_encounter']],
    2339                                          'codes': tuple(self._payload[self._idx['pk_generic_codes_aoe']])
    2340                                  }
    2341                          })
    2342                  # add new codes
    2343                  for pk_code in pk_codes:
    2344                          queries.append ({
    2345                                  'cmd': u'INSERT INTO clin.lnk_code2aoe (fk_item, fk_generic_code) VALUES (%(enc)s, %(pk_code)s)',
    2346                                  'args': {
    2347                                          'enc': self._payload[self._idx['pk_encounter']],
    2348                                          'pk_code': pk_code
    2349                                  }
    2350                          })
    2351                  if len(queries) == 0:
    2352                          return
    2353                  # run it all in one transaction
    2354                  rows, idx = gmPG2.run_rw_queries(queries = queries)
    2355                  self.refetch_payload()
    2356                  return
    2357
    2358          generic_codes_aoe = property(_get_generic_codes_aoe, _set_generic_codes_aoe)
    2359          #--------------------------------------------------------
    2360 -        def _get_praxis_branch(self):
    2361                  if self._payload[self._idx['pk_org_unit']] is None:
    2362                          return None
    2363                  return gmPraxis.get_praxis_branch_by_org_unit(pk_org_unit = self._payload[self._idx['pk_org_unit']])
    2364
    2365          praxis_branch = property(_get_praxis_branch, lambda x:x)
    2366          #--------------------------------------------------------
    2367 -        def _get_org_unit(self):
    2368                  if self._payload[self._idx['pk_org_unit']] is None:
    2369                          return None
    2370                  return gmOrganization.cOrgUnit(aPK_obj = self._payload[self._idx['pk_org_unit']])
    2371
    2372          org_unit = property(_get_org_unit, lambda x:x)
    2373
    2374  #-----------------------------------------------------------
    2375 -def create_encounter(fk_patient=None, enc_type=None):
    2376          """Creates a new encounter for a patient.
    2377
    2378          fk_patient - patient PK
    2379          enc_type - type of encounter
    2380          """
    2381          if enc_type is None:
    2382                  enc_type = u'in surgery'
    2383          # insert new encounter
    2384          queries = []
    2385          try:
    2386                  enc_type = int(enc_type)
    2387                  cmd = u"""
    2388                          INSERT INTO clin.encounter (fk_patient, fk_type)
    2389                          VALUES (%(pat)s, %(typ)s) RETURNING pk"""
    2390          except ValueError:
    2391                  enc_type = enc_type
    2392                  cmd = u"""
    2393                          INSERT INTO clin.encounter (fk_patient, fk_type)
    2394                          VALUES (
    2395                                  %(pat)s,
    2396                                  coalesce (
    2397                                          (select pk from clin.encounter_type where description = %(typ)s),
    2398                                          -- pick the first available
    2399                                          (select pk from clin.encounter_type limit 1)
    2400                                  )
    2401                          ) RETURNING pk"""
    2402          args = {'pat': fk_patient, 'typ': enc_type}
    2403          queries.append({'cmd': cmd, 'args': args})
    2404          rows, idx = gmPG2.run_rw_queries(queries = queries, return_data = True, get_col_idx = False)
    2405          encounter = cEncounter(aPK_obj = rows[0]['pk'])
    2406
    2407          return encounter
    2408
    2409  #-----------------------------------------------------------
    2410  # encounter types handling
    2411  #-----------------------------------------------------------
    2412 -def update_encounter_type(description=None, l10n_description=None):
    2413
    2414          rows, idx = gmPG2.run_rw_queries(
    2415                  queries = [{
    2416                  'cmd': u"select i18n.upd_tx(%(desc)s, %(l10n_desc)s)",
    2417                  'args': {'desc': description, 'l10n_desc': l10n_description}
    2418                  }],
    2419                  return_data = True
    2420          )
    2421
    2422          success = rows[0][0]
    2423          if not success:
    2424                  _log.warning('updating encounter type [%s] to [%s] failed', description, l10n_description)
    2425
    2426          return {'description': description, 'l10n_description': l10n_description}
    2427  #-----------------------------------------------------------
    2428 -def create_encounter_type(description=None, l10n_description=None):
    2429          """This will attempt to create a NEW encounter type."""
    2430
    2431          # need a system name, so derive one if necessary
    2432          if description is None:
    2433                  description = l10n_description
    2434
    2435          args = {
    2436                  'desc': description,
    2437                  'l10n_desc': l10n_description
    2438          }
    2439
    2440          _log.debug('creating encounter type: %s, %s', description, l10n_description)
    2441
    2442          # does it exist already ?
    2443          cmd = u"select description, _(description) from clin.encounter_type where description = %(desc)s"
    2444          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
    2445
    2446          # yes
    2447          if len(rows) > 0:
    2448                  # both system and l10n name are the same so all is well
    2449                  if (rows[0][0] == description) and (rows[0][1] == l10n_description):
    2450                          _log.info('encounter type [%s] already exists with the proper translation')
    2451                          return {'description': description, 'l10n_description': l10n_description}
    2452
    2453                  # or maybe there just wasn't a translation to
    2454                  # the current language for this type yet ?
    2455                  cmd = u"select exists (select 1 from i18n.translations where orig = %(desc)s and lang = i18n.get_curr_lang())"
    2456                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
    2457
    2458                  # there was, so fail
    2459                  if rows[0][0]:
    2460                          _log.error('encounter type [%s] already exists but with another translation')
    2461                          return None
    2462
    2463                  # else set it
    2464                  cmd = u"select i18n.upd_tx(%(desc)s, %(l10n_desc)s)"
    2465                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    2466                  return {'description': description, 'l10n_description': l10n_description}
    2467
    2468          # no
    2469          queries = [
    2470                  {'cmd': u"insert into clin.encounter_type (description) values (%(desc)s)", 'args': args},
    2471                  {'cmd': u"select i18n.upd_tx(%(desc)s, %(l10n_desc)s)", 'args': args}
    2472          ]
    2473          rows, idx = gmPG2.run_rw_queries(queries = queries)
    2474
    2475          return {'description': description, 'l10n_description': l10n_description}
    2476
    2477  #-----------------------------------------------------------
    2478 -def get_most_commonly_used_encounter_type():
    2479          cmd = u"""
    2480                  SELECT
    2481                          COUNT(1) AS type_count,
    2482                          fk_type
    2483                  FROM clin.encounter
    2484                  GROUP BY fk_type
    2485                  ORDER BY type_count DESC
    2486                  LIMIT 1
    2487          """
    2488          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = False)
    2489          if len(rows) == 0:
    2490                  return None
    2491          return rows[0]['fk_type']
    2492
    2493  #-----------------------------------------------------------
    2494 -def get_encounter_types():
    2495          cmd = u"""
    2496                  SELECT
    2497                          _(description) AS l10n_description,
    2498                          description
    2499                  FROM
    2500                          clin.encounter_type
    2501                  ORDER BY
    2502                          l10n_description
    2503          """
    2504          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = False)
    2505          return rows
    2506
    2507  #-----------------------------------------------------------
    2508 -def get_encounter_type(description=None):
    2509          cmd = u"SELECT * from clin.encounter_type where description = %s"
    2510          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [description]}])
    2511          return rows
    2512
    2513  #-----------------------------------------------------------
    2514 -def delete_encounter_type(description=None):
    2515          cmd = u"delete from clin.encounter_type where description = %(desc)s"
    2516          args = {'desc': description}
    2517          try:
    2518                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    2519          except gmPG2.dbapi.IntegrityError, e:
    2520                  if e.pgcode == gmPG2.sql_error_codes.FOREIGN_KEY_VIOLATION:
    2521                          return False
    2522                  raise
    2523
    2524          return True
    2525  #============================================================
    2526 -class cProblem(gmBusinessDBObject.cBusinessDBObject):
    2527          """Represents one problem.
    2528
    2529          problems are the aggregation of
    2530                  .clinically_relevant=True issues and
    2531                  .is_open=True episodes
    2532          """
    2533          _cmd_fetch_payload = u''                                        # will get programmatically defined in __init__
    2534          _cmds_store_payload = [u"select 1"]
    2535          _updatable_fields = []
    2536
    2537          #--------------------------------------------------------
    2538 -        def __init__(self, aPK_obj=None, try_potential_problems=False):
    2539                  """Initialize.
    2540
    2541                  aPK_obj must contain the keys
    2542                          pk_patient
    2543                          pk_episode
    2544                          pk_health_issue
    2545                  """
    2546                  if aPK_obj is None:
    2547                          raise gmExceptions.ConstructorError, 'cannot instatiate cProblem for PK: [%s]' % (aPK_obj)
    2548
    2549                  # As problems are rows from a view of different emr struct items,
    2550                  # the PK can't be a single field and, as some of the values of the
    2551                  # composed PK may be None, they must be queried using 'is null',
    2552                  # so we must programmatically construct the SQL query
    2553                  where_parts = []
    2554                  pk = {}
    2555                  for col_name in aPK_obj.keys():
    2556                          val = aPK_obj[col_name]
    2557                          if val is None:
    2558                                  where_parts.append('%s IS NULL' % col_name)
    2559                          else:
    2560                                  where_parts.append('%s = %%(%s)s' % (col_name, col_name))
    2561                                  pk[col_name] = val
    2562
    2563                  # try to instantiate from true problem view
    2564                  cProblem._cmd_fetch_payload = u"""
    2565                          SELECT *, False as is_potential_problem
    2566                          FROM clin.v_problem_list
    2567                          WHERE %s""" % u' AND '.join(where_parts)
    2568
    2569                  try:
    2570                          gmBusinessDBObject.cBusinessDBObject.__init__(self, aPK_obj=pk)
    2571                          return
    2572                  except gmExceptions.ConstructorError:
    2573                          _log.exception('actual problem not found, trying "potential" problems')
    2574                          if try_potential_problems is False:
    2575                                  raise
    2576
    2577                  # try to instantiate from potential-problems view
    2578                  cProblem._cmd_fetch_payload = u"""
    2579                          SELECT *, True as is_potential_problem
    2580                          FROM clin.v_potential_problem_list
    2581                          WHERE %s""" % u' AND '.join(where_parts)
    2582                  gmBusinessDBObject.cBusinessDBObject.__init__(self, aPK_obj=pk)
    2583          #--------------------------------------------------------
    2584 -        def get_as_episode(self):
    2585                  """
    2586                  Retrieve the cEpisode instance equivalent to this problem.
    2587                  The problem's type attribute must be 'episode'
    2588                  """
    2589                  if self._payload[self._idx['type']] != 'episode':
    2590                          _log.error('cannot convert problem [%s] of type [%s] to episode' % (self._payload[self._idx['problem']], self._payload[self._idx['type']]))
    2591                          return None
    2592                  return cEpisode(aPK_obj = self._payload[self._idx['pk_episode']])
    2593          #--------------------------------------------------------
    2594 -        def get_as_health_issue(self):
    2595                  """
    2596                  Retrieve the cHealthIssue instance equivalent to this problem.
    2597                  The problem's type attribute must be 'issue'
    2598                  """
    2599                  if self._payload[self._idx['type']] != 'issue':
    2600                          _log.error('cannot convert problem [%s] of type [%s] to health issue' % (self._payload[self._idx['problem']], self._payload[self._idx['type']]))
    2601                          return None
    2602                  return cHealthIssue(aPK_obj = self._payload[self._idx['pk_health_issue']])
    2603          #--------------------------------------------------------
    2604 -        def get_visual_progress_notes(self, encounter_id=None):
    2605
    2606                  if self._payload[self._idx['type']] == u'issue':
    2607                          episodes = [ cHealthIssue(aPK_obj = self._payload[self._idx['pk_health_issue']]).latest_episode ]
    2608                          #xxxxxxxxxxxxx
    2609
    2610                  emr = patient.get_emr()
    2611
    2612                  doc_folder = gmDocuments.cDocumentFolder(aPKey = patient.ID)
    2613                  return doc_folder.get_visual_progress_notes (
    2614                          health_issue = self._payload[self._idx['pk_health_issue']],
    2615                          episode = self._payload[self._idx['pk_episode']]
    2616                  )
    2617          #--------------------------------------------------------
    2618          # properties
    2619          #--------------------------------------------------------
    2620          # doubles as 'diagnostic_certainty_description' getter:
    2621 -        def get_diagnostic_certainty_description(self):
    2622                  return diagnostic_certainty_classification2str(self._payload[self._idx['diagnostic_certainty_classification']])
    2623
    2624          diagnostic_certainty_description = property(get_diagnostic_certainty_description, lambda x:x)
    2625          #--------------------------------------------------------
    2626 -        def _get_generic_codes(self):
    2627                  if self._payload[self._idx['type']] == u'issue':
    2628                          cmd = u"""
    2629                                  SELECT * FROM clin.v_linked_codes WHERE
    2630                                          item_table = 'clin.lnk_code2h_issue'::regclass
    2631                                                  AND
    2632                                          pk_item = %(item)s
    2633                          """
    2634                          args = {'item': self._payload[self._idx['pk_health_issue']]}
    2635                  else:
    2636                          cmd = u"""
    2637                                  SELECT * FROM clin.v_linked_codes WHERE
    2638                                          item_table = 'clin.lnk_code2episode'::regclass
    2639                                                  AND
    2640                                          pk_item = %(item)s
    2641                          """
    2642                          args = {'item': self._payload[self._idx['pk_episode']]}
    2643
    2644                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2645                  return [ gmCoding.cGenericLinkedCode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_lnk_code2item'}) for r in rows ]
    2646
    2647          generic_codes = property(_get_generic_codes, lambda x:x)
    2648  #-----------------------------------------------------------
    2649 -def problem2episode(problem=None):
    2650          """Retrieve the cEpisode instance equivalent to the given problem.
    2651
    2652          The problem's type attribute must be 'episode'
    2653
    2654          @param problem: The problem to retrieve its related episode for
    2655          @type problem: A gmEMRStructItems.cProblem instance
    2656          """
    2657          if isinstance(problem, cEpisode):
    2658                  return problem
    2659
    2660          exc = TypeError('cannot convert [%s] to episode' % problem)
    2661
    2662          if not isinstance(problem, cProblem):
    2663                  raise exc
    2664
    2665          if problem['type'] != 'episode':
    2666                  raise exc
    2667
    2668          return cEpisode(aPK_obj = problem['pk_episode'])
    2669  #-----------------------------------------------------------
    2670 -def problem2issue(problem=None):
    2671          """Retrieve the cIssue instance equivalent to the given problem.
    2672
    2673          The problem's type attribute must be 'issue'.
    2674
    2675          @param problem: The problem to retrieve the corresponding issue for
    2676          @type problem: A gmEMRStructItems.cProblem instance
    2677          """
    2678          if isinstance(problem, cHealthIssue):
    2679                  return problem
    2680
    2681          exc = TypeError('cannot convert [%s] to health issue' % problem)
    2682
    2683          if not isinstance(problem, cProblem):
    2684                  raise exc
    2685
    2686          if  problem['type'] != 'issue':
    2687                  raise exc
    2688
    2689          return cHealthIssue(aPK_obj = problem['pk_health_issue'])
    2690  #-----------------------------------------------------------
    2691 -def reclass_problem(self, problem=None):
    2692          """Transform given problem into either episode or health issue instance.
    2693          """
    2694          if isinstance(problem, (cEpisode, cHealthIssue)):
    2695                  return problem
    2696
    2697          exc = TypeError('cannot reclass [%s] instance to either episode or health issue' % type(problem))
    2698
    2699          if not isinstance(problem, cProblem):
    2700                  _log.debug(u'%s' % problem)
    2701                  raise exc
    2702
    2703          if problem['type'] == 'episode':
    2704                  return cEpisode(aPK_obj = problem['pk_episode'])
    2705
    2706          if problem['type'] == 'issue':
    2707                  return cHealthIssue(aPK_obj = problem['pk_health_issue'])
    2708
    2709          raise exc
    2710
    2711  #============================================================
    2712  _SQL_get_hospital_stays = u"select * from clin.v_hospital_stays where %s"
    2713
    2714 -class cHospitalStay(gmBusinessDBObject.cBusinessDBObject):
    2715
    2716          _cmd_fetch_payload = _SQL_get_hospital_stays % u"pk_hospital_stay = %s"
    2717          _cmds_store_payload = [
    2718                  u"""UPDATE clin.hospital_stay SET
    2719                                  clin_when = %(admission)s,
    2720                                  discharge = %(discharge)s,
    2721                                  fk_org_unit = %(pk_org_unit)s,
    2722                                  narrative = gm.nullify_empty_string(%(comment)s),
    2723                                  fk_episode = %(pk_episode)s,
    2724                                  fk_encounter = %(pk_encounter)s
    2725                          WHERE
    2726                                  pk = %(pk_hospital_stay)s
    2727                                          AND
    2728                                  xmin = %(xmin_hospital_stay)s
    2729                          RETURNING
    2730                                  xmin AS xmin_hospital_stay
    2731                          """
    2732          ]
    2733          _updatable_fields = [
    2734                  'admission',
    2735                  'discharge',
    2736                  'pk_org_unit',
    2737                  'pk_episode',
    2738                  'pk_encounter',
    2739                  'comment'
    2740          ]
    2741          #-------------------------------------------------------
    2742 -        def format(self, left_margin=0, include_procedures=False, include_docs=False):
    2743
    2744                  if self._payload[self._idx['discharge']] is not None:
    2745                          discharge = u' - %s' % gmDateTime.pydt_strftime(self._payload[self._idx['discharge']], '%Y %b %d')
    2746                  else:
    2747                          discharge = u''
    2748
    2749                  line = u'%s%s%s (%s@%s): %s%s%s' % (
    2750                          u' ' * left_margin,
    2751                          gmDateTime.pydt_strftime(self._payload[self._idx['admission']], '%Y %b %d'),
    2752                          discharge,
    2753                          self._payload[self._idx['ward']],
    2754                          self._payload[self._idx['hospital']],
    2755                          gmTools.u_left_double_angle_quote,
    2756                          self._payload[self._idx['episode']],
    2757                          gmTools.u_right_double_angle_quote
    2758                  )
    2759                  return line
    2760
    2761  #-----------------------------------------------------------
    2762 -def get_latest_patient_hospital_stay(patient=None):
    2763          cmd = _SQL_get_hospital_stays % u"pk_patient = %(pat)s ORDER BY admission DESC LIMIT 1"
    2764          queries = [{
    2765                  # this assumes non-overarching stays
    2766                  #'cmd': u'SELECT * FROM clin.v_hospital_stays WHERE pk_patient = %(pat)s ORDER BY admission DESC LIMIT 1',
    2767                  'cmd': cmd,
    2768                  'args': {'pat': patient}
    2769          }]
    2770          rows, idx = gmPG2.run_ro_queries(queries = queries, get_col_idx = True)
    2771          if len(rows) == 0:
    2772                  return None
    2773          return cHospitalStay(row = {'idx': idx, 'data': rows[0], 'pk_field': 'pk_hospital_stay'})
    2774
    2775  #-----------------------------------------------------------
    2776 -def get_patient_hospital_stays(patient=None, ongoing_only=False):
    2777          args = {'pat': patient}
    2778          if ongoing_only:
    2779                  cmd = _SQL_get_hospital_stays % u"pk_patient = %(pat)s AND discharge is NULL ORDER BY admission"
    2780          else:
    2781                  cmd = _SQL_get_hospital_stays % u"pk_patient = %(pat)s ORDER BY admission"
    2782
    2783          queries = [{'cmd': cmd, 'args': args}]
    2784          rows, idx = gmPG2.run_ro_queries(queries = queries, get_col_idx = True)
    2785
    2786          return [ cHospitalStay(row = {'idx': idx, 'data': r, 'pk_field': 'pk_hospital_stay'})  for r in rows ]
    2787
    2788  #-----------------------------------------------------------
    2789 -def create_hospital_stay(encounter=None, episode=None, fk_org_unit=None):
    2790
    2791          queries = [{
    2792                   'cmd': u'INSERT INTO clin.hospital_stay (fk_encounter, fk_episode, fk_org_unit) VALUES (%(enc)s, %(epi)s, %(fk_org_unit)s) RETURNING pk',
    2793                   'args': {'enc': encounter, 'epi': episode, 'fk_org_unit': fk_org_unit}
    2794          }]
    2795          rows, idx = gmPG2.run_rw_queries(queries = queries, return_data = True)
    2796
    2797          return cHospitalStay(aPK_obj = rows[0][0])
    2798
    2799  #-----------------------------------------------------------
    2800 -def delete_hospital_stay(stay=None):
    2801          cmd = u'DELETE FROM clin.hospital_stay WHERE pk = %(pk)s'
    2802          args = {'pk': stay}
    2803          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    2804          return True
    2805
    2806  #============================================================
    2807  _SQL_get_procedures = u"select * from clin.v_procedures where %s"
    2808
    2809 -class cPerformedProcedure(gmBusinessDBObject.cBusinessDBObject):
    2810
    2811          _cmd_fetch_payload = _SQL_get_procedures % u"pk_procedure = %s"
    2812          _cmds_store_payload = [
    2813                  u"""UPDATE clin.procedure SET
    2814                                  soap_cat = 'p',
    2815                                  clin_when = %(clin_when)s,
    2816                                  clin_end = %(clin_end)s,
    2817                                  is_ongoing = %(is_ongoing)s,
    2818                                  narrative = gm.nullify_empty_string(%(performed_procedure)s),
    2819                                  fk_hospital_stay = %(pk_hospital_stay)s,
    2820                                  fk_org_unit = (CASE
    2821                                          WHEN %(pk_hospital_stay)s IS NULL THEN %(pk_org_unit)s
    2822                                          ELSE NULL
    2823                                  END)::integer,
    2824                                  fk_episode = %(pk_episode)s,
    2825                                  fk_encounter = %(pk_encounter)s
    2826                          WHERE
    2827                                  pk = %(pk_procedure)s AND
    2828                                  xmin = %(xmin_procedure)s
    2829                          RETURNING xmin as xmin_procedure"""
    2830          ]
    2831          _updatable_fields = [
    2832                  'clin_when',
    2833                  'clin_end',
    2834                  'is_ongoing',
    2835                  'performed_procedure',
    2836                  'pk_hospital_stay',
    2837                  'pk_org_unit',
    2838                  'pk_episode',
    2839                  'pk_encounter'
    2840          ]
    2841          #-------------------------------------------------------
    2842 -        def __setitem__(self, attribute, value):
    2843
    2844                  if (attribute == 'pk_hospital_stay') and (value is not None):
    2845                          gmBusinessDBObject.cBusinessDBObject.__setitem__(self, 'pk_org_unit', None)
    2846
    2847                  if (attribute == 'pk_org_unit') and (value is not None):
    2848                          gmBusinessDBObject.cBusinessDBObject.__setitem__(self, 'pk_hospital_stay', None)
    2849
    2850                  gmBusinessDBObject.cBusinessDBObject.__setitem__(self, attribute, value)
    2851          #-------------------------------------------------------
    2852 -        def format(self, left_margin=0, include_episode=True, include_codes=False):
    2853
    2854                  if self._payload[self._idx['is_ongoing']]:
    2855                          end = _(' (ongoing)')
    2856                  else:
    2857                          end = self._payload[self._idx['clin_end']]
    2858                          if end is None:
    2859                                  end = u''
    2860                          else:
    2861                                  end = u' - %s' % gmDateTime.pydt_strftime(end, '%Y %b %d')
    2862
    2863                  line = u'%s%s%s (%s @ %s): %s' % (
    2864                          (u' ' * left_margin),
    2865                          gmDateTime.pydt_strftime(self._payload[self._idx['clin_when']], '%Y %b %d'),
    2866                          end,
    2867                          self._payload[self._idx['unit']],
    2868                          self._payload[self._idx['organization']],
    2869                          self._payload[self._idx['performed_procedure']]
    2870                  )
    2871                  if include_episode:
    2872                          line = u'%s (%s)' % (line, self._payload[self._idx['episode']])
    2873
    2874                  if include_codes:
    2875                          codes = self.generic_codes
    2876                          if len(codes) > 0:
    2877                                  line += u'\n'
    2878                          for c in codes:
    2879                                  line += u'%s  %s: %s (%s - %s)\n' % (
    2880                                          (u' ' * left_margin),
    2881                                          c['code'],
    2882                                          c['term'],
    2883                                          c['name_short'],
    2884                                          c['version']
    2885                                  )
    2886                          del codes
    2887
    2888                  return line
    2889          #--------------------------------------------------------
    2890 -        def add_code(self, pk_code=None):
    2891                  """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
    2892                  cmd = u"INSERT INTO clin.lnk_code2procedure (fk_item, fk_generic_code) values (%(issue)s, %(code)s)"
    2893                  args = {
    2894                          'issue': self._payload[self._idx['pk_procedure']],
    2895                          'code': pk_code
    2896                  }
    2897                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    2898                  return True
    2899          #--------------------------------------------------------
    2900 -        def remove_code(self, pk_code=None):
    2901                  """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
    2902                  cmd = u"DELETE FROM clin.lnk_code2procedure WHERE fk_item = %(issue)s AND fk_generic_code = %(code)s"
    2903                  args = {
    2904                          'issue': self._payload[self._idx['pk_procedure']],
    2905                          'code': pk_code
    2906                  }
    2907                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    2908                  return True
    2909          #--------------------------------------------------------
    2910          # properties
    2911          #--------------------------------------------------------
    2912 -        def _get_generic_codes(self):
    2913                  if len(self._payload[self._idx['pk_generic_codes']]) == 0:
    2914                          return []
    2915
    2916                  cmd = gmCoding._SQL_get_generic_linked_codes % u'pk_generic_code IN %(pks)s'
    2917                  args = {'pks': tuple(self._payload[self._idx['pk_generic_codes']])}
    2918                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2919                  return [ gmCoding.cGenericLinkedCode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_lnk_code2item'}) for r in rows ]
    2920
    2921 -        def _set_generic_codes(self, pk_codes):
    2922                  queries = []
    2923                  # remove all codes
    2924                  if len(self._payload[self._idx['pk_generic_codes']]) > 0:
    2925                          queries.append ({
    2926                                  'cmd': u'DELETE FROM clin.lnk_code2procedure WHERE fk_item = %(proc)s AND fk_generic_code IN %(codes)s',
    2927                                  'args': {
    2928                                          'proc': self._payload[self._idx['pk_procedure']],
    2929                                          'codes': tuple(self._payload[self._idx['pk_generic_codes']])
    2930                                  }
    2931                          })
    2932                  # add new codes
    2933                  for pk_code in pk_codes:
    2934                          queries.append ({
    2935                                  'cmd': u'INSERT INTO clin.lnk_code2procedure (fk_item, fk_generic_code) VALUES (%(proc)s, %(pk_code)s)',
    2936                                  'args': {
    2937                                          'proc': self._payload[self._idx['pk_procedure']],
    2938                                          'pk_code': pk_code
    2939                                  }
    2940                          })
    2941                  if len(queries) == 0:
    2942                          return
    2943                  # run it all in one transaction
    2944                  rows, idx = gmPG2.run_rw_queries(queries = queries)
    2945                  return
    2946
    2947          generic_codes = property(_get_generic_codes, _set_generic_codes)
    2948  #-----------------------------------------------------------
    2949 -def get_performed_procedures(patient=None):
    2950
    2951          queries = [
    2952                  {
    2953                  'cmd': u'SELECT * FROM clin.v_procedures WHERE pk_patient = %(pat)s ORDER BY clin_when',
    2954                  'args': {'pat': patient}
    2955                  }
    2956          ]
    2957
    2958          rows, idx = gmPG2.run_ro_queries(queries = queries, get_col_idx = True)
    2959
    2960          return [ cPerformedProcedure(row = {'idx': idx, 'data': r, 'pk_field': 'pk_procedure'})  for r in rows ]
    2961  #-----------------------------------------------------------
    2962 -def get_latest_performed_procedure(patient=None):
    2963          queries = [
    2964                  {
    2965                  'cmd': u'select * FROM clin.v_procedures WHERE pk_patient = %(pat)s ORDER BY clin_when DESC LIMIT 1',
    2966                  'args': {'pat': patient}
    2967                  }
    2968          ]
    2969          rows, idx = gmPG2.run_ro_queries(queries = queries, get_col_idx = True)
    2970          if len(rows) == 0:
    2971                  return None
    2972          return cPerformedProcedure(row = {'idx': idx, 'data': rows[0], 'pk_field': 'pk_procedure'})
    2973  #-----------------------------------------------------------
    2974 -def create_performed_procedure(encounter=None, episode=None, location=None, hospital_stay=None, procedure=None):
    2975
    2976          queries = [{
    2977                  'cmd': u"""
    2978                          INSERT INTO clin.procedure (
    2979                                  fk_encounter,
    2980                                  fk_episode,
    2981                                  soap_cat,
    2982                                  fk_org_unit,
    2983                                  fk_hospital_stay,
    2984                                  narrative
    2985                          ) VALUES (
    2986                                  %(enc)s,
    2987                                  %(epi)s,
    2988                                  'p',
    2989                                  %(loc)s,
    2990                                  %(stay)s,
    2991                                  gm.nullify_empty_string(%(proc)s)
    2992                          )
    2993                          RETURNING pk""",
    2994                  'args': {'enc': encounter, 'epi': episode, 'loc': location, 'stay': hospital_stay, 'proc': procedure}
    2995          }]
    2996
    2997          rows, idx = gmPG2.run_rw_queries(queries = queries, return_data = True)
    2998
    2999          return cPerformedProcedure(aPK_obj = rows[0][0])
    3000
    3001  #-----------------------------------------------------------
    3002 -def delete_performed_procedure(procedure=None):
    3003          cmd = u'delete from clin.procedure where pk = %(pk)s'
    3004          args = {'pk': procedure}
    3005          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    3006          return True
    3007
    3008  #============================================================
    3009  # main - unit testing
    3010  #------------------------------------------------------------
    3011  if __name__ == '__main__':
    3012
    3013          if len(sys.argv) < 2:
    3014                  sys.exit()
    3015
    3016          if sys.argv[1] != 'test':
    3017                  sys.exit()
    3018
    3019          #--------------------------------------------------------
    3020          # define tests
    3021          #--------------------------------------------------------
    3022 -        def test_problem():
    3023                  print "\nProblem test"
    3024                  print "------------"
    3025                  prob = cProblem(aPK_obj={'pk_patient': 12, 'pk_health_issue': 1, 'pk_episode': None})
    3026                  print prob
    3027                  fields = prob.get_fields()
    3028                  for field in fields:
    3029                          print field, ':', prob[field]
    3030                  print '\nupdatable:', prob.get_updatable_fields()
    3031                  epi = prob.get_as_episode()
    3032                  print '\nas episode:'
    3033                  if epi is not None:
    3034                          for field in epi.get_fields():
    3035                                  print '   .%s : %s' % (field, epi[field])
    3036          #--------------------------------------------------------
    3037 -        def test_health_issue():
    3038                  print "\nhealth issue test"
    3039                  print "-----------------"
    3040                  h_issue = cHealthIssue(aPK_obj=2)
    3041                  print h_issue
    3042                  print h_issue.latest_access_date
    3043                  print h_issue.end_date
    3044  #               fields = h_issue.get_fields()
    3045  #               for field in fields:
    3046  #                       print field, ':', h_issue[field]
    3047  #               print "has open episode:", h_issue.has_open_episode()
    3048  #               print "open episode:", h_issue.get_open_episode()
    3049  #               print "updateable:", h_issue.get_updatable_fields()
    3050  #               h_issue.close_expired_episode(ttl=7300)
    3051  #               h_issue = cHealthIssue(encounter = 1, name = u'post appendectomy/peritonitis')
    3052  #               print h_issue
    3053  #               print h_issue.format_as_journal()
    3054          #--------------------------------------------------------
    3055 -        def test_episode():
    3056                  print "\nepisode test"
    3057                  print "------------"
    3058                  episode = cEpisode(aPK_obj=1)
    3059                  print episode
    3060                  fields = episode.get_fields()
    3061                  for field in fields:
    3062                          print field, ':', episode[field]
    3063                  print "updatable:", episode.get_updatable_fields()
    3064                  raw_input('ENTER to continue')
    3065
    3066                  old_description = episode['description']
    3067                  old_enc = cEncounter(aPK_obj = 1)
    3068
    3069                  desc = '1-%s' % episode['description']
    3070                  print "==> renaming to", desc
    3071                  successful = episode.rename (
    3072                          description = desc
    3073                  )
    3074                  if not successful:
    3075                          print "error"
    3076                  else:
    3077                          print "success"
    3078                          for field in fields:
    3079                                  print field, ':', episode[field]
    3080
    3081                  print "episode range:", episode.get_access_range()
    3082
    3083                  raw_input('ENTER to continue')
    3084
    3085          #--------------------------------------------------------
    3086 -        def test_encounter():
    3087                  print "\nencounter test"
    3088                  print "--------------"
    3089                  encounter = cEncounter(aPK_obj=1)
    3090                  print encounter
    3091                  fields = encounter.get_fields()
    3092                  for field in fields:
    3093                          print field, ':', encounter[field]
    3094                  print "updatable:", encounter.get_updatable_fields()
    3095          #--------------------------------------------------------
    3096 -        def test_encounter2latex():
    3097                  encounter = cEncounter(aPK_obj=1)
    3098                  print encounter
    3099                  print ""
    3100                  print encounter.format_latex()
    3101          #--------------------------------------------------------
    3102 -        def test_performed_procedure():
    3103                  procs = get_performed_procedures(patient = 12)
    3104                  for proc in procs:
    3105                          print proc.format(left_margin=2)
    3106          #--------------------------------------------------------
    3107 -        def test_hospital_stay():
    3108                  stay = create_hospital_stay(encounter = 1, episode = 2, fk_org_unit = 1)
    3109  #               stay['hospital'] = u'Starfleet Galaxy General Hospital'
    3110  #               stay.save_payload()
    3111                  print stay
    3112                  for s in get_patient_hospital_stays(12):
    3113                          print s
    3114                  delete_hospital_stay(stay['pk_hospital_stay'])
    3115                  stay = create_hospital_stay(encounter = 1, episode = 4, fk_org_unit = 1)
    3116          #--------------------------------------------------------
    3117 -        def test_diagnostic_certainty_classification_map():
    3118                  tests = [None, 'A', 'B', 'C', 'D', 'E']
    3119
    3120                  for t in tests:
    3121                          print type(t), t
    3122                          print type(diagnostic_certainty_classification2str(t)), diagnostic_certainty_classification2str(t)
    3123          #--------------------------------------------------------
    3124 -        def test_episode_codes():
    3125                  epi = cEpisode(aPK_obj = 2)
    3126                  print epi
    3127                  print epi.generic_codes
    3128          #--------------------------------------------------------
    3129          # run them
    3130          #test_episode()
    3131          #test_problem()
    3132          #test_encounter()
    3133          test_health_issue()
    3134          #test_hospital_stay()
    3135          #test_performed_procedure()
    3136          #test_diagnostic_certainty_classification_map()
    3137          #test_encounter2latex()
    3138          #test_episode_codes()
    3139  #============================================================
    3140