Gnumed.business.gmPathLab

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

# Source Code for Module Gnumed.business.gmPathLab

       1  """GNUmed measurements related business objects."""
       2
       3  # FIXME: use UCUM from Regenstrief Institute
       4  #============================================================
       5  __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
       6  __license__ = "GPL"
       7
       8
       9  import sys
      10  import logging
      11  import io
      12  import decimal
      13  import re as regex
      14  import os.path
      15
      16
      17  if __name__ == '__main__':
      18          sys.path.insert(0, '../../')
      19
      20  from Gnumed.pycommon import gmDateTime
      21  if __name__ == '__main__':
      22          from Gnumed.pycommon import gmLog2
      23          from Gnumed.pycommon import gmI18N
      24          gmI18N.activate_locale()
      25          gmI18N.install_domain('gnumed')
      26          gmDateTime.init()
      27  from Gnumed.pycommon import gmExceptions
      28  from Gnumed.pycommon import gmBusinessDBObject
      29  from Gnumed.pycommon import gmPG2
      30  from Gnumed.pycommon import gmTools
      31  from Gnumed.pycommon import gmDispatcher
      32  from Gnumed.pycommon import gmHooks
      33  from Gnumed.pycommon import gmCfg2
      34
      35  from Gnumed.business import gmOrganization
      36  from Gnumed.business import gmCoding
      37
      38  _log = logging.getLogger('gm.lab')
      39  _cfg = gmCfg2.gmCfgData()
      40
      41  #============================================================
      42  HL7_RESULT_STATI = {
      43          None: _('unknown'),
      44          '': _('empty status'),
      45          'C': _('C (HL7: Correction, replaces previous final)'),
      46          'D': _('D (HL7: Deletion)'),
      47          'F': _('F (HL7: Final)'),
      48          'I': _('I (HL7: pending, specimen In lab)'),
      49          'P': _('P (HL7: Preliminary)'),
      50          'R': _('R (HL7: result entered, not yet verified)'),
      51          'S': _('S (HL7: partial)'),
      52          'X': _('X (HL7: cannot obtain results for this observation)'),
      53          'U': _('U (HL7: mark as final (I/P/R/S -> F, value Unchanged)'),
      54          'W': _('W (HL7: original Wrong (say, wrong patient))')
      55  }
      56
      57  URL_test_result_information = 'http://www.laborlexikon.de'
      58  URL_test_result_information_search = "http://www.google.de/search?as_oq=%(search_term)s&num=10&as_sitesearch=laborlexikon.de"
      59
      60  #============================================================
      61 -def _on_test_result_modified():
      62          """Always relates to the active patient."""
      63          gmHooks.run_hook_script(hook = 'after_test_result_modified')
      64
      65  gmDispatcher.connect(_on_test_result_modified, 'clin.test_result_mod_db')
      66
      67  #============================================================
      68  _SQL_get_test_orgs = "SELECT * FROM clin.v_test_orgs WHERE %s"
      69
      70 -class cTestOrg(gmBusinessDBObject.cBusinessDBObject):
      71          """Represents one test org/lab."""
      72          _cmd_fetch_payload = _SQL_get_test_orgs % 'pk_test_org = %s'
      73          _cmds_store_payload = [
      74                  """UPDATE clin.test_org SET
      75                                  fk_org_unit = %(pk_org_unit)s,
      76                                  contact = gm.nullify_empty_string(%(test_org_contact)s),
      77                                  comment = gm.nullify_empty_string(%(comment)s)
      78                          WHERE
      79                                  pk = %(pk_test_org)s
      80                                          AND
      81                                  xmin = %(xmin_test_org)s
      82                          RETURNING
      83                                  xmin AS xmin_test_org
      84                  """
      85          ]
      86          _updatable_fields = [
      87                  'pk_org_unit',
      88                  'test_org_contact',
      89                  'comment'
      90          ]
      91  #------------------------------------------------------------
      92 -def create_test_org(name=None, comment=None, pk_org_unit=None, link_obj=None):
      93
      94          _log.debug('creating test org [%s:%s:%s]', name, comment, pk_org_unit)
      95
      96          if name is None:
      97                  name = 'unassigned lab'
      98
      99          # get org unit
     100          if pk_org_unit is None:
     101                  org = gmOrganization.create_org (
     102                          link_obj = link_obj,
     103                          organization = name,
     104                          category = 'Laboratory'
     105                  )
     106                  org_unit = gmOrganization.create_org_unit (
     107                          link_obj = link_obj,
     108                          pk_organization = org['pk_org'],
     109                          unit = name
     110                  )
     111                  pk_org_unit = org_unit['pk_org_unit']
     112
     113          # test org exists ?
     114          args = {'pk_unit': pk_org_unit}
     115          cmd = 'SELECT pk_test_org FROM clin.v_test_orgs WHERE pk_org_unit = %(pk_unit)s'
     116          rows, idx = gmPG2.run_ro_queries(link_obj = link_obj, queries = [{'cmd': cmd, 'args': args}])
     117
     118          if len(rows) == 0:
     119                  cmd = 'INSERT INTO clin.test_org (fk_org_unit) VALUES (%(pk_unit)s) RETURNING pk'
     120                  rows, idx = gmPG2.run_rw_queries(link_obj = link_obj, queries = [{'cmd': cmd, 'args': args}], return_data = True)
     121
     122          test_org = cTestOrg(link_obj = link_obj, aPK_obj = rows[0][0])
     123          if comment is not None:
     124                  comment = comment.strip()
     125          test_org['comment'] = comment
     126          test_org.save(conn = link_obj)
     127
     128          return test_org
     129  #------------------------------------------------------------
     130 -def delete_test_org(test_org=None):
     131          args = {'pk': test_org}
     132          cmd = """
     133                  DELETE FROM clin.test_org
     134                  WHERE
     135                          pk = %(pk)s
     136                                  AND
     137                          NOT EXISTS (SELECT 1 FROM clin.lab_request WHERE fk_test_org = %(pk)s LIMIT 1)
     138                                  AND
     139                          NOT EXISTS (SELECT 1 FROM clin.test_type WHERE fk_test_org = %(pk)s LIMIT 1)
     140          """
     141          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     142  #------------------------------------------------------------
     143 -def get_test_orgs(order_by='unit', return_pks=False):
     144          cmd = 'SELECT * FROM clin.v_test_orgs ORDER BY %s' % order_by
     145          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True)
     146          if return_pks:
     147                  return [ r['pk_test_org'] for r in rows ]
     148          return [ cTestOrg(row = {'pk_field': 'pk_test_org', 'data': r, 'idx': idx}) for r in rows ]
     149
     150  #============================================================
     151  # test panels / profiles
     152  #------------------------------------------------------------
     153  _SQL_get_test_panels = "SELECT * FROM clin.v_test_panels WHERE %s"
     154
     155 -class cTestPanel(gmBusinessDBObject.cBusinessDBObject):
     156          """Represents a grouping/listing of tests into a panel."""
     157
     158          _cmd_fetch_payload = _SQL_get_test_panels % "pk_test_panel = %s"
     159          _cmds_store_payload = [
     160                  """
     161                          UPDATE clin.test_panel SET
     162                                  description = gm.nullify_empty_string(%(description)s),
     163                                  comment = gm.nullify_empty_string(%(comment)s)
     164                          WHERE
     165                                  pk = %(pk_test_panel)s
     166                                          AND
     167                                  xmin = %(xmin_test_panel)s
     168                          RETURNING
     169                                  xmin AS xmin_test_panel
     170                  """
     171          ]
     172          _updatable_fields = [
     173                  'description',
     174                  'comment'
     175          ]
     176          #--------------------------------------------------------
     177 -        def format(self):
     178                  txt = _('Test panel "%s"          [#%s]\n') % (
     179                          self._payload[self._idx['description']],
     180                          self._payload[self._idx['pk_test_panel']]
     181                  )
     182
     183                  if self._payload[self._idx['comment']] is not None:
     184                          txt += '\n'
     185                          txt += gmTools.wrap (
     186                                  text = self._payload[self._idx['comment']],
     187                                  width = 50,
     188                                  initial_indent = ' ',
     189                                  subsequent_indent = ' '
     190                          )
     191                          txt += '\n'
     192
     193                  txt += '\n'
     194                  txt += _('Includes:\n')
     195                  if len(self.included_loincs) == 0:
     196                          txt += _('no tests')
     197                  else:
     198                          tts_by_loinc = {}
     199                          for loinc in self._payload[self._idx['loincs']]:
     200                                  tts_by_loinc[loinc] = []
     201                          for ttype in self.test_types:
     202                                  tts_by_loinc[ttype['loinc']].append(ttype)
     203                          for loinc, ttypes in tts_by_loinc.items():
     204                                  # maybe resolve LOINC, too
     205                                  txt += _(' %s: %s\n') % (
     206                                          loinc,
     207                                          '; '.join([ '%(abbrev)s@%(name_org)s [#%(pk_test_type)s]' % tt for tt in ttypes ])
     208                                  )
     209
     210                  codes = self.generic_codes
     211                  if len(codes) > 0:
     212                          txt += '\n'
     213                          for c in codes:
     214                                  txt += '%s  %s: %s (%s - %s)\n' % (
     215                                          (' ' * left_margin),
     216                                          c['code'],
     217                                          c['term'],
     218                                          c['name_short'],
     219                                          c['version']
     220                                  )
     221
     222                  return txt
     223
     224          #--------------------------------------------------------
     225 -        def add_code(self, pk_code=None):
     226                  """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
     227                  cmd = "INSERT INTO clin.lnk_code2tst_pnl (fk_item, fk_generic_code) values (%(tp)s, %(code)s)"
     228                  args = {
     229                          'tp': self._payload[self._idx['pk_test_panel']],
     230                          'code': pk_code
     231                  }
     232                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     233                  return True
     234
     235          #--------------------------------------------------------
     236 -        def remove_code(self, pk_code=None):
     237                  """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
     238                  cmd = "DELETE FROM clin.lnk_code2tst_pnl WHERE fk_item = %(tp)s AND fk_generic_code = %(code)s"
     239                  args = {
     240                          'tp': self._payload[self._idx['pk_test_panel']],
     241                          'code': pk_code
     242                  }
     243                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     244                  return True
     245
     246          #--------------------------------------------------------
     247 -        def get_test_types_for_results(self, pk_patient, order_by=None, unique_meta_types=False):
     248                  """Retrieve data about test types on this panel (for which this patient has results)."""
     249
     250                  if order_by is None:
     251                          order_by = ''
     252                  else:
     253                          order_by = 'ORDER BY %s' % order_by
     254
     255                  if unique_meta_types:
     256                          cmd = """
     257                                  SELECT * FROM clin.v_test_types c_vtt
     258                                  WHERE c_vtt.pk_test_type IN (
     259                                                  SELECT DISTINCT ON (c_vtr1.pk_meta_test_type) c_vtr1.pk_test_type
     260                                                  FROM clin.v_test_results c_vtr1
     261                                                  WHERE
     262                                                          c_vtr1.pk_test_type IN %%(pks)s
     263                                                                  AND
     264                                                          c_vtr1.pk_patient = %%(pat)s
     265                                                                  AND
     266                                                          c_vtr1.pk_meta_test_type IS NOT NULL
     267                                          UNION ALL
     268                                                  SELECT DISTINCT ON (c_vtr2.pk_test_type) c_vtr2.pk_test_type
     269                                                  FROM clin.v_test_results c_vtr2
     270                                                  WHERE
     271                                                          c_vtr2.pk_test_type IN %%(pks)s
     272                                                                  AND
     273                                                          c_vtr2.pk_patient = %%(pat)s
     274                                                                  AND
     275                                                          c_vtr2.pk_meta_test_type IS NULL
     276                                  )
     277                                  %s""" % order_by
     278                  else:
     279                          cmd = """
     280                                  SELECT * FROM clin.v_test_types c_vtt
     281                                  WHERE c_vtt.pk_test_type IN (
     282                                          SELECT DISTINCT ON (c_vtr.pk_test_type) c_vtr.pk_test_type
     283                                          FROM clin.v_test_results c_vtr
     284                                          WHERE
     285                                                  c_vtr.pk_test_type IN %%(pks)s
     286                                                                  AND
     287                                                  c_vtr.pk_patient = %%(pat)s
     288                                  )
     289                                  %s""" % order_by
     290
     291                  args = {
     292                          'pat': pk_patient,
     293                          'pks': tuple([ tt['pk_test_type'] for tt in self._payload[self._idx['test_types']] ])
     294                  }
     295                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     296                  return [ cMeasurementType(row = {'pk_field': 'pk_test_type', 'idx': idx, 'data': r}) for r in rows ]
     297
     298          #--------------------------------------------------------
     299 -        def add_loinc(self, loinc):
     300                  if self._payload[self._idx['loincs']] is not None:
     301                          if loinc in self._payload[self._idx['loincs']]:
     302                                  return
     303                  gmPG2.run_rw_queries(queries = [{
     304                          'cmd': 'INSERT INTO clin.lnk_loinc2test_panel (fk_test_panel, loinc) VALUES (%(pk_pnl)s, %(loinc)s)',
     305                          'args': {'loinc': loinc, 'pk_pnl': self._payload[self._idx['pk_test_panel']]}
     306                  }])
     307                  return
     308
     309          #--------------------------------------------------------
     310 -        def remove_loinc(self, loinc):
     311                  if self._payload[self._idx['loincs']] is None:
     312                          return
     313                  if loinc not in self._payload[self._idx['loincs']]:
     314                          return
     315                  gmPG2.run_rw_queries(queries = [{
     316                          'cmd': 'DELETE FROM clin.lnk_loinc2test_panel WHERE fk_test_panel = %(pk_pnl)s AND loinc = %(loinc)s',
     317                          'args': {'loinc': loinc, 'pk_pnl': self._payload[self._idx['pk_test_panel']]}
     318                  }])
     319                  return
     320
     321          #--------------------------------------------------------
     322          # properties
     323          #--------------------------------------------------------
     324 -        def _get_included_loincs(self):
     325                  return self._payload[self._idx['loincs']]
     326
     327 -        def _set_included_loincs(self, loincs):
     328                  queries = []
     329                  # remove those which don't belong
     330                  if len(loincs) == 0:
     331                          cmd = 'DELETE FROM clin.lnk_loinc2test_panel WHERE fk_test_panel = %(pk_pnl)s'
     332                  else:
     333                          cmd = 'DELETE FROM clin.lnk_loinc2test_panel WHERE fk_test_panel = %(pk_pnl)s AND loinc NOT IN %(loincs)s'
     334                  queries.append({'cmd': cmd, 'args': {'loincs': tuple(loincs), 'pk_pnl': self._payload[self._idx['pk_test_panel']]}})
     335                  # add those not there yet
     336                  if len(loincs) > 0:
     337                          for loinc in loincs:
     338                                  cmd = """INSERT INTO clin.lnk_loinc2test_panel (fk_test_panel, loinc)
     339                                  SELECT %(pk_pnl)s, %(loinc)s WHERE NOT EXISTS (
     340                                          SELECT 1 FROM clin.lnk_loinc2test_panel WHERE
     341                                                  fk_test_panel = %(pk_pnl)s
     342                                                          AND
     343                                                  loinc = %(loinc)s
     344                                  )"""
     345                                  queries.append({'cmd': cmd, 'args': {'loinc': loinc, 'pk_pnl': self._payload[self._idx['pk_test_panel']]}})
     346                  return gmPG2.run_rw_queries(queries = queries)
     347
     348          included_loincs = property(_get_included_loincs, _set_included_loincs)
     349
     350          #--------------------------------------------------------
     351 -        def _get_test_types(self):
     352                  if len(self._payload[self._idx['test_types']]) == 0:
     353                          return []
     354
     355                  rows, idx = gmPG2.run_ro_queries (
     356                          queries = [{
     357                                  'cmd': _SQL_get_test_types % 'pk_test_type IN %(pks)s ORDER BY unified_abbrev',
     358                                  'args': {'pks': tuple([ tt['pk_test_type'] for tt in self._payload[self._idx['test_types']] ])}
     359                          }],
     360                          get_col_idx = True
     361                  )
     362                  return [ cMeasurementType(row = {'data': r, 'idx': idx, 'pk_field': 'pk_test_type'}) for r in rows ]
     363
     364          test_types = property(_get_test_types, lambda x:x)
     365
     366          #--------------------------------------------------------
     367 -        def _get_generic_codes(self):
     368                  if len(self._payload[self._idx['pk_generic_codes']]) == 0:
     369                          return []
     370
     371                  cmd = gmCoding._SQL_get_generic_linked_codes % 'pk_generic_code IN %(pks)s'
     372                  args = {'pks': tuple(self._payload[self._idx['pk_generic_codes']])}
     373                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     374                  return [ gmCoding.cGenericLinkedCode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_lnk_code2item'}) for r in rows ]
     375
     376 -        def _set_generic_codes(self, pk_codes):
     377                  queries = []
     378                  # remove all codes
     379                  if len(self._payload[self._idx['pk_generic_codes']]) > 0:
     380                          queries.append ({
     381                                  'cmd': 'DELETE FROM clin.lnk_code2tst_pnl WHERE fk_item = %(tp)s AND fk_generic_code IN %(codes)s',
     382                                  'args': {
     383                                          'tp': self._payload[self._idx['pk_test_panel']],
     384                                          'codes': tuple(self._payload[self._idx['pk_generic_codes']])
     385                                  }
     386                          })
     387                  # add new codes
     388                  for pk_code in pk_codes:
     389                          queries.append ({
     390                                  'cmd': 'INSERT INTO clin.lnk_code2test_panel (fk_item, fk_generic_code) VALUES (%(tp)s, %(pk_code)s)',
     391                                  'args': {
     392                                          'tp': self._payload[self._idx['pk_test_panel']],
     393                                          'pk_code': pk_code
     394                                  }
     395                          })
     396                  if len(queries) == 0:
     397                          return
     398                  # run it all in one transaction
     399                  rows, idx = gmPG2.run_rw_queries(queries = queries)
     400                  return
     401
     402          generic_codes = property(_get_generic_codes, _set_generic_codes)
     403
     404          #--------------------------------------------------------
     405 -        def get_most_recent_results(self, pk_patient=None, order_by=None, group_by_meta_type=False, include_missing=False):
     406
     407                  if len(self._payload[self._idx['test_types']]) == 0:
     408                          return []
     409
     410                  pnl_results = get_most_recent_results_for_panel (
     411                          pk_patient = pk_patient,
     412                          pk_panel = self._payload[self._idx['pk_test_panel']],
     413                          order_by = order_by,
     414                          group_by_meta_type = group_by_meta_type
     415                  )
     416                  if not include_missing:
     417                          return pnl_results
     418
     419                  loincs_found = [ r['loinc_tt'] for r in pnl_results ]
     420                  loincs_found.extend([ r['loinc_meta'] for r in pnl_results if r['loinc_meta'] not in loincs_found ])
     421                  loincs2consider = set([ tt['loinc'] for tt in self._payload[self._idx['test_types']] ])
     422                  loincs_missing = loincs2consider - set(loincs_found)
     423                  pnl_results.extend(loincs_missing)
     424                  return pnl_results
     425
     426  #------------------------------------------------------------
     427 -def get_test_panels(order_by=None, loincs=None, return_pks=False):
     428          where_args = {}
     429          if loincs is None:
     430                  where_parts = ['true']
     431          else:
     432                  where_parts = ['loincs @> %(loincs)s']
     433                  where_args['loincs'] = list(loincs)
     434
     435          if order_by is None:
     436                  order_by = u''
     437          else:
     438                  order_by = ' ORDER BY %s' % order_by
     439
     440          cmd = (_SQL_get_test_panels % ' AND '.join(where_parts)) + order_by
     441          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': where_args}], get_col_idx = True)
     442          if return_pks:
     443                  return [ r['pk_test_panel'] for r in rows ]
     444          return [ cTestPanel(row = {'data': r, 'idx': idx, 'pk_field': 'pk_test_panel'}) for r in rows ]
     445
     446  #------------------------------------------------------------
     447 -def create_test_panel(description=None):
     448
     449          args = {'desc': description.strip()}
     450          cmd = """
     451                  INSERT INTO clin.test_panel (description)
     452                  VALUES (gm.nullify_empty_string(%(desc)s))
     453                  RETURNING pk
     454          """
     455          rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
     456
     457          return cTestPanel(aPK_obj = rows[0]['pk'])
     458
     459  #------------------------------------------------------------
     460 -def delete_test_panel(pk=None):
     461          args = {'pk': pk}
     462          cmd = "DELETE FROM clin.test_panel WHERE pk = %(pk)s"
     463          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     464          return True
     465
     466  #============================================================
     467 -class cMetaTestType(gmBusinessDBObject.cBusinessDBObject):
     468          """Represents one meta test type under which actual test types can be aggregated."""
     469
     470          _cmd_fetch_payload = "SELECT *, xmin FROM clin.meta_test_type WHERE pk = %s"
     471          _cmds_store_payload = ["""
     472                  UPDATE clin.meta_test_type SET
     473                          abbrev = %(abbrev)s,
     474                          name = %(name)s,
     475                          loinc = gm.nullify_empty_string(%(loinc)s),
     476                          comment = gm.nullify_empty_string(%(comment)s)
     477                  WHERE
     478                          pk = %(pk)s
     479                                  AND
     480                          xmin = %(xmin)s
     481                  RETURNING
     482                          xmin
     483          """]
     484          _updatable_fields = [
     485                  'abbrev',
     486                  'name',
     487                  'loinc',
     488                  'comment'
     489          ]
     490          #--------------------------------------------------------
     491 -        def format(self, with_tests=False, patient=None):
     492                  txt = _('Meta (%s=aggregate) test type              [#%s]\n\n') % (gmTools.u_sum, self._payload[self._idx['pk']])
     493                  txt += _(' Name: %s (%s)\n') % (
     494                          self._payload[self._idx['abbrev']],
     495                          self._payload[self._idx['name']]
     496                  )
     497                  if self._payload[self._idx['loinc']] is not None:
     498                          txt += ' LOINC: %s\n' % self._payload[self._idx['loinc']]
     499                  if self._payload[self._idx['comment']] is not None:
     500                          txt += _(' Comment: %s\n') % self._payload[self._idx['comment']]
     501                  if with_tests:
     502                          ttypes = self.included_test_types
     503                          if len(ttypes) > 0:
     504                                  txt += _(' Aggregates the following test types:\n')
     505                          for ttype in ttypes:
     506                                  txt += '  - %s (%s)%s%s%s      [#%s]\n' % (
     507                                          ttype['name'],
     508                                          ttype['abbrev'],
     509                                          gmTools.coalesce(ttype['reference_unit'], '', ', %s'),
     510                                          gmTools.coalesce(ttype['name_org'], '', ' (%s)'),
     511                                          gmTools.coalesce(ttype['loinc'], '', ', LOINC: %s'),
     512                                          ttype['pk_test_type']
     513                                  )
     514                  if patient is not None:
     515                          txt += '\n'
     516                          most_recent = self.get_most_recent_result(patient = patient)
     517                          if most_recent is not None:
     518                                  txt += _(' Most recent (%s): %s%s%s') % (
     519                                          gmDateTime.pydt_strftime(most_recent['clin_when'], '%Y %b %d'),
     520                                          most_recent['unified_val'],
     521                                          gmTools.coalesce(most_recent['val_unit'], '', ' %s'),
     522                                          gmTools.coalesce(most_recent['abnormality_indicator'], '', ' (%s)')
     523                                  )
     524                          oldest = self.get_oldest_result(patient = patient)
     525                          if oldest is not None:
     526                                  txt += '\n'
     527                                  txt += _(' Oldest (%s): %s%s%s') % (
     528                                          gmDateTime.pydt_strftime(oldest['clin_when'], '%Y %b %d'),
     529                                          oldest['unified_val'],
     530                                          gmTools.coalesce(oldest['val_unit'], '', ' %s'),
     531                                          gmTools.coalesce(oldest['abnormality_indicator'], '', ' (%s)')
     532                                  )
     533                  return txt
     534
     535          #--------------------------------------------------------
     536 -        def get_most_recent_result(self, patient=None):
     537                  args = {
     538                          'pat': patient,
     539                          'mttyp': self._payload[self._idx['pk']]
     540                  }
     541                  cmd = """
     542                          SELECT * FROM clin.v_test_results
     543                          WHERE
     544                                  pk_patient = %(pat)s
     545                                          AND
     546                                  pk_meta_test_type = %(mttyp)s
     547                          ORDER BY clin_when DESC
     548                          LIMIT 1"""
     549                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     550                  if len(rows) == 0:
     551                          return None
     552
     553                  return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
     554
     555          #--------------------------------------------------------
     556 -        def get_oldest_result(self, patient=None):
     557                  args = {
     558                          'pat': patient,
     559                          'mttyp': self._payload[self._idx['pk']]
     560                  }
     561                  cmd = """
     562                          SELECT * FROM clin.v_test_results
     563                          WHERE
     564                                  pk_patient = %(pat)s
     565                                          AND
     566                                  pk_meta_test_type = %(mttyp)s
     567                          ORDER BY clin_when
     568                          LIMIT 1"""
     569                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     570                  if len(rows) == 0:
     571                          return None
     572
     573                  return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
     574
     575          #--------------------------------------------------------
     576 -        def get_temporally_closest_result(self, date, pk_patient):
     577
     578                  args = {
     579                          'pat': pk_patient,
     580                          'mtyp': self._payload[self._idx['pk']],
     581                          'mloinc': self._payload[self._idx['loinc']],
     582                          'when': date
     583                  }
     584                  WHERE_meta = ''
     585
     586                  SQL = """
     587                          SELECT * FROM clin.v_test_results
     588                          WHERE
     589                                  pk_patient = %%(pat)s
     590                                          AND
     591                                  clin_when %s %%(when)s
     592                                          AND
     593                                  ((pk_meta_test_type = %%(mtyp)s) OR (loinc_meta = %%(mloinc)s))
     594                          ORDER BY clin_when
     595                          LIMIT 1"""
     596
     597                  # get earlier results by meta type
     598                  earlier_result = None
     599                  cmd = SQL % '<'
     600                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     601                  if len(rows) > 0:
     602                          earlier_result = cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
     603
     604                  # get later results by meta type ?
     605                  later_result = None
     606                  cmd = SQL % '>'
     607                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     608                  if len(rows) > 0:
     609                          later_result = cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
     610
     611                  if earlier_result is None:
     612                          return later_result
     613                  if later_result is None:
     614                          return earlier_result
     615
     616                  earlier_ago = date - earlier_result['clin_when']
     617                  later_ago = later_result['clin_when'] - date
     618                  if earlier_ago < later_ago:
     619                          return earlier_result
     620                  return later_result
     621
     622          #--------------------------------------------------------
     623          # properties
     624          #--------------------------------------------------------
     625 -        def _get_included_test_types(self):
     626                  cmd = _SQL_get_test_types % 'pk_meta_test_type = %(pk_meta)s'
     627                  args = {'pk_meta': self._payload[self._idx['pk']]}
     628                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     629                  return [ cMeasurementType(row = {'pk_field': 'pk_test_type', 'data': r, 'idx': idx}) for r in rows ]
     630
     631          included_test_types = property(_get_included_test_types, lambda x:x)
     632
     633  #------------------------------------------------------------
     634 -def create_meta_type(name=None, abbreviation=None, return_existing=False):
     635          cmd = """
     636                  INSERT INTO clin.meta_test_type (name, abbrev)
     637                  SELECT
     638                          %(name)s,
     639                          %(abbr)s
     640                  WHERE NOT EXISTS (
     641                          SELECT 1 FROM clin.meta_test_type
     642                          WHERE
     643                                  name = %(name)s
     644                                          AND
     645                                  abbrev = %(abbr)s
     646                  )
     647                   RETURNING *, xmin
     648          """
     649          args = {
     650                  'name': name.strip(),
     651                  'abbr': abbreviation.strip()
     652          }
     653          rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True, return_data = True)
     654          if len(rows) == 0:
     655                  if not return_existing:
     656                          return None
     657                  cmd = "SELECT *, xmin FROM clin.meta_test_type WHERE name = %(name)s and %(abbr)s"
     658                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     659
     660          return cMetaTestType(row = {'pk_field': 'pk', 'idx': idx, 'data': rows[0]})
     661
     662  #------------------------------------------------------------
     663 -def delete_meta_type(meta_type=None):
     664          cmd = """
     665                  DELETE FROM clin.meta_test_type
     666                  WHERE
     667                          pk = %(pk)s
     668                                  AND
     669                          NOT EXISTS (
     670                                  SELECT 1 FROM clin.test_type
     671                                  WHERE fk_meta_test_type = %(pk)s
     672                          )"""
     673          args = {'pk': meta_type}
     674          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     675
     676  #------------------------------------------------------------
     677 -def get_meta_test_types(return_pks=False):
     678          cmd = 'SELECT *, xmin FROM clin.meta_test_type'
     679          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True)
     680          if return_pks:
     681                  return [ r['pk'] for r in rows ]
     682          return [ cMetaTestType(row = {'pk_field': 'pk', 'data': r, 'idx': idx}) for r in rows ]
     683
     684  #============================================================
     685  _SQL_get_test_types = "SELECT * FROM clin.v_test_types WHERE %s"
     686
     687 -class cMeasurementType(gmBusinessDBObject.cBusinessDBObject):
     688          """Represents one test result type."""
     689
     690          _cmd_fetch_payload = _SQL_get_test_types % "pk_test_type = %s"
     691
     692          _cmds_store_payload = [
     693                  """UPDATE clin.test_type SET
     694                                  abbrev = gm.nullify_empty_string(%(abbrev)s),
     695                                  name = gm.nullify_empty_string(%(name)s),
     696                                  loinc = gm.nullify_empty_string(%(loinc)s),
     697                                  comment = gm.nullify_empty_string(%(comment_type)s),
     698                                  reference_unit = gm.nullify_empty_string(%(reference_unit)s),
     699                                  fk_test_org = %(pk_test_org)s,
     700                                  fk_meta_test_type = %(pk_meta_test_type)s
     701                          WHERE
     702                                  pk = %(pk_test_type)s
     703                                          AND
     704                                  xmin = %(xmin_test_type)s
     705                          RETURNING
     706                                  xmin AS xmin_test_type"""
     707          ]
     708
     709          _updatable_fields = [
     710                  'abbrev',
     711                  'name',
     712                  'loinc',
     713                  'comment_type',
     714                  'reference_unit',
     715                  'pk_test_org',
     716                  'pk_meta_test_type'
     717          ]
     718          #--------------------------------------------------------
     719          # properties
     720          #--------------------------------------------------------
     721 -        def _get_in_use(self):
     722                  cmd = 'SELECT EXISTS(SELECT 1 FROM clin.test_result WHERE fk_type = %(pk_type)s)'
     723                  args = {'pk_type': self._payload[self._idx['pk_test_type']]}
     724                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
     725                  return rows[0][0]
     726
     727          in_use = property(_get_in_use, lambda x:x)
     728
     729          #--------------------------------------------------------
     730 -        def get_most_recent_results(self, patient=None, max_no_of_results=1):
     731                  results = get_most_recent_results_for_test_type (
     732                          test_type = self._payload[self._idx['pk_test_type']],
     733                          max_no_of_results = max_no_of_results,
     734                          patient = patient
     735                  )
     736                  if len(results) > 0:
     737                          return results
     738
     739                  if self._payload[self._idx['loinc']] is None:
     740                          return []
     741
     742                  return get_most_recent_results_in_loinc_group (
     743                          loincs = tuple(self._payload[self._idx['loinc']]),
     744                          max_no_of_results = max_no_of_results,
     745                          patient = patient
     746                          # ?
     747                  )
     748
     749          #--------------------------------------------------------
     750 -        def get_oldest_result(self, patient=None):
     751                  result = get_oldest_result (
     752                          test_type = self._payload[self._idx['pk_test_type']],
     753                          loinc = None,
     754                          patient = patient
     755                  )
     756                  if result is None:
     757                          if self._payload[self._idx['loinc']] is not None:
     758                                  result = get_oldest_result (
     759                                          test_type = None,
     760                                          loinc = self._payload[self._idx['loinc']],
     761                                          patient = patient
     762                                  )
     763                  return result
     764
     765          #--------------------------------------------------------
     766 -        def _get_test_panels(self):
     767                  if self._payload[self._idx['pk_test_panels']] is None:
     768                          return None
     769
     770                  return [ cTestPanel(aPK_obj = pk) for pk in self._payload[self._idx['pk_test_panels']] ]
     771
     772          test_panels = property(_get_test_panels, lambda x:x)
     773
     774          #--------------------------------------------------------
     775 -        def get_meta_test_type(self, real_one_only=True):
     776                  if real_one_only is False:
     777                          return cMetaTestType(aPK_obj = self._payload[self._idx['pk_meta_test_type']])
     778                  if self._payload[self._idx['is_fake_meta_type']]:
     779                          return None
     780                  return cMetaTestType(aPK_obj = self._payload[self._idx['pk_meta_test_type']])
     781
     782          meta_test_type = property(get_meta_test_type, lambda x:x)
     783
     784          #--------------------------------------------------------
     785 -        def get_temporally_closest_normal_range(self, unit, timestamp=None):
     786                  """Returns the closest test result which does have normal range information.
     787
     788                  - needs <unit>
     789                  - if <timestamp> is None it will assume now() and thus return the most recent
     790                  """
     791                  if timestamp is None:
     792                          timestamp = gmDateTime.pydt_now_here()
     793                  cmd = """
     794  SELECT * FROM clin.v_test_results
     795  WHERE
     796          pk_test_type = %(pk_type)s
     797                  AND
     798          val_unit = %(unit)s
     799                  AND
     800          (
     801                  (val_normal_min IS NOT NULL)
     802                          OR
     803                  (val_normal_max IS NOT NULL)
     804                          OR
     805                  (val_normal_range IS NOT NULL)
     806          )
     807  ORDER BY
     808          CASE
     809                  WHEN clin_when > %(clin_when)s THEN clin_when - %(clin_when)s
     810                  ELSE %(clin_when)s - clin_when
     811          END
     812  LIMIT 1"""
     813                  args = {
     814                          'pk_type': self._payload[self._idx['pk_test_type']],
     815                          'unit': unit,
     816                          'clin_when': timestamp
     817                  }
     818                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     819                  if len(rows) == 0:
     820                          return None
     821                  r = rows[0]
     822                  return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r})
     823
     824          #--------------------------------------------------------
     825 -        def get_temporally_closest_target_range(self, unit, patient, timestamp=None):
     826                  """Returns the closest test result which does have target range information.
     827
     828                  - needs <unit>
     829                  - needs <patient> (as target will be per-patient)
     830                  - if <timestamp> is None it will assume now() and thus return the most recent
     831                  """
     832                  if timestamp is None:
     833                          timestamp = gmDateTime.pydt_now_here()
     834                  cmd = """
     835  SELECT * FROM clin.v_test_results
     836  WHERE
     837          pk_test_type = %(pk_type)s
     838                  AND
     839          val_unit = %(unit)s
     840                  AND
     841          pk_patient = %(pat)s
     842                  AND
     843          (
     844                  (val_target_min IS NOT NULL)
     845                          OR
     846                  (val_target_max IS NOT NULL)
     847                          OR
     848                  (val_target_range IS NOT NULL)
     849          )
     850  ORDER BY
     851          CASE
     852                  WHEN clin_when > %(clin_when)s THEN clin_when - %(clin_when)s
     853                  ELSE %(clin_when)s - clin_when
     854          END
     855  LIMIT 1"""
     856                  args = {
     857                          'pk_type': self._payload[self._idx['pk_test_type']],
     858                          'unit': unit,
     859                          'pat': patient,
     860                          'clin_when': timestamp
     861                  }
     862                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     863                  if len(rows) == 0:
     864                          return None
     865                  r = rows[0]
     866                  return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r})
     867
     868          #--------------------------------------------------------
     869 -        def get_temporally_closest_unit(self, timestamp=None):
     870                  """Returns the unit of the closest test result.
     871
     872                  - if <timestamp> is None it will assume now() and thus return the most recent
     873                  """
     874                  if timestamp is None:
     875                          timestamp = gmDateTime.pydt_now_here()
     876                  cmd = """
     877  SELECT val_unit FROM clin.v_test_results
     878  WHERE
     879          pk_test_type = %(pk_type)s
     880                  AND
     881          val_unit IS NOT NULL
     882  ORDER BY
     883          CASE
     884                  WHEN clin_when > %(clin_when)s THEN clin_when - %(clin_when)s
     885                  ELSE %(clin_when)s - clin_when
     886          END
     887  LIMIT 1"""
     888                  args = {
     889                          'pk_type': self._payload[self._idx['pk_test_type']],
     890                          'clin_when': timestamp
     891                  }
     892                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     893                  if len(rows) == 0:
     894                          return None
     895                  return rows[0]['val_unit']
     896
     897          temporally_closest_unit = property(get_temporally_closest_unit, lambda x:x)
     898
     899          #--------------------------------------------------------
     900 -        def format(self, patient=None):
     901                  tt = ''
     902                  tt += _('Test type "%s" (%s)          [#%s]\n') % (
     903                          self._payload[self._idx['name']],
     904                          self._payload[self._idx['abbrev']],
     905                          self._payload[self._idx['pk_test_type']]
     906                  )
     907                  tt += '\n'
     908                  tt += gmTools.coalesce(self._payload[self._idx['loinc']], '', ' LOINC: %s\n')
     909                  tt += gmTools.coalesce(self._payload[self._idx['reference_unit']], '', _(' Reference unit: %s\n'))
     910                  tt += gmTools.coalesce(self._payload[self._idx['comment_type']], '', _(' Comment: %s\n'))
     911
     912                  tt += '\n'
     913                  tt += _('Lab details:\n')
     914                  tt += _(' Name: %s\n') % gmTools.coalesce(self._payload[self._idx['name_org']], '')
     915                  tt += gmTools.coalesce(self._payload[self._idx['contact_org']], '', _(' Contact: %s\n'))
     916                  tt += gmTools.coalesce(self._payload[self._idx['comment_org']], '', _(' Comment: %s\n'))
     917
     918                  if self._payload[self._idx['is_fake_meta_type']] is False:
     919                          tt += '\n'
     920                          tt += _('Aggregated under meta type:\n')
     921                          tt += _(' Name: %s - %s             [#%s]\n') % (
     922                                  self._payload[self._idx['abbrev_meta']],
     923                                  self._payload[self._idx['name_meta']],
     924                                  self._payload[self._idx['pk_meta_test_type']]
     925                          )
     926                          tt += gmTools.coalesce(self._payload[self._idx['loinc_meta']], '', ' LOINC: %s\n')
     927                          tt += gmTools.coalesce(self._payload[self._idx['comment_meta']], '', _(' Comment: %s\n'))
     928
     929                  panels = self.test_panels
     930                  if panels is not None:
     931                          tt += '\n'
     932                          tt += _('Listed in test panels:\n')
     933                          for panel in panels:
     934                                  tt += _(' Panel "%s"             [#%s]\n') % (
     935                                          panel['description'],
     936                                          panel['pk_test_panel']
     937                                  )
     938
     939                  if patient is not None:
     940                          tt += '\n'
     941                          results = self.get_most_recent_results(patient = patient, max_no_of_results = 1)
     942                          if len(results) > 0:
     943                                  result = results[0]
     944                                  tt += _(' Most recent (%s): %s%s%s') % (
     945                                          gmDateTime.pydt_strftime(result['clin_when'], '%Y %b %d'),
     946                                          result['unified_val'],
     947                                          gmTools.coalesce(result['val_unit'], '', ' %s'),
     948                                          gmTools.coalesce(result['abnormality_indicator'], '', ' (%s)')
     949                                  )
     950                          result = self.get_oldest_result(patient = patient)
     951                          if result is not None:
     952                                  tt += '\n'
     953                                  tt += _(' Oldest (%s): %s%s%s') % (
     954                                          gmDateTime.pydt_strftime(result['clin_when'], '%Y %b %d'),
     955                                          result['unified_val'],
     956                                          gmTools.coalesce(result['val_unit'], '', ' %s'),
     957                                          gmTools.coalesce(result['abnormality_indicator'], '', ' (%s)')
     958                                  )
     959
     960                  return tt
     961
     962  #------------------------------------------------------------
     963 -def get_measurement_types(order_by=None, loincs=None, return_pks=False):
     964          args = {}
     965          where_parts = []
     966          if loincs is not None:
     967                  if len(loincs) > 0:
     968                          where_parts.append('loinc IN %(loincs)s')
     969                          args['loincs'] = tuple(loincs)
     970          if len(where_parts) == 0:
     971                  where_parts.append('TRUE')
     972          WHERE_clause = ' AND '.join(where_parts)
     973          cmd = (_SQL_get_test_types % WHERE_clause) + gmTools.coalesce(order_by, '', ' ORDER BY %s')
     974          #cmd = 'select * from clin.v_test_types %s' % gmTools.coalesce(order_by, '', 'order by %s')
     975          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     976          if return_pks:
     977                  return [ r['pk_test_type'] for r in rows ]
     978          return [ cMeasurementType(row = {'pk_field': 'pk_test_type', 'data': r, 'idx': idx}) for r in rows ]
     979
     980  #------------------------------------------------------------
     981 -def find_measurement_type(lab=None, abbrev=None, name=None, link_obj=None):
     982
     983                  if (abbrev is None) and (name is None):
     984                          raise ValueError('must have <abbrev> and/or <name> set')
     985
     986                  where_snippets = []
     987
     988                  if lab is None:
     989                          where_snippets.append('pk_test_org IS NULL')
     990                  else:
     991                          try:
     992                                  int(lab)
     993                                  where_snippets.append('pk_test_org = %(lab)s')
     994                          except (TypeError, ValueError):
     995                                  where_snippets.append('pk_test_org = (SELECT pk_test_org FROM clin.v_test_orgs WHERE unit = %(lab)s)')
     996
     997                  if abbrev is not None:
     998                          where_snippets.append('abbrev = %(abbrev)s')
     999
    1000                  if name is not None:
    1001                          where_snippets.append('name = %(name)s')
    1002
    1003                  where_clause = ' and '.join(where_snippets)
    1004                  cmd = "select * from clin.v_test_types where %s" % where_clause
    1005                  args = {'lab': lab, 'abbrev': abbrev, 'name': name}
    1006
    1007                  rows, idx = gmPG2.run_ro_queries(link_obj = link_obj, queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1008
    1009                  if len(rows) == 0:
    1010                          return None
    1011
    1012                  tt = cMeasurementType(row = {'pk_field': 'pk_test_type', 'data': rows[0], 'idx': idx})
    1013                  return tt
    1014
    1015  #------------------------------------------------------------
    1016 -def delete_measurement_type(measurement_type=None):
    1017          cmd = 'delete from clin.test_type where pk = %(pk)s'
    1018          args = {'pk': measurement_type}
    1019          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    1020
    1021  #------------------------------------------------------------
    1022 -def create_measurement_type(lab=None, abbrev=None, unit=None, name=None, link_obj=None):
    1023          """Create or get test type."""
    1024
    1025          ttype = find_measurement_type(lab = lab, abbrev = abbrev, name = name, link_obj = link_obj)
    1026          # found ?
    1027          if ttype is not None:
    1028                  return ttype
    1029
    1030          _log.debug('creating test type [%s:%s:%s:%s]', lab, abbrev, name, unit)
    1031
    1032          # not found, so create it
    1033  #       if unit is None:
    1034  #               _log.error('need <unit> to create test type: %s:%s:%s:%s' % (lab, abbrev, name, unit))
    1035  #               raise ValueError('need <unit> to create test type')
    1036
    1037          # make query
    1038          cols = []
    1039          val_snippets = []
    1040          vals = {}
    1041
    1042          # lab
    1043          if lab is None:
    1044                  lab = create_test_org(link_obj = link_obj)['pk_test_org']
    1045
    1046          cols.append('fk_test_org')
    1047          try:
    1048                  vals['lab'] = int(lab)
    1049                  val_snippets.append('%(lab)s')
    1050          except Exception:
    1051                  vals['lab'] = lab
    1052                  val_snippets.append('(SELECT pk_test_org FROM clin.v_test_orgs WHERE unit = %(lab)s)')
    1053
    1054          # code
    1055          cols.append('abbrev')
    1056          val_snippets.append('%(abbrev)s')
    1057          vals['abbrev'] = abbrev
    1058
    1059          # unit
    1060          if unit is not None:
    1061                  cols.append('reference_unit')
    1062                  val_snippets.append('%(unit)s')
    1063                  vals['unit'] = unit
    1064
    1065          # name
    1066          if name is not None:
    1067                  cols.append('name')
    1068                  val_snippets.append('%(name)s')
    1069                  vals['name'] = name
    1070
    1071          col_clause = ', '.join(cols)
    1072          val_clause = ', '.join(val_snippets)
    1073          queries = [
    1074                  {'cmd': 'insert into clin.test_type (%s) values (%s)' % (col_clause, val_clause), 'args': vals},
    1075                  {'cmd': "select * from clin.v_test_types where pk_test_type = currval(pg_get_serial_sequence('clin.test_type', 'pk'))"}
    1076          ]
    1077          rows, idx = gmPG2.run_rw_queries(link_obj = link_obj, queries = queries, get_col_idx = True, return_data = True)
    1078          ttype = cMeasurementType(row = {'pk_field': 'pk_test_type', 'data': rows[0], 'idx': idx})
    1079
    1080          return ttype
    1081
    1082  #============================================================
    1083 -class cTestResult(gmBusinessDBObject.cBusinessDBObject):
    1084          """Represents one test result."""
    1085
    1086          _cmd_fetch_payload = "select * from clin.v_test_results where pk_test_result = %s"
    1087
    1088          _cmds_store_payload = [
    1089                  """UPDATE clin.test_result SET
    1090                                  clin_when = %(clin_when)s,
    1091                                  narrative = nullif(trim(%(comment)s), ''),
    1092                                  val_num = %(val_num)s,
    1093                                  val_alpha = nullif(trim(%(val_alpha)s), ''),
    1094                                  val_unit = nullif(trim(%(val_unit)s), ''),
    1095                                  val_normal_min = %(val_normal_min)s,
    1096                                  val_normal_max = %(val_normal_max)s,
    1097                                  val_normal_range = nullif(trim(%(val_normal_range)s), ''),
    1098                                  val_target_min = %(val_target_min)s,
    1099                                  val_target_max = %(val_target_max)s,
    1100                                  val_target_range = nullif(trim(%(val_target_range)s), ''),
    1101                                  abnormality_indicator = nullif(trim(%(abnormality_indicator)s), ''),
    1102                                  norm_ref_group = nullif(trim(%(norm_ref_group)s), ''),
    1103                                  note_test_org = nullif(trim(%(note_test_org)s), ''),
    1104                                  material = nullif(trim(%(material)s), ''),
    1105                                  material_detail = nullif(trim(%(material_detail)s), ''),
    1106                                  status = gm.nullify_empty_string(%(status)s),
    1107                                  val_grouping = gm.nullify_empty_string(%(val_grouping)s),
    1108                                  source_data = gm.nullify_empty_string(%(source_data)s),
    1109                                  fk_intended_reviewer = %(pk_intended_reviewer)s,
    1110                                  fk_encounter = %(pk_encounter)s,
    1111                                  fk_episode = %(pk_episode)s,
    1112                                  fk_type = %(pk_test_type)s,
    1113                                  fk_request = %(pk_request)s
    1114                          WHERE
    1115                                  pk = %(pk_test_result)s AND
    1116                                  xmin = %(xmin_test_result)s
    1117                          RETURNING
    1118                                  xmin AS xmin_test_result
    1119                  """
    1120  #               , u"""select xmin_test_result from clin.v_test_results where pk_test_result = %(pk_test_result)s"""
    1121          ]
    1122
    1123          _updatable_fields = [
    1124                  'clin_when',
    1125                  'comment',
    1126                  'val_num',
    1127                  'val_alpha',
    1128                  'val_unit',
    1129                  'val_normal_min',
    1130                  'val_normal_max',
    1131                  'val_normal_range',
    1132                  'val_target_min',
    1133                  'val_target_max',
    1134                  'val_target_range',
    1135                  'abnormality_indicator',
    1136                  'norm_ref_group',
    1137                  'note_test_org',
    1138                  'material',
    1139                  'material_detail',
    1140                  'status',
    1141                  'val_grouping',
    1142                  'source_data',
    1143                  'pk_intended_reviewer',
    1144                  'pk_encounter',
    1145                  'pk_episode',
    1146                  'pk_test_type',
    1147                  'pk_request'
    1148          ]
    1149
    1150          #--------------------------------------------------------
    1151 -        def format_concisely(self, date_format='%Y %b %d', with_notes=True):
    1152                  range_info = gmTools.coalesce (
    1153                          self.formatted_clinical_range,
    1154                          self.formatted_normal_range
    1155                  )
    1156                  review = gmTools.bool2subst (
    1157                          self._payload[self._idx['reviewed']],
    1158                          '',
    1159                          ' ' + gmTools.u_writing_hand,
    1160                          ' ' + gmTools.u_writing_hand
    1161                  )
    1162                  txt = '%s %s: %s%s%s%s%s%s' % (
    1163                          gmDateTime.pydt_strftime (
    1164                                  self._payload[self._idx['clin_when']],
    1165                                  date_format
    1166                          ),
    1167                          self._payload[self._idx['name_tt']],
    1168                          self._payload[self._idx['unified_val']],
    1169                          gmTools.coalesce(self._payload[self._idx['val_unit']], '', ' %s'),
    1170                          gmTools.coalesce(self._payload[self._idx['abnormality_indicator']], '', ' %s'),
    1171                          gmTools.coalesce(range_info, '', ' (%s)'),
    1172                          gmTools.coalesce(self._payload[self._idx['status']], '', ' [%s]')[:2],
    1173                          review
    1174                  )
    1175                  if with_notes:
    1176                          txt += '\n'
    1177                          if self._payload[self._idx['note_test_org']] is not None:
    1178                                  txt += ' ' + _('Lab comment: %s\n') % _('\n Lab comment: ').join(self._payload[self._idx['note_test_org']].split('\n'))
    1179                          if self._payload[self._idx['comment']] is not None:
    1180                                  txt += ' ' + _('Praxis comment: %s\n') % _('\n Praxis comment: ').join(self._payload[self._idx['comment']].split('\n'))
    1181
    1182                  return txt.strip('\n')
    1183
    1184          #--------------------------------------------------------
    1185 -        def format(self, with_review=True, with_evaluation=True, with_ranges=True, with_episode=True, with_type_details=True, with_source_data=False, date_format='%Y %b %d %H:%M'):
    1186
    1187                  # FIXME: add battery, request details
    1188
    1189                  # header
    1190                  tt = _('Result from %s             \n') % gmDateTime.pydt_strftime (
    1191                          self._payload[self._idx['clin_when']],
    1192                          date_format
    1193                  )
    1194
    1195                  # basics
    1196                  tt += ' ' + _('Type: "%(name)s" (%(abbr)s)  [#%(pk_type)s]\n') % ({
    1197                          'name': self._payload[self._idx['name_tt']],
    1198                          'abbr': self._payload[self._idx['abbrev_tt']],
    1199                          'pk_type': self._payload[self._idx['pk_test_type']]
    1200                  })
    1201                  if self.is_long_text:
    1202                          sso = gmTools.u_superscript_one
    1203                  else:
    1204                          sso = ''
    1205                  tt += ' ' + _('%(sso)sResult: %(val)s%(unit)s%(ind)s  [#%(pk_result)s]\n') % ({
    1206                          'sso': sso,
    1207                          'val': self._payload[self._idx['unified_val']],
    1208                          'unit': gmTools.coalesce(self._payload[self._idx['val_unit']], '', ' %s'),
    1209                          'ind': gmTools.coalesce(self._payload[self._idx['abnormality_indicator']], '', ' (%s)'),
    1210                          'pk_result': self._payload[self._idx['pk_test_result']]
    1211                  })
    1212
    1213                  if self._payload[self._idx['status']] is not None:
    1214                          try:
    1215                                  stat = HL7_RESULT_STATI[self._payload[self._idx['status']]]
    1216                          except KeyError:
    1217                                  stat = self._payload[self._idx['status']]
    1218                          tt += ' ' + _('Status: %s\n') % stat
    1219                  if self._payload[self._idx['val_grouping']] is not None:
    1220                          tt += ' ' + _('Grouping: %s\n') % self._payload[self._idx['val_grouping']]
    1221
    1222                  if with_evaluation:
    1223                          norm_eval = None
    1224                          if self._payload[self._idx['val_num']] is not None:
    1225                                  # 1) normal range
    1226                                  # lowered ?
    1227                                  if (self._payload[self._idx['val_normal_min']] is not None) and (self._payload[self._idx['val_num']] < self._payload[self._idx['val_normal_min']]):
    1228                                          try:
    1229                                                  percent = (self._payload[self._idx['val_num']] * 100) / self._payload[self._idx['val_normal_min']]
    1230                                          except ZeroDivisionError:
    1231                                                  percent = None
    1232                                          if percent is not None:
    1233                                                  if percent < 6:
    1234                                                          norm_eval = _('%.1f %% of the normal lower limit') % percent
    1235                                                  else:
    1236                                                          norm_eval = _('%.0f %% of the normal lower limit') % percent
    1237                                  # raised ?
    1238                                  if (self._payload[self._idx['val_normal_max']] is not None) and (self._payload[self._idx['val_num']] > self._payload[self._idx['val_normal_max']]):
    1239                                          try:
    1240                                                  x_times = self._payload[self._idx['val_num']] / self._payload[self._idx['val_normal_max']]
    1241                                          except ZeroDivisionError:
    1242                                                  x_times = None
    1243                                          if x_times is not None:
    1244                                                  if x_times < 10:
    1245                                                          norm_eval = _('%.1f times the normal upper limit') % x_times
    1246                                                  else:
    1247                                                          norm_eval = _('%.0f times the normal upper limit') % x_times
    1248                                  if norm_eval is not None:
    1249                                          tt += '  = %s\n' % norm_eval
    1250          #                       #-------------------------------------
    1251          #                       # this idea was shot down on the list
    1252          #                       #-------------------------------------
    1253          #                       # bandwidth of deviation
    1254          #                       if None not in [self._payload[self._idx['val_normal_min']], self._payload[self._idx['val_normal_max']]]:
    1255          #                               normal_width = self._payload[self._idx['val_normal_max']] - self._payload[self._idx['val_normal_min']]
    1256          #                               deviation_from_normal_range = None
    1257          #                               # below ?
    1258          #                               if self._payload[self._idx['val_num']] < self._payload[self._idx['val_normal_min']]:
    1259          #                                       deviation_from_normal_range = self._payload[self._idx['val_normal_min']] - self._payload[self._idx['val_num']]
    1260          #                               # above ?
    1261          #                               elif self._payload[self._idx['val_num']] > self._payload[self._idx['val_normal_max']]:
    1262          #                                       deviation_from_normal_range = self._payload[self._idx['val_num']] - self._payload[self._idx['val_normal_max']]
    1263          #                               if deviation_from_normal_range is None:
    1264          #                                       try:
    1265          #                                               times_deviation = deviation_from_normal_range / normal_width
    1266          #                                       except ZeroDivisionError:
    1267          #                                               times_deviation = None
    1268          #                                       if times_deviation is not None:
    1269          #                                               if times_deviation < 10:
    1270          #                                                       tt += u'  (%s)\n' % _(u'deviates by %.1f times of the normal range') % times_deviation
    1271          #                                               else:
    1272          #                                                       tt += u'  (%s)\n' % _(u'deviates by %.0f times of the normal range') % times_deviation
    1273          #                       #-------------------------------------
    1274
    1275                                  # 2) clinical target range
    1276                                  norm_eval = None
    1277                                  # lowered ?
    1278                                  if (self._payload[self._idx['val_target_min']] is not None) and (self._payload[self._idx['val_num']] < self._payload[self._idx['val_target_min']]):
    1279                                          try:
    1280                                                  percent = (self._payload[self._idx['val_num']] * 100) / self._payload[self._idx['val_target_min']]
    1281                                          except ZeroDivisionError:
    1282                                                  percent = None
    1283                                          if percent is not None:
    1284                                                  if percent < 6:
    1285                                                          norm_eval = _('%.1f %% of the target lower limit') % percent
    1286                                                  else:
    1287                                                          norm_eval = _('%.0f %% of the target lower limit') % percent
    1288                                  # raised ?
    1289                                  if (self._payload[self._idx['val_target_max']] is not None) and (self._payload[self._idx['val_num']] > self._payload[self._idx['val_target_max']]):
    1290                                          try:
    1291                                                  x_times = self._payload[self._idx['val_num']] / self._payload[self._idx['val_target_max']]
    1292                                          except ZeroDivisionError:
    1293                                                  x_times = None
    1294                                          if x_times is not None:
    1295                                                  if x_times < 10:
    1296                                                          norm_eval = _('%.1f times the target upper limit') % x_times
    1297                                                  else:
    1298                                                          norm_eval = _('%.0f times the target upper limit') % x_times
    1299                                  if norm_eval is not None:
    1300                                          tt += ' = %s\n' % norm_eval
    1301          #                       #-------------------------------------
    1302          #                       # this idea was shot down on the list
    1303          #                       #-------------------------------------
    1304          #                       # bandwidth of deviation
    1305          #                       if None not in [self._payload[self._idx['val_target_min']], self._payload[self._idx['val_target_max']]]:
    1306          #                               normal_width = self._payload[self._idx['val_target_max']] - self._payload[self._idx['val_target_min']]
    1307          #                               deviation_from_target_range = None
    1308          #                               # below ?
    1309          #                               if self._payload[self._idx['val_num']] < self._payload[self._idx['val_target_min']]:
    1310          #                                       deviation_from_target_range = self._payload[self._idx['val_target_min']] - self._payload[self._idx['val_num']]
    1311          #                               # above ?
    1312          #                               elif self._payload[self._idx['val_num']] > self._payload[self._idx['val_target_max']]:
    1313          #                                       deviation_from_target_range = self._payload[self._idx['val_num']] - self._payload[self._idx['val_target_max']]
    1314          #                               if deviation_from_target_range is None:
    1315          #                                       try:
    1316          #                                               times_deviation = deviation_from_target_range / normal_width
    1317          #                                       except ZeroDivisionError:
    1318          #                                               times_deviation = None
    1319          #                               if times_deviation is not None:
    1320          #                                       if times_deviation < 10:
    1321          #                                               tt += u'  (%s)\n' % _(u'deviates by %.1f times of the target range') % times_deviation
    1322          #                                       else:
    1323          #                                               tt += u'  (%s)\n' % _(u'deviates by %.0f times of the target range') % times_deviation
    1324          #                       #-------------------------------------
    1325
    1326                  tmp = ('%s%s' % (
    1327                          gmTools.coalesce(self._payload[self._idx['name_test_org']], ''),
    1328                          gmTools.coalesce(self._payload[self._idx['contact_test_org']], '', ' (%s)'),
    1329                  )).strip()
    1330                  if tmp != '':
    1331                          tt += ' ' + _('Source: %s\n') % tmp
    1332                  tt += '\n'
    1333                  if self._payload[self._idx['note_test_org']] is not None:
    1334                          tt += ' ' + gmTools.u_superscript_one + _('Lab comment: %s\n') % _('\n Lab comment: ').join(self._payload[self._idx['note_test_org']].split('\n'))
    1335                  if self._payload[self._idx['comment']] is not None:
    1336                          tt += ' ' + gmTools.u_superscript_one + _('Praxis comment: %s\n') % _('\n Praxis comment: ').join(self._payload[self._idx['comment']].split('\n'))
    1337
    1338                  if with_ranges:
    1339                          tt += gmTools.coalesce(self.formatted_normal_range, '', ' ' + _('Standard normal range: %s\n'))
    1340                          tt += gmTools.coalesce(self.formatted_clinical_range, '', ' ' + _('Clinical target range: %s\n'))
    1341                          tt += gmTools.coalesce(self._payload[self._idx['norm_ref_group']], '', ' ' + _('Reference group: %s\n'))
    1342
    1343                  # metadata
    1344                  if with_episode:
    1345                          tt += ' ' + _('Episode: %s\n') % self._payload[self._idx['episode']]
    1346                          if self._payload[self._idx['health_issue']] is not None:
    1347                                  tt += ' ' + _('Issue: %s\n') % self._payload[self._idx['health_issue']]
    1348                  if self._payload[self._idx['material']] is not None:
    1349                          tt += ' ' + _('Material: %s\n') % self._payload[self._idx['material']]
    1350                  if self._payload[self._idx['material_detail']] is not None:
    1351                          tt += ' ' + _('Details: %s\n') % self._payload[self._idx['material_detail']]
    1352                  tt += '\n'
    1353
    1354                  if with_review:
    1355                          if self._payload[self._idx['reviewed']]:
    1356                                  review = gmDateTime.pydt_strftime (
    1357                                          self._payload[self._idx['last_reviewed']],
    1358                                          date_format
    1359                                  )
    1360                          else:
    1361                                  review = _('not yet')
    1362                          tt += _('Signed (%(sig_hand)s): %(reviewed)s\n') % ({
    1363                                  'sig_hand': gmTools.u_writing_hand,
    1364                                  'reviewed': review
    1365                          })
    1366                          tt += ' ' + _('Responsible clinician: %s\n') % gmTools.bool2subst (
    1367                                  self._payload[self._idx['you_are_responsible']],
    1368                                  _('you'),
    1369                                  self._payload[self._idx['responsible_reviewer']]
    1370                          )
    1371                          if self._payload[self._idx['reviewed']]:
    1372                                  tt += ' ' + _('Last reviewer: %(reviewer)s\n') % ({
    1373                                          'reviewer': gmTools.bool2subst (
    1374                                                  self._payload[self._idx['review_by_you']],
    1375                                                  _('you'),
    1376                                                  gmTools.coalesce(self._payload[self._idx['last_reviewer']], '?')
    1377                                          )
    1378                                  })
    1379                                  tt += ' ' + _(' Technically abnormal: %(abnormal)s\n') % ({
    1380                                          'abnormal': gmTools.bool2subst (
    1381                                                  self._payload[self._idx['is_technically_abnormal']],
    1382                                                  _('yes'),
    1383                                                  _('no'),
    1384                                                  '?'
    1385                                          )
    1386                                  })
    1387                                  tt += ' ' + _(' Clinically relevant: %(relevant)s\n') % ({
    1388                                          'relevant': gmTools.bool2subst (
    1389                                                  self._payload[self._idx['is_clinically_relevant']],
    1390                                                  _('yes'),
    1391                                                  _('no'),
    1392                                                  '?'
    1393                                          )
    1394                                  })
    1395                          if self._payload[self._idx['review_comment']] is not None:
    1396                                  tt += ' ' + _(' Comment: %s\n') % self._payload[self._idx['review_comment']].strip()
    1397                          tt += '\n'
    1398
    1399                  # type
    1400                  if with_type_details:
    1401                          has_details = None not in [self._payload[self._idx['comment_tt']], self._payload[self._idx['pk_meta_test_type']], self._payload[self._idx['comment_meta']]]
    1402                          if has_details:
    1403                                  tt += _('Test type details:\n')
    1404                          if self._payload[self._idx['comment_tt']] is not None:
    1405                                  tt += ' ' + _('Type comment: %s\n') % _('\n Type comment:').join(self._payload[self._idx['comment_tt']].split('\n'))
    1406                          if self._payload[self._idx['pk_meta_test_type']] is not None:
    1407                                  tt += ' ' + _('Aggregated (%s) under: %s (%s)  [#%s]\n') % (
    1408                                          gmTools.u_sum,
    1409                                          self._payload[self._idx['name_meta']],
    1410                                          self._payload[self._idx['abbrev_meta']],
    1411                                          self._payload[self._idx['pk_meta_test_type']]
    1412                                  )
    1413                          if self._payload[self._idx['comment_meta']] is not None:
    1414                                  tt += ' ' + _('Group comment: %s\n') % _('\n Group comment: ').join(self._payload[self._idx['comment_meta']].split('\n'))
    1415                          if has_details:
    1416                                  tt += '\n'
    1417
    1418                  if with_source_data:
    1419                          if self._payload[self._idx['source_data']] is not None:
    1420                                  tt += _('Source data:\n')
    1421                                  tt += ' ' + self._payload[self._idx['source_data']]
    1422                                  tt += '\n\n'
    1423
    1424                  if with_review:
    1425                          tt += _('Revisions: %(row_ver)s, last %(mod_when)s by %(mod_by)s.') % ({
    1426                                  'row_ver': self._payload[self._idx['row_version']],
    1427                                  'mod_when': gmDateTime.pydt_strftime(self._payload[self._idx['modified_when']],date_format),
    1428                                  'mod_by': self._payload[self._idx['modified_by']]
    1429                          })
    1430
    1431                  return tt
    1432
    1433          #--------------------------------------------------------
    1434 -        def _get_has_normal_min_or_max(self):
    1435                  return (
    1436                          self._payload[self._idx['val_normal_min']] is not None
    1437                  ) or (
    1438                          self._payload[self._idx['val_normal_max']] is not None
    1439                  )
    1440
    1441          has_normal_min_or_max = property(_get_has_normal_min_or_max, lambda x:x)
    1442
    1443          #--------------------------------------------------------
    1444 -        def _get_normal_min_max(self):
    1445                  has_range_info = (
    1446                          self._payload[self._idx['val_normal_min']] is not None
    1447                  ) or (
    1448                          self._payload[self._idx['val_normal_max']] is not None
    1449                  )
    1450                  if has_range_info is False:
    1451                          return None
    1452
    1453                  return '%s - %s' % (
    1454                          gmTools.coalesce(self._payload[self._idx['val_normal_min']], '?'),
    1455                          gmTools.coalesce(self._payload[self._idx['val_normal_max']], '?')
    1456                  )
    1457
    1458          normal_min_max = property(_get_normal_min_max, lambda x:x)
    1459
    1460          #--------------------------------------------------------
    1461 -        def _get_formatted_normal_range(self):
    1462                  has_numerical_range = (
    1463                          self._payload[self._idx['val_normal_min']] is not None
    1464                  ) or (
    1465                          self._payload[self._idx['val_normal_max']] is not None
    1466                  )
    1467                  if has_numerical_range:
    1468                          numerical_range = '%s - %s' % (
    1469                                  gmTools.coalesce(self._payload[self._idx['val_normal_min']], '?'),
    1470                                  gmTools.coalesce(self._payload[self._idx['val_normal_max']], '?')
    1471                          )
    1472                  else:
    1473                          numerical_range = ''
    1474                  textual_range = gmTools.coalesce (
    1475                          self._payload[self._idx['val_normal_range']],
    1476                          '',
    1477                          gmTools.bool2subst (
    1478                                  has_numerical_range,
    1479                                  ' / %s',
    1480                                  '%s'
    1481                          )
    1482                  )
    1483                  range_info = '%s%s' % (numerical_range, textual_range)
    1484                  if range_info == '':
    1485                          return None
    1486                  return range_info
    1487
    1488          formatted_normal_range = property(_get_formatted_normal_range, lambda x:x)
    1489
    1490          #--------------------------------------------------------
    1491 -        def _get_has_clinical_min_or_max(self):
    1492                  return (
    1493                          self._payload[self._idx['val_target_min']] is not None
    1494                  ) or (
    1495                          self._payload[self._idx['val_target_max']] is not None
    1496                  )
    1497
    1498          has_clinical_min_or_max = property(_get_has_clinical_min_or_max, lambda x:x)
    1499
    1500          #--------------------------------------------------------
    1501 -        def _get_clinical_min_max(self):
    1502                  has_range_info = (
    1503                          self._payload[self._idx['val_target_min']] is not None
    1504                  ) or (
    1505                          self._payload[self._idx['val_target_max']] is not None
    1506                  )
    1507                  if has_range_info is False:
    1508                          return None
    1509
    1510                  return '%s - %s' % (
    1511                          gmTools.coalesce(self._payload[self._idx['val_target_min']], '?'),
    1512                          gmTools.coalesce(self._payload[self._idx['val_target_max']], '?')
    1513                  )
    1514
    1515          clinical_min_max = property(_get_clinical_min_max, lambda x:x)
    1516
    1517          #--------------------------------------------------------
    1518 -        def _get_formatted_clinical_range(self):
    1519                  has_numerical_range = (
    1520                          self._payload[self._idx['val_target_min']] is not None
    1521                  ) or (
    1522                          self._payload[self._idx['val_target_max']] is not None
    1523                  )
    1524                  if has_numerical_range:
    1525                          numerical_range = '%s - %s' % (
    1526                                  gmTools.coalesce(self._payload[self._idx['val_target_min']], '?'),
    1527                                  gmTools.coalesce(self._payload[self._idx['val_target_max']], '?')
    1528                          )
    1529                  else:
    1530                          numerical_range = ''
    1531                  textual_range = gmTools.coalesce (
    1532                          self._payload[self._idx['val_target_range']],
    1533                          '',
    1534                          gmTools.bool2subst (
    1535                                  has_numerical_range,
    1536                                  ' / %s',
    1537                                  '%s'
    1538                          )
    1539                  )
    1540                  range_info = '%s%s' % (numerical_range, textual_range)
    1541                  if range_info == '':
    1542                          return None
    1543                  return range_info
    1544
    1545          formatted_clinical_range = property(_get_formatted_clinical_range, lambda x:x)
    1546
    1547          #--------------------------------------------------------
    1548 -        def _get_temporally_closest_normal_range(self):
    1549                  """Returns the closest test result which does have normal range information."""
    1550                  if self._payload[self._idx['val_normal_min']] is not None:
    1551                          return self
    1552                  if self._payload[self._idx['val_normal_max']] is not None:
    1553                          return self
    1554                  if self._payload[self._idx['val_normal_range']] is not None:
    1555                          return self
    1556                  cmd = """
    1557                          SELECT * from clin.v_test_results
    1558                          WHERE
    1559                                  pk_type = %(pk_type)s
    1560                                          AND
    1561                                  val_unit = %(unit)s
    1562                                          AND
    1563                                  (
    1564                                          (val_normal_min IS NOT NULL)
    1565                                                  OR
    1566                                          (val_normal_max IS NOT NULL)
    1567                                                  OR
    1568                                          (val_normal_range IS NOT NULL)
    1569                                  )
    1570                          ORDER BY
    1571                                  CASE
    1572                                          WHEN clin_when > %(clin_when)s THEN clin_when - %(clin_when)s
    1573                                          ELSE %(clin_when)s - clin_when
    1574                                  END
    1575                          LIMIT 1"""
    1576                  args = {
    1577                          'pk_type': self._payload[self._idx['pk_test_type']],
    1578                          'unit': self._payload[self._idx['val_unit']],
    1579                          'clin_when': self._payload[self._idx['clin_when']]
    1580                  }
    1581                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1582                  if len(rows) == 0:
    1583                          return None
    1584                  return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
    1585
    1586          temporally_closest_normal_range = property(_get_temporally_closest_normal_range, lambda x:x)
    1587
    1588          #--------------------------------------------------------
    1589 -        def _get_formatted_range(self):
    1590
    1591                  has_normal_min_or_max = (
    1592                          self._payload[self._idx['val_normal_min']] is not None
    1593                  ) or (
    1594                          self._payload[self._idx['val_normal_max']] is not None
    1595                  )
    1596                  if has_normal_min_or_max:
    1597                          normal_min_max = '%s - %s' % (
    1598                                  gmTools.coalesce(self._payload[self._idx['val_normal_min']], '?'),
    1599                                  gmTools.coalesce(self._payload[self._idx['val_normal_max']], '?')
    1600                          )
    1601
    1602                  has_clinical_min_or_max = (
    1603                          self._payload[self._idx['val_target_min']] is not None
    1604                  ) or (
    1605                          self._payload[self._idx['val_target_max']] is not None
    1606                  )
    1607                  if has_clinical_min_or_max:
    1608                          clinical_min_max = '%s - %s' % (
    1609                                  gmTools.coalesce(self._payload[self._idx['val_target_min']], '?'),
    1610                                  gmTools.coalesce(self._payload[self._idx['val_target_max']], '?')
    1611                          )
    1612
    1613                  if has_clinical_min_or_max:
    1614                          return _('Target: %(clin_min_max)s%(clin_range)s') % ({
    1615                                  'clin_min_max': clinical_min_max,
    1616                                  'clin_range': gmTools.coalesce (
    1617                                          self._payload[self._idx['val_target_range']],
    1618                                          '',
    1619                                          gmTools.bool2subst (
    1620                                                  has_clinical_min_or_max,
    1621                                                  ' / %s',
    1622                                                  '%s'
    1623                                          )
    1624                                  )
    1625                          })
    1626
    1627                  if has_normal_min_or_max:
    1628                          return _('Norm: %(norm_min_max)s%(norm_range)s') % ({
    1629                                  'norm_min_max': normal_min_max,
    1630                                  'norm_range': gmTools.coalesce (
    1631                                          self._payload[self._idx['val_normal_range']],
    1632                                          '',
    1633                                          gmTools.bool2subst (
    1634                                                  has_normal_min_or_max,
    1635                                                  ' / %s',
    1636                                                  '%s'
    1637                                          )
    1638                                  )
    1639                          })
    1640
    1641                  if self._payload[self._idx['val_target_range']] is not None:
    1642                          return _('Target: %s') % self._payload[self._idx['val_target_range']],
    1643
    1644                  if self._payload[self._idx['val_normal_range']] is not None:
    1645                          return _('Norm: %s') % self._payload[self._idx['val_normal_range']]
    1646
    1647                  return None
    1648
    1649          formatted_range = property(_get_formatted_range, lambda x:x)
    1650
    1651          #--------------------------------------------------------
    1652 -        def _get_test_type(self):
    1653                  return cMeasurementType(aPK_obj = self._payload[self._idx['pk_test_type']])
    1654
    1655          test_type = property(_get_test_type, lambda x:x)
    1656
    1657          #--------------------------------------------------------
    1658 -        def _get_is_considered_elevated(self):
    1659                  # 1) the user is right (review)
    1660                  if self._payload[self._idx['is_technically_abnormal']] is False:
    1661                          return False
    1662                  # 2) the lab is right (result.abnormality_indicator)
    1663                  indicator = self._payload[self._idx['abnormality_indicator']]
    1664                  if indicator is not None:
    1665                          indicator = indicator.strip()
    1666                          if indicator != '':
    1667                                  if indicator.strip('+') == '':
    1668                                          return True
    1669                                  if indicator.strip('-') == '':
    1670                                          return False
    1671                  # 3) non-numerical value ?
    1672                  if self._payload[self._idx['val_num']] is None:
    1673                          return None
    1674                  # 4) the target range is right
    1675                  target_max = self._payload[self._idx['val_target_max']]
    1676                  if target_max is not None:
    1677                          if target_max < self._payload[self._idx['val_num']]:
    1678                                  return True
    1679                  # 4) the normal range is right
    1680                  normal_max = self._payload[self._idx['val_normal_max']]
    1681                  if normal_max is not None:
    1682                          if normal_max < self._payload[self._idx['val_num']]:
    1683                                  return True
    1684                  return None
    1685
    1686          is_considered_elevated = property(_get_is_considered_elevated, lambda x:x)
    1687
    1688          #--------------------------------------------------------
    1689 -        def _get_is_considered_lowered(self):
    1690                  # 1) the user is right (review)
    1691                  if self._payload[self._idx['is_technically_abnormal']] is False:
    1692                          return False
    1693                  # 2) the lab is right (result.abnormality_indicator)
    1694                  indicator = self._payload[self._idx['abnormality_indicator']]
    1695                  if indicator is not None:
    1696                          indicator = indicator.strip()
    1697                          if indicator != '':
    1698                                  if indicator.strip('+') == '':
    1699                                          return False
    1700                                  if indicator.strip('-') == '':
    1701                                          return True
    1702                  # 3) non-numerical value ?
    1703                  if self._payload[self._idx['val_num']] is None:
    1704                          return None
    1705                  # 4) the target range is right
    1706                  target_min = self._payload[self._idx['val_target_min']]
    1707                  if target_min is not None:
    1708                          if target_min > self._payload[self._idx['val_num']]:
    1709                                  return True
    1710                  # 4) the normal range is right
    1711                  normal_min = self._payload[self._idx['val_normal_min']]
    1712                  if normal_min is not None:
    1713                          if normal_min > self._payload[self._idx['val_num']]:
    1714                                  return True
    1715                  return None
    1716
    1717          is_considered_lowered = property(_get_is_considered_lowered, lambda x:x)
    1718
    1719          #--------------------------------------------------------
    1720 -        def _get_is_considered_abnormal(self):
    1721                  if self.is_considered_lowered is True:
    1722                          return True
    1723                  if self.is_considered_elevated is True:
    1724                          return True
    1725                  if (self.is_considered_lowered is False) and (self.is_considered_elevated is False):
    1726                          return False
    1727                  return self._payload[self._idx['is_technically_abnormal']]
    1728
    1729          is_considered_abnormal = property(_get_is_considered_abnormal, lambda x:x)
    1730
    1731          #--------------------------------------------------------
    1732 -        def _set_reference_range(self, ref_range):
    1733                  """Parse reference range from string.
    1734
    1735                          Note: does NOT save the result.
    1736                  """
    1737                  ref_range = ref_range.strip().replace(' ', '')
    1738
    1739                  is_range = regex.match('-{0,1}\d+[.,]{0,1}\d*--{0,1}\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE)
    1740                  if is_range is not None:
    1741                          min_val = regex.match('-{0,1}\d+[.,]{0,1}\d*-', ref_range, regex.UNICODE).group(0).rstrip('-')
    1742                          success, min_val = gmTools.input2decimal(min_val)
    1743                          max_val = (regex.search('--{0,1}\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE).group(0))[1:]
    1744                          success, max_val = gmTools.input2decimal(max_val)
    1745                          self['val_normal_min'] = min_val
    1746                          self['val_normal_max'] = max_val
    1747                          return
    1748
    1749                  if ref_range.startswith('<'):
    1750                          is_range = regex.match('<\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE)
    1751                          if is_range is not None:
    1752                                  max_val = ref_range[1:]
    1753                                  success, max_val = gmTools.input2decimal(max_val)
    1754                                  self['val_normal_min'] = 0
    1755                                  self['val_normal_max'] = max_val
    1756                                  return
    1757
    1758                  if ref_range.startswith('<-'):
    1759                          is_range = regex.match('<-\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE)
    1760                          if is_range is not None:
    1761                                  max_val = ref_range[1:]
    1762                                  success, max_val = gmTools.input2decimal(max_val)
    1763                                  self['val_normal_min'] = None
    1764                                  self['val_normal_max'] = max_val
    1765                                  return
    1766
    1767                  if ref_range.startswith('>'):
    1768                          is_range = regex.match('>\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE)
    1769                          if is_range is not None:
    1770                                  min_val = ref_range[1:]
    1771                                  success, min_val = gmTools.input2decimal(min_val)
    1772                                  self['val_normal_min'] = min_val
    1773                                  self['val_normal_max'] = None
    1774                                  return
    1775
    1776                  if ref_range.startswith('>-'):
    1777                          is_range = regex.match('>-\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE)
    1778                          if is_range is not None:
    1779                                  min_val = ref_range[1:]
    1780                                  success, min_val = gmTools.input2decimal(min_val)
    1781                                  self['val_normal_min'] = min_val
    1782                                  self['val_normal_max'] = 0
    1783                                  return
    1784
    1785                  self['val_normal_range'] = ref_range
    1786                  return
    1787
    1788          reference_range = property(lambda x:x, _set_reference_range)
    1789
    1790          #--------------------------------------------------------
    1791 -        def _get_formatted_abnormality_indicator(self):
    1792                  # 1) the user is right
    1793                  if self._payload[self._idx['is_technically_abnormal']] is False:
    1794                          return ''
    1795                  # 2) the lab is right (result.abnormality_indicator)
    1796                  indicator = self._payload[self._idx['abnormality_indicator']]
    1797                  if indicator is not None:
    1798                          indicator = indicator.strip()
    1799                          if indicator != '':
    1800                                  return indicator
    1801                  # 3) non-numerical value ? then we can't know more
    1802                  if self._payload[self._idx['val_num']] is None:
    1803                          return None
    1804                  # 4) the target range is right
    1805                  target_min = self._payload[self._idx['val_target_min']]
    1806                  if target_min is not None:
    1807                          if target_min > self._payload[self._idx['val_num']]:
    1808                                  return '-'
    1809                  target_max = self._payload[self._idx['val_target_max']]
    1810                  if target_max is not None:
    1811                          if target_max < self._payload[self._idx['val_num']]:
    1812                                  return '+'
    1813                  # 4) the normal range is right
    1814                  normal_min = self._payload[self._idx['val_normal_min']]
    1815                  if normal_min is not None:
    1816                          if normal_min > self._payload[self._idx['val_num']]:
    1817                                  return '-'
    1818                  normal_max = self._payload[self._idx['val_normal_max']]
    1819                  if normal_max is not None:
    1820                          if normal_max < self._payload[self._idx['val_num']]:
    1821                                  return '+'
    1822                  # reviewed, abnormal, but no indicator available
    1823                  if self._payload[self._idx['is_technically_abnormal']] is True:
    1824                          return gmTools.u_plus_minus
    1825
    1826                  return None
    1827
    1828          formatted_abnormality_indicator = property(_get_formatted_abnormality_indicator, lambda x:x)
    1829
    1830          #--------------------------------------------------------
    1831 -        def _get_is_long_text(self):
    1832                  if self._payload[self._idx['val_alpha']] is None:
    1833                          return False
    1834                  lines = gmTools.strip_empty_lines(text = self._payload[self._idx['val_alpha']], eol = '\n', return_list = True)
    1835                  if len(lines) > 4:
    1836                          return True
    1837                  return False
    1838
    1839          is_long_text = property(_get_is_long_text, lambda x:x)
    1840
    1841          #--------------------------------------------------------
    1842 -        def _get_estimate_numeric_value_from_alpha(self):
    1843                  if self._payload[self._idx['val_alpha']] is None:
    1844                          return None
    1845                  val = self._payload[self._idx['val_alpha']].lstrip()
    1846                  if val[0] == '<':
    1847                          factor = decimal.Decimal(0.5)
    1848                          val = val[1:]
    1849                  elif val[0] == '>':
    1850                          factor = 2
    1851                          val = val[1:]
    1852                  else:
    1853                          return None
    1854                  success, val = gmTools.input2decimal(initial = val)
    1855                  if not success:
    1856                          return None
    1857                  return val * factor
    1858
    1859          estimate_numeric_value_from_alpha = property(_get_estimate_numeric_value_from_alpha, lambda x:x)
    1860
    1861          #--------------------------------------------------------
    1862 -        def set_review(self, technically_abnormal=None, clinically_relevant=None, comment=None, make_me_responsible=False):
    1863
    1864                  # FIXME: this is not concurrency safe
    1865                  if self._payload[self._idx['reviewed']]:
    1866                          self.__change_existing_review (
    1867                                  technically_abnormal = technically_abnormal,
    1868                                  clinically_relevant = clinically_relevant,
    1869                                  comment = comment
    1870                          )
    1871                  else:
    1872                          # do not sign off unreviewed results if
    1873                          # NOTHING AT ALL is known about them
    1874                          if technically_abnormal is None:
    1875                                  if clinically_relevant is None:
    1876                                          comment = gmTools.none_if(comment, '', strip_string = True)
    1877                                          if comment is None:
    1878                                                  if make_me_responsible is False:
    1879                                                          return True
    1880                          self.__set_new_review (
    1881                                  technically_abnormal = technically_abnormal,
    1882                                  clinically_relevant = clinically_relevant,
    1883                                  comment = comment
    1884                          )
    1885
    1886                  if make_me_responsible is True:
    1887                          cmd = "SELECT pk FROM dem.staff WHERE db_user = current_user"
    1888                          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
    1889                          self['pk_intended_reviewer'] = rows[0][0]
    1890                          self.save_payload()
    1891                          return
    1892
    1893                  self.refetch_payload()
    1894
    1895          #--------------------------------------------------------
    1896 -        def get_adjacent_results(self, desired_earlier_results=1, desired_later_results=1, max_offset=None):
    1897
    1898                  if desired_earlier_results < 1:
    1899                          raise ValueError('<desired_earlier_results> must be > 0')
    1900
    1901                  if desired_later_results < 1:
    1902                          raise ValueError('<desired_later_results> must be > 0')
    1903
    1904                  args = {
    1905                          'pat': self._payload[self._idx['pk_patient']],
    1906                          'ttyp': self._payload[self._idx['pk_test_type']],
    1907                          'tloinc': self._payload[self._idx['loinc_tt']],
    1908                          'mtyp': self._payload[self._idx['pk_meta_test_type']],
    1909                          'mloinc': self._payload[self._idx['loinc_meta']],
    1910                          'when': self._payload[self._idx['clin_when']],
    1911                          'offset': max_offset
    1912                  }
    1913                  WHERE = '((pk_test_type = %(ttyp)s) OR (loinc_tt = %(tloinc)s))'
    1914                  WHERE_meta = '((pk_meta_test_type = %(mtyp)s) OR (loinc_meta = %(mloinc)s))'
    1915                  if max_offset is not None:
    1916                          WHERE = WHERE + ' AND (clin_when BETWEEN (%(when)s - %(offset)s) AND (%(when)s + %(offset)s))'
    1917                          WHERE_meta = WHERE_meta + ' AND (clin_when BETWEEN (%(when)s - %(offset)s) AND (%(when)s + %(offset)s))'
    1918
    1919                  SQL = """
    1920                          SELECT * FROM clin.v_test_results
    1921                          WHERE
    1922                                  pk_patient = %%(pat)s
    1923                                          AND
    1924                                  clin_when %s %%(when)s
    1925                                          AND
    1926                                  %s
    1927                          ORDER BY clin_when
    1928                          LIMIT %s"""
    1929
    1930                  # get earlier results
    1931                  earlier_results = []
    1932                  # by type
    1933                  cmd = SQL % ('<', WHERE, desired_earlier_results)
    1934                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1935                  if len(rows) > 0:
    1936                          earlier_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ])
    1937                  # by meta type ?
    1938                  missing_results = desired_earlier_results - len(earlier_results)
    1939                  if  missing_results > 0:
    1940                          cmd = SQL % ('<', WHERE_meta, missing_results)
    1941                          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1942                          if len(rows) > 0:
    1943                                  earlier_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ])
    1944
    1945                  # get later results
    1946                  later_results = []
    1947                  # by type
    1948                  cmd = SQL % ('>', WHERE, desired_later_results)
    1949                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1950                  if len(rows) > 0:
    1951                          later_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ])
    1952                  # by meta type ?
    1953                  missing_results = desired_later_results - len(later_results)
    1954                  if  missing_results > 0:
    1955                          cmd = SQL % ('>', WHERE_meta, missing_results)
    1956                          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1957                          if len(rows) > 0:
    1958                                  later_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ])
    1959
    1960                  return earlier_results, later_results
    1961
    1962          #--------------------------------------------------------
    1963          # internal API
    1964          #--------------------------------------------------------
    1965 -        def __set_new_review(self, technically_abnormal=None, clinically_relevant=None, comment=None):
    1966                  """Add a review to a row.
    1967
    1968                          - if technically abnormal is not provided/None it will be set
    1969                            to True if the lab's indicator has a meaningful value
    1970                          - if clinically relevant is not provided/None it is set to
    1971                            whatever technically abnormal is
    1972                  """
    1973                  if technically_abnormal is None:
    1974                          technically_abnormal = False
    1975                          if self._payload[self._idx['abnormality_indicator']] is not None:
    1976                                  if self._payload[self._idx['abnormality_indicator']].strip() != '':
    1977                                          technically_abnormal = True
    1978
    1979                  if clinically_relevant is None:
    1980                          clinically_relevant = technically_abnormal
    1981
    1982                  cmd = """
    1983  INSERT INTO clin.reviewed_test_results (
    1984          fk_reviewed_row,
    1985          is_technically_abnormal,
    1986          clinically_relevant,
    1987          comment
    1988  ) VALUES (
    1989          %(pk)s,
    1990          %(abnormal)s,
    1991          %(relevant)s,
    1992          gm.nullify_empty_string(%(cmt)s)
    1993  )"""
    1994                  args = {
    1995                          'pk': self._payload[self._idx['pk_test_result']],
    1996                          'abnormal': technically_abnormal,
    1997                          'relevant': clinically_relevant,
    1998                          'cmt': comment
    1999                  }
    2000
    2001                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    2002
    2003          #--------------------------------------------------------
    2004 -        def __change_existing_review(self, technically_abnormal=None, clinically_relevant=None, comment=None):
    2005                  """Change a review on a row.
    2006
    2007                          - if technically abnormal/clinically relevant are
    2008                            None they are not set
    2009                  """
    2010                  args = {
    2011                          'pk_result': self._payload[self._idx['pk_test_result']],
    2012                          'abnormal': technically_abnormal,
    2013                          'relevant': clinically_relevant,
    2014                          'cmt': comment
    2015                  }
    2016
    2017                  set_parts = [
    2018                          'fk_reviewer = (SELECT pk FROM dem.staff WHERE db_user = current_user)',
    2019                          'comment = gm.nullify_empty_string(%(cmt)s)'
    2020                  ]
    2021
    2022                  if technically_abnormal is not None:
    2023                          set_parts.append('is_technically_abnormal = %(abnormal)s')
    2024
    2025                  if clinically_relevant is not None:
    2026                          set_parts.append('clinically_relevant = %(relevant)s')
    2027
    2028                  cmd = """
    2029  UPDATE clin.reviewed_test_results SET
    2030          %s
    2031  WHERE
    2032          fk_reviewed_row = %%(pk_result)s
    2033  """ % ',\n      '.join(set_parts)
    2034
    2035                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    2036
    2037  #------------------------------------------------------------
    2038 -def get_test_results(pk_patient=None, encounters=None, episodes=None, order_by=None, return_pks=False):
    2039
    2040          where_parts = []
    2041
    2042          if pk_patient is not None:
    2043                  where_parts.append('pk_patient = %(pat)s')
    2044                  args = {'pat': pk_patient}
    2045
    2046  #       if tests is not None:
    2047  #               where_parts.append(u'pk_test_type IN %(tests)s')
    2048  #               args['tests'] = tuple(tests)
    2049
    2050          if encounters is not None:
    2051                  where_parts.append('pk_encounter IN %(encs)s')
    2052                  args['encs'] = tuple(encounters)
    2053
    2054          if episodes is not None:
    2055                  where_parts.append('pk_episode IN %(epis)s')
    2056                  args['epis'] = tuple(episodes)
    2057
    2058          if order_by is None:
    2059                  order_by = ''
    2060          else:
    2061                  order_by = 'ORDER BY %s' % order_by
    2062
    2063          cmd = """
    2064                  SELECT * FROM clin.v_test_results
    2065                  WHERE %s
    2066                  %s
    2067          """ % (
    2068                  ' AND '.join(where_parts),
    2069                  order_by
    2070          )
    2071          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2072          if return_pks:
    2073                  return [ r['pk_test_result'] for r in rows ]
    2074          tests = [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
    2075          return tests
    2076
    2077  #------------------------------------------------------------
    2078 -def get_most_recent_results_for_panel(pk_patient=None, pk_panel=None, order_by=None, group_by_meta_type=False):
    2079
    2080          if order_by is None:
    2081                  order_by = ''
    2082          else:
    2083                  order_by = 'ORDER BY %s' % order_by
    2084
    2085          args = {
    2086                  'pat': pk_patient,
    2087                  'pk_pnl': pk_panel
    2088          }
    2089
    2090          if group_by_meta_type:
    2091                  # return most recent results in panel grouped by
    2092                  # meta test type if any, non-grouped results are
    2093                  # returned ungrouped :-)
    2094                  cmd = """
    2095                          SELECT c_vtr.*
    2096                          FROM (
    2097                                  -- max(clin_when) per test_type-in-panel for patient
    2098                                  SELECT
    2099                                          pk_meta_test_type,
    2100                                          MAX(clin_when) AS max_clin_when
    2101                                  FROM clin.v_test_results
    2102                                  WHERE
    2103                                          pk_patient = %(pat)s
    2104                                                  AND
    2105                                          pk_meta_test_type IS DISTINCT FROM NULL
    2106                                                  AND
    2107                                          pk_test_type IN (
    2108                                                  (SELECT c_vtt4tp.pk_test_type FROM clin.v_test_types4test_panel c_vtt4tp WHERE c_vtt4tp.pk_test_panel = %(pk_pnl)s)
    2109                                          )
    2110                                  GROUP BY pk_meta_test_type
    2111                          ) AS latest_results
    2112                                  INNER JOIN clin.v_test_results c_vtr ON
    2113                                          c_vtr.pk_meta_test_type = latest_results.pk_meta_test_type
    2114                                                  AND
    2115                                          c_vtr.clin_when = latest_results.max_clin_when
    2116
    2117                  UNION ALL
    2118
    2119                  SELECT c_vtr.*
    2120                  FROM (
    2121                          -- max(clin_when) per test_type-in-panel for patient
    2122                          SELECT
    2123                                  pk_test_type,
    2124                                  MAX(clin_when) AS max_clin_when
    2125                          FROM clin.v_test_results
    2126                          WHERE
    2127                                  pk_patient = %(pat)s
    2128                                          AND
    2129                                  pk_meta_test_type IS NULL
    2130                                          AND
    2131                                  pk_test_type IN (
    2132                                          (SELECT c_vtt4tp.pk_test_type FROM clin.v_test_types4test_panel c_vtt4tp WHERE c_vtt4tp.pk_test_panel = %(pk_pnl)s)
    2133                                  )
    2134                          GROUP BY pk_test_type
    2135                  ) AS latest_results
    2136                          INNER JOIN clin.v_test_results c_vtr ON
    2137                                  c_vtr.pk_test_type = latest_results.pk_test_type
    2138                                          AND
    2139                                  c_vtr.clin_when = latest_results.max_clin_when
    2140                  """
    2141          else:
    2142                  # return most recent results in panel regardless of whether
    2143                  # distinct test types in this panel are grouped under the
    2144                  # same meta test type
    2145                  cmd = """
    2146                          SELECT c_vtr.*
    2147                          FROM (
    2148                                  -- max(clin_when) per test_type-in-panel for patient
    2149                                  SELECT
    2150                                          pk_test_type,
    2151                                          MAX(clin_when) AS max_clin_when
    2152                                  FROM clin.v_test_results
    2153                                  WHERE
    2154                                          pk_patient = %(pat)s
    2155                                                  AND
    2156                                          pk_test_type IN (
    2157                                                  (SELECT c_vtt4tp.pk_test_type FROM clin.v_test_types4test_panel c_vtt4tp WHERE c_vtt4tp.pk_test_panel = %(pk_pnl)s)
    2158                                          )
    2159                                  GROUP BY pk_test_type
    2160                          ) AS latest_results
    2161                                  -- this INNER join makes certain we do not expand
    2162                                  -- the row selection beyond the patient's rows
    2163                                  -- which we constrained to inside the SELECT
    2164                                  -- producing "latest_results"
    2165                                  INNER JOIN clin.v_test_results c_vtr ON
    2166                                          c_vtr.pk_test_type = latest_results.pk_test_type
    2167                                                  AND
    2168                                          c_vtr.clin_when = latest_results.max_clin_when
    2169                          """
    2170          cmd += order_by
    2171          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2172          return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
    2173
    2174  #------------------------------------------------------------
    2175 -def get_result_at_timestamp(timestamp=None, test_type=None, loinc=None, tolerance_interval=None, patient=None):
    2176
    2177          if None not in [test_type, loinc]:
    2178                  raise ValueError('either <test_type> or <loinc> must be None')
    2179
    2180          args = {
    2181                  'pat': patient,
    2182                  'ttyp': test_type,
    2183                  'loinc': loinc,
    2184                  'ts': timestamp,
    2185                  'intv': tolerance_interval
    2186          }
    2187
    2188          where_parts = ['pk_patient = %(pat)s']
    2189          if test_type is not None:
    2190                  where_parts.append('pk_test_type = %(ttyp)s')           # consider: pk_meta_test_type = %(pkmtt)s / self._payload[self._idx['pk_meta_test_type']]
    2191          elif loinc is not None:
    2192                  where_parts.append('((loinc_tt IN %(loinc)s) OR (loinc_meta IN %(loinc)s))')
    2193                  args['loinc'] = tuple(loinc)
    2194
    2195          if tolerance_interval is None:
    2196                  where_parts.append('clin_when = %(ts)s')
    2197          else:
    2198                  where_parts.append('clin_when between (%(ts)s - %(intv)s::interval) AND (%(ts)s + %(intv)s::interval)')
    2199
    2200          cmd = """
    2201                  SELECT * FROM clin.v_test_results
    2202                  WHERE
    2203                          %s
    2204                  ORDER BY
    2205                          abs(extract(epoch from age(clin_when, %%(ts)s)))
    2206                  LIMIT 1""" % ' AND '.join(where_parts)
    2207
    2208          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2209          if len(rows) == 0:
    2210                  return None
    2211
    2212          return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
    2213
    2214  #------------------------------------------------------------
    2215 -def get_results_for_day(timestamp=None, patient=None, order_by=None):
    2216
    2217          args = {
    2218                  'pat': patient,
    2219                  'ts': timestamp
    2220          }
    2221
    2222          where_parts = [
    2223                  'pk_patient = %(pat)s',
    2224                  "date_trunc('day'::text, clin_when) = date_trunc('day'::text, %(ts)s)"
    2225          ]
    2226
    2227          cmd = """
    2228                  SELECT * FROM clin.v_test_results
    2229                  WHERE
    2230                          %s
    2231                  ORDER BY
    2232                          val_grouping,
    2233                          abbrev_tt,
    2234                          clin_when DESC
    2235          """ % ' AND '.join(where_parts)
    2236          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2237          return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
    2238
    2239  #------------------------------------------------------------
    2240 -def get_results_for_issue(pk_health_issue=None, order_by=None):
    2241          args = {'pk_issue': pk_health_issue}
    2242          where_parts = ['pk_health_issue = %(pk_issue)s']
    2243          cmd = """
    2244                  SELECT * FROM clin.v_test_results
    2245                  WHERE %s
    2246                  ORDER BY
    2247                          val_grouping,
    2248                          abbrev_tt,
    2249                          clin_when DESC
    2250          """ % ' AND '.join(where_parts)
    2251          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2252          return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
    2253
    2254  #------------------------------------------------------------
    2255 -def get_results_for_episode(pk_episode=None):
    2256          args = {'pk_epi': pk_episode}
    2257          where_parts = ['pk_episode = %(pk_epi)s']
    2258          cmd = """
    2259                  SELECT * FROM clin.v_test_results
    2260                  WHERE %s
    2261                  ORDER BY
    2262                          val_grouping,
    2263                          abbrev_tt,
    2264                          clin_when DESC
    2265          """ % ' AND '.join(where_parts)
    2266          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2267          return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
    2268
    2269  #------------------------------------------------------------
    2270 -def get_most_recent_results_in_loinc_group(loincs=None, max_no_of_results=1, patient=None, max_age=None, consider_indirect_matches=False):
    2271          """Get N most recent results *among* a list of tests selected by LOINC.
    2272
    2273                  1) get test types with LOINC (or meta type LOINC) in the group of <loincs>
    2274                  2) from these get the test results for <patient> within the given <max_age>
    2275                  3) from these return "the N=<max_no_of_results> most recent ones" or "None"
    2276
    2277                  <loinc> must be a list or tuple or set, NOT a single string
    2278                  <max_age> must be a string holding a PG interval or else a pydt interval
    2279          """
    2280          assert (max_no_of_results > 0), '<max_no_of_results> must be >0'
    2281
    2282          args = {'pat': patient, 'loincs': tuple(loincs)}
    2283          if max_age is None:
    2284                  max_age_cond = ''
    2285          else:
    2286                  max_age_cond = 'AND clin_when > (now() - %(max_age)s::interval)'
    2287                  args['max_age'] = max_age
    2288          cmd = """
    2289                  SELECT * FROM (
    2290                          SELECT DISTINCT ON (pk_test_result) *
    2291                          FROM clin.v_test_results
    2292                          WHERE
    2293                                  pk_patient = %%(pat)s
    2294                                          AND
    2295                                  unified_loinc IN %%(loincs)s
    2296                                  %s
    2297                  ) AS distinct_results
    2298                  ORDER BY
    2299                          clin_when DESC
    2300                  LIMIT %s""" % (
    2301                  max_age_cond,
    2302                  max_no_of_results
    2303          )
    2304          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2305          if len(rows) > 0:
    2306                  return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
    2307
    2308          if not consider_indirect_matches:
    2309                  return []
    2310
    2311          cmd = """
    2312          -- get the test results
    2313          SELECT * FROM clin.v_test_results c_vtr
    2314          WHERE
    2315                  -- for this <patient>
    2316                  pk_patient = %%(pat)s
    2317                          AND
    2318                  -- not having *any* LOINC (if the result did have a LOINC and had not been caught by the by-LOINC search it does not apply)
    2319                  unified_loinc IS NULL
    2320                          AND
    2321                  -- with these meta test types (= results for the explicit equivalance group)
    2322                  c_vtr.pk_meta_test_type IN (
    2323                          -- get the meta test types for those types
    2324                          SELECT pk_meta_test_type
    2325                          FROM clin.v_test_types
    2326                          WHERE
    2327                                  pk_meta_test_type IS NOT NULL
    2328                                          AND
    2329                                  (-- retrieve test types which have .LOINC in <loincs>
    2330                                          (loinc IN %%(loincs)s)
    2331                                                  OR
    2332                                          (loinc_meta IN  %%(loincs)s)
    2333                                  )
    2334                                          AND
    2335                                  -- but no result for <patient>
    2336                                  pk_test_type NOT IN (
    2337                                          select pk_test_type from clin.v_test_results WHERE pk_patient = %%(pat)s
    2338                                  ) %s
    2339                  )
    2340          -- return the N most resent result
    2341          ORDER BY clin_when DESC
    2342          LIMIT %s
    2343          """ % (
    2344                  max_age_cond,
    2345                  max_no_of_results
    2346          )
    2347          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2348          return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
    2349
    2350  #------------------------------------------------------------
    2351 -def get_most_recent_results_for_test_type(test_type=None, max_no_of_results=1, patient=None):
    2352          """Get N most recent results for *one* defined test type."""
    2353
    2354          assert (test_type is not None), '<test_type> must not be None'
    2355          assert (max_no_of_results > 0), '<max_no_of_results> must be > 0'
    2356
    2357          args = {'pat': patient, 'ttyp': test_type}
    2358          where_parts = ['pk_patient = %(pat)s']
    2359          where_parts.append('pk_test_type = %(ttyp)s')           # ?consider: pk_meta_test_type = %(pkmtt)s / self._payload[self._idx['pk_meta_test_type']]
    2360          cmd = """
    2361                  SELECT * FROM clin.v_test_results
    2362                  WHERE
    2363                          %s
    2364                  ORDER BY clin_when DESC
    2365                  LIMIT %s""" % (
    2366                          ' AND '.join(where_parts),
    2367                          max_no_of_results
    2368                  )
    2369          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2370          return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
    2371
    2372  #------------------------------------------------------------
    2373  _SQL_most_recent_result_for_test_types = """
    2374  -- return the one most recent result for each of a list of test types
    2375  -- without regard to whether they belong to a meta test type
    2376  SELECT * FROM (
    2377          SELECT
    2378                  *,
    2379                  MAX(clin_when) OVER relevant_tests AS max_clin_when
    2380          FROM
    2381                  clin.v_test_results
    2382          WHERE
    2383                  %s
    2384          WINDOW relevant_tests AS (PARTITION BY pk_patient, pk_test_type)
    2385  ) AS windowed_tests
    2386  WHERE
    2387          clin_when = max_clin_when
    2388  %s
    2389  """
    2390
    2391  _SQL_most_recent_result_for_test_types_without_meta_type = """
    2392  -- return the one most recent result for each of a list of test types
    2393  -- none of which may belong to a meta test type
    2394  SELECT * FROM (
    2395          SELECT
    2396                  *,
    2397                  MAX(clin_when) OVER relevant_tests AS max_clin_when
    2398          FROM
    2399                  clin.v_test_results
    2400          WHERE
    2401                  pk_meta_test_type IS NULL
    2402                          AND
    2403                  %s
    2404          WINDOW relevant_tests AS (PARTITION BY pk_patient, pk_test_type)
    2405  ) AS windowed_tests
    2406  WHERE
    2407          clin_when = max_clin_when
    2408  """
    2409
    2410  _SQL_most_recent_result_for_test_types_by_meta_type = """
    2411  -- return the one most recent result for each of a list of meta test types
    2412  -- derived from a list of test types
    2413  SELECT * FROM (
    2414          SELECT
    2415                  *,
    2416                  MAX(clin_when) OVER relevant_tests AS max_clin_when
    2417          FROM
    2418                  clin.v_test_results
    2419          WHERE
    2420                  pk_meta_test_type IS NOT NULL
    2421                          AND
    2422                  %s
    2423          WINDOW relevant_tests AS (PARTITION BY pk_patient, pk_meta_test_type)
    2424  ) AS windowed_tests
    2425  WHERE
    2426          clin_when = max_clin_when
    2427  """
    2428
    2429 -def get_most_recent_result_for_test_types(pk_test_types=None, pk_patient=None, return_pks=False, consider_meta_type=False, order_by=None):
    2430          """Return the one most recent result for *each* of a list of test types.
    2431
    2432                  If <pk_test_types> is not given, most recent results for *each*
    2433                  test type the patient has any results for is returned.
    2434          """
    2435          where_parts = ['pk_patient = %(pat)s']
    2436          args = {'pat': pk_patient}
    2437
    2438          if pk_test_types is not None:
    2439                  where_parts.append('pk_test_type IN %(ttyps)s')
    2440                  args['ttyps'] = tuple(pk_test_types)
    2441
    2442          order_by = 'ORDER BY clin_when DESC' if order_by is None else 'ORDER BY %s' % order_by
    2443
    2444          if consider_meta_type:
    2445                  cmd = 'SELECT * FROM ((%s) UNION ALL (%s)) AS result_union %s' % (
    2446                          _SQL_most_recent_result_for_test_types_without_meta_type % ' AND '.join(where_parts),
    2447                          _SQL_most_recent_result_for_test_types_by_meta_type % ' AND '.join(where_parts),
    2448                          order_by
    2449                  )
    2450          else:
    2451                  cmd = _SQL_most_recent_result_for_test_types % (
    2452                          ' AND '.join(where_parts),
    2453                          order_by
    2454                  )
    2455          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2456          if return_pks:
    2457                  return [ r['pk_test_result'] for r in rows ]
    2458
    2459          return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
    2460
    2461  #------------------------------------------------------------
    2462 -def get_most_recent_results_for_patient(no_of_results=1, patient=None):
    2463          """Get N most recent results for a given patient."""
    2464
    2465          if no_of_results < 1:
    2466                  raise ValueError('<no_of_results> must be > 0')
    2467
    2468          args = {'pat': patient}
    2469          cmd = """
    2470                  SELECT * FROM clin.v_test_results
    2471                  WHERE
    2472                          pk_patient = %%(pat)s
    2473                  ORDER BY clin_when DESC
    2474                  LIMIT %s""" % no_of_results
    2475          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2476          return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
    2477
    2478  #------------------------------------------------------------
    2479 -def get_oldest_result(test_type=None, loinc=None, patient=None):
    2480
    2481          if None not in [test_type, loinc]:
    2482                  raise ValueError('either <test_type> or <loinc> must be None')
    2483
    2484          args = {
    2485                  'pat': patient,
    2486                  'ttyp': test_type,
    2487                  'loinc': loinc
    2488          }
    2489
    2490          where_parts = ['pk_patient = %(pat)s']
    2491          if test_type is not None:
    2492                  where_parts.append('pk_test_type = %(ttyp)s')           # consider: pk_meta_test_type = %(pkmtt)s / self._payload[self._idx['pk_meta_test_type']]
    2493          elif loinc is not None:
    2494                  where_parts.append('((loinc_tt IN %(loinc)s) OR (loinc_meta IN %(loinc)s))')
    2495                  args['loinc'] = tuple(loinc)
    2496
    2497          cmd = """
    2498                  SELECT * FROM clin.v_test_results
    2499                  WHERE
    2500                          %s
    2501                  ORDER BY clin_when
    2502                  LIMIT 1""" % ' AND '.join(where_parts)
    2503          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    2504          if len(rows) == 0:
    2505                  return None
    2506
    2507          return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
    2508
    2509  #------------------------------------------------------------
    2510 -def delete_test_result(result=None):
    2511          try:
    2512                  pk = int(result)
    2513          except (TypeError, AttributeError):
    2514                  pk = result['pk_test_result']
    2515
    2516          cmd = 'DELETE FROM clin.test_result WHERE pk = %(pk)s'
    2517          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': pk}}])
    2518
    2519  #------------------------------------------------------------
    2520 -def create_test_result(encounter=None, episode=None, type=None, intended_reviewer=None, val_num=None, val_alpha=None, unit=None, link_obj=None):
    2521
    2522          cmd1 = """
    2523                  INSERT INTO clin.test_result (
    2524                          fk_encounter,
    2525                          fk_episode,
    2526                          fk_type,
    2527                          fk_intended_reviewer,
    2528                          val_num,
    2529                          val_alpha,
    2530                          val_unit
    2531                  ) VALUES (
    2532                          %(enc)s,
    2533                          %(epi)s,
    2534                          %(type)s,
    2535                          %(rev)s,
    2536                          %(v_num)s,
    2537                          %(v_alpha)s,
    2538                          %(unit)s
    2539                  )
    2540          """
    2541          cmd2 = "SELECT * from clin.v_test_results WHERE pk_test_result = currval(pg_get_serial_sequence('clin.test_result', 'pk'))"
    2542          args = {
    2543                  'enc': encounter,
    2544                  'epi': episode,
    2545                  'type': type,
    2546                  'rev': intended_reviewer,
    2547                  'v_num': val_num,
    2548                  'v_alpha': val_alpha,
    2549                  'unit': unit
    2550          }
    2551          rows, idx = gmPG2.run_rw_queries (
    2552                  link_obj = link_obj,
    2553                  queries = [
    2554                          {'cmd': cmd1, 'args': args},
    2555                          {'cmd': cmd2}
    2556                  ],
    2557                  return_data = True,
    2558                  get_col_idx = True
    2559          )
    2560          tr = cTestResult(row = {
    2561                  'pk_field': 'pk_test_result',
    2562                  'idx': idx,
    2563                  'data': rows[0]
    2564          })
    2565          return tr
    2566
    2567  #------------------------------------------------------------
    2568 -def format_test_results(results=None, output_format='latex'):
    2569
    2570          _log.debug('formatting test results into [%s]', output_format)
    2571
    2572          if output_format == 'latex':
    2573                  return __format_test_results_latex(results = results)
    2574
    2575          msg = _('unknown test results output format [%s]') % output_format
    2576          _log.error(msg)
    2577          return msg
    2578
    2579  #------------------------------------------------------------
    2580 -def __tests2latex_minipage(results=None, width='1.5cm', show_time=False, show_range=True):
    2581
    2582          if len(results) == 0:
    2583                  return '\\begin{minipage}{%s} \\end{minipage}' % width
    2584
    2585          lines = []
    2586          for t in results:
    2587
    2588                  tmp = ''
    2589
    2590                  if show_time:
    2591                          tmp += '{\\tiny (%s)} ' % t['clin_when'].strftime('%H:%M')
    2592
    2593                  tmp += '%.8s' % t['unified_val']
    2594
    2595                  lines.append(tmp)
    2596                  tmp = ''
    2597
    2598                  if show_range:
    2599                          has_range = (
    2600                                  t['unified_target_range'] is not None
    2601                                          or
    2602                                  t['unified_target_min'] is not None
    2603                                          or
    2604                                  t['unified_target_max'] is not None
    2605                          )
    2606                          if has_range:
    2607                                  if t['unified_target_range'] is not None:
    2608                                          tmp += '{\\tiny %s}' % t['unified_target_range']
    2609                                  else:
    2610                                          tmp += '{\\tiny %s}' % (
    2611                                                  gmTools.coalesce(t['unified_target_min'], '- ', '%s - '),
    2612                                                  gmTools.coalesce(t['unified_target_max'], '', '%s')
    2613                                          )
    2614                                  lines.append(tmp)
    2615
    2616          return '\\begin{minipage}{%s} \\begin{flushright} %s \\end{flushright} \\end{minipage}' % (width, ' \\\\ '.join(lines))
    2617
    2618  #------------------------------------------------------------
    2619 -def __tests2latex_cell(results=None, show_time=False, show_range=True):
    2620
    2621          if len(results) == 0:
    2622                  return ''
    2623
    2624          lines = []
    2625          for t in results:
    2626
    2627                  tmp = ''
    2628
    2629                  if show_time:
    2630                          tmp += '\\tiny %s ' % t['clin_when'].strftime('%H:%M')
    2631
    2632                  tmp += '\\normalsize %.8s' % t['unified_val']
    2633
    2634                  lines.append(tmp)
    2635                  tmp = '\\tiny %s' % gmTools.coalesce(t['val_unit'], '', '%s ')
    2636
    2637                  if not show_range:
    2638                          lines.append(tmp)
    2639                          continue
    2640
    2641                  has_range = (
    2642                          t['unified_target_range'] is not None
    2643                                  or
    2644                          t['unified_target_min'] is not None
    2645                                  or
    2646                          t['unified_target_max'] is not None
    2647                  )
    2648
    2649                  if not has_range:
    2650                          lines.append(tmp)
    2651                          continue
    2652
    2653                  if t['unified_target_range'] is not None:
    2654                          tmp += '[%s]' % t['unified_target_range']
    2655                  else:
    2656                          tmp += '[%s%s]' % (
    2657                                  gmTools.coalesce(t['unified_target_min'], '--', '%s--'),
    2658                                  gmTools.coalesce(t['unified_target_max'], '', '%s')
    2659                          )
    2660                  lines.append(tmp)
    2661
    2662          return ' \\\\ '.join(lines)
    2663
    2664  #------------------------------------------------------------
    2665 -def __format_test_results_latex(results=None):
    2666
    2667          if len(results) == 0:
    2668                  return '\\noindent %s' % _('No test results to format.')
    2669
    2670          # discover the columns and rows
    2671          dates = {}
    2672          tests = {}
    2673          grid = {}
    2674          for result in results:
    2675  #               row_label = u'%s \\ \\tiny (%s)}' % (result['unified_abbrev'], result['unified_name'])
    2676                  row_label = result['unified_abbrev']
    2677                  tests[row_label] = None
    2678                  col_label = '{\\scriptsize %s}' % result['clin_when'].strftime('%Y-%m-%d')
    2679                  dates[col_label] = None
    2680                  try:
    2681                          grid[row_label]
    2682                  except KeyError:
    2683                          grid[row_label] = {}
    2684                  try:
    2685                          grid[row_label][col_label].append(result)
    2686                  except KeyError:
    2687                          grid[row_label][col_label] = [result]
    2688
    2689          col_labels = sorted(dates, reverse = True)
    2690          del dates
    2691          row_labels = sorted(tests)
    2692          del tests
    2693
    2694          col_def = len(col_labels) * '>{\\raggedleft}p{1.7cm}|'
    2695
    2696          # format them
    2697          tex = """\\noindent %s
    2698
    2699  \\noindent \\begin{tabular}{|l|%s}
    2700  \\hline
    2701   & %s \\tabularnewline
    2702  \\hline
    2703
    2704  %%s \\tabularnewline
    2705
    2706  \\hline
    2707
    2708  \\end{tabular}""" % (
    2709                  _('Test results'),
    2710                  col_def,
    2711                  ' & '.join(col_labels)
    2712          )
    2713
    2714          rows = []
    2715
    2716          # loop over rows
    2717          for rl in row_labels:
    2718                  cells = [rl]
    2719                  # loop over cols per row
    2720                  for cl in col_labels:
    2721                          try:
    2722                                  # get tests for this (row/col) position
    2723                                  tests = grid[rl][cl]
    2724                          except KeyError:
    2725                                  # none there, so insert empty cell
    2726                                  cells.append(' ')
    2727                                  continue
    2728
    2729                          cells.append (
    2730                                  __tests2latex_cell (
    2731                                          results = tests,
    2732                                          show_time = (len(tests) > 1),
    2733                                          show_range = True
    2734                                  )
    2735                          )
    2736
    2737                  rows.append(' & '.join(cells))
    2738
    2739          return tex % ' \\tabularnewline\n \\hline\n'.join(rows)
    2740
    2741  #============================================================
    2742 -def export_results_for_gnuplot(results=None, filename=None, show_year=True, patient=None):
    2743
    2744          sandbox_dir = os.path.join(gmTools.gmPaths().tmp_dir, 'gplot')
    2745          if not gmTools.mkdir(sandbox_dir):
    2746                  sandbox_dir = gmTools.mk_sandbox_dir(prefix = 'gm2gpl-')
    2747          _log.debug('sandbox directory: [%s]', sandbox_dir)
    2748          if filename is None:
    2749                  filename = gmTools.get_unique_filename(prefix = 'gm2gpl-', suffix = '.dat', tmp_dir = sandbox_dir)
    2750
    2751          # sort results into series by test type
    2752          series = {}
    2753          for r in results:
    2754                  try:
    2755                          series[r['unified_name']].append(r)
    2756                  except KeyError:
    2757                          series[r['unified_name']] = [r]
    2758
    2759          conf_name = '%s.conf' % filename
    2760          gplot_conf = io.open(conf_name, mode = 'wt', encoding = 'utf8')
    2761          gplot_conf.write('# settings for stacked multiplot layouts:\n')
    2762          sub_title = _('plotted %s (GNUmed v%s)') % (
    2763                  gmDateTime.pydt_now_here().strftime('%Y %b %d %H:%M'),
    2764                  _cfg.get(option = 'client_version')
    2765          )
    2766          if patient is None:
    2767                  plot_title = sub_title
    2768          else:
    2769                  plot_title = '%s - %s\\n%s' % (
    2770                          patient.get_description_gender(with_nickname = False).strip(),
    2771                          patient.get_formatted_dob(format = '%Y %b %d', none_string = _('unknown DOB'), honor_estimation = True),
    2772                          sub_title
    2773                  )
    2774          gplot_conf.write('multiplot_title = "%s"\n' % plot_title)
    2775          gplot_conf.write('multiplot_no_of_tests = %s    # number of index blocks (resp. test types)\n' % len(series))
    2776          gplot_conf.write('array multiplot_y_labels[multiplot_no_of_tests]       # list for ylabels suitable for stacked multiplots\n')
    2777          gplot_conf.write('\n')
    2778          gplot_conf.write('# settings for individual plots, stacked or not:\n')
    2779
    2780          gplot_data = io.open(filename, mode = 'wt', encoding = 'utf8')
    2781          gplot_data.write('# -------------------------------------------------------------\n')
    2782          gplot_data.write('# GNUmed test results export for Gnuplot plotting\n')
    2783          gplot_data.write('# -------------------------------------------------------------\n')
    2784          gplot_data.write('# first line of each index: test type abbreviation & name,\n')
    2785          gplot_data.write('# can be used as title for plots: set key ... autotitle columnheader\n')
    2786          gplot_data.write('#\n')
    2787          gplot_data.write('# Columns in each index:\n')
    2788          gplot_data.write('# 1 - clin_when at full precision\n')
    2789          gplot_data.write('#      set timefmt "%Y-%m-%d_%H:%M"\n')
    2790          gplot_data.write('#      timecolumn(1, "%Y-%m-%d_%H:%M")\n')
    2791          gplot_data.write('# 2 - value\n')
    2792          gplot_data.write('# 3 - unit\n')
    2793          gplot_data.write('# 4 - unified (target or normal) range: lower bound\n')
    2794          gplot_data.write('# 5 - unified (target or normal) range: upper bound\n')
    2795          gplot_data.write('# 6 - normal range: lower bound\n')
    2796          gplot_data.write('# 7 - normal range: upper bound\n')
    2797          gplot_data.write('# 8 - target range: lower bound\n')
    2798          gplot_data.write('# 9 - target range: upper bound\n')
    2799          gplot_data.write('# 10 - clin_when formatted into string (say, as x-axis tic label)\n')
    2800          gplot_data.write('#\n')
    2801          gplot_data.write('# index rows are NOT sorted by clin_when, so plotting\n')
    2802          gplot_data.write('# with lined styles will make the lines go all over\n')
    2803          gplot_data.write('# -------------------------------------------------------------\n')
    2804          gplot_data.write('#\n')
    2805          gplot_data.write('# the file <%s.conf>\n' % filename)
    2806          gplot_data.write('# will contain various gnuplot settings specific to this plot,\n')
    2807          gplot_data.write('# such as <ylabel>, <y2label>, <title>,\n')
    2808          gplot_data.write('# there will also be settings suitable for stacked multiplots\n')
    2809          gplot_data.write('# -------------------------------------------------------------\n')
    2810
    2811          series_keys = list(series)
    2812          for test_type_idx in range(len(series_keys)):
    2813                  test_type = series_keys[test_type_idx]
    2814                  if len(series[test_type]) == 0:
    2815                          continue
    2816                  result = series[test_type][0]
    2817                  if test_type_idx == 0:
    2818                          gplot_conf.write('set title "%s" enhanced\n' % plot_title)
    2819                          gplot_conf.write('\n')
    2820                          gplot_conf.write('set ylabel "%s"\n' % result['unified_name'])
    2821                  elif test_type_idx == 1:
    2822                          gplot_conf.write('set y2label "%s"\n' % result['unified_name'])
    2823                  gplot_conf.write('multiplot_y_labels[%s] = "%s (%s)"\n' % (test_type_idx + 1, result['unified_name'], result['unified_abbrev']))
    2824                  title = '%s (%s)' % (
    2825                          result['unified_abbrev'],
    2826                          result['unified_name']
    2827                  )
    2828                  gplot_data.write('\n\n"%s" "%s"\n' % (title, title))
    2829                  prev_date = None
    2830                  prev_year = None
    2831                  for result in series[test_type]:
    2832                          curr_date = gmDateTime.pydt_strftime(result['clin_when'], '%Y-%m-%d', 'utf8', gmDateTime.acc_days)
    2833                          if curr_date == prev_date:
    2834                                  gplot_data.write('\n# %s\n' % _('blank line inserted to allow for discontinued line drawing of same-day values'))
    2835                          if show_year:
    2836                                  if result['clin_when'].year == prev_year:
    2837                                          when_template = '%b %d %H:%M'
    2838                                  else:
    2839                                          when_template = '%b %d %H:%M (%Y)'
    2840                                  prev_year = result['clin_when'].year
    2841                          else:
    2842                                  when_template = '%b %d'
    2843                          val = result['val_num']
    2844                          if val is None:
    2845                                  val = result.estimate_numeric_value_from_alpha
    2846                          if val is None:
    2847                                  continue                # skip distinctly non-numericable values
    2848                          gplot_data.write ('%s %s "%s" %s %s %s %s %s %s "%s"\n' % (
    2849                                  #result['clin_when'].strftime('%Y-%m-%d_%H:%M'),
    2850                                  gmDateTime.pydt_strftime(result['clin_when'], '%Y-%m-%d_%H:%M', 'utf8', gmDateTime.acc_minutes),
    2851                                  val,
    2852                                  gmTools.coalesce(result['val_unit'], '"<?>"'),
    2853                                  gmTools.coalesce(result['unified_target_min'], '"<?>"'),
    2854                                  gmTools.coalesce(result['unified_target_max'], '"<?>"'),
    2855                                  gmTools.coalesce(result['val_normal_min'], '"<?>"'),
    2856                                  gmTools.coalesce(result['val_normal_max'], '"<?>"'),
    2857                                  gmTools.coalesce(result['val_target_min'], '"<?>"'),
    2858                                  gmTools.coalesce(result['val_target_max'], '"<?>"'),
    2859                                  gmDateTime.pydt_strftime (
    2860                                          result['clin_when'],
    2861                                          format = when_template,
    2862                                          accuracy = gmDateTime.acc_minutes
    2863                                  )
    2864                          ))
    2865                          prev_date = curr_date
    2866          gplot_data.close()
    2867          gplot_conf.close()
    2868
    2869          return filename
    2870
    2871  #============================================================
    2872 -class cLabResult(gmBusinessDBObject.cBusinessDBObject):
    2873          """Represents one lab result."""
    2874
    2875          _cmd_fetch_payload = """
    2876                  select *, xmin_test_result from v_results4lab_req
    2877                  where pk_result=%s"""
    2878          _cmds_lock_rows_for_update = [
    2879                  """select 1 from test_result where pk=%(pk_result)s and xmin=%(xmin_test_result)s for update"""
    2880          ]
    2881          _cmds_store_payload = [
    2882                  """update test_result set
    2883                                  clin_when = %(val_when)s,
    2884                                  narrative = %(progress_note_result)s,
    2885                                  fk_type = %(pk_test_type)s,
    2886                                  val_num = %(val_num)s::numeric,
    2887                                  val_alpha = %(val_alpha)s,
    2888                                  val_unit = %(val_unit)s,
    2889                                  val_normal_min = %(val_normal_min)s,
    2890                                  val_normal_max = %(val_normal_max)s,
    2891                                  val_normal_range = %(val_normal_range)s,
    2892                                  val_target_min = %(val_target_min)s,
    2893                                  val_target_max = %(val_target_max)s,
    2894                                  val_target_range = %(val_target_range)s,
    2895                                  abnormality_indicator = %(abnormal)s,
    2896                                  norm_ref_group = %(ref_group)s,
    2897                                  note_provider = %(note_provider)s,
    2898                                  material = %(material)s,
    2899                                  material_detail = %(material_detail)s
    2900                          where pk = %(pk_result)s""",
    2901                  """select xmin_test_result from v_results4lab_req where pk_result=%(pk_result)s"""
    2902                  ]
    2903
    2904          _updatable_fields = [
    2905                  'val_when',
    2906                  'progress_note_result',
    2907                  'val_num',
    2908                  'val_alpha',
    2909                  'val_unit',
    2910                  'val_normal_min',
    2911                  'val_normal_max',
    2912                  'val_normal_range',
    2913                  'val_target_min',
    2914                  'val_target_max',
    2915                  'val_target_range',
    2916                  'abnormal',
    2917                  'ref_group',
    2918                  'note_provider',
    2919                  'material',
    2920                  'material_detail'
    2921          ]
    2922          #--------------------------------------------------------
    2923 -        def __init__(self, aPK_obj=None, row=None):
    2924                  """Instantiate.
    2925
    2926                  aPK_obj as dict:
    2927                          - patient_id
    2928                          - when_field (see view definition)
    2929                          - when
    2930                          - test_type
    2931                          - val_num
    2932                          - val_alpha
    2933                          - unit
    2934                  """
    2935                  # instantiate from row data ?
    2936                  if aPK_obj is None:
    2937                          gmBusinessDBObject.cBusinessDBObject.__init__(self, row=row)
    2938                          return
    2939                  pk = aPK_obj
    2940                  # find PK from row data ?
    2941                  if type(aPK_obj) == dict:
    2942                          # sanity checks
    2943                          if None in [aPK_obj['patient_id'], aPK_obj['when'], aPK_obj['when_field'], aPK_obj['test_type'], aPK_obj['unit']]:
    2944                                  raise gmExceptions.ConstructorError('parameter error: %s' % aPK_obj)
    2945                          if (aPK_obj['val_num'] is None) and (aPK_obj['val_alpha'] is None):
    2946                                  raise gmExceptions.ConstructorError('parameter error: val_num and val_alpha cannot both be None')
    2947                          # get PK
    2948                          where_snippets = [
    2949                                  'pk_patient=%(patient_id)s',
    2950                                  'pk_test_type=%(test_type)s',
    2951                                  '%s=%%(when)s' % aPK_obj['when_field'],
    2952                                  'val_unit=%(unit)s'
    2953                          ]
    2954                          if aPK_obj['val_num'] is not None:
    2955                                  where_snippets.append('val_num=%(val_num)s::numeric')
    2956                          if aPK_obj['val_alpha'] is not None:
    2957                                  where_snippets.append('val_alpha=%(val_alpha)s')
    2958
    2959                          where_clause = ' and '.join(where_snippets)
    2960                          cmd = "select pk_result from v_results4lab_req where %s" % where_clause
    2961                          data = gmPG.run_ro_query('historica', cmd, None, aPK_obj)
    2962                          if data is None:
    2963                                  raise gmExceptions.ConstructorError('error getting lab result for: %s' % aPK_obj)
    2964                          if len(data) == 0:
    2965                                  raise gmExceptions.NoSuchClinItemError('no lab result for: %s' % aPK_obj)
    2966                          pk = data[0][0]
    2967                  # instantiate class
    2968                  gmBusinessDBObject.cBusinessDBObject.__init__(self, aPK_obj=pk)
    2969          #--------------------------------------------------------
    2970 -        def get_patient(self):
    2971                  cmd = """
    2972                          select
    2973                                  %s,
    2974                                  vbp.title,
    2975                                  vbp.firstnames,
    2976                                  vbp.lastnames,
    2977                                  vbp.dob
    2978                          from v_active_persons vbp
    2979                          where vbp.pk_identity = %%s""" % self._payload[self._idx['pk_patient']]
    2980                  pat = gmPG.run_ro_query('historica', cmd, None, self._payload[self._idx['pk_patient']])
    2981                  return pat[0]
    2982
    2983  #============================================================
    2984 -class cLabRequest(gmBusinessDBObject.cBusinessDBObject):
    2985          """Represents one lab request."""
    2986
    2987          _cmd_fetch_payload = """
    2988                  select *, xmin_lab_request from v_lab_requests
    2989                  where pk_request=%s"""
    2990          _cmds_lock_rows_for_update = [
    2991                  """select 1 from lab_request where pk=%(pk_request)s and xmin=%(xmin_lab_request)s for update"""
    2992          ]
    2993          _cmds_store_payload = [
    2994                  """update lab_request set
    2995                                  request_id=%(request_id)s,
    2996                                  lab_request_id=%(lab_request_id)s,
    2997                                  clin_when=%(sampled_when)s,
    2998                                  lab_rxd_when=%(lab_rxd_when)s,
    2999                                  results_reported_when=%(results_reported_when)s,
    3000                                  request_status=%(request_status)s,
    3001                                  is_pending=%(is_pending)s::bool,
    3002                                  narrative=%(progress_note)s
    3003                          where pk=%(pk_request)s""",
    3004                  """select xmin_lab_request from v_lab_requests where pk_request=%(pk_request)s"""
    3005          ]
    3006          _updatable_fields = [
    3007                  'request_id',
    3008                  'lab_request_id',
    3009                  'sampled_when',
    3010                  'lab_rxd_when',
    3011                  'results_reported_when',
    3012                  'request_status',
    3013                  'is_pending',
    3014                  'progress_note'
    3015          ]
    3016          #--------------------------------------------------------
    3017 -        def __init__(self, aPK_obj=None, row=None):
    3018                  """Instantiate lab request.
    3019
    3020                  The aPK_obj can be either a dict with the keys "req_id"
    3021                  and "lab" or a simple primary key.
    3022                  """
    3023                  # instantiate from row data ?
    3024                  if aPK_obj is None:
    3025                          gmBusinessDBObject.cBusinessDBObject.__init__(self, row=row)
    3026                          return
    3027                  pk = aPK_obj
    3028                  # instantiate from "req_id" and "lab" ?
    3029                  if type(aPK_obj) == dict:
    3030                          # sanity check
    3031                          try:
    3032                                  aPK_obj['req_id']
    3033                                  aPK_obj['lab']
    3034                          except Exception:
    3035                                  _log.exception('[%s:??]: faulty <aPK_obj> structure: [%s]' % (self.__class__.__name__, aPK_obj), sys.exc_info())
    3036                                  raise gmExceptions.ConstructorError('[%s:??]: cannot derive PK from [%s]' % (self.__class__.__name__, aPK_obj))
    3037                          # generate query
    3038                          where_snippets = []
    3039                          vals = {}
    3040                          where_snippets.append('request_id=%(req_id)s')
    3041                          if type(aPK_obj['lab']) == int:
    3042                                  where_snippets.append('pk_test_org=%(lab)s')
    3043                          else:
    3044                                  where_snippets.append('lab_name=%(lab)s')
    3045                          where_clause = ' and '.join(where_snippets)
    3046                          cmd = "select pk_request from v_lab_requests where %s" % where_clause
    3047                          # get pk
    3048                          data = gmPG.run_ro_query('historica', cmd, None, aPK_obj)
    3049                          if data is None:
    3050                                  raise gmExceptions.ConstructorError('[%s:??]: error getting lab request for [%s]' % (self.__class__.__name__, aPK_obj))
    3051                          if len(data) == 0:
    3052                                  raise gmExceptions.NoSuchClinItemError('[%s:??]: no lab request for [%s]' % (self.__class__.__name__, aPK_obj))
    3053                          pk = data[0][0]
    3054                  # instantiate class
    3055                  gmBusinessDBObject.cBusinessDBObject.__init__(self, aPK_obj=pk)
    3056          #--------------------------------------------------------
    3057 -        def get_patient(self):
    3058                  cmd = """
    3059                          select vpi.pk_patient, vbp.title, vbp.firstnames, vbp.lastnames, vbp.dob
    3060                          from v_pat_items vpi, v_active_persons vbp
    3061                          where
    3062                                  vpi.pk_item=%s
    3063                                          and
    3064                                  vbp.pk_identity=vpi.pk_patient"""
    3065                  pat = gmPG.run_ro_query('historica', cmd, None, self._payload[self._idx['pk_item']])
    3066                  if pat is None:
    3067                          _log.error('cannot get patient for lab request [%s]' % self._payload[self._idx['pk_item']])
    3068                          return None
    3069                  if len(pat) == 0:
    3070                          _log.error('no patient associated with lab request [%s]' % self._payload[self._idx['pk_item']])
    3071                          return None
    3072                  return pat[0]
    3073
    3074  #============================================================
    3075  # convenience functions
    3076  #------------------------------------------------------------
    3077 -def create_lab_request(lab=None, req_id=None, pat_id=None, encounter_id=None, episode_id=None):
    3078          """Create or get lab request.
    3079
    3080                  returns tuple (status, value):
    3081                          (True, lab request instance)
    3082                          (False, error message)
    3083                          (None, housekeeping_todo primary key)
    3084          """
    3085          req = None
    3086          aPK_obj = {
    3087                  'lab': lab,
    3088                  'req_id': req_id
    3089          }
    3090          try:
    3091                  req = cLabRequest (aPK_obj)
    3092          except gmExceptions.NoSuchClinItemError as msg:
    3093                  _log.info('%s: will try to create lab request' % str(msg))
    3094          except gmExceptions.ConstructorError as msg:
    3095                  _log.exception(str(msg), sys.exc_info(), verbose=0)
    3096                  return (False, msg)
    3097          # found
    3098          if req is not None:
    3099                  db_pat = req.get_patient()
    3100                  if db_pat is None:
    3101                          _log.error('cannot cross-check patient on lab request')
    3102                          return (None, '')
    3103                  # yes but ambigous
    3104                  if pat_id != db_pat[0]:
    3105                          _log.error('lab request found for [%s:%s] but patient mismatch: expected [%s], in DB [%s]' % (lab, req_id, pat_id, db_pat))
    3106                          me = '$RCSfile: gmPathLab.py,v $ $Revision: 1.81 $'
    3107                          to = 'user'
    3108                          prob = _('The lab request already exists but belongs to a different patient.')
    3109                          sol = _('Verify which patient this lab request really belongs to.')
    3110                          ctxt = _('lab [%s], request ID [%s], expected link with patient [%s], currently linked to patient [%s]') % (lab, req_id, pat_id, db_pat)
    3111                          cat = 'lab'
    3112                          status, data = gmPG.add_housekeeping_todo(me, to, prob, sol, ctxt, cat)
    3113                          return (None, data)
    3114                  return (True, req)
    3115          # not found
    3116          queries = []
    3117          if type(lab) is int:
    3118                  cmd = "insert into lab_request (fk_encounter, fk_episode, fk_test_org, request_id) values (%s, %s, %s, %s)"
    3119          else:
    3120                  cmd = "insert into lab_request (fk_encounter, fk_episode, fk_test_org, request_id) values (%s, %s, (select pk from test_org where internal_OBSOLETE_name=%s), %s)"
    3121          queries.append((cmd, [encounter_id, episode_id, str(lab), req_id]))
    3122          cmd = "select currval('lab_request_pk_seq')"
    3123          queries.append((cmd, []))
    3124          # insert new
    3125          result, err = gmPG.run_commit('historica', queries, True)
    3126          if result is None:
    3127                  return (False, err)
    3128          try:
    3129                  req = cLabRequest(aPK_obj=result[0][0])
    3130          except gmExceptions.ConstructorError as msg:
    3131                  _log.exception(str(msg), sys.exc_info(), verbose=0)
    3132                  return (False, msg)
    3133          return (True, req)
    3134  #------------------------------------------------------------
    3135 -def create_lab_result(patient_id=None, when_field=None, when=None, test_type=None, val_num=None, val_alpha=None, unit=None, encounter_id=None, request=None):
    3136          tres = None
    3137          data = {
    3138                  'patient_id': patient_id,
    3139                  'when_field': when_field,
    3140                  'when': when,
    3141                  'test_type': test_type,
    3142                  'val_num': val_num,
    3143                  'val_alpha': val_alpha,
    3144                  'unit': unit
    3145          }
    3146          try:
    3147                  tres = cLabResult(aPK_obj=data)
    3148                  # exists already, so fail
    3149                  _log.error('will not overwrite existing test result')
    3150                  _log.debug(str(tres))
    3151                  return (None, tres)
    3152          except gmExceptions.NoSuchClinItemError:
    3153                  _log.debug('test result not found - as expected, will create it')
    3154          except gmExceptions.ConstructorError as msg:
    3155                  _log.exception(str(msg), sys.exc_info(), verbose=0)
    3156                  return (False, msg)
    3157          if request is None:
    3158                  return (False, _('need lab request when inserting lab result'))
    3159          # not found
    3160          if encounter_id is None:
    3161                  encounter_id = request['pk_encounter']
    3162          queries = []
    3163          cmd = "insert into test_result (fk_encounter, fk_episode, fk_type, val_num, val_alpha, val_unit) values (%s, %s, %s, %s, %s, %s)"
    3164          queries.append((cmd, [encounter_id, request['pk_episode'], test_type, val_num, val_alpha, unit]))
    3165          cmd = "insert into lnk_result2lab_req (fk_result, fk_request) values ((select currval('test_result_pk_seq')), %s)"
    3166          queries.append((cmd, [request['pk_request']]))
    3167          cmd = "select currval('test_result_pk_seq')"
    3168          queries.append((cmd, []))
    3169          # insert new
    3170          result, err = gmPG.run_commit('historica', queries, True)
    3171          if result is None:
    3172                  return (False, err)
    3173          try:
    3174                  tres = cLabResult(aPK_obj=result[0][0])
    3175          except gmExceptions.ConstructorError as msg:
    3176                  _log.exception(str(msg), sys.exc_info(), verbose=0)
    3177                  return (False, msg)
    3178          return (True, tres)
    3179  #------------------------------------------------------------
    3180 -def get_unreviewed_results(limit=50):
    3181          # sanity check
    3182          if limit < 1:
    3183                  limit = 1
    3184          # retrieve one more row than needed so we know there's more available ;-)
    3185          lim = limit + 1
    3186          cmd = """
    3187                  select pk_result
    3188                  from v_results4lab_req
    3189                  where reviewed is false
    3190                  order by pk_patient
    3191                  limit %s""" % lim
    3192          rows = gmPG.run_ro_query('historica', cmd)
    3193          if rows is None:
    3194                  _log.error('error retrieving unreviewed lab results')
    3195                  return (None, _('error retrieving unreviewed lab results'))
    3196          if len(rows) == 0:
    3197                  return (False, [])
    3198          # more than LIMIT rows ?
    3199          if len(rows) == lim:
    3200                  more_avail = True
    3201                  # but deliver only LIMIT rows so that our assumption holds true...
    3202                  del rows[limit]
    3203          else:
    3204                  more_avail = False
    3205          results = []
    3206          for row in rows:
    3207                  try:
    3208                          results.append(cLabResult(aPK_obj=row[0]))
    3209                  except gmExceptions.ConstructorError:
    3210                          _log.exception('skipping unreviewed lab result [%s]' % row[0], sys.exc_info(), verbose=0)
    3211          return (more_avail, results)
    3212
    3213  #------------------------------------------------------------
    3214 -def get_pending_requests(limit=250):
    3215          lim = limit + 1
    3216          cmd = "select pk from lab_request where is_pending is true limit %s" % lim
    3217          rows = gmPG.run_ro_query('historica', cmd)
    3218          if rows is None:
    3219                  _log.error('error retrieving pending lab requests')
    3220                  return (None, None)
    3221          if len(rows) == 0:
    3222                  return (False, [])
    3223          results = []
    3224          # more than LIMIT rows ?
    3225          if len(rows) == lim:
    3226                  too_many = True
    3227                  # but deliver only LIMIT rows so that our assumption holds true...
    3228                  del rows[limit]
    3229          else:
    3230                  too_many = False
    3231          requests = []
    3232          for row in rows:
    3233                  try:
    3234                          requests.append(cLabRequest(aPK_obj=row[0]))
    3235                  except gmExceptions.ConstructorError:
    3236                          _log.exception('skipping pending lab request [%s]' % row[0], sys.exc_info(), verbose=0)
    3237          return (too_many, requests)
    3238
    3239  #------------------------------------------------------------
    3240 -def get_next_request_ID(lab=None, incrementor_func=None):
    3241          """Get logically next request ID for given lab.
    3242
    3243          - incrementor_func:
    3244            - if not supplied the next ID is guessed
    3245            - if supplied it is applied to the most recently used ID
    3246          """
    3247          if type(lab) == int:
    3248                  lab_snippet = 'vlr.fk_test_org=%s'
    3249          else:
    3250                  lab_snippet = 'vlr.lab_name=%s'
    3251                  lab = str(lab)
    3252          cmd =  """
    3253                  select request_id
    3254                  from lab_request lr0
    3255                  where lr0.clin_when = (
    3256                          select max(vlr.sampled_when)
    3257                          from v_lab_requests vlr
    3258                          where %s
    3259                  )""" % lab_snippet
    3260          rows = gmPG.run_ro_query('historica', cmd, None, lab)
    3261          if rows is None:
    3262                  _log.warning('error getting most recently used request ID for lab [%s]' % lab)
    3263                  return ''
    3264          if len(rows) == 0:
    3265                  return ''
    3266          most_recent = rows[0][0]
    3267          # apply supplied incrementor
    3268          if incrementor_func is not None:
    3269                  try:
    3270                          next = incrementor_func(most_recent)
    3271                  except TypeError:
    3272                          _log.error('cannot call incrementor function [%s]' % str(incrementor_func))
    3273                          return most_recent
    3274                  return next
    3275          # try to be smart ourselves
    3276          for pos in range(len(most_recent)):
    3277                  header = most_recent[:pos]
    3278                  trailer = most_recent[pos:]
    3279                  try:
    3280                          return '%s%s' % (header, str(int(trailer) + 1))
    3281                  except ValueError:
    3282                          header = most_recent[:-1]
    3283                          trailer = most_recent[-1:]
    3284                          return '%s%s' % (header, chr(ord(trailer) + 1))
    3285
    3286  #============================================================
    3287 -def calculate_bmi(mass=None, height=None, age=None):
    3288          """Calculate BMI.
    3289
    3290          mass: kg
    3291          height: cm
    3292          age: not yet used
    3293
    3294          returns:
    3295                  (True/False, data)
    3296                  True: data = (bmi, lower_normal, upper_normal)
    3297                  False: data = error message
    3298          """
    3299          converted, mass = gmTools.input2decimal(mass)
    3300          if not converted:
    3301                  return False, 'mass: cannot convert <%s> to Decimal' % mass
    3302
    3303          converted, height = gmTools.input2decimal(height)
    3304          if not converted:
    3305                  return False, 'height: cannot convert <%s> to Decimal' % height
    3306
    3307          approx_surface = (height / decimal.Decimal(100))**2
    3308          bmi = mass / approx_surface
    3309
    3310          print(mass, height, '->', approx_surface, '->', bmi)
    3311
    3312          lower_normal_mass = 20.0 * approx_surface
    3313          upper_normal_mass = 25.0 * approx_surface
    3314
    3315          return True, (bmi, lower_normal_mass, upper_normal_mass)
    3316
    3317  #============================================================
    3318  # main - unit testing
    3319  #------------------------------------------------------------
    3320  if __name__ == '__main__':
    3321
    3322          if len(sys.argv) < 2:
    3323                  sys.exit()
    3324
    3325          if sys.argv[1] != 'test':
    3326                  sys.exit()
    3327
    3328          import time
    3329
    3330          gmI18N.activate_locale()
    3331          gmI18N.install_domain()
    3332
    3333          #------------------------------------------
    3334 -        def test_create_test_result():
    3335                  tr = create_test_result (
    3336                          encounter = 1,
    3337                          episode = 1,
    3338                          type = 1,
    3339                          intended_reviewer = 1,
    3340                          val_num = '12',
    3341                          val_alpha=None,
    3342                          unit = 'mg/dl'
    3343                  )
    3344                  print(tr)
    3345                  return tr
    3346          #------------------------------------------
    3347 -        def test_delete_test_result():
    3348                  tr = test_create_test_result()
    3349                  delete_test_result(tr)
    3350          #------------------------------------------
    3351 -        def test_result():
    3352                  r = cTestResult(aPK_obj=6)
    3353                  #print r
    3354                  #print r.reference_ranges
    3355                  #print r.formatted_range
    3356                  #print r.temporally_closest_normal_range
    3357                  print(r.estimate_numeric_value_from_alpha)
    3358          #------------------------------------------
    3359 -        def test_lab_result():
    3360                  print("test_result()")
    3361  #               lab_result = cLabResult(aPK_obj=4)
    3362                  data = {
    3363                          'patient_id': 12,
    3364                          'when_field': 'val_when',
    3365                          'when': '2000-09-17 18:23:00+02',
    3366                          'test_type': 9,
    3367                          'val_num': 17.3,
    3368                          'val_alpha': None,
    3369                          'unit': 'mg/l'
    3370                  }
    3371                  lab_result = cLabResult(aPK_obj=data)
    3372                  print(lab_result)
    3373                  fields = lab_result.get_fields()
    3374                  for field in fields:
    3375                          print(field, ':', lab_result[field])
    3376                  print("updatable:", lab_result.get_updatable_fields())
    3377                  print(time.time())
    3378                  print(lab_result.get_patient())
    3379                  print(time.time())
    3380          #------------------------------------------
    3381 -        def test_request():
    3382                  print("test_request()")
    3383                  try:
    3384  #                       lab_req = cLabRequest(aPK_obj=1)
    3385  #                       lab_req = cLabRequest(req_id='EML#SC937-0176-CEC#11', lab=2)
    3386                          data = {
    3387                                  'req_id': 'EML#SC937-0176-CEC#11',
    3388                                  'lab': 'Enterprise Main Lab'
    3389                          }
    3390                          lab_req = cLabRequest(aPK_obj=data)
    3391                  except gmExceptions.ConstructorError as msg:
    3392                          print("no such lab request:", msg)
    3393                          return
    3394                  print(lab_req)
    3395                  fields = lab_req.get_fields()
    3396                  for field in fields:
    3397                          print(field, ':', lab_req[field])
    3398                  print("updatable:", lab_req.get_updatable_fields())
    3399                  print(time.time())
    3400                  print(lab_req.get_patient())
    3401                  print(time.time())
    3402          #--------------------------------------------------------
    3403 -        def test_unreviewed():
    3404                  data = get_unreviewed_results()
    3405                  for result in data:
    3406                          print(result)
    3407          #--------------------------------------------------------
    3408 -        def test_pending():
    3409                  data = get_pending_requests()
    3410                  for result in data:
    3411                          print(result)
    3412          #--------------------------------------------------------
    3413 -        def test_create_measurement_type():
    3414                  print(create_measurement_type (
    3415                          lab = None,
    3416                          abbrev = 'tBZ2',
    3417                          unit = 'mg%',
    3418                          name = 'BZ (test 2)'
    3419                  ))
    3420          #--------------------------------------------------------
    3421 -        def test_meta_test_type():
    3422                  mtt = cMetaTestType(aPK_obj = 1)
    3423                  print(mtt)
    3424                  print(get_meta_test_types())
    3425          #--------------------------------------------------------
    3426 -        def test_test_type():
    3427                  tt = cMeasurementType(aPK_obj = 1)
    3428                  print(tt)
    3429                  print(get_measurement_types())
    3430          #--------------------------------------------------------
    3431 -        def test_format_test_results():
    3432                  results = [
    3433                          cTestResult(aPK_obj=1),
    3434                          cTestResult(aPK_obj=2),
    3435                          cTestResult(aPK_obj=3)
    3436  #                       cTestResult(aPK_obj=4)
    3437                  ]
    3438                  print(format_test_results(results = results))
    3439          #--------------------------------------------------------
    3440 -        def test_calculate_bmi():
    3441                  done, data = calculate_bmi(mass = sys.argv[2], height = sys.argv[3])
    3442                  bmi, low, high = data
    3443                  print("BMI:", bmi)
    3444                  print("low:", low, "kg")
    3445                  print("hi :", high, "kg")
    3446
    3447          #--------------------------------------------------------
    3448 -        def test_test_panel():
    3449                  tp = cTestPanel(aPK_obj = 2)
    3450                  print(tp)
    3451                  print(tp.test_types)
    3452                  print(tp.format())
    3453
    3454          #--------------------------------------------------------
    3455 -        def test_get_most_recent_results_for_panel():
    3456                  tp = cTestPanel(aPK_obj = 1)
    3457                  #print tp.included_loincs
    3458                  #tp = cTestPanel(aPK_obj = 3)
    3459                  print(tp.format())
    3460                  #most_recent = tp.get_most_recent_results(pk_patient = 12, group_by_meta_type = False)
    3461                  #most_recent = tp.get_most_recent_results(pk_patient = 138, group_by_meta_type = False)
    3462                  #print len(most_recent)
    3463                  most_recent = tp.get_most_recent_results(pk_patient = 12, group_by_meta_type = True, include_missing = True)
    3464                  #most_recent = tp.get_most_recent_results(pk_patient = 138, group_by_meta_type = True)
    3465                  print('found:', len(most_recent))
    3466
    3467                  for t in most_recent:
    3468                          print('--------------')
    3469                          if t['pk_meta_test_type'] is None:
    3470                                  print("standalone")
    3471                          else:
    3472                                  print("meta")
    3473                          print(t.format())
    3474
    3475          #--------------------------------------------------------
    3476 -        def test_get_most_recent_results_in_loinc_group():
    3477                  most_recent = get_most_recent_results_in_loinc_group (
    3478                          #loincs = [u'pseudo LOINC [C-reactive protein (EML)::9] (v21->v22 test panel conversion)'],
    3479                          #loincs = ['8867-4'],
    3480                          loincs = ['2160-0', '14682-9', '40264-4', '40248-7'],
    3481                          max_no_of_results = 6,
    3482                          patient = 201,
    3483                          consider_indirect_matches = False
    3484                  )
    3485                  for t in most_recent:
    3486                          print(t['loinc_tt'], t['loinc_meta'], t['unified_loinc'])
    3487                          if t['pk_meta_test_type'] is None:
    3488                                  print("---- standalone ----")
    3489                          else:
    3490                                  print("---- meta ----")
    3491                          print(t.format())
    3492                          input('next')
    3493
    3494                  return
    3495
    3496                  most_recent = get_most_recent_results_in_loinc_group (
    3497                          #loincs = [u'pseudo LOINC [C-reactive protein (EML)::9] (v21->v22 test panel conversion)'],
    3498                          #loincs = ['8867-4'],
    3499                          loincs = ['2160-0', '14682-9', '40264-4', '40248-7'],
    3500                          max_no_of_results = 2,
    3501                          patient = 201,
    3502                          consider_indirect_matches = False
    3503                          #consider_indirect_matches = True
    3504                  )
    3505                  for t in most_recent:
    3506                          if t['pk_meta_test_type'] is None:
    3507                                  print("---- standalone ----")
    3508                          else:
    3509                                  print("---- meta ----")
    3510                          print(t.format())
    3511
    3512          #--------------------------------------------------------
    3513
    3514          #test_result()
    3515          #test_create_test_result()
    3516          #test_delete_test_result()
    3517          #test_create_measurement_type()
    3518          #test_lab_result()
    3519          #test_request()
    3520          #test_create_result()
    3521          #test_unreviewed()
    3522          #test_pending()
    3523          #test_meta_test_type()
    3524          #test_test_type()
    3525          #test_format_test_results()
    3526          #test_calculate_bmi()
    3527          #test_test_panel()
    3528          #test_get_most_recent_results_for_panel()
    3529          test_get_most_recent_results_in_loinc_group()
    3530
    3531  #============================================================
    3532