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