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

Source Code for Module Gnumed.wxpython.gmEMRBrowser

   1  """GNUmed patient EMR tree browser.""" 
   2  #================================================================ 
   3  __author__ = "cfmoro1976@yahoo.es, sjtan@swiftdsl.com.au, Karsten.Hilbert@gmx.net" 
   4  __license__ = "GPL v2 or later" 
   5   
   6  # std lib 
   7  import sys 
   8  import os.path 
   9  import io 
  10  import logging 
  11   
  12   
  13  # 3rd party 
  14  import wx 
  15  import wx.lib.mixins.treemixin as treemixin 
  16   
  17   
  18  # GNUmed libs 
  19  from Gnumed.pycommon import gmI18N 
  20  from Gnumed.pycommon import gmDispatcher 
  21  from Gnumed.pycommon import gmExceptions 
  22  from Gnumed.pycommon import gmTools 
  23  from Gnumed.pycommon import gmDateTime 
  24  from Gnumed.pycommon import gmLog2 
  25   
  26  from Gnumed.exporters import gmPatientExporter 
  27   
  28  from Gnumed.business import gmEMRStructItems 
  29  from Gnumed.business import gmPerson 
  30  from Gnumed.business import gmSOAPimporter 
  31  from Gnumed.business import gmPersonSearch 
  32  from Gnumed.business import gmSoapDefs 
  33  from Gnumed.business import gmClinicalRecord 
  34   
  35  from Gnumed.wxpython import gmGuiHelpers 
  36  from Gnumed.wxpython import gmEMRStructWidgets 
  37  from Gnumed.wxpython import gmEncounterWidgets 
  38  from Gnumed.wxpython import gmSOAPWidgets 
  39  from Gnumed.wxpython import gmAllergyWidgets 
  40  from Gnumed.wxpython import gmDemographicsWidgets 
  41  from Gnumed.wxpython import gmNarrativeWidgets 
  42  from Gnumed.wxpython import gmNarrativeWorkflows 
  43  from Gnumed.wxpython import gmPatSearchWidgets 
  44  from Gnumed.wxpython import gmVaccWidgets 
  45  from Gnumed.wxpython import gmFamilyHistoryWidgets 
  46  from Gnumed.wxpython import gmFormWidgets 
  47  from Gnumed.wxpython import gmTimer 
  48  from Gnumed.wxpython import gmHospitalStayWidgets 
  49  from Gnumed.wxpython import gmProcedureWidgets 
  50   
  51   
  52  _log = logging.getLogger('gm.ui') 
  53   
  54  #============================================================ 
55 -def export_emr_to_ascii(parent=None):
56 """ 57 Dump the patient's EMR from GUI client 58 @param parent - The parent widget 59 @type parent - A wx.Window instance 60 """ 61 # sanity checks 62 if parent is None: 63 raise TypeError('expected wx.Window instance as parent, got <None>') 64 65 pat = gmPerson.gmCurrentPatient() 66 if not pat.connected: 67 gmDispatcher.send(signal='statustext', msg=_('Cannot export EMR. No active patient.')) 68 return False 69 70 # get file name 71 wc = "%s (*.txt)|*.txt|%s (*)|*" % (_("text files"), _("all files")) 72 defdir = os.path.abspath(os.path.expanduser(os.path.join('~', 'gnumed', pat.subdir_name))) 73 gmTools.mkdir(defdir) 74 fname = '%s-%s_%s.txt' % (_('emr-export'), pat['lastnames'], pat['firstnames']) 75 dlg = wx.FileDialog ( 76 parent = parent, 77 message = _("Save patient's EMR as..."), 78 defaultDir = defdir, 79 defaultFile = fname, 80 wildcard = wc, 81 style = wx.FD_SAVE 82 ) 83 choice = dlg.ShowModal() 84 fname = dlg.GetPath() 85 dlg.Destroy() 86 if choice != wx.ID_OK: 87 return None 88 89 _log.debug('exporting EMR to [%s]', fname) 90 91 output_file = io.open(fname, mode = 'wt', encoding = 'utf8', errors = 'replace') 92 exporter = gmPatientExporter.cEmrExport(patient = pat) 93 exporter.set_output_file(output_file) 94 exporter.dump_constraints() 95 exporter.dump_demographic_record(True) 96 exporter.dump_clinical_record() 97 exporter.dump_med_docs() 98 output_file.close() 99 100 gmDispatcher.send('statustext', msg = _('EMR successfully exported to file: %s') % fname, beep = False) 101 return fname
102 103 #============================================================
104 -class cEMRTree(wx.TreeCtrl, treemixin.ExpansionState):
105 """This wx.TreeCtrl derivative displays a tree view of a medical record.""" 106 107 #--------------------------------------------------------
108 - def __init__(self, parent, id, *args, **kwds):
109 """Set up our specialised tree. 110 """ 111 kwds['style'] = wx.TR_HAS_BUTTONS | wx.NO_BORDER | wx.TR_SINGLE 112 wx.TreeCtrl.__init__(self, parent, id, *args, **kwds) 113 114 self.__soap_display = None 115 self.__soap_display_mode = 'details' # "details" or "journal" or "revisions" 116 self.__img_display = None 117 self.__cb__enable_display_mode_selection = lambda x:x 118 self.__cb__select_edit_mode = lambda x:x 119 self.__cb__add_soap_editor = lambda x:x 120 self.__pat = None 121 self.__curr_node = None 122 self.__expanded_nodes = None 123 124 self.__make_popup_menus() 125 self.__register_events()
126 127 #-------------------------------------------------------- 128 # external API 129 #--------------------------------------------------------
130 - def _get_soap_display(self):
131 return self.__soap_display
132
133 - def _set_soap_display(self, soap_display=None):
134 self.__soap_display = soap_display 135 self.__soap_display_prop_font = soap_display.GetFont() 136 self.__soap_display_mono_font = wx.Font(self.__soap_display_prop_font.GetNativeFontInfo()) 137 self.__soap_display_mono_font.SetFamily(wx.FONTFAMILY_TELETYPE) 138 self.__soap_display_mono_font.SetPointSize(self.__soap_display_prop_font.GetPointSize() - 2)
139 140 soap_display = property(_get_soap_display, _set_soap_display) 141 142 #--------------------------------------------------------
143 - def _get_image_display(self):
144 return self.__img_display
145
146 - def _set_image_display(self, image_display=None):
147 self.__img_display = image_display
148 149 image_display = property(_get_image_display, _set_image_display) 150 151 #--------------------------------------------------------
153 if not callable(callback): 154 raise ValueError('callback [%s] not callable' % callback) 155 self.__cb__enable_display_mode_selection = callback
156 157 #--------------------------------------------------------
158 - def _set_edit_mode_selector(self, callback):
159 if callback is None: 160 callback = lambda x:x 161 if not callable(callback): 162 raise ValueError('edit mode selector [%s] not callable' % callback) 163 self.__cb__select_edit_mode = callback
164 165 edit_mode_selector = property(lambda x:x, _set_edit_mode_selector) 166 167 #--------------------------------------------------------
168 - def _set_soap_editor_adder(self, callback):
169 if callback is None: 170 callback = lambda x:x 171 if not callable(callback): 172 raise ValueError('soap editor adder [%s] not callable' % callback) 173 self.__cb__add_soap_editor = callback
174 175 soap_editor_adder = property(lambda x:x, _set_soap_editor_adder) 176 177 #-------------------------------------------------------- 178 # ExpansionState mixin API 179 #--------------------------------------------------------
180 - def GetItemIdentity(self, item):
181 if item is None: 182 return 'invalid item' 183 184 if not item.IsOk(): 185 return 'invalid item' 186 187 try: 188 node_data = self.GetItemData(item) 189 except wx.wxAssertionError: 190 _log.exception('unfathomable self.GetItemData() problem occurred, faking root node') 191 _log.error('real node: %s', item) 192 _log.error('node.IsOk(): %s', item.IsOk()) # already survived this further up 193 _log.error('is root node: %s', item == self.GetRootItem()) 194 _log.error('node attributes: %s', dir(item)) 195 gmLog2.log_stack_trace() 196 return 'invalid item' 197 198 if isinstance(node_data, gmEMRStructItems.cHealthIssue): 199 return 'issue::%s' % node_data['pk_health_issue'] 200 if isinstance(node_data, gmEMRStructItems.cEpisode): 201 return 'episode::%s' % node_data['pk_episode'] 202 if isinstance(node_data, gmEMRStructItems.cEncounter): 203 return 'encounter::%s' % node_data['pk_encounter'] 204 # unassociated episodes 205 if isinstance(node_data, type({})): 206 return 'dummy node::%s' % self.__pat.ID 207 # root node == EMR level 208 return 'root node::%s' % self.__pat.ID
209 210 #-------------------------------------------------------- 211 # internal helpers 212 #--------------------------------------------------------
213 - def __register_events(self):
214 """Configures enabled event signals.""" 215 self.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_tree_item_selected) 216 self.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self._on_tree_item_right_clicked) 217 self.Bind(wx.EVT_TREE_ITEM_EXPANDING, self._on_tree_item_expanding) 218 219 # handle tooltips 220 # self.Bind(wx.EVT_MOTION, self._on_mouse_motion) 221 self.Bind(wx.EVT_TREE_ITEM_GETTOOLTIP, self._on_tree_item_gettooltip) 222 223 # FIXME: xxxxx signal 224 gmDispatcher.connect(signal = 'narrative_mod_db', receiver = self._on_narrative_mod_db) 225 gmDispatcher.connect(signal = 'clin.episode_mod_db', receiver = self._on_episode_mod_db) 226 gmDispatcher.connect(signal = 'clin.health_issue_mod_db', receiver = self._on_issue_mod_db) 227 gmDispatcher.connect(signal = 'clin.family_history_mod_db', receiver = self._on_issue_mod_db)
228 229 #--------------------------------------------------------
230 - def clear_tree(self):
231 self.DeleteAllItems() 232 self.__expanded_nodes = None
233 234 #--------------------------------------------------------
235 - def __populate_tree(self):
236 """Updates EMR browser data.""" 237 # FIXME: auto select the previously self.__curr_node if not None 238 # FIXME: error handling 239 240 _log.debug('populating EMR tree') 241 242 wx.BeginBusyCursor() 243 244 if self.__pat is None: 245 self.clear_tree() 246 self.__expanded_nodes = None 247 wx.EndBusyCursor() 248 return True 249 250 # init new tree 251 root_item = self.__populate_root_node() 252 self.__curr_node = root_item 253 if self.__expanded_nodes is not None: 254 self.ExpansionState = self.__expanded_nodes 255 self.SelectItem(root_item) 256 self.Expand(root_item) 257 self.__update_text_for_selected_node() # this is fairly slow, too 258 259 wx.EndBusyCursor() 260 return True
261 262 #--------------------------------------------------------
263 - def __populate_root_node(self):
264 265 self.DeleteAllItems() 266 267 root_item = self.AddRoot(_('EMR of %(lastnames)s, %(firstnames)s') % self.__pat.get_active_name()) 268 self.SetItemData(root_item, None) 269 self.SetItemHasChildren(root_item, True) 270 271 self.__root_tooltip = self.__pat['description_gender'] + '\n' 272 if self.__pat['deceased'] is None: 273 self.__root_tooltip += ' %s (%s)\n\n' % ( 274 self.__pat.get_formatted_dob(format = '%d %b %Y'), 275 self.__pat['medical_age'] 276 ) 277 else: 278 template = ' %s - %s (%s)\n\n' 279 self.__root_tooltip += template % ( 280 self.__pat.get_formatted_dob(format = '%d.%b %Y'), 281 gmDateTime.pydt_strftime(self.__pat['deceased'], '%Y %b %d'), 282 self.__pat['medical_age'] 283 ) 284 self.__root_tooltip += gmTools.coalesce(self.__pat['comment'], '', '%s\n\n') 285 doc = self.__pat.primary_provider 286 if doc is not None: 287 self.__root_tooltip += '%s:\n' % _('Primary provider in this praxis') 288 self.__root_tooltip += ' %s %s %s (%s)%s\n\n' % ( 289 gmTools.coalesce(doc['title'], gmPerson.map_gender2salutation(gender = doc['gender'])), 290 doc['firstnames'], 291 doc['lastnames'], 292 doc['short_alias'], 293 gmTools.bool2subst(doc['is_active'], '', ' [%s]' % _('inactive')) 294 ) 295 if not ((self.__pat['emergency_contact'] is None) and (self.__pat['pk_emergency_contact'] is None)): 296 self.__root_tooltip += _('In case of emergency contact:') + '\n' 297 if self.__pat['emergency_contact'] is not None: 298 self.__root_tooltip += gmTools.wrap ( 299 text = '%s\n' % self.__pat['emergency_contact'], 300 width = 60, 301 initial_indent = ' ', 302 subsequent_indent = ' ' 303 ) 304 if self.__pat['pk_emergency_contact'] is not None: 305 contact = self.__pat.emergency_contact_in_database 306 self.__root_tooltip += ' %s\n' % contact['description_gender'] 307 self.__root_tooltip = self.__root_tooltip.strip('\n') 308 if self.__root_tooltip == '': 309 self.__root_tooltip = ' ' 310 311 return root_item
312 313 #--------------------------------------------------------
315 """Displays information for the selected tree node.""" 316 317 if self.__soap_display is None: 318 return 319 320 self.__soap_display.Clear() 321 self.__img_display.clear() 322 323 if self.__curr_node is None: 324 return 325 326 if not self.__curr_node.IsOk(): 327 return 328 329 try: 330 node_data = self.GetItemData(self.__curr_node) 331 except wx.wxAssertionError: 332 node_data = None # fake a root node 333 _log.exception('unfathomable self.GetItemData() problem occurred, faking root node') 334 _log.error('real node: %s', self.__curr_node) 335 _log.error('node.IsOk(): %s', self.__curr_node.IsOk()) # already survived this further up 336 _log.error('is root node: %s', self.__curr_node == self.GetRootItem()) 337 _log.error('node attributes: %s', dir(self.__curr_node)) 338 gmLog2.log_stack_trace() 339 340 doc_folder = self.__pat.get_document_folder() 341 342 if isinstance(node_data, gmEMRStructItems.cHealthIssue): 343 self.__cb__enable_display_mode_selection(True) 344 txt = 'invalid SOAP display mode [%s]' % self.__soap_display_mode 345 if self.__soap_display_mode == 'details': 346 txt = node_data.format(left_margin = 1, patient = self.__pat) 347 font = self.__soap_display_prop_font 348 if self.__soap_display_mode == 'journal': 349 txt = node_data.format_as_journal(left_margin = 1) 350 font = self.__soap_display_prop_font 351 if self.__soap_display_mode == 'revisions': 352 txt = node_data.formatted_revision_history 353 font = self.__soap_display_mono_font 354 epis = node_data.episodes 355 if len(epis) > 0: 356 self.__img_display.refresh ( 357 document_folder = doc_folder, 358 episodes = [ epi['pk_episode'] for epi in epis ], 359 async = True 360 ) 361 self.__soap_display.SetFont(font) 362 self.__soap_display.WriteText(txt) 363 self.__soap_display.ShowPosition(0) 364 return 365 366 # unassociated episodes # FIXME: turn into real dummy issue 367 if isinstance(node_data, type({})): 368 self.__cb__enable_display_mode_selection(True) 369 if self.__soap_display_mode == 'details': 370 txt = _('Pool of unassociated episodes "%s":\n') % node_data['description'] 371 epis = self.__pat.emr.get_episodes(unlinked_only = True, order_by = 'episode_open DESC, description') 372 if len(epis) > 0: 373 txt += '\n' 374 for epi in epis: 375 txt += epi.format ( 376 left_margin = 1, 377 patient = self.__pat, 378 with_summary = True, 379 with_codes = False, 380 with_encounters = False, 381 with_documents = False, 382 with_hospital_stays = False, 383 with_procedures = False, 384 with_family_history = False, 385 with_tests = False, 386 with_vaccinations = False, 387 with_health_issue = False 388 ) 389 txt += '\n' 390 else: 391 epis = self.__pat.emr.get_episodes(unlinked_only = True, order_by = 'episode_open DESC, description') 392 txt = '' 393 if len(epis) > 0: 394 txt += _(' Listing of unassociated episodes\n') 395 for epi in epis: 396 txt += ' %s\n' % (gmTools.u_box_horiz_4dashes * 60) 397 txt += epi.format ( 398 left_margin = 1, 399 patient = self.__pat, 400 with_summary = False, 401 with_codes = False, 402 with_encounters = False, 403 with_documents = False, 404 with_hospital_stays = False, 405 with_procedures = False, 406 with_family_history = False, 407 with_tests = False, 408 with_vaccinations = False, 409 with_health_issue = False 410 ) 411 txt += '\n' 412 txt += epi.format_as_journal(left_margin = 2) 413 self.__soap_display.SetFont(self.__soap_display_prop_font) 414 self.__soap_display.WriteText(txt) 415 self.__soap_display.ShowPosition(0) 416 return 417 418 if isinstance(node_data, gmEMRStructItems.cEpisode): 419 self.__cb__enable_display_mode_selection(True) 420 txt = 'invalid SOAP display mode [%s]' % self.__soap_display_mode 421 if self.__soap_display_mode == 'details': 422 txt = node_data.format(left_margin = 1, patient = self.__pat) 423 font = self.__soap_display_prop_font 424 if self.__soap_display_mode == 'journal': 425 txt = node_data.format_as_journal(left_margin = 1) 426 font = self.__soap_display_prop_font 427 if self.__soap_display_mode == 'revisions': 428 txt = node_data.formatted_revision_history 429 font = self.__soap_display_mono_font 430 self.__img_display.refresh ( 431 document_folder = doc_folder, 432 episodes = [ node_data['pk_episode'] ] 433 ) 434 self.__soap_display.SetFont(font) 435 self.__soap_display.WriteText(txt) 436 self.__soap_display.ShowPosition(0) 437 return 438 439 if isinstance(node_data, gmEMRStructItems.cEncounter): 440 self.__cb__enable_display_mode_selection(True) 441 epi = self.GetItemData(self.GetItemParent(self.__curr_node)) 442 if self.__soap_display_mode == 'revisions': 443 txt = node_data.formatted_revision_history 444 font = self.__soap_display_mono_font 445 else: 446 txt = node_data.format ( 447 episodes = [epi['pk_episode']], 448 with_soap = True, 449 left_margin = 1, 450 patient = self.__pat, 451 with_co_encountlet_hints = True 452 ) 453 font = self.__soap_display_prop_font 454 self.__img_display.refresh ( 455 document_folder = doc_folder, 456 episodes = [ epi['pk_episode'] ], 457 encounter = node_data['pk_encounter'] 458 ) 459 self.__soap_display.SetFont(font) 460 self.__soap_display.WriteText(txt) 461 self.__soap_display.ShowPosition(0) 462 return 463 464 # root node == EMR level 465 self.__cb__enable_display_mode_selection(True) 466 if self.__soap_display_mode == 'details': 467 emr = self.__pat.emr 468 txt = emr.format_summary() 469 else: 470 txt = self.__pat.emr.format_as_journal(left_margin = 1, patient = self.__pat) 471 self.__soap_display.SetFont(self.__soap_display_prop_font) 472 self.__soap_display.WriteText(txt) 473 self.__soap_display.ShowPosition(0)
474 475 #--------------------------------------------------------
476 - def __make_popup_menus(self):
477 478 # - root node 479 self.__root_context_popup = wx.Menu(title = _('EMR Actions:')) 480 item = self.__root_context_popup.Append(-1, _('Print EMR')) 481 self.Bind(wx.EVT_MENU, self.__print_emr, item) 482 item = self.__root_context_popup.Append(-1, _('Create health issue')) 483 self.Bind(wx.EVT_MENU, self.__create_issue, item) 484 item = self.__root_context_popup.Append(-1, _('Create episode')) 485 self.Bind(wx.EVT_MENU, self.__create_episode, item) 486 item = self.__root_context_popup.Append(-1, _('Create progress note')) 487 self.Bind(wx.EVT_MENU, self.__create_soap_editor, item) 488 item = self.__root_context_popup.Append(-1, _('Manage allergies')) 489 self.Bind(wx.EVT_MENU, self.__document_allergy, item) 490 item = self.__root_context_popup.Append(-1, _('Manage family history')) 491 self.Bind(wx.EVT_MENU, self.__manage_family_history, item) 492 item = self.__root_context_popup.Append(-1, _('Manage hospitalizations')) 493 self.Bind(wx.EVT_MENU, self.__manage_hospital_stays, item) 494 item = self.__root_context_popup.Append(-1, _('Manage occupation')) 495 self.Bind(wx.EVT_MENU, self.__manage_occupation, item) 496 item = self.__root_context_popup.Append(-1, _('Manage procedures')) 497 self.Bind(wx.EVT_MENU, self.__manage_procedures, item) 498 item = self.__root_context_popup.Append(-1, _('Manage vaccinations')) 499 self.Bind(wx.EVT_MENU, self.__manage_vaccinations, item) 500 501 self.__root_context_popup.AppendSeparator() 502 503 # expand tree 504 expand_menu = wx.Menu() 505 self.__root_context_popup.Append(wx.NewId(), _('Open EMR to ...'), expand_menu) 506 item = expand_menu.Append(-1, _('... issue level')) 507 self.Bind(wx.EVT_MENU, self.__expand_to_issue_level, item) 508 item = expand_menu.Append(-1, _('... episode level')) 509 self.Bind(wx.EVT_MENU, self.__expand_to_episode_level, item) 510 item = expand_menu.Append(-1, _('... encounter level')) 511 self.Bind(wx.EVT_MENU, self.__expand_to_encounter_level, item) 512 513 # - health issues 514 self.__issue_context_popup = wx.Menu(title = _('Health Issue Actions:')) 515 item = self.__issue_context_popup.Append(-1, _('Edit details')) 516 self.Bind(wx.EVT_MENU, self.__edit_issue, item) 517 item = self.__issue_context_popup.Append(-1, _('Delete')) 518 self.Bind(wx.EVT_MENU, self.__delete_issue, item) 519 self.__issue_context_popup.AppendSeparator() 520 item = self.__issue_context_popup.Append(-1, _('Open to encounter level')) 521 self.Bind(wx.EVT_MENU, self.__expand_issue_to_encounter_level, item) 522 # print " attach issue to another patient" 523 # print " move all episodes to another issue" 524 item = self.__issue_context_popup.Append(-1, _('Create progress note')) 525 self.Bind(wx.EVT_MENU, self.__create_soap_editor, item) 526 527 # - episodes 528 self.__epi_context_popup = wx.Menu(title = _('Episode Actions:')) 529 item = self.__epi_context_popup.Append(-1, _('Toggle ongoing/closed')) 530 self.Bind(wx.EVT_MENU, self.__toggle_episode_open_close, item) 531 item = self.__epi_context_popup.Append(-1, _('Edit details')) 532 self.Bind(wx.EVT_MENU, self.__edit_episode, item) 533 item = self.__epi_context_popup.Append(-1, _('Delete')) 534 self.Bind(wx.EVT_MENU, self.__delete_episode, item) 535 item = self.__epi_context_popup.Append(-1, _('Promote')) 536 self.Bind(wx.EVT_MENU, self.__promote_episode_to_issue, item) 537 item = self.__epi_context_popup.Append(-1, _('Create progress note')) 538 self.Bind(wx.EVT_MENU, self.__create_soap_editor, item) 539 item = self.__epi_context_popup.Append(-1, _('Move encounters')) 540 self.Bind(wx.EVT_MENU, self.__move_encounters, item) 541 542 # - encounters 543 self.__enc_context_popup = wx.Menu(title = _('Encounter Actions:')) 544 item = self.__enc_context_popup.Append(-1, _('Move data to another episode')) 545 self.Bind(wx.EVT_MENU, self.__relink_encounter_data2episode, item) 546 item = self.__enc_context_popup.Append(-1, _('Edit details')) 547 self.Bind(wx.EVT_MENU, self.__edit_encounter_details, item) 548 # would require pre-configurable save-under which we don't have 549 #item = self.__enc_context_popup.Append(-1, _('Create progress note')) 550 #self.Bind(wx.EVT_MENU, self.__create_soap_editor, item) 551 item = self.__enc_context_popup.Append(-1, _('Edit progress notes')) 552 self.Bind(wx.EVT_MENU, self.__edit_progress_notes, item) 553 item = self.__enc_context_popup.Append(-1, _('Move progress notes')) 554 self.Bind(wx.EVT_MENU, self.__move_progress_notes, item) 555 item = self.__enc_context_popup.Append(-1, _('Export for Medistar')) 556 self.Bind(wx.EVT_MENU, self.__export_encounter_for_medistar, item)
557 558 #--------------------------------------------------------
559 - def __handle_root_context(self, pos=wx.DefaultPosition):
560 self.PopupMenu(self.__root_context_popup, pos)
561 562 #--------------------------------------------------------
563 - def __handle_issue_context(self, pos=wx.DefaultPosition):
564 self.PopupMenu(self.__issue_context_popup, pos)
565 566 #--------------------------------------------------------
567 - def __handle_episode_context(self, pos=wx.DefaultPosition):
568 self.PopupMenu(self.__epi_context_popup, pos)
569 570 #--------------------------------------------------------
571 - def __handle_encounter_context(self, pos=wx.DefaultPosition):
572 self.PopupMenu(self.__enc_context_popup, pos)
573 574 #-------------------------------------------------------- 575 # episode level 576 #--------------------------------------------------------
577 - def __move_encounters(self, event):
578 episode = self.GetItemData(self.__curr_node) 579 580 gmNarrativeWorkflows.move_progress_notes_to_another_encounter ( 581 parent = self, 582 episodes = [episode['pk_episode']], 583 move_all = True 584 )
585 586 #--------------------------------------------------------
587 - def __toggle_episode_open_close(self, event):
588 self.__curr_node_data['episode_open'] = not self.__curr_node_data['episode_open'] 589 self.__curr_node_data.save()
590 591 #--------------------------------------------------------
592 - def __edit_episode(self, event):
593 gmEMRStructWidgets.edit_episode(parent = self, episode = self.__curr_node_data)
594 595 #--------------------------------------------------------
596 - def __promote_episode_to_issue(self, evt):
597 gmEMRStructWidgets.promote_episode_to_issue(parent=self, episode = self.__curr_node_data, emr = self.__pat.emr)
598 599 #--------------------------------------------------------
600 - def __delete_episode(self, event):
601 dlg = gmGuiHelpers.c2ButtonQuestionDlg ( 602 parent = self, 603 id = -1, 604 caption = _('Deleting episode'), 605 button_defs = [ 606 {'label': _('Yes, delete'), 'tooltip': _('Delete the episode if possible (it must be completely empty).')}, 607 {'label': _('No, cancel'), 'tooltip': _('Cancel and do NOT delete the episode.')} 608 ], 609 question = _( 610 'Are you sure you want to delete this episode ?\n' 611 '\n' 612 ' "%s"\n' 613 ) % self.__curr_node_data['description'] 614 ) 615 result = dlg.ShowModal() 616 if result != wx.ID_YES: 617 return 618 619 if not gmEMRStructItems.delete_episode(episode = self.__curr_node_data): 620 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete episode. There is still clinical data recorded for it.'))
621 622 #--------------------------------------------------------
623 - def __expand_episode_node(self, episode_node=None):
624 self.DeleteChildren(episode_node) 625 626 emr = self.__pat.emr 627 epi = self.GetItemData(episode_node) 628 encounters = emr.get_encounters(episodes = [epi['pk_episode']], skip_empty = True) 629 if len(encounters) == 0: 630 self.SetItemHasChildren(episode_node, False) 631 return 632 633 self.SetItemHasChildren(episode_node, True) 634 635 for enc in encounters: 636 label = '%s: %s' % ( 637 enc['started'].strftime('%Y-%m-%d'), 638 gmTools.unwrap ( 639 gmTools.coalesce ( 640 gmTools.coalesce ( 641 gmTools.coalesce ( 642 enc.get_latest_soap ( # soAp 643 soap_cat = 'a', 644 episode = epi['pk_episode'] 645 ), 646 enc['assessment_of_encounter'] # or AOE 647 ), 648 enc['reason_for_encounter'] # or RFE 649 ), 650 enc['l10n_type'] # or type 651 ), 652 max_length = 40 653 ) 654 ) 655 encounter_node = self.AppendItem(episode_node, label) 656 self.SetItemData(encounter_node, enc) 657 # we don't expand encounter nodes (what for ?) 658 self.SetItemHasChildren(encounter_node, False) 659 660 self.SortChildren(episode_node)
661 662 #-------------------------------------------------------- 663 # encounter level 664 #--------------------------------------------------------
665 - def __move_progress_notes(self, evt):
666 encounter = self.GetItemData(self.__curr_node) 667 node_parent = self.GetItemParent(self.__curr_node) 668 episode = self.GetItemData(node_parent) 669 670 gmNarrativeWorkflows.move_progress_notes_to_another_encounter ( 671 parent = self, 672 encounters = [encounter['pk_encounter']], 673 episodes = [episode['pk_episode']] 674 )
675 676 #--------------------------------------------------------
677 - def __edit_progress_notes(self, event):
678 encounter = self.GetItemData(self.__curr_node) 679 node_parent = self.GetItemParent(self.__curr_node) 680 episode = self.GetItemData(node_parent) 681 682 gmNarrativeWorkflows.manage_progress_notes ( 683 parent = self, 684 encounters = [encounter['pk_encounter']], 685 episodes = [episode['pk_episode']] 686 )
687 688 #--------------------------------------------------------
689 - def __edit_encounter_details(self, event):
690 node_data = self.GetItemData(self.__curr_node) 691 gmEncounterWidgets.edit_encounter(parent = self, encounter = node_data) 692 self.__populate_tree()
693 694 #-------------------------------------------------------- 712 713 #-------------------------------------------------------- 714 # issue level 715 #--------------------------------------------------------
716 - def __edit_issue(self, event):
717 gmEMRStructWidgets.edit_health_issue(parent = self, issue = self.__curr_node_data)
718 719 #--------------------------------------------------------
720 - def __delete_issue(self, event):
721 dlg = gmGuiHelpers.c2ButtonQuestionDlg ( 722 parent = self, 723 id = -1, 724 caption = _('Deleting health issue'), 725 button_defs = [ 726 {'label': _('Yes, delete'), 'tooltip': _('Delete the health issue if possible (it must be completely empty).')}, 727 {'label': _('No, cancel'), 'tooltip': _('Cancel and do NOT delete the health issue.')} 728 ], 729 question = _( 730 'Are you sure you want to delete this health issue ?\n' 731 '\n' 732 ' "%s"\n' 733 ) % self.__curr_node_data['description'] 734 ) 735 result = dlg.ShowModal() 736 if result != wx.ID_YES: 737 dlg.Destroy() 738 return 739 740 dlg.Destroy() 741 742 if not gmEMRStructItems.delete_health_issue(health_issue = self.__curr_node_data): 743 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete health issue. There is still clinical data recorded for it.'))
744 745 #--------------------------------------------------------
746 - def __expand_issue_to_encounter_level(self, evt):
747 748 if not self.__curr_node.IsOk(): 749 return 750 751 self.Expand(self.__curr_node) 752 753 epi, epi_cookie = self.GetFirstChild(self.__curr_node) 754 while epi.IsOk(): 755 self.Expand(epi) 756 epi, epi_cookie = self.GetNextChild(self.__curr_node, epi_cookie)
757 758 #--------------------------------------------------------
759 - def __expand_issue_node(self, issue_node=None):
760 self.DeleteChildren(issue_node) 761 762 issue = self.GetItemData(issue_node) 763 episodes = self.__pat.emr.get_episodes(issues = [issue['pk_health_issue']]) 764 if len(episodes) == 0: 765 self.SetItemHasChildren(issue_node, False) 766 return 767 768 self.SetItemHasChildren(issue_node, True) 769 770 for episode in episodes: 771 range_str, range_str_verb, duration_str = episode.formatted_clinical_duration 772 episode_node = self.AppendItem(issue_node, '%s (%s)' % ( 773 episode['description'], 774 range_str 775 )) 776 self.SetItemData(episode_node, episode) 777 # assume children so we can try to expand it 778 self.SetItemHasChildren(episode_node, True) 779 780 self.SortChildren(issue_node)
781 782 #--------------------------------------------------------
783 - def __expand_pseudo_issue_node(self, fake_issue_node=None):
784 self.DeleteChildren(fake_issue_node) 785 786 episodes = self.__pat.emr.unlinked_episodes 787 if len(episodes) == 0: 788 self.SetItemHasChildren(fake_issue_node, False) 789 return 790 791 self.SetItemHasChildren(fake_issue_node, True) 792 793 for episode in episodes: 794 range_str, range_str_verb, duration_str = episode.formatted_clinical_duration 795 episode_node = self.AppendItem(fake_issue_node, '%s (%s)' % ( 796 episode['description'], 797 range_str 798 )) 799 self.SetItemData(episode_node, episode) 800 if episode['episode_open']: 801 self.SetItemBold(fake_issue_node, True) 802 # assume children so we can try to expand it 803 self.SetItemHasChildren(episode_node, True) 804 805 self.SortChildren(fake_issue_node)
806 807 #-------------------------------------------------------- 808 # EMR level 809 #--------------------------------------------------------
810 - def __print_emr(self, event):
812 813 #--------------------------------------------------------
814 - def __create_issue(self, event):
815 gmEMRStructWidgets.edit_health_issue(parent = self, issue = None)
816 817 #--------------------------------------------------------
818 - def __create_episode(self, event):
819 gmEMRStructWidgets.edit_episode(parent = self, episode = None)
820 821 #--------------------------------------------------------
822 - def __create_soap_editor(self, event):
823 self.__cb__select_edit_mode(True) 824 self.__cb__add_soap_editor(problem = self.__curr_node_data, allow_same_problem = False)
825 826 #--------------------------------------------------------
827 - def __document_allergy(self, event):
828 dlg = gmAllergyWidgets.cAllergyManagerDlg(parent=self, id=-1) 829 # FIXME: use signal and use node level update 830 if dlg.ShowModal() == wx.ID_OK: 831 self.__expanded_nodes = self.ExpansionState 832 self.__populate_tree() 833 dlg.Destroy() 834 return
835 836 #--------------------------------------------------------
837 - def __manage_procedures(self, event):
839 840 #--------------------------------------------------------
841 - def __manage_family_history(self, event):
843 844 #--------------------------------------------------------
845 - def __manage_hospital_stays(self, event):
847 848 #--------------------------------------------------------
849 - def __manage_occupation(self, event):
851 852 #--------------------------------------------------------
853 - def __manage_vaccinations(self, event):
855 856 #--------------------------------------------------------
857 - def __expand_to_issue_level(self, evt):
858 859 root_item = self.GetRootItem() 860 861 if not root_item.IsOk(): 862 return 863 864 self.Expand(root_item) 865 866 # collapse episodes and issues 867 issue, issue_cookie = self.GetFirstChild(root_item) 868 while issue.IsOk(): 869 self.Collapse(issue) 870 epi, epi_cookie = self.GetFirstChild(issue) 871 while epi.IsOk(): 872 self.Collapse(epi) 873 epi, epi_cookie = self.GetNextChild(issue, epi_cookie) 874 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
875 876 #--------------------------------------------------------
877 - def __expand_to_episode_level(self, evt):
878 879 root_item = self.GetRootItem() 880 881 if not root_item.IsOk(): 882 return 883 884 self.Expand(root_item) 885 886 # collapse episodes, expand issues 887 issue, issue_cookie = self.GetFirstChild(root_item) 888 while issue.IsOk(): 889 self.Expand(issue) 890 epi, epi_cookie = self.GetFirstChild(issue) 891 while epi.IsOk(): 892 self.Collapse(epi) 893 epi, epi_cookie = self.GetNextChild(issue, epi_cookie) 894 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
895 896 #--------------------------------------------------------
897 - def __expand_to_encounter_level(self, evt):
898 899 root_item = self.GetRootItem() 900 901 if not root_item.IsOk(): 902 return 903 904 self.Expand(root_item) 905 906 # collapse episodes, expand issues 907 issue, issue_cookie = self.GetFirstChild(root_item) 908 while issue.IsOk(): 909 self.Expand(issue) 910 epi, epi_cookie = self.GetFirstChild(issue) 911 while epi.IsOk(): 912 self.Expand(epi) 913 epi, epi_cookie = self.GetNextChild(issue, epi_cookie) 914 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
915 916 #--------------------------------------------------------
917 - def __export_encounter_for_medistar(self, evt):
918 gmNarrativeWorkflows.export_narrative_for_medistar_import ( 919 parent = self, 920 soap_cats = 'soapu', 921 encounter = self.__curr_node_data 922 )
923 #--------------------------------------------------------
924 - def __expand_root_node(self):
925 root_node = self.GetRootItem() 926 self.DeleteChildren(root_node) 927 928 issues = [{ 929 'description': _('Unattributed episodes'), 930 'laterality': None, 931 'diagnostic_certainty_classification': None, 932 'has_open_episode': False, 933 'pk_health_issue': None 934 }] 935 issues.extend(self.__pat.emr.health_issues) 936 for issue in issues: 937 issue_node = self.AppendItem(root_node, '%s%s%s' % ( 938 issue['description'], 939 gmTools.coalesce(issue['laterality'], '', ' [%s]', none_equivalents = [None, 'na']), 940 gmTools.coalesce(issue['diagnostic_certainty_classification'], '', ' [%s]') 941 )) 942 self.SetItemBold(issue_node, issue['has_open_episode']) 943 self.SetItemData(issue_node, issue) 944 # fake it so we can expand it 945 self.SetItemHasChildren(issue_node, True) 946 947 self.SetItemHasChildren(root_node, (len(issues) != 0)) 948 self.SortChildren(root_node)
949 950 #-------------------------------------------------------- 951 # event handlers 952 #--------------------------------------------------------
953 - def _on_narrative_mod_db(self, *args, **kwargs):
954 self.__update_text_for_selected_node()
955 956 #--------------------------------------------------------
957 - def _on_episode_mod_db(self, *args, **kwargs):
958 self.__expanded_nodes = self.ExpansionState 959 self.__populate_tree()
960 961 #--------------------------------------------------------
962 - def _on_issue_mod_db(self, *args, **kwargs):
963 self.__expanded_nodes = self.ExpansionState 964 self.__populate_tree()
965 966 #--------------------------------------------------------
967 - def _on_tree_item_expanding(self, event):
968 event.Skip() 969 970 node = event.GetItem() 971 if node == self.GetRootItem(): 972 self.__expand_root_node() 973 return 974 975 node_data = self.GetItemData(node) 976 977 if isinstance(node_data, gmEMRStructItems.cHealthIssue): 978 self.__expand_issue_node(issue_node = node) 979 return 980 981 if isinstance(node_data, gmEMRStructItems.cEpisode): 982 self.__expand_episode_node(episode_node = node) 983 return 984 985 # pseudo node "free-standing episodes" 986 if type(node_data) == type({}): 987 self.__expand_pseudo_issue_node(fake_issue_node = node) 988 return
989 990 # encounter nodes do not need expanding 991 #if isinstance(node_data, gmEMRStructItems.cEncounter): 992 993 #--------------------------------------------------------
994 - def _on_tree_item_selected(self, event):
995 sel_item = event.GetItem() 996 self.__curr_node = sel_item 997 self.__update_text_for_selected_node() 998 return True
999 1000 # #-------------------------------------------------------- 1001 # def _on_mouse_motion(self, event): 1002 # 1003 # cursor_pos = (event.GetX(), event.GetY()) 1004 # 1005 # self.SetToolTip(u'') 1006 # 1007 # if cursor_pos != self._old_cursor_pos: 1008 # self._old_cursor_pos = cursor_pos 1009 # (item, flags) = self.HitTest(cursor_pos) 1010 # #if flags != wx.TREE_HITTEST_NOWHERE: 1011 # if flags == wx.TREE_HITTEST_ONITEMLABEL: 1012 # data = self.GetItemData(item) 1013 # 1014 # if not isinstance(data, gmEMRStructItems.cEncounter): 1015 # return 1016 # 1017 # self.SetToolTip(u'%s %s %s - %s\n\nRFE: %s\nAOE: %s' % ( 1018 # gmDateTime.pydt_strftime(data['started'], '%Y %b %d'), 1019 # data['l10n_type'], 1020 # data['started'].strftime('%H:%m'), 1021 # data['last_affirmed'].strftime('%H:%m'), 1022 # gmTools.coalesce(data['reason_for_encounter'], u''), 1023 # gmTools.coalesce(data['assessment_of_encounter'], u'') 1024 # )) 1025 #--------------------------------------------------------
1026 - def _on_tree_item_gettooltip(self, event):
1027 1028 item = event.GetItem() 1029 1030 if not item.IsOk(): 1031 event.SetToolTip(' ') 1032 return 1033 1034 data = self.GetItemData(item) 1035 1036 if isinstance(data, gmEMRStructItems.cEncounter): 1037 tt = '%s %s %s - %s\n' % ( 1038 gmDateTime.pydt_strftime(data['started'], '%Y %b %d'), 1039 data['l10n_type'], 1040 data['started'].strftime('%H:%M'), 1041 data['last_affirmed'].strftime('%H:%M') 1042 ) 1043 if data['reason_for_encounter'] is not None: 1044 tt += '\n' 1045 tt += _('RFE: %s') % data['reason_for_encounter'] 1046 if len(data['pk_generic_codes_rfe']) > 0: 1047 for code in data.generic_codes_rfe: 1048 tt += '\n %s: %s%s%s\n (%s %s)' % ( 1049 code['code'], 1050 gmTools.u_left_double_angle_quote, 1051 code['term'], 1052 gmTools.u_right_double_angle_quote, 1053 code['name_short'], 1054 code['version'] 1055 ) 1056 if data['assessment_of_encounter'] is not None: 1057 tt += '\n' 1058 tt += _('AOE: %s') % data['assessment_of_encounter'] 1059 if len(data['pk_generic_codes_aoe']) > 0: 1060 for code in data.generic_codes_aoe: 1061 tt += '\n %s: %s%s%s\n (%s %s)' % ( 1062 code['code'], 1063 gmTools.u_left_double_angle_quote, 1064 code['term'], 1065 gmTools.u_right_double_angle_quote, 1066 code['name_short'], 1067 code['version'] 1068 ) 1069 1070 elif isinstance(data, gmEMRStructItems.cEpisode): 1071 tt = '' 1072 tt += gmTools.bool2subst ( 1073 (data['diagnostic_certainty_classification'] is not None), 1074 data.diagnostic_certainty_description + '\n\n', 1075 '' 1076 ) 1077 tt += gmTools.bool2subst ( 1078 data['episode_open'], 1079 _('ongoing episode'), 1080 _('closed episode'), 1081 'error: episode state is None' 1082 ) + '\n' 1083 tt += gmTools.coalesce(data['summary'], '', '\n%s') 1084 if len(data['pk_generic_codes']) > 0: 1085 tt += '\n' 1086 for code in data.generic_codes: 1087 tt += '%s: %s%s%s\n (%s %s)\n' % ( 1088 code['code'], 1089 gmTools.u_left_double_angle_quote, 1090 code['term'], 1091 gmTools.u_right_double_angle_quote, 1092 code['name_short'], 1093 code['version'] 1094 ) 1095 1096 tt = tt.strip('\n') 1097 if tt == '': 1098 tt = ' ' 1099 1100 elif isinstance(data, gmEMRStructItems.cHealthIssue): 1101 tt = '' 1102 tt += gmTools.bool2subst(data['is_confidential'], _('*** CONFIDENTIAL ***\n\n'), '') 1103 tt += gmTools.bool2subst ( 1104 (data['diagnostic_certainty_classification'] is not None), 1105 data.diagnostic_certainty_description + '\n', 1106 '' 1107 ) 1108 tt += gmTools.bool2subst ( 1109 (data['laterality'] not in [None, 'na']), 1110 data.laterality_description + '\n', 1111 '' 1112 ) 1113 # noted_at_age is too costly 1114 tt += gmTools.bool2subst(data['is_active'], _('active') + '\n', '') 1115 tt += gmTools.bool2subst(data['clinically_relevant'], _('clinically relevant') + '\n', '') 1116 tt += gmTools.bool2subst(data['is_cause_of_death'], _('contributed to death') + '\n', '') 1117 tt += gmTools.coalesce(data['grouping'], '\n', _('Grouping: %s') + '\n') 1118 tt += gmTools.coalesce(data['summary'], '', '\n%s') 1119 if len(data['pk_generic_codes']) > 0: 1120 tt += '\n' 1121 for code in data.generic_codes: 1122 tt += '%s: %s%s%s\n (%s %s)\n' % ( 1123 code['code'], 1124 gmTools.u_left_double_angle_quote, 1125 code['term'], 1126 gmTools.u_right_double_angle_quote, 1127 code['name_short'], 1128 code['version'] 1129 ) 1130 1131 tt = tt.strip('\n') 1132 if tt == '': 1133 tt = ' ' 1134 1135 else: 1136 tt = self.__root_tooltip 1137 1138 event.SetToolTip(tt)
1139 1140 # doing this prevents the tooltip from showing at all 1141 #event.Skip() 1142 1143 #widgetXY.GetToolTip().Enable(False) 1144 # 1145 #seems to work, supposing the tooltip is actually set for the widget, 1146 #otherwise a test would be needed 1147 #if widgetXY.GetToolTip(): 1148 # widgetXY.GetToolTip().Enable(False) 1149 1150 #--------------------------------------------------------
1151 - def _on_tree_item_right_clicked(self, event):
1152 """Right button clicked: display the popup for the tree""" 1153 1154 node = event.GetItem() 1155 self.SelectItem(node) 1156 self.__curr_node_data = self.GetItemData(node) 1157 self.__curr_node = node 1158 1159 pos = wx.DefaultPosition 1160 if isinstance(self.__curr_node_data, gmEMRStructItems.cHealthIssue): 1161 self.__handle_issue_context(pos=pos) 1162 elif isinstance(self.__curr_node_data, gmEMRStructItems.cEpisode): 1163 self.__handle_episode_context(pos=pos) 1164 elif isinstance(self.__curr_node_data, gmEMRStructItems.cEncounter): 1165 self.__handle_encounter_context(pos=pos) 1166 elif node == self.GetRootItem(): 1167 self.__handle_root_context() 1168 elif type(self.__curr_node_data) == type({}): 1169 # ignore pseudo node "free-standing episodes" 1170 pass 1171 else: 1172 print("error: unknown node type, no popup menu") 1173 event.Skip()
1174 1175 #--------------------------------------------------------
1176 - def OnCompareItems (self, node1=None, node2=None):
1177 """Used in sorting items. 1178 1179 -1: 1 < 2 1180 0: 1 = 2 1181 1: 1 > 2 1182 """ 1183 # FIXME: implement sort modes, chron, reverse cron, by regex, etc 1184 1185 if not node1: 1186 _log.debug('invalid node 1') 1187 return 0 1188 if not node2: 1189 _log.debug('invalid node 2') 1190 return 0 1191 1192 if not node1.IsOk(): 1193 _log.debug('invalid node 1') 1194 return 0 1195 if not node2.IsOk(): 1196 _log.debug('invalid node 2') 1197 return 0 1198 1199 item1 = self.GetItemData(node1) 1200 item2 = self.GetItemData(node2) 1201 1202 # dummy health issue always on top 1203 if isinstance(item1, type({})): 1204 return -1 1205 if isinstance(item2, type({})): 1206 return 1 1207 1208 # encounters: reverse chronologically 1209 if isinstance(item1, gmEMRStructItems.cEncounter): 1210 if item1['started'] == item2['started']: 1211 return 0 1212 if item1['started'] > item2['started']: 1213 return -1 1214 return 1 1215 1216 # episodes: open, then reverse chronologically 1217 if isinstance(item1, gmEMRStructItems.cEpisode): 1218 # open episodes first 1219 if item1['episode_open']: 1220 return -1 1221 if item2['episode_open']: 1222 return 1 1223 start1 = item1.best_guess_clinical_start_date 1224 start2 = item2.best_guess_clinical_start_date 1225 if start1 == start2: 1226 return 0 1227 if start1 < start2: 1228 return 1 1229 return -1 1230 1231 # issues: alpha by grouping, no grouping at the bottom 1232 if isinstance(item1, gmEMRStructItems.cHealthIssue): 1233 1234 # no grouping below grouping 1235 if item1['grouping'] is None: 1236 if item2['grouping'] is not None: 1237 return 1 1238 1239 # grouping above no grouping 1240 if item1['grouping'] is not None: 1241 if item2['grouping'] is None: 1242 return -1 1243 1244 # both no grouping: alpha on description 1245 if (item1['grouping'] is None) and (item2['grouping'] is None): 1246 if item1['description'].lower() < item2['description'].lower(): 1247 return -1 1248 if item1['description'].lower() > item2['description'].lower(): 1249 return 1 1250 return 0 1251 1252 # both with grouping: alpha on grouping, then alpha on description 1253 if item1['grouping'] < item2['grouping']: 1254 return -1 1255 1256 if item1['grouping'] > item2['grouping']: 1257 return 1 1258 1259 if item1['description'].lower() < item2['description'].lower(): 1260 return -1 1261 1262 if item1['description'].lower() > item2['description'].lower(): 1263 return 1 1264 1265 return 0 1266 1267 _log.error('unknown item type during sorting EMR tree:') 1268 _log.error('item1: %s', type(item1)) 1269 _log.error('item2: %s', type(item2)) 1270 1271 return 0
1272 1273 #-------------------------------------------------------- 1274 # properties 1275 #--------------------------------------------------------
1276 - def _get_patient(self):
1277 return self.__pat
1278
1279 - def _set_patient(self, patient):
1280 if self.__pat == patient: 1281 return 1282 self.__pat = patient 1283 if patient is None: 1284 self.clear_tree() 1285 return 1286 return self.__populate_tree()
1287 1288 patient = property(_get_patient, _set_patient) 1289 #--------------------------------------------------------
1290 - def _get_details_display_mode(self):
1291 return self.__soap_display_mode
1292
1293 - def _set_details_display_mode(self, mode):
1294 if mode not in ['details', 'journal', 'revisions']: 1295 raise ValueError('details display mode must be one of "details", "journal", "revisions"') 1296 if self.__soap_display_mode == mode: 1297 return 1298 self.__soap_display_mode = mode 1299 self.__update_text_for_selected_node()
1300 1301 details_display_mode = property(_get_details_display_mode, _set_details_display_mode)
1302 1303 #================================================================ 1304 # FIXME: still needed ? 1305 from Gnumed.wxGladeWidgets import wxgScrolledEMRTreePnl 1306
1307 -class cScrolledEMRTreePnl(wxgScrolledEMRTreePnl.wxgScrolledEMRTreePnl):
1308 """A scrollable panel holding an EMR tree. 1309 1310 Lacks a widget to display details for selected items. The 1311 tree data will be refetched - if necessary - whenever 1312 repopulate_ui() is called, e.g., when the patient is changed. 1313 """
1314 - def __init__(self, *args, **kwds):
1316 1317 #============================================================ 1318 from Gnumed.wxGladeWidgets import wxgSplittedEMRTreeBrowserPnl 1319
1320 -class cSplittedEMRTreeBrowserPnl(wxgSplittedEMRTreeBrowserPnl.wxgSplittedEMRTreeBrowserPnl):
1321 """A splitter window holding an EMR tree. 1322 1323 The left hand side displays a scrollable EMR tree while 1324 on the right details for selected items are displayed. 1325 1326 Expects to be put into a Notebook. 1327 """
1328 - def __init__(self, *args, **kwds):
1329 wxgSplittedEMRTreeBrowserPnl.wxgSplittedEMRTreeBrowserPnl.__init__(self, *args, **kwds) 1330 self._pnl_emr_tree._emr_tree.soap_display = self._TCTRL_item_details 1331 self._pnl_emr_tree._emr_tree.image_display = self._PNL_visual_soap 1332 self._pnl_emr_tree._emr_tree.set_enable_display_mode_selection_callback(self.enable_display_mode_selection) 1333 self._pnl_emr_tree._emr_tree.soap_editor_adder = self._add_soap_editor 1334 self._pnl_emr_tree._emr_tree.edit_mode_selector = self._select_edit_mode 1335 self.__register_events() 1336 1337 self.editing = False
1338 #--------------------------------------------------------
1339 - def __register_events(self):
1340 gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection) 1341 gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection) 1342 return True
1343 1344 #--------------------------------------------------------
1345 - def _get_editing(self):
1346 return self.__editing
1347
1348 - def _set_editing(self, editing):
1349 self.__editing = editing 1350 self.enable_display_mode_selection(enable = not self.__editing) 1351 if self.__editing: 1352 self._BTN_switch_browse_edit.SetLabel(_('&Browse %s') % gmTools.u_ellipsis) 1353 self._PNL_browse.Hide() 1354 self._PNL_visual_soap.Hide() 1355 self._PNL_edit.Show() 1356 else: 1357 self._BTN_switch_browse_edit.SetLabel(_('&New notes %s') % gmTools.u_ellipsis) 1358 self._PNL_edit.Hide() 1359 self._PNL_visual_soap.Show() 1360 self._PNL_browse.Show() 1361 self._PNL_right_side.GetSizer().Layout()
1362 1363 editing = property(_get_editing, _set_editing) 1364 1365 #-------------------------------------------------------- 1366 # event handler 1367 #--------------------------------------------------------
1369 self._pnl_emr_tree._emr_tree.patient = None 1370 self._PNL_edit.patient = None 1371 return True
1372 1373 #--------------------------------------------------------
1374 - def _on_post_patient_selection(self):
1375 if self.GetParent().GetCurrentPage() != self: 1376 return True 1377 self.repopulate_ui() 1378 return True
1379 1380 #--------------------------------------------------------
1381 - def _on_show_details_selected(self, event):
1382 self._pnl_emr_tree._emr_tree.details_display_mode = 'details'
1383 1384 #--------------------------------------------------------
1385 - def _on_show_journal_selected(self, event):
1386 self._pnl_emr_tree._emr_tree.details_display_mode = 'journal'
1387 1388 #--------------------------------------------------------
1389 - def _on_show_revisions_selected(self, event):
1390 self._pnl_emr_tree._emr_tree.details_display_mode = 'revisions'
1391 1392 #--------------------------------------------------------
1394 self.editing = not self.__editing
1395 1396 #-------------------------------------------------------- 1397 # external API 1398 #--------------------------------------------------------
1399 - def repopulate_ui(self):
1400 """Fills UI with data.""" 1401 pat = gmPerson.gmCurrentPatient() 1402 self._pnl_emr_tree._emr_tree.patient = pat 1403 self._PNL_edit.patient = pat 1404 self._splitter_browser.SetSashPosition(self._splitter_browser.GetSize()[0] // 3, True) 1405 1406 return True
1407 1408 #--------------------------------------------------------
1409 - def enable_display_mode_selection(self, enable):
1410 if self.editing: 1411 enable = False 1412 if enable: 1413 self._RBTN_details.Enable(True) 1414 self._RBTN_journal.Enable(True) 1415 self._RBTN_revisions.Enable(True) 1416 return 1417 self._RBTN_details.Enable(False) 1418 self._RBTN_journal.Enable(False) 1419 self._RBTN_revisions.Enable(False)
1420 1421 #--------------------------------------------------------
1422 - def _add_soap_editor(self, problem=None, allow_same_problem=False):
1423 self._PNL_edit._NB_soap_editors.add_editor(problem = problem, allow_same_problem = allow_same_problem)
1424 1425 #--------------------------------------------------------
1426 - def _select_edit_mode(self, edit=True):
1427 self.editing = edit
1428 1429 #================================================================ 1430 from Gnumed.wxGladeWidgets import wxgEMRJournalPluginPnl 1431
1432 -class cEMRJournalPluginPnl(wxgEMRJournalPluginPnl.wxgEMRJournalPluginPnl):
1433
1434 - def __init__(self, *args, **kwds):
1435 1436 wxgEMRJournalPluginPnl.wxgEMRJournalPluginPnl.__init__(self, *args, **kwds) 1437 self._TCTRL_journal.disable_keyword_expansions() 1438 self._TCTRL_journal.SetValue('')
1439 #-------------------------------------------------------- 1440 # external API 1441 #--------------------------------------------------------
1442 - def repopulate_ui(self):
1443 self._TCTRL_journal.SetValue('') 1444 exporter = gmPatientExporter.cEMRJournalExporter() 1445 if self._RBTN_by_encounter.GetValue(): 1446 fname = exporter.save_to_file_by_encounter(patient = gmPerson.gmCurrentPatient()) 1447 else: 1448 fname = exporter.save_to_file_by_mod_time(patient = gmPerson.gmCurrentPatient()) 1449 1450 f = io.open(fname, mode = 'rt', encoding = 'utf8', errors = 'replace') 1451 for line in f: 1452 self._TCTRL_journal.AppendText(line) 1453 f.close() 1454 1455 self._TCTRL_journal.ShowPosition(self._TCTRL_journal.GetLastPosition()) 1456 return True
1457 #-------------------------------------------------------- 1458 # internal helpers 1459 #--------------------------------------------------------
1460 - def __register_events(self):
1461 gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection) 1462 gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection) 1463 return True
1464 1465 #-------------------------------------------------------- 1466 # event handlers 1467 #--------------------------------------------------------
1469 self._TCTRL_journal.SetValue('') 1470 return True
1471 1472 #--------------------------------------------------------
1473 - def _on_post_patient_selection(self):
1474 if self.GetParent().GetCurrentPage() != self: 1475 return True 1476 self.repopulate_ui() 1477 return True
1478 1479 #--------------------------------------------------------
1480 - def _on_order_by_encounter_selected(self, event):
1481 self.repopulate_ui()
1482 1483 #--------------------------------------------------------
1484 - def _on_order_by_last_mod_selected(self, event):
1485 self.repopulate_ui()
1486 1487 #--------------------------------------------------------
1488 - def _on_button_find_pressed(self, event):
1489 self._TCTRL_journal.show_find_dialog(title = _('Find text in EMR Journal'))
1490 1491 #================================================================ 1492 from Gnumed.wxGladeWidgets import wxgEMRListJournalPluginPnl 1493
1494 -class cEMRListJournalPluginPnl(wxgEMRListJournalPluginPnl.wxgEMRListJournalPluginPnl):
1495
1496 - def __init__(self, *args, **kwds):
1497 1498 wxgEMRListJournalPluginPnl.wxgEMRListJournalPluginPnl.__init__(self, *args, **kwds) 1499 1500 self._LCTRL_journal.select_callback = self._on_row_selected 1501 self._TCTRL_details.SetValue('') 1502 1503 self.__load_timer = gmTimer.cTimer(callback = self._on_load_details, delay = 1000, cookie = 'EMRListJournalPluginDBLoadTimer') 1504 1505 self.__data = {}
1506 1507 #-------------------------------------------------------- 1508 # external API 1509 #--------------------------------------------------------
1510 - def repopulate_ui(self):
1511 self._LCTRL_journal.remove_items_safely() 1512 self._TCTRL_details.SetValue('') 1513 1514 if self._RBTN_by_encounter.Value: # (... is True:) 1515 order_by = 'encounter_started, pk_episode, src_table, scr, modified_when' 1516 #, clin_when (should not make a relevant difference) 1517 date_col_header = _('Encounter') 1518 date_fields = ['encounter_started', 'modified_when'] 1519 elif self._RBTN_by_last_modified.Value: # (... is True:) 1520 order_by = 'modified_when, pk_episode, src_table, scr' 1521 #, clin_when (should not make a relevant difference) 1522 date_col_header = _('Modified') 1523 date_fields = ['modified_when'] 1524 elif self._RBTN_by_item_time.Value: # (... is True:) 1525 order_by = 'clin_when, pk_episode, src_table, scr, modified_when' 1526 date_col_header = _('Clinical time') 1527 date_fields = ['clin_when', 'modified_when'] 1528 else: 1529 raise ValueError('invalid EMR journal list sort state') 1530 1531 self._LCTRL_journal.set_columns([date_col_header, '', _('Entry'), _('Who / When')]) 1532 self._LCTRL_journal.set_resize_column(3) 1533 1534 journal = gmPerson.gmCurrentPatient().emr.get_as_journal(order_by = order_by) 1535 1536 items = [] 1537 data = [] 1538 self.__data = {} 1539 prev_date = None 1540 for entry in journal: 1541 if entry['narrative'].strip() == '': 1542 continue 1543 soap_cat = gmSoapDefs.soap_cat2l10n[entry['soap_cat']] 1544 who = '%s (%s)' % (entry['modified_by'], entry['date_modified']) 1545 try: 1546 entry_date = gmDateTime.pydt_strftime(entry[date_fields[0]], '%Y-%m-%d') 1547 except KeyError: 1548 entry_date = gmDateTime.pydt_strftime(entry[date_fields[1]], '%Y-%m-%d') 1549 if entry_date == prev_date: 1550 date2show = '' 1551 else: 1552 date2show = entry_date 1553 prev_date = entry_date 1554 lines = entry['narrative'].strip().split('\n') 1555 line_0 = lines[0].rstrip() # assumes there's at least one line ... 1556 if len(lines) == 1: 1557 delim = gmTools.u_box_horiz_light_3dashes * 10 + gmTools.u_box_T_left 1558 else: 1559 delim = (gmTools.u_box_horiz_light_3dashes * 10) + gmTools.u_box_top_right_arc 1560 entry_line = '%s %s' % (line_0, delim) 1561 items.append([date2show, soap_cat, entry_line, who]) 1562 try: 1563 self.__data[entry['src_table']] 1564 except KeyError: 1565 self.__data[entry['src_table']] = {} 1566 try: 1567 self.__data[entry['src_table']][entry['src_pk']] 1568 except KeyError: 1569 self.__data[entry['src_table']][entry['src_pk']] = {} 1570 self.__data[entry['src_table']][entry['src_pk']]['entry'] = entry 1571 self.__data[entry['src_table']][entry['src_pk']]['formatted_instance'] = None 1572 if entry['encounter_started'] is None: 1573 enc_duration = gmTools.u_diameter 1574 else: 1575 enc_duration = '%s - %s' % ( 1576 gmDateTime.pydt_strftime(entry['encounter_started'], '%Y %b %d %H:%M'), 1577 gmDateTime.pydt_strftime(entry['encounter_last_affirmed'], '%H:%M') 1578 ) 1579 self.__data[entry['src_table']][entry['src_pk']]['formatted_header'] = _( 1580 'Chart entry: %s [#%s in %s]\n' 1581 ' Modified: %s by %s (%s rev %s)\n' 1582 '\n' 1583 'Health issue: %s%s\n' 1584 'Episode: %s%s\n' 1585 'Encounter: %s%s' 1586 ) % ( 1587 gmClinicalRecord.format_clin_root_item_type(entry['src_table']), 1588 entry['src_pk'], 1589 entry['src_table'], 1590 entry['date_modified'], 1591 entry['modified_by'], 1592 gmTools.u_arrow2right, 1593 entry['row_version'], 1594 gmTools.coalesce(entry['health_issue'], gmTools.u_diameter, '%s'), 1595 gmTools.bool2subst(entry['issue_active'], ' (' + _('active') + ')', ' (' + _('inactive') + ')', ''), 1596 gmTools.coalesce(entry['episode'], gmTools.u_diameter, '%s'), 1597 gmTools.bool2subst(entry['episode_open'], ' (' + _('open') + ')', ' (' + _('closed') + ')', ''), 1598 enc_duration, 1599 gmTools.coalesce(entry['encounter_l10n_type'], '', ' (%s)'), 1600 ) 1601 self.__data[entry['src_table']][entry['src_pk']]['formatted_root_item'] = _( 1602 '%s\n' 1603 '\n' 1604 ' rev %s (%s) by %s in <%s>' 1605 ) % ( 1606 entry['narrative'].strip(), 1607 entry['row_version'], 1608 entry['date_modified'], 1609 entry['modified_by'], 1610 entry['src_table'] 1611 ) 1612 data.append ({ 1613 'table': entry['src_table'], 1614 'pk': entry['src_pk'] 1615 }) 1616 if len(lines) > 1: 1617 lines = lines[1:] 1618 idx = 0 1619 last_line = len(lines) 1620 for entry_line in lines: 1621 idx += 1 1622 if entry_line.strip() == '': 1623 continue 1624 if idx == last_line: 1625 bar = gmTools.u_box_bottom_left_arc 1626 else: 1627 bar = gmTools.u_box_vert_light_4dashes 1628 items.append(['', bar, entry_line.rstrip(), who]) 1629 data.append ({ 1630 'table': entry['src_table'], 1631 'pk': entry['src_pk'] 1632 }) 1633 1634 self._LCTRL_journal.set_string_items(items) 1635 self._LCTRL_journal.set_column_widths([wx.LIST_AUTOSIZE, wx.LIST_AUTOSIZE, wx.LIST_AUTOSIZE, wx.LIST_AUTOSIZE_USEHEADER]) 1636 self._LCTRL_journal.set_data(data) 1637 1638 self._LCTRL_journal.SetFocus() 1639 return True
1640 1641 #-------------------------------------------------------- 1642 # internal helpers 1643 #--------------------------------------------------------
1644 - def __register_events(self):
1645 gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection) 1646 gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection) 1647 return True
1648 1649 #-------------------------------------------------------- 1650 # event handlers 1651 #--------------------------------------------------------
1653 self._LCTRL_journal.remove_items_safely() 1654 self._TCTRL_details.SetValue('') 1655 self.__data = {} 1656 return True
1657 1658 #--------------------------------------------------------
1659 - def _on_post_patient_selection(self):
1660 if self.GetParent().GetCurrentPage() != self: 1661 return True 1662 self.repopulate_ui() 1663 return True
1664 1665 #--------------------------------------------------------
1666 - def _on_row_selected(self, evt):
1667 # FIXME: work on all selected 1668 data = self._LCTRL_journal.get_item_data(item_idx = evt.Index) 1669 if self.__data[data['table']][data['pk']]['formatted_instance'] is None: 1670 txt = _( 1671 '%s\n' 1672 '%s\n' 1673 '%s' 1674 ) % ( 1675 self.__data[data['table']][data['pk']]['formatted_header'], 1676 gmTools.u_box_horiz_4dashes * 40, 1677 self.__data[data['table']][data['pk']]['formatted_root_item'] 1678 ) 1679 1680 self._TCTRL_details.SetValue(txt) 1681 self.__load_timer.Stop() 1682 self.__load_timer.Start(oneShot = True) 1683 return 1684 1685 txt = _( 1686 '%s\n' 1687 '%s\n' 1688 '%s' 1689 ) % ( 1690 self.__data[data['table']][data['pk']]['formatted_header'], 1691 gmTools.u_box_horiz_4dashes * 40, 1692 self.__data[data['table']][data['pk']]['formatted_instance'] 1693 ) 1694 self._TCTRL_details.SetValue(txt)
1695 1696 #--------------------------------------------------------
1697 - def _on_load_details(self, cookie):
1698 data = self._LCTRL_journal.get_selected_item_data(only_one = True) 1699 if self.__data[data['table']][data['pk']]['formatted_instance'] is None: 1700 self.__data[data['table']][data['pk']]['formatted_instance'] = gmClinicalRecord.format_clin_root_item(data['table'], data['pk'], patient = gmPerson.gmCurrentPatient()) 1701 txt = _( 1702 '%s\n' 1703 '%s\n' 1704 '%s' 1705 ) % ( 1706 self.__data[data['table']][data['pk']]['formatted_header'], 1707 gmTools.u_box_horiz_4dashes * 40, 1708 self.__data[data['table']][data['pk']]['formatted_instance'] 1709 ) 1710 wx.CallAfter(self._TCTRL_details.SetValue, txt)
1711 1712 #--------------------------------------------------------
1713 - def _on_order_by_encounter_selected(self, event):
1714 self.repopulate_ui()
1715 1716 #--------------------------------------------------------
1717 - def _on_order_by_last_mod_selected(self, event):
1718 self.repopulate_ui()
1719 1720 #--------------------------------------------------------
1721 - def _on_order_by_item_time_selected(self, event):
1722 self.repopulate_ui()
1723 1724 #--------------------------------------------------------
1725 - def _on_order_by_item_time_selected(self, event):
1726 event.Skip() 1727 self.repopulate_ui()
1728 1729 #--------------------------------------------------------
1730 - def _on_edit_button_pressed(self, event):
1731 event.Skip()
1732 1733 #--------------------------------------------------------
1734 - def _on_delete_button_pressed(self, event):
1735 event.Skip()
1736 1737 #-------------------------------------------------------- 1738 # def _on_button_find_pressed(self, event): 1739 # self._TCTRL_details.show_find_dialog(title = _('Find text in EMR Journal')) 1740 1741 #================================================================ 1742 # MAIN 1743 #---------------------------------------------------------------- 1744 if __name__ == '__main__': 1745 1746 _log.info("starting emr browser...") 1747 1748 try: 1749 # obtain patient 1750 patient = gmPersonSearch.ask_for_patient() 1751 if patient is None: 1752 print("No patient. Exiting gracefully...") 1753 sys.exit(0) 1754 gmPatSearchWidgets.set_active_patient(patient = patient) 1755 1756 # display standalone browser 1757 application = wx.PyWidgetTester(size=(800,600)) 1758 emr_browser = cEMRBrowserPanel(application.frame, -1) 1759 emr_browser.refresh_tree() 1760 1761 application.frame.Show(True) 1762 application.MainLoop() 1763 1764 # clean up 1765 if patient is not None: 1766 try: 1767 patient.cleanup() 1768 except: 1769 print("error cleaning up patient") 1770 except Exception: 1771 _log.exception("unhandled exception caught !") 1772 # but re-raise them 1773 raise 1774 1775 _log.info("closing emr browser...") 1776 1777 #================================================================ 1778