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 #--------------------------------------------------------
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):
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 event.SetToolTip(u'%s %s %s - %s\n\nRFE: %s\nAOE: %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''), 668 gmTools.coalesce(data['assessment_of_encounter'], u'') 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 tt = tt.strip(u'\n') 686 if tt == u'': 687 tt = u' ' 688 event.SetToolTip(tt) 689 690 elif isinstance(data, gmEMRStructItems.cHealthIssue): 691 tt = u'' 692 tt += gmTools.bool2subst(data['is_confidential'], _('*** CONFIDENTIAL ***\n\n'), u'') 693 tt += gmTools.bool2subst ( 694 (data['diagnostic_certainty_classification'] is not None), 695 data.diagnostic_certainty_description + u'\n', 696 u'' 697 ) 698 tt += gmTools.bool2subst ( 699 (data['laterality'] not in [None, u'na']), 700 data.laterality_description + u'\n', 701 u'' 702 ) 703 # noted_at_age is too costly 704 tt += gmTools.bool2subst(data['is_active'], _('active') + u'\n', u'') 705 tt += gmTools.bool2subst(data['clinically_relevant'], _('clinically relevant') + u'\n', u'') 706 tt += gmTools.bool2subst(data['is_cause_of_death'], _('contributed to death') + u'\n', u'') 707 tt += gmTools.coalesce(data['grouping'], u'\n', _('Grouping: %s') + u'\n\n') 708 tt += gmTools.coalesce(data['summary'], u'') 709 tt = tt.strip(u'\n') 710 if tt == u'': 711 tt = u' ' 712 event.SetToolTip(tt) 713 714 else: 715 event.SetToolTip(self.__root_tooltip)
716 #event.SetToolTip(u' ') 717 ##self.SetToolTipString(u'') 718 719 # doing this prevents the tooltip from showing at all 720 #event.Skip() 721 722 #widgetXY.GetToolTip().Enable(False) 723 # 724 #seems to work, supposing the tooltip is actually set for the widget, 725 #otherwise a test would be needed 726 #if widgetXY.GetToolTip(): 727 # widgetXY.GetToolTip().Enable(False) 728 #--------------------------------------------------------
729 - def _on_tree_item_right_clicked(self, event):
730 """Right button clicked: display the popup for the tree""" 731 732 node = event.GetItem() 733 self.SelectItem(node) 734 self.__curr_node_data = self.GetPyData(node) 735 self.__curr_node = node 736 737 pos = wx.DefaultPosition 738 if isinstance(self.__curr_node_data, gmEMRStructItems.cHealthIssue): 739 self.__handle_issue_context(pos=pos) 740 elif isinstance(self.__curr_node_data, gmEMRStructItems.cEpisode): 741 self.__handle_episode_context(pos=pos) 742 elif isinstance(self.__curr_node_data, gmEMRStructItems.cEncounter): 743 self.__handle_encounter_context(pos=pos) 744 elif node == self.GetRootItem(): 745 self.__handle_root_context() 746 elif type(self.__curr_node_data) == type({}): 747 # ignore pseudo node "free-standing episodes" 748 pass 749 else: 750 print "error: unknown node type, no popup menu" 751 event.Skip()
752 #--------------------------------------------------------
753 - def OnCompareItems (self, node1=None, node2=None):
754 """Used in sorting items. 755 756 -1: 1 < 2 757 0: 1 = 2 758 1: 1 > 2 759 """ 760 # FIXME: implement sort modes, chron, reverse cron, by regex, etc 761 762 item1 = self.GetPyData(node1) 763 item2 = self.GetPyData(node2) 764 765 # dummy health issue always on top 766 if isinstance(item1, type({})): 767 return -1 768 if isinstance(item2, type({})): 769 return 1 770 771 # encounters: reverse chronologically 772 if isinstance(item1, gmEMRStructItems.cEncounter): 773 if item1['started'] == item2['started']: 774 return 0 775 if item1['started'] > item2['started']: 776 return -1 777 return 1 778 779 # episodes: chronologically 780 if isinstance(item1, gmEMRStructItems.cEpisode): 781 start1 = item1.get_access_range()[0] 782 start2 = item2.get_access_range()[0] 783 if start1 == start2: 784 return 0 785 if start1 < start2: 786 return -1 787 return 1 788 789 # issues: alpha by grouping, no grouping at the bottom 790 if isinstance(item1, gmEMRStructItems.cHealthIssue): 791 792 # no grouping below grouping 793 if item1['grouping'] is None: 794 if item2['grouping'] is not None: 795 return 1 796 797 # grouping above no grouping 798 if item1['grouping'] is not None: 799 if item2['grouping'] is None: 800 return -1 801 802 # both no grouping: alpha on description 803 if (item1['grouping'] is None) and (item2['grouping'] is None): 804 if item1['description'].lower() < item2['description'].lower(): 805 return -1 806 if item1['description'].lower() > item2['description'].lower(): 807 return 1 808 return 0 809 810 # both with grouping: alpha on grouping, then alpha on description 811 if item1['grouping'] < item2['grouping']: 812 return -1 813 814 if item1['grouping'] > item2['grouping']: 815 return 1 816 817 if item1['description'].lower() < item2['description'].lower(): 818 return -1 819 820 if item1['description'].lower() > item2['description'].lower(): 821 return 1 822 823 return 0 824 825 _log.error('unknown item type during sorting EMR tree:') 826 _log.error('item1: %s', type(item1)) 827 _log.error('item2: %s', type(item2)) 828 829 return 0
830 #-------------------------------------------------------- 831 # properties 832 #--------------------------------------------------------
834 return self.__details_display_mode
835
836 - def _set_details_display_mode(self, mode):
837 if mode not in [u'details', u'journal']: 838 raise ValueError('details display mode must be one of "details", "journal"') 839 if self.__details_display_mode == mode: 840 return 841 self.__details_display_mode = mode 842 self.__update_text_for_selected_node()
843 844 details_display_mode = property(_get_details_display_mode, _set_details_display_mode)
845 #================================================================ 846 from Gnumed.wxGladeWidgets import wxgScrolledEMRTreePnl 847
848 -class cScrolledEMRTreePnl(wxgScrolledEMRTreePnl.wxgScrolledEMRTreePnl):
849 """A scrollable panel holding an EMR tree. 850 851 Lacks a widget to display details for selected items. The 852 tree data will be refetched - if necessary - whenever 853 repopulate_ui() is called, e.g., when then patient is changed. 854 """
855 - def __init__(self, *args, **kwds):
857 #--------------------------------------------------------
858 - def repopulate_ui(self):
859 self._emr_tree.refresh() 860 return True
861 #============================================================ 862 from Gnumed.wxGladeWidgets import wxgSplittedEMRTreeBrowserPnl 863
864 -class cSplittedEMRTreeBrowserPnl(wxgSplittedEMRTreeBrowserPnl.wxgSplittedEMRTreeBrowserPnl):
865 """A splitter window holding an EMR tree. 866 867 The left hand side displays a scrollable EMR tree while 868 on the right details for selected items are displayed. 869 870 Expects to be put into a Notebook. 871 """
872 - def __init__(self, *args, **kwds):
873 wxgSplittedEMRTreeBrowserPnl.wxgSplittedEMRTreeBrowserPnl.__init__(self, *args, **kwds) 874 self._pnl_emr_tree._emr_tree.set_narrative_display(narrative_display = self._TCTRL_item_details) 875 self._pnl_emr_tree._emr_tree.set_image_display(image_display = self._PNL_visual_soap) 876 self.__register_events()
877 #--------------------------------------------------------
878 - def __register_events(self):
879 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection) 880 return True
881 #-------------------------------------------------------- 882 # event handler 883 #--------------------------------------------------------
884 - def _on_post_patient_selection(self):
885 if self.GetParent().GetCurrentPage() == self: 886 self.repopulate_ui() 887 return True
888 #--------------------------------------------------------
889 - def _on_show_details_selected(self, event):
890 #event.Skip() 891 self._pnl_emr_tree._emr_tree.details_display_mode = u'details'
892 #--------------------------------------------------------
893 - def _on_show_journal_selected(self, event):
894 #event.Skip() 895 self._pnl_emr_tree._emr_tree.details_display_mode = u'journal'
896 #-------------------------------------------------------- 897 # external API 898 #--------------------------------------------------------
899 - def repopulate_ui(self):
900 """Fills UI with data.""" 901 self._pnl_emr_tree.repopulate_ui() 902 self._splitter_browser.SetSashPosition(self._splitter_browser.GetSizeTuple()[0]/3, True) 903 return True
904 #================================================================
905 -class cEMRJournalPanel(wx.Panel):
906 - def __init__(self, *args, **kwargs):
907 wx.Panel.__init__(self, *args, **kwargs) 908 909 self.__do_layout() 910 self.__register_events()
911 #--------------------------------------------------------
912 - def __do_layout(self):
913 self.__journal = wx.TextCtrl ( 914 self, 915 -1, 916 _('No EMR data loaded.'), 917 style = wx.TE_MULTILINE | wx.TE_READONLY 918 ) 919 self.__journal.SetFont(wx.Font(10, wx.MODERN, wx.NORMAL, wx.NORMAL)) 920 # arrange widgets 921 szr_outer = wx.BoxSizer(wx.VERTICAL) 922 szr_outer.Add(self.__journal, 1, wx.EXPAND, 0) 923 # and do layout 924 self.SetAutoLayout(1) 925 self.SetSizer(szr_outer) 926 szr_outer.Fit(self) 927 szr_outer.SetSizeHints(self) 928 self.Layout()
929 #--------------------------------------------------------
930 - def __register_events(self):
931 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
932 #--------------------------------------------------------
933 - def _on_post_patient_selection(self):
934 """Expects to be in a Notebook.""" 935 if self.GetParent().GetCurrentPage() == self: 936 self.repopulate_ui() 937 return True
938 #-------------------------------------------------------- 939 # notebook plugin API 940 #--------------------------------------------------------
941 - def repopulate_ui(self):
942 txt = StringIO.StringIO() 943 exporter = gmPatientExporter.cEMRJournalExporter() 944 # FIXME: if journal is large this will error out, use generator/yield etc 945 # FIXME: turn into proper list 946 try: 947 exporter.export(txt) 948 self.__journal.SetValue(txt.getvalue()) 949 except ValueError: 950 _log.exception('cannot get EMR journal') 951 self.__journal.SetValue (_( 952 'An error occurred while retrieving the EMR\n' 953 'in journal form for the active patient.\n\n' 954 'Please check the log file for details.' 955 )) 956 txt.close() 957 self.__journal.ShowPosition(self.__journal.GetLastPosition()) 958 return True
959 #================================================================ 960 # MAIN 961 #---------------------------------------------------------------- 962 if __name__ == '__main__': 963 964 _log.info("starting emr browser...") 965 966 try: 967 # obtain patient 968 patient = gmPersonSearch.ask_for_patient() 969 if patient is None: 970 print "No patient. Exiting gracefully..." 971 sys.exit(0) 972 gmPatSearchWidgets.set_active_patient(patient = patient) 973 974 # display standalone browser 975 application = wx.PyWidgetTester(size=(800,600)) 976 emr_browser = cEMRBrowserPanel(application.frame, -1) 977 emr_browser.refresh_tree() 978 979 application.frame.Show(True) 980 application.MainLoop() 981 982 # clean up 983 if patient is not None: 984 try: 985 patient.cleanup() 986 except: 987 print "error cleaning up patient" 988 except StandardError: 989 _log.exception("unhandled exception caught !") 990 # but re-raise them 991 raise 992 993 _log.info("closing emr browser...") 994 995 #================================================================ 996