Package Gnumed :: Package wxpython :: Module gmEMRBrowser
[frames] | no frames]

Source Code for Module Gnumed.wxpython.gmEMRBrowser

   1  """GNUmed patient EMR tree browser. 
   2  """ 
   3  #================================================================ 
   4  __version__ = "$Revision: 1.111 $" 
   5  __author__ = "cfmoro1976@yahoo.es, sjtan@swiftdsl.com.au, Karsten.Hilbert@gmx.net" 
   6  __license__ = "GPL" 
   7   
   8  # std lib 
   9  import sys, os.path, StringIO, codecs, logging 
  10   
  11   
  12  # 3rd party 
  13  import wx 
  14   
  15   
  16  # GNUmed libs 
  17  from Gnumed.pycommon import gmI18N, gmDispatcher, gmExceptions, gmTools 
  18  from Gnumed.exporters import gmPatientExporter 
  19  from Gnumed.business import gmEMRStructItems, gmPerson, gmSOAPimporter, gmPersonSearch 
  20  from Gnumed.wxpython import gmGuiHelpers 
  21  from Gnumed.wxpython import gmEMRStructWidgets 
  22  from Gnumed.wxpython import gmSOAPWidgets 
  23  from Gnumed.wxpython import gmAllergyWidgets 
  24  from Gnumed.wxpython import gmDemographicsWidgets 
  25  from Gnumed.wxpython import gmNarrativeWidgets 
  26  from Gnumed.wxpython import gmPatSearchWidgets 
  27  from Gnumed.wxpython import gmVaccWidgets 
  28  from Gnumed.wxpython import gmFamilyHistoryWidgets 
  29   
  30   
  31  _log = logging.getLogger('gm.ui') 
  32  _log.info(__version__) 
  33   
  34  #============================================================ 
35 -def export_emr_to_ascii(parent=None):
36 """ 37 Dump the patient's EMR from GUI client 38 @param parent - The parent widget 39 @type parent - A wx.Window instance 40 """ 41 # sanity checks 42 if parent is None: 43 raise TypeError('expected wx.Window instance as parent, got <None>') 44 45 pat = gmPerson.gmCurrentPatient() 46 if not pat.connected: 47 gmDispatcher.send(signal='statustext', msg=_('Cannot export EMR. No active patient.')) 48 return False 49 50 # get file name 51 wc = "%s (*.txt)|*.txt|%s (*)|*" % (_("text files"), _("all files")) 52 defdir = os.path.abspath(os.path.expanduser(os.path.join('~', 'gnumed', 'export', 'EMR', pat['dirname']))) 53 gmTools.mkdir(defdir) 54 fname = '%s-%s_%s.txt' % (_('emr-export'), pat['lastnames'], pat['firstnames']) 55 dlg = wx.FileDialog ( 56 parent = parent, 57 message = _("Save patient's EMR as..."), 58 defaultDir = defdir, 59 defaultFile = fname, 60 wildcard = wc, 61 style = wx.SAVE 62 ) 63 choice = dlg.ShowModal() 64 fname = dlg.GetPath() 65 dlg.Destroy() 66 if choice != wx.ID_OK: 67 return None 68 69 _log.debug('exporting EMR to [%s]', fname) 70 71 # output_file = open(fname, 'wb') 72 output_file = codecs.open(fname, 'wb', encoding='utf8', errors='replace') 73 exporter = gmPatientExporter.cEmrExport(patient = pat) 74 exporter.set_output_file(output_file) 75 exporter.dump_constraints() 76 exporter.dump_demographic_record(True) 77 exporter.dump_clinical_record() 78 exporter.dump_med_docs() 79 output_file.close() 80 81 gmDispatcher.send('statustext', msg = _('EMR successfully exported to file: %s') % fname, beep = False) 82 return fname
83 #============================================================
84 -class cEMRTree(wx.TreeCtrl, gmGuiHelpers.cTreeExpansionHistoryMixin):
85 """This wx.TreeCtrl derivative displays a tree view of the medical record.""" 86 87 #--------------------------------------------------------
88 - def __init__(self, parent, id, *args, **kwds):
89 """Set up our specialised tree. 90 """ 91 kwds['style'] = wx.TR_HAS_BUTTONS | wx.NO_BORDER | wx.TR_SINGLE 92 wx.TreeCtrl.__init__(self, parent, id, *args, **kwds) 93 94 gmGuiHelpers.cTreeExpansionHistoryMixin.__init__(self) 95 96 self.__details_display = None 97 self.__details_display_mode = u'details' # "details" or "journal" 98 self.__enable_display_mode_selection = None 99 self.__pat = gmPerson.gmCurrentPatient() 100 self.__curr_node = None 101 self.__exporter = gmPatientExporter.cEmrExport(patient = self.__pat) 102 103 self._old_cursor_pos = None 104 105 self.__make_popup_menus() 106 self.__register_events()
107 #-------------------------------------------------------- 108 # external API 109 #--------------------------------------------------------
110 - def refresh(self):
111 if not self.__pat.connected: 112 gmDispatcher.send(signal='statustext', msg=_('Cannot load clinical narrative. No active patient.'),) 113 return False 114 115 if not self.__populate_tree(): 116 return False 117 118 return True
119 #--------------------------------------------------------
120 - def set_narrative_display(self, narrative_display=None):
121 self.__details_display = narrative_display
122 #--------------------------------------------------------
123 - def set_image_display(self, image_display=None):
124 self.__img_display = image_display
125 #--------------------------------------------------------
127 if not callable(callback): 128 raise ValueError('callback [%s] not callable' % callback) 129 130 self.__enable_display_mode_selection = callback
131 #-------------------------------------------------------- 132 # internal helpers 133 #--------------------------------------------------------
134 - def __register_events(self):
135 """Configures enabled event signals.""" 136 wx.EVT_TREE_SEL_CHANGED (self, self.GetId(), self._on_tree_item_selected) 137 wx.EVT_TREE_ITEM_RIGHT_CLICK (self, self.GetId(), self._on_tree_item_right_clicked) 138 139 # handle tooltips 140 # wx.EVT_MOTION(self, self._on_mouse_motion) 141 wx.EVT_TREE_ITEM_GETTOOLTIP(self, -1, self._on_tree_item_gettooltip) 142 143 gmDispatcher.connect(signal = 'narrative_mod_db', receiver = self._on_narrative_mod_db) 144 gmDispatcher.connect(signal = 'episode_mod_db', receiver = self._on_episode_mod_db) 145 gmDispatcher.connect(signal = 'health_issue_mod_db', receiver = self._on_issue_mod_db) 146 gmDispatcher.connect(signal = 'family_history_mod_db', receiver = self._on_issue_mod_db)
147 #--------------------------------------------------------
148 - def __populate_tree(self):
149 """Updates EMR browser data.""" 150 # FIXME: auto select the previously self.__curr_node if not None 151 # FIXME: error handling 152 153 wx.BeginBusyCursor() 154 155 # self.snapshot_expansion() 156 157 # init new tree 158 self.DeleteAllItems() 159 root_item = self.AddRoot(_('EMR of %(lastnames)s, %(firstnames)s') % self.__pat.get_active_name()) 160 self.SetItemPyData(root_item, None) 161 self.SetItemHasChildren(root_item, True) 162 self.__root_tooltip = self.__pat['description_gender'] + u'\n' 163 if self.__pat['deceased'] is None: 164 self.__root_tooltip += u' %s (%s)\n\n' % ( 165 self.__pat.get_formatted_dob(format = '%d %b %Y', encoding = gmI18N.get_encoding()), 166 self.__pat['medical_age'] 167 ) 168 else: 169 template = u' %s - %s (%s)\n\n' 170 self.__root_tooltip += template % ( 171 self.__pat.get_formatted_dob(format = '%d.%b %Y', encoding = gmI18N.get_encoding()), 172 self.__pat['deceased'].strftime('%d.%b %Y').decode(gmI18N.get_encoding()), 173 self.__pat['medical_age'] 174 ) 175 self.__root_tooltip += gmTools.coalesce(self.__pat['comment'], u'', u'%s\n\n') 176 doc = self.__pat.primary_provider 177 if doc is not None: 178 self.__root_tooltip += u'%s:\n' % _('Primary provider in this praxis') 179 self.__root_tooltip += u' %s %s %s (%s)%s\n\n' % ( 180 gmTools.coalesce(doc['title'], gmPerson.map_gender2salutation(gender = doc['gender'])), 181 doc['firstnames'], 182 doc['lastnames'], 183 doc['short_alias'], 184 gmTools.bool2subst(doc['is_active'], u'', u' [%s]' % _('inactive')) 185 ) 186 if not ((self.__pat['emergency_contact'] is None) and (self.__pat['pk_emergency_contact'] is None)): 187 self.__root_tooltip += _('In case of emergency contact:') + u'\n' 188 if self.__pat['emergency_contact'] is not None: 189 self.__root_tooltip += gmTools.wrap ( 190 text = u'%s\n' % self.__pat['emergency_contact'], 191 width = 60, 192 initial_indent = u' ', 193 subsequent_indent = u' ' 194 ) 195 if self.__pat['pk_emergency_contact'] is not None: 196 contact = self.__pat.emergency_contact_in_database 197 self.__root_tooltip += u' %s\n' % contact['description_gender'] 198 self.__root_tooltip = self.__root_tooltip.strip('\n') 199 if self.__root_tooltip == u'': 200 self.__root_tooltip = u' ' 201 202 # have the tree filled by the exporter 203 self.__exporter.get_historical_tree(self) 204 self.__curr_node = root_item 205 206 self.SelectItem(root_item) 207 self.Expand(root_item) 208 self.__update_text_for_selected_node() 209 210 # self.restore_expansion() 211 212 wx.EndBusyCursor() 213 return True
214 #--------------------------------------------------------
216 """Displays information for the selected tree node.""" 217 218 if self.__details_display is None: 219 self.__img_display.clear() 220 return 221 222 if self.__curr_node is None: 223 self.__img_display.clear() 224 return 225 226 node_data = self.GetPyData(self.__curr_node) 227 doc_folder = self.__pat.get_document_folder() 228 229 if isinstance(node_data, gmEMRStructItems.cHealthIssue): 230 self.__enable_display_mode_selection(True) 231 if self.__details_display_mode == u'details': 232 txt = node_data.format(left_margin=1, patient = self.__pat) 233 else: 234 txt = node_data.format_as_journal(left_margin = 1) 235 236 self.__img_display.refresh ( 237 document_folder = doc_folder, 238 episodes = [ epi['pk_episode'] for epi in node_data.episodes ] 239 ) 240 241 elif isinstance(node_data, type({})): 242 self.__enable_display_mode_selection(False) 243 # FIXME: turn into real dummy issue 244 txt = _('Pool of unassociated episodes:\n\n "%s"') % node_data['description'] 245 self.__img_display.clear() 246 247 elif isinstance(node_data, gmEMRStructItems.cEpisode): 248 self.__enable_display_mode_selection(True) 249 if self.__details_display_mode == u'details': 250 txt = node_data.format(left_margin = 1, patient = self.__pat) 251 else: 252 txt = node_data.format_as_journal(left_margin = 1) 253 self.__img_display.refresh ( 254 document_folder = doc_folder, 255 episodes = [node_data['pk_episode']] 256 ) 257 258 elif isinstance(node_data, gmEMRStructItems.cEncounter): 259 self.__enable_display_mode_selection(False) 260 epi = self.GetPyData(self.GetItemParent(self.__curr_node)) 261 txt = node_data.format ( 262 episodes = [epi['pk_episode']], 263 with_soap = True, 264 left_margin = 1, 265 patient = self.__pat, 266 with_co_encountlet_hints = True 267 ) 268 self.__img_display.refresh ( 269 document_folder = doc_folder, 270 episodes = [epi['pk_episode']], 271 encounter = node_data['pk_encounter'] 272 ) 273 274 # root node == EMR level 275 else: 276 self.__enable_display_mode_selection(False) 277 emr = self.__pat.get_emr() 278 txt = emr.format_summary(dob = self.__pat['dob']) 279 self.__img_display.clear() 280 281 self.__details_display.Clear() 282 self.__details_display.WriteText(txt) 283 self.__details_display.ShowPosition(0)
284 #--------------------------------------------------------
285 - def __make_popup_menus(self):
286 287 # - episodes 288 self.__epi_context_popup = wx.Menu(title = _('Episode Actions:')) 289 290 menu_id = wx.NewId() 291 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Edit details'))) 292 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__edit_episode) 293 294 menu_id = wx.NewId() 295 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Delete'))) 296 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__delete_episode) 297 298 menu_id = wx.NewId() 299 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Promote'))) 300 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__promote_episode_to_issue) 301 302 menu_id = wx.NewId() 303 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Move encounters'))) 304 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__move_encounters) 305 306 # - encounters 307 self.__enc_context_popup = wx.Menu(title = _('Encounter Actions:')) 308 # - move data 309 menu_id = wx.NewId() 310 self.__enc_context_popup.AppendItem(wx.MenuItem(self.__enc_context_popup, menu_id, _('Move data to another episode'))) 311 wx.EVT_MENU(self.__enc_context_popup, menu_id, self.__relink_encounter_data2episode) 312 # - edit encounter details 313 menu_id = wx.NewId() 314 self.__enc_context_popup.AppendItem(wx.MenuItem(self.__enc_context_popup, menu_id, _('Edit details'))) 315 wx.EVT_MENU(self.__enc_context_popup, menu_id, self.__edit_encounter_details) 316 317 item = self.__enc_context_popup.Append(-1, _('Edit progress notes')) 318 self.Bind(wx.EVT_MENU, self.__edit_progress_notes, item) 319 320 item = self.__enc_context_popup.Append(-1, _('Move progress notes')) 321 self.Bind(wx.EVT_MENU, self.__move_progress_notes, item) 322 323 item = self.__enc_context_popup.Append(-1, _('Export for Medistar')) 324 self.Bind(wx.EVT_MENU, self.__export_encounter_for_medistar, item) 325 326 # - health issues 327 self.__issue_context_popup = wx.Menu(title = _('Health Issue Actions:')) 328 329 menu_id = wx.NewId() 330 self.__issue_context_popup.AppendItem(wx.MenuItem(self.__issue_context_popup, menu_id, _('Edit details'))) 331 wx.EVT_MENU(self.__issue_context_popup, menu_id, self.__edit_issue) 332 333 menu_id = wx.NewId() 334 self.__issue_context_popup.AppendItem(wx.MenuItem(self.__issue_context_popup, menu_id, _('Delete'))) 335 wx.EVT_MENU(self.__issue_context_popup, menu_id, self.__delete_issue) 336 337 self.__issue_context_popup.AppendSeparator() 338 339 menu_id = wx.NewId() 340 self.__issue_context_popup.AppendItem(wx.MenuItem(self.__issue_context_popup, menu_id, _('Open to encounter level'))) 341 wx.EVT_MENU(self.__issue_context_popup, menu_id, self.__expand_issue_to_encounter_level) 342 # print " attach issue to another patient" 343 # print " move all episodes to another issue" 344 345 # - root node 346 self.__root_context_popup = wx.Menu(title = _('EMR Actions:')) 347 348 menu_id = wx.NewId() 349 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Create health issue'))) 350 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__create_issue) 351 352 item = self.__root_context_popup.Append(-1, _('Create episode')) 353 self.Bind(wx.EVT_MENU, self.__create_episode, item) 354 355 menu_id = wx.NewId() 356 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage allergies'))) 357 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__document_allergy) 358 359 menu_id = wx.NewId() 360 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage family history'))) 361 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_family_history) 362 363 menu_id = wx.NewId() 364 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage hospitalizations'))) 365 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_hospital_stays) 366 367 menu_id = wx.NewId() 368 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage occupation'))) 369 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_occupation) 370 371 menu_id = wx.NewId() 372 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage procedures'))) 373 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_procedures) 374 375 menu_id = wx.NewId() 376 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage vaccinations'))) 377 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_vaccinations) 378 379 self.__root_context_popup.AppendSeparator() 380 381 # expand tree 382 expand_menu = wx.Menu() 383 self.__root_context_popup.AppendMenu(wx.NewId(), _('Open EMR to ...'), expand_menu) 384 385 menu_id = wx.NewId() 386 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... issue level'))) 387 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_issue_level) 388 389 menu_id = wx.NewId() 390 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... episode level'))) 391 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_episode_level) 392 393 menu_id = wx.NewId() 394 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... encounter level'))) 395 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_encounter_level)
396 #--------------------------------------------------------
397 - def __handle_root_context(self, pos=wx.DefaultPosition):
398 self.PopupMenu(self.__root_context_popup, pos)
399 #--------------------------------------------------------
400 - def __handle_issue_context(self, pos=wx.DefaultPosition):
401 # self.__issue_context_popup.SetTitle(_('Episode %s') % episode['description']) 402 self.PopupMenu(self.__issue_context_popup, pos)
403 #--------------------------------------------------------
404 - def __handle_episode_context(self, pos=wx.DefaultPosition):
405 # self.__epi_context_popup.SetTitle(_('Episode %s') % self.__curr_node_data['description']) 406 self.PopupMenu(self.__epi_context_popup, pos)
407 #--------------------------------------------------------
408 - def __handle_encounter_context(self, pos=wx.DefaultPosition):
409 self.PopupMenu(self.__enc_context_popup, pos)
410 #-------------------------------------------------------- 411 # episode level 412 #--------------------------------------------------------
413 - def __move_encounters(self, event):
414 episode = self.GetPyData(self.__curr_node) 415 416 gmNarrativeWidgets.move_progress_notes_to_another_encounter ( 417 parent = self, 418 episodes = [episode['pk_episode']], 419 move_all = True 420 )
421 #--------------------------------------------------------
422 - def __edit_episode(self, event):
423 gmEMRStructWidgets.edit_episode(parent = self, episode = self.__curr_node_data)
424 #--------------------------------------------------------
425 - def __promote_episode_to_issue(self, evt):
426 pat = gmPerson.gmCurrentPatient() 427 gmEMRStructWidgets.promote_episode_to_issue(parent=self, episode = self.__curr_node_data, emr = pat.get_emr())
428 #--------------------------------------------------------
429 - def __delete_episode(self, event):
430 dlg = gmGuiHelpers.c2ButtonQuestionDlg ( 431 parent = self, 432 id = -1, 433 caption = _('Deleting episode'), 434 button_defs = [ 435 {'label': _('Yes, delete'), 'tooltip': _('Delete the episode if possible (it must be completely empty).')}, 436 {'label': _('No, cancel'), 'tooltip': _('Cancel and do NOT delete the episode.')} 437 ], 438 question = _( 439 'Are you sure you want to delete this episode ?\n' 440 '\n' 441 ' "%s"\n' 442 ) % self.__curr_node_data['description'] 443 ) 444 result = dlg.ShowModal() 445 if result != wx.ID_YES: 446 return 447 448 try: 449 gmEMRStructItems.delete_episode(episode = self.__curr_node_data) 450 except gmExceptions.DatabaseObjectInUseError: 451 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete episode. There is still clinical data recorded for it.')) 452 return
453 #-------------------------------------------------------- 454 # encounter level 455 #--------------------------------------------------------
456 - def __move_progress_notes(self, evt):
457 encounter = self.GetPyData(self.__curr_node) 458 node_parent = self.GetItemParent(self.__curr_node) 459 episode = self.GetPyData(node_parent) 460 461 gmNarrativeWidgets.move_progress_notes_to_another_encounter ( 462 parent = self, 463 encounters = [encounter['pk_encounter']], 464 episodes = [episode['pk_episode']] 465 )
466 #--------------------------------------------------------
467 - def __edit_progress_notes(self, event):
468 encounter = self.GetPyData(self.__curr_node) 469 node_parent = self.GetItemParent(self.__curr_node) 470 episode = self.GetPyData(node_parent) 471 472 gmNarrativeWidgets.manage_progress_notes ( 473 parent = self, 474 encounters = [encounter['pk_encounter']], 475 episodes = [episode['pk_episode']] 476 )
477 #--------------------------------------------------------
478 - def __edit_encounter_details(self, event):
479 node_data = self.GetPyData(self.__curr_node) 480 gmEMRStructWidgets.edit_encounter(parent = self, encounter = node_data) 481 self.__populate_tree()
482 #-------------------------------------------------------- 500 #-------------------------------------------------------- 501 # issue level 502 #--------------------------------------------------------
503 - def __edit_issue(self, event):
504 gmEMRStructWidgets.edit_health_issue(parent = self, issue = self.__curr_node_data)
505 #--------------------------------------------------------
506 - def __delete_issue(self, event):
507 dlg = gmGuiHelpers.c2ButtonQuestionDlg ( 508 parent = self, 509 id = -1, 510 caption = _('Deleting health issue'), 511 button_defs = [ 512 {'label': _('Yes, delete'), 'tooltip': _('Delete the health issue if possible (it must be completely empty).')}, 513 {'label': _('No, cancel'), 'tooltip': _('Cancel and do NOT delete the health issue.')} 514 ], 515 question = _( 516 'Are you sure you want to delete this health issue ?\n' 517 '\n' 518 ' "%s"\n' 519 ) % self.__curr_node_data['description'] 520 ) 521 result = dlg.ShowModal() 522 if result != wx.ID_YES: 523 dlg.Destroy() 524 return 525 526 dlg.Destroy() 527 528 try: 529 gmEMRStructItems.delete_health_issue(health_issue = self.__curr_node_data) 530 except gmExceptions.DatabaseObjectInUseError: 531 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete health issue. There is still clinical data recorded for it.'))
532 #--------------------------------------------------------
533 - def __expand_issue_to_encounter_level(self, evt):
534 535 if not self.__curr_node.IsOk(): 536 return 537 538 self.Expand(self.__curr_node) 539 540 epi, epi_cookie = self.GetFirstChild(self.__curr_node) 541 while epi.IsOk(): 542 self.Expand(epi) 543 epi, epi_cookie = self.GetNextChild(self.__curr_node, epi_cookie)
544 #-------------------------------------------------------- 545 # EMR level 546 #--------------------------------------------------------
547 - def __create_issue(self, event):
548 gmEMRStructWidgets.edit_health_issue(parent = self, issue = None)
549 #--------------------------------------------------------
550 - def __create_episode(self, event):
551 gmEMRStructWidgets.edit_episode(parent = self, episode = None)
552 #--------------------------------------------------------
553 - def __document_allergy(self, event):
554 dlg = gmAllergyWidgets.cAllergyManagerDlg(parent=self, id=-1) 555 # FIXME: use signal and use node level update 556 if dlg.ShowModal() == wx.ID_OK: 557 self.__populate_tree() 558 dlg.Destroy() 559 return
560 #--------------------------------------------------------
561 - def __manage_procedures(self, event):
563 #--------------------------------------------------------
564 - def __manage_family_history(self, event):
566 #--------------------------------------------------------
567 - def __manage_hospital_stays(self, event):
569 #--------------------------------------------------------
570 - def __manage_occupation(self, event):
572 #--------------------------------------------------------
573 - def __manage_vaccinations(self, event):
574 gmVaccWidgets.manage_vaccinations(parent = self)
575 #--------------------------------------------------------
576 - def __expand_to_issue_level(self, evt):
577 578 root_item = self.GetRootItem() 579 580 if not root_item.IsOk(): 581 return 582 583 self.Expand(root_item) 584 585 # collapse episodes and issues 586 issue, issue_cookie = self.GetFirstChild(root_item) 587 while issue.IsOk(): 588 self.Collapse(issue) 589 epi, epi_cookie = self.GetFirstChild(issue) 590 while epi.IsOk(): 591 self.Collapse(epi) 592 epi, epi_cookie = self.GetNextChild(issue, epi_cookie) 593 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
594 #--------------------------------------------------------
595 - def __expand_to_episode_level(self, evt):
596 597 root_item = self.GetRootItem() 598 599 if not root_item.IsOk(): 600 return 601 602 self.Expand(root_item) 603 604 # collapse episodes, expand issues 605 issue, issue_cookie = self.GetFirstChild(root_item) 606 while issue.IsOk(): 607 self.Expand(issue) 608 epi, epi_cookie = self.GetFirstChild(issue) 609 while epi.IsOk(): 610 self.Collapse(epi) 611 epi, epi_cookie = self.GetNextChild(issue, epi_cookie) 612 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
613 #--------------------------------------------------------
614 - def __expand_to_encounter_level(self, evt):
615 616 root_item = self.GetRootItem() 617 618 if not root_item.IsOk(): 619 return 620 621 self.Expand(root_item) 622 623 # collapse episodes, expand issues 624 issue, issue_cookie = self.GetFirstChild(root_item) 625 while issue.IsOk(): 626 self.Expand(issue) 627 epi, epi_cookie = self.GetFirstChild(issue) 628 while epi.IsOk(): 629 self.Expand(epi) 630 epi, epi_cookie = self.GetNextChild(issue, epi_cookie) 631 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
632 #--------------------------------------------------------
633 - def __export_encounter_for_medistar(self, evt):
634 gmNarrativeWidgets.export_narrative_for_medistar_import ( 635 parent = self, 636 soap_cats = u'soap', 637 encounter = self.__curr_node_data 638 )
639 #-------------------------------------------------------- 640 # event handlers 641 #--------------------------------------------------------
642 - def _on_narrative_mod_db(self, *args, **kwargs):
643 wx.CallAfter(self.__update_text_for_selected_node)
644 #--------------------------------------------------------
645 - def _on_episode_mod_db(self, *args, **kwargs):
646 wx.CallAfter(self.__populate_tree)
647 #--------------------------------------------------------
648 - def _on_issue_mod_db(self, *args, **kwargs):
649 wx.CallAfter(self.__populate_tree)
650 #--------------------------------------------------------
651 - def _on_tree_item_selected(self, event):
652 sel_item = event.GetItem() 653 self.__curr_node = sel_item 654 self.__update_text_for_selected_node() 655 return True
656 # #-------------------------------------------------------- 657 # def _on_mouse_motion(self, event): 658 # 659 # cursor_pos = (event.GetX(), event.GetY()) 660 # 661 # self.SetToolTipString(u'') 662 # 663 # if cursor_pos != self._old_cursor_pos: 664 # self._old_cursor_pos = cursor_pos 665 # (item, flags) = self.HitTest(cursor_pos) 666 # #if flags != wx.TREE_HITTEST_NOWHERE: 667 # if flags == wx.TREE_HITTEST_ONITEMLABEL: 668 # data = self.GetPyData(item) 669 # 670 # if not isinstance(data, gmEMRStructItems.cEncounter): 671 # return 672 # 673 # self.SetToolTip(u'%s %s %s - %s\n\nRFE: %s\nAOE: %s' % ( 674 # data['started'].strftime('%x'), 675 # data['l10n_type'], 676 # data['started'].strftime('%H:%m'), 677 # data['last_affirmed'].strftime('%H:%m'), 678 # gmTools.coalesce(data['reason_for_encounter'], u''), 679 # gmTools.coalesce(data['assessment_of_encounter'], u'') 680 # )) 681 #--------------------------------------------------------
682 - def _on_tree_item_gettooltip(self, event):
683 684 item = event.GetItem() 685 686 if not item.IsOk(): 687 event.SetToolTip(u' ') 688 return 689 690 data = self.GetPyData(item) 691 692 if isinstance(data, gmEMRStructItems.cEncounter): 693 tt = u'%s %s %s - %s\n' % ( 694 data['started'].strftime('%x'), 695 data['l10n_type'], 696 data['started'].strftime('%H:%M'), 697 data['last_affirmed'].strftime('%H:%M') 698 ) 699 if data['reason_for_encounter'] is not None: 700 tt += u'\n' 701 tt += _('RFE: %s') % data['reason_for_encounter'] 702 if len(data['pk_generic_codes_rfe']) > 0: 703 for code in data.generic_codes_rfe: 704 tt += u'\n %s: %s%s%s\n (%s %s)' % ( 705 code['code'], 706 gmTools.u_left_double_angle_quote, 707 code['term'], 708 gmTools.u_right_double_angle_quote, 709 code['name_short'], 710 code['version'] 711 ) 712 if data['assessment_of_encounter'] is not None: 713 tt += u'\n' 714 tt += _('AOE: %s') % data['assessment_of_encounter'] 715 if len(data['pk_generic_codes_aoe']) > 0: 716 for code in data.generic_codes_aoe: 717 tt += u'\n %s: %s%s%s\n (%s %s)' % ( 718 code['code'], 719 gmTools.u_left_double_angle_quote, 720 code['term'], 721 gmTools.u_right_double_angle_quote, 722 code['name_short'], 723 code['version'] 724 ) 725 726 elif isinstance(data, gmEMRStructItems.cEpisode): 727 tt = u'' 728 tt += gmTools.bool2subst ( 729 (data['diagnostic_certainty_classification'] is not None), 730 data.diagnostic_certainty_description + u'\n\n', 731 u'' 732 ) 733 tt += gmTools.bool2subst ( 734 data['episode_open'], 735 _('ongoing episode'), 736 _('closed episode'), 737 'error: episode state is None' 738 ) + u'\n' 739 tt += gmTools.coalesce(data['summary'], u'', u'\n%s') 740 if len(data['pk_generic_codes']) > 0: 741 tt += u'\n' 742 for code in data.generic_codes: 743 tt += u'%s: %s%s%s\n (%s %s)\n' % ( 744 code['code'], 745 gmTools.u_left_double_angle_quote, 746 code['term'], 747 gmTools.u_right_double_angle_quote, 748 code['name_short'], 749 code['version'] 750 ) 751 752 tt = tt.strip(u'\n') 753 if tt == u'': 754 tt = u' ' 755 756 elif isinstance(data, gmEMRStructItems.cHealthIssue): 757 tt = u'' 758 tt += gmTools.bool2subst(data['is_confidential'], _('*** CONFIDENTIAL ***\n\n'), u'') 759 tt += gmTools.bool2subst ( 760 (data['diagnostic_certainty_classification'] is not None), 761 data.diagnostic_certainty_description + u'\n', 762 u'' 763 ) 764 tt += gmTools.bool2subst ( 765 (data['laterality'] not in [None, u'na']), 766 data.laterality_description + u'\n', 767 u'' 768 ) 769 # noted_at_age is too costly 770 tt += gmTools.bool2subst(data['is_active'], _('active') + u'\n', u'') 771 tt += gmTools.bool2subst(data['clinically_relevant'], _('clinically relevant') + u'\n', u'') 772 tt += gmTools.bool2subst(data['is_cause_of_death'], _('contributed to death') + u'\n', u'') 773 tt += gmTools.coalesce(data['grouping'], u'\n', _('Grouping: %s') + u'\n') 774 tt += gmTools.coalesce(data['summary'], u'', u'\n%s') 775 if len(data['pk_generic_codes']) > 0: 776 tt += u'\n' 777 for code in data.generic_codes: 778 tt += u'%s: %s%s%s\n (%s %s)\n' % ( 779 code['code'], 780 gmTools.u_left_double_angle_quote, 781 code['term'], 782 gmTools.u_right_double_angle_quote, 783 code['name_short'], 784 code['version'] 785 ) 786 787 tt = tt.strip(u'\n') 788 if tt == u'': 789 tt = u' ' 790 791 else: 792 tt = self.__root_tooltip 793 794 event.SetToolTip(tt)
795 796 # doing this prevents the tooltip from showing at all 797 #event.Skip() 798 799 #widgetXY.GetToolTip().Enable(False) 800 # 801 #seems to work, supposing the tooltip is actually set for the widget, 802 #otherwise a test would be needed 803 #if widgetXY.GetToolTip(): 804 # widgetXY.GetToolTip().Enable(False) 805 #--------------------------------------------------------
806 - def _on_tree_item_right_clicked(self, event):
807 """Right button clicked: display the popup for the tree""" 808 809 node = event.GetItem() 810 self.SelectItem(node) 811 self.__curr_node_data = self.GetPyData(node) 812 self.__curr_node = node 813 814 pos = wx.DefaultPosition 815 if isinstance(self.__curr_node_data, gmEMRStructItems.cHealthIssue): 816 self.__handle_issue_context(pos=pos) 817 elif isinstance(self.__curr_node_data, gmEMRStructItems.cEpisode): 818 self.__handle_episode_context(pos=pos) 819 elif isinstance(self.__curr_node_data, gmEMRStructItems.cEncounter): 820 self.__handle_encounter_context(pos=pos) 821 elif node == self.GetRootItem(): 822 self.__handle_root_context() 823 elif type(self.__curr_node_data) == type({}): 824 # ignore pseudo node "free-standing episodes" 825 pass 826 else: 827 print "error: unknown node type, no popup menu" 828 event.Skip()
829 #--------------------------------------------------------
830 - def OnCompareItems (self, node1=None, node2=None):
831 """Used in sorting items. 832 833 -1: 1 < 2 834 0: 1 = 2 835 1: 1 > 2 836 """ 837 # FIXME: implement sort modes, chron, reverse cron, by regex, etc 838 839 if not node1: 840 _log.debug('invalid node 1') 841 return 0 842 if not node2: 843 _log.debug('invalid node 2') 844 return 0 845 846 if not node1.IsOk(): 847 _log.debug('invalid node 1') 848 return 0 849 if not node2.IsOk(): 850 _log.debug('invalid node 2') 851 return 0 852 853 item1 = self.GetPyData(node1) 854 item2 = self.GetPyData(node2) 855 856 # dummy health issue always on top 857 if isinstance(item1, type({})): 858 return -1 859 if isinstance(item2, type({})): 860 return 1 861 862 # encounters: reverse chronologically 863 if isinstance(item1, gmEMRStructItems.cEncounter): 864 if item1['started'] == item2['started']: 865 return 0 866 if item1['started'] > item2['started']: 867 return -1 868 return 1 869 870 # episodes: chronologically 871 if isinstance(item1, gmEMRStructItems.cEpisode): 872 start1 = item1.get_access_range()[0] 873 start2 = item2.get_access_range()[0] 874 if start1 == start2: 875 return 0 876 if start1 < start2: 877 return -1 878 return 1 879 880 # issues: alpha by grouping, no grouping at the bottom 881 if isinstance(item1, gmEMRStructItems.cHealthIssue): 882 883 # no grouping below grouping 884 if item1['grouping'] is None: 885 if item2['grouping'] is not None: 886 return 1 887 888 # grouping above no grouping 889 if item1['grouping'] is not None: 890 if item2['grouping'] is None: 891 return -1 892 893 # both no grouping: alpha on description 894 if (item1['grouping'] is None) and (item2['grouping'] is None): 895 if item1['description'].lower() < item2['description'].lower(): 896 return -1 897 if item1['description'].lower() > item2['description'].lower(): 898 return 1 899 return 0 900 901 # both with grouping: alpha on grouping, then alpha on description 902 if item1['grouping'] < item2['grouping']: 903 return -1 904 905 if item1['grouping'] > item2['grouping']: 906 return 1 907 908 if item1['description'].lower() < item2['description'].lower(): 909 return -1 910 911 if item1['description'].lower() > item2['description'].lower(): 912 return 1 913 914 return 0 915 916 _log.error('unknown item type during sorting EMR tree:') 917 _log.error('item1: %s', type(item1)) 918 _log.error('item2: %s', type(item2)) 919 920 return 0
921 #-------------------------------------------------------- 922 # properties 923 #--------------------------------------------------------
924 - def _get_details_display_mode(self):
925 return self.__details_display_mode
926
927 - def _set_details_display_mode(self, mode):
928 if mode not in [u'details', u'journal']: 929 raise ValueError('details display mode must be one of "details", "journal"') 930 if self.__details_display_mode == mode: 931 return 932 self.__details_display_mode = mode 933 self.__update_text_for_selected_node()
934 935 details_display_mode = property(_get_details_display_mode, _set_details_display_mode)
936 #================================================================ 937 from Gnumed.wxGladeWidgets import wxgScrolledEMRTreePnl 938
939 -class cScrolledEMRTreePnl(wxgScrolledEMRTreePnl.wxgScrolledEMRTreePnl):
940 """A scrollable panel holding an EMR tree. 941 942 Lacks a widget to display details for selected items. The 943 tree data will be refetched - if necessary - whenever 944 repopulate_ui() is called, e.g., when then patient is changed. 945 """
946 - def __init__(self, *args, **kwds):
948 #--------------------------------------------------------
949 - def repopulate_ui(self):
950 self._emr_tree.refresh() 951 return True
952 #============================================================ 953 from Gnumed.wxGladeWidgets import wxgSplittedEMRTreeBrowserPnl 954
955 -class cSplittedEMRTreeBrowserPnl(wxgSplittedEMRTreeBrowserPnl.wxgSplittedEMRTreeBrowserPnl):
956 """A splitter window holding an EMR tree. 957 958 The left hand side displays a scrollable EMR tree while 959 on the right details for selected items are displayed. 960 961 Expects to be put into a Notebook. 962 """
963 - def __init__(self, *args, **kwds):
964 wxgSplittedEMRTreeBrowserPnl.wxgSplittedEMRTreeBrowserPnl.__init__(self, *args, **kwds) 965 self._pnl_emr_tree._emr_tree.set_narrative_display(narrative_display = self._TCTRL_item_details) 966 self._pnl_emr_tree._emr_tree.set_image_display(image_display = self._PNL_visual_soap) 967 self._pnl_emr_tree._emr_tree.set_enable_display_mode_selection_callback(self.enable_display_mode_selection) 968 self.__register_events()
969 #--------------------------------------------------------
970 - def __register_events(self):
971 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection) 972 return True
973 #-------------------------------------------------------- 974 # event handler 975 #--------------------------------------------------------
976 - def _on_post_patient_selection(self):
977 if self.GetParent().GetCurrentPage() == self: 978 self.repopulate_ui() 979 return True
980 #--------------------------------------------------------
981 - def _on_show_details_selected(self, event):
982 #event.Skip() 983 self._pnl_emr_tree._emr_tree.details_display_mode = u'details'
984 #--------------------------------------------------------
985 - def _on_show_journal_selected(self, event):
986 #event.Skip() 987 self._pnl_emr_tree._emr_tree.details_display_mode = u'journal'
988 #-------------------------------------------------------- 989 # external API 990 #--------------------------------------------------------
991 - def repopulate_ui(self):
992 """Fills UI with data.""" 993 self._pnl_emr_tree.repopulate_ui() 994 self._splitter_browser.SetSashPosition(self._splitter_browser.GetSizeTuple()[0]/3, True) 995 return True
996 #--------------------------------------------------------
997 - def enable_display_mode_selection(self, enable):
998 if enable: 999 self._RBTN_details.Enable(True) 1000 self._RBTN_journal.Enable(True) 1001 return 1002 self._RBTN_details.Enable(False) 1003 self._RBTN_journal.Enable(False)
1004 #================================================================
1005 -class cEMRJournalPanel(wx.Panel):
1006 - def __init__(self, *args, **kwargs):
1007 wx.Panel.__init__(self, *args, **kwargs) 1008 1009 self.__do_layout() 1010 self.__register_events()
1011 #--------------------------------------------------------
1012 - def __do_layout(self):
1013 self.__journal = wx.TextCtrl ( 1014 self, 1015 -1, 1016 _('No EMR data loaded.'), 1017 style = wx.TE_MULTILINE | wx.TE_READONLY 1018 ) 1019 self.__journal.SetFont(wx.Font(10, wx.MODERN, wx.NORMAL, wx.NORMAL)) 1020 # arrange widgets 1021 szr_outer = wx.BoxSizer(wx.VERTICAL) 1022 szr_outer.Add(self.__journal, 1, wx.EXPAND, 0) 1023 # and do layout 1024 self.SetAutoLayout(1) 1025 self.SetSizer(szr_outer) 1026 szr_outer.Fit(self) 1027 szr_outer.SetSizeHints(self) 1028 self.Layout()
1029 #--------------------------------------------------------
1030 - def __register_events(self):
1031 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
1032 #--------------------------------------------------------
1033 - def _on_post_patient_selection(self):
1034 """Expects to be in a Notebook.""" 1035 if self.GetParent().GetCurrentPage() == self: 1036 self.repopulate_ui() 1037 return True
1038 #-------------------------------------------------------- 1039 # notebook plugin API 1040 #--------------------------------------------------------
1041 - def repopulate_ui(self):
1042 txt = StringIO.StringIO() 1043 exporter = gmPatientExporter.cEMRJournalExporter() 1044 # FIXME: if journal is large this will error out, use generator/yield etc 1045 # FIXME: turn into proper list 1046 try: 1047 exporter.export(txt) 1048 self.__journal.SetValue(txt.getvalue()) 1049 except ValueError: 1050 _log.exception('cannot get EMR journal') 1051 self.__journal.SetValue (_( 1052 'An error occurred while retrieving the EMR\n' 1053 'in journal form for the active patient.\n\n' 1054 'Please check the log file for details.' 1055 )) 1056 txt.close() 1057 self.__journal.ShowPosition(self.__journal.GetLastPosition()) 1058 return True
1059 #================================================================ 1060 # MAIN 1061 #---------------------------------------------------------------- 1062 if __name__ == '__main__': 1063 1064 _log.info("starting emr browser...") 1065 1066 try: 1067 # obtain patient 1068 patient = gmPersonSearch.ask_for_patient() 1069 if patient is None: 1070 print "No patient. Exiting gracefully..." 1071 sys.exit(0) 1072 gmPatSearchWidgets.set_active_patient(patient = patient) 1073 1074 # display standalone browser 1075 application = wx.PyWidgetTester(size=(800,600)) 1076 emr_browser = cEMRBrowserPanel(application.frame, -1) 1077 emr_browser.refresh_tree() 1078 1079 application.frame.Show(True) 1080 application.MainLoop() 1081 1082 # clean up 1083 if patient is not None: 1084 try: 1085 patient.cleanup() 1086 except: 1087 print "error cleaning up patient" 1088 except StandardError: 1089 _log.exception("unhandled exception caught !") 1090 # but re-raise them 1091 raise 1092 1093 _log.info("closing emr browser...") 1094 1095 #================================================================ 1096