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 types
      10  import sys
      11  import logging
      12  import codecs
      13  import decimal
      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          gmDateTime.init()
      24  from Gnumed.pycommon import gmExceptions
      25  from Gnumed.pycommon import gmBusinessDBObject
      26  from Gnumed.pycommon import gmPG2
      27  from Gnumed.pycommon import gmTools
      28  from Gnumed.pycommon import gmDispatcher
      29  from Gnumed.pycommon import gmHooks
      30  from Gnumed.business import gmOrganization
      31  from Gnumed.business import gmCoding
      32
      33
      34  _log = logging.getLogger('gm.lab')
      35
      36  #============================================================
      37 -def _on_test_result_modified():
      38          """Always relates to the active patient."""
      39          gmHooks.run_hook_script(hook = u'after_test_result_modified')
      40
      41  gmDispatcher.connect(_on_test_result_modified, u'test_result_mod_db')
      42
      43  #============================================================
      44 -class cTestOrg(gmBusinessDBObject.cBusinessDBObject):
      45          """Represents one test org/lab."""
      46          _cmd_fetch_payload = u"""SELECT * FROM clin.v_test_orgs WHERE pk_test_org = %s"""
      47          _cmds_store_payload = [
      48                  u"""UPDATE clin.test_org SET
      49                                  fk_org_unit = %(pk_org_unit)s,
      50                                  contact = gm.nullify_empty_string(%(test_org_contact)s),
      51                                  comment = gm.nullify_empty_string(%(comment)s)
      52                          WHERE
      53                                  pk = %(pk_test_org)s
      54                                          AND
      55                                  xmin = %(xmin_test_org)s
      56                          RETURNING
      57                                  xmin AS xmin_test_org
      58                  """
      59          ]
      60          _updatable_fields = [
      61                  u'pk_org_unit',
      62                  u'test_org_contact',
      63                  u'comment'
      64          ]
      65  #------------------------------------------------------------
      66 -def create_test_org(name=None, comment=None, pk_org_unit=None):
      67
      68          if name is None:
      69                  name = _('inhouse lab')
      70                  comment = _('auto-generated')
      71
      72          # get org unit
      73          if pk_org_unit is None:
      74                  org = gmOrganization.org_exists(organization = name)
      75                  if org is None:
      76                          org = gmOrganization.create_org (
      77                                  organization = name,
      78                                  category = u'Laboratory'
      79                          )
      80                  org_unit = gmOrganization.create_org_unit (
      81                          pk_organization = org['pk_org'],
      82                          unit = name
      83                  )
      84                  pk_org_unit = org_unit['pk_org_unit']
      85
      86          # test org exists ?
      87          args = {'pk_unit': pk_org_unit}
      88          cmd = u'SELECT pk_test_org FROM clin.v_test_orgs WHERE pk_org_unit = %(pk_unit)s'
      89          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
      90
      91          if len(rows) == 0:
      92                  cmd = u'INSERT INTO clin.test_org (fk_org_unit) VALUES (%(pk_unit)s) RETURNING pk'
      93                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True)
      94
      95          test_org = cTestOrg(aPK_obj = rows[0][0])
      96          if comment is not None:
      97                  comment = comment.strip()
      98          test_org['comment'] = comment
      99          test_org.save()
     100
     101          return test_org
     102  #------------------------------------------------------------
     103 -def delete_test_org(test_org=None):
     104          args = {'pk': test_org}
     105          cmd = u"""
     106                  DELETE FROM clin.test_org
     107                  WHERE
     108                          pk = %(pk)s
     109                                  AND
     110                          NOT EXISTS (SELECT 1 FROM clin.lab_request WHERE fk_test_org = %(pk)s LIMIT 1)
     111                                  AND
     112                          NOT EXISTS (SELECT 1 FROM clin.test_type WHERE fk_test_org = %(pk)s LIMIT 1)
     113          """
     114          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     115  #------------------------------------------------------------
     116 -def get_test_orgs(order_by=u'unit'):
     117          cmd = u'SELECT * FROM clin.v_test_orgs ORDER BY %s' % order_by
     118          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True)
     119          return [ cTestOrg(row = {'pk_field': 'pk_test_org', 'data': r, 'idx': idx}) for r in rows ]
     120
     121  #============================================================
     122  # test panels / profiles
     123  #------------------------------------------------------------
     124  _SQL_get_test_panels = u"SELECT * FROM clin.v_test_panels WHERE %s"
     125
     126 -class cTestPanel(gmBusinessDBObject.cBusinessDBObject):
     127          """Represents a grouping/listing of tests into a panel."""
     128
     129          _cmd_fetch_payload = _SQL_get_test_panels % u"pk_test_panel = %s"
     130          _cmds_store_payload = [
     131                  u"""
     132                          UPDATE clin.test_panel SET
     133                                  description = gm.nullify_empty_string(%(description)s),
     134                                  comment = gm.nullify_empty_string(%(comment)s),
     135                                  fk_test_types = %(pk_test_types)s
     136                          WHERE
     137                                  pk = %(pk_test_panel)s
     138                                          AND
     139                                  xmin = %(xmin_test_panel)s
     140                          RETURNING
     141                                  xmin AS xmin_test_panel
     142                  """
     143          ]
     144          _updatable_fields = [
     145                  u'description',
     146                  u'comment',
     147                  u'pk_test_types'
     148          ]
     149          #--------------------------------------------------------
     150 -        def add_code(self, pk_code=None):
     151                  """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
     152                  cmd = u"INSERT INTO clin.lnk_code2tst_pnl (fk_item, fk_generic_code) values (%(tp)s, %(code)s)"
     153                  args = {
     154                          'tp': self._payload[self._idx['pk_test_panel']],
     155                          'code': pk_code
     156                  }
     157                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     158                  return True
     159          #--------------------------------------------------------
     160 -        def remove_code(self, pk_code=None):
     161                  """<pk_code> must be a value from ref.coding_system_root.pk_coding_system (clin.lnk_code2item_root.fk_generic_code)"""
     162                  cmd = u"DELETE FROM clin.lnk_code2tst_pnl WHERE fk_item = %(tp)s AND fk_generic_code = %(code)s"
     163                  args = {
     164                          'tp': self._payload[self._idx['pk_test_panel']],
     165                          'code': pk_code
     166                  }
     167                  rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     168                  return True
     169          #--------------------------------------------------------
     170          # properties
     171          #--------------------------------------------------------
     172 -        def _get_test_types(self):
     173                  if self._payload[self._idx['pk_test_types']] is None:
     174                          return None
     175
     176                  rows, idx = gmPG2.run_ro_queries (
     177                          queries = [{
     178                          'cmd': _SQL_get_test_types % u'pk_test_type IN %(pks)s ORDER BY unified_abbrev',
     179                          'args': {'pks': tuple(self._payload[self._idx['pk_test_types']])}
     180                          }],
     181                          get_col_idx = True
     182                  )
     183                  return [ cMeasurementType(row = {'data': r, 'idx': idx, 'pk_field': 'pk_test_type'}) for r in rows ]
     184
     185          test_types = property(_get_test_types, lambda x:x)
     186          #--------------------------------------------------------
     187 -        def _get_generic_codes(self):
     188                  if len(self._payload[self._idx['pk_generic_codes']]) == 0:
     189                          return []
     190
     191                  cmd = gmCoding._SQL_get_generic_linked_codes % u'pk_generic_code IN %(pks)s'
     192                  args = {'pks': tuple(self._payload[self._idx['pk_generic_codes']])}
     193                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     194                  return [ gmCoding.cGenericLinkedCode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_lnk_code2item'}) for r in rows ]
     195
     196 -        def _set_generic_codes(self, pk_codes):
     197                  queries = []
     198                  # remove all codes
     199                  if len(self._payload[self._idx['pk_generic_codes']]) > 0:
     200                          queries.append ({
     201                                  'cmd': u'DELETE FROM clin.lnk_code2tst_pnl WHERE fk_item = %(tp)s AND fk_generic_code IN %(codes)s',
     202                                  'args': {
     203                                          'tp': self._payload[self._idx['pk_test_panel']],
     204                                          'codes': tuple(self._payload[self._idx['pk_generic_codes']])
     205                                  }
     206                          })
     207                  # add new codes
     208                  for pk_code in pk_codes:
     209                          queries.append ({
     210                                  'cmd': u'INSERT INTO clin.lnk_code2test_panel (fk_item, fk_generic_code) VALUES (%(tp)s, %(pk_code)s)',
     211                                  'args': {
     212                                          'tp': self._payload[self._idx['pk_test_panel']],
     213                                          'pk_code': pk_code
     214                                  }
     215                          })
     216                  if len(queries) == 0:
     217                          return
     218                  # run it all in one transaction
     219                  rows, idx = gmPG2.run_rw_queries(queries = queries)
     220                  return
     221
     222          generic_codes = property(_get_generic_codes, _set_generic_codes)
     223          #--------------------------------------------------------
     224 -        def format(self):
     225                  txt = _('Test panel "%s"          [#%s]\n') % (
     226                          self._payload[self._idx['description']],
     227                          self._payload[self._idx['pk_test_panel']]
     228                  )
     229
     230                  if self._payload[self._idx['comment']] is not None:
     231                          txt += u'\n'
     232                          txt += gmTools.wrap (
     233                                  text = self._payload[self._idx['comment']],
     234                                  width = 50,
     235                                  initial_indent = u' ',
     236                                  subsequent_indent = u' '
     237                          )
     238                          txt += u'\n'
     239
     240                  tts = self.test_types
     241                  if tts is not None:
     242                          txt += u'\n'
     243                          txt += _('Included test types:\n')
     244                          for tt in tts:
     245                                  txt += u' %s: %s\n' % (
     246                                          tt['abbrev'],
     247                                          tt['name']
     248                                  )
     249
     250                  codes = self.generic_codes
     251                  if len(codes) > 0:
     252                          txt += u'\n'
     253                          for c in codes:
     254                                  txt += u'%s  %s: %s (%s - %s)\n' % (
     255                                          (u' ' * left_margin),
     256                                          c['code'],
     257                                          c['term'],
     258                                          c['name_short'],
     259                                          c['version']
     260                                  )
     261
     262                  return txt
     263  #------------------------------------------------------------
     264 -def get_test_panels(order_by=None):
     265          if order_by is None:
     266                  order_by = u'true'
     267          else:
     268                  order_by = u'true ORDER BY %s' % order_by
     269
     270          cmd = _SQL_get_test_panels % order_by
     271          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True)
     272          return [ cTestPanel(row = {'data': r, 'idx': idx, 'pk_field': 'pk_test_panel'}) for r in rows ]
     273  #------------------------------------------------------------
     274 -def create_test_panel(description=None):
     275
     276          args = {u'desc': description.strip()}
     277          cmd = u"""
     278                  INSERT INTO clin.test_panel (description)
     279                  VALUES (gm.nullify_empty_string(%(desc)s))
     280                  RETURNING pk
     281          """
     282          rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
     283
     284          return cTestPanel(aPK_obj = rows[0]['pk'])
     285  #------------------------------------------------------------
     286 -def delete_test_panel(pk=None):
     287          args = {'pk': pk}
     288          cmd = u"DELETE FROM clin.test_panel WHERE pk = %(pk)s"
     289          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     290          return True
     291
     292  #============================================================
     293 -class cMetaTestType(gmBusinessDBObject.cBusinessDBObject):
     294          """Represents one meta test type under which actual test types can be aggregated."""
     295
     296          _cmd_fetch_payload = u"""select * from clin.meta_test_type where pk = %s"""
     297
     298          _cmds_store_payload = []
     299
     300          _updatable_fields = []
     301  #------------------------------------------------------------
     302 -def delete_meta_type(meta_type=None):
     303          cmd = u'delete from clin.meta_test_type where pk = %(pk)s'
     304          args = {'pk': meta_type}
     305          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     306  #------------------------------------------------------------
     307 -def get_meta_test_types():
     308          cmd = u'select * from clin.meta_test_type'
     309          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True)
     310          return [ cMetaTestType(row = {'pk_field': 'pk', 'data': r, 'idx': idx}) for r in rows ]
     311
     312  #============================================================
     313  _SQL_get_test_types = u"SELECT * FROM clin.v_test_types WHERE %s"
     314
     315 -class cMeasurementType(gmBusinessDBObject.cBusinessDBObject):
     316          """Represents one test result type."""
     317
     318          _cmd_fetch_payload = _SQL_get_test_types % u"pk_test_type = %s"
     319
     320          _cmds_store_payload = [
     321                  u"""UPDATE clin.test_type SET
     322                                  abbrev = gm.nullify_empty_string(%(abbrev)s),
     323                                  name = gm.nullify_empty_string(%(name)s),
     324                                  loinc = gm.nullify_empty_string(%(loinc)s),
     325                                  comment = gm.nullify_empty_string(%(comment_type)s),
     326                                  conversion_unit = gm.nullify_empty_string(%(conversion_unit)s),
     327                                  fk_test_org = %(pk_test_org)s,
     328                                  fk_meta_test_type = %(pk_meta_test_type)s
     329                          WHERE
     330                                  pk = %(pk_test_type)s
     331                                          AND
     332                                  xmin = %(xmin_test_type)s
     333                          RETURNING
     334                                  xmin AS xmin_test_type"""
     335          ]
     336
     337          _updatable_fields = [
     338                  'abbrev',
     339                  'name',
     340                  'loinc',
     341                  'comment_type',
     342                  'conversion_unit',
     343                  'pk_test_org',
     344                  'pk_meta_test_type'
     345          ]
     346          #--------------------------------------------------------
     347  #       def __setitem__(self, attribute, value):
     348  #
     349  #               # find fk_test_org from name
     350  #               if (attribute == 'fk_test_org') and (value is not None):
     351  #                       try:
     352  #                               int(value)
     353  #                       except:
     354  #                               cmd = u"select pk from clin.test_org where internal _name = %(val)s"
     355  #                               rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'val': value}}])
     356  #                               if len(rows) == 0:
     357  #                                       raise ValueError('[%s]: no test org for [%s], cannot set <%s>' % (self.__class__.__name__, value, attribute))
     358  #                               value = rows[0][0]
     359  #
     360  #               gmBusinessDBObject.cBusinessDBObject.__setitem__(self, attribute, value)
     361          #--------------------------------------------------------
     362 -        def _get_in_use(self):
     363                  cmd = u'SELECT EXISTS(SELECT 1 FROM clin.test_result WHERE fk_type = %(pk_type)s)'
     364                  args = {'pk_type': self._payload[self._idx['pk_test_type']]}
     365                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
     366                  return rows[0][0]
     367
     368          in_use = property(_get_in_use, lambda x:x)
     369          #--------------------------------------------------------
     370 -        def get_most_recent_results(self, patient=None, no_of_results=1):
     371                  results = get_most_recent_results (
     372                          test_type = self._payload[self._idx['pk_test_type']],
     373                          loinc = None,
     374                          no_of_results = no_of_results,
     375                          patient = patient
     376                  )
     377                  if results is None:
     378                          if self._payload[self._idx['loinc']] is not None:
     379                                  results = get_most_recent_results (
     380                                          test_type = None,
     381                                          loinc = self._payload[self._idx['loinc']],
     382                                          no_of_results = no_of_results,
     383                                          patient = patient
     384                                  )
     385                  return results
     386          #--------------------------------------------------------
     387 -        def _get_test_panels(self):
     388                  if self._payload[self._idx['pk_test_panels']] is None:
     389                          return None
     390
     391                  return [ cTestPanel(aPK_obj = pk) for pk in self._payload[self._idx['pk_test_panels']] ]
     392
     393          test_panels = property(_get_test_panels, lambda x:x)
     394          #--------------------------------------------------------
     395 -        def format(self, patient=None):
     396                  tt = u''
     397                  tt += _('Test type "%s" (%s)          [#%s]\n') % (
     398                          self._payload[self._idx['name']],
     399                          self._payload[self._idx['abbrev']],
     400                          self._payload[self._idx['pk_test_type']]
     401                  )
     402                  tt += u'\n'
     403                  tt += gmTools.coalesce(self._payload[self._idx['loinc']], u'', u' LOINC: %s\n')
     404                  tt += gmTools.coalesce(self._payload[self._idx['conversion_unit']], u'', _(' Conversion unit: %s\n'))
     405                  tt += gmTools.coalesce(self._payload[self._idx['comment_type']], u'', _(' Comment: %s\n'))
     406
     407                  tt += u'\n'
     408                  tt += _('Lab details:\n')
     409                  tt += _(' Name: %s\n') % self._payload[self._idx['name_org']]
     410                  tt += gmTools.coalesce(self._payload[self._idx['contact_org']], u'', _(' Contact: %s\n'))
     411                  tt += gmTools.coalesce(self._payload[self._idx['comment_org']], u'', _(' Comment: %s\n'))
     412
     413                  if self._payload[self._idx['is_fake_meta_type']] is False:
     414                          tt += u'\n'
     415                          tt += _('Aggregated under meta type:\n')
     416                          tt += _(' Name: %s - %s             [#%s]\n') % (
     417                                  self._payload[self._idx['abbrev_meta']],
     418                                  self._payload[self._idx['name_meta']],
     419                                  self._payload[self._idx['pk_meta_test_type']]
     420                          )
     421                          tt += gmTools.coalesce(self._payload[self._idx['loinc_meta']], u'', u' LOINC: %s\n')
     422                          tt += gmTools.coalesce(self._payload[self._idx['comment_meta']], u'', _(' Comment: %s\n'))
     423
     424                  panels = self.test_panels
     425                  if panels is not None:
     426                          tt += u'\n'
     427                          tt += _('Listed in test panels:\n')
     428                          for panel in panels:
     429                                  tt += _(' Panel "%s"             [#%s]\n') % (
     430                                          panel['description'],
     431                                          panel['pk_test_panel']
     432                                  )
     433
     434                  if patient is not None:
     435                          result = self.get_most_recent_results(patient = patient, no_of_results = 1)
     436                          if result is not None:
     437                                  tt += u'\n'
     438                                  tt += _('Most recent result:\n')
     439                                  tt += _(' %s: %s%s%s') % (
     440                                          result['clin_when'].strftime('%Y-%m-%d'),
     441                                          result['unified_val'],
     442                                          gmTools.coalesce(result['val_unit'], u'', u' %s'),
     443                                          gmTools.coalesce(result['abnormality_indicator'], u'', u' (%s)')
     444                                  )
     445
     446                  return tt
     447
     448  #------------------------------------------------------------
     449 -def get_measurement_types(order_by=None):
     450          cmd = u'select * from clin.v_test_types %s' % gmTools.coalesce(order_by, u'', u'order by %s')
     451          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True)
     452          return [ cMeasurementType(row = {'pk_field': 'pk_test_type', 'data': r, 'idx': idx}) for r in rows ]
     453  #------------------------------------------------------------
     454 -def find_measurement_type(lab=None, abbrev=None, name=None):
     455
     456                  if (abbrev is None) and (name is None):
     457                          raise ValueError('must have <abbrev> and/or <name> set')
     458
     459                  where_snippets = []
     460
     461                  if lab is None:
     462                          where_snippets.append('pk_test_org IS NULL')
     463                  else:
     464                          try:
     465                                  int(lab)
     466                                  where_snippets.append('pk_test_org = %(lab)s')
     467                          except (TypeError, ValueError):
     468                                  where_snippets.append('pk_test_org = (SELECT pk_test_org FROM clin.v_test_orgs WHERE unit = %(lab)s)')
     469
     470                  if abbrev is not None:
     471                          where_snippets.append('abbrev = %(abbrev)s')
     472
     473                  if name is not None:
     474                          where_snippets.append('name = %(name)s')
     475
     476                  where_clause = u' and '.join(where_snippets)
     477                  cmd = u"select * from clin.v_test_types where %s" % where_clause
     478                  args = {'lab': lab, 'abbrev': abbrev, 'name': name}
     479
     480                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
     481
     482                  if len(rows) == 0:
     483                          return None
     484
     485                  tt = cMeasurementType(row = {'pk_field': 'pk_test_type', 'data': rows[0], 'idx': idx})
     486                  return tt
     487  #------------------------------------------------------------
     488 -def delete_measurement_type(measurement_type=None):
     489          cmd = u'delete from clin.test_type where pk = %(pk)s'
     490          args = {'pk': measurement_type}
     491          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
     492  #------------------------------------------------------------
     493 -def create_measurement_type(lab=None, abbrev=None, unit=None, name=None):
     494          """Create or get test type."""
     495
     496          ttype = find_measurement_type(lab = lab, abbrev = abbrev, name = name)
     497          # found ?
     498          if ttype is not None:
     499                  return ttype
     500
     501          _log.debug('creating test type [%s:%s:%s:%s]', lab, abbrev, name, unit)
     502
     503          # not found, so create it
     504          if unit is None:
     505                  _log.error('need <unit> to create test type: %s:%s:%s:%s' % (lab, abbrev, name, unit))
     506                  raise ValueError('need <unit> to create test type')
     507
     508          # make query
     509          cols = []
     510          val_snippets = []
     511          vals = {}
     512
     513          # lab
     514          if lab is None:
     515                  lab = create_test_org()['pk_test_org']
     516
     517          cols.append('fk_test_org')
     518          try:
     519                  vals['lab'] = int(lab)
     520                  val_snippets.append('%(lab)s')
     521          except:
     522                  vals['lab'] = lab
     523                  val_snippets.append('(SELECT pk_test_org FROM clin.v_test_orgs WHERE unit = %(lab)s)')
     524
     525          # code
     526          cols.append('abbrev')
     527          val_snippets.append('%(abbrev)s')
     528          vals['abbrev'] = abbrev
     529
     530          # unit
     531          cols.append('conversion_unit')
     532          val_snippets.append('%(unit)s')
     533          vals['unit'] = unit
     534
     535          # name
     536          if name is not None:
     537                  cols.append('name')
     538                  val_snippets.append('%(name)s')
     539                  vals['name'] = name
     540
     541          col_clause = u', '.join(cols)
     542          val_clause = u', '.join(val_snippets)
     543          queries = [
     544                  {'cmd': u'insert into clin.test_type (%s) values (%s)' % (col_clause, val_clause), 'args': vals},
     545                  {'cmd': u"select * from clin.v_test_types where pk_test_type = currval(pg_get_serial_sequence('clin.test_type', 'pk'))"}
     546          ]
     547          rows, idx = gmPG2.run_rw_queries(queries = queries, get_col_idx = True, return_data = True)
     548          ttype = cMeasurementType(row = {'pk_field': 'pk_test_type', 'data': rows[0], 'idx': idx})
     549
     550          return ttype
     551
     552  #============================================================
     553 -class cTestResult(gmBusinessDBObject.cBusinessDBObject):
     554          """Represents one test result."""
     555
     556          _cmd_fetch_payload = u"select * from clin.v_test_results where pk_test_result = %s"
     557
     558          _cmds_store_payload = [
     559                  u"""update clin.test_result set
     560                                  clin_when = %(clin_when)s,
     561                                  narrative = nullif(trim(%(comment)s), ''),
     562                                  val_num = %(val_num)s,
     563                                  val_alpha = nullif(trim(%(val_alpha)s), ''),
     564                                  val_unit = nullif(trim(%(val_unit)s), ''),
     565                                  val_normal_min = %(val_normal_min)s,
     566                                  val_normal_max = %(val_normal_max)s,
     567                                  val_normal_range = nullif(trim(%(val_normal_range)s), ''),
     568                                  val_target_min = %(val_target_min)s,
     569                                  val_target_max = %(val_target_max)s,
     570                                  val_target_range = nullif(trim(%(val_target_range)s), ''),
     571                                  abnormality_indicator = nullif(trim(%(abnormality_indicator)s), ''),
     572                                  norm_ref_group = nullif(trim(%(norm_ref_group)s), ''),
     573                                  note_test_org = nullif(trim(%(note_test_org)s), ''),
     574                                  material = nullif(trim(%(material)s), ''),
     575                                  material_detail = nullif(trim(%(material_detail)s), ''),
     576                                  fk_intended_reviewer = %(pk_intended_reviewer)s,
     577                                  fk_encounter = %(pk_encounter)s,
     578                                  fk_episode = %(pk_episode)s,
     579                                  fk_type = %(pk_test_type)s,
     580                                  fk_request = %(pk_request)s
     581                          where
     582                                  pk = %(pk_test_result)s and
     583                                  xmin = %(xmin_test_result)s""",
     584                  u"""select xmin_test_result from clin.v_test_results where pk_test_result = %(pk_test_result)s"""
     585          ]
     586
     587          _updatable_fields = [
     588                  'clin_when',
     589                  'comment',
     590                  'val_num',
     591                  'val_alpha',
     592                  'val_unit',
     593                  'val_normal_min',
     594                  'val_normal_max',
     595                  'val_normal_range',
     596                  'val_target_min',
     597                  'val_target_max',
     598                  'val_target_range',
     599                  'abnormality_indicator',
     600                  'norm_ref_group',
     601                  'note_test_org',
     602                  'material',
     603                  'material_detail',
     604                  'pk_intended_reviewer',
     605                  'pk_encounter',
     606                  'pk_episode',
     607                  'pk_test_type',
     608                  'pk_request'
     609          ]
     610          #--------------------------------------------------------
     611  #       def format_old(self, with_review=True, with_comments=True, date_format='%Y-%m-%d %H:%M'):
     612  #
     613  #               lines = []
     614  #
     615  #               lines.append(u' %s %s (%s): %s %s%s' % (
     616  #                       self._payload[self._idx['clin_when']].strftime(date_format),
     617  #                       self._payload[self._idx['unified_abbrev']],
     618  #                       self._payload[self._idx['unified_name']],
     619  #                       self._payload[self._idx['unified_val']],
     620  #                       self._payload[self._idx['val_unit']],
     621  #                       gmTools.coalesce(self._payload[self._idx['abnormality_indicator']], u'', u' (%s)')
     622  #               ))
     623  #
     624  #               if with_comments:
     625  #                       if gmTools.coalesce(self._payload[self._idx['comment']], u'').strip() != u'':
     626  #                               lines.append(_('   Doc: %s') % self._payload[self._idx['comment']].strip())
     627  #                       if gmTools.coalesce(self._payload[self._idx['note_test_org']], u'').strip() != u'':
     628  #                               lines.append(_('   MTA: %s') % self._payload[self._idx['note_test_org']].strip())
     629  #
     630  #               if with_review:
     631  #                       if self._payload[self._idx['reviewed']]:
     632  #                               if self._payload[self._idx['is_clinically_relevant']]:
     633  #                                       lines.append(u'   %s  %s: %s' % (
     634  #                                               self._payload[self._idx['last_reviewer']],
     635  #                                               self._payload[self._idx['last_reviewed']].strftime('%Y-%m-%d %H:%M'),
     636  #                                               gmTools.bool2subst (
     637  #                                                       self._payload[self._idx['is_technically_abnormal']],
     638  #                                                       _('abnormal and relevant'),
     639  #                                                       _('normal but relevant')
     640  #                                               )
     641  #                                       ))
     642  #                       else:
     643  #                               lines.append(_('   unreviewed'))
     644  #
     645  #               return lines
     646          #--------------------------------------------------------
     647 -        def format(self, with_review=True, with_evaluation=True, with_ranges=True, with_episode=True, with_type_details=True, date_format='%Y %b %d %H:%M'):
     648
     649                  # FIXME: add battery, request details
     650
     651                  has_normal_min_or_max = (
     652                          self._payload[self._idx['val_normal_min']] is not None
     653                  ) or (
     654                          self._payload[self._idx['val_normal_max']] is not None
     655                  )
     656                  if has_normal_min_or_max:
     657                          normal_min_max = u'%s - %s' % (
     658                                  gmTools.coalesce(self._payload[self._idx['val_normal_min']], u'?'),
     659                                  gmTools.coalesce(self._payload[self._idx['val_normal_max']], u'?')
     660                          )
     661                  else:
     662                          normal_min_max = u''
     663
     664                  has_clinical_min_or_max = (
     665                          self._payload[self._idx['val_target_min']] is not None
     666                  ) or (
     667                          self._payload[self._idx['val_target_max']] is not None
     668                  )
     669                  if has_clinical_min_or_max:
     670                          clinical_min_max = u'%s - %s' % (
     671                                  gmTools.coalesce(self._payload[self._idx['val_target_min']], u'?'),
     672                                  gmTools.coalesce(self._payload[self._idx['val_target_max']], u'?')
     673                          )
     674                  else:
     675                          clinical_min_max = u''
     676
     677                  # header
     678                  tt = _(u'Result from %s             \n') % gmDateTime.pydt_strftime (
     679                          self._payload[self._idx['clin_when']],
     680                          date_format
     681                  )
     682
     683                  # basics
     684                  tt += u' ' + _(u'Type: "%(name)s" (%(abbr)s)  [#%(pk_type)s]\n') % ({
     685                          'name': self._payload[self._idx['name_tt']],
     686                          'abbr': self._payload[self._idx['abbrev_tt']],
     687                          'pk_type': self._payload[self._idx['pk_test_type']]
     688                  })
     689                  tt += u' ' + _(u'Result: %(val)s%(unit)s%(ind)s  [#%(pk_result)s]\n') % ({
     690                          'val': self._payload[self._idx['unified_val']],
     691                          'unit': gmTools.coalesce(self._payload[self._idx['val_unit']], u'', u' %s'),
     692                          'ind': gmTools.coalesce(self._payload[self._idx['abnormality_indicator']], u'', u' (%s)'),
     693                          'pk_result': self._payload[self._idx['pk_test_result']]
     694                  })
     695                  tmp = (u'%s%s' % (
     696                          gmTools.coalesce(self._payload[self._idx['name_test_org']], u''),
     697                          gmTools.coalesce(self._payload[self._idx['contact_test_org']], u'', u' (%s)'),
     698                  )).strip()
     699                  if tmp != u'':
     700                          tt += u' ' + _(u'Source: %s\n') % tmp
     701                  tt += u'\n'
     702
     703                  if with_evaluation:
     704                          norm_eval = None
     705                          if self._payload[self._idx['val_num']] is not None:
     706                                  # 1) normal range
     707                                  # lowered ?
     708                                  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']]):
     709                                          try:
     710                                                  percent = (self._payload[self._idx['val_num']] * 100) / self._payload[self._idx['val_normal_min']]
     711                                          except ZeroDivisionError:
     712                                                  percent = None
     713                                          if percent is not None:
     714                                                  if percent < 6:
     715                                                          norm_eval = _(u'%.1f %% of the normal lower limit') % percent
     716                                                  else:
     717                                                          norm_eval = _(u'%.0f %% of the normal lower limit') % percent
     718                                  # raised ?
     719                                  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']]):
     720                                          try:
     721                                                  x_times = self._payload[self._idx['val_num']] / self._payload[self._idx['val_normal_max']]
     722                                          except ZeroDivisionError:
     723                                                  x_times = None
     724                                          if x_times is not None:
     725                                                  if x_times < 10:
     726                                                          norm_eval = _(u'%.1f times the normal upper limit') % x_times
     727                                                  else:
     728                                                          norm_eval = _(u'%.0f times the normal upper limit') % x_times
     729                                  if norm_eval is not None:
     730                                          tt += u'  (%s)\n' % norm_eval
     731          #                       #-------------------------------------
     732          #                       # this idea was shot down on the list
     733          #                       #-------------------------------------
     734          #                       # bandwidth of deviation
     735          #                       if None not in [self._payload[self._idx['val_normal_min']], self._payload[self._idx['val_normal_max']]]:
     736          #                               normal_width = self._payload[self._idx['val_normal_max']] - self._payload[self._idx['val_normal_min']]
     737          #                               deviation_from_normal_range = None
     738          #                               # below ?
     739          #                               if self._payload[self._idx['val_num']] < self._payload[self._idx['val_normal_min']]:
     740          #                                       deviation_from_normal_range = self._payload[self._idx['val_normal_min']] - self._payload[self._idx['val_num']]
     741          #                               # above ?
     742          #                               elif self._payload[self._idx['val_num']] > self._payload[self._idx['val_normal_max']]:
     743          #                                       deviation_from_normal_range = self._payload[self._idx['val_num']] - self._payload[self._idx['val_normal_max']]
     744          #                               if deviation_from_normal_range is None:
     745          #                                       try:
     746          #                                               times_deviation = deviation_from_normal_range / normal_width
     747          #                                       except ZeroDivisionError:
     748          #                                               times_deviation = None
     749          #                                       if times_deviation is not None:
     750          #                                               if times_deviation < 10:
     751          #                                                       tt += u'  (%s)\n' % _(u'deviates by %.1f times of the normal range') % times_deviation
     752          #                                               else:
     753          #                                                       tt += u'  (%s)\n' % _(u'deviates by %.0f times of the normal range') % times_deviation
     754          #                       #-------------------------------------
     755
     756                                  # 2) clinical target range
     757                                  norm_eval = None
     758                                  # lowered ?
     759                                  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']]):
     760                                          try:
     761                                                  percent = (self._payload[self._idx['val_num']] * 100) / self._payload[self._idx['val_target_min']]
     762                                          except ZeroDivisionError:
     763                                                  percent = None
     764                                          if percent is not None:
     765                                                  if percent < 6:
     766                                                          norm_eval = _(u'%.1f %% of the target lower limit') % percent
     767                                                  else:
     768                                                          norm_eval = _(u'%.0f %% of the target lower limit') % percent
     769                                  # raised ?
     770                                  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']]):
     771                                          try:
     772                                                  x_times = self._payload[self._idx['val_num']] / self._payload[self._idx['val_target_max']]
     773                                          except ZeroDivisionError:
     774                                                  x_times = None
     775                                          if x_times is not None:
     776                                                  if x_times < 10:
     777                                                          norm_eval = _(u'%.1f times the target upper limit') % x_times
     778                                                  else:
     779                                                          norm_eval = _(u'%.0f times the target upper limit') % x_times
     780                                  if norm_eval is not None:
     781                                          tt += u' (%s)\n' % norm_eval
     782          #                       #-------------------------------------
     783          #                       # this idea was shot down on the list
     784          #                       #-------------------------------------
     785          #                       # bandwidth of deviation
     786          #                       if None not in [self._payload[self._idx['val_target_min']], self._payload[self._idx['val_target_max']]]:
     787          #                               normal_width = self._payload[self._idx['val_target_max']] - self._payload[self._idx['val_target_min']]
     788          #                               deviation_from_target_range = None
     789          #                               # below ?
     790          #                               if self._payload[self._idx['val_num']] < self._payload[self._idx['val_target_min']]:
     791          #                                       deviation_from_target_range = self._payload[self._idx['val_target_min']] - self._payload[self._idx['val_num']]
     792          #                               # above ?
     793          #                               elif self._payload[self._idx['val_num']] > self._payload[self._idx['val_target_max']]:
     794          #                                       deviation_from_target_range = self._payload[self._idx['val_num']] - self._payload[self._idx['val_target_max']]
     795          #                               if deviation_from_target_range is None:
     796          #                                       try:
     797          #                                               times_deviation = deviation_from_target_range / normal_width
     798          #                                       except ZeroDivisionError:
     799          #                                               times_deviation = None
     800          #                               if times_deviation is not None:
     801          #                                       if times_deviation < 10:
     802          #                                               tt += u'  (%s)\n' % _(u'deviates by %.1f times of the target range') % times_deviation
     803          #                                       else:
     804          #                                               tt += u'  (%s)\n' % _(u'deviates by %.0f times of the target range') % times_deviation
     805          #                       #-------------------------------------
     806
     807                  if with_ranges:
     808                          tt += u' ' + _(u'Standard normal range: %(norm_min_max)s%(norm_range)s  \n') % ({
     809                                  'norm_min_max': normal_min_max,
     810                                  'norm_range': gmTools.coalesce (
     811                                          self._payload[self._idx['val_normal_range']],
     812                                          u'',
     813                                          gmTools.bool2subst (
     814                                                  has_normal_min_or_max,
     815                                                  u' / %s',
     816                                                  u'%s'
     817                                          )
     818                                  )
     819                          })
     820                          if self._payload[self._idx['norm_ref_group']] is not None:
     821                                  tt += u' ' + _(u'Reference group: %s\n') % self._payload[self._idx['norm_ref_group']]
     822                          tt += u' ' + _(u'Clinical target range: %(clin_min_max)s%(clin_range)s  \n') % ({
     823                                  'clin_min_max': clinical_min_max,
     824                                  'clin_range': gmTools.coalesce (
     825                                          self._payload[self._idx['val_target_range']],
     826                                          u'',
     827                                          gmTools.bool2subst (
     828                                                  has_clinical_min_or_max,
     829                                                  u' / %s',
     830                                                  u'%s'
     831                                          )
     832                                  )
     833                          })
     834
     835                  # metadata
     836                  if self._payload[self._idx['comment']] is not None:
     837                          tt += u' ' + _(u'Doc: %s\n') % _(u'\n Doc: ').join(self._payload[self._idx['comment']].split(u'\n'))
     838                  if self._payload[self._idx['note_test_org']] is not None:
     839                          tt += u' ' + _(u'Lab: %s\n') % _(u'\n Lab: ').join(self._payload[self._idx['note_test_org']].split(u'\n'))
     840                  if with_episode:
     841                          tt += u' ' + _(u'Episode: %s\n') % self._payload[self._idx['episode']]
     842                          if self._payload[self._idx['health_issue']] is not None:
     843                                  tt += u' ' + _(u'Issue: %s\n') % self._payload[self._idx['health_issue']]
     844                  if self._payload[self._idx['material']] is not None:
     845                          tt += u' ' + _(u'Material: %s\n') % self._payload[self._idx['material']]
     846                  if self._payload[self._idx['material_detail']] is not None:
     847                          tt += u' ' + _(u'Details: %s\n') % self._payload[self._idx['material_detail']]
     848                  tt += u'\n'
     849
     850                  if with_review:
     851                          if self._payload[self._idx['reviewed']]:
     852                                  review = gmDateTime.pydt_strftime (
     853                                          self._payload[self._idx['last_reviewed']],
     854                                          date_format
     855                                  )
     856                          else:
     857                                  review = _('not yet')
     858                          tt += _(u'Signed (%(sig_hand)s): %(reviewed)s\n') % ({
     859                                  'sig_hand': gmTools.u_writing_hand,
     860                                  'reviewed': review
     861                          })
     862                          tt += u' ' + _(u'Responsible clinician: %s\n') % gmTools.bool2subst (
     863                                  self._payload[self._idx['you_are_responsible']],
     864                                  _('you'),
     865                                  self._payload[self._idx['responsible_reviewer']]
     866                          )
     867                          if self._payload[self._idx['reviewed']]:
     868                                  tt += u' ' + _(u'Last reviewer: %(reviewer)s\n') % ({
     869                                          'reviewer': gmTools.bool2subst (
     870                                                  self._payload[self._idx['review_by_you']],
     871                                                  _('you'),
     872                                                  gmTools.coalesce(self._payload[self._idx['last_reviewer']], u'?')
     873                                          )
     874                                  })
     875                                  tt += u' ' + _(u' Technically abnormal: %(abnormal)s\n') % ({
     876                                          'abnormal': gmTools.bool2subst (
     877                                                  self._payload[self._idx['is_technically_abnormal']],
     878                                                  _('yes'),
     879                                                  _('no'),
     880                                                  u'?'
     881                                          )
     882                                  })
     883                                  tt += u' ' + _(u' Clinically relevant: %(relevant)s\n') % ({
     884                                          'relevant': gmTools.bool2subst (
     885                                                  self._payload[self._idx['is_clinically_relevant']],
     886                                                  _('yes'),
     887                                                  _('no'),
     888                                                  u'?'
     889                                          )
     890                                  })
     891                          if self._payload[self._idx['review_comment']] is not None:
     892                                  tt += u' ' + _(u' Comment: %s\n') % self._payload[self._idx['review_comment']].strip()
     893                          tt += u'\n'
     894
     895                  # type
     896                  if with_type_details:
     897                          tt += _(u'Test type details:\n')
     898                          tt += u' ' + _(u'Grouped under "%(name_meta)s" (%(abbrev_meta)s)  [#%(pk_u_type)s]\n') % ({
     899                                  'name_meta': gmTools.coalesce(self._payload[self._idx['name_meta']], u''),
     900                                  'abbrev_meta': gmTools.coalesce(self._payload[self._idx['abbrev_meta']], u''),
     901                                  'pk_u_type': self._payload[self._idx['pk_meta_test_type']]
     902                          })
     903                          if self._payload[self._idx['comment_tt']] is not None:
     904                                  tt += u' ' + _(u'Type comment: %s\n') % _(u'\n Type comment:').join(self._payload[self._idx['comment_tt']].split(u'\n'))
     905                          if self._payload[self._idx['comment_meta']] is not None:
     906                                  tt += u' ' + _(u'Group comment: %s\n') % _(u'\n Group comment: ').join(self._payload[self._idx['comment_meta']].split(u'\n'))
     907                          tt += u'\n'
     908
     909                  if with_review:
     910                          tt += _(u'Revisions: %(row_ver)s, last %(mod_when)s by %(mod_by)s.') % ({
     911                                  'row_ver': self._payload[self._idx['row_version']],
     912                                  'mod_when': gmDateTime.pydt_strftime(self._payload[self._idx['modified_when']],date_format),
     913                                  'mod_by': self._payload[self._idx['modified_by']]
     914                          })
     915
     916                  return tt
     917          #--------------------------------------------------------
     918 -        def _get_reference_ranges(self):
     919
     920                  cmd = u"""
     921  select
     922  distinct on (norm_ref_group_str, val_unit, val_normal_min, val_normal_max, val_normal_range, val_target_min, val_target_max, val_target_range)
     923          pk_patient,
     924          val_unit,
     925          val_normal_min, val_normal_max, val_normal_range,
     926          val_target_min, val_target_max, val_target_range,
     927          norm_ref_group,
     928          coalesce(norm_ref_group, '') as norm_ref_group_str
     929  from
     930          clin.v_test_results
     931  where
     932          pk_test_type = %(pk_type)s
     933  """
     934                  args = {'pk_type': self._payload[self._idx['pk_test_type']]}
     935                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
     936                  return rows
     937
     938 -        def _set_reference_ranges(self, val):
     939                  raise AttributeError('[%s]: reference ranges not settable') % self.__class__.__name__
     940
     941          reference_ranges = property(_get_reference_ranges, _set_reference_ranges)
     942          #--------------------------------------------------------
     943 -        def _get_test_type(self):
     944                  return cMeasurementType(aPK_obj = self._payload[self._idx['pk_test_type']])
     945
     946          test_type = property(_get_test_type, lambda x:x)
     947          #--------------------------------------------------------
     948 -        def set_review(self, technically_abnormal=None, clinically_relevant=None, comment=None, make_me_responsible=False):
     949
     950                  # FIXME: this is not concurrency safe
     951                  if self._payload[self._idx['reviewed']]:
     952                          self.__change_existing_review (
     953                                  technically_abnormal = technically_abnormal,
     954                                  clinically_relevant = clinically_relevant,
     955                                  comment = comment
     956                          )
     957                  else:
     958                          # do not sign off unreviewed results if
     959                          # NOTHING AT ALL is known about them
     960                          if technically_abnormal is None:
     961                                  if clinically_relevant is None:
     962                                          comment = gmTools.none_if(comment, u'', strip_string = True)
     963                                          if comment is None:
     964                                                  if make_me_responsible is False:
     965                                                          return True
     966                          self.__set_new_review (
     967                                  technically_abnormal = technically_abnormal,
     968                                  clinically_relevant = clinically_relevant,
     969                                  comment = comment
     970                          )
     971
     972                  if make_me_responsible is True:
     973                          cmd = u"SELECT pk FROM dem.staff WHERE db_user = current_user"
     974                          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
     975                          self['pk_intended_reviewer'] = rows[0][0]
     976                          self.save_payload()
     977                          return
     978
     979                  self.refetch_payload()
     980          #--------------------------------------------------------
     981 -        def get_adjacent_results(self, desired_earlier_results=1, desired_later_results=1, max_offset=None):
     982
     983                  if desired_earlier_results < 1:
     984                          raise ValueError('<desired_earlier_results> must be > 0')
     985
     986                  if desired_later_results < 1:
     987                          raise ValueError('<desired_later_results> must be > 0')
     988
     989                  args = {
     990                          'pat': self._payload[self._idx['pk_patient']],
     991                          'ttyp': self._payload[self._idx['pk_test_type']],
     992                          'tloinc': self._payload[self._idx['loinc_tt']],
     993                          'mtyp': self._payload[self._idx['pk_meta_test_type']],
     994                          'mloinc': self._payload[self._idx['loinc_meta']],
     995                          'when': self._payload[self._idx['clin_when']],
     996                          'offset': max_offset
     997                  }
     998                  WHERE = u'((pk_test_type = %(ttyp)s) OR (loinc_tt = %(tloinc)s))'
     999                  WHERE_meta = u'((pk_meta_test_type = %(mtyp)s) OR (loinc_meta = %(mloinc)s))'
    1000                  if max_offset is not None:
    1001                          WHERE = WHERE + u' AND (clin_when BETWEEN (%(when)s - %(offset)s) AND (%(when)s + %(offset)s))'
    1002                          WHERE_meta = WHERE_meta + u' AND (clin_when BETWEEN (%(when)s - %(offset)s) AND (%(when)s + %(offset)s))'
    1003
    1004                  SQL = u"""
    1005                          SELECT * FROM clin.v_test_results
    1006                          WHERE
    1007                                  pk_patient = %%(pat)s
    1008                                          AND
    1009                                  clin_when %s %%(when)s
    1010                                          AND
    1011                                  %s
    1012                          ORDER BY clin_when
    1013                          LIMIT %s"""
    1014
    1015                  # get earlier results
    1016                  earlier_results = []
    1017                  # by type
    1018                  cmd = SQL % (u'<', WHERE, desired_earlier_results)
    1019                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1020                  if len(rows) > 0:
    1021                          earlier_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ])
    1022                  # by meta type ?
    1023                  missing_results = desired_earlier_results - len(earlier_results)
    1024                  if  missing_results > 0:
    1025                          cmd = SQL % (u'<', WHERE_meta, missing_results)
    1026                          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1027                          if len(rows) > 0:
    1028                                  earlier_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ])
    1029
    1030                  # get later results
    1031                  later_results = []
    1032                  # by type
    1033                  cmd = SQL % (u'>', WHERE, desired_later_results)
    1034                  rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1035                  if len(rows) > 0:
    1036                          later_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ])
    1037                  # by meta type ?
    1038                  missing_results = desired_later_results - len(later_results)
    1039                  if  missing_results > 0:
    1040                          cmd = SQL % (u'>', WHERE_meta, missing_results)
    1041                          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1042                          if len(rows) > 0:
    1043                                  later_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ])
    1044
    1045                  return earlier_results, later_results
    1046          #--------------------------------------------------------
    1047          # internal API
    1048          #--------------------------------------------------------
    1049 -        def __set_new_review(self, technically_abnormal=None, clinically_relevant=None, comment=None):
    1050                  """Add a review to a row.
    1051
    1052                          - if technically abnormal is not provided/None it will be set
    1053                            to True if the lab's indicator has a meaningful value
    1054                          - if clinically relevant is not provided/None it is set to
    1055                            whatever technically abnormal is
    1056                  """
    1057                  if technically_abnormal is None:
    1058                          technically_abnormal = False
    1059                          if self._payload[self._idx['abnormality_indicator']] is not None:
    1060                                  if self._payload[self._idx['abnormality_indicator']].strip() != u'':
    1061                                          technically_abnormal = True
    1062
    1063                  if clinically_relevant is None:
    1064                          clinically_relevant = technically_abnormal
    1065
    1066                  cmd = u"""
    1067  INSERT INTO clin.reviewed_test_results (
    1068          fk_reviewed_row,
    1069          is_technically_abnormal,
    1070          clinically_relevant,
    1071          comment
    1072  ) VALUES (
    1073          %(pk)s,
    1074          %(abnormal)s,
    1075          %(relevant)s,
    1076          gm.nullify_empty_string(%(cmt)s)
    1077  )"""
    1078                  args = {
    1079                          'pk': self._payload[self._idx['pk_test_result']],
    1080                          'abnormal': technically_abnormal,
    1081                          'relevant': clinically_relevant,
    1082                          'cmt': comment
    1083                  }
    1084
    1085                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    1086          #--------------------------------------------------------
    1087 -        def __change_existing_review(self, technically_abnormal=None, clinically_relevant=None, comment=None):
    1088                  """Change a review on a row.
    1089
    1090                          - if technically abnormal/clinically relevant are
    1091                            None they are not set
    1092                  """
    1093                  args = {
    1094                          'pk_row': self._payload[self._idx['pk_test_result']],
    1095                          'abnormal': technically_abnormal,
    1096                          'relevant': clinically_relevant,
    1097                          'cmt': comment
    1098                  }
    1099
    1100                  set_parts = [
    1101                          u'fk_reviewer = (SELECT pk FROM dem.staff WHERE db_user = current_user)',
    1102                          u'comment = gm.nullify_empty_string(%(cmt)s)'
    1103                  ]
    1104
    1105                  if technically_abnormal is not None:
    1106                          set_parts.append(u'is_technically_abnormal = %(abnormal)s')
    1107
    1108                  if clinically_relevant is not None:
    1109                          set_parts.append(u'clinically_relevant = %(relevant)s')
    1110
    1111                  cmd = u"""
    1112  UPDATE clin.reviewed_test_results SET
    1113          %s
    1114  WHERE
    1115          fk_reviewed_row = %%(pk_row)s
    1116  """ % u',\n     '.join(set_parts)
    1117
    1118                  gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
    1119
    1120  #------------------------------------------------------------
    1121 -def get_test_results(pk_patient=None, encounters=None, episodes=None, order_by=None):
    1122
    1123          where_parts = []
    1124
    1125          if pk_patient is not None:
    1126                  where_parts.append(u'pk_patient = %(pat)s')
    1127                  args = {'pat': pk_patient}
    1128
    1129  #       if tests is not None:
    1130  #               where_parts.append(u'pk_test_type IN %(tests)s')
    1131  #               args['tests'] = tuple(tests)
    1132
    1133          if encounters is not None:
    1134                  where_parts.append(u'pk_encounter IN %(encs)s')
    1135                  args['encs'] = tuple(encounters)
    1136
    1137          if episodes is not None:
    1138                  where_parts.append(u'pk_episode IN %(epis)s')
    1139                  args['epis'] = tuple(episodes)
    1140
    1141          if order_by is None:
    1142                  order_by = u''
    1143          else:
    1144                  order_by = u'ORDER BY %s' % order_by
    1145
    1146          cmd = u"""
    1147                  SELECT * FROM clin.v_test_results
    1148                  WHERE %s
    1149                  %s
    1150          """ % (
    1151                  u' AND '.join(where_parts),
    1152                  order_by
    1153          )
    1154          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1155
    1156          tests = [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
    1157          return tests
    1158
    1159  #------------------------------------------------------------
    1160 -def get_result_at_timestamp(timestamp=None, test_type=None, loinc=None, tolerance_interval=None, patient=None):
    1161
    1162          if None not in [test_type, loinc]:
    1163                  raise ValueError('either <test_type> or <loinc> must be None')
    1164
    1165          args = {
    1166                  'pat': patient,
    1167                  'ttyp': test_type,
    1168                  'loinc': loinc,
    1169                  'ts': timestamp,
    1170                  'intv': tolerance_interval
    1171          }
    1172
    1173          where_parts = [u'pk_patient = %(pat)s']
    1174          if test_type is not None:
    1175                  where_parts.append(u'pk_test_type = %(ttyp)s')          # consider: pk_meta_test_type = %(pkmtt)s / self._payload[self._idx['pk_meta_test_type']]
    1176          elif loinc is not None:
    1177                  where_parts.append(u'((loinc_tt IN %(loinc)s) OR (loinc_meta IN %(loinc)s))')
    1178                  args['loinc'] = tuple(loinc)
    1179
    1180          if tolerance_interval is None:
    1181                  where_parts.append(u'clin_when = %(ts)s')
    1182          else:
    1183                  where_parts.append(u'clin_when between (%(ts)s - %(intv)s::interval) AND (%(ts)s + %(intv)s::interval)')
    1184
    1185          cmd = u"""
    1186                  SELECT * FROM clin.v_test_results
    1187                  WHERE
    1188                          %s
    1189                  ORDER BY
    1190                          abs(extract(epoch from age(clin_when, %%(ts)s)))
    1191                  LIMIT 1""" % u' AND '.join(where_parts)
    1192
    1193          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1194          if len(rows) == 0:
    1195                  return None
    1196
    1197          return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
    1198
    1199  #------------------------------------------------------------
    1200 -def get_most_recent_results(test_type=None, loinc=None, no_of_results=1, patient=None):
    1201
    1202          if None not in [test_type, loinc]:
    1203                  raise ValueError('either <test_type> or <loinc> must be None')
    1204
    1205          if no_of_results < 1:
    1206                  raise ValueError('<no_of_results> must be > 0')
    1207
    1208          args = {
    1209                  'pat': patient,
    1210                  'ttyp': test_type,
    1211                  'loinc': loinc
    1212          }
    1213
    1214          where_parts = [u'pk_patient = %(pat)s']
    1215          if test_type is not None:
    1216                  where_parts.append(u'pk_test_type = %(ttyp)s')          # consider: pk_meta_test_type = %(pkmtt)s / self._payload[self._idx['pk_meta_test_type']]
    1217          elif loinc is not None:
    1218                  where_parts.append(u'((loinc_tt IN %(loinc)s) OR (loinc_meta IN %(loinc)s))')
    1219                  args['loinc'] = tuple(loinc)
    1220
    1221          cmd = u"""
    1222                  SELECT * FROM clin.v_test_results
    1223                  WHERE
    1224                          %s
    1225                  ORDER BY clin_when DESC
    1226                  LIMIT %s""" % (
    1227                          u' AND '.join(where_parts),
    1228                          no_of_results
    1229                  )
    1230          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
    1231          if len(rows) == 0:
    1232                  return None
    1233
    1234          if no_of_results == 1:
    1235                  return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
    1236
    1237          return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
    1238
    1239  #------------------------------------------------------------
    1240 -def delete_test_result(result=None):
    1241          try:
    1242                  pk = int(result)
    1243          except (TypeError, AttributeError):
    1244                  pk = result['pk_test_result']
    1245
    1246          cmd = u'DELETE FROM clin.test_result WHERE pk = %(pk)s'
    1247          gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': pk}}])
    1248
    1249  #------------------------------------------------------------
    1250 -def create_test_result(encounter=None, episode=None, type=None, intended_reviewer=None, val_num=None, val_alpha=None, unit=None):
    1251
    1252          cmd1 = u"""
    1253  insert into clin.test_result (
    1254          fk_encounter,
    1255          fk_episode,
    1256          fk_type,
    1257          fk_intended_reviewer,
    1258          val_num,
    1259          val_alpha,
    1260          val_unit
    1261  ) values (
    1262          %(enc)s,
    1263          %(epi)s,
    1264          %(type)s,
    1265          %(rev)s,
    1266          %(v_num)s,
    1267          %(v_alpha)s,
    1268          %(unit)s
    1269  )"""
    1270
    1271          cmd2 = u"""
    1272  select *
    1273  from
    1274          clin.v_test_results
    1275  where
    1276          pk_test_result = currval(pg_get_serial_sequence('clin.test_result', 'pk'))"""
    1277
    1278          args = {
    1279                  u'enc': encounter,
    1280                  u'epi': episode,
    1281                  u'type': type,
    1282                  u'rev': intended_reviewer,
    1283                  u'v_num': val_num,
    1284                  u'v_alpha': val_alpha,
    1285                  u'unit': unit
    1286          }
    1287
    1288          rows, idx = gmPG2.run_rw_queries (
    1289                  queries = [
    1290                          {'cmd': cmd1, 'args': args},
    1291                          {'cmd': cmd2}
    1292                  ],
    1293                  return_data = True,
    1294                  get_col_idx = True
    1295          )
    1296
    1297          tr = cTestResult(row = {
    1298                  'pk_field': 'pk_test_result',
    1299                  'idx': idx,
    1300                  'data': rows[0]
    1301          })
    1302
    1303          return tr
    1304
    1305  #------------------------------------------------------------
    1306 -def format_test_results(results=None, output_format=u'latex'):
    1307
    1308          _log.debug(u'formatting test results into [%s]', output_format)
    1309
    1310          if output_format == u'latex':
    1311                  return __format_test_results_latex(results = results)
    1312
    1313          msg = _('unknown test results output format [%s]') % output_format
    1314          _log.error(msg)
    1315          return msg
    1316
    1317  #------------------------------------------------------------
    1318 -def __tests2latex_minipage(results=None, width=u'1.5cm', show_time=False, show_range=True):
    1319
    1320          if len(results) == 0:
    1321                  return u'\\begin{minipage}{%s} \\end{minipage}' % width
    1322
    1323          lines = []
    1324          for t in results:
    1325
    1326                  tmp = u''
    1327
    1328                  if show_time:
    1329                          tmp += u'{\\tiny (%s)} ' % t['clin_when'].strftime('%H:%M')
    1330
    1331                  tmp += u'%.8s' % t['unified_val']
    1332
    1333                  lines.append(tmp)
    1334                  tmp = u''
    1335
    1336                  if show_range:
    1337                          has_range = (
    1338                                  t['unified_target_range'] is not None
    1339                                          or
    1340                                  t['unified_target_min'] is not None
    1341                                          or
    1342                                  t['unified_target_max'] is not None
    1343                          )
    1344                          if has_range:
    1345                                  if t['unified_target_range'] is not None:
    1346                                          tmp += u'{\\tiny %s}' % t['unified_target_range']
    1347                                  else:
    1348                                          tmp += u'{\\tiny %s}' % (
    1349                                                  gmTools.coalesce(t['unified_target_min'], u'- ', u'%s - '),
    1350                                                  gmTools.coalesce(t['unified_target_max'], u'', u'%s')
    1351                                          )
    1352                                  lines.append(tmp)
    1353
    1354          return u'\\begin{minipage}{%s} \\begin{flushright} %s \\end{flushright} \\end{minipage}' % (width, u' \\\\ '.join(lines))
    1355
    1356  #------------------------------------------------------------
    1357 -def __tests2latex_cell(results=None, show_time=False, show_range=True):
    1358
    1359          if len(results) == 0:
    1360                  return u''
    1361
    1362          lines = []
    1363          for t in results:
    1364
    1365                  tmp = u''
    1366
    1367                  if show_time:
    1368                          tmp += u'\\tiny %s ' % t['clin_when'].strftime('%H:%M')
    1369
    1370                  tmp += u'\\normalsize %.8s' % t['unified_val']
    1371
    1372                  lines.append(tmp)
    1373                  tmp = u'\\tiny %s' % gmTools.coalesce(t['val_unit'], u'', u'%s ')
    1374
    1375                  if not show_range:
    1376                          lines.append(tmp)
    1377                          continue
    1378
    1379                  has_range = (
    1380                          t['unified_target_range'] is not None
    1381                                  or
    1382                          t['unified_target_min'] is not None
    1383                                  or
    1384                          t['unified_target_max'] is not None
    1385                  )
    1386
    1387                  if not has_range:
    1388                          lines.append(tmp)
    1389                          continue
    1390
    1391                  if t['unified_target_range'] is not None:
    1392                          tmp += u'[%s]' % t['unified_target_range']
    1393                  else:
    1394                          tmp += u'[%s%s]' % (
    1395                                  gmTools.coalesce(t['unified_target_min'], u'--', u'%s--'),
    1396                                  gmTools.coalesce(t['unified_target_max'], u'', u'%s')
    1397                          )
    1398                  lines.append(tmp)
    1399
    1400          return u' \\\\ '.join(lines)
    1401
    1402  #------------------------------------------------------------
    1403 -def __format_test_results_latex(results=None):
    1404
    1405          if len(results) == 0:
    1406                  return u'\\noindent %s' % _('No test results to format.')
    1407
    1408          # discover the columns and rows
    1409          dates = {}
    1410          tests = {}
    1411          grid = {}
    1412          for result in results:
    1413  #               row_label = u'%s \\ \\tiny (%s)}' % (result['unified_abbrev'], result['unified_name'])
    1414                  row_label = result['unified_abbrev']
    1415                  tests[row_label] = None
    1416                  col_label = u'{\\scriptsize %s}' % result['clin_when'].strftime('%Y-%m-%d')
    1417                  dates[col_label] = None
    1418                  try:
    1419                          grid[row_label]
    1420                  except KeyError:
    1421                          grid[row_label] = {}
    1422                  try:
    1423                          grid[row_label][col_label].append(result)
    1424                  except KeyError:
    1425                          grid[row_label][col_label] = [result]
    1426
    1427          col_labels = sorted(dates.keys(), reverse = True)
    1428          del dates
    1429          row_labels = sorted(tests.keys())
    1430          del tests
    1431
    1432          col_def = len(col_labels) * u'>{\\raggedleft}p{1.7cm}|'
    1433
    1434          # format them
    1435          tex = u"""\\noindent %s
    1436
    1437  \\noindent \\begin{tabular}{|l|%s}
    1438  \\hline
    1439   & %s \\tabularnewline
    1440  \\hline
    1441
    1442  %%s \\tabularnewline
    1443
    1444  \\hline
    1445
    1446  \\end{tabular}""" % (
    1447                  _('Test results'),
    1448                  col_def,
    1449                  u' & '.join(col_labels)
    1450          )
    1451
    1452          rows = []
    1453
    1454          # loop over rows
    1455          for rl in row_labels:
    1456                  cells = [rl]
    1457                  # loop over cols per row
    1458                  for cl in col_labels:
    1459                          try:
    1460                                  # get tests for this (row/col) position
    1461                                  tests = grid[rl][cl]
    1462                          except KeyError:
    1463                                  # none there, so insert empty cell
    1464                                  cells.append(u' ')
    1465                                  continue
    1466
    1467                          cells.append (
    1468                                  __tests2latex_cell (
    1469                                          results = tests,
    1470                                          show_time = (len(tests) > 1),
    1471                                          show_range = True
    1472                                  )
    1473                          )
    1474
    1475                  rows.append(u' & '.join(cells))
    1476
    1477          return tex % u' \\tabularnewline\n \\hline\n'.join(rows)
    1478
    1479  #============================================================
    1480 -def export_results_for_gnuplot(results=None, filename=None, show_year=True):
    1481
    1482          if filename is None:
    1483                  filename = gmTools.get_unique_filename(prefix = u'gm2gpl-', suffix = '.dat')
    1484
    1485          # sort results into series by test type
    1486          series = {}
    1487          for r in results:
    1488                  try:
    1489                          series[r['unified_name']].append(r)
    1490                  except KeyError:
    1491                          series[r['unified_name']] = [r]
    1492
    1493          gp_data = codecs.open(filename, 'wb', 'utf8')
    1494
    1495          gp_data.write(u'# %s\n' % _('GNUmed test results export for Gnuplot plotting'))
    1496          gp_data.write(u'# -------------------------------------------------------------\n')
    1497          gp_data.write(u'# first line of index: test type abbreviation & name\n')
    1498          gp_data.write(u'#\n')
    1499          gp_data.write(u'# clin_when at full precision\n')
    1500          gp_data.write(u'# value\n')
    1501          gp_data.write(u'# unit\n')
    1502          gp_data.write(u'# unified (target or normal) range: lower bound\n')
    1503          gp_data.write(u'# unified (target or normal) range: upper bound\n')
    1504          gp_data.write(u'# normal range: lower bound\n')
    1505          gp_data.write(u'# normal range: upper bound\n')
    1506          gp_data.write(u'# target range: lower bound\n')
    1507          gp_data.write(u'# target range: upper bound\n')
    1508          gp_data.write(u'# clin_when formatted into string as x-axis tic label\n')
    1509          gp_data.write(u'# -------------------------------------------------------------\n')
    1510
    1511          for test_type in series.keys():
    1512                  if len(series[test_type]) == 0:
    1513                          continue
    1514
    1515                  r = series[test_type][0]
    1516                  title = u'%s (%s)' % (
    1517                          r['unified_abbrev'],
    1518                          r['unified_name']
    1519                  )
    1520                  gp_data.write(u'\n\n"%s" "%s"\n' % (title, title))
    1521
    1522                  prev_date = None
    1523                  prev_year = None
    1524                  for r in series[test_type]:
    1525                          curr_date = r['clin_when'].strftime('%Y-%m-%d')
    1526                          if curr_date == prev_date:
    1527                                  gp_data.write(u'\n# %s\n' % _('blank line inserted to allow for discontinued line drawing for same-day values'))
    1528                          if show_year:
    1529                                  if r['clin_when'].year == prev_year:
    1530                                          when_template = '%b %d %H:%M'
    1531                                  else:
    1532                                          when_template = '%b %d %H:%M (%Y)'
    1533                                  prev_year = r['clin_when'].year
    1534                          else:
    1535                                  when_template = '%b %d'
    1536                          gp_data.write (u'%s %s "%s" %s %s %s %s %s %s "%s"\n' % (
    1537                                  r['clin_when'].strftime('%Y-%m-%d_%H:%M'),
    1538                                  r['unified_val'],
    1539                                  gmTools.coalesce(r['val_unit'], u'"<?>"'),
    1540                                  gmTools.coalesce(r['unified_target_min'], u'"<?>"'),
    1541                                  gmTools.coalesce(r['unified_target_max'], u'"<?>"'),
    1542                                  gmTools.coalesce(r['val_normal_min'], u'"<?>"'),
    1543                                  gmTools.coalesce(r['val_normal_max'], u'"<?>"'),
    1544                                  gmTools.coalesce(r['val_target_min'], u'"<?>"'),
    1545                                  gmTools.coalesce(r['val_target_max'], u'"<?>"'),
    1546                                  gmDateTime.pydt_strftime (
    1547                                          r['clin_when'],
    1548                                          format = when_template,
    1549                                          accuracy = gmDateTime.acc_minutes
    1550                                  )
    1551                          ))
    1552                          prev_date = curr_date
    1553
    1554          gp_data.close()
    1555
    1556          return filename
    1557
    1558  #============================================================
    1559 -class cLabResult(gmBusinessDBObject.cBusinessDBObject):
    1560          """Represents one lab result."""
    1561
    1562          _cmd_fetch_payload = """
    1563                  select *, xmin_test_result from v_results4lab_req
    1564                  where pk_result=%s"""
    1565          _cmds_lock_rows_for_update = [
    1566                  """select 1 from test_result where pk=%(pk_result)s and xmin=%(xmin_test_result)s for update"""
    1567          ]
    1568          _cmds_store_payload = [
    1569                  """update test_result set
    1570                                  clin_when = %(val_when)s,
    1571                                  narrative = %(progress_note_result)s,
    1572                                  fk_type = %(pk_test_type)s,
    1573                                  val_num = %(val_num)s::numeric,
    1574                                  val_alpha = %(val_alpha)s,
    1575                                  val_unit = %(val_unit)s,
    1576                                  val_normal_min = %(val_normal_min)s,
    1577                                  val_normal_max = %(val_normal_max)s,
    1578                                  val_normal_range = %(val_normal_range)s,
    1579                                  val_target_min = %(val_target_min)s,
    1580                                  val_target_max = %(val_target_max)s,
    1581                                  val_target_range = %(val_target_range)s,
    1582                                  abnormality_indicator = %(abnormal)s,
    1583                                  norm_ref_group = %(ref_group)s,
    1584                                  note_provider = %(note_provider)s,
    1585                                  material = %(material)s,
    1586                                  material_detail = %(material_detail)s
    1587                          where pk = %(pk_result)s""",
    1588                  """select xmin_test_result from v_results4lab_req where pk_result=%(pk_result)s"""
    1589                  ]
    1590
    1591          _updatable_fields = [
    1592                  'val_when',
    1593                  'progress_note_result',
    1594                  'val_num',
    1595                  'val_alpha',
    1596                  'val_unit',
    1597                  'val_normal_min',
    1598                  'val_normal_max',
    1599                  'val_normal_range',
    1600                  'val_target_min',
    1601                  'val_target_max',
    1602                  'val_target_range',
    1603                  'abnormal',
    1604                  'ref_group',
    1605                  'note_provider',
    1606                  'material',
    1607                  'material_detail'
    1608          ]
    1609          #--------------------------------------------------------
    1610 -        def __init__(self, aPK_obj=None, row=None):
    1611                  """Instantiate.
    1612
    1613                  aPK_obj as dict:
    1614                          - patient_id
    1615                          - when_field (see view definition)
    1616                          - when
    1617                          - test_type
    1618                          - val_num
    1619                          - val_alpha
    1620                          - unit
    1621                  """
    1622                  # instantiate from row data ?
    1623                  if aPK_obj is None:
    1624                          gmBusinessDBObject.cBusinessDBObject.__init__(self, row=row)
    1625                          return
    1626                  pk = aPK_obj
    1627                  # find PK from row data ?
    1628                  if type(aPK_obj) == types.DictType:
    1629                          # sanity checks
    1630                          if None in [aPK_obj['patient_id'], aPK_obj['when'], aPK_obj['when_field'], aPK_obj['test_type'], aPK_obj['unit']]:
    1631                                  raise gmExceptions.ConstructorError, 'parameter error: %s' % aPK_obj
    1632                          if (aPK_obj['val_num'] is None) and (aPK_obj['val_alpha'] is None):
    1633                                  raise gmExceptions.ConstructorError, 'parameter error: val_num and val_alpha cannot both be None'
    1634                          # get PK
    1635                          where_snippets = [
    1636                                  'pk_patient=%(patient_id)s',
    1637                                  'pk_test_type=%(test_type)s',
    1638                                  '%s=%%(when)s' % aPK_obj['when_field'],
    1639                                  'val_unit=%(unit)s'
    1640                          ]
    1641                          if aPK_obj['val_num'] is not None:
    1642                                  where_snippets.append('val_num=%(val_num)s::numeric')
    1643                          if aPK_obj['val_alpha'] is not None:
    1644                                  where_snippets.append('val_alpha=%(val_alpha)s')
    1645
    1646                          where_clause = ' and '.join(where_snippets)
    1647                          cmd = "select pk_result from v_results4lab_req where %s" % where_clause
    1648                          data = gmPG.run_ro_query('historica', cmd, None, aPK_obj)
    1649                          if data is None:
    1650                                  raise gmExceptions.ConstructorError, 'error getting lab result for: %s' % aPK_obj
    1651                          if len(data) == 0:
    1652                                  raise gmExceptions.NoSuchClinItemError, 'no lab result for: %s' % aPK_obj
    1653                          pk = data[0][0]
    1654                  # instantiate class
    1655                  gmBusinessDBObject.cBusinessDBObject.__init__(self, aPK_obj=pk)
    1656          #--------------------------------------------------------
    1657 -        def get_patient(self):
    1658                  cmd = """
    1659                          select
    1660                                  %s,
    1661                                  vbp.title,
    1662                                  vbp.firstnames,
    1663                                  vbp.lastnames,
    1664                                  vbp.dob
    1665                          from v_basic_person vbp
    1666                          where vbp.pk_identity=%%s""" % self._payload[self._idx['pk_patient']]
    1667                  pat = gmPG.run_ro_query('historica', cmd, None, self._payload[self._idx['pk_patient']])
    1668                  return pat[0]
    1669  #============================================================
    1670 -class cLabRequest(gmBusinessDBObject.cBusinessDBObject):
    1671          """Represents one lab request."""
    1672
    1673          _cmd_fetch_payload = """
    1674                  select *, xmin_lab_request from v_lab_requests
    1675                  where pk_request=%s"""
    1676          _cmds_lock_rows_for_update = [
    1677                  """select 1 from lab_request where pk=%(pk_request)s and xmin=%(xmin_lab_request)s for update"""
    1678          ]
    1679          _cmds_store_payload = [
    1680                  """update lab_request set
    1681                                  request_id=%(request_id)s,
    1682                                  lab_request_id=%(lab_request_id)s,
    1683                                  clin_when=%(sampled_when)s,
    1684                                  lab_rxd_when=%(lab_rxd_when)s,
    1685                                  results_reported_when=%(results_reported_when)s,
    1686                                  request_status=%(request_status)s,
    1687                                  is_pending=%(is_pending)s::bool,
    1688                                  narrative=%(progress_note)s
    1689                          where pk=%(pk_request)s""",
    1690                  """select xmin_lab_request from v_lab_requests where pk_request=%(pk_request)s"""
    1691          ]
    1692          _updatable_fields = [
    1693                  'request_id',
    1694                  'lab_request_id',
    1695                  'sampled_when',
    1696                  'lab_rxd_when',
    1697                  'results_reported_when',
    1698                  'request_status',
    1699                  'is_pending',
    1700                  'progress_note'
    1701          ]
    1702          #--------------------------------------------------------
    1703 -        def __init__(self, aPK_obj=None, row=None):
    1704                  """Instantiate lab request.
    1705
    1706                  The aPK_obj can be either a dict with the keys "req_id"
    1707                  and "lab" or a simple primary key.
    1708                  """
    1709                  # instantiate from row data ?
    1710                  if aPK_obj is None:
    1711                          gmBusinessDBObject.cBusinessDBObject.__init__(self, row=row)
    1712                          return
    1713                  pk = aPK_obj
    1714                  # instantiate from "req_id" and "lab" ?
    1715                  if type(aPK_obj) == types.DictType:
    1716                          # sanity check
    1717                          try:
    1718                                  aPK_obj['req_id']
    1719                                  aPK_obj['lab']
    1720                          except:
    1721                                  _log.exception('[%s:??]: faulty <aPK_obj> structure: [%s]' % (self.__class__.__name__, aPK_obj), sys.exc_info())
    1722                                  raise gmExceptions.ConstructorError, '[%s:??]: cannot derive PK from [%s]' % (self.__class__.__name__, aPK_obj)
    1723                          # generate query
    1724                          where_snippets = []
    1725                          vals = {}
    1726                          where_snippets.append('request_id=%(req_id)s')
    1727                          if type(aPK_obj['lab']) == types.IntType:
    1728                                  where_snippets.append('pk_test_org=%(lab)s')
    1729                          else:
    1730                                  where_snippets.append('lab_name=%(lab)s')
    1731                          where_clause = ' and '.join(where_snippets)
    1732                          cmd = "select pk_request from v_lab_requests where %s" % where_clause
    1733                          # get pk
    1734                          data = gmPG.run_ro_query('historica', cmd, None, aPK_obj)
    1735                          if data is None:
    1736                                  raise gmExceptions.ConstructorError, '[%s:??]: error getting lab request for [%s]' % (self.__class__.__name__, aPK_obj)
    1737                          if len(data) == 0:
    1738                                  raise gmExceptions.NoSuchClinItemError, '[%s:??]: no lab request for [%s]' % (self.__class__.__name__, aPK_obj)
    1739                          pk = data[0][0]
    1740                  # instantiate class
    1741                  gmBusinessDBObject.cBusinessDBObject.__init__(self, aPK_obj=pk)
    1742          #--------------------------------------------------------
    1743 -        def get_patient(self):
    1744                  cmd = """
    1745                          select vpi.pk_patient, vbp.title, vbp.firstnames, vbp.lastnames, vbp.dob
    1746                          from v_pat_items vpi, v_basic_person vbp
    1747                          where
    1748                                  vpi.pk_item=%s
    1749                                          and
    1750                                  vbp.pk_identity=vpi.pk_patient"""
    1751                  pat = gmPG.run_ro_query('historica', cmd, None, self._payload[self._idx['pk_item']])
    1752                  if pat is None:
    1753                          _log.error('cannot get patient for lab request [%s]' % self._payload[self._idx['pk_item']])
    1754                          return None
    1755                  if len(pat) == 0:
    1756                          _log.error('no patient associated with lab request [%s]' % self._payload[self._idx['pk_item']])
    1757                          return None
    1758                  return pat[0]
    1759  #============================================================
    1760  # convenience functions
    1761  #------------------------------------------------------------
    1762 -def create_lab_request(lab=None, req_id=None, pat_id=None, encounter_id=None, episode_id=None):
    1763          """Create or get lab request.
    1764
    1765                  returns tuple (status, value):
    1766                          (True, lab request instance)
    1767                          (False, error message)
    1768                          (None, housekeeping_todo primary key)
    1769          """
    1770          req = None
    1771          aPK_obj = {
    1772                  'lab': lab,
    1773                  'req_id': req_id
    1774          }
    1775          try:
    1776                  req = cLabRequest (aPK_obj)
    1777          except gmExceptions.NoSuchClinItemError, msg:
    1778                  _log.info('%s: will try to create lab request' % str(msg))
    1779          except gmExceptions.ConstructorError, msg:
    1780                  _log.exception(str(msg), sys.exc_info(), verbose=0)
    1781                  return (False, msg)
    1782          # found
    1783          if req is not None:
    1784                  db_pat = req.get_patient()
    1785                  if db_pat is None:
    1786                          _log.error('cannot cross-check patient on lab request')
    1787                          return (None, '')
    1788                  # yes but ambigous
    1789                  if pat_id != db_pat[0]:
    1790                          _log.error('lab request found for [%s:%s] but patient mismatch: expected [%s], in DB [%s]' % (lab, req_id, pat_id, db_pat))
    1791                          me = '$RCSfile: gmPathLab.py,v $ $Revision: 1.81 $'
    1792                          to = 'user'
    1793                          prob = _('The lab request already exists but belongs to a different patient.')
    1794                          sol = _('Verify which patient this lab request really belongs to.')
    1795                          ctxt = _('lab [%s], request ID [%s], expected link with patient [%s], currently linked to patient [%s]') % (lab, req_id, pat_id, db_pat)
    1796                          cat = 'lab'
    1797                          status, data = gmPG.add_housekeeping_todo(me, to, prob, sol, ctxt, cat)
    1798                          return (None, data)
    1799                  return (True, req)
    1800          # not found
    1801          queries = []
    1802          if type(lab) is types.IntType:
    1803                  cmd = "insert into lab_request (fk_encounter, fk_episode, fk_test_org, request_id) values (%s, %s, %s, %s)"
    1804          else:
    1805                  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)"
    1806          queries.append((cmd, [encounter_id, episode_id, str(lab), req_id]))
    1807          cmd = "select currval('lab_request_pk_seq')"
    1808          queries.append((cmd, []))
    1809          # insert new
    1810          result, err = gmPG.run_commit('historica', queries, True)
    1811          if result is None:
    1812                  return (False, err)
    1813          try:
    1814                  req = cLabRequest(aPK_obj=result[0][0])
    1815          except gmExceptions.ConstructorError, msg:
    1816                  _log.exception(str(msg), sys.exc_info(), verbose=0)
    1817                  return (False, msg)
    1818          return (True, req)
    1819  #------------------------------------------------------------
    1820 -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):
    1821          tres = None
    1822          data = {
    1823                  'patient_id': patient_id,
    1824                  'when_field': when_field,
    1825                  'when': when,
    1826                  'test_type': test_type,
    1827                  'val_num': val_num,
    1828                  'val_alpha': val_alpha,
    1829                  'unit': unit
    1830          }
    1831          try:
    1832                  tres = cLabResult(aPK_obj=data)
    1833                  # exists already, so fail
    1834                  _log.error('will not overwrite existing test result')
    1835                  _log.debug(str(tres))
    1836                  return (None, tres)
    1837          except gmExceptions.NoSuchClinItemError:
    1838                  _log.debug('test result not found - as expected, will create it')
    1839          except gmExceptions.ConstructorError, msg:
    1840                  _log.exception(str(msg), sys.exc_info(), verbose=0)
    1841                  return (False, msg)
    1842          if request is None:
    1843                  return (False, _('need lab request when inserting lab result'))
    1844          # not found
    1845          if encounter_id is None:
    1846                  encounter_id = request['pk_encounter']
    1847          queries = []
    1848          cmd = "insert into test_result (fk_encounter, fk_episode, fk_type, val_num, val_alpha, val_unit) values (%s, %s, %s, %s, %s, %s)"
    1849          queries.append((cmd, [encounter_id, request['pk_episode'], test_type, val_num, val_alpha, unit]))
    1850          cmd = "insert into lnk_result2lab_req (fk_result, fk_request) values ((select currval('test_result_pk_seq')), %s)"
    1851          queries.append((cmd, [request['pk_request']]))
    1852          cmd = "select currval('test_result_pk_seq')"
    1853          queries.append((cmd, []))
    1854          # insert new
    1855          result, err = gmPG.run_commit('historica', queries, True)
    1856          if result is None:
    1857                  return (False, err)
    1858          try:
    1859                  tres = cLabResult(aPK_obj=result[0][0])
    1860          except gmExceptions.ConstructorError, msg:
    1861                  _log.exception(str(msg), sys.exc_info(), verbose=0)
    1862                  return (False, msg)
    1863          return (True, tres)
    1864  #------------------------------------------------------------
    1865 -def get_unreviewed_results(limit=50):
    1866          # sanity check
    1867          if limit < 1:
    1868                  limit = 1
    1869          # retrieve one more row than needed so we know there's more available ;-)
    1870          lim = limit + 1
    1871          cmd = """
    1872                  select pk_result
    1873                  from v_results4lab_req
    1874                  where reviewed is false
    1875                  order by pk_patient
    1876                  limit %s""" % lim
    1877          rows = gmPG.run_ro_query('historica', cmd)
    1878          if rows is None:
    1879                  _log.error('error retrieving unreviewed lab results')
    1880                  return (None, _('error retrieving unreviewed lab results'))
    1881          if len(rows) == 0:
    1882                  return (False, [])
    1883          # more than LIMIT rows ?
    1884          if len(rows) == lim:
    1885                  more_avail = True
    1886                  # but deliver only LIMIT rows so that our assumption holds true...
    1887                  del rows[limit]
    1888          else:
    1889                  more_avail = False
    1890          results = []
    1891          for row in rows:
    1892                  try:
    1893                          results.append(cLabResult(aPK_obj=row[0]))
    1894                  except gmExceptions.ConstructorError:
    1895                          _log.exception('skipping unreviewed lab result [%s]' % row[0], sys.exc_info(), verbose=0)
    1896          return (more_avail, results)
    1897  #------------------------------------------------------------
    1898 -def get_pending_requests(limit=250):
    1899          lim = limit + 1
    1900          cmd = "select pk from lab_request where is_pending is true limit %s" % lim
    1901          rows = gmPG.run_ro_query('historica', cmd)
    1902          if rows is None:
    1903                  _log.error('error retrieving pending lab requests')
    1904                  return (None, None)
    1905          if len(rows) == 0:
    1906                  return (False, [])
    1907          results = []
    1908          # more than LIMIT rows ?
    1909          if len(rows) == lim:
    1910                  too_many = True
    1911                  # but deliver only LIMIT rows so that our assumption holds true...
    1912                  del rows[limit]
    1913          else:
    1914                  too_many = False
    1915          requests = []
    1916          for row in rows:
    1917                  try:
    1918                          requests.append(cLabRequest(aPK_obj=row[0]))
    1919                  except gmExceptions.ConstructorError:
    1920                          _log.exception('skipping pending lab request [%s]' % row[0], sys.exc_info(), verbose=0)
    1921          return (too_many, requests)
    1922  #------------------------------------------------------------
    1923 -def get_next_request_ID(lab=None, incrementor_func=None):
    1924          """Get logically next request ID for given lab.
    1925
    1926          - incrementor_func:
    1927            - if not supplied the next ID is guessed
    1928            - if supplied it is applied to the most recently used ID
    1929          """
    1930          if type(lab) == types.IntType:
    1931                  lab_snippet = 'vlr.fk_test_org=%s'
    1932          else:
    1933                  lab_snippet = 'vlr.lab_name=%s'
    1934                  lab = str(lab)
    1935          cmd =  """
    1936                  select request_id
    1937                  from lab_request lr0
    1938                  where lr0.clin_when = (
    1939                          select max(vlr.sampled_when)
    1940                          from v_lab_requests vlr
    1941                          where %s
    1942                  )""" % lab_snippet
    1943          rows = gmPG.run_ro_query('historica', cmd, None, lab)
    1944          if rows is None:
    1945                  _log.warning('error getting most recently used request ID for lab [%s]' % lab)
    1946                  return ''
    1947          if len(rows) == 0:
    1948                  return ''
    1949          most_recent = rows[0][0]
    1950          # apply supplied incrementor
    1951          if incrementor_func is not None:
    1952                  try:
    1953                          next = incrementor_func(most_recent)
    1954                  except TypeError:
    1955                          _log.error('cannot call incrementor function [%s]' % str(incrementor_func))
    1956                          return most_recent
    1957                  return next
    1958          # try to be smart ourselves
    1959          for pos in range(len(most_recent)):
    1960                  header = most_recent[:pos]
    1961                  trailer = most_recent[pos:]
    1962                  try:
    1963                          return '%s%s' % (header, str(int(trailer) + 1))
    1964                  except ValueError:
    1965                          header = most_recent[:-1]
    1966                          trailer = most_recent[-1:]
    1967                          return '%s%s' % (header, chr(ord(trailer) + 1))
    1968  #============================================================
    1969 -def calculate_bmi(mass=None, height=None, age=None):
    1970          """Calculate BMI.
    1971
    1972          mass: kg
    1973          height: cm
    1974          age: not yet used
    1975
    1976          returns:
    1977                  (True/False, data)
    1978                  True: data = (bmi, lower_normal, upper_normal)
    1979                  False: data = error message
    1980          """
    1981          converted, mass = gmTools.input2decimal(mass)
    1982          if not converted:
    1983                  return False, u'mass: cannot convert <%s> to Decimal' % mass
    1984
    1985          converted, height = gmTools.input2decimal(height)
    1986          if not converted:
    1987                  return False, u'height: cannot convert <%s> to Decimal' % height
    1988
    1989          approx_surface = (height / decimal.Decimal(100))**2
    1990          bmi = mass / approx_surface
    1991
    1992          print mass, height, '->', approx_surface, '->', bmi
    1993
    1994          lower_normal_mass = 20.0 * approx_surface
    1995          upper_normal_mass = 25.0 * approx_surface
    1996
    1997          return True, (bmi, lower_normal_mass, upper_normal_mass)
    1998  #============================================================
    1999  # main - unit testing
    2000  #------------------------------------------------------------
    2001  if __name__ == '__main__':
    2002
    2003          if len(sys.argv) < 2:
    2004                  sys.exit()
    2005
    2006          if sys.argv[1] != 'test':
    2007                  sys.exit()
    2008
    2009          import time
    2010
    2011          gmI18N.activate_locale()
    2012          gmI18N.install_domain()
    2013
    2014          #------------------------------------------
    2015 -        def test_create_test_result():
    2016                  tr = create_test_result (
    2017                          encounter = 1,
    2018                          episode = 1,
    2019                          type = 1,
    2020                          intended_reviewer = 1,
    2021                          val_num = '12',
    2022                          val_alpha=None,
    2023                          unit = 'mg/dl'
    2024                  )
    2025                  print tr
    2026                  return tr
    2027          #------------------------------------------
    2028 -        def test_delete_test_result():
    2029                  tr = test_create_test_result()
    2030                  delete_test_result(tr)
    2031          #------------------------------------------
    2032 -        def test_result():
    2033                  r = cTestResult(aPK_obj=1)
    2034                  print r
    2035                  print r.reference_ranges
    2036          #------------------------------------------
    2037 -        def test_lab_result():
    2038                  print "test_result()"
    2039  #               lab_result = cLabResult(aPK_obj=4)
    2040                  data = {
    2041                          'patient_id': 12,
    2042                          'when_field': 'val_when',
    2043                          'when': '2000-09-17 18:23:00+02',
    2044                          'test_type': 9,
    2045                          'val_num': 17.3,
    2046                          'val_alpha': None,
    2047                          'unit': 'mg/l'
    2048                  }
    2049                  lab_result = cLabResult(aPK_obj=data)
    2050                  print lab_result
    2051                  fields = lab_result.get_fields()
    2052                  for field in fields:
    2053                          print field, ':', lab_result[field]
    2054                  print "updatable:", lab_result.get_updatable_fields()
    2055                  print time.time()
    2056                  print lab_result.get_patient()
    2057                  print time.time()
    2058          #------------------------------------------
    2059 -        def test_request():
    2060                  print "test_request()"
    2061                  try:
    2062  #                       lab_req = cLabRequest(aPK_obj=1)
    2063  #                       lab_req = cLabRequest(req_id='EML#SC937-0176-CEC#11', lab=2)
    2064                          data = {
    2065                                  'req_id': 'EML#SC937-0176-CEC#11',
    2066                                  'lab': 'Enterprise Main Lab'
    2067                          }
    2068                          lab_req = cLabRequest(aPK_obj=data)
    2069                  except gmExceptions.ConstructorError, msg:
    2070                          print "no such lab request:", msg
    2071                          return
    2072                  print lab_req
    2073                  fields = lab_req.get_fields()
    2074                  for field in fields:
    2075                          print field, ':', lab_req[field]
    2076                  print "updatable:", lab_req.get_updatable_fields()
    2077                  print time.time()
    2078                  print lab_req.get_patient()
    2079                  print time.time()
    2080          #--------------------------------------------------------
    2081 -        def test_unreviewed():
    2082                  data = get_unreviewed_results()
    2083                  for result in data:
    2084                          print result
    2085          #--------------------------------------------------------
    2086 -        def test_pending():
    2087                  data = get_pending_requests()
    2088                  for result in data:
    2089                          print result
    2090          #--------------------------------------------------------
    2091 -        def test_create_measurement_type():
    2092                  print create_measurement_type (
    2093                          lab = None,
    2094                          abbrev = u'tBZ2',
    2095                          unit = u'mg%',
    2096                          name = 'BZ (test 2)'
    2097                  )
    2098          #--------------------------------------------------------
    2099 -        def test_meta_test_type():
    2100                  mtt = cMetaTestType(aPK_obj = 1)
    2101                  print mtt
    2102                  print get_meta_test_types()
    2103          #--------------------------------------------------------
    2104 -        def test_test_type():
    2105                  tt = cMeasurementType(aPK_obj = 1)
    2106                  print tt
    2107                  print get_measurement_types()
    2108          #--------------------------------------------------------
    2109 -        def test_format_test_results():
    2110                  results = [
    2111                          cTestResult(aPK_obj=1),
    2112                          cTestResult(aPK_obj=2),
    2113                          cTestResult(aPK_obj=3)
    2114  #                       cTestResult(aPK_obj=4)
    2115                  ]
    2116                  print format_test_results(results = results)
    2117          #--------------------------------------------------------
    2118 -        def test_calculate_bmi():
    2119                  done, data = calculate_bmi(mass = sys.argv[2], height = sys.argv[3])
    2120                  bmi, low, high = data
    2121
    2122                  print "BMI:", bmi
    2123                  print "low:", low, "kg"
    2124                  print "hi :", high, "kg"
    2125          #--------------------------------------------------------
    2126 -        def test_test_panel():
    2127                  tp = cTestPanel(aPK_obj = 1)
    2128                  print tp
    2129                  print tp.format()
    2130          #--------------------------------------------------------
    2131
    2132          #test_result()
    2133          #test_create_test_result()
    2134          #test_delete_test_result()
    2135          #test_create_measurement_type()
    2136          #test_lab_result()
    2137          #test_request()
    2138          #test_create_result()
    2139          #test_unreviewed()
    2140          #test_pending()
    2141          #test_meta_test_type()
    2142          #test_test_type()
    2143          #test_format_test_results()
    2144          #test_calculate_bmi()
    2145          test_test_panel()
    2146
    2147  #============================================================
    2148