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

Source Code for Module Gnumed.wxpython.gmSOAPWidgets

   1  """GNUmed SOAP related widgets. 
   2  """ 
   3  #============================================================ 
   4  __version__ = "$Revision: 1.114 $" 
   5  __author__ = "Carlos Moro <cfmoro1976@yahoo.es>, K.Hilbert <Karsten.Hilbert@gmx.net>" 
   6  __license__ = "GPL" 
   7   
   8  # std library 
   9  import types, logging 
  10   
  11   
  12  # 3rd party 
  13  import wx 
  14   
  15   
  16  # GNUmed 
  17  from Gnumed.pycommon import gmDispatcher, gmI18N, gmExceptions, gmMatchProvider, gmTools, gmCfg 
  18  from Gnumed.wxpython import gmResizingWidgets, gmPhraseWheel, gmEMRStructWidgets, gmGuiHelpers, gmRegetMixin, gmEditArea, gmPatSearchWidgets 
  19  from Gnumed.business import gmPerson, gmEMRStructItems, gmSOAPimporter, gmPraxis, gmPersonSearch, gmStaff 
  20   
  21  _log = logging.getLogger('gm.ui') 
  22  _log.info(__version__) 
  23   
  24  #============================================================ 
25 -def create_issue_popup(parent, pos, size, style, data_sink):
26 ea = gmEMRStructWidgets.cHealthIssueEditArea ( 27 parent, 28 -1, 29 wx.DefaultPosition, 30 wx.DefaultSize, 31 wx.NO_BORDER | wx.TAB_TRAVERSAL, 32 data_sink = data_sink 33 ) 34 popup = gmEditArea.cEditAreaPopup ( 35 parent = parent, 36 id = -1, 37 title = '', 38 pos = pos, 39 size = size, 40 style = style, 41 name = '', 42 edit_area = ea 43 ) 44 return popup
45 #============================================================
46 -def create_vacc_popup(parent, pos, size, style, data_sink):
47 ea = gmVaccWidgets.cVaccinationEditArea ( 48 parent = parent, 49 id = -1, 50 pos = pos, 51 size = size, 52 style = style, 53 data_sink = data_sink 54 ) 55 popup = gmEditArea.cEditAreaPopup ( 56 parent = parent, 57 id = -1, 58 title = _('Enter vaccination given'), 59 pos = pos, 60 size = size, 61 style = style, 62 name = '', 63 edit_area = ea 64 ) 65 return popup
66 #============================================================ 67 # FIXME: keywords hardcoded for now, load from cfg in backend instead 68 progress_note_keywords = { 69 's': { 70 '$missing_action': {}, 71 'phx$': { 72 'widget_factory': create_issue_popup, 73 'widget_data_sink': None 74 }, 75 'ea$:': { 76 'widget_factory': create_issue_popup, 77 'widget_data_sink': None 78 }, 79 '$vacc': { 80 'widget_factory': create_vacc_popup, 81 'widget_data_sink': None 82 }, 83 'impf:': { 84 'widget_factory': create_vacc_popup, 85 'widget_data_sink': None 86 }, 87 'icpc:': {}, 88 'icpc?': {} 89 }, 90 'o': { 91 'icpc:': {}, 92 'icpc?': {} 93 }, 94 'a': { 95 'icpc:': {}, 96 'icpc?': {} 97 }, 98 'p': { 99 '$vacc': { 100 'widget_factory': create_vacc_popup, 101 'widget_data_sink': None 102 }, 103 'icpc:': {}, 104 'icpc?': {} 105 } 106 } 107 #============================================================
108 -class cProgressNoteInputNotebook(wx.Notebook, gmRegetMixin.cRegetOnPaintMixin):
109 """A notebook holding panels with progress note editors. 110 111 There is one progress note editor panel for each episode being worked on. 112 """
113 - def __init__(self, parent, id, pos=wx.DefaultPosition, size=wx.DefaultSize):
114 wx.Notebook.__init__ ( 115 self, 116 parent = parent, 117 id = id, 118 pos = pos, 119 size = size, 120 style = wx.NB_TOP | wx.NB_MULTILINE | wx.NO_BORDER, 121 name = self.__class__.__name__ 122 ) 123 gmRegetMixin.cRegetOnPaintMixin.__init__(self) 124 self.__pat = gmPerson.gmCurrentPatient() 125 self.__do_layout() 126 self.__register_interests()
127 #-------------------------------------------------------- 128 # public API 129 #--------------------------------------------------------
130 - def add_editor(self, problem=None, allow_same_problem=False):
131 """Add a progress note editor page. 132 133 The way <allow_same_problem> is currently used in callers 134 it only applies to unassociated episodes. 135 """ 136 problem_to_add = problem 137 138 # determine label 139 if problem_to_add is None: 140 label = _('new problem') 141 else: 142 # normalize problem type 143 emr = self.__pat.get_emr() 144 if isinstance(problem_to_add, gmEMRStructItems.cEpisode): 145 problem_to_add = emr.episode2problem(episode = problem_to_add) 146 elif isinstance(problem_to_add, gmEMRStructItems.cHealthIssue): 147 problem_to_add = emr.health_issue2problem(issue = problem_to_add) 148 if not isinstance(problem_to_add, gmEMRStructItems.cProblem): 149 raise TypeError('cannot open progress note editor for [%s]' % problem_to_add) 150 label = problem_to_add['problem'] 151 # FIXME: configure maximum length 152 if len(label) > 23: 153 label = label[:21] + gmTools.u_ellipsis 154 155 if allow_same_problem: 156 new_page = cResizingSoapPanel(parent = self, problem = problem_to_add) 157 result = self.AddPage ( 158 page = new_page, 159 text = label, 160 select = True 161 ) 162 return result 163 164 # check for dupes 165 # new unassociated problem 166 if problem_to_add is None: 167 # check for dupes 168 for page_idx in range(self.GetPageCount()): 169 page = self.GetPage(page_idx) 170 # found 171 if page.get_problem() is None: 172 self.SetSelection(page_idx) 173 return True 174 continue 175 # not found 176 new_page = cResizingSoapPanel(parent = self, problem = problem_to_add) 177 result = self.AddPage ( 178 page = new_page, 179 text = label, 180 select = True 181 ) 182 return result 183 184 # real problem 185 # - raise existing editor ? 186 for page_idx in range(self.GetPageCount()): 187 page = self.GetPage(page_idx) 188 problem_of_page = page.get_problem() 189 # editor is for unassociated new problem 190 if problem_of_page is None: 191 continue 192 # editor is for episode 193 if problem_of_page['type'] == 'episode': 194 if problem_to_add['type'] == 'issue': 195 is_equal = (problem_of_page['pk_health_issue'] == problem_to_add['pk_health_issue']) 196 else: 197 is_equal = (problem_of_page['pk_episode'] == problem_to_add['pk_episode']) 198 if is_equal: 199 self.SetSelection(page_idx) 200 return True 201 continue 202 # editor is for health issue 203 if problem_of_page['type'] == 'issue': 204 if problem_of_page['pk_health_issue'] == problem_to_add['pk_health_issue']: 205 self.SetSelection(page_idx) 206 return True 207 continue 208 209 # - add new editor 210 new_page = cResizingSoapPanel(parent = self, problem = problem_to_add) 211 result = self.AddPage ( 212 page = new_page, 213 text = label, 214 select = True 215 ) 216 217 return result
218 #--------------------------------------------------------
219 - def close_current_editor(self):
220 221 page_idx = self.GetSelection() 222 page = self.GetPage(page_idx) 223 224 if not page.editor_empty(): 225 really_discard = gmGuiHelpers.gm_show_question ( 226 _('Are you sure you really want to\n' 227 'discard this progress note ?\n' 228 ), 229 _('Discarding progress note') 230 ) 231 if really_discard is False: 232 return 233 234 self.DeletePage(page_idx) 235 236 # always keep one unassociated editor open 237 if self.GetPageCount() == 0: 238 self.add_editor()
239 #--------------------------------------------------------
240 - def warn_on_unsaved_soap(self):
241 242 for page_idx in range(self.GetPageCount()): 243 page = self.GetPage(page_idx) 244 if page.editor_empty(): 245 continue 246 247 gmGuiHelpers.gm_show_warning ( 248 _('There are unsaved progress notes !\n'), 249 _('Unsaved progress notes') 250 ) 251 return False 252 253 return True
254 #--------------------------------------------------------
255 - def save_unsaved_soap(self):
256 save_all = False 257 dlg = None 258 for page_idx in range(self.GetPageCount()): 259 page = self.GetPage(page_idx) 260 if page.editor_empty(): 261 continue 262 263 if dlg is None: 264 dlg = gmGuiHelpers.c3ButtonQuestionDlg ( 265 self, 266 -1, 267 caption = _('Unsaved progress note'), 268 question = _( 269 'This progress note has not been saved yet.\n' 270 '\n' 271 'Do you want to save it or discard it ?\n\n' 272 ), 273 button_defs = [ 274 {'label': _('&Save'), 'tooltip': _('Save this progress note'), 'default': True}, 275 {'label': _('&Discard'), 'tooltip': _('Discard this progress note'), 'default': False}, 276 {'label': _('Save &all'), 'tooltip': _('Save all remaining unsaved progress notes'), 'default': False} 277 ] 278 ) 279 280 if not save_all: 281 self.ChangeSelection(page_idx) 282 decision = dlg.ShowModal() 283 if decision == wx.ID_NO: 284 _log.info('user requested discarding of unsaved progress note') 285 continue 286 if decision == wx.ID_CANCEL: 287 save_all = True 288 page.save() 289 290 if dlg is not None: 291 dlg.Destroy()
292 #-------------------------------------------------------- 293 # internal API 294 #--------------------------------------------------------
295 - def __do_layout(self):
296 # add one empty unassociated progress note editor - which to 297 # have (by all sensible accounts) seems to be the intent when 298 # instantiating this class 299 self.add_editor()
300 #-------------------------------------------------------- 301 # reget mixin API 302 #--------------------------------------------------------
303 - def _populate_with_data(self):
304 print '[%s._populate_with_data] nothing to do, really...' % self.__class__.__name__ 305 return True
306 #-------------------------------------------------------- 307 # event handling 308 #--------------------------------------------------------
309 - def __register_interests(self):
310 """Configure enabled event signals 311 """ 312 # wxPython events 313 314 # client internal signals 315 gmDispatcher.connect(signal = u'post_patient_selection', receiver=self._on_post_patient_selection) 316 # gmDispatcher.connect(signal = u'application_closing', receiver=self._on_application_closing) 317 318 self.__pat.register_pre_selection_callback(callback = self._pre_selection_callback) 319 320 gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
321 #--------------------------------------------------------
322 - def _pre_selection_callback(self):
323 """Another patient is about to be activated. 324 325 Patient change will not proceed before this returns True. 326 """ 327 return self.warn_on_unsaved_soap()
328 #--------------------------------------------------------
329 - def _pre_exit_callback(self):
330 """The client is about to be shut down. 331 332 Shutdown will not proceed before this returns. 333 """ 334 self.save_unsaved_soap()
335 #--------------------------------------------------------
336 - def _on_post_patient_selection(self):
337 """Patient changed.""" 338 self.DeleteAllPages() 339 self.add_editor() 340 self._schedule_data_reget()
341 #-------------------------------------------------------- 342 # def _on_application_closing(self): 343 # """GNUmed is shutting down.""" 344 # print "[%s]: the application is closing down" % self.__class__.__name__ 345 # print "************************************" 346 # print "need to ask user about SOAP saving !" 347 # print "************************************" 348 #-------------------------------------------------------- 349 # def _on_episodes_modified(self): 350 # print "[%s]: episode modified" % self.__class__.__name__ 351 # print "need code to deal with:" 352 # print "- deleted episode that we show so we can notify the user" 353 # print "- renamed episode so we can update our episode label" 354 # self._schedule_data_reget() 355 # pass 356 #============================================================
357 -class cNotebookedProgressNoteInputPanel(wx.Panel):
358 """A panel for entering multiple progress notes in context. 359 360 Expects to be used as a notebook page. 361 362 Left hand side: 363 - problem list (health issues and active episodes) 364 365 Right hand side: 366 - notebook with progress note editors 367 368 Listens to patient change signals, thus acts on the current patient. 369 """ 370 #--------------------------------------------------------
371 - def __init__(self, parent, id):
372 """Contructs a new instance of SOAP input panel 373 374 @param parent: Wx parent widget 375 @param id: Wx widget id 376 """ 377 # Call parents constructors 378 wx.Panel.__init__ ( 379 self, 380 parent = parent, 381 id = id, 382 pos = wx.DefaultPosition, 383 size = wx.DefaultSize, 384 style = wx.NO_BORDER 385 ) 386 self.__pat = gmPerson.gmCurrentPatient() 387 388 # ui contruction and event handling set up 389 self.__do_layout() 390 self.__register_interests() 391 self.reset_ui_content()
392 #-------------------------------------------------------- 393 # public API 394 #--------------------------------------------------------
395 - def reset_ui_content(self):
396 """ 397 Clear all information from input panel 398 """ 399 self.__LST_problems.Clear() 400 self.__soap_notebook.DeleteAllPages() 401 self.__soap_notebook.add_editor()
402 #-------------------------------------------------------- 403 # internal helpers 404 #--------------------------------------------------------
405 - def __do_layout(self):
406 """Arrange widgets. 407 408 left: problem list (mix of issues and episodes) 409 right: soap editors 410 """ 411 # SOAP input panel main splitter window 412 self.__splitter = wx.SplitterWindow(self, -1) 413 414 # left hand side 415 PNL_list = wx.Panel(self.__splitter, -1) 416 # - header 417 list_header = wx.StaticText ( 418 parent = PNL_list, 419 id = -1, 420 label = _('Active problems'), 421 style = wx.NO_BORDER | wx.ALIGN_CENTRE 422 ) 423 # - problem list 424 self.__LST_problems = wx.ListBox ( 425 PNL_list, 426 -1, 427 style= wx.NO_BORDER 428 ) 429 # - arrange 430 szr_left = wx.BoxSizer(wx.VERTICAL) 431 szr_left.Add(list_header, 0) 432 szr_left.Add(self.__LST_problems, 1, wx.EXPAND) 433 PNL_list.SetSizerAndFit(szr_left) 434 435 # right hand side 436 # - soap inputs panel 437 PNL_soap_editors = wx.Panel(self.__splitter, -1) 438 # - progress note notebook 439 self.__soap_notebook = cProgressNoteInputNotebook(PNL_soap_editors, -1) 440 # - buttons 441 self.__BTN_add_unassociated = wx.Button(PNL_soap_editors, -1, _('&New')) 442 tt = _( 443 'Add editor for a new unassociated progress note.\n\n' 444 'There is a configuration option whether or not to\n' 445 'allow several new unassociated progress notes at once.' 446 ) 447 self.__BTN_add_unassociated.SetToolTipString(tt) 448 449 self.__BTN_save = wx.Button(PNL_soap_editors, -1, _('&Save')) 450 self.__BTN_save.SetToolTipString(_('Save progress note into medical record and close this editor.')) 451 452 self.__BTN_clear = wx.Button(PNL_soap_editors, -1, _('&Clear')) 453 self.__BTN_clear.SetToolTipString(_('Clear this progress note editor.')) 454 455 self.__BTN_discard = wx.Button(PNL_soap_editors, -1, _('&Discard')) 456 self.__BTN_discard.SetToolTipString(_('Discard progress note and close this editor. You will loose any data already typed into this editor !')) 457 458 # - arrange 459 szr_btns_right = wx.BoxSizer(wx.HORIZONTAL) 460 szr_btns_right.Add(self.__BTN_add_unassociated, 0, wx.SHAPED) 461 szr_btns_right.Add(self.__BTN_clear, 0, wx.SHAPED) 462 szr_btns_right.Add(self.__BTN_save, 0, wx.SHAPED) 463 szr_btns_right.Add(self.__BTN_discard, 0, wx.SHAPED) 464 465 szr_right = wx.BoxSizer(wx.VERTICAL) 466 szr_right.Add(self.__soap_notebook, 1, wx.EXPAND) 467 szr_right.Add(szr_btns_right) 468 PNL_soap_editors.SetSizerAndFit(szr_right) 469 470 # arrange widgets 471 self.__splitter.SetMinimumPaneSize(20) 472 self.__splitter.SplitVertically(PNL_list, PNL_soap_editors) 473 474 szr_main = wx.BoxSizer(wx.VERTICAL) 475 szr_main.Add(self.__splitter, 1, wx.EXPAND, 0) 476 self.SetSizerAndFit(szr_main)
477 #--------------------------------------------------------
478 - def __refresh_problem_list(self):
479 """Update health problems list. 480 """ 481 self.__LST_problems.Clear() 482 emr = self.__pat.get_emr() 483 problems = emr.get_problems() 484 for problem in problems: 485 if not problem['problem_active']: 486 continue 487 if problem['type'] == 'issue': 488 issue = emr.problem2issue(problem) 489 last_encounter = emr.get_last_encounter(issue_id = issue['pk_health_issue']) 490 if last_encounter is None: 491 last = issue['modified_when'].strftime('%m/%Y') 492 else: 493 last = last_encounter['last_affirmed'].strftime('%m/%Y') 494 label = u'%s: %s "%s"' % (last, problem['l10n_type'], problem['problem']) 495 elif problem['type'] == 'episode': 496 epi = emr.problem2episode(problem) 497 last_encounter = emr.get_last_encounter(episode_id = epi['pk_episode']) 498 if last_encounter is None: 499 last = epi['episode_modified_when'].strftime('%m/%Y') 500 else: 501 last = last_encounter['last_affirmed'].strftime('%m/%Y') 502 label = u'%s: %s "%s"%s' % ( 503 last, 504 problem['l10n_type'], 505 problem['problem'], 506 gmTools.coalesce(initial = epi['health_issue'], instead = '', template_initial = ' (%s)') 507 ) 508 self.__LST_problems.Append(label, problem) 509 splitter_width = self.__splitter.GetSizeTuple()[0] 510 self.__splitter.SetSashPosition((splitter_width / 2), True) 511 self.Refresh() 512 #self.Update() 513 return True
514 #-------------------------------------------------------- 515 # event handling 516 #--------------------------------------------------------
517 - def __register_interests(self):
518 """Configure enabled event signals 519 """ 520 # wxPython events 521 wx.EVT_LISTBOX_DCLICK(self.__LST_problems, self.__LST_problems.GetId(), self.__on_problem_activated) 522 wx.EVT_BUTTON(self.__BTN_save, self.__BTN_save.GetId(), self.__on_save) 523 wx.EVT_BUTTON(self.__BTN_clear, self.__BTN_clear.GetId(), self.__on_clear) 524 wx.EVT_BUTTON(self.__BTN_discard, self.__BTN_discard.GetId(), self.__on_discard) 525 wx.EVT_BUTTON(self.__BTN_add_unassociated, self.__BTN_add_unassociated.GetId(), self.__on_add_unassociated) 526 527 # client internal signals 528 gmDispatcher.connect(signal='post_patient_selection', receiver=self._on_post_patient_selection) 529 gmDispatcher.connect(signal = 'clin.episode_mod_db', receiver = self._on_episode_issue_mod_db) 530 gmDispatcher.connect(signal = 'clin.health_issue_mod_db', receiver = self._on_episode_issue_mod_db)
531 #--------------------------------------------------------
532 - def _on_post_patient_selection(self):
533 """Patient changed.""" 534 if self.GetParent().GetCurrentPage() == self: 535 self.reset_ui_content()
536 #--------------------------------------------------------
537 - def _on_episode_issue_mod_db(self):
538 if self.GetParent().GetCurrentPage() == self: 539 self.__refresh_problem_list()
540 #--------------------------------------------------------
541 - def __on_clear(self, event):
542 """Clear raised SOAP input widget. 543 """ 544 soap_nb_page = self.__soap_notebook.GetPage(self.__soap_notebook.GetSelection()) 545 soap_nb_page.Clear()
546 #--------------------------------------------------------
547 - def __on_discard(self, event):
548 """Discard raised SOAP input widget. 549 550 Will throw away data ! 551 """ 552 self.__soap_notebook.close_current_editor()
553 #--------------------------------------------------------
554 - def __on_add_unassociated(self, evt):
555 """Add new editor for as-yet unassociated progress note. 556 557 Clinical logic as per discussion with Jim Busser: 558 559 - if patient has no episodes: 560 - new patient 561 - always allow several NEWs 562 - if patient has episodes: 563 - allow several NEWs per configuration 564 """ 565 emr = self.__pat.get_emr() 566 epis = emr.get_episodes() 567 568 if len(epis) == 0: 569 value = True 570 else: 571 dbcfg = gmCfg.cCfgSQL() 572 value = bool(dbcfg.get2 ( 573 option = u'horstspace.soap_editor.allow_same_episode_multiple_times', 574 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace, 575 bias = u'user', 576 default = False 577 )) 578 579 self.__soap_notebook.add_editor(allow_same_problem = value)
580 #--------------------------------------------------------
581 - def __on_problem_activated(self, event):
582 """ 583 When the user changes health issue selection, update selected issue 584 reference and update buttons according its input status. 585 586 when the user selects a problem in the problem list: 587 - check whether selection is issue or episode 588 - if editor for episode exists: focus it 589 - if no editor for episode exists: create one and focus it 590 """ 591 problem_idx = self.__LST_problems.GetSelection() 592 problem = self.__LST_problems.GetClientData(problem_idx) 593 594 if self.__soap_notebook.add_editor(problem = problem): 595 return True 596 597 gmGuiHelpers.gm_show_error ( 598 aMessage = _( 599 'Cannot open progress note editor for\n\n' 600 '[%s].\n\n' 601 ) % problem['problem'], 602 aTitle = _('opening progress note editor') 603 ) 604 return False
605 #--------------------------------------------------------
606 - def __on_save(self, event):
607 """Save data to backend and close editor. 608 """ 609 page_idx = self.__soap_notebook.GetSelection() 610 soap_nb_page = self.__soap_notebook.GetPage(page_idx) 611 if not soap_nb_page.save(): 612 gmDispatcher.send(signal='statustext', msg=_('Problem saving progress note: duplicate information ?')) 613 return False 614 self.__soap_notebook.DeletePage(page_idx) 615 # always keep one unassociated editor open 616 self.__soap_notebook.add_editor() 617 #self.__refresh_problem_list() 618 return True
619 #-------------------------------------------------------- 620 # notebook plugin API 621 #--------------------------------------------------------
622 - def repopulate_ui(self):
623 self.__refresh_problem_list()
624 #============================================================
625 -class cSOAPLineDef:
626 - def __init__(self):
627 self.label = _('label missing') 628 self.text = '' 629 self.soap_cat = _('soap cat missing') 630 self.is_rfe = False # later support via types 631 self.data = None
632 #============================================================ 633 # FIXME: this should be a more generic(ally named) class 634 # FIXME: living elsewhere
635 -class cPopupDataHolder:
636 637 _data_savers = {} 638
639 - def __init__(self):
640 self.__data = {}
641 #--------------------------------------------------------
642 - def store_data(self, popup_type=None, desc=None, data=None, old_desc=None):
643 # FIXME: do fancy validations 644 645 print "storing popup data:", desc 646 print "type", popup_type 647 print "data", data 648 649 # verify structure 650 try: 651 self.__data[popup_type] 652 except KeyError: 653 self.__data[popup_type] = {} 654 # store new data 655 self.__data[popup_type][desc] = { 656 'data': data 657 } 658 # remove old data if necessary 659 try: 660 del self.__data[popup_type][old_desc] 661 except: 662 pass 663 return True
664 #--------------------------------------------------------
665 - def save(self):
666 for popup_type in self.__data.keys(): 667 try: 668 saver_func = self.__data_savers[popup_type] 669 except KeyError: 670 _log.exception('no saver for popup data type [%s] configured', popup_type) 671 return False 672 for desc in self.__data[popup_type].keys(): 673 data = self.__data[popup_type][desc]['data'] 674 saver_func(data) 675 return True
676 #--------------------------------------------------------
677 - def clear(self):
678 self.__data = {}
679 #-------------------------------------------------------- 680 # def remove_data(self, popup_type=None, desc=None): 681 # del self.__data[popup_type][desc] 682 #-------------------------------------------------------- 683 # def get_descs(self, popup_type=None, origination_soap=None): 684 # def get_data(self, desc=None): 685 # def rename_data(self, old_desc=None, new_desc=None): 686 #============================================================
687 -class cResizingSoapWin(gmResizingWidgets.cResizingWindow):
688
689 - def __init__(self, parent, size, input_defs=None, problem=None):
690 """Resizing SOAP note input editor. 691 692 This is a wrapper around a few resizing STCs (the 693 labels and categories are settable) which are 694 customized to accept progress note input. It provides 695 the unified resizing behaviour. 696 697 Knows how to save it's data into the backend. 698 699 @param input_defs: note's labels and categories 700 @type input_defs: list of cSOAPLineDef instances 701 """ 702 if input_defs is None or len(input_defs) == 0: 703 raise gmExceptions.ConstructorError, 'cannot generate note with field defs [%s]' % input_defs 704 705 # FIXME: *actually* this should be a session-local 706 # FIXME: holding store at the cClinicalRecord level 707 self.__embedded_data_holder = cPopupDataHolder() 708 709 self.__input_defs = input_defs 710 711 gmResizingWidgets.cResizingWindow.__init__(self, parent, id=-1, size=size) 712 713 self.__problem = problem 714 if isinstance(problem, gmEMRStructItems.cEpisode): 715 self.__problem = emr.episode2problem(episode = problem) 716 elif isinstance(problem, gmEMRStructItems.cHealthIssue): 717 self.__problem = emr.health_issue2problem(issue = problem) 718 self.__pat = gmPerson.gmCurrentPatient()
719 #-------------------------------------------------------- 720 # cResizingWindow API 721 #--------------------------------------------------------
722 - def DoLayout(self):
723 """Visually display input note according to user defined labels. 724 """ 725 # configure keywords 726 for soap_cat in progress_note_keywords.keys(): 727 category = progress_note_keywords[soap_cat] 728 for kwd in category.keys(): 729 category[kwd]['widget_data_sink'] = self.__embedded_data_holder.store_data 730 input_fields = [] 731 # add fields to edit widget 732 # note: this may produce identically labelled lines 733 for line_def in self.__input_defs: 734 input_field = gmResizingWidgets.cResizingSTC(self, -1, data = line_def) 735 input_field.SetText(line_def.text) 736 kwds = progress_note_keywords[line_def.soap_cat] 737 input_field.set_keywords(popup_keywords=kwds) 738 # FIXME: pending matcher setup 739 self.AddWidget(widget=input_field, label=line_def.label) 740 self.Newline() 741 input_fields.append(input_field) 742 # setup tab navigation between input fields 743 for field_idx in range(len(input_fields)): 744 # previous 745 try: 746 input_fields[field_idx].prev_in_tab_order = input_fields[field_idx-1] 747 except IndexError: 748 input_fields[field_idx].prev_in_tab_order = None 749 # next 750 try: 751 input_fields[field_idx].next_in_tab_order = input_fields[field_idx+1] 752 except IndexError: 753 input_fields[field_idx].next_in_tab_order = None
754 #-------------------------------------------------------- 755 # public API 756 #--------------------------------------------------------
757 - def save(self):
758 """Save data into backend.""" 759 760 # fill progress_note for import 761 progress_note = [] 762 aoe = u'' 763 rfe = u'' 764 has_rfe = False 765 soap_lines_contents = self.GetValue() 766 for line_content in soap_lines_contents.values(): 767 if line_content.text.strip() == u'': 768 continue 769 progress_note.append ({ 770 gmSOAPimporter.soap_bundle_SOAP_CAT_KEY: line_content.data.soap_cat, 771 gmSOAPimporter.soap_bundle_TYPES_KEY: [], # these types need to come from the editor 772 gmSOAPimporter.soap_bundle_TEXT_KEY: line_content.text.rstrip() 773 }) 774 if line_content.data.is_rfe: 775 has_rfe = True 776 rfe += line_content.text.rstrip() 777 if line_content.data.soap_cat == u'a': 778 aoe += line_content.text.rstrip() 779 780 emr = self.__pat.get_emr() 781 782 # - new episode, must get name from narrative (or user) 783 if (self.__problem is None) or (self.__problem['type'] == 'issue'): 784 # work out episode name 785 epi_name = u'' 786 if len(aoe) != 0: 787 epi_name = aoe 788 else: 789 epi_name = rfe 790 791 dlg = wx.TextEntryDialog ( 792 parent = self, 793 message = _('Enter a descriptive name for this new problem:'), 794 caption = _('Creating a problem (episode) to save the notelet under ...'), 795 defaultValue = epi_name.replace('\r', '//').replace('\n', '//'), 796 style = wx.OK | wx.CANCEL | wx.CENTRE 797 ) 798 decision = dlg.ShowModal() 799 if decision != wx.ID_OK: 800 return False 801 802 epi_name = dlg.GetValue().strip() 803 if epi_name == u'': 804 gmGuiHelpers.gm_show_error(_('Cannot save a new problem without a name.'), _('saving progress note')) 805 return False 806 807 # new unassociated episode 808 new_episode = emr.add_episode(episode_name = epi_name[:45], pk_health_issue = None, is_open = True) 809 810 if self.__problem is not None: 811 issue = emr.problem2issue(self.__problem) 812 if not gmEMRStructWidgets.move_episode_to_issue(episode = new_episode, target_issue = issue, save_to_backend = True): 813 print "error moving episode to issue" 814 815 epi_id = new_episode['pk_episode'] 816 else: 817 epi_id = self.__problem['pk_episode'] 818 819 # set up clinical context in progress note 820 encounter = emr.active_encounter 821 staff_id = gmStaff.gmCurrentProvider()['pk_staff'] 822 clin_ctx = { 823 gmSOAPimporter.soap_bundle_EPISODE_ID_KEY: epi_id, 824 gmSOAPimporter.soap_bundle_ENCOUNTER_ID_KEY: encounter['pk_encounter'], 825 gmSOAPimporter.soap_bundle_STAFF_ID_KEY: staff_id 826 } 827 for line in progress_note: 828 line[gmSOAPimporter.soap_bundle_CLIN_CTX_KEY] = clin_ctx 829 830 # dump progress note to backend 831 importer = gmSOAPimporter.cSOAPImporter() 832 if not importer.import_soap(progress_note): 833 gmGuiHelpers.gm_show_error(_('Error saving progress note.'), _('saving progress note')) 834 return False 835 836 # dump embedded data to backend 837 if not self.__embedded_data_holder.save(): 838 gmGuiHelpers.gm_show_error ( 839 _('Error saving embedded data.'), 840 _('saving progress note') 841 ) 842 return False 843 self.__embedded_data_holder.clear() 844 845 return True
846 #--------------------------------------------------------
847 - def get_problem(self):
848 return self.__problem
849 #--------------------------------------------------------
850 - def is_empty(self):
851 editor_content = self.GetValue() 852 853 for field_content in editor_content.values(): 854 if field_content.text.strip() != u'': 855 return False 856 857 return True
858 #============================================================
859 -class cResizingSoapPanel(wx.Panel):
860 """Basic progress note panel. 861 862 It provides a gmResizingWindow based progress note editor 863 with a header line. The header either displays the episode 864 this progress note is associated with or it allows for 865 entering an episode name. The episode name either names 866 an existing episode or is the name for a new episode. 867 868 This panel knows how to save it's data into the backend. 869 870 Can work as: 871 a) Progress note creation: displays an empty set of soap entries to 872 create a new soap note for the given episode (or unassociated) 873 """ 874 #--------------------------------------------------------
875 - def __init__(self, parent, problem=None, input_defs=None):
876 """ 877 Construct a new SOAP input widget. 878 879 @param parent: the parent widget 880 881 @param episode: the episode to create the SOAP editor for. 882 @type episode gmEMRStructItems.cEpisode instance or None (to create an 883 unassociated progress note). A gmEMRStructItems.cProblem instance is 884 also allowed to be passed, as the widget will obtain the related cEpisode. 885 886 @param input_defs: the display and associated data for each displayed narrative 887 @type input_defs: a list of cSOAPLineDef instances 888 """ 889 if not isinstance(problem, (gmEMRStructItems.cHealthIssue, gmEMRStructItems.cEpisode, gmEMRStructItems.cProblem, types.NoneType)): 890 raise gmExceptions.ConstructorError, 'problem [%s] is of type %s, must be issue, episode, problem or None' % (str(problem), type(problem)) 891 892 self.__is_saved = False 893 # do layout 894 wx.Panel.__init__(self, parent, -1, style = wx.NO_BORDER | wx.TAB_TRAVERSAL) 895 # - editor 896 if input_defs is None: 897 soap_lines = [] 898 # make Richard the default ;-) 899 # FIXME: actually, should be read from backend 900 line = cSOAPLineDef() 901 line.label = _('Visit Purpose') 902 line.soap_cat = 's' 903 line.is_rfe = True 904 soap_lines.append(line) 905 906 line = cSOAPLineDef() 907 line.label = _('History Taken') 908 line.soap_cat = 's' 909 soap_lines.append(line) 910 911 line = cSOAPLineDef() 912 line.label = _('Findings') 913 line.soap_cat = 'o' 914 soap_lines.append(line) 915 916 line = cSOAPLineDef() 917 line.label = _('Assessment') 918 line.soap_cat = 'a' 919 soap_lines.append(line) 920 921 line = cSOAPLineDef() 922 line.label = _('Plan') 923 line.soap_cat = 'p' 924 soap_lines.append(line) 925 else: 926 soap_lines = input_defs 927 self.__soap_editor = cResizingSoapWin ( 928 self, 929 size = wx.DefaultSize, 930 input_defs = soap_lines, 931 problem = problem 932 ) 933 # - arrange 934 self.__szr_main = wx.BoxSizer(wx.VERTICAL) 935 self.__szr_main.Add(self.__soap_editor, 1, wx.EXPAND) 936 self.SetSizerAndFit(self.__szr_main)
937 #-------------------------------------------------------- 938 # public API 939 #--------------------------------------------------------
940 - def get_problem(self):
941 """Retrieve the related problem for this SOAP input widget. 942 """ 943 return self.__soap_editor.get_problem()
944 #--------------------------------------------------------
945 - def is_unassociated_editor(self):
946 """ 947 Retrieves whether the current editor is not associated 948 with any episode. 949 """ 950 return ((self.__problem is None) or (self.__problem['type'] == 'issue'))
951 #--------------------------------------------------------
952 - def get_editor(self):
953 """Retrieves widget's SOAP text editor. 954 """ 955 return self.__soap_editor
956 #--------------------------------------------------------
957 - def Clear(self):
958 """Clear any entries in widget's SOAP text editor 959 """ 960 self.__soap_editor.Clear()
961 #--------------------------------------------------------
962 - def SetSaved(self, is_saved):
963 """ 964 Set SOAP input widget saved (dumped to backend) state 965 966 @param is_saved: Flag indicating wether the SOAP has been dumped to 967 persistent backend 968 @type is_saved: boolean 969 """ 970 self.__is_saved = is_saved 971 self.Clear()
972 #--------------------------------------------------------
973 - def IsSaved(self):
974 """ 975 Check SOAP input widget saved (dumped to backend) state 976 """ 977 return self.__is_saved
978 #--------------------------------------------------------
979 - def save(self):
980 return self.__soap_editor.save()
981 #--------------------------------------------------------
982 - def editor_empty(self):
983 return self.__soap_editor.is_empty()
984 #============================================================
985 -class cSingleBoxSOAP(wx.TextCtrl):
986 """if we separate it out like this it can transparently gain features"""
987 - def __init__(self, *args, **kwargs):
988 wx.TextCtrl.__init__(self, *args, **kwargs)
989 #============================================================
990 -class cSingleBoxSOAPPanel(wx.Panel):
991 """Single Box free text SOAP input. 992 993 This widget was suggested by David Guest on the mailing 994 list. All it does is provide a single multi-line textbox 995 for typing free-text clinical notes which are stored as 996 Subjective. 997 """
998 - def __init__(self, *args, **kwargs):
999 wx.Panel.__init__(self, *args, **kwargs) 1000 self.__do_layout() 1001 self.__pat = gmPerson.gmCurrentPatient() 1002 if not self.__register_events(): 1003 raise gmExceptions.ConstructorError, 'cannot register interests'
1004 #--------------------------------------------------------
1005 - def __do_layout(self):
1006 # large box for free-text clinical notes 1007 self.__soap_box = cSingleBoxSOAP ( 1008 self, 1009 -1, 1010 '', 1011 style = wx.TE_MULTILINE 1012 ) 1013 # buttons below that 1014 self.__BTN_save = wx.Button(self, wx.NewId(), _("save")) 1015 self.__BTN_save.SetToolTipString(_('save clinical note in EMR')) 1016 self.__BTN_discard = wx.Button(self, wx.NewId(), _("discard")) 1017 self.__BTN_discard.SetToolTipString(_('discard clinical note')) 1018 szr_btns = wx.BoxSizer(wx.HORIZONTAL) 1019 szr_btns.Add(self.__BTN_save, 1, wx.ALIGN_CENTER_HORIZONTAL, 0) 1020 szr_btns.Add(self.__BTN_discard, 1, wx.ALIGN_CENTER_HORIZONTAL, 0) 1021 # arrange widgets 1022 szr_outer = wx.StaticBoxSizer(wx.StaticBox(self, -1, _("clinical progress note")), wx.VERTICAL) 1023 szr_outer.Add(self.__soap_box, 1, wx.EXPAND, 0) 1024 szr_outer.Add(szr_btns, 0, wx.EXPAND, 0) 1025 # and do layout 1026 self.SetAutoLayout(1) 1027 self.SetSizer(szr_outer) 1028 szr_outer.Fit(self) 1029 szr_outer.SetSizeHints(self) 1030 self.Layout()
1031 #--------------------------------------------------------
1032 - def __register_events(self):
1033 # wxPython events 1034 wx.EVT_BUTTON(self.__BTN_save, self.__BTN_save.GetId(), self._on_save_note) 1035 wx.EVT_BUTTON(self.__BTN_discard, self.__BTN_discard.GetId(), self._on_discard_note) 1036 1037 # client internal signals 1038 gmDispatcher.connect(signal = 'pre_patient_selection', receiver = self._save_note) 1039 gmDispatcher.connect(signal = 'application_closing', receiver = self._save_note) 1040 1041 return True
1042 #-------------------------------------------------------- 1043 # event handlers 1044 #--------------------------------------------------------
1045 - def _on_save_note(self, event):
1046 self.__save_note()
1047 #event.Skip() 1048 #--------------------------------------------------------
1049 - def _on_discard_note(self, event):
1050 # FIXME: maybe ask for confirmation ? 1051 self.__soap_box.SetValue('')
1052 #event.Skip() 1053 #-------------------------------------------------------- 1054 # internal helpers 1055 #--------------------------------------------------------
1056 - def _save_note(self):
1057 wx.CallAfter(self.__save_note)
1058 #--------------------------------------------------------
1059 - def __save_note(self):
1060 # sanity checks 1061 if self.__pat is None: 1062 return True 1063 if not self.__pat.connected: 1064 return True 1065 if not self.__soap_box.IsModified(): 1066 return True 1067 note = self.__soap_box.GetValue() 1068 if note.strip() == '': 1069 return True 1070 # now save note 1071 emr = self.__pat.get_emr() 1072 if emr is None: 1073 _log.error('cannot access clinical record of patient') 1074 return False 1075 if not emr.add_clin_narrative(note, soap_cat='s'): 1076 _log.error('error saving clinical note') 1077 return False 1078 self.__soap_box.SetValue('') 1079 return True
1080 #============================================================ 1081 # main 1082 #------------------------------------------------------------ 1083 if __name__ == "__main__": 1084 1085 import sys 1086 1087 from Gnumed.pycommon import gmPG2 1088 #--------------------------------------------------------
1089 - def get_narrative(pk_encounter=None, pk_health_issue = None, default_labels=None):
1090 """ 1091 Retrieve the soap editor input lines definitions built from 1092 all the narratives for the given issue along a specific 1093 encounter. 1094 1095 @param pk_health_issue The id of the health issue to obtain the narratives for. 1096 @param pk_health_issue An integer instance 1097 1098 @param pk_encounter The id of the encounter to obtain the narratives for. 1099 @type A gmEMRStructItems.cEncounter instance. 1100 1101 @param default_labels: The user customized labels for each 1102 soap category. 1103 @type default_labels: A dictionary instance which keys are 1104 soap categories. 1105 """ 1106 # custom labels 1107 if default_labels is None: 1108 default_labels = { 1109 's': _('History Taken'), 1110 'o': _('Findings'), 1111 'a': _('Assessment'), 1112 'p': _('Plan') 1113 } 1114 1115 pat = gmPerson.gmCurrentPatient() 1116 emr = pat.get_emr() 1117 soap_lines = [] 1118 # for each soap cat 1119 for soap_cat in gmSOAPimporter.soap_bundle_SOAP_CATS: 1120 # retrieve narrative for given encounter 1121 narr_items = emr.get_clin_narrative ( 1122 encounters = [pk_encounter], 1123 issues = [pk_health_issue], 1124 soap_cats = [soap_cat] 1125 ) 1126 for narrative in narr_items: 1127 try: 1128 # FIXME: add more data such as doctor sig 1129 label_txt = default_labels[narrative['soap_cat']] 1130 except: 1131 label_txt = narrative['soap_cat'] 1132 line = cSOAPLineDef() 1133 line.label = label_txt 1134 line.text = narrative['narrative'] 1135 # line.data['narrative instance'] = narrative 1136 soap_lines.append(line) 1137 return soap_lines
1138 #--------------------------------------------------------
1139 - def create_widget_on_test_kwd1(*args, **kwargs):
1140 print "test keyword must have been typed..." 1141 print "actually this would have to return a suitable wx.Window subclass instance" 1142 print "args:", args 1143 print "kwd args:" 1144 for key in kwargs.keys(): 1145 print key, "->", kwargs[key]
1146 #--------------------------------------------------------
1147 - def create_widget_on_test_kwd2(*args, **kwargs):
1148 msg = ( 1149 "test keyword must have been typed...\n" 1150 "actually this would have to return a suitable wx.Window subclass instance\n" 1151 ) 1152 for arg in args: 1153 msg = msg + "\narg ==> %s" % arg 1154 for key in kwargs.keys(): 1155 msg = msg + "\n%s ==> %s" % (key, kwargs[key]) 1156 gmGuiHelpers.gm_show_info ( 1157 aMessage = msg, 1158 aTitle = 'msg box on create_widget from test_keyword' 1159 )
1160 #--------------------------------------------------------
1161 - def test_soap_notebook():
1162 print 'testing notebooked soap input...' 1163 application = wx.PyWidgetTester(size=(800,500)) 1164 soap_input = cProgressNoteInputNotebook(application.frame, -1) 1165 application.frame.Show(True) 1166 application.MainLoop()
1167 #--------------------------------------------------------
1168 - def test_soap_notebook_panel():
1169 print 'testing notebooked soap panel...' 1170 application = wx.PyWidgetTester(size=(800,500)) 1171 soap_input = cNotebookedProgressNoteInputPanel(application.frame, -1) 1172 application.frame.Show(True) 1173 application.MainLoop()
1174 #-------------------------------------------------------- 1175 1176 try: 1177 # obtain patient 1178 patient = gmPersonSearch.ask_for_patient() 1179 if patient is None: 1180 print "No patient. Exiting gracefully..." 1181 sys.exit(0) 1182 gmPatSearchWidgets.set_active_patient(patient=patient) 1183 1184 #test_soap_notebook() 1185 test_soap_notebook_panel() 1186 1187 # # multisash soap 1188 # print 'testing multisashed soap input...' 1189 # application = wx.PyWidgetTester(size=(800,500)) 1190 # soap_input = cMultiSashedProgressNoteInputPanel(application.frame, -1) 1191 # application.frame.Show(True) 1192 # application.MainLoop() 1193 1194 # # soap widget displaying all narratives for an issue along an encounter 1195 # print 'testing soap editor for encounter narratives...' 1196 # episode = gmEMRStructItems.cEpisode(aPK_obj=1) 1197 # encounter = gmEMRStructItems.cEncounter(aPK_obj=1) 1198 # narrative = get_narrative(pk_encounter = encounter['pk_encounter'], pk_health_issue = episode['pk_health_issue']) 1199 # default_labels = {'s':'Subjective', 'o':'Objective', 'a':'Assesment', 'p':'Plan'} 1200 # app = wx.PyWidgetTester(size=(300,500)) 1201 # app.SetWidget(cResizingSoapPanel, episode, narrative) 1202 # app.MainLoop() 1203 # del app 1204 1205 # # soap progress note for episode 1206 # print 'testing soap editor for episode...' 1207 # app = wx.PyWidgetTester(size=(300,300)) 1208 # app.SetWidget(cResizingSoapPanel, episode) 1209 # app.MainLoop() 1210 # del app 1211 1212 # # soap progress note for problem 1213 # print 'testing soap editor for problem...' 1214 # problem = gmEMRStructItems.cProblem(aPK_obj={'pk_patient': 12, 'pk_health_issue': 1, 'pk_episode': 1}) 1215 # app = wx.PyWidgetTester(size=(300,300)) 1216 # app.SetWidget(cResizingSoapPanel, problem) 1217 # app.MainLoop() 1218 # del app 1219 1220 # # unassociated soap progress note 1221 # print 'testing unassociated soap editor...' 1222 # app = wx.PyWidgetTester(size=(300,300)) 1223 # app.SetWidget(cResizingSoapPanel, None) 1224 # app.MainLoop() 1225 # del app 1226 1227 # # unstructured progress note 1228 # print 'testing unstructured progress note...' 1229 # app = wx.PyWidgetTester(size=(600,600)) 1230 # app.SetWidget(cSingleBoxSOAPPanel, -1) 1231 # app.MainLoop() 1232 1233 except StandardError: 1234 _log.exception("unhandled exception caught !") 1235 # but re-raise them 1236 raise 1237 1238 #============================================================ 1239