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