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

Source Code for Module Gnumed.wxpython.gmEMRBrowser

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