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