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

Source Code for Module Gnumed.business.gmClinicalRecord

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