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