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