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, gmSurgery, 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 = 'episode_mod_db', receiver = self._on_episode_issue_mod_db)
     530                  gmDispatcher.connect(signal = '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 = gmSurgery.gmCurrentPractice().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