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