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

Source Code for Module Gnumed.business.gmPathLab

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