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

Source Code for Module Gnumed.business.gmClinicalRecord

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