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

Source Code for Module Gnumed.business.gmClinicalRecord

   1  # -*- coding: utf-8 -*- 
   2  """GNUmed clinical patient record.""" 
   3  #============================================================ 
   4  __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>" 
   5  __license__ = "GPL v2 or later" 
   6   
   7  # standard libs 
   8  import sys 
   9  import logging 
  10  import threading 
  11  import datetime as pydt 
  12   
  13  if __name__ == '__main__': 
  14          sys.path.insert(0, '../../') 
  15   
  16  from Gnumed.pycommon import gmI18N 
  17  from Gnumed.pycommon import gmDateTime 
  18   
  19  if __name__ == '__main__': 
  20          from Gnumed.pycommon import gmLog2 
  21          gmI18N.activate_locale() 
  22          gmI18N.install_domain() 
  23          gmDateTime.init() 
  24   
  25  from Gnumed.pycommon import gmExceptions 
  26  from Gnumed.pycommon import gmPG2 
  27  from Gnumed.pycommon import gmDispatcher 
  28  from Gnumed.pycommon import gmCfg 
  29  from Gnumed.pycommon import gmTools 
  30   
  31  from Gnumed.business import gmGenericEMRItem 
  32  from Gnumed.business import gmAllergy 
  33  from Gnumed.business import gmPathLab 
  34  from Gnumed.business import gmLOINC 
  35  from Gnumed.business import gmClinNarrative 
  36  from Gnumed.business import gmSoapDefs 
  37  from Gnumed.business import gmEMRStructItems 
  38  from Gnumed.business import gmMedication 
  39  from Gnumed.business import gmVaccination 
  40  from Gnumed.business import gmFamilyHistory 
  41  from Gnumed.business import gmExternalCare 
  42  from Gnumed.business import gmOrganization 
  43  from Gnumed.business import gmAutoHints 
  44  from Gnumed.business.gmDemographicRecord import get_occupations 
  45   
  46   
  47  _log = logging.getLogger('gm.emr') 
  48   
  49  _here = None 
  50  #============================================================ 
  51  # helper functions 
  52  #------------------------------------------------------------ 
  53  #_func_ask_user = None 
  54  # 
  55  #def set_func_ask_user(a_func = None): 
  56  #       if not callable(a_func): 
  57  #               _log.error('[%] not callable, not setting _func_ask_user', a_func) 
  58  #               return False 
  59  # 
  60  #       _log.debug('setting _func_ask_user to [%s]', a_func) 
  61  # 
  62  #       global _func_ask_user 
  63  #       _func_ask_user = a_func 
  64   
  65  #============================================================ 
  66  from Gnumed.business.gmDocuments import cDocument 
  67  from Gnumed.business.gmProviderInbox import cInboxMessage 
  68   
  69  _map_table2class = { 
  70          'clin.encounter': gmEMRStructItems.cEncounter, 
  71          'clin.episode': gmEMRStructItems.cEpisode, 
  72          'clin.health_issue': gmEMRStructItems.cHealthIssue, 
  73          'clin.external_care': gmExternalCare.cExternalCareItem, 
  74          'clin.vaccination': gmVaccination.cVaccination, 
  75          'clin.clin_narrative': gmClinNarrative.cNarrative, 
  76          'clin.test_result': gmPathLab.cTestResult, 
  77          'clin.substance_intake': gmMedication.cSubstanceIntakeEntry, 
  78          'clin.hospital_stay': gmEMRStructItems.cHospitalStay, 
  79          'clin.procedure': gmEMRStructItems.cPerformedProcedure, 
  80          'clin.allergy': gmAllergy.cAllergy, 
  81          'clin.allergy_state': gmAllergy.cAllergyState, 
  82          'clin.family_history': gmFamilyHistory.cFamilyHistory, 
  83          'clin.suppressed_hint': gmAutoHints.cSuppressedHint, 
  84          'blobs.doc_med': cDocument, 
  85          'dem.message_inbox': cInboxMessage, 
  86          'ref.auto_hint': gmAutoHints.cDynamicHint 
  87  } 
  88   
89 -def instantiate_clin_root_item(table, pk):
90 try: 91 item_class = _map_table2class[table] 92 except KeyError: 93 _log.error('unmapped clin_root_item entry [%s], cannot instantiate', table) 94 return None 95 96 return item_class(aPK_obj = pk)
97 98 #------------------------------------------------------------
99 -def format_clin_root_item(table, pk, patient=None):
100 101 instance = instantiate_clin_root_item(table, pk) 102 if instance is None: 103 return _('cannot instantiate clinical root item <%s(%s)>' % (table, pk)) 104 105 # if patient is not None: 106 # if patient.ID != instance['pk_patient']: 107 # raise ValueError(u'patient passed in: [%s], but instance is: [%s:%s:%s]' % (patient.ID, table, pk, instance['pk_patient'])) 108 109 if hasattr(instance, 'format_maximum_information'): 110 return '\n'.join(instance.format_maximum_information(patient = patient)) 111 112 if hasattr(instance, 'format'): 113 try: 114 formatted = instance.format(patient = patient) 115 except TypeError: 116 formatted = instance.format() 117 if type(formatted) == type([]): 118 return '\n'.join(formatted) 119 return formatted 120 121 d = instance.fields_as_dict ( 122 date_format = '%Y %b %d %H:%M', 123 none_string = gmTools.u_diameter, 124 escape_style = None, 125 bool_strings = [_('True'), _('False')] 126 ) 127 return gmTools.format_dict_like(d, tabular = True, value_delimiters = None)
128 129 #============================================================
130 -def __noop_delayed_execute(*args, **kwargs):
131 pass
132 133 134 _delayed_execute = __noop_delayed_execute 135 136
137 -def set_delayed_executor(executor):
138 if not callable(executor): 139 raise TypeError('executor <%s> is not callable' % executor) 140 global _delayed_execute 141 _delayed_execute = executor 142 _log.debug('setting delayed executor to <%s>', executor)
143 144 #------------------------------------------------------------
145 -class cClinicalRecord(object):
146
147 - def __init__(self, aPKey=None):#, allow_user_interaction=True, encounter=None):
148 """Fails if 149 150 - no connection to database possible 151 - patient referenced by aPKey does not exist 152 """ 153 self.pk_patient = aPKey # == identity.pk == primary key 154 self.gender = None 155 self.dob = None 156 157 from Gnumed.business import gmPraxis 158 global _here 159 if _here is None: 160 _here = gmPraxis.gmCurrentPraxisBranch() 161 162 self.__encounter = None 163 self.__setup_active_encounter() 164 165 # register backend notification interests 166 # (keep this last so we won't hang on threads when 167 # failing this constructor for other reasons ...) 168 if not self._register_interests(): 169 raise gmExceptions.ConstructorError("cannot register signal interests") 170 171 gmAllergy.ensure_has_allergy_state(encounter = self.current_encounter['pk_encounter']) 172 #_delayed_execute(gmAllergy.ensure_has_allergy_state, encounter = self.current_encounter['pk_encounter']) 173 174 self.__calculator = None 175 176 _log.debug('Instantiated clinical record for patient [%s].' % self.pk_patient)
177 178 # #-------------------------------------------------------- 179 # def __old_style_init(self, allow_user_interaction=True): 180 # 181 # _log.error('%s.__old_style_init() used', self.__class__.__name__) 182 # print u'*** GNUmed [%s]: __old_style_init() used ***' % self.__class__.__name__ 183 # 184 # # FIXME: delegate to worker thread 185 # # log access to patient record (HIPAA, for example) 186 # cmd = u'SELECT gm.log_access2emr(%(todo)s)' 187 # args = {'todo': u'patient [%s]' % self.pk_patient} 188 # gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}]) 189 # 190 # # load current or create new encounter 191 # if _func_ask_user is None: 192 # _log.error('[_func_ask_user] is None') 193 # print "*** GNUmed [%s]: _func_ask_user is not set ***" % self.__class__.__name__ 194 # 195 ## # FIXME: delegate to worker thread ? 196 # self.remove_empty_encounters() 197 # 198 # self.__encounter = None 199 # if not self.__initiate_active_encounter(allow_user_interaction = allow_user_interaction): 200 # raise gmExceptions.ConstructorError("cannot activate an encounter for patient [%s]" % self.pk_patient) 201 # 202 ## # FIXME: delegate to worker thread 203 # gmAllergy.ensure_has_allergy_state(encounter = self.current_encounter['pk_encounter']) 204 205 #--------------------------------------------------------
206 - def cleanup(self):
207 _log.debug('cleaning up after clinical record for patient [%s]' % self.pk_patient) 208 if self.__encounter is not None: 209 self.__encounter.unlock(exclusive = False) 210 return True
211 212 #--------------------------------------------------------
213 - def log_access(self, action=None):
214 if action is None: 215 action = 'EMR access for pk_identity [%s]' % self.pk_patient 216 args = {'action': action} 217 cmd = 'SELECT gm.log_access2emr(%(action)s)' 218 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
219 220 #--------------------------------------------------------
221 - def _get_calculator(self):
222 if self.__calculator is None: 223 from Gnumed.business.gmClinicalCalculator import cClinicalCalculator 224 self.__calculator = cClinicalCalculator() 225 from Gnumed.business.gmPerson import gmCurrentPatient 226 curr_pat = gmCurrentPatient() 227 if curr_pat.ID == self.pk_patient: 228 self.__calculator.patient = curr_pat 229 else: 230 from Gnumed.business.gmPerson import cPatient 231 self.__calculator.patient = cPatient(self.pk_patient) 232 return self.__calculator
233 234 calculator = property(_get_calculator, lambda x:x) 235 236 #-------------------------------------------------------- 237 # messaging 238 #--------------------------------------------------------
239 - def _register_interests(self):
240 #gmDispatcher.connect(signal = u'clin.encounter_mod_db', receiver = self.db_callback_encounter_mod_db) 241 gmDispatcher.connect(signal = 'gm_table_mod', receiver = self.db_modification_callback) 242 243 return True
244 245 #--------------------------------------------------------
246 - def db_modification_callback(self, **kwds):
247 248 if kwds['table'] != 'clin.encounter': 249 return True 250 if self.current_encounter is None: 251 _log.debug('no local current-encounter, ignoring encounter modification signal') 252 return True 253 if int(kwds['pk_of_row']) != self.current_encounter['pk_encounter']: 254 _log.debug('modified encounter [%s] != local encounter [%s], ignoring signal', kwds['pk_of_row'], self.current_encounter['pk_encounter']) 255 return True 256 257 _log.debug('modification of our encounter (%s) signalled (%s)', self.current_encounter['pk_encounter'], kwds['pk_of_row']) 258 259 # get the current encounter as an extra instance 260 # from the database to check for changes 261 curr_enc_in_db = gmEMRStructItems.cEncounter(aPK_obj = self.current_encounter['pk_encounter']) 262 263 # the encounter just retrieved and the active encounter 264 # have got the same transaction ID so there's no change 265 # in the database, there could be a local change in 266 # the active encounter but that doesn't matter because 267 # no one else can have written to the DB so far 268 if curr_enc_in_db['xmin_encounter'] == self.current_encounter['xmin_encounter']: 269 _log.debug('same XMIN, no difference between DB and in-client instance of current encounter expected') 270 if self.current_encounter.is_modified(): 271 _log.error('encounter modification signal from DB with same XMIN as in local in-client instance of encounter BUT local instance ALSO has .is_modified()=True') 272 _log.error('this hints at an error in .is_modified handling') 273 gmTools.compare_dict_likes(self.current_encounter.fields_as_dict(), curr_enc_in_db.fields_as_dict(), 'modified enc in client', 'enc loaded from DB') 274 return True 275 276 # there must have been a change to the active encounter 277 # committed to the database from elsewhere, 278 # we must fail propagating the change, however, if 279 # there are local changes pending 280 if self.current_encounter.is_modified(): 281 gmTools.compare_dict_likes(self.current_encounter.fields_as_dict(), curr_enc_in_db.fields_as_dict(), 'modified enc in client', 'signalled enc loaded from DB') 282 raise ValueError('unsaved changes in locally active encounter [%s], cannot switch to DB state of encounter [%s]' % ( 283 self.current_encounter['pk_encounter'], 284 curr_enc_in_db['pk_encounter'] 285 )) 286 287 # don't do this: same_payload() does not compare _all_ fields 288 # so we can get into a reality disconnect if we don't 289 # announce the mod 290 # if self.current_encounter.same_payload(another_object = curr_enc_in_db): 291 # _log.debug('clin.encounter_mod_db received but no change to active encounter payload') 292 # return True 293 294 # there was a change in the database from elsewhere, 295 # locally, however, we don't have any pending changes, 296 # therefore we can propagate the remote change locally 297 # without losing anything 298 # this really should be the standard case 299 gmTools.compare_dict_likes(self.current_encounter.fields_as_dict(), curr_enc_in_db.fields_as_dict(), 'modified enc in client', 'enc loaded from DB') 300 _log.debug('active encounter modified remotely, no locally pending changes, reloading from DB and locally announcing the remote modification') 301 self.current_encounter.refetch_payload() 302 gmDispatcher.send('current_encounter_modified') 303 304 return True
305 306 #--------------------------------------------------------
307 - def db_callback_encounter_mod_db(self, **kwds):
308 309 # get the current encounter as an extra instance 310 # from the database to check for changes 311 curr_enc_in_db = gmEMRStructItems.cEncounter(aPK_obj = self.current_encounter['pk_encounter']) 312 313 # the encounter just retrieved and the active encounter 314 # have got the same transaction ID so there's no change 315 # in the database, there could be a local change in 316 # the active encounter but that doesn't matter because 317 # no one else can have written to the DB so far 318 # THIS DOES NOT WORK 319 # if curr_enc_in_db['xmin_encounter'] == self.current_encounter['xmin_encounter']: 320 # return True 321 322 # there must have been a change to the active encounter 323 # committed to the database from elsewhere, 324 # we must fail propagating the change, however, if 325 # there are local changes 326 if self.current_encounter.is_modified(): 327 gmTools.compare_dict_likes(self.current_encounter.fields_as_dict(), curr_enc_in_db.fields_as_dict(), 'modified enc in client', 'enc loaded from DB') 328 _log.error('current in client: %s', self.current_encounter) 329 raise ValueError('unsaved changes in active encounter [%s], cannot switch [%s]' % ( 330 self.current_encounter['pk_encounter'], 331 curr_enc_in_db['pk_encounter'] 332 )) 333 334 if self.current_encounter.same_payload(another_object = curr_enc_in_db): 335 _log.debug('clin.encounter_mod_db received but no change to active encounter payload') 336 return True 337 338 # there was a change in the database from elsewhere, 339 # locally, however, we don't have any changes, therefore 340 # we can propagate the remote change locally without 341 # losing anything 342 gmTools.compare_dict_likes(self.current_encounter.fields_as_dict(), curr_enc_in_db.fields_as_dict(), 'modified enc in client', 'enc loaded from DB') 343 _log.debug('active encounter modified remotely, reloading from DB and locally announcing the modification') 344 self.current_encounter.refetch_payload() 345 gmDispatcher.send('current_encounter_modified') 346 347 return True
348 349 #-------------------------------------------------------- 350 # API: family history 351 #--------------------------------------------------------
352 - def get_family_history(self, episodes=None, issues=None, encounters=None):
353 fhx = gmFamilyHistory.get_family_history ( 354 order_by = 'l10n_relation, condition', 355 patient = self.pk_patient 356 ) 357 358 if episodes is not None: 359 fhx = [ f for f in fhx if f['pk_episode'] in episodes ] 360 361 if issues is not None: 362 fhx = [ f for f in fhx if f['pk_health_issue'] in issues ] 363 364 if encounters is not None: 365 fhx = [ f for f in fhx if f['pk_encounter'] in encounters ] 366 367 return fhx
368 369 #--------------------------------------------------------
370 - def add_family_history(self, episode=None, condition=None, relation=None):
371 return gmFamilyHistory.create_family_history ( 372 encounter = self.current_encounter['pk_encounter'], 373 episode = episode, 374 condition = condition, 375 relation = relation 376 )
377 378 #-------------------------------------------------------- 379 # API: pregnancy 380 #--------------------------------------------------------
381 - def _get_gender(self):
382 if self.__gender is not None: 383 return self.__gender 384 cmd = 'SELECT gender, dob FROM dem.v_all_persons WHERE pk_identity = %(pat)s' 385 args = {'pat': self.pk_patient} 386 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 387 self.__gender = rows[0]['gender'] 388 self.__dob = rows[0]['dob']
389
390 - def _set_gender(self, gender):
391 self.__gender = gender
392 393 gender = property(_get_gender, _set_gender) 394 395 #--------------------------------------------------------
396 - def _get_dob(self):
397 if self.__dob is not None: 398 return self.__dob 399 cmd = 'SELECT gender, dob FROM dem.v_all_persons WHERE pk_identity = %(pat)s' 400 args = {'pat': self.pk_patient} 401 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 402 self.__gender = rows[0]['gender'] 403 self.__dob = rows[0]['dob']
404
405 - def _set_dob(self, dob):
406 self.__dob = dob
407 408 dob = property(_get_dob, _set_dob) 409 410 #--------------------------------------------------------
411 - def _get_EDC(self):
412 cmd = 'SELECT edc FROM clin.patient WHERE fk_identity = %(pat)s' 413 args = {'pat': self.pk_patient} 414 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 415 if len(rows) == 0: 416 return None 417 return rows[0]['edc']
418
419 - def _set_EDC(self, edc):
420 cmd = """ 421 INSERT INTO clin.patient (fk_identity, edc) SELECT 422 %(pat)s, 423 %(edc)s 424 WHERE NOT EXISTS ( 425 SELECT 1 FROM clin.patient WHERE fk_identity = %(pat)s 426 ) 427 RETURNING pk""" 428 args = {'pat': self.pk_patient, 'edc': edc} 429 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False, return_data = True) 430 if len(rows) == 0: 431 cmd = 'UPDATE clin.patient SET edc = %(edc)s WHERE fk_identity = %(pat)s' 432 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
433 434 EDC = property(_get_EDC, _set_EDC) 435 436 #--------------------------------------------------------
437 - def _EDC_is_fishy(self):
438 edc = self.EDC 439 if edc is None: 440 return False 441 if self.gender != 'f': 442 return True 443 now = gmDateTime.pydt_now_here() 444 # mother too young 445 if (self.dob + pydt.timedelta(weeks = 5 * 52)) > now: 446 return True 447 # mother too old 448 if (self.dob + pydt.timedelta(weeks = 55 * 52)) < now: 449 return True 450 # Beulah Hunter, 375 days (http://www.reference.com/motif/health/longest-human-pregnancy-on-record) 451 # EDC too far in the future 452 if (edc - pydt.timedelta(days = 380)) > now: 453 return True 454 # even if the pregnancy would have *started* when it 455 # was documented to *end* it would be over by now by 456 # all accounts 457 # EDC too far in the past 458 if edc < (now - pydt.timedelta(days = 380)): 459 return True
460 461 EDC_is_fishy = property(_EDC_is_fishy, lambda x:x) 462 463 #--------------------------------------------------------
464 - def __normalize_smoking_details(self, details):
465 try: 466 details['quit_when'] 467 except KeyError: 468 details['quit_when'] = None 469 470 try: 471 details['last_confirmed'] 472 if details['last_confirmed'] is None: 473 details['last_confirmed'] = gmDateTime.pydt_now_here() 474 except KeyError: 475 details['last_confirmed'] = gmDateTime.pydt_now_here() 476 477 try: 478 details['comment'] 479 if details['comment'].strip() == '': 480 details['comment'] = None 481 except KeyError: 482 details['comment'] = None 483 484 return details
485 486 #--------------------------------------------------------
487 - def _get_smoking_status(self):
488 use = self.harmful_substance_use 489 if use is None: 490 return None 491 return use['tobacco']
492
493 - def _set_smoking_status(self, status):
494 # valid ? 495 status_flag, details = status 496 self.__harmful_substance_use = None 497 args = { 498 'pat': self.pk_patient, 499 'status': status_flag 500 } 501 if status_flag is None: 502 cmd = 'UPDATE clin.patient SET smoking_status = %(status)s, smoking_details = NULL WHERE fk_identity = %(pat)s' 503 elif status_flag == 0: 504 details['quit_when'] = None 505 args['details'] = gmTools.dict2json(self.__normalize_smoking_details(details)) 506 cmd = 'UPDATE clin.patient SET smoking_status = %(status)s, smoking_details = %(details)s WHERE fk_identity = %(pat)s' 507 else: 508 args['details'] = gmTools.dict2json(self.__normalize_smoking_details(details)) 509 cmd = 'UPDATE clin.patient SET smoking_status = %(status)s, smoking_details = %(details)s WHERE fk_identity = %(pat)s' 510 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
511 512 smoking_status = property(_get_smoking_status, _set_smoking_status) 513 514 #--------------------------------------------------------
515 - def _get_alcohol_status(self):
516 use = self.harmful_substance_use 517 if use is None: 518 return None 519 return use['alcohol']
520
521 - def _set_alcohol_status(self, status):
522 # valid ? 523 harmful, details = status 524 self.__harmful_substance_use = None 525 args = {'pat': self.pk_patient} 526 if harmful is None: 527 cmd = 'UPDATE clin.patient SET c2_currently_harmful_use = NULL, c2_details = NULL WHERE fk_identity = %(pat)s' 528 elif harmful is False: 529 cmd = 'UPDATE clin.patient SET c2_currently_harmful_use = FALSE, c2_details = gm.nullify_empty_string(%(details)s) WHERE fk_identity = %(pat)s' 530 else: 531 cmd = 'UPDATE clin.patient SET c2_currently_harmful_use = TRUE, c2_details = gm.nullify_empty_string(%(details)s) WHERE fk_identity = %(pat)s' 532 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
533 534 alcohol_status = property(_get_alcohol_status, _set_alcohol_status) 535 536 #--------------------------------------------------------
537 - def _get_drugs_status(self):
538 use = self.harmful_substance_use 539 if use is None: 540 return None 541 return use['drugs']
542
543 - def _set_drugs_status(self, status):
544 # valid ? 545 harmful, details = status 546 self.__harmful_substance_use = None 547 args = {'pat': self.pk_patient} 548 if harmful is None: 549 cmd = 'UPDATE clin.patient SET drugs_currently_harmful_use = NULL, drugs_details = NULL WHERE fk_identity = %(pat)s' 550 elif harmful is False: 551 cmd = 'UPDATE clin.patient SET drugs_currently_harmful_use = FALSE, drugs_details = gm.nullify_empty_string(%(details)s) WHERE fk_identity = %(pat)s' 552 else: 553 cmd = 'UPDATE clin.patient SET drugs_currently_harmful_use = TRUE, drugs_details = gm.nullify_empty_string(%(details)s) WHERE fk_identity = %(pat)s' 554 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
555 556 drugs_status = property(_get_drugs_status, _set_drugs_status) 557 558 #--------------------------------------------------------
559 - def _get_harmful_substance_use(self):
560 # caching does not take into account status changes from elsewhere 561 try: 562 self.__harmful_substance_use 563 except AttributeError: 564 self.__harmful_substance_use = None 565 566 if self.__harmful_substance_use is not None: 567 return self.__harmful_substance_use 568 569 args = {'pat': self.pk_patient} 570 cmd = """ 571 SELECT 572 -- tobacco use 573 smoking_status, 574 smoking_details, 575 (smoking_details->>'last_confirmed')::timestamp with time zone 576 AS ts_last, 577 (smoking_details->>'quit_when')::timestamp with time zone 578 AS ts_quit, 579 -- c2 use 580 c2_currently_harmful_use, 581 c2_details, 582 -- other drugs use 583 drugs_currently_harmful_use, 584 drugs_details 585 FROM clin.patient 586 WHERE fk_identity = %(pat)s 587 """ 588 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 589 if len(rows) == 0: 590 return None 591 # disentangle smoking 592 status = rows[0]['smoking_status'] 593 details = rows[0]['smoking_details'] 594 if status is not None: 595 details['last_confirmed'] = rows[0]['ts_last'] 596 details['quit_when'] = rows[0]['ts_quit'] 597 # set fields 598 self.__harmful_substance_use = { 599 'tobacco': (status, details), 600 'alcohol': (rows[0]['c2_currently_harmful_use'], rows[0]['c2_details']), 601 'drugs': (rows[0]['drugs_currently_harmful_use'], rows[0]['drugs_details']) 602 } 603 604 return self.__harmful_substance_use
605 606
607 - def _get_harmful_substance_use2(self):
608 cmd = 'SELECT * FROM clin.v_substance_intakes WHERE harmful_use_type = %s'
609 610 harmful_substance_use = property(_get_harmful_substance_use, lambda x:x) 611 612 #--------------------------------------------------------
613 - def format_harmful_substance_use(self, include_tobacco=True, include_alcohol=True, include_drugs=True, include_nonuse=True, include_unknown=True):
614 use = self.harmful_substance_use 615 if use is None: 616 return [] 617 618 lines = [] 619 620 if include_tobacco: 621 status, details = use['tobacco'] 622 add_details = False 623 if status is None: 624 if include_unknown: 625 lines.append(_('unknown smoking status')) 626 elif status == 0: 627 if include_nonuse: 628 lines.append('%s (%s)' % (_('non-smoker'), gmDateTime.pydt_strftime(details['last_confirmed'], '%Y %b %d'))) 629 add_details = True 630 elif status == 1: # now or previous 631 if details['quit_when'] is None: 632 lines.append('%s (%s)' % (_('current smoker'), gmDateTime.pydt_strftime(details['last_confirmed'], '%Y %b %d'))) 633 add_details = True 634 elif details['quit_when'] < gmDateTime.pydt_now_here(): 635 if include_nonuse: 636 lines.append('%s (%s)' % (_('ex-smoker'), gmDateTime.pydt_strftime(details['last_confirmed'], '%Y %b %d'))) 637 add_details = True 638 else: 639 lines.append('%s (%s)' % (_('current smoker'), gmDateTime.pydt_strftime(details['last_confirmed'], '%Y %b %d'))) 640 add_details = True 641 elif status == 2: # addicted 642 lines.append('%s (%s)' % (_('tobacco addiction'), gmDateTime.pydt_strftime(details['last_confirmed'], '%Y %b %d'))) 643 add_details = True 644 if add_details: 645 if details['quit_when'] is not None: 646 lines.append(' %s: %s' % (_('Quit date'), gmDateTime.pydt_strftime(details['quit_when'], '%Y %b %d'))) 647 if details['comment'] is not None: 648 lines.append(' %s' % details['comment']) 649 650 if include_alcohol: 651 status, details = use['alcohol'] 652 if status is False: 653 if include_nonuse: 654 if len(lines) > 0: 655 lines.append('') 656 lines.append(_('no or non-harmful alcohol use')) 657 lines.append(' %s' % details) 658 elif status is True: 659 if len(lines) > 0: 660 lines.append('') 661 lines.append(_('harmful alcohol use')) 662 lines.append(' %s' % details) 663 else: 664 if include_unknown: 665 if len(lines) > 0: 666 lines.append('') 667 lines.append(_('unknown alcohol use')) 668 lines.append(' %s' % details) 669 670 if include_drugs: 671 status, details = use['drugs'] 672 if status is False: 673 if include_nonuse: 674 if len(lines) > 0: 675 lines.append('') 676 lines.append(_('no or non-harmful drug use')) 677 lines.append(' %s' % details) 678 elif status is True: 679 if len(lines) > 0: 680 lines.append('') 681 lines.append(_('harmful drug use')) 682 lines.append(' %s' % details) 683 else: 684 if include_unknown: 685 if len(lines) > 0: 686 lines.append('') 687 lines.append(_('unknown drug use')) 688 lines.append(' %s' % details) 689 690 return lines
691 692 #--------------------------------------------------------
693 - def _get_currently_abuses_substances(self):
694 # returns True / False / None (= unknown) 695 696 use = self.harmful_substance_use 697 # we know that at least one group is used: 698 if use['alcohol'][0] is True: 699 return True 700 if use['drugs'][0] is True: 701 return True 702 if use['tobacco'][0] > 0: 703 # is True: 704 if use['tobacco'][1]['quit_when'] is None: 705 return True 706 # at this point no group is currently used for sure 707 # we don't know about some of the groups so we can NOT say: no abuse at all: 708 if use['alcohol'][0] is None: 709 return None 710 if use['drugs'][0] is None: 711 return None 712 if use['tobacco'][0] is None: 713 return None 714 # at this point all groups must be FALSE, except for 715 # tobacco which can also be TRUE _but_, if so, a quit 716 # date has been set, which is considered non-abuse 717 return False
718 719 currently_abuses_substances = property(_get_currently_abuses_substances, lambda x:x) 720 721 #-------------------------------------------------------- 722 # API: performed procedures 723 #--------------------------------------------------------
724 - def get_performed_procedures(self, episodes=None, issues=None):
725 726 procs = gmEMRStructItems.get_performed_procedures(patient = self.pk_patient) 727 728 if episodes is not None: 729 procs = [ p for p in procs if p['pk_episode'] in episodes ] 730 731 if issues is not None: 732 procs = [ p for p in procs if p['pk_health_issue'] in issues ] 733 734 return procs
735 736 performed_procedures = property(get_performed_procedures, lambda x:x) 737 #--------------------------------------------------------
738 - def get_latest_performed_procedure(self):
739 return gmEMRStructItems.get_latest_performed_procedure(patient = self.pk_patient)
740 #--------------------------------------------------------
741 - def add_performed_procedure(self, episode=None, location=None, hospital_stay=None, procedure=None):
742 return gmEMRStructItems.create_performed_procedure ( 743 encounter = self.current_encounter['pk_encounter'], 744 episode = episode, 745 location = location, 746 hospital_stay = hospital_stay, 747 procedure = procedure 748 )
749 #--------------------------------------------------------
750 - def get_procedure_locations_as_org_units(self):
751 where = 'pk_org_unit IN (SELECT DISTINCT pk_org_unit FROM clin.v_procedures_not_at_hospital WHERE pk_patient = %(pat)s)' 752 args = {'pat': self.pk_patient} 753 cmd = gmOrganization._SQL_get_org_unit % where 754 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 755 return [ gmOrganization.cOrgUnit(row = {'pk_field': 'pk_org_unit', 'data': r, 'idx': idx}) for r in rows ]
756 757 #-------------------------------------------------------- 758 # API: hospitalizations 759 #--------------------------------------------------------
760 - def get_hospital_stays(self, episodes=None, issues=None, ongoing_only=False):
761 stays = gmEMRStructItems.get_patient_hospital_stays(patient = self.pk_patient, ongoing_only = ongoing_only) 762 if episodes is not None: 763 stays = [ s for s in stays if s['pk_episode'] in episodes ] 764 if issues is not None: 765 stays = [ s for s in stays if s['pk_health_issue'] in issues ] 766 return stays
767 768 hospital_stays = property(get_hospital_stays, lambda x:x) 769 #--------------------------------------------------------
770 - def get_latest_hospital_stay(self):
771 return gmEMRStructItems.get_latest_patient_hospital_stay(patient = self.pk_patient)
772 #--------------------------------------------------------
773 - def add_hospital_stay(self, episode=None, fk_org_unit=None):
774 return gmEMRStructItems.create_hospital_stay ( 775 encounter = self.current_encounter['pk_encounter'], 776 episode = episode, 777 fk_org_unit = fk_org_unit 778 )
779 #--------------------------------------------------------
780 - def get_hospital_stay_stats_by_hospital(self, cover_period=None):
781 args = {'pat': self.pk_patient, 'range': cover_period} 782 where_parts = ['pk_patient = %(pat)s'] 783 if cover_period is not None: 784 where_parts.append('discharge > (now() - %(range)s)') 785 786 cmd = """ 787 SELECT hospital, count(1) AS frequency 788 FROM clin.v_hospital_stays 789 WHERE 790 %s 791 GROUP BY hospital 792 ORDER BY frequency DESC 793 """ % ' AND '.join(where_parts) 794 795 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 796 return rows
797 #--------------------------------------------------------
798 - def get_attended_hospitals_as_org_units(self):
799 where = 'pk_org_unit IN (SELECT DISTINCT pk_org_unit FROM clin.v_hospital_stays WHERE pk_patient = %(pat)s)' 800 args = {'pat': self.pk_patient} 801 cmd = gmOrganization._SQL_get_org_unit % where 802 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 803 return [ gmOrganization.cOrgUnit(row = {'pk_field': 'pk_org_unit', 'data': r, 'idx': idx}) for r in rows ]
804 805 #-------------------------------------------------------- 806 # API: narrative 807 #--------------------------------------------------------
808 - def add_notes(self, notes=None, episode=None, encounter=None):
809 enc = gmTools.coalesce ( 810 encounter, 811 self.current_encounter['pk_encounter'] 812 ) 813 for note in notes: 814 gmClinNarrative.create_narrative_item ( 815 narrative = note[1], 816 soap_cat = note[0], 817 episode_id = episode, 818 encounter_id = enc 819 ) 820 return True
821 822 #--------------------------------------------------------
823 - def add_clin_narrative(self, note='', soap_cat='s', episode=None, link_obj=None):
824 if note.strip() == '': 825 _log.info('will not create empty clinical note') 826 return None 827 if isinstance(episode, gmEMRStructItems.cEpisode): 828 episode = episode['pk_episode'] 829 instance = gmClinNarrative.create_narrative_item ( 830 link_obj = link_obj, 831 narrative = note, 832 soap_cat = soap_cat, 833 episode_id = episode, 834 encounter_id = self.current_encounter['pk_encounter'] 835 ) 836 return instance
837 838 #--------------------------------------------------------
839 - def get_clin_narrative(self, encounters=None, episodes=None, issues=None, soap_cats=None, providers=None):
840 """Get SOAP notes pertinent to this encounter. 841 842 encounters 843 - list of encounters the narrative of which are to be retrieved 844 episodes 845 - list of episodes the narrative of which are to be retrieved 846 issues 847 - list of health issues the narrative of which are to be retrieved 848 soap_cats 849 - list of SOAP categories of the narrative to be retrieved 850 """ 851 where_parts = ['pk_patient = %(pat)s'] 852 args = {'pat': self.pk_patient} 853 854 if issues is not None: 855 where_parts.append('pk_health_issue IN %(issues)s') 856 if len(issues) == 0: 857 args['issues'] = tuple() 858 else: 859 if isinstance(issues[0], gmEMRStructItems.cHealthIssue): 860 args['issues'] = tuple([ i['pk_health_issue'] for i in issues ]) 861 elif isinstance(issues[0], int): 862 args['issues'] = tuple(issues) 863 else: 864 raise ValueError('<issues> must be list of type int (=pk) or cHealthIssue, but 1st issue is: %s' % issues[0]) 865 866 if episodes is not None: 867 where_parts.append('pk_episode IN %(epis)s') 868 if len(episodes) == 0: 869 args['epis'] = tuple() 870 else: 871 if isinstance(episodes[0], gmEMRStructItems.cEpisode): 872 args['epis'] = tuple([ e['pk_episode'] for e in episodes ]) 873 elif isinstance(episodes[0], int): 874 args['epis'] = tuple(episodes) 875 else: 876 raise ValueError('<episodes> must be list of type int (=pk) or cEpisode, but 1st episode is: %s' % episodes[0]) 877 878 if encounters is not None: 879 where_parts.append('pk_encounter IN %(encs)s') 880 if len(encounters) == 0: 881 args['encs'] = tuple() 882 else: 883 if isinstance(encounters[0], gmEMRStructItems.cEncounter): 884 args['encs'] = tuple([ e['pk_encounter'] for e in encounters ]) 885 elif isinstance(encounters[0], int): 886 args['encs'] = tuple(encounters) 887 else: 888 raise ValueError('<encounters> must be list of type int (=pk) or cEncounter, but 1st encounter is: %s' % encounters[0]) 889 890 if soap_cats is not None: 891 where_parts.append('c_vn.soap_cat IN %(cats)s') 892 args['cats'] = tuple(gmSoapDefs.soap_cats2list(soap_cats)) 893 894 if providers is not None: 895 where_parts.append('c_vn.modified_by IN %(docs)s') 896 args['docs'] = tuple(providers) 897 898 cmd = """ 899 SELECT 900 c_vn.*, 901 c_scr.rank AS soap_rank 902 FROM 903 clin.v_narrative c_vn 904 LEFT JOIN clin.soap_cat_ranks c_scr on c_vn.soap_cat = c_scr.soap_cat 905 WHERE %s 906 ORDER BY date, soap_rank 907 """ % ' AND '.join(where_parts) 908 909 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 910 return [ gmClinNarrative.cNarrative(row = {'pk_field': 'pk_narrative', 'idx': idx, 'data': row}) for row in rows ]
911 912 #--------------------------------------------------------
913 - def search_narrative_simple(self, search_term=''):
914 915 search_term = search_term.strip() 916 if search_term == '': 917 return [] 918 919 cmd = """ 920 SELECT 921 *, 922 coalesce((SELECT description FROM clin.episode WHERE pk = vn4s.pk_episode), vn4s.src_table) 923 as episode, 924 coalesce((SELECT description FROM clin.health_issue WHERE pk = vn4s.pk_health_issue), vn4s.src_table) 925 as health_issue, 926 (SELECT started FROM clin.encounter WHERE pk = vn4s.pk_encounter) 927 as encounter_started, 928 (SELECT last_affirmed FROM clin.encounter WHERE pk = vn4s.pk_encounter) 929 as encounter_ended, 930 (SELECT _(description) FROM clin.encounter_type WHERE pk = (SELECT fk_type FROM clin.encounter WHERE pk = vn4s.pk_encounter)) 931 as encounter_type 932 -- CHANGE BACK IN V23: 933 --FROM clin.v_narrative4search vn4s 934 FROM v_narrative4search vn4s 935 WHERE 936 pk_patient = %(pat)s and 937 vn4s.narrative ~ %(term)s 938 order by 939 encounter_started 940 """ # case sensitive 941 #rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pat': self.pk_patient, 'term': search_term}}]) 942 queries = [ 943 {'cmd': gmClinNarrative._VIEW_clin_v_narrative4search}, 944 {'cmd': cmd, 'args': {'pat': self.pk_patient, 'term': search_term}} 945 ] 946 rows, idx = gmPG2.run_rw_queries(queries = queries, get_col_idx = True, return_data = True) 947 return rows
948 949 #--------------------------------------------------------
950 - def get_text_dump(self, since=None, until=None, encounters=None, episodes=None, issues=None):
951 fields = [ 952 'age', 953 "to_char(modified_when, 'YYYY-MM-DD @ HH24:MI') as modified_when", 954 'modified_by', 955 'clin_when', 956 "case is_modified when false then '%s' else '%s' end as modified_string" % (_('original entry'), _('modified entry')), 957 'pk_item', 958 'pk_encounter', 959 'pk_episode', 960 'pk_health_issue', 961 'src_table' 962 ] 963 select_from = "SELECT %s FROM clin.v_pat_items" % ', '.join(fields) 964 # handle constraint conditions 965 where_snippets = [] 966 params = {} 967 where_snippets.append('pk_patient=%(pat_id)s') 968 params['pat_id'] = self.pk_patient 969 if not since is None: 970 where_snippets.append('clin_when >= %(since)s') 971 params['since'] = since 972 if not until is None: 973 where_snippets.append('clin_when <= %(until)s') 974 params['until'] = until 975 # FIXME: these are interrelated, eg if we constrain encounter 976 # we automatically constrain issue/episode, so handle that, 977 # encounters 978 if not encounters is None and len(encounters) > 0: 979 params['enc'] = encounters 980 if len(encounters) > 1: 981 where_snippets.append('fk_encounter in %(enc)s') 982 else: 983 where_snippets.append('fk_encounter=%(enc)s') 984 # episodes 985 if not episodes is None and len(episodes) > 0: 986 params['epi'] = episodes 987 if len(episodes) > 1: 988 where_snippets.append('fk_episode in %(epi)s') 989 else: 990 where_snippets.append('fk_episode=%(epi)s') 991 # health issues 992 if not issues is None and len(issues) > 0: 993 params['issue'] = issues 994 if len(issues) > 1: 995 where_snippets.append('fk_health_issue in %(issue)s') 996 else: 997 where_snippets.append('fk_health_issue=%(issue)s') 998 999 where_clause = ' and '.join(where_snippets) 1000 order_by = 'order by src_table, age' 1001 cmd = "%s WHERE %s %s" % (select_from, where_clause, order_by) 1002 1003 rows, view_col_idx = gmPG.run_ro_query('historica', cmd, 1, params) 1004 if rows is None: 1005 _log.error('cannot load item links for patient [%s]' % self.pk_patient) 1006 return None 1007 1008 # -- sort the data -- 1009 # FIXME: by issue/encounter/episode, eg formatting 1010 # aggregate by src_table for item retrieval 1011 items_by_table = {} 1012 for item in rows: 1013 src_table = item[view_col_idx['src_table']] 1014 pk_item = item[view_col_idx['pk_item']] 1015 if src_table not in items_by_table: 1016 items_by_table[src_table] = {} 1017 items_by_table[src_table][pk_item] = item 1018 1019 # get mapping for issue/episode IDs 1020 issues = self.get_health_issues() 1021 issue_map = {} 1022 for issue in issues: 1023 issue_map[issue['pk_health_issue']] = issue['description'] 1024 episodes = self.get_episodes() 1025 episode_map = {} 1026 for episode in episodes: 1027 episode_map[episode['pk_episode']] = episode['description'] 1028 emr_data = {} 1029 # get item data from all source tables 1030 ro_conn = self._conn_pool.GetConnection('historica') 1031 curs = ro_conn.cursor() 1032 for src_table in items_by_table: 1033 item_ids = list(items_by_table[src_table]) 1034 # we don't know anything about the columns of 1035 # the source tables but, hey, this is a dump 1036 if len(item_ids) == 0: 1037 _log.info('no items in table [%s] ?!?' % src_table) 1038 continue 1039 elif len(item_ids) == 1: 1040 cmd = "SELECT * FROM %s WHERE pk_item=%%s order by modified_when" % src_table 1041 if not gmPG.run_query(curs, None, cmd, item_ids[0]): 1042 _log.error('cannot load items from table [%s]' % src_table) 1043 # skip this table 1044 continue 1045 elif len(item_ids) > 1: 1046 cmd = "SELECT * FROM %s WHERE pk_item in %%s order by modified_when" % src_table 1047 if not gmPG.run_query(curs, None, cmd, (tuple(item_ids),)): 1048 _log.error('cannot load items from table [%s]' % src_table) 1049 # skip this table 1050 continue 1051 rows = curs.fetchall() 1052 table_col_idx = gmPG.get_col_indices(curs) 1053 # format per-table items 1054 for row in rows: 1055 # FIXME: make this get_pkey_name() 1056 pk_item = row[table_col_idx['pk_item']] 1057 view_row = items_by_table[src_table][pk_item] 1058 age = view_row[view_col_idx['age']] 1059 # format metadata 1060 try: 1061 episode_name = episode_map[view_row[view_col_idx['pk_episode']]] 1062 except Exception: 1063 episode_name = view_row[view_col_idx['pk_episode']] 1064 try: 1065 issue_name = issue_map[view_row[view_col_idx['pk_health_issue']]] 1066 except Exception: 1067 issue_name = view_row[view_col_idx['pk_health_issue']] 1068 1069 if age not in emr_data: 1070 emr_data[age] = [] 1071 1072 emr_data[age].append( 1073 _('%s: encounter (%s)') % ( 1074 view_row[view_col_idx['clin_when']], 1075 view_row[view_col_idx['pk_encounter']] 1076 ) 1077 ) 1078 emr_data[age].append(_('health issue: %s') % issue_name) 1079 emr_data[age].append(_('episode : %s') % episode_name) 1080 # format table specific data columns 1081 # - ignore those, they are metadata, some 1082 # are in clin.v_pat_items data already 1083 cols2ignore = [ 1084 'pk_audit', 'row_version', 'modified_when', 'modified_by', 1085 'pk_item', 'id', 'fk_encounter', 'fk_episode', 'pk' 1086 ] 1087 col_data = [] 1088 for col_name in table_col_idx: 1089 if col_name in cols2ignore: 1090 continue 1091 emr_data[age].append("=> %s: %s" % (col_name, row[table_col_idx[col_name]])) 1092 emr_data[age].append("----------------------------------------------------") 1093 emr_data[age].append("-- %s from table %s" % ( 1094 view_row[view_col_idx['modified_string']], 1095 src_table 1096 )) 1097 emr_data[age].append("-- written %s by %s" % ( 1098 view_row[view_col_idx['modified_when']], 1099 view_row[view_col_idx['modified_by']] 1100 )) 1101 emr_data[age].append("----------------------------------------------------") 1102 curs.close() 1103 return emr_data
1104 #--------------------------------------------------------
1105 - def get_patient_ID(self):
1106 return self.pk_patient
1107 #--------------------------------------------------------
1108 - def get_statistics(self):
1109 union_query = '\n union all\n'.join ([ 1110 """ 1111 SELECT (( 1112 -- all relevant health issues + active episodes WITH health issue 1113 SELECT COUNT(1) 1114 FROM clin.v_problem_list 1115 WHERE 1116 pk_patient = %(pat)s 1117 AND 1118 pk_health_issue is not null 1119 ) + ( 1120 -- active episodes WITHOUT health issue 1121 SELECT COUNT(1) 1122 FROM clin.v_problem_list 1123 WHERE 1124 pk_patient = %(pat)s 1125 AND 1126 pk_health_issue is null 1127 ))""", 1128 'SELECT count(1) FROM clin.encounter WHERE fk_patient = %(pat)s', 1129 'SELECT count(1) FROM clin.v_pat_items WHERE pk_patient = %(pat)s', 1130 'SELECT count(1) FROM blobs.v_doc_med WHERE pk_patient = %(pat)s', 1131 'SELECT count(1) FROM clin.v_test_results WHERE pk_patient = %(pat)s', 1132 'SELECT count(1) FROM clin.v_hospital_stays WHERE pk_patient = %(pat)s', 1133 'SELECT count(1) FROM clin.v_procedures WHERE pk_patient = %(pat)s', 1134 # active and approved substances == medication 1135 """ 1136 SELECT count(1) 1137 FROM clin.v_substance_intakes 1138 WHERE 1139 pk_patient = %(pat)s 1140 AND 1141 is_currently_active IN (null, true) 1142 AND 1143 intake_is_approved_of IN (null, true)""", 1144 'SELECT count(1) FROM clin.v_vaccinations WHERE pk_patient = %(pat)s' 1145 ]) 1146 1147 rows, idx = gmPG2.run_ro_queries ( 1148 queries = [{'cmd': union_query, 'args': {'pat': self.pk_patient}}], 1149 get_col_idx = False 1150 ) 1151 1152 stats = dict ( 1153 problems = rows[0][0], 1154 encounters = rows[1][0], 1155 items = rows[2][0], 1156 documents = rows[3][0], 1157 results = rows[4][0], 1158 stays = rows[5][0], 1159 procedures = rows[6][0], 1160 active_drugs = rows[7][0], 1161 vaccinations = rows[8][0] 1162 ) 1163 1164 return stats
1165 #--------------------------------------------------------
1166 - def format_statistics(self):
1167 return _( 1168 'Medical problems: %(problems)s\n' 1169 'Total encounters: %(encounters)s\n' 1170 'Total EMR entries: %(items)s\n' 1171 'Active medications: %(active_drugs)s\n' 1172 'Documents: %(documents)s\n' 1173 'Test results: %(results)s\n' 1174 'Hospitalizations: %(stays)s\n' 1175 'Procedures: %(procedures)s\n' 1176 'Vaccinations: %(vaccinations)s' 1177 ) % self.get_statistics()
1178 #--------------------------------------------------------
1179 - def format_summary(self):
1180 1181 cmd = "SELECT dob FROM dem.v_all_persons WHERE pk_identity = %(pk)s" 1182 args = {'pk': self.pk_patient} 1183 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 1184 dob = rows[0]['dob'] 1185 1186 stats = self.get_statistics() 1187 first = self.get_first_encounter() 1188 last = self.get_last_encounter() 1189 probs = self.get_problems() 1190 1191 txt = '' 1192 if len(probs) > 0: 1193 txt += _(' %s known problems, clinically relevant thereof:\n') % stats['problems'] 1194 else: 1195 txt += _(' %s known problems\n') % stats['problems'] 1196 for prob in probs: 1197 if not prob['clinically_relevant']: 1198 continue 1199 txt += ' \u00BB%s\u00AB (%s)\n' % ( 1200 prob['problem'], 1201 gmTools.bool2subst(prob['problem_active'], _('active'), _('inactive')) 1202 ) 1203 txt += '\n' 1204 txt += _(' %s encounters from %s to %s\n') % ( 1205 stats['encounters'], 1206 gmDateTime.pydt_strftime(first['started'], '%Y %b %d'), 1207 gmDateTime.pydt_strftime(last['started'], '%Y %b %d') 1208 ) 1209 txt += _(' %s active medications\n') % stats['active_drugs'] 1210 txt += _(' %s documents\n') % stats['documents'] 1211 txt += _(' %s test results\n') % stats['results'] 1212 txt += _(' %s hospitalizations') % stats['stays'] 1213 if stats['stays'] == 0: 1214 txt += '\n' 1215 else: 1216 txt += _(', most recently:\n%s\n') % self.get_latest_hospital_stay().format(left_margin = 3) 1217 # FIXME: perhaps only count "ongoing ones" 1218 txt += _(' %s performed procedures') % stats['procedures'] 1219 if stats['procedures'] == 0: 1220 txt += '\n' 1221 else: 1222 txt += _(', most recently:\n%s\n') % self.get_latest_performed_procedure().format(left_margin = 3) 1223 1224 txt += '\n' 1225 txt += _('Allergies and Intolerances\n') 1226 1227 allg_state = self.allergy_state 1228 txt += (' ' + allg_state.state_string) 1229 if allg_state['last_confirmed'] is not None: 1230 txt += _(' (last confirmed %s)') % gmDateTime.pydt_strftime(allg_state['last_confirmed'], '%Y %b %d') 1231 txt += '\n' 1232 txt += gmTools.coalesce(allg_state['comment'], '', ' %s\n') 1233 for allg in self.get_allergies(): 1234 txt += ' %s: %s\n' % ( 1235 allg['descriptor'], 1236 gmTools.coalesce(allg['reaction'], _('unknown reaction')) 1237 ) 1238 1239 meds = self.get_current_medications(order_by = 'intake_is_approved_of DESC, substance') 1240 if len(meds) > 0: 1241 txt += '\n' 1242 txt += _('Medications and Substances') 1243 txt += '\n' 1244 for m in meds: 1245 txt += '%s\n' % m.format_as_single_line(left_margin = 1) 1246 1247 fhx = self.get_family_history() 1248 if len(fhx) > 0: 1249 txt += '\n' 1250 txt += _('Family History') 1251 txt += '\n' 1252 for f in fhx: 1253 txt += '%s\n' % f.format(left_margin = 1) 1254 1255 jobs = get_occupations(pk_identity = self.pk_patient) 1256 if len(jobs) > 0: 1257 txt += '\n' 1258 txt += _('Occupations') 1259 txt += '\n' 1260 for job in jobs: 1261 txt += ' %s%s\n' % ( 1262 job['l10n_occupation'], 1263 gmTools.coalesce(job['activities'], '', ': %s') 1264 ) 1265 1266 vaccs = self.get_latest_vaccinations() 1267 if len(vaccs) > 0: 1268 txt += '\n' 1269 txt += _('Vaccinations') 1270 txt += '\n' 1271 inds = sorted(vaccs) 1272 for ind in inds: 1273 ind_count, vacc = vaccs[ind] 1274 if dob is None: 1275 age_given = '' 1276 else: 1277 age_given = ' @ %s' % gmDateTime.format_apparent_age_medically(gmDateTime.calculate_apparent_age ( 1278 start = dob, 1279 end = vacc['date_given'] 1280 )) 1281 since = _('%s ago') % gmDateTime.format_interval_medically(vacc['interval_since_given']) 1282 txt += ' %s (%s%s): %s%s (%s %s%s%s)\n' % ( 1283 ind, 1284 gmTools.u_sum, 1285 ind_count, 1286 #gmDateTime.pydt_strftime(vacc['date_given'], '%b %Y'), 1287 since, 1288 age_given, 1289 vacc['vaccine'], 1290 gmTools.u_left_double_angle_quote, 1291 vacc['batch_no'], 1292 gmTools.u_right_double_angle_quote 1293 ) 1294 1295 care = self.get_external_care_items(order_by = 'issue, organization, unit, provider', exclude_inactive = True) 1296 if len(care) > 0: 1297 txt += '\n' 1298 txt += _('External care') 1299 txt += '\n' 1300 for item in care: 1301 txt += ' %s: %s\n' % ( 1302 item['issue'], 1303 gmTools.coalesce ( 1304 item['provider'], 1305 '%s@%s' % (item['unit'], item['organization']), 1306 '%%s (%s@%s)' % (item['unit'], item['organization']) 1307 ) 1308 ) 1309 1310 return txt
1311 1312 #--------------------------------------------------------
1313 - def format_as_journal(self, left_margin=0, patient=None):
1314 txt = '' 1315 for enc in self.get_encounters(skip_empty = True): 1316 txt += gmTools.u_box_horiz_4dashes * 70 + '\n' 1317 txt += enc.format ( 1318 episodes = None, # means: each touched upon 1319 left_margin = left_margin, 1320 patient = patient, 1321 fancy_header = False, 1322 with_soap = True, 1323 with_docs = True, 1324 with_tests = True, 1325 with_vaccinations = True, 1326 with_co_encountlet_hints = False, # irrelevant 1327 with_rfe_aoe = True, 1328 with_family_history = True, 1329 by_episode = True 1330 ) 1331 1332 return txt
1333 1334 #--------------------------------------------------------
1335 - 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):
1336 return gmClinNarrative.get_as_journal ( 1337 patient = self.pk_patient, 1338 since = since, 1339 until = until, 1340 encounters = encounters, 1341 episodes = episodes, 1342 issues = issues, 1343 soap_cats = soap_cats, 1344 providers = providers, 1345 order_by = order_by, 1346 time_range = time_range, 1347 active_encounter = self.active_encounter 1348 )
1349 1350 #------------------------------------------------------------------
1351 - def get_generic_emr_items(self, pk_encounters=None, pk_episodes=None, pk_health_issues=None, use_active_encounter=False, order_by=None):
1352 if use_active_encounter: 1353 active_encounter = self.active_encounter 1354 else: 1355 active_encounter = None 1356 return gmGenericEMRItem.get_generic_emr_items ( 1357 patient = self.pk_patient, 1358 encounters = pk_encounters, 1359 episodes = pk_episodes, 1360 issues = pk_health_issues, 1361 active_encounter = active_encounter, 1362 order_by = order_by 1363 )
1364 1365 #-------------------------------------------------------- 1366 # API: allergy 1367 #--------------------------------------------------------
1368 - def get_allergies(self, remove_sensitivities=False, since=None, until=None, encounters=None, episodes=None, issues=None, ID_list=None):
1369 """Retrieves patient allergy items. 1370 1371 remove_sensitivities 1372 - retrieve real allergies only, without sensitivities 1373 since 1374 - initial date for allergy items 1375 until 1376 - final date for allergy items 1377 encounters 1378 - list of encounters whose allergies are to be retrieved 1379 episodes 1380 - list of episodes whose allergies are to be retrieved 1381 issues 1382 - list of health issues whose allergies are to be retrieved 1383 """ 1384 cmd = "SELECT * FROM clin.v_pat_allergies WHERE pk_patient=%s order by descriptor" 1385 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient]}], get_col_idx = True) 1386 filtered_allergies = [] 1387 for r in rows: 1388 filtered_allergies.append(gmAllergy.cAllergy(row = {'data': r, 'idx': idx, 'pk_field': 'pk_allergy'})) 1389 1390 # ok, let's constrain our list 1391 if ID_list is not None: 1392 filtered_allergies = [ allg for allg in filtered_allergies if allg['pk_allergy'] in ID_list ] 1393 if len(filtered_allergies) == 0: 1394 _log.error('no allergies of list [%s] found for patient [%s]' % (str(ID_list), self.pk_patient)) 1395 # better fail here contrary to what we do elsewhere 1396 return None 1397 else: 1398 return filtered_allergies 1399 1400 if remove_sensitivities: 1401 filtered_allergies = [ allg for allg in filtered_allergies if allg['type'] == 'allergy' ] 1402 if since is not None: 1403 filtered_allergies = [ allg for allg in filtered_allergies if allg['date'] >= since ] 1404 if until is not None: 1405 filtered_allergies = [ allg for allg in filtered_allergies if allg['date'] < until ] 1406 if issues is not None: 1407 filtered_allergies = [ allg for allg in filtered_allergies if allg['pk_health_issue'] in issues ] 1408 if episodes is not None: 1409 filtered_allergies = [ allg for allg in filtered_allergies if allg['pk_episode'] in episodes ] 1410 if encounters is not None: 1411 filtered_allergies = [ allg for allg in filtered_allergies if allg['pk_encounter'] in encounters ] 1412 1413 return filtered_allergies
1414 #--------------------------------------------------------
1415 - def add_allergy(self, allergene=None, allg_type=None, encounter_id=None, episode_id=None):
1416 if encounter_id is None: 1417 encounter_id = self.current_encounter['pk_encounter'] 1418 1419 if episode_id is None: 1420 issue = self.add_health_issue(issue_name = _('Allergies/Intolerances')) 1421 epi = self.add_episode(episode_name = _('Allergy detail: %s') % allergene, pk_health_issue = issue['pk_health_issue']) 1422 episode_id = epi['pk_episode'] 1423 1424 new_allergy = gmAllergy.create_allergy ( 1425 allergene = allergene, 1426 allg_type = allg_type, 1427 encounter_id = encounter_id, 1428 episode_id = episode_id 1429 ) 1430 1431 return new_allergy
1432 #--------------------------------------------------------
1433 - def delete_allergy(self, pk_allergy=None):
1434 cmd = 'delete FROM clin.allergy WHERE pk=%(pk_allg)s' 1435 args = {'pk_allg': pk_allergy} 1436 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
1437 1438 #--------------------------------------------------------
1439 - def is_allergic_to(self, atcs=None, inns=None, product_name=None):
1440 """Cave: only use with one potential allergic agent 1441 otherwise you won't know which of the agents the allergy is to.""" 1442 1443 # we don't know the state 1444 if self.allergy_state is None: 1445 return None 1446 1447 # we know there's no allergies 1448 if self.allergy_state == 0: 1449 return False 1450 1451 args = { 1452 'atcs': atcs, 1453 'inns': inns, 1454 'prod_name': product_name, 1455 'pat': self.pk_patient 1456 } 1457 allergenes = [] 1458 where_parts = [] 1459 1460 if len(atcs) == 0: 1461 atcs = None 1462 if atcs is not None: 1463 where_parts.append('atc_code in %(atcs)s') 1464 if len(inns) == 0: 1465 inns = None 1466 if inns is not None: 1467 where_parts.append('generics in %(inns)s') 1468 allergenes.extend(inns) 1469 if product_name is not None: 1470 where_parts.append('substance = %(prod_name)s') 1471 allergenes.append(product_name) 1472 1473 if len(allergenes) != 0: 1474 where_parts.append('allergene in %(allgs)s') 1475 args['allgs'] = tuple(allergenes) 1476 1477 cmd = """ 1478 SELECT * FROM clin.v_pat_allergies 1479 WHERE 1480 pk_patient = %%(pat)s 1481 AND ( %s )""" % ' OR '.join(where_parts) 1482 1483 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 1484 1485 if len(rows) == 0: 1486 return False 1487 1488 return gmAllergy.cAllergy(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_allergy'})
1489 #--------------------------------------------------------
1490 - def _set_allergy_state(self, state):
1491 1492 if state not in gmAllergy.allergy_states: 1493 raise ValueError('[%s].__set_allergy_state(): <state> must be one of %s' % (self.__class__.__name__, gmAllergy.allergy_states)) 1494 1495 allg_state = gmAllergy.ensure_has_allergy_state(encounter = self.current_encounter['pk_encounter']) 1496 allg_state['has_allergy'] = state 1497 allg_state.save_payload() 1498 return True
1499
1500 - def _get_allergy_state(self):
1501 return gmAllergy.ensure_has_allergy_state(encounter = self.current_encounter['pk_encounter'])
1502 1503 allergy_state = property(_get_allergy_state, _set_allergy_state) 1504 #-------------------------------------------------------- 1505 # API: external care 1506 #--------------------------------------------------------
1507 - def get_external_care_items(self, order_by=None, exclude_inactive=False):
1508 return gmExternalCare.get_external_care_items ( 1509 pk_identity = self.pk_patient, 1510 order_by = order_by, 1511 exclude_inactive = exclude_inactive 1512 )
1513 1514 external_care_items = property(get_external_care_items, lambda x:x) 1515 1516 #-------------------------------------------------------- 1517 # API: episodes 1518 #--------------------------------------------------------
1519 - def get_episodes(self, id_list=None, issues=None, open_status=None, order_by=None, unlinked_only=False):
1520 """Fetches from backend patient episodes. 1521 1522 id_list - Episodes' PKs list 1523 issues - Health issues' PKs list to filter episodes by 1524 open_status - return all (None) episodes, only open (True) or closed (False) one(s) 1525 """ 1526 if (unlinked_only is True) and (issues is not None): 1527 raise ValueError('<unlinked_only> cannot be TRUE if <issues> is not None') 1528 1529 if order_by is None: 1530 order_by = '' 1531 else: 1532 order_by = 'ORDER BY %s' % order_by 1533 1534 args = { 1535 'pat': self.pk_patient, 1536 'open': open_status 1537 } 1538 where_parts = ['pk_patient = %(pat)s'] 1539 1540 if open_status is not None: 1541 where_parts.append('episode_open IS %(open)s') 1542 1543 if unlinked_only: 1544 where_parts.append('pk_health_issue is NULL') 1545 1546 if issues is not None: 1547 where_parts.append('pk_health_issue IN %(issues)s') 1548 args['issues'] = tuple(issues) 1549 1550 if id_list is not None: 1551 where_parts.append('pk_episode IN %(epis)s') 1552 args['epis'] = tuple(id_list) 1553 1554 cmd = "SELECT * FROM clin.v_pat_episodes WHERE %s %s" % ( 1555 ' AND '.join(where_parts), 1556 order_by 1557 ) 1558 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 1559 1560 return [ gmEMRStructItems.cEpisode(row = {'data': r, 'idx': idx, 'pk_field': 'pk_episode'}) for r in rows ]
1561 1562 episodes = property(get_episodes, lambda x:x) 1563 #------------------------------------------------------------------
1564 - def get_unlinked_episodes(self, open_status=None, order_by=None):
1565 return self.get_episodes(open_status = open_status, order_by = order_by, unlinked_only = True)
1566 1567 unlinked_episodes = property(get_unlinked_episodes, lambda x:x) 1568 #------------------------------------------------------------------
1569 - def get_episodes_by_encounter(self, pk_encounter=None):
1570 cmd = """SELECT distinct pk_episode 1571 from clin.v_pat_items 1572 WHERE pk_encounter=%(enc)s and pk_patient=%(pat)s""" 1573 args = { 1574 'enc': gmTools.coalesce(pk_encounter, self.current_encounter['pk_encounter']), 1575 'pat': self.pk_patient 1576 } 1577 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}]) 1578 if len(rows) == 0: 1579 return [] 1580 epis = [] 1581 for row in rows: 1582 epis.append(row[0]) 1583 return self.get_episodes(id_list=epis)
1584 #------------------------------------------------------------------
1585 - def add_episode(self, episode_name=None, pk_health_issue=None, is_open=False, allow_dupes=False, link_obj=None):
1586 """Add episode 'episode_name' for a patient's health issue. 1587 1588 - silently returns if episode already exists 1589 """ 1590 episode = gmEMRStructItems.create_episode ( 1591 link_obj = link_obj, 1592 pk_health_issue = pk_health_issue, 1593 episode_name = episode_name, 1594 is_open = is_open, 1595 encounter = self.current_encounter['pk_encounter'], 1596 allow_dupes = allow_dupes 1597 ) 1598 return episode
1599 #--------------------------------------------------------
1600 - def get_most_recent_episode(self, issue=None):
1601 # try to find the episode with the most recently modified clinical item 1602 issue_where = gmTools.coalesce ( 1603 value2test = issue, 1604 return_instead = '', 1605 value2return = 'and pk_health_issue = %(issue)s' 1606 ) 1607 cmd = """ 1608 SELECT pk 1609 from clin.episode 1610 WHERE pk = ( 1611 SELECT distinct on(pk_episode) pk_episode 1612 from clin.v_pat_items 1613 WHERE 1614 pk_patient = %%(pat)s 1615 and 1616 modified_when = ( 1617 SELECT max(vpi.modified_when) 1618 from clin.v_pat_items vpi 1619 WHERE vpi.pk_patient = %%(pat)s 1620 ) 1621 %s 1622 -- guard against several episodes created at the same moment of time 1623 limit 1 1624 )""" % issue_where 1625 rows, idx = gmPG2.run_ro_queries(queries = [ 1626 {'cmd': cmd, 'args': {'pat': self.pk_patient, 'issue': issue}} 1627 ]) 1628 if len(rows) != 0: 1629 return gmEMRStructItems.cEpisode(aPK_obj=rows[0][0]) 1630 1631 # no clinical items recorded, so try to find 1632 # the youngest episode for this patient 1633 cmd = """ 1634 SELECT vpe0.pk_episode 1635 from 1636 clin.v_pat_episodes vpe0 1637 WHERE 1638 vpe0.pk_patient = %%(pat)s 1639 and 1640 vpe0.episode_modified_when = ( 1641 SELECT max(vpe1.episode_modified_when) 1642 from clin.v_pat_episodes vpe1 1643 WHERE vpe1.pk_episode = vpe0.pk_episode 1644 ) 1645 %s""" % issue_where 1646 rows, idx = gmPG2.run_ro_queries(queries = [ 1647 {'cmd': cmd, 'args': {'pat': self.pk_patient, 'issue': issue}} 1648 ]) 1649 if len(rows) != 0: 1650 return gmEMRStructItems.cEpisode(aPK_obj=rows[0][0]) 1651 1652 return None
1653 #--------------------------------------------------------
1654 - def episode2problem(self, episode=None):
1655 return gmEMRStructItems.episode2problem(episode=episode)
1656 #-------------------------------------------------------- 1657 # API: problems 1658 #--------------------------------------------------------
1659 - def get_problems(self, episodes=None, issues=None, include_closed_episodes=False, include_irrelevant_issues=False):
1660 """Retrieve a patient's problems. 1661 1662 "Problems" are the UNION of: 1663 1664 - issues which are .clinically_relevant 1665 - episodes which are .is_open 1666 1667 Therefore, both an issue and the open episode 1668 thereof can each be listed as a problem. 1669 1670 include_closed_episodes/include_irrelevant_issues will 1671 include those -- which departs from the definition of 1672 the problem list being "active" items only ... 1673 1674 episodes - episodes' PKs to filter problems by 1675 issues - health issues' PKs to filter problems by 1676 """ 1677 # FIXME: this could use a good measure of streamlining, probably 1678 1679 args = {'pat': self.pk_patient} 1680 1681 cmd = """SELECT pk_health_issue, pk_episode FROM clin.v_problem_list WHERE pk_patient = %(pat)s ORDER BY problem""" 1682 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 1683 1684 # Instantiate problem items 1685 problems = [] 1686 for row in rows: 1687 pk_args = { 1688 'pk_patient': self.pk_patient, 1689 'pk_health_issue': row['pk_health_issue'], 1690 'pk_episode': row['pk_episode'] 1691 } 1692 problems.append(gmEMRStructItems.cProblem(aPK_obj = pk_args, try_potential_problems = False)) 1693 1694 # include non-problems ? 1695 other_rows = [] 1696 if include_closed_episodes: 1697 cmd = """SELECT pk_health_issue, pk_episode FROM clin.v_potential_problem_list WHERE pk_patient = %(pat)s and type = 'episode'""" 1698 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 1699 other_rows.extend(rows) 1700 1701 if include_irrelevant_issues: 1702 cmd = """SELECT pk_health_issue, pk_episode FROM clin.v_potential_problem_list WHERE pk_patient = %(pat)s and type = 'health issue'""" 1703 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 1704 other_rows.extend(rows) 1705 1706 if len(other_rows) > 0: 1707 for row in other_rows: 1708 pk_args = { 1709 'pk_patient': self.pk_patient, 1710 'pk_health_issue': row['pk_health_issue'], 1711 'pk_episode': row['pk_episode'] 1712 } 1713 problems.append(gmEMRStructItems.cProblem(aPK_obj = pk_args, try_potential_problems = True)) 1714 1715 # filter 1716 if issues is not None: 1717 problems = [ p for p in problems if p['pk_health_issue'] in issues ] 1718 if episodes is not None: 1719 problems = [ p for p in problems if p['pk_episode'] in episodes ] 1720 1721 return problems
1722 1723 #--------------------------------------------------------
1724 - def problem2episode(self, problem=None):
1725 return gmEMRStructItems.problem2episode(problem = problem)
1726 1727 #--------------------------------------------------------
1728 - def problem2issue(self, problem=None):
1729 return gmEMRStructItems.problem2issue(problem = problem)
1730 1731 #--------------------------------------------------------
1732 - def reclass_problem(self, problem):
1733 return gmEMRStructItems.reclass_problem(problem = problem)
1734 1735 #--------------------------------------------------------
1736 - def get_candidate_diagnoses(self):
1737 cmd = "SELECT * FROM clin.v_candidate_diagnoses WHERE pk_patient = %(pat)s" 1738 rows, idx = gmPG2.run_ro_queries ( 1739 queries = [{'cmd': cmd, 'args': {'pat': self.pk_patient}}], 1740 get_col_idx = False 1741 ) 1742 return rows
1743 1744 candidate_diagnoses = property(get_candidate_diagnoses) 1745 1746 #-------------------------------------------------------- 1747 # API: health issues 1748 #--------------------------------------------------------
1749 - def get_health_issues(self, id_list = None):
1750 1751 cmd = "SELECT *, xmin_health_issue FROM clin.v_health_issues WHERE pk_patient = %(pat)s ORDER BY description" 1752 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pat': self.pk_patient}}], get_col_idx = True) 1753 issues = [ gmEMRStructItems.cHealthIssue(row = {'idx': idx, 'data': r, 'pk_field': 'pk_health_issue'}) for r in rows ] 1754 1755 if id_list is None: 1756 return issues 1757 1758 if len(id_list) == 0: 1759 raise ValueError('id_list to filter by is empty, most likely a programming error') 1760 1761 filtered_issues = [] 1762 for issue in issues: 1763 if issue['pk_health_issue'] in id_list: 1764 filtered_issues.append(issue) 1765 1766 return filtered_issues
1767 1768 health_issues = property(get_health_issues, lambda x:x) 1769 1770 #------------------------------------------------------------------
1771 - def add_health_issue(self, issue_name=None):
1772 """Adds patient health issue.""" 1773 return gmEMRStructItems.create_health_issue ( 1774 description = issue_name, 1775 encounter = self.current_encounter['pk_encounter'], 1776 patient = self.pk_patient 1777 )
1778 #--------------------------------------------------------
1779 - def health_issue2problem(self, issue=None):
1780 return gmEMRStructItems.health_issue2problem(issue = issue)
1781 #-------------------------------------------------------- 1782 # API: substance intake 1783 #--------------------------------------------------------
1784 - def get_current_medications(self, include_inactive=True, include_unapproved=False, order_by=None, episodes=None, issues=None):
1785 return self._get_current_substance_intakes ( 1786 include_inactive = include_inactive, 1787 include_unapproved = include_unapproved, 1788 order_by = order_by, 1789 episodes = episodes, 1790 issues = issues, 1791 exclude_medications = False, 1792 exclude_potential_abuses = True 1793 )
1794 1795 #--------------------------------------------------------
1796 - def _get_abused_substances(self, order_by=None):
1797 return self._get_current_substance_intakes ( 1798 include_inactive = True, 1799 include_unapproved = True, 1800 order_by = order_by, 1801 episodes = None, 1802 issues = None, 1803 exclude_medications = True, 1804 exclude_potential_abuses = False 1805 )
1806 1807 abused_substances = property(_get_abused_substances, lambda x:x) 1808 1809 #--------------------------------------------------------
1810 - def _get_current_substance_intakes(self, include_inactive=True, include_unapproved=False, order_by=None, episodes=None, issues=None, exclude_potential_abuses=False, exclude_medications=False):
1811 1812 where_parts = ['pk_patient = %(pat)s'] 1813 args = {'pat': self.pk_patient} 1814 1815 if not include_inactive: 1816 where_parts.append('is_currently_active IN (TRUE, NULL)') 1817 1818 if not include_unapproved: 1819 where_parts.append('intake_is_approved_of IN (TRUE, NULL)') 1820 1821 if exclude_potential_abuses: 1822 where_parts.append('harmful_use_type IS NULL') 1823 1824 if exclude_medications: 1825 where_parts.append('harmful_use_type IS NOT NULL') 1826 1827 if order_by is None: 1828 order_by = '' 1829 else: 1830 order_by = 'ORDER BY %s' % order_by 1831 1832 cmd = "SELECT * FROM clin.v_substance_intakes WHERE %s %s" % ( 1833 '\nAND '.join(where_parts), 1834 order_by 1835 ) 1836 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 1837 intakes = [ gmMedication.cSubstanceIntakeEntry(row = {'idx': idx, 'data': r, 'pk_field': 'pk_substance_intake'}) for r in rows ] 1838 1839 if episodes is not None: 1840 intakes = [ i for i in intakes if i['pk_episode'] in episodes ] 1841 1842 if issues is not None: 1843 intakes = [ i for i in intakes if i ['pk_health_issue'] in issues ] 1844 1845 return intakes
1846 1847 #--------------------------------------------------------
1848 - def add_substance_intake(self, pk_component=None, pk_episode=None, pk_drug_product=None, pk_health_issue=None):
1849 pk_enc = self.current_encounter['pk_encounter'] 1850 if pk_episode is None: 1851 pk_episode = gmMedication.create_default_medication_history_episode ( 1852 pk_health_issue = pk_health_issue, 1853 encounter = pk_enc 1854 ) 1855 return gmMedication.create_substance_intake ( 1856 pk_component = pk_component, 1857 pk_encounter = pk_enc, 1858 pk_episode = pk_episode, 1859 pk_drug_product = pk_drug_product 1860 )
1861 1862 #--------------------------------------------------------
1863 - def substance_intake_exists(self, pk_component=None, pk_substance=None, pk_drug_product=None):
1864 return gmMedication.substance_intake_exists ( 1865 pk_component = pk_component, 1866 pk_substance = pk_substance, 1867 pk_identity = self.pk_patient, 1868 pk_drug_product = pk_drug_product 1869 )
1870 1871 #-------------------------------------------------------- 1872 # API: vaccinations 1873 #--------------------------------------------------------
1874 - def add_vaccination(self, episode=None, vaccine=None, batch_no=None):
1875 return gmVaccination.create_vaccination ( 1876 encounter = self.current_encounter['pk_encounter'], 1877 episode = episode, 1878 vaccine = vaccine, 1879 batch_no = batch_no 1880 )
1881 1882 #--------------------------------------------------------
1883 - def get_latest_vaccinations(self, episodes=None, issues=None, atc_indications=None):
1884 """Returns latest given vaccination for each vaccinated indication. 1885 1886 as a dict {'l10n_indication': cVaccination instance} 1887 1888 Note that this will produce duplicate vaccination instances on combi-indication vaccines ! 1889 """ 1890 args = {'pat': self.pk_patient} 1891 where_parts = ['c_v_shots.pk_patient = %(pat)s'] 1892 1893 if (episodes is not None) and (len(episodes) > 0): 1894 where_parts.append('c_v_shots.pk_episode IN %(epis)s') 1895 args['epis'] = tuple(episodes) 1896 1897 if (issues is not None) and (len(issues) > 0): 1898 where_parts.append('c_v_shots.pk_episode IN (select pk from clin.episode where fk_health_issue IN %(issues)s)') 1899 args['issues'] = tuple(issues) 1900 1901 if (atc_indications is not None) and (len(atc_indications) > 0): 1902 where_parts.append('c_v_plv4i.atc_indication IN %(atc_inds)s') 1903 args['atc_inds'] = tuple(atc_indications) 1904 1905 # find the shots 1906 cmd = """ 1907 SELECT 1908 c_v_shots.*, 1909 c_v_plv4i.l10n_indication, 1910 c_v_plv4i.no_of_shots 1911 FROM 1912 clin.v_vaccinations c_v_shots 1913 JOIN clin.v_pat_last_vacc4indication c_v_plv4i ON (c_v_shots.pk_vaccination = c_v_plv4i.pk_vaccination) 1914 WHERE %s 1915 """ % '\nAND '.join(where_parts) 1916 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 1917 1918 # none found 1919 if len(rows) == 0: 1920 return {} 1921 1922 # turn them into vaccinations 1923 # (idx is constant) 1924 vaccs = {} 1925 for shot_row in rows: 1926 vaccs[shot_row['l10n_indication']] = ( 1927 shot_row['no_of_shots'], 1928 gmVaccination.cVaccination(row = {'idx': idx, 'data': shot_row, 'pk_field': 'pk_vaccination'}) 1929 ) 1930 1931 return vaccs
1932 1933 #--------------------------------------------------------
1934 - def get_vaccinations(self, order_by=None, episodes=None, issues=None, encounters=None):
1935 return gmVaccination.get_vaccinations ( 1936 pk_identity = self.pk_patient, 1937 pk_episodes = episodes, 1938 pk_health_issues = issues, 1939 pk_encounters = encounters, 1940 order_by = order_by, 1941 return_pks = False 1942 )
1943 1944 vaccinations = property(get_vaccinations, lambda x:x) 1945 1946 #-------------------------------------------------------- 1947 # old/obsolete: 1948 #--------------------------------------------------------
1949 - def get_scheduled_vaccination_regimes(self, ID=None, indications=None):
1950 """Retrieves vaccination regimes the patient is on. 1951 1952 optional: 1953 * ID - PK of the vaccination regime 1954 * indications - indications we want to retrieve vaccination 1955 regimes for, must be primary language, not l10n_indication 1956 """ 1957 # FIXME: use course, not regime 1958 # retrieve vaccination regimes definitions 1959 cmd = """SELECT distinct on(pk_course) pk_course 1960 FROM clin.v_vaccs_scheduled4pat 1961 WHERE pk_patient=%s""" 1962 rows = gmPG.run_ro_query('historica', cmd, None, self.pk_patient) 1963 if rows is None: 1964 _log.error('cannot retrieve scheduled vaccination courses') 1965 return None 1966 # Instantiate vaccination items and keep cache 1967 for row in rows: 1968 self.__db_cache['vaccinations']['scheduled regimes'].append(gmVaccination.cVaccinationCourse(aPK_obj=row[0])) 1969 1970 # ok, let's constrain our list 1971 filtered_regimes = [] 1972 filtered_regimes.extend(self.__db_cache['vaccinations']['scheduled regimes']) 1973 if ID is not None: 1974 filtered_regimes = [ r for r in filtered_regimes if r['pk_course'] == ID ] 1975 if len(filtered_regimes) == 0: 1976 _log.error('no vaccination course [%s] found for patient [%s]' % (ID, self.pk_patient)) 1977 return [] 1978 else: 1979 return filtered_regimes[0] 1980 if indications is not None: 1981 filtered_regimes = [ r for r in filtered_regimes if r['indication'] in indications ] 1982 1983 return filtered_regimes
1984 #-------------------------------------------------------- 1985 # def get_vaccinated_indications(self): 1986 # """Retrieves patient vaccinated indications list. 1987 # 1988 # Note that this does NOT rely on the patient being on 1989 # some schedule or other but rather works with what the 1990 # patient has ACTUALLY been vaccinated against. This is 1991 # deliberate ! 1992 # """ 1993 # # most likely, vaccinations will be fetched close 1994 # # by so it makes sense to count on the cache being 1995 # # filled (or fill it for nearby use) 1996 # vaccinations = self.get_vaccinations() 1997 # if vaccinations is None: 1998 # _log.error('cannot load vaccinated indications for patient [%s]' % self.pk_patient) 1999 # return (False, [[_('ERROR: cannot retrieve vaccinated indications'), _('ERROR: cannot retrieve vaccinated indications')]]) 2000 # if len(vaccinations) == 0: 2001 # return (True, [[_('no vaccinations recorded'), _('no vaccinations recorded')]]) 2002 # v_indications = [] 2003 # for vacc in vaccinations: 2004 # tmp = [vacc['indication'], vacc['l10n_indication']] 2005 # # remove duplicates 2006 # if tmp in v_indications: 2007 # continue 2008 # v_indications.append(tmp) 2009 # return (True, v_indications) 2010 #--------------------------------------------------------
2011 - def get_vaccinations_old(self, ID=None, indications=None, since=None, until=None, encounters=None, episodes=None, issues=None):
2012 """Retrieves list of vaccinations the patient has received. 2013 2014 optional: 2015 * ID - PK of a vaccination 2016 * indications - indications we want to retrieve vaccination 2017 items for, must be primary language, not l10n_indication 2018 * since - initial date for allergy items 2019 * until - final date for allergy items 2020 * encounters - list of encounters whose allergies are to be retrieved 2021 * episodes - list of episodes whose allergies are to be retrieved 2022 * issues - list of health issues whose allergies are to be retrieved 2023 """ 2024 try: 2025 self.__db_cache['vaccinations']['vaccinated'] 2026 except KeyError: 2027 self.__db_cache['vaccinations']['vaccinated'] = [] 2028 # Important fetch ordering by indication, date to know if a vaccination is booster 2029 cmd= """SELECT * FROM clin.v_pat_vaccinations4indication 2030 WHERE pk_patient=%s 2031 order by indication, date""" 2032 rows, idx = gmPG.run_ro_query('historica', cmd, True, self.pk_patient) 2033 if rows is None: 2034 _log.error('cannot load given vaccinations for patient [%s]' % self.pk_patient) 2035 del self.__db_cache['vaccinations']['vaccinated'] 2036 return None 2037 # Instantiate vaccination items 2038 vaccs_by_ind = {} 2039 for row in rows: 2040 vacc_row = { 2041 'pk_field': 'pk_vaccination', 2042 'idx': idx, 2043 'data': row 2044 } 2045 vacc = gmVaccination.cVaccination(row=vacc_row) 2046 self.__db_cache['vaccinations']['vaccinated'].append(vacc) 2047 # keep them, ordered by indication 2048 try: 2049 vaccs_by_ind[vacc['indication']].append(vacc) 2050 except KeyError: 2051 vaccs_by_ind[vacc['indication']] = [vacc] 2052 2053 # calculate sequence number and is_booster 2054 for ind in vaccs_by_ind: 2055 vacc_regimes = self.get_scheduled_vaccination_regimes(indications = [ind]) 2056 for vacc in vaccs_by_ind[ind]: 2057 # due to the "order by indication, date" the vaccinations are in the 2058 # right temporal order inside the indication-keyed dicts 2059 seq_no = vaccs_by_ind[ind].index(vacc) + 1 2060 vacc['seq_no'] = seq_no 2061 # if no active schedule for indication we cannot 2062 # check for booster status (eg. seq_no > max_shot) 2063 if (vacc_regimes is None) or (len(vacc_regimes) == 0): 2064 continue 2065 if seq_no > vacc_regimes[0]['shots']: 2066 vacc['is_booster'] = True 2067 del vaccs_by_ind 2068 2069 # ok, let's constrain our list 2070 filtered_shots = [] 2071 filtered_shots.extend(self.__db_cache['vaccinations']['vaccinated']) 2072 if ID is not None: 2073 filtered_shots = filter(lambda shot: shot['pk_vaccination'] == ID, filtered_shots) 2074 if len(filtered_shots) == 0: 2075 _log.error('no vaccination [%s] found for patient [%s]' % (ID, self.pk_patient)) 2076 return None 2077 else: 2078 return filtered_shots[0] 2079 if since is not None: 2080 filtered_shots = filter(lambda shot: shot['date'] >= since, filtered_shots) 2081 if until is not None: 2082 filtered_shots = filter(lambda shot: shot['date'] < until, filtered_shots) 2083 if issues is not None: 2084 filtered_shots = filter(lambda shot: shot['pk_health_issue'] in issues, filtered_shots) 2085 if episodes is not None: 2086 filtered_shots = filter(lambda shot: shot['pk_episode'] in episodes, filtered_shots) 2087 if encounters is not None: 2088 filtered_shots = filter(lambda shot: shot['pk_encounter'] in encounters, filtered_shots) 2089 if indications is not None: 2090 filtered_shots = filter(lambda shot: shot['indication'] in indications, filtered_shots) 2091 return filtered_shots
2092 #--------------------------------------------------------
2093 - def get_scheduled_vaccinations(self, indications=None):
2094 """Retrieves vaccinations scheduled for a regime a patient is on. 2095 2096 The regime is referenced by its indication (not l10n) 2097 2098 * indications - List of indications (not l10n) of regimes we want scheduled 2099 vaccinations to be fetched for 2100 """ 2101 try: 2102 self.__db_cache['vaccinations']['scheduled'] 2103 except KeyError: 2104 self.__db_cache['vaccinations']['scheduled'] = [] 2105 cmd = """SELECT * FROM clin.v_vaccs_scheduled4pat WHERE pk_patient=%s""" 2106 rows, idx = gmPG.run_ro_query('historica', cmd, True, self.pk_patient) 2107 if rows is None: 2108 _log.error('cannot load scheduled vaccinations for patient [%s]' % self.pk_patient) 2109 del self.__db_cache['vaccinations']['scheduled'] 2110 return None 2111 # Instantiate vaccination items 2112 for row in rows: 2113 vacc_row = { 2114 'pk_field': 'pk_vacc_def', 2115 'idx': idx, 2116 'data': row 2117 } 2118 self.__db_cache['vaccinations']['scheduled'].append(gmVaccination.cScheduledVaccination(row = vacc_row)) 2119 2120 # ok, let's constrain our list 2121 if indications is None: 2122 return self.__db_cache['vaccinations']['scheduled'] 2123 filtered_shots = [] 2124 filtered_shots.extend(self.__db_cache['vaccinations']['scheduled']) 2125 filtered_shots = filter(lambda shot: shot['indication'] in indications, filtered_shots) 2126 return filtered_shots
2127 #--------------------------------------------------------
2128 - def get_missing_vaccinations(self, indications=None):
2129 try: 2130 self.__db_cache['vaccinations']['missing'] 2131 except KeyError: 2132 self.__db_cache['vaccinations']['missing'] = {} 2133 # 1) non-booster 2134 self.__db_cache['vaccinations']['missing']['due'] = [] 2135 # get list of (indication, seq_no) tuples 2136 cmd = "SELECT indication, seq_no FROM clin.v_pat_missing_vaccs WHERE pk_patient=%s" 2137 rows = gmPG.run_ro_query('historica', cmd, None, self.pk_patient) 2138 if rows is None: 2139 _log.error('error loading (indication, seq_no) for due/overdue vaccinations for patient [%s]' % self.pk_patient) 2140 return None 2141 pk_args = {'pat_id': self.pk_patient} 2142 if rows is not None: 2143 for row in rows: 2144 pk_args['indication'] = row[0] 2145 pk_args['seq_no'] = row[1] 2146 self.__db_cache['vaccinations']['missing']['due'].append(gmVaccination.cMissingVaccination(aPK_obj=pk_args)) 2147 2148 # 2) boosters 2149 self.__db_cache['vaccinations']['missing']['boosters'] = [] 2150 # get list of indications 2151 cmd = "SELECT indication, seq_no FROM clin.v_pat_missing_boosters WHERE pk_patient=%s" 2152 rows = gmPG.run_ro_query('historica', cmd, None, self.pk_patient) 2153 if rows is None: 2154 _log.error('error loading indications for missing boosters for patient [%s]' % self.pk_patient) 2155 return None 2156 pk_args = {'pat_id': self.pk_patient} 2157 if rows is not None: 2158 for row in rows: 2159 pk_args['indication'] = row[0] 2160 self.__db_cache['vaccinations']['missing']['boosters'].append(gmVaccination.cMissingBooster(aPK_obj=pk_args)) 2161 2162 # if any filters ... 2163 if indications is None: 2164 return self.__db_cache['vaccinations']['missing'] 2165 if len(indications) == 0: 2166 return self.__db_cache['vaccinations']['missing'] 2167 # ... apply them 2168 filtered_shots = { 2169 'due': [], 2170 'boosters': [] 2171 } 2172 for due_shot in self.__db_cache['vaccinations']['missing']['due']: 2173 if due_shot['indication'] in indications: #and due_shot not in filtered_shots['due']: 2174 filtered_shots['due'].append(due_shot) 2175 for due_shot in self.__db_cache['vaccinations']['missing']['boosters']: 2176 if due_shot['indication'] in indications: #and due_shot not in filtered_shots['boosters']: 2177 filtered_shots['boosters'].append(due_shot) 2178 return filtered_shots
2179 2180 #------------------------------------------------------------------ 2181 # API: encounters 2182 #------------------------------------------------------------------
2183 - def _get_current_encounter(self):
2184 return self.__encounter
2185
2186 - def _set_current_encounter(self, encounter):
2187 # first ever setting ? -> fast path 2188 if self.__encounter is None: 2189 _log.debug('first setting of active encounter in this clinical record instance') 2190 encounter.lock(exclusive = False) # lock new 2191 self.__encounter = encounter 2192 gmDispatcher.send('current_encounter_switched') 2193 return True 2194 2195 # real switch -> slow path 2196 _log.debug('switching of active encounter') 2197 # fail if the currently active encounter has unsaved changes 2198 if self.__encounter.is_modified(): 2199 gmTools.compare_dict_likes(self.__encounter, encounter, 'modified enc in client', 'enc to switch to') 2200 _log.error('current in client: %s', self.__encounter) 2201 raise ValueError('unsaved changes in active encounter [%s], cannot switch to another one [%s]' % ( 2202 self.__encounter['pk_encounter'], 2203 encounter['pk_encounter'] 2204 )) 2205 2206 prev_enc = self.__encounter 2207 encounter.lock(exclusive = False) # lock new 2208 self.__encounter = encounter 2209 prev_enc.unlock(exclusive = False) # unlock old 2210 gmDispatcher.send('current_encounter_switched') 2211 2212 return True
2213 2214 current_encounter = property(_get_current_encounter, _set_current_encounter) 2215 active_encounter = property(_get_current_encounter, _set_current_encounter) 2216 2217 #--------------------------------------------------------
2218 - def __setup_active_encounter(self):
2219 _log.debug('setting up active encounter for identity [%s]', self.pk_patient) 2220 2221 # log access to patient record (HIPAA, for example) 2222 _delayed_execute(self.log_access, action = 'pulling chart for identity [%s]' % self.pk_patient) 2223 2224 # cleanup (not async, because we don't want recent encounters 2225 # to become the active one just because they are recent) 2226 self.remove_empty_encounters() 2227 2228 # activate very recent encounter if available 2229 if self.__activate_very_recent_encounter(): 2230 return 2231 2232 fairly_recent_enc = self.__get_fairly_recent_encounter() 2233 2234 # create new encounter for the time being 2235 self.start_new_encounter() 2236 2237 if fairly_recent_enc is None: 2238 return 2239 2240 # but check whether user wants to continue a "fairly recent" one 2241 gmDispatcher.send ( 2242 signal = 'ask_for_encounter_continuation', 2243 new_encounter = self.__encounter, 2244 fairly_recent_encounter = fairly_recent_enc 2245 )
2246 2247 #------------------------------------------------------------------
2248 - def __activate_very_recent_encounter(self):
2249 """Try to attach to a "very recent" encounter if there is one. 2250 2251 returns: 2252 False: no "very recent" encounter 2253 True: success 2254 """ 2255 cfg_db = gmCfg.cCfgSQL() 2256 min_ttl = cfg_db.get2 ( 2257 option = 'encounter.minimum_ttl', 2258 workplace = _here.active_workplace, 2259 bias = 'user', 2260 default = '1 hour 30 minutes' 2261 ) 2262 cmd = gmEMRStructItems.SQL_get_encounters % """pk_encounter = ( 2263 SELECT pk_encounter 2264 FROM clin.v_most_recent_encounters 2265 WHERE 2266 pk_patient = %s 2267 and 2268 last_affirmed > (now() - %s::interval) 2269 ORDER BY 2270 last_affirmed DESC 2271 LIMIT 1 2272 )""" 2273 enc_rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient, min_ttl]}], get_col_idx = True) 2274 2275 # none found 2276 if len(enc_rows) == 0: 2277 _log.debug('no <very recent> encounter (younger than [%s]) found' % min_ttl) 2278 return False 2279 2280 _log.debug('"very recent" encounter [%s] found and re-activated' % enc_rows[0]['pk_encounter']) 2281 2282 # attach to existing 2283 self.current_encounter = gmEMRStructItems.cEncounter(row = {'data': enc_rows[0], 'idx': idx, 'pk_field': 'pk_encounter'}) 2284 return True
2285 2286 #------------------------------------------------------------------
2287 - def __get_fairly_recent_encounter(self):
2288 cfg_db = gmCfg.cCfgSQL() 2289 min_ttl = cfg_db.get2 ( 2290 option = 'encounter.minimum_ttl', 2291 workplace = _here.active_workplace, 2292 bias = 'user', 2293 default = '1 hour 30 minutes' 2294 ) 2295 max_ttl = cfg_db.get2 ( 2296 option = 'encounter.maximum_ttl', 2297 workplace = _here.active_workplace, 2298 bias = 'user', 2299 default = '6 hours' 2300 ) 2301 2302 # do we happen to have a "fairly recent" candidate ? 2303 cmd = gmEMRStructItems.SQL_get_encounters % """pk_encounter = ( 2304 SELECT pk_encounter 2305 FROM clin.v_most_recent_encounters 2306 WHERE 2307 pk_patient=%s 2308 AND 2309 last_affirmed BETWEEN (now() - %s::interval) AND (now() - %s::interval) 2310 ORDER BY 2311 last_affirmed DESC 2312 LIMIT 1 2313 )""" 2314 enc_rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient, max_ttl, min_ttl]}], get_col_idx = True) 2315 2316 # none found 2317 if len(enc_rows) == 0: 2318 _log.debug('no <fairly recent> encounter (between [%s] and [%s] old) found' % (min_ttl, max_ttl)) 2319 return None 2320 2321 _log.debug('"fairly recent" encounter [%s] found', enc_rows[0]['pk_encounter']) 2322 return gmEMRStructItems.cEncounter(row = {'data': enc_rows[0], 'idx': idx, 'pk_field': 'pk_encounter'})
2323 2324 # #------------------------------------------------------------------ 2325 # def __check_for_fairly_recent_encounter(self): 2326 # 2327 # cfg_db = gmCfg.cCfgSQL() 2328 # min_ttl = cfg_db.get2 ( 2329 # option = u'encounter.minimum_ttl', 2330 # workplace = _here.active_workplace, 2331 # bias = u'user', 2332 # default = u'1 hour 30 minutes' 2333 # ) 2334 # max_ttl = cfg_db.get2 ( 2335 # option = u'encounter.maximum_ttl', 2336 # workplace = _here.active_workplace, 2337 # bias = u'user', 2338 # default = u'6 hours' 2339 # ) 2340 # 2341 # # do we happen to have a "fairly recent" candidate ? 2342 # cmd = gmEMRStructItems.SQL_get_encounters % u"""pk_encounter = ( 2343 # SELECT pk_encounter 2344 # FROM clin.v_most_recent_encounters 2345 # WHERE 2346 # pk_patient=%s 2347 # AND 2348 # last_affirmed BETWEEN (now() - %s::interval) AND (now() - %s::interval) 2349 # ORDER BY 2350 # last_affirmed DESC 2351 # LIMIT 1 2352 # )""" 2353 # enc_rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient, max_ttl, min_ttl]}], get_col_idx = True) 2354 # 2355 # # none found 2356 # if len(enc_rows) == 0: 2357 # _log.debug('no <fairly recent> encounter (between [%s] and [%s] old) found' % (min_ttl, max_ttl)) 2358 # return 2359 # 2360 # _log.debug('"fairly recent" encounter [%s] found', enc_rows[0]['pk_encounter']) 2361 # fairly_recent_enc = gmEMRStructItems.cEncounter(row = {'data': enc_rows[0], 'idx': idx, 'pk_field': 'pk_encounter'}) 2362 # gmDispatcher.send(u'ask_for_encounter_continuation', current = self.__encounter, fairly_recent_encounter = fairly_recent_enc) 2363 2364 # #------------------------------------------------------------------ 2365 # def __activate_fairly_recent_encounter(self, allow_user_interaction=True): 2366 # """Try to attach to a "fairly recent" encounter if there is one. 2367 # 2368 # returns: 2369 # False: no "fairly recent" encounter, create new one 2370 # True: success 2371 # """ 2372 # if _func_ask_user is None: 2373 # _log.debug('cannot ask user for guidance, not looking for fairly recent encounter') 2374 # return False 2375 # 2376 # if not allow_user_interaction: 2377 # _log.exception('user interaction not desired, not looking for fairly recent encounter') 2378 # return False 2379 # 2380 # cfg_db = gmCfg.cCfgSQL() 2381 # min_ttl = cfg_db.get2 ( 2382 # option = u'encounter.minimum_ttl', 2383 # workplace = _here.active_workplace, 2384 # bias = u'user', 2385 # default = u'1 hour 30 minutes' 2386 # ) 2387 # max_ttl = cfg_db.get2 ( 2388 # option = u'encounter.maximum_ttl', 2389 # workplace = _here.active_workplace, 2390 # bias = u'user', 2391 # default = u'6 hours' 2392 # ) 2393 # cmd = u""" 2394 # SELECT pk_encounter 2395 # FROM clin.v_most_recent_encounters 2396 # WHERE 2397 # pk_patient=%s 2398 # AND 2399 # last_affirmed BETWEEN (now() - %s::interval) AND (now() - %s::interval) 2400 # ORDER BY 2401 # last_affirmed DESC""" 2402 # enc_rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient, max_ttl, min_ttl]}]) 2403 # # none found 2404 # if len(enc_rows) == 0: 2405 # _log.debug('no <fairly recent> encounter (between [%s] and [%s] old) found' % (min_ttl, max_ttl)) 2406 # return False 2407 # 2408 # _log.debug('"fairly recent" encounter [%s] found', enc_rows[0][0]) 2409 # 2410 # encounter = gmEMRStructItems.cEncounter(aPK_obj=enc_rows[0][0]) 2411 # # ask user whether to attach or not 2412 # cmd = u""" 2413 # SELECT title, firstnames, lastnames, gender, dob 2414 # FROM dem.v_all_persons WHERE pk_identity=%s""" 2415 # pats, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient]}]) 2416 # pat = pats[0] 2417 # pat_str = u'%s %s %s (%s), %s [#%s]' % ( 2418 # gmTools.coalesce(pat[0], u'')[:5], 2419 # pat[1][:15], 2420 # pat[2][:15], 2421 # pat[3], 2422 # gmDateTime.pydt_strftime(pat[4], '%Y %b %d'), 2423 # self.pk_patient 2424 # ) 2425 # msg = _( 2426 # '%s\n' 2427 # '\n' 2428 # "This patient's chart was worked on only recently:\n" 2429 # '\n' 2430 # ' %s %s - %s (%s)\n' 2431 # '\n' 2432 # ' Reason for Encounter:\n' 2433 # ' %s\n' 2434 # ' Assessment of Encounter:\n' 2435 # ' %s\n' 2436 # '\n' 2437 # 'Do you want to continue that consultation\n' 2438 # 'or do you want to start a new one ?\n' 2439 # ) % ( 2440 # pat_str, 2441 # gmDateTime.pydt_strftime(encounter['started'], '%Y %b %d'), 2442 # gmDateTime.pydt_strftime(encounter['started'], '%H:%M'), gmDateTime.pydt_strftime(encounter['last_affirmed'], '%H:%M'), 2443 # encounter['l10n_type'], 2444 # gmTools.coalesce(encounter['reason_for_encounter'], _('none given')), 2445 # gmTools.coalesce(encounter['assessment_of_encounter'], _('none given')), 2446 # ) 2447 # attach = False 2448 # try: 2449 # attach = _func_ask_user(msg = msg, caption = _('Starting patient encounter'), encounter = encounter) 2450 # except Exception: 2451 # _log.exception('cannot ask user for guidance, not attaching to existing encounter') 2452 # return False 2453 # if not attach: 2454 # return False 2455 # 2456 # # attach to existing 2457 # self.current_encounter = encounter 2458 # _log.debug('"fairly recent" encounter re-activated') 2459 # return True 2460 2461 #------------------------------------------------------------------
2462 - def start_new_encounter(self):
2463 cfg_db = gmCfg.cCfgSQL() 2464 enc_type = cfg_db.get2 ( 2465 option = 'encounter.default_type', 2466 workplace = _here.active_workplace, 2467 bias = 'user' 2468 ) 2469 if enc_type is None: 2470 enc_type = gmEMRStructItems.get_most_commonly_used_encounter_type() 2471 if enc_type is None: 2472 enc_type = 'in surgery' 2473 enc = gmEMRStructItems.create_encounter(fk_patient = self.pk_patient, enc_type = enc_type) 2474 enc['pk_org_unit'] = _here['pk_org_unit'] 2475 enc.save() 2476 self.current_encounter = enc 2477 _log.debug('new encounter [%s] activated', enc['pk_encounter'])
2478 2479 #------------------------------------------------------------------
2480 - def get_encounters(self, since=None, until=None, id_list=None, episodes=None, issues=None, skip_empty=False, order_by=None, max_encounters=None):
2481 """Retrieves patient's encounters. 2482 2483 id_list - PKs of encounters to fetch 2484 since - initial date for encounter items, DateTime instance 2485 until - final date for encounter items, DateTime instance 2486 episodes - PKs of the episodes the encounters belong to (many-to-many relation) 2487 issues - PKs of the health issues the encounters belong to (many-to-many relation) 2488 skip_empty - do NOT return those which do not have any of documents/clinical items/RFE/AOE 2489 2490 NOTE: if you specify *both* issues and episodes 2491 you will get the *aggregate* of all encounters even 2492 if the episodes all belong to the health issues listed. 2493 IOW, the issues broaden the episode list rather than 2494 the episode list narrowing the episodes-from-issues 2495 list. 2496 Rationale: If it was the other way round it would be 2497 redundant to specify the list of issues at all. 2498 """ 2499 # if issues are given, translate them to their episodes 2500 if (issues is not None) and (len(issues) > 0): 2501 # - find episodes corresponding to the health issues in question 2502 cmd = "SELECT distinct pk_episode FROM clin.v_pat_episodes WHERE pk_health_issue in %(issue_pks)s AND pk_patient = %(pat)s" 2503 args = {'issue_pks': tuple(issues), 'pat': self.pk_patient} 2504 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}]) 2505 epis4issues_pks = [ r['pk_episode'] for r in rows ] 2506 if episodes is None: 2507 episodes = [] 2508 episodes.extend(epis4issues_pks) 2509 2510 if (episodes is not None) and (len(episodes) > 0): 2511 # since the episodes to filter by belong to the patient in question so will 2512 # the encounters found with them - hence we don't need a WHERE on the patient ... 2513 # but, better safe than sorry ... 2514 args = {'epi_pks': tuple(episodes), 'pat': self.pk_patient} 2515 cmd = "SELECT distinct fk_encounter FROM clin.clin_root_item WHERE fk_episode IN %(epi_pks)s AND fk_encounter IN (SELECT pk FROM clin.encounter WHERE fk_patient = %(pat)s)" 2516 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}]) 2517 encs4epis_pks = [ r['fk_encounter'] for r in rows ] 2518 if id_list is None: 2519 id_list = [] 2520 id_list.extend(encs4epis_pks) 2521 2522 where_parts = ['c_vpe.pk_patient = %(pat)s'] 2523 args = {'pat': self.pk_patient} 2524 2525 if skip_empty: 2526 where_parts.append("""NOT ( 2527 gm.is_null_or_blank_string(c_vpe.reason_for_encounter) 2528 AND 2529 gm.is_null_or_blank_string(c_vpe.assessment_of_encounter) 2530 AND 2531 NOT EXISTS ( 2532 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 2533 UNION ALL 2534 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 2535 ))""") 2536 2537 if since is not None: 2538 where_parts.append('c_vpe.started >= %(start)s') 2539 args['start'] = since 2540 2541 if until is not None: 2542 where_parts.append('c_vpe.last_affirmed <= %(end)s') 2543 args['end'] = since 2544 2545 if (id_list is not None) and (len(id_list) > 0): 2546 where_parts.append('c_vpe.pk_encounter IN %(enc_pks)s') 2547 args['enc_pks'] = tuple(id_list) 2548 2549 if order_by is None: 2550 order_by = 'c_vpe.started' 2551 2552 if max_encounters is None: 2553 limit = '' 2554 else: 2555 limit = 'LIMIT %s' % max_encounters 2556 2557 cmd = """ 2558 SELECT * FROM clin.v_pat_encounters c_vpe 2559 WHERE 2560 %s 2561 ORDER BY %s %s 2562 """ % ( 2563 ' AND '.join(where_parts), 2564 order_by, 2565 limit 2566 ) 2567 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 2568 encounters = [ gmEMRStructItems.cEncounter(row = {'data': r, 'idx': idx, 'pk_field': 'pk_encounter'}) for r in rows ] 2569 2570 # we've got the encounters, start filtering 2571 filtered_encounters = [] 2572 filtered_encounters.extend(encounters) 2573 2574 if (episodes is not None) and (len(episodes) > 0): 2575 # since the episodes to filter by belong to the patient in question so will 2576 # the encounters found with them - hence we don't need a WHERE on the patient ... 2577 # but, better safe than sorry ... 2578 args = {'epi_pks': tuple(episodes), 'pat': self.pk_patient} 2579 cmd = "SELECT distinct fk_encounter FROM clin.clin_root_item WHERE fk_episode IN %(epi_pks)s AND fk_encounter IN (SELECT pk FROM clin.encounter WHERE fk_patient = %(pat)s)" 2580 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}]) 2581 encs4epis_pks = [ r['fk_encounter'] for r in rows ] 2582 filtered_encounters = [ enc for enc in filtered_encounters if enc['pk_encounter'] in encs4epis_pks ] 2583 2584 return filtered_encounters
2585 2586 #--------------------------------------------------------
2587 - def get_first_encounter(self, issue_id=None, episode_id=None):
2588 """Retrieves first encounter for a particular issue and/or episode. 2589 2590 issue_id - First encounter associated health issue 2591 episode - First encounter associated episode 2592 """ 2593 if issue_id is None: 2594 issues = None 2595 else: 2596 issues = [issue_id] 2597 2598 if episode_id is None: 2599 episodes = None 2600 else: 2601 episodes = [episode_id] 2602 2603 encounters = self.get_encounters(issues = issues, episodes = episodes, order_by = 'started', max_encounters = 1) 2604 if len(encounters) == 0: 2605 return None 2606 2607 return encounters[0]
2608 2609 first_encounter = property(get_first_encounter, lambda x:x) 2610 2611 #--------------------------------------------------------
2612 - def get_earliest_care_date(self):
2613 args = {'pat': self.pk_patient} 2614 cmd = """ 2615 SELECT MIN(earliest) FROM ( 2616 ( 2617 SELECT MIN(episode_modified_when) AS earliest FROM clin.v_pat_episodes WHERE pk_patient = %(pat)s 2618 2619 ) UNION ALL ( 2620 2621 SELECT MIN(modified_when) AS earliest FROM clin.v_health_issues WHERE pk_patient = %(pat)s 2622 2623 ) UNION ALL ( 2624 2625 SELECT MIN(modified_when) AS earliest FROM clin.encounter WHERE fk_patient = %(pat)s 2626 2627 ) UNION ALL ( 2628 2629 SELECT MIN(started) AS earliest FROM clin.v_pat_encounters WHERE pk_patient = %(pat)s 2630 2631 ) UNION ALL ( 2632 2633 SELECT MIN(modified_when) AS earliest FROM clin.v_pat_items WHERE pk_patient = %(pat)s 2634 2635 ) UNION ALL ( 2636 2637 SELECT MIN(modified_when) AS earliest FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat)s 2638 2639 ) UNION ALL ( 2640 2641 SELECT MIN(last_confirmed) AS earliest FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat)s 2642 2643 ) 2644 ) AS candidates""" 2645 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 2646 return rows[0][0]
2647 2648 earliest_care_date = property(get_earliest_care_date, lambda x:x) 2649 2650 #--------------------------------------------------------
2651 - def get_most_recent_care_date(self):
2652 encounters = self.get_encounters(order_by = 'started DESC', max_encounters = 1) 2653 if len(encounters) == 0: 2654 return None 2655 return encounters[0]['last_affirmed']
2656 2657 most_recent_care_date = property(get_most_recent_care_date) 2658 2659 #--------------------------------------------------------
2660 - def get_last_encounter(self, issue_id=None, episode_id=None):
2661 """Retrieves last encounter for a concrete issue and/or episode 2662 2663 issue_id - Last encounter associated health issue 2664 episode_id - Last encounter associated episode 2665 """ 2666 if issue_id is None: 2667 issues = None 2668 else: 2669 issues = [issue_id] 2670 2671 if episode_id is None: 2672 episodes = None 2673 else: 2674 episodes = [episode_id] 2675 2676 encounters = self.get_encounters(issues = issues, episodes = episodes, order_by = 'started DESC', max_encounters = 1) 2677 if len(encounters) == 0: 2678 return None 2679 2680 return encounters[0]
2681 2682 last_encounter = property(get_last_encounter, lambda x:x) 2683 2684 #------------------------------------------------------------------
2685 - def get_encounter_stats_by_type(self, cover_period=None):
2686 args = {'pat': self.pk_patient, 'range': cover_period} 2687 where_parts = ['pk_patient = %(pat)s'] 2688 if cover_period is not None: 2689 where_parts.append('last_affirmed > now() - %(range)s') 2690 2691 cmd = """ 2692 SELECT l10n_type, count(1) AS frequency 2693 FROM clin.v_pat_encounters 2694 WHERE 2695 %s 2696 GROUP BY l10n_type 2697 ORDER BY frequency DESC 2698 """ % ' AND '.join(where_parts) 2699 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 2700 return rows
2701 2702 #------------------------------------------------------------------
2703 - def get_last_but_one_encounter(self, issue_id=None, episode_id=None):
2704 2705 args = {'pat': self.pk_patient} 2706 2707 if (issue_id is None) and (episode_id is None): 2708 cmd = """ 2709 SELECT * FROM clin.v_pat_encounters 2710 WHERE pk_patient = %(pat)s 2711 ORDER BY started DESC 2712 LIMIT 2 2713 """ 2714 else: 2715 where_parts = [] 2716 2717 if issue_id is not None: 2718 where_parts.append('pk_health_issue = %(issue)s') 2719 args['issue'] = issue_id 2720 2721 if episode_id is not None: 2722 where_parts.append('pk_episode = %(epi)s') 2723 args['epi'] = episode_id 2724 2725 cmd = """ 2726 SELECT * 2727 FROM clin.v_pat_encounters 2728 WHERE 2729 pk_patient = %%(pat)s 2730 AND 2731 pk_encounter IN ( 2732 SELECT distinct pk_encounter 2733 FROM clin.v_narrative 2734 WHERE 2735 %s 2736 ) 2737 ORDER BY started DESC 2738 LIMIT 2 2739 """ % ' AND '.join(where_parts) 2740 2741 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 2742 2743 if len(rows) == 0: 2744 return None 2745 2746 # just one encounter within the above limits 2747 if len(rows) == 1: 2748 # is it the current encounter ? 2749 if rows[0]['pk_encounter'] == self.current_encounter['pk_encounter']: 2750 # yes 2751 return None 2752 # no 2753 return gmEMRStructItems.cEncounter(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_encounter'}) 2754 2755 # more than one encounter 2756 if rows[0]['pk_encounter'] == self.current_encounter['pk_encounter']: 2757 return gmEMRStructItems.cEncounter(row = {'data': rows[1], 'idx': idx, 'pk_field': 'pk_encounter'}) 2758 2759 return gmEMRStructItems.cEncounter(row = {'data': rows[0], 'idx': idx, 'pk_field': 'pk_encounter'})
2760 2761 last_but_one_encounter = property(get_last_but_one_encounter, lambda x:x) 2762 2763 #------------------------------------------------------------------
2764 - def remove_empty_encounters(self):
2765 _log.debug('removing empty encounters for pk_identity [%s]', self.pk_patient) 2766 cfg_db = gmCfg.cCfgSQL() 2767 ttl = cfg_db.get2 ( 2768 option = 'encounter.ttl_if_empty', 2769 workplace = _here.active_workplace, 2770 bias = 'user', 2771 default = '1 week' 2772 ) 2773 # # FIXME: this should be done async 2774 cmd = "SELECT clin.remove_old_empty_encounters(%(pat)s::INTEGER, %(ttl)s::INTERVAL)" 2775 args = {'pat': self.pk_patient, 'ttl': ttl} 2776 try: 2777 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True) 2778 except Exception: 2779 _log.exception('error deleting empty encounters') 2780 return False 2781 2782 if not rows[0][0]: 2783 _log.debug('no encounters deleted (less than 2 exist)') 2784 2785 return True
2786 2787 #------------------------------------------------------------------ 2788 # API: measurements / test results 2789 #------------------------------------------------------------------
2790 - def get_most_recent_results_for_patient(self, no_of_results=1):
2791 return gmPathLab.get_most_recent_results_for_patient ( 2792 no_of_results = no_of_results, 2793 patient = self.pk_patient 2794 )
2795 2796 #------------------------------------------------------------------
2797 - def get_most_recent_results_in_loinc_group(self, loincs=None, max_no_of_results=1, consider_indirect_matches=False):
2798 return gmPathLab.get_most_recent_results_in_loinc_group ( 2799 loincs = loincs, 2800 max_no_of_results = max_no_of_results, 2801 patient = self.pk_patient, 2802 consider_indirect_matches = consider_indirect_matches 2803 )
2804 2805 #------------------------------------------------------------------
2806 - def get_most_recent_results_for_test_type(self, test_type=None, max_no_of_results=1):
2807 return gmPathLab.get_most_recent_results_for_test_type ( 2808 test_type = test_type, 2809 max_no_of_results = max_no_of_results, 2810 patient = self.pk_patient 2811 )
2812 2813 #------------------------------------------------------------------
2814 - def get_most_recent_result_for_test_types(self, pk_test_types=None):
2815 return gmPathLab.get_most_recent_result_for_test_types ( 2816 pk_test_types = pk_test_types, 2817 pk_patient = self.pk_patient 2818 )
2819 2820 #------------------------------------------------------------------
2821 - def get_result_at_timestamp(self, timestamp=None, test_type=None, loinc=None, tolerance_interval='12 hours'):
2822 return gmPathLab.get_result_at_timestamp ( 2823 timestamp = timestamp, 2824 test_type = test_type, 2825 loinc = loinc, 2826 tolerance_interval = tolerance_interval, 2827 patient = self.pk_patient 2828 )
2829 2830 #------------------------------------------------------------------
2831 - def get_results_for_day(self, timestamp=None, order_by=None):
2832 return gmPathLab.get_results_for_day ( 2833 timestamp = timestamp, 2834 patient = self.pk_patient, 2835 order_by = order_by 2836 )
2837 2838 #------------------------------------------------------------------
2839 - def get_results_for_issue(self, pk_health_issue=None, order_by=None):
2840 return gmPathLab.get_results_for_issue ( 2841 pk_health_issue = pk_health_issue, 2842 order_by = order_by 2843 )
2844 2845 #------------------------------------------------------------------
2846 - def get_results_for_episode(self, pk_episode=None):
2847 return gmPathLab.get_results_for_episode(pk_episode = pk_episode)
2848 2849 #------------------------------------------------------------------
2850 - def get_unsigned_results(self, order_by=None):
2851 if order_by is None: 2852 order_by = '' 2853 else: 2854 order_by = 'ORDER BY %s' % order_by 2855 cmd = """ 2856 SELECT * FROM clin.v_test_results 2857 WHERE 2858 pk_patient = %%(pat)s 2859 AND 2860 reviewed IS FALSE 2861 %s""" % order_by 2862 args = {'pat': self.pk_patient} 2863 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 2864 return [ gmPathLab.cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ]
2865 2866 #------------------------------------------------------------------ 2867 # FIXME: use psyopg2 dbapi extension of named cursors - they are *server* side !
2868 - def get_test_types_for_results(self, order_by=None, unique_meta_types=False):
2869 """Retrieve data about test types for which this patient has results.""" 2870 if order_by is None: 2871 order_by = '' 2872 else: 2873 order_by = 'ORDER BY %s' % order_by 2874 2875 if unique_meta_types: 2876 cmd = """ 2877 SELECT * FROM clin.v_test_types c_vtt 2878 WHERE c_vtt.pk_test_type IN ( 2879 SELECT DISTINCT ON (c_vtr1.pk_meta_test_type) c_vtr1.pk_test_type 2880 FROM clin.v_test_results c_vtr1 2881 WHERE 2882 c_vtr1.pk_patient = %%(pat)s 2883 AND 2884 c_vtr1.pk_meta_test_type IS NOT NULL 2885 UNION ALL 2886 SELECT DISTINCT ON (c_vtr2.pk_test_type) c_vtr2.pk_test_type 2887 FROM clin.v_test_results c_vtr2 2888 WHERE 2889 c_vtr2.pk_patient = %%(pat)s 2890 AND 2891 c_vtr2.pk_meta_test_type IS NULL 2892 ) 2893 %s""" % order_by 2894 else: 2895 cmd = """ 2896 SELECT * FROM clin.v_test_types c_vtt 2897 WHERE c_vtt.pk_test_type IN ( 2898 SELECT DISTINCT ON (c_vtr.pk_test_type) c_vtr.pk_test_type 2899 FROM clin.v_test_results c_vtr 2900 WHERE c_vtr.pk_patient = %%(pat)s 2901 ) 2902 %s""" % order_by 2903 2904 args = {'pat': self.pk_patient} 2905 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 2906 return [ gmPathLab.cMeasurementType(row = {'pk_field': 'pk_test_type', 'idx': idx, 'data': r}) for r in rows ]
2907 2908 #------------------------------------------------------------------
2909 - def get_dates_for_results(self, tests=None, reverse_chronological=True):
2910 """Get the dates for which we have results.""" 2911 where_parts = ['pk_patient = %(pat)s'] 2912 args = {'pat': self.pk_patient} 2913 2914 if tests is not None: 2915 where_parts.append('pk_test_type IN %(tests)s') 2916 args['tests'] = tuple(tests) 2917 2918 cmd = """ 2919 SELECT DISTINCT ON (clin_when_day) 2920 clin_when_day, 2921 is_reviewed 2922 FROM ( 2923 SELECT 2924 date_trunc('day', clin_when) 2925 AS clin_when_day, 2926 bool_and(reviewed) 2927 AS is_reviewed 2928 FROM ( 2929 SELECT 2930 clin_when, 2931 reviewed, 2932 pk_patient, 2933 pk_test_result 2934 FROM clin.v_test_results 2935 WHERE %s 2936 ) 2937 AS patient_tests 2938 GROUP BY clin_when_day 2939 ) 2940 AS grouped_days 2941 ORDER BY clin_when_day %s 2942 """ % ( 2943 ' AND '.join(where_parts), 2944 gmTools.bool2subst(reverse_chronological, 'DESC', 'ASC', 'DESC') 2945 ) 2946 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 2947 return rows
2948 2949 #------------------------------------------------------------------
2950 - def get_issues_or_episodes_for_results(self, tests=None):
2951 """Get the issues/episodes for which we have results.""" 2952 where_parts = ['pk_patient = %(pat)s'] 2953 args = {'pat': self.pk_patient} 2954 2955 if tests is not None: 2956 where_parts.append('pk_test_type IN %(tests)s') 2957 args['tests'] = tuple(tests) 2958 where = ' AND '.join(where_parts) 2959 cmd = """ 2960 SELECT * FROM (( 2961 -- issues, each including all it"s episodes 2962 SELECT 2963 health_issue AS problem, 2964 pk_health_issue, 2965 NULL::integer AS pk_episode, 2966 1 AS rank 2967 FROM clin.v_test_results 2968 WHERE pk_health_issue IS NOT NULL AND %s 2969 GROUP BY pk_health_issue, problem 2970 ) UNION ALL ( 2971 -- episodes w/o issue 2972 SELECT 2973 episode AS problem, 2974 NULL::integer AS pk_health_issue, 2975 pk_episode, 2976 2 AS rank 2977 FROM clin.v_test_results 2978 WHERE pk_health_issue IS NULL AND %s 2979 GROUP BY pk_episode, problem 2980 )) AS grouped_union 2981 ORDER BY rank, problem 2982 """ % (where, where) 2983 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 2984 return rows
2985 2986 #------------------------------------------------------------------
2987 - def get_test_results(self, encounters=None, episodes=None, tests=None, order_by=None):
2988 return gmPathLab.get_test_results ( 2989 pk_patient = self.pk_patient, 2990 encounters = encounters, 2991 episodes = episodes, 2992 order_by = order_by 2993 )
2994 #------------------------------------------------------------------
2995 - def get_test_results_by_date(self, encounter=None, episodes=None, tests=None, reverse_chronological=True):
2996 2997 where_parts = ['pk_patient = %(pat)s'] 2998 args = {'pat': self.pk_patient} 2999 3000 if tests is not None: 3001 where_parts.append('pk_test_type IN %(tests)s') 3002 args['tests'] = tuple(tests) 3003 3004 if encounter is not None: 3005 where_parts.append('pk_encounter = %(enc)s') 3006 args['enc'] = encounter 3007 3008 if episodes is not None: 3009 where_parts.append('pk_episode IN %(epis)s') 3010 args['epis'] = tuple(episodes) 3011 3012 cmd = """ 3013 SELECT * FROM clin.v_test_results 3014 WHERE %s 3015 ORDER BY clin_when %s, pk_episode, unified_name 3016 """ % ( 3017 ' AND '.join(where_parts), 3018 gmTools.bool2subst(reverse_chronological, 'DESC', 'ASC', 'DESC') 3019 ) 3020 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 3021 3022 tests = [ gmPathLab.cTestResult(row = {'pk_field': 'pk_test_result', 'idx': idx, 'data': r}) for r in rows ] 3023 3024 return tests
3025 #------------------------------------------------------------------
3026 - def add_test_result(self, episode=None, type=None, intended_reviewer=None, val_num=None, val_alpha=None, unit=None, link_obj=None):
3027 3028 try: 3029 epi = int(episode) 3030 except Exception: 3031 epi = episode['pk_episode'] 3032 3033 try: 3034 type = int(type) 3035 except Exception: 3036 type = type['pk_test_type'] 3037 3038 tr = gmPathLab.create_test_result ( 3039 link_obj = link_obj, 3040 encounter = self.current_encounter['pk_encounter'], 3041 episode = epi, 3042 type = type, 3043 intended_reviewer = intended_reviewer, 3044 val_num = val_num, 3045 val_alpha = val_alpha, 3046 unit = unit 3047 ) 3048 3049 return tr
3050 3051 #------------------------------------------------------------------
3052 - def get_labs_as_org_units(self):
3053 where = 'pk_org_unit IN (%s)' % """ 3054 SELECT DISTINCT fk_org_unit FROM clin.test_org WHERE pk IN ( 3055 SELECT DISTINCT pk_test_org FROM clin.v_test_results where pk_patient = %(pat)s 3056 )""" 3057 args = {'pat': self.pk_patient} 3058 cmd = gmOrganization._SQL_get_org_unit % where 3059 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 3060 return [ gmOrganization.cOrgUnit(row = {'pk_field': 'pk_org_unit', 'data': r, 'idx': idx}) for r in rows ]
3061 3062 #------------------------------------------------------------------
3063 - def _get_best_gfr_or_crea(self):
3064 measured_gfrs = self.get_most_recent_results_in_loinc_group(loincs = gmLOINC.LOINC_gfr_quantity, max_no_of_results = 1) 3065 measured_gfr = measured_gfrs[0] if len(measured_gfrs) > 0 else None 3066 creas = self.get_most_recent_results_in_loinc_group(loincs = gmLOINC.LOINC_creatinine_quantity, max_no_of_results = 1) 3067 crea = creas[0] if len(creas) > 0 else None 3068 3069 if (measured_gfr is None) and (crea is None): 3070 return None 3071 3072 if (measured_gfr is not None) and (crea is None): 3073 return measured_gfr 3074 3075 # from here, Crea cannot be None anymore 3076 if measured_gfr is None: 3077 eGFR = self.calculator.eGFR 3078 if eGFR.numeric_value is None: 3079 return crea 3080 return eGFR 3081 3082 # from here, measured_gfr cannot be None anymore, either 3083 two_weeks = pydt.timedelta(weeks = 2) 3084 gfr_too_old = (crea['clin_when'] - measured_gfr['clin_when']) > two_weeks 3085 if not gfr_too_old: 3086 return measured_gfr 3087 3088 # from here, measured_gfr is considered too 3089 # old, so attempt a more timely estimate 3090 eGFR = self.calculator.eGFR 3091 if eGFR.numeric_value is None: 3092 # return crea since we cannot get a 3093 # better estimate for some reason 3094 return crea 3095 3096 return eGFR
3097 3098 best_gfr_or_crea = property(_get_best_gfr_or_crea, lambda x:x) 3099 3100 #------------------------------------------------------------------
3101 - def _get_bmi(self):
3102 return self.calculator.bmi
3103 3104 bmi = property(_get_bmi, lambda x:x) 3105 3106 #------------------------------------------------------------------
3107 - def _get_dynamic_hints(self):
3108 return gmAutoHints.get_hints_for_patient(pk_identity = self.pk_patient, pk_encounter = self.current_encounter['pk_encounter'])
3109 3110 dynamic_hints = property(_get_dynamic_hints, lambda x:x) 3111 3112 #------------------------------------------------------------------ 3113 #------------------------------------------------------------------ 3114 #------------------------------------------------------------------
3115 - def get_lab_request(self, pk=None, req_id=None, lab=None):
3116 # FIXME: verify that it is our patient ? ... 3117 req = gmPathLab.cLabRequest(aPK_obj=pk, req_id=req_id, lab=lab) 3118 return req
3119 #------------------------------------------------------------------
3120 - def add_lab_request(self, lab=None, req_id=None, encounter_id=None, episode_id=None):
3121 if encounter_id is None: 3122 encounter_id = self.current_encounter['pk_encounter'] 3123 status, data = gmPathLab.create_lab_request( 3124 lab=lab, 3125 req_id=req_id, 3126 pat_id=self.pk_patient, 3127 encounter_id=encounter_id, 3128 episode_id=episode_id 3129 ) 3130 if not status: 3131 _log.error(str(data)) 3132 return None 3133 return data
3134 3135 #============================================================ 3136 # main 3137 #------------------------------------------------------------ 3138 if __name__ == "__main__": 3139 3140 if len(sys.argv) == 1: 3141 sys.exit() 3142 3143 if sys.argv[1] != 'test': 3144 sys.exit() 3145 3146 from Gnumed.pycommon import gmLog2 3147 3148 from Gnumed.business import gmPraxis 3149 branches = gmPraxis.get_praxis_branches() 3150 praxis = gmPraxis.gmCurrentPraxisBranch(branches[0]) 3151
3152 - def _do_delayed(*args, **kwargs):
3153 print(args) 3154 print(kwargs) 3155 args[0](*args[1:], **kwargs)
3156 3157 set_delayed_executor(_do_delayed) 3158 3159 #-----------------------------------------
3160 - def test_allergy_state():
3161 emr = cClinicalRecord(aPKey=1) 3162 state = emr.allergy_state 3163 print("allergy state is:", state) 3164 3165 print("setting state to 0") 3166 emr.allergy_state = 0 3167 3168 print("setting state to None") 3169 emr.allergy_state = None 3170 3171 print("setting state to 'abc'") 3172 emr.allergy_state = 'abc'
3173 3174 #-----------------------------------------
3175 - def test_get_test_names():
3176 emr = cClinicalRecord(aPKey = 6) 3177 rows = emr.get_test_types_for_results(unique_meta_types = True) 3178 print("test result names:", len(rows))
3179 # for row in rows: 3180 # print row 3181 3182 #-----------------------------------------
3183 - def test_get_dates_for_results():
3184 emr = cClinicalRecord(aPKey=12) 3185 rows = emr.get_dates_for_results() 3186 print("test result dates:") 3187 for row in rows: 3188 print(row)
3189 3190 #-----------------------------------------
3191 - def test_get_measurements():
3192 emr = cClinicalRecord(aPKey=12) 3193 rows, idx = emr.get_measurements_by_date() 3194 print("test results:") 3195 for row in rows: 3196 print(row)
3197 3198 #-----------------------------------------
3199 - def test_get_test_results_by_date():
3200 emr = cClinicalRecord(aPKey=12) 3201 tests = emr.get_test_results_by_date() 3202 print("test results:") 3203 for test in tests: 3204 print(test)
3205 3206 #-----------------------------------------
3207 - def test_get_statistics():
3208 emr = cClinicalRecord(aPKey=12) 3209 for key, item in emr.get_statistics().items(): 3210 print(key, ":", item)
3211 3212 #-----------------------------------------
3213 - def test_get_problems():
3214 emr = cClinicalRecord(aPKey=12) 3215 3216 probs = emr.get_problems() 3217 print("normal probs (%s):" % len(probs)) 3218 for p in probs: 3219 print('%s (%s)' % (p['problem'], p['type'])) 3220 3221 probs = emr.get_problems(include_closed_episodes=True) 3222 print("probs + closed episodes (%s):" % len(probs)) 3223 for p in probs: 3224 print('%s (%s)' % (p['problem'], p['type'])) 3225 3226 probs = emr.get_problems(include_irrelevant_issues=True) 3227 print("probs + issues (%s):" % len(probs)) 3228 for p in probs: 3229 print('%s (%s)' % (p['problem'], p['type'])) 3230 3231 probs = emr.get_problems(include_closed_episodes=True, include_irrelevant_issues=True) 3232 print("probs + issues + epis (%s):" % len(probs)) 3233 for p in probs: 3234 print('%s (%s)' % (p['problem'], p['type']))
3235 3236 #-----------------------------------------
3237 - def test_add_test_result():
3238 emr = cClinicalRecord(aPKey=12) 3239 tr = emr.add_test_result ( 3240 episode = 1, 3241 intended_reviewer = 1, 3242 type = 1, 3243 val_num = 75, 3244 val_alpha = 'somewhat obese', 3245 unit = 'kg' 3246 ) 3247 print(tr)
3248 3249 #-----------------------------------------
3250 - def test_get_most_recent_episode():
3251 emr = cClinicalRecord(aPKey=12) 3252 print(emr.get_most_recent_episode(issue = 2))
3253 3254 #-----------------------------------------
3255 - def test_get_almost_recent_encounter():
3256 emr = cClinicalRecord(aPKey=12) 3257 print(emr.get_last_encounter(issue_id=2)) 3258 print(emr.get_last_but_one_encounter(issue_id=2))
3259 3260 #-----------------------------------------
3261 - def test_get_encounters():
3262 emr = cClinicalRecord(aPKey = 5) 3263 print(emr.get_first_encounter(episode_id = 1638)) 3264 print(emr.get_last_encounter(episode_id = 1638))
3265 3266 #-----------------------------------------
3267 - def test_get_issues():
3268 emr = cClinicalRecord(aPKey = 12) 3269 for issue in emr.health_issues: 3270 print(issue['description'])
3271 3272 #-----------------------------------------
3273 - def test_get_dx():
3274 emr = cClinicalRecord(aPKey = 12) 3275 for dx in emr.candidate_diagnoses: 3276 print(dx)
3277 3278 #-----------------------------------------
3279 - def test_get_meds():
3280 emr = cClinicalRecord(aPKey=12) 3281 for med in emr.get_current_medications(): 3282 print(med)
3283 3284 #-----------------------------------------
3285 - def test_get_abuses():
3286 emr = cClinicalRecord(aPKey=12) 3287 for med in emr.abused_substances: 3288 print(med.format(single_line = True))
3289 3290 #-----------------------------------------
3291 - def test_is_allergic_to():
3292 emr = cClinicalRecord(aPKey = 12) 3293 print(emr.is_allergic_to(atcs = tuple(sys.argv[2:]), inns = tuple(sys.argv[2:]), product_name = sys.argv[2]))
3294 3295 #-----------------------------------------
3296 - def test_get_as_journal():
3297 emr = cClinicalRecord(aPKey = 12) 3298 for journal_line in emr.get_as_journal(): 3299 #print(list(journal_line)) 3300 print('%(date)s %(modified_by)s %(soap_cat)s %(narrative)s' % journal_line) 3301 print("")
3302 3303 #-----------------------------------------
3304 - def test_get_most_recent():
3305 emr = cClinicalRecord(aPKey=12) 3306 print(emr.get_most_recent_results_for_test_type())
3307 3308 #-----------------------------------------
3309 - def test_episodes():
3310 emr = cClinicalRecord(aPKey=12) 3311 print("episodes:", emr.episodes) 3312 print("unlinked:", emr.unlinked_episodes)
3313 3314 #-----------------------------------------
3315 - def test_format_as_journal():
3316 emr = cClinicalRecord(aPKey=12) 3317 from Gnumed.business.gmPerson import cPatient 3318 pat = cPatient(aPK_obj = 12) 3319 print(emr.format_as_journal(left_margin = 1, patient = pat))
3320 3321 #-----------------------------------------
3322 - def test_smoking():
3323 emr = cClinicalRecord(aPKey=12) 3324 #print emr.is_or_was_smoker 3325 smoking, details = emr.smoking_status 3326 print('status:', smoking) 3327 print('details:') 3328 print(details) 3329 emr.smoking_status = (True, {'comment': '2', 'last_confirmed': gmDateTime.pydt_now_here()}) 3330 print(emr.smoking_status) 3331 print(emr.alcohol_status) 3332 print(emr.drugs_status)
3333 3334 #----------------------------------------- 3335 3336 #test_allergy_state() 3337 #test_is_allergic_to() 3338 3339 #test_get_test_names() 3340 #test_get_dates_for_results() 3341 #test_get_measurements() 3342 #test_get_test_results_by_date() 3343 #test_get_statistics() 3344 #test_get_problems() 3345 #test_add_test_result() 3346 #test_get_most_recent_episode() 3347 #test_get_almost_recent_encounter() 3348 #test_get_meds() 3349 #test_get_as_journal() 3350 #test_get_most_recent() 3351 #test_episodes() 3352 #test_format_as_journal() 3353 #test_smoking() 3354 #test_get_abuses() 3355 #test_get_encounters() 3356 #test_get_issues() 3357 #test_get_dx() 3358 3359 emr = cClinicalRecord(aPKey = 12) 3360 3361 # # Vacc regimes 3362 # vacc_regimes = emr.get_scheduled_vaccination_regimes(indications = ['tetanus']) 3363 # print '\nVaccination regimes: ' 3364 # for a_regime in vacc_regimes: 3365 # pass 3366 # #print a_regime 3367 # vacc_regime = emr.get_scheduled_vaccination_regimes(ID=10) 3368 # #print vacc_regime 3369 3370 # # vaccination regimes and vaccinations for regimes 3371 # scheduled_vaccs = emr.get_scheduled_vaccinations(indications = ['tetanus']) 3372 # print 'Vaccinations for the regime:' 3373 # for a_scheduled_vacc in scheduled_vaccs: 3374 # pass 3375 # #print ' %s' %(a_scheduled_vacc) 3376 3377 # # vaccination next shot and booster 3378 v1 = emr.vaccinations 3379 print(v1) 3380 v2 = gmVaccination.get_vaccinations(pk_identity = 12, return_pks = True) 3381 print(v2) 3382 for v in v1: 3383 if v['pk_vaccination'] not in v2: 3384 print('ERROR') 3385 3386 # for a_vacc in vaccinations: 3387 # 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']) 3388 3389 # # first and last encounters 3390 # first_encounter = emr.get_first_encounter(issue_id = 1) 3391 # print '\nFirst encounter: ' + str(first_encounter) 3392 # last_encounter = emr.get_last_encounter(episode_id = 1) 3393 # print '\nLast encounter: ' + str(last_encounter) 3394 # print '' 3395 3396 #dump = record.get_missing_vaccinations() 3397 #f = io.open('vaccs.lst', 'wb') 3398 #if dump is not None: 3399 # print "=== due ===" 3400 # f.write(u"=== due ===\n") 3401 # for row in dump['due']: 3402 # print row 3403 # f.write(repr(row)) 3404 # f.write(u'\n') 3405 # print "=== overdue ===" 3406 # f.write(u"=== overdue ===\n") 3407 # for row in dump['overdue']: 3408 # print row 3409 # f.write(repr(row)) 3410 # f.write(u'\n') 3411 #f.close() 3412