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

Source Code for Module Gnumed.business.gmClinicalRecord

   1  """GNUmed clinical patient record. 
   2   
   3  This is a clinical record object intended to let a useful 
   4  client-side API crystallize from actual use in true XP fashion. 
   5   
   6  Make sure to call set_func_ask_user() and set_encounter_ttl() 
   7  early on in your code (before cClinicalRecord.__init__() is 
   8  called for the first time). 
   9  """ 
  10  #============================================================ 
  11  __version__ = "$Revision: 1.308 $" 
  12  __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>" 
  13  __license__ = "GPL" 
  14   
  15  #=================================================== 
  16  # TODO 
  17  # Basically we'll probably have to: 
  18  # 
  19  # a) serialize access to re-getting data from the cache so 
  20  #   that later-but-concurrent cache accesses spin until 
  21  #   the first one completes the refetch from the database 
  22  # 
  23  # b) serialize access to the cache per-se such that cache 
  24  #    flushes vs. cache regets happen atomically (where 
  25  #    flushes would abort/restart current regets) 
  26  #=================================================== 
  27   
  28  # standard libs 
  29  import sys, string, time, copy, locale 
  30   
  31   
  32  # 3rd party 
  33  import logging 
  34   
  35   
  36  if __name__ == '__main__': 
  37          sys.path.insert(0, '../../') 
  38          from Gnumed.pycommon import gmLog2, gmDateTime, gmI18N 
  39          gmI18N.activate_locale() 
  40          gmI18N.install_domain() 
  41          gmDateTime.init() 
  42   
  43  from Gnumed.pycommon import gmExceptions, gmPG2, gmDispatcher, gmI18N, gmCfg, gmTools, gmDateTime 
  44  from Gnumed.business import gmAllergy, gmEMRStructItems, gmClinNarrative, gmPathLab 
  45  from Gnumed.business import gmMedication, gmVaccination 
  46   
  47   
  48  _log = logging.getLogger('gm.emr') 
  49  _log.debug(__version__) 
  50   
  51  _me = None 
  52  _here = None 
  53  #============================================================ 
  54  # helper functions 
  55  #------------------------------------------------------------ 
  56  _func_ask_user = None 
  57   
58 -def set_func_ask_user(a_func = None):
59 if not callable(a_func): 60 _log.error('[%] not callable, not setting _func_ask_user', a_func) 61 return False 62 63 _log.debug('setting _func_ask_user to [%s]', a_func) 64 65 global _func_ask_user 66 _func_ask_user = a_func
67 68 #============================================================
69 -class cClinicalRecord(object):
70 71 _clin_root_item_children_union_query = None 72
73 - def __init__(self, aPKey = None):
74 """Fails if 75 76 - no connection to database possible 77 - patient referenced by aPKey does not exist 78 """ 79 self.pk_patient = aPKey # == identity.pk == primary key 80 81 # log access to patient record (HIPAA, for example) 82 cmd = u'SELECT gm.log_access2emr(%(todo)s)' 83 args = {'todo': u'patient [%s]' % aPKey} 84 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}]) 85 86 from Gnumed.business import gmSurgery, gmPerson 87 global _me 88 if _me is None: 89 _me = gmPerson.gmCurrentProvider() 90 global _here 91 if _here is None: 92 _here = gmSurgery.gmCurrentPractice() 93 94 # ........................................... 95 # this is a hack to speed up get_encounters() 96 clin_root_item_children = gmPG2.get_child_tables('clin', 'clin_root_item') 97 if cClinicalRecord._clin_root_item_children_union_query is None: 98 union_phrase = u""" 99 SELECT fk_encounter from 100 %s.%s cn 101 inner join 102 (SELECT pk FROM clin.episode ep WHERE ep.fk_health_issue in %%s) as epi 103 on (cn.fk_episode = epi.pk) 104 """ 105 cClinicalRecord._clin_root_item_children_union_query = u'union\n'.join ( 106 [ union_phrase % (child[0], child[1]) for child in clin_root_item_children ] 107 ) 108 # ........................................... 109 110 self.__db_cache = {} 111 112 # load current or create new encounter 113 if _func_ask_user is None: 114 _log.error('[_func_ask_user] is None') 115 print "*** GNUmed [%s]: _func_ask_user is not set ***" % self.__class__.__name__ 116 self.remove_empty_encounters() 117 self.__encounter = None 118 if not self.__initiate_active_encounter(): 119 raise gmExceptions.ConstructorError, "cannot activate an encounter for patient [%s]" % aPKey 120 121 gmAllergy.ensure_has_allergy_state(encounter = self.current_encounter['pk_encounter']) 122 123 # register backend notification interests 124 # (keep this last so we won't hang on threads when 125 # failing this constructor for other reasons ...) 126 if not self._register_interests(): 127 raise gmExceptions.ConstructorError, "cannot register signal interests" 128 129 _log.debug('Instantiated clinical record for patient [%s].' % self.pk_patient)
130 #--------------------------------------------------------
131 - def __del__(self):
132 pass
133 #--------------------------------------------------------
134 - def cleanup(self):
135 _log.debug('cleaning up after clinical record for patient [%s]' % self.pk_patient) 136 137 return True
138 #-------------------------------------------------------- 139 # messaging 140 #--------------------------------------------------------
141 - def _register_interests(self):
142 gmDispatcher.connect(signal = u'encounter_mod_db', receiver = self.db_callback_encounter_mod_db) 143 144 return True
145 #--------------------------------------------------------
146 - def db_callback_encounter_mod_db(self, **kwds):
147 # get the current encounter as an extra instance 148 # from the database to check for changes 149 curr_enc_in_db = gmEMRStructItems.cEncounter(aPK_obj = self.current_encounter['pk_encounter']) 150 151 # the encounter just retrieved and the active encounter 152 # have got the same transaction ID so there's no change 153 # in the database, there could be a local change in 154 # the active encounter but that doesn't matter 155 if curr_enc_in_db['xmin_encounter'] == self.current_encounter['xmin_encounter']: 156 return True 157 158 # there must have been a change to the active encounter 159 # committed to the database from elsewhere, 160 # we must fail propagating the change, however, if 161 # there are local changes 162 if self.current_encounter.is_modified(): 163 _log.debug('unsaved changes in active encounter, cannot switch to another one') 164 raise ValueError('unsaved changes in active encounter, cannot switch to another one') 165 166 # there was a change in the database from elsewhere, 167 # locally, however, we don't have any changes, therefore 168 # we can propagate the remote change locally without 169 # losing anything 170 _log.debug('active encounter modified remotely, reloading and announcing the modification') 171 self.current_encounter.refetch_payload() 172 gmDispatcher.send(u'current_encounter_modified') 173 174 return True
175 #--------------------------------------------------------
176 - def db_callback_vaccs_modified(self, **kwds):
177 return True
178 #--------------------------------------------------------
179 - def _health_issues_modified(self):
180 try: 181 del self.__db_cache['health issues'] 182 except KeyError: 183 pass 184 return 1
185 #--------------------------------------------------------
187 # try: 188 # del self.__db_cache['episodes'] 189 # except KeyError: 190 # pass 191 return 1
192 #--------------------------------------------------------
193 - def _clin_item_modified(self):
194 _log.debug('DB: clin_root_item modification')
195 #-------------------------------------------------------- 196 # API: performed procedures 197 #--------------------------------------------------------
198 - def get_performed_procedures(self, episodes=None, issues=None):
199 200 procs = gmEMRStructItems.get_performed_procedures(patient = self.pk_patient) 201 202 if episodes is not None: 203 procs = filter(lambda p: p['pk_episode'] in episodes, procs) 204 205 if issues is not None: 206 procs = filter(lambda p: p['pk_health_issue'] in issues, procs) 207 208 return procs
209 #--------------------------------------------------------
210 - def add_performed_procedure(self, episode=None, location=None, hospital_stay=None, procedure=None):
211 return gmEMRStructItems.create_performed_procedure ( 212 encounter = self.current_encounter['pk_encounter'], 213 episode = episode, 214 location = location, 215 hospital_stay = hospital_stay, 216 procedure = procedure 217 )
218 #-------------------------------------------------------- 219 # API: hospital stays 220 #--------------------------------------------------------
221 - def get_hospital_stays(self, episodes=None, issues=None):
222 223 stays = gmEMRStructItems.get_patient_hospital_stays(patient = self.pk_patient) 224 225 if episodes is not None: 226 stays = filter(lambda s: s['pk_episode'] in episodes, stays) 227 228 if issues is not None: 229 stays = filter(lambda s: s['pk_health_issue'] in issues, stays) 230 231 return stays
232 #-------------------------------------------------------- 233 # Narrative API 234 #--------------------------------------------------------
235 - def add_notes(self, notes=None, episode=None):
236 237 for note in notes: 238 success, data = gmClinNarrative.create_clin_narrative ( 239 narrative = note[1], 240 soap_cat = note[0], 241 episode_id = episode, 242 encounter_id = self.current_encounter['pk_encounter'] 243 ) 244 245 return True
246 #--------------------------------------------------------
247 - def add_clin_narrative(self, note='', soap_cat='s', episode=None):
248 if note.strip() == '': 249 _log.info('will not create empty clinical note') 250 return None 251 status, data = gmClinNarrative.create_clin_narrative ( 252 narrative = note, 253 soap_cat = soap_cat, 254 episode_id = episode['pk_episode'], 255 encounter_id = self.current_encounter['pk_encounter'] 256 ) 257 if not status: 258 _log.error(str(data)) 259 return None 260 return data
261 #--------------------------------------------------------
262 - def get_clin_narrative(self, since=None, until=None, encounters=None, episodes=None, issues=None, soap_cats=None, providers=None):
263 """Get SOAP notes pertinent to this encounter. 264 265 since 266 - initial date for narrative items 267 until 268 - final date for narrative items 269 encounters 270 - list of encounters whose narrative are to be retrieved 271 episodes 272 - list of episodes whose narrative are to be retrieved 273 issues 274 - list of health issues whose narrative are to be retrieved 275 soap_cats 276 - list of SOAP categories of the narrative to be retrieved 277 """ 278 cmd = u""" 279 SELECT cvpn.*, (SELECT rank FROM clin.soap_cat_ranks WHERE soap_cat = cvpn.soap_cat) as soap_rank 280 from clin.v_pat_narrative cvpn 281 WHERE pk_patient = %s 282 order by date, soap_rank 283 """ 284 285 #xxxxxxxxxxxxxxxx 286 # support row_version in narrative for display in tree 287 288 rows, idx = gmPG2.run_ro_queries(queries=[{'cmd': cmd, 'args': [self.pk_patient]}], get_col_idx=True) 289 290 filtered_narrative = [ gmClinNarrative.cNarrative(row = {'pk_field': 'pk_narrative', 'idx': idx, 'data': row}) for row in rows ] 291 292 if since is not None: 293 filtered_narrative = filter(lambda narr: narr['date'] >= since, filtered_narrative) 294 295 if until is not None: 296 filtered_narrative = filter(lambda narr: narr['date'] < until, filtered_narrative) 297 298 if issues is not None: 299 filtered_narrative = filter(lambda narr: narr['pk_health_issue'] in issues, filtered_narrative) 300 301 if episodes is not None: 302 filtered_narrative = filter(lambda narr: narr['pk_episode'] in episodes, filtered_narrative) 303 304 if encounters is not None: 305 filtered_narrative = filter(lambda narr: narr['pk_encounter'] in encounters, filtered_narrative) 306 307 if soap_cats is not None: 308 soap_cats = map(lambda c: c.lower(), soap_cats) 309 filtered_narrative = filter(lambda narr: narr['soap_cat'] in soap_cats, filtered_narrative) 310 311 if providers is not None: 312 filtered_narrative = filter(lambda narr: narr['provider'] in providers, filtered_narrative) 313 314 return filtered_narrative
315 #--------------------------------------------------------
316 - def search_narrative_simple(self, search_term=''):
317 318 search_term = search_term.strip() 319 if search_term == '': 320 return [] 321 322 cmd = u""" 323 SELECT 324 *, 325 coalesce((SELECT description FROM clin.episode WHERE pk = vn4s.pk_episode), vn4s.src_table) 326 as episode, 327 coalesce((SELECT description FROM clin.health_issue WHERE pk = vn4s.pk_health_issue), vn4s.src_table) 328 as health_issue, 329 (SELECT started FROM clin.encounter WHERE pk = vn4s.pk_encounter) 330 as encounter_started, 331 (SELECT last_affirmed FROM clin.encounter WHERE pk = vn4s.pk_encounter) 332 as encounter_ended, 333 (SELECT _(description) FROM clin.encounter_type WHERE pk = (SELECT fk_type FROM clin.encounter WHERE pk = vn4s.pk_encounter)) 334 as encounter_type 335 from clin.v_narrative4search vn4s 336 WHERE 337 pk_patient = %(pat)s and 338 vn4s.narrative ~ %(term)s 339 order by 340 encounter_started 341 """ # case sensitive 342 rows, idx = gmPG2.run_ro_queries(queries = [ 343 {'cmd': cmd, 'args': {'pat': self.pk_patient, 'term': search_term}} 344 ]) 345 return rows
346 #--------------------------------------------------------
347 - def get_text_dump_old(self):
348 # don't know how to invalidate this by means of 349 # a notify without catching notifies from *all* 350 # child tables, the best solution would be if 351 # inserts in child tables would also fire triggers 352 # of ancestor tables, but oh well, 353 # until then the text dump will not be cached ... 354 try: 355 return self.__db_cache['text dump old'] 356 except KeyError: 357 pass 358 # not cached so go get it 359 fields = [ 360 "to_char(modified_when, 'YYYY-MM-DD @ HH24:MI') as modified_when", 361 'modified_by', 362 'clin_when', 363 "case is_modified when false then '%s' else '%s' end as modified_string" % (_('original entry'), _('modified entry')), 364 'pk_item', 365 'pk_encounter', 366 'pk_episode', 367 'pk_health_issue', 368 'src_table' 369 ] 370 cmd = "SELECT %s FROM clin.v_pat_items WHERE pk_patient=%%s order by src_table, clin_when" % string.join(fields, ', ') 371 ro_conn = self._conn_pool.GetConnection('historica') 372 curs = ro_conn.cursor() 373 if not gmPG2.run_query(curs, None, cmd, self.pk_patient): 374 _log.error('cannot load item links for patient [%s]' % self.pk_patient) 375 curs.close() 376 return None 377 rows = curs.fetchall() 378 view_col_idx = gmPG2.get_col_indices(curs) 379 380 # aggregate by src_table for item retrieval 381 items_by_table = {} 382 for item in rows: 383 src_table = item[view_col_idx['src_table']] 384 pk_item = item[view_col_idx['pk_item']] 385 if not items_by_table.has_key(src_table): 386 items_by_table[src_table] = {} 387 items_by_table[src_table][pk_item] = item 388 389 # get mapping for issue/episode IDs 390 issues = self.get_health_issues() 391 issue_map = {} 392 for issue in issues: 393 issue_map[issue['pk']] = issue['description'] 394 episodes = self.get_episodes() 395 episode_map = {} 396 for episode in episodes: 397 episode_map[episode['pk_episode']] = episode['description'] 398 emr_data = {} 399 # get item data from all source tables 400 for src_table in items_by_table.keys(): 401 item_ids = items_by_table[src_table].keys() 402 # we don't know anything about the columns of 403 # the source tables but, hey, this is a dump 404 if len(item_ids) == 0: 405 _log.info('no items in table [%s] ?!?' % src_table) 406 continue 407 elif len(item_ids) == 1: 408 cmd = "SELECT * FROM %s WHERE pk_item=%%s order by modified_when" % src_table 409 if not gmPG2.run_query(curs, None, cmd, item_ids[0]): 410 _log.error('cannot load items from table [%s]' % src_table) 411 # skip this table 412 continue 413 elif len(item_ids) > 1: 414 cmd = "SELECT * FROM %s WHERE pk_item in %%s order by modified_when" % src_table 415 if not gmPG.run_query(curs, None, cmd, (tuple(item_ids),)): 416 _log.error('cannot load items from table [%s]' % src_table) 417 # skip this table 418 continue 419 rows = curs.fetchall() 420 table_col_idx = gmPG.get_col_indices(curs) 421 # format per-table items 422 for row in rows: 423 # FIXME: make this get_pkey_name() 424 pk_item = row[table_col_idx['pk_item']] 425 view_row = items_by_table[src_table][pk_item] 426 age = view_row[view_col_idx['age']] 427 # format metadata 428 try: 429 episode_name = episode_map[view_row[view_col_idx['pk_episode']]] 430 except: 431 episode_name = view_row[view_col_idx['pk_episode']] 432 try: 433 issue_name = issue_map[view_row[view_col_idx['pk_health_issue']]] 434 except: 435 issue_name = view_row[view_col_idx['pk_health_issue']] 436 437 if not emr_data.has_key(age): 438 emr_data[age] = [] 439 440 emr_data[age].append( 441 _('%s: encounter (%s)') % ( 442 view_row[view_col_idx['clin_when']], 443 view_row[view_col_idx['pk_encounter']] 444 ) 445 ) 446 emr_data[age].append(_('health issue: %s') % issue_name) 447 emr_data[age].append(_('episode : %s') % episode_name) 448 # format table specific data columns 449 # - ignore those, they are metadata, some 450 # are in clin.v_pat_items data already 451 cols2ignore = [ 452 'pk_audit', 'row_version', 'modified_when', 'modified_by', 453 'pk_item', 'id', 'fk_encounter', 'fk_episode' 454 ] 455 col_data = [] 456 for col_name in table_col_idx.keys(): 457 if col_name in cols2ignore: 458 continue 459 emr_data[age].append("=> %s:" % col_name) 460 emr_data[age].append(row[table_col_idx[col_name]]) 461 emr_data[age].append("----------------------------------------------------") 462 emr_data[age].append("-- %s from table %s" % ( 463 view_row[view_col_idx['modified_string']], 464 src_table 465 )) 466 emr_data[age].append("-- written %s by %s" % ( 467 view_row[view_col_idx['modified_when']], 468 view_row[view_col_idx['modified_by']] 469 )) 470 emr_data[age].append("----------------------------------------------------") 471 curs.close() 472 self._conn_pool.ReleaseConnection('historica') 473 return emr_data
474 #--------------------------------------------------------
475 - def get_text_dump(self, since=None, until=None, encounters=None, episodes=None, issues=None):
476 # don't know how to invalidate this by means of 477 # a notify without catching notifies from *all* 478 # child tables, the best solution would be if 479 # inserts in child tables would also fire triggers 480 # of ancestor tables, but oh well, 481 # until then the text dump will not be cached ... 482 try: 483 return self.__db_cache['text dump'] 484 except KeyError: 485 pass 486 # not cached so go get it 487 # -- get the data -- 488 fields = [ 489 'age', 490 "to_char(modified_when, 'YYYY-MM-DD @ HH24:MI') as modified_when", 491 'modified_by', 492 'clin_when', 493 "case is_modified when false then '%s' else '%s' end as modified_string" % (_('original entry'), _('modified entry')), 494 'pk_item', 495 'pk_encounter', 496 'pk_episode', 497 'pk_health_issue', 498 'src_table' 499 ] 500 select_from = "SELECT %s FROM clin.v_pat_items" % ', '.join(fields) 501 # handle constraint conditions 502 where_snippets = [] 503 params = {} 504 where_snippets.append('pk_patient=%(pat_id)s') 505 params['pat_id'] = self.pk_patient 506 if not since is None: 507 where_snippets.append('clin_when >= %(since)s') 508 params['since'] = since 509 if not until is None: 510 where_snippets.append('clin_when <= %(until)s') 511 params['until'] = until 512 # FIXME: these are interrelated, eg if we constrain encounter 513 # we automatically constrain issue/episode, so handle that, 514 # encounters 515 if not encounters is None and len(encounters) > 0: 516 params['enc'] = encounters 517 if len(encounters) > 1: 518 where_snippets.append('fk_encounter in %(enc)s') 519 else: 520 where_snippets.append('fk_encounter=%(enc)s') 521 # episodes 522 if not episodes is None and len(episodes) > 0: 523 params['epi'] = episodes 524 if len(episodes) > 1: 525 where_snippets.append('fk_episode in %(epi)s') 526 else: 527 where_snippets.append('fk_episode=%(epi)s') 528 # health issues 529 if not issues is None and len(issues) > 0: 530 params['issue'] = issues 531 if len(issues) > 1: 532 where_snippets.append('fk_health_issue in %(issue)s') 533 else: 534 where_snippets.append('fk_health_issue=%(issue)s') 535 536 where_clause = ' and '.join(where_snippets) 537 order_by = 'order by src_table, age' 538 cmd = "%s WHERE %s %s" % (select_from, where_clause, order_by) 539 540 rows, view_col_idx = gmPG.run_ro_query('historica', cmd, 1, params) 541 if rows is None: 542 _log.error('cannot load item links for patient [%s]' % self.pk_patient) 543 return None 544 545 # -- sort the data -- 546 # FIXME: by issue/encounter/episode, eg formatting 547 # aggregate by src_table for item retrieval 548 items_by_table = {} 549 for item in rows: 550 src_table = item[view_col_idx['src_table']] 551 pk_item = item[view_col_idx['pk_item']] 552 if not items_by_table.has_key(src_table): 553 items_by_table[src_table] = {} 554 items_by_table[src_table][pk_item] = item 555 556 # get mapping for issue/episode IDs 557 issues = self.get_health_issues() 558 issue_map = {} 559 for issue in issues: 560 issue_map[issue['pk_health_issue']] = issue['description'] 561 episodes = self.get_episodes() 562 episode_map = {} 563 for episode in episodes: 564 episode_map[episode['pk_episode']] = episode['description'] 565 emr_data = {} 566 # get item data from all source tables 567 ro_conn = self._conn_pool.GetConnection('historica') 568 curs = ro_conn.cursor() 569 for src_table in items_by_table.keys(): 570 item_ids = items_by_table[src_table].keys() 571 # we don't know anything about the columns of 572 # the source tables but, hey, this is a dump 573 if len(item_ids) == 0: 574 _log.info('no items in table [%s] ?!?' % src_table) 575 continue 576 elif len(item_ids) == 1: 577 cmd = "SELECT * FROM %s WHERE pk_item=%%s order by modified_when" % src_table 578 if not gmPG.run_query(curs, None, cmd, item_ids[0]): 579 _log.error('cannot load items from table [%s]' % src_table) 580 # skip this table 581 continue 582 elif len(item_ids) > 1: 583 cmd = "SELECT * FROM %s WHERE pk_item in %%s order by modified_when" % src_table 584 if not gmPG.run_query(curs, None, cmd, (tuple(item_ids),)): 585 _log.error('cannot load items from table [%s]' % src_table) 586 # skip this table 587 continue 588 rows = curs.fetchall() 589 table_col_idx = gmPG.get_col_indices(curs) 590 # format per-table items 591 for row in rows: 592 # FIXME: make this get_pkey_name() 593 pk_item = row[table_col_idx['pk_item']] 594 view_row = items_by_table[src_table][pk_item] 595 age = view_row[view_col_idx['age']] 596 # format metadata 597 try: 598 episode_name = episode_map[view_row[view_col_idx['pk_episode']]] 599 except: 600 episode_name = view_row[view_col_idx['pk_episode']] 601 try: 602 issue_name = issue_map[view_row[view_col_idx['pk_health_issue']]] 603 except: 604 issue_name = view_row[view_col_idx['pk_health_issue']] 605 606 if not emr_data.has_key(age): 607 emr_data[age] = [] 608 609 emr_data[age].append( 610 _('%s: encounter (%s)') % ( 611 view_row[view_col_idx['clin_when']], 612 view_row[view_col_idx['pk_encounter']] 613 ) 614 ) 615 emr_data[age].append(_('health issue: %s') % issue_name) 616 emr_data[age].append(_('episode : %s') % episode_name) 617 # format table specific data columns 618 # - ignore those, they are metadata, some 619 # are in clin.v_pat_items data already 620 cols2ignore = [ 621 'pk_audit', 'row_version', 'modified_when', 'modified_by', 622 'pk_item', 'id', 'fk_encounter', 'fk_episode', 'pk' 623 ] 624 col_data = [] 625 for col_name in table_col_idx.keys(): 626 if col_name in cols2ignore: 627 continue 628 emr_data[age].append("=> %s: %s" % (col_name, row[table_col_idx[col_name]])) 629 emr_data[age].append("----------------------------------------------------") 630 emr_data[age].append("-- %s from table %s" % ( 631 view_row[view_col_idx['modified_string']], 632 src_table 633 )) 634 emr_data[age].append("-- written %s by %s" % ( 635 view_row[view_col_idx['modified_when']], 636 view_row[view_col_idx['modified_by']] 637 )) 638 emr_data[age].append("----------------------------------------------------") 639 curs.close() 640 return emr_data
641 #--------------------------------------------------------
642 - def get_patient_ID(self):
643 return self.pk_patient
644 #--------------------------------------------------------
645 - def get_statistics(self):
646 union_query = u'\n union all\n'.join ([ 647 u""" 648 SELECT (( 649 -- all relevant health issues + active episodes WITH health issue 650 SELECT COUNT(1) 651 FROM clin.v_problem_list 652 WHERE 653 pk_patient = %(pat)s 654 AND 655 pk_health_issue is not null 656 ) + ( 657 -- active episodes WITHOUT health issue 658 SELECT COUNT(1) 659 FROM clin.v_problem_list 660 WHERE 661 pk_patient = %(pat)s 662 AND 663 pk_health_issue is null 664 ))""", 665 u'SELECT count(1) FROM clin.encounter WHERE fk_patient = %(pat)s', 666 u'SELECT count(1) FROM clin.v_pat_items WHERE pk_patient = %(pat)s', 667 u'SELECT count(1) FROM blobs.v_doc_med WHERE pk_patient = %(pat)s', 668 u'SELECT count(1) FROM clin.v_test_results WHERE pk_patient = %(pat)s', 669 u'SELECT count(1) FROM clin.v_pat_hospital_stays WHERE pk_patient = %(pat)s', 670 u'SELECT count(1) FROM clin.v_pat_procedures WHERE pk_patient = %(pat)s', 671 # active and approved substances == medication 672 u""" 673 SELECT count(1) 674 from clin.v_pat_substance_intake 675 WHERE 676 pk_patient = %(pat)s 677 and is_currently_active in (null, true) 678 and intake_is_approved_of in (null, true)""", 679 u'SELECT count(1) FROM clin.v_pat_vaccinations WHERE pk_patient = %(pat)s' 680 ]) 681 682 rows, idx = gmPG2.run_ro_queries ( 683 queries = [{'cmd': union_query, 'args': {'pat': self.pk_patient}}], 684 get_col_idx = False 685 ) 686 687 stats = dict ( 688 problems = rows[0][0], 689 encounters = rows[1][0], 690 items = rows[2][0], 691 documents = rows[3][0], 692 results = rows[4][0], 693 stays = rows[5][0], 694 procedures = rows[6][0], 695 active_drugs = rows[7][0], 696 vaccinations = rows[8][0] 697 ) 698 699 return stats
700 #--------------------------------------------------------
701 - def format_statistics(self):
702 return _("""Medical problems: %(problems)s 703 Total encounters: %(encounters)s 704 Total EMR entries: %(items)s 705 Active medications: %(active_drugs)s 706 Documents: %(documents)s 707 Test results: %(results)s 708 Hospital stays: %(stays)s 709 Procedures: %(procedures)s 710 Vaccinations: %(vaccinations)s 711 """ ) % self.get_statistics()
712 #--------------------------------------------------------
713 - def format_summary(self):
714 715 stats = self.get_statistics() 716 first = self.get_first_encounter() 717 last = self.get_last_encounter() 718 probs = self.get_problems() 719 720 txt = _('EMR Statistics\n\n') 721 if len(probs) > 0: 722 txt += _(' %s known problems. Clinically relevant thereof:\n') % stats['problems'] 723 else: 724 txt += _(' %s known problems\n') % stats['problems'] 725 for prob in probs: 726 if not prob['clinically_relevant']: 727 continue 728 txt += u' \u00BB%s\u00AB (%s)\n' % ( 729 prob['problem'], 730 gmTools.bool2subst(prob['problem_active'], _('active'), _('inactive')) 731 ) 732 txt += _(' %s encounters from %s to %s\n') % ( 733 stats['encounters'], 734 first['started'].strftime('%x').decode(gmI18N.get_encoding()), 735 last['started'].strftime('%x').decode(gmI18N.get_encoding()) 736 ) 737 txt += _(' %s active medications\n') % stats['active_drugs'] 738 txt += _(' %s documents\n') % stats['documents'] 739 txt += _(' %s test results\n') % stats['results'] 740 txt += _(' %s hospital stays\n') % stats['stays'] 741 txt += _(' %s performed procedures\n\n') % stats['procedures'] 742 743 txt += _('Allergies and Intolerances\n\n') 744 745 allg_state = self.allergy_state 746 txt += (u' ' + allg_state.state_string) 747 if allg_state['last_confirmed'] is not None: 748 txt += (_(' (last confirmed %s)') % allg_state['last_confirmed'].strftime('%x').decode(gmI18N.get_encoding())) 749 txt += u'\n' 750 txt += gmTools.coalesce(allg_state['comment'], u'', u' %s\n') 751 for allg in self.get_allergies(): 752 txt += u' %s: %s\n' % ( 753 allg['descriptor'], 754 gmTools.coalesce(allg['reaction'], _('unknown reaction')) 755 ) 756 757 txt += u'\n' 758 txt += _('Vaccinations') 759 txt += u'\n' 760 761 vaccs = self.get_latest_vaccinations() 762 inds = sorted(vaccs.keys()) 763 for ind in inds: 764 ind_count, vacc = vaccs[ind] 765 txt += u' %s (%s%s): %s (%s %s%s%s)\n' % ( 766 ind, 767 gmTools.u_sum, 768 ind_count, 769 vacc['date_given'].strftime('%b %Y').decode(gmI18N.get_encoding()), 770 vacc['vaccine'], 771 gmTools.u_left_double_angle_quote, 772 vacc['batch_no'], 773 gmTools.u_right_double_angle_quote 774 ) 775 776 return txt
777 #-------------------------------------------------------- 778 # allergy API 779 #--------------------------------------------------------
780 - def get_allergies(self, remove_sensitivities=False, since=None, until=None, encounters=None, episodes=None, issues=None, ID_list=None):
781 """Retrieves patient allergy items. 782 783 remove_sensitivities 784 - retrieve real allergies only, without sensitivities 785 since 786 - initial date for allergy items 787 until 788 - final date for allergy items 789 encounters 790 - list of encounters whose allergies are to be retrieved 791 episodes 792 - list of episodes whose allergies are to be retrieved 793 issues 794 - list of health issues whose allergies are to be retrieved 795 """ 796 cmd = u"SELECT * FROM clin.v_pat_allergies WHERE pk_patient=%s order by descriptor" 797 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient]}], get_col_idx = True) 798 allergies = [] 799 for r in rows: 800 allergies.append(gmAllergy.cAllergy(row = {'data': r, 'idx': idx, 'pk_field': 'pk_allergy'})) 801 802 # ok, let's constrain our list 803 filtered_allergies = [] 804 filtered_allergies.extend(allergies) 805 806 if ID_list is not None: 807 filtered_allergies = filter(lambda allg: allg['pk_allergy'] in ID_list, filtered_allergies) 808 if len(filtered_allergies) == 0: 809 _log.error('no allergies of list [%s] found for patient [%s]' % (str(ID_list), self.pk_patient)) 810 # better fail here contrary to what we do elsewhere 811 return None 812 else: 813 return filtered_allergies 814 815 if remove_sensitivities: 816 filtered_allergies = filter(lambda allg: allg['type'] == 'allergy', filtered_allergies) 817 if since is not None: 818 filtered_allergies = filter(lambda allg: allg['date'] >= since, filtered_allergies) 819 if until is not None: 820 filtered_allergies = filter(lambda allg: allg['date'] < until, filtered_allergies) 821 if issues is not None: 822 filtered_allergies = filter(lambda allg: allg['pk_health_issue'] in issues, filtered_allergies) 823 if episodes is not None: 824 filtered_allergies = filter(lambda allg: allg['pk_episode'] in episodes, filtered_allergies) 825 if encounters is not None: 826 filtered_allergies = filter(lambda allg: allg['pk_encounter'] in encounters, filtered_allergies) 827 828 return filtered_allergies
829 #--------------------------------------------------------
830 - def add_allergy(self, allergene=None, allg_type=None, encounter_id=None, episode_id=None):
831 if encounter_id is None: 832 encounter_id = self.current_encounter['pk_encounter'] 833 834 if episode_id is None: 835 issue = self.add_health_issue(issue_name = _('allergies/intolerances')) 836 epi = self.add_episode(episode_name = allergene, pk_health_issue = issue['pk_health_issue']) 837 episode_id = epi['pk_episode'] 838 839 new_allergy = gmAllergy.create_allergy ( 840 allergene = allergene, 841 allg_type = allg_type, 842 encounter_id = encounter_id, 843 episode_id = episode_id 844 ) 845 846 return new_allergy
847 #--------------------------------------------------------
848 - def delete_allergy(self, pk_allergy=None):
849 cmd = u'delete FROM clin.allergy WHERE pk=%(pk_allg)s' 850 args = {'pk_allg': pk_allergy} 851 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
852 #--------------------------------------------------------
853 - def is_allergic_to(self, atcs=None, inns=None, brand=None):
854 """Cave: only use with one potential allergic agent 855 otherwise you won't know which of the agents the allergy is to.""" 856 857 # we don't know the state 858 if self.allergy_state is None: 859 return None 860 861 # we know there's no allergies 862 if self.allergy_state == 0: 863 return False 864 865 args = { 866 'atcs': atcs, 867 'inns': inns, 868 'brand': brand, 869 'pat': self.pk_patient 870 } 871 allergenes = [] 872 where_parts = [] 873 874 if len(atcs) == 0: 875 atcs = None 876 if atcs is not None: 877 where_parts.append(u'atc_code in %(atcs)s') 878 if len(inns) == 0: 879 inns = None 880 if inns is not None: 881 where_parts.append(u'generics in %(inns)s') 882 allergenes.extend(inns) 883 if brand is not None: 884 where_parts.append(u'substance = %(brand)s') 885 allergenes.append(brand) 886 887 if len(allergenes) != 0: 888 where_parts.append(u'allergene in %(allgs)s') 889 args['allgs'] = tuple(allergenes) 890 891 cmd = u""" 892 SELECT * FROM clin.v_pat_allergies 893 WHERE 894 pk_patient = %%(pat)s 895 AND ( %s )""" % u' OR '.join(where_parts) 896 897 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 898 899 if len(rows) == 0: 900 return False 901 902 return gmAllergy.cAllergy(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_allergy'})
903 #--------------------------------------------------------
904 - def _set_allergy_state(self, state):
905 906 if state not in gmAllergy.allergy_states: 907 raise ValueError('[%s].__set_allergy_state(): <state> must be one of %s' % (self.__class__.__name__, gmAllergy.allergy_states)) 908 909 allg_state = gmAllergy.ensure_has_allergy_state(encounter = self.current_encounter['pk_encounter']) 910 allg_state['has_allergy'] = state 911 allg_state.save_payload() 912 return True
913
914 - def _get_allergy_state(self):
915 return gmAllergy.ensure_has_allergy_state(encounter = self.current_encounter['pk_encounter'])
916 917 allergy_state = property(_get_allergy_state, _set_allergy_state) 918 #-------------------------------------------------------- 919 # episodes API 920 #--------------------------------------------------------
921 - def get_episodes(self, id_list=None, issues=None, open_status=None):
922 """Fetches from backend patient episodes. 923 924 id_list - Episodes' PKs list 925 issues - Health issues' PKs list to filter episodes by 926 open_status - return all episodes, only open or closed one(s) 927 """ 928 cmd = u"SELECT * FROM clin.v_pat_episodes WHERE pk_patient=%s" 929 rows, idx = gmPG2.run_ro_queries(queries=[{'cmd': cmd, 'args': [self.pk_patient]}], get_col_idx=True) 930 tmp = [] 931 for r in rows: 932 tmp.append(gmEMRStructItems.cEpisode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_episode'})) 933 934 # now filter 935 if (id_list is None) and (issues is None) and (open_status is None): 936 return tmp 937 938 # ok, let's filter episode list 939 filtered_episodes = [] 940 filtered_episodes.extend(tmp) 941 if open_status is not None: 942 filtered_episodes = filter(lambda epi: epi['episode_open'] == open_status, filtered_episodes) 943 944 if issues is not None: 945 filtered_episodes = filter(lambda epi: epi['pk_health_issue'] in issues, filtered_episodes) 946 947 if id_list is not None: 948 filtered_episodes = filter(lambda epi: epi['pk_episode'] in id_list, filtered_episodes) 949 950 return filtered_episodes
951 #------------------------------------------------------------------
952 - def get_episodes_by_encounter(self, pk_encounter=None):
953 cmd = u"""SELECT distinct pk_episode 954 from clin.v_pat_items 955 WHERE pk_encounter=%(enc)s and pk_patient=%(pat)s""" 956 args = { 957 'enc': gmTools.coalesce(pk_encounter, self.current_encounter['pk_encounter']), 958 'pat': self.pk_patient 959 } 960 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}]) 961 if len(rows) == 0: 962 return [] 963 epis = [] 964 for row in rows: 965 epis.append(row[0]) 966 return self.get_episodes(id_list=epis)
967 #------------------------------------------------------------------
968 - def add_episode(self, episode_name=None, pk_health_issue=None, is_open=False):
969 """Add episode 'episode_name' for a patient's health issue. 970 971 - silently returns if episode already exists 972 """ 973 episode = gmEMRStructItems.create_episode ( 974 pk_health_issue = pk_health_issue, 975 episode_name = episode_name, 976 is_open = is_open, 977 encounter = self.current_encounter['pk_encounter'] 978 ) 979 return episode
980 #--------------------------------------------------------
981 - def get_most_recent_episode(self, issue=None):
982 # try to find the episode with the most recently modified clinical item 983 984 issue_where = gmTools.coalesce(issue, u'', u'and pk_health_issue = %(issue)s') 985 986 cmd = u""" 987 SELECT pk 988 from clin.episode 989 WHERE pk = ( 990 SELECT distinct on(pk_episode) pk_episode 991 from clin.v_pat_items 992 WHERE 993 pk_patient = %%(pat)s 994 and 995 modified_when = ( 996 SELECT max(vpi.modified_when) 997 from clin.v_pat_items vpi 998 WHERE vpi.pk_patient = %%(pat)s 999 ) 1000 %s 1001 -- guard against several episodes created at the same moment of time 1002 limit 1 1003 )""" % issue_where 1004 rows, idx = gmPG2.run_ro_queries(queries = [ 1005 {'cmd': cmd, 'args': {'pat': self.pk_patient, 'issue': issue}} 1006 ]) 1007 if len(rows) != 0: 1008 return gmEMRStructItems.cEpisode(aPK_obj=rows[0][0]) 1009 1010 # no clinical items recorded, so try to find 1011 # the youngest episode for this patient 1012 cmd = u""" 1013 SELECT vpe0.pk_episode 1014 from 1015 clin.v_pat_episodes vpe0 1016 WHERE 1017 vpe0.pk_patient = %%(pat)s 1018 and 1019 vpe0.episode_modified_when = ( 1020 SELECT max(vpe1.episode_modified_when) 1021 from clin.v_pat_episodes vpe1 1022 WHERE vpe1.pk_episode = vpe0.pk_episode 1023 ) 1024 %s""" % issue_where 1025 rows, idx = gmPG2.run_ro_queries(queries = [ 1026 {'cmd': cmd, 'args': {'pat': self.pk_patient, 'issue': issue}} 1027 ]) 1028 if len(rows) != 0: 1029 return gmEMRStructItems.cEpisode(aPK_obj=rows[0][0]) 1030 1031 return None
1032 #--------------------------------------------------------
1033 - def episode2problem(self, episode=None):
1034 return gmEMRStructItems.episode2problem(episode=episode)
1035 #-------------------------------------------------------- 1036 # problems API 1037 #--------------------------------------------------------
1038 - def get_problems(self, episodes=None, issues=None, include_closed_episodes=False, include_irrelevant_issues=False):
1039 """Retrieve a patient's problems. 1040 1041 "Problems" are the UNION of: 1042 1043 - issues which are .clinically_relevant 1044 - episodes which are .is_open 1045 1046 Therefore, both an issue and the open episode 1047 thereof can each be listed as a problem. 1048 1049 include_closed_episodes/include_irrelevant_issues will 1050 include those -- which departs from the definition of 1051 the problem list being "active" items only ... 1052 1053 episodes - episodes' PKs to filter problems by 1054 issues - health issues' PKs to filter problems by 1055 """ 1056 # FIXME: this could use a good measure of streamlining, probably 1057 1058 args = {'pat': self.pk_patient} 1059 1060 cmd = u"""SELECT pk_health_issue, pk_episode FROM clin.v_problem_list WHERE pk_patient = %(pat)s""" 1061 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 1062 1063 # Instantiate problem items 1064 problems = [] 1065 for row in rows: 1066 pk_args = { 1067 u'pk_patient': self.pk_patient, 1068 u'pk_health_issue': row['pk_health_issue'], 1069 u'pk_episode': row['pk_episode'] 1070 } 1071 problems.append(gmEMRStructItems.cProblem(aPK_obj = pk_args, try_potential_problems = False)) 1072 1073 # include non-problems ? 1074 other_rows = [] 1075 if include_closed_episodes: 1076 cmd = u"""SELECT pk_health_issue, pk_episode FROM clin.v_potential_problem_list WHERE pk_patient = %(pat)s and type = 'episode'""" 1077 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 1078 other_rows.extend(rows) 1079 1080 if include_irrelevant_issues: 1081 cmd = u"""SELECT pk_health_issue, pk_episode FROM clin.v_potential_problem_list WHERE pk_patient = %(pat)s and type = 'health issue'""" 1082 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 1083 other_rows.extend(rows) 1084 1085 if len(other_rows) > 0: 1086 for row in other_rows: 1087 pk_args = { 1088 u'pk_patient': self.pk_patient, 1089 u'pk_health_issue': row['pk_health_issue'], 1090 u'pk_episode': row['pk_episode'] 1091 } 1092 problems.append(gmEMRStructItems.cProblem(aPK_obj = pk_args, try_potential_problems = True)) 1093 1094 # filter ? 1095 if (episodes is None) and (issues is None): 1096 return problems 1097 1098 # filter 1099 if issues is not None: 1100 problems = filter(lambda epi: epi['pk_health_issue'] in issues, problems) 1101 if episodes is not None: 1102 problems = filter(lambda epi: epi['pk_episode'] in episodes, problems) 1103 1104 return problems
1105 #--------------------------------------------------------
1106 - def problem2episode(self, problem=None):
1107 """ 1108 Retrieve the cEpisode instance equivalent to the given problem. 1109 The problem's type attribute must be 'episode' 1110 1111 @param problem: The problem to retrieve its related episode for 1112 @type problem: A gmEMRStructItems.cProblem instance 1113 """ 1114 if isinstance(problem, gmEMRStructItems.cProblem) and (problem['type'] == 'episode'): 1115 return self.get_episodes(id_list=[problem['pk_episode']])[0] 1116 1117 if isinstance(problem, gmEMRStructItems.cEpisode): 1118 return problem 1119 1120 raise TypeError('cannot convert [%s] to episode' % problem)
1121 #--------------------------------------------------------
1122 - def problem2issue(self, problem=None):
1123 """ 1124 Retrieve the cIssue instance equivalent to the given problem. 1125 The problem's type attribute must be 'issue'. 1126 1127 @param problem: The problem to retrieve the corresponding issue for 1128 @type problem: A gmEMRStructItems.cProblem instance 1129 """ 1130 if isinstance(problem, gmEMRStructItems.cProblem) and (problem['type'] == 'issue'): 1131 return self.get_health_issues(id_list=[problem['pk_health_issue']])[0] 1132 1133 if isinstance(problem, gmEMRStructItems.cHealthIssue): 1134 return problem 1135 1136 raise TypeError('cannot convert [%s] to health issue' % problem)
1137 #--------------------------------------------------------
1138 - def reclass_problem(self, problem):
1139 """Transform given problem into either episode or health issue instance. 1140 """ 1141 if not isinstance(problem, gmEMRStructItems.cProblem): 1142 _log.debug(str(problem)) 1143 raise TypeError, 'cannot reclass [%s] instance to problem' % type(problem) 1144 if problem['type'] == 'episode': 1145 return self.get_episodes(id_list=[problem['pk_episode']])[0] 1146 if problem['type'] == 'issue': 1147 return self.get_health_issues(id_list=[problem['pk_health_issue']])[0] 1148 return None
1149 #-------------------------------------------------------- 1150 # health issues API 1151 #--------------------------------------------------------
1152 - def get_health_issues(self, id_list = None):
1153 1154 cmd = u"SELECT *, xmin_health_issue FROM clin.v_health_issues WHERE pk_patient=%(pat)s" 1155 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pat': self.pk_patient}}], get_col_idx = True) 1156 issues = [] 1157 for row in rows: 1158 r = {'idx': idx, 'data': row, 'pk_field': 'pk_health_issue'} 1159 issues.append(gmEMRStructItems.cHealthIssue(row=r)) 1160 1161 if id_list is None: 1162 return issues 1163 1164 if len(id_list) == 0: 1165 raise ValueError('id_list to filter by is empty, most likely a programming error') 1166 1167 filtered_issues = [] 1168 for issue in issues: 1169 if issue['pk_health_issue'] in id_list: 1170 filtered_issues.append(issue) 1171 1172 return filtered_issues
1173 #------------------------------------------------------------------
1174 - def add_health_issue(self, issue_name=None):
1175 """Adds patient health issue.""" 1176 return gmEMRStructItems.create_health_issue ( 1177 description = issue_name, 1178 encounter = self.current_encounter['pk_encounter'], 1179 patient = self.pk_patient 1180 )
1181 #--------------------------------------------------------
1182 - def health_issue2problem(self, issue=None):
1183 return gmEMRStructItems.health_issue2problem(issue = issue)
1184 #-------------------------------------------------------- 1185 # API: substance intake 1186 #--------------------------------------------------------
1187 - def get_current_substance_intake(self, include_inactive=True, include_unapproved=False, order_by=None, episodes=None, issues=None):
1188 1189 where_parts = [u'pk_patient = %(pat)s'] 1190 1191 if not include_inactive: 1192 where_parts.append(u'is_currently_active in (true, null)') 1193 1194 if not include_unapproved: 1195 where_parts.append(u'intake_is_approved_of in (true, null)') 1196 1197 if order_by is None: 1198 order_by = u'' 1199 else: 1200 order_by = u'order by %s' % order_by 1201 1202 cmd = u"SELECT * FROM clin.v_pat_substance_intake WHERE %s %s" % ( 1203 u'\nand '.join(where_parts), 1204 order_by 1205 ) 1206 1207 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pat': self.pk_patient}}], get_col_idx = True) 1208 1209 meds = [ gmMedication.cSubstanceIntakeEntry(row = {'idx': idx, 'data': r, 'pk_field': 'pk_substance_intake'}) for r in rows ] 1210 1211 if episodes is not None: 1212 meds = filter(lambda s: s['pk_episode'] in episodes, meds) 1213 1214 if issues is not None: 1215 meds = filter(lambda s: s['pk_health_issue'] in issues, meds) 1216 1217 return meds
1218 #--------------------------------------------------------
1219 - def add_substance_intake(self, substance=None, atc=None, episode=None, preparation=None):
1220 return gmMedication.create_substance_intake ( 1221 substance = substance, 1222 atc = atc, 1223 encounter = self.current_encounter['pk_encounter'], 1224 episode = episode, 1225 preparation = preparation 1226 )
1227 #-------------------------------------------------------- 1228 # vaccinations API 1229 #--------------------------------------------------------
1230 - def add_vaccination(self, episode=None, vaccine=None, batch_no=None):
1231 return gmVaccination.create_vaccination ( 1232 encounter = self.current_encounter['pk_encounter'], 1233 episode = episode, 1234 vaccine = vaccine, 1235 batch_no = batch_no 1236 )
1237 #--------------------------------------------------------
1238 - def get_latest_vaccinations(self, episodes=None, issues=None):
1239 """Returns latest given vaccination for each vaccinated indication. 1240 1241 as a dict {'l10n_indication': cVaccination instance} 1242 1243 Note that this will produce duplicate vaccination instances on combi-indication vaccines ! 1244 """ 1245 # find the PKs 1246 args = {'pat': self.pk_patient} 1247 where_parts = [u'pk_patient = %(pat)s'] 1248 1249 if (episodes is not None) and (len(episodes) > 0): 1250 where_parts.append(u'pk_episode IN %(epis)s') 1251 args['epis'] = tuple(episodes) 1252 1253 if (issues is not None) and (len(issues) > 0): 1254 where_parts.append(u'pk_episode IN (select pk from clin.episode where fk_health_issue IN %(issues)s)') 1255 args['issues'] = tuple(issues) 1256 1257 cmd = u'SELECT pk_vaccination, l10n_indication, indication_count FROM clin.v_pat_last_vacc4indication WHERE %s' % u'\nAND '.join(where_parts) 1258 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 1259 1260 # none found 1261 if len(rows) == 0: 1262 return {} 1263 1264 vpks = [ ind['pk_vaccination'] for ind in rows ] 1265 vinds = [ ind['l10n_indication'] for ind in rows ] 1266 ind_counts = [ ind['indication_count'] for ind in rows ] 1267 1268 # turn them into vaccinations 1269 cmd = gmVaccination.sql_fetch_vaccination % u'pk_vaccination IN %(pks)s' 1270 args = {'pks': tuple(vpks)} 1271 rows, row_idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 1272 1273 vaccs = {} 1274 for idx in range(len(vpks)): 1275 pk = vpks[idx] 1276 ind_count = ind_counts[idx] 1277 for r in rows: 1278 if r['pk_vaccination'] == pk: 1279 vaccs[vinds[idx]] = (ind_count, gmVaccination.cVaccination(row = {'idx': row_idx, 'data': r, 'pk_field': 'pk_vaccination'})) 1280 1281 return vaccs
1282 #--------------------------------------------------------
1283 - def get_vaccinations(self, order_by=None, episodes=None, issues=None, encounters=None):
1284 1285 args = {'pat': self.pk_patient} 1286 where_parts = [u'pk_patient = %(pat)s'] 1287 1288 if order_by is None: 1289 order_by = u'' 1290 else: 1291 order_by = u'order by %s' % order_by 1292 1293 if (episodes is not None) and (len(episodes) > 0): 1294 where_parts.append(u'pk_episode IN %(epis)s') 1295 args['epis'] = tuple(episodes) 1296 1297 if (issues is not None) and (len(issues) > 0): 1298 where_parts.append(u'pk_episode IN (select pk from clin.episode where fk_health_issue IN %(issues)s)') 1299 args['issues'] = tuple(issues) 1300 1301 if (encounters is not None) and (len(encounters) > 0): 1302 where_parts.append(u'pk_encounter IN %(encs)s') 1303 args['encs'] = tuple(encounters) 1304 1305 cmd = u'%s %s' % ( 1306 gmVaccination.sql_fetch_vaccination % u'\nAND '.join(where_parts), 1307 order_by 1308 ) 1309 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 1310 vaccs = [ gmVaccination.cVaccination(row = {'idx': idx, 'data': r, 'pk_field': 'pk_vaccination'}) for r in rows ] 1311 1312 return vaccs
1313 #-------------------------------------------------------- 1314 # old/obsolete: 1315 #--------------------------------------------------------
1316 - def get_scheduled_vaccination_regimes(self, ID=None, indications=None):
1317 """Retrieves vaccination regimes the patient is on. 1318 1319 optional: 1320 * ID - PK of the vaccination regime 1321 * indications - indications we want to retrieve vaccination 1322 regimes for, must be primary language, not l10n_indication 1323 """ 1324 # FIXME: use course, not regime 1325 try: 1326 self.__db_cache['vaccinations']['scheduled regimes'] 1327 except KeyError: 1328 # retrieve vaccination regimes definitions 1329 self.__db_cache['vaccinations']['scheduled regimes'] = [] 1330 cmd = """SELECT distinct on(pk_course) pk_course 1331 FROM clin.v_vaccs_scheduled4pat 1332 WHERE pk_patient=%s""" 1333 rows = gmPG.run_ro_query('historica', cmd, None, self.pk_patient) 1334 if rows is None: 1335 _log.error('cannot retrieve scheduled vaccination courses') 1336 del self.__db_cache['vaccinations']['scheduled regimes'] 1337 return None 1338 # Instantiate vaccination items and keep cache 1339 for row in rows: 1340 self.__db_cache['vaccinations']['scheduled regimes'].append(gmVaccination.cVaccinationCourse(aPK_obj=row[0])) 1341 1342 # ok, let's constrain our list 1343 filtered_regimes = [] 1344 filtered_regimes.extend(self.__db_cache['vaccinations']['scheduled regimes']) 1345 if ID is not None: 1346 filtered_regimes = filter(lambda regime: regime['pk_course'] == ID, filtered_regimes) 1347 if len(filtered_regimes) == 0: 1348 _log.error('no vaccination course [%s] found for patient [%s]' % (ID, self.pk_patient)) 1349 return [] 1350 else: 1351 return filtered_regimes[0] 1352 if indications is not None: 1353 filtered_regimes = filter(lambda regime: regime['indication'] in indications, filtered_regimes) 1354 1355 return filtered_regimes
1356 #-------------------------------------------------------- 1357 # def get_vaccinated_indications(self): 1358 # """Retrieves patient vaccinated indications list. 1359 # 1360 # Note that this does NOT rely on the patient being on 1361 # some schedule or other but rather works with what the 1362 # patient has ACTUALLY been vaccinated against. This is 1363 # deliberate ! 1364 # """ 1365 # # most likely, vaccinations will be fetched close 1366 # # by so it makes sense to count on the cache being 1367 # # filled (or fill it for nearby use) 1368 # vaccinations = self.get_vaccinations() 1369 # if vaccinations is None: 1370 # _log.error('cannot load vaccinated indications for patient [%s]' % self.pk_patient) 1371 # return (False, [[_('ERROR: cannot retrieve vaccinated indications'), _('ERROR: cannot retrieve vaccinated indications')]]) 1372 # if len(vaccinations) == 0: 1373 # return (True, [[_('no vaccinations recorded'), _('no vaccinations recorded')]]) 1374 # v_indications = [] 1375 # for vacc in vaccinations: 1376 # tmp = [vacc['indication'], vacc['l10n_indication']] 1377 # # remove duplicates 1378 # if tmp in v_indications: 1379 # continue 1380 # v_indications.append(tmp) 1381 # return (True, v_indications) 1382 #--------------------------------------------------------
1383 - def get_vaccinations_old(self, ID=None, indications=None, since=None, until=None, encounters=None, episodes=None, issues=None):
1384 """Retrieves list of vaccinations the patient has received. 1385 1386 optional: 1387 * ID - PK of a vaccination 1388 * indications - indications we want to retrieve vaccination 1389 items for, must be primary language, not l10n_indication 1390 * since - initial date for allergy items 1391 * until - final date for allergy items 1392 * encounters - list of encounters whose allergies are to be retrieved 1393 * episodes - list of episodes whose allergies are to be retrieved 1394 * issues - list of health issues whose allergies are to be retrieved 1395 """ 1396 try: 1397 self.__db_cache['vaccinations']['vaccinated'] 1398 except KeyError: 1399 self.__db_cache['vaccinations']['vaccinated'] = [] 1400 # Important fetch ordering by indication, date to know if a vaccination is booster 1401 cmd= """SELECT * FROM clin.v_pat_vaccinations4indication 1402 WHERE pk_patient=%s 1403 order by indication, date""" 1404 rows, idx = gmPG.run_ro_query('historica', cmd, True, self.pk_patient) 1405 if rows is None: 1406 _log.error('cannot load given vaccinations for patient [%s]' % self.pk_patient) 1407 del self.__db_cache['vaccinations']['vaccinated'] 1408 return None 1409 # Instantiate vaccination items 1410 vaccs_by_ind = {} 1411 for row in rows: 1412 vacc_row = { 1413 'pk_field': 'pk_vaccination', 1414 'idx': idx, 1415 'data': row 1416 } 1417 vacc = gmVaccination.cVaccination(row=vacc_row) 1418 self.__db_cache['vaccinations']['vaccinated'].append(vacc) 1419 # keep them, ordered by indication 1420 try: 1421 vaccs_by_ind[vacc['indication']].append(vacc) 1422 except KeyError: 1423 vaccs_by_ind[vacc['indication']] = [vacc] 1424 1425 # calculate sequence number and is_booster 1426 for ind in vaccs_by_ind.keys(): 1427 vacc_regimes = self.get_scheduled_vaccination_regimes(indications = [ind]) 1428 for vacc in vaccs_by_ind[ind]: 1429 # due to the "order by indication, date" the vaccinations are in the 1430 # right temporal order inside the indication-keyed dicts 1431 seq_no = vaccs_by_ind[ind].index(vacc) + 1 1432 vacc['seq_no'] = seq_no 1433 # if no active schedule for indication we cannot 1434 # check for booster status (eg. seq_no > max_shot) 1435 if (vacc_regimes is None) or (len(vacc_regimes) == 0): 1436 continue 1437 if seq_no > vacc_regimes[0]['shots']: 1438 vacc['is_booster'] = True 1439 del vaccs_by_ind 1440 1441 # ok, let's constrain our list 1442 filtered_shots = [] 1443 filtered_shots.extend(self.__db_cache['vaccinations']['vaccinated']) 1444 if ID is not None: 1445 filtered_shots = filter(lambda shot: shot['pk_vaccination'] == ID, filtered_shots) 1446 if len(filtered_shots) == 0: 1447 _log.error('no vaccination [%s] found for patient [%s]' % (ID, self.pk_patient)) 1448 return None 1449 else: 1450 return filtered_shots[0] 1451 if since is not None: 1452 filtered_shots = filter(lambda shot: shot['date'] >= since, filtered_shots) 1453 if until is not None: 1454 filtered_shots = filter(lambda shot: shot['date'] < until, filtered_shots) 1455 if issues is not None: 1456 filtered_shots = filter(lambda shot: shot['pk_health_issue'] in issues, filtered_shots) 1457 if episodes is not None: 1458 filtered_shots = filter(lambda shot: shot['pk_episode'] in episodes, filtered_shots) 1459 if encounters is not None: 1460 filtered_shots = filter(lambda shot: shot['pk_encounter'] in encounters, filtered_shots) 1461 if indications is not None: 1462 filtered_shots = filter(lambda shot: shot['indication'] in indications, filtered_shots) 1463 return filtered_shots
1464 #--------------------------------------------------------
1465 - def get_scheduled_vaccinations(self, indications=None):
1466 """Retrieves vaccinations scheduled for a regime a patient is on. 1467 1468 The regime is referenced by its indication (not l10n) 1469 1470 * indications - List of indications (not l10n) of regimes we want scheduled 1471 vaccinations to be fetched for 1472 """ 1473 try: 1474 self.__db_cache['vaccinations']['scheduled'] 1475 except KeyError: 1476 self.__db_cache['vaccinations']['scheduled'] = [] 1477 cmd = """SELECT * FROM clin.v_vaccs_scheduled4pat WHERE pk_patient=%s""" 1478 rows, idx = gmPG.run_ro_query('historica', cmd, True, self.pk_patient) 1479 if rows is None: 1480 _log.error('cannot load scheduled vaccinations for patient [%s]' % self.pk_patient) 1481 del self.__db_cache['vaccinations']['scheduled'] 1482 return None 1483 # Instantiate vaccination items 1484 for row in rows: 1485 vacc_row = { 1486 'pk_field': 'pk_vacc_def', 1487 'idx': idx, 1488 'data': row 1489 } 1490 self.__db_cache['vaccinations']['scheduled'].append(gmVaccination.cScheduledVaccination(row = vacc_row)) 1491 1492 # ok, let's constrain our list 1493 if indications is None: 1494 return self.__db_cache['vaccinations']['scheduled'] 1495 filtered_shots = [] 1496 filtered_shots.extend(self.__db_cache['vaccinations']['scheduled']) 1497 filtered_shots = filter(lambda shot: shot['indication'] in indications, filtered_shots) 1498 return filtered_shots
1499 #--------------------------------------------------------
1500 - def get_missing_vaccinations(self, indications=None):
1501 try: 1502 self.__db_cache['vaccinations']['missing'] 1503 except KeyError: 1504 self.__db_cache['vaccinations']['missing'] = {} 1505 # 1) non-booster 1506 self.__db_cache['vaccinations']['missing']['due'] = [] 1507 # get list of (indication, seq_no) tuples 1508 cmd = "SELECT indication, seq_no FROM clin.v_pat_missing_vaccs WHERE pk_patient=%s" 1509 rows = gmPG.run_ro_query('historica', cmd, None, self.pk_patient) 1510 if rows is None: 1511 _log.error('error loading (indication, seq_no) for due/overdue vaccinations for patient [%s]' % self.pk_patient) 1512 return None 1513 pk_args = {'pat_id': self.pk_patient} 1514 if rows is not None: 1515 for row in rows: 1516 pk_args['indication'] = row[0] 1517 pk_args['seq_no'] = row[1] 1518 self.__db_cache['vaccinations']['missing']['due'].append(gmVaccination.cMissingVaccination(aPK_obj=pk_args)) 1519 1520 # 2) boosters 1521 self.__db_cache['vaccinations']['missing']['boosters'] = [] 1522 # get list of indications 1523 cmd = "SELECT indication, seq_no FROM clin.v_pat_missing_boosters WHERE pk_patient=%s" 1524 rows = gmPG.run_ro_query('historica', cmd, None, self.pk_patient) 1525 if rows is None: 1526 _log.error('error loading indications for missing boosters for patient [%s]' % self.pk_patient) 1527 return None 1528 pk_args = {'pat_id': self.pk_patient} 1529 if rows is not None: 1530 for row in rows: 1531 pk_args['indication'] = row[0] 1532 self.__db_cache['vaccinations']['missing']['boosters'].append(gmVaccination.cMissingBooster(aPK_obj=pk_args)) 1533 1534 # if any filters ... 1535 if indications is None: 1536 return self.__db_cache['vaccinations']['missing'] 1537 if len(indications) == 0: 1538 return self.__db_cache['vaccinations']['missing'] 1539 # ... apply them 1540 filtered_shots = { 1541 'due': [], 1542 'boosters': [] 1543 } 1544 for due_shot in self.__db_cache['vaccinations']['missing']['due']: 1545 if due_shot['indication'] in indications: #and due_shot not in filtered_shots['due']: 1546 filtered_shots['due'].append(due_shot) 1547 for due_shot in self.__db_cache['vaccinations']['missing']['boosters']: 1548 if due_shot['indication'] in indications: #and due_shot not in filtered_shots['boosters']: 1549 filtered_shots['boosters'].append(due_shot) 1550 return filtered_shots
1551 #------------------------------------------------------------------ 1552 # API: encounters 1553 #------------------------------------------------------------------
1554 - def _get_current_encounter(self):
1555 return self.__encounter
1556
1557 - def _set_current_encounter(self, encounter):
1558 1559 # first ever setting ? 1560 if self.__encounter is None: 1561 _log.debug('first setting of active encounter in this clinical record instance') 1562 else: 1563 _log.debug('switching of active encounter') 1564 # fail if the currently active encounter has unsaved changes 1565 if self.__encounter.is_modified(): 1566 _log.debug('unsaved changes in active encounter, cannot switch to another one') 1567 raise ValueError('unsaved changes in active encounter, cannot switch to another one') 1568 1569 # set the currently active encounter and announce that change 1570 if encounter['started'].strftime('%Y-%m-%d %H:%M') == encounter['last_affirmed'].strftime('%Y-%m-%d %H:%M'): 1571 encounter['last_affirmed'] = gmDateTime.pydt_now_here() # this will trigger an "encounter_mod_db" 1572 encounter.save() 1573 self.__encounter = encounter 1574 gmDispatcher.send(u'current_encounter_switched') 1575 1576 return True
1577 1578 current_encounter = property(_get_current_encounter, _set_current_encounter) 1579 active_encounter = property(_get_current_encounter, _set_current_encounter) 1580 #------------------------------------------------------------------
1582 1583 # 1) "very recent" encounter recorded ? 1584 if self.__activate_very_recent_encounter(): 1585 return True 1586 1587 # 2) "fairly recent" encounter recorded ? 1588 if self.__activate_fairly_recent_encounter(): 1589 return True 1590 1591 # 3) start a completely new encounter 1592 self.start_new_encounter() 1593 return True
1594 #------------------------------------------------------------------
1596 """Try to attach to a "very recent" encounter if there is one. 1597 1598 returns: 1599 False: no "very recent" encounter, create new one 1600 True: success 1601 """ 1602 cfg_db = gmCfg.cCfgSQL() 1603 min_ttl = cfg_db.get2 ( 1604 option = u'encounter.minimum_ttl', 1605 workplace = _here.active_workplace, 1606 bias = u'user', 1607 default = u'1 hour 30 minutes' 1608 ) 1609 cmd = u""" 1610 SELECT pk_encounter 1611 from clin.v_most_recent_encounters 1612 WHERE 1613 pk_patient = %s 1614 and 1615 last_affirmed > (now() - %s::interval)""" 1616 enc_rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient, min_ttl]}]) 1617 # none found 1618 if len(enc_rows) == 0: 1619 _log.debug('no <very recent> encounter (younger than [%s]) found' % min_ttl) 1620 return False 1621 # attach to existing 1622 self.current_encounter = gmEMRStructItems.cEncounter(aPK_obj=enc_rows[0][0]) 1623 _log.debug('"very recent" encounter [%s] found and re-activated' % enc_rows[0][0]) 1624 return True
1625 #------------------------------------------------------------------
1627 """Try to attach to a "fairly recent" encounter if there is one. 1628 1629 returns: 1630 False: no "fairly recent" encounter, create new one 1631 True: success 1632 """ 1633 cfg_db = gmCfg.cCfgSQL() 1634 min_ttl = cfg_db.get2 ( 1635 option = u'encounter.minimum_ttl', 1636 workplace = _here.active_workplace, 1637 bias = u'user', 1638 default = u'1 hour 30 minutes' 1639 ) 1640 max_ttl = cfg_db.get2 ( 1641 option = u'encounter.maximum_ttl', 1642 workplace = _here.active_workplace, 1643 bias = u'user', 1644 default = u'6 hours' 1645 ) 1646 cmd = u""" 1647 SELECT pk_encounter 1648 from clin.v_most_recent_encounters 1649 WHERE 1650 pk_patient=%s 1651 and 1652 last_affirmed between (now() - %s::interval) and (now() - %s::interval)""" 1653 enc_rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient, max_ttl, min_ttl]}]) 1654 # none found 1655 if len(enc_rows) == 0: 1656 _log.debug('no <fairly recent> encounter (between [%s] and [%s] old) found' % (min_ttl, max_ttl)) 1657 return False 1658 encounter = gmEMRStructItems.cEncounter(aPK_obj=enc_rows[0][0]) 1659 # ask user whether to attach or not 1660 cmd = u""" 1661 SELECT title, firstnames, lastnames, gender, dob 1662 from dem.v_basic_person WHERE pk_identity=%s""" 1663 pats, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient]}]) 1664 pat = pats[0] 1665 pat_str = u'%s %s %s (%s), %s [#%s]' % ( 1666 gmTools.coalesce(pat[0], u'')[:5], 1667 pat[1][:15], 1668 pat[2][:15], 1669 pat[3], 1670 pat[4].strftime('%x'), 1671 self.pk_patient 1672 ) 1673 enc = gmI18N.get_encoding() 1674 msg = _( 1675 '%s\n' 1676 '\n' 1677 "This patient's chart was worked on only recently:\n" 1678 '\n' 1679 ' %s %s - %s (%s)\n' 1680 '\n' 1681 ' Request: %s\n' 1682 ' Outcome: %s\n' 1683 '\n' 1684 'Do you want to continue that consultation\n' 1685 'or do you want to start a new one ?\n' 1686 ) % ( 1687 pat_str, 1688 encounter['started'].strftime('%x').decode(enc), 1689 encounter['started'].strftime('%H:%M'), encounter['last_affirmed'].strftime('%H:%M'), 1690 encounter['l10n_type'], 1691 gmTools.coalesce(encounter['reason_for_encounter'], _('none given')), 1692 gmTools.coalesce(encounter['assessment_of_encounter'], _('none given')), 1693 ) 1694 attach = False 1695 try: 1696 attach = _func_ask_user(msg = msg, caption = _('Starting patient encounter'), encounter = encounter) 1697 except: 1698 _log.exception('cannot ask user for guidance, not attaching to existing encounter') 1699 return False 1700 if not attach: 1701 return False 1702 1703 # attach to existing 1704 self.current_encounter = encounter 1705 1706 _log.debug('"fairly recent" encounter [%s] found and re-activated' % enc_rows[0][0]) 1707 return True
1708 #------------------------------------------------------------------
1709 - def start_new_encounter(self):
1710 cfg_db = gmCfg.cCfgSQL() 1711 # FIXME: look for MRU/MCU encounter type config here 1712 enc_type = cfg_db.get2 ( 1713 option = u'encounter.default_type', 1714 workplace = _here.active_workplace, 1715 bias = u'user', 1716 default = u'in surgery' 1717 ) 1718 self.current_encounter = gmEMRStructItems.create_encounter(fk_patient = self.pk_patient, enc_type = enc_type) 1719 _log.debug('new encounter [%s] initiated' % self.current_encounter['pk_encounter'])
1720 #------------------------------------------------------------------
1721 - def get_encounters(self, since=None, until=None, id_list=None, episodes=None, issues=None):
1722 """Retrieves patient's encounters. 1723 1724 id_list - PKs of encounters to fetch 1725 since - initial date for encounter items, DateTime instance 1726 until - final date for encounter items, DateTime instance 1727 episodes - PKs of the episodes the encounters belong to (many-to-many relation) 1728 issues - PKs of the health issues the encounters belong to (many-to-many relation) 1729 1730 NOTE: if you specify *both* issues and episodes 1731 you will get the *aggregate* of all encounters even 1732 if the episodes all belong to the health issues listed. 1733 IOW, the issues broaden the episode list rather than 1734 the episode list narrowing the episodes-from-issues 1735 list. 1736 Rationale: If it was the other way round it would be 1737 redundant to specify the list of issues at all. 1738 """ 1739 # fetch all encounters for patient 1740 cmd = u"SELECT * FROM clin.v_pat_encounters WHERE pk_patient=%s order by started" 1741 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient]}], get_col_idx=True) 1742 encounters = [] 1743 for r in rows: 1744 encounters.append(gmEMRStructItems.cEncounter(row={'data': r, 'idx': idx, 'pk_field': 'pk_encounter'})) 1745 1746 # we've got the encounters, start filtering 1747 filtered_encounters = [] 1748 filtered_encounters.extend(encounters) 1749 if id_list is not None: 1750 filtered_encounters = filter(lambda enc: enc['pk_encounter'] in id_list, filtered_encounters) 1751 if since is not None: 1752 filtered_encounters = filter(lambda enc: enc['started'] >= since, filtered_encounters) 1753 if until is not None: 1754 filtered_encounters = filter(lambda enc: enc['last_affirmed'] <= until, filtered_encounters) 1755 1756 if (issues is not None) and (len(issues) > 0): 1757 1758 issues = tuple(issues) 1759 1760 # Syan attests that an explicit union of child tables is way faster 1761 # as there seem to be problems with parent table expansion and use 1762 # of child table indexes, so if get_encounter() runs very slow on 1763 # your machine use the lines below 1764 1765 # rows = gmPG.run_ro_query('historica', cClinicalRecord._clin_root_item_children_union_query, None, (tuple(issues),)) 1766 # if rows is None: 1767 # _log.error('cannot load encounters for issues [%s] (patient [%s])' % (str(issues), self.pk_patient)) 1768 # else: 1769 # enc_ids = map(lambda x:x[0], rows) 1770 # filtered_encounters = filter(lambda enc: enc['pk_encounter'] in enc_ids, filtered_encounters) 1771 1772 # this problem seems fixed for us as of PostgreSQL 8.2 :-) 1773 1774 # however, this seems like the proper approach: 1775 # - find episodes corresponding to the health issues in question 1776 cmd = u"SELECT distinct pk FROM clin.episode WHERE fk_health_issue in %(issues)s" 1777 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'issues': issues}}]) 1778 epi_ids = map(lambda x:x[0], rows) 1779 if episodes is None: 1780 episodes = [] 1781 episodes.extend(epi_ids) 1782 1783 if (episodes is not None) and (len(episodes) > 0): 1784 1785 episodes = tuple(episodes) 1786 1787 # if the episodes to filter by belong to the patient in question so will 1788 # the encounters found with them - hence we don't need a WHERE on the patient ... 1789 cmd = u"SELECT distinct fk_encounter FROM clin.clin_root_item WHERE fk_episode in %(epis)s" 1790 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'epis': episodes}}]) 1791 enc_ids = map(lambda x:x[0], rows) 1792 filtered_encounters = filter(lambda enc: enc['pk_encounter'] in enc_ids, filtered_encounters) 1793 1794 return filtered_encounters
1795 #--------------------------------------------------------
1796 - def get_first_encounter(self, issue_id=None, episode_id=None):
1797 """Retrieves first encounter for a particular issue and/or episode 1798 1799 issue_id - First encounter associated health issue 1800 episode - First encounter associated episode 1801 """ 1802 # FIXME: use direct query 1803 1804 if issue_id is None: 1805 issues = None 1806 else: 1807 issues = [issue_id] 1808 1809 if episode_id is None: 1810 episodes = None 1811 else: 1812 episodes = [episode_id] 1813 1814 encounters = self.get_encounters(issues=issues, episodes=episodes) 1815 if len(encounters) == 0: 1816 return None 1817 1818 # FIXME: this does not scale particularly well, I assume 1819 encounters.sort(lambda x,y: cmp(x['started'], y['started'])) 1820 return encounters[0]
1821 #--------------------------------------------------------
1822 - def get_last_encounter(self, issue_id=None, episode_id=None):
1823 """Retrieves last encounter for a concrete issue and/or episode 1824 1825 issue_id - Last encounter associated health issue 1826 episode_id - Last encounter associated episode 1827 """ 1828 # FIXME: use direct query 1829 1830 if issue_id is None: 1831 issues = None 1832 else: 1833 issues = [issue_id] 1834 1835 if episode_id is None: 1836 episodes = None 1837 else: 1838 episodes = [episode_id] 1839 1840 encounters = self.get_encounters(issues=issues, episodes=episodes) 1841 if len(encounters) == 0: 1842 return None 1843 1844 # FIXME: this does not scale particularly well, I assume 1845 encounters.sort(lambda x,y: cmp(x['started'], y['started'])) 1846 return encounters[-1]
1847 #------------------------------------------------------------------
1848 - def get_last_but_one_encounter(self, issue_id=None, episode_id=None):
1849 1850 args = {'pat': self.pk_patient} 1851 1852 if (issue_id is None) and (episode_id is None): 1853 1854 cmd = u""" 1855 SELECT * FROM clin.v_pat_encounters 1856 WHERE pk_patient = %(pat)s 1857 order by started desc 1858 limit 2 1859 """ 1860 else: 1861 where_parts = [] 1862 1863 if issue_id is not None: 1864 where_parts.append(u'pk_health_issue = %(issue)s') 1865 args['issue'] = issue_id 1866 1867 if episode_id is not None: 1868 where_parts.append(u'pk_episode = %(epi)s') 1869 args['epi'] = episode_id 1870 1871 cmd = u""" 1872 SELECT * 1873 from clin.v_pat_encounters 1874 WHERE 1875 pk_patient = %%(pat)s 1876 and 1877 pk_encounter in ( 1878 SELECT distinct pk_encounter 1879 from clin.v_pat_narrative 1880 WHERE 1881 %s 1882 ) 1883 order by started desc 1884 limit 2 1885 """ % u' and '.join(where_parts) 1886 1887 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 1888 1889 if len(rows) == 0: 1890 return None 1891 1892 # just one encounter within the above limits 1893 if len(rows) == 1: 1894 # is it the current encounter ? 1895 if rows[0]['pk_encounter'] == self.current_encounter['pk_encounter']: 1896 # yes 1897 return None 1898 # no 1899 return gmEMRStructItems.cEncounter(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_encounter'}) 1900 1901 # more than one encounter 1902 if rows[0]['pk_encounter'] == self.current_encounter['pk_encounter']: 1903 return gmEMRStructItems.cEncounter(row = {'data': rows[1], 'idx': idx, 'pk_field': 'pk_encounter'}) 1904 1905 return gmEMRStructItems.cEncounter(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_encounter'})
1906 #------------------------------------------------------------------
1907 - def remove_empty_encounters(self):
1908 cfg_db = gmCfg.cCfgSQL() 1909 ttl = cfg_db.get2 ( 1910 option = u'encounter.ttl_if_empty', 1911 workplace = _here.active_workplace, 1912 bias = u'user', 1913 default = u'1 week' 1914 ) 1915 1916 # FIXME: this should be done async 1917 cmd = u""" 1918 delete FROM clin.encounter 1919 WHERE 1920 clin.encounter.fk_patient = %(pat)s 1921 and 1922 age(clin.encounter.last_affirmed) > %(ttl)s::interval 1923 and 1924 not exists (SELECT 1 FROM clin.clin_root_item WHERE fk_encounter = clin.encounter.pk) 1925 and 1926 not exists (SELECT 1 FROM blobs.doc_med WHERE fk_encounter = clin.encounter.pk) 1927 and 1928 not exists (SELECT 1 FROM clin.episode WHERE fk_encounter = clin.encounter.pk) 1929 and 1930 not exists (SELECT 1 FROM clin.health_issue WHERE fk_encounter = clin.encounter.pk) 1931 and 1932 not exists (SELECT 1 FROM clin.operation WHERE fk_encounter = clin.encounter.pk) 1933 and 1934 not exists (SELECT 1 FROM clin.allergy_state WHERE fk_encounter = clin.encounter.pk) 1935 """ 1936 try: 1937 rows, idx = gmPG2.run_rw_queries(queries = [{ 1938 'cmd': cmd, 1939 'args': {'pat': self.pk_patient, 'ttl': ttl} 1940 }]) 1941 except: 1942 _log.exception('error deleting empty encounters') 1943 1944 return True
1945 #------------------------------------------------------------------ 1946 # measurements API 1947 #------------------------------------------------------------------ 1948 # FIXME: use psyopg2 dbapi extension of named cursors - they are *server* side !
1949 - def get_test_types_for_results(self):
1950 """Retrieve data about test types for which this patient has results.""" 1951 1952 cmd = u""" 1953 SELECT * FROM ( 1954 SELECT DISTINCT ON (pk_test_type) pk_test_type, clin_when, unified_name 1955 FROM clin.v_test_results 1956 WHERE pk_patient = %(pat)s 1957 ) AS foo 1958 ORDER BY clin_when desc, unified_name 1959 """ 1960 args = {'pat': self.pk_patient} 1961 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 1962 return [ gmPathLab.cUnifiedTestType(aPK_obj = row['pk_test_type']) for row in rows ]
1963 #------------------------------------------------------------------
1964 - def get_test_types_details(self):
1965 """Retrieve details on tests grouped under unified names for this patient's results.""" 1966 cmd = u""" 1967 SELECT * FROM clin.v_unified_test_types WHERE pk_test_type in ( 1968 SELECT distinct on (unified_name, unified_abbrev) pk_test_type 1969 from clin.v_test_results 1970 WHERE pk_patient = %(pat)s 1971 ) 1972 order by unified_name""" 1973 args = {'pat': self.pk_patient} 1974 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 1975 return rows, idx
1976 #------------------------------------------------------------------
1977 - def get_dates_for_results(self):
1978 """Get the dates for which we have results.""" 1979 cmd = u""" 1980 SELECT distinct on (cwhen) date_trunc('day', clin_when) as cwhen 1981 from clin.v_test_results 1982 WHERE pk_patient = %(pat)s 1983 order by cwhen desc""" 1984 args = {'pat': self.pk_patient} 1985 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 1986 return rows
1987 #------------------------------------------------------------------
1988 - def get_test_results_by_date(self, encounter=None, episodes=None):
1989 1990 cmd = u""" 1991 SELECT *, xmin_test_result FROM clin.v_test_results 1992 WHERE pk_patient = %(pat)s 1993 order by clin_when desc, pk_episode, unified_name""" 1994 args = {'pat': self.pk_patient} 1995 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 1996 1997 tests = [ gmPathLab.cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ] 1998 1999 if episodes is not None: 2000 tests = [ t for t in tests if t['pk_episode'] in episodes ] 2001 2002 if encounter is not None: 2003 tests = [ t for t in tests if t['pk_encounter'] == encounter ] 2004 2005 return tests
2006 #------------------------------------------------------------------
2007 - def add_test_result(self, episode=None, type=None, intended_reviewer=None, val_num=None, val_alpha=None, unit=None):
2008 2009 try: 2010 epi = int(episode) 2011 except: 2012 epi = episode['pk_episode'] 2013 2014 try: 2015 type = int(type) 2016 except: 2017 type = type['pk_test_type'] 2018 2019 if intended_reviewer is None: 2020 from Gnumed.business import gmPerson 2021 intended_reviewer = _me['pk_staff'] 2022 2023 tr = gmPathLab.create_test_result ( 2024 encounter = self.current_encounter['pk_encounter'], 2025 episode = epi, 2026 type = type, 2027 intended_reviewer = intended_reviewer, 2028 val_num = val_num, 2029 val_alpha = val_alpha, 2030 unit = unit 2031 ) 2032 2033 return tr
2034 #------------------------------------------------------------------ 2035 #------------------------------------------------------------------
2036 - def get_lab_results(self, limit=None, since=None, until=None, encounters=None, episodes=None, issues=None):
2037 """Retrieves lab result clinical items. 2038 2039 limit - maximum number of results to retrieve 2040 since - initial date 2041 until - final date 2042 encounters - list of encounters 2043 episodes - list of episodes 2044 issues - list of health issues 2045 """ 2046 try: 2047 return self.__db_cache['lab results'] 2048 except KeyError: 2049 pass 2050 self.__db_cache['lab results'] = [] 2051 if limit is None: 2052 lim = '' 2053 else: 2054 # only use limit if all other constraints are None 2055 if since is None and until is None and encounters is None and episodes is None and issues is None: 2056 lim = "limit %s" % limit 2057 else: 2058 lim = '' 2059 2060 cmd = """SELECT * FROM clin.v_results4lab_req WHERE pk_patient=%%s %s""" % lim 2061 rows, idx = gmPG.run_ro_query('historica', cmd, True, self.pk_patient) 2062 if rows is None: 2063 return False 2064 for row in rows: 2065 lab_row = { 2066 'pk_field': 'pk_result', 2067 'idx': idx, 2068 'data': row 2069 } 2070 lab_result = gmPathLab.cLabResult(row=lab_row) 2071 self.__db_cache['lab results'].append(lab_result) 2072 2073 # ok, let's constrain our list 2074 filtered_lab_results = [] 2075 filtered_lab_results.extend(self.__db_cache['lab results']) 2076 if since is not None: 2077 filtered_lab_results = filter(lambda lres: lres['req_when'] >= since, filtered_lab_results) 2078 if until is not None: 2079 filtered_lab_results = filter(lambda lres: lres['req_when'] < until, filtered_lab_results) 2080 if issues is not None: 2081 filtered_lab_results = filter(lambda lres: lres['pk_health_issue'] in issues, filtered_lab_results) 2082 if episodes is not None: 2083 filtered_lab_results = filter(lambda lres: lres['pk_episode'] in episodes, filtered_lab_results) 2084 if encounters is not None: 2085 filtered_lab_results = filter(lambda lres: lres['pk_encounter'] in encounters, filtered_lab_results) 2086 return filtered_lab_results
2087 #------------------------------------------------------------------
2088 - def get_lab_request(self, pk=None, req_id=None, lab=None):
2089 # FIXME: verify that it is our patient ? ... 2090 req = gmPathLab.cLabRequest(aPK_obj=pk, req_id=req_id, lab=lab) 2091 return req
2092 #------------------------------------------------------------------
2093 - def add_lab_request(self, lab=None, req_id=None, encounter_id=None, episode_id=None):
2094 if encounter_id is None: 2095 encounter_id = self.current_encounter['pk_encounter'] 2096 status, data = gmPathLab.create_lab_request( 2097 lab=lab, 2098 req_id=req_id, 2099 pat_id=self.pk_patient, 2100 encounter_id=encounter_id, 2101 episode_id=episode_id 2102 ) 2103 if not status: 2104 _log.error(str(data)) 2105 return None 2106 return data
2107 #============================================================ 2108 # main 2109 #------------------------------------------------------------ 2110 if __name__ == "__main__": 2111 2112 if len(sys.argv) == 1: 2113 sys.exit() 2114 2115 if sys.argv[1] != 'test': 2116 sys.exit() 2117 2118 from Gnumed.pycommon import gmLog2 2119 #-----------------------------------------
2120 - def test_allergy_state():
2121 emr = cClinicalRecord(aPKey=1) 2122 state = emr.allergy_state 2123 print "allergy state is:", state 2124 2125 print "setting state to 0" 2126 emr.allergy_state = 0 2127 2128 print "setting state to None" 2129 emr.allergy_state = None 2130 2131 print "setting state to 'abc'" 2132 emr.allergy_state = 'abc'
2133 #-----------------------------------------
2134 - def test_get_test_names():
2135 emr = cClinicalRecord(aPKey=12) 2136 rows = emr.get_test_types_for_results() 2137 print "test result names:" 2138 for row in rows: 2139 print row
2140 #-----------------------------------------
2141 - def test_get_dates_for_results():
2142 emr = cClinicalRecord(aPKey=12) 2143 rows = emr.get_dates_for_results() 2144 print "test result dates:" 2145 for row in rows: 2146 print row
2147 #-----------------------------------------
2148 - def test_get_measurements():
2149 emr = cClinicalRecord(aPKey=12) 2150 rows, idx = emr.get_measurements_by_date() 2151 print "test results:" 2152 for row in rows: 2153 print row
2154 #-----------------------------------------
2155 - def test_get_test_results_by_date():
2156 emr = cClinicalRecord(aPKey=12) 2157 tests = emr.get_test_results_by_date() 2158 print "test results:" 2159 for test in tests: 2160 print test
2161 #-----------------------------------------
2162 - def test_get_test_types_details():
2163 emr = cClinicalRecord(aPKey=12) 2164 rows, idx = emr.get_test_types_details() 2165 print "test type details:" 2166 for row in rows: 2167 print row
2168 #-----------------------------------------
2169 - def test_get_statistics():
2170 emr = cClinicalRecord(aPKey=12) 2171 for key, item in emr.get_statistics().iteritems(): 2172 print key, ":", item
2173 #-----------------------------------------
2174 - def test_get_problems():
2175 emr = cClinicalRecord(aPKey=12) 2176 2177 probs = emr.get_problems() 2178 print "normal probs (%s):" % len(probs) 2179 for p in probs: 2180 print u'%s (%s)' % (p['problem'], p['type']) 2181 2182 probs = emr.get_problems(include_closed_episodes=True) 2183 print "probs + closed episodes (%s):" % len(probs) 2184 for p in probs: 2185 print u'%s (%s)' % (p['problem'], p['type']) 2186 2187 probs = emr.get_problems(include_irrelevant_issues=True) 2188 print "probs + issues (%s):" % len(probs) 2189 for p in probs: 2190 print u'%s (%s)' % (p['problem'], p['type']) 2191 2192 probs = emr.get_problems(include_closed_episodes=True, include_irrelevant_issues=True) 2193 print "probs + issues + epis (%s):" % len(probs) 2194 for p in probs: 2195 print u'%s (%s)' % (p['problem'], p['type'])
2196 #-----------------------------------------
2197 - def test_add_test_result():
2198 emr = cClinicalRecord(aPKey=12) 2199 tr = emr.add_test_result ( 2200 episode = 1, 2201 intended_reviewer = 1, 2202 type = 1, 2203 val_num = 75, 2204 val_alpha = u'somewhat obese', 2205 unit = u'kg' 2206 ) 2207 print tr
2208 #-----------------------------------------
2209 - def test_get_most_recent_episode():
2210 emr = cClinicalRecord(aPKey=12) 2211 print emr.get_most_recent_episode(issue = 2)
2212 #-----------------------------------------
2213 - def test_get_almost_recent_encounter():
2214 emr = cClinicalRecord(aPKey=12) 2215 print emr.get_last_encounter(issue_id=2) 2216 print emr.get_last_but_one_encounter(issue_id=2)
2217 #-----------------------------------------
2218 - def test_get_meds():
2219 emr = cClinicalRecord(aPKey=12) 2220 for med in emr.get_current_substance_intake(): 2221 print med
2222 #-----------------------------------------
2223 - def test_is_allergic_to():
2224 emr = cClinicalRecord(aPKey = 12) 2225 print emr.is_allergic_to(atcs = tuple(sys.argv[2:]), inns = tuple(sys.argv[2:]), brand = sys.argv[2])
2226 #----------------------------------------- 2227 #test_allergy_state() 2228 test_is_allergic_to() 2229 2230 #test_get_test_names() 2231 #test_get_dates_for_results() 2232 #test_get_measurements() 2233 #test_get_test_results_by_date() 2234 #test_get_test_types_details() 2235 #test_get_statistics() 2236 #test_get_problems() 2237 #test_add_test_result() 2238 #test_get_most_recent_episode() 2239 #test_get_almost_recent_encounter() 2240 #test_get_meds() 2241 2242 # emr = cClinicalRecord(aPKey = 12) 2243 2244 # # Vacc regimes 2245 # vacc_regimes = emr.get_scheduled_vaccination_regimes(indications = ['tetanus']) 2246 # print '\nVaccination regimes: ' 2247 # for a_regime in vacc_regimes: 2248 # pass 2249 # #print a_regime 2250 # vacc_regime = emr.get_scheduled_vaccination_regimes(ID=10) 2251 # #print vacc_regime 2252 2253 # # vaccination regimes and vaccinations for regimes 2254 # scheduled_vaccs = emr.get_scheduled_vaccinations(indications = ['tetanus']) 2255 # print 'Vaccinations for the regime:' 2256 # for a_scheduled_vacc in scheduled_vaccs: 2257 # pass 2258 # #print ' %s' %(a_scheduled_vacc) 2259 2260 # # vaccination next shot and booster 2261 # vaccinations = emr.get_vaccinations() 2262 # for a_vacc in vaccinations: 2263 # print '\nVaccination %s , date: %s, booster: %s, seq no: %s' %(a_vacc['batch_no'], a_vacc['date'].strftime('%Y-%m-%d'), a_vacc['is_booster'], a_vacc['seq_no']) 2264 2265 # # first and last encounters 2266 # first_encounter = emr.get_first_encounter(issue_id = 1) 2267 # print '\nFirst encounter: ' + str(first_encounter) 2268 # last_encounter = emr.get_last_encounter(episode_id = 1) 2269 # print '\nLast encounter: ' + str(last_encounter) 2270 # print '' 2271 2272 # # lab results 2273 # lab = emr.get_lab_results() 2274 # lab_file = open('lab-data.txt', 'wb') 2275 # for lab_result in lab: 2276 # lab_file.write(str(lab_result)) 2277 # lab_file.write('\n') 2278 # lab_file.close() 2279 2280 #dump = record.get_missing_vaccinations() 2281 #f = open('vaccs.lst', 'wb') 2282 #if dump is not None: 2283 # print "=== due ===" 2284 # f.write("=== due ===\n") 2285 # for row in dump['due']: 2286 # print row 2287 # f.write(repr(row)) 2288 # f.write('\n') 2289 # print "=== overdue ===" 2290 # f.write("=== overdue ===\n") 2291 # for row in dump['overdue']: 2292 # print row 2293 # f.write(repr(row)) 2294 # f.write('\n') 2295 #f.close() 2296