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