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