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

Source Code for Module Gnumed.business.gmClinicalRecord

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