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