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