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 #--------------------------------------------------------
266 - def __make_popup_menus(self):
267 268 # - episodes 269 self.__epi_context_popup = wx.Menu(title = _('Episode Menu')) 270 271 menu_id = wx.NewId() 272 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Edit details'))) 273 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__edit_episode) 274 275 menu_id = wx.NewId() 276 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Delete'))) 277 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__delete_episode) 278 279 menu_id = wx.NewId() 280 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Promote'))) 281 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__promote_episode_to_issue) 282 283 menu_id = wx.NewId() 284 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Move encounters'))) 285 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__move_encounters) 286 287 # - encounters 288 self.__enc_context_popup = wx.Menu(title = _('Encounter Menu')) 289 # - move data 290 menu_id = wx.NewId() 291 self.__enc_context_popup.AppendItem(wx.MenuItem(self.__enc_context_popup, menu_id, _('Move data to another episode'))) 292 wx.EVT_MENU(self.__enc_context_popup, menu_id, self.__relink_encounter_data2episode) 293 # - edit encounter details 294 menu_id = wx.NewId() 295 self.__enc_context_popup.AppendItem(wx.MenuItem(self.__enc_context_popup, menu_id, _('Edit details'))) 296 wx.EVT_MENU(self.__enc_context_popup, menu_id, self.__edit_encounter_details) 297 298 item = self.__enc_context_popup.Append(-1, _('Edit progress notes')) 299 self.Bind(wx.EVT_MENU, self.__edit_progress_notes, item) 300 301 item = self.__enc_context_popup.Append(-1, _('Move progress notes')) 302 self.Bind(wx.EVT_MENU, self.__move_progress_notes, item) 303 304 item = self.__enc_context_popup.Append(-1, _('Export for Medistar')) 305 self.Bind(wx.EVT_MENU, self.__export_encounter_for_medistar, item) 306 307 # - health issues 308 self.__issue_context_popup = wx.Menu(title = _('Health Issue Menu')) 309 310 menu_id = wx.NewId() 311 self.__issue_context_popup.AppendItem(wx.MenuItem(self.__issue_context_popup, menu_id, _('Edit details'))) 312 wx.EVT_MENU(self.__issue_context_popup, menu_id, self.__edit_issue) 313 314 menu_id = wx.NewId() 315 self.__issue_context_popup.AppendItem(wx.MenuItem(self.__issue_context_popup, menu_id, _('Delete'))) 316 wx.EVT_MENU(self.__issue_context_popup, menu_id, self.__delete_issue) 317 318 self.__issue_context_popup.AppendSeparator() 319 320 menu_id = wx.NewId() 321 self.__issue_context_popup.AppendItem(wx.MenuItem(self.__issue_context_popup, menu_id, _('Open to encounter level'))) 322 wx.EVT_MENU(self.__issue_context_popup, menu_id, self.__expand_issue_to_encounter_level) 323 # print " attach issue to another patient" 324 # print " move all episodes to another issue" 325 326 # - root node 327 self.__root_context_popup = wx.Menu(title = _('EMR Menu')) 328 329 menu_id = wx.NewId() 330 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Create health issue'))) 331 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__create_issue) 332 333 menu_id = wx.NewId() 334 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage allergies'))) 335 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__document_allergy) 336 337 menu_id = wx.NewId() 338 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage vaccinations'))) 339 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_vaccinations) 340 341 menu_id = wx.NewId() 342 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage procedures'))) 343 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_procedures) 344 345 menu_id = wx.NewId() 346 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage hospitalizations'))) 347 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_hospital_stays) 348 349 menu_id = wx.NewId() 350 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage occupation'))) 351 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_occupation) 352 353 self.__root_context_popup.AppendSeparator() 354 355 # expand tree 356 expand_menu = wx.Menu() 357 self.__root_context_popup.AppendMenu(wx.NewId(), _('Open EMR to ...'), expand_menu) 358 359 menu_id = wx.NewId() 360 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... issue level'))) 361 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_issue_level) 362 363 menu_id = wx.NewId() 364 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... episode level'))) 365 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_episode_level) 366 367 menu_id = wx.NewId() 368 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... encounter level'))) 369 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_encounter_level)
370 #--------------------------------------------------------
371 - def __handle_root_context(self, pos=wx.DefaultPosition):
372 self.PopupMenu(self.__root_context_popup, pos)
373 #--------------------------------------------------------
374 - def __handle_issue_context(self, pos=wx.DefaultPosition):
375 # self.__issue_context_popup.SetTitle(_('Episode %s') % episode['description']) 376 self.PopupMenu(self.__issue_context_popup, pos)
377 #--------------------------------------------------------
378 - def __handle_episode_context(self, pos=wx.DefaultPosition):
379 self.__epi_context_popup.SetTitle(_('Episode %s') % self.__curr_node_data['description']) 380 self.PopupMenu(self.__epi_context_popup, pos)
381 #--------------------------------------------------------
382 - def __handle_encounter_context(self, pos=wx.DefaultPosition):
383 self.PopupMenu(self.__enc_context_popup, pos)
384 #-------------------------------------------------------- 385 # episode level 386 #--------------------------------------------------------
387 - def __move_encounters(self, event):
388 episode = self.GetPyData(self.__curr_node) 389 390 gmNarrativeWidgets.move_progress_notes_to_another_encounter ( 391 parent = self, 392 episodes = [episode['pk_episode']], 393 move_all = True 394 )
395 #--------------------------------------------------------
396 - def __edit_episode(self, event):
397 gmEMRStructWidgets.edit_episode(parent = self, episode = self.__curr_node_data)
398 #--------------------------------------------------------
399 - def __promote_episode_to_issue(self, evt):
400 pat = gmPerson.gmCurrentPatient() 401 gmEMRStructWidgets.promote_episode_to_issue(parent=self, episode = self.__curr_node_data, emr = pat.get_emr())
402 #--------------------------------------------------------
403 - def __delete_episode(self, event):
404 dlg = gmGuiHelpers.c2ButtonQuestionDlg ( 405 parent = self, 406 id = -1, 407 caption = _('Deleting episode'), 408 button_defs = [ 409 {'label': _('Yes, delete'), 'tooltip': _('Delete the episode if possible (it must be completely empty).')}, 410 {'label': _('No, cancel'), 'tooltip': _('Cancel and do NOT delete the episode.')} 411 ], 412 question = _( 413 'Are you sure you want to delete this episode ?\n' 414 '\n' 415 ' "%s"\n' 416 ) % self.__curr_node_data['description'] 417 ) 418 result = dlg.ShowModal() 419 if result != wx.ID_YES: 420 return 421 422 try: 423 gmEMRStructItems.delete_episode(episode = self.__curr_node_data) 424 except gmExceptions.DatabaseObjectInUseError: 425 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete episode. There is still clinical data recorded for it.')) 426 return
427 #-------------------------------------------------------- 428 # encounter level 429 #--------------------------------------------------------
430 - def __move_progress_notes(self, evt):
431 encounter = self.GetPyData(self.__curr_node) 432 node_parent = self.GetItemParent(self.__curr_node) 433 episode = self.GetPyData(node_parent) 434 435 gmNarrativeWidgets.move_progress_notes_to_another_encounter ( 436 parent = self, 437 encounters = [encounter['pk_encounter']], 438 episodes = [episode['pk_episode']] 439 )
440 #--------------------------------------------------------
441 - def __edit_progress_notes(self, event):
442 encounter = self.GetPyData(self.__curr_node) 443 node_parent = self.GetItemParent(self.__curr_node) 444 episode = self.GetPyData(node_parent) 445 446 gmNarrativeWidgets.manage_progress_notes ( 447 parent = self, 448 encounters = [encounter['pk_encounter']], 449 episodes = [episode['pk_episode']] 450 )
451 #--------------------------------------------------------
452 - def __edit_encounter_details(self, event):
453 node_data = self.GetPyData(self.__curr_node) 454 gmEMRStructWidgets.edit_encounter(parent = self, encounter = node_data) 455 self.__populate_tree()
456 #-------------------------------------------------------- 474 #-------------------------------------------------------- 475 # issue level 476 #--------------------------------------------------------
477 - def __edit_issue(self, event):
478 gmEMRStructWidgets.edit_health_issue(parent = self, issue = self.__curr_node_data)
479 #--------------------------------------------------------
480 - def __delete_issue(self, event):
481 dlg = gmGuiHelpers.c2ButtonQuestionDlg ( 482 parent = self, 483 id = -1, 484 caption = _('Deleting health issue'), 485 button_defs = [ 486 {'label': _('Yes, delete'), 'tooltip': _('Delete the health issue if possible (it must be completely empty).')}, 487 {'label': _('No, cancel'), 'tooltip': _('Cancel and do NOT delete the health issue.')} 488 ], 489 question = _( 490 'Are you sure you want to delete this health issue ?\n' 491 '\n' 492 ' "%s"\n' 493 ) % self.__curr_node_data['description'] 494 ) 495 result = dlg.ShowModal() 496 if result != wx.ID_YES: 497 dlg.Destroy() 498 return 499 500 dlg.Destroy() 501 502 try: 503 gmEMRStructItems.delete_health_issue(health_issue = self.__curr_node_data) 504 except gmExceptions.DatabaseObjectInUseError: 505 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete health issue. There is still clinical data recorded for it.'))
506 #--------------------------------------------------------
508 509 if not self.__curr_node.IsOk(): 510 return 511 512 self.Expand(self.__curr_node) 513 514 epi, epi_cookie = self.GetFirstChild(self.__curr_node) 515 while epi.IsOk(): 516 self.Expand(epi) 517 epi, epi_cookie = self.GetNextChild(self.__curr_node, epi_cookie)
518 #-------------------------------------------------------- 519 # EMR level 520 #--------------------------------------------------------
521 - def __create_issue(self, event):
522 gmEMRStructWidgets.edit_health_issue(parent = self, issue = None)
523 #--------------------------------------------------------
524 - def __document_allergy(self, event):
525 dlg = gmAllergyWidgets.cAllergyManagerDlg(parent=self, id=-1) 526 # FIXME: use signal and use node level update 527 if dlg.ShowModal() == wx.ID_OK: 528 self.__populate_tree() 529 dlg.Destroy() 530 return
531 #--------------------------------------------------------
532 - def __manage_procedures(self, event):
534 #--------------------------------------------------------
535 - def __manage_hospital_stays(self, event):
537 #--------------------------------------------------------
538 - def __manage_occupation(self, event):
540 #--------------------------------------------------------
541 - def __manage_vaccinations(self, event):
543 #--------------------------------------------------------
544 - def __expand_to_issue_level(self, evt):
545 546 root_item = self.GetRootItem() 547 548 if not root_item.IsOk(): 549 return 550 551 self.Expand(root_item) 552 553 # collapse episodes and issues 554 issue, issue_cookie = self.GetFirstChild(root_item) 555 while issue.IsOk(): 556 self.Collapse(issue) 557 epi, epi_cookie = self.GetFirstChild(issue) 558 while epi.IsOk(): 559 self.Collapse(epi) 560 epi, epi_cookie = self.GetNextChild(issue, epi_cookie) 561 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
562 #--------------------------------------------------------
563 - def __expand_to_episode_level(self, evt):
564 565 root_item = self.GetRootItem() 566 567 if not root_item.IsOk(): 568 return 569 570 self.Expand(root_item) 571 572 # collapse episodes, expand issues 573 issue, issue_cookie = self.GetFirstChild(root_item) 574 while issue.IsOk(): 575 self.Expand(issue) 576 epi, epi_cookie = self.GetFirstChild(issue) 577 while epi.IsOk(): 578 self.Collapse(epi) 579 epi, epi_cookie = self.GetNextChild(issue, epi_cookie) 580 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
581 #--------------------------------------------------------
582 - def __expand_to_encounter_level(self, evt):
583 584 root_item = self.GetRootItem() 585 586 if not root_item.IsOk(): 587 return 588 589 self.Expand(root_item) 590 591 # collapse episodes, expand issues 592 issue, issue_cookie = self.GetFirstChild(root_item) 593 while issue.IsOk(): 594 self.Expand(issue) 595 epi, epi_cookie = self.GetFirstChild(issue) 596 while epi.IsOk(): 597 self.Expand(epi) 598 epi, epi_cookie = self.GetNextChild(issue, epi_cookie) 599 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
600 #--------------------------------------------------------
601 - def __export_encounter_for_medistar(self, evt):
602 gmNarrativeWidgets.export_narrative_for_medistar_import ( 603 parent = self, 604 soap_cats = u'soap', 605 encounter = self.__curr_node_data 606 )
607 #-------------------------------------------------------- 608 # event handlers 609 #--------------------------------------------------------
610 - def _on_narrative_mod_db(self, *args, **kwargs):
611 wx.CallAfter(self.__update_text_for_selected_node)
612 #--------------------------------------------------------
613 - def _on_episode_mod_db(self, *args, **kwargs):
614 wx.CallAfter(self.__populate_tree)
615 #--------------------------------------------------------
616 - def _on_issue_mod_db(self, *args, **kwargs):
617 wx.CallAfter(self.__populate_tree)
618 #--------------------------------------------------------
619 - def _on_tree_item_selected(self, event):
620 sel_item = event.GetItem() 621 self.__curr_node = sel_item 622 self.__update_text_for_selected_node() 623 return True
624 # #-------------------------------------------------------- 625 # def _on_mouse_motion(self, event): 626 # 627 # cursor_pos = (event.GetX(), event.GetY()) 628 # 629 # self.SetToolTipString(u'') 630 # 631 # if cursor_pos != self._old_cursor_pos: 632 # self._old_cursor_pos = cursor_pos 633 # (item, flags) = self.HitTest(cursor_pos) 634 # #if flags != wx.TREE_HITTEST_NOWHERE: 635 # if flags == wx.TREE_HITTEST_ONITEMLABEL: 636 # data = self.GetPyData(item) 637 # 638 # if not isinstance(data, gmEMRStructItems.cEncounter): 639 # return 640 # 641 # self.SetToolTip(u'%s %s %s - %s\n\nRFE: %s\nAOE: %s' % ( 642 # data['started'].strftime('%x'), 643 # data['l10n_type'], 644 # data['started'].strftime('%H:%m'), 645 # data['last_affirmed'].strftime('%H:%m'), 646 # gmTools.coalesce(data['reason_for_encounter'], u''), 647 # gmTools.coalesce(data['assessment_of_encounter'], u'') 648 # )) 649 #--------------------------------------------------------
650 - def _on_tree_item_gettooltip(self, event):
651 652 item = event.GetItem() 653 654 if not item.IsOk(): 655 event.SetToolTip(u' ') 656 return 657 658 data = self.GetPyData(item) 659 660 if isinstance(data, gmEMRStructItems.cEncounter): 661 event.SetToolTip(u'%s %s %s - %s\n\nRFE: %s\nAOE: %s' % ( 662 data['started'].strftime('%x'), 663 data['l10n_type'], 664 data['started'].strftime('%H:%M'), 665 data['last_affirmed'].strftime('%H:%M'), 666 gmTools.coalesce(data['reason_for_encounter'], u''), 667 gmTools.coalesce(data['assessment_of_encounter'], u'') 668 )) 669 670 elif isinstance(data, gmEMRStructItems.cEpisode): 671 tt = u'' 672 tt += gmTools.bool2subst ( 673 (data['diagnostic_certainty_classification'] is not None), 674 data.diagnostic_certainty_description + u'\n\n', 675 u'' 676 ) 677 tt += gmTools.bool2subst ( 678 data['episode_open'], 679 _('ongoing episode'), 680 _('closed episode'), 681 'error: episode state is None' 682 ) + u'\n' 683 tt += gmTools.coalesce(data['summary'], u'', u'\n%s') 684 tt = tt.strip(u'\n') 685 if tt == u'': 686 tt = u' ' 687 event.SetToolTip(tt) 688 689 elif isinstance(data, gmEMRStructItems.cHealthIssue): 690 tt = u'' 691 tt += gmTools.bool2subst(data['is_confidential'], _('*** CONFIDENTIAL ***\n\n'), u'') 692 tt += gmTools.bool2subst ( 693 (data['diagnostic_certainty_classification'] is not None), 694 data.diagnostic_certainty_description + u'\n', 695 u'' 696 ) 697 tt += gmTools.bool2subst ( 698 (data['laterality'] not in [None, u'na']), 699 data.laterality_description + u'\n', 700 u'' 701 ) 702 # noted_at_age is too costly 703 tt += gmTools.bool2subst(data['is_active'], _('active') + u'\n', u'') 704 tt += gmTools.bool2subst(data['clinically_relevant'], _('clinically relevant') + u'\n', u'') 705 tt += gmTools.bool2subst(data['is_cause_of_death'], _('contributed to death') + u'\n', u'') 706 tt += gmTools.coalesce(data['grouping'], u'\n', _('Grouping: %s') + u'\n\n') 707 tt += gmTools.coalesce(data['summary'], u'') 708 tt = tt.strip(u'\n') 709 if tt == u'': 710 tt = u' ' 711 event.SetToolTip(tt) 712 713 else: 714 event.SetToolTip(self.__root_tooltip)
715 #event.SetToolTip(u' ') 716 ##self.SetToolTipString(u'') 717 718 # doing this prevents the tooltip from showing at all 719 #event.Skip() 720 721 #widgetXY.GetToolTip().Enable(False) 722 # 723 #seems to work, supposing the tooltip is actually set for the widget, 724 #otherwise a test would be needed 725 #if widgetXY.GetToolTip(): 726 # widgetXY.GetToolTip().Enable(False) 727 #--------------------------------------------------------
728 - def _on_tree_item_right_clicked(self, event):
729 """Right button clicked: display the popup for the tree""" 730 731 node = event.GetItem() 732 self.SelectItem(node) 733 self.__curr_node_data = self.GetPyData(node) 734 self.__curr_node = node 735 736 pos = wx.DefaultPosition 737 if isinstance(self.__curr_node_data, gmEMRStructItems.cHealthIssue): 738 self.__handle_issue_context(pos=pos) 739 elif isinstance(self.__curr_node_data, gmEMRStructItems.cEpisode): 740 self.__handle_episode_context(pos=pos) 741 elif isinstance(self.__curr_node_data, gmEMRStructItems.cEncounter): 742 self.__handle_encounter_context(pos=pos) 743 elif node == self.GetRootItem(): 744 self.__handle_root_context() 745 elif type(self.__curr_node_data) == type({}): 746 # ignore pseudo node "free-standing episodes" 747 pass 748 else: 749 print "error: unknown node type, no popup menu" 750 event.Skip()
751 #--------------------------------------------------------
752 - def OnCompareItems (self, node1=None, node2=None):
753 """Used in sorting items. 754 755 -1: 1 < 2 756 0: 1 = 2 757 1: 1 > 2 758 """ 759 # FIXME: implement sort modes, chron, reverse cron, by regex, etc 760 761 item1 = self.GetPyData(node1) 762 item2 = self.GetPyData(node2) 763 764 # dummy health issue always on top 765 if isinstance(item1, type({})): 766 return -1 767 if isinstance(item2, type({})): 768 return 1 769 770 # encounters: reverse chronologically 771 if isinstance(item1, gmEMRStructItems.cEncounter): 772 if item1['started'] == item2['started']: 773 return 0 774 if item1['started'] > item2['started']: 775 return -1 776 return 1 777 778 # episodes: chronologically 779 if isinstance(item1, gmEMRStructItems.cEpisode): 780 start1 = item1.get_access_range()[0] 781 start2 = item2.get_access_range()[0] 782 if start1 == start2: 783 return 0 784 if start1 < start2: 785 return -1 786 return 1 787 788 # issues: alpha by grouping, no grouping at the bottom 789 if isinstance(item1, gmEMRStructItems.cHealthIssue): 790 791 # no grouping below grouping 792 if item1['grouping'] is None: 793 if item2['grouping'] is not None: 794 return 1 795 796 # grouping above no grouping 797 if item1['grouping'] is not None: 798 if item2['grouping'] is None: 799 return -1 800 801 # both no grouping: alpha on description 802 if (item1['grouping'] is None) and (item2['grouping'] is None): 803 if item1['description'].lower() < item2['description'].lower(): 804 return -1 805 if item1['description'].lower() > item2['description'].lower(): 806 return 1 807 return 0 808 809 # both with grouping: alpha on grouping, then alpha on description 810 if item1['grouping'] < item2['grouping']: 811 return -1 812 813 if item1['grouping'] > item2['grouping']: 814 return 1 815 816 if item1['description'].lower() < item2['description'].lower(): 817 return -1 818 819 if item1['description'].lower() > item2['description'].lower(): 820 return 1 821 822 return 0 823 824 _log.error('unknown item type during sorting EMR tree:') 825 _log.error('item1: %s', type(item1)) 826 _log.error('item2: %s', type(item2)) 827 828 return 0
829 #-------------------------------------------------------- 830 # properties 831 #--------------------------------------------------------
833 return self.__details_display_mode
834
835 - def _set_details_display_mode(self, mode):
836 if mode not in [u'details', u'journal']: 837 raise ValueError('details display mode must be one of "details", "journal"') 838 if self.__details_display_mode == mode: 839 return 840 self.__details_display_mode = mode 841 self.__update_text_for_selected_node()
842 843 details_display_mode = property(_get_details_display_mode, _set_details_display_mode)
844 #================================================================ 845 from Gnumed.wxGladeWidgets import wxgScrolledEMRTreePnl 846
847 -class cScrolledEMRTreePnl(wxgScrolledEMRTreePnl.wxgScrolledEMRTreePnl):
848 """A scrollable panel holding an EMR tree. 849 850 Lacks a widget to display details for selected items. The 851 tree data will be refetched - if necessary - whenever 852 repopulate_ui() is called, e.g., when then patient is changed. 853 """
854 - def __init__(self, *args, **kwds):
856 #--------------------------------------------------------
857 - def repopulate_ui(self):
858 self._emr_tree.refresh() 859 return True
860 #============================================================ 861 from Gnumed.wxGladeWidgets import wxgSplittedEMRTreeBrowserPnl 862
863 -class cSplittedEMRTreeBrowserPnl(wxgSplittedEMRTreeBrowserPnl.wxgSplittedEMRTreeBrowserPnl):
864 """A splitter window holding an EMR tree. 865 866 The left hand side displays a scrollable EMR tree while 867 on the right details for selected items are displayed. 868 869 Expects to be put into a Notebook. 870 """
871 - def __init__(self, *args, **kwds):
872 wxgSplittedEMRTreeBrowserPnl.wxgSplittedEMRTreeBrowserPnl.__init__(self, *args, **kwds) 873 self._pnl_emr_tree._emr_tree.set_narrative_display(narrative_display = self._TCTRL_item_details) 874 self._pnl_emr_tree._emr_tree.set_image_display(image_display = self._PNL_visual_soap) 875 self.__register_events()
876 #--------------------------------------------------------
877 - def __register_events(self):
878 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection) 879 return True
880 #-------------------------------------------------------- 881 # event handler 882 #--------------------------------------------------------
883 - def _on_post_patient_selection(self):
884 if self.GetParent().GetCurrentPage() == self: 885 self.repopulate_ui() 886 return True
887 #--------------------------------------------------------
888 - def _on_show_details_selected(self, event):
889 #event.Skip() 890 self._pnl_emr_tree._emr_tree.details_display_mode = u'details'
891 #--------------------------------------------------------
892 - def _on_show_journal_selected(self, event):
893 #event.Skip() 894 self._pnl_emr_tree._emr_tree.details_display_mode = u'journal'
895 #-------------------------------------------------------- 896 # external API 897 #--------------------------------------------------------
898 - def repopulate_ui(self):
899 """Fills UI with data.""" 900 self._pnl_emr_tree.repopulate_ui() 901 self._splitter_browser.SetSashPosition(self._splitter_browser.GetSizeTuple()[0]/3, True) 902 return True
903 #================================================================
904 -class cEMRJournalPanel(wx.Panel):
905 - def __init__(self, *args, **kwargs):
906 wx.Panel.__init__(self, *args, **kwargs) 907 908 self.__do_layout() 909 self.__register_events()
910 #--------------------------------------------------------
911 - def __do_layout(self):
912 self.__journal = wx.TextCtrl ( 913 self, 914 -1, 915 _('No EMR data loaded.'), 916 style = wx.TE_MULTILINE | wx.TE_READONLY 917 ) 918 self.__journal.SetFont(wx.Font(10, wx.MODERN, wx.NORMAL, wx.NORMAL)) 919 # arrange widgets 920 szr_outer = wx.BoxSizer(wx.VERTICAL) 921 szr_outer.Add(self.__journal, 1, wx.EXPAND, 0) 922 # and do layout 923 self.SetAutoLayout(1) 924 self.SetSizer(szr_outer) 925 szr_outer.Fit(self) 926 szr_outer.SetSizeHints(self) 927 self.Layout()
928 #--------------------------------------------------------
929 - def __register_events(self):
930 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
931 #--------------------------------------------------------
932 - def _on_post_patient_selection(self):
933 """Expects to be in a Notebook.""" 934 if self.GetParent().GetCurrentPage() == self: 935 self.repopulate_ui() 936 return True
937 #-------------------------------------------------------- 938 # notebook plugin API 939 #--------------------------------------------------------
940 - def repopulate_ui(self):
941 txt = StringIO.StringIO() 942 exporter = gmPatientExporter.cEMRJournalExporter() 943 # FIXME: if journal is large this will error out, use generator/yield etc 944 # FIXME: turn into proper list 945 try: 946 exporter.export(txt) 947 self.__journal.SetValue(txt.getvalue()) 948 except ValueError: 949 _log.exception('cannot get EMR journal') 950 self.__journal.SetValue (_( 951 'An error occurred while retrieving the EMR\n' 952 'in journal form for the active patient.\n\n' 953 'Please check the log file for details.' 954 )) 955 txt.close() 956 self.__journal.ShowPosition(self.__journal.GetLastPosition()) 957 return True
958 #================================================================ 959 # MAIN 960 #---------------------------------------------------------------- 961 if __name__ == '__main__': 962 963 _log.info("starting emr browser...") 964 965 try: 966 # obtain patient 967 patient = gmPersonSearch.ask_for_patient() 968 if patient is None: 969 print "No patient. Exiting gracefully..." 970 sys.exit(0) 971 gmPatSearchWidgets.set_active_patient(patient = patient) 972 973 # display standalone browser 974 application = wx.PyWidgetTester(size=(800,600)) 975 emr_browser = cEMRBrowserPanel(application.frame, -1) 976 emr_browser.refresh_tree() 977 978 application.frame.Show(True) 979 application.MainLoop() 980 981 # clean up 982 if patient is not None: 983 try: 984 patient.cleanup() 985 except: 986 print "error cleaning up patient" 987 except StandardError: 988 _log.exception("unhandled exception caught !") 989 # but re-raise them 990 raise 991 992 _log.info("closing emr browser...") 993 994 #================================================================ 995