Gnumed.business.gmPathLab

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

# Source Code for Module Gnumed.business.gmPathLab

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