Gnumed.wxpython.gmNarrativeWidgets

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

# Source Code for Module Gnumed.wxpython.gmNarrativeWidgets

       1  """GNUmed narrative handling widgets."""
       2  #================================================================
       3  __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>"
       4  __license__ = "GPL v2 or later (details at http://www.gnu.org)"
       5
       6  import sys
       7  import logging
       8  import os
       9  import os.path
      10  import time
      11  import re as regex
      12  import shutil
      13
      14
      15  import wx
      16  import wx.lib.expando as wx_expando
      17  import wx.lib.agw.supertooltip as agw_stt
      18  import wx.lib.statbmp as wx_genstatbmp
      19
      20
      21  if __name__ == '__main__':
      22          sys.path.insert(0, '../../')
      23
      24  from Gnumed.pycommon import gmI18N
      25
      26  if __name__ == '__main__':
      27          gmI18N.activate_locale()
      28          gmI18N.install_domain()
      29
      30  from Gnumed.pycommon import gmDispatcher
      31  from Gnumed.pycommon import gmTools
      32  from Gnumed.pycommon import gmDateTime
      33  from Gnumed.pycommon import gmShellAPI
      34  from Gnumed.pycommon import gmPG2
      35  from Gnumed.pycommon import gmCfg
      36  from Gnumed.pycommon import gmMatchProvider
      37
      38  from Gnumed.business import gmPerson
      39  from Gnumed.business import gmStaff
      40  from Gnumed.business import gmEMRStructItems
      41  from Gnumed.business import gmClinNarrative
      42  from Gnumed.business import gmPraxis
      43  from Gnumed.business import gmForms
      44  from Gnumed.business import gmDocuments
      45  from Gnumed.business import gmPersonSearch
      46  from Gnumed.business import gmKeywordExpansion
      47
      48  from Gnumed.wxpython import gmListWidgets
      49  from Gnumed.wxpython import gmEMRStructWidgets
      50  from Gnumed.wxpython import gmEncounterWidgets
      51  from Gnumed.wxpython import gmRegetMixin
      52  from Gnumed.wxpython import gmPhraseWheel
      53  from Gnumed.wxpython import gmGuiHelpers
      54  from Gnumed.wxpython import gmCfgWidgets
      55  from Gnumed.wxpython import gmDocumentWidgets
      56  from Gnumed.wxpython import gmKeywordExpansionWidgets
      57  from Gnumed.wxpython.gmPatSearchWidgets import set_active_patient
      58
      59  from Gnumed.exporters import gmPatientExporter
      60
      61
      62  _log = logging.getLogger('gm.ui')
      63  #============================================================
      64  # narrative related widgets/functions
      65  #------------------------------------------------------------
      66 -def move_progress_notes_to_another_encounter(parent=None, encounters=None, episodes=None, patient=None, move_all=False):
      67
      68          # sanity checks
      69          if patient is None:
      70                  patient = gmPerson.gmCurrentPatient()
      71
      72          if not patient.connected:
      73                  gmDispatcher.send(signal = 'statustext', msg = _('Cannot move progress notes. No active patient.'))
      74                  return False
      75
      76          if parent is None:
      77                  parent = wx.GetApp().GetTopWindow()
      78
      79          emr = patient.get_emr()
      80
      81          if encounters is None:
      82                  encs = emr.get_encounters(episodes = episodes)
      83                  encounters = gmEncounterWidgets.select_encounters (
      84                          parent = parent,
      85                          patient = patient,
      86                          single_selection = False,
      87                          encounters = encs
      88                  )
      89                  # cancelled
      90                  if encounters is None:
      91                          return True
      92                  # none selected
      93                  if len(encounters) == 0:
      94                          return True
      95
      96          notes = emr.get_clin_narrative (
      97                  encounters = encounters,
      98                  episodes = episodes
      99          )
     100
     101          # which narrative
     102          if move_all:
     103                  selected_narr = notes
     104          else:
     105                  selected_narr = gmListWidgets.get_choices_from_list (
     106                          parent = parent,
     107                          caption = _('Moving progress notes between encounters ...'),
     108                          single_selection = False,
     109                          can_return_empty = True,
     110                          data = notes,
     111                          msg = _('\n Select the progress notes to move from the list !\n\n'),
     112                          columns = [_('when'), _('who'), _('type'), _('entry')],
     113                          choices = [
     114                                  [       narr['date'].strftime('%x %H:%M'),
     115                                          narr['modified_by'],
     116                                          gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
     117                                          narr['narrative'].replace('\n', '/').replace('\r', '/')
     118                                  ] for narr in notes
     119                          ]
     120                  )
     121
     122          if not selected_narr:
     123                  return True
     124
     125          # which encounter to move to
     126          enc2move2 = gmEncounterWidgets.select_encounters (
     127                  parent = parent,
     128                  patient = patient,
     129                  single_selection = True
     130          )
     131
     132          if not enc2move2:
     133                  return True
     134
     135          for narr in selected_narr:
     136                  narr['pk_encounter'] = enc2move2['pk_encounter']
     137                  narr.save()
     138
     139          return True
     140  #------------------------------------------------------------
     141 -def manage_progress_notes(parent=None, encounters=None, episodes=None, patient=None):
     142
     143          # sanity checks
     144          if patient is None:
     145                  patient = gmPerson.gmCurrentPatient()
     146
     147          if not patient.connected:
     148                  gmDispatcher.send(signal = 'statustext', msg = _('Cannot edit progress notes. No active patient.'))
     149                  return False
     150
     151          if parent is None:
     152                  parent = wx.GetApp().GetTopWindow()
     153
     154          emr = patient.get_emr()
     155          #--------------------------
     156          def delete(item):
     157                  if item is None:
     158                          return False
     159                  dlg = gmGuiHelpers.c2ButtonQuestionDlg (
     160                          parent,
     161                          -1,
     162                          caption = _('Deleting progress note'),
     163                          question = _(
     164                                  'Are you positively sure you want to delete this\n'
     165                                  'progress note from the medical record ?\n'
     166                                  '\n'
     167                                  'Note that even if you chose to delete the entry it will\n'
     168                                  'still be (invisibly) kept in the audit trail to protect\n'
     169                                  'you from litigation because physical deletion is known\n'
     170                                  'to be unlawful in some jurisdictions.\n'
     171                          ),
     172                          button_defs = (
     173                                  {'label': _('Delete'), 'tooltip': _('Yes, delete the progress note.'), 'default': False},
     174                                  {'label': _('Cancel'), 'tooltip': _('No, do NOT delete the progress note.'), 'default': True}
     175                          )
     176                  )
     177                  decision = dlg.ShowModal()
     178
     179                  if decision != wx.ID_YES:
     180                          return False
     181
     182                  gmClinNarrative.delete_clin_narrative(narrative = item['pk_narrative'])
     183                  return True
     184          #--------------------------
     185          def edit(item):
     186                  if item is None:
     187                          return False
     188
     189                  dlg = gmGuiHelpers.cMultilineTextEntryDlg (
     190                          parent,
     191                          -1,
     192                          title = _('Editing progress note'),
     193                          msg = _('This is the original progress note:'),
     194                          data = item.format(left_margin = u' ', fancy = True),
     195                          text = item['narrative']
     196                  )
     197                  decision = dlg.ShowModal()
     198
     199                  if decision != wx.ID_SAVE:
     200                          return False
     201
     202                  val = dlg.value
     203                  dlg.Destroy()
     204                  if val.strip() == u'':
     205                          return False
     206
     207                  item['narrative'] = val
     208                  item.save_payload()
     209
     210                  return True
     211          #--------------------------
     212          def refresh(lctrl):
     213                  notes = emr.get_clin_narrative (
     214                          encounters = encounters,
     215                          episodes = episodes,
     216                          providers = [ gmStaff.gmCurrentProvider()['short_alias'] ]
     217                  )
     218                  lctrl.set_string_items(items = [
     219                          [       narr['date'].strftime('%x %H:%M'),
     220                                  gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
     221                                  narr['narrative'].replace('\n', '/').replace('\r', '/')
     222                          ] for narr in notes
     223                  ])
     224                  lctrl.set_data(data = notes)
     225          #--------------------------
     226
     227          gmListWidgets.get_choices_from_list (
     228                  parent = parent,
     229                  caption = _('Managing progress notes'),
     230                  msg = _(
     231                          '\n'
     232                          ' This list shows the progress notes by %s.\n'
     233                          '\n'
     234                  ) % gmStaff.gmCurrentProvider()['short_alias'],
     235                  columns = [_('when'), _('type'), _('entry')],
     236                  single_selection = True,
     237                  can_return_empty = False,
     238                  edit_callback = edit,
     239                  delete_callback = delete,
     240                  refresh_callback = refresh
     241          )
     242  #------------------------------------------------------------
     243 -def search_narrative_across_emrs(parent=None):
     244
     245          if parent is None:
     246                  parent = wx.GetApp().GetTopWindow()
     247
     248          search_term_dlg = wx.TextEntryDialog (
     249                  parent = parent,
     250                  message = _('Enter (regex) term to search for across all EMRs:'),
     251                  caption = _('Text search across all EMRs'),
     252                  style = wx.OK | wx.CANCEL | wx.CENTRE
     253          )
     254          result = search_term_dlg.ShowModal()
     255
     256          if result != wx.ID_OK:
     257                  return
     258
     259          wx.BeginBusyCursor()
     260          search_term = search_term_dlg.GetValue()
     261          search_term_dlg.Destroy()
     262          results = gmClinNarrative.search_text_across_emrs(search_term = search_term)
     263          wx.EndBusyCursor()
     264
     265          if len(results) == 0:
     266                  gmGuiHelpers.gm_show_info (
     267                          _(
     268                          'Nothing found for search term:\n'
     269                          ' "%s"'
     270                          ) % search_term,
     271                          _('Search results')
     272                  )
     273                  return
     274
     275          items = [ [
     276                  gmPerson.cIdentity(aPK_obj = r['pk_patient'])['description_gender'],
     277                  r['narrative'],
     278                  r['src_table']
     279          ] for r in results ]
     280
     281          selected_patient = gmListWidgets.get_choices_from_list (
     282                  parent = parent,
     283                  caption = _('Search results for [%s]') % search_term,
     284                  choices = items,
     285                  columns = [_('Patient'), _('Match'), _('Match location')],
     286                  data = [ r['pk_patient'] for r in results ],
     287                  single_selection = True,
     288                  can_return_empty = False
     289          )
     290
     291          if selected_patient is None:
     292                  return
     293
     294          wx.CallAfter(set_active_patient, patient = gmPerson.cIdentity(aPK_obj = selected_patient))
     295  #------------------------------------------------------------
     296 -def search_narrative_in_emr(parent=None, patient=None):
     297
     298          # sanity checks
     299          if patient is None:
     300                  patient = gmPerson.gmCurrentPatient()
     301
     302          if not patient.connected:
     303                  gmDispatcher.send(signal = 'statustext', msg = _('Cannot search EMR. No active patient.'))
     304                  return False
     305
     306          if parent is None:
     307                  parent = wx.GetApp().GetTopWindow()
     308
     309          search_term_dlg = wx.TextEntryDialog (
     310                  parent = parent,
     311                  message = _('Enter search term:'),
     312                  caption = _('Text search of entire EMR of active patient'),
     313                  style = wx.OK | wx.CANCEL | wx.CENTRE
     314          )
     315          result = search_term_dlg.ShowModal()
     316
     317          if result != wx.ID_OK:
     318                  search_term_dlg.Destroy()
     319                  return False
     320
     321          wx.BeginBusyCursor()
     322          val = search_term_dlg.GetValue()
     323          search_term_dlg.Destroy()
     324          emr = patient.get_emr()
     325          rows = emr.search_narrative_simple(val)
     326          wx.EndBusyCursor()
     327
     328          if len(rows) == 0:
     329                  gmGuiHelpers.gm_show_info (
     330                          _(
     331                          'Nothing found for search term:\n'
     332                          ' "%s"'
     333                          ) % val,
     334                          _('Search results')
     335                  )
     336                  return True
     337
     338          txt = u''
     339          for row in rows:
     340                  txt += u'%s: %s\n' % (
     341                          row['soap_cat'],
     342                          row['narrative']
     343                  )
     344
     345                  txt += u' %s: %s - %s %s\n' % (
     346                          _('Encounter'),
     347                          row['encounter_started'].strftime('%x %H:%M'),
     348                          row['encounter_ended'].strftime('%H:%M'),
     349                          row['encounter_type']
     350                  )
     351                  txt += u' %s: %s\n' % (
     352                          _('Episode'),
     353                          row['episode']
     354                  )
     355                  txt += u' %s: %s\n\n' % (
     356                          _('Health issue'),
     357                          row['health_issue']
     358                  )
     359
     360          msg = _(
     361                  'Search term was: "%s"\n'
     362                  '\n'
     363                  'Search results:\n\n'
     364                  '%s\n'
     365          ) % (val, txt)
     366
     367          dlg = wx.MessageDialog (
     368                  parent = parent,
     369                  message = msg,
     370                  caption = _('Search results for [%s]') % val,
     371                  style = wx.OK | wx.STAY_ON_TOP
     372          )
     373          dlg.ShowModal()
     374          dlg.Destroy()
     375
     376          return True
     377  #------------------------------------------------------------
     378 -def export_narrative_for_medistar_import(parent=None, soap_cats=u'soapu', encounter=None):
     379
     380          # sanity checks
     381          pat = gmPerson.gmCurrentPatient()
     382          if not pat.connected:
     383                  gmDispatcher.send(signal = 'statustext', msg = _('Cannot export EMR for Medistar. No active patient.'))
     384                  return False
     385
     386          if encounter is None:
     387                  encounter = pat.get_emr().active_encounter
     388
     389          if parent is None:
     390                  parent = wx.GetApp().GetTopWindow()
     391
     392          # get file name
     393          aWildcard = "%s (*.txt)|*.txt|%s (*)|*" % (_("text files"), _("all files"))
     394                  # FIXME: make configurable
     395          aDefDir = os.path.abspath(os.path.expanduser(os.path.join('~', 'gnumed')))
     396                  # FIXME: make configurable
     397          fname = '%s-%s-%s-%s-%s.txt' % (
     398                  'Medistar-MD',
     399                  time.strftime('%Y-%m-%d',time.localtime()),
     400                  pat['lastnames'].replace(' ', '-'),
     401                  pat['firstnames'].replace(' ', '_'),
     402                  pat.get_formatted_dob(format = '%Y-%m-%d')
     403          )
     404          dlg = wx.FileDialog (
     405                  parent = parent,
     406                  message = _("Save EMR extract for MEDISTAR import as..."),
     407                  defaultDir = aDefDir,
     408                  defaultFile = fname,
     409                  wildcard = aWildcard,
     410                  style = wx.SAVE
     411          )
     412          choice = dlg.ShowModal()
     413          fname = dlg.GetPath()
     414          dlg.Destroy()
     415          if choice != wx.ID_OK:
     416                  return False
     417
     418          wx.BeginBusyCursor()
     419          _log.debug('exporting encounter for medistar import to [%s]', fname)
     420          exporter = gmPatientExporter.cMedistarSOAPExporter()
     421          successful, fname = exporter.export_to_file (
     422                  filename = fname,
     423                  encounter = encounter,
     424                  soap_cats = u'soapu',
     425                  export_to_import_file = True
     426          )
     427          if not successful:
     428                  gmGuiHelpers.gm_show_error (
     429                          _('Error exporting progress notes for MEDISTAR import.'),
     430                          _('MEDISTAR progress notes export')
     431                  )
     432                  wx.EndBusyCursor()
     433                  return False
     434
     435          gmDispatcher.send(signal = 'statustext', msg = _('Successfully exported progress notes into file [%s] for Medistar import.') % fname, beep=False)
     436
     437          wx.EndBusyCursor()
     438          return True
     439
     440  #------------------------------------------------------------
     441 -def select_narrative(parent=None, soap_cats=None, msg=None):
     442
     443          pat = gmPerson.gmCurrentPatient()
     444          emr = pat.get_emr()
     445
     446          if parent is None:
     447                  parent = wx.GetApp().GetTopWindow()
     448
     449          if soap_cats is None:
     450                  soap_cats = u'soapu'
     451          soap_cats = list(soap_cats)
     452          i18n_soap_cats = [ gmClinNarrative.soap_cat2l10n[cat].upper() for cat in soap_cats ]
     453
     454          if msg is None:
     455                  msg = _('Pick the [%s] narrative you want to use.') % u'/'.join(i18n_soap_cats)
     456
     457          #-----------------------------------------------
     458          def get_tooltip(soap):
     459                  return soap.format(fancy = True, width = 60)
     460          #-----------------------------------------------
     461          def refresh(lctrl):
     462                  lctrl.secondary_sort_column = 0
     463                  soap = emr.get_clin_narrative(soap_cats = soap_cats)
     464                  lctrl.set_string_items ([ [
     465                          gmDateTime.pydt_strftime(s['date'], '%Y %m %d'),
     466                          s['modified_by'],
     467                          gmClinNarrative.soap_cat2l10n[s['soap_cat']],
     468                          s['narrative'],
     469                          s['episode'],
     470                          s['health_issue']
     471                  ] for s in soap ])
     472                  lctrl.set_data(soap)
     473          #-----------------------------------------------
     474          return gmListWidgets.get_choices_from_list (
     475                  parent = parent,
     476                  msg = msg,
     477                  caption = _('Picking [%s] narrative') % (u'/'.join(i18n_soap_cats)),
     478                  columns = [_('When'), _('Who'), _('Type'), _('Entry'), _('Episode'), _('Issue')],
     479                  single_selection = False,
     480                  can_return_empty = False,
     481                  refresh_callback = refresh,
     482                  list_tooltip_callback = get_tooltip
     483          )
     484
     485  #------------------------------------------------------------
     486 -def select_narrative_by_issue(parent=None, soap_cats=None):
     487
     488          pat = gmPerson.gmCurrentPatient()
     489          emr = pat.get_emr()
     490
     491          # not useful if you think about it:
     492  #       issues = [ i for i in emr.health_issues ]
     493  #       if len(issues) == 0:
     494  #               gmDispatcher.send(signal = 'statustext', msg = _('No progress notes found.'))
     495  #               return []
     496
     497          if parent is None:
     498                  parent = wx.GetApp().GetTopWindow()
     499
     500          if soap_cats is None:
     501                  soap_cats = u'soapu'
     502          soap_cats = list(soap_cats)
     503          i18n_soap_cats = [ gmClinNarrative.soap_cat2l10n[cat].upper() for cat in soap_cats ]
     504
     505          selected_soap = {}
     506          #selected_narrative_pks = []
     507
     508          #-----------------------------------------------
     509          def get_soap_tooltip(soap):
     510                  return soap.format(fancy = True, width = 60)
     511          #-----------------------------------------------
     512          def pick_soap_from_issue(issue):
     513
     514                  if issue is None:
     515                          return False
     516
     517                  narr_for_issue = emr.get_clin_narrative(issues = [issue['pk_health_issue']], soap_cats = soap_cats)
     518
     519                  if len(narr_for_issue) == 0:
     520                          gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for this health issue.'))
     521                          return True
     522
     523                  selected_narr = gmListWidgets.get_choices_from_list (
     524                          parent = parent,
     525                          msg = _('Pick the [%s] narrative you want to include in the report.') % u'/'.join(i18n_soap_cats),
     526                          caption = _('Picking [%s] from %s%s%s') % (
     527                                  u'/'.join(i18n_soap_cats),
     528                                  gmTools.u_left_double_angle_quote,
     529                                  issue['description'],
     530                                  gmTools.u_right_double_angle_quote
     531                          ),
     532                          columns = [_('When'), _('Who'), _('Type'), _('Entry')],
     533                          choices = [ [
     534                                  gmDateTime.pydt_strftime(narr['date'], '%Y %b %d  %H:%M', accuracy = gmDateTime.acc_minutes),
     535                                  narr['modified_by'],
     536                                  gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
     537                                  narr['narrative'].replace('\n', '//').replace('\r', '//')
     538                          ] for narr in narr_for_issue ],
     539                          data = narr_for_issue,
     540                          #selections=None,
     541                          #edit_callback=None,
     542                          single_selection = False,
     543                          can_return_empty = False,
     544                          list_tooltip_callback = get_soap_tooltip
     545                  )
     546
     547                  if selected_narr is None:
     548                          return True
     549
     550                  for narr in selected_narr:
     551                          selected_soap[narr['pk_narrative']] = narr
     552
     553                  return True
     554          #-----------------------------------------------
     555          def edit_issue(issue):
     556                  return gmEMRStructWidgets.edit_health_issue(parent = parent, issue = issue)
     557          #-----------------------------------------------
     558          def refresh_issues(lctrl):
     559                  #issues = [ i for i in emr.health_issues ]
     560                  issues = emr.health_issues
     561                  lctrl.set_string_items ([ [
     562                                  gmTools.bool2subst(i['is_confidential'], _('!! CONFIDENTIAL !!'), u''),
     563                                  i['description'],
     564                                  gmTools.bool2subst(i['is_active'], _('active'), _('inactive'))
     565                          ] for i in issues
     566                  ])
     567                  lctrl.set_data(issues)
     568          #-----------------------------------------------
     569          def get_issue_tooltip(issue):
     570                  return issue.format (
     571                          patient = pat,
     572                          with_encounters = False,
     573                          with_medications = False,
     574                          with_hospital_stays = False,
     575                          with_procedures = False,
     576                          with_family_history = False,
     577                          with_documents = False,
     578                          with_tests = False,
     579                          with_vaccinations = False
     580                  )
     581          #-----------------------------------------------
     582          #selected_episode_pks = []
     583
     584          issues_picked_from = gmListWidgets.get_choices_from_list (
     585                  parent = parent,
     586                  msg = _('\n Select the issue you want to report on.'),
     587                  caption = _('Picking [%s] from health issues') % u'/'.join(i18n_soap_cats),
     588                  columns = [_('Privacy'), _('Issue'), _('Status')],
     589                  edit_callback = edit_issue,
     590                  refresh_callback = refresh_issues,
     591                  single_selection = True,
     592                  can_return_empty = True,
     593                  ignore_OK_button = False,
     594                  left_extra_button = (
     595                          _('&Pick notes'),
     596                          _('Pick [%s] entries from selected health issue') % u'/'.join(i18n_soap_cats),
     597                          pick_soap_from_issue
     598                  ),
     599                  list_tooltip_callback = get_issue_tooltip
     600          )
     601
     602          if issues_picked_from is None:
     603                  return []
     604
     605          return selected_soap.values()
     606
     607  #       selection_idxs = []
     608  #       for idx in range(len(all_epis)):
     609  #               if all_epis[idx]['pk_episode'] in selected_episode_pks:
     610  #                       selection_idxs.append(idx)
     611  #       if len(selection_idxs) != 0:
     612  #               dlg.set_selections(selections = selection_idxs)
     613  #------------------------------------------------------------
     614 -def select_narrative_by_episode(parent=None, soap_cats=None):
     615
     616          pat = gmPerson.gmCurrentPatient()
     617          emr = pat.get_emr()
     618
     619          all_epis = [ epi for epi in emr.get_episodes(order_by = u'description') if epi.has_narrative ]
     620          if len(all_epis) == 0:
     621                  gmDispatcher.send(signal = 'statustext', msg = _('No episodes with progress notes found.'))
     622                  return []
     623
     624          if parent is None:
     625                  parent = wx.GetApp().GetTopWindow()
     626
     627          if soap_cats is None:
     628                  soap_cats = u'soapu'
     629          soap_cats = list(soap_cats)
     630          i18n_soap_cats = [ gmClinNarrative.soap_cat2l10n[cat].upper() for cat in soap_cats ]
     631
     632          selected_soap = {}
     633          #selected_narrative_pks = []
     634
     635          #-----------------------------------------------
     636          def get_soap_tooltip(soap):
     637                  return soap.format(fancy = True, width = 60)
     638          #-----------------------------------------------
     639          def pick_soap_from_episode(episode):
     640
     641                  if episode is None:
     642                          return False
     643
     644                  narr_for_epi = emr.get_clin_narrative(episodes = [episode['pk_episode']], soap_cats = soap_cats)
     645
     646                  if len(narr_for_epi) == 0:
     647                          gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for selected episode.'))
     648                          return True
     649
     650                  selected_narr = gmListWidgets.get_choices_from_list (
     651                          parent = parent,
     652                          msg = _('Pick the [%s] narrative you want to include in the report.') % u'/'.join(i18n_soap_cats),
     653                          caption = _('Picking [%s] from %s%s%s') % (
     654                                  u'/'.join(i18n_soap_cats),
     655                                  gmTools.u_left_double_angle_quote,
     656                                  episode['description'],
     657                                  gmTools.u_right_double_angle_quote
     658                          ),
     659                          columns = [_('When'), _('Who'), _('Type'), _('Entry')],
     660                          choices = [ [
     661                                  gmDateTime.pydt_strftime(narr['date'], '%Y %b %d  %H:%M', accuracy = gmDateTime.acc_minutes),
     662                                  narr['modified_by'],
     663                                  gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
     664                                  narr['narrative'].replace('\n', '//').replace('\r', '//')
     665                          ] for narr in narr_for_epi ],
     666                          data = narr_for_epi,
     667                          #selections=None,
     668                          #edit_callback=None,
     669                          single_selection = False,
     670                          can_return_empty = False,
     671                          list_tooltip_callback = get_soap_tooltip
     672                  )
     673
     674                  if selected_narr is None:
     675                          return True
     676
     677                  for narr in selected_narr:
     678                          selected_soap[narr['pk_narrative']] = narr
     679
     680                  return True
     681
     682  #               selection_idxs = []
     683  #               for idx in range(len(narr_for_epi)):
     684  #                       if narr_for_epi[idx]['pk_narrative'] in selected_narrative_pks:
     685  #                               selection_idxs.append(idx)
     686  #               if len(selection_idxs) != 0:
     687  #                       dlg.set_selections(selections = selection_idxs)
     688
     689  #               selected_narrative_pks = [ i['pk_narrative'] for i in selected_narr ]
     690  #               for narr in selected_narr:
     691  #                       selected_soap[narr['pk_narrative']] = narr
     692  #
     693  #               print "before returning from picking soap"
     694  #
     695  #               return True
     696  #       #-----------------------------------------------
     697          def edit_episode(episode):
     698                  return gmEMRStructWidgets.edit_episode(parent = parent, episode = episode)
     699          #-----------------------------------------------
     700          def refresh_episodes(lctrl):
     701                  all_epis = [ epi for epi in emr.get_episodes(order_by = u'description') if epi.has_narrative ]
     702                  lctrl.set_string_items ([ [
     703                                  u'%s%s' % (e['description'], gmTools.coalesce(e['health_issue'], u'', u' (%s)')),
     704                                  gmTools.bool2subst(e['episode_open'], _('open'), _('closed'))
     705                          ] for e in all_epis
     706                  ])
     707                  lctrl.set_data(all_epis)
     708          #-----------------------------------------------
     709          def get_episode_tooltip(episode):
     710                  return episode.format (
     711                          patient = pat,
     712                          with_encounters = False,
     713                          with_documents = False,
     714                          with_hospital_stays = False,
     715                          with_procedures = False,
     716                          with_family_history = False,
     717                          with_tests = False,
     718                          with_vaccinations = False
     719                  )
     720          #-----------------------------------------------
     721          #selected_episode_pks = []
     722
     723          epis_picked_from = gmListWidgets.get_choices_from_list (
     724                  parent = parent,
     725                  msg = _('\n Select the episode you want to report on.'),
     726                  caption = _('Picking [%s] from episodes') % u'/'.join(i18n_soap_cats),
     727                  columns = [_('Episode'), _('Status')],
     728                  edit_callback = edit_episode,
     729                  refresh_callback = refresh_episodes,
     730                  single_selection = True,
     731                  can_return_empty = True,
     732                  ignore_OK_button = False,
     733                  left_extra_button = (
     734                          _('&Pick notes'),
     735                          _('Pick [%s] entries from selected episode') % u'/'.join(i18n_soap_cats),
     736                          pick_soap_from_episode
     737                  ),
     738                  list_tooltip_callback = get_episode_tooltip
     739          )
     740
     741          if epis_picked_from is None:
     742                  return []
     743
     744          return selected_soap.values()
     745
     746  #       selection_idxs = []
     747  #       for idx in range(len(all_epis)):
     748  #               if all_epis[idx]['pk_episode'] in selected_episode_pks:
     749  #                       selection_idxs.append(idx)
     750  #       if len(selection_idxs) != 0:
     751  #               dlg.set_selections(selections = selection_idxs)
     752  #------------------------------------------------------------
     753 -def select_narrative_from_episodes(parent=None, soap_cats=None):
     754          """soap_cats needs to be a list"""
     755
     756          pat = gmPerson.gmCurrentPatient()
     757          emr = pat.get_emr()
     758
     759          if parent is None:
     760                  parent = wx.GetApp().GetTopWindow()
     761
     762          selected_soap = {}
     763          selected_issue_pks = []
     764          selected_episode_pks = []
     765          selected_narrative_pks = []
     766
     767          while 1:
     768                  # 1) select health issues to select episodes from
     769                  all_issues = emr.get_health_issues()
     770                  all_issues.insert(0, gmEMRStructItems.get_dummy_health_issue())
     771                  dlg = gmEMRStructWidgets.cIssueListSelectorDlg (
     772                          parent = parent,
     773                          id = -1,
     774                          issues = all_issues,
     775                          msg = _('\n In the list below mark the health issues you want to report on.\n')
     776                  )
     777                  selection_idxs = []
     778                  for idx in range(len(all_issues)):
     779                          if all_issues[idx]['pk_health_issue'] in selected_issue_pks:
     780                                  selection_idxs.append(idx)
     781                  if len(selection_idxs) != 0:
     782                          dlg.set_selections(selections = selection_idxs)
     783                  btn_pressed = dlg.ShowModal()
     784                  selected_issues = dlg.get_selected_item_data()
     785                  dlg.Destroy()
     786
     787                  if btn_pressed == wx.ID_CANCEL:
     788                          return selected_soap.values()
     789
     790                  selected_issue_pks = [ i['pk_health_issue'] for i in selected_issues ]
     791
     792                  while 1:
     793                          # 2) select episodes to select items from
     794                          all_epis = emr.get_episodes(issues = selected_issue_pks)
     795
     796                          if len(all_epis) == 0:
     797                                  gmDispatcher.send(signal = 'statustext', msg = _('No episodes recorded for the health issues selected.'))
     798                                  break
     799
     800                          dlg = gmEMRStructWidgets.cEpisodeListSelectorDlg (
     801                                  parent = parent,
     802                                  id = -1,
     803                                  episodes = all_epis,
     804                                  msg = _(
     805                                          '\n These are the episodes known for the health issues just selected.\n\n'
     806                                          ' Now, mark the the episodes you want to report on.\n'
     807                                  )
     808                          )
     809                          selection_idxs = []
     810                          for idx in range(len(all_epis)):
     811                                  if all_epis[idx]['pk_episode'] in selected_episode_pks:
     812                                          selection_idxs.append(idx)
     813                          if len(selection_idxs) != 0:
     814                                  dlg.set_selections(selections = selection_idxs)
     815                          btn_pressed = dlg.ShowModal()
     816                          selected_epis = dlg.get_selected_item_data()
     817                          dlg.Destroy()
     818
     819                          if btn_pressed == wx.ID_CANCEL:
     820                                  break
     821
     822                          selected_episode_pks = [ i['pk_episode'] for i in selected_epis ]
     823
     824                          # 3) select narrative corresponding to the above constraints
     825                          all_narr = emr.get_clin_narrative(episodes = selected_episode_pks, soap_cats = soap_cats)
     826
     827                          if len(all_narr) == 0:
     828                                  gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for selected episodes.'))
     829                                  continue
     830
     831                          dlg = cNarrativeListSelectorDlg (
     832                                  parent = parent,
     833                                  id = -1,
     834                                  narrative = all_narr,
     835                                  msg = _(
     836                                          '\n This is the narrative (type %s) for the chosen episodes.\n\n'
     837                                          ' Now, mark the entries you want to include in your report.\n'
     838                                  ) % u'/'.join([ gmClinNarrative.soap_cat2l10n[cat] for cat in gmTools.coalesce(soap_cats, list(u'soapu')) ])
     839                          )
     840                          selection_idxs = []
     841                          for idx in range(len(all_narr)):
     842                                  if all_narr[idx]['pk_narrative'] in selected_narrative_pks:
     843                                          selection_idxs.append(idx)
     844                          if len(selection_idxs) != 0:
     845                                  dlg.set_selections(selections = selection_idxs)
     846                          btn_pressed = dlg.ShowModal()
     847                          selected_narr = dlg.get_selected_item_data()
     848                          dlg.Destroy()
     849
     850                          if btn_pressed == wx.ID_CANCEL:
     851                                  continue
     852
     853                          selected_narrative_pks = [ i['pk_narrative'] for i in selected_narr ]
     854                          for narr in selected_narr:
     855                                  selected_soap[narr['pk_narrative']] = narr
     856  #------------------------------------------------------------
     857 -class cNarrativeListSelectorDlg(gmListWidgets.cGenericListSelectorDlg):
     858
     859 -        def __init__(self, *args, **kwargs):
     860
     861                  narrative = kwargs['narrative']
     862                  del kwargs['narrative']
     863
     864                  gmListWidgets.cGenericListSelectorDlg.__init__(self, *args, **kwargs)
     865
     866                  self.SetTitle(_('Select the narrative you are interested in ...'))
     867                  # FIXME: add epi/issue
     868                  self._LCTRL_items.set_columns([_('when'), _('who'), _('type'), _('entry')]) #, _('Episode'), u'', _('Health Issue')])
     869                  # FIXME: date used should be date of encounter, not date_modified
     870                  self._LCTRL_items.set_string_items (
     871                          items = [ [narr['date'].strftime('%x %H:%M'), narr['modified_by'], gmClinNarrative.soap_cat2l10n[narr['soap_cat']], narr['narrative'].replace('\n', '/').replace('\r', '/')] for narr in narrative ]
     872                  )
     873                  self._LCTRL_items.set_column_widths()
     874                  self._LCTRL_items.set_data(data = narrative)
     875  #------------------------------------------------------------
     876  from Gnumed.wxGladeWidgets import wxgMoveNarrativeDlg
     877
     878 -class cMoveNarrativeDlg(wxgMoveNarrativeDlg.wxgMoveNarrativeDlg):
     879
     880 -        def __init__(self, *args, **kwargs):
     881
     882                  self.encounter = kwargs['encounter']
     883                  self.source_episode = kwargs['episode']
     884                  del kwargs['encounter']
     885                  del kwargs['episode']
     886
     887                  wxgMoveNarrativeDlg.wxgMoveNarrativeDlg.__init__(self, *args, **kwargs)
     888
     889                  self.LBL_source_episode.SetLabel(u'%s%s' % (self.source_episode['description'], gmTools.coalesce(self.source_episode['health_issue'], u'', u' (%s)')))
     890                  self.LBL_encounter.SetLabel('%s: %s %s - %s' % (
     891                          gmDateTime.pydt_strftime(self.encounter['started'], '%Y %b %d'),
     892                          self.encounter['l10n_type'],
     893                          gmDateTime.pydt_strftime(self.encounter['started'], '%H:%M'),
     894                          gmDateTime.pydt_strftime(self.encounter['last_affirmed'], '%H:%M')
     895                  ))
     896                  pat = gmPerson.gmCurrentPatient()
     897                  emr = pat.get_emr()
     898                  narr = emr.get_clin_narrative(episodes=[self.source_episode['pk_episode']], encounters=[self.encounter['pk_encounter']])
     899                  if len(narr) == 0:
     900                          narr = [{'narrative': _('There is no narrative for this episode in this encounter.')}]
     901                  self.LBL_narrative.SetLabel(u'\n'.join([n['narrative'] for n in narr]))
     902
     903          #------------------------------------------------------------
     904 -        def _on_move_button_pressed(self, event):
     905
     906                  target_episode = self._PRW_episode_selector.GetData(can_create = False)
     907
     908                  if target_episode is None:
     909                          gmDispatcher.send(signal='statustext', msg=_('Must select episode to move narrative to first.'))
     910                          # FIXME: set to pink
     911                          self._PRW_episode_selector.SetFocus()
     912                          return False
     913
     914                  target_episode = gmEMRStructItems.cEpisode(aPK_obj=target_episode)
     915
     916                  self.encounter.transfer_clinical_data (
     917                          source_episode = self.source_episode,
     918                          target_episode = target_episode
     919                  )
     920
     921                  if self.IsModal():
     922                          self.EndModal(wx.ID_OK)
     923                  else:
     924                          self.Close()
     925  #============================================================
     926  from Gnumed.wxGladeWidgets import wxgSoapPluginPnl
     927
     928 -class cSoapPluginPnl(wxgSoapPluginPnl.wxgSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
     929          """A panel for in-context editing of progress notes.
     930
     931          Expects to be used as a notebook page.
     932
     933          Left hand side:
     934          - problem list (health issues and active episodes)
     935          - previous notes
     936
     937          Right hand side:
     938          - panel handling
     939                  - encounter details fields
     940                  - notebook with progress note editors
     941                  - visual progress notes
     942
     943          Listens to patient change signals, thus acts on the current patient.
     944          """
     945 -        def __init__(self, *args, **kwargs):
     946
     947                  wxgSoapPluginPnl.wxgSoapPluginPnl.__init__(self, *args, **kwargs)
     948                  gmRegetMixin.cRegetOnPaintMixin.__init__(self)
     949
     950                  self.__pat = gmPerson.gmCurrentPatient()
     951                  self.__init_ui()
     952                  self.__reset_ui_content()
     953                  self.__register_interests()
     954          #--------------------------------------------------------
     955          # internal helpers
     956          #--------------------------------------------------------
     957 -        def __init_ui(self):
     958                  self._LCTRL_active_problems.set_columns([_('Last'), _('Problem'), _('In health issue')])
     959                  self._LCTRL_active_problems.set_string_items()
     960
     961                  self._splitter_main.SetSashGravity(0.5)
     962                  self._splitter_left.SetSashGravity(0.5)
     963
     964                  splitter_size = self._splitter_main.GetSizeTuple()[0]
     965                  self._splitter_main.SetSashPosition(splitter_size * 3 / 10, True)
     966
     967                  splitter_size = self._splitter_left.GetSizeTuple()[1]
     968                  self._splitter_left.SetSashPosition(splitter_size * 6 / 20, True)
     969          #--------------------------------------------------------
     970 -        def __reset_ui_content(self):
     971                  """Clear all information from input panel."""
     972
     973                  self._LCTRL_active_problems.set_string_items()
     974
     975                  self._TCTRL_recent_notes.SetValue(u'')
     976                  self._SZR_recent_notes_staticbox.SetLabel(_('Most recent notes on selected problem'))
     977
     978                  self._PNL_editors.patient = None
     979          #--------------------------------------------------------
     980 -        def __refresh_problem_list(self):
     981                  """Update health problems list."""
     982
     983                  self._LCTRL_active_problems.set_string_items()
     984
     985                  emr = self.__pat.get_emr()
     986                  problems = emr.get_problems (
     987                          include_closed_episodes = self._CHBOX_show_closed_episodes.IsChecked(),
     988                          include_irrelevant_issues = self._CHBOX_irrelevant_issues.IsChecked()
     989                  )
     990
     991                  list_items = []
     992                  active_problems = []
     993                  for problem in problems:
     994                          if not problem['problem_active']:
     995                                  if not problem['is_potential_problem']:
     996                                          continue
     997
     998                          active_problems.append(problem)
     999
    1000                          if problem['type'] == 'issue':
    1001                                  issue = emr.problem2issue(problem)
    1002                                  last_encounter = emr.get_last_encounter(issue_id = issue['pk_health_issue'])
    1003                                  if last_encounter is None:
    1004                                          last = issue['modified_when'].strftime('%m/%Y')
    1005                                  else:
    1006                                          last = last_encounter['last_affirmed'].strftime('%m/%Y')
    1007
    1008                                  list_items.append([last, problem['problem'], gmTools.u_left_arrow_with_tail])
    1009
    1010                          elif problem['type'] == 'episode':
    1011                                  epi = emr.problem2episode(problem)
    1012                                  last_encounter = emr.get_last_encounter(episode_id = epi['pk_episode'])
    1013                                  if last_encounter is None:
    1014                                          last = epi['episode_modified_when'].strftime('%m/%Y')
    1015                                  else:
    1016                                          last = last_encounter['last_affirmed'].strftime('%m/%Y')
    1017
    1018                                  list_items.append ([
    1019                                          last,
    1020                                          problem['problem'],
    1021                                          gmTools.coalesce(initial = epi['health_issue'], instead = u'?')         #gmTools.u_diameter
    1022                                  ])
    1023
    1024                  self._LCTRL_active_problems.set_string_items(items = list_items)
    1025                  self._LCTRL_active_problems.set_column_widths()
    1026                  self._LCTRL_active_problems.set_data(data = active_problems)
    1027
    1028                  showing_potential_problems = (
    1029                          self._CHBOX_show_closed_episodes.IsChecked()
    1030                                  or
    1031                          self._CHBOX_irrelevant_issues.IsChecked()
    1032                  )
    1033                  if showing_potential_problems:
    1034                          self._SZR_problem_list_staticbox.SetLabel(_('%s (active+potential) problems') % len(list_items))
    1035                  else:
    1036                          self._SZR_problem_list_staticbox.SetLabel(_('%s active problems') % len(list_items))
    1037
    1038                  return True
    1039          #--------------------------------------------------------
    1040 -        def __get_info_for_issue_problem(self, problem=None, fancy=False):
    1041                  soap = u''
    1042                  emr = self.__pat.get_emr()
    1043                  prev_enc = emr.get_last_but_one_encounter(issue_id = problem['pk_health_issue'])
    1044                  if prev_enc is not None:
    1045                          soap += prev_enc.format (
    1046                                  issues = [ problem['pk_health_issue'] ],
    1047                                  with_soap = True,
    1048                                  with_docs = fancy,
    1049                                  with_tests = fancy,
    1050                                  patient = self.__pat,
    1051                                  fancy_header = False,
    1052                                  with_rfe_aoe = True
    1053                          )
    1054
    1055                  tmp = emr.active_encounter.format_soap (
    1056                          soap_cats = 'soapu',
    1057                          emr = emr,
    1058                          issues = [ problem['pk_health_issue'] ],
    1059                  )
    1060                  if len(tmp) > 0:
    1061                          soap += _('Current encounter:') + u'\n'
    1062                          soap += u'\n'.join(tmp) + u'\n'
    1063
    1064                  if problem['summary'] is not None:
    1065                          soap += u'\n-- %s ----------\n%s' % (
    1066                                  _('Cumulative summary'),
    1067                                  gmTools.wrap (
    1068                                          text = problem['summary'],
    1069                                          width = 45,
    1070                                          initial_indent = u' ',
    1071                                          subsequent_indent = u' '
    1072                                  ).strip('\n')
    1073                          )
    1074
    1075                  return soap
    1076          #--------------------------------------------------------
    1077 -        def __get_info_for_episode_problem(self, problem=None, fancy=False):
    1078                  soap = u''
    1079                  emr = self.__pat.get_emr()
    1080                  prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_episode'])
    1081                  if prev_enc is not None:
    1082                          soap += prev_enc.format (
    1083                                  episodes = [ problem['pk_episode'] ],
    1084                                  with_soap = True,
    1085                                  with_docs = fancy,
    1086                                  with_tests = fancy,
    1087                                  patient = self.__pat,
    1088                                  fancy_header = False,
    1089                                  with_rfe_aoe = True
    1090                          )
    1091                  else:
    1092                          if problem['pk_health_issue'] is not None:
    1093                                  prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_health_issue'])
    1094                                  if prev_enc is not None:
    1095                                          soap += prev_enc.format (
    1096                                                  with_soap = True,
    1097                                                  with_docs = fancy,
    1098                                                  with_tests = fancy,
    1099                                                  patient = self.__pat,
    1100                                                  issues = [ problem['pk_health_issue'] ],
    1101                                                  fancy_header = False,
    1102                                                  with_rfe_aoe = True
    1103                                          )
    1104
    1105                  tmp = emr.active_encounter.format_soap (
    1106                          soap_cats = 'soapu',
    1107                          emr = emr,
    1108                          issues = [ problem['pk_health_issue'] ],
    1109                  )
    1110                  if len(tmp) > 0:
    1111                          soap += _('Current encounter:') + u'\n'
    1112                          soap += u'\n'.join(tmp) + u'\n'
    1113
    1114                  if problem['summary'] is not None:
    1115                          soap += u'\n-- %s ----------\n%s' % (
    1116                                  _('Cumulative summary'),
    1117                                  gmTools.wrap (
    1118                                          text = problem['summary'],
    1119                                          width = 45,
    1120                                          initial_indent = u' ',
    1121                                          subsequent_indent = u' '
    1122                                  ).strip('\n')
    1123                          )
    1124
    1125                  return soap
    1126          #--------------------------------------------------------
    1127 -        def __refresh_recent_notes(self, problem=None):
    1128                  """This refreshes the recent-notes part."""
    1129
    1130                  if problem is None:
    1131                          caption = u'<?>'
    1132                          txt = u''
    1133                  elif problem['type'] == u'issue':
    1134                          caption = problem['problem'][:35]
    1135                          txt = self.__get_info_for_issue_problem(problem = problem, fancy = not self._RBTN_notes_only.GetValue())
    1136                  elif problem['type'] == u'episode':
    1137                          caption = problem['problem'][:35]
    1138                          txt = self.__get_info_for_episode_problem(problem = problem, fancy = not self._RBTN_notes_only.GetValue())
    1139
    1140                  self._TCTRL_recent_notes.SetValue(txt)
    1141                  self._TCTRL_recent_notes.ShowPosition(self._TCTRL_recent_notes.GetLastPosition())
    1142                  self._SZR_recent_notes_staticbox.SetLabel(_('Most recent info on %s%s%s') % (
    1143                          gmTools.u_left_double_angle_quote,
    1144                          caption,
    1145                          gmTools.u_right_double_angle_quote
    1146                  ))
    1147
    1148                  self._TCTRL_recent_notes.Refresh()
    1149
    1150                  return True
    1151          #--------------------------------------------------------
    1152          # event handling
    1153          #--------------------------------------------------------
    1154 -        def __register_interests(self):
    1155                  """Configure enabled event signals."""
    1156                  # client internal signals
    1157                  gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
    1158                  gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
    1159                  gmDispatcher.connect(signal = u'clin.episode_mod_db', receiver = self._on_episode_issue_mod_db)
    1160                  gmDispatcher.connect(signal = u'clin.health_issue_mod_db', receiver = self._on_episode_issue_mod_db)
    1161                  gmDispatcher.connect(signal = u'clin.episode_code_mod_db', receiver = self._on_episode_issue_mod_db)
    1162          #--------------------------------------------------------
    1163 -        def _on_pre_patient_selection(self):
    1164                  wx.CallAfter(self.__on_pre_patient_selection)
    1165
    1166 -        def __on_pre_patient_selection(self):
    1167                  self.__reset_ui_content()
    1168          #--------------------------------------------------------
    1169 -        def _on_post_patient_selection(self):
    1170                  wx.CallAfter(self.__on_post_patient_selection)
    1171
    1172 -        def __on_post_patient_selection(self):
    1173                  self._schedule_data_reget()
    1174                  self._PNL_editors.patient = self.__pat
    1175          #--------------------------------------------------------
    1176 -        def _on_episode_issue_mod_db(self):
    1177                  wx.CallAfter(self._schedule_data_reget)
    1178          #--------------------------------------------------------
    1179          # problem list specific events
    1180          #--------------------------------------------------------
    1181 -        def _on_problem_focused(self, event):
    1182                  """Show related note at the bottom."""
    1183                  pass
    1184          #--------------------------------------------------------
    1185 -        def _on_problem_rclick(self, event):
    1186                  problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
    1187                  if problem['type'] == u'issue':
    1188                          gmEMRStructWidgets.edit_health_issue(parent = self, issue = problem.get_as_health_issue())
    1189                          return
    1190
    1191                  if problem['type'] == u'episode':
    1192                          gmEMRStructWidgets.edit_episode(parent = self, episode = problem.get_as_episode())
    1193                          return
    1194
    1195                  event.Skip()
    1196          #--------------------------------------------------------
    1197 -        def _on_problem_selected(self, event):
    1198                  """Show related note at the bottom."""
    1199                  self.__refresh_recent_notes (
    1200                          problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
    1201                  )
    1202          #--------------------------------------------------------
    1203 -        def _on_problem_activated(self, event):
    1204                  """Open progress note editor for this problem.
    1205                  """
    1206                  problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
    1207                  if problem is None:
    1208                          return True
    1209
    1210                  dbcfg = gmCfg.cCfgSQL()
    1211                  allow_duplicate_editors = bool(dbcfg.get2 (
    1212                          option = u'horstspace.soap_editor.allow_same_episode_multiple_times',
    1213                          workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
    1214                          bias = u'user',
    1215                          default = False
    1216                  ))
    1217                  if self._PNL_editors.add_editor(problem = problem, allow_same_problem = allow_duplicate_editors):
    1218                          return True
    1219
    1220                  gmGuiHelpers.gm_show_error (
    1221                          aMessage = _(
    1222                                  'Cannot open progress note editor for\n\n'
    1223                                  '[%s].\n\n'
    1224                          ) % problem['problem'],
    1225                          aTitle = _('opening progress note editor')
    1226                  )
    1227                  return False
    1228          #--------------------------------------------------------
    1229 -        def _on_show_closed_episodes_checked(self, event):
    1230                  self.__refresh_problem_list()
    1231          #--------------------------------------------------------
    1232 -        def _on_irrelevant_issues_checked(self, event):
    1233                  self.__refresh_problem_list()
    1234          #--------------------------------------------------------
    1235          # recent-notes specific events
    1236          #--------------------------------------------------------
    1237 -        def _on_notes_only_selected(self, event):
    1238                  self.__refresh_recent_notes (
    1239                          problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
    1240                  )
    1241          #--------------------------------------------------------
    1242 -        def _on_full_encounter_selected(self, event):
    1243                  self.__refresh_recent_notes (
    1244                          problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
    1245                  )
    1246          #--------------------------------------------------------
    1247          # reget mixin API
    1248          #--------------------------------------------------------
    1249          # only needed for debugging:
    1250          #def _schedule_data_reget(self):
    1251          #       gmRegetMixin.cRegetOnPaintMixin._schedule_data_reget(self)
    1252          #--------------------------------------------------------
    1253 -        def _populate_with_data(self):
    1254                  self.__refresh_problem_list()
    1255                  return True
    1256
    1257  #============================================================
    1258  from Gnumed.wxGladeWidgets import wxgFancySoapEditorPnl
    1259
    1260 -class cFancySoapEditorPnl(wxgFancySoapEditorPnl.wxgFancySoapEditorPnl):
    1261          """A panel holding everything needed to edit
    1262
    1263                  - encounter metadata
    1264                  - textual progress notes
    1265                  - visual progress notes
    1266
    1267          in context. Does NOT act on the current patient.
    1268          """
    1269 -        def __init__(self, *args, **kwargs):
    1270
    1271                  wxgFancySoapEditorPnl.wxgFancySoapEditorPnl.__init__(self, *args, **kwargs)
    1272
    1273                  self.__init_ui()
    1274                  self.patient = None
    1275                  self.__register_interests()
    1276          #--------------------------------------------------------
    1277          # public API
    1278          #--------------------------------------------------------
    1279 -        def add_editor(self, problem=None, allow_same_problem=False):
    1280                  return self._NB_soap_editors.add_editor(problem = problem, allow_same_problem = allow_same_problem)
    1281          #--------------------------------------------------------
    1282 -        def _get_patient(self):
    1283                  return self.__pat
    1284
    1285 -        def _set_patient(self, patient):
    1286                  #if
    1287                  #       self.__pat.register_pre_selection_callback(callback = self._pre_selection_callback)
    1288                  self.__pat = patient
    1289                  self.__refresh_encounter()
    1290                  self.__refresh_soap_notebook()
    1291
    1292          patient = property(_get_patient, _set_patient)
    1293          #--------------------------------------------------------
    1294 -        def save_encounter(self):
    1295
    1296                  if self.__pat is None:
    1297                          return True
    1298
    1299                  if not self.__encounter_valid_for_save():
    1300                          return False
    1301
    1302                  enc = self.__pat.emr.active_encounter
    1303
    1304                  rfe = self._TCTRL_rfe.GetValue().strip()
    1305                  if len(rfe) == 0:
    1306                          enc['reason_for_encounter'] = None
    1307                  else:
    1308                          enc['reason_for_encounter'] = rfe
    1309                  aoe = self._TCTRL_aoe.GetValue().strip()
    1310                  if len(aoe) == 0:
    1311                          enc['assessment_of_encounter'] = None
    1312                  else:
    1313                          enc['assessment_of_encounter'] = aoe
    1314
    1315                  enc.save_payload()
    1316
    1317                  enc.generic_codes_rfe = [ c['data'] for c in self._PRW_rfe_codes.GetData() ]
    1318                  enc.generic_codes_aoe = [ c['data'] for c in self._PRW_aoe_codes.GetData() ]
    1319
    1320                  return True
    1321          #--------------------------------------------------------
    1322          # internal helpers
    1323          #--------------------------------------------------------
    1324 -        def __init_ui(self):
    1325                  self._NB_soap_editors.MoveAfterInTabOrder(self._PRW_aoe_codes)
    1326          #--------------------------------------------------------
    1327 -        def __reset_soap_notebook(self):
    1328                  self._NB_soap_editors.DeleteAllPages()
    1329                  self._NB_soap_editors.add_editor()
    1330          #--------------------------------------------------------
    1331 -        def __refresh_soap_notebook(self):
    1332                  self.__reset_soap_notebook()
    1333
    1334                  if self.__pat is None:
    1335                          return
    1336
    1337                  dbcfg = gmCfg.cCfgSQL()
    1338                  auto_open_recent_problems = bool(dbcfg.get2 (
    1339                          option = u'horstspace.soap_editor.auto_open_latest_episodes',
    1340                          workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
    1341                          bias = u'user',
    1342                          default = True
    1343                  ))
    1344
    1345                  emr = self.__pat.emr
    1346                  recent_epis = emr.active_encounter.get_episodes()
    1347                  prev_enc = emr.get_last_but_one_encounter()
    1348                  if prev_enc is not None:
    1349                          recent_epis.extend(prev_enc.get_episodes())
    1350
    1351                  for epi in recent_epis:
    1352                          if not epi['episode_open']:
    1353                                  continue
    1354                          self._NB_soap_editors.add_editor(problem = epi)
    1355          #--------------------------------------------------------
    1356 -        def __reset_encounter_fields(self):
    1357                  self._TCTRL_rfe.SetValue(u'')
    1358                  self._PRW_rfe_codes.SetText(suppress_smarts = True)
    1359                  self._TCTRL_aoe.SetValue(u'')
    1360                  self._PRW_aoe_codes.SetText(suppress_smarts = True)
    1361          #--------------------------------------------------------
    1362 -        def __refresh_encounter(self):
    1363                  """Update encounter fields."""
    1364
    1365                  self.__reset_encounter_fields()
    1366
    1367                  if self.__pat is None:
    1368                          return
    1369
    1370                  enc = self.__pat.emr.active_encounter
    1371
    1372                  self._TCTRL_rfe.SetValue(gmTools.coalesce(enc['reason_for_encounter'], u''))
    1373                  val, data = self._PRW_rfe_codes.generic_linked_codes2item_dict(enc.generic_codes_rfe)
    1374                  self._PRW_rfe_codes.SetText(val, data)
    1375
    1376                  self._TCTRL_aoe.SetValue(gmTools.coalesce(enc['assessment_of_encounter'], u''))
    1377                  val, data = self._PRW_aoe_codes.generic_linked_codes2item_dict(enc.generic_codes_aoe)
    1378                  self._PRW_aoe_codes.SetText(val, data)
    1379
    1380                  self._TCTRL_rfe.Refresh()
    1381                  self._PRW_rfe_codes.Refresh()
    1382                  self._TCTRL_aoe.Refresh()
    1383                  self._PRW_aoe_codes.Refresh()
    1384          #--------------------------------------------------------
    1385 -        def __refresh_current_editor(self):
    1386                  self._NB_soap_editors.refresh_current_editor()
    1387  #       #--------------------------------------------------------
    1388  #       def __encounter_modified(self):
    1389  #               """Assumes that the field data is valid."""
    1390  #
    1391  #               emr = self.__pat.get_emr()
    1392  #               enc = emr.active_encounter
    1393  #
    1394  #               data = {
    1395  #                       'pk_type': enc['pk_type'],
    1396  #                       'reason_for_encounter': gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u''),
    1397  #                       'assessment_of_encounter': gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
    1398  #                       'pk_location': enc['pk_org_unit'],
    1399  #                       'pk_patient': enc['pk_patient'],
    1400  #                       'pk_generic_codes_rfe': self._PRW_rfe_codes.GetData(),
    1401  #                       'pk_generic_codes_aoe': self._PRW_aoe_codes.GetData(),
    1402  #                       'started': enc['started'],
    1403  #                       'last_affirmed': enc['last_affirmed']
    1404  #               }
    1405  #
    1406  #               return not enc.same_payload(another_object = data)
    1407          #--------------------------------------------------------
    1408 -        def __encounter_valid_for_save(self):
    1409                  return True
    1410          #--------------------------------------------------------
    1411          # event handling
    1412          #--------------------------------------------------------
    1413 -        def __register_interests(self):
    1414                  """Configure enabled event signals."""
    1415                  # synchronous signals
    1416                  gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
    1417
    1418                  # client internal signals
    1419                  gmDispatcher.connect(signal = u'blobs.doc_med_mod_db', receiver = self._on_doc_mod_db)                  # visual progress notes
    1420                  gmDispatcher.connect(signal = u'current_encounter_modified', receiver = self._on_current_encounter_modified)
    1421                  gmDispatcher.connect(signal = u'current_encounter_switched', receiver = self._on_current_encounter_switched)
    1422                  gmDispatcher.connect(signal = u'clin.rfe_code_mod_db', receiver = self._on_encounter_code_modified)
    1423                  gmDispatcher.connect(signal = u'clin.aoe_code_mod_db', receiver = self._on_encounter_code_modified)
    1424          #--------------------------------------------------------
    1425 -        def _pre_selection_callback(self):
    1426                  """Another patient is about to be activated.
    1427
    1428                  Patient change will not proceed before this returns True.
    1429                  """
    1430                  # don't worry about the encounter here - it will be offered
    1431                  # for editing higher up if anything was saved to the EMR
    1432                  if self.__pat is None:
    1433                          return True
    1434                  return self._NB_soap_editors.warn_on_unsaved_soap()
    1435          #--------------------------------------------------------
    1436 -        def _pre_exit_callback(self):
    1437                  """The client is about to (be) shut down.
    1438
    1439                  Shutdown will not proceed before this returns.
    1440                  """
    1441                  if self.__pat is None:
    1442                          return True
    1443
    1444  #               if self.__encounter_modified():
    1445  #                       do_save_enc = gmGuiHelpers.gm_show_question (
    1446  #                               aMessage = _(
    1447  #                                       'You have modified the details\n'
    1448  #                                       'of the current encounter.\n'
    1449  #                                       '\n'
    1450  #                                       'Do you want to save those changes ?'
    1451  #                               ),
    1452  #                               aTitle = _('Starting new encounter')
    1453  #                       )
    1454  #                       if do_save_enc:
    1455  #                               if not self.save_encounter():
    1456  #                                       gmDispatcher.send(signal = u'statustext', msg = _('Error saving current encounter.'), beep = True)
    1457
    1458                  saved = self._NB_soap_editors.save_all_editors (
    1459                          emr = self.__pat.emr,
    1460                          episode_name_candidates = [
    1461                                  gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
    1462                                  gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u'')
    1463                          ]
    1464                  )
    1465                  if not saved:
    1466                          gmDispatcher.send(signal = 'statustext', msg = _('Cannot save all editors. Some were kept open.'), beep = True)
    1467                  return True
    1468          #--------------------------------------------------------
    1469 -        def _on_doc_mod_db(self):
    1470                  wx.CallAfter(self.__refresh_current_editor)
    1471          #--------------------------------------------------------
    1472 -        def _on_encounter_code_modified(self):
    1473                  wx.CallAfter(self.__on_encounter_code_modified)
    1474          #--------------------------------------------------------
    1475 -        def __on_encounter_code_modified(self):
    1476                  self.__pat.emr.active_encounter.refetch_payload()
    1477                  self.__refresh_encounter()
    1478          #--------------------------------------------------------
    1479 -        def _on_current_encounter_modified(self):
    1480                  wx.CallAfter(self.__refresh_encounter)
    1481          #--------------------------------------------------------
    1482 -        def _on_current_encounter_switched(self):
    1483                  wx.CallAfter(self.__refresh_encounter)
    1484          #--------------------------------------------------------
    1485          # SOAP editor specific buttons
    1486          #--------------------------------------------------------
    1487 -        def _on_discard_editor_button_pressed(self, event):
    1488                  self._NB_soap_editors.close_current_editor()
    1489                  event.Skip()
    1490          #--------------------------------------------------------
    1491 -        def _on_new_editor_button_pressed(self, event):
    1492                  self._NB_soap_editors.add_editor(allow_same_problem = True)
    1493                  event.Skip()
    1494          #--------------------------------------------------------
    1495 -        def _on_clear_editor_button_pressed(self, event):
    1496                  self._NB_soap_editors.clear_current_editor()
    1497                  event.Skip()
    1498          #--------------------------------------------------------
    1499 -        def _on_save_note_button_pressed(self, event):
    1500                  self._NB_soap_editors.save_current_editor (
    1501                          emr = self.__pat.emr,
    1502                          episode_name_candidates = [
    1503                                  gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
    1504                                  gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u'')
    1505                          ]
    1506                  )
    1507                  event.Skip()
    1508          #--------------------------------------------------------
    1509 -        def _on_save_note_under_button_pressed(self, event):
    1510                  encounter = gmEncounterWidgets.select_encounters (
    1511                          parent = self,
    1512                          patient = self.__pat,
    1513                          single_selection = True
    1514                  )
    1515                  # cancelled or None selected:
    1516                  if encounter is None:
    1517                          return
    1518
    1519                  self._NB_soap_editors.save_current_editor (
    1520                          emr = self.__pat.emr,
    1521                          encounter = encounter['pk_encounter'],
    1522                          episode_name_candidates = [
    1523                                  gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
    1524                                  gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u'')
    1525                          ]
    1526                  )
    1527                  event.Skip()
    1528          #--------------------------------------------------------
    1529 -        def _on_image_button_pressed(self, event):
    1530                  self._NB_soap_editors.add_visual_progress_note_to_current_problem()
    1531                  event.Skip()
    1532          #--------------------------------------------------------
    1533          # encounter specific buttons
    1534          #--------------------------------------------------------
    1535 -        def _on_save_encounter_button_pressed(self, event):
    1536                  self.save_encounter()
    1537                  event.Skip()
    1538          #--------------------------------------------------------
    1539          # other buttons
    1540          #--------------------------------------------------------
    1541 -        def _on_save_all_button_pressed(self, event):
    1542                  self.save_encounter()
    1543                  time.sleep(0.3)
    1544                  event.Skip()
    1545                  wx.SafeYield()
    1546
    1547                  wx.CallAfter(self._save_all_button_pressed_bottom_half)
    1548                  wx.SafeYield()
    1549          #--------------------------------------------------------
    1550 -        def _save_all_button_pressed_bottom_half(self):
    1551                  emr = self.__pat.get_emr()
    1552                  saved = self._NB_soap_editors.save_all_editors (
    1553                          emr = emr,
    1554                          episode_name_candidates = [
    1555                                  gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
    1556                                  gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u'')
    1557                          ]
    1558                  )
    1559                  if not saved:
    1560                          gmDispatcher.send(signal = 'statustext', msg = _('Cannot save all editors. Some were kept open.'), beep = True)
    1561
    1562  #============================================================
    1563 -class cSoapNoteInputNotebook(wx.Notebook):
    1564          """A notebook holding panels with progress note editors.
    1565
    1566          There can be one or several progress note editor panel
    1567          for each episode being worked on. The editor class in
    1568          each panel is configurable.
    1569
    1570          There will always be one open editor.
    1571          """
    1572 -        def __init__(self, *args, **kwargs):
    1573
    1574                  kwargs['style'] = wx.NB_TOP | wx.NB_MULTILINE | wx.NO_BORDER
    1575
    1576                  wx.Notebook.__init__(self, *args, **kwargs)
    1577          #--------------------------------------------------------
    1578          # public API
    1579          #--------------------------------------------------------
    1580 -        def add_editor(self, problem=None, allow_same_problem=False):
    1581                  """Add a progress note editor page.
    1582
    1583                  The way <allow_same_problem> is currently used in callers
    1584                  it only applies to unassociated episodes.
    1585                  """
    1586                  problem_to_add = problem
    1587
    1588                  # determine label
    1589                  if problem_to_add is None:
    1590                          label = _('new problem')
    1591                  else:
    1592                          # normalize problem type
    1593                          if isinstance(problem_to_add, gmEMRStructItems.cEpisode):
    1594                                  problem_to_add = gmEMRStructItems.episode2problem(episode = problem_to_add, allow_closed = True)
    1595
    1596                          elif isinstance(problem_to_add, gmEMRStructItems.cHealthIssue):
    1597                                  problem_to_add = gmEMRStructItems.health_issue2problem(health_issue = problem_to_add, allow_irrelevant = True)
    1598
    1599                          if not isinstance(problem_to_add, gmEMRStructItems.cProblem):
    1600                                  raise TypeError('cannot open progress note editor for [%s]' % problem_to_add)
    1601
    1602                          label = problem_to_add['problem']
    1603                          # FIXME: configure maximum length
    1604                          if len(label) > 23:
    1605                                  label = label[:21] + gmTools.u_ellipsis
    1606
    1607                  # new unassociated problem or dupes allowed
    1608                  if allow_same_problem:
    1609                          new_page = cSoapNoteExpandoEditAreaPnl(parent = self, id = -1, problem = problem_to_add)
    1610                          result = self.AddPage (
    1611                                  page = new_page,
    1612                                  text = label,
    1613                                  select = True
    1614                          )
    1615                          return result
    1616
    1617                  # real problem, no dupes allowed
    1618                  # - raise existing editor
    1619                  for page_idx in range(self.GetPageCount()):
    1620                          page = self.GetPage(page_idx)
    1621
    1622                          if problem_to_add is None:
    1623                                  if page.problem is None:
    1624                                          self.SetSelection(page_idx)
    1625                                          gmDispatcher.send(signal = u'statustext', msg = u'Raising existing editor.', beep = True)
    1626                                          return True
    1627                                  continue
    1628
    1629                          # editor is for unassociated new problem
    1630                          if page.problem is None:
    1631                                  continue
    1632
    1633                          # editor is for episode
    1634                          if page.problem['type'] == 'episode':
    1635                                  if page.problem['pk_episode'] == problem_to_add['pk_episode']:
    1636                                          self.SetSelection(page_idx)
    1637                                          gmDispatcher.send(signal = u'statustext', msg = u'Raising existing editor.', beep = True)
    1638                                          return True
    1639                                  continue
    1640
    1641                          # editor is for health issue
    1642                          if page.problem['type'] == 'issue':
    1643                                  if page.problem['pk_health_issue'] == problem_to_add['pk_health_issue']:
    1644                                          self.SetSelection(page_idx)
    1645                                          gmDispatcher.send(signal = u'statustext', msg = u'Raising existing editor.', beep = True)
    1646                                          return True
    1647                                  continue
    1648
    1649                  # - or add new editor
    1650                  new_page = cSoapNoteExpandoEditAreaPnl(parent = self, id = -1, problem = problem_to_add)
    1651                  result = self.AddPage (
    1652                          page = new_page,
    1653                          text = label,
    1654                          select = True
    1655                  )
    1656
    1657                  return result
    1658          #--------------------------------------------------------
    1659 -        def close_current_editor(self):
    1660
    1661                  page_idx = self.GetSelection()
    1662                  page = self.GetPage(page_idx)
    1663
    1664                  if not page.empty:
    1665                          really_discard = gmGuiHelpers.gm_show_question (
    1666                                  _('Are you sure you really want to\n'
    1667                                    'discard this progress note ?\n'
    1668                                  ),
    1669                                  _('Discarding progress note')
    1670                          )
    1671                          if really_discard is False:
    1672                                  return
    1673
    1674                  self.DeletePage(page_idx)
    1675
    1676                  # always keep one unassociated editor open
    1677                  if self.GetPageCount() == 0:
    1678                          self.add_editor()
    1679          #--------------------------------------------------------
    1680 -        def save_current_editor(self, emr=None, episode_name_candidates=None, encounter=None):
    1681
    1682                  page_idx = self.GetSelection()
    1683                  page = self.GetPage(page_idx)
    1684
    1685                  if not page.save(emr = emr, episode_name_candidates = episode_name_candidates, encounter = encounter):
    1686                          return
    1687
    1688                  self.DeletePage(page_idx)
    1689
    1690                  # always keep one unassociated editor open
    1691                  if self.GetPageCount() == 0:
    1692                          self.add_editor()
    1693          #--------------------------------------------------------
    1694 -        def warn_on_unsaved_soap(self):
    1695                  for page_idx in range(self.GetPageCount()):
    1696                          page = self.GetPage(page_idx)
    1697                          if page.empty:
    1698                                  continue
    1699
    1700                          gmGuiHelpers.gm_show_warning (
    1701                                  _('There are unsaved progress notes !\n'),
    1702                                  _('Unsaved progress notes')
    1703                          )
    1704                          return False
    1705
    1706                  return True
    1707          #--------------------------------------------------------
    1708 -        def save_all_editors(self, emr=None, episode_name_candidates=None):
    1709
    1710                  _log.debug('saving editors: %s', self.GetPageCount())
    1711
    1712                  all_closed = True
    1713                  for page_idx in range((self.GetPageCount() - 1), -1, -1):
    1714                          _log.debug('#%s of %s', page_idx, self.GetPageCount())
    1715                          try:
    1716                                  self.ChangeSelection(page_idx)
    1717                                  _log.debug('editor raised')
    1718                          except:
    1719                                  _log.exception('cannot raise editor')
    1720                          page = self.GetPage(page_idx)
    1721                          if page.save(emr = emr, episode_name_candidates = episode_name_candidates):
    1722                                  _log.debug('saved, deleting now')
    1723                                  self.DeletePage(page_idx)
    1724                          else:
    1725                                  _log.debug('not saved, not deleting')
    1726                                  all_closed = False
    1727
    1728                  # always keep one unassociated editor open
    1729                  if self.GetPageCount() == 0:
    1730                          self.add_editor()
    1731
    1732                  return (all_closed is True)
    1733          #--------------------------------------------------------
    1734 -        def clear_current_editor(self):
    1735                  page_idx = self.GetSelection()
    1736                  page = self.GetPage(page_idx)
    1737                  page.clear()
    1738          #--------------------------------------------------------
    1739 -        def get_current_problem(self):
    1740                  page_idx = self.GetSelection()
    1741                  page = self.GetPage(page_idx)
    1742                  return page.problem
    1743          #--------------------------------------------------------
    1744 -        def refresh_current_editor(self):
    1745                  page_idx = self.GetSelection()
    1746                  page = self.GetPage(page_idx)
    1747                  page.refresh()
    1748          #--------------------------------------------------------
    1749 -        def add_visual_progress_note_to_current_problem(self):
    1750                  page_idx = self.GetSelection()
    1751                  page = self.GetPage(page_idx)
    1752                  page.add_visual_progress_note()
    1753
    1754  #============================================================
    1755  from Gnumed.wxGladeWidgets import wxgSoapNoteExpandoEditAreaPnl
    1756
    1757 -class cSoapNoteExpandoEditAreaPnl(wxgSoapNoteExpandoEditAreaPnl.wxgSoapNoteExpandoEditAreaPnl):
    1758          """An Edit Area like panel for entering progress notes.
    1759
    1760          Subjective:                                     Codes:
    1761                  expando text ctrl
    1762          Objective:                                      Codes:
    1763                  expando text ctrl
    1764          Assessment:                                     Codes:
    1765                  expando text ctrl
    1766          Plan:                                           Codes:
    1767                  expando text ctrl
    1768          visual progress notes
    1769                  panel with images
    1770          Episode synopsis:                       Codes:
    1771                  text ctrl
    1772
    1773          - knows the problem this edit area is about
    1774          - can deal with issue or episode type problems
    1775          """
    1776
    1777 -        def __init__(self, *args, **kwargs):
    1778
    1779                  try:
    1780                          self.problem = kwargs['problem']
    1781                          del kwargs['problem']
    1782                  except KeyError:
    1783                          self.problem = None
    1784
    1785                  wxgSoapNoteExpandoEditAreaPnl.wxgSoapNoteExpandoEditAreaPnl.__init__(self, *args, **kwargs)
    1786
    1787                  self.soap_fields = [
    1788                          self._TCTRL_Soap,
    1789                          self._TCTRL_sOap,
    1790                          self._TCTRL_soAp,
    1791                          self._TCTRL_soaP
    1792                  ]
    1793
    1794                  self.__init_ui()
    1795                  self.__register_interests()
    1796          #--------------------------------------------------------
    1797 -        def __init_ui(self):
    1798                  self.refresh_summary()
    1799                  if self.problem is not None:
    1800                          if self.problem['summary'] is None:
    1801                                  self._TCTRL_episode_summary.SetValue(u'')
    1802                  self.refresh_visual_soap()
    1803          #--------------------------------------------------------
    1804 -        def refresh(self):
    1805                  self.refresh_summary()
    1806                  self.refresh_visual_soap()
    1807          #--------------------------------------------------------
    1808 -        def refresh_summary(self):
    1809                  self._TCTRL_episode_summary.SetValue(u'')
    1810                  self._PRW_episode_codes.SetText(u'', self._PRW_episode_codes.list2data_dict([]))
    1811                  self._LBL_summary.SetLabel(_('Episode synopsis'))
    1812
    1813                  # new problem ?
    1814                  if self.problem is None:
    1815                          return
    1816
    1817                  # issue-level problem ?
    1818                  if self.problem['type'] == u'issue':
    1819                          return
    1820
    1821                  # episode-level problem
    1822                  caption = _(u'Synopsis (%s)') % (
    1823                          gmDateTime.pydt_strftime (
    1824                                  self.problem['modified_when'],
    1825                                  format = '%B %Y',
    1826                                  accuracy = gmDateTime.acc_days
    1827                          )
    1828                  )
    1829                  self._LBL_summary.SetLabel(caption)
    1830
    1831                  if self.problem['summary'] is not None:
    1832                          self._TCTRL_episode_summary.SetValue(self.problem['summary'].strip())
    1833
    1834                  val, data = self._PRW_episode_codes.generic_linked_codes2item_dict(self.problem.generic_codes)
    1835                  self._PRW_episode_codes.SetText(val, data)
    1836          #--------------------------------------------------------
    1837 -        def refresh_visual_soap(self):
    1838                  if self.problem is None:
    1839                          self._PNL_visual_soap.refresh(document_folder = None)
    1840                          return
    1841
    1842                  if self.problem['type'] == u'issue':
    1843                          self._PNL_visual_soap.refresh(document_folder = None)
    1844                          return
    1845
    1846                  if self.problem['type'] == u'episode':
    1847                          pat = gmPerson.gmCurrentPatient()
    1848                          doc_folder = pat.get_document_folder()
    1849                          emr = pat.get_emr()
    1850                          self._PNL_visual_soap.refresh (
    1851                                  document_folder = doc_folder,
    1852                                  episodes = [self.problem['pk_episode']],
    1853                                  encounter = emr.active_encounter['pk_encounter']
    1854                          )
    1855                          return
    1856          #--------------------------------------------------------
    1857 -        def clear(self):
    1858                  for field in self.soap_fields:
    1859                          field.SetValue(u'')
    1860                  self._TCTRL_episode_summary.SetValue(u'')
    1861                  self._LBL_summary.SetLabel(_('Episode synopsis'))
    1862                  self._PRW_episode_codes.SetText(u'', self._PRW_episode_codes.list2data_dict([]))
    1863                  self._PNL_visual_soap.clear()
    1864          #--------------------------------------------------------
    1865 -        def add_visual_progress_note(self):
    1866                  fname, discard_unmodified = select_visual_progress_note_template(parent = self)
    1867                  if fname is None:
    1868                          return False
    1869
    1870                  if self.problem is None:
    1871                          issue = None
    1872                          episode = None
    1873                  elif self.problem['type'] == 'issue':
    1874                          issue = self.problem['pk_health_issue']
    1875                          episode = None
    1876                  else:
    1877                          issue = self.problem['pk_health_issue']
    1878                          episode = gmEMRStructItems.problem2episode(self.problem)
    1879
    1880                  wx.CallAfter (
    1881                          edit_visual_progress_note,
    1882                          filename = fname,
    1883                          episode = episode,
    1884                          discard_unmodified = discard_unmodified,
    1885                          health_issue = issue
    1886                  )
    1887          #--------------------------------------------------------
    1888 -        def save(self, emr=None, episode_name_candidates=None, encounter=None):
    1889
    1890                  if self.empty:
    1891                          return True
    1892
    1893                  # new episode (standalone=unassociated or new-in-issue)
    1894                  if (self.problem is None) or (self.problem['type'] == 'issue'):
    1895                          episode = self.__create_new_episode(emr = emr, episode_name_candidates = episode_name_candidates)
    1896                          # user cancelled
    1897                          if episode is None:
    1898                                  return False
    1899                  # existing episode
    1900                  else:
    1901                          episode = emr.problem2episode(self.problem)
    1902
    1903                  if encounter is None:
    1904                          encounter = emr.current_encounter['pk_encounter']
    1905
    1906                  soap_notes = []
    1907                  for note in self.soap:
    1908                          saved, data = gmClinNarrative.create_clin_narrative (
    1909                                  soap_cat = note[0],
    1910                                  narrative = note[1],
    1911                                  episode_id = episode['pk_episode'],
    1912                                  encounter_id = encounter
    1913                          )
    1914                          if saved:
    1915                                  soap_notes.append(data)
    1916
    1917                  # codes per narrative !
    1918  #               for note in soap_notes:
    1919  #                       if note['soap_cat'] == u's':
    1920  #                               codes = self._PRW_Soap_codes
    1921  #                       elif note['soap_cat'] == u'o':
    1922  #                       elif note['soap_cat'] == u'a':
    1923  #                       elif note['soap_cat'] == u'p':
    1924
    1925                  # set summary but only if not already set above for a
    1926                  # newly created episode (either standalone or within
    1927                  # a health issue)
    1928                  if self.problem is not None:
    1929                          if self.problem['type'] == 'episode':
    1930                                  episode['summary'] = self._TCTRL_episode_summary.GetValue().strip()
    1931                                  episode.save()
    1932
    1933                  # codes for episode
    1934                  episode.generic_codes = [ d['data'] for d in self._PRW_episode_codes.GetData() ]
    1935
    1936                  return True
    1937          #--------------------------------------------------------
    1938          # internal helpers
    1939          #--------------------------------------------------------
    1940 -        def __create_new_episode(self, emr=None, episode_name_candidates=None):
    1941
    1942                  episode_name_candidates.append(self._TCTRL_episode_summary.GetValue().strip())
    1943                  for candidate in episode_name_candidates:
    1944                          if candidate is None:
    1945                                  continue
    1946                          epi_name = candidate.strip().replace('\r', '//').replace('\n', '//')
    1947                          break
    1948
    1949                  dlg = wx.TextEntryDialog (
    1950                          parent = self,
    1951                          message = _('Enter a short working name for this new problem:'),
    1952                          caption = _('Creating a problem (episode) to save the notelet under ...'),
    1953                          defaultValue = epi_name,
    1954                          style = wx.OK | wx.CANCEL | wx.CENTRE
    1955                  )
    1956                  decision = dlg.ShowModal()
    1957                  if decision != wx.ID_OK:
    1958                          return None
    1959
    1960                  epi_name = dlg.GetValue().strip()
    1961                  if epi_name == u'':
    1962                          gmGuiHelpers.gm_show_error(_('Cannot save a new problem without a name.'), _('saving progress note'))
    1963                          return None
    1964
    1965                  # create episode
    1966                  new_episode = emr.add_episode(episode_name = epi_name[:45], pk_health_issue = None, is_open = True)
    1967                  new_episode['summary'] = self._TCTRL_episode_summary.GetValue().strip()
    1968                  new_episode.save()
    1969
    1970                  if self.problem is not None:
    1971                          issue = emr.problem2issue(self.problem)
    1972                          if not gmEMRStructWidgets.move_episode_to_issue(episode = new_episode, target_issue = issue, save_to_backend = True):
    1973                                  gmGuiHelpers.gm_show_warning (
    1974                                          _(
    1975                                                  'The new episode:\n'
    1976                                                  '\n'
    1977                                                  ' "%s"\n'
    1978                                                  '\n'
    1979                                                  'will remain unassociated despite the editor\n'
    1980                                                  'having been invoked from the health issue:\n'
    1981                                                  '\n'
    1982                                                  ' "%s"'
    1983                                          ) % (
    1984                                                  new_episode['description'],
    1985                                                  issue['description']
    1986                                          ),
    1987                                          _('saving progress note')
    1988                                  )
    1989
    1990                  return new_episode
    1991          #--------------------------------------------------------
    1992          # event handling
    1993          #--------------------------------------------------------
    1994 -        def __register_interests(self):
    1995                  for field in self.soap_fields:
    1996                          wx_expando.EVT_ETC_LAYOUT_NEEDED(field, field.GetId(), self._on_expando_needs_layout)
    1997                  wx_expando.EVT_ETC_LAYOUT_NEEDED(self._TCTRL_episode_summary, self._TCTRL_episode_summary.GetId(), self._on_expando_needs_layout)
    1998                  gmDispatcher.connect(signal = u'blobs.doc_obj_mod_db', receiver = self._refresh_visual_soap)
    1999          #--------------------------------------------------------
    2000 -        def _refresh_visual_soap(self):
    2001                  wx.CallAfter(self.refresh_visual_soap)
    2002          #--------------------------------------------------------
    2003 -        def _on_expando_needs_layout(self, evt):
    2004                  # need to tell ourselves to re-Layout to refresh scroll bars
    2005
    2006                  # provoke adding scrollbar if needed
    2007                  #self.Fit()                             # works on Linux but not on Windows
    2008                  self.FitInside()                # needed on Windows rather than self.Fit()
    2009
    2010                  if self.HasScrollbar(wx.VERTICAL):
    2011                          # scroll panel to show cursor
    2012                          expando = self.FindWindowById(evt.GetId())
    2013                          y_expando = expando.GetPositionTuple()[1]
    2014                          h_expando = expando.GetSizeTuple()[1]
    2015                          line_cursor = expando.PositionToXY(expando.GetInsertionPoint())[1] + 1
    2016                          if expando.NumberOfLines == 0:
    2017                                  no_of_lines = 1
    2018                          else:
    2019                                  no_of_lines = expando.NumberOfLines
    2020                          y_cursor = int(round((float(line_cursor) / no_of_lines) * h_expando))
    2021                          y_desired_visible = y_expando + y_cursor
    2022
    2023                          y_view = self.ViewStart[1]
    2024                          h_view = self.GetClientSizeTuple()[1]
    2025
    2026  #                       print "expando:", y_expando, "->", h_expando, ", lines:", expando.NumberOfLines
    2027  #                       print "cursor :", y_cursor, "at line", line_cursor, ", insertion point:", expando.GetInsertionPoint()
    2028  #                       print "wanted :", y_desired_visible
    2029  #                       print "view-y :", y_view
    2030  #                       print "scroll2:", h_view
    2031
    2032                          # expando starts before view
    2033                          if y_desired_visible < y_view:
    2034  #                               print "need to scroll up"
    2035                                  self.Scroll(0, y_desired_visible)
    2036
    2037                          if y_desired_visible > h_view:
    2038  #                               print "need to scroll down"
    2039                                  self.Scroll(0, y_desired_visible)
    2040          #--------------------------------------------------------
    2041          # properties
    2042          #--------------------------------------------------------
    2043 -        def _get_soap(self):
    2044                  soap_notes = []
    2045
    2046                  tmp = self._TCTRL_Soap.GetValue().strip()
    2047                  if tmp != u'':
    2048                          soap_notes.append(['s', tmp])
    2049
    2050                  tmp = self._TCTRL_sOap.GetValue().strip()
    2051                  if tmp != u'':
    2052                          soap_notes.append(['o', tmp])
    2053
    2054                  tmp = self._TCTRL_soAp.GetValue().strip()
    2055                  if tmp != u'':
    2056                          soap_notes.append(['a', tmp])
    2057
    2058                  tmp = self._TCTRL_soaP.GetValue().strip()
    2059                  if tmp != u'':
    2060                          soap_notes.append(['p', tmp])
    2061
    2062                  return soap_notes
    2063
    2064          soap = property(_get_soap, lambda x:x)
    2065          #--------------------------------------------------------
    2066 -        def _get_empty(self):
    2067
    2068                  # soap fields
    2069                  for field in self.soap_fields:
    2070                          if field.GetValue().strip() != u'':
    2071                                  return False
    2072
    2073                  # summary
    2074                  summary = self._TCTRL_episode_summary.GetValue().strip()
    2075                  if self.problem is None:
    2076                          if summary != u'':
    2077                                  return False
    2078                  elif self.problem['type'] == u'issue':
    2079                          if summary != u'':
    2080                                  return False
    2081                  else:
    2082                          if self.problem['summary'] is None:
    2083                                  if summary != u'':
    2084                                          return False
    2085                          else:
    2086                                  if summary != self.problem['summary'].strip():
    2087                                          return False
    2088
    2089                  # codes
    2090                  new_codes = self._PRW_episode_codes.GetData()
    2091                  if self.problem is None:
    2092                          if len(new_codes) > 0:
    2093                                  return False
    2094                  elif self.problem['type'] == u'issue':
    2095                          if len(new_codes) > 0:
    2096                                  return False
    2097                  else:
    2098                          old_code_pks = self.problem.generic_codes
    2099                          if len(old_code_pks) != len(new_codes):
    2100                                  return False
    2101                          for code in new_codes:
    2102                                  if code['data'] not in old_code_pks:
    2103                                          return False
    2104
    2105                  return True
    2106
    2107          empty = property(_get_empty, lambda x:x)
    2108  #============================================================
    2109 -class cSoapLineTextCtrl(wx_expando.ExpandoTextCtrl, gmKeywordExpansionWidgets.cKeywordExpansion_TextCtrlMixin):
    2110
    2111 -        def __init__(self, *args, **kwargs):
    2112
    2113                  wx_expando.ExpandoTextCtrl.__init__(self, *args, **kwargs)
    2114                  gmKeywordExpansionWidgets.cKeywordExpansion_TextCtrlMixin.__init__(self)
    2115                  self.enable_keyword_expansions()
    2116
    2117                  self.__register_interests()
    2118          #------------------------------------------------
    2119          # monkeypatch platform expando.py
    2120          #------------------------------------------------
    2121 -        def _wrapLine(self, line, dc, width):
    2122
    2123                  if (wx.MAJOR_VERSION >= 2) and (wx.MINOR_VERSION > 8):
    2124                          return wx_expando.ExpandoTextCtrl._wrapLine(line, dc, width)
    2125
    2126                  # THIS FIX LIFTED FROM TRUNK IN SVN:
    2127                  # Estimate where the control will wrap the lines and
    2128                  # return the count of extra lines needed.
    2129                  pte = dc.GetPartialTextExtents(line)
    2130                  width -= wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X)
    2131                  idx = 0
    2132                  start = 0
    2133                  count = 0
    2134                  spc = -1
    2135                  while idx < len(pte):
    2136                      if line[idx] == ' ':
    2137                          spc = idx
    2138                      if pte[idx] - start > width:
    2139                          # we've reached the max width, add a new line
    2140                          count += 1
    2141                          # did we see a space? if so restart the count at that pos
    2142                          if spc != -1:
    2143                              idx = spc + 1
    2144                              spc = -1
    2145                          if idx < len(pte):
    2146                              start = pte[idx]
    2147                      else:
    2148                          idx += 1
    2149                  return count
    2150          #------------------------------------------------
    2151          # event handling
    2152          #------------------------------------------------
    2153 -        def __register_interests(self):
    2154                  #wx.EVT_KEY_DOWN (self, self.__on_key_down)
    2155                  #wx.EVT_KEY_UP (self, self.__OnKeyUp)
    2156                  wx.EVT_SET_FOCUS(self, self.__on_focus)
    2157          #--------------------------------------------------------
    2158 -        def __on_focus(self, evt):
    2159                  evt.Skip()
    2160                  wx.CallAfter(self._after_on_focus)
    2161          #--------------------------------------------------------
    2162 -        def _after_on_focus(self):
    2163                  # robustify against PyDeadObjectError - since we are called
    2164                  # from wx.CallAfter this SoapCtrl may be gone by the time
    2165                  # we get to handling this layout request, say, on patient
    2166                  # change or some such
    2167                  if not self:
    2168                          return
    2169                  #wx.CallAfter(self._adjustCtrl)
    2170                  evt = wx.PyCommandEvent(wx_expando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId())
    2171                  evt.SetEventObject(self)
    2172                  #evt.height = None
    2173                  #evt.numLines = None
    2174                  #evt.height = self.GetSize().height
    2175                  #evt.numLines = self.GetNumberOfLines()
    2176                  self.GetEventHandler().ProcessEvent(evt)
    2177
    2178  #============================================================
    2179  # visual progress notes
    2180  #============================================================
    2181 -def configure_visual_progress_note_editor():
    2182
    2183          def is_valid(value):
    2184
    2185                  if value is None:
    2186                          gmDispatcher.send (
    2187                                  signal = 'statustext',
    2188                                  msg = _('You need to actually set an editor.'),
    2189                                  beep = True
    2190                          )
    2191                          return False, value
    2192
    2193                  if value.strip() == u'':
    2194                          gmDispatcher.send (
    2195                                  signal = 'statustext',
    2196                                  msg = _('You need to actually set an editor.'),
    2197                                  beep = True
    2198                          )
    2199                          return False, value
    2200
    2201                  found, binary = gmShellAPI.detect_external_binary(value)
    2202                  if not found:
    2203                          gmDispatcher.send (
    2204                                  signal = 'statustext',
    2205                                  msg = _('The command [%s] is not found.') % value,
    2206                                  beep = True
    2207                          )
    2208                          return True, value
    2209
    2210                  return True, binary
    2211          #------------------------------------------
    2212          cmd = gmCfgWidgets.configure_string_option (
    2213                  message = _(
    2214                          'Enter the shell command with which to start\n'
    2215                          'the image editor for visual progress notes.\n'
    2216                          '\n'
    2217                          'Any "%(img)s" included with the arguments\n'
    2218                          'will be replaced by the file name of the\n'
    2219                          'note template.'
    2220                  ),
    2221                  option = u'external.tools.visual_soap_editor_cmd',
    2222                  bias = 'user',
    2223                  default_value = None,
    2224                  validator = is_valid
    2225          )
    2226
    2227          return cmd
    2228  #============================================================
    2229 -def select_file_as_visual_progress_note_template(parent=None):
    2230          if parent is None:
    2231                  parent = wx.GetApp().GetTopWindow()
    2232
    2233          dlg = wx.FileDialog (
    2234                  parent = parent,
    2235                  message = _('Choose file to use as template for new visual progress note'),
    2236                  defaultDir = os.path.expanduser('~'),
    2237                  defaultFile = '',
    2238                  #wildcard = "%s (*)|*|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')),
    2239                  style = wx.OPEN | wx.HIDE_READONLY | wx.FILE_MUST_EXIST
    2240          )
    2241          result = dlg.ShowModal()
    2242
    2243          if result == wx.ID_CANCEL:
    2244                  dlg.Destroy()
    2245                  return None
    2246
    2247          full_filename = dlg.GetPath()
    2248          dlg.Hide()
    2249          dlg.Destroy()
    2250          return full_filename
    2251  #------------------------------------------------------------
    2252 -def select_visual_progress_note_template(parent=None):
    2253
    2254          if parent is None:
    2255                  parent = wx.GetApp().GetTopWindow()
    2256
    2257          dlg = gmGuiHelpers.c3ButtonQuestionDlg (
    2258                  parent,
    2259                  -1,
    2260                  caption = _('Visual progress note source'),
    2261                  question = _('From which source do you want to pick the image template ?'),
    2262                  button_defs = [
    2263                          {'label': _('Database'), 'tooltip': _('List of templates in the database.'), 'default': True},
    2264                          {'label': _('File'), 'tooltip': _('Files in the filesystem.'), 'default': False},
    2265                          {'label': _('Device'), 'tooltip': _('Image capture devices (scanners, cameras, etc)'), 'default': False}
    2266                  ]
    2267          )
    2268          result = dlg.ShowModal()
    2269          dlg.Destroy()
    2270
    2271          # 1) select from template
    2272          if result == wx.ID_YES:
    2273                  _log.debug('visual progress note template from: database template')
    2274                  from Gnumed.wxpython import gmFormWidgets
    2275                  template = gmFormWidgets.manage_form_templates (
    2276                          parent = parent,
    2277                          template_types = [gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE],
    2278                          active_only = True
    2279                  )
    2280                  if template is None:
    2281                          return (None, None)
    2282                  filename = template.export_to_file()
    2283                  if filename is None:
    2284                          gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note template for [%s].') % template['name_long'])
    2285                          return (None, None)
    2286                  return (filename, True)
    2287
    2288          # 2) select from disk file
    2289          if result == wx.ID_NO:
    2290                  _log.debug('visual progress note template from: disk file')
    2291                  fname = select_file_as_visual_progress_note_template(parent = parent)
    2292                  if fname is None:
    2293                          return (None, None)
    2294                  # create a copy of the picked file -- don't modify the original
    2295                  ext = os.path.splitext(fname)[1]
    2296                  tmp_name = gmTools.get_unique_filename(suffix = ext)
    2297                  _log.debug('visual progress note from file: [%s] -> [%s]', fname, tmp_name)
    2298                  shutil.copy2(fname, tmp_name)
    2299                  return (tmp_name, False)
    2300
    2301          # 3) acquire from capture device
    2302          if result == wx.ID_CANCEL:
    2303                  _log.debug('visual progress note template from: image capture device')
    2304                  fnames = gmDocumentWidgets.acquire_images_from_capture_device(device = None, calling_window = parent)
    2305                  if fnames is None:
    2306                          return (None, None)
    2307                  if len(fnames) == 0:
    2308                          return (None, None)
    2309                  return (fnames[0], False)
    2310
    2311          _log.debug('no visual progress note template source selected')
    2312          return (None, None)
    2313  #------------------------------------------------------------
    2314 -def edit_visual_progress_note(filename=None, episode=None, discard_unmodified=False, doc_part=None, health_issue=None):
    2315          """This assumes <filename> contains an image which can be handled by the configured image editor."""
    2316
    2317          if doc_part is not None:
    2318                  filename = doc_part.export_to_file()
    2319                  if filename is None:
    2320                          gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note to file.'))
    2321                          return None
    2322
    2323          dbcfg = gmCfg.cCfgSQL()
    2324          cmd = dbcfg.get2 (
    2325                  option = u'external.tools.visual_soap_editor_cmd',
    2326                  workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
    2327                  bias = 'user'
    2328          )
    2329
    2330          if cmd is None:
    2331                  gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = False)
    2332                  cmd = configure_visual_progress_note_editor()
    2333                  if cmd is None:
    2334                          gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = True)
    2335                          return None
    2336
    2337          if u'%(img)s' in cmd:
    2338                  cmd = cmd % {u'img': filename}
    2339          else:
    2340                  cmd = u'%s %s' % (cmd, filename)
    2341
    2342          if discard_unmodified:
    2343                  original_stat = os.stat(filename)
    2344                  original_md5 = gmTools.file2md5(filename)
    2345
    2346          success = gmShellAPI.run_command_in_shell(cmd, blocking = True)
    2347          if not success:
    2348                  gmGuiHelpers.gm_show_error (
    2349                          _(
    2350                                  'There was a problem with running the editor\n'
    2351                                  'for visual progress notes.\n'
    2352                                  '\n'
    2353                                  ' [%s]\n'
    2354                                  '\n'
    2355                          ) % cmd,
    2356                          _('Editing visual progress note')
    2357                  )
    2358                  return None
    2359
    2360          try:
    2361                  open(filename, 'r').close()
    2362          except StandardError:
    2363                  _log.exception('problem accessing visual progress note file [%s]', filename)
    2364                  gmGuiHelpers.gm_show_error (
    2365                          _(
    2366                                  'There was a problem reading the visual\n'
    2367                                  'progress note from the file:\n'
    2368                                  '\n'
    2369                                  ' [%s]\n'
    2370                                  '\n'
    2371                          ) % filename,
    2372                          _('Saving visual progress note')
    2373                  )
    2374                  return None
    2375
    2376          if discard_unmodified:
    2377                  modified_stat = os.stat(filename)
    2378                  # same size ?
    2379                  if original_stat.st_size == modified_stat.st_size:
    2380                          modified_md5 = gmTools.file2md5(filename)
    2381                          # same hash ?
    2382                          if original_md5 == modified_md5:
    2383                                  _log.debug('visual progress note (template) not modified')
    2384                                  # ask user to decide
    2385                                  msg = _(
    2386                                          u'You either created a visual progress note from a template\n'
    2387                                          u'in the database (rather than from a file on disk) or you\n'
    2388                                          u'edited an existing visual progress note.\n'
    2389                                          u'\n'
    2390                                          u'The template/original was not modified at all, however.\n'
    2391                                          u'\n'
    2392                                          u'Do you still want to save the unmodified image as a\n'
    2393                                          u'visual progress note into the EMR of the patient ?\n'
    2394                                  )
    2395                                  save_unmodified = gmGuiHelpers.gm_show_question (
    2396                                          msg,
    2397                                          _('Saving visual progress note')
    2398                                  )
    2399                                  if not save_unmodified:
    2400                                          _log.debug('user discarded unmodified note')
    2401                                          return
    2402
    2403          if doc_part is not None:
    2404                  _log.debug('updating visual progress note')
    2405                  doc_part.update_data_from_file(fname = filename)
    2406                  doc_part.set_reviewed(technically_abnormal = False, clinically_relevant = True)
    2407                  return None
    2408
    2409          if not isinstance(episode, gmEMRStructItems.cEpisode):
    2410                  if episode is None:
    2411                          episode = _('visual progress notes')
    2412                  pat = gmPerson.gmCurrentPatient()
    2413                  emr = pat.get_emr()
    2414                  episode = emr.add_episode(episode_name = episode.strip(), pk_health_issue = health_issue, is_open = False)
    2415
    2416          doc = gmDocumentWidgets.save_file_as_new_document (
    2417                  filename = filename,
    2418                  document_type = gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE,
    2419                  episode = episode,
    2420                  unlock_patient = False
    2421          )
    2422          doc.set_reviewed(technically_abnormal = False, clinically_relevant = True)
    2423
    2424          return doc
    2425  #============================================================
    2426 -class cVisualSoapTemplatePhraseWheel(gmPhraseWheel.cPhraseWheel):
    2427          """Phrasewheel to allow selection of visual SOAP template."""
    2428
    2429 -        def __init__(self, *args, **kwargs):
    2430
    2431                  gmPhraseWheel.cPhraseWheel.__init__ (self, *args, **kwargs)
    2432
    2433                  query = u"""
    2434  SELECT
    2435          pk AS data,
    2436          name_short AS list_label,
    2437          name_sort AS field_label
    2438  FROM
    2439          ref.paperwork_templates
    2440  WHERE
    2441          fk_template_type = (SELECT pk FROM ref.form_types WHERE name = '%s') AND (
    2442                  name_long %%(fragment_condition)s
    2443                          OR
    2444                  name_short %%(fragment_condition)s
    2445          )
    2446  ORDER BY list_label
    2447  LIMIT 15
    2448  """     % gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE
    2449
    2450                  mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
    2451                  mp.setThresholds(2, 3, 5)
    2452
    2453                  self.matcher = mp
    2454                  self.selection_only = True
    2455          #--------------------------------------------------------
    2456 -        def _data2instance(self):
    2457                  if self.GetData() is None:
    2458                          return None
    2459
    2460                  return gmForms.cFormTemplate(aPK_obj = self.GetData())
    2461  #============================================================
    2462  from Gnumed.wxGladeWidgets import wxgVisualSoapPresenterPnl
    2463
    2464 -class cVisualSoapPresenterPnl(wxgVisualSoapPresenterPnl.wxgVisualSoapPresenterPnl):
    2465
    2466 -        def __init__(self, *args, **kwargs):
    2467                  wxgVisualSoapPresenterPnl.wxgVisualSoapPresenterPnl.__init__(self, *args, **kwargs)
    2468                  self._SZR_soap = self.GetSizer()
    2469                  self.__bitmaps = []
    2470          #--------------------------------------------------------
    2471          # external API
    2472          #--------------------------------------------------------
    2473 -        def refresh(self, document_folder=None, episodes=None, encounter=None):
    2474
    2475                  self.clear()
    2476                  if document_folder is not None:
    2477                          soap_docs = document_folder.get_visual_progress_notes(episodes = episodes, encounter = encounter)
    2478                          if len(soap_docs) > 0:
    2479                                  for soap_doc in soap_docs:
    2480                                          parts = soap_doc.parts
    2481                                          if len(parts) == 0:
    2482                                                  continue
    2483                                          part = parts[0]
    2484                                          fname = part.export_to_file()
    2485                                          if fname is None:
    2486                                                  continue
    2487
    2488                                          # create bitmap
    2489                                          img = gmGuiHelpers.file2scaled_image (
    2490                                                  filename = fname,
    2491                                                  height = 30
    2492                                          )
    2493                                          #bmp = wx.StaticBitmap(self, -1, img, style = wx.NO_BORDER)
    2494                                          bmp = wx_genstatbmp.GenStaticBitmap(self, -1, img, style = wx.NO_BORDER)
    2495
    2496                                          # create tooltip
    2497                                          img = gmGuiHelpers.file2scaled_image (
    2498                                                  filename = fname,
    2499                                                  height = 150
    2500                                          )
    2501                                          tip = agw_stt.SuperToolTip (
    2502                                                  u'',
    2503                                                  bodyImage = img,
    2504                                                  header = _('Created: %s') % gmDateTime.pydt_strftime(part['date_generated'], '%Y %b %d'),
    2505                                                  footer = gmTools.coalesce(part['doc_comment'], u'').strip()
    2506                                          )
    2507                                          tip.SetTopGradientColor('white')
    2508                                          tip.SetMiddleGradientColor('white')
    2509                                          tip.SetBottomGradientColor('white')
    2510                                          tip.SetTarget(bmp)
    2511
    2512                                          bmp.doc_part = part
    2513                                          bmp.Bind(wx.EVT_LEFT_UP, self._on_bitmap_leftclicked)
    2514                                          # FIXME: add context menu for Delete/Clone/Add/Configure
    2515                                          self._SZR_soap.Add(bmp, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM | wx.EXPAND, 3)
    2516                                          self.__bitmaps.append(bmp)
    2517
    2518                  self.GetParent().Layout()
    2519          #--------------------------------------------------------
    2520 -        def clear(self):
    2521                  while len(self._SZR_soap.GetChildren()) > 0:
    2522                          self._SZR_soap.Detach(0)
    2523  #               for child_idx in range(len(self._SZR_soap.GetChildren())):
    2524  #                       self._SZR_soap.Detach(child_idx)
    2525                  for bmp in self.__bitmaps:
    2526                          bmp.Destroy()
    2527                  self.__bitmaps = []
    2528          #--------------------------------------------------------
    2529 -        def _on_bitmap_leftclicked(self, evt):
    2530                  wx.CallAfter (
    2531                          edit_visual_progress_note,
    2532                          doc_part = evt.GetEventObject().doc_part,
    2533                          discard_unmodified = True
    2534                  )
    2535
    2536  #============================================================
    2537  #============================================================
    2538  from Gnumed.wxGladeWidgets import wxgSimpleSoapPluginPnl
    2539
    2540 -class cSimpleSoapPluginPnl(wxgSimpleSoapPluginPnl.wxgSimpleSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
    2541 -        def __init__(self, *args, **kwargs):
    2542
    2543                  wxgSimpleSoapPluginPnl.wxgSimpleSoapPluginPnl.__init__(self, *args, **kwargs)
    2544                  gmRegetMixin.cRegetOnPaintMixin.__init__(self)
    2545
    2546                  self.__curr_pat = gmPerson.gmCurrentPatient()
    2547                  self.__problem = None
    2548                  self.__init_ui()
    2549                  self.__register_interests()
    2550          #-----------------------------------------------------
    2551          # internal API
    2552          #-----------------------------------------------------
    2553 -        def __init_ui(self):
    2554                  self._LCTRL_problems.set_columns(columns = [_('Problem list')])
    2555                  self._LCTRL_problems.activate_callback = self._on_problem_activated
    2556                  self._LCTRL_problems.item_tooltip_callback = self._on_get_problem_tooltip
    2557
    2558                  self._splitter_main.SetSashGravity(0.5)
    2559                  splitter_width = self._splitter_main.GetSizeTuple()[0]
    2560                  self._splitter_main.SetSashPosition(splitter_width / 2, True)
    2561
    2562                  self._TCTRL_soap.Disable()
    2563                  self._BTN_save_soap.Disable()
    2564                  self._BTN_clear_soap.Disable()
    2565          #-----------------------------------------------------
    2566 -        def __reset_ui(self):
    2567                  self._LCTRL_problems.set_string_items()
    2568                  self._TCTRL_soap_problem.SetValue(_('<above, double-click problem to start entering SOAP note>'))
    2569                  self._TCTRL_soap.SetValue(u'')
    2570                  self._CHBOX_filter_by_problem.SetLabel(_('&Filter by problem'))
    2571                  self._TCTRL_journal.SetValue(u'')
    2572
    2573                  self._TCTRL_soap.Disable()
    2574                  self._BTN_save_soap.Disable()
    2575                  self._BTN_clear_soap.Disable()
    2576          #-----------------------------------------------------
    2577 -        def __save_soap(self):
    2578                  if not self.__curr_pat.connected:
    2579                          return None
    2580
    2581                  if self.__problem is None:
    2582                          return None
    2583
    2584                  saved = self.__curr_pat.emr.add_clin_narrative (
    2585                          note = self._TCTRL_soap.GetValue().strip(),
    2586                          soap_cat = u'u',
    2587                          episode = self.__problem
    2588                  )
    2589
    2590                  if saved is None:
    2591                          return False
    2592
    2593                  self._TCTRL_soap.SetValue(u'')
    2594                  self.__refresh_journal()
    2595                  return True
    2596          #-----------------------------------------------------
    2597 -        def __perhaps_save_soap(self):
    2598                  if self._TCTRL_soap.GetValue().strip() == u'':
    2599                          return True
    2600                  if self.__problem is None:
    2601                          # FIXME: this could potentially lose input
    2602                          self._TCTRL_soap.SetValue(u'')
    2603                          return None
    2604                  save_it = gmGuiHelpers.gm_show_question (
    2605                          title = _('Saving SOAP note'),
    2606                          question = _('Do you want to save the SOAP note ?')
    2607                  )
    2608                  if save_it:
    2609                          return self.__save_soap()
    2610                  return False
    2611          #-----------------------------------------------------
    2612 -        def __refresh_problem_list(self):
    2613                  self._LCTRL_problems.set_string_items()
    2614                  emr = self.__curr_pat.get_emr()
    2615                  epis = emr.get_episodes(open_status = True)
    2616                  if len(epis) > 0:
    2617                          self._LCTRL_problems.set_string_items(items = [ u'%s%s' % (
    2618                                  e['description'],
    2619                                  gmTools.coalesce(e['health_issue'], u'', u' (%s)')
    2620                          ) for e in epis ])
    2621                          self._LCTRL_problems.set_data(epis)
    2622          #-----------------------------------------------------
    2623 -        def __refresh_journal(self):
    2624                  self._TCTRL_journal.SetValue(u'')
    2625                  epi = self._LCTRL_problems.get_selected_item_data(only_one = True)
    2626
    2627                  if epi is not None:
    2628                          self._CHBOX_filter_by_problem.SetLabel(_('&Filter by problem %s%s%s') % (
    2629                                  gmTools.u_left_double_angle_quote,
    2630                                  epi['description'],
    2631                                  gmTools.u_right_double_angle_quote
    2632                          ))
    2633                          self._CHBOX_filter_by_problem.Refresh()
    2634
    2635                  if not self._CHBOX_filter_by_problem.IsChecked():
    2636                          self._TCTRL_journal.SetValue(self.__curr_pat.emr.format_summary())
    2637                          return
    2638
    2639                  if epi is None:
    2640                          return
    2641
    2642                  self._TCTRL_journal.SetValue(epi.format_as_journal())
    2643          #-----------------------------------------------------
    2644          # event handling
    2645          #-----------------------------------------------------
    2646 -        def __register_interests(self):
    2647                  """Configure enabled event signals."""
    2648                  # client internal signals
    2649                  gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
    2650                  gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
    2651                  gmDispatcher.connect(signal = u'clin.episode_mod_db', receiver = self._on_episode_issue_mod_db)
    2652                  gmDispatcher.connect(signal = u'clin.health_issue_mod_db', receiver = self._on_episode_issue_mod_db)
    2653
    2654                  # synchronous signals
    2655                  self.__curr_pat.register_pre_selection_callback(callback = self._pre_selection_callback)
    2656                  gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
    2657          #-----------------------------------------------------
    2658 -        def _pre_selection_callback(self):
    2659                  """Another patient is about to be activated.
    2660
    2661                  Patient change will not proceed before this returns True.
    2662                  """
    2663                  if not self.__curr_pat.connected:
    2664                          return True
    2665                  self.__perhaps_save_soap()
    2666                  self.__problem = None
    2667                  return True
    2668          #-----------------------------------------------------
    2669 -        def _pre_exit_callback(self):
    2670                  """The client is about to be shut down.
    2671
    2672                  Shutdown will not proceed before this returns.
    2673                  """
    2674                  if not self.__curr_pat.connected:
    2675                          return
    2676                  if not self.__save_soap():
    2677                          gmDispatcher.send(signal = 'statustext', msg = _('Cannot save SimpleNotes SOAP note.'), beep = True)
    2678                  return
    2679          #-----------------------------------------------------
    2680 -        def _on_pre_patient_selection(self):
    2681                  wx.CallAfter(self.__reset_ui)
    2682          #-----------------------------------------------------
    2683 -        def _on_post_patient_selection(self):
    2684                  wx.CallAfter(self._schedule_data_reget)
    2685          #-----------------------------------------------------
    2686 -        def _on_episode_issue_mod_db(self):
    2687                  wx.CallAfter(self._schedule_data_reget)
    2688          #-----------------------------------------------------
    2689 -        def _on_problem_activated(self, event):
    2690                  self.__perhaps_save_soap()
    2691                  epi = self._LCTRL_problems.get_selected_item_data(only_one = True)
    2692                  self._TCTRL_soap_problem.SetValue(_('Progress note: %s%s') % (
    2693                          epi['description'],
    2694                          gmTools.coalesce(epi['health_issue'], u'', u' (%s)')
    2695                  ))
    2696                  self.__problem = epi
    2697                  self._TCTRL_soap.SetValue(u'')
    2698
    2699                  self._TCTRL_soap.Enable()
    2700                  self._BTN_save_soap.Enable()
    2701                  self._BTN_clear_soap.Enable()
    2702          #-----------------------------------------------------
    2703 -        def _on_get_problem_tooltip(self, episode):
    2704                  return episode.format (
    2705                          patient = self.__curr_pat,
    2706                          with_summary = False,
    2707                          with_codes = True,
    2708                          with_encounters = False,
    2709                          with_documents = False,
    2710                          with_hospital_stays = False,
    2711                          with_procedures = False,
    2712                          with_family_history = False,
    2713                          with_tests = False,
    2714                          with_vaccinations = False,
    2715                          with_health_issue = True
    2716                  )
    2717          #-----------------------------------------------------
    2718 -        def _on_list_item_selected(self, event):
    2719                  event.Skip()
    2720                  self.__refresh_journal()
    2721          #-----------------------------------------------------
    2722 -        def _on_filter_by_problem_checked(self, event):
    2723                  event.Skip()
    2724                  self.__refresh_journal()
    2725          #-----------------------------------------------------
    2726 -        def _on_add_problem_button_pressed(self, event):
    2727                  event.Skip()
    2728                  epi_name = wx.GetTextFromUser (
    2729                          _('Please enter a name for the new problem:'),
    2730                          caption = _('Adding a problem'),
    2731                          parent = self
    2732                  ).strip()
    2733                  if epi_name == u'':
    2734                          return
    2735                  self.__curr_pat.emr.add_episode (
    2736                          episode_name = epi_name,
    2737                          pk_health_issue = None,
    2738                          is_open = True
    2739                  )
    2740          #-----------------------------------------------------
    2741 -        def _on_edit_problem_button_pressed(self, event):
    2742                  event.Skip()
    2743                  epi = self._LCTRL_problems.get_selected_item_data(only_one = True)
    2744                  if epi is None:
    2745                          return
    2746                  gmEMRStructWidgets.edit_episode(parent = self, episode = epi)
    2747          #-----------------------------------------------------
    2748 -        def _on_delete_problem_button_pressed(self, event):
    2749                  event.Skip()
    2750                  epi = self._LCTRL_problems.get_selected_item_data(only_one = True)
    2751                  if epi is None:
    2752                          return
    2753                  if not gmEMRStructItems.delete_episode(episode = epi):
    2754                          gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete problem. There is still clinical data recorded for it.'))
    2755          #-----------------------------------------------------
    2756 -        def _on_save_soap_button_pressed(self, event):
    2757                  event.Skip()
    2758                  self.__save_soap()
    2759          #-----------------------------------------------------
    2760 -        def _on_clear_soap_button_pressed(self, event):
    2761                  event.Skip()
    2762                  self._TCTRL_soap.SetValue(u'')
    2763          #-----------------------------------------------------
    2764          # reget-on-paint mixin API
    2765          #-----------------------------------------------------
    2766 -        def _populate_with_data(self):
    2767                  self.__refresh_problem_list()
    2768                  self.__refresh_journal()
    2769                  self._TCTRL_soap.SetValue(u'')
    2770                  return True
    2771
    2772  #============================================================
    2773  # main
    2774  #------------------------------------------------------------
    2775  if __name__ == '__main__':
    2776
    2777          if len(sys.argv) < 2:
    2778                  sys.exit()
    2779
    2780          if sys.argv[1] != 'test':
    2781                  sys.exit()
    2782
    2783          gmI18N.activate_locale()
    2784          gmI18N.install_domain(domain = 'gnumed')
    2785
    2786          #----------------------------------------
    2787 -        def test_select_narrative_from_episodes():
    2788                  pat = gmPersonSearch.ask_for_patient()
    2789                  set_active_patient(patient = pat)
    2790                  app = wx.PyWidgetTester(size = (200, 200))
    2791                  sels = select_narrative_from_episodes_new()
    2792                  print "selected:"
    2793                  for sel in sels:
    2794                          print sel
    2795          #----------------------------------------
    2796 -        def test_select_narrative():
    2797                  pat = gmPersonSearch.ask_for_patient()
    2798                  set_active_patient(patient = pat)
    2799                  app = wx.PyWidgetTester(size = (200, 200))
    2800                  sels = select_narrative(parent=None, soap_cats = None)
    2801                  print "selected:"
    2802                  for sel in sels:
    2803                          print sel
    2804          #----------------------------------------
    2805 -        def test_cSoapNoteExpandoEditAreaPnl():
    2806                  pat = gmPersonSearch.ask_for_patient()
    2807                  application = wx.PyWidgetTester(size=(800,500))
    2808                  soap_input = cSoapNoteExpandoEditAreaPnl(application.frame, -1)
    2809                  application.frame.Show(True)
    2810                  application.MainLoop()
    2811          #----------------------------------------
    2812 -        def test_cSoapPluginPnl():
    2813                  patient = gmPersonSearch.ask_for_patient()
    2814                  if patient is None:
    2815                          print "No patient. Exiting gracefully..."
    2816                          return
    2817                  set_active_patient(patient=patient)
    2818
    2819                  application = wx.PyWidgetTester(size=(800,500))
    2820                  soap_input = cSoapPluginPnl(application.frame, -1)
    2821                  application.frame.Show(True)
    2822                  soap_input._schedule_data_reget()
    2823                  application.MainLoop()
    2824          #----------------------------------------
    2825          #test_select_narrative_from_episodes()
    2826          test_select_narrative()
    2827          #test_cSoapNoteExpandoEditAreaPnl()
    2828          #test_cSoapPluginPnl()
    2829
    2830  #============================================================
    2831