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 results = self.get_most_recent_results(patient = patient, max_no_of_results = 1) 942 if len(results) > 0: 943 result = results[0] 944 tt += _(' Most recent (%s): %s%s%s') % ( 945 gmDateTime.pydt_strftime(result['clin_when'], '%Y %b %d'), 946 result['unified_val'], 947 gmTools.coalesce(result['val_unit'], '', ' %s'), 948 gmTools.coalesce(result['abnormality_indicator'], '', ' (%s)') 949 ) 950 result = self.get_oldest_result(patient = patient) 951 if result is not None: 952 tt += '\n' 953 tt += _(' Oldest (%s): %s%s%s') % ( 954 gmDateTime.pydt_strftime(result['clin_when'], '%Y %b %d'), 955 result['unified_val'], 956 gmTools.coalesce(result['val_unit'], '', ' %s'), 957 gmTools.coalesce(result['abnormality_indicator'], '', ' (%s)') 958 ) 959 960 return tt
961 962 #------------------------------------------------------------
963 -def get_measurement_types(order_by=None, loincs=None, return_pks=False):
964 args = {} 965 where_parts = [] 966 if loincs is not None: 967 if len(loincs) > 0: 968 where_parts.append('loinc IN %(loincs)s') 969 args['loincs'] = tuple(loincs) 970 if len(where_parts) == 0: 971 where_parts.append('TRUE') 972 WHERE_clause = ' AND '.join(where_parts) 973 cmd = (_SQL_get_test_types % WHERE_clause) + gmTools.coalesce(order_by, '', ' ORDER BY %s') 974 #cmd = 'select * from clin.v_test_types %s' % gmTools.coalesce(order_by, '', 'order by %s') 975 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 976 if return_pks: 977 return [ r['pk_test_type'] for r in rows ] 978 return [ cMeasurementType(row = {'pk_field': 'pk_test_type', 'data': r, 'idx': idx}) for r in rows ]
979 980 #------------------------------------------------------------
981 -def find_measurement_type(lab=None, abbrev=None, name=None, link_obj=None):
982 983 if (abbrev is None) and (name is None): 984 raise ValueError('must have <abbrev> and/or <name> set') 985 986 where_snippets = [] 987 988 if lab is None: 989 where_snippets.append('pk_test_org IS NULL') 990 else: 991 try: 992 int(lab) 993 where_snippets.append('pk_test_org = %(lab)s') 994 except (TypeError, ValueError): 995 where_snippets.append('pk_test_org = (SELECT pk_test_org FROM clin.v_test_orgs WHERE unit = %(lab)s)') 996 997 if abbrev is not None: 998 where_snippets.append('abbrev = %(abbrev)s') 999 1000 if name is not None: 1001 where_snippets.append('name = %(name)s') 1002 1003 where_clause = ' and '.join(where_snippets) 1004 cmd = "select * from clin.v_test_types where %s" % where_clause 1005 args = {'lab': lab, 'abbrev': abbrev, 'name': name} 1006 1007 rows, idx = gmPG2.run_ro_queries(link_obj = link_obj, queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 1008 1009 if len(rows) == 0: 1010 return None 1011 1012 tt = cMeasurementType(row = {'pk_field': 'pk_test_type', 'data': rows[0], 'idx': idx}) 1013 return tt
1014 1015 #------------------------------------------------------------
1016 -def delete_measurement_type(measurement_type=None):
1017 cmd = 'delete from clin.test_type where pk = %(pk)s' 1018 args = {'pk': measurement_type} 1019 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
1020 1021 #------------------------------------------------------------
1022 -def create_measurement_type(lab=None, abbrev=None, unit=None, name=None, link_obj=None):
1023 """Create or get test type.""" 1024 1025 ttype = find_measurement_type(lab = lab, abbrev = abbrev, name = name, link_obj = link_obj) 1026 # found ? 1027 if ttype is not None: 1028 return ttype 1029 1030 _log.debug('creating test type [%s:%s:%s:%s]', lab, abbrev, name, unit) 1031 1032 # not found, so create it 1033 # if unit is None: 1034 # _log.error('need <unit> to create test type: %s:%s:%s:%s' % (lab, abbrev, name, unit)) 1035 # raise ValueError('need <unit> to create test type') 1036 1037 # make query 1038 cols = [] 1039 val_snippets = [] 1040 vals = {} 1041 1042 # lab 1043 if lab is None: 1044 lab = create_test_org(link_obj = link_obj)['pk_test_org'] 1045 1046 cols.append('fk_test_org') 1047 try: 1048 vals['lab'] = int(lab) 1049 val_snippets.append('%(lab)s') 1050 except Exception: 1051 vals['lab'] = lab 1052 val_snippets.append('(SELECT pk_test_org FROM clin.v_test_orgs WHERE unit = %(lab)s)') 1053 1054 # code 1055 cols.append('abbrev') 1056 val_snippets.append('%(abbrev)s') 1057 vals['abbrev'] = abbrev 1058 1059 # unit 1060 if unit is not None: 1061 cols.append('reference_unit') 1062 val_snippets.append('%(unit)s') 1063 vals['unit'] = unit 1064 1065 # name 1066 if name is not None: 1067 cols.append('name') 1068 val_snippets.append('%(name)s') 1069 vals['name'] = name 1070 1071 col_clause = ', '.join(cols) 1072 val_clause = ', '.join(val_snippets) 1073 queries = [ 1074 {'cmd': 'insert into clin.test_type (%s) values (%s)' % (col_clause, val_clause), 'args': vals}, 1075 {'cmd': "select * from clin.v_test_types where pk_test_type = currval(pg_get_serial_sequence('clin.test_type', 'pk'))"} 1076 ] 1077 rows, idx = gmPG2.run_rw_queries(link_obj = link_obj, queries = queries, get_col_idx = True, return_data = True) 1078 ttype = cMeasurementType(row = {'pk_field': 'pk_test_type', 'data': rows[0], 'idx': idx}) 1079 1080 return ttype
1081 1082 #============================================================
1083 -class cTestResult(gmBusinessDBObject.cBusinessDBObject):
1084 """Represents one test result.""" 1085 1086 _cmd_fetch_payload = "select * from clin.v_test_results where pk_test_result = %s" 1087 1088 _cmds_store_payload = [ 1089 """UPDATE clin.test_result SET 1090 clin_when = %(clin_when)s, 1091 narrative = nullif(trim(%(comment)s), ''), 1092 val_num = %(val_num)s, 1093 val_alpha = nullif(trim(%(val_alpha)s), ''), 1094 val_unit = nullif(trim(%(val_unit)s), ''), 1095 val_normal_min = %(val_normal_min)s, 1096 val_normal_max = %(val_normal_max)s, 1097 val_normal_range = nullif(trim(%(val_normal_range)s), ''), 1098 val_target_min = %(val_target_min)s, 1099 val_target_max = %(val_target_max)s, 1100 val_target_range = nullif(trim(%(val_target_range)s), ''), 1101 abnormality_indicator = nullif(trim(%(abnormality_indicator)s), ''), 1102 norm_ref_group = nullif(trim(%(norm_ref_group)s), ''), 1103 note_test_org = nullif(trim(%(note_test_org)s), ''), 1104 material = nullif(trim(%(material)s), ''), 1105 material_detail = nullif(trim(%(material_detail)s), ''), 1106 status = gm.nullify_empty_string(%(status)s), 1107 val_grouping = gm.nullify_empty_string(%(val_grouping)s), 1108 source_data = gm.nullify_empty_string(%(source_data)s), 1109 fk_intended_reviewer = %(pk_intended_reviewer)s, 1110 fk_encounter = %(pk_encounter)s, 1111 fk_episode = %(pk_episode)s, 1112 fk_type = %(pk_test_type)s, 1113 fk_request = %(pk_request)s 1114 WHERE 1115 pk = %(pk_test_result)s AND 1116 xmin = %(xmin_test_result)s 1117 RETURNING 1118 xmin AS xmin_test_result 1119 """ 1120 # , u"""select xmin_test_result from clin.v_test_results where pk_test_result = %(pk_test_result)s""" 1121 ] 1122 1123 _updatable_fields = [ 1124 'clin_when', 1125 'comment', 1126 'val_num', 1127 'val_alpha', 1128 'val_unit', 1129 'val_normal_min', 1130 'val_normal_max', 1131 'val_normal_range', 1132 'val_target_min', 1133 'val_target_max', 1134 'val_target_range', 1135 'abnormality_indicator', 1136 'norm_ref_group', 1137 'note_test_org', 1138 'material', 1139 'material_detail', 1140 'status', 1141 'val_grouping', 1142 'source_data', 1143 'pk_intended_reviewer', 1144 'pk_encounter', 1145 'pk_episode', 1146 'pk_test_type', 1147 'pk_request' 1148 ] 1149 1150 #--------------------------------------------------------
1151 - def format_concisely(self, date_format='%Y %b %d', with_notes=True):
1152 range_info = gmTools.coalesce ( 1153 self.formatted_clinical_range, 1154 self.formatted_normal_range 1155 ) 1156 review = gmTools.bool2subst ( 1157 self._payload[self._idx['reviewed']], 1158 '', 1159 ' ' + gmTools.u_writing_hand, 1160 ' ' + gmTools.u_writing_hand 1161 ) 1162 txt = '%s %s: %s%s%s%s%s%s' % ( 1163 gmDateTime.pydt_strftime ( 1164 self._payload[self._idx['clin_when']], 1165 date_format 1166 ), 1167 self._payload[self._idx['name_tt']], 1168 self._payload[self._idx['unified_val']], 1169 gmTools.coalesce(self._payload[self._idx['val_unit']], '', ' %s'), 1170 gmTools.coalesce(self._payload[self._idx['abnormality_indicator']], '', ' %s'), 1171 gmTools.coalesce(range_info, '', ' (%s)'), 1172 gmTools.coalesce(self._payload[self._idx['status']], '', ' [%s]')[:2], 1173 review 1174 ) 1175 if with_notes: 1176 txt += '\n' 1177 if self._payload[self._idx['note_test_org']] is not None: 1178 txt += ' ' + _('Lab comment: %s\n') % _('\n Lab comment: ').join(self._payload[self._idx['note_test_org']].split('\n')) 1179 if self._payload[self._idx['comment']] is not None: 1180 txt += ' ' + _('Praxis comment: %s\n') % _('\n Praxis comment: ').join(self._payload[self._idx['comment']].split('\n')) 1181 1182 return txt.strip('\n')
1183 1184 #--------------------------------------------------------
1185 - 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'):
1186 1187 # FIXME: add battery, request details 1188 1189 # header 1190 tt = _('Result from %s \n') % gmDateTime.pydt_strftime ( 1191 self._payload[self._idx['clin_when']], 1192 date_format 1193 ) 1194 1195 # basics 1196 tt += ' ' + _('Type: "%(name)s" (%(abbr)s) [#%(pk_type)s]\n') % ({ 1197 'name': self._payload[self._idx['name_tt']], 1198 'abbr': self._payload[self._idx['abbrev_tt']], 1199 'pk_type': self._payload[self._idx['pk_test_type']] 1200 }) 1201 if self.is_long_text: 1202 sso = gmTools.u_superscript_one 1203 else: 1204 sso = '' 1205 tt += ' ' + _('%(sso)sResult: %(val)s%(unit)s%(ind)s [#%(pk_result)s]\n') % ({ 1206 'sso': sso, 1207 'val': self._payload[self._idx['unified_val']], 1208 'unit': gmTools.coalesce(self._payload[self._idx['val_unit']], '', ' %s'), 1209 'ind': gmTools.coalesce(self._payload[self._idx['abnormality_indicator']], '', ' (%s)'), 1210 'pk_result': self._payload[self._idx['pk_test_result']] 1211 }) 1212 1213 if self._payload[self._idx['status']] is not None: 1214 try: 1215 stat = HL7_RESULT_STATI[self._payload[self._idx['status']]] 1216 except KeyError: 1217 stat = self._payload[self._idx['status']] 1218 tt += ' ' + _('Status: %s\n') % stat 1219 if self._payload[self._idx['val_grouping']] is not None: 1220 tt += ' ' + _('Grouping: %s\n') % self._payload[self._idx['val_grouping']] 1221 1222 if with_evaluation: 1223 norm_eval = None 1224 if self._payload[self._idx['val_num']] is not None: 1225 # 1) normal range 1226 # lowered ? 1227 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']]): 1228 try: 1229 percent = (self._payload[self._idx['val_num']] * 100) / self._payload[self._idx['val_normal_min']] 1230 except ZeroDivisionError: 1231 percent = None 1232 if percent is not None: 1233 if percent < 6: 1234 norm_eval = _('%.1f %% of the normal lower limit') % percent 1235 else: 1236 norm_eval = _('%.0f %% of the normal lower limit') % percent 1237 # raised ? 1238 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']]): 1239 try: 1240 x_times = self._payload[self._idx['val_num']] / self._payload[self._idx['val_normal_max']] 1241 except ZeroDivisionError: 1242 x_times = None 1243 if x_times is not None: 1244 if x_times < 10: 1245 norm_eval = _('%.1f times the normal upper limit') % x_times 1246 else: 1247 norm_eval = _('%.0f times the normal upper limit') % x_times 1248 if norm_eval is not None: 1249 tt += ' = %s\n' % norm_eval 1250 # #------------------------------------- 1251 # # this idea was shot down on the list 1252 # #------------------------------------- 1253 # # bandwidth of deviation 1254 # if None not in [self._payload[self._idx['val_normal_min']], self._payload[self._idx['val_normal_max']]]: 1255 # normal_width = self._payload[self._idx['val_normal_max']] - self._payload[self._idx['val_normal_min']] 1256 # deviation_from_normal_range = None 1257 # # below ? 1258 # if self._payload[self._idx['val_num']] < self._payload[self._idx['val_normal_min']]: 1259 # deviation_from_normal_range = self._payload[self._idx['val_normal_min']] - self._payload[self._idx['val_num']] 1260 # # above ? 1261 # elif self._payload[self._idx['val_num']] > self._payload[self._idx['val_normal_max']]: 1262 # deviation_from_normal_range = self._payload[self._idx['val_num']] - self._payload[self._idx['val_normal_max']] 1263 # if deviation_from_normal_range is None: 1264 # try: 1265 # times_deviation = deviation_from_normal_range / normal_width 1266 # except ZeroDivisionError: 1267 # times_deviation = None 1268 # if times_deviation is not None: 1269 # if times_deviation < 10: 1270 # tt += u' (%s)\n' % _(u'deviates by %.1f times of the normal range') % times_deviation 1271 # else: 1272 # tt += u' (%s)\n' % _(u'deviates by %.0f times of the normal range') % times_deviation 1273 # #------------------------------------- 1274 1275 # 2) clinical target range 1276 norm_eval = None 1277 # lowered ? 1278 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']]): 1279 try: 1280 percent = (self._payload[self._idx['val_num']] * 100) / self._payload[self._idx['val_target_min']] 1281 except ZeroDivisionError: 1282 percent = None 1283 if percent is not None: 1284 if percent < 6: 1285 norm_eval = _('%.1f %% of the target lower limit') % percent 1286 else: 1287 norm_eval = _('%.0f %% of the target lower limit') % percent 1288 # raised ? 1289 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']]): 1290 try: 1291 x_times = self._payload[self._idx['val_num']] / self._payload[self._idx['val_target_max']] 1292 except ZeroDivisionError: 1293 x_times = None 1294 if x_times is not None: 1295 if x_times < 10: 1296 norm_eval = _('%.1f times the target upper limit') % x_times 1297 else: 1298 norm_eval = _('%.0f times the target upper limit') % x_times 1299 if norm_eval is not None: 1300 tt += ' = %s\n' % norm_eval 1301 # #------------------------------------- 1302 # # this idea was shot down on the list 1303 # #------------------------------------- 1304 # # bandwidth of deviation 1305 # if None not in [self._payload[self._idx['val_target_min']], self._payload[self._idx['val_target_max']]]: 1306 # normal_width = self._payload[self._idx['val_target_max']] - self._payload[self._idx['val_target_min']] 1307 # deviation_from_target_range = None 1308 # # below ? 1309 # if self._payload[self._idx['val_num']] < self._payload[self._idx['val_target_min']]: 1310 # deviation_from_target_range = self._payload[self._idx['val_target_min']] - self._payload[self._idx['val_num']] 1311 # # above ? 1312 # elif self._payload[self._idx['val_num']] > self._payload[self._idx['val_target_max']]: 1313 # deviation_from_target_range = self._payload[self._idx['val_num']] - self._payload[self._idx['val_target_max']] 1314 # if deviation_from_target_range is None: 1315 # try: 1316 # times_deviation = deviation_from_target_range / normal_width 1317 # except ZeroDivisionError: 1318 # times_deviation = None 1319 # if times_deviation is not None: 1320 # if times_deviation < 10: 1321 # tt += u' (%s)\n' % _(u'deviates by %.1f times of the target range') % times_deviation 1322 # else: 1323 # tt += u' (%s)\n' % _(u'deviates by %.0f times of the target range') % times_deviation 1324 # #------------------------------------- 1325 1326 tmp = ('%s%s' % ( 1327 gmTools.coalesce(self._payload[self._idx['name_test_org']], ''), 1328 gmTools.coalesce(self._payload[self._idx['contact_test_org']], '', ' (%s)'), 1329 )).strip() 1330 if tmp != '': 1331 tt += ' ' + _('Source: %s\n') % tmp 1332 tt += '\n' 1333 if self._payload[self._idx['note_test_org']] is not None: 1334 tt += ' ' + gmTools.u_superscript_one + _('Lab comment: %s\n') % _('\n Lab comment: ').join(self._payload[self._idx['note_test_org']].split('\n')) 1335 if self._payload[self._idx['comment']] is not None: 1336 tt += ' ' + gmTools.u_superscript_one + _('Praxis comment: %s\n') % _('\n Praxis comment: ').join(self._payload[self._idx['comment']].split('\n')) 1337 1338 if with_ranges: 1339 tt += gmTools.coalesce(self.formatted_normal_range, '', ' ' + _('Standard normal range: %s\n')) 1340 tt += gmTools.coalesce(self.formatted_clinical_range, '', ' ' + _('Clinical target range: %s\n')) 1341 tt += gmTools.coalesce(self._payload[self._idx['norm_ref_group']], '', ' ' + _('Reference group: %s\n')) 1342 1343 # metadata 1344 if with_episode: 1345 tt += ' ' + _('Episode: %s\n') % self._payload[self._idx['episode']] 1346 if self._payload[self._idx['health_issue']] is not None: 1347 tt += ' ' + _('Issue: %s\n') % self._payload[self._idx['health_issue']] 1348 if self._payload[self._idx['material']] is not None: 1349 tt += ' ' + _('Material: %s\n') % self._payload[self._idx['material']] 1350 if self._payload[self._idx['material_detail']] is not None: 1351 tt += ' ' + _('Details: %s\n') % self._payload[self._idx['material_detail']] 1352 tt += '\n' 1353 1354 if with_review: 1355 if self._payload[self._idx['reviewed']]: 1356 review = gmDateTime.pydt_strftime ( 1357 self._payload[self._idx['last_reviewed']], 1358 date_format 1359 ) 1360 else: 1361 review = _('not yet') 1362 tt += _('Signed (%(sig_hand)s): %(reviewed)s\n') % ({ 1363 'sig_hand': gmTools.u_writing_hand, 1364 'reviewed': review 1365 }) 1366 tt += ' ' + _('Responsible clinician: %s\n') % gmTools.bool2subst ( 1367 self._payload[self._idx['you_are_responsible']], 1368 _('you'), 1369 self._payload[self._idx['responsible_reviewer']] 1370 ) 1371 if self._payload[self._idx['reviewed']]: 1372 tt += ' ' + _('Last reviewer: %(reviewer)s\n') % ({ 1373 'reviewer': gmTools.bool2subst ( 1374 self._payload[self._idx['review_by_you']], 1375 _('you'), 1376 gmTools.coalesce(self._payload[self._idx['last_reviewer']], '?') 1377 ) 1378 }) 1379 tt += ' ' + _(' Technically abnormal: %(abnormal)s\n') % ({ 1380 'abnormal': gmTools.bool2subst ( 1381 self._payload[self._idx['is_technically_abnormal']], 1382 _('yes'), 1383 _('no'), 1384 '?' 1385 ) 1386 }) 1387 tt += ' ' + _(' Clinically relevant: %(relevant)s\n') % ({ 1388 'relevant': gmTools.bool2subst ( 1389 self._payload[self._idx['is_clinically_relevant']], 1390 _('yes'), 1391 _('no'), 1392 '?' 1393 ) 1394 }) 1395 if self._payload[self._idx['review_comment']] is not None: 1396 tt += ' ' + _(' Comment: %s\n') % self._payload[self._idx['review_comment']].strip() 1397 tt += '\n' 1398 1399 # type 1400 if with_type_details: 1401 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']]] 1402 if has_details: 1403 tt += _('Test type details:\n') 1404 if self._payload[self._idx['comment_tt']] is not None: 1405 tt += ' ' + _('Type comment: %s\n') % _('\n Type comment:').join(self._payload[self._idx['comment_tt']].split('\n')) 1406 if self._payload[self._idx['pk_meta_test_type']] is not None: 1407 tt += ' ' + _('Aggregated (%s) under: %s (%s) [#%s]\n') % ( 1408 gmTools.u_sum, 1409 self._payload[self._idx['name_meta']], 1410 self._payload[self._idx['abbrev_meta']], 1411 self._payload[self._idx['pk_meta_test_type']] 1412 ) 1413 if self._payload[self._idx['comment_meta']] is not None: 1414 tt += ' ' + _('Group comment: %s\n') % _('\n Group comment: ').join(self._payload[self._idx['comment_meta']].split('\n')) 1415 if has_details: 1416 tt += '\n' 1417 1418 if with_source_data: 1419 if self._payload[self._idx['source_data']] is not None: 1420 tt += _('Source data:\n') 1421 tt += ' ' + self._payload[self._idx['source_data']] 1422 tt += '\n\n' 1423 1424 if with_review: 1425 tt += _('Revisions: %(row_ver)s, last %(mod_when)s by %(mod_by)s.') % ({ 1426 'row_ver': self._payload[self._idx['row_version']], 1427 'mod_when': gmDateTime.pydt_strftime(self._payload[self._idx['modified_when']],date_format), 1428 'mod_by': self._payload[self._idx['modified_by']] 1429 }) 1430 1431 return tt
1432 1433 #--------------------------------------------------------
1434 - def _get_has_normal_min_or_max(self):
1435 return ( 1436 self._payload[self._idx['val_normal_min']] is not None 1437 ) or ( 1438 self._payload[self._idx['val_normal_max']] is not None 1439 )
1440 1441 has_normal_min_or_max = property(_get_has_normal_min_or_max, lambda x:x) 1442 1443 #--------------------------------------------------------
1444 - def _get_normal_min_max(self):
1445 has_range_info = ( 1446 self._payload[self._idx['val_normal_min']] is not None 1447 ) or ( 1448 self._payload[self._idx['val_normal_max']] is not None 1449 ) 1450 if has_range_info is False: 1451 return None 1452 1453 return '%s - %s' % ( 1454 gmTools.coalesce(self._payload[self._idx['val_normal_min']], '?'), 1455 gmTools.coalesce(self._payload[self._idx['val_normal_max']], '?') 1456 )
1457 1458 normal_min_max = property(_get_normal_min_max, lambda x:x) 1459 1460 #--------------------------------------------------------
1462 has_numerical_range = ( 1463 self._payload[self._idx['val_normal_min']] is not None 1464 ) or ( 1465 self._payload[self._idx['val_normal_max']] is not None 1466 ) 1467 if has_numerical_range: 1468 numerical_range = '%s - %s' % ( 1469 gmTools.coalesce(self._payload[self._idx['val_normal_min']], '?'), 1470 gmTools.coalesce(self._payload[self._idx['val_normal_max']], '?') 1471 ) 1472 else: 1473 numerical_range = '' 1474 textual_range = gmTools.coalesce ( 1475 self._payload[self._idx['val_normal_range']], 1476 '', 1477 gmTools.bool2subst ( 1478 has_numerical_range, 1479 ' / %s', 1480 '%s' 1481 ) 1482 ) 1483 range_info = '%s%s' % (numerical_range, textual_range) 1484 if range_info == '': 1485 return None 1486 return range_info
1487 1488 formatted_normal_range = property(_get_formatted_normal_range, lambda x:x) 1489 1490 #--------------------------------------------------------
1492 return ( 1493 self._payload[self._idx['val_target_min']] is not None 1494 ) or ( 1495 self._payload[self._idx['val_target_max']] is not None 1496 )
1497 1498 has_clinical_min_or_max = property(_get_has_clinical_min_or_max, lambda x:x) 1499 1500 #--------------------------------------------------------
1501 - def _get_clinical_min_max(self):
1502 has_range_info = ( 1503 self._payload[self._idx['val_target_min']] is not None 1504 ) or ( 1505 self._payload[self._idx['val_target_max']] is not None 1506 ) 1507 if has_range_info is False: 1508 return None 1509 1510 return '%s - %s' % ( 1511 gmTools.coalesce(self._payload[self._idx['val_target_min']], '?'), 1512 gmTools.coalesce(self._payload[self._idx['val_target_max']], '?') 1513 )
1514 1515 clinical_min_max = property(_get_clinical_min_max, lambda x:x) 1516 1517 #--------------------------------------------------------
1519 has_numerical_range = ( 1520 self._payload[self._idx['val_target_min']] is not None 1521 ) or ( 1522 self._payload[self._idx['val_target_max']] is not None 1523 ) 1524 if has_numerical_range: 1525 numerical_range = '%s - %s' % ( 1526 gmTools.coalesce(self._payload[self._idx['val_target_min']], '?'), 1527 gmTools.coalesce(self._payload[self._idx['val_target_max']], '?') 1528 ) 1529 else: 1530 numerical_range = '' 1531 textual_range = gmTools.coalesce ( 1532 self._payload[self._idx['val_target_range']], 1533 '', 1534 gmTools.bool2subst ( 1535 has_numerical_range, 1536 ' / %s', 1537 '%s' 1538 ) 1539 ) 1540 range_info = '%s%s' % (numerical_range, textual_range) 1541 if range_info == '': 1542 return None 1543 return range_info
1544 1545 formatted_clinical_range = property(_get_formatted_clinical_range, lambda x:x) 1546 1547 #--------------------------------------------------------
1549 """Returns the closest test result which does have normal range information.""" 1550 if self._payload[self._idx['val_normal_min']] is not None: 1551 return self 1552 if self._payload[self._idx['val_normal_max']] is not None: 1553 return self 1554 if self._payload[self._idx['val_normal_range']] is not None: 1555 return self 1556 cmd = """ 1557 SELECT * from clin.v_test_results 1558 WHERE 1559 pk_type = %(pk_type)s 1560 AND 1561 val_unit = %(unit)s 1562 AND 1563 ( 1564 (val_normal_min IS NOT NULL) 1565 OR 1566 (val_normal_max IS NOT NULL) 1567 OR 1568 (val_normal_range IS NOT NULL) 1569 ) 1570 ORDER BY 1571 CASE 1572 WHEN clin_when > %(clin_when)s THEN clin_when - %(clin_when)s 1573 ELSE %(clin_when)s - clin_when 1574 END 1575 LIMIT 1""" 1576 args = { 1577 'pk_type': self._payload[self._idx['pk_test_type']], 1578 'unit': self._payload[self._idx['val_unit']], 1579 'clin_when': self._payload[self._idx['clin_when']] 1580 } 1581 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 1582 if len(rows) == 0: 1583 return None 1584 return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
1585 1586 temporally_closest_normal_range = property(_get_temporally_closest_normal_range, lambda x:x) 1587 1588 #--------------------------------------------------------
1589 - def _get_formatted_range(self):
1590 1591 has_normal_min_or_max = ( 1592 self._payload[self._idx['val_normal_min']] is not None 1593 ) or ( 1594 self._payload[self._idx['val_normal_max']] is not None 1595 ) 1596 if has_normal_min_or_max: 1597 normal_min_max = '%s - %s' % ( 1598 gmTools.coalesce(self._payload[self._idx['val_normal_min']], '?'), 1599 gmTools.coalesce(self._payload[self._idx['val_normal_max']], '?') 1600 ) 1601 1602 has_clinical_min_or_max = ( 1603 self._payload[self._idx['val_target_min']] is not None 1604 ) or ( 1605 self._payload[self._idx['val_target_max']] is not None 1606 ) 1607 if has_clinical_min_or_max: 1608 clinical_min_max = '%s - %s' % ( 1609 gmTools.coalesce(self._payload[self._idx['val_target_min']], '?'), 1610 gmTools.coalesce(self._payload[self._idx['val_target_max']], '?') 1611 ) 1612 1613 if has_clinical_min_or_max: 1614 return _('Target: %(clin_min_max)s%(clin_range)s') % ({ 1615 'clin_min_max': clinical_min_max, 1616 'clin_range': gmTools.coalesce ( 1617 self._payload[self._idx['val_target_range']], 1618 '', 1619 gmTools.bool2subst ( 1620 has_clinical_min_or_max, 1621 ' / %s', 1622 '%s' 1623 ) 1624 ) 1625 }) 1626 1627 if has_normal_min_or_max: 1628 return _('Norm: %(norm_min_max)s%(norm_range)s') % ({ 1629 'norm_min_max': normal_min_max, 1630 'norm_range': gmTools.coalesce ( 1631 self._payload[self._idx['val_normal_range']], 1632 '', 1633 gmTools.bool2subst ( 1634 has_normal_min_or_max, 1635 ' / %s', 1636 '%s' 1637 ) 1638 ) 1639 }) 1640 1641 if self._payload[self._idx['val_target_range']] is not None: 1642 return _('Target: %s') % self._payload[self._idx['val_target_range']], 1643 1644 if self._payload[self._idx['val_normal_range']] is not None: 1645 return _('Norm: %s') % self._payload[self._idx['val_normal_range']] 1646 1647 return None
1648 1649 formatted_range = property(_get_formatted_range, lambda x:x) 1650 1651 #--------------------------------------------------------
1652 - def _get_test_type(self):
1653 return cMeasurementType(aPK_obj = self._payload[self._idx['pk_test_type']])
1654 1655 test_type = property(_get_test_type, lambda x:x) 1656 1657 #--------------------------------------------------------
1659 # 1) the user is right (review) 1660 if self._payload[self._idx['is_technically_abnormal']] is False: 1661 return False 1662 # 2) the lab is right (result.abnormality_indicator) 1663 indicator = self._payload[self._idx['abnormality_indicator']] 1664 if indicator is not None: 1665 indicator = indicator.strip() 1666 if indicator != '': 1667 if indicator.strip('+') == '': 1668 return True 1669 if indicator.strip('-') == '': 1670 return False 1671 # 3) non-numerical value ? 1672 if self._payload[self._idx['val_num']] is None: 1673 return None 1674 # 4) the target range is right 1675 target_max = self._payload[self._idx['val_target_max']] 1676 if target_max is not None: 1677 if target_max < self._payload[self._idx['val_num']]: 1678 return True 1679 # 4) the normal range is right 1680 normal_max = self._payload[self._idx['val_normal_max']] 1681 if normal_max is not None: 1682 if normal_max < self._payload[self._idx['val_num']]: 1683 return True 1684 return None
1685 1686 is_considered_elevated = property(_get_is_considered_elevated, lambda x:x) 1687 1688 #--------------------------------------------------------
1689 - def _get_is_considered_lowered(self):
1690 # 1) the user is right (review) 1691 if self._payload[self._idx['is_technically_abnormal']] is False: 1692 return False 1693 # 2) the lab is right (result.abnormality_indicator) 1694 indicator = self._payload[self._idx['abnormality_indicator']] 1695 if indicator is not None: 1696 indicator = indicator.strip() 1697 if indicator != '': 1698 if indicator.strip('+') == '': 1699 return False 1700 if indicator.strip('-') == '': 1701 return True 1702 # 3) non-numerical value ? 1703 if self._payload[self._idx['val_num']] is None: 1704 return None 1705 # 4) the target range is right 1706 target_min = self._payload[self._idx['val_target_min']] 1707 if target_min is not None: 1708 if target_min > self._payload[self._idx['val_num']]: 1709 return True 1710 # 4) the normal range is right 1711 normal_min = self._payload[self._idx['val_normal_min']] 1712 if normal_min is not None: 1713 if normal_min > self._payload[self._idx['val_num']]: 1714 return True 1715 return None
1716 1717 is_considered_lowered = property(_get_is_considered_lowered, lambda x:x) 1718 1719 #--------------------------------------------------------
1721 if self.is_considered_lowered is True: 1722 return True 1723 if self.is_considered_elevated is True: 1724 return True 1725 if (self.is_considered_lowered is False) and (self.is_considered_elevated is False): 1726 return False 1727 return self._payload[self._idx['is_technically_abnormal']]
1728 1729 is_considered_abnormal = property(_get_is_considered_abnormal, lambda x:x) 1730 1731 #--------------------------------------------------------
1732 - def _set_reference_range(self, ref_range):
1733 """Parse reference range from string. 1734 1735 Note: does NOT save the result. 1736 """ 1737 ref_range = ref_range.strip().replace(' ', '') 1738 1739 is_range = regex.match('-{0,1}\d+[.,]{0,1}\d*--{0,1}\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE) 1740 if is_range is not None: 1741 min_val = regex.match('-{0,1}\d+[.,]{0,1}\d*-', ref_range, regex.UNICODE).group(0).rstrip('-') 1742 success, min_val = gmTools.input2decimal(min_val) 1743 max_val = (regex.search('--{0,1}\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE).group(0))[1:] 1744 success, max_val = gmTools.input2decimal(max_val) 1745 self['val_normal_min'] = min_val 1746 self['val_normal_max'] = max_val 1747 return 1748 1749 if ref_range.startswith('<'): 1750 is_range = regex.match('<\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE) 1751 if is_range is not None: 1752 max_val = ref_range[1:] 1753 success, max_val = gmTools.input2decimal(max_val) 1754 self['val_normal_min'] = 0 1755 self['val_normal_max'] = max_val 1756 return 1757 1758 if ref_range.startswith('<-'): 1759 is_range = regex.match('<-\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE) 1760 if is_range is not None: 1761 max_val = ref_range[1:] 1762 success, max_val = gmTools.input2decimal(max_val) 1763 self['val_normal_min'] = None 1764 self['val_normal_max'] = max_val 1765 return 1766 1767 if ref_range.startswith('>'): 1768 is_range = regex.match('>\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE) 1769 if is_range is not None: 1770 min_val = ref_range[1:] 1771 success, min_val = gmTools.input2decimal(min_val) 1772 self['val_normal_min'] = min_val 1773 self['val_normal_max'] = None 1774 return 1775 1776 if ref_range.startswith('>-'): 1777 is_range = regex.match('>-\d+[.,]{0,1}\d*$', ref_range, regex.UNICODE) 1778 if is_range is not None: 1779 min_val = ref_range[1:] 1780 success, min_val = gmTools.input2decimal(min_val) 1781 self['val_normal_min'] = min_val 1782 self['val_normal_max'] = 0 1783 return 1784 1785 self['val_normal_range'] = ref_range 1786 return
1787 1788 reference_range = property(lambda x:x, _set_reference_range) 1789 1790 #--------------------------------------------------------
1792 # 1) the user is right 1793 if self._payload[self._idx['is_technically_abnormal']] is False: 1794 return '' 1795 # 2) the lab is right (result.abnormality_indicator) 1796 indicator = self._payload[self._idx['abnormality_indicator']] 1797 if indicator is not None: 1798 indicator = indicator.strip() 1799 if indicator != '': 1800 return indicator 1801 # 3) non-numerical value ? then we can't know more 1802 if self._payload[self._idx['val_num']] is None: 1803 return None 1804 # 4) the target range is right 1805 target_min = self._payload[self._idx['val_target_min']] 1806 if target_min is not None: 1807 if target_min > self._payload[self._idx['val_num']]: 1808 return '-' 1809 target_max = self._payload[self._idx['val_target_max']] 1810 if target_max is not None: 1811 if target_max < self._payload[self._idx['val_num']]: 1812 return '+' 1813 # 4) the normal range is right 1814 normal_min = self._payload[self._idx['val_normal_min']] 1815 if normal_min is not None: 1816 if normal_min > self._payload[self._idx['val_num']]: 1817 return '-' 1818 normal_max = self._payload[self._idx['val_normal_max']] 1819 if normal_max is not None: 1820 if normal_max < self._payload[self._idx['val_num']]: 1821 return '+' 1822 # reviewed, abnormal, but no indicator available 1823 if self._payload[self._idx['is_technically_abnormal']] is True: 1824 return gmTools.u_plus_minus 1825 1826 return None
1827 1828 formatted_abnormality_indicator = property(_get_formatted_abnormality_indicator, lambda x:x) 1829 1830 #--------------------------------------------------------
1831 - def _get_is_long_text(self):
1832 if self._payload[self._idx['val_alpha']] is None: 1833 return False 1834 lines = gmTools.strip_empty_lines(text = self._payload[self._idx['val_alpha']], eol = '\n', return_list = True) 1835 if len(lines) > 4: 1836 return True 1837 return False
1838 1839 is_long_text = property(_get_is_long_text, lambda x:x) 1840 1841 #--------------------------------------------------------
1843 if self._payload[self._idx['val_alpha']] is None: 1844 return None 1845 val = self._payload[self._idx['val_alpha']].lstrip() 1846 if val[0] == '<': 1847 factor = decimal.Decimal(0.5) 1848 val = val[1:] 1849 elif val[0] == '>': 1850 factor = 2 1851 val = val[1:] 1852 else: 1853 return None 1854 success, val = gmTools.input2decimal(initial = val) 1855 if not success: 1856 return None 1857 return val * factor
1858 1859 estimate_numeric_value_from_alpha = property(_get_estimate_numeric_value_from_alpha, lambda x:x) 1860 1861 #--------------------------------------------------------
1862 - def set_review(self, technically_abnormal=None, clinically_relevant=None, comment=None, make_me_responsible=False):
1863 1864 # FIXME: this is not concurrency safe 1865 if self._payload[self._idx['reviewed']]: 1866 self.__change_existing_review ( 1867 technically_abnormal = technically_abnormal, 1868 clinically_relevant = clinically_relevant, 1869 comment = comment 1870 ) 1871 else: 1872 # do not sign off unreviewed results if 1873 # NOTHING AT ALL is known about them 1874 if technically_abnormal is None: 1875 if clinically_relevant is None: 1876 comment = gmTools.none_if(comment, '', strip_string = True) 1877 if comment is None: 1878 if make_me_responsible is False: 1879 return True 1880 self.__set_new_review ( 1881 technically_abnormal = technically_abnormal, 1882 clinically_relevant = clinically_relevant, 1883 comment = comment 1884 ) 1885 1886 if make_me_responsible is True: 1887 cmd = "SELECT pk FROM dem.staff WHERE db_user = current_user" 1888 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}]) 1889 self['pk_intended_reviewer'] = rows[0][0] 1890 self.save_payload() 1891 return 1892 1893 self.refetch_payload()
1894 1895 #--------------------------------------------------------
1896 - def get_adjacent_results(self, desired_earlier_results=1, desired_later_results=1, max_offset=None):
1897 1898 if desired_earlier_results < 1: 1899 raise ValueError('<desired_earlier_results> must be > 0') 1900 1901 if desired_later_results < 1: 1902 raise ValueError('<desired_later_results> must be > 0') 1903 1904 args = { 1905 'pat': self._payload[self._idx['pk_patient']], 1906 'ttyp': self._payload[self._idx['pk_test_type']], 1907 'tloinc': self._payload[self._idx['loinc_tt']], 1908 'mtyp': self._payload[self._idx['pk_meta_test_type']], 1909 'mloinc': self._payload[self._idx['loinc_meta']], 1910 'when': self._payload[self._idx['clin_when']], 1911 'offset': max_offset 1912 } 1913 WHERE = '((pk_test_type = %(ttyp)s) OR (loinc_tt = %(tloinc)s))' 1914 WHERE_meta = '((pk_meta_test_type = %(mtyp)s) OR (loinc_meta = %(mloinc)s))' 1915 if max_offset is not None: 1916 WHERE = WHERE + ' AND (clin_when BETWEEN (%(when)s - %(offset)s) AND (%(when)s + %(offset)s))' 1917 WHERE_meta = WHERE_meta + ' AND (clin_when BETWEEN (%(when)s - %(offset)s) AND (%(when)s + %(offset)s))' 1918 1919 SQL = """ 1920 SELECT * FROM clin.v_test_results 1921 WHERE 1922 pk_patient = %%(pat)s 1923 AND 1924 clin_when %s %%(when)s 1925 AND 1926 %s 1927 ORDER BY clin_when 1928 LIMIT %s""" 1929 1930 # get earlier results 1931 earlier_results = [] 1932 # by type 1933 cmd = SQL % ('<', WHERE, desired_earlier_results) 1934 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 1935 if len(rows) > 0: 1936 earlier_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]) 1937 # by meta type ? 1938 missing_results = desired_earlier_results - len(earlier_results) 1939 if missing_results > 0: 1940 cmd = SQL % ('<', WHERE_meta, missing_results) 1941 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 1942 if len(rows) > 0: 1943 earlier_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]) 1944 1945 # get later results 1946 later_results = [] 1947 # by type 1948 cmd = SQL % ('>', WHERE, desired_later_results) 1949 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 1950 if len(rows) > 0: 1951 later_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]) 1952 # by meta type ? 1953 missing_results = desired_later_results - len(later_results) 1954 if missing_results > 0: 1955 cmd = SQL % ('>', WHERE_meta, missing_results) 1956 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 1957 if len(rows) > 0: 1958 later_results.extend([ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]) 1959 1960 return earlier_results, later_results
1961 1962 #-------------------------------------------------------- 1963 # internal API 1964 #--------------------------------------------------------
1965 - def __set_new_review(self, technically_abnormal=None, clinically_relevant=None, comment=None):
1966 """Add a review to a row. 1967 1968 - if technically abnormal is not provided/None it will be set 1969 to True if the lab's indicator has a meaningful value 1970 - if clinically relevant is not provided/None it is set to 1971 whatever technically abnormal is 1972 """ 1973 if technically_abnormal is None: 1974 technically_abnormal = False 1975 if self._payload[self._idx['abnormality_indicator']] is not None: 1976 if self._payload[self._idx['abnormality_indicator']].strip() != '': 1977 technically_abnormal = True 1978 1979 if clinically_relevant is None: 1980 clinically_relevant = technically_abnormal 1981 1982 cmd = """ 1983 INSERT INTO clin.reviewed_test_results ( 1984 fk_reviewed_row, 1985 is_technically_abnormal, 1986 clinically_relevant, 1987 comment 1988 ) VALUES ( 1989 %(pk)s, 1990 %(abnormal)s, 1991 %(relevant)s, 1992 gm.nullify_empty_string(%(cmt)s) 1993 )""" 1994 args = { 1995 'pk': self._payload[self._idx['pk_test_result']], 1996 'abnormal': technically_abnormal, 1997 'relevant': clinically_relevant, 1998 'cmt': comment 1999 } 2000 2001 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
2002 2003 #--------------------------------------------------------
2004 - def __change_existing_review(self, technically_abnormal=None, clinically_relevant=None, comment=None):
2005 """Change a review on a row. 2006 2007 - if technically abnormal/clinically relevant are 2008 None they are not set 2009 """ 2010 args = { 2011 'pk_result': self._payload[self._idx['pk_test_result']], 2012 'abnormal': technically_abnormal, 2013 'relevant': clinically_relevant, 2014 'cmt': comment 2015 } 2016 2017 set_parts = [ 2018 'fk_reviewer = (SELECT pk FROM dem.staff WHERE db_user = current_user)', 2019 'comment = gm.nullify_empty_string(%(cmt)s)' 2020 ] 2021 2022 if technically_abnormal is not None: 2023 set_parts.append('is_technically_abnormal = %(abnormal)s') 2024 2025 if clinically_relevant is not None: 2026 set_parts.append('clinically_relevant = %(relevant)s') 2027 2028 cmd = """ 2029 UPDATE clin.reviewed_test_results SET 2030 %s 2031 WHERE 2032 fk_reviewed_row = %%(pk_result)s 2033 """ % ',\n '.join(set_parts) 2034 2035 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
2036 2037 #------------------------------------------------------------
2038 -def get_test_results(pk_patient=None, encounters=None, episodes=None, order_by=None, return_pks=False):
2039 2040 where_parts = [] 2041 2042 if pk_patient is not None: 2043 where_parts.append('pk_patient = %(pat)s') 2044 args = {'pat': pk_patient} 2045 2046 # if tests is not None: 2047 # where_parts.append(u'pk_test_type IN %(tests)s') 2048 # args['tests'] = tuple(tests) 2049 2050 if encounters is not None: 2051 where_parts.append('pk_encounter IN %(encs)s') 2052 args['encs'] = tuple(encounters) 2053 2054 if episodes is not None: 2055 where_parts.append('pk_episode IN %(epis)s') 2056 args['epis'] = tuple(episodes) 2057 2058 if order_by is None: 2059 order_by = '' 2060 else: 2061 order_by = 'ORDER BY %s' % order_by 2062 2063 cmd = """ 2064 SELECT * FROM clin.v_test_results 2065 WHERE %s 2066 %s 2067 """ % ( 2068 ' AND '.join(where_parts), 2069 order_by 2070 ) 2071 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 2072 if return_pks: 2073 return [ r['pk_test_result'] for r in rows ] 2074 tests = [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ] 2075 return tests
2076 2077 #------------------------------------------------------------
2078 -def get_most_recent_results_for_panel(pk_patient=None, pk_panel=None, order_by=None, group_by_meta_type=False):
2079 2080 if order_by is None: 2081 order_by = '' 2082 else: 2083 order_by = 'ORDER BY %s' % order_by 2084 2085 args = { 2086 'pat': pk_patient, 2087 'pk_pnl': pk_panel 2088 } 2089 2090 if group_by_meta_type: 2091 # return most recent results in panel grouped by 2092 # meta test type if any, non-grouped results are 2093 # returned ungrouped :-) 2094 cmd = """ 2095 SELECT c_vtr.* 2096 FROM ( 2097 -- max(clin_when) per test_type-in-panel for patient 2098 SELECT 2099 pk_meta_test_type, 2100 MAX(clin_when) AS max_clin_when 2101 FROM clin.v_test_results 2102 WHERE 2103 pk_patient = %(pat)s 2104 AND 2105 pk_meta_test_type IS DISTINCT FROM NULL 2106 AND 2107 pk_test_type IN ( 2108 (SELECT c_vtt4tp.pk_test_type FROM clin.v_test_types4test_panel c_vtt4tp WHERE c_vtt4tp.pk_test_panel = %(pk_pnl)s) 2109 ) 2110 GROUP BY pk_meta_test_type 2111 ) AS latest_results 2112 INNER JOIN clin.v_test_results c_vtr ON 2113 c_vtr.pk_meta_test_type = latest_results.pk_meta_test_type 2114 AND 2115 c_vtr.clin_when = latest_results.max_clin_when 2116 2117 UNION ALL 2118 2119 SELECT c_vtr.* 2120 FROM ( 2121 -- max(clin_when) per test_type-in-panel for patient 2122 SELECT 2123 pk_test_type, 2124 MAX(clin_when) AS max_clin_when 2125 FROM clin.v_test_results 2126 WHERE 2127 pk_patient = %(pat)s 2128 AND 2129 pk_meta_test_type IS NULL 2130 AND 2131 pk_test_type IN ( 2132 (SELECT c_vtt4tp.pk_test_type FROM clin.v_test_types4test_panel c_vtt4tp WHERE c_vtt4tp.pk_test_panel = %(pk_pnl)s) 2133 ) 2134 GROUP BY pk_test_type 2135 ) AS latest_results 2136 INNER JOIN clin.v_test_results c_vtr ON 2137 c_vtr.pk_test_type = latest_results.pk_test_type 2138 AND 2139 c_vtr.clin_when = latest_results.max_clin_when 2140 """ 2141 else: 2142 # return most recent results in panel regardless of whether 2143 # distinct test types in this panel are grouped under the 2144 # same meta test type 2145 cmd = """ 2146 SELECT c_vtr.* 2147 FROM ( 2148 -- max(clin_when) per test_type-in-panel for patient 2149 SELECT 2150 pk_test_type, 2151 MAX(clin_when) AS max_clin_when 2152 FROM clin.v_test_results 2153 WHERE 2154 pk_patient = %(pat)s 2155 AND 2156 pk_test_type IN ( 2157 (SELECT c_vtt4tp.pk_test_type FROM clin.v_test_types4test_panel c_vtt4tp WHERE c_vtt4tp.pk_test_panel = %(pk_pnl)s) 2158 ) 2159 GROUP BY pk_test_type 2160 ) AS latest_results 2161 -- this INNER join makes certain we do not expand 2162 -- the row selection beyond the patient's rows 2163 -- which we constrained to inside the SELECT 2164 -- producing "latest_results" 2165 INNER JOIN clin.v_test_results c_vtr ON 2166 c_vtr.pk_test_type = latest_results.pk_test_type 2167 AND 2168 c_vtr.clin_when = latest_results.max_clin_when 2169 """ 2170 cmd += order_by 2171 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 2172 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2173 2174 #------------------------------------------------------------
2175 -def get_result_at_timestamp(timestamp=None, test_type=None, loinc=None, tolerance_interval=None, patient=None):
2176 2177 if None not in [test_type, loinc]: 2178 raise ValueError('either <test_type> or <loinc> must be None') 2179 2180 args = { 2181 'pat': patient, 2182 'ttyp': test_type, 2183 'loinc': loinc, 2184 'ts': timestamp, 2185 'intv': tolerance_interval 2186 } 2187 2188 where_parts = ['pk_patient = %(pat)s'] 2189 if test_type is not None: 2190 where_parts.append('pk_test_type = %(ttyp)s') # consider: pk_meta_test_type = %(pkmtt)s / self._payload[self._idx['pk_meta_test_type']] 2191 elif loinc is not None: 2192 where_parts.append('((loinc_tt IN %(loinc)s) OR (loinc_meta IN %(loinc)s))') 2193 args['loinc'] = tuple(loinc) 2194 2195 if tolerance_interval is None: 2196 where_parts.append('clin_when = %(ts)s') 2197 else: 2198 where_parts.append('clin_when between (%(ts)s - %(intv)s::interval) AND (%(ts)s + %(intv)s::interval)') 2199 2200 cmd = """ 2201 SELECT * FROM clin.v_test_results 2202 WHERE 2203 %s 2204 ORDER BY 2205 abs(extract(epoch from age(clin_when, %%(ts)s))) 2206 LIMIT 1""" % ' AND '.join(where_parts) 2207 2208 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 2209 if len(rows) == 0: 2210 return None 2211 2212 return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
2213 2214 #------------------------------------------------------------
2215 -def get_results_for_day(timestamp=None, patient=None, order_by=None):
2216 2217 args = { 2218 'pat': patient, 2219 'ts': timestamp 2220 } 2221 2222 where_parts = [ 2223 'pk_patient = %(pat)s', 2224 "date_trunc('day'::text, clin_when) = date_trunc('day'::text, %(ts)s)" 2225 ] 2226 2227 cmd = """ 2228 SELECT * FROM clin.v_test_results 2229 WHERE 2230 %s 2231 ORDER BY 2232 val_grouping, 2233 abbrev_tt, 2234 clin_when DESC 2235 """ % ' AND '.join(where_parts) 2236 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 2237 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2238 2239 #------------------------------------------------------------
2240 -def get_results_for_issue(pk_health_issue=None, order_by=None):
2241 args = {'pk_issue': pk_health_issue} 2242 where_parts = ['pk_health_issue = %(pk_issue)s'] 2243 cmd = """ 2244 SELECT * FROM clin.v_test_results 2245 WHERE %s 2246 ORDER BY 2247 val_grouping, 2248 abbrev_tt, 2249 clin_when DESC 2250 """ % ' AND '.join(where_parts) 2251 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 2252 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2253 2254 #------------------------------------------------------------
2255 -def get_results_for_episode(pk_episode=None):
2256 args = {'pk_epi': pk_episode} 2257 where_parts = ['pk_episode = %(pk_epi)s'] 2258 cmd = """ 2259 SELECT * FROM clin.v_test_results 2260 WHERE %s 2261 ORDER BY 2262 val_grouping, 2263 abbrev_tt, 2264 clin_when DESC 2265 """ % ' AND '.join(where_parts) 2266 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 2267 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2268 2269 #------------------------------------------------------------
2270 -def get_most_recent_results_in_loinc_group(loincs=None, max_no_of_results=1, patient=None, max_age=None, consider_indirect_matches=False):
2271 """Get N most recent results *among* a list of tests selected by LOINC. 2272 2273 1) get test types with LOINC (or meta type LOINC) in the group of <loincs> 2274 2) from these get the test results for <patient> within the given <max_age> 2275 3) from these return "the N=<max_no_of_results> most recent ones" or "None" 2276 2277 <loinc> must be a list or tuple or set, NOT a single string 2278 <max_age> must be a string holding a PG interval or else a pydt interval 2279 """ 2280 assert (max_no_of_results > 0), '<max_no_of_results> must be >0' 2281 2282 args = {'pat': patient, 'loincs': tuple(loincs)} 2283 if max_age is None: 2284 max_age_cond = '' 2285 else: 2286 max_age_cond = 'AND clin_when > (now() - %(max_age)s::interval)' 2287 args['max_age'] = max_age 2288 cmd = """ 2289 SELECT * FROM ( 2290 SELECT DISTINCT ON (pk_test_result) * 2291 FROM clin.v_test_results 2292 WHERE 2293 pk_patient = %%(pat)s 2294 AND 2295 unified_loinc IN %%(loincs)s 2296 %s 2297 ) AS distinct_results 2298 ORDER BY 2299 clin_when DESC 2300 LIMIT %s""" % ( 2301 max_age_cond, 2302 max_no_of_results 2303 ) 2304 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 2305 if len(rows) > 0: 2306 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ] 2307 2308 if not consider_indirect_matches: 2309 return [] 2310 2311 cmd = """ 2312 -- get the test results 2313 SELECT * FROM clin.v_test_results c_vtr 2314 WHERE 2315 -- for this <patient> 2316 pk_patient = %%(pat)s 2317 AND 2318 -- 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) 2319 unified_loinc IS NULL 2320 AND 2321 -- with these meta test types (= results for the explicit equivalance group) 2322 c_vtr.pk_meta_test_type IN ( 2323 -- get the meta test types for those types 2324 SELECT pk_meta_test_type 2325 FROM clin.v_test_types 2326 WHERE 2327 pk_meta_test_type IS NOT NULL 2328 AND 2329 (-- retrieve test types which have .LOINC in <loincs> 2330 (loinc IN %%(loincs)s) 2331 OR 2332 (loinc_meta IN %%(loincs)s) 2333 ) 2334 AND 2335 -- but no result for <patient> 2336 pk_test_type NOT IN ( 2337 select pk_test_type from clin.v_test_results WHERE pk_patient = %%(pat)s 2338 ) %s 2339 ) 2340 -- return the N most resent result 2341 ORDER BY clin_when DESC 2342 LIMIT %s 2343 """ % ( 2344 max_age_cond, 2345 max_no_of_results 2346 ) 2347 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 2348 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2349 2350 #------------------------------------------------------------
2351 -def get_most_recent_results_for_test_type(test_type=None, max_no_of_results=1, patient=None):
2352 """Get N most recent results for *one* defined test type.""" 2353 2354 assert (test_type is not None), '<test_type> must not be None' 2355 assert (max_no_of_results > 0), '<max_no_of_results> must be > 0' 2356 2357 args = {'pat': patient, 'ttyp': test_type} 2358 where_parts = ['pk_patient = %(pat)s'] 2359 where_parts.append('pk_test_type = %(ttyp)s') # ?consider: pk_meta_test_type = %(pkmtt)s / self._payload[self._idx['pk_meta_test_type']] 2360 cmd = """ 2361 SELECT * FROM clin.v_test_results 2362 WHERE 2363 %s 2364 ORDER BY clin_when DESC 2365 LIMIT %s""" % ( 2366 ' AND '.join(where_parts), 2367 max_no_of_results 2368 ) 2369 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 2370 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2371 2372 #------------------------------------------------------------ 2373 _SQL_most_recent_result_for_test_types = """ 2374 -- return the one most recent result for each of a list of test types 2375 -- without regard to whether they belong to a meta test type 2376 SELECT * FROM ( 2377 SELECT 2378 *, 2379 MAX(clin_when) OVER relevant_tests AS max_clin_when 2380 FROM 2381 clin.v_test_results 2382 WHERE 2383 %s 2384 WINDOW relevant_tests AS (PARTITION BY pk_patient, pk_test_type) 2385 ) AS windowed_tests 2386 WHERE 2387 clin_when = max_clin_when 2388 %s 2389 """ 2390 2391 _SQL_most_recent_result_for_test_types_without_meta_type = """ 2392 -- return the one most recent result for each of a list of test types 2393 -- none of which may belong to a meta test type 2394 SELECT * FROM ( 2395 SELECT 2396 *, 2397 MAX(clin_when) OVER relevant_tests AS max_clin_when 2398 FROM 2399 clin.v_test_results 2400 WHERE 2401 pk_meta_test_type IS NULL 2402 AND 2403 %s 2404 WINDOW relevant_tests AS (PARTITION BY pk_patient, pk_test_type) 2405 ) AS windowed_tests 2406 WHERE 2407 clin_when = max_clin_when 2408 """ 2409 2410 _SQL_most_recent_result_for_test_types_by_meta_type = """ 2411 -- return the one most recent result for each of a list of meta test types 2412 -- derived from a list of test types 2413 SELECT * FROM ( 2414 SELECT 2415 *, 2416 MAX(clin_when) OVER relevant_tests AS max_clin_when 2417 FROM 2418 clin.v_test_results 2419 WHERE 2420 pk_meta_test_type IS NOT NULL 2421 AND 2422 %s 2423 WINDOW relevant_tests AS (PARTITION BY pk_patient, pk_meta_test_type) 2424 ) AS windowed_tests 2425 WHERE 2426 clin_when = max_clin_when 2427 """ 2428
2429 -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):
2430 """Return the one most recent result for *each* of a list of test types. 2431 2432 If <pk_test_types> is not given, most recent results for *each* 2433 test type the patient has any results for is returned. 2434 """ 2435 where_parts = ['pk_patient = %(pat)s'] 2436 args = {'pat': pk_patient} 2437 2438 if pk_test_types is not None: 2439 where_parts.append('pk_test_type IN %(ttyps)s') 2440 args['ttyps'] = tuple(pk_test_types) 2441 2442 order_by = 'ORDER BY clin_when DESC' if order_by is None else 'ORDER BY %s' % order_by 2443 2444 if consider_meta_type: 2445 cmd = 'SELECT * FROM ((%s) UNION ALL (%s)) AS result_union %s' % ( 2446 _SQL_most_recent_result_for_test_types_without_meta_type % ' AND '.join(where_parts), 2447 _SQL_most_recent_result_for_test_types_by_meta_type % ' AND '.join(where_parts), 2448 order_by 2449 ) 2450 else: 2451 cmd = _SQL_most_recent_result_for_test_types % ( 2452 ' AND '.join(where_parts), 2453 order_by 2454 ) 2455 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 2456 if return_pks: 2457 return [ r['pk_test_result'] for r in rows ] 2458 2459 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2460 2461 #------------------------------------------------------------
2462 -def get_most_recent_results_for_patient(no_of_results=1, patient=None):
2463 """Get N most recent results for a given patient.""" 2464 2465 if no_of_results < 1: 2466 raise ValueError('<no_of_results> must be > 0') 2467 2468 args = {'pat': patient} 2469 cmd = """ 2470 SELECT * FROM clin.v_test_results 2471 WHERE 2472 pk_patient = %%(pat)s 2473 ORDER BY clin_when DESC 2474 LIMIT %s""" % no_of_results 2475 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 2476 return [ cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2477 2478 #------------------------------------------------------------
2479 -def get_oldest_result(test_type=None, loinc=None, patient=None):
2480 2481 if None not in [test_type, loinc]: 2482 raise ValueError('either <test_type> or <loinc> must be None') 2483 2484 args = { 2485 'pat': patient, 2486 'ttyp': test_type, 2487 'loinc': loinc 2488 } 2489 2490 where_parts = ['pk_patient = %(pat)s'] 2491 if test_type is not None: 2492 where_parts.append('pk_test_type = %(ttyp)s') # consider: pk_meta_test_type = %(pkmtt)s / self._payload[self._idx['pk_meta_test_type']] 2493 elif loinc is not None: 2494 where_parts.append('((loinc_tt IN %(loinc)s) OR (loinc_meta IN %(loinc)s))') 2495 args['loinc'] = tuple(loinc) 2496 2497 cmd = """ 2498 SELECT * FROM clin.v_test_results 2499 WHERE 2500 %s 2501 ORDER BY clin_when 2502 LIMIT 1""" % ' AND '.join(where_parts) 2503 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 2504 if len(rows) == 0: 2505 return None 2506 2507 return cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': rows[0]})
2508 2509 #------------------------------------------------------------
2510 -def delete_test_result(result=None):
2511 try: 2512 pk = int(result) 2513 except (TypeError, AttributeError): 2514 pk = result['pk_test_result'] 2515 2516 cmd = 'DELETE FROM clin.test_result WHERE pk = %(pk)s' 2517 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': pk}}])
2518 2519 #------------------------------------------------------------
2520 -def create_test_result(encounter=None, episode=None, type=None, intended_reviewer=None, val_num=None, val_alpha=None, unit=None, link_obj=None):
2521 2522 cmd1 = """ 2523 INSERT INTO clin.test_result ( 2524 fk_encounter, 2525 fk_episode, 2526 fk_type, 2527 fk_intended_reviewer, 2528 val_num, 2529 val_alpha, 2530 val_unit 2531 ) VALUES ( 2532 %(enc)s, 2533 %(epi)s, 2534 %(type)s, 2535 %(rev)s, 2536 %(v_num)s, 2537 %(v_alpha)s, 2538 %(unit)s 2539 ) 2540 """ 2541 cmd2 = "SELECT * from clin.v_test_results WHERE pk_test_result = currval(pg_get_serial_sequence('clin.test_result', 'pk'))" 2542 args = { 2543 'enc': encounter, 2544 'epi': episode, 2545 'type': type, 2546 'rev': intended_reviewer, 2547 'v_num': val_num, 2548 'v_alpha': val_alpha, 2549 'unit': unit 2550 } 2551 rows, idx = gmPG2.run_rw_queries ( 2552 link_obj = link_obj, 2553 queries = [ 2554 {'cmd': cmd1, 'args': args}, 2555 {'cmd': cmd2} 2556 ], 2557 return_data = True, 2558 get_col_idx = True 2559 ) 2560 tr = cTestResult(row = { 2561 'pk_field': 'pk_test_result', 2562 'idx': idx, 2563 'data': rows[0] 2564 }) 2565 return tr
2566 2567 #------------------------------------------------------------
2568 -def format_test_results(results=None, output_format='latex'):
2569 2570 _log.debug('formatting test results into [%s]', output_format) 2571 2572 if output_format == 'latex': 2573 return __format_test_results_latex(results = results) 2574 2575 msg = _('unknown test results output format [%s]') % output_format 2576 _log.error(msg) 2577 return msg
2578 2579 #------------------------------------------------------------
2580 -def __tests2latex_minipage(results=None, width='1.5cm', show_time=False, show_range=True):
2581 2582 if len(results) == 0: 2583 return '\\begin{minipage}{%s} \\end{minipage}' % width 2584 2585 lines = [] 2586 for t in results: 2587 2588 tmp = '' 2589 2590 if show_time: 2591 tmp += '{\\tiny (%s)} ' % t['clin_when'].strftime('%H:%M') 2592 2593 tmp += '%.8s' % t['unified_val'] 2594 2595 lines.append(tmp) 2596 tmp = '' 2597 2598 if show_range: 2599 has_range = ( 2600 t['unified_target_range'] is not None 2601 or 2602 t['unified_target_min'] is not None 2603 or 2604 t['unified_target_max'] is not None 2605 ) 2606 if has_range: 2607 if t['unified_target_range'] is not None: 2608 tmp += '{\\tiny %s}' % t['unified_target_range'] 2609 else: 2610 tmp += '{\\tiny %s}' % ( 2611 gmTools.coalesce(t['unified_target_min'], '- ', '%s - '), 2612 gmTools.coalesce(t['unified_target_max'], '', '%s') 2613 ) 2614 lines.append(tmp) 2615 2616 return '\\begin{minipage}{%s} \\begin{flushright} %s \\end{flushright} \\end{minipage}' % (width, ' \\\\ '.join(lines))
2617 2618 #------------------------------------------------------------
2619 -def __tests2latex_cell(results=None, show_time=False, show_range=True):
2620 2621 if len(results) == 0: 2622 return '' 2623 2624 lines = [] 2625 for t in results: 2626 2627 tmp = '' 2628 2629 if show_time: 2630 tmp += '\\tiny %s ' % t['clin_when'].strftime('%H:%M') 2631 2632 tmp += '\\normalsize %.8s' % t['unified_val'] 2633 2634 lines.append(tmp) 2635 tmp = '\\tiny %s' % gmTools.coalesce(t['val_unit'], '', '%s ') 2636 2637 if not show_range: 2638 lines.append(tmp) 2639 continue 2640 2641 has_range = ( 2642 t['unified_target_range'] is not None 2643 or 2644 t['unified_target_min'] is not None 2645 or 2646 t['unified_target_max'] is not None 2647 ) 2648 2649 if not has_range: 2650 lines.append(tmp) 2651 continue 2652 2653 if t['unified_target_range'] is not None: 2654 tmp += '[%s]' % t['unified_target_range'] 2655 else: 2656 tmp += '[%s%s]' % ( 2657 gmTools.coalesce(t['unified_target_min'], '--', '%s--'), 2658 gmTools.coalesce(t['unified_target_max'], '', '%s') 2659 ) 2660 lines.append(tmp) 2661 2662 return ' \\\\ '.join(lines)
2663 2664 #------------------------------------------------------------
2665 -def __format_test_results_latex(results=None):
2666 2667 if len(results) == 0: 2668 return '\\noindent %s' % _('No test results to format.') 2669 2670 # discover the columns and rows 2671 dates = {} 2672 tests = {} 2673 grid = {} 2674 for result in results: 2675 # row_label = u'%s \\ \\tiny (%s)}' % (result['unified_abbrev'], result['unified_name']) 2676 row_label = result['unified_abbrev'] 2677 tests[row_label] = None 2678 col_label = '{\\scriptsize %s}' % result['clin_when'].strftime('%Y-%m-%d') 2679 dates[col_label] = None 2680 try: 2681 grid[row_label] 2682 except KeyError: 2683 grid[row_label] = {} 2684 try: 2685 grid[row_label][col_label].append(result) 2686 except KeyError: 2687 grid[row_label][col_label] = [result] 2688 2689 col_labels = sorted(dates.keys(), reverse = True) 2690 del dates 2691 row_labels = sorted(tests.keys()) 2692 del tests 2693 2694 col_def = len(col_labels) * '>{\\raggedleft}p{1.7cm}|' 2695 2696 # format them 2697 tex = """\\noindent %s 2698 2699 \\noindent \\begin{tabular}{|l|%s} 2700 \\hline 2701 & %s \\tabularnewline 2702 \\hline 2703 2704 %%s \\tabularnewline 2705 2706 \\hline 2707 2708 \\end{tabular}""" % ( 2709 _('Test results'), 2710 col_def, 2711 ' & '.join(col_labels) 2712 ) 2713 2714 rows = [] 2715 2716 # loop over rows 2717 for rl in row_labels: 2718 cells = [rl] 2719 # loop over cols per row 2720 for cl in col_labels: 2721 try: 2722 # get tests for this (row/col) position 2723 tests = grid[rl][cl] 2724 except KeyError: 2725 # none there, so insert empty cell 2726 cells.append(' ') 2727 continue 2728 2729 cells.append ( 2730 __tests2latex_cell ( 2731 results = tests, 2732 show_time = (len(tests) > 1), 2733 show_range = True 2734 ) 2735 ) 2736 2737 rows.append(' & '.join(cells)) 2738 2739 return tex % ' \\tabularnewline\n \\hline\n'.join(rows)
2740 2741 #============================================================
2742 -def export_results_for_gnuplot(results=None, filename=None, show_year=True, patient=None):
2743 2744 sandbox_dir = os.path.join(gmTools.gmPaths().tmp_dir, 'gplot') 2745 if not gmTools.mkdir(sandbox_dir): 2746 sandbox_dir = gmTools.mk_sandbox_dir(prefix = 'gm2gpl-') 2747 _log.debug('sandbox directory: [%s]', sandbox_dir) 2748 if filename is None: 2749 filename = gmTools.get_unique_filename(prefix = 'gm2gpl-', suffix = '.dat', tmp_dir = sandbox_dir) 2750 2751 # sort results into series by test type 2752 series = {} 2753 for r in results: 2754 try: 2755 series[r['unified_name']].append(r) 2756 except KeyError: 2757 series[r['unified_name']] = [r] 2758 2759 conf_name = '%s.conf' % filename 2760 gplot_conf = io.open(conf_name, mode = 'wt', encoding = 'utf8') 2761 gplot_conf.write('# settings for stacked multiplot layouts:\n') 2762 sub_title = _('plotted %s (GNUmed v%s)') % ( 2763 gmDateTime.pydt_now_here().strftime('%Y %b %d %H:%M'), 2764 _cfg.get(option = 'client_version') 2765 ) 2766 if patient is None: 2767 plot_title = sub_title 2768 else: 2769 plot_title = '%s - %s\\n%s' % ( 2770 patient.get_description_gender(with_nickname = False).strip(), 2771 patient.get_formatted_dob(format = '%Y %b %d', none_string = _('unknown DOB'), honor_estimation = True), 2772 sub_title 2773 ) 2774 gplot_conf.write('multiplot_title = "%s"\n' % plot_title) 2775 gplot_conf.write('multiplot_no_of_tests = %s # number of index blocks (resp. test types)\n' % len(series)) 2776 gplot_conf.write('array multiplot_y_labels[multiplot_no_of_tests] # list for ylabels suitable for stacked multiplots\n') 2777 gplot_conf.write('\n') 2778 gplot_conf.write('# settings for individual plots, stacked or not:\n') 2779 2780 gplot_data = io.open(filename, mode = 'wt', encoding = 'utf8') 2781 gplot_data.write('# -------------------------------------------------------------\n') 2782 gplot_data.write('# GNUmed test results export for Gnuplot plotting\n') 2783 gplot_data.write('# -------------------------------------------------------------\n') 2784 gplot_data.write('# first line of each index: test type abbreviation & name,\n') 2785 gplot_data.write('# can be used as title for plots: set key ... autotitle columnheader\n') 2786 gplot_data.write('#\n') 2787 gplot_data.write('# Columns in each index:\n') 2788 gplot_data.write('# 1 - clin_when at full precision\n') 2789 gplot_data.write('# set timefmt "%Y-%m-%d_%H:%M"\n') 2790 gplot_data.write('# timecolumn(1, "%Y-%m-%d_%H:%M")\n') 2791 gplot_data.write('# 2 - value\n') 2792 gplot_data.write('# 3 - unit\n') 2793 gplot_data.write('# 4 - unified (target or normal) range: lower bound\n') 2794 gplot_data.write('# 5 - unified (target or normal) range: upper bound\n') 2795 gplot_data.write('# 6 - normal range: lower bound\n') 2796 gplot_data.write('# 7 - normal range: upper bound\n') 2797 gplot_data.write('# 8 - target range: lower bound\n') 2798 gplot_data.write('# 9 - target range: upper bound\n') 2799 gplot_data.write('# 10 - clin_when formatted into string (say, as x-axis tic label)\n') 2800 gplot_data.write('#\n') 2801 gplot_data.write('# index rows are NOT sorted by clin_when, so plotting\n') 2802 gplot_data.write('# with lined styles will make the lines go all over\n') 2803 gplot_data.write('# -------------------------------------------------------------\n') 2804 gplot_data.write('#\n') 2805 gplot_data.write('# the file <%s.conf>\n' % filename) 2806 gplot_data.write('# will contain various gnuplot settings specific to this plot,\n') 2807 gplot_data.write('# such as <ylabel>, <y2label>, <title>,\n') 2808 gplot_data.write('# there will also be settings suitable for stacked multiplots\n') 2809 gplot_data.write('# -------------------------------------------------------------\n') 2810 2811 series_keys = list(series.keys()) 2812 for test_type_idx in range(len(series_keys)): 2813 test_type = series_keys[test_type_idx] 2814 if len(series[test_type]) == 0: 2815 continue 2816 result = series[test_type][0] 2817 if test_type_idx == 0: 2818 gplot_conf.write('set title "%s" enhanced\n' % plot_title) 2819 gplot_conf.write('\n') 2820 gplot_conf.write('set ylabel "%s"\n' % result['unified_name']) 2821 elif test_type_idx == 1: 2822 gplot_conf.write('set y2label "%s"\n' % result['unified_name']) 2823 gplot_conf.write('multiplot_y_labels[%s] = "%s (%s)"\n' % (test_type_idx + 1, result['unified_name'], result['unified_abbrev'])) 2824 title = '%s (%s)' % ( 2825 result['unified_abbrev'], 2826 result['unified_name'] 2827 ) 2828 gplot_data.write('\n\n"%s" "%s"\n' % (title, title)) 2829 prev_date = None 2830 prev_year = None 2831 for result in series[test_type]: 2832 curr_date = gmDateTime.pydt_strftime(result['clin_when'], '%Y-%m-%d', 'utf8', gmDateTime.acc_days) 2833 if curr_date == prev_date: 2834 gplot_data.write('\n# %s\n' % _('blank line inserted to allow for discontinued line drawing of same-day values')) 2835 if show_year: 2836 if result['clin_when'].year == prev_year: 2837 when_template = '%b %d %H:%M' 2838 else: 2839 when_template = '%b %d %H:%M (%Y)' 2840 prev_year = result['clin_when'].year 2841 else: 2842 when_template = '%b %d' 2843 val = result['val_num'] 2844 if val is None: 2845 val = result.estimate_numeric_value_from_alpha 2846 if val is None: 2847 continue # skip distinctly non-numericable values 2848 gplot_data.write ('%s %s "%s" %s %s %s %s %s %s "%s"\n' % ( 2849 #result['clin_when'].strftime('%Y-%m-%d_%H:%M'), 2850 gmDateTime.pydt_strftime(result['clin_when'], '%Y-%m-%d_%H:%M', 'utf8', gmDateTime.acc_minutes), 2851 val, 2852 gmTools.coalesce(result['val_unit'], '"<?>"'), 2853 gmTools.coalesce(result['unified_target_min'], '"<?>"'), 2854 gmTools.coalesce(result['unified_target_max'], '"<?>"'), 2855 gmTools.coalesce(result['val_normal_min'], '"<?>"'), 2856 gmTools.coalesce(result['val_normal_max'], '"<?>"'), 2857 gmTools.coalesce(result['val_target_min'], '"<?>"'), 2858 gmTools.coalesce(result['val_target_max'], '"<?>"'), 2859 gmDateTime.pydt_strftime ( 2860 result['clin_when'], 2861 format = when_template, 2862 accuracy = gmDateTime.acc_minutes 2863 ) 2864 )) 2865 prev_date = curr_date 2866 gplot_data.close() 2867 gplot_conf.close() 2868 2869 return filename
2870 2871 #============================================================
2872 -class cLabResult(gmBusinessDBObject.cBusinessDBObject):
2873 """Represents one lab result.""" 2874 2875 _cmd_fetch_payload = """ 2876 select *, xmin_test_result from v_results4lab_req 2877 where pk_result=%s""" 2878 _cmds_lock_rows_for_update = [ 2879 """select 1 from test_result where pk=%(pk_result)s and xmin=%(xmin_test_result)s for update""" 2880 ] 2881 _cmds_store_payload = [ 2882 """update test_result set 2883 clin_when = %(val_when)s, 2884 narrative = %(progress_note_result)s, 2885 fk_type = %(pk_test_type)s, 2886 val_num = %(val_num)s::numeric, 2887 val_alpha = %(val_alpha)s, 2888 val_unit = %(val_unit)s, 2889 val_normal_min = %(val_normal_min)s, 2890 val_normal_max = %(val_normal_max)s, 2891 val_normal_range = %(val_normal_range)s, 2892 val_target_min = %(val_target_min)s, 2893 val_target_max = %(val_target_max)s, 2894 val_target_range = %(val_target_range)s, 2895 abnormality_indicator = %(abnormal)s, 2896 norm_ref_group = %(ref_group)s, 2897 note_provider = %(note_provider)s, 2898 material = %(material)s, 2899 material_detail = %(material_detail)s 2900 where pk = %(pk_result)s""", 2901 """select xmin_test_result from v_results4lab_req where pk_result=%(pk_result)s""" 2902 ] 2903 2904 _updatable_fields = [ 2905 'val_when', 2906 'progress_note_result', 2907 'val_num', 2908 'val_alpha', 2909 'val_unit', 2910 'val_normal_min', 2911 'val_normal_max', 2912 'val_normal_range', 2913 'val_target_min', 2914 'val_target_max', 2915 'val_target_range', 2916 'abnormal', 2917 'ref_group', 2918 'note_provider', 2919 'material', 2920 'material_detail' 2921 ] 2922 #--------------------------------------------------------
2923 - def __init__(self, aPK_obj=None, row=None):
2924 """Instantiate. 2925 2926 aPK_obj as dict: 2927 - patient_id 2928 - when_field (see view definition) 2929 - when 2930 - test_type 2931 - val_num 2932 - val_alpha 2933 - unit 2934 """ 2935 # instantiate from row data ? 2936 if aPK_obj is None: 2937 gmBusinessDBObject.cBusinessDBObject.__init__(self, row=row) 2938 return 2939 pk = aPK_obj 2940 # find PK from row data ? 2941 if type(aPK_obj) == dict: 2942 # sanity checks 2943 if None in [aPK_obj['patient_id'], aPK_obj['when'], aPK_obj['when_field'], aPK_obj['test_type'], aPK_obj['unit']]: 2944 raise gmExceptions.ConstructorError('parameter error: %s' % aPK_obj) 2945 if (aPK_obj['val_num'] is None) and (aPK_obj['val_alpha'] is None): 2946 raise gmExceptions.ConstructorError('parameter error: val_num and val_alpha cannot both be None') 2947 # get PK 2948 where_snippets = [ 2949 'pk_patient=%(patient_id)s', 2950 'pk_test_type=%(test_type)s', 2951 '%s=%%(when)s' % aPK_obj['when_field'], 2952 'val_unit=%(unit)s' 2953 ] 2954 if aPK_obj['val_num'] is not None: 2955 where_snippets.append('val_num=%(val_num)s::numeric') 2956 if aPK_obj['val_alpha'] is not None: 2957 where_snippets.append('val_alpha=%(val_alpha)s') 2958 2959 where_clause = ' and '.join(where_snippets) 2960 cmd = "select pk_result from v_results4lab_req where %s" % where_clause 2961 data = gmPG.run_ro_query('historica', cmd, None, aPK_obj) 2962 if data is None: 2963 raise gmExceptions.ConstructorError('error getting lab result for: %s' % aPK_obj) 2964 if len(data) == 0: 2965 raise gmExceptions.NoSuchClinItemError('no lab result for: %s' % aPK_obj) 2966 pk = data[0][0] 2967 # instantiate class 2968 gmBusinessDBObject.cBusinessDBObject.__init__(self, aPK_obj=pk)
2969 #--------------------------------------------------------
2970 - def get_patient(self):
2971 cmd = """ 2972 select 2973 %s, 2974 vbp.title, 2975 vbp.firstnames, 2976 vbp.lastnames, 2977 vbp.dob 2978 from v_active_persons vbp 2979 where vbp.pk_identity = %%s""" % self._payload[self._idx['pk_patient']] 2980 pat = gmPG.run_ro_query('historica', cmd, None, self._payload[self._idx['pk_patient']]) 2981 return pat[0]
2982 2983 #============================================================
2984 -class cLabRequest(gmBusinessDBObject.cBusinessDBObject):
2985 """Represents one lab request.""" 2986 2987 _cmd_fetch_payload = """ 2988 select *, xmin_lab_request from v_lab_requests 2989 where pk_request=%s""" 2990 _cmds_lock_rows_for_update = [ 2991 """select 1 from lab_request where pk=%(pk_request)s and xmin=%(xmin_lab_request)s for update""" 2992 ] 2993 _cmds_store_payload = [ 2994 """update lab_request set 2995 request_id=%(request_id)s, 2996 lab_request_id=%(lab_request_id)s, 2997 clin_when=%(sampled_when)s, 2998 lab_rxd_when=%(lab_rxd_when)s, 2999 results_reported_when=%(results_reported_when)s, 3000 request_status=%(request_status)s, 3001 is_pending=%(is_pending)s::bool, 3002 narrative=%(progress_note)s 3003 where pk=%(pk_request)s""", 3004 """select xmin_lab_request from v_lab_requests where pk_request=%(pk_request)s""" 3005 ] 3006 _updatable_fields = [ 3007 'request_id', 3008 'lab_request_id', 3009 'sampled_when', 3010 'lab_rxd_when', 3011 'results_reported_when', 3012 'request_status', 3013 'is_pending', 3014 'progress_note' 3015 ] 3016 #--------------------------------------------------------
3017 - def __init__(self, aPK_obj=None, row=None):
3018 """Instantiate lab request. 3019 3020 The aPK_obj can be either a dict with the keys "req_id" 3021 and "lab" or a simple primary key. 3022 """ 3023 # instantiate from row data ? 3024 if aPK_obj is None: 3025 gmBusinessDBObject.cBusinessDBObject.__init__(self, row=row) 3026 return 3027 pk = aPK_obj 3028 # instantiate from "req_id" and "lab" ? 3029 if type(aPK_obj) == dict: 3030 # sanity check 3031 try: 3032 aPK_obj['req_id'] 3033 aPK_obj['lab'] 3034 except Exception: 3035 _log.exception('[%s:??]: faulty <aPK_obj> structure: [%s]' % (self.__class__.__name__, aPK_obj), sys.exc_info()) 3036 raise gmExceptions.ConstructorError('[%s:??]: cannot derive PK from [%s]' % (self.__class__.__name__, aPK_obj)) 3037 # generate query 3038 where_snippets = [] 3039 vals = {} 3040 where_snippets.append('request_id=%(req_id)s') 3041 if type(aPK_obj['lab']) == int: 3042 where_snippets.append('pk_test_org=%(lab)s') 3043 else: 3044 where_snippets.append('lab_name=%(lab)s') 3045 where_clause = ' and '.join(where_snippets) 3046 cmd = "select pk_request from v_lab_requests where %s" % where_clause 3047 # get pk 3048 data = gmPG.run_ro_query('historica', cmd, None, aPK_obj) 3049 if data is None: 3050 raise gmExceptions.ConstructorError('[%s:??]: error getting lab request for [%s]' % (self.__class__.__name__, aPK_obj)) 3051 if len(data) == 0: 3052 raise gmExceptions.NoSuchClinItemError('[%s:??]: no lab request for [%s]' % (self.__class__.__name__, aPK_obj)) 3053 pk = data[0][0] 3054 # instantiate class 3055 gmBusinessDBObject.cBusinessDBObject.__init__(self, aPK_obj=pk)
3056 #--------------------------------------------------------
3057 - def get_patient(self):
3058 cmd = """ 3059 select vpi.pk_patient, vbp.title, vbp.firstnames, vbp.lastnames, vbp.dob 3060 from v_pat_items vpi, v_active_persons vbp 3061 where 3062 vpi.pk_item=%s 3063 and 3064 vbp.pk_identity=vpi.pk_patient""" 3065 pat = gmPG.run_ro_query('historica', cmd, None, self._payload[self._idx['pk_item']]) 3066 if pat is None: 3067 _log.error('cannot get patient for lab request [%s]' % self._payload[self._idx['pk_item']]) 3068 return None 3069 if len(pat) == 0: 3070 _log.error('no patient associated with lab request [%s]' % self._payload[self._idx['pk_item']]) 3071 return None 3072 return pat[0]
3073 3074 #============================================================ 3075 # convenience functions 3076 #------------------------------------------------------------
3077 -def create_lab_request(lab=None, req_id=None, pat_id=None, encounter_id=None, episode_id=None):
3078 """Create or get lab request. 3079 3080 returns tuple (status, value): 3081 (True, lab request instance) 3082 (False, error message) 3083 (None, housekeeping_todo primary key) 3084 """ 3085 req = None 3086 aPK_obj = { 3087 'lab': lab, 3088 'req_id': req_id 3089 } 3090 try: 3091 req = cLabRequest (aPK_obj) 3092 except gmExceptions.NoSuchClinItemError as msg: 3093 _log.info('%s: will try to create lab request' % str(msg)) 3094 except gmExceptions.ConstructorError as msg: 3095 _log.exception(str(msg), sys.exc_info(), verbose=0) 3096 return (False, msg) 3097 # found 3098 if req is not None: 3099 db_pat = req.get_patient() 3100 if db_pat is None: 3101 _log.error('cannot cross-check patient on lab request') 3102 return (None, '') 3103 # yes but ambigous 3104 if pat_id != db_pat[0]: 3105 _log.error('lab request found for [%s:%s] but patient mismatch: expected [%s], in DB [%s]' % (lab, req_id, pat_id, db_pat)) 3106 me = '$RCSfile: gmPathLab.py,v $ $Revision: 1.81 $' 3107 to = 'user' 3108 prob = _('The lab request already exists but belongs to a different patient.') 3109 sol = _('Verify which patient this lab request really belongs to.') 3110 ctxt = _('lab [%s], request ID [%s], expected link with patient [%s], currently linked to patient [%s]') % (lab, req_id, pat_id, db_pat) 3111 cat = 'lab' 3112 status, data = gmPG.add_housekeeping_todo(me, to, prob, sol, ctxt, cat) 3113 return (None, data) 3114 return (True, req) 3115 # not found 3116 queries = [] 3117 if type(lab) is int: 3118 cmd = "insert into lab_request (fk_encounter, fk_episode, fk_test_org, request_id) values (%s, %s, %s, %s)" 3119 else: 3120 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)" 3121 queries.append((cmd, [encounter_id, episode_id, str(lab), req_id])) 3122 cmd = "select currval('lab_request_pk_seq')" 3123 queries.append((cmd, [])) 3124 # insert new 3125 result, err = gmPG.run_commit('historica', queries, True) 3126 if result is None: 3127 return (False, err) 3128 try: 3129 req = cLabRequest(aPK_obj=result[0][0]) 3130 except gmExceptions.ConstructorError as msg: 3131 _log.exception(str(msg), sys.exc_info(), verbose=0) 3132 return (False, msg) 3133 return (True, req)
3134 #------------------------------------------------------------
3135 -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):
3136 tres = None 3137 data = { 3138 'patient_id': patient_id, 3139 'when_field': when_field, 3140 'when': when, 3141 'test_type': test_type, 3142 'val_num': val_num, 3143 'val_alpha': val_alpha, 3144 'unit': unit 3145 } 3146 try: 3147 tres = cLabResult(aPK_obj=data) 3148 # exists already, so fail 3149 _log.error('will not overwrite existing test result') 3150 _log.debug(str(tres)) 3151 return (None, tres) 3152 except gmExceptions.NoSuchClinItemError: 3153 _log.debug('test result not found - as expected, will create it') 3154 except gmExceptions.ConstructorError as msg: 3155 _log.exception(str(msg), sys.exc_info(), verbose=0) 3156 return (False, msg) 3157 if request is None: 3158 return (False, _('need lab request when inserting lab result')) 3159 # not found 3160 if encounter_id is None: 3161 encounter_id = request['pk_encounter'] 3162 queries = [] 3163 cmd = "insert into test_result (fk_encounter, fk_episode, fk_type, val_num, val_alpha, val_unit) values (%s, %s, %s, %s, %s, %s)" 3164 queries.append((cmd, [encounter_id, request['pk_episode'], test_type, val_num, val_alpha, unit])) 3165 cmd = "insert into lnk_result2lab_req (fk_result, fk_request) values ((select currval('test_result_pk_seq')), %s)" 3166 queries.append((cmd, [request['pk_request']])) 3167 cmd = "select currval('test_result_pk_seq')" 3168 queries.append((cmd, [])) 3169 # insert new 3170 result, err = gmPG.run_commit('historica', queries, True) 3171 if result is None: 3172 return (False, err) 3173 try: 3174 tres = cLabResult(aPK_obj=result[0][0]) 3175 except gmExceptions.ConstructorError as msg: 3176 _log.exception(str(msg), sys.exc_info(), verbose=0) 3177 return (False, msg) 3178 return (True, tres)
3179 #------------------------------------------------------------
3180 -def get_unreviewed_results(limit=50):
3181 # sanity check 3182 if limit < 1: 3183 limit = 1 3184 # retrieve one more row than needed so we know there's more available ;-) 3185 lim = limit + 1 3186 cmd = """ 3187 select pk_result 3188 from v_results4lab_req 3189 where reviewed is false 3190 order by pk_patient 3191 limit %s""" % lim 3192 rows = gmPG.run_ro_query('historica', cmd) 3193 if rows is None: 3194 _log.error('error retrieving unreviewed lab results') 3195 return (None, _('error retrieving unreviewed lab results')) 3196 if len(rows) == 0: 3197 return (False, []) 3198 # more than LIMIT rows ? 3199 if len(rows) == lim: 3200 more_avail = True 3201 # but deliver only LIMIT rows so that our assumption holds true... 3202 del rows[limit] 3203 else: 3204 more_avail = False 3205 results = [] 3206 for row in rows: 3207 try: 3208 results.append(cLabResult(aPK_obj=row[0])) 3209 except gmExceptions.ConstructorError: 3210 _log.exception('skipping unreviewed lab result [%s]' % row[0], sys.exc_info(), verbose=0) 3211 return (more_avail, results)
3212 3213 #------------------------------------------------------------
3214 -def get_pending_requests(limit=250):
3215 lim = limit + 1 3216 cmd = "select pk from lab_request where is_pending is true limit %s" % lim 3217 rows = gmPG.run_ro_query('historica', cmd) 3218 if rows is None: 3219 _log.error('error retrieving pending lab requests') 3220 return (None, None) 3221 if len(rows) == 0: 3222 return (False, []) 3223 results = [] 3224 # more than LIMIT rows ? 3225 if len(rows) == lim: 3226 too_many = True 3227 # but deliver only LIMIT rows so that our assumption holds true... 3228 del rows[limit] 3229 else: 3230 too_many = False 3231 requests = [] 3232 for row in rows: 3233 try: 3234 requests.append(cLabRequest(aPK_obj=row[0])) 3235 except gmExceptions.ConstructorError: 3236 _log.exception('skipping pending lab request [%s]' % row[0], sys.exc_info(), verbose=0) 3237 return (too_many, requests)
3238 3239 #------------------------------------------------------------
3240 -def get_next_request_ID(lab=None, incrementor_func=None):
3241 """Get logically next request ID for given lab. 3242 3243 - incrementor_func: 3244 - if not supplied the next ID is guessed 3245 - if supplied it is applied to the most recently used ID 3246 """ 3247 if type(lab) == int: 3248 lab_snippet = 'vlr.fk_test_org=%s' 3249 else: 3250 lab_snippet = 'vlr.lab_name=%s' 3251 lab = str(lab) 3252 cmd = """ 3253 select request_id 3254 from lab_request lr0 3255 where lr0.clin_when = ( 3256 select max(vlr.sampled_when) 3257 from v_lab_requests vlr 3258 where %s 3259 )""" % lab_snippet 3260 rows = gmPG.run_ro_query('historica', cmd, None, lab) 3261 if rows is None: 3262 _log.warning('error getting most recently used request ID for lab [%s]' % lab) 3263 return '' 3264 if len(rows) == 0: 3265 return '' 3266 most_recent = rows[0][0] 3267 # apply supplied incrementor 3268 if incrementor_func is not None: 3269 try: 3270 next = incrementor_func(most_recent) 3271 except TypeError: 3272 _log.error('cannot call incrementor function [%s]' % str(incrementor_func)) 3273 return most_recent 3274 return next 3275 # try to be smart ourselves 3276 for pos in range(len(most_recent)): 3277 header = most_recent[:pos] 3278 trailer = most_recent[pos:] 3279 try: 3280 return '%s%s' % (header, str(int(trailer) + 1)) 3281 except ValueError: 3282 header = most_recent[:-1] 3283 trailer = most_recent[-1:] 3284 return '%s%s' % (header, chr(ord(trailer) + 1))
3285 3286 #============================================================
3287 -def calculate_bmi(mass=None, height=None, age=None):
3288 """Calculate BMI. 3289 3290 mass: kg 3291 height: cm 3292 age: not yet used 3293 3294 returns: 3295 (True/False, data) 3296 True: data = (bmi, lower_normal, upper_normal) 3297 False: data = error message 3298 """ 3299 converted, mass = gmTools.input2decimal(mass) 3300 if not converted: 3301 return False, 'mass: cannot convert <%s> to Decimal' % mass 3302 3303 converted, height = gmTools.input2decimal(height) 3304 if not converted: 3305 return False, 'height: cannot convert <%s> to Decimal' % height 3306 3307 approx_surface = (height / decimal.Decimal(100))**2 3308 bmi = mass / approx_surface 3309 3310 print(mass, height, '->', approx_surface, '->', bmi) 3311 3312 lower_normal_mass = 20.0 * approx_surface 3313 upper_normal_mass = 25.0 * approx_surface 3314 3315 return True, (bmi, lower_normal_mass, upper_normal_mass)
3316 3317 #============================================================ 3318 # main - unit testing 3319 #------------------------------------------------------------ 3320 if __name__ == '__main__': 3321 3322 if len(sys.argv) < 2: 3323 sys.exit() 3324 3325 if sys.argv[1] != 'test': 3326 sys.exit() 3327 3328 import time 3329 3330 gmI18N.activate_locale() 3331 gmI18N.install_domain() 3332 3333 #------------------------------------------
3334 - def test_create_test_result():
3335 tr = create_test_result ( 3336 encounter = 1, 3337 episode = 1, 3338 type = 1, 3339 intended_reviewer = 1, 3340 val_num = '12', 3341 val_alpha=None, 3342 unit = 'mg/dl' 3343 ) 3344 print(tr) 3345 return tr
3346 #------------------------------------------
3347 - def test_delete_test_result():
3348 tr = test_create_test_result() 3349 delete_test_result(tr)
3350 #------------------------------------------
3351 - def test_result():
3352 r = cTestResult(aPK_obj=6) 3353 #print r 3354 #print r.reference_ranges 3355 #print r.formatted_range 3356 #print r.temporally_closest_normal_range 3357 print(r.estimate_numeric_value_from_alpha)
3358 #------------------------------------------
3359 - def test_lab_result():
3360 print("test_result()") 3361 # lab_result = cLabResult(aPK_obj=4) 3362 data = { 3363 'patient_id': 12, 3364 'when_field': 'val_when', 3365 'when': '2000-09-17 18:23:00+02', 3366 'test_type': 9, 3367 'val_num': 17.3, 3368 'val_alpha': None, 3369 'unit': 'mg/l' 3370 } 3371 lab_result = cLabResult(aPK_obj=data) 3372 print(lab_result) 3373 fields = lab_result.get_fields() 3374 for field in fields: 3375 print(field, ':', lab_result[field]) 3376 print("updatable:", lab_result.get_updatable_fields()) 3377 print(time.time()) 3378 print(lab_result.get_patient()) 3379 print(time.time())
3380 #------------------------------------------
3381 - def test_request():
3382 print("test_request()") 3383 try: 3384 # lab_req = cLabRequest(aPK_obj=1) 3385 # lab_req = cLabRequest(req_id='EML#SC937-0176-CEC#11', lab=2) 3386 data = { 3387 'req_id': 'EML#SC937-0176-CEC#11', 3388 'lab': 'Enterprise Main Lab' 3389 } 3390 lab_req = cLabRequest(aPK_obj=data) 3391 except gmExceptions.ConstructorError as msg: 3392 print("no such lab request:", msg) 3393 return 3394 print(lab_req) 3395 fields = lab_req.get_fields() 3396 for field in fields: 3397 print(field, ':', lab_req[field]) 3398 print("updatable:", lab_req.get_updatable_fields()) 3399 print(time.time()) 3400 print(lab_req.get_patient()) 3401 print(time.time())
3402 #--------------------------------------------------------
3403 - def test_unreviewed():
3404 data = get_unreviewed_results() 3405 for result in data: 3406 print(result)
3407 #--------------------------------------------------------
3408 - def test_pending():
3409 data = get_pending_requests() 3410 for result in data: 3411 print(result)
3412 #--------------------------------------------------------
3413 - def test_create_measurement_type():
3414 print(create_measurement_type ( 3415 lab = None, 3416 abbrev = 'tBZ2', 3417 unit = 'mg%', 3418 name = 'BZ (test 2)' 3419 ))
3420 #--------------------------------------------------------
3421 - def test_meta_test_type():
3422 mtt = cMetaTestType(aPK_obj = 1) 3423 print(mtt) 3424 print(get_meta_test_types())
3425 #--------------------------------------------------------
3426 - def test_test_type():
3427 tt = cMeasurementType(aPK_obj = 1) 3428 print(tt) 3429 print(get_measurement_types())
3430 #--------------------------------------------------------
3431 - def test_format_test_results():
3432 results = [ 3433 cTestResult(aPK_obj=1), 3434 cTestResult(aPK_obj=2), 3435 cTestResult(aPK_obj=3) 3436 # cTestResult(aPK_obj=4) 3437 ] 3438 print(format_test_results(results = results))
3439 #--------------------------------------------------------
3440 - def test_calculate_bmi():
3441 done, data = calculate_bmi(mass = sys.argv[2], height = sys.argv[3]) 3442 bmi, low, high = data 3443 print("BMI:", bmi) 3444 print("low:", low, "kg") 3445 print("hi :", high, "kg")
3446 3447 #--------------------------------------------------------
3448 - def test_test_panel():
3449 tp = cTestPanel(aPK_obj = 2) 3450 print(tp) 3451 print(tp.test_types) 3452 print(tp.format())
3453 3454 #--------------------------------------------------------
3455 - def test_get_most_recent_results_for_panel():
3456 tp = cTestPanel(aPK_obj = 1) 3457 #print tp.included_loincs 3458 #tp = cTestPanel(aPK_obj = 3) 3459 print(tp.format()) 3460 #most_recent = tp.get_most_recent_results(pk_patient = 12, group_by_meta_type = False) 3461 #most_recent = tp.get_most_recent_results(pk_patient = 138, group_by_meta_type = False) 3462 #print len(most_recent) 3463 most_recent = tp.get_most_recent_results(pk_patient = 12, group_by_meta_type = True, include_missing = True) 3464 #most_recent = tp.get_most_recent_results(pk_patient = 138, group_by_meta_type = True) 3465 print('found:', len(most_recent)) 3466 3467 for t in most_recent: 3468 print('--------------') 3469 if t['pk_meta_test_type'] is None: 3470 print("standalone") 3471 else: 3472 print("meta") 3473 print(t.format())
3474 3475 #--------------------------------------------------------
3476 - def test_get_most_recent_results_in_loinc_group():
3477 most_recent = get_most_recent_results_in_loinc_group ( 3478 #loincs = [u'pseudo LOINC [C-reactive protein (EML)::9] (v21->v22 test panel conversion)'], 3479 #loincs = ['8867-4'], 3480 loincs = ['2160-0', '14682-9', '40264-4', '40248-7'], 3481 max_no_of_results = 6, 3482 patient = 201, 3483 consider_indirect_matches = False 3484 ) 3485 for t in most_recent: 3486 print(t['loinc_tt'], t['loinc_meta'], t['unified_loinc']) 3487 if t['pk_meta_test_type'] is None: 3488 print("---- standalone ----") 3489 else: 3490 print("---- meta ----") 3491 print(t.format()) 3492 input('next') 3493 3494 return 3495 3496 most_recent = get_most_recent_results_in_loinc_group ( 3497 #loincs = [u'pseudo LOINC [C-reactive protein (EML)::9] (v21->v22 test panel conversion)'], 3498 #loincs = ['8867-4'], 3499 loincs = ['2160-0', '14682-9', '40264-4', '40248-7'], 3500 max_no_of_results = 2, 3501 patient = 201, 3502 consider_indirect_matches = False 3503 #consider_indirect_matches = True 3504 ) 3505 for t in most_recent: 3506 if t['pk_meta_test_type'] is None: 3507 print("---- standalone ----") 3508 else: 3509 print("---- meta ----") 3510 print(t.format())
3511 3512 #-------------------------------------------------------- 3513 3514 #test_result() 3515 #test_create_test_result() 3516 #test_delete_test_result() 3517 #test_create_measurement_type() 3518 #test_lab_result() 3519 #test_request() 3520 #test_create_result() 3521 #test_unreviewed() 3522 #test_pending() 3523 #test_meta_test_type() 3524 #test_test_type() 3525 #test_format_test_results() 3526 #test_calculate_bmi() 3527 #test_test_panel() 3528 #test_get_most_recent_results_for_panel() 3529 test_get_most_recent_results_in_loinc_group() 3530 3531 #============================================================ 3532