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