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