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