Gnumed.business.gmEMRStructItems

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

# Source Code for Module Gnumed.business.gmEMRStructItems

       1  # -*- coding: utf-8 -*-
       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 sys
      10  import datetime
      11  import logging
      12  import io
      13  import os
      14
      15
      16  if __name__ == '__main__':
      17          sys.path.insert(0, '../../')
      18  from Gnumed.pycommon import gmPG2
      19  from Gnumed.pycommon import gmI18N
      20  from Gnumed.pycommon import gmTools
      21  from Gnumed.pycommon import gmDateTime
      22  from Gnumed.pycommon import gmBusinessDBObject
      23  from Gnumed.pycommon import gmNull
      24  from Gnumed.pycommon import gmExceptions
      25
      26  from Gnumed.business import gmClinNarrative
      27  from Gnumed.business import gmSoapDefs
      28  from Gnumed.business import gmCoding
      29  from Gnumed.business import gmPraxis
      30  from Gnumed.business import gmOrganization
      31  from Gnumed.business import gmExternalCare
      32  from Gnumed.business import gmDocuments
      33
      34
      35  _log = logging.getLogger('gm.emr')
      36
      37
      38  if __name__ == '__main__':
      39          gmI18N.activate_locale()
      40          gmI18N.install_domain('gnumed')
      41
      42  #============================================================
      43  # diagnostic certainty classification
      44  #============================================================
      45  __diagnostic_certainty_classification_map = None
      46
      47 -def diagnostic_certainty_classification2str(classification):
      48
      49          global __diagnostic_certainty_classification_map
      50
      51          if __diagnostic_certainty_classification_map is None:
      52                  __diagnostic_certainty_classification_map = {
      53                          None: '',
      54                          'A': _('A: Sign'),
      55                          'B': _('B: Cluster of signs'),
      56                          'C': _('C: Syndromic diagnosis'),
      57                          'D': _('D: Scientific diagnosis')
      58                  }
      59
      60          try:
      61                  return __diagnostic_certainty_classification_map[classification]
      62          except KeyError:
      63                  return _('<%s>: unknown diagnostic certainty classification') % classification
      64
      65  #============================================================
      66  # Health Issues API
      67  #============================================================
      68  laterality2str = {
      69          None: '?',
      70          'na': '',
      71          'sd': _('bilateral'),
      72          'ds': _('bilateral'),
      73          's': _('left'),
      74          'd': _('right')
      75  }
      76
      77  #============================================================
      78 -class cHealthIssue(gmBusinessDBObject.cBusinessDBObject):
      79          """Represents one health issue."""
      80
      81          #_cmd_fetch_payload = u"select *, xmin_health_issue from clin.v_health_issues where pk_health_issue=%s"
      82          _cmd_fetch_payload = "select * from clin.v_health_issues where pk_health_issue = %s"
      83          _cmds_store_payload = [
      84                  """update clin.health_issue set
      85                                  description = %(description)s,
      86                                  summary = gm.nullify_empty_string(%(summary)s),
      87                                  age_noted = %(age_noted)s,
      88                                  laterality = gm.nullify_empty_string(%(laterality)s),
      89                                  grouping = gm.nullify_empty_string(%(grouping)s),
      90                                  diagnostic_certainty_classification = gm.nullify_empty_string(%(diagnostic_certainty_classification)s),
      91                                  is_active = %(is_active)s,
      92                                  clinically_relevant = %(clinically_relevant)s,
      93                                  is_confidential = %(is_confidential)s,
      94                                  is_cause_of_death = %(is_cause_of_death)s
      95                          WHERE
      96                                  pk = %(pk_health_issue)s
      97                                          AND
      98                                  xmin = %(xmin_health_issue)s""",
      99                  "select xmin as xmin_health_issue from clin.health_issue where pk = %(pk_health_issue)s"
     100          ]
     101          _updatable_fields = [
     102                  'description',
     103                  'summary',
     104                  'grouping',
     105                  'age_noted',
     106                  'laterality',
     107                  'is_active',
     108                  'clinically_relevant',
     109                  'is_confidential',
     110                  'is_cause_of_death',
     111                  'diagnostic_certainty_classification'
     112          ]
     113
     114          #--------------------------------------------------------
     115 -        def __init__(self, aPK_obj=None, encounter=None, name='xxxDEFAULTxxx', patient=None, row=None):
     116                  pk = aPK_obj
     117
     118                  if (pk is not None) or (row is not None):
     119                          gmBusinessDBObject.cBusinessDBObject.__init__(self, aPK_obj=pk, row=row)
     120                          return
     121
     122                  if patient is None:
     123                          cmd = """select *, xmin_health_issue from clin.v_health_issues
     124                                          where
     125                                                  description = %(desc)s
     126                                                          and
     127                                                  pk_patient = (select fk_patient from clin.encounter where pk = %(enc)s)"""
     128                  else:
     129                          cmd = """select *, xmin_health_issue from clin.v_health_issues
     130                                          where
     131                                                  description = %(desc)s
     132                                                          and
     133                                                  pk_patient = %(pat)s"""
     134
     135                  queries = [{'cmd': cmd, 'args': {'enc': encounter, 'desc': name, 'pat': patient}}]
     136                  rows, idx = gmPG2.run_ro_queries(queries = queries,     get_col_idx = True)
     137
     138                  if len(rows) == 0:
     139                          raise gmExceptions.NoSuchBusinessObjectError('no health issue for [enc:%s::desc:%s::pat:%s]' % (encounter, name, patient))
     140
     141                  pk = rows[0][0]
     142                  r = {'idx': idx, 'data': rows[0], 'pk_field': 'pk_health_issue'}
     143
     144                  gmBusinessDBObject.cBusinessDBObject.__init__(self, row=r)
     145
     146          #--------------------------------------------------------
     147          # external API
     148          #--------------------------------------------------------
     149 -        def rename(self, description=None):
     150                  """Method for issue renaming.
     151
     152                  @param description
     153                          - the new descriptive name for the issue
     154                  @type description
     155                          - a string instance
     156                  """
     157                  # sanity check
     158                  if not type(description) in [str, str] or description.strip() == '':
     159                          _log.error('<description> must be a non-empty string')
     160                          return False
     161                  # update the issue description
     162                  old_description = self._payload[self._idx['description']]
     163                  self._payload[self._idx['description']] = description.strip()
     164                  self._is_modified = True
     165                  successful, data = self.save_payload()
     166                  if not successful:
     167                          _log.error('cannot rename health issue [%s] with [%s]' % (self, description))
     168                          self._payload[self._idx['description']] = old_description
     169                          return False
     170                  return True
     171
     172          #--------------------------------------------------------
     173 -        def get_episodes(self):
     174                  cmd = "SELECT * FROM clin.v_pat_episodes WHERE pk_health_issue = %(pk)s"
     175                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}], get_col_idx = True)
     176                  return [ cEpisode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_episode'})  for r in rows ]
     177
     178          #--------------------------------------------------------
     179 -        def close_expired_episode(self, ttl=180):
     180                  """ttl in days"""
     181                  open_episode = self.open_episode
     182                  if open_episode is None:
     183                          return True
     184                  #clinical_end = open_episode.best_guess_clinical_end_date
     185                  clinical_end = open_episode.latest_access_date          # :-/
     186                  ttl = datetime.timedelta(ttl)
     187                  now = datetime.datetime.now(tz = clinical_end.tzinfo)
     188                  if (clinical_end + ttl) > now:
     189                          return False
     190                  open_episode['episode_open'] = False
     191                  success, data = open_episode.save_payload()
     192                  if success:
     193                          return True
     194                  return False            # should be an exception
     195
     196          #--------------------------------------------------------
     197 -        def close_episode(self):
     198                  open_episode = self.get_open_episode()
     199                  open_episode['episode_open'] = False
     200                  success, data = open_episode.save_payload()
     201                  if success:
     202                          return True
     203                  return False
     204
     205          #--------------------------------------------------------
     206 -        def has_open_episode(self):
     207                  return self._payload[self._idx['has_open_episode']]
     208
     209          #--------------------------------------------------------
     210 -        def get_open_episode(self):
     211                  cmd = "select pk from clin.episode where fk_health_issue = %s and is_open IS True LIMIT 1"
     212                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}])
     213                  if len(rows) == 0:
     214                          return None
     215                  return cEpisode(aPK_obj=rows[0][0])
     216
     217          #--------------------------------------------------------
     218 -        def age_noted_human_readable(self):
     219                  if self._payload[self._idx['age_noted']] is None:
     220                          return '<???>'
     221
     222                  # since we've already got an interval we are bound to use it,
     223                  # further transformation will only introduce more errors,
     224                  # later we can improve this deeper inside
     225                  return gmDateTime.format_interval_medically(self._payload[self._idx['age_noted']])
     226
     227          #--------------------------------------------------------
     228 -        def add_code(self, pk_code=None):
     229                  """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
     230                  cmd = "INSERT INTO clin.lnk_code2h_issue (fk_item, fk_generic_code) values (%(item)s, %(code)s)"
     231                  args = {
     232                          'item': self._payload[self._idx['pk_health_issue']],
     233                          'code': pk_code
     234                  }
     235                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     236                  return True
     237
     238          #--------------------------------------------------------
     239 -        def remove_code(self, pk_code=None):
     240                  """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
     241                  cmd = "DELETE FROM clin.lnk_code2h_issue WHERE fk_item = %(item)s AND fk_generic_code = %(code)s"
     242                  args = {
     243                          'item': self._payload[self._idx['pk_health_issue']],
     244                          'code': pk_code
     245                  }
     246                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     247                  return True
     248
     249          #--------------------------------------------------------
     250 -        def format_as_journal(self, left_margin=0, date_format='%Y %b %d, %a'):
     251                  rows = gmClinNarrative.get_as_journal (
     252                          issues = (self.pk_obj,),
     253                          order_by = 'pk_episode, pk_encounter, clin_when, scr, src_table'
     254                  )
     255
     256                  if len(rows) == 0:
     257                          return ''
     258
     259                  left_margin = ' ' * left_margin
     260
     261                  lines = []
     262                  lines.append(_('Clinical data generated during encounters under this health issue:'))
     263
     264                  prev_epi = None
     265                  for row in rows:
     266                          if row['pk_episode'] != prev_epi:
     267                                  lines.append('')
     268                                  prev_epi = row['pk_episode']
     269
     270                          when = gmDateTime.pydt_strftime(row['clin_when'], date_format)
     271                          top_row = '%s%s %s (%s) %s' % (
     272                                  gmTools.u_box_top_left_arc,
     273                                  gmTools.u_box_horiz_single,
     274                                  gmSoapDefs.soap_cat2l10n_str[row['real_soap_cat']],
     275                                  when,
     276                                  gmTools.u_box_horiz_single * 5
     277                          )
     278                          soap = gmTools.wrap (
     279                                  text = row['narrative'],
     280                                  width = 60,
     281                                  initial_indent = '  ',
     282                                  subsequent_indent = '  ' + left_margin
     283                          )
     284                          row_ver = ''
     285                          if row['row_version'] > 0:
     286                                  row_ver = 'v%s: ' % row['row_version']
     287                          bottom_row = '%s%s %s, %s%s %s' % (
     288                                  ' ' * 40,
     289                                  gmTools.u_box_horiz_light_heavy,
     290                                  row['modified_by'],
     291                                  row_ver,
     292                                  gmDateTime.pydt_strftime(row['modified_when'], date_format),
     293                                  gmTools.u_box_horiz_heavy_light
     294                          )
     295
     296                          lines.append(top_row)
     297                          lines.append(soap)
     298                          lines.append(bottom_row)
     299
     300                  eol_w_margin = '\n%s' % left_margin
     301                  return left_margin + eol_w_margin.join(lines) + '\n'
     302
     303          #--------------------------------------------------------
     304 -        def format (self, left_margin=0, patient=None,
     305                  with_summary=True,
     306                  with_codes=True,
     307                  with_episodes=True,
     308                  with_encounters=True,
     309                  with_medications=True,
     310                  with_hospital_stays=True,
     311                  with_procedures=True,
     312                  with_family_history=True,
     313                  with_documents=True,
     314                  with_tests=True,
     315                  with_vaccinations=True,
     316                  with_external_care=True
     317          ):
     318
     319                  lines = []
     320
     321                  lines.append(_('Health Issue %s%s%s%s   [#%s]') % (
     322                          '\u00BB',
     323                          self._payload[self._idx['description']],
     324                          '\u00AB',
     325                          gmTools.coalesce (
     326                                  initial = self.laterality_description,
     327                                  instead = '',
     328                                  template_initial = ' (%s)',
     329                                  none_equivalents = [None, '', '?']
     330                          ),
     331                          self._payload[self._idx['pk_health_issue']]
     332                  ))
     333
     334                  if self._payload[self._idx['is_confidential']]:
     335                          lines.append('')
     336                          lines.append(_(' ***** CONFIDENTIAL *****'))
     337                          lines.append('')
     338
     339                  if self._payload[self._idx['is_cause_of_death']]:
     340                          lines.append('')
     341                          lines.append(_(' contributed to death of patient'))
     342                          lines.append('')
     343
     344                  enc = cEncounter(aPK_obj = self._payload[self._idx['pk_encounter']])
     345                  lines.append (_(' Created during encounter: %s (%s - %s)   [#%s]') % (
     346                          enc['l10n_type'],
     347                          enc['started_original_tz'].strftime('%Y-%m-%d %H:%M'),
     348                          enc['last_affirmed_original_tz'].strftime('%H:%M'),
     349                          self._payload[self._idx['pk_encounter']]
     350                  ))
     351
     352                  if self._payload[self._idx['age_noted']] is not None:
     353                          lines.append(_(' Noted at age: %s') % self.age_noted_human_readable())
     354
     355                  lines.append(' ' + _('Status') + ': %s, %s%s' % (
     356                          gmTools.bool2subst(self._payload[self._idx['is_active']], _('active'), _('inactive')),
     357                          gmTools.bool2subst(self._payload[self._idx['clinically_relevant']], _('clinically relevant'), _('not clinically relevant')),
     358                          gmTools.coalesce (
     359                                  initial = diagnostic_certainty_classification2str(self._payload[self._idx['diagnostic_certainty_classification']]),
     360                                  instead = '',
     361                                  template_initial = ', %s',
     362                                  none_equivalents = [None, '']
     363                          )
     364                  ))
     365
     366                  if with_summary:
     367                          if self._payload[self._idx['summary']] is not None:
     368                                  lines.append(' %s:' % _('Synopsis'))
     369                                  lines.append(gmTools.wrap (
     370                                          text = self._payload[self._idx['summary']],
     371                                          width = 60,
     372                                          initial_indent = '  ',
     373                                          subsequent_indent = '  '
     374                                  ))
     375
     376                  # codes ?
     377                  if with_codes:
     378                          codes = self.generic_codes
     379                          if len(codes) > 0:
     380                                  lines.append('')
     381                          for c in codes:
     382                                  lines.append(' %s: %s (%s - %s)' % (
     383                                          c['code'],
     384                                          c['term'],
     385                                          c['name_short'],
     386                                          c['version']
     387                                  ))
     388                          del codes
     389
     390                  lines.append('')
     391
     392                  # patient/emr dependant
     393                  if patient is not None:
     394                          if patient.ID != self._payload[self._idx['pk_patient']]:
     395                                  msg = '<patient>.ID = %s but health issue %s belongs to patient %s' % (
     396                                          patient.ID,
     397                                          self._payload[self._idx['pk_health_issue']],
     398                                          self._payload[self._idx['pk_patient']]
     399                                  )
     400                                  raise ValueError(msg)
     401                          emr = patient.emr
     402
     403                          # episodes
     404                          if with_episodes:
     405                                  epis = self.get_episodes()
     406                                  if epis is None:
     407                                          lines.append(_('Error retrieving episodes for this health issue.'))
     408                                  elif len(epis) == 0:
     409                                          lines.append(_('There are no episodes for this health issue.'))
     410                                  else:
     411                                          lines.append (
     412                                                  _('Episodes: %s (most recent: %s%s%s)') % (
     413                                                          len(epis),
     414                                                          gmTools.u_left_double_angle_quote,
     415                                                          emr.get_most_recent_episode(issue = self._payload[self._idx['pk_health_issue']])['description'],
     416                                                          gmTools.u_right_double_angle_quote
     417                                                  )
     418                                          )
     419                                          for epi in epis:
     420                                                  lines.append(' \u00BB%s\u00AB (%s)' % (
     421                                                          epi['description'],
     422                                                          gmTools.bool2subst(epi['episode_open'], _('ongoing'), _('closed'))
     423                                                  ))
     424                                  lines.append('')
     425
     426                          # encounters
     427                          if with_encounters:
     428                                  first_encounter = emr.get_first_encounter(issue_id = self._payload[self._idx['pk_health_issue']])
     429                                  last_encounter = emr.get_last_encounter(issue_id = self._payload[self._idx['pk_health_issue']])
     430
     431                                  if first_encounter is None or last_encounter is None:
     432                                          lines.append(_('No encounters found for this health issue.'))
     433                                  else:
     434                                          encs = emr.get_encounters(issues = [self._payload[self._idx['pk_health_issue']]])
     435                                          lines.append(_('Encounters: %s (%s - %s):') % (
     436                                                  len(encs),
     437                                                  first_encounter['started_original_tz'].strftime('%m/%Y'),
     438                                                  last_encounter['last_affirmed_original_tz'].strftime('%m/%Y')
     439                                          ))
     440                                          lines.append(_(' Most recent: %s - %s') % (
     441                                                  last_encounter['started_original_tz'].strftime('%Y-%m-%d %H:%M'),
     442                                                  last_encounter['last_affirmed_original_tz'].strftime('%H:%M')
     443                                          ))
     444
     445                          # medications
     446                          if with_medications:
     447                                  meds = emr.get_current_medications (
     448                                          issues = [ self._payload[self._idx['pk_health_issue']] ],
     449                                          order_by = 'is_currently_active DESC, started, substance'
     450                                  )
     451                                  if len(meds) > 0:
     452                                          lines.append('')
     453                                          lines.append(_('Medications and Substances'))
     454                                  for m in meds:
     455                                          lines.append(m.format(left_margin = (left_margin + 1)))
     456                                  del meds
     457
     458                          # hospitalizations
     459                          if with_hospital_stays:
     460                                  stays = emr.get_hospital_stays (
     461                                          issues = [ self._payload[self._idx['pk_health_issue']] ]
     462                                  )
     463                                  if len(stays) > 0:
     464                                          lines.append('')
     465                                          lines.append(_('Hospitalizations: %s') % len(stays))
     466                                  for s in stays:
     467                                          lines.append(s.format(left_margin = (left_margin + 1)))
     468                                  del stays
     469
     470                          # procedures
     471                          if with_procedures:
     472                                  procs = emr.get_performed_procedures (
     473                                          issues = [ self._payload[self._idx['pk_health_issue']] ]
     474                                  )
     475                                  if len(procs) > 0:
     476                                          lines.append('')
     477                                          lines.append(_('Procedures performed: %s') % len(procs))
     478                                  for p in procs:
     479                                          lines.append(p.format(left_margin = (left_margin + 1)))
     480                                  del procs
     481
     482                          # family history
     483                          if with_family_history:
     484                                  fhx = emr.get_family_history(issues = [ self._payload[self._idx['pk_health_issue']] ])
     485                                  if len(fhx) > 0:
     486                                          lines.append('')
     487                                          lines.append(_('Family History: %s') % len(fhx))
     488                                  for f in fhx:
     489                                          lines.append(f.format (
     490                                                  left_margin = (left_margin + 1),
     491                                                  include_episode = True,
     492                                                  include_comment = True,
     493                                                  include_codes = False
     494                                          ))
     495                                  del fhx
     496
     497                          epis = self.get_episodes()
     498                          if len(epis) > 0:
     499                                  epi_pks = [ e['pk_episode'] for e in epis ]
     500
     501                                  # documents
     502                                  if with_documents:
     503                                          doc_folder = patient.get_document_folder()
     504                                          docs = doc_folder.get_documents(pk_episodes = epi_pks)
     505                                          if len(docs) > 0:
     506                                                  lines.append('')
     507                                                  lines.append(_('Documents: %s') % len(docs))
     508                                          del docs
     509
     510                                  # test results
     511                                  if with_tests:
     512                                          tests = emr.get_test_results_by_date(episodes = epi_pks)
     513                                          if len(tests) > 0:
     514                                                  lines.append('')
     515                                                  lines.append(_('Measurements and Results: %s') % len(tests))
     516                                          del tests
     517
     518                                  # vaccinations
     519                                  if with_vaccinations:
     520                                          vaccs = emr.get_vaccinations(episodes = epi_pks, order_by = 'date_given, vaccine')
     521                                          if len(vaccs) > 0:
     522                                                  lines.append('')
     523                                                  lines.append(_('Vaccinations:'))
     524                                          for vacc in vaccs:
     525                                                  lines.extend(vacc.format(with_reaction = True))
     526                                          del vaccs
     527
     528                          del epis
     529
     530                  if with_external_care:
     531                          care = self._get_external_care(order_by = 'organization, unit, provider')
     532                          if len(care) > 0:
     533                                  lines.append('')
     534                                  lines.append(_('External care:'))
     535                          for item in care:
     536                                  lines.append(' %s%s@%s%s' % (
     537                                          gmTools.coalesce(item['provider'], '', '%s: '),
     538                                          item['unit'],
     539                                          item['organization'],
     540                                          gmTools.coalesce(item['comment'], '', ' (%s)')
     541                                  ))
     542
     543                  left_margin = ' ' * left_margin
     544                  eol_w_margin = '\n%s' % left_margin
     545                  lines = gmTools.strip_trailing_empty_lines(lines = lines, eol = '\n')
     546                  return left_margin + eol_w_margin.join(lines) + '\n'
     547          #--------------------------------------------------------
     548          # properties
     549          #--------------------------------------------------------
     550 -        def _get_external_care(self, order_by=None):
     551                  return gmExternalCare.get_external_care_items(pk_health_issue = self.pk_obj, order_by = order_by)
     552
     553          external_care = property(_get_external_care, lambda x:x)
     554
     555          #--------------------------------------------------------
     556          episodes = property(get_episodes, lambda x:x)
     557
     558          open_episode = property(get_open_episode, lambda x:x)
     559
     560          has_open_episode = property(has_open_episode, lambda x:x)
     561
     562          #--------------------------------------------------------
     563 -        def _get_first_episode(self):
     564
     565                  args = {'pk_issue': self.pk_obj}
     566
     567                  cmd = """SELECT
     568                          earliest, pk_episode
     569                  FROM (
     570                                  -- .modified_when of all episodes of this issue,
     571                                  -- earliest-possible thereof = when created,
     572                                  -- should actually go all the way back into audit.log_episode
     573                                  (SELECT
     574                                          c_epi.modified_when AS earliest,
     575                                          c_epi.pk AS pk_episode
     576                                   FROM clin.episode c_epi
     577                                   WHERE c_epi.fk_health_issue = %(pk_issue)s
     578                                  )
     579                          UNION ALL
     580
     581                                  -- last modification of encounter in which episodes of this issue were created,
     582                                  -- earliest-possible thereof = initial creation of that encounter
     583                                  (SELECT
     584                                          c_enc.modified_when AS earliest,
     585                                          c_epi.pk AS pk_episode
     586                                   FROM
     587                                          clin.episode c_epi
     588                                                  INNER JOIN clin.encounter c_enc ON (c_enc.pk = c_epi.fk_encounter)
     589                                                  INNER JOIN clin.health_issue c_hi ON (c_hi.pk = c_epi.fk_health_issue)
     590                                   WHERE c_hi.pk = %(pk_issue)s
     591                                  )
     592                          UNION ALL
     593
     594                                  -- start of encounter in which episodes of this issue were created,
     595                                  -- earliest-possible thereof = set by user
     596                                  (SELECT
     597                                          c_enc.started AS earliest,
     598                                          c_epi.pk AS pk_episode
     599                                   FROM
     600                                          clin.episode c_epi
     601                                                  INNER JOIN clin.encounter c_enc ON (c_enc.pk = c_epi.fk_encounter)
     602                                                  INNER JOIN clin.health_issue c_hi ON (c_hi.pk = c_epi.fk_health_issue)
     603                                   WHERE c_hi.pk = %(pk_issue)s
     604                                  )
     605                          UNION ALL
     606
     607                                  -- start of encounters of clinical items linked to episodes of this issue,
     608                                  -- earliest-possible thereof = explicitely set by user
     609                                  (SELECT
     610                                          c_enc.started AS earliest,
     611                                          c_epi.pk AS pk_episode
     612                                   FROM
     613                                          clin.clin_root_item c_cri
     614                                                  INNER JOIN clin.encounter c_enc ON (c_cri.fk_encounter = c_enc.pk)
     615                                                  INNER JOIN clin.episode c_epi ON (c_cri.fk_episode = c_epi.pk)
     616                                                          INNER JOIN clin.health_issue c_hi ON (c_epi.fk_health_issue = c_hi.pk)
     617                                   WHERE c_hi.pk = %(pk_issue)s
     618                                  )
     619                          UNION ALL
     620
     621                                  -- .clin_when of clinical items linked to episodes of this issue,
     622                                  -- earliest-possible thereof = explicitely set by user
     623                                  (SELECT
     624                                          c_cri.clin_when AS earliest,
     625                                          c_epi.pk AS pk_episode
     626                                   FROM
     627                                          clin.clin_root_item c_cri
     628                                                  INNER JOIN clin.episode c_epi ON (c_cri.fk_episode = c_epi.pk)
     629                                                          INNER JOIN clin.health_issue c_hi ON (c_epi.fk_health_issue = c_hi.pk)
     630                                   WHERE c_hi.pk = %(pk_issue)s
     631                                  )
     632                          UNION ALL
     633
     634                                  -- earliest modification time of clinical items linked to episodes of this issue
     635                                  -- this CAN be used since if an item is linked to an episode it can be
     636                                  -- assumed the episode (should have) existed at the time of creation
     637                                  (SELECT
     638                                          c_cri.modified_when AS earliest,
     639                                          c_epi.pk AS pk_episode
     640                                   FROM
     641                                          clin.clin_root_item c_cri
     642                                                  INNER JOIN clin.episode c_epi ON (c_cri.fk_episode = c_epi.pk)
     643                                                          INNER JOIN clin.health_issue c_hi ON (c_epi.fk_health_issue = c_hi.pk)
     644                                   WHERE c_hi.pk = %(pk_issue)s
     645                                  )
     646                          UNION ALL
     647
     648                                  -- there may not be items, but there may still be documents ...
     649                                  (SELECT
     650                                          b_dm.clin_when AS earliest,
     651                                          c_epi.pk AS pk_episode
     652                                   FROM
     653                                          blobs.doc_med b_dm
     654                                                  INNER JOIN clin.episode c_epi ON (b_dm.fk_episode = c_epi.pk)
     655                                                          INNER JOIN clin.health_issue c_hi ON (c_epi.fk_health_issue = c_hi.pk)
     656                                    WHERE c_hi.pk = %(pk_issue)s
     657                                  )
     658                  ) AS candidates
     659                  ORDER BY earliest NULLS LAST
     660                  LIMIT 1"""
     661                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     662                  if len(rows) == 0:
     663                          return None
     664                  return cEpisode(aPK_obj = rows[0]['pk_episode'])
     665
     666          first_episode = property(_get_first_episode, lambda x:x)
     667
     668          #--------------------------------------------------------
     669 -        def _get_latest_episode(self):
     670
     671                  # explicit always wins:
     672                  if self._payload[self._idx['has_open_episode']]:
     673                          return self.open_episode
     674
     675                  args = {'pk_issue': self.pk_obj}
     676
     677                  # cheap query first: any episodes at all ?
     678                  cmd = "SELECT 1 FROM clin.episode WHERE fk_health_issue = %(pk_issue)s"
     679                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     680                  if len(rows) == 0:
     681                          return None
     682
     683                  cmd = """SELECT
     684                          latest, pk_episode
     685                  FROM (
     686                                  -- .clin_when of clinical items linked to episodes of this issue,
     687                                  -- latest-possible thereof = explicitely set by user
     688                                  (SELECT
     689                                          c_cri.clin_when AS latest,
     690                                          c_epi.pk AS pk_episode,
     691                                          1 AS rank
     692                                   FROM
     693                                          clin.clin_root_item c_cri
     694                                                  INNER JOIN clin.episode c_epi ON (c_cri.fk_episode = c_epi.pk)
     695                                                          INNER JOIN clin.health_issue c_hi ON (c_epi.fk_health_issue = c_hi.pk)
     696                                   WHERE c_hi.pk = %(pk_issue)s
     697                                  )
     698                          UNION ALL
     699
     700                                  -- .clin_when of documents linked to episodes of this issue
     701                                  (SELECT
     702                                          b_dm.clin_when AS latest,
     703                                          c_epi.pk AS pk_episode,
     704                                          1 AS rank
     705                                   FROM
     706                                          blobs.doc_med b_dm
     707                                                  INNER JOIN clin.episode c_epi ON (b_dm.fk_episode = c_epi.pk)
     708                                                          INNER JOIN clin.health_issue c_hi ON (c_epi.fk_health_issue = c_hi.pk)
     709                                   WHERE c_hi.pk = %(pk_issue)s
     710                                  )
     711                          UNION ALL
     712
     713                                  -- last_affirmed of encounter in which episodes of this issue were created,
     714                                  -- earliest-possible thereof = set by user
     715                                  (SELECT
     716                                          c_enc.last_affirmed AS latest,
     717                                          c_epi.pk AS pk_episode,
     718                                          2 AS rank
     719                                   FROM
     720                                          clin.episode c_epi
     721                                                  INNER JOIN clin.encounter c_enc ON (c_enc.pk = c_epi.fk_encounter)
     722                                                  INNER JOIN clin.health_issue c_hi ON (c_hi.pk = c_epi.fk_health_issue)
     723                                   WHERE c_hi.pk = %(pk_issue)s
     724                                  )
     725
     726                  ) AS candidates
     727                  WHERE
     728                          -- weed out NULL rows due to episodes w/o clinical items and w/o documents
     729                          latest IS NOT NULL
     730                  ORDER BY
     731                          rank,
     732                          latest DESC
     733                  LIMIT 1
     734                  """
     735                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     736                  if len(rows) == 0:
     737                          # there were no episodes for this issue
     738                          return None
     739                  return cEpisode(aPK_obj = rows[0]['pk_episode'])
     740
     741          latest_episode = property(_get_latest_episode, lambda x:x)
     742
     743          #--------------------------------------------------------
     744          # Steffi suggested we divide into safe and assumed (= possible) start dates
     745 -        def _get_safe_start_date(self):
     746                  """This returns the date when we can assume to safely KNOW
     747                     the health issue existed (because the provider said so)."""
     748
     749                  args = {
     750                          'enc': self._payload[self._idx['pk_encounter']],
     751                          'pk': self._payload[self._idx['pk_health_issue']]
     752                  }
     753                  cmd = """SELECT COALESCE (
     754                          -- this one must override all:
     755                          -- .age_noted if not null and DOB is known
     756                          (CASE
     757                                  WHEN c_hi.age_noted IS NULL
     758                                  THEN NULL::timestamp with time zone
     759                                  WHEN
     760                                          (SELECT d_i.dob FROM dem.identity d_i WHERE d_i.pk = (
     761                                                  SELECT c_enc.fk_patient FROM clin.encounter c_enc WHERE c_enc.pk = %(enc)s
     762                                          )) IS NULL
     763                                  THEN NULL::timestamp with time zone
     764                                  ELSE
     765                                          c_hi.age_noted + (
     766                                                  SELECT d_i.dob FROM dem.identity d_i WHERE d_i.pk = (
     767                                                          SELECT c_enc.fk_patient FROM clin.encounter c_enc WHERE c_enc.pk = %(enc)s
     768                                                  )
     769                                          )
     770                          END),
     771
     772                          -- look at best_guess_clinical_start_date of all linked episodes
     773
     774                          -- start of encounter in which created, earliest = explicitely set
     775                          (SELECT c_enc.started AS earliest FROM clin.encounter c_enc WHERE c_enc.pk = c_hi.fk_encounter)
     776                  )
     777                  FROM clin.health_issue c_hi
     778                  WHERE c_hi.pk = %(pk)s"""
     779                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
     780                  return rows[0][0]
     781
     782          safe_start_date = property(_get_safe_start_date, lambda x:x)
     783
     784          #--------------------------------------------------------
     785 -        def _get_possible_start_date(self):
     786                  args = {'pk': self._payload[self._idx['pk_health_issue']]}
     787                  cmd = """
     788  SELECT MIN(earliest) FROM (
     789          -- last modification, earliest = when created in/changed to the current state
     790          (SELECT modified_when AS earliest FROM clin.health_issue WHERE pk = %(pk)s)
     791
     792  UNION ALL
     793          -- last modification of encounter in which created, earliest = initial creation of that encounter
     794          (SELECT c_enc.modified_when AS earliest FROM clin.encounter c_enc WHERE c_enc.pk = (
     795                  SELECT c_hi.fk_encounter FROM clin.health_issue c_hi WHERE c_hi.pk = %(pk)s
     796          ))
     797
     798  UNION ALL
     799          -- earliest explicit .clin_when of clinical items linked to this health_issue
     800          (SELECT MIN(c_vpi.clin_when) AS earliest FROM clin.v_pat_items c_vpi WHERE c_vpi.pk_health_issue = %(pk)s)
     801
     802  UNION ALL
     803          -- earliest modification time of clinical items linked to this health issue
     804          -- this CAN be used since if an item is linked to a health issue it can be
     805          -- assumed the health issue (should have) existed at the time of creation
     806          (SELECT MIN(c_vpi.modified_when) AS earliest FROM clin.v_pat_items c_vpi WHERE c_vpi.pk_health_issue = %(pk)s)
     807
     808  UNION ALL
     809          -- earliest start of encounters of clinical items linked to this episode
     810          (SELECT MIN(c_enc.started) AS earliest FROM clin.encounter c_enc WHERE c_enc.pk IN (
     811                  SELECT c_vpi.pk_encounter FROM clin.v_pat_items c_vpi WHERE c_vpi.pk_health_issue = %(pk)s
     812          ))
     813
     814  -- here we should be looking at
     815  -- .best_guess_clinical_start_date of all episodes linked to this encounter
     816
     817  ) AS candidates"""
     818
     819                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
     820                  return rows[0][0]
     821
     822          possible_start_date = property(_get_possible_start_date)
     823
     824          #--------------------------------------------------------
     825 -        def _get_clinical_end_date(self):
     826                  if self._payload[self._idx['is_active']]:
     827                          return None
     828                  if self._payload[self._idx['has_open_episode']]:
     829                          return None
     830                  latest_episode = self.latest_episode
     831                  if latest_episode is not None:
     832                          return latest_episode.best_guess_clinical_end_date
     833                  # apparently, there are no episodes for this issue
     834                  # and the issue is not active either
     835                  # so, we simply do not know, the safest assumption is:
     836                  return self.safe_start_date
     837
     838          clinical_end_date = property(_get_clinical_end_date)
     839
     840          #--------------------------------------------------------
     841 -        def _get_latest_access_date(self):
     842                  args = {
     843                          'enc': self._payload[self._idx['pk_encounter']],
     844                          'pk': self._payload[self._idx['pk_health_issue']]
     845                  }
     846                  cmd = """
     847  SELECT
     848          MAX(latest)
     849  FROM (
     850          -- last modification, latest = when last changed to the current state
     851          -- DO NOT USE: database upgrades may change this field
     852          (SELECT modified_when AS latest FROM clin.health_issue WHERE pk = %(pk)s)
     853
     854          --UNION ALL
     855          -- last modification of encounter in which created, latest = initial creation of that encounter
     856          -- DO NOT USE: just because one corrects a typo does not mean the issue took any longer
     857          --(SELECT c_enc.modified_when AS latest FROM clin.encounter c_enc WHERE c_enc.pk = (
     858          --      SELECT fk_encounter FROM clin.episode WHERE pk = %(pk)s
     859          -- )
     860          --)
     861
     862          --UNION ALL
     863          -- end of encounter in which created, latest = explicitely set
     864          -- DO NOT USE: we can retrospectively create issues which
     865          -- DO NOT USE: are long since finished
     866          --(SELECT c_enc.last_affirmed AS latest FROM clin.encounter c_enc WHERE c_enc.pk = (
     867          --      SELECT fk_encounter FROM clin.episode WHERE pk = %(pk)s
     868          -- )
     869          --)
     870
     871          UNION ALL
     872          -- latest end of encounters of clinical items linked to this issue
     873          (SELECT
     874                  MAX(last_affirmed) AS latest
     875           FROM clin.encounter
     876           WHERE pk IN (
     877                  SELECT pk_encounter FROM clin.v_pat_items WHERE pk_health_issue = %(pk)s
     878           )
     879          )
     880
     881          UNION ALL
     882          -- latest explicit .clin_when of clinical items linked to this issue
     883          (SELECT
     884                  MAX(clin_when) AS latest
     885           FROM clin.v_pat_items
     886           WHERE pk_health_issue = %(pk)s
     887          )
     888
     889          -- latest modification time of clinical items linked to this issue
     890          -- this CAN be used since if an item is linked to an issue it can be
     891          -- assumed the issue (should have) existed at the time of modification
     892          -- DO NOT USE, because typo fixes should not extend the issue
     893          --(SELECT MIN(modified_when) AS latest FROM clin.clin_root_item WHERE fk_episode = %(pk)s)
     894
     895  ) AS candidates"""
     896                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}])
     897                  return rows[0][0]
     898
     899          latest_access_date = property(_get_latest_access_date)
     900
     901          #--------------------------------------------------------
     902 -        def _get_laterality_description(self):
     903                  try:
     904                          return laterality2str[self._payload[self._idx['laterality']]]
     905                  except KeyError:
     906                          return '<?>'
     907
     908          laterality_description = property(_get_laterality_description, lambda x:x)
     909
     910          #--------------------------------------------------------
     911 -        def _get_diagnostic_certainty_description(self):
     912                  return diagnostic_certainty_classification2str(self._payload[self._idx['diagnostic_certainty_classification']])
     913
     914          diagnostic_certainty_description = property(_get_diagnostic_certainty_description, lambda x:x)
     915
     916          #--------------------------------------------------------
     917 -        def _get_formatted_revision_history(self):
     918                  cmd = """SELECT
     919                                  'NONE (live row)'::text as audit__action_applied,
     920                                  NULL AS audit__action_when,
     921                                  NULL AS audit__action_by,
     922                                  pk_audit,
     923                                  row_version,
     924                                  modified_when,
     925                                  modified_by,
     926                                  pk,
     927                                  description,
     928                                  laterality,
     929                                  age_noted,
     930                                  is_active,
     931                                  clinically_relevant,
     932                                  is_confidential,
     933                                  is_cause_of_death,
     934                                  fk_encounter,
     935                                  grouping,
     936                                  diagnostic_certainty_classification,
     937                                  summary
     938                          FROM clin.health_issue
     939                          WHERE pk = %(pk_health_issue)s
     940                  UNION ALL (
     941                          SELECT
     942                                  audit_action as audit__action_applied,
     943                                  audit_when as audit__action_when,
     944                                  audit_by as audit__action_by,
     945                                  pk_audit,
     946                                  orig_version as row_version,
     947                                  orig_when as modified_when,
     948                                  orig_by as modified_by,
     949                                  pk,
     950                                  description,
     951                                  laterality,
     952                                  age_noted,
     953                                  is_active,
     954                                  clinically_relevant,
     955                                  is_confidential,
     956                                  is_cause_of_death,
     957                                  fk_encounter,
     958                                  grouping,
     959                                  diagnostic_certainty_classification,
     960                                  summary
     961                          FROM audit.log_health_issue
     962                          WHERE pk = %(pk_health_issue)s
     963                  )
     964                  ORDER BY row_version DESC
     965                  """
     966                  args = {'pk_health_issue': self.pk_obj}
     967                  title = _('Health issue: %s%s%s') % (
     968                          gmTools.u_left_double_angle_quote,
     969                          self._payload[self._idx['description']],
     970                          gmTools.u_right_double_angle_quote
     971                  )
     972                  return '\n'.join(self._get_revision_history(cmd, args, title))
     973
     974          formatted_revision_history = property(_get_formatted_revision_history, lambda x:x)
     975          #--------------------------------------------------------
     976 -        def _get_generic_codes(self):
     977                  if len(self._payload[self._idx['pk_generic_codes']]) == 0:
     978                          return []
     979
     980                  cmd = gmCoding._SQL_get_generic_linked_codes % 'pk_generic_code IN %(pks)s'
     981                  args = {'pks': tuple(self._payload[self._idx['pk_generic_codes']])}
     982                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     983                  return [ gmCoding.cGenericLinkedCode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_lnk_code2item'}) for r in rows ]
     984
     985 -        def _set_generic_codes(self, pk_codes):
     986                  queries = []
     987                  # remove all codes
     988                  if len(self._payload[self._idx['pk_generic_codes']]) > 0:
     989                          queries.append ({
     990                                  'cmd': 'DELETE FROM clin.lnk_code2h_issue WHERE fk_item = %(issue)s AND fk_generic_code IN %(codes)s',
     991                                  'args': {
     992                                          'issue': self._payload[self._idx['pk_health_issue']],
     993                                          'codes': tuple(self._payload[self._idx['pk_generic_codes']])
     994                                  }
     995                          })
     996                  # add new codes
     997                  for pk_code in pk_codes:
     998                          queries.append ({
     999                                  'cmd': 'INSERT INTO clin.lnk_code2h_issue (fk_item, fk_generic_code) VALUES (%(issue)s, %(pk_code)s)',
    1000                                  'args': {
    1001                                          'issue': self._payload[self._idx['pk_health_issue']],
    1002                                          'pk_code': pk_code
    1003                                  }
    1004                          })
    1005                  if len(queries) == 0:
    1006                          return
    1007                  # run it all in one transaction
    1008                  rows, idx = gmPG2.run_rw_queries(queries = queries)
    1009                  return
    1010
    1011          generic_codes = property(_get_generic_codes, _set_generic_codes)
    1012
    1013  #============================================================
    1014 -def create_health_issue(description=None, encounter=None, patient=None):
    1015          """Creates a new health issue for a given patient.
    1016
    1017          description - health issue name
    1018          """
    1019          try:
    1020                  h_issue = cHealthIssue(name = description, encounter = encounter, patient = patient)
    1021                  return h_issue
    1022          except gmExceptions.NoSuchBusinessObjectError:
    1023                  pass
    1024
    1025          queries = []
    1026          cmd = "insert into clin.health_issue (description, fk_encounter) values (%(desc)s, %(enc)s)"
    1027          queries.append({'cmd': cmd, 'args': {'desc': description, 'enc': encounter}})
    1028
    1029          cmd = "select currval('clin.health_issue_pk_seq')"
    1030          queries.append({'cmd': cmd})
    1031
    1032          rows, idx = gmPG2.run_rw_queries(queries = queries, return_data = True)
    1033          h_issue = cHealthIssue(aPK_obj = rows[0][0])
    1034
    1035          return h_issue
    1036
    1037  #-----------------------------------------------------------
    1038 -def delete_health_issue(health_issue=None):
    1039          if isinstance(health_issue, cHealthIssue):
    1040                  args = {'pk': health_issue['pk_health_issue']}
    1041          else:
    1042                  args = {'pk': int(health_issue)}
    1043          try:
    1044                  gmPG2.run_rw_queries(queries = [{'cmd': 'DELETE FROM clin.health_issue WHERE pk = %(pk)s', 'args': args}])
    1045          except gmPG2.dbapi.IntegrityError:
    1046                  # should be parsing pgcode/and or error message
    1047                  _log.exception('cannot delete health issue')
    1048                  return False
    1049
    1050          return True
    1051
    1052  #------------------------------------------------------------
    1053  # use as dummy for unassociated episodes
    1054 -def get_dummy_health_issue():
    1055          issue = {
    1056                  'pk_health_issue': None,
    1057                  'description': _('Unattributed episodes'),
    1058                  'age_noted': None,
    1059                  'laterality': 'na',
    1060                  'is_active': True,
    1061                  'clinically_relevant': True,
    1062                  'is_confidential': None,
    1063                  'is_cause_of_death': False,
    1064                  'is_dummy': True,
    1065                  'grouping': None
    1066          }
    1067          return issue
    1068
    1069  #-----------------------------------------------------------
    1070 -def health_issue2problem(health_issue=None, allow_irrelevant=False):
    1071          return cProblem (
    1072                  aPK_obj = {
    1073                          'pk_patient': health_issue['pk_patient'],
    1074                          'pk_health_issue': health_issue['pk_health_issue'],
    1075                          'pk_episode': None
    1076                  },
    1077                  try_potential_problems = allow_irrelevant
    1078          )
    1079
    1080  #============================================================
    1081  # episodes API
    1082  #============================================================
    1083 -class cEpisode(gmBusinessDBObject.cBusinessDBObject):
    1084          """Represents one clinical episode.
    1085          """
    1086          _cmd_fetch_payload = "select * from clin.v_pat_episodes where pk_episode=%s"
    1087          _cmds_store_payload = [
    1088                  """update clin.episode set
    1089                                  fk_health_issue = %(pk_health_issue)s,
    1090                                  is_open = %(episode_open)s::boolean,
    1091                                  description = %(description)s,
    1092                                  summary = gm.nullify_empty_string(%(summary)s),
    1093                                  diagnostic_certainty_classification = gm.nullify_empty_string(%(diagnostic_certainty_classification)s)
    1094                          where
    1095                                  pk = %(pk_episode)s and
    1096                                  xmin = %(xmin_episode)s""",
    1097                  """select xmin_episode from clin.v_pat_episodes where pk_episode = %(pk_episode)s"""
    1098          ]
    1099          _updatable_fields = [
    1100                  'pk_health_issue',
    1101                  'episode_open',
    1102                  'description',
    1103                  'summary',
    1104                  'diagnostic_certainty_classification'
    1105          ]
    1106          #--------------------------------------------------------
    1107 -        def __init__(self, aPK_obj=None, id_patient=None, name='xxxDEFAULTxxx', health_issue=None, row=None, encounter=None, link_obj=None):
    1108                  pk = aPK_obj
    1109                  if pk is None and row is None:
    1110
    1111                          where_parts = ['description = %(desc)s']
    1112
    1113                          if id_patient is not None:
    1114                                  where_parts.append('pk_patient = %(pat)s')
    1115
    1116                          if health_issue is not None:
    1117                                  where_parts.append('pk_health_issue = %(issue)s')
    1118
    1119                          if encounter is not None:
    1120                                  where_parts.append('pk_patient = (SELECT fk_patient FROM clin.encounter WHERE pk = %(enc)s)')
    1121
    1122                          args = {
    1123                                  'pat': id_patient,
    1124                                  'issue': health_issue,
    1125                                  'enc': encounter,
    1126                                  'desc': name
    1127                          }
    1128
    1129                          cmd = 'SELECT * FROM clin.v_pat_episodes WHERE %s' % ' AND '.join(where_parts)
    1130
    1131                          rows, idx = gmPG2.run_ro_queries (
    1132                                  link_obj = link_obj,
    1133                                  queries = [{'cmd': cmd, 'args': args}],
    1134                                  get_col_idx=True
    1135                          )
    1136
    1137                          if len(rows) == 0:
    1138                                  raise gmExceptions.NoSuchBusinessObjectError('no episode for [%s:%s:%s:%s]' % (id_patient, name, health_issue, encounter))
    1139
    1140                          r = {'idx': idx, 'data': rows[0], 'pk_field': 'pk_episode'}
    1141                          gmBusinessDBObject.cBusinessDBObject.__init__(self, row=r)
    1142
    1143                  else:
    1144                          gmBusinessDBObject.cBusinessDBObject.__init__(self, aPK_obj=pk, row=row, link_obj = link_obj)
    1145
    1146          #--------------------------------------------------------
    1147          # external API
    1148          #--------------------------------------------------------
    1149 -        def get_patient(self):
    1150                  return self._payload[self._idx['pk_patient']]
    1151
    1152          #--------------------------------------------------------
    1153 -        def get_narrative(self, soap_cats=None, encounters=None, order_by = None):
    1154                  return gmClinNarrative.get_narrative (
    1155                          soap_cats = soap_cats,
    1156                          encounters = encounters,
    1157                          episodes = [self.pk_obj],
    1158                          order_by = order_by
    1159                  )
    1160
    1161          #--------------------------------------------------------
    1162 -        def rename(self, description=None):
    1163                  """Method for episode editing, that is, episode renaming.
    1164
    1165                  @param description
    1166                          - the new descriptive name for the encounter
    1167                  @type description
    1168                          - a string instance
    1169                  """
    1170                  # sanity check
    1171                  if description.strip() == '':
    1172                          _log.error('<description> must be a non-empty string instance')
    1173                          return False
    1174                  # update the episode description
    1175                  old_description = self._payload[self._idx['description']]
    1176                  self._payload[self._idx['description']] = description.strip()
    1177                  self._is_modified = True
    1178                  successful, data = self.save_payload()
    1179                  if not successful:
    1180                          _log.error('cannot rename episode [%s] to [%s]' % (self, description))
    1181                          self._payload[self._idx['description']] = old_description
    1182                          return False
    1183                  return True
    1184
    1185          #--------------------------------------------------------
    1186 -        def add_code(self, pk_code=None):
    1187                  """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
    1188
    1189                  if pk_code in self._payload[self._idx['pk_generic_codes']]:
    1190                          return
    1191
    1192                  cmd = """
    1193                          INSERT INTO clin.lnk_code2episode
    1194                                  (fk_item, fk_generic_code)
    1195                          SELECT
    1196                                  %(item)s,
    1197                                  %(code)s
    1198                          WHERE NOT EXISTS (
    1199                                  SELECT 1 FROM clin.lnk_code2episode
    1200                                  WHERE
    1201                                          fk_item = %(item)s
    1202                                                  AND
    1203                                          fk_generic_code = %(code)s
    1204                          )"""
    1205                  args = {
    1206                          'item': self._payload[self._idx['pk_episode']],
    1207                          'code': pk_code
    1208                  }
    1209                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    1210                  return
    1211
    1212          #--------------------------------------------------------
    1213 -        def remove_code(self, pk_code=None):
    1214                  """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
    1215                  cmd = "DELETE FROM clin.lnk_code2episode WHERE fk_item = %(item)s AND fk_generic_code = %(code)s"
    1216                  args = {
    1217                          'item': self._payload[self._idx['pk_episode']],
    1218                          'code': pk_code
    1219                  }
    1220                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    1221                  return True
    1222
    1223          #--------------------------------------------------------
    1224 -        def format_as_journal(self, left_margin=0, date_format='%Y %b %d, %a'):
    1225                  rows = gmClinNarrative.get_as_journal (
    1226                          episodes = (self.pk_obj,),
    1227                          order_by = 'pk_encounter, clin_when, scr, src_table'
    1228                          #order_by = u'pk_encounter, scr, clin_when, src_table'
    1229                  )
    1230
    1231                  if len(rows) == 0:
    1232                          return ''
    1233
    1234                  lines = []
    1235
    1236                  lines.append(_('Clinical data generated during encounters within this episode:'))
    1237
    1238                  left_margin = ' ' * left_margin
    1239
    1240                  prev_enc = None
    1241                  for row in rows:
    1242                          if row['pk_encounter'] != prev_enc:
    1243                                  lines.append('')
    1244                                  prev_enc = row['pk_encounter']
    1245
    1246                          when = row['clin_when'].strftime(date_format)
    1247                          top_row = '%s%s %s (%s) %s' % (
    1248                                  gmTools.u_box_top_left_arc,
    1249                                  gmTools.u_box_horiz_single,
    1250                                  gmSoapDefs.soap_cat2l10n_str[row['real_soap_cat']],
    1251                                  when,
    1252                                  gmTools.u_box_horiz_single * 5
    1253                          )
    1254                          soap = gmTools.wrap (
    1255                                  text = row['narrative'],
    1256                                  width = 60,
    1257                                  initial_indent = '  ',
    1258                                  subsequent_indent = '  ' + left_margin
    1259                          )
    1260                          row_ver = ''
    1261                          if row['row_version'] > 0:
    1262                                  row_ver = 'v%s: ' % row['row_version']
    1263                          bottom_row = '%s%s %s, %s%s %s' % (
    1264                                  ' ' * 40,
    1265                                  gmTools.u_box_horiz_light_heavy,
    1266                                  row['modified_by'],
    1267                                  row_ver,
    1268                                  gmDateTime.pydt_strftime(row['modified_when'], date_format),
    1269                                  gmTools.u_box_horiz_heavy_light
    1270                          )
    1271
    1272                          lines.append(top_row)
    1273                          lines.append(soap)
    1274                          lines.append(bottom_row)
    1275
    1276                  eol_w_margin = '\n%s' % left_margin
    1277                  return left_margin + eol_w_margin.join(lines) + '\n'
    1278
    1279          #--------------------------------------------------------
    1280 -        def format_maximum_information(self, patient=None):
    1281                  if patient is None:
    1282                          from Gnumed.business.gmPerson import gmCurrentPatient, cPerson
    1283                          if self._payload[self._idx['pk_patient']] == gmCurrentPatient().ID:
    1284                                  patient = gmCurrentPatient()
    1285                          else:
    1286                                  patient = cPerson(self._payload[self._idx['pk_patient']])
    1287
    1288                  return self.format (
    1289                          patient = patient,
    1290                          with_summary = True,
    1291                          with_codes = True,
    1292                          with_encounters = True,
    1293                          with_documents = True,
    1294                          with_hospital_stays = True,
    1295                          with_procedures = True,
    1296                          with_family_history = True,
    1297                          with_tests = False,             # does not inform on the episode itself
    1298                          with_vaccinations = True,
    1299                          with_health_issue = True,
    1300                          return_list = True
    1301                  )
    1302
    1303          #--------------------------------------------------------
    1304 -        def format(self, left_margin=0, patient=None,
    1305                  with_summary=True,
    1306                  with_codes=True,
    1307                  with_encounters=True,
    1308                  with_documents=True,
    1309                  with_hospital_stays=True,
    1310                  with_procedures=True,
    1311                  with_family_history=True,
    1312                  with_tests=True,
    1313                  with_vaccinations=True,
    1314                  with_health_issue=False,
    1315                  return_list=False
    1316          ):
    1317
    1318                  if patient is not None:
    1319                          if patient.ID != self._payload[self._idx['pk_patient']]:
    1320                                  msg = '<patient>.ID = %s but episode %s belongs to patient %s' % (
    1321                                          patient.ID,
    1322                                          self._payload[self._idx['pk_episode']],
    1323                                          self._payload[self._idx['pk_patient']]
    1324                                  )
    1325                                  raise ValueError(msg)
    1326                          emr = patient.emr
    1327                  else:
    1328                          with_encounters = False
    1329                          with_documents = False
    1330                          with_hospital_stays = False
    1331                          with_procedures = False
    1332                          with_family_history = False
    1333                          with_tests = False
    1334                          with_vaccinations = False
    1335
    1336                  lines = []
    1337
    1338                  # episode details
    1339                  lines.append (_('Episode %s%s%s   [#%s]') % (
    1340                          gmTools.u_left_double_angle_quote,
    1341                          self._payload[self._idx['description']],
    1342                          gmTools.u_right_double_angle_quote,
    1343                          self._payload[self._idx['pk_episode']]
    1344                  ))
    1345
    1346                  enc = cEncounter(aPK_obj = self._payload[self._idx['pk_encounter']])
    1347                  lines.append (' ' + _('Created during encounter: %s (%s - %s)   [#%s]') % (
    1348                          enc['l10n_type'],
    1349                          enc['started_original_tz'].strftime('%Y-%m-%d %H:%M'),
    1350                          enc['last_affirmed_original_tz'].strftime('%H:%M'),
    1351                          self._payload[self._idx['pk_encounter']]
    1352                  ))
    1353
    1354                  if patient is not None:
    1355                          range_str, range_str_verb, duration_str = self.formatted_clinical_duration
    1356                          lines.append(_(' Duration: %s (%s)') % (duration_str, range_str_verb))
    1357
    1358                  lines.append(' ' + _('Status') + ': %s%s' % (
    1359                          gmTools.bool2subst(self._payload[self._idx['episode_open']], _('active'), _('finished')),
    1360                          gmTools.coalesce (
    1361                                  initial = diagnostic_certainty_classification2str(self._payload[self._idx['diagnostic_certainty_classification']]),
    1362                                  instead = '',
    1363                                  template_initial = ', %s',
    1364                                  none_equivalents = [None, '']
    1365                          )
    1366                  ))
    1367
    1368                  if with_health_issue:
    1369                          lines.append(' ' + _('Health issue') + ': %s' % gmTools.coalesce (
    1370                                  self._payload[self._idx['health_issue']],
    1371                                  _('none associated')
    1372                          ))
    1373
    1374                  if with_summary:
    1375                          if self._payload[self._idx['summary']] is not None:
    1376                                  lines.append(' %s:' % _('Synopsis'))
    1377                                  lines.append(gmTools.wrap (
    1378                                                  text = self._payload[self._idx['summary']],
    1379                                                  width = 60,
    1380                                                  initial_indent = '  ',
    1381                                                  subsequent_indent = '  '
    1382                                          )
    1383                                  )
    1384
    1385                  # codes
    1386                  if with_codes:
    1387                          codes = self.generic_codes
    1388                          if len(codes) > 0:
    1389                                  lines.append('')
    1390                          for c in codes:
    1391                                  lines.append(' %s: %s (%s - %s)' % (
    1392                                          c['code'],
    1393                                          c['term'],
    1394                                          c['name_short'],
    1395                                          c['version']
    1396                                  ))
    1397                          del codes
    1398
    1399                  lines.append('')
    1400
    1401                  # encounters
    1402                  if with_encounters:
    1403                          encs = emr.get_encounters(episodes = [self._payload[self._idx['pk_episode']]])
    1404                          if encs is None:
    1405                                  lines.append(_('Error retrieving encounters for this episode.'))
    1406                          elif len(encs) == 0:
    1407                                  #lines.append(_('There are no encounters for this issue.'))
    1408                                  pass
    1409                          else:
    1410                                  first_encounter = emr.get_first_encounter(episode_id = self._payload[self._idx['pk_episode']])
    1411                                  last_encounter = emr.get_last_encounter(episode_id = self._payload[self._idx['pk_episode']])
    1412                                  lines.append(_('Last worked on: %s\n') % last_encounter['last_affirmed_original_tz'].strftime('%Y-%m-%d %H:%M'))
    1413                                  if len(encs) < 4:
    1414                                          line = _('%s encounter(s) (%s - %s):')
    1415                                  else:
    1416                                          line = _('1st and (up to 3) most recent (of %s) encounters (%s - %s):')
    1417                                  lines.append(line % (
    1418                                          len(encs),
    1419                                          first_encounter['started'].strftime('%m/%Y'),
    1420                                          last_encounter['last_affirmed'].strftime('%m/%Y')
    1421                                  ))
    1422                                  lines.append(' %s - %s (%s):%s' % (
    1423                                          first_encounter['started_original_tz'].strftime('%Y-%m-%d %H:%M'),
    1424                                          first_encounter['last_affirmed_original_tz'].strftime('%H:%M'),
    1425                                          first_encounter['l10n_type'],
    1426                                          gmTools.coalesce (
    1427                                                  first_encounter['assessment_of_encounter'],
    1428                                                  gmTools.coalesce (
    1429                                                          first_encounter['reason_for_encounter'],
    1430                                                          '',
    1431                                                          ' \u00BB%s\u00AB' + (' (%s)' % _('RFE'))
    1432                                                  ),
    1433                                                  ' \u00BB%s\u00AB' + (' (%s)' % _('AOE'))
    1434                                          )
    1435                                  ))
    1436                                  if len(encs) > 4:
    1437                                          lines.append(_(' %s %s skipped %s') % (
    1438                                                  gmTools.u_ellipsis,
    1439                                                  (len(encs) - 4),
    1440                                                  gmTools.u_ellipsis
    1441                                          ))
    1442                                  for enc in encs[1:][-3:]:
    1443                                          lines.append(' %s - %s (%s):%s' % (
    1444                                                  enc['started_original_tz'].strftime('%Y-%m-%d %H:%M'),
    1445                                                  enc['last_affirmed_original_tz'].strftime('%H:%M'),
    1446                                                  enc['l10n_type'],
    1447                                                  gmTools.coalesce (
    1448                                                          enc['assessment_of_encounter'],
    1449                                                          gmTools.coalesce (
    1450                                                                  enc['reason_for_encounter'],
    1451                                                                  '',
    1452                                                                  ' \u00BB%s\u00AB' + (' (%s)' % _('RFE'))
    1453                                                          ),
    1454                                                          ' \u00BB%s\u00AB' + (' (%s)' % _('AOE'))
    1455                                                  )
    1456                                          ))
    1457                                  del encs
    1458                                  # spell out last encounter
    1459                                  if last_encounter is not None:
    1460                                          lines.append('')
    1461                                          lines.append(_('Progress notes in most recent encounter:'))
    1462                                          lines.extend(last_encounter.format_soap (
    1463                                                  episodes = [ self._payload[self._idx['pk_episode']] ],
    1464                                                  left_margin = left_margin,
    1465                                                  soap_cats = 'soapu',
    1466                                                  emr = emr
    1467                                          ))
    1468
    1469                  # documents
    1470                  if with_documents:
    1471                          doc_folder = patient.get_document_folder()
    1472                          docs = doc_folder.get_documents (
    1473                                  pk_episodes = [ self._payload[self._idx['pk_episode']] ]
    1474                          )
    1475                          if len(docs) > 0:
    1476                                  lines.append('')
    1477                                  lines.append(_('Documents: %s') % len(docs))
    1478                          for d in docs:
    1479                                  lines.append(' ' + d.format(single_line = True))
    1480                          del docs
    1481
    1482                  # hospitalizations
    1483                  if with_hospital_stays:
    1484                          stays = emr.get_hospital_stays(episodes = [ self._payload[self._idx['pk_episode']] ])
    1485                          if len(stays) > 0:
    1486                                  lines.append('')
    1487                                  lines.append(_('Hospitalizations: %s') % len(stays))
    1488                          for s in stays:
    1489                                  lines.append(s.format(left_margin = (left_margin + 1)))
    1490                          del stays
    1491
    1492                  # procedures
    1493                  if with_procedures:
    1494                          procs = emr.get_performed_procedures(episodes = [ self._payload[self._idx['pk_episode']] ])
    1495                          if len(procs) > 0:
    1496                                  lines.append('')
    1497                                  lines.append(_('Procedures performed: %s') % len(procs))
    1498                          for p in procs:
    1499                                  lines.append(p.format (
    1500                                          left_margin = (left_margin + 1),
    1501                                          include_episode = False,
    1502                                          include_codes = True
    1503                                  ))
    1504                          del procs
    1505
    1506                  # family history
    1507                  if with_family_history:
    1508                          fhx = emr.get_family_history(episodes = [ self._payload[self._idx['pk_episode']] ])
    1509                          if len(fhx) > 0:
    1510                                  lines.append('')
    1511                                  lines.append(_('Family History: %s') % len(fhx))
    1512                          for f in fhx:
    1513                                  lines.append(f.format (
    1514                                          left_margin = (left_margin + 1),
    1515                                          include_episode = False,
    1516                                          include_comment = True,
    1517                                          include_codes = True
    1518                                  ))
    1519                          del fhx
    1520
    1521                  # test results
    1522                  if with_tests:
    1523                          tests = emr.get_test_results_by_date(episodes = [ self._payload[self._idx['pk_episode']] ])
    1524                          if len(tests) > 0:
    1525                                  lines.append('')
    1526                                  lines.append(_('Measurements and Results:'))
    1527                          for t in tests:
    1528                                  lines.append(' ' + t.format_concisely(date_format = '%Y %b %d', with_notes = True))
    1529                          del tests
    1530
    1531                  # vaccinations
    1532                  if with_vaccinations:
    1533                          vaccs = emr.get_vaccinations (
    1534                                  episodes = [ self._payload[self._idx['pk_episode']] ],
    1535                                  order_by = 'date_given DESC, vaccine'
    1536                          )
    1537                          if len(vaccs) > 0:
    1538                                  lines.append('')
    1539                                  lines.append(_('Vaccinations:'))
    1540                          for vacc in vaccs:
    1541                                  lines.extend(vacc.format (
    1542                                          with_indications = True,
    1543                                          with_comment = True,
    1544                                          with_reaction = True,
    1545                                          date_format = '%Y-%m-%d'
    1546                                  ))
    1547                          del vaccs
    1548
    1549                  lines = gmTools.strip_trailing_empty_lines(lines = lines, eol = '\n')
    1550                  if return_list:
    1551                          return lines
    1552
    1553                  left_margin = ' ' * left_margin
    1554                  eol_w_margin = '\n%s' % left_margin
    1555                  return left_margin + eol_w_margin.join(lines) + '\n'
    1556
    1557          #--------------------------------------------------------
    1558          # properties
    1559          #--------------------------------------------------------
    1560 -        def _get_best_guess_clinical_start_date(self):
    1561                  cmd = """SELECT MIN(earliest) FROM
    1562                  (
    1563                          -- last modification of episode,
    1564                          -- earliest-possible thereof = when created,
    1565                          -- should actually go all the way back into audit.log_episode
    1566                          (SELECT c_epi.modified_when AS earliest FROM clin.episode c_epi WHERE c_epi.pk = %(pk)s)
    1567
    1568                                  UNION ALL
    1569
    1570                          -- last modification of encounter in which created,
    1571                          -- earliest-possible thereof = initial creation of that encounter
    1572                          (SELECT c_enc.modified_when AS earliest FROM clin.encounter c_enc WHERE c_enc.pk = (
    1573                                  SELECT fk_encounter FROM clin.episode WHERE pk = %(pk)s
    1574                          ))
    1575                                  UNION ALL
    1576
    1577                          -- start of encounter in which created,
    1578                          -- earliest-possible thereof = explicitely set by user
    1579                          (SELECT c_enc.started AS earliest FROM clin.encounter c_enc WHERE c_enc.pk = (
    1580                                  SELECT fk_encounter FROM clin.episode WHERE pk = %(pk)s
    1581                          ))
    1582                                  UNION ALL
    1583
    1584                          -- start of encounters of clinical items linked to this episode,
    1585                          -- earliest-possible thereof = explicitely set by user
    1586                          (SELECT MIN(started) AS earliest FROM clin.encounter WHERE pk IN (
    1587                                  SELECT fk_encounter FROM clin.clin_root_item WHERE fk_episode = %(pk)s
    1588                          ))
    1589                                  UNION ALL
    1590
    1591                          -- .clin_when of clinical items linked to this episode,
    1592                          -- earliest-possible thereof = explicitely set by user
    1593                          (SELECT MIN(clin_when) AS earliest FROM clin.clin_root_item WHERE fk_episode = %(pk)s)
    1594
    1595                                  UNION ALL
    1596
    1597                          -- earliest modification time of clinical items linked to this episode
    1598                          -- this CAN be used since if an item is linked to an episode it can be
    1599                          -- assumed the episode (should have) existed at the time of creation
    1600                          (SELECT MIN(modified_when) AS earliest FROM clin.clin_root_item WHERE fk_episode = %(pk)s)
    1601
    1602                                  UNION ALL
    1603
    1604                          -- there may not be items, but there may still be documents ...
    1605                          (SELECT MIN(clin_when) AS earliest FROM blobs.doc_med WHERE fk_episode = %(pk)s)
    1606                  ) AS candidates"""
    1607                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}])
    1608                  return rows[0][0]
    1609
    1610          best_guess_clinical_start_date = property(_get_best_guess_clinical_start_date)
    1611
    1612          #--------------------------------------------------------
    1613 -        def _get_best_guess_clinical_end_date(self):
    1614                  if self._payload[self._idx['episode_open']]:
    1615                          return None
    1616
    1617                  cmd = """SELECT COALESCE (
    1618                          (SELECT
    1619                                  latest --, source_type
    1620                           FROM (
    1621                                  -- latest explicit .clin_when of clinical items linked to this episode
    1622                                  (SELECT
    1623                                          MAX(clin_when) AS latest,
    1624                                          'clin.episode.pk = clin.clin_root_item.fk_episode -> .clin_when'::text AS source_type
    1625                                   FROM clin.clin_root_item
    1626                                   WHERE fk_episode = %(pk)s
    1627                                  )
    1628                                          UNION ALL
    1629                                  -- latest explicit .clin_when of documents linked to this episode
    1630                                  (SELECT
    1631                                          MAX(clin_when) AS latest,
    1632                                          'clin.episode.pk = blobs.doc_med.fk_episode -> .clin_when'::text AS source_type
    1633                                   FROM blobs.doc_med
    1634                                   WHERE fk_episode = %(pk)s
    1635                                  )
    1636                           ) AS candidates
    1637                           ORDER BY latest DESC NULLS LAST
    1638                           LIMIT 1
    1639                          ),
    1640                          -- last ditch, always exists, only use when no clinical items or documents linked:
    1641                          -- last modification, latest = when last changed to the current state
    1642                          (SELECT c_epi.modified_when AS latest --, 'clin.episode.modified_when'::text AS source_type
    1643                           FROM clin.episode c_epi WHERE c_epi.pk = %(pk)s
    1644                          )
    1645                  )"""
    1646                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}], get_col_idx = False)
    1647                  return rows[0][0]
    1648
    1649          best_guess_clinical_end_date = property(_get_best_guess_clinical_end_date)
    1650
    1651          #--------------------------------------------------------
    1652 -        def _get_formatted_clinical_duration(self):
    1653                  start = self.best_guess_clinical_start_date
    1654                  end = self.best_guess_clinical_end_date
    1655                  if end is None:
    1656                          range_str = '%s-%s' % (
    1657                                  gmDateTime.pydt_strftime(start, "%b'%y"),
    1658                                  gmTools.u_ellipsis
    1659                          )
    1660                          range_str_verb = '%s - %s' % (
    1661                                  gmDateTime.pydt_strftime(start, '%b %d %Y'),
    1662                                  gmTools.u_ellipsis
    1663                          )
    1664                          duration_str = _('%s so far') % gmDateTime.format_interval_medically(gmDateTime.pydt_now_here() - start)
    1665                          return (range_str, range_str_verb, duration_str)
    1666
    1667                  duration_str = gmDateTime.format_interval_medically(end - start)
    1668                  # year different:
    1669                  if end.year != start.year:
    1670                          range_str = '%s-%s' % (
    1671                                  gmDateTime.pydt_strftime(start, "%b'%y"),
    1672                                  gmDateTime.pydt_strftime(end, "%b'%y")
    1673                          )
    1674                          range_str_verb = '%s - %s' % (
    1675                                  gmDateTime.pydt_strftime(start, '%b %d %Y'),
    1676                                  gmDateTime.pydt_strftime(end, '%b %d %Y')
    1677                          )
    1678                          return (range_str, range_str_verb, duration_str)
    1679                  # same year:
    1680                  if end.month != start.month:
    1681                          range_str = '%s-%s' % (
    1682                                  gmDateTime.pydt_strftime(start, '%b'),
    1683                                  gmDateTime.pydt_strftime(end, "%b'%y")
    1684                          )
    1685                          range_str_verb = '%s - %s' % (
    1686                                  gmDateTime.pydt_strftime(start, '%b %d'),
    1687                                  gmDateTime.pydt_strftime(end, '%b %d %Y')
    1688                          )
    1689                          return (range_str, range_str_verb, duration_str)
    1690
    1691                  # same year and same month
    1692                  range_str = gmDateTime.pydt_strftime(start, "%b'%y")
    1693                  range_str_verb = gmDateTime.pydt_strftime(start, '%b %d %Y')
    1694                  return (range_str, range_str_verb, duration_str)
    1695
    1696          formatted_clinical_duration = property(_get_formatted_clinical_duration)
    1697
    1698          #--------------------------------------------------------
    1699 -        def _get_latest_access_date(self):
    1700                  cmd = """SELECT MAX(latest) FROM (
    1701                          -- last modification, latest = when last changed to the current state
    1702                          (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)
    1703
    1704                                  UNION ALL
    1705
    1706                          -- last modification of encounter in which created, latest = initial creation of that encounter
    1707                          -- DO NOT USE: just because one corrects a typo does not mean the episode took longer
    1708                          --(SELECT c_enc.modified_when AS latest FROM clin.encounter c_enc WHERE c_enc.pk = (
    1709                          --      SELECT fk_encounter FROM clin.episode WHERE pk = %(pk)s
    1710                          --))
    1711
    1712                          -- end of encounter in which created, latest = explicitely set
    1713                          -- DO NOT USE: we can retrospectively create episodes which
    1714                          -- DO NOT USE: are long since finished
    1715                          --(SELECT c_enc.last_affirmed AS latest FROM clin.encounter c_enc WHERE c_enc.pk = (
    1716                          --      SELECT fk_encounter FROM clin.episode WHERE pk = %(pk)s
    1717                          --))
    1718
    1719                          -- latest end of encounters of clinical items linked to this episode
    1720                          (SELECT
    1721                                  MAX(last_affirmed) AS latest,
    1722                                  'clin.episode.pk = clin.clin_root_item,fk_episode -> .fk_encounter.last_affirmed'::text AS candidate
    1723                           FROM clin.encounter
    1724                           WHERE pk IN (
    1725                                  SELECT fk_encounter FROM clin.clin_root_item WHERE fk_episode = %(pk)s
    1726                          ))
    1727                                  UNION ALL
    1728
    1729                          -- latest explicit .clin_when of clinical items linked to this episode
    1730                          (SELECT
    1731                                  MAX(clin_when) AS latest,
    1732                                  'clin.episode.pk = clin.clin_root_item,fk_episode -> .clin_when'::text AS candidate
    1733                           FROM clin.clin_root_item
    1734                           WHERE fk_episode = %(pk)s
    1735                          )
    1736
    1737                          -- latest modification time of clinical items linked to this episode
    1738                          -- this CAN be used since if an item is linked to an episode it can be
    1739                          -- assumed the episode (should have) existed at the time of creation
    1740                          -- DO NOT USE, because typo fixes should not extend the episode
    1741                          --(SELECT MIN(modified_when) AS latest FROM clin.clin_root_item WHERE fk_episode = %(pk)s)
    1742
    1743                          -- not sure about this one:
    1744                          -- .pk -> clin.clin_root_item.fk_encounter.modified_when
    1745
    1746                  ) AS candidates"""
    1747                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}])
    1748                  return rows[0][0]
    1749
    1750          latest_access_date = property(_get_latest_access_date)
    1751
    1752          #--------------------------------------------------------
    1753 -        def _get_diagnostic_certainty_description(self):
    1754                  return diagnostic_certainty_classification2str(self._payload[self._idx['diagnostic_certainty_classification']])
    1755
    1756          diagnostic_certainty_description = property(_get_diagnostic_certainty_description, lambda x:x)
    1757
    1758          #--------------------------------------------------------
    1759 -        def _get_formatted_revision_history(self):
    1760                  cmd = """SELECT
    1761                                  'NONE (live row)'::text as audit__action_applied,
    1762                                  NULL AS audit__action_when,
    1763                                  NULL AS audit__action_by,
    1764                                  pk_audit,
    1765                                  row_version,
    1766                                  modified_when,
    1767                                  modified_by,
    1768                                  pk, fk_health_issue, description, is_open, fk_encounter,
    1769                                  diagnostic_certainty_classification,
    1770                                  summary
    1771                          FROM clin.episode
    1772                          WHERE pk = %(pk_episode)s
    1773                  UNION ALL (
    1774                          SELECT
    1775                                  audit_action as audit__action_applied,
    1776                                  audit_when as audit__action_when,
    1777                                  audit_by as audit__action_by,
    1778                                  pk_audit,
    1779                                  orig_version as row_version,
    1780                                  orig_when as modified_when,
    1781                                  orig_by as modified_by,
    1782                                  pk, fk_health_issue, description, is_open, fk_encounter,
    1783                                  diagnostic_certainty_classification,
    1784                                  summary
    1785                          FROM audit.log_episode
    1786                          WHERE pk = %(pk_episode)s
    1787                  )
    1788                  ORDER BY row_version DESC
    1789                  """
    1790                  args = {'pk_episode': self.pk_obj}
    1791                  title = _('Episode: %s%s%s') % (
    1792                          gmTools.u_left_double_angle_quote,
    1793                          self._payload[self._idx['description']],
    1794                          gmTools.u_right_double_angle_quote
    1795                  )
    1796                  return '\n'.join(self._get_revision_history(cmd, args, title))
    1797
    1798          formatted_revision_history = property(_get_formatted_revision_history, lambda x:x)
    1799
    1800          #--------------------------------------------------------
    1801 -        def _get_generic_codes(self):
    1802                  if len(self._payload[self._idx['pk_generic_codes']]) == 0:
    1803                          return []
    1804
    1805                  cmd = gmCoding._SQL_get_generic_linked_codes % 'pk_generic_code IN %(pks)s'
    1806                  args = {'pks': tuple(self._payload[self._idx['pk_generic_codes']])}
    1807                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1808                  return [ gmCoding.cGenericLinkedCode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_lnk_code2item'}) for r in rows ]
    1809
    1810 -        def _set_generic_codes(self, pk_codes):
    1811                  queries = []
    1812                  # remove all codes
    1813                  if len(self._payload[self._idx['pk_generic_codes']]) > 0:
    1814                          queries.append ({
    1815                                  'cmd': 'DELETE FROM clin.lnk_code2episode WHERE fk_item = %(epi)s AND fk_generic_code IN %(codes)s',
    1816                                  'args': {
    1817                                          'epi': self._payload[self._idx['pk_episode']],
    1818                                          'codes': tuple(self._payload[self._idx['pk_generic_codes']])
    1819                                  }
    1820                          })
    1821                  # add new codes
    1822                  for pk_code in pk_codes:
    1823                          queries.append ({
    1824                                  'cmd': 'INSERT INTO clin.lnk_code2episode (fk_item, fk_generic_code) VALUES (%(epi)s, %(pk_code)s)',
    1825                                  'args': {
    1826                                          'epi': self._payload[self._idx['pk_episode']],
    1827                                          'pk_code': pk_code
    1828                                  }
    1829                          })
    1830                  if len(queries) == 0:
    1831                          return
    1832                  # run it all in one transaction
    1833                  rows, idx = gmPG2.run_rw_queries(queries = queries)
    1834                  return
    1835
    1836          generic_codes = property(_get_generic_codes, _set_generic_codes)
    1837
    1838          #--------------------------------------------------------
    1839 -        def _get_has_narrative(self):
    1840                  cmd = """SELECT EXISTS (
    1841                          SELECT 1 FROM clin.clin_narrative
    1842                          WHERE
    1843                                  fk_episode = %(epi)s
    1844                                          AND
    1845                                  fk_encounter IN (
    1846                                          SELECT pk FROM clin.encounter WHERE fk_patient = %(pat)s
    1847                                  )
    1848                  )"""
    1849                  args = {
    1850                          'pat': self._payload[self._idx['pk_patient']],
    1851                          'epi': self._payload[self._idx['pk_episode']]
    1852                  }
    1853                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
    1854                  return rows[0][0]
    1855
    1856          has_narrative = property(_get_has_narrative, lambda x:x)
    1857
    1858          #--------------------------------------------------------
    1859 -        def _get_health_issue(self):
    1860                  if self._payload[self._idx['pk_health_issue']] is None:
    1861                          return None
    1862                  return cHealthIssue(self._payload[self._idx['pk_health_issue']])
    1863
    1864          health_issue = property(_get_health_issue)
    1865
    1866  #============================================================
    1867 -def create_episode(pk_health_issue=None, episode_name=None, is_open=False, allow_dupes=False, encounter=None, link_obj=None):
    1868          """Creates a new episode for a given patient's health issue.
    1869
    1870          pk_health_issue - given health issue PK
    1871          episode_name - name of episode
    1872          """
    1873          if not allow_dupes:
    1874                  try:
    1875                          episode = cEpisode(name = episode_name, health_issue = pk_health_issue, encounter = encounter, link_obj = link_obj)
    1876                          if episode['episode_open'] != is_open:
    1877                                  episode['episode_open'] = is_open
    1878                                  episode.save_payload()
    1879                          return episode
    1880                  except gmExceptions.ConstructorError:
    1881                          pass
    1882
    1883          queries = []
    1884          cmd = "INSERT INTO clin.episode (fk_health_issue, description, is_open, fk_encounter) VALUES (%s, %s, %s::boolean, %s)"
    1885          queries.append({'cmd': cmd, 'args': [pk_health_issue, episode_name, is_open, encounter]})
    1886          queries.append({'cmd': cEpisode._cmd_fetch_payload % "currval('clin.episode_pk_seq')"})
    1887          rows, idx = gmPG2.run_rw_queries(link_obj = link_obj, queries = queries, return_data=True, get_col_idx=True)
    1888
    1889          episode = cEpisode(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_episode'})
    1890          return episode
    1891
    1892  #-----------------------------------------------------------
    1893 -def delete_episode(episode=None):
    1894          if isinstance(episode, cEpisode):
    1895                  pk = episode['pk_episode']
    1896          else:
    1897                  pk = int(episode)
    1898
    1899          cmd = 'DELETE FROM clin.episode WHERE pk = %(pk)s'
    1900
    1901          try:
    1902                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': pk}}])
    1903          except gmPG2.dbapi.IntegrityError:
    1904                  # should be parsing pgcode/and or error message
    1905                  _log.exception('cannot delete episode, it is in use')
    1906                  return False
    1907
    1908          return True
    1909  #-----------------------------------------------------------
    1910 -def episode2problem(episode=None, allow_closed=False):
    1911          return cProblem (
    1912                  aPK_obj = {
    1913                          'pk_patient': episode['pk_patient'],
    1914                          'pk_episode': episode['pk_episode'],
    1915                          'pk_health_issue': episode['pk_health_issue']
    1916                  },
    1917                  try_potential_problems = allow_closed
    1918          )
    1919
    1920  #============================================================
    1921  # encounter API
    1922  #============================================================
    1923  SQL_get_encounters = "SELECT * FROM clin.v_pat_encounters WHERE %s"
    1924
    1925 -class cEncounter(gmBusinessDBObject.cBusinessDBObject):
    1926          """Represents one encounter."""
    1927
    1928          _cmd_fetch_payload = SQL_get_encounters % 'pk_encounter = %s'
    1929          _cmds_store_payload = [
    1930                  """UPDATE clin.encounter SET
    1931                                  started = %(started)s,
    1932                                  last_affirmed = %(last_affirmed)s,
    1933                                  fk_location = %(pk_org_unit)s,
    1934                                  fk_type = %(pk_type)s,
    1935                                  reason_for_encounter = gm.nullify_empty_string(%(reason_for_encounter)s),
    1936                                  assessment_of_encounter = gm.nullify_empty_string(%(assessment_of_encounter)s)
    1937                          WHERE
    1938                                  pk = %(pk_encounter)s AND
    1939                                  xmin = %(xmin_encounter)s
    1940                          """,
    1941                  # need to return all fields so we can survive in-place upgrades
    1942                  "SELECT * FROM clin.v_pat_encounters WHERE pk_encounter = %(pk_encounter)s"
    1943          ]
    1944          _updatable_fields = [
    1945                  'started',
    1946                  'last_affirmed',
    1947                  'pk_org_unit',
    1948                  'pk_type',
    1949                  'reason_for_encounter',
    1950                  'assessment_of_encounter'
    1951          ]
    1952          #--------------------------------------------------------
    1953 -        def set_active(self):
    1954                  """Set the encounter as the active one.
    1955
    1956                  "Setting active" means making sure the encounter
    1957                  row has the youngest "last_affirmed" timestamp of
    1958                  all encounter rows for this patient.
    1959                  """
    1960                  self['last_affirmed'] = gmDateTime.pydt_now_here()
    1961                  self.save()
    1962          #--------------------------------------------------------
    1963 -        def lock(self, exclusive=False, link_obj=None):
    1964                  return lock_encounter(self.pk_obj, exclusive = exclusive, link_obj = link_obj)
    1965          #--------------------------------------------------------
    1966 -        def unlock(self, exclusive=False, link_obj=None):
    1967                  return unlock_encounter(self.pk_obj, exclusive = exclusive, link_obj = link_obj)
    1968          #--------------------------------------------------------
    1969 -        def transfer_clinical_data(self, source_episode=None, target_episode=None):
    1970                  """
    1971                  Moves every element currently linked to the current encounter
    1972                  and the source_episode onto target_episode.
    1973
    1974                  @param source_episode The episode the elements are currently linked to.
    1975                  @type target_episode A cEpisode intance.
    1976                  @param target_episode The episode the elements will be relinked to.
    1977                  @type target_episode A cEpisode intance.
    1978                  """
    1979                  if source_episode['pk_episode'] == target_episode['pk_episode']:
    1980                          return True
    1981
    1982                  queries = []
    1983                  cmd = """
    1984                          UPDATE clin.clin_root_item
    1985                          SET fk_episode = %(trg)s
    1986                          WHERE
    1987                                  fk_encounter = %(enc)s AND
    1988                                  fk_episode = %(src)s
    1989                          """
    1990                  rows, idx = gmPG2.run_rw_queries(queries = [{
    1991                          'cmd': cmd,
    1992                          'args': {
    1993                                  'trg': target_episode['pk_episode'],
    1994                                  'enc': self.pk_obj,
    1995                                  'src': source_episode['pk_episode']
    1996                          }
    1997                  }])
    1998                  self.refetch_payload()
    1999                  return True
    2000
    2001          #--------------------------------------------------------
    2002 -        def transfer_all_data_to_another_encounter(self, pk_target_encounter=None):
    2003                  if pk_target_encounter == self.pk_obj:
    2004                          return True
    2005                  cmd = "SELECT clin.transfer_all_encounter_data(%(src)s, %(trg)s)"
    2006                  args = {
    2007                          'src': self.pk_obj,
    2008                          'trg': pk_target_encounter
    2009                  }
    2010                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    2011                  return True
    2012
    2013  #       conn = gmPG2.get_connection()
    2014  #       curs = conn.cursor()
    2015  #       curs.callproc('clin.get_hints_for_patient', [pk_identity])
    2016  #       rows = curs.fetchall()
    2017  #       idx = gmPG2.get_col_indices(curs)
    2018  #       curs.close()
    2019  #       conn.rollback()
    2020
    2021          #--------------------------------------------------------
    2022 -        def same_payload(self, another_object=None):
    2023
    2024                  relevant_fields = [
    2025                          'pk_org_unit',
    2026                          'pk_type',
    2027                          'pk_patient',
    2028                          'reason_for_encounter',
    2029                          'assessment_of_encounter'
    2030                  ]
    2031                  for field in relevant_fields:
    2032                          if self._payload[self._idx[field]] != another_object[field]:
    2033                                  _log.debug('mismatch on [%s]: "%s" vs. "%s"', field, self._payload[self._idx[field]], another_object[field])
    2034                                  return False
    2035
    2036                  relevant_fields = [
    2037                          'started',
    2038                          'last_affirmed',
    2039                  ]
    2040                  for field in relevant_fields:
    2041                          if self._payload[self._idx[field]] is None:
    2042                                  if another_object[field] is None:
    2043                                          continue
    2044                                  _log.debug('mismatch on [%s]: here="%s", other="%s"', field, self._payload[self._idx[field]], another_object[field])
    2045                                  return False
    2046
    2047                          if another_object[field] is None:
    2048                                  return False
    2049
    2050                          # compares at seconds granularity
    2051                          if self._payload[self._idx[field]].strftime('%Y-%m-%d %H:%M:%S') != another_object[field].strftime('%Y-%m-%d %H:%M:%S'):
    2052                                  _log.debug('mismatch on [%s]: here="%s", other="%s"', field, self._payload[self._idx[field]], another_object[field])
    2053                                  return False
    2054
    2055                  # compare codes
    2056                  # 1) RFE
    2057                  if another_object['pk_generic_codes_rfe'] is None:
    2058                          if self._payload[self._idx['pk_generic_codes_rfe']] is not None:
    2059                                  return False
    2060                  if another_object['pk_generic_codes_rfe'] is not None:
    2061                          if self._payload[self._idx['pk_generic_codes_rfe']] is None:
    2062                                  return False
    2063                  if (
    2064                          (another_object['pk_generic_codes_rfe'] is None)
    2065                                  and
    2066                          (self._payload[self._idx['pk_generic_codes_rfe']] is None)
    2067                  ) is False:
    2068                          if set(another_object['pk_generic_codes_rfe']) != set(self._payload[self._idx['pk_generic_codes_rfe']]):
    2069                                  return False
    2070                  # 2) AOE
    2071                  if another_object['pk_generic_codes_aoe'] is None:
    2072                          if self._payload[self._idx['pk_generic_codes_aoe']] is not None:
    2073                                  return False
    2074                  if another_object['pk_generic_codes_aoe'] is not None:
    2075                          if self._payload[self._idx['pk_generic_codes_aoe']] is None:
    2076                                  return False
    2077                  if (
    2078                          (another_object['pk_generic_codes_aoe'] is None)
    2079                                  and
    2080                          (self._payload[self._idx['pk_generic_codes_aoe']] is None)
    2081                  ) is False:
    2082                          if set(another_object['pk_generic_codes_aoe']) != set(self._payload[self._idx['pk_generic_codes_aoe']]):
    2083                                  return False
    2084
    2085                  return True
    2086          #--------------------------------------------------------
    2087 -        def has_clinical_data(self):
    2088                  cmd = """
    2089  select exists (
    2090          select 1 from clin.v_pat_items where pk_patient = %(pat)s and pk_encounter = %(enc)s
    2091                  union all
    2092          select 1 from blobs.v_doc_med where pk_patient = %(pat)s and pk_encounter = %(enc)s
    2093  )"""
    2094                  args = {
    2095                          'pat': self._payload[self._idx['pk_patient']],
    2096                          'enc': self.pk_obj
    2097                  }
    2098                  rows, idx = gmPG2.run_ro_queries (
    2099                          queries = [{
    2100                                  'cmd': cmd,
    2101                                  'args': args
    2102                          }]
    2103                  )
    2104                  return rows[0][0]
    2105
    2106          #--------------------------------------------------------
    2107 -        def has_narrative(self):
    2108                  cmd = """
    2109  select exists (
    2110          select 1 from clin.v_pat_items where pk_patient=%(pat)s and pk_encounter=%(enc)s
    2111  )"""
    2112                  args = {
    2113                          'pat': self._payload[self._idx['pk_patient']],
    2114                          'enc': self.pk_obj
    2115                  }
    2116                  rows, idx = gmPG2.run_ro_queries (
    2117                          queries = [{
    2118                                  'cmd': cmd,
    2119                                  'args': args
    2120                          }]
    2121                  )
    2122                  return rows[0][0]
    2123          #--------------------------------------------------------
    2124 -        def has_soap_narrative(self, soap_cats=None):
    2125                  """soap_cats: <space> = admin category"""
    2126
    2127                  if soap_cats is None:
    2128                          soap_cats = 'soap '
    2129                  else:
    2130                          soap_cats = soap_cats.lower()
    2131
    2132                  cats = []
    2133                  for cat in soap_cats:
    2134                          if cat in 'soapu':
    2135                                  cats.append(cat)
    2136                                  continue
    2137                          if cat == ' ':
    2138                                  cats.append(None)
    2139
    2140                  cmd = """
    2141                          SELECT EXISTS (
    2142                                  SELECT 1 FROM clin.clin_narrative
    2143                                  WHERE
    2144                                          fk_encounter = %(enc)s
    2145                                                  AND
    2146                                          soap_cat IN %(cats)s
    2147                                  LIMIT 1
    2148                          )
    2149                  """
    2150                  args = {'enc': self._payload[self._idx['pk_encounter']], 'cats': tuple(cats)}
    2151                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd,'args': args}])
    2152                  return rows[0][0]
    2153          #--------------------------------------------------------
    2154 -        def has_documents(self):
    2155                  cmd = """
    2156  select exists (
    2157          select 1 from blobs.v_doc_med where pk_patient = %(pat)s and pk_encounter = %(enc)s
    2158  )"""
    2159                  args = {
    2160                          'pat': self._payload[self._idx['pk_patient']],
    2161                          'enc': self.pk_obj
    2162                  }
    2163                  rows, idx = gmPG2.run_ro_queries (
    2164                          queries = [{
    2165                                  'cmd': cmd,
    2166                                  'args': args
    2167                          }]
    2168                  )
    2169                  return rows[0][0]
    2170          #--------------------------------------------------------
    2171 -        def get_latest_soap(self, soap_cat=None, episode=None):
    2172
    2173                  if soap_cat is not None:
    2174                          soap_cat = soap_cat.lower()
    2175
    2176                  if episode is None:
    2177                          epi_part = 'fk_episode is null'
    2178                  else:
    2179                          epi_part = 'fk_episode = %(epi)s'
    2180
    2181                  cmd = """
    2182  select narrative
    2183  from clin.clin_narrative
    2184  where
    2185          fk_encounter = %%(enc)s
    2186                  and
    2187          soap_cat = %%(cat)s
    2188                  and
    2189          %s
    2190  order by clin_when desc
    2191  limit 1
    2192                  """ % epi_part
    2193
    2194                  args = {'enc': self.pk_obj, 'cat': soap_cat, 'epi': episode}
    2195
    2196                  rows, idx = gmPG2.run_ro_queries (
    2197                          queries = [{
    2198                                  'cmd': cmd,
    2199                                  'args': args
    2200                          }]
    2201                  )
    2202                  if len(rows) == 0:
    2203                          return None
    2204
    2205                  return rows[0][0]
    2206          #--------------------------------------------------------
    2207 -        def get_episodes(self, exclude=None):
    2208                  cmd = """
    2209                          SELECT * FROM clin.v_pat_episodes
    2210                          WHERE pk_episode IN (
    2211                                          SELECT DISTINCT fk_episode
    2212                                          FROM clin.clin_root_item
    2213                                          WHERE fk_encounter = %%(enc)s
    2214
    2215                                                  UNION
    2216
    2217                                          SELECT DISTINCT fk_episode
    2218                                          FROM blobs.doc_med
    2219                                          WHERE fk_encounter = %%(enc)s
    2220                          ) %s"""
    2221                  args = {'enc': self.pk_obj}
    2222                  if exclude is not None:
    2223                          cmd = cmd % 'AND pk_episode NOT IN %(excluded)s'
    2224                          args['excluded'] = tuple(exclude)
    2225                  else:
    2226                          cmd = cmd % ''
    2227
    2228                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2229
    2230                  return [ cEpisode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_episode'})  for r in rows ]
    2231
    2232          episodes = property(get_episodes, lambda x:x)
    2233          #--------------------------------------------------------
    2234 -        def add_code(self, pk_code=None, field=None):
    2235                  """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
    2236                  if field == 'rfe':
    2237                          cmd = "INSERT INTO clin.lnk_code2rfe (fk_item, fk_generic_code) values (%(item)s, %(code)s)"
    2238                  elif field == 'aoe':
    2239                          cmd = "INSERT INTO clin.lnk_code2aoe (fk_item, fk_generic_code) values (%(item)s, %(code)s)"
    2240                  else:
    2241                          raise ValueError('<field> must be one of "rfe" or "aoe", not "%s"', field)
    2242                  args = {
    2243                          'item': self._payload[self._idx['pk_encounter']],
    2244                          'code': pk_code
    2245                  }
    2246                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    2247                  return True
    2248          #--------------------------------------------------------
    2249 -        def remove_code(self, pk_code=None, field=None):
    2250                  """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
    2251                  if field == 'rfe':
    2252                          cmd = "DELETE FROM clin.lnk_code2rfe WHERE fk_item = %(item)s AND fk_generic_code = %(code)s"
    2253                  elif field == 'aoe':
    2254                          cmd = "DELETE FROM clin.lnk_code2aoe WHERE fk_item = %(item)s AND fk_generic_code = %(code)s"
    2255                  else:
    2256                          raise ValueError('<field> must be one of "rfe" or "aoe", not "%s"', field)
    2257                  args = {
    2258                          'item': self._payload[self._idx['pk_encounter']],
    2259                          'code': pk_code
    2260                  }
    2261                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    2262                  return True
    2263
    2264          #--------------------------------------------------------
    2265          # data formatting
    2266          #--------------------------------------------------------
    2267 -        def format_soap(self, episodes=None, left_margin=0, soap_cats='soapu', emr=None, issues=None):
    2268
    2269                  lines = []
    2270                  for soap_cat in gmSoapDefs.soap_cats2list(soap_cats):
    2271                          soap_cat_narratives = emr.get_clin_narrative (
    2272                                  episodes = episodes,
    2273                                  issues = issues,
    2274                                  encounters = [self._payload[self._idx['pk_encounter']]],
    2275                                  soap_cats = [soap_cat]
    2276                          )
    2277                          if soap_cat_narratives is None:
    2278                                  continue
    2279                          if len(soap_cat_narratives) == 0:
    2280                                  continue
    2281
    2282                          lines.append('%s%s %s %s' % (
    2283                                  gmTools.u_box_top_left_arc,
    2284                                  gmTools.u_box_horiz_single,
    2285                                  gmSoapDefs.soap_cat2l10n_str[soap_cat],
    2286                                  gmTools.u_box_horiz_single * 5
    2287                          ))
    2288                          for soap_entry in soap_cat_narratives:
    2289                                  txt = gmTools.wrap (
    2290                                          text = soap_entry['narrative'],
    2291                                          width = 75,
    2292                                          initial_indent = '',
    2293                                          subsequent_indent = (' ' * left_margin)
    2294                                  )
    2295                                  lines.append(txt)
    2296                                  when = gmDateTime.pydt_strftime (
    2297                                          soap_entry['date'],
    2298                                          format = '%Y-%m-%d %H:%M',
    2299                                          accuracy = gmDateTime.acc_minutes
    2300                                  )
    2301                                  txt = '%s%s %.8s, %s %s' % (
    2302                                          ' ' * 40,
    2303                                          gmTools.u_box_horiz_light_heavy,
    2304                                          soap_entry['modified_by'],
    2305                                          when,
    2306                                          gmTools.u_box_horiz_heavy_light
    2307                                  )
    2308                                  lines.append(txt)
    2309                                  lines.append('')
    2310
    2311                  return lines
    2312
    2313          #--------------------------------------------------------
    2314 -        def format_latex(self, date_format=None, soap_cats=None, soap_order=None):
    2315
    2316                  nothing2format = (
    2317                          (self._payload[self._idx['reason_for_encounter']] is None)
    2318                                  and
    2319                          (self._payload[self._idx['assessment_of_encounter']] is None)
    2320                                  and
    2321                          (self.has_soap_narrative(soap_cats = 'soapu') is False)
    2322                  )
    2323                  if nothing2format:
    2324                          return ''
    2325
    2326                  if date_format is None:
    2327                          date_format = '%A, %b %d %Y'
    2328
    2329                  tex =  '% -------------------------------------------------------------\n'
    2330                  tex += '% much recommended: \\usepackage(tabu)\n'
    2331                  tex += '% much recommended: \\usepackage(longtable)\n'
    2332                  tex += '% best wrapped in: "\\begin{longtabu} to \\textwidth {lX[,L]}"\n'
    2333                  tex += '% -------------------------------------------------------------\n'
    2334                  tex += '\\hline \n'
    2335                  tex += '\\multicolumn{2}{l}{%s: %s ({\\footnotesize %s - %s})} \\tabularnewline \n' % (
    2336                          gmTools.tex_escape_string(self._payload[self._idx['l10n_type']]),
    2337                          gmTools.tex_escape_string (
    2338                                  gmDateTime.pydt_strftime (
    2339                                          self._payload[self._idx['started']],
    2340                                          date_format,
    2341                                          accuracy = gmDateTime.acc_days
    2342                                  )
    2343                          ),
    2344                          gmTools.tex_escape_string (
    2345                                  gmDateTime.pydt_strftime (
    2346                                          self._payload[self._idx['started']],
    2347                                          '%H:%M',
    2348                                          accuracy = gmDateTime.acc_minutes
    2349                                  )
    2350                          ),
    2351                          gmTools.tex_escape_string (
    2352                                  gmDateTime.pydt_strftime (
    2353                                          self._payload[self._idx['last_affirmed']],
    2354                                          '%H:%M',
    2355                                          accuracy = gmDateTime.acc_minutes
    2356                                  )
    2357                          )
    2358                  )
    2359                  tex += '\\hline \n'
    2360
    2361                  if self._payload[self._idx['reason_for_encounter']] is not None:
    2362                          tex += '%s & %s \\tabularnewline \n' % (
    2363                                  gmTools.tex_escape_string(_('RFE')),
    2364                                  gmTools.tex_escape_string(self._payload[self._idx['reason_for_encounter']])
    2365                          )
    2366                  if self._payload[self._idx['assessment_of_encounter']] is not None:
    2367                          tex += '%s & %s \\tabularnewline \n' % (
    2368                                  gmTools.tex_escape_string(_('AOE')),
    2369                                  gmTools.tex_escape_string(self._payload[self._idx['assessment_of_encounter']])
    2370                          )
    2371
    2372                  for epi in self.get_episodes():
    2373                          soaps = epi.get_narrative(soap_cats = soap_cats, encounters = [self.pk_obj], order_by = soap_order)
    2374                          if len(soaps) == 0:
    2375                                  continue
    2376                          tex += '\\multicolumn{2}{l}{\\emph{%s: %s%s}} \\tabularnewline \n' % (
    2377                                  gmTools.tex_escape_string(_('Problem')),
    2378                                  gmTools.tex_escape_string(epi['description']),
    2379                                  gmTools.tex_escape_string (
    2380                                          gmTools.coalesce (
    2381                                                  initial = diagnostic_certainty_classification2str(epi['diagnostic_certainty_classification']),
    2382                                                  instead = '',
    2383                                                  template_initial = ' {\\footnotesize [%s]}',
    2384                                                  none_equivalents = [None, '']
    2385                                          )
    2386                                  )
    2387                          )
    2388                          if epi['pk_health_issue'] is not None:
    2389                                  tex += '\\multicolumn{2}{l}{\\emph{%s: %s%s}} \\tabularnewline \n' % (
    2390                                          gmTools.tex_escape_string(_('Health issue')),
    2391                                          gmTools.tex_escape_string(epi['health_issue']),
    2392                                          gmTools.tex_escape_string (
    2393                                                  gmTools.coalesce (
    2394                                                          initial = diagnostic_certainty_classification2str(epi['diagnostic_certainty_classification_issue']),
    2395                                                          instead = '',
    2396                                                          template_initial = ' {\\footnotesize [%s]}',
    2397                                                          none_equivalents = [None, '']
    2398                                                  )
    2399                                          )
    2400                                  )
    2401                          for soap in soaps:
    2402                                  tex += '{\\small %s} & {\\small %s} \\tabularnewline \n' % (
    2403                                          gmTools.tex_escape_string(gmSoapDefs.soap_cat2l10n[soap['soap_cat']]),
    2404                                          gmTools.tex_escape_string(soap['narrative'], replace_eol = True)
    2405                                  )
    2406                          tex += ' & \\tabularnewline \n'
    2407
    2408                  return tex
    2409
    2410          #--------------------------------------------------------
    2411 -        def __format_header_fancy(self, left_margin=0):
    2412                  lines = []
    2413
    2414                  lines.append('%s%s: %s - %s (@%s)%s [#%s]' % (
    2415                          ' ' * left_margin,
    2416                          self._payload[self._idx['l10n_type']],
    2417                          self._payload[self._idx['started_original_tz']].strftime('%Y-%m-%d %H:%M'),
    2418                          self._payload[self._idx['last_affirmed_original_tz']].strftime('%H:%M'),
    2419                          self._payload[self._idx['source_time_zone']],
    2420                          gmTools.coalesce (
    2421                                  self._payload[self._idx['assessment_of_encounter']],
    2422                                  '',
    2423                                  ' %s%%s%s' % (gmTools.u_left_double_angle_quote, gmTools.u_right_double_angle_quote)
    2424                          ),
    2425                          self._payload[self._idx['pk_encounter']]
    2426                  ))
    2427
    2428                  lines.append(_('  your time: %s - %s  (@%s = %s%s)\n') % (
    2429                          self._payload[self._idx['started']].strftime('%Y-%m-%d %H:%M'),
    2430                          self._payload[self._idx['last_affirmed']].strftime('%H:%M'),
    2431                          gmDateTime.current_local_iso_numeric_timezone_string,
    2432                          gmTools.bool2subst (
    2433                                  gmDateTime.dst_currently_in_effect,
    2434                                  gmDateTime.py_dst_timezone_name,
    2435                                  gmDateTime.py_timezone_name
    2436                          ),
    2437                          gmTools.bool2subst(gmDateTime.dst_currently_in_effect, ' - ' + _('daylight savings time in effect'), '')
    2438                  ))
    2439
    2440                  if self._payload[self._idx['praxis_branch']] is not None:
    2441                          lines.append(_('Location: %s (%s)') % (self._payload[self._idx['praxis_branch']], self._payload[self._idx['praxis']]))
    2442
    2443                  if self._payload[self._idx['reason_for_encounter']] is not None:
    2444                          lines.append('%s: %s' % (_('RFE'), self._payload[self._idx['reason_for_encounter']]))
    2445                          codes = self.generic_codes_rfe
    2446                          for c in codes:
    2447                                  lines.append(' %s: %s (%s - %s)' % (
    2448                                          c['code'],
    2449                                          c['term'],
    2450                                          c['name_short'],
    2451                                          c['version']
    2452                                  ))
    2453                          if len(codes) > 0:
    2454                                  lines.append('')
    2455
    2456                  if self._payload[self._idx['assessment_of_encounter']] is not None:
    2457                          lines.append('%s: %s' % (_('AOE'), self._payload[self._idx['assessment_of_encounter']]))
    2458                          codes = self.generic_codes_aoe
    2459                          for c in codes:
    2460                                  lines.append(' %s: %s (%s - %s)' % (
    2461                                          c['code'],
    2462                                          c['term'],
    2463                                          c['name_short'],
    2464                                          c['version']
    2465                                  ))
    2466                          if len(codes) > 0:
    2467                                  lines.append('')
    2468                          del codes
    2469                  return lines
    2470
    2471          #--------------------------------------------------------
    2472 -        def format_header(self, fancy_header=True, left_margin=0, with_rfe_aoe=False):
    2473                  lines = []
    2474
    2475                  if fancy_header:
    2476                          return self.__format_header_fancy(left_margin = left_margin)
    2477
    2478                  now = gmDateTime.pydt_now_here()
    2479                  if now.strftime('%Y-%m-%d') == self._payload[self._idx['started_original_tz']].strftime('%Y-%m-%d'):
    2480                          start = '%s %s' % (
    2481                                  _('today'),
    2482                                  self._payload[self._idx['started_original_tz']].strftime('%H:%M')
    2483                          )
    2484                  else:
    2485                          start = self._payload[self._idx['started_original_tz']].strftime('%Y-%m-%d %H:%M')
    2486                  lines.append('%s%s: %s - %s%s%s' % (
    2487                          ' ' * left_margin,
    2488                          self._payload[self._idx['l10n_type']],
    2489                          start,
    2490                          self._payload[self._idx['last_affirmed_original_tz']].strftime('%H:%M'),
    2491                          gmTools.coalesce(self._payload[self._idx['assessment_of_encounter']], '', ' \u00BB%s\u00AB'),
    2492                          gmTools.coalesce(self._payload[self._idx['praxis_branch']], '', ' @%s')
    2493                  ))
    2494                  if with_rfe_aoe:
    2495                          if self._payload[self._idx['reason_for_encounter']] is not None:
    2496                                  lines.append('%s: %s' % (_('RFE'), self._payload[self._idx['reason_for_encounter']]))
    2497                          codes = self.generic_codes_rfe
    2498                          for c in codes:
    2499                                  lines.append(' %s: %s (%s - %s)' % (
    2500                                          c['code'],
    2501                                          c['term'],
    2502                                          c['name_short'],
    2503                                          c['version']
    2504                                  ))
    2505                          if len(codes) > 0:
    2506                                  lines.append('')
    2507                          if self._payload[self._idx['assessment_of_encounter']] is not None:
    2508                                  lines.append('%s: %s' % (_('AOE'), self._payload[self._idx['assessment_of_encounter']]))
    2509                          codes = self.generic_codes_aoe
    2510                          if len(codes) > 0:
    2511                                  lines.append('')
    2512                          for c in codes:
    2513                                  lines.append(' %s: %s (%s - %s)' % (
    2514                                          c['code'],
    2515                                          c['term'],
    2516                                          c['name_short'],
    2517                                          c['version']
    2518                                  ))
    2519                          if len(codes) > 0:
    2520                                  lines.append('')
    2521                          del codes
    2522
    2523                  return lines
    2524
    2525          #--------------------------------------------------------
    2526 -        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):
    2527
    2528                  if patient is not None:
    2529                          emr = patient.emr
    2530
    2531                  lines = []
    2532                  if episodes is None:
    2533                          episodes = [ e['pk_episode'] for e in self.episodes ]
    2534
    2535                  for pk in episodes:
    2536                          epi = cEpisode(aPK_obj = pk)
    2537                          lines.append(_('\nEpisode %s%s%s%s:') % (
    2538                                  gmTools.u_left_double_angle_quote,
    2539                                  epi['description'],
    2540                                  gmTools.u_right_double_angle_quote,
    2541                                  gmTools.coalesce(epi['health_issue'], '', ' (%s)')
    2542                          ))
    2543
    2544                          # soap
    2545                          if with_soap:
    2546                                  if patient.ID != self._payload[self._idx['pk_patient']]:
    2547                                          msg = '<patient>.ID = %s but encounter %s belongs to patient %s' % (
    2548                                                  patient.ID,
    2549                                                  self._payload[self._idx['pk_encounter']],
    2550                                                  self._payload[self._idx['pk_patient']]
    2551                                          )
    2552                                          raise ValueError(msg)
    2553                                  lines.extend(self.format_soap (
    2554                                          episodes = [pk],
    2555                                          left_margin = left_margin,
    2556                                          soap_cats = None,               # meaning: all
    2557                                          emr = emr,
    2558                                          issues = issues
    2559                                  ))
    2560
    2561                          # test results
    2562                          if with_tests:
    2563                                  tests = emr.get_test_results_by_date (
    2564                                          episodes = [pk],
    2565                                          encounter = self._payload[self._idx['pk_encounter']]
    2566                                  )
    2567                                  if len(tests) > 0:
    2568                                          lines.append('')
    2569                                          lines.append(_('Measurements and Results:'))
    2570
    2571                                  for t in tests:
    2572                                          lines.append(t.format())
    2573
    2574                                  del tests
    2575
    2576                          # vaccinations
    2577                          if with_vaccinations:
    2578                                  vaccs = emr.get_vaccinations (
    2579                                          episodes = [pk],
    2580                                          encounters = [ self._payload[self._idx['pk_encounter']] ],
    2581                                          order_by = 'date_given DESC, vaccine'
    2582                                  )
    2583                                  if len(vaccs) > 0:
    2584                                          lines.append('')
    2585                                          lines.append(_('Vaccinations:'))
    2586                                  for vacc in vaccs:
    2587                                          lines.extend(vacc.format (
    2588                                                  with_indications = True,
    2589                                                  with_comment = True,
    2590                                                  with_reaction = True,
    2591                                                  date_format = '%Y-%m-%d'
    2592                                          ))
    2593                                  del vaccs
    2594
    2595                          # family history
    2596                          if with_family_history:
    2597                                  fhx = emr.get_family_history(episodes = [pk])
    2598                                  if len(fhx) > 0:
    2599                                          lines.append('')
    2600                                          lines.append(_('Family History: %s') % len(fhx))
    2601                                  for f in fhx:
    2602                                          lines.append(f.format (
    2603                                                  left_margin = (left_margin + 1),
    2604                                                  include_episode = False,
    2605                                                  include_comment = True
    2606                                          ))
    2607                                  del fhx
    2608
    2609                          # documents
    2610                          if with_docs:
    2611                                  doc_folder = patient.get_document_folder()
    2612                                  docs = doc_folder.get_documents (
    2613                                          pk_episodes = [pk],
    2614                                          encounter = self._payload[self._idx['pk_encounter']]
    2615                                  )
    2616                                  if len(docs) > 0:
    2617                                          lines.append('')
    2618                                          lines.append(_('Documents:'))
    2619                                  for d in docs:
    2620                                          lines.append(' ' + d.format(single_line = True))
    2621                                  del docs
    2622
    2623                  return lines
    2624
    2625          #--------------------------------------------------------
    2626 -        def format_maximum_information(self, patient=None):
    2627                  if patient is None:
    2628                          from Gnumed.business.gmPerson import gmCurrentPatient, cPerson
    2629                          if self._payload[self._idx['pk_patient']] == gmCurrentPatient().ID:
    2630                                  patient = gmCurrentPatient()
    2631                          else:
    2632                                  patient = cPerson(self._payload[self._idx['pk_patient']])
    2633
    2634                  return self.format (
    2635                          patient = patient,
    2636                          fancy_header = True,
    2637                          with_rfe_aoe = True,
    2638                          with_soap = True,
    2639                          with_docs = True,
    2640                          with_tests = False,
    2641                          with_vaccinations = True,
    2642                          with_co_encountlet_hints = True,
    2643                          with_family_history = True,
    2644                          by_episode = False,
    2645                          return_list = True
    2646                  )
    2647
    2648          #--------------------------------------------------------
    2649 -        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, return_list=False):
    2650                  """Format an encounter.
    2651
    2652                  with_co_encountlet_hints:
    2653                          - whether to include which *other* episodes were discussed during this encounter
    2654                          - (only makes sense if episodes != None)
    2655                  """
    2656                  lines = self.format_header (
    2657                          fancy_header = fancy_header,
    2658                          left_margin = left_margin,
    2659                          with_rfe_aoe = with_rfe_aoe
    2660                  )
    2661
    2662                  if patient is None:
    2663                          _log.debug('no patient, cannot load patient related data')
    2664                          with_soap = False
    2665                          with_tests = False
    2666                          with_vaccinations = False
    2667                          with_docs = False
    2668
    2669                  if by_episode:
    2670                          lines.extend(self.format_by_episode (
    2671                                  episodes = episodes,
    2672                                  issues = issues,
    2673                                  left_margin = left_margin,
    2674                                  patient = patient,
    2675                                  with_soap = with_soap,
    2676                                  with_tests = with_tests,
    2677                                  with_docs = with_docs,
    2678                                  with_vaccinations = with_vaccinations,
    2679                                  with_family_history = with_family_history
    2680                          ))
    2681                  else:
    2682                          if with_soap:
    2683                                  lines.append('')
    2684                                  if patient.ID != self._payload[self._idx['pk_patient']]:
    2685                                          msg = '<patient>.ID = %s but encounter %s belongs to patient %s' % (
    2686                                                  patient.ID,
    2687                                                  self._payload[self._idx['pk_encounter']],
    2688                                                  self._payload[self._idx['pk_patient']]
    2689                                          )
    2690                                          raise ValueError(msg)
    2691                                  emr = patient.emr
    2692                                  lines.extend(self.format_soap (
    2693                                          episodes = episodes,
    2694                                          left_margin = left_margin,
    2695                                          soap_cats = None,               # meaning: all
    2696                                          emr = emr,
    2697                                          issues = issues
    2698                                  ))
    2699
    2700          #               # family history
    2701          #               if with_family_history:
    2702          #                       if episodes is not None:
    2703          #                               fhx = emr.get_family_history(episodes = episodes)
    2704          #                               if len(fhx) > 0:
    2705          #                                       lines.append(u'')
    2706          #                                       lines.append(_('Family History: %s') % len(fhx))
    2707          #                               for f in fhx:
    2708          #                                       lines.append(f.format (
    2709          #                                               left_margin = (left_margin + 1),
    2710          #                                               include_episode = False,
    2711          #                                               include_comment = True
    2712          #                                       ))
    2713          #                               del fhx
    2714
    2715                          # test results
    2716                          if with_tests:
    2717                                  emr = patient.emr
    2718                                  tests = emr.get_test_results_by_date (
    2719                                          episodes = episodes,
    2720                                          encounter = self._payload[self._idx['pk_encounter']]
    2721                                  )
    2722                                  if len(tests) > 0:
    2723                                          lines.append('')
    2724                                          lines.append(_('Measurements and Results:'))
    2725                                  for t in tests:
    2726                                          lines.append(t.format())
    2727                                  del tests
    2728
    2729                          # vaccinations
    2730                          if with_vaccinations:
    2731                                  emr = patient.emr
    2732                                  vaccs = emr.get_vaccinations (
    2733                                          episodes = episodes,
    2734                                          encounters = [ self._payload[self._idx['pk_encounter']] ],
    2735                                          order_by = 'date_given DESC, vaccine'
    2736                                  )
    2737                                  if len(vaccs) > 0:
    2738                                          lines.append('')
    2739                                          lines.append(_('Vaccinations:'))
    2740                                  for vacc in vaccs:
    2741                                          lines.extend(vacc.format (
    2742                                                  with_indications = True,
    2743                                                  with_comment = True,
    2744                                                  with_reaction = True,
    2745                                                  date_format = '%Y-%m-%d'
    2746                                          ))
    2747                                  del vaccs
    2748
    2749                          # documents
    2750                          if with_docs:
    2751                                  doc_folder = patient.get_document_folder()
    2752                                  docs = doc_folder.get_documents (
    2753                                          pk_episodes = episodes,
    2754                                          encounter = self._payload[self._idx['pk_encounter']]
    2755                                  )
    2756                                  if len(docs) > 0:
    2757                                          lines.append('')
    2758                                          lines.append(_('Documents:'))
    2759                                  for d in docs:
    2760                                          lines.append(' ' + d.format(single_line = True))
    2761                                  del docs
    2762
    2763                          # co-encountlets
    2764                          if with_co_encountlet_hints:
    2765                                  if episodes is not None:
    2766                                          other_epis = self.get_episodes(exclude = episodes)
    2767                                          if len(other_epis) > 0:
    2768                                                  lines.append('')
    2769                                                  lines.append(_('%s other episodes touched upon during this encounter:') % len(other_epis))
    2770                                          for epi in other_epis:
    2771                                                  lines.append(' %s%s%s%s' % (
    2772                                                          gmTools.u_left_double_angle_quote,
    2773                                                          epi['description'],
    2774                                                          gmTools.u_right_double_angle_quote,
    2775                                                          gmTools.coalesce(epi['health_issue'], '', ' (%s)')
    2776                                                  ))
    2777
    2778                  if return_list:
    2779                          return lines
    2780
    2781                  eol_w_margin = '\n%s' % (' ' * left_margin)
    2782                  return '%s\n' % eol_w_margin.join(lines)
    2783
    2784          #--------------------------------------------------------
    2785          # properties
    2786          #--------------------------------------------------------
    2787 -        def _get_generic_codes_rfe(self):
    2788                  if len(self._payload[self._idx['pk_generic_codes_rfe']]) == 0:
    2789                          return []
    2790
    2791                  cmd = gmCoding._SQL_get_generic_linked_codes % 'pk_generic_code IN %(pks)s'
    2792                  args = {'pks': tuple(self._payload[self._idx['pk_generic_codes_rfe']])}
    2793                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2794                  return [ gmCoding.cGenericLinkedCode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_lnk_code2item'}) for r in rows ]
    2795
    2796 -        def _set_generic_codes_rfe(self, pk_codes):
    2797                  queries = []
    2798                  # remove all codes
    2799                  if len(self._payload[self._idx['pk_generic_codes_rfe']]) > 0:
    2800                          queries.append ({
    2801                                  'cmd': 'DELETE FROM clin.lnk_code2rfe WHERE fk_item = %(enc)s AND fk_generic_code IN %(codes)s',
    2802                                  'args': {
    2803                                          'enc': self._payload[self._idx['pk_encounter']],
    2804                                          'codes': tuple(self._payload[self._idx['pk_generic_codes_rfe']])
    2805                                  }
    2806                          })
    2807                  # add new codes
    2808                  for pk_code in pk_codes:
    2809                          queries.append ({
    2810                                  'cmd': 'INSERT INTO clin.lnk_code2rfe (fk_item, fk_generic_code) VALUES (%(enc)s, %(pk_code)s)',
    2811                                  'args': {
    2812                                          'enc': self._payload[self._idx['pk_encounter']],
    2813                                          'pk_code': pk_code
    2814                                  }
    2815                          })
    2816                  if len(queries) == 0:
    2817                          return
    2818                  # run it all in one transaction
    2819                  rows, idx = gmPG2.run_rw_queries(queries = queries)
    2820                  self.refetch_payload()
    2821                  return
    2822
    2823          generic_codes_rfe = property(_get_generic_codes_rfe, _set_generic_codes_rfe)
    2824          #--------------------------------------------------------
    2825 -        def _get_generic_codes_aoe(self):
    2826                  if len(self._payload[self._idx['pk_generic_codes_aoe']]) == 0:
    2827                          return []
    2828
    2829                  cmd = gmCoding._SQL_get_generic_linked_codes % 'pk_generic_code IN %(pks)s'
    2830                  args = {'pks': tuple(self._payload[self._idx['pk_generic_codes_aoe']])}
    2831                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2832                  return [ gmCoding.cGenericLinkedCode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_lnk_code2item'}) for r in rows ]
    2833
    2834 -        def _set_generic_codes_aoe(self, pk_codes):
    2835                  queries = []
    2836                  # remove all codes
    2837                  if len(self._payload[self._idx['pk_generic_codes_aoe']]) > 0:
    2838                          queries.append ({
    2839                                  'cmd': 'DELETE FROM clin.lnk_code2aoe WHERE fk_item = %(enc)s AND fk_generic_code IN %(codes)s',
    2840                                  'args': {
    2841                                          'enc': self._payload[self._idx['pk_encounter']],
    2842                                          'codes': tuple(self._payload[self._idx['pk_generic_codes_aoe']])
    2843                                  }
    2844                          })
    2845                  # add new codes
    2846                  for pk_code in pk_codes:
    2847                          queries.append ({
    2848                                  'cmd': 'INSERT INTO clin.lnk_code2aoe (fk_item, fk_generic_code) VALUES (%(enc)s, %(pk_code)s)',
    2849                                  'args': {
    2850                                          'enc': self._payload[self._idx['pk_encounter']],
    2851                                          'pk_code': pk_code
    2852                                  }
    2853                          })
    2854                  if len(queries) == 0:
    2855                          return
    2856                  # run it all in one transaction
    2857                  rows, idx = gmPG2.run_rw_queries(queries = queries)
    2858                  self.refetch_payload()
    2859                  return
    2860
    2861          generic_codes_aoe = property(_get_generic_codes_aoe, _set_generic_codes_aoe)
    2862          #--------------------------------------------------------
    2863 -        def _get_praxis_branch(self):
    2864                  if self._payload[self._idx['pk_org_unit']] is None:
    2865                          return None
    2866                  return gmPraxis.get_praxis_branch_by_org_unit(pk_org_unit = self._payload[self._idx['pk_org_unit']])
    2867
    2868          praxis_branch = property(_get_praxis_branch, lambda x:x)
    2869          #--------------------------------------------------------
    2870 -        def _get_org_unit(self):
    2871                  if self._payload[self._idx['pk_org_unit']] is None:
    2872                          return None
    2873                  return gmOrganization.cOrgUnit(aPK_obj = self._payload[self._idx['pk_org_unit']])
    2874
    2875          org_unit = property(_get_org_unit, lambda x:x)
    2876          #--------------------------------------------------------
    2877 -        def _get_formatted_revision_history(self):
    2878                  cmd = """SELECT
    2879                                  'NONE (live row)'::text as audit__action_applied,
    2880                                  NULL AS audit__action_when,
    2881                                  NULL AS audit__action_by,
    2882                                  pk_audit,
    2883                                  row_version,
    2884                                  modified_when,
    2885                                  modified_by,
    2886                                  pk, fk_patient, fk_type, fk_location, source_time_zone, reason_for_encounter, assessment_of_encounter, started, last_affirmed
    2887                          FROM clin.encounter
    2888                          WHERE pk = %(pk_encounter)s
    2889                  UNION ALL (
    2890                          SELECT
    2891                                  audit_action as audit__action_applied,
    2892                                  audit_when as audit__action_when,
    2893                                  audit_by as audit__action_by,
    2894                                  pk_audit,
    2895                                  orig_version as row_version,
    2896                                  orig_when as modified_when,
    2897                                  orig_by as modified_by,
    2898                                  pk, fk_patient, fk_type, fk_location, source_time_zone, reason_for_encounter, assessment_of_encounter, started, last_affirmed
    2899                          FROM audit.log_encounter
    2900                          WHERE pk = %(pk_encounter)s
    2901                  )
    2902                  ORDER BY row_version DESC
    2903                  """
    2904                  args = {'pk_encounter': self._payload[self._idx['pk_encounter']]}
    2905                  title = _('Encounter: %s%s%s') % (
    2906                          gmTools.u_left_double_angle_quote,
    2907                          self._payload[self._idx['l10n_type']],
    2908                          gmTools.u_right_double_angle_quote
    2909                  )
    2910                  return '\n'.join(self._get_revision_history(cmd, args, title))
    2911
    2912          formatted_revision_history = property(_get_formatted_revision_history, lambda x:x)
    2913
    2914  #-----------------------------------------------------------
    2915 -def create_encounter(fk_patient=None, enc_type=None):
    2916          """Creates a new encounter for a patient.
    2917
    2918          fk_patient - patient PK
    2919          enc_type - type of encounter
    2920          """
    2921          if enc_type is None:
    2922                  enc_type = 'in surgery'
    2923          # insert new encounter
    2924          queries = []
    2925          try:
    2926                  enc_type = int(enc_type)
    2927                  cmd = """
    2928                          INSERT INTO clin.encounter (fk_patient, fk_type, fk_location)
    2929                          VALUES (%(pat)s, %(typ)s, %(prax)s) RETURNING pk"""
    2930          except ValueError:
    2931                  enc_type = enc_type
    2932                  cmd = """
    2933                          INSERT INTO clin.encounter (fk_patient, fk_location, fk_type)
    2934                          VALUES (
    2935                                  %(pat)s,
    2936                                  %(prax)s,
    2937                                  coalesce (
    2938                                          (select pk from clin.encounter_type where description = %(typ)s),
    2939                                          -- pick the first available
    2940                                          (select pk from clin.encounter_type limit 1)
    2941                                  )
    2942                          ) RETURNING pk"""
    2943          praxis = gmPraxis.gmCurrentPraxisBranch()
    2944          args = {'pat': fk_patient, 'typ': enc_type, 'prax': praxis['pk_org_unit']}
    2945          queries.append({'cmd': cmd, 'args': args})
    2946          rows, idx = gmPG2.run_rw_queries(queries = queries, return_data = True, get_col_idx = False)
    2947          encounter = cEncounter(aPK_obj = rows[0]['pk'])
    2948
    2949          return encounter
    2950
    2951  #------------------------------------------------------------
    2952 -def lock_encounter(pk_encounter, exclusive=False, link_obj=None):
    2953          """Used to protect against deletion of active encounter from another client."""
    2954          return gmPG2.lock_row(link_obj = link_obj, table = 'clin.encounter', pk = pk_encounter, exclusive = exclusive)
    2955
    2956  #------------------------------------------------------------
    2957 -def unlock_encounter(pk_encounter, exclusive=False, link_obj=None):
    2958          return gmPG2.unlock_row(link_obj = link_obj, table = 'clin.encounter', pk = pk_encounter, exclusive = exclusive)
    2959
    2960  #-----------------------------------------------------------
    2961 -def delete_encounter(pk_encounter):
    2962          """Deletes an encounter by PK.
    2963
    2964          - attempts to obtain an exclusive lock which should
    2965            fail if the encounter is the active encounter in
    2966            this or any other client
    2967          - catches DB exceptions which should mostly be related
    2968            to clinical data already having been attached to
    2969            the encounter thus making deletion fail
    2970          """
    2971          conn = gmPG2.get_connection(readonly = False)
    2972          if not lock_encounter(pk_encounter, exclusive = True, link_obj = conn):
    2973                  _log.debug('cannot lock encounter [%s] for deletion, it seems in use', pk_encounter)
    2974                  return False
    2975          cmd = """DELETE FROM clin.encounter WHERE pk = %(enc)s"""
    2976          args = {'enc': pk_encounter}
    2977          try:
    2978                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    2979          except gmPG2.dbapi.Error:
    2980                  _log.exception('cannot delete encounter [%s]', pk_encounter)
    2981                  unlock_encounter(pk_encounter, exclusive = True, link_obj = conn)
    2982                  return False
    2983          unlock_encounter(pk_encounter, exclusive = True, link_obj = conn)
    2984          return True
    2985
    2986  #-----------------------------------------------------------
    2987  # encounter types handling
    2988  #-----------------------------------------------------------
    2989 -def update_encounter_type(description=None, l10n_description=None):
    2990
    2991          rows, idx = gmPG2.run_rw_queries(
    2992                  queries = [{
    2993                  'cmd': "select i18n.upd_tx(%(desc)s, %(l10n_desc)s)",
    2994                  'args': {'desc': description, 'l10n_desc': l10n_description}
    2995                  }],
    2996                  return_data = True
    2997          )
    2998
    2999          success = rows[0][0]
    3000          if not success:
    3001                  _log.warning('updating encounter type [%s] to [%s] failed', description, l10n_description)
    3002
    3003          return {'description': description, 'l10n_description': l10n_description}
    3004  #-----------------------------------------------------------
    3005 -def create_encounter_type(description=None, l10n_description=None):
    3006          """This will attempt to create a NEW encounter type."""
    3007
    3008          # need a system name, so derive one if necessary
    3009          if description is None:
    3010                  description = l10n_description
    3011
    3012          args = {
    3013                  'desc': description,
    3014                  'l10n_desc': l10n_description
    3015          }
    3016
    3017          _log.debug('creating encounter type: %s, %s', description, l10n_description)
    3018
    3019          # does it exist already ?
    3020          cmd = "select description, _(description) from clin.encounter_type where description = %(desc)s"
    3021          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
    3022
    3023          # yes
    3024          if len(rows) > 0:
    3025                  # both system and l10n name are the same so all is well
    3026                  if (rows[0][0] == description) and (rows[0][1] == l10n_description):
    3027                          _log.info('encounter type [%s] already exists with the proper translation')
    3028                          return {'description': description, 'l10n_description': l10n_description}
    3029
    3030                  # or maybe there just wasn't a translation to
    3031                  # the current language for this type yet ?
    3032                  cmd = "select exists (select 1 from i18n.translations where orig = %(desc)s and lang = i18n.get_curr_lang())"
    3033                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
    3034
    3035                  # there was, so fail
    3036                  if rows[0][0]:
    3037                          _log.error('encounter type [%s] already exists but with another translation')
    3038                          return None
    3039
    3040                  # else set it
    3041                  cmd = "select i18n.upd_tx(%(desc)s, %(l10n_desc)s)"
    3042                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    3043                  return {'description': description, 'l10n_description': l10n_description}
    3044
    3045          # no
    3046          queries = [
    3047                  {'cmd': "insert into clin.encounter_type (description) values (%(desc)s)", 'args': args},
    3048                  {'cmd': "select i18n.upd_tx(%(desc)s, %(l10n_desc)s)", 'args': args}
    3049          ]
    3050          rows, idx = gmPG2.run_rw_queries(queries = queries)
    3051
    3052          return {'description': description, 'l10n_description': l10n_description}
    3053
    3054  #-----------------------------------------------------------
    3055 -def get_most_commonly_used_encounter_type():
    3056          cmd = """
    3057                  SELECT
    3058                          COUNT(1) AS type_count,
    3059                          fk_type
    3060                  FROM clin.encounter
    3061                  GROUP BY fk_type
    3062                  ORDER BY type_count DESC
    3063                  LIMIT 1
    3064          """
    3065          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = False)
    3066          if len(rows) == 0:
    3067                  return None
    3068          return rows[0]['fk_type']
    3069
    3070  #-----------------------------------------------------------
    3071 -def get_encounter_types():
    3072          cmd = """
    3073                  SELECT
    3074                          _(description) AS l10n_description,
    3075                          description
    3076                  FROM
    3077                          clin.encounter_type
    3078                  ORDER BY
    3079                          l10n_description
    3080          """
    3081          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = False)
    3082          return rows
    3083
    3084  #-----------------------------------------------------------
    3085 -def get_encounter_type(description=None):
    3086          cmd = "SELECT * from clin.encounter_type where description = %s"
    3087          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [description]}])
    3088          return rows
    3089
    3090  #-----------------------------------------------------------
    3091 -def delete_encounter_type(description=None):
    3092          cmd = "delete from clin.encounter_type where description = %(desc)s"
    3093          args = {'desc': description}
    3094          try:
    3095                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    3096          except gmPG2.dbapi.IntegrityError as e:
    3097                  if e.pgcode == gmPG2.sql_error_codes.FOREIGN_KEY_VIOLATION:
    3098                          return False
    3099                  raise
    3100
    3101          return True
    3102
    3103  #============================================================
    3104 -class cProblem(gmBusinessDBObject.cBusinessDBObject):
    3105          """Represents one problem.
    3106
    3107          problems are the aggregation of
    3108                  .clinically_relevant=True issues and
    3109                  .is_open=True episodes
    3110          """
    3111          _cmd_fetch_payload = ''                                 # will get programmatically defined in __init__
    3112          _cmds_store_payload = ["select 1"]
    3113          _updatable_fields = []
    3114
    3115          #--------------------------------------------------------
    3116 -        def __init__(self, aPK_obj=None, try_potential_problems=False):
    3117                  """Initialize.
    3118
    3119                  aPK_obj must contain the keys
    3120                          pk_patient
    3121                          pk_episode
    3122                          pk_health_issue
    3123                  """
    3124                  if aPK_obj is None:
    3125                          raise gmExceptions.ConstructorError('cannot instatiate cProblem for PK: [%s]' % (aPK_obj))
    3126
    3127                  # As problems are rows from a view of different emr struct items,
    3128                  # the PK can't be a single field and, as some of the values of the
    3129                  # composed PK may be None, they must be queried using 'is null',
    3130                  # so we must programmatically construct the SQL query
    3131                  where_parts = []
    3132                  pk = {}
    3133                  for col_name in aPK_obj.keys():
    3134                          val = aPK_obj[col_name]
    3135                          if val is None:
    3136                                  where_parts.append('%s IS NULL' % col_name)
    3137                          else:
    3138                                  where_parts.append('%s = %%(%s)s' % (col_name, col_name))
    3139                                  pk[col_name] = val
    3140
    3141                  # try to instantiate from true problem view
    3142                  cProblem._cmd_fetch_payload = """
    3143                          SELECT *, False as is_potential_problem
    3144                          FROM clin.v_problem_list
    3145                          WHERE %s""" % ' AND '.join(where_parts)
    3146
    3147                  try:
    3148                          gmBusinessDBObject.cBusinessDBObject.__init__(self, aPK_obj=pk)
    3149                          return
    3150                  except gmExceptions.ConstructorError:
    3151                          _log.exception('actual problem not found, trying "potential" problems')
    3152                          if try_potential_problems is False:
    3153                                  raise
    3154
    3155                  # try to instantiate from potential-problems view
    3156                  cProblem._cmd_fetch_payload = """
    3157                          SELECT *, True as is_potential_problem
    3158                          FROM clin.v_potential_problem_list
    3159                          WHERE %s""" % ' AND '.join(where_parts)
    3160                  gmBusinessDBObject.cBusinessDBObject.__init__(self, aPK_obj=pk)
    3161          #--------------------------------------------------------
    3162 -        def get_as_episode(self):
    3163                  """
    3164                  Retrieve the cEpisode instance equivalent to this problem.
    3165                  The problem's type attribute must be 'episode'
    3166                  """
    3167                  if self._payload[self._idx['type']] != 'episode':
    3168                          _log.error('cannot convert problem [%s] of type [%s] to episode' % (self._payload[self._idx['problem']], self._payload[self._idx['type']]))
    3169                          return None
    3170                  return cEpisode(aPK_obj = self._payload[self._idx['pk_episode']])
    3171          #--------------------------------------------------------
    3172 -        def get_as_health_issue(self):
    3173                  """
    3174                  Retrieve the cHealthIssue instance equivalent to this problem.
    3175                  The problem's type attribute must be 'issue'
    3176                  """
    3177                  if self._payload[self._idx['type']] != 'issue':
    3178                          _log.error('cannot convert problem [%s] of type [%s] to health issue' % (self._payload[self._idx['problem']], self._payload[self._idx['type']]))
    3179                          return None
    3180                  return cHealthIssue(aPK_obj = self._payload[self._idx['pk_health_issue']])
    3181          #--------------------------------------------------------
    3182 -        def get_visual_progress_notes(self, encounter_id=None):
    3183
    3184                  if self._payload[self._idx['type']] == 'issue':
    3185                          latest = cHealthIssue(aPK_obj = self._payload[self._idx['pk_health_issue']]).latest_episode
    3186                          if latest is None:
    3187                                  return []
    3188                          episodes = [ latest ]
    3189
    3190                  emr = patient.emr
    3191
    3192                  doc_folder = gmDocuments.cDocumentFolder(aPKey = patient.ID)
    3193                  return doc_folder.get_visual_progress_notes (
    3194                          health_issue = self._payload[self._idx['pk_health_issue']],
    3195                          episode = self._payload[self._idx['pk_episode']]
    3196                  )
    3197
    3198          #--------------------------------------------------------
    3199          # properties
    3200          #--------------------------------------------------------
    3201          # doubles as 'diagnostic_certainty_description' getter:
    3202 -        def get_diagnostic_certainty_description(self):
    3203                  return diagnostic_certainty_classification2str(self._payload[self._idx['diagnostic_certainty_classification']])
    3204
    3205          diagnostic_certainty_description = property(get_diagnostic_certainty_description, lambda x:x)
    3206          #--------------------------------------------------------
    3207 -        def _get_generic_codes(self):
    3208                  if self._payload[self._idx['type']] == 'issue':
    3209                          cmd = """
    3210                                  SELECT * FROM clin.v_linked_codes WHERE
    3211                                          item_table = 'clin.lnk_code2h_issue'::regclass
    3212                                                  AND
    3213                                          pk_item = %(item)s
    3214                          """
    3215                          args = {'item': self._payload[self._idx['pk_health_issue']]}
    3216                  else:
    3217                          cmd = """
    3218                                  SELECT * FROM clin.v_linked_codes WHERE
    3219                                          item_table = 'clin.lnk_code2episode'::regclass
    3220                                                  AND
    3221                                          pk_item = %(item)s
    3222                          """
    3223                          args = {'item': self._payload[self._idx['pk_episode']]}
    3224
    3225                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    3226                  return [ gmCoding.cGenericLinkedCode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_lnk_code2item'}) for r in rows ]
    3227
    3228          generic_codes = property(_get_generic_codes, lambda x:x)
    3229  #-----------------------------------------------------------
    3230 -def problem2episode(problem=None):
    3231          """Retrieve the cEpisode instance equivalent to the given problem.
    3232
    3233          The problem's type attribute must be 'episode'
    3234
    3235          @param problem: The problem to retrieve its related episode for
    3236          @type problem: A gmEMRStructItems.cProblem instance
    3237          """
    3238          if isinstance(problem, cEpisode):
    3239                  return problem
    3240
    3241          exc = TypeError('cannot convert [%s] to episode' % problem)
    3242
    3243          if not isinstance(problem, cProblem):
    3244                  raise exc
    3245
    3246          if problem['type'] != 'episode':
    3247                  raise exc
    3248
    3249          return cEpisode(aPK_obj = problem['pk_episode'])
    3250  #-----------------------------------------------------------
    3251 -def problem2issue(problem=None):
    3252          """Retrieve the cIssue instance equivalent to the given problem.
    3253
    3254          The problem's type attribute must be 'issue'.
    3255
    3256          @param problem: The problem to retrieve the corresponding issue for
    3257          @type problem: A gmEMRStructItems.cProblem instance
    3258          """
    3259          if isinstance(problem, cHealthIssue):
    3260                  return problem
    3261
    3262          exc = TypeError('cannot convert [%s] to health issue' % problem)
    3263
    3264          if not isinstance(problem, cProblem):
    3265                  raise exc
    3266
    3267          if  problem['type'] != 'issue':
    3268                  raise exc
    3269
    3270          return cHealthIssue(aPK_obj = problem['pk_health_issue'])
    3271  #-----------------------------------------------------------
    3272 -def reclass_problem(self, problem=None):
    3273          """Transform given problem into either episode or health issue instance.
    3274          """
    3275          if isinstance(problem, (cEpisode, cHealthIssue)):
    3276                  return problem
    3277
    3278          exc = TypeError('cannot reclass [%s] instance to either episode or health issue' % type(problem))
    3279
    3280          if not isinstance(problem, cProblem):
    3281                  _log.debug('%s' % problem)
    3282                  raise exc
    3283
    3284          if problem['type'] == 'episode':
    3285                  return cEpisode(aPK_obj = problem['pk_episode'])
    3286
    3287          if problem['type'] == 'issue':
    3288                  return cHealthIssue(aPK_obj = problem['pk_health_issue'])
    3289
    3290          raise exc
    3291
    3292  #============================================================
    3293  _SQL_get_hospital_stays = "select * from clin.v_hospital_stays where %s"
    3294
    3295 -class cHospitalStay(gmBusinessDBObject.cBusinessDBObject):
    3296
    3297          _cmd_fetch_payload = _SQL_get_hospital_stays % "pk_hospital_stay = %s"
    3298          _cmds_store_payload = [
    3299                  """UPDATE clin.hospital_stay SET
    3300                                  clin_when = %(admission)s,
    3301                                  discharge = %(discharge)s,
    3302                                  fk_org_unit = %(pk_org_unit)s,
    3303                                  narrative = gm.nullify_empty_string(%(comment)s),
    3304                                  fk_episode = %(pk_episode)s,
    3305                                  fk_encounter = %(pk_encounter)s
    3306                          WHERE
    3307                                  pk = %(pk_hospital_stay)s
    3308                                          AND
    3309                                  xmin = %(xmin_hospital_stay)s
    3310                          RETURNING
    3311                                  xmin AS xmin_hospital_stay
    3312                          """
    3313          ]
    3314          _updatable_fields = [
    3315                  'admission',
    3316                  'discharge',
    3317                  'pk_org_unit',
    3318                  'pk_episode',
    3319                  'pk_encounter',
    3320                  'comment'
    3321          ]
    3322
    3323          #--------------------------------------------------------
    3324 -        def format_maximum_information(self, patient=None):
    3325                  return self.format (
    3326                          include_procedures = True,
    3327                          include_docs = True
    3328                  ).split('\n')
    3329
    3330          #-------------------------------------------------------
    3331 -        def format(self, left_margin=0, include_procedures=False, include_docs=False, include_episode=True):
    3332
    3333                  if self._payload[self._idx['discharge']] is not None:
    3334                          discharge = ' - %s' % gmDateTime.pydt_strftime(self._payload[self._idx['discharge']], '%Y %b %d')
    3335                  else:
    3336                          discharge = ''
    3337
    3338                  episode = ''
    3339                  if include_episode:
    3340                          episode = ': %s%s%s' % (
    3341                                  gmTools.u_left_double_angle_quote,
    3342                                  self._payload[self._idx['episode']],
    3343                                  gmTools.u_right_double_angle_quote
    3344                          )
    3345
    3346                  lines = ['%s%s%s (%s@%s)%s' % (
    3347                          ' ' * left_margin,
    3348                          gmDateTime.pydt_strftime(self._payload[self._idx['admission']], '%Y %b %d'),
    3349                          discharge,
    3350                          self._payload[self._idx['ward']],
    3351                          self._payload[self._idx['hospital']],
    3352                          episode
    3353                  )]
    3354
    3355                  if include_docs:
    3356                          for doc in self.documents:
    3357                                  lines.append('%s%s: %s\n' % (
    3358                                          ' ' * left_margin,
    3359                                          _('Document'),
    3360                                          doc.format(single_line = True)
    3361                                  ))
    3362
    3363                  return '\n'.join(lines)
    3364
    3365          #--------------------------------------------------------
    3366 -        def _get_documents(self):
    3367                  return [ gmDocuments.cDocument(aPK_obj = pk_doc) for pk_doc in  self._payload[self._idx['pk_documents']] ]
    3368
    3369          documents = property(_get_documents, lambda x:x)
    3370
    3371  #-----------------------------------------------------------
    3372 -def get_latest_patient_hospital_stay(patient=None):
    3373          cmd = _SQL_get_hospital_stays % "pk_patient = %(pat)s ORDER BY admission DESC LIMIT 1"
    3374          queries = [{
    3375                  # this assumes non-overarching stays
    3376                  #'cmd': u'SELECT * FROM clin.v_hospital_stays WHERE pk_patient = %(pat)s ORDER BY admission DESC LIMIT 1',
    3377                  'cmd': cmd,
    3378                  'args': {'pat': patient}
    3379          }]
    3380          rows, idx = gmPG2.run_ro_queries(queries = queries, get_col_idx = True)
    3381          if len(rows) == 0:
    3382                  return None
    3383          return cHospitalStay(row = {'idx': idx, 'data': rows[0], 'pk_field': 'pk_hospital_stay'})
    3384
    3385  #-----------------------------------------------------------
    3386 -def get_patient_hospital_stays(patient=None, ongoing_only=False):
    3387          args = {'pat': patient}
    3388          if ongoing_only:
    3389                  cmd = _SQL_get_hospital_stays % "pk_patient = %(pat)s AND discharge is NULL ORDER BY admission"
    3390          else:
    3391                  cmd = _SQL_get_hospital_stays % "pk_patient = %(pat)s ORDER BY admission"
    3392
    3393          queries = [{'cmd': cmd, 'args': args}]
    3394          rows, idx = gmPG2.run_ro_queries(queries = queries, get_col_idx = True)
    3395
    3396          return [ cHospitalStay(row = {'idx': idx, 'data': r, 'pk_field': 'pk_hospital_stay'})  for r in rows ]
    3397
    3398  #-----------------------------------------------------------
    3399 -def create_hospital_stay(encounter=None, episode=None, fk_org_unit=None):
    3400
    3401          queries = [{
    3402                   'cmd': 'INSERT INTO clin.hospital_stay (fk_encounter, fk_episode, fk_org_unit) VALUES (%(enc)s, %(epi)s, %(fk_org_unit)s) RETURNING pk',
    3403                   'args': {'enc': encounter, 'epi': episode, 'fk_org_unit': fk_org_unit}
    3404          }]
    3405          rows, idx = gmPG2.run_rw_queries(queries = queries, return_data = True)
    3406
    3407          return cHospitalStay(aPK_obj = rows[0][0])
    3408
    3409  #-----------------------------------------------------------
    3410 -def delete_hospital_stay(stay=None):
    3411          cmd = 'DELETE FROM clin.hospital_stay WHERE pk = %(pk)s'
    3412          args = {'pk': stay}
    3413          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    3414          return True
    3415
    3416  #============================================================
    3417  _SQL_get_procedures = "select * from clin.v_procedures where %s"
    3418
    3419 -class cPerformedProcedure(gmBusinessDBObject.cBusinessDBObject):
    3420
    3421          _cmd_fetch_payload = _SQL_get_procedures % "pk_procedure = %s"
    3422          _cmds_store_payload = [
    3423                  """UPDATE clin.procedure SET
    3424                                  soap_cat = 'p',
    3425                                  clin_when = %(clin_when)s,
    3426                                  clin_end = %(clin_end)s,
    3427                                  is_ongoing = %(is_ongoing)s,
    3428                                  narrative = gm.nullify_empty_string(%(performed_procedure)s),
    3429                                  fk_hospital_stay = %(pk_hospital_stay)s,
    3430                                  fk_org_unit = (CASE
    3431                                          WHEN %(pk_hospital_stay)s IS NULL THEN %(pk_org_unit)s
    3432                                          ELSE NULL
    3433                                  END)::integer,
    3434                                  fk_episode = %(pk_episode)s,
    3435                                  fk_encounter = %(pk_encounter)s,
    3436                                  fk_doc = %(pk_doc)s,
    3437                                  comment = gm.nullify_empty_string(%(comment)s)
    3438                          WHERE
    3439                                  pk = %(pk_procedure)s AND
    3440                                  xmin = %(xmin_procedure)s
    3441                          RETURNING xmin as xmin_procedure"""
    3442          ]
    3443          _updatable_fields = [
    3444                  'clin_when',
    3445                  'clin_end',
    3446                  'is_ongoing',
    3447                  'performed_procedure',
    3448                  'pk_hospital_stay',
    3449                  'pk_org_unit',
    3450                  'pk_episode',
    3451                  'pk_encounter',
    3452                  'pk_doc',
    3453                  'comment'
    3454          ]
    3455          #-------------------------------------------------------
    3456 -        def __setitem__(self, attribute, value):
    3457
    3458                  if (attribute == 'pk_hospital_stay') and (value is not None):
    3459                          gmBusinessDBObject.cBusinessDBObject.__setitem__(self, 'pk_org_unit', None)
    3460
    3461                  if (attribute == 'pk_org_unit') and (value is not None):
    3462                          gmBusinessDBObject.cBusinessDBObject.__setitem__(self, 'pk_hospital_stay', None)
    3463
    3464                  gmBusinessDBObject.cBusinessDBObject.__setitem__(self, attribute, value)
    3465
    3466          #--------------------------------------------------------
    3467 -        def format_maximum_information(self, left_margin=0, patient=None):
    3468                  return self.format (
    3469                          left_margin = left_margin,
    3470                          include_episode = True,
    3471                          include_codes = True,
    3472                          include_address = True,
    3473                          include_comm = True,
    3474                          include_doc = True
    3475                  ).split('\n')
    3476
    3477          #-------------------------------------------------------
    3478 -        def format(self, left_margin=0, include_episode=True, include_codes=False, include_address=False, include_comm=False, include_doc=False):
    3479
    3480                  if self._payload[self._idx['is_ongoing']]:
    3481                          end = _(' (ongoing)')
    3482                  else:
    3483                          end = self._payload[self._idx['clin_end']]
    3484                          if end is None:
    3485                                  end = ''
    3486                          else:
    3487                                  end = ' - %s' % gmDateTime.pydt_strftime(end, '%Y %b %d')
    3488
    3489                  line = '%s%s%s: %s%s [%s @ %s]' % (
    3490                          (' ' * left_margin),
    3491                          gmDateTime.pydt_strftime(self._payload[self._idx['clin_when']], '%Y %b %d'),
    3492                          end,
    3493                          self._payload[self._idx['performed_procedure']],
    3494                          gmTools.bool2str(include_episode, ' (%s)' % self._payload[self._idx['episode']], ''),
    3495                          self._payload[self._idx['unit']],
    3496                          self._payload[self._idx['organization']]
    3497                  )
    3498
    3499                  line += gmTools.coalesce(self._payload[self._idx['comment']], '', '\n' + (' ' * left_margin) + _('Comment: ') + '%s')
    3500
    3501                  if include_comm:
    3502                          for channel in self.org_unit.comm_channels:
    3503                                  line += ('\n%(comm_type)s: %(url)s' % channel)
    3504
    3505                  if include_address:
    3506                          adr = self.org_unit.address
    3507                          if adr is not None:
    3508                                  line += '\n'
    3509                                  line += '\n'.join(adr.format(single_line = False, show_type = False))
    3510                                  line += '\n'
    3511
    3512                  if include_doc:
    3513                          doc = self.doc
    3514                          if doc is not None:
    3515                                  line += '\n'
    3516                                  line += _('Document') + ': ' + doc.format(single_line = True)
    3517                                  line += '\n'
    3518
    3519                  if include_codes:
    3520                          codes = self.generic_codes
    3521                          if len(codes) > 0:
    3522                                  line += '\n'
    3523                          for c in codes:
    3524                                  line += '%s  %s: %s (%s - %s)\n' % (
    3525                                          (' ' * left_margin),
    3526                                          c['code'],
    3527                                          c['term'],
    3528                                          c['name_short'],
    3529                                          c['version']
    3530                                  )
    3531                          del codes
    3532
    3533                  return line
    3534
    3535          #--------------------------------------------------------
    3536 -        def add_code(self, pk_code=None):
    3537                  """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
    3538                  cmd = "INSERT INTO clin.lnk_code2procedure (fk_item, fk_generic_code) values (%(issue)s, %(code)s)"
    3539                  args = {
    3540                          'issue': self._payload[self._idx['pk_procedure']],
    3541                          'code': pk_code
    3542                  }
    3543                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    3544                  return True
    3545
    3546          #--------------------------------------------------------
    3547 -        def remove_code(self, pk_code=None):
    3548                  """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
    3549                  cmd = "DELETE FROM clin.lnk_code2procedure WHERE fk_item = %(issue)s AND fk_generic_code = %(code)s"
    3550                  args = {
    3551                          'issue': self._payload[self._idx['pk_procedure']],
    3552                          'code': pk_code
    3553                  }
    3554                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    3555                  return True
    3556
    3557          #--------------------------------------------------------
    3558          # properties
    3559          #--------------------------------------------------------
    3560 -        def _get_stay(self):
    3561                  if self._payload[self._idx['pk_hospital_stay']] is None:
    3562                          return None
    3563                  return cHospitalStay(aPK_obj = self._payload[self._idx['pk_hospital_stay']])
    3564
    3565          hospital_stay = property(_get_stay, lambda x:x)
    3566
    3567          #--------------------------------------------------------
    3568 -        def _get_org_unit(self):
    3569                  return gmOrganization.cOrgUnit(self._payload[self._idx['pk_org_unit']])
    3570
    3571          org_unit = property(_get_org_unit, lambda x:x)
    3572
    3573          #--------------------------------------------------------
    3574 -        def _get_doc(self):
    3575                  if self._payload[self._idx['pk_doc']] is None:
    3576                          return None
    3577                  return gmDocuments.cDocument(aPK_obj = self._payload[self._idx['pk_doc']])
    3578
    3579          doc = property(_get_doc, lambda x:x)
    3580
    3581          #--------------------------------------------------------
    3582 -        def _get_generic_codes(self):
    3583                  if len(self._payload[self._idx['pk_generic_codes']]) == 0:
    3584                          return []
    3585
    3586                  cmd = gmCoding._SQL_get_generic_linked_codes % 'pk_generic_code IN %(pks)s'
    3587                  args = {'pks': tuple(self._payload[self._idx['pk_generic_codes']])}
    3588                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    3589                  return [ gmCoding.cGenericLinkedCode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_lnk_code2item'}) for r in rows ]
    3590
    3591 -        def _set_generic_codes(self, pk_codes):
    3592                  queries = []
    3593                  # remove all codes
    3594                  if len(self._payload[self._idx['pk_generic_codes']]) > 0:
    3595                          queries.append ({
    3596                                  'cmd': 'DELETE FROM clin.lnk_code2procedure WHERE fk_item = %(proc)s AND fk_generic_code IN %(codes)s',
    3597                                  'args': {
    3598                                          'proc': self._payload[self._idx['pk_procedure']],
    3599                                          'codes': tuple(self._payload[self._idx['pk_generic_codes']])
    3600                                  }
    3601                          })
    3602                  # add new codes
    3603                  for pk_code in pk_codes:
    3604                          queries.append ({
    3605                                  'cmd': 'INSERT INTO clin.lnk_code2procedure (fk_item, fk_generic_code) VALUES (%(proc)s, %(pk_code)s)',
    3606                                  'args': {
    3607                                          'proc': self._payload[self._idx['pk_procedure']],
    3608                                          'pk_code': pk_code
    3609                                  }
    3610                          })
    3611                  if len(queries) == 0:
    3612                          return
    3613                  # run it all in one transaction
    3614                  rows, idx = gmPG2.run_rw_queries(queries = queries)
    3615                  return
    3616
    3617          generic_codes = property(_get_generic_codes, _set_generic_codes)
    3618
    3619  #-----------------------------------------------------------
    3620 -def get_performed_procedures(patient=None):
    3621
    3622          queries = [{
    3623                  'cmd': 'SELECT * FROM clin.v_procedures WHERE pk_patient = %(pat)s ORDER BY clin_when',
    3624                  'args': {'pat': patient}
    3625          }]
    3626          rows, idx = gmPG2.run_ro_queries(queries = queries, get_col_idx = True)
    3627          return [ cPerformedProcedure(row = {'idx': idx, 'data': r, 'pk_field': 'pk_procedure'})  for r in rows ]
    3628
    3629  #-----------------------------------------------------------
    3630 -def get_procedures4document(pk_document=None):
    3631          args = {'pk_doc': pk_document}
    3632          cmd = _SQL_get_procedures % 'pk_doc = %(pk_doc)s'
    3633          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    3634          return [ cPerformedProcedure(row = {'idx': idx, 'data': r, 'pk_field': 'pk_procedure'})  for r in rows ]
    3635
    3636  #-----------------------------------------------------------
    3637 -def get_latest_performed_procedure(patient=None):
    3638          queries = [{
    3639                  'cmd': 'select * FROM clin.v_procedures WHERE pk_patient = %(pat)s ORDER BY clin_when DESC LIMIT 1',
    3640                  'args': {'pat': patient}
    3641          }]
    3642          rows, idx = gmPG2.run_ro_queries(queries = queries, get_col_idx = True)
    3643          if len(rows) == 0:
    3644                  return None
    3645          return cPerformedProcedure(row = {'idx': idx, 'data': rows[0], 'pk_field': 'pk_procedure'})
    3646
    3647  #-----------------------------------------------------------
    3648 -def create_performed_procedure(encounter=None, episode=None, location=None, hospital_stay=None, procedure=None):
    3649
    3650          queries = [{
    3651                  'cmd': """
    3652                          INSERT INTO clin.procedure (
    3653                                  fk_encounter,
    3654                                  fk_episode,
    3655                                  soap_cat,
    3656                                  fk_org_unit,
    3657                                  fk_hospital_stay,
    3658                                  narrative
    3659                          ) VALUES (
    3660                                  %(enc)s,
    3661                                  %(epi)s,
    3662                                  'p',
    3663                                  %(loc)s,
    3664                                  %(stay)s,
    3665                                  gm.nullify_empty_string(%(proc)s)
    3666                          )
    3667                          RETURNING pk""",
    3668                  'args': {'enc': encounter, 'epi': episode, 'loc': location, 'stay': hospital_stay, 'proc': procedure}
    3669          }]
    3670
    3671          rows, idx = gmPG2.run_rw_queries(queries = queries, return_data = True)
    3672
    3673          return cPerformedProcedure(aPK_obj = rows[0][0])
    3674
    3675  #-----------------------------------------------------------
    3676 -def delete_performed_procedure(procedure=None):
    3677          cmd = 'delete from clin.procedure where pk = %(pk)s'
    3678          args = {'pk': procedure}
    3679          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    3680          return True
    3681
    3682  #============================================================
    3683 -def export_emr_structure(patient=None, filename=None):
    3684
    3685          if filename is None:
    3686                  filename = gmTools.get_unique_filename(prefix = 'gm-emr_struct-%s-' % patient.subdir_name, suffix = '.txt')
    3687
    3688          f = io.open(filename, 'w+', encoding = 'utf8')
    3689
    3690          f.write('patient [%s]\n' % patient['description_gender'])
    3691          emr = patient.emr
    3692          for issue in emr.health_issues:
    3693                  f.write('\n')
    3694                  f.write('\n')
    3695                  f.write('issue [%s] #%s\n' % (issue['description'], issue['pk_health_issue']))
    3696                  f.write(' is active     : %s\n' % issue['is_active'])
    3697                  f.write(' has open epi  : %s\n' % issue['has_open_episode'])
    3698                  f.write(' possible start: %s\n' % issue.possible_start_date)
    3699                  f.write(' safe start    : %s\n' % issue.safe_start_date)
    3700                  end = issue.clinical_end_date
    3701                  if end is None:
    3702                          f.write(' end           : active and/or open episode\n')
    3703                  else:
    3704                          f.write(' end           : %s\n' % end)
    3705                  f.write(' latest access : %s\n' % issue.latest_access_date)
    3706                  first = issue.first_episode
    3707                  if first is not None:
    3708                          first = first['description']
    3709                  f.write(' 1st episode   : %s\n' % first)
    3710                  last = issue.latest_episode
    3711                  if last is not None:
    3712                          last = last['description']
    3713                  f.write(' latest episode: %s\n' % last)
    3714                  epis = sorted(issue.get_episodes(), key = lambda e: e.best_guess_clinical_start_date)
    3715                  for epi in epis:
    3716                          f.write('\n')
    3717                          f.write(' episode [%s] #%s\n' % (epi['description'], epi['pk_episode']))
    3718                          f.write('  is open         : %s\n' % epi['episode_open'])
    3719                          f.write('  best guess start: %s\n' % epi.best_guess_clinical_start_date)
    3720                          f.write('  best guess end  : %s\n' % epi.best_guess_clinical_end_date)
    3721                          f.write('  latest access   : %s\n' % epi.latest_access_date)
    3722                          f.write('  start 1st enc   : %s\n' % epi['started_first'])
    3723                          f.write('  start last enc  : %s\n' % epi['started_last'])
    3724                          f.write('  end last enc    : %s\n' % epi['last_affirmed'])
    3725
    3726          f.close()
    3727          return filename
    3728
    3729  #============================================================
    3730  # tools
    3731  #------------------------------------------------------------
    3732 -def check_fk_encounter_fk_episode_x_ref():
    3733
    3734          aggregate_result = 0
    3735
    3736          fks_linking2enc = gmPG2.get_foreign_keys2column(schema = 'clin', table = 'encounter', column = 'pk')
    3737          tables_linking2enc = set([ r['referencing_table'] for r in fks_linking2enc ])
    3738
    3739          fks_linking2epi = gmPG2.get_foreign_keys2column(schema = 'clin', table = 'episode', column = 'pk')
    3740          tables_linking2epi = [ r['referencing_table'] for r in fks_linking2epi ]
    3741
    3742          tables_linking2both = tables_linking2enc.intersection(tables_linking2epi)
    3743
    3744          tables_linking2enc = {}
    3745          for fk in fks_linking2enc:
    3746                  table = fk['referencing_table']
    3747                  tables_linking2enc[table] = fk
    3748
    3749          tables_linking2epi = {}
    3750          for fk in fks_linking2epi:
    3751                  table = fk['referencing_table']
    3752                  tables_linking2epi[table] = fk
    3753
    3754          for t in tables_linking2both:
    3755
    3756                  table_file_name = 'x-check_enc_epi_xref-%s.log' % t
    3757                  table_file = io.open(table_file_name, 'w+', encoding = 'utf8')
    3758
    3759                  # get PK column
    3760                  args = {'table': t}
    3761                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': gmPG2.SQL_get_pk_col_def, 'args': args}])
    3762                  pk_col = rows[0][0]
    3763                  print("checking table:", t, '- pk col:', pk_col)
    3764                  print(' =>', table_file_name)
    3765                  table_file.write('table: %s\n' % t)
    3766                  table_file.write('PK col: %s\n' % pk_col)
    3767
    3768                  # get PKs
    3769                  cmd = 'select %s from %s' % (pk_col, t)
    3770                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
    3771                  pks = [ r[0] for r in rows ]
    3772                  for pk in pks:
    3773                          args = {'pk': pk, 'tbl': t}
    3774                          enc_cmd = "select fk_patient from clin.encounter where pk = (select fk_encounter from %s where %s = %%(pk)s)" % (t, pk_col)
    3775                          epi_cmd = "select fk_patient from clin.encounter where pk = (select fk_encounter from clin.episode where pk = (select fk_episode from %s where %s = %%(pk)s))" % (t, pk_col)
    3776                          enc_rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': enc_cmd, 'args': args}])
    3777                          epi_rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': epi_cmd, 'args': args}])
    3778                          enc_pat = enc_rows[0][0]
    3779                          epi_pat = epi_rows[0][0]
    3780                          args['pat_enc'] = enc_pat
    3781                          args['pat_epi'] = epi_pat
    3782                          if epi_pat != enc_pat:
    3783                                  print(' mismatch: row pk=%s, enc pat=%s, epi pat=%s' % (pk, enc_pat, epi_pat))
    3784                                  aggregate_result = -2
    3785
    3786                                  table_file.write('--------------------------------------------------------------------------------\n')
    3787                                  table_file.write('mismatch on row with pk: %s\n' % pk)
    3788                                  table_file.write('\n')
    3789
    3790                                  table_file.write('journal entry:\n')
    3791                                  cmd = 'SELECT * from clin.v_emr_journal where src_table = %(tbl)s AND src_pk = %(pk)s'
    3792                                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
    3793                                  if len(rows) > 0:
    3794                                          table_file.write(gmTools.format_dict_like(rows[0], left_margin = 1, tabular = False, value_delimiters = None))
    3795                                  table_file.write('\n\n')
    3796
    3797                                  table_file.write('row data:\n')
    3798                                  cmd = 'SELECT * from %s where %s = %%(pk)s' % (t, pk_col)
    3799                                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
    3800                                  table_file.write(gmTools.format_dict_like(rows[0], left_margin = 1, tabular = False, value_delimiters = None))
    3801                                  table_file.write('\n\n')
    3802
    3803                                  table_file.write('episode:\n')
    3804                                  cmd = 'SELECT * from clin.v_pat_episodes WHERE pk_episode = (select fk_episode from %s where %s = %%(pk)s)' % (t, pk_col)
    3805                                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
    3806                                  table_file.write(gmTools.format_dict_like(rows[0], left_margin = 1, tabular = False, value_delimiters = None))
    3807                                  table_file.write('\n\n')
    3808
    3809                                  table_file.write('patient of episode:\n')
    3810                                  cmd = 'SELECT * FROM dem.v_persons WHERE pk_identity = %(pat_epi)s'
    3811                                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
    3812                                  table_file.write(gmTools.format_dict_like(rows[0], left_margin = 1, tabular = False, value_delimiters = None))
    3813                                  table_file.write('\n\n')
    3814
    3815                                  table_file.write('encounter:\n')
    3816                                  cmd = 'SELECT * from clin.v_pat_encounters WHERE pk_encounter = (select fk_encounter from %s where %s = %%(pk)s)' % (t, pk_col)
    3817                                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
    3818                                  table_file.write(gmTools.format_dict_like(rows[0], left_margin = 1, tabular = False, value_delimiters = None))
    3819                                  table_file.write('\n\n')
    3820
    3821                                  table_file.write('patient of encounter:\n')
    3822                                  cmd = 'SELECT * FROM dem.v_persons WHERE pk_identity = %(pat_enc)s'
    3823                                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
    3824                                  table_file.write(gmTools.format_dict_like(rows[0], left_margin = 1, tabular = False, value_delimiters = None))
    3825                                  table_file.write('\n')
    3826
    3827                  table_file.write('done\n')
    3828                  table_file.close()
    3829
    3830          return aggregate_result
    3831
    3832  #------------------------------------------------------------
    3833 -def export_patient_emr_structure():
    3834          from Gnumed.business import gmPersonSearch
    3835          praxis = gmPraxis.gmCurrentPraxisBranch(branch = gmPraxis.get_praxis_branches()[0])
    3836          pat = gmPersonSearch.ask_for_patient()
    3837          while pat is not None:
    3838                  print('patient:', pat['description_gender'])
    3839                  fname = os.path.expanduser('~/gnumed/gm-emr_structure-%s.txt' % pat.subdir_name)
    3840                  print('exported into:', export_emr_structure(patient = pat, filename = fname))
    3841                  pat = gmPersonSearch.ask_for_patient()
    3842
    3843          return 0
    3844
    3845  #============================================================
    3846  # main - unit testing
    3847  #------------------------------------------------------------
    3848  if __name__ == '__main__':
    3849
    3850          if len(sys.argv) < 2:
    3851                  sys.exit()
    3852
    3853          if sys.argv[1] != 'test':
    3854                  sys.exit()
    3855
    3856          #--------------------------------------------------------
    3857          # define tests
    3858          #--------------------------------------------------------
    3859 -        def test_problem():
    3860                  print("\nProblem test")
    3861                  print("------------")
    3862                  prob = cProblem(aPK_obj={'pk_patient': 12, 'pk_health_issue': 1, 'pk_episode': None})
    3863                  print(prob)
    3864                  fields = prob.get_fields()
    3865                  for field in fields:
    3866                          print(field, ':', prob[field])
    3867                  print('\nupdatable:', prob.get_updatable_fields())
    3868                  epi = prob.get_as_episode()
    3869                  print('\nas episode:')
    3870                  if epi is not None:
    3871                          for field in epi.get_fields():
    3872                                  print('   .%s : %s' % (field, epi[field]))
    3873
    3874          #--------------------------------------------------------
    3875 -        def test_health_issue():
    3876                  print("\nhealth issue test")
    3877                  print("-----------------")
    3878                  h_issue = cHealthIssue(aPK_obj=2)
    3879                  print(h_issue)
    3880                  print(h_issue.latest_access_date)
    3881                  print(h_issue.clinical_end_date)
    3882  #               fields = h_issue.get_fields()
    3883  #               for field in fields:
    3884  #                       print field, ':', h_issue[field]
    3885  #               print "has open episode:", h_issue.has_open_episode()
    3886  #               print "open episode:", h_issue.get_open_episode()
    3887  #               print "updateable:", h_issue.get_updatable_fields()
    3888  #               h_issue.close_expired_episode(ttl=7300)
    3889  #               h_issue = cHealthIssue(encounter = 1, name = u'post appendectomy/peritonitis')
    3890  #               print h_issue
    3891  #               print h_issue.format_as_journal()
    3892                  print(h_issue.formatted_revision_history)
    3893
    3894          #--------------------------------------------------------
    3895 -        def test_episode():
    3896                  print("episode test")
    3897                  print("------------")
    3898                  episode = cEpisode(aPK_obj = 1322) #1674) #1354) #1461) #1299)
    3899
    3900                  print(episode['description'])
    3901                  print('start:', episode.best_guess_clinical_start_date)
    3902                  print('end  :', episode.best_guess_clinical_end_date)
    3903                  return
    3904
    3905                  print(episode)
    3906                  fields = episode.get_fields()
    3907                  for field in fields:
    3908                          print(field, ':', episode[field])
    3909                  print("updatable:", episode.get_updatable_fields())
    3910                  input('ENTER to continue')
    3911
    3912                  old_description = episode['description']
    3913                  old_enc = cEncounter(aPK_obj = 1)
    3914
    3915                  desc = '1-%s' % episode['description']
    3916                  print("==> renaming to", desc)
    3917                  successful = episode.rename (
    3918                          description = desc
    3919                  )
    3920                  if not successful:
    3921                          print("error")
    3922                  else:
    3923                          print("success")
    3924                          for field in fields:
    3925                                  print(field, ':', episode[field])
    3926
    3927                  print(episode.formatted_revision_history)
    3928
    3929                  input('ENTER to continue')
    3930
    3931          #--------------------------------------------------------
    3932 -        def test_encounter():
    3933                  print("\nencounter test")
    3934                  print("--------------")
    3935                  encounter = cEncounter(aPK_obj=1)
    3936                  print(encounter)
    3937                  fields = encounter.get_fields()
    3938                  for field in fields:
    3939                          print(field, ':', encounter[field])
    3940                  print("updatable:", encounter.get_updatable_fields())
    3941                  #print encounter.formatted_revision_history
    3942                  print(encounter.transfer_all_data_to_another_encounter(pk_target_encounter = 2))
    3943
    3944          #--------------------------------------------------------
    3945 -        def test_encounter2latex():
    3946                  encounter = cEncounter(aPK_obj=1)
    3947                  print(encounter)
    3948                  print("")
    3949                  print(encounter.format_latex())
    3950          #--------------------------------------------------------
    3951 -        def test_performed_procedure():
    3952                  procs = get_performed_procedures(patient = 12)
    3953                  for proc in procs:
    3954                          print(proc.format(left_margin=2))
    3955          #--------------------------------------------------------
    3956 -        def test_hospital_stay():
    3957                  stay = create_hospital_stay(encounter = 1, episode = 2, fk_org_unit = 1)
    3958  #               stay['hospital'] = u'Starfleet Galaxy General Hospital'
    3959  #               stay.save_payload()
    3960                  print(stay)
    3961                  for s in get_patient_hospital_stays(12):
    3962                          print(s)
    3963                  delete_hospital_stay(stay['pk_hospital_stay'])
    3964                  stay = create_hospital_stay(encounter = 1, episode = 4, fk_org_unit = 1)
    3965          #--------------------------------------------------------
    3966 -        def test_diagnostic_certainty_classification_map():
    3967                  tests = [None, 'A', 'B', 'C', 'D', 'E']
    3968
    3969                  for t in tests:
    3970                          print(type(t), t)
    3971                          print(type(diagnostic_certainty_classification2str(t)), diagnostic_certainty_classification2str(t))
    3972          #--------------------------------------------------------
    3973 -        def test_episode_codes():
    3974                  epi = cEpisode(aPK_obj = 2)
    3975                  print(epi)
    3976                  print(epi.generic_codes)
    3977          #--------------------------------------------------------
    3978 -        def test_episode_encounters():
    3979                  epi = cEpisode(aPK_obj = 1638)
    3980                  print(epi.format())
    3981
    3982          #--------------------------------------------------------
    3983 -        def test_export_emr_structure():
    3984                  export_patient_emr_structure()
    3985                  #praxis = gmPraxis.gmCurrentPraxisBranch(branch = gmPraxis.get_praxis_branches()[0])
    3986                  #from Gnumed.business import gmPerson
    3987                  ## 12 / 20 / 138 / 58 / 20 / 5 / 14
    3988                  #pat = gmPerson.gmCurrentPatient(gmPerson.cPatient(aPK_obj = 138))
    3989                  #fname = os.path.expanduser(u'~/gnumed/emr_structure-%s.txt' % pat.subdir_name)
    3990                  #print export_emr_structure(patient = pat, filename = fname)
    3991
    3992          #--------------------------------------------------------
    3993          # run them
    3994          #test_episode()
    3995          #test_episode_encounters()
    3996          #test_problem()
    3997          #test_encounter()
    3998          #test_health_issue()
    3999          #test_hospital_stay()
    4000          #test_performed_procedure()
    4001          #test_diagnostic_certainty_classification_map()
    4002          #test_encounter2latex()
    4003          #test_episode_codes()
    4004
    4005          test_export_emr_structure()
    4006
    4007  #============================================================
    4008