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

Source Code for Module Gnumed.business.gmClinicalRecord

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