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

Source Code for Module Gnumed.business.gmPathLab

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