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

# Source Code for Module Gnumed.wxpython.gmDocumentWidgets

       1  # -*- coding: utf-8 -*-
       2  #============================================================
       3
       4
       5  __doc__ = """GNUmed medical document handling widgets."""
       6
       7  __license__ = "GPL v2 or later"
       8  __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>"
       9
      10  #============================================================
      11  import os.path
      12  import os
      13  import sys
      14  import re as regex
      15  import logging
      16  import datetime as pydt
      17
      18
      19  import wx
      20  import wx.lib.mixins.treemixin as treemixin
      21
      22
      23  if __name__ == '__main__':
      24          sys.path.insert(0, '../../')
      25  from Gnumed.pycommon import gmI18N
      26  if __name__ == '__main__':
      27          gmI18N.activate_locale()
      28          gmI18N.install_domain(domain = 'gnumed')
      29  from Gnumed.pycommon import gmCfg
      30  from Gnumed.pycommon import gmCfg2
      31  from Gnumed.pycommon import gmPG2
      32  from Gnumed.pycommon import gmMimeLib
      33  from Gnumed.pycommon import gmMatchProvider
      34  from Gnumed.pycommon import gmDispatcher
      35  from Gnumed.pycommon import gmDateTime
      36  from Gnumed.pycommon import gmTools
      37  from Gnumed.pycommon import gmShellAPI
      38  from Gnumed.pycommon import gmHooks
      39  from Gnumed.pycommon import gmNetworkTools
      40  from Gnumed.pycommon import gmMimeLib
      41
      42  from Gnumed.business import gmPerson
      43  from Gnumed.business import gmStaff
      44  from Gnumed.business import gmDocuments
      45  from Gnumed.business import gmEMRStructItems
      46  from Gnumed.business import gmPraxis
      47  from Gnumed.business import gmDICOM
      48  from Gnumed.business import gmProviderInbox
      49  from Gnumed.business import gmOrganization
      50
      51  from Gnumed.wxpython import gmGuiHelpers
      52  from Gnumed.wxpython import gmRegetMixin
      53  from Gnumed.wxpython import gmPhraseWheel
      54  from Gnumed.wxpython import gmPlugin
      55  from Gnumed.wxpython import gmEncounterWidgets
      56  from Gnumed.wxpython import gmListWidgets
      57  from Gnumed.wxpython import gmRegetMixin
      58
      59
      60  _log = logging.getLogger('gm.ui')
      61
      62
      63  default_chunksize = 1 * 1024 * 1024             # 1 MB
      64
      65  #============================================================
      66 -def manage_document_descriptions(parent=None, document=None):
      67
      68          #-----------------------------------
      69          def delete_item(item):
      70                  doit = gmGuiHelpers.gm_show_question (
      71                          _(      'Are you sure you want to delete this\n'
      72                                  'description from the document ?\n'
      73                          ),
      74                          _('Deleting document description')
      75                  )
      76                  if not doit:
      77                          return True
      78
      79                  document.delete_description(pk = item[0])
      80                  return True
      81          #-----------------------------------
      82          def add_item():
      83                  dlg = gmGuiHelpers.cMultilineTextEntryDlg (
      84                          parent,
      85                          -1,
      86                          title = _('Adding document description'),
      87                          msg = _('Below you can add a document description.\n')
      88                  )
      89                  result = dlg.ShowModal()
      90                  if result == wx.ID_SAVE:
      91                          document.add_description(dlg.value)
      92
      93                  dlg.DestroyLater()
      94                  return True
      95          #-----------------------------------
      96          def edit_item(item):
      97                  dlg = gmGuiHelpers.cMultilineTextEntryDlg (
      98                          parent,
      99                          -1,
     100                          title = _('Editing document description'),
     101                          msg = _('Below you can edit the document description.\n'),
     102                          text = item[1]
     103                  )
     104                  result = dlg.ShowModal()
     105                  if result == wx.ID_SAVE:
     106                          document.update_description(pk = item[0], description = dlg.value)
     107
     108                  dlg.DestroyLater()
     109                  return True
     110          #-----------------------------------
     111          def refresh_list(lctrl):
     112                  descriptions = document.get_descriptions()
     113
     114                  lctrl.set_string_items(items = [
     115                          '%s%s' % ( (' '.join(regex.split('\r\n+|\r+|\n+|\t+', desc[1])))[:30], gmTools.u_ellipsis )
     116                          for desc in descriptions
     117                  ])
     118                  lctrl.set_data(data = descriptions)
     119          #-----------------------------------
     120
     121          gmListWidgets.get_choices_from_list (
     122                  parent = parent,
     123                  msg = _('Select the description you are interested in.\n'),
     124                  caption = _('Managing document descriptions'),
     125                  columns = [_('Description')],
     126                  edit_callback = edit_item,
     127                  new_callback = add_item,
     128                  delete_callback = delete_item,
     129                  refresh_callback = refresh_list,
     130                  single_selection = True,
     131                  can_return_empty = True
     132          )
     133
     134          return True
     135
     136  #============================================================
     137 -def _save_file_as_new_document(**kwargs):
     138          try:
     139                  del kwargs['signal']
     140                  del kwargs['sender']
     141          except KeyError:
     142                  pass
     143          wx.CallAfter(save_file_as_new_document, **kwargs)
     144
     145 -def _save_files_as_new_document(**kwargs):
     146          try:
     147                  del kwargs['signal']
     148                  del kwargs['sender']
     149          except KeyError:
     150                  pass
     151          wx.CallAfter(save_files_as_new_document, **kwargs)
     152  #----------------------
     153 -def save_file_as_new_document(parent=None, filename=None, document_type=None, unlock_patient=False, episode=None, review_as_normal=False, pk_org_unit=None):
     154          return save_files_as_new_document (
     155                  parent = parent,
     156                  filenames = [filename],
     157                  document_type = document_type,
     158                  unlock_patient = unlock_patient,
     159                  episode = episode,
     160                  review_as_normal = review_as_normal,
     161                  pk_org_unit = pk_org_unit
     162          )
     163
     164  #----------------------
     165 -def save_files_as_new_document(parent=None, filenames=None, document_type=None, unlock_patient=False, episode=None, review_as_normal=False, reference=None, pk_org_unit=None, date_generated=None, comment=None, reviewer=None, pk_document_type=None):
     166
     167          pat = gmPerson.gmCurrentPatient()
     168          if not pat.connected:
     169                  return None
     170
     171          emr = pat.emr
     172
     173          if parent is None:
     174                  parent = wx.GetApp().GetTopWindow()
     175
     176          if episode is None:
     177                  all_epis = emr.get_episodes()
     178                  # FIXME: what to do here ? probably create dummy episode
     179                  if len(all_epis) == 0:
     180                          episode = emr.add_episode(episode_name = _('Documents'), is_open = False)
     181                  else:
     182                          from Gnumed.wxpython.gmEMRStructWidgets import cEpisodeListSelectorDlg
     183                          dlg = cEpisodeListSelectorDlg(parent, -1, episodes = all_epis)
     184                          dlg.SetTitle(_('Select the episode under which to file the document ...'))
     185                          btn_pressed = dlg.ShowModal()
     186                          episode = dlg.get_selected_item_data(only_one = True)
     187                          dlg.DestroyLater()
     188
     189                          if (btn_pressed == wx.ID_CANCEL) or (episode is None):
     190                                  if unlock_patient:
     191                                          pat.locked = False
     192                                  return None
     193
     194          wx.BeginBusyCursor()
     195
     196          if pk_document_type is None:
     197                  pk_document_type = gmDocuments.create_document_type(document_type = document_type)['pk_doc_type']
     198
     199          docs_folder = pat.get_document_folder()
     200          doc = docs_folder.add_document (
     201                  document_type = pk_document_type,
     202                  encounter = emr.active_encounter['pk_encounter'],
     203                  episode = episode['pk_episode']
     204          )
     205          if doc is None:
     206                  wx.EndBusyCursor()
     207                  gmGuiHelpers.gm_show_error (
     208                          aMessage = _('Cannot create new document.'),
     209                          aTitle = _('saving document')
     210                  )
     211                  return False
     212
     213          if reference is not None:
     214                  doc['ext_ref'] = reference
     215          if pk_org_unit is not None:
     216                  doc['pk_org_unit'] = pk_org_unit
     217          if date_generated is not None:
     218                  doc['clin_when'] = date_generated
     219          if comment is not None:
     220                  if comment != '':
     221                          doc['comment'] = comment
     222          doc.save()
     223
     224          success, msg, filename = doc.add_parts_from_files(files = filenames, reviewer = reviewer)
     225          if not success:
     226                  wx.EndBusyCursor()
     227                  gmGuiHelpers.gm_show_error (
     228                          aMessage = msg,
     229                          aTitle = _('saving document')
     230                  )
     231                  return False
     232
     233          if review_as_normal:
     234                  doc.set_reviewed(technically_abnormal = False, clinically_relevant = False)
     235
     236          if unlock_patient:
     237                  pat.locked = False
     238
     239          gmDispatcher.send(signal = 'statustext', msg = _('Imported new document from %s.') % filenames, beep = True)
     240
     241          # inform user
     242          cfg = gmCfg.cCfgSQL()
     243          show_id = bool (
     244                  cfg.get2 (
     245                          option = 'horstspace.scan_index.show_doc_id',
     246                          workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
     247                          bias = 'user'
     248                  )
     249          )
     250
     251          wx.EndBusyCursor()
     252
     253          if not show_id:
     254                  gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved new document.'))
     255          else:
     256                  if reference is None:
     257                          msg = _('Successfully saved the new document.')
     258                  else:
     259                          msg = _('The reference ID for the new document is:\n'
     260                                          '\n'
     261                                          ' <%s>\n'
     262                                          '\n'
     263                                          'You probably want to write it down on the\n'
     264                                          'original documents.\n'
     265                                          '\n'
     266                                          "If you don't care about the ID you can switch\n"
     267                                          'off this message in the GNUmed configuration.\n'
     268                          ) % reference
     269                  gmGuiHelpers.gm_show_info (
     270                          aMessage = msg,
     271                          aTitle = _('Saving document')
     272                  )
     273
     274          # remove non-temp files
     275          tmp_dir = gmTools.gmPaths().tmp_dir
     276          files2remove = [ f for f in filenames if not f.startswith(tmp_dir) ]
     277          if len(files2remove) > 0:
     278                  do_delete = gmGuiHelpers.gm_show_question (
     279                          _(      'Successfully imported files as document.\n'
     280                                  '\n'
     281                                  'Do you want to delete imported files from the filesystem ?\n'
     282                                  '\n'
     283                                  ' %s'
     284                          ) % '\n '.join(files2remove),
     285                          _('Removing files')
     286                  )
     287                  if do_delete:
     288                          for fname in files2remove:
     289                                  gmTools.remove_file(fname)
     290
     291          return doc
     292
     293  #----------------------
     294  gmDispatcher.connect(signal = 'import_document_from_file', receiver = _save_file_as_new_document)
     295  gmDispatcher.connect(signal = 'import_document_from_files', receiver = _save_files_as_new_document)
     296
     297  #============================================================
     298 -class cDocumentPhraseWheel(gmPhraseWheel.cPhraseWheel):
     299
     300 -        def __init__(self, *args, **kwargs):
     301
     302                  gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
     303
     304                  ctxt = {'ctxt_pat': {
     305                          'where_part': '(pk_patient = %(pat)s) AND',
     306                          'placeholder': 'pat'
     307                  }}
     308
     309                  mp = gmMatchProvider.cMatchProvider_SQL2 (
     310                          queries = ["""
     311  SELECT DISTINCT ON (list_label)
     312          pk_doc AS data,
     313          l10n_type || ' (' || to_char(clin_when, 'YYYY Mon DD') || ')' || coalesce(': ' || unit || '@' || organization, '') || ' - ' || episode || coalesce(' (' || health_issue || ')', '') AS list_label,
     314          l10n_type || ' (' || to_char(clin_when, 'YYYY Mon DD') || ')' || coalesce(': ' || organization, '') || ' - ' || coalesce(' (' || health_issue || ')', episode) AS field_label
     315  FROM blobs.v_doc_med
     316  WHERE
     317                  %(ctxt_pat)s
     318  (
     319          l10n_type %(fragment_condition)s
     320                  OR
     321          unit %(fragment_condition)s
     322                  OR
     323          organization %(fragment_condition)s
     324                  OR
     325          episode %(fragment_condition)s
     326                  OR
     327          health_issue %(fragment_condition)s
     328  )
     329  ORDER BY list_label
     330  LIMIT 25"""],
     331                          context = ctxt
     332                  )
     333                  mp.setThresholds(1, 3, 5)
     334                  #mp.unset_context('pat')
     335                  pat = gmPerson.gmCurrentPatient()
     336                  mp.set_context('pat', pat.ID)
     337
     338                  self.matcher = mp
     339                  self.picklist_delay = 50
     340                  self.selection_only = True
     341
     342                  self.SetToolTip(_('Select a document.'))
     343
     344          #--------------------------------------------------------
     345 -        def _data2instance(self):
     346                  if len(self._data) == 0:
     347                          return None
     348                  return gmDocuments.cDocument(aPK_obj = self.GetData())
     349
     350          #--------------------------------------------------------
     351 -        def _get_data_tooltip(self):
     352                  if len(self._data) == 0:
     353                          return ''
     354                  return gmDocuments.cDocument(aPK_obj = self.GetData()).format(single_line = False)
     355
     356  #============================================================
     357 -class cDocumentCommentPhraseWheel(gmPhraseWheel.cPhraseWheel):
     358          """Let user select a document comment from all existing comments."""
     359 -        def __init__(self, *args, **kwargs):
     360
     361                  gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
     362
     363                  context = {
     364                          'ctxt_doc_type': {
     365                                  'where_part': 'and fk_type = %(pk_doc_type)s',
     366                                  'placeholder': 'pk_doc_type'
     367                          }
     368                  }
     369
     370                  mp = gmMatchProvider.cMatchProvider_SQL2 (
     371                          queries = ["""
     372  SELECT
     373          data,
     374          field_label,
     375          list_label
     376  FROM (
     377          SELECT DISTINCT ON (field_label) *
     378          FROM (
     379                  -- constrained by doc type
     380                  SELECT
     381                          comment AS data,
     382                          comment AS field_label,
     383                          comment AS list_label,
     384                          1 AS rank
     385                  FROM blobs.doc_med
     386                  WHERE
     387                          comment %(fragment_condition)s
     388                          %(ctxt_doc_type)s
     389
     390                  UNION ALL
     391
     392                  SELECT
     393                          comment AS data,
     394                          comment AS field_label,
     395                          comment AS list_label,
     396                          2 AS rank
     397                  FROM blobs.doc_med
     398                  WHERE
     399                          comment %(fragment_condition)s
     400          ) AS q_union
     401  ) AS q_distinct
     402  ORDER BY rank, list_label
     403  LIMIT 25"""],
     404                          context = context
     405                  )
     406                  mp.setThresholds(3, 5, 7)
     407                  mp.unset_context('pk_doc_type')
     408
     409                  self.matcher = mp
     410                  self.picklist_delay = 50
     411
     412                  self.SetToolTip(_('Enter a comment on the document.'))
     413
     414  #============================================================
     415  # document type widgets
     416  #============================================================
     417 -def manage_document_types(parent=None):
     418
     419          if parent is None:
     420                  parent = wx.GetApp().GetTopWindow()
     421
     422          dlg = cEditDocumentTypesDlg(parent = parent)
     423          dlg.ShowModal()
     424
     425  #============================================================
     426  from Gnumed.wxGladeWidgets import wxgEditDocumentTypesDlg
     427
     428 -class cEditDocumentTypesDlg(wxgEditDocumentTypesDlg.wxgEditDocumentTypesDlg):
     429          """A dialog showing a cEditDocumentTypesPnl."""
     430
     431 -        def __init__(self, *args, **kwargs):
     432                  wxgEditDocumentTypesDlg.wxgEditDocumentTypesDlg.__init__(self, *args, **kwargs)
     433
     434  #============================================================
     435  from Gnumed.wxGladeWidgets import wxgEditDocumentTypesPnl
     436
     437 -class cEditDocumentTypesPnl(wxgEditDocumentTypesPnl.wxgEditDocumentTypesPnl):
     438          """A panel grouping together fields to edit the list of document types."""
     439
     440 -        def __init__(self, *args, **kwargs):
     441                  wxgEditDocumentTypesPnl.wxgEditDocumentTypesPnl.__init__(self, *args, **kwargs)
     442                  self.__init_ui()
     443                  self.__register_interests()
     444                  self.repopulate_ui()
     445          #--------------------------------------------------------
     446 -        def __init_ui(self):
     447                  self._LCTRL_doc_type.set_columns([_('Type'), _('Translation'), _('User defined'), _('In use')])
     448                  self._LCTRL_doc_type.set_column_widths()
     449          #--------------------------------------------------------
     450 -        def __register_interests(self):
     451                  gmDispatcher.connect(signal = 'blobs.doc_type_mod_db', receiver = self._on_doc_type_mod_db)
     452          #--------------------------------------------------------
     453 -        def _on_doc_type_mod_db(self):
     454                  self.repopulate_ui()
     455          #--------------------------------------------------------
     456 -        def repopulate_ui(self):
     457
     458                  self._LCTRL_doc_type.DeleteAllItems()
     459
     460                  doc_types = gmDocuments.get_document_types()
     461                  pos = len(doc_types) + 1
     462
     463                  for doc_type in doc_types:
     464                          row_num = self._LCTRL_doc_type.InsertItem(pos, label = doc_type['type'])
     465                          self._LCTRL_doc_type.SetItem(index = row_num, column = 1, label = doc_type['l10n_type'])
     466                          if doc_type['is_user_defined']:
     467                                  self._LCTRL_doc_type.SetItem(index = row_num, column = 2, label = ' X ')
     468                          if doc_type['is_in_use']:
     469                                  self._LCTRL_doc_type.SetItem(index = row_num, column = 3, label = ' X ')
     470
     471                  if len(doc_types) > 0:
     472                          self._LCTRL_doc_type.set_data(data = doc_types)
     473                          self._LCTRL_doc_type.SetColumnWidth(0, wx.LIST_AUTOSIZE)
     474                          self._LCTRL_doc_type.SetColumnWidth(1, wx.LIST_AUTOSIZE)
     475                          self._LCTRL_doc_type.SetColumnWidth(2, wx.LIST_AUTOSIZE_USEHEADER)
     476                          self._LCTRL_doc_type.SetColumnWidth(3, wx.LIST_AUTOSIZE_USEHEADER)
     477
     478                  self._TCTRL_type.SetValue('')
     479                  self._TCTRL_l10n_type.SetValue('')
     480
     481                  self._BTN_set_translation.Enable(False)
     482                  self._BTN_delete.Enable(False)
     483                  self._BTN_add.Enable(False)
     484                  self._BTN_reassign.Enable(False)
     485
     486                  self._LCTRL_doc_type.SetFocus()
     487          #--------------------------------------------------------
     488          # event handlers
     489          #--------------------------------------------------------
     490 -        def _on_list_item_selected(self, evt):
     491                  doc_type = self._LCTRL_doc_type.get_selected_item_data()
     492
     493                  self._TCTRL_type.SetValue(doc_type['type'])
     494                  self._TCTRL_l10n_type.SetValue(doc_type['l10n_type'])
     495
     496                  self._BTN_set_translation.Enable(True)
     497                  self._BTN_delete.Enable(not bool(doc_type['is_in_use']))
     498                  self._BTN_add.Enable(False)
     499                  self._BTN_reassign.Enable(True)
     500
     501                  return
     502          #--------------------------------------------------------
     503 -        def _on_type_modified(self, event):
     504                  self._BTN_set_translation.Enable(False)
     505                  self._BTN_delete.Enable(False)
     506                  self._BTN_reassign.Enable(False)
     507
     508                  self._BTN_add.Enable(True)
     509  #               self._LCTRL_doc_type.deselect_selected_item()
     510                  return
     511          #--------------------------------------------------------
     512 -        def _on_set_translation_button_pressed(self, event):
     513                  doc_type = self._LCTRL_doc_type.get_selected_item_data()
     514                  if doc_type.set_translation(translation = self._TCTRL_l10n_type.GetValue().strip()):
     515                          self.repopulate_ui()
     516
     517                  return
     518          #--------------------------------------------------------
     519 -        def _on_delete_button_pressed(self, event):
     520                  doc_type = self._LCTRL_doc_type.get_selected_item_data()
     521                  if doc_type['is_in_use']:
     522                          gmGuiHelpers.gm_show_info (
     523                                  _(
     524                                          'Cannot delete document type\n'
     525                                          ' [%s]\n'
     526                                          'because it is currently in use.'
     527                                  ) % doc_type['l10n_type'],
     528                                  _('deleting document type')
     529                          )
     530                          return
     531
     532                  gmDocuments.delete_document_type(document_type = doc_type)
     533
     534                  return
     535          #--------------------------------------------------------
     536 -        def _on_add_button_pressed(self, event):
     537                  desc = self._TCTRL_type.GetValue().strip()
     538                  if desc != '':
     539                          doc_type = gmDocuments.create_document_type(document_type = desc)               # does not create dupes
     540                          l10n_desc = self._TCTRL_l10n_type.GetValue().strip()
     541                          if (l10n_desc != '') and (l10n_desc != doc_type['l10n_type']):
     542                                  doc_type.set_translation(translation = l10n_desc)
     543
     544                  return
     545          #--------------------------------------------------------
     546 -        def _on_reassign_button_pressed(self, event):
     547
     548                  orig_type = self._LCTRL_doc_type.get_selected_item_data()
     549                  doc_types = gmDocuments.get_document_types()
     550
     551                  new_type = gmListWidgets.get_choices_from_list (
     552                          parent = self,
     553                          msg = _(
     554                                  'From the list below select the document type you want\n'
     555                                  'all documents currently classified as:\n\n'
     556                                  ' "%s"\n\n'
     557                                  'to be changed to.\n\n'
     558                                  'Be aware that this change will be applied to ALL such documents. If there\n'
     559                                  'are many documents to change it can take quite a while.\n\n'
     560                                  'Make sure this is what you want to happen !\n'
     561                          ) % orig_type['l10n_type'],
     562                          caption = _('Reassigning document type'),
     563                          choices = [ [gmTools.bool2subst(dt['is_user_defined'], 'X', ''), dt['type'], dt['l10n_type']] for dt in doc_types ],
     564                          columns = [_('User defined'), _('Type'), _('Translation')],
     565                          data = doc_types,
     566                          single_selection = True
     567                  )
     568
     569                  if new_type is None:
     570                          return
     571
     572                  wx.BeginBusyCursor()
     573                  gmDocuments.reclassify_documents_by_type(original_type = orig_type, target_type = new_type)
     574                  wx.EndBusyCursor()
     575
     576                  return
     577
     578  #============================================================
     579 -class cDocumentTypeSelectionPhraseWheel(gmPhraseWheel.cPhraseWheel):
     580          """Let user select a document type."""
     581 -        def __init__(self, *args, **kwargs):
     582
     583                  gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
     584
     585                  mp = gmMatchProvider.cMatchProvider_SQL2 (
     586                          queries = [
     587  """SELECT
     588          data,
     589          field_label,
     590          list_label
     591  FROM ((
     592          SELECT
     593                  pk_doc_type AS data,
     594                  l10n_type AS field_label,
     595                  l10n_type AS list_label,
     596                  1 AS rank
     597          FROM blobs.v_doc_type
     598          WHERE
     599                  is_user_defined IS True
     600                          AND
     601                  l10n_type %(fragment_condition)s
     602  ) UNION (
     603          SELECT
     604                  pk_doc_type AS data,
     605                  l10n_type AS field_label,
     606                  l10n_type AS list_label,
     607                  2 AS rank
     608          FROM blobs.v_doc_type
     609          WHERE
     610                  is_user_defined IS False
     611                          AND
     612                  l10n_type %(fragment_condition)s
     613  )) AS q1
     614  ORDER BY q1.rank, q1.list_label"""]
     615                          )
     616                  mp.setThresholds(2, 4, 6)
     617
     618                  self.matcher = mp
     619                  self.picklist_delay = 50
     620
     621                  self.SetToolTip(_('Select the document type.'))
     622          #--------------------------------------------------------
     623 -        def _create_data(self):
     624
     625                  doc_type = self.GetValue().strip()
     626                  if doc_type == '':
     627                          gmDispatcher.send(signal = 'statustext', msg = _('Cannot create document type without name.'), beep = True)
     628                          _log.debug('cannot create document type without name')
     629                          return
     630
     631                  pk = gmDocuments.create_document_type(doc_type)['pk_doc_type']
     632                  if pk is None:
     633                          self.data = {}
     634                  else:
     635                          self.SetText (
     636                                  value = doc_type,
     637                                  data = pk
     638                          )
     639
     640  #============================================================
     641  # document review widgets
     642  #============================================================
     643 -def review_document_part(parent=None, part=None):
     644          if parent is None:
     645                  parent = wx.GetApp().GetTopWindow()
     646          dlg = cReviewDocPartDlg (
     647                  parent = parent,
     648                  id = -1,
     649                  part = part
     650          )
     651          dlg.ShowModal()
     652          dlg.DestroyLater()
     653
     654  #------------------------------------------------------------
     655 -def review_document(parent=None, document=None):
     656          return review_document_part(parent = parent, part = document)
     657
     658  #------------------------------------------------------------
     659  from Gnumed.wxGladeWidgets import wxgReviewDocPartDlg
     660
     661 -class cReviewDocPartDlg(wxgReviewDocPartDlg.wxgReviewDocPartDlg):
     662 -        def __init__(self, *args, **kwds):
     663                  """Support parts and docs now.
     664                  """
     665                  part = kwds['part']
     666                  del kwds['part']
     667                  wxgReviewDocPartDlg.wxgReviewDocPartDlg.__init__(self, *args, **kwds)
     668
     669                  if isinstance(part, gmDocuments.cDocumentPart):
     670                          self.__part = part
     671                          self.__doc = self.__part.containing_document
     672                          self.__reviewing_doc = False
     673                  elif isinstance(part, gmDocuments.cDocument):
     674                          self.__doc = part
     675                          if len(self.__doc.parts) == 0:
     676                                  self.__part = None
     677                          else:
     678                                  self.__part = self.__doc.parts[0]
     679                          self.__reviewing_doc = True
     680                  else:
     681                          raise ValueError('<part> must be gmDocuments.cDocument or gmDocuments.cDocumentPart instance, got <%s>' % type(part))
     682
     683                  self.__init_ui_data()
     684
     685          #--------------------------------------------------------
     686          # internal API
     687          #--------------------------------------------------------
     688 -        def __init_ui_data(self):
     689                  # FIXME: fix this
     690                  # associated episode (add " " to avoid popping up pick list)
     691                  self._PhWheel_episode.SetText('%s ' % self.__doc['episode'], self.__doc['pk_episode'])
     692                  self._PhWheel_doc_type.SetText(value = self.__doc['l10n_type'], data = self.__doc['pk_type'])
     693                  self._PhWheel_doc_type.add_callback_on_set_focus(self._on_doc_type_gets_focus)
     694                  self._PhWheel_doc_type.add_callback_on_lose_focus(self._on_doc_type_loses_focus)
     695
     696                  if self.__reviewing_doc:
     697                          self._PRW_doc_comment.SetText(gmTools.coalesce(self.__doc['comment'], ''))
     698                          self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = self.__doc['pk_type'])
     699                  else:
     700                          self._PRW_doc_comment.SetText(gmTools.coalesce(self.__part['obj_comment'], ''))
     701
     702                  if self.__doc['pk_org_unit'] is not None:
     703                          self._PRW_org.SetText(value = '%s @ %s' % (self.__doc['unit'], self.__doc['organization']), data = self.__doc['pk_org_unit'])
     704
     705                  if self.__doc['unit_is_receiver']:
     706                          self._RBTN_org_is_receiver.Value = True
     707                  else:
     708                          self._RBTN_org_is_source.Value = True
     709
     710                  if self.__reviewing_doc:
     711                          self._PRW_org.Enable()
     712                  else:
     713                          self._PRW_org.Disable()
     714
     715                  if self.__doc['pk_hospital_stay'] is not None:
     716                          self._PRW_hospital_stay.SetText(data = self.__doc['pk_hospital_stay'])
     717
     718                  fts = gmDateTime.cFuzzyTimestamp(timestamp = self.__doc['clin_when'])
     719                  self._PhWheel_doc_date.SetText(fts.strftime('%Y-%m-%d'), fts)
     720                  self._TCTRL_reference.SetValue(gmTools.coalesce(self.__doc['ext_ref'], ''))
     721                  if self.__reviewing_doc:
     722                          self._TCTRL_filename.Enable(False)
     723                          self._SPINCTRL_seq_idx.Enable(False)
     724                  else:
     725                          self._TCTRL_filename.SetValue(gmTools.coalesce(self.__part['filename'], ''))
     726                          self._SPINCTRL_seq_idx.SetValue(gmTools.coalesce(self.__part['seq_idx'], 0))
     727
     728                  self._LCTRL_existing_reviews.InsertColumn(0, _('who'))
     729                  self._LCTRL_existing_reviews.InsertColumn(1, _('when'))
     730                  self._LCTRL_existing_reviews.InsertColumn(2, _('+/-'))
     731                  self._LCTRL_existing_reviews.InsertColumn(3, _('!'))
     732                  self._LCTRL_existing_reviews.InsertColumn(4, _('comment'))
     733
     734                  self.__reload_existing_reviews()
     735
     736                  if self._LCTRL_existing_reviews.GetItemCount() > 0:
     737                          self._LCTRL_existing_reviews.SetColumnWidth(0, wx.LIST_AUTOSIZE)
     738                          self._LCTRL_existing_reviews.SetColumnWidth(1, wx.LIST_AUTOSIZE)
     739                          self._LCTRL_existing_reviews.SetColumnWidth(2, wx.LIST_AUTOSIZE_USEHEADER)
     740                          self._LCTRL_existing_reviews.SetColumnWidth(3, wx.LIST_AUTOSIZE_USEHEADER)
     741                          self._LCTRL_existing_reviews.SetColumnWidth(4, wx.LIST_AUTOSIZE)
     742
     743                  if self.__part is None:
     744                          self._ChBOX_review.SetValue(False)
     745                          self._ChBOX_review.Enable(False)
     746                          self._ChBOX_abnormal.Enable(False)
     747                          self._ChBOX_relevant.Enable(False)
     748                          self._ChBOX_sign_all_pages.Enable(False)
     749                  else:
     750                          me = gmStaff.gmCurrentProvider()
     751                          if self.__part['pk_intended_reviewer'] == me['pk_staff']:
     752                                  msg = _('(you are the primary reviewer)')
     753                          else:
     754                                  other = gmStaff.cStaff(aPK_obj = self.__part['pk_intended_reviewer'])
     755                                  msg = _('(someone else is the intended reviewer: %s)') % other['short_alias']
     756                          self._TCTRL_responsible.SetValue(msg)
     757                          # init my review if any
     758                          if self.__part['reviewed_by_you']:
     759                                  revs = self.__part.get_reviews()
     760                                  for rev in revs:
     761                                          if rev['is_your_review']:
     762                                                  self._ChBOX_abnormal.SetValue(bool(rev[2]))
     763                                                  self._ChBOX_relevant.SetValue(bool(rev[3]))
     764                                                  break
     765
     766                          self._ChBOX_sign_all_pages.SetValue(self.__reviewing_doc)
     767
     768                  return True
     769
     770          #--------------------------------------------------------
     771 -        def __reload_existing_reviews(self):
     772                  self._LCTRL_existing_reviews.DeleteAllItems()
     773                  if self.__part is None:
     774                          return True
     775                  revs = self.__part.get_reviews()                # FIXME: this is ugly as sin, it should be dicts, not lists
     776                  if len(revs) == 0:
     777                          return True
     778                  # find special reviews
     779                  review_by_responsible_doc = None
     780                  reviews_by_others = []
     781                  for rev in revs:
     782                          if rev['is_review_by_responsible_reviewer'] and not rev['is_your_review']:
     783                                  review_by_responsible_doc = rev
     784                          if not (rev['is_review_by_responsible_reviewer'] or rev['is_your_review']):
     785                                  reviews_by_others.append(rev)
     786                  # display them
     787                  if review_by_responsible_doc is not None:
     788                          row_num = self._LCTRL_existing_reviews.InsertItem(sys.maxsize, label=review_by_responsible_doc[0])
     789                          self._LCTRL_existing_reviews.SetItemTextColour(row_num, wx.BLUE)
     790                          self._LCTRL_existing_reviews.SetItem(index = row_num, column=0, label=review_by_responsible_doc[0])
     791                          self._LCTRL_existing_reviews.SetItem(index = row_num, column=1, label=review_by_responsible_doc[1].strftime('%x %H:%M'))
     792                          if review_by_responsible_doc['is_technically_abnormal']:
     793                                  self._LCTRL_existing_reviews.SetItem(index = row_num, column=2, label='X')
     794                          if review_by_responsible_doc['clinically_relevant']:
     795                                  self._LCTRL_existing_reviews.SetItem(index = row_num, column=3, label='X')
     796                          self._LCTRL_existing_reviews.SetItem(index = row_num, column=4, label=review_by_responsible_doc[6])
     797                          row_num += 1
     798                  for rev in reviews_by_others:
     799                          row_num = self._LCTRL_existing_reviews.InsertItem(sys.maxsize, label=rev[0])
     800                          self._LCTRL_existing_reviews.SetItem(index = row_num, column=0, label=rev[0])
     801                          self._LCTRL_existing_reviews.SetItem(index = row_num, column=1, label=rev[1].strftime('%x %H:%M'))
     802                          if rev['is_technically_abnormal']:
     803                                  self._LCTRL_existing_reviews.SetItem(index = row_num, column=2, label='X')
     804                          if rev['clinically_relevant']:
     805                                  self._LCTRL_existing_reviews.SetItem(index = row_num, column=3, label='X')
     806                          self._LCTRL_existing_reviews.SetItem(index = row_num, column=4, label=rev[6])
     807                  return True
     808
     809          #--------------------------------------------------------
     810          # event handlers
     811          #--------------------------------------------------------
     812 -        def _on_save_button_pressed(self, evt):
     813                  """Save the metadata to the backend."""
     814
     815                  evt.Skip()
     816
     817                  # 1) handle associated episode
     818                  pk_episode = self._PhWheel_episode.GetData(can_create=True, is_open=True)
     819                  if pk_episode is None:
     820                          gmGuiHelpers.gm_show_error (
     821                                  _('Cannot create episode\n [%s]'),
     822                                  _('Editing document properties')
     823                          )
     824                          return False
     825
     826                  doc_type = self._PhWheel_doc_type.GetData(can_create = True)
     827                  if doc_type is None:
     828                          gmDispatcher.send(signal='statustext', msg=_('Cannot change document type to [%s].') % self._PhWheel_doc_type.GetValue().strip())
     829                          return False
     830
     831                  # since the phrasewheel operates on the active
     832                  # patient all episodes really should belong
     833                  # to it so we don't check patient change
     834                  self.__doc['pk_episode'] = pk_episode
     835                  self.__doc['pk_type'] = doc_type
     836                  if self.__reviewing_doc:
     837                          self.__doc['comment'] = self._PRW_doc_comment.GetValue().strip()
     838                  # FIXME: a rather crude way of error checking:
     839                  if self._PhWheel_doc_date.GetData() is not None:
     840                          self.__doc['clin_when'] = self._PhWheel_doc_date.GetData().get_pydt()
     841                  self.__doc['ext_ref'] = self._TCTRL_reference.GetValue().strip()
     842                  self.__doc['pk_org_unit'] = self._PRW_org.GetData()
     843                  if self._RBTN_org_is_receiver.Value is True:
     844                          self.__doc['unit_is_receiver'] = True
     845                  else:
     846                          self.__doc['unit_is_receiver'] = False
     847                  self.__doc['pk_hospital_stay'] = self._PRW_hospital_stay.GetData()
     848
     849                  success, data = self.__doc.save()
     850                  if not success:
     851                          gmGuiHelpers.gm_show_error (
     852                                  _('Cannot link the document to episode\n\n [%s]') % epi_name,
     853                                  _('Editing document properties')
     854                          )
     855                          return False
     856
     857                  # 2) handle review
     858                  if self._ChBOX_review.GetValue():
     859                          provider = gmStaff.gmCurrentProvider()
     860                          abnormal = self._ChBOX_abnormal.GetValue()
     861                          relevant = self._ChBOX_relevant.GetValue()
     862                          msg = None
     863                          if self.__reviewing_doc:                # - on all pages
     864                                  if not self.__doc.set_reviewed(technically_abnormal = abnormal, clinically_relevant = relevant):
     865                                          msg = _('Error setting "reviewed" status of this document.')
     866                                  if self._ChBOX_responsible.GetValue():
     867                                          if not self.__doc.set_primary_reviewer(reviewer = provider['pk_staff']):
     868                                                  msg = _('Error setting responsible clinician for this document.')
     869                          else:                                                           # - just on this page
     870                                  if not self.__part.set_reviewed(technically_abnormal = abnormal, clinically_relevant = relevant):
     871                                          msg = _('Error setting "reviewed" status of this part.')
     872                                  if self._ChBOX_responsible.GetValue():
     873                                          self.__part['pk_intended_reviewer'] = provider['pk_staff']
     874                          if msg is not None:
     875                                  gmGuiHelpers.gm_show_error(msg, _('Editing document properties'))
     876                                  return False
     877
     878                  # 3) handle "page" specific parts
     879                  if not self.__reviewing_doc:
     880                          self.__part['filename'] = gmTools.none_if(self._TCTRL_filename.GetValue().strip(), '')
     881                          new_idx = gmTools.none_if(self._SPINCTRL_seq_idx.GetValue(), 0)
     882                          if self.__part['seq_idx'] != new_idx:
     883                                  if new_idx in self.__doc['seq_idx_list']:
     884                                          msg = _(
     885                                                  'Cannot set page number to [%s] because\n'
     886                                                  'another page with this number exists.\n'
     887                                                  '\n'
     888                                                  'Page numbers in use:\n'
     889                                                  '\n'
     890                                                  ' %s'
     891                                          ) % (
     892                                                  new_idx,
     893                                                  self.__doc['seq_idx_list']
     894                                          )
     895                                          gmGuiHelpers.gm_show_error(msg, _('Editing document part properties'))
     896                                  else:
     897                                          self.__part['seq_idx'] = new_idx
     898                          self.__part['obj_comment'] = self._PRW_doc_comment.GetValue().strip()
     899                          success, data = self.__part.save_payload()
     900                          if not success:
     901                                  gmGuiHelpers.gm_show_error (
     902                                          _('Error saving part properties.'),
     903                                          _('Editing document part properties')
     904                                  )
     905                                  return False
     906
     907                  return True
     908
     909          #--------------------------------------------------------
     910 -        def _on_reviewed_box_checked(self, evt):
     911                  state = self._ChBOX_review.GetValue()
     912                  self._ChBOX_abnormal.Enable(enable = state)
     913                  self._ChBOX_relevant.Enable(enable = state)
     914                  self._ChBOX_responsible.Enable(enable = state)
     915
     916          #--------------------------------------------------------
     917 -        def _on_doc_type_gets_focus(self):
     918                  """Per Jim: Changing the doc type happens a lot more often
     919                     then correcting spelling, hence select-all on getting focus.
     920                  """
     921                  self._PhWheel_doc_type.SetSelection(-1, -1)
     922
     923          #--------------------------------------------------------
     924 -        def _on_doc_type_loses_focus(self):
     925                  pk_doc_type = self._PhWheel_doc_type.GetData()
     926                  if pk_doc_type is None:
     927                          self._PRW_doc_comment.unset_context(context = 'pk_doc_type')
     928                  else:
     929                          self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type)
     930                  return True
     931
     932  #============================================================
     933 -def acquire_images_from_capture_device(device=None, calling_window=None):
     934
     935          _log.debug('acquiring images from [%s]', device)
     936
     937          # do not import globally since we might want to use
     938          # this module without requiring any scanner to be available
     939          from Gnumed.pycommon import gmScanBackend
     940          try:
     941                  fnames = gmScanBackend.acquire_pages_into_files (
     942                          device = device,
     943                          delay = 5,
     944                          calling_window = calling_window
     945                  )
     946          except OSError:
     947                  _log.exception('problem acquiring image from source')
     948                  gmGuiHelpers.gm_show_error (
     949                          aMessage = _(
     950                                  'No images could be acquired from the source.\n\n'
     951                                  'This may mean the scanner driver is not properly installed.\n\n'
     952                                  'On Windows you must install the TWAIN Python module\n'
     953                                  'while on Linux and MacOSX it is recommended to install\n'
     954                                  'the XSane package.'
     955                          ),
     956                          aTitle = _('Acquiring images')
     957                  )
     958                  return None
     959
     960          _log.debug('acquired %s images', len(fnames))
     961
     962          return fnames
     963
     964  #------------------------------------------------------------
     965  from Gnumed.wxGladeWidgets import wxgScanIdxPnl
     966
     967 -class cScanIdxDocsPnl(wxgScanIdxPnl.wxgScanIdxPnl, gmPlugin.cPatientChange_PluginMixin):
     968
     969 -        def __init__(self, *args, **kwds):
     970                  wxgScanIdxPnl.wxgScanIdxPnl.__init__(self, *args, **kwds)
     971                  gmPlugin.cPatientChange_PluginMixin.__init__(self)
     972
     973                  self._PhWheel_reviewer.matcher = gmPerson.cMatchProvider_Provider()
     974
     975                  self.__init_ui_data()
     976                  self._PhWheel_doc_type.add_callback_on_lose_focus(self._on_doc_type_loses_focus)
     977
     978                  # make me and listctrl file drop targets
     979                  dt = gmGuiHelpers.cFileDropTarget(target = self)
     980                  self.SetDropTarget(dt)
     981                  dt = gmGuiHelpers.cFileDropTarget(on_drop_callback = self._drop_target_consume_filenames)
     982                  self._LCTRL_doc_pages.SetDropTarget(dt)
     983
     984                  # do not import globally since we might want to use
     985                  # this module without requiring any scanner to be available
     986                  from Gnumed.pycommon import gmScanBackend
     987                  self.scan_module = gmScanBackend
     988
     989          #--------------------------------------------------------
     990          # file drop target API
     991          #--------------------------------------------------------
     992 -        def _drop_target_consume_filenames(self, filenames):
     993                  pat = gmPerson.gmCurrentPatient()
     994                  if not pat.connected:
     995                          gmDispatcher.send(signal='statustext', msg=_('Cannot accept new documents. No active patient.'))
     996                          return
     997
     998                  # dive into folders dropped onto us and extract files (one level deep only)
     999                  real_filenames = []
    1000                  for pathname in filenames:
    1001                          try:
    1002                                  files = os.listdir(pathname)
    1003                                  source = _('directory dropped on client')
    1004                                  gmDispatcher.send(signal = 'statustext', msg = _('Extracting files from folder [%s] ...') % pathname)
    1005                                  for filename in files:
    1006                                          fullname = os.path.join(pathname, filename)
    1007                                          if not os.path.isfile(fullname):
    1008                                                  continue
    1009                                          real_filenames.append(fullname)
    1010                          except OSError:
    1011                                  source = _('file dropped on client')
    1012                                  real_filenames.append(pathname)
    1013
    1014                  self.add_parts_from_files(real_filenames, source)
    1015
    1016          #--------------------------------------------------------
    1017 -        def repopulate_ui(self):
    1018                  pass
    1019
    1020          #--------------------------------------------------------
    1021          # patient change plugin API
    1022          #--------------------------------------------------------
    1023 -        def _pre_patient_unselection(self, **kwds):
    1024                  # FIXME: persist pending data from here
    1025                  pass
    1026
    1027          #--------------------------------------------------------
    1028 -        def _post_patient_selection(self, **kwds):
    1029                  self.__init_ui_data()
    1030
    1031          #--------------------------------------------------------
    1032          # internal API
    1033          #--------------------------------------------------------
    1034 -        def __init_ui_data(self):
    1035                  # -----------------------------
    1036                  self._PhWheel_episode.SetText(value = _('other documents'), suppress_smarts = True)
    1037                  self._PhWheel_doc_type.SetText('')
    1038                  # -----------------------------
    1039                  # FIXME: make this configurable: either now() or last_date()
    1040                  fts = gmDateTime.cFuzzyTimestamp()
    1041                  self._PhWheel_doc_date.SetText(fts.strftime('%Y-%m-%d'), fts)
    1042                  self._PRW_doc_comment.SetText('')
    1043                  self._PhWheel_source.SetText('', None)
    1044                  self._RBTN_org_is_source.SetValue(1)
    1045                  # FIXME: should be set to patient's primary doc
    1046                  self._PhWheel_reviewer.selection_only = True
    1047                  me = gmStaff.gmCurrentProvider()
    1048                  self._PhWheel_reviewer.SetText (
    1049                          value = '%s (%s%s %s)' % (me['short_alias'], gmTools.coalesce(me['title'], ''), me['firstnames'], me['lastnames']),
    1050                          data = me['pk_staff']
    1051                  )
    1052                  # -----------------------------
    1053                  # FIXME: set from config item
    1054                  self._ChBOX_reviewed.SetValue(False)
    1055                  self._ChBOX_abnormal.Disable()
    1056                  self._ChBOX_abnormal.SetValue(False)
    1057                  self._ChBOX_relevant.Disable()
    1058                  self._ChBOX_relevant.SetValue(False)
    1059                  # -----------------------------
    1060                  self._TBOX_description.SetValue('')
    1061                  # -----------------------------
    1062                  # the list holding our page files
    1063                  self._LCTRL_doc_pages.remove_items_safely()
    1064                  self._LCTRL_doc_pages.set_columns([_('file'), _('path')])
    1065                  self._LCTRL_doc_pages.set_column_widths()
    1066
    1067                  self._TCTRL_metadata.SetValue('')
    1068
    1069                  self._PhWheel_doc_type.SetFocus()
    1070
    1071          #--------------------------------------------------------
    1072 -        def add_parts_from_files(self, filenames, source=''):
    1073                  rows = gmTools.coalesce(self._LCTRL_doc_pages.string_items, [])
    1074                  data = gmTools.coalesce(self._LCTRL_doc_pages.data, [])
    1075                  rows.extend([ [gmTools.fname_from_path(f), gmTools.fname_dir(f)] for f in filenames ])
    1076                  data.extend([ [f, source] for f in filenames ])
    1077                  self._LCTRL_doc_pages.string_items = rows
    1078                  self._LCTRL_doc_pages.data = data
    1079                  self._LCTRL_doc_pages.set_column_widths()
    1080
    1081          #--------------------------------------------------------
    1082 -        def __valid_for_save(self):
    1083                  title = _('saving document')
    1084
    1085                  if self._LCTRL_doc_pages.ItemCount == 0:
    1086                          dbcfg = gmCfg.cCfgSQL()
    1087                          allow_empty = bool(dbcfg.get2 (
    1088                                  option =  'horstspace.scan_index.allow_partless_documents',
    1089                                  workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
    1090                                  bias = 'user',
    1091                                  default = False
    1092                          ))
    1093                          if allow_empty:
    1094                                  save_empty = gmGuiHelpers.gm_show_question (
    1095                                          aMessage = _('No parts to save. Really save an empty document as a reference ?'),
    1096                                          aTitle = title
    1097                                  )
    1098                                  if not save_empty:
    1099                                          return False
    1100                          else:
    1101                                  gmGuiHelpers.gm_show_error (
    1102                                          aMessage = _('No parts to save. Aquire some parts first.'),
    1103                                          aTitle = title
    1104                                  )
    1105                                  return False
    1106
    1107                  doc_type_pk = self._PhWheel_doc_type.GetData(can_create = True)
    1108                  if doc_type_pk is None:
    1109                          gmGuiHelpers.gm_show_error (
    1110                                  aMessage = _('No document type applied. Choose a document type'),
    1111                                  aTitle = title
    1112                          )
    1113                          return False
    1114
    1115                  # this should be optional, actually
    1116  #               if self._PRW_doc_comment.GetValue().strip() == '':
    1117  #                       gmGuiHelpers.gm_show_error (
    1118  #                               aMessage = _('No document comment supplied. Add a comment for this document.'),
    1119  #                               aTitle = title
    1120  #                       )
    1121  #                       return False
    1122
    1123                  if self._PhWheel_episode.GetValue().strip() == '':
    1124                          gmGuiHelpers.gm_show_error (
    1125                                  aMessage = _('You must select an episode to save this document under.'),
    1126                                  aTitle = title
    1127                          )
    1128                          return False
    1129
    1130                  if self._PhWheel_reviewer.GetData() is None:
    1131                          gmGuiHelpers.gm_show_error (
    1132                                  aMessage = _('You need to select from the list of staff members the doctor who is intended to sign the document.'),
    1133                                  aTitle = title
    1134                          )
    1135                          return False
    1136
    1137                  if self._PhWheel_doc_date.is_valid_timestamp(empty_is_valid = True) is False:
    1138                          gmGuiHelpers.gm_show_error (
    1139                                  aMessage = _('Invalid date of generation.'),
    1140                                  aTitle = title
    1141                          )
    1142                          return False
    1143
    1144                  return True
    1145
    1146          #--------------------------------------------------------
    1147 -        def get_device_to_use(self, reconfigure=False):
    1148
    1149                  if not reconfigure:
    1150                          dbcfg = gmCfg.cCfgSQL()
    1151                          device = dbcfg.get2 (
    1152                                  option =  'external.xsane.default_device',
    1153                                  workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
    1154                                  bias = 'workplace',
    1155                                  default = ''
    1156                          )
    1157                          if device.strip() == '':
    1158                                  device = None
    1159                          if device is not None:
    1160                                  return device
    1161
    1162                  try:
    1163                          devices = self.scan_module.get_devices()
    1164                  except:
    1165                          _log.exception('cannot retrieve list of image sources')
    1166                          gmDispatcher.send(signal = 'statustext', msg = _('There is no scanner support installed on this machine.'))
    1167                          return None
    1168
    1169                  if devices is None:
    1170                          # get_devices() not implemented for TWAIN yet
    1171                          # XSane has its own chooser (so does TWAIN)
    1172                          return None
    1173
    1174                  if len(devices) == 0:
    1175                          gmDispatcher.send(signal = 'statustext', msg = _('Cannot find an active scanner.'))
    1176                          return None
    1177
    1178  #               device_names = []
    1179  #               for device in devices:
    1180  #                       device_names.append('%s (%s)' % (device[2], device[0]))
    1181
    1182                  device = gmListWidgets.get_choices_from_list (
    1183                          parent = self,
    1184                          msg = _('Select an image capture device'),
    1185                          caption = _('device selection'),
    1186                          choices = [ '%s (%s)' % (d[2], d[0]) for d in devices ],
    1187                          columns = [_('Device')],
    1188                          data = devices,
    1189                          single_selection = True
    1190                  )
    1191                  if device is None:
    1192                          return None
    1193
    1194                  # FIXME: add support for actually reconfiguring
    1195                  return device[0]
    1196
    1197          #--------------------------------------------------------
    1198          # event handling API
    1199          #--------------------------------------------------------
    1200 -        def _scan_btn_pressed(self, evt):
    1201
    1202                  chosen_device = self.get_device_to_use()
    1203
    1204                  # FIXME: configure whether to use XSane or sane directly
    1205                  # FIXME: add support for xsane_device_settings argument
    1206                  try:
    1207                          fnames = self.scan_module.acquire_pages_into_files (
    1208                                  device = chosen_device,
    1209                                  delay = 5,
    1210                                  calling_window = self
    1211                          )
    1212                  except OSError:
    1213                          _log.exception('problem acquiring image from source')
    1214                          gmGuiHelpers.gm_show_error (
    1215                                  aMessage = _(
    1216                                          'No pages could be acquired from the source.\n\n'
    1217                                          'This may mean the scanner driver is not properly installed.\n\n'
    1218                                          'On Windows you must install the TWAIN Python module\n'
    1219                                          'while on Linux and MacOSX it is recommended to install\n'
    1220                                          'the XSane package.'
    1221                                  ),
    1222                                  aTitle = _('acquiring page')
    1223                          )
    1224                          return None
    1225
    1226                  if len(fnames) == 0:            # no pages scanned
    1227                          return True
    1228
    1229                  self.add_parts_from_files(fnames, _('captured by imaging device'))
    1230                  return True
    1231
    1232          #--------------------------------------------------------
    1233 -        def _load_btn_pressed(self, evt):
    1234                  # patient file chooser
    1235                  dlg = wx.FileDialog (
    1236                          parent = None,
    1237                          message = _('Choose a file'),
    1238                          defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')),
    1239                          defaultFile = '',
    1240                          wildcard = "%s (*)|*|TIFFs (*.tif)|*.tif|JPEGs (*.jpg)|*.jpg|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')),
    1241                          style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE
    1242                  )
    1243                  result = dlg.ShowModal()
    1244                  files = dlg.GetPaths()
    1245                  if result == wx.ID_CANCEL:
    1246                          dlg.DestroyLater()
    1247                          return
    1248
    1249                  self.add_parts_from_files(files, _('picked from storage media'))
    1250
    1251          #--------------------------------------------------------
    1252 -        def _clipboard_btn_pressed(self, event):
    1253                  event.Skip()
    1254                  clip = gmGuiHelpers.clipboard2file()
    1255                  if clip is None:
    1256                          return
    1257                  if clip is False:
    1258                          return
    1259                  self.add_parts_from_files([clip], _('pasted from clipboard'))
    1260
    1261          #--------------------------------------------------------
    1262 -        def _show_btn_pressed(self, evt):
    1263
    1264                  # nothing to do
    1265                  if self._LCTRL_doc_pages.ItemCount == 0:
    1266                          return
    1267
    1268                  # only one page, show that, regardless of whether selected or not
    1269                  if self._LCTRL_doc_pages.ItemCount == 1:
    1270                          page_fnames = [ self._LCTRL_doc_pages.get_item_data(0)[0] ]
    1271                  else:
    1272                          # did user select one of multiple pages ?
    1273                          page_fnames = [ data[0] for data in self._LCTRL_doc_pages.selected_item_data ]
    1274                          if len(page_fnames) == 0:
    1275                                  gmDispatcher.send(signal = 'statustext', msg = _('No part selected for viewing.'), beep = True)
    1276                                  return
    1277
    1278                  for page_fname in page_fnames:
    1279                          (success, msg) = gmMimeLib.call_viewer_on_file(page_fname)
    1280                          if not success:
    1281                                  gmGuiHelpers.gm_show_warning (
    1282                                          aMessage = _('Cannot display document part:\n%s') % msg,
    1283                                          aTitle = _('displaying part')
    1284                                  )
    1285
    1286          #--------------------------------------------------------
    1287 -        def _del_btn_pressed(self, event):
    1288
    1289                  if len(self._LCTRL_doc_pages.selected_items) == 0:
    1290                          gmDispatcher.send(signal = 'statustext', msg = _('No part selected for removal.'), beep = True)
    1291                          return
    1292
    1293                  sel_idx = self._LCTRL_doc_pages.GetFirstSelected()
    1294                  rows = self._LCTRL_doc_pages.string_items
    1295                  data = self._LCTRL_doc_pages.data
    1296                  del rows[sel_idx]
    1297                  del data[sel_idx]
    1298                  self._LCTRL_doc_pages.string_items = rows
    1299                  self._LCTRL_doc_pages.data = data
    1300                  self._LCTRL_doc_pages.set_column_widths()
    1301                  self._TCTRL_metadata.SetValue('')
    1302
    1303          #--------------------------------------------------------
    1304 -        def _save_btn_pressed(self, evt):
    1305
    1306                  if not self.__valid_for_save():
    1307                          return False
    1308
    1309                  # external reference
    1310                  cfg = gmCfg.cCfgSQL()
    1311                  generate_uuid = bool (
    1312                          cfg.get2 (
    1313                                  option = 'horstspace.scan_index.generate_doc_uuid',
    1314                                  workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
    1315                                  bias = 'user',
    1316                                  default = False
    1317                          )
    1318                  )
    1319                  if generate_uuid:
    1320                          ext_ref = gmDocuments.get_ext_ref()
    1321                  else:
    1322                          ext_ref = None
    1323
    1324                  # create document
    1325                  date = self._PhWheel_doc_date.GetData()
    1326                  if date is not None:
    1327                          date = date.get_pydt()
    1328                  new_doc = save_files_as_new_document (
    1329                          parent = self,
    1330                          filenames = [ data[0] for data in self._LCTRL_doc_pages.data ],
    1331                          document_type = self._PhWheel_doc_type.GetValue().strip(),
    1332                          pk_document_type = self._PhWheel_doc_type.GetData(),
    1333                          unlock_patient = False,
    1334                          episode = self._PhWheel_episode.GetData(can_create = True, is_open = True, as_instance = True),
    1335                          review_as_normal = False,
    1336                          reference = ext_ref,
    1337                          pk_org_unit = self._PhWheel_source.GetData(),
    1338                          date_generated = date,
    1339                          comment = self._PRW_doc_comment.GetLineText(0).strip(),
    1340                          reviewer = self._PhWheel_reviewer.GetData()
    1341                  )
    1342                  if new_doc is None:
    1343                          return False
    1344
    1345                  if self._RBTN_org_is_receiver.Value is True:
    1346                          new_doc['unit_is_receiver'] = True
    1347                          new_doc.save()
    1348
    1349                  # - long description
    1350                  description = self._TBOX_description.GetValue().strip()
    1351                  if description != '':
    1352                          if not new_doc.add_description(description):
    1353                                  wx.EndBusyCursor()
    1354                                  gmGuiHelpers.gm_show_error (
    1355                                          aMessage = _('Cannot add document description.'),
    1356                                          aTitle = _('saving document')
    1357                                  )
    1358                                  return False
    1359
    1360                  # set reviewed status
    1361                  if self._ChBOX_reviewed.GetValue():
    1362                          if not new_doc.set_reviewed (
    1363                                  technically_abnormal = self._ChBOX_abnormal.GetValue(),
    1364                                  clinically_relevant = self._ChBOX_relevant.GetValue()
    1365                          ):
    1366                                  msg = _('Error setting "reviewed" status of new document.')
    1367
    1368                  self.__init_ui_data()
    1369
    1370                  gmHooks.run_hook_script(hook = 'after_new_doc_created')
    1371
    1372                  return True
    1373
    1374          #--------------------------------------------------------
    1375 -        def _startover_btn_pressed(self, evt):
    1376                  self.__init_ui_data()
    1377
    1378          #--------------------------------------------------------
    1379 -        def _reviewed_box_checked(self, evt):
    1380                  self._ChBOX_abnormal.Enable(enable = self._ChBOX_reviewed.GetValue())
    1381                  self._ChBOX_relevant.Enable(enable = self._ChBOX_reviewed.GetValue())
    1382
    1383          #--------------------------------------------------------
    1384 -        def _on_doc_type_loses_focus(self):
    1385                  pk_doc_type = self._PhWheel_doc_type.GetData()
    1386                  if pk_doc_type is None:
    1387                          self._PRW_doc_comment.unset_context(context = 'pk_doc_type')
    1388                  else:
    1389                          self._PRW_doc_comment.set_context(context = 'pk_doc_type', val = pk_doc_type)
    1390                  return True
    1391
    1392          #--------------------------------------------------------
    1393 -        def _on_update_file_description(self, result):
    1394                  status, description = result
    1395                  fname, source = self._LCTRL_doc_pages.get_selected_item_data(only_one = True)
    1396                  txt = _(
    1397                          'Source: %s\n'
    1398                          'File: %s\n'
    1399                          '\n'
    1400                          '%s'
    1401                  ) % (
    1402                          source,
    1403                          fname,
    1404                          description
    1405                  )
    1406                  wx.CallAfter(self._TCTRL_metadata.SetValue, txt)
    1407
    1408          #--------------------------------------------------------
    1409 -        def _on_part_selected(self, event):
    1410                  event.Skip()
    1411                  fname, source = self._LCTRL_doc_pages.get_item_data(item_idx = event.Index)
    1412                  self._TCTRL_metadata.SetValue('Retrieving details from [%s] ...' % fname)
    1413                  gmMimeLib.describe_file(fname, callback = self._on_update_file_description)
    1414
    1415  #============================================================
    1416 -def display_document_part(parent=None, part=None):
    1417
    1418          if parent is None:
    1419                  parent = wx.GetApp().GetTopWindow()
    1420
    1421          # sanity check
    1422          if part['size'] == 0:
    1423                  _log.debug('cannot display part [%s] - 0 bytes', part['pk_obj'])
    1424                  gmGuiHelpers.gm_show_error (
    1425                          aMessage = _('Document part does not seem to exist in database !'),
    1426                          aTitle = _('showing document')
    1427                  )
    1428                  return None
    1429
    1430          wx.BeginBusyCursor()
    1431          cfg = gmCfg.cCfgSQL()
    1432
    1433          # determine database export chunk size
    1434          chunksize = int(
    1435          cfg.get2 (
    1436                  option = "horstspace.blob_export_chunk_size",
    1437                  workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
    1438                  bias = 'workplace',
    1439                  default = 2048
    1440          ))
    1441
    1442          # shall we force blocking during view ?
    1443          block_during_view = bool( cfg.get2 (
    1444                  option = 'horstspace.document_viewer.block_during_view',
    1445                  workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
    1446                  bias = 'user',
    1447                  default = None
    1448          ))
    1449
    1450          wx.EndBusyCursor()
    1451
    1452          # display it
    1453          successful, msg = part.display_via_mime (
    1454                  chunksize = chunksize,
    1455                  block = block_during_view
    1456          )
    1457          if not successful:
    1458                  gmGuiHelpers.gm_show_error (
    1459                          aMessage = _('Cannot display document part:\n%s') % msg,
    1460                          aTitle = _('showing document')
    1461                  )
    1462                  return None
    1463
    1464          # handle review after display
    1465          # 0: never
    1466          # 1: always
    1467          # 2: if no review by myself exists yet
    1468          # 3: if no review at all exists yet
    1469          # 4: if no review by responsible reviewer
    1470          review_after_display = int(cfg.get2 (
    1471                  option = 'horstspace.document_viewer.review_after_display',
    1472                  workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
    1473                  bias = 'user',
    1474                  default = 3
    1475          ))
    1476          if review_after_display == 1:                   # always review
    1477                  review_document_part(parent = parent, part = part)
    1478          elif review_after_display == 2:                 # review if no review by me exists
    1479                  review_by_me = [ rev for rev in part.get_reviews() if rev['is_your_review'] ]
    1480                  if len(review_by_me) == 0:
    1481                          review_document_part(parent = parent, part = part)
    1482          elif review_after_display == 3:
    1483                  if len(part.get_reviews()) == 0:
    1484                          review_document_part(parent = parent, part = part)
    1485          elif review_after_display == 4:
    1486                  reviewed_by_responsible = [ rev for rev in part.get_reviews() if rev['is_review_by_responsible_reviewer'] ]
    1487                  if len(reviewed_by_responsible) == 0:
    1488                          review_document_part(parent = parent, part = part)
    1489
    1490          return True
    1491
    1492  #============================================================
    1493 -def manage_documents(parent=None, msg=None, single_selection=True, pk_types=None, pk_episodes=None):
    1494
    1495          pat = gmPerson.gmCurrentPatient()
    1496
    1497          if parent is None:
    1498                  parent = wx.GetApp().GetTopWindow()
    1499
    1500          #--------------------------------------------------------
    1501          def edit(document=None):
    1502                  return
    1503                  #return edit_substance(parent = parent, substance = substance, single_entry = (substance is not None))
    1504
    1505          #--------------------------------------------------------
    1506          def delete(document):
    1507                  return
    1508  #               if substance.is_in_use_by_patients:
    1509  #                       gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete this substance. It is in use.'), beep = True)
    1510  #                       return False
    1511  #
    1512  #               return gmMedication.delete_x_substance(substance = substance['pk'])
    1513
    1514          #------------------------------------------------------------
    1515          def refresh(lctrl):
    1516                  docs = pat.document_folder.get_documents(pk_types = pk_types, pk_episodes = pk_episodes)
    1517                  items = [ [
    1518                          gmDateTime.pydt_strftime(d['clin_when'], '%Y %b %d', accuracy = gmDateTime.acc_days),
    1519                          d['l10n_type'],
    1520                          gmTools.coalesce(d['comment'], ''),
    1521                          gmTools.coalesce(d['ext_ref'], ''),
    1522                          d['pk_doc']
    1523                  ] for d in docs ]
    1524                  lctrl.set_string_items(items)
    1525                  lctrl.set_data(docs)
    1526
    1527          #--------------------------------------------------------
    1528          def show_doc(doc):
    1529                  if doc is None:
    1530                          return
    1531                  for fname in doc.save_parts_to_files():
    1532                          gmMimeLib.call_viewer_on_file(aFile = fname, block = False)
    1533
    1534          #------------------------------------------------------------
    1535          return gmListWidgets.get_choices_from_list (
    1536                  parent = parent,
    1537                  caption = _('Patient document list'),
    1538                  columns = [_('Generated'), _('Type'), _('Comment'), _('Ref #'), '#'],
    1539                  single_selection = single_selection,
    1540                  #new_callback = edit,
    1541                  #edit_callback = edit,
    1542                  #delete_callback = delete,
    1543                  refresh_callback = refresh,
    1544                  left_extra_button = (_('Show'), _('Show all parts of this document in external viewer.'), show_doc)
    1545          )
    1546
    1547  #============================================================
    1548  from Gnumed.wxGladeWidgets import wxgSelectablySortedDocTreePnl
    1549
    1550 -class cSelectablySortedDocTreePnl(wxgSelectablySortedDocTreePnl.wxgSelectablySortedDocTreePnl):
    1551          """A panel with a document tree which can be sorted.
    1552
    1553                  On the right there's a list ctrl showing
    1554                  details of the selected node.
    1555          """
    1556 -        def __init__(self, parent, id, *args, **kwds):
    1557                  wxgSelectablySortedDocTreePnl.wxgSelectablySortedDocTreePnl.__init__(self, parent, id, *args, **kwds)
    1558
    1559                  self._LCTRL_details.set_columns(['', ''])
    1560
    1561                  self.__metainfo4parts = {}
    1562                  self.__pk_curr_pat = None
    1563                  self.__pk_curr_doc_part = None
    1564
    1565                  self._doc_tree.show_details_callback = self._update_details
    1566
    1567          #--------------------------------------------------------
    1568          # inherited event handlers
    1569          #--------------------------------------------------------
    1570 -        def _on_sort_by_age_selected(self, evt):
    1571                  self._doc_tree.sort_mode = 'age'
    1572                  self._doc_tree.SetFocus()
    1573                  self._rbtn_sort_by_age.SetValue(True)
    1574
    1575          #--------------------------------------------------------
    1576 -        def _on_sort_by_review_selected(self, evt):
    1577                  self._doc_tree.sort_mode = 'review'
    1578                  self._doc_tree.SetFocus()
    1579                  self._rbtn_sort_by_review.SetValue(True)
    1580
    1581          #--------------------------------------------------------
    1582 -        def _on_sort_by_episode_selected(self, evt):
    1583                  self._doc_tree.sort_mode = 'episode'
    1584                  self._doc_tree.SetFocus()
    1585                  self._rbtn_sort_by_episode.SetValue(True)
    1586
    1587          #--------------------------------------------------------
    1588 -        def _on_sort_by_issue_selected(self, event):
    1589                  self._doc_tree.sort_mode = 'issue'
    1590                  self._doc_tree.SetFocus()
    1591                  self._rbtn_sort_by_issue.SetValue(True)
    1592
    1593          #--------------------------------------------------------
    1594 -        def _on_sort_by_type_selected(self, evt):
    1595                  self._doc_tree.sort_mode = 'type'
    1596                  self._doc_tree.SetFocus()
    1597                  self._rbtn_sort_by_type.SetValue(True)
    1598
    1599          #--------------------------------------------------------
    1600 -        def _on_sort_by_org_selected(self, evt):
    1601                  self._doc_tree.sort_mode = 'org'
    1602                  self._doc_tree.SetFocus()
    1603                  self._rbtn_sort_by_org.SetValue(True)
    1604
    1605          #--------------------------------------------------------
    1606          #--------------------------------------------------------
    1607 -        def _update_details(self, issue=None, episode=None, org_unit=None, document=None, part=None):
    1608
    1609                  self._LCTRL_details.set_string_items([])
    1610                  self._TCTRL_metainfo.Value = ''
    1611
    1612                  if document is None:
    1613                          if part is not None:
    1614                                  document = part.document
    1615
    1616                  if issue is None:
    1617                          if episode is not None:
    1618                                  issue = episode.health_issue
    1619
    1620                  items = []
    1621                  items.extend(self.__process_issue(issue))
    1622                  items.extend(self.__process_episode(episode))
    1623                  items.extend(self.__process_org_unit(org_unit))
    1624                  items.extend(self.__process_document(document))
    1625                  # keep this last so self.__pk_curr_doc_part stays current
    1626                  items.extend(self.__process_document_part(part))
    1627                  self._LCTRL_details.set_string_items(items)
    1628                  self._LCTRL_details.set_column_widths()
    1629                  self._LCTRL_details.set_resize_column(1)
    1630
    1631          #--------------------------------------------------------
    1632          # internal helper logic
    1633          #--------------------------------------------------------
    1634 -        def __check_cache_validity(self, pk_patient):
    1635                  if self.__pk_curr_pat == pk_patient:
    1636                          return
    1637                  self.__metainfo4parts = {}
    1638
    1639          #--------------------------------------------------------
    1640 -        def __receive_metainfo_from_worker(self, result):
    1641                  success, desc, pk_obj = result
    1642                  self.__metainfo4parts[pk_obj] = desc
    1643                  if not success:
    1644                          del self.__metainfo4parts[pk_obj]
    1645                  # safely cross thread boundaries
    1646                  wx.CallAfter(self.__update_metainfo, pk_obj)
    1647
    1648          #--------------------------------------------------------
    1649 -        def __update_metainfo(self, pk_obj, document_part=None):
    1650                  if self.__pk_curr_doc_part != pk_obj:
    1651                          # worker result arrived too late
    1652                          # but don't empty metainfo, might already contain cached value from new doc part node
    1653                          #self._TCTRL_metainfo.Value = ''
    1654                          return
    1655
    1656                  try:
    1657                          self._TCTRL_metainfo.Value = self.__metainfo4parts[pk_obj]
    1658                  except KeyError:
    1659                          document_part.format_metainfo(callback = self.__receive_metainfo_from_worker)
    1660
    1661          #--------------------------------------------------------
    1662 -        def __process_issue(self, issue):
    1663                  if issue is None:
    1664                          return []
    1665
    1666                  self.__check_cache_validity(issue['pk_patient'])
    1667                  self.__pk_curr_doc_part = None
    1668
    1669                  items = []
    1670                  items.append([_('Health issue'), '%s%s [#%s]' % (
    1671                          issue['description'],
    1672                          gmTools.coalesce (
    1673                                  value2test = issue['laterality'],
    1674                                  return_instead = '',
    1675                                  template4value = ' (%s)',
    1676                                  none_equivalents = [None, '', '?']
    1677                          ),
    1678                          issue['pk_health_issue']
    1679                  )])
    1680                  items.append([_('Status'), '%s, %s %s' % (
    1681                          gmTools.bool2subst(issue['is_active'], _('active'), _('inactive')),
    1682                          gmTools.bool2subst(issue['clinically_relevant'], _('clinically relevant'), _('not clinically relevant')),
    1683                          issue.diagnostic_certainty_description
    1684                  )])
    1685                  items.append([_('Confidential'), issue['is_confidential']])
    1686                  items.append([_('Age noted'), issue.age_noted_human_readable()])
    1687                  return items
    1688
    1689          #--------------------------------------------------------
    1690 -        def __process_episode(self, episode):
    1691                  if episode is None:
    1692                          return []
    1693
    1694                  self.__check_cache_validity(episode['pk_patient'])
    1695                  self.__pk_curr_doc_part = None
    1696
    1697                  items = []
    1698                  items.append([_('Episode'), '%s [#%s]' % (
    1699                          episode['description'],
    1700                          episode['pk_episode']
    1701                  )])
    1702                  items.append([_('Status'), '%s %s' % (
    1703                          gmTools.bool2subst(episode['episode_open'], _('active'), _('finished')),
    1704                          episode.diagnostic_certainty_description
    1705                  )])
    1706                  items.append([_('Health issue'), gmTools.coalesce(episode['health_issue'], '')])
    1707                  return items
    1708
    1709          #--------------------------------------------------------
    1710 -        def __process_org_unit(self, org_unit):
    1711                  if org_unit is None:
    1712                          return []
    1713
    1714                  # cannot check for cache validity: no patient reference
    1715                  # the doc-part-in-context, however, _will_ have changed
    1716                  self.__pk_curr_doc_part = None
    1717                  self._TCTRL_metainfo.Value = ''
    1718
    1719                  items = []
    1720                  items.append([_('Organization'), '%s (%s) [#%s]' % (
    1721                          org_unit['organization'],
    1722                          org_unit['l10n_organization_category'],
    1723                          org_unit['pk_org']
    1724                  )])
    1725                  items.append([_('Department'), '%s%s [#%s]' % (
    1726                          org_unit['unit'],
    1727                          gmTools.coalesce(org_unit['l10n_unit_category'], '', ' (%s)'),
    1728                          org_unit['pk_org_unit']
    1729                  )])
    1730                  adr = org_unit.address
    1731                  if adr is not None:
    1732                          lines = adr.format()
    1733                          items.append([lines[0], lines[1]])
    1734                          for line in lines[2:]:
    1735                                  items.append(['', line])
    1736                  for comm in org_unit.comm_channels:
    1737                          items.append([comm['l10n_comm_type'], '%s%s' % (
    1738                                  comm['url'],
    1739                                  gmTools.bool2subst(comm['is_confidential'], _(' (confidential)'), '', '')
    1740                          )])
    1741                  return items
    1742
    1743          #--------------------------------------------------------
    1744 -        def __process_document(self, document):
    1745                  if document is None:
    1746                          return []
    1747
    1748                  self.__check_cache_validity(document['pk_patient'])
    1749                  self.__pk_curr_doc_part = None
    1750
    1751                  items = []
    1752                  items.append([_('Document'), '%s [#%s]' % (document['l10n_type'], document['pk_doc'])])
    1753                  items.append([_('Generated'), gmDateTime.pydt_strftime(document['clin_when'], '%Y %b %d')])
    1754                  items.append([_('Health issue'), gmTools.coalesce(document['health_issue'], '', '%%s [#%s]' % document['pk_health_issue'])])
    1755                  items.append([_('Episode'), '%s (%s) [#%s]' % (
    1756                          document['episode'],
    1757                          gmTools.bool2subst(document['episode_open'], _('open'), _('closed')),
    1758                          document['pk_episode']
    1759                  )])
    1760                  if document['pk_org_unit'] is not None:
    1761                          if document['unit_is_receiver']:
    1762                                  header = _('Receiver')
    1763                          else:
    1764                                  header = _('Sender')
    1765                          items.append([header, '%s @ %s' % (document['unit'], document['organization'])])
    1766                  if document['ext_ref'] is not None:
    1767                          items.append([_('Reference'), document['ext_ref']])
    1768                  if document['comment'] is not None:
    1769                          items.append([_('Comment'), ' / '.join(document['comment'].split('\n'))])
    1770                  for proc in document.procedures:
    1771                          items.append([_('Procedure'), proc.format (
    1772                                  left_margin = 0,
    1773                                  include_episode = False,
    1774                                  include_codes = False,
    1775                                  include_address = False,
    1776                                  include_comm = False,
    1777                                  include_doc = False
    1778                          )])
    1779                  stay = document.hospital_stay
    1780                  if stay is not None:
    1781                          items.append([_('Hospital stay'), stay.format(include_episode = False)])
    1782                  for bill in document.bills:
    1783                          items.append([_('Bill'), bill.format (
    1784                                  include_receiver = False,
    1785                                  include_doc = False
    1786                          )])
    1787                  items.append([_('Modified'), gmDateTime.pydt_strftime(document['modified_when'], '%Y %b %d')])
    1788                  items.append([_('... by'), document['modified_by']])
    1789                  items.append([_('# encounter'), document['pk_encounter']])
    1790                  return items
    1791
    1792          #--------------------------------------------------------
    1793 -        def __process_document_part(self, document_part):
    1794                  if document_part is None:
    1795                          return []
    1796
    1797                  self.__check_cache_validity(document_part['pk_patient'])
    1798                  self.__pk_curr_doc_part = document_part['pk_obj']
    1799                  self.__update_metainfo(document_part['pk_obj'], document_part)
    1800                  items = []
    1801                  items.append(['', ''])
    1802                  if document_part['seq_idx'] is None:
    1803                          items.append([_('Part'), '#%s' % document_part['pk_obj']])
    1804                  else:
    1805                          items.append([_('Part'), '%s [#%s]' % (document_part['seq_idx'], document_part['pk_obj'])])
    1806                  if document_part['obj_comment'] is not None:
    1807                          items.append([_('Comment'), document_part['obj_comment']])
    1808                  if document_part['filename'] is not None:
    1809                          items.append([_('Filename'), document_part['filename']])
    1810                  items.append([_('Data size'), gmTools.size2str(document_part['size'])])
    1811                  review_parts = []
    1812                  if document_part['reviewed_by_you']:
    1813                          review_parts.append(_('by you'))
    1814                  if document_part['reviewed_by_intended_reviewer']:
    1815                          review_parts.append(_('by intended reviewer'))
    1816                  review = ', '.join(review_parts)
    1817                  if review == '':
    1818                          review = gmTools.u_diameter
    1819                  items.append([_('Reviewed'), review])
    1820                  #items.append([_(u'Reviewed'), gmTools.bool2subst(document_part['reviewed'], review, u'', u'?')])
    1821                  return items
    1822
    1823  #============================================================
    1824 -class cDocTree(wx.TreeCtrl, gmRegetMixin.cRegetOnPaintMixin, treemixin.ExpansionState):
    1825          """This wx.TreeCtrl derivative displays a tree view of stored medical documents.
    1826
    1827          It listens to document and patient changes and updates itself accordingly.
    1828
    1829          This acts on the current patient.
    1830          """
    1831          _sort_modes = ['age', 'review', 'episode', 'type', 'issue', 'org']
    1832          _root_node_labels = None
    1833
    1834          #--------------------------------------------------------
    1835 -        def __init__(self, parent, id, *args, **kwds):
    1836                  """Set up our specialised tree.
    1837                  """
    1838                  kwds['style'] = wx.TR_NO_BUTTONS | wx.NO_BORDER | wx.TR_SINGLE
    1839                  wx.TreeCtrl.__init__(self, parent, id, *args, **kwds)
    1840
    1841                  gmRegetMixin.cRegetOnPaintMixin.__init__(self)
    1842
    1843                  tmp = _('available documents (%s)')
    1844                  unsigned = _('unsigned (%s) on top') % '\u270D'
    1845                  cDocTree._root_node_labels = {
    1846                          'age': tmp % _('most recent on top'),
    1847                          'review': tmp % unsigned,
    1848                          'episode': tmp % _('sorted by episode'),
    1849                          'issue': tmp % _('sorted by health issue'),
    1850                          'type': tmp % _('sorted by type'),
    1851                          'org': tmp % _('sorted by organization')
    1852                  }
    1853
    1854                  self.root = None
    1855                  self.__sort_mode = 'age'
    1856
    1857                  self.__expanded_nodes = None
    1858                  self.__show_details_callback = None
    1859
    1860                  self.__build_context_menus()
    1861                  self.__register_interests()
    1862                  self._schedule_data_reget()
    1863
    1864          #--------------------------------------------------------
    1865          # external API
    1866          #--------------------------------------------------------
    1867 -        def display_selected_part(self, *args, **kwargs):
    1868
    1869                  node = self.GetSelection()
    1870                  node_data = self.GetItemData(node)
    1871
    1872                  if not isinstance(node_data, gmDocuments.cDocumentPart):
    1873                          return True
    1874
    1875                  self.__display_part(part = node_data)
    1876                  return True
    1877
    1878          #--------------------------------------------------------
    1879          # properties
    1880          #--------------------------------------------------------
    1881 -        def _get_sort_mode(self):
    1882                  return self.__sort_mode
    1883
    1884 -        def _set_sort_mode(self, mode):
    1885                  if mode is None:
    1886                          mode = 'age'
    1887
    1888                  if mode == self.__sort_mode:
    1889                          return
    1890
    1891                  if mode not in cDocTree._sort_modes:
    1892                          raise ValueError('invalid document tree sort mode [%s], valid modes: %s' % (mode, cDocTree._sort_modes))
    1893
    1894                  self.__sort_mode = mode
    1895                  self.__expanded_nodes = None
    1896
    1897                  curr_pat = gmPerson.gmCurrentPatient()
    1898                  if not curr_pat.connected:
    1899                          return
    1900
    1901                  self._schedule_data_reget()
    1902
    1903          sort_mode = property(_get_sort_mode, _set_sort_mode)
    1904
    1905          #--------------------------------------------------------
    1906 -        def _set_show_details_callback(self, callback):
    1907                  if callback is not None:
    1908                          if not callable(callback):
    1909                                  raise ValueError('<%s> is not callable')
    1910                  self.__show_details_callback = callback
    1911
    1912          show_details_callback = property(lambda x:x, _set_show_details_callback)
    1913
    1914          #--------------------------------------------------------
    1915          # reget-on-paint API
    1916          #--------------------------------------------------------
    1917 -        def _populate_with_data(self):
    1918                  curr_pat = gmPerson.gmCurrentPatient()
    1919                  if not curr_pat.connected:
    1920                          gmDispatcher.send(signal = 'statustext', msg = _('Cannot load documents. No active patient.'))
    1921                          return False
    1922
    1923                  if not self.__populate_tree():
    1924                          return False
    1925
    1926                  return True
    1927
    1928          #--------------------------------------------------------
    1929          # internal helpers
    1930          #--------------------------------------------------------
    1931 -        def __register_interests(self):
    1932                  self.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_tree_item_selected)
    1933                  self.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self._on_activate)
    1934                  self.Bind(wx.EVT_TREE_ITEM_MENU, self._on_tree_item_context_menu)
    1935                  self.Bind(wx.EVT_TREE_ITEM_GETTOOLTIP, self._on_tree_item_gettooltip)
    1936
    1937                  gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection)
    1938                  gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection)
    1939                  gmDispatcher.connect(signal = 'blobs.doc_med_mod_db', receiver = self._on_doc_mod_db)
    1940                  gmDispatcher.connect(signal = 'blobs.doc_obj_mod_db', receiver = self._on_doc_page_mod_db)
    1941
    1942          #--------------------------------------------------------
    1943 -        def __build_context_menus(self):
    1944
    1945                  # --- part context menu ---
    1946                  self.__part_context_menu = wx.Menu(title = _('Part Actions:'))
    1947
    1948                  item = self.__part_context_menu.Append(-1, _('Display part'))
    1949                  self.Bind(wx.EVT_MENU, self.__display_curr_part, item)
    1950                  item = self.__part_context_menu.Append(-1, _('%s Sign/Edit properties') % '\u270D')
    1951                  self.Bind(wx.EVT_MENU, self.__review_curr_part, item)
    1952
    1953                  self.__part_context_menu.AppendSeparator()
    1954
    1955                  item = self.__part_context_menu.Append(-1, _('Delete part'))
    1956                  self.Bind(wx.EVT_MENU, self.__delete_part, item, item)
    1957                  item = self.__part_context_menu.Append(-1, _('Move part'))
    1958                  self.Bind(wx.EVT_MENU, self.__move_part, item)
    1959                  item = self.__part_context_menu.Append(-1, _('Print part'))
    1960                  self.Bind(wx.EVT_MENU, self.__print_part, item)
    1961                  item = self.__part_context_menu.Append(-1, _('Fax part'))
    1962                  self.Bind(wx.EVT_MENU, self.__fax_part, item)
    1963                  item = self.__part_context_menu.Append(-1, _('Mail part'))
    1964                  self.Bind(wx.EVT_MENU, self.__mail_part, item)
    1965                  item = self.__part_context_menu.Append(-1, _('Save part to disk'))
    1966                  self.Bind(wx.EVT_MENU, self.__save_part_to_disk, item)
    1967
    1968                  self.__part_context_menu.AppendSeparator()                      # so we can append more items
    1969
    1970                  # --- doc context menu ---
    1971                  self.__doc_context_menu = wx.Menu(title = _('Document Actions:'))
    1972
    1973                  item = self.__doc_context_menu.Append(-1, _('%s Sign/Edit properties') % '\u270D')
    1974                  self.Bind(wx.EVT_MENU, self.__review_curr_part, item)
    1975                  item = self.__doc_context_menu.Append(-1, _('Delete document'))
    1976                  self.Bind(wx.EVT_MENU, self.__delete_document, item)
    1977
    1978                  self.__doc_context_menu.AppendSeparator()
    1979
    1980                  item = self.__doc_context_menu.Append(-1, _('Add parts'))
    1981                  self.Bind(wx.EVT_MENU, self.__add_part, item)
    1982                  item = self.__doc_context_menu.Append(-1, _('Add part from clipboard'))
    1983                  self.Bind(wx.EVT_MENU, self.__add_part_from_clipboard, item, item)
    1984                  item = self.__doc_context_menu.Append(-1, _('Print all parts'))
    1985                  self.Bind(wx.EVT_MENU, self.__print_doc, item)
    1986                  item = self.__doc_context_menu.Append(-1, _('Fax all parts'))
    1987                  self.Bind(wx.EVT_MENU, self.__fax_doc, item)
    1988                  item = self.__doc_context_menu.Append(-1, _('Mail all parts'))
    1989                  self.Bind(wx.EVT_MENU, self.__mail_doc, item)
    1990                  item = self.__doc_context_menu.Append(-1, _('Save all parts to disk'))
    1991                  self.Bind(wx.EVT_MENU, self.__save_doc_to_disk, item)
    1992                  item = self.__doc_context_menu.Append(-1, _('Copy all parts to export area'))
    1993                  self.Bind(wx.EVT_MENU, self.__copy_doc_to_export_area, item)
    1994
    1995                  self.__doc_context_menu.AppendSeparator()
    1996
    1997                  item = self.__doc_context_menu.Append(-1, _('Access external original'))
    1998                  self.Bind(wx.EVT_MENU, self.__access_external_original, item)
    1999                  item = self.__doc_context_menu.Append(-1, _('Edit corresponding encounter'))
    2000                  self.Bind(wx.EVT_MENU, self.__edit_encounter_details, item)
    2001                  item = self.__doc_context_menu.Append(-1, _('Select corresponding encounter'))
    2002                  self.Bind(wx.EVT_MENU, self.__select_encounter, item)
    2003                  item = self.__doc_context_menu.Append(-1, _('Manage descriptions'))
    2004                  self.Bind(wx.EVT_MENU, self.__manage_document_descriptions, item)
    2005
    2006                  # document / description
    2007  #               self.__desc_menu = wx.Menu()
    2008  #               item = self.__doc_context_menu.Append(-1, _('Descriptions ...'), self.__desc_menu)
    2009  #               item = self.__desc_menu.Append(-1, _('Add new description'))
    2010  #               self.Bind(wx.EVT_MENU, self.__desc_menu, self.__add_doc_desc, item)
    2011  #               item = self.__desc_menu.Append(-1, _('Delete description'))
    2012  #               self.Bind(wx.EVT_MENU, self.__desc_menu, self.__del_doc_desc, item)
    2013  #               self.__desc_menu.AppendSeparator()
    2014
    2015          #--------------------------------------------------------
    2016 -        def __populate_tree(self):
    2017
    2018                  wx.BeginBusyCursor()
    2019
    2020                  # clean old tree
    2021                  if self.root is not None:
    2022                          self.DeleteAllItems()
    2023
    2024                  # init new tree
    2025                  self.root = self.AddRoot(cDocTree._root_node_labels[self.__sort_mode], -1, -1)
    2026                  self.SetItemData(self.root, None)
    2027                  self.SetItemHasChildren(self.root, False)
    2028
    2029                  # read documents from database
    2030                  curr_pat = gmPerson.gmCurrentPatient()
    2031                  docs_folder = curr_pat.get_document_folder()
    2032                  docs = docs_folder.get_documents()
    2033
    2034                  if docs is None:
    2035                          gmGuiHelpers.gm_show_error (
    2036                                  aMessage = _('Error searching documents.'),
    2037                                  aTitle = _('loading document list')
    2038                          )
    2039                          # avoid recursion of GUI updating
    2040                          wx.EndBusyCursor()
    2041                          return True
    2042
    2043                  if len(docs) == 0:
    2044                          wx.EndBusyCursor()
    2045                          return True
    2046
    2047                  # fill new tree from document list
    2048                  self.SetItemHasChildren(self.root, True)
    2049
    2050                  # add our documents as first level nodes
    2051                  intermediate_nodes = {}
    2052                  for doc in docs:
    2053
    2054                          parts = doc.parts
    2055
    2056                          if len(parts) == 0:
    2057                                  no_parts = _('no parts')
    2058                          elif len(parts) == 1:
    2059                                  no_parts = _('1 part')
    2060                          else:
    2061                                  no_parts = _('%s parts') % len(parts)
    2062
    2063                          # need intermediate branch level ?
    2064                          if self.__sort_mode == 'episode':
    2065                                  intermediate_label = '%s%s' % (doc['episode'], gmTools.coalesce(doc['health_issue'], '', ' (%s)'))
    2066                                  doc_label = _('%s%7s %s:%s (%s)') % (
    2067                                          gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
    2068                                          gmDateTime.pydt_strftime(doc['clin_when'], '%m/%Y'),
    2069                                          doc['l10n_type'][:26],
    2070                                          gmTools.coalesce(value2test = doc['comment'], return_instead = '', template4value = ' %s'),
    2071                                          no_parts
    2072                                  )
    2073                                  if intermediate_label not in intermediate_nodes:
    2074                                          intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
    2075                                          self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
    2076                                          self.SetItemData(intermediate_nodes[intermediate_label], {'pk_episode': doc['pk_episode']})
    2077                                          self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
    2078                                  parent = intermediate_nodes[intermediate_label]
    2079
    2080                          elif self.__sort_mode == 'type':
    2081                                  intermediate_label = doc['l10n_type']
    2082                                  doc_label = _('%s%7s (%s):%s') % (
    2083                                          gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
    2084                                          gmDateTime.pydt_strftime(doc['clin_when'], '%m/%Y'),
    2085                                          no_parts,
    2086                                          gmTools.coalesce(value2test = doc['comment'], return_instead = '', template4value = ' %s')
    2087                                  )
    2088                                  if intermediate_label not in intermediate_nodes:
    2089                                          intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
    2090                                          self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
    2091                                          self.SetItemData(intermediate_nodes[intermediate_label], None)
    2092                                          self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
    2093                                  parent = intermediate_nodes[intermediate_label]
    2094
    2095                          elif self.__sort_mode == 'issue':
    2096                                  if doc['health_issue'] is None:
    2097                                          intermediate_label = _('%s (unattributed episode)') % doc['episode']
    2098                                  else:
    2099                                          intermediate_label = doc['health_issue']
    2100                                  doc_label = _('%s%7s %s:%s (%s)') % (
    2101                                          gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
    2102                                          gmDateTime.pydt_strftime(doc['clin_when'], '%m/%Y'),
    2103                                          doc['l10n_type'][:26],
    2104                                          gmTools.coalesce(value2test = doc['comment'], return_instead = '', template4value = ' %s'),
    2105                                          no_parts
    2106                                  )
    2107                                  if intermediate_label not in intermediate_nodes:
    2108                                          intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
    2109                                          self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
    2110                                          self.SetItemData(intermediate_nodes[intermediate_label], {'pk_health_issue': doc['pk_health_issue']})
    2111                                          self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
    2112                                  parent = intermediate_nodes[intermediate_label]
    2113
    2114                          elif self.__sort_mode == 'org':
    2115                                  if doc['pk_org'] is None:
    2116                                          intermediate_label = _('unknown organization')
    2117                                  else:
    2118                                          if doc['unit_is_receiver']:
    2119                                                  direction = _('to: %s')
    2120                                          else:
    2121                                                  direction = _('from: %s')
    2122                                          # this praxis ?
    2123                                          if doc['pk_org'] == gmPraxis.gmCurrentPraxisBranch()['pk_org']:
    2124                                                  org_str = _('this praxis')
    2125                                          else:
    2126                                                   org_str = doc['organization']
    2127                                          intermediate_label = direction % org_str
    2128                                  doc_label = _('%s%7s %s:%s (%s)') % (
    2129                                          gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
    2130                                          gmDateTime.pydt_strftime(doc['clin_when'], '%m/%Y'),
    2131                                          doc['l10n_type'][:26],
    2132                                          gmTools.coalesce(value2test = doc['comment'], return_instead = '', template4value = ' %s'),
    2133                                          no_parts
    2134                                  )
    2135                                  if intermediate_label not in intermediate_nodes:
    2136                                          intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
    2137                                          self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
    2138                                          # not quite right: always shows data of the _last_ document of _any_ org unit of this org
    2139                                          self.SetItemData(intermediate_nodes[intermediate_label], doc.org_unit)
    2140                                          self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
    2141                                  parent = intermediate_nodes[intermediate_label]
    2142
    2143                          elif self.__sort_mode == 'age':
    2144                                  intermediate_label = gmDateTime.pydt_strftime(doc['clin_when'], '%Y')
    2145                                  doc_label = _('%s%7s %s:%s (%s)') % (
    2146                                          gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
    2147                                          gmDateTime.pydt_strftime(doc['clin_when'], '%b %d'),
    2148                                          doc['l10n_type'][:26],
    2149                                          gmTools.coalesce(value2test = doc['comment'], return_instead = '', template4value = ' %s'),
    2150                                          no_parts
    2151                                  )
    2152                                  if intermediate_label not in intermediate_nodes:
    2153                                          intermediate_nodes[intermediate_label] = self.AppendItem(parent = self.root, text = intermediate_label)
    2154                                          self.SetItemBold(intermediate_nodes[intermediate_label], bold = True)
    2155                                          self.SetItemData(intermediate_nodes[intermediate_label], doc['clin_when'])
    2156                                          self.SetItemHasChildren(intermediate_nodes[intermediate_label], True)
    2157                                  parent = intermediate_nodes[intermediate_label]
    2158
    2159                          else:
    2160                                  doc_label = _('%s%7s %s:%s (%s)') % (
    2161                                          gmTools.bool2subst(doc.has_unreviewed_parts, gmTools.u_writing_hand, '', '?'),
    2162                                          gmDateTime.pydt_strftime(doc['clin_when'], '%Y-%m'),
    2163                                          doc['l10n_type'][:26],
    2164                                          gmTools.coalesce(value2test = doc['comment'], return_instead = '', template4value = ' %s'),
    2165                                          no_parts
    2166                                  )
    2167                                  parent = self.root
    2168
    2169                          doc_node = self.AppendItem(parent = parent, text = doc_label)
    2170                          #self.SetItemBold(doc_node, bold = True)
    2171                          self.SetItemData(doc_node, doc)
    2172                          if len(parts) == 0:
    2173                                  self.SetItemHasChildren(doc_node, False)
    2174                          else:
    2175                                  self.SetItemHasChildren(doc_node, True)
    2176
    2177                          # now add parts as child nodes
    2178                          for part in parts:
    2179                                  f_ext = ''
    2180                                  if part['filename'] is not None:
    2181                                          f_ext = os.path.splitext(part['filename'])[1].strip('.').strip()
    2182                                  if f_ext != '':
    2183                                          f_ext = ' .' + f_ext.upper()
    2184                                  label = '%s%s (%s%s)%s' % (
    2185                                          gmTools.bool2str (
    2186                                                  boolean = part['reviewed'] or part['reviewed_by_you'] or part['reviewed_by_intended_reviewer'],
    2187                                                  true_str = '',
    2188                                                  false_str = gmTools.u_writing_hand
    2189                                          ),
    2190                                          _('part %2s') % part['seq_idx'],
    2191                                          gmTools.size2str(part['size']),
    2192                                          f_ext,
    2193                                          gmTools.coalesce (
    2194                                                  part['obj_comment'],
    2195                                                  '',
    2196                                                  ': %s%%s%s' % (gmTools.u_left_double_angle_quote, gmTools.u_right_double_angle_quote)
    2197                                          )
    2198                                  )
    2199
    2200                                  part_node = self.AppendItem(parent = doc_node, text = label)
    2201                                  self.SetItemData(part_node, part)
    2202                                  self.SetItemHasChildren(part_node, False)
    2203
    2204                  self.__sort_nodes()
    2205                  self.SelectItem(self.root)
    2206
    2207                  # restore expansion state
    2208                  if self.__expanded_nodes is not None:
    2209                          self.ExpansionState = self.__expanded_nodes
    2210                  # but always expand root node
    2211                  self.Expand(self.root)
    2212                  # if no expansion state available then
    2213                  # expand intermediate nodes as well
    2214                  if self.__expanded_nodes is None:
    2215                          # but only if there are any
    2216                          if self.__sort_mode in ['episode', 'type', 'issue', 'org']:
    2217                                  for key in intermediate_nodes.keys():
    2218                                          self.Expand(intermediate_nodes[key])
    2219
    2220                  wx.EndBusyCursor()
    2221
    2222                  return True
    2223
    2224          #------------------------------------------------------------------------
    2225 -        def OnCompareItems (self, node1=None, node2=None):
    2226                  """Used in sorting items.
    2227
    2228                  -1: 1 < 2
    2229                   0: 1 = 2
    2230                   1: 1 > 2
    2231                  """
    2232                  # Windows can send bogus events so ignore that
    2233                  if not node1:
    2234                          _log.debug('invalid node 1')
    2235                          return 0
    2236                  if not node2:
    2237                          _log.debug('invalid node 2')
    2238                          return 0
    2239                  if not node1.IsOk():
    2240                          _log.debug('no data on node 1')
    2241                          return 0
    2242                  if not node2.IsOk():
    2243                          _log.debug('no data on node 2')
    2244                          return 0
    2245
    2246                  data1 = self.GetItemData(node1)
    2247                  data2 = self.GetItemData(node2)
    2248
    2249                  # doc node
    2250                  if isinstance(data1, gmDocuments.cDocument):
    2251                          date_field = 'clin_when'
    2252                          #date_field = 'modified_when'
    2253                          if self.__sort_mode == 'age':
    2254                                  # reverse sort by date
    2255                                  if data1[date_field] > data2[date_field]:
    2256                                          return -1
    2257                                  if data1[date_field] == data2[date_field]:
    2258                                          return 0
    2259                                  return 1
    2260                          if self.__sort_mode == 'episode':
    2261                                  if data1['episode'] < data2['episode']:
    2262                                          return -1
    2263                                  if data1['episode'] == data2['episode']:
    2264                                          # inner sort: reverse by date
    2265                                          if data1[date_field] > data2[date_field]:
    2266                                                  return -1
    2267                                          if data1[date_field] == data2[date_field]:
    2268                                                  return 0
    2269                                          return 1
    2270                                  return 1
    2271                          if self.__sort_mode == 'issue':
    2272                                  if data1['health_issue'] == data2['health_issue']:
    2273                                          # inner sort: reverse by date
    2274                                          if data1[date_field] > data2[date_field]:
    2275                                                  return -1
    2276                                          if data1[date_field] == data2[date_field]:
    2277                                                  return 0
    2278                                          return 1
    2279                                  if data1['health_issue'] < data2['health_issue']:
    2280                                          return -1
    2281                                  return 1
    2282                          if self.__sort_mode == 'review':
    2283                                  # equality
    2284                                  if data1.has_unreviewed_parts == data2.has_unreviewed_parts:
    2285                                          # inner sort: reverse by date
    2286                                          if data1[date_field] > data2[date_field]:
    2287                                                  return -1
    2288                                          if data1[date_field] == data2[date_field]:
    2289                                                  return 0
    2290                                          return 1
    2291                                  if data1.has_unreviewed_parts:
    2292                                          return -1
    2293                                  return 1
    2294                          if self.__sort_mode == 'type':
    2295                                  if data1['l10n_type'] < data2['l10n_type']:
    2296                                          return -1
    2297                                  if data1['l10n_type'] == data2['l10n_type']:
    2298                                          # inner sort: reverse by date
    2299                                          if data1[date_field] > data2[date_field]:
    2300                                                  return -1
    2301                                          if data1[date_field] == data2[date_field]:
    2302                                                  return 0
    2303                                          return 1
    2304                                  return 1
    2305                          if self.__sort_mode == 'org':
    2306                                  if (data1['organization'] is None) and (data2['organization'] is None):
    2307                                          return 0
    2308                                  if (data1['organization'] is None) and (data2['organization'] is not None):
    2309                                          return 1
    2310                                  if (data1['organization'] is not None) and (data2['organization'] is None):
    2311                                          return -1
    2312                                  txt1 = '%s %s' % (data1['organization'], data1['unit'])
    2313                                  txt2 = '%s %s' % (data2['organization'], data2['unit'])
    2314                                  if txt1 < txt2:
    2315                                          return -1
    2316                                  if txt1 == txt2:
    2317                                          # inner sort: reverse by date
    2318                                          if data1[date_field] > data2[date_field]:
    2319                                                  return -1
    2320                                          if data1[date_field] == data2[date_field]:
    2321                                                  return 0
    2322                                          return 1
    2323                                  return 1
    2324
    2325                          _log.error('unknown document sort mode [%s], reverse-sorting by age', self.__sort_mode)
    2326                          # reverse sort by date
    2327                          if data1[date_field] > data2[date_field]:
    2328                                  return -1
    2329                          if data1[date_field] == data2[date_field]:
    2330                                  return 0
    2331                          return 1
    2332
    2333                  # part node
    2334                  if isinstance(data1, gmDocuments.cDocumentPart):
    2335                          # compare sequence IDs (= "page" numbers)
    2336                          # FIXME: wrong order ?
    2337                          if data1['seq_idx'] < data2['seq_idx']:
    2338                                  return -1
    2339                          if data1['seq_idx'] == data2['seq_idx']:
    2340                                  return 0
    2341                          return 1
    2342
    2343                  # org unit node
    2344                  if isinstance(data1, gmOrganization.cOrgUnit):
    2345                          l1 = self.GetItemText(node1)
    2346                          l2 = self.GetItemText(node2)
    2347                          if l1 < l2:
    2348                                  return -1
    2349                          if l1 == l2:
    2350                                  return 0
    2351                          return 1
    2352
    2353                  # episode or issue node
    2354                  if isinstance(data1, dict):
    2355                          if ('pk_episode' in data1) or ('pk_health_issue' in data1):
    2356                                  l1 = self.GetItemText(node1)
    2357                                  l2 = self.GetItemText(node2)
    2358                                  if l1 < l2:
    2359                                          return -1
    2360                                  if l1 == l2:
    2361                                          return 0
    2362                                  return 1
    2363                          _log.error('dict but unknown content: %s', data1.keys())
    2364                          return 1
    2365
    2366                  # doc.year node
    2367                  if isinstance(data1, pydt.datetime):
    2368                          y1 = gmDateTime.pydt_strftime(data1, '%Y')
    2369                          y2 = gmDateTime.pydt_strftime(data2, '%Y')
    2370                          # reverse chronologically
    2371                          if y1 < y2:
    2372                                  return 1
    2373                          if y1 == y2:
    2374                                  return 0
    2375                          return -1
    2376
    2377                  # type node
    2378                  # else sort alphabetically by label
    2379                  if None in [data1, data2]:
    2380                          l1 = self.GetItemText(node1)
    2381                          l2 = self.GetItemText(node2)
    2382                          if l1 < l2:
    2383                                  return -1
    2384                          if l1 == l2:
    2385                                  return 0
    2386                  else:
    2387                          if data1 < data2:
    2388                                  return -1
    2389                          if data1 == data2:
    2390                                  return 0
    2391                  return 1
    2392
    2393          #------------------------------------------------------------------------
    2394          # event handlers
    2395          #------------------------------------------------------------------------
    2396 -        def _on_doc_mod_db(self, *args, **kwargs):
    2397                  self.__expanded_nodes = self.ExpansionState
    2398                  self._schedule_data_reget()
    2399
    2400          #------------------------------------------------------------------------
    2401 -        def _on_doc_page_mod_db(self, *args, **kwargs):
    2402                  self.__expanded_nodes = self.ExpansionState
    2403                  self._schedule_data_reget()
    2404
    2405          #------------------------------------------------------------------------
    2406 -        def _on_pre_patient_unselection(self, *args, **kwargs):
    2407                  # empty out tree
    2408                  if self.root is not None:
    2409                          self.DeleteAllItems()
    2410                  self.root = None
    2411
    2412          #------------------------------------------------------------------------
    2413 -        def _on_post_patient_selection(self, *args, **kwargs):
    2414                  # FIXME: self.__load_expansion_history_from_db (but not apply it !)
    2415                  self.__expanded_nodes = None
    2416                  self._schedule_data_reget()
    2417
    2418          #--------------------------------------------------------
    2419 -        def __update_details_view(self):
    2420                  if self.__curr_node_data is None:
    2421                          return
    2422
    2423                  # pseudo root node or "type"
    2424                  if self.__curr_node_data is None:
    2425                          self.__show_details_callback(document = None, part = None)
    2426                          return
    2427
    2428                  # document node
    2429                  if isinstance(self.__curr_node_data, gmDocuments.cDocument):
    2430                          self.__show_details_callback(document = self.__curr_node_data, part = None)
    2431                          return
    2432
    2433                  if isinstance(self.__curr_node_data, gmDocuments.cDocumentPart):
    2434                          doc = self.GetItemData(self.GetItemParent(self.__curr_node))
    2435                          self.__show_details_callback(document = doc, part = self.__curr_node_data)
    2436                          return
    2437
    2438                  if isinstance(self.__curr_node_data, gmOrganization.cOrgUnit):
    2439                          self.__show_details_callback(org_unit = self.__curr_node_data)
    2440                          return
    2441
    2442                  if isinstance(self.__curr_node_data, pydt.datetime):
    2443                          # could be getting some statistics about the year
    2444                          return
    2445
    2446                  if isinstance(self.__curr_node_data, dict):
    2447                          _log.debug('node data is dict: %s', self.__curr_node_data)
    2448                          try:
    2449                                  if self.__curr_node_data['pk_health_issue'] is None:
    2450                                          _log.debug('node data dict holds pseudo-issue for unattributed episodes, ignoring')
    2451                                  else:
    2452                                          issue = gmEMRStructItems.cHealthIssue(aPK_obj = self.__curr_node_data['pk_health_issue'])
    2453                          except KeyError:
    2454                                  issue = None
    2455                          try:
    2456                                  episode = gmEMRStructItems.cEpisode(aPK_obj = self.__curr_node_data['pk_episode'])
    2457                          except KeyError:
    2458                                  episode = None
    2459                          self.__show_details_callback(issue = issue, episode = episode)
    2460                          return
    2461
    2462  #               # string nodes are labels such as episodes which may or may not have children
    2463  #               if isinstance(self.__curr_node_data, str):
    2464  #                       self.__show_details_callback(document = None, part = None)
    2465  #                       return
    2466
    2467                  raise ValueError('invalid document tree node data type: %s' % type(self.__curr_node_data))
    2468
    2469          #--------------------------------------------------------
    2470 -        def _on_tree_item_selected(self, event):
    2471                  event.Skip()
    2472                  self.__curr_node = event.GetItem()
    2473                  self.__curr_node_data = self.GetItemData(self.__curr_node)
    2474                  self.__update_details_view()
    2475
    2476          #------------------------------------------------------------------------
    2477 -        def _on_activate(self, event):
    2478                  node = event.GetItem()
    2479                  node_data = self.GetItemData(node)
    2480
    2481                  # exclude pseudo root node
    2482                  if node_data is None:
    2483                          return None
    2484
    2485                  if isinstance(node_data, gmDocuments.cDocumentPart):
    2486                          self.__display_part(part = node_data)
    2487                          return True
    2488
    2489                  event.Skip()
    2490
    2491          #--------------------------------------------------------
    2510
    2511          #--------------------------------------------------------
    2512 -        def __activate_as_current_photo(self, evt):
    2513                  self.__curr_node_data.set_as_active_photograph()
    2514
    2515          #--------------------------------------------------------
    2516 -        def __display_curr_part(self, evt):
    2517                  self.__display_part(part = self.__curr_node_data)
    2518
    2519          #--------------------------------------------------------
    2520 -        def __review_curr_part(self, evt):
    2521                  self.__review_part(part = self.__curr_node_data)
    2522
    2523          #--------------------------------------------------------
    2524 -        def __manage_document_descriptions(self, evt):
    2525                  manage_document_descriptions(parent = self, document = self.__curr_node_data)
    2526
    2527          #--------------------------------------------------------
    2528 -        def _on_tree_item_gettooltip(self, event):
    2529
    2530                  item = event.GetItem()
    2531
    2532                  if not item.IsOk():
    2533                          event.SetToolTip('')
    2534                          return
    2535
    2536                  data = self.GetItemData(item)
    2537                  # documents, parts
    2538                  if isinstance(data, (gmDocuments.cDocument, gmDocuments.cDocumentPart)):
    2539                          tt = data.format()
    2540                  elif isinstance(data, gmOrganization.cOrgUnit):
    2541                          tt = '\n'.join(data.format(with_address = True, with_org = True, with_comms = True))
    2542                  elif isinstance(data, dict):
    2543                          try:
    2544                                  tt = data['tooltip']
    2545                          except KeyError:
    2546                                  tt = ''
    2547  #               # explicit tooltip strings
    2548  #               elif isinstance(data, str):
    2549  #                       tt = data
    2550  #               # other (root, "None")
    2551                  else:
    2552                          tt = ''
    2553
    2554                  event.SetToolTip(tt)
    2555
    2556          #--------------------------------------------------------
    2557          # internal API
    2558          #--------------------------------------------------------
    2559 -        def __sort_nodes(self, start_node=None):
    2560
    2561                  if start_node is None:
    2562                          start_node = self.GetRootItem()
    2563
    2564                  # protect against empty tree where not even
    2565                  # a root node exists
    2566                  if not start_node.IsOk():
    2567                          return True
    2568
    2569                  self.SortChildren(start_node)
    2570
    2571                  child_node, cookie = self.GetFirstChild(start_node)
    2572                  while child_node.IsOk():
    2573                          self.__sort_nodes(start_node = child_node)
    2574                          child_node, cookie = self.GetNextChild(start_node, cookie)
    2575
    2576                  return
    2577
    2578          #--------------------------------------------------------
    2579 -        def __handle_doc_context(self):
    2580                  self.PopupMenu(self.__doc_context_menu, wx.DefaultPosition)
    2581
    2582          #--------------------------------------------------------
    2583 -        def __handle_part_context(self):
    2584                  ID = None
    2585                  # make active patient photograph
    2586                  if self.__curr_node_data['type'] == 'patient photograph':
    2587                          item = self.__part_context_menu.Append(-1, _('Activate as current photo'))
    2588                          self.Bind(wx.EVT_MENU, self.__activate_as_current_photo, item)
    2589                          ID = item.Id
    2590
    2591                  self.PopupMenu(self.__part_context_menu, wx.DefaultPosition)
    2592
    2593                  if ID is not None:
    2594                          self.__part_context_menu.Delete(ID)
    2595
    2596          #--------------------------------------------------------
    2597          # part level context menu handlers
    2598          #--------------------------------------------------------
    2599 -        def __display_part(self, part):
    2600                  """Display document part."""
    2601
    2602                  # sanity check
    2603                  if part['size'] == 0:
    2604                          _log.debug('cannot display part [%s] - 0 bytes', part['pk_obj'])
    2605                          gmGuiHelpers.gm_show_error (
    2606                                  aMessage = _('Document part does not seem to exist in database !'),
    2607                                  aTitle = _('showing document')
    2608                          )
    2609                          return None
    2610
    2611                  wx.BeginBusyCursor()
    2612
    2613                  cfg = gmCfg.cCfgSQL()
    2614
    2615                  # determine database export chunk size
    2616                  chunksize = int(
    2617                  cfg.get2 (
    2618                          option = "horstspace.blob_export_chunk_size",
    2619                          workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
    2620                          bias = 'workplace',
    2621                          default = default_chunksize
    2622                  ))
    2623
    2624                  # shall we force blocking during view ?
    2625                  block_during_view = bool( cfg.get2 (
    2626                          option = 'horstspace.document_viewer.block_during_view',
    2627                          workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
    2628                          bias = 'user',
    2629                          default = None
    2630                  ))
    2631
    2632                  # display it
    2633                  successful, msg = part.display_via_mime (
    2634                          chunksize = chunksize,
    2635                          block = block_during_view
    2636                  )
    2637
    2638                  wx.EndBusyCursor()
    2639
    2640                  if not successful:
    2641                          gmGuiHelpers.gm_show_error (
    2642                                  aMessage = _('Cannot display document part:\n%s') % msg,
    2643                                  aTitle = _('showing document')
    2644                          )
    2645                          return None
    2646
    2647                  # handle review after display
    2648                  # 0: never
    2649                  # 1: always
    2650                  # 2: if no review by myself exists yet
    2651                  # 3: if no review at all exists yet
    2652                  # 4: if no review by responsible reviewer
    2653                  review_after_display = int(cfg.get2 (
    2654                          option = 'horstspace.document_viewer.review_after_display',
    2655                          workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
    2656                          bias = 'user',
    2657                          default = 3
    2658                  ))
    2659                  if review_after_display == 1:                   # always review
    2660                          self.__review_part(part=part)
    2661                  elif review_after_display == 2:                 # review if no review by me exists
    2662                          review_by_me = [ rev for rev in part.get_reviews() if rev['is_your_review'] ]
    2663                          if len(review_by_me) == 0:
    2664                                  self.__review_part(part = part)
    2665                  elif review_after_display == 3:
    2666                          if len(part.get_reviews()) == 0:
    2667                                  self.__review_part(part = part)
    2668                  elif review_after_display == 4:
    2669                          reviewed_by_responsible = [ rev for rev in part.get_reviews() if rev['is_review_by_responsible_reviewer'] ]
    2670                          if len(reviewed_by_responsible) == 0:
    2671                                  self.__review_part(part = part)
    2672
    2673                  return True
    2674          #--------------------------------------------------------
    2675 -        def __review_part(self, part=None):
    2676                  dlg = cReviewDocPartDlg (
    2677                          parent = self,
    2678                          id = -1,
    2679                          part = part
    2680                  )
    2681                  dlg.ShowModal()
    2682                  dlg.DestroyLater()
    2683          #--------------------------------------------------------
    2684 -        def __move_part(self, evt):
    2685                  target_doc = manage_documents (
    2686                          parent = self,
    2687                          msg = _('\nSelect the document into which to move the selected part !\n')
    2688                  )
    2689                  if target_doc is None:
    2690                          return
    2691                  if not self.__curr_node_data.reattach(pk_doc = target_doc['pk_doc']):
    2692                          gmGuiHelpers.gm_show_error (
    2693                                  aMessage = _('Cannot move document part.'),
    2694                                  aTitle = _('Moving document part')
    2695                          )
    2696          #--------------------------------------------------------
    2697 -        def __delete_part(self, evt):
    2698                  delete_it = gmGuiHelpers.gm_show_question (
    2699                          cancel_button = True,
    2700                          title = _('Deleting document part'),
    2701                          question = _(
    2702                                  'Are you sure you want to delete the %s part #%s\n'
    2703                                  '\n'
    2704                                  '%s'
    2705                                  'from the following document\n'
    2706                                  '\n'
    2707                                  ' %s (%s)\n'
    2708                                  '%s'
    2709                                  '\n'
    2710                                  'Really delete ?\n'
    2711                                  '\n'
    2712                                  '(this action cannot be reversed)'
    2713                          ) % (
    2714                                  gmTools.size2str(self.__curr_node_data['size']),
    2715                                  self.__curr_node_data['seq_idx'],
    2716                                  gmTools.coalesce(self.__curr_node_data['obj_comment'], '', ' "%s"\n\n'),
    2717                                  self.__curr_node_data['l10n_type'],
    2718                                  gmDateTime.pydt_strftime(self.__curr_node_data['date_generated'], format = '%Y-%m-%d', accuracy = gmDateTime.acc_days),
    2719                                  gmTools.coalesce(self.__curr_node_data['doc_comment'], '', ' "%s"\n')
    2720                          )
    2721                  )
    2722                  if not delete_it:
    2723                          return
    2724
    2725                  gmDocuments.delete_document_part (
    2726                          part_pk = self.__curr_node_data['pk_obj'],
    2727                          encounter_pk = gmPerson.gmCurrentPatient().emr.active_encounter['pk_encounter']
    2728                  )
    2729          #--------------------------------------------------------
    2730 -        def __process_part(self, action=None, l10n_action=None):
    2731
    2732                  gmHooks.run_hook_script(hook = 'before_%s_doc_part' % action)
    2733
    2734                  wx.BeginBusyCursor()
    2735
    2736                  # detect wrapper
    2737                  found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc' % action)
    2738                  if not found:
    2739                          found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc.bat' % action)
    2740                  if not found:
    2741                          _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action)
    2742                          wx.EndBusyCursor()
    2743                          gmGuiHelpers.gm_show_error (
    2744                                  _('Cannot %(l10n_action)s document part - %(l10n_action)s command not found.\n'
    2745                                    '\n'
    2746                                    'Either of gm-%(action)s_doc or gm-%(action)s_doc.bat\n'
    2747                                    'must be in the execution path. The command will\n'
    2748                                    'be passed the filename to %(l10n_action)s.'
    2749                                  ) % {'action': action, 'l10n_action': l10n_action},
    2750                                  _('Processing document part: %s') % l10n_action
    2751                          )
    2752                          return
    2753
    2754                  cfg = gmCfg.cCfgSQL()
    2755
    2756                  # determine database export chunk size
    2757                  chunksize = int(cfg.get2 (
    2758                          option = "horstspace.blob_export_chunk_size",
    2759                          workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
    2760                          bias = 'workplace',
    2761                          default = default_chunksize
    2762                  ))
    2763
    2764                  part_file = self.__curr_node_data.save_to_file(aChunkSize = chunksize)
    2765
    2766                  if action == 'print':
    2767                          cmd = '%s generic_document %s' % (external_cmd, part_file)
    2768                  else:
    2769                          cmd = '%s %s' % (external_cmd, part_file)
    2770                  if os.name == 'nt':
    2771                          blocking = True
    2772                  else:
    2773                          blocking = False
    2774                  success = gmShellAPI.run_command_in_shell (
    2775                          command = cmd,
    2776                          blocking = blocking
    2777                  )
    2778
    2779                  wx.EndBusyCursor()
    2780
    2781                  if not success:
    2782                          _log.error('%s command failed: [%s]', action, cmd)
    2783                          gmGuiHelpers.gm_show_error (
    2784                                  _('Cannot %(l10n_action)s document part - %(l10n_action)s command failed.\n'
    2785                                    '\n'
    2786                                    'You may need to check and fix either of\n'
    2787                                    ' gm-%(action)s_doc (Unix/Mac) or\n'
    2788                                    ' gm-%(action)s_doc.bat (Windows)\n'
    2789                                    '\n'
    2790                                    'The command is passed the filename to %(l10n_action)s.'
    2791                                  ) % {'action': action, 'l10n_action': l10n_action},
    2792                                  _('Processing document part: %s') % l10n_action
    2793                          )
    2794                  else:
    2795                          if action == 'mail':
    2796                                  curr_pat = gmPerson.gmCurrentPatient()
    2797                                  emr = curr_pat.emr
    2798                                  emr.add_clin_narrative (
    2799                                          soap_cat = None,
    2800                                          note =  _('document part handed over to email program: %s') % self.__curr_node_data.format(single_line = True),
    2801                                          episode = self.__curr_node_data['pk_episode']
    2802                                  )
    2803          #--------------------------------------------------------
    2804 -        def __print_part(self, evt):
    2805                  self.__process_part(action = 'print', l10n_action = _('print'))
    2806          #--------------------------------------------------------
    2807 -        def __fax_part(self, evt):
    2808                  self.__process_part(action = 'fax', l10n_action = _('fax'))
    2809          #--------------------------------------------------------
    2810 -        def __mail_part(self, evt):
    2811                  self.__process_part(action = 'mail', l10n_action = _('mail'))
    2812          #--------------------------------------------------------
    2813 -        def __save_part_to_disk(self, evt):
    2814                  """Save document part into directory."""
    2815                  dlg = wx.DirDialog (
    2816                          parent = self,
    2817                          message = _('Save document part to directory ...'),
    2818                          defaultPath = os.path.expanduser(os.path.join('~', 'gnumed')),
    2819                          style = wx.DD_DEFAULT_STYLE
    2820                  )
    2821                  result = dlg.ShowModal()
    2822                  dirname = dlg.GetPath()
    2823                  dlg.DestroyLater()
    2824
    2825                  if result != wx.ID_OK:
    2826                          return True
    2827
    2828                  wx.BeginBusyCursor()
    2829
    2830                  pat = gmPerson.gmCurrentPatient()
    2831                  fname = self.__curr_node_data.get_useful_filename (
    2832                          patient = pat,
    2833                          make_unique = True,
    2834                          directory = dirname
    2835                  )
    2836
    2837                  cfg = gmCfg.cCfgSQL()
    2838
    2839                  # determine database export chunk size
    2840                  chunksize = int(cfg.get2 (
    2841                          option = "horstspace.blob_export_chunk_size",
    2842                          workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
    2843                          bias = 'workplace',
    2844                          default = default_chunksize
    2845                  ))
    2846
    2847                  fname = self.__curr_node_data.save_to_file (
    2848                          aChunkSize = chunksize,
    2849                          filename = fname,
    2850                          target_mime = None
    2851                  )
    2852
    2853                  wx.EndBusyCursor()
    2854
    2855                  gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved document part as [%s].') % fname)
    2856
    2857                  return True
    2858
    2859          #--------------------------------------------------------
    2860          # document level context menu handlers
    2861          #--------------------------------------------------------
    2862 -        def __select_encounter(self, evt):
    2863                  enc = gmEncounterWidgets.select_encounters (
    2864                          parent = self,
    2865                          patient = gmPerson.gmCurrentPatient()
    2866                  )
    2867                  if not enc:
    2868                          return
    2869                  self.__curr_node_data['pk_encounter'] = enc['pk_encounter']
    2870                  self.__curr_node_data.save()
    2871          #--------------------------------------------------------
    2872 -        def __edit_encounter_details(self, evt):
    2873                  enc = gmEMRStructItems.cEncounter(aPK_obj = self.__curr_node_data['pk_encounter'])
    2874                  gmEncounterWidgets.edit_encounter(parent = self, encounter = enc)
    2875          #--------------------------------------------------------
    2876 -        def __process_doc(self, action=None, l10n_action=None):
    2877
    2878                  gmHooks.run_hook_script(hook = 'before_%s_doc' % action)
    2879
    2880                  wx.BeginBusyCursor()
    2881
    2882                  # detect wrapper
    2883                  found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc' % action)
    2884                  if not found:
    2885                          found, external_cmd = gmShellAPI.detect_external_binary('gm-%s_doc.bat' % action)
    2886                  if not found:
    2887                          _log.error('neither of gm-%s_doc or gm-%s_doc.bat found', action, action)
    2888                          wx.EndBusyCursor()
    2889                          gmGuiHelpers.gm_show_error (
    2890                                  _('Cannot %(l10n_action)s document - %(l10n_action)s command not found.\n'
    2891                                    '\n'
    2892                                    'Either of gm-%(action)s_doc or gm-%(action)s_doc.bat\n'
    2893                                    'must be in the execution path. The command will\n'
    2894                                    'be passed a list of filenames to %(l10n_action)s.'
    2895                                  ) % {'action': action, 'l10n_action': l10n_action},
    2896                                  _('Processing document: %s') % l10n_action
    2897                          )
    2898                          return
    2899
    2900                  cfg = gmCfg.cCfgSQL()
    2901
    2902                  # determine database export chunk size
    2903                  chunksize = int(cfg.get2 (
    2904                          option = "horstspace.blob_export_chunk_size",
    2905                          workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
    2906                          bias = 'workplace',
    2907                          default = default_chunksize
    2908                  ))
    2909
    2910                  part_files = self.__curr_node_data.save_parts_to_files(chunksize = chunksize)
    2911
    2912                  if os.name == 'nt':
    2913                          blocking = True
    2914                  else:
    2915                          blocking = False
    2916
    2917                  if action == 'print':
    2918                          cmd = '%s %s %s' % (
    2919                                  external_cmd,
    2920                                  'generic_document',
    2921                                  ' '.join(part_files)
    2922                          )
    2923                  else:
    2924                          cmd = external_cmd + ' ' + ' '.join(part_files)
    2925                  success = gmShellAPI.run_command_in_shell (
    2926                          command = cmd,
    2927                          blocking = blocking
    2928                  )
    2929
    2930                  wx.EndBusyCursor()
    2931
    2932                  if not success:
    2933                          _log.error('%s command failed: [%s]', action, cmd)
    2934                          gmGuiHelpers.gm_show_error (
    2935                                  _('Cannot %(l10n_action)s document - %(l10n_action)s command failed.\n'
    2936                                    '\n'
    2937                                    'You may need to check and fix either of\n'
    2938                                    ' gm-%(action)s_doc (Unix/Mac) or\n'
    2939                                    ' gm-%(action)s_doc.bat (Windows)\n'
    2940                                    '\n'
    2941                                    'The command is passed a list of filenames to %(l10n_action)s.'
    2942                                  ) % {'action': action, 'l10n_action': l10n_action},
    2943                                  _('Processing document: %s') % l10n_action
    2944                          )
    2945
    2946          #--------------------------------------------------------
    2947 -        def __print_doc(self, evt):
    2948                  self.__process_doc(action = 'print', l10n_action = _('print'))
    2949
    2950          #--------------------------------------------------------
    2951 -        def __fax_doc(self, evt):
    2952                  self.__process_doc(action = 'fax', l10n_action = _('fax'))
    2953
    2954          #--------------------------------------------------------
    2955 -        def __mail_doc(self, evt):
    2956                  self.__process_doc(action = 'mail', l10n_action = _('mail'))
    2957
    2958          #--------------------------------------------------------
    2959 -        def __add_part(self, evt):
    2960                  dlg = wx.FileDialog (
    2961                          parent = self,
    2962                          message = _('Choose a file'),
    2963                          defaultDir = os.path.expanduser(os.path.join('~', 'gnumed')),
    2964                          defaultFile = '',
    2965                          wildcard = "%s (*)|*|PNGs (*.png)|*.png|PDFs (*.pdf)|*.pdf|TIFFs (*.tif)|*.tif|JPEGs (*.jpg)|*.jpg|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')),
    2966                          style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE
    2967                  )
    2968                  result = dlg.ShowModal()
    2969                  if result != wx.ID_CANCEL:
    2970                          self.__curr_node_data.add_parts_from_files(files = dlg.GetPaths(), reviewer = gmStaff.gmCurrentProvider()['pk_staff'])
    2971                  dlg.DestroyLater()
    2972
    2973          #--------------------------------------------------------
    2974 -        def __add_part_from_clipboard(self, evt):
    2975                  clip = gmGuiHelpers.clipboard2file()
    2976                  if clip is None:
    2977                          return
    2978                  if clip is False:
    2979                          return
    2980                  gmMimeLib.call_viewer_on_file(clip, block = False)
    2981                  really_add = gmGuiHelpers.gm_show_question (
    2982                          question = _('Really add the displayed clipboard item into the document ?'),
    2983                          title = _('Document part from clipboard')
    2984                  )
    2985                  if not really_add:
    2986                          return
    2987                  self.__curr_node_data.add_parts_from_files(files = [clip], reviewer = gmStaff.gmCurrentProvider()['pk_staff'])
    2988          #--------------------------------------------------------
    2989 -        def __access_external_original(self, evt):
    2990
    2991                  gmHooks.run_hook_script(hook = 'before_external_doc_access')
    2992
    2993                  wx.BeginBusyCursor()
    2994
    2995                  # detect wrapper
    2996                  found, external_cmd = gmShellAPI.detect_external_binary('gm_access_external_doc.sh')
    2997                  if not found:
    2998                          found, external_cmd = gmShellAPI.detect_external_binary('gm_access_external_doc.bat')
    2999                  if not found:
    3000                          _log.error('neither of gm_access_external_doc.sh or .bat found')
    3001                          wx.EndBusyCursor()
    3002                          gmGuiHelpers.gm_show_error (
    3003                                  _('Cannot access external document - access command not found.\n'
    3004                                    '\n'
    3005                                    'Either of gm_access_external_doc.sh or *.bat must be\n'
    3006                                    'in the execution path. The command will be passed the\n'
    3007                                    'document type and the reference URL for processing.'
    3008                                  ),
    3009                                  _('Accessing external document')
    3010                          )
    3011                          return
    3012
    3013                  cmd = '%s "%s" "%s"' % (external_cmd, self.__curr_node_data['type'], self.__curr_node_data['ext_ref'])
    3014                  if os.name == 'nt':
    3015                          blocking = True
    3016                  else:
    3017                          blocking = False
    3018                  success = gmShellAPI.run_command_in_shell (
    3019                          command = cmd,
    3020                          blocking = blocking
    3021                  )
    3022
    3023                  wx.EndBusyCursor()
    3024
    3025                  if not success:
    3026                          _log.error('External access command failed: [%s]', cmd)
    3027                          gmGuiHelpers.gm_show_error (
    3028                                  _('Cannot access external document - access command failed.\n'
    3029                                    '\n'
    3030                                    'You may need to check and fix either of\n'
    3031                                    ' gm_access_external_doc.sh (Unix/Mac) or\n'
    3032                                    ' gm_access_external_doc.bat (Windows)\n'
    3033                                    '\n'
    3034                                    'The command is passed the document type and the\n'
    3035                                    'external reference URL on the command line.'
    3036                                  ),
    3037                                  _('Accessing external document')
    3038                          )
    3039          #--------------------------------------------------------
    3040 -        def __save_doc_to_disk(self, evt):
    3041                  """Save document into directory.
    3042
    3043                  - one file per object
    3044                  - into subdirectory named after patient
    3045                  """
    3046                  pat = gmPerson.gmCurrentPatient()
    3047                  def_dir = os.path.expanduser(os.path.join('~', 'gnumed', pat.subdir_name))
    3048                  gmTools.mkdir(def_dir)
    3049
    3050                  dlg = wx.DirDialog (
    3051                          parent = self,
    3052                          message = _('Save document into directory ...'),
    3053                          defaultPath = def_dir,
    3054                          style = wx.DD_DEFAULT_STYLE
    3055                  )
    3056                  result = dlg.ShowModal()
    3057                  dirname = dlg.GetPath()
    3058                  dlg.DestroyLater()
    3059
    3060                  if result != wx.ID_OK:
    3061                          return True
    3062
    3063                  wx.BeginBusyCursor()
    3064
    3065                  cfg = gmCfg.cCfgSQL()
    3066
    3067                  # determine database export chunk size
    3068                  chunksize = int(cfg.get2 (
    3069                          option = "horstspace.blob_export_chunk_size",
    3070                          workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
    3071                          bias = 'workplace',
    3072                          default = default_chunksize
    3073                  ))
    3074
    3075                  fnames = self.__curr_node_data.save_parts_to_files(export_dir = dirname, chunksize = chunksize)
    3076
    3077                  wx.EndBusyCursor()
    3078
    3079                  gmDispatcher.send(signal='statustext', msg=_('Successfully saved %s parts into the directory [%s].') % (len(fnames), dirname))
    3080
    3081                  return True
    3082
    3083          #--------------------------------------------------------
    3084 -        def __copy_doc_to_export_area(self, evt):
    3085                  gmPerson.gmCurrentPatient().export_area.add_documents(documents = [self.__curr_node_data])
    3086
    3087          #--------------------------------------------------------
    3088 -        def __delete_document(self, evt):
    3089                  delete_it = gmGuiHelpers.gm_show_question (
    3090                          aMessage = _('Are you sure you want to delete the document ?'),
    3091                          aTitle = _('Deleting document')
    3092                  )
    3093                  if delete_it is True:
    3094                          curr_pat = gmPerson.gmCurrentPatient()
    3095                          emr = curr_pat.emr
    3096                          enc = emr.active_encounter
    3097                          gmDocuments.delete_document(document_id = self.__curr_node_data['pk_doc'], encounter_id = enc['pk_encounter'])
    3098
    3099  #============================================================
    3100  #============================================================
    3101  # PACS
    3102  #============================================================
    3103  from Gnumed.wxGladeWidgets.wxgPACSPluginPnl import wxgPACSPluginPnl
    3104
    3105 -class cPACSPluginPnl(wxgPACSPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
    3106
    3107 -        def __init__(self, *args, **kwargs):
    3108                  wxgPACSPluginPnl.__init__(self, *args, **kwargs)
    3109                  gmRegetMixin.cRegetOnPaintMixin.__init__(self)
    3110                  self.__pacs = None
    3111                  self.__patient = gmPerson.gmCurrentPatient()
    3112                  self.__orthanc_patient = None
    3113                  self.__image_data = None
    3114
    3115                  self.__init_ui()
    3116                  self.__register_interests()
    3117
    3118          #--------------------------------------------------------
    3119          # internal helpers
    3120          #--------------------------------------------------------
    3121 -        def __init_ui(self):
    3122
    3123                  login = gmPG2.get_default_login()
    3124                  self._TCTRL_host.Value = gmTools.coalesce(login.host, 'localhost')
    3125                  self._TCTRL_port.Value = '8042'
    3126
    3127                  self._LCTRL_studies.set_columns(columns = [_('Date'), _('Description'), _('Organization'), _('Authority')])
    3128                  self._LCTRL_studies.select_callback = self._on_studies_list_item_selected
    3129                  self._LCTRL_studies.deselect_callback = self._on_studies_list_item_deselected
    3130
    3131                  self._LCTRL_series.set_columns(columns = [_('Time'), _('Method'), _('Body part'), _('Description')])
    3132                  self._LCTRL_series.select_callback = self._on_series_list_item_selected
    3133                  self._LCTRL_series.deselect_callback = self._on_series_list_item_deselected
    3134
    3135                  self._LCTRL_details.set_columns(columns = [_('DICOM field'), _('Value')])
    3136                  self._LCTRL_details.set_column_widths()
    3137
    3138                  self._BMP_preview.SetBitmap(wx.Bitmap.FromRGBA(50,50, red=0, green=0, blue=0, alpha = wx.ALPHA_TRANSPARENT))
    3139
    3140                  # pre-make thumbnail context menu
    3141                  self.__thumbnail_menu = wx.Menu()
    3142                  item = self.__thumbnail_menu.Append(-1, _('Show in DICOM viewer'))
    3143                  self.Bind(wx.EVT_MENU, self._on_show_image_as_dcm, item)
    3144                  item = self.__thumbnail_menu.Append(-1, _('Show in image viewer'))
    3145                  self.Bind(wx.EVT_MENU, self._on_show_image_as_png, item)
    3146                  self.__thumbnail_menu.AppendSeparator()
    3147                  item = self.__thumbnail_menu.Append(-1, _('Copy to export area'))
    3148                  self.Bind(wx.EVT_MENU, self._on_copy_image_to_export_area, item)
    3149                  item = self.__thumbnail_menu.Append(-1, _('Save as DICOM file (.dcm)'))
    3150                  self.Bind(wx.EVT_MENU, self._on_save_image_as_dcm, item)
    3151                  item = self.__thumbnail_menu.Append(-1, _('Save as image file (.png)'))
    3152                  self.Bind(wx.EVT_MENU, self._on_save_image_as_png, item)
    3153
    3154                  # pre-make studies context menu
    3155                  self.__studies_menu = wx.Menu('Studies:')
    3156                  self.__studies_menu.AppendSeparator()
    3157                  item = self.__studies_menu.Append(-1, _('Show in DICOM viewer'))
    3158                  self.Bind(wx.EVT_MENU, self._on_studies_show_button_pressed, item)
    3159                  self.__studies_menu.AppendSeparator()
    3160                  # export
    3161                  item = self.__studies_menu.Append(-1, _('Selected into export area'))
    3162                  self.Bind(wx.EVT_MENU, self._on_copy_selected_studies_to_export_area, item)
    3163                  item = self.__studies_menu.Append(-1, _('ZIP of selected into export area'))
    3164                  self.Bind(wx.EVT_MENU, self._on_copy_zip_of_selected_studies_to_export_area, item)
    3165                  item = self.__studies_menu.Append(-1, _('All into export area'))
    3166                  self.Bind(wx.EVT_MENU, self._on_copy_all_studies_to_export_area, item)
    3167                  item = self.__studies_menu.Append(-1, _('ZIP of all into export area'))
    3168                  self.Bind(wx.EVT_MENU, self._on_copy_zip_of_all_studies_to_export_area, item)
    3169                  self.__studies_menu.AppendSeparator()
    3170                  # save
    3171                  item = self.__studies_menu.Append(-1, _('Save selected'))
    3172                  self.Bind(wx.EVT_MENU, self._on_save_selected_studies, item)
    3173                  item = self.__studies_menu.Append(-1, _('Save ZIP of selected'))
    3174                  self.Bind(wx.EVT_MENU, self._on_save_zip_of_selected_studies, item)
    3175                  item = self.__studies_menu.Append(-1, _('Save all'))
    3176                  self.Bind(wx.EVT_MENU, self._on_save_all_studies, item)
    3177                  item = self.__studies_menu.Append(-1, _('Save ZIP of all'))
    3178                  self.Bind(wx.EVT_MENU, self._on_save_zip_of_all_studies, item)
    3179                  self.__studies_menu.AppendSeparator()
    3180                  # dicomize
    3181                  item = self.__studies_menu.Append(-1, _('Add PDF to study'))
    3182                  self.Bind(wx.EVT_MENU, self._on_add_pdf_to_study, item)
    3183
    3184          #--------------------------------------------------------
    3185 -        def __set_button_states(self):
    3186                  # disable all buttons
    3187                  # server
    3188                  self._BTN_browse_pacs.Disable()
    3189                  self._BTN_upload.Disable()
    3190                  self._BTN_modify_orthanc_content.Disable()
    3191                  # patient (= all studies of patient)
    3192                  self._BTN_browse_patient.Disable()
    3193                  self._BTN_verify_patient_data.Disable()
    3194                  # study
    3195                  self._BTN_browse_study.Disable()
    3196                  self._BTN_studies_show.Disable()
    3197                  self._BTN_studies_export.Disable()
    3198                  # series
    3199                  # image
    3200                  self._BTN_image_show.Disable()
    3201                  self._BTN_image_export.Disable()
    3202                  self._BTN_previous_image.Disable()
    3203                  self._BTN_next_image.Disable()
    3204
    3205                  if self.__pacs is None:
    3206                          return
    3207
    3208                  # server buttons
    3209                  self._BTN_browse_pacs.Enable()
    3210                  self._BTN_upload.Enable()
    3211                  self._BTN_modify_orthanc_content.Enable()
    3212
    3213                  if not self.__patient.connected:
    3214                          return
    3215
    3216                  # patient buttons (= all studies of patient)
    3217                  self._BTN_verify_patient_data.Enable()
    3218                  if self.__orthanc_patient is not None:
    3219                          self._BTN_browse_patient.Enable()
    3220
    3221                  if len(self._LCTRL_studies.selected_items) == 0:
    3222                          return
    3223
    3224                  # study buttons
    3225                  self._BTN_browse_study.Enable()
    3226                  study_data = self._LCTRL_studies.get_selected_item_data(only_one = True)
    3227                  self._BTN_studies_show.Enable()
    3228                  self._BTN_studies_export.Enable()
    3229
    3230                  if len(self._LCTRL_series.selected_items) == 0:
    3231                          return
    3232
    3233                  series = self._LCTRL_series.get_selected_item_data(only_one = True)
    3234                  if len(series['instances']) == 0:
    3235                          return
    3236
    3237                  # image buttons
    3238                  self._BTN_image_show.Enable()
    3239                  self._BTN_image_export.Enable()
    3240                  if len(series['instances']) > 1:
    3241                          self._BTN_previous_image.Enable()
    3242                          self._BTN_next_image.Enable()
    3243
    3244          #--------------------------------------------------------
    3245 -        def __reset_patient_data(self):
    3246                  self._LBL_patient_identification.SetLabel('')
    3247                  self._LCTRL_studies.set_string_items(items = [])
    3248                  self._LCTRL_series.set_string_items(items = [])
    3249                  self.__refresh_image()
    3250                  self.__refresh_details()
    3251
    3252          #--------------------------------------------------------
    3253 -        def __reset_server_identification(self):
    3254                  self._LBL_PACS_identification.SetLabel(_('<not connected>'))
    3255
    3256          #--------------------------------------------------------
    3257 -        def __reset_ui_content(self):
    3258                  self.__reset_server_identification()
    3259                  self.__reset_patient_data()
    3260                  self.__set_button_states()
    3261
    3262          #-----------------------------------------------------
    3263 -        def __connect(self):
    3264
    3265                  self.__pacs = None
    3266                  self.__orthanc_patient = None
    3267                  self.__set_button_states()
    3268                  self.__reset_server_identification()
    3269
    3270                  host = self._TCTRL_host.Value.strip()
    3271                  port = self._TCTRL_port.Value.strip()[:6]
    3272                  if port == '':
    3273                          self._LBL_PACS_identification.SetLabel(_('Cannot connect without port (try 8042).'))
    3274                          return False
    3275                  if len(port) < 4:
    3276                          return False
    3277                  try:
    3278                          int(port)
    3279                  except ValueError:
    3280                          self._LBL_PACS_identification.SetLabel(_('Invalid port (try 8042).'))
    3281                          return False
    3282
    3283                  user = self._TCTRL_user.Value
    3284                  if user == '':
    3285                          user = None
    3286                  self._LBL_PACS_identification.SetLabel(_('Connect to [%s] @ port %s as "%s".') % (host, port, user))
    3287                  password = self._TCTRL_password.Value
    3288                  if password == '':
    3289                          password = None
    3290
    3291                  pacs = gmDICOM.cOrthancServer()
    3292                  if not pacs.connect(host = host, port = port, user = user, password = password):                #, expected_aet = 'another AET'
    3293                          self._LBL_PACS_identification.SetLabel(_('Cannot connect to PACS.'))
    3294                          _log.error('error connecting to server: %s', pacs.connect_error)
    3295                          return False
    3296
    3297                  #self._LBL_PACS_identification.SetLabel(_('PACS: Orthanc "%s" (AET "%s", Version %s, API v%s, DB v%s)') % (
    3298                  self._LBL_PACS_identification.SetLabel(_('PACS: Orthanc "%s" (AET "%s", Version %s, DB v%s)') % (
    3299                          pacs.server_identification['Name'],
    3300                          pacs.server_identification['DicomAet'],
    3301                          pacs.server_identification['Version'],
    3302                          #pacs.server_identification['ApiVersion'],
    3303                          pacs.server_identification['DatabaseVersion']
    3304                  ))
    3305
    3306                  self.__pacs = pacs
    3307                  self.__set_button_states()
    3308                  return True
    3309
    3310          #--------------------------------------------------------
    3311 -        def __refresh_patient_data(self):
    3312
    3313                  self.__orthanc_patient = None
    3314
    3315                  if not self.__patient.connected:
    3316                          self.__reset_patient_data()
    3317                          self.__set_button_states()
    3318                          return True
    3319
    3320                  if not self.__connect():
    3321                          return False
    3322
    3323                  tt_lines = [_('Known PACS IDs:')]
    3324                  for pacs_id in self.__patient.suggest_external_ids(target = 'PACS'):
    3325                          tt_lines.append(' ' + _('generic: %s') % pacs_id)
    3326                  for pacs_id in self.__patient.get_external_ids(id_type = 'PACS', issuer = self.__pacs.as_external_id_issuer):
    3327                          tt_lines.append(' ' + _('stored: "%(value)s" @ [%(issuer)s]') % pacs_id)
    3328                  tt_lines.append('')
    3329                  tt_lines.append(_('Patients found in PACS:'))
    3330
    3331                  info_lines = []
    3332                  # try to find patient
    3333                  matching_pats = self.__pacs.get_matching_patients(person = self.__patient)
    3334                  if len(matching_pats) == 0:
    3335                          info_lines.append(_('PACS: no patients with matching IDs found'))
    3336                  no_of_studies = 0
    3337                  for pat in matching_pats:
    3338                          info_lines.append('"%s" %s "%s (%s) %s"' % (
    3339                                  pat['MainDicomTags']['PatientID'],
    3340                                  gmTools.u_arrow2right,
    3341                                  gmTools.coalesce(pat['MainDicomTags']['PatientName'], '?'),
    3342                                  gmTools.coalesce(pat['MainDicomTags']['PatientSex'], '?'),
    3343                                  gmTools.coalesce(pat['MainDicomTags']['PatientBirthDate'], '?')
    3344                          ))
    3345                          no_of_studies += len(pat['Studies'])
    3346                          tt_lines.append('%s [#%s]' % (
    3347                                  gmTools.format_dict_like (
    3348                                          pat['MainDicomTags'],
    3349                                          relevant_keys = ['PatientName', 'PatientSex', 'PatientBirthDate', 'PatientID'],
    3350                                          template = ' %(PatientID)s = %(PatientName)s (%(PatientSex)s) %(PatientBirthDate)s',
    3351                                          missing_key_template = '?'
    3352                                  ),
    3353                                  pat['ID']
    3354                          ))
    3355                  if len(matching_pats) > 1:
    3356                          info_lines.append(_('PACS: more than one patient with matching IDs found, carefully check studies'))
    3357                  self._LBL_patient_identification.SetLabel('\n'.join(info_lines))
    3358                  tt_lines.append('')
    3359                  tt_lines.append(_('Studies found: %s') % no_of_studies)
    3360                  self._LBL_patient_identification.SetToolTip('\n'.join(tt_lines))
    3361
    3362                  # get studies
    3363                  study_list_items = []
    3364                  study_list_data = []
    3365                  if len(matching_pats) > 0:
    3366                          # we don't at this point really expect more than one patient matching
    3367                          self.__orthanc_patient = matching_pats[0]
    3368                          for pat in self.__pacs.get_studies_list_by_orthanc_patient_list(orthanc_patients = matching_pats):
    3369                                  for study in pat['studies']:
    3370                                          docs = []
    3371                                          if study['referring_doc'] is not None:
    3372                                                  docs.append(study['referring_doc'])
    3373                                          if study['requesting_doc'] is not None:
    3374                                                  if study['requesting_doc'] not in docs:
    3375                                                          docs.append(study['requesting_doc'])
    3376                                          if study['performing_doc'] is not None:
    3377                                                  if study['performing_doc'] not in docs:
    3378                                                          docs.append(study['requesting_doc'])
    3379                                          if study['operator_name'] is not None:
    3380                                                  if study['operator_name'] not in docs:
    3381                                                          docs.append(study['operator_name'])
    3382                                          if study['radiographer_code'] is not None:
    3383                                                  if study['radiographer_code'] not in docs:
    3384                                                          docs.append(study['radiographer_code'])
    3385                                          org_name = u'@'.join ([
    3386                                                  o for o in [study['radiology_dept'], study['radiology_org']]
    3387                                                  if o is not None
    3388                                          ])
    3389                                          org = '%s%s%s' % (
    3390                                                  org_name,
    3391                                                  gmTools.coalesce(study['station_name'], '', ' [%s]'),
    3392                                                  gmTools.coalesce(study['radiology_org_addr'], '', ' (%s)').replace('\r\n', ' [CR] ')
    3393                                          )
    3394                                          if study['date'] is None:
    3395                                                  study_date = '?'
    3396                                          else:
    3397                                                  study_date = '%s-%s-%s' % (
    3398                                                          study['date'][:4],
    3399                                                          study['date'][4:6],
    3400                                                          study['date'][6:8]
    3401                                                  )
    3402                                          study_list_items.append ( [
    3403                                                  study_date,
    3404                                                  _('%s series%s') % (
    3405                                                          len(study['series']),
    3406                                                          gmTools.coalesce(study['description'], '', ': %s')
    3407                                                  ),
    3408                                                  org.strip(),
    3409                                                  gmTools.u_arrow2right.join(docs)
    3410                                          ] )
    3411                                          study_list_data.append(study)
    3412
    3413                  self._LCTRL_studies.set_string_items(items = study_list_items)
    3414                  self._LCTRL_studies.set_data(data = study_list_data)
    3415                  self._LCTRL_studies.SortListItems(0, 0)
    3416                  self._LCTRL_studies.set_column_widths()
    3417
    3418                  self.__refresh_image()
    3419                  self.__refresh_details()
    3420                  self.__set_button_states()
    3421
    3422                  return True
    3423
    3424          #--------------------------------------------------------
    3425 -        def __refresh_details(self):
    3426
    3427                  self._LCTRL_details.remove_items_safely()
    3428                  if self.__pacs is None:
    3429                          return
    3430
    3431                  # study available ?
    3432                  study_data = self._LCTRL_studies.get_selected_item_data(only_one = True)
    3433                  if study_data is None:
    3434                          return
    3435                  items = []
    3436                  items = [ [key, study_data['all_tags'][key]] for key in study_data['all_tags'] if ('%s' % study_data['all_tags'][key]).strip() != '' ]
    3437
    3438                  # series available ?
    3439                  series = self._LCTRL_series.get_selected_item_data(only_one = True)
    3440                  if series is None:
    3441                          self._LCTRL_details.set_string_items(items = items)
    3442                          self._LCTRL_details.set_column_widths()
    3443                          return
    3444                  items.append ([
    3445                          ' %s ' % (gmTools.u_box_horiz_single * 5),
    3446                          '%s %s %s' % (
    3447                                  gmTools.u_box_horiz_single * 3,
    3448                                  _('Series'),
    3449                                  gmTools.u_box_horiz_single * 10
    3450                          )
    3451                  ])
    3452                  items.extend([ [key, series['all_tags'][key]] for key in series['all_tags'] if ('%s' % series['all_tags'][key]).strip() != '' ])
    3453
    3454                  # image available ?
    3455                  if self.__image_data is None:
    3456                          self._LCTRL_details.set_string_items(items = items)
    3457                          self._LCTRL_details.set_column_widths()
    3458                          return
    3459                  items.append ([
    3460                          ' %s ' % (gmTools.u_box_horiz_single * 5),
    3461                          '%s %s %s' % (
    3462                                  gmTools.u_box_horiz_single * 3,
    3463                                  _('Image'),
    3464                                  gmTools.u_box_horiz_single * 10
    3465                          )
    3466                  ])
    3467                  tags = self.__pacs.get_instance_dicom_tags(instance_id = self.__image_data['uuid'])
    3468                  if tags is False:
    3469                          items.extend(['image', '<tags not found in PACS>'])
    3470                  else:
    3471                          items.extend([ [key, tags[key]] for key in tags if ('%s' % tags[key]).strip() != '' ])
    3472
    3473                  self._LCTRL_details.set_string_items(items = items)
    3474                  self._LCTRL_details.set_column_widths()
    3475
    3476          #--------------------------------------------------------
    3477 -        def __refresh_image(self, idx=None):
    3478
    3479                  self.__image_data = None
    3480                  self._SZR_image_buttons.StaticBox.SetLabel(_('Image'))
    3481                  self._BMP_preview.SetBitmap(wx.Bitmap.FromRGBA(50,50, red=0, green=0, blue=0, alpha = wx.ALPHA_TRANSPARENT))
    3482
    3483                  if idx is None:
    3484                          self._BMP_preview.ContainingSizer.Layout()
    3485                          return
    3486                  if self.__pacs is None:
    3487                          self._BMP_preview.ContainingSizer.Layout()
    3488                          return
    3489                  series = self._LCTRL_series.get_selected_item_data(only_one = True)
    3490                  if series is None:
    3491                          self._BMP_preview.ContainingSizer.Layout()
    3492                          return
    3493                  if idx > len(series['instances']) - 1:
    3494                          raise ValueError('trying to go beyond instances in series: %s of %s', idx, len(series['instances']))
    3495
    3496                  # get image
    3497                  uuid = series['instances'][idx]
    3498                  img_file = self.__pacs.get_instance_preview(instance_id = uuid)
    3499                  # scale
    3500                  wx_bmp = gmGuiHelpers.file2scaled_image(filename = img_file, height = 100)
    3501                  # show
    3502                  if wx_bmp is None:
    3503                          _log.error('cannot load DICOM instance from PACS: %s', uuid)
    3504                  else:
    3505                          self.__image_data = {'idx': idx, 'uuid': uuid}
    3506                          self._BMP_preview.SetBitmap(wx_bmp)
    3507                          self._SZR_image_buttons.StaticBox.SetLabel(_('Image %s/%s') % (idx+1, len(series['instances'])))
    3508
    3509                  if idx == 0:
    3510                          self._BTN_previous_image.Disable()
    3511                  else:
    3512                          self._BTN_previous_image.Enable()
    3513                  if idx == len(series['instances']) - 1:
    3514                          self._BTN_next_image.Disable()
    3515                  else:
    3516                          self._BTN_next_image.Enable()
    3517
    3518                  self._BMP_preview.ContainingSizer.Layout()
    3519
    3520          #--------------------------------------------------------
    3521 -        def __show_image(self, as_dcm=False, as_png=False):
    3522                  if self.__image_data is None:
    3523                          return False
    3524
    3525                  uuid = self.__image_data['uuid']
    3526                  img_file = None
    3527                  if as_dcm:
    3528                          img_file = self.__pacs.get_instance(instance_id = uuid)
    3529                  if as_png:
    3530                          img_file = self.__pacs.get_instance_preview(instance_id = uuid)
    3531                  if img_file is not None:
    3532                          (success, msg) = gmMimeLib.call_viewer_on_file(img_file)
    3533                          if not success:
    3534                                  gmGuiHelpers.gm_show_warning (
    3535                                          aMessage = _('Cannot show image:\n%s') % msg,
    3536                                          aTitle = _('Previewing DICOM image')
    3537                                  )
    3538                          return success
    3539
    3540                  # try DCM
    3541                  img_file = self.__pacs.get_instance(instance_id = uuid)
    3542                  (success, msg) = gmMimeLib.call_viewer_on_file(img_file)
    3543                  if success:
    3544                          return True
    3545
    3546                  # try PNG
    3547                  img_file = self.__pacs.get_instance_preview(instance_id = uuid)
    3548                  (success, msg) = gmMimeLib.call_viewer_on_file(img_file)
    3549                  if success:
    3550                          return True
    3551
    3552                  gmGuiHelpers.gm_show_warning (
    3553                          aMessage = _('Cannot show in DICOM or image viewer:\n%s') % msg,
    3554                          aTitle = _('Previewing DICOM image')
    3555                  )
    3556
    3557          #--------------------------------------------------------
    3558 -        def __save_image(self, as_dcm=False, as_png=False, nice_filename=False):
    3559                  if self.__image_data is None:
    3560                          return False, None
    3561
    3562                  fnames = {}
    3563                  uuid = self.__image_data['uuid']
    3564                  if as_dcm:
    3565                          if nice_filename:
    3566                                  fname = gmTools.get_unique_filename (
    3567                                          prefix = '%s-orthanc_%s--' % (self.__patient.subdir_name, uuid),
    3568                                          suffix = '.dcm',
    3569                                          tmp_dir = os.path.join(gmTools.gmPaths().home_dir, 'gnumed')
    3570                                  )
    3571                          else:
    3572                                  fname = None
    3573                          img_fname = self.__pacs.get_instance(filename = fname, instance_id = uuid)
    3574                          if img_fname is None:
    3575                                  gmGuiHelpers.gm_show_warning (
    3576                                          aMessage = _('Cannot save image as DICOM file.'),
    3577                                          aTitle = _('Saving DICOM image')
    3578                                  )
    3579                                  return False, fnames
    3580
    3581                          fnames['dcm'] = img_fname
    3582                          gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved as [%s].') % img_fname)
    3583
    3584                  if as_png:
    3585                          if nice_filename:
    3586                                  fname = gmTools.get_unique_filename (
    3587                                          prefix = '%s-orthanc_%s--' % (self.__patient.subdir_name, uuid),
    3588                                          suffix = '.png',
    3589                                          tmp_dir = os.path.join(gmTools.gmPaths().home_dir, 'gnumed')
    3590                                  )
    3591                          else:
    3592                                  fname = None
    3593                          img_fname = self.__pacs.get_instance_preview(filename = fname, instance_id = uuid)
    3594                          if img_fname is None:
    3595                                  gmGuiHelpers.gm_show_warning (
    3596                                          aMessage = _('Cannot save image as PNG file.'),
    3597                                          aTitle = _('Saving DICOM image')
    3598                                  )
    3599                                  return False, fnames
    3600                          fnames['png'] = img_fname
    3601                          gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved as [%s].') % img_fname)
    3602
    3603                  return True, fnames
    3604
    3605          #--------------------------------------------------------
    3606 -        def __copy_image_to_export_area(self):
    3607                  if self.__image_data is None:
    3608                          return False
    3609
    3610                  success, fnames = self.__save_image(as_dcm = True, as_png = True)
    3611                  if not success:
    3612                          return False
    3613
    3614                  wx.BeginBusyCursor()
    3615                  self.__patient.export_area.add_files (
    3616                          filenames = [fnames['png'], fnames['dcm']],
    3617                          hint = _('DICOM image of [%s] from Orthanc PACS "%s" (AET "%s")') % (
    3618                                  self.__orthanc_patient['MainDicomTags']['PatientID'],
    3619                                  self.__pacs.server_identification['Name'],
    3620                                  self.__pacs.server_identification['DicomAet']
    3621                          )
    3622                  )
    3623                  wx.EndBusyCursor()
    3624
    3625                  gmDispatcher.send(signal = 'statustext', msg = _('Successfully stored in export area.'))
    3626
    3627          #--------------------------------------------------------
    3628          #--------------------------------------------------------
    3629 -        def __browse_studies(self):
    3630                  if self.__pacs is None:
    3631                          return
    3632
    3633                  study_data = self._LCTRL_studies.get_selected_item_data(only_one = True)
    3634                  if len(study_data) == 0:
    3635                          return
    3636
    3637                  gmNetworkTools.open_url_in_browser (
    3638                          self.__pacs.get_url_browse_study(study_id = study_data['orthanc_id']),
    3639                          new = 2,
    3640                          autoraise = True
    3641                  )
    3642
    3643          #--------------------------------------------------------
    3644 -        def __show_studies(self):
    3645                  if self.__pacs is None:
    3646                          return
    3647
    3648                  study_data = self._LCTRL_studies.get_selected_item_data(only_one = False)
    3649                  if len(study_data) == 0:
    3650                          return
    3651
    3652                  wx.BeginBusyCursor()
    3653                  target_dir = self.__pacs.get_studies_with_dicomdir(study_ids = [ s['orthanc_id'] for s in study_data ])
    3654                  wx.EndBusyCursor()
    3655                  if target_dir is False:
    3656                          gmGuiHelpers.gm_show_error (
    3657                                  title = _('Showing DICOM studies'),
    3658                                  error = _('Unable to show selected studies.')
    3659                          )
    3660                          return
    3661                  DICOMDIR = os.path.join(target_dir, 'DICOMDIR')
    3662                  if os.path.isfile(DICOMDIR):
    3663                          (success, msg) = gmMimeLib.call_viewer_on_file(DICOMDIR, block = False)
    3664                          if success:
    3665                                  return
    3666                  else:
    3667                          _log.error('cannot find DICOMDIR in: %s', target_dir)
    3668
    3669                  gmMimeLib.call_viewer_on_file(target_dir, block = False)
    3670
    3671                  # FIXME: on failure export as JPG and call dir viewer
    3672
    3673          #--------------------------------------------------------
    3674 -        def __copy_all_studies_to_export_area(self):
    3675                  if self.__pacs is None:
    3676                          return
    3677
    3678                  study_data = self._LCTRL_studies.get_item_data()
    3679                  if len(study_data) == 0:
    3680                          return
    3681
    3682                  self.__copy_studies_to_export_area(study_data)
    3683
    3684          #--------------------------------------------------------
    3685 -        def __copy_selected_studies_to_export_area(self):
    3686                  if self.__pacs is None:
    3687                          return
    3688
    3689                  study_data = self._LCTRL_studies.get_selected_item_data(only_one = False)
    3690                  if len(study_data) == 0:
    3691                          return
    3692
    3693                  self.__copy_studies_to_export_area(study_data)
    3694
    3695          #--------------------------------------------------------
    3696 -        def __copy_studies_to_export_area(self, study_data):
    3697                  wx.BeginBusyCursor()
    3698                  target_dir = gmTools.mk_sandbox_dir (
    3699                          prefix = 'dcm-',
    3700                          base_dir = os.path.join(gmTools.gmPaths().home_dir, '.gnumed', self.__patient.subdir_name)
    3701                  )
    3702                  target_dir = self.__pacs.get_studies_with_dicomdir(study_ids = [ s['orthanc_id'] for s in study_data ], target_dir = target_dir)
    3703                  if target_dir is False:
    3704                          wx.EndBusyCursor()
    3705                          gmGuiHelpers.gm_show_error (
    3706                                  title = _('Copying DICOM studies'),
    3707                                  error = _('Unable to put studies into export area.')
    3708                          )
    3709                          return
    3710
    3711                  comment = _('DICOM studies of [%s] from Orthanc PACS "%s" (AET "%s") [%s/]') % (
    3712                          self.__orthanc_patient['MainDicomTags']['PatientID'],
    3713                          self.__pacs.server_identification['Name'],
    3714                          self.__pacs.server_identification['DicomAet'],
    3715                          target_dir
    3716                  )
    3717                  if self.__patient.export_area.add_path(target_dir, comment):
    3718                          wx.EndBusyCursor()
    3719                          return
    3720
    3721                  wx.EndBusyCursor()
    3722                  gmGuiHelpers.gm_show_error (
    3723                          title = _('Adding DICOM studies to export area'),
    3724                          error = _('Cannot add the following path to the export area:\n%s ') % target_dir
    3725                  )
    3726
    3727          #--------------------------------------------------------
    3728 -        def __copy_zip_of_all_studies_to_export_area(self):
    3729                  if self.__pacs is None:
    3730                          return
    3731
    3732                  study_data = self._LCTRL_studies.get_item_data()
    3733                  if len(study_data) == 0:
    3734                          return
    3735
    3736                  self.__copy_zip_of_studies_to_export_area(study_data)
    3737
    3738          #--------------------------------------------------------
    3739 -        def __copy_zip_of_selected_studies_to_export_area(self):
    3740                  if self.__pacs is None:
    3741                          return
    3742
    3743                  study_data = self._LCTRL_studies.get_selected_item_data(only_one = False)
    3744                  if len(study_data) == 0:
    3745                          return
    3746
    3747                  self.__copy_zip_of_studies_to_export_area(study_data)
    3748
    3749          #--------------------------------------------------------
    3750 -        def __copy_zip_of_studies_to_export_area(self, study_data):
    3751                  wx.BeginBusyCursor()
    3752                  zip_fname = self.__pacs.get_studies_with_dicomdir (
    3753                          study_ids = [ s['orthanc_id'] for s in study_data ],
    3754                          create_zip = True
    3755                  )
    3756                  if zip_fname is False:
    3757                          wx.EndBusyCursor()
    3758                          gmGuiHelpers.gm_show_error (
    3759                                  title = _('Adding DICOM studies to export area'),
    3760                                  error = _('Unable to put ZIP of studies into export area.')
    3761                          )
    3762                          return
    3763
    3764                  # check size and confirm if huge
    3765                  zip_size = os.path.getsize(zip_fname)
    3766                  if zip_size > (300 * gmTools._MB):              # ~ 1/2 CD-ROM
    3767                          wx.EndBusyCursor()
    3768                          really_export = gmGuiHelpers.gm_show_question (
    3769                                  title = _('Exporting DICOM studies'),
    3770                                  question = _('The DICOM studies are %s in compressed size.\n\nReally move into export area ?') % gmTools.size2str(zip_size),
    3771                                  cancel_button = False
    3772                          )
    3773                          if not really_export:
    3774                                  return
    3775
    3776                  hint = _('DICOM studies of [%s] from Orthanc PACS "%s" (AET "%s")') % (
    3777                          self.__orthanc_patient['MainDicomTags']['PatientID'],
    3778                          self.__pacs.server_identification['Name'],
    3779                          self.__pacs.server_identification['DicomAet']
    3780                  )
    3781                  if self.__patient.export_area.add_file(filename = zip_fname, hint = hint):
    3782                          #gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved as [%s].') % filename)
    3783                          wx.EndBusyCursor()
    3784                          return
    3785
    3786                  wx.EndBusyCursor()
    3787                  gmGuiHelpers.gm_show_error (
    3788                          title = _('Adding DICOM studies to export area'),
    3789                          error = _('Cannot add the following archive to the export area:\n%s ') % zip_fname
    3790                  )
    3791
    3792          #--------------------------------------------------------
    3793 -        def __save_selected_studies(self):
    3794                  if self.__pacs is None:
    3795                          return
    3796
    3797                  study_data = self._LCTRL_studies.get_selected_item_data(only_one = False)
    3798                  if len(study_data) == 0:
    3799                          return
    3800
    3801                  self.__save_studies_to_disk(study_data)
    3802
    3803          #--------------------------------------------------------
    3804 -        def __on_save_all_studies(self):
    3805                  if self.__pacs is None:
    3806                          return
    3807
    3808                  study_data = self._LCTRL_studies.get_item_data()
    3809                  if len(study_data) == 0:
    3810                          return
    3811
    3812                  self.__save_studies_to_disk(study_data)
    3813
    3814          #--------------------------------------------------------
    3815 -        def __save_studies_to_disk(self, study_data):
    3816                  default_path = os.path.join(gmTools.gmPaths().home_dir, 'gnumed', self.__patient.subdir_name)
    3817                  gmTools.mkdir(default_path)
    3818                  dlg = wx.DirDialog (
    3819                          self,
    3820                          message = _('Select the directory into which to save the DICOM studies.'),
    3821                          defaultPath = default_path
    3822                  )
    3823                  choice = dlg.ShowModal()
    3824                  target_dir = dlg.GetPath()
    3825                  dlg.DestroyLater()
    3826                  if choice != wx.ID_OK:
    3827                          return True
    3828
    3829                  wx.BeginBusyCursor()
    3830                  target_dir = self.__pacs.get_studies_with_dicomdir(study_ids = [ s['orthanc_id'] for s in study_data ], target_dir = target_dir)
    3831                  wx.EndBusyCursor()
    3832
    3833                  if target_dir is False:
    3834                          gmGuiHelpers.gm_show_error (
    3835                                  title = _('Saving DICOM studies'),
    3836                                  error = _('Unable to save DICOM studies.')
    3837                          )
    3838                          return
    3839                  gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved to [%s].') % target_dir)
    3840
    3841          #--------------------------------------------------------
    3842 -        def __save_zip_of_selected_studies(self):
    3843                  if self.__pacs is None:
    3844                          return
    3845
    3846                  study_data = self._LCTRL_studies.get_selected_item_data(only_one = False)
    3847                  if len(study_data) == 0:
    3848                          return
    3849
    3850                  self.__save_zip_of_studies_to_disk(study_data)
    3851
    3852          #--------------------------------------------------------
    3853 -        def ___on_save_zip_of_all_studies(self):
    3854                  if self.__pacs is None:
    3855                          return
    3856
    3857                  study_data = self._LCTRL_studies.get_item_data()
    3858                  if len(study_data) == 0:
    3859                          return
    3860
    3861                  self.__save_zip_of_studies_to_disk(study_data)
    3862
    3863          #--------------------------------------------------------
    3864 -        def __save_zip_of_studies_to_disk(self, study_data):
    3865                  default_path = os.path.join(gmTools.gmPaths().home_dir, 'gnumed', self.__patient.subdir_name)
    3866                  gmTools.mkdir(default_path)
    3867                  dlg = wx.DirDialog (
    3868                          self,
    3869                          message = _('Select the directory into which to save the DICOM studies ZIP.'),
    3870                          defaultPath = default_path
    3871                  )
    3872                  choice = dlg.ShowModal()
    3873                  target_dir = dlg.GetPath()
    3874                  dlg.DestroyLater()
    3875                  if choice != wx.ID_OK:
    3876                          return True
    3877
    3878                  wx.BeginBusyCursor()
    3879                  filename = self.__pacs.get_studies_with_dicomdir(study_ids = [ s['orthanc_id'] for s in study_data ], target_dir = target_dir, create_zip = True)
    3880                  wx.EndBusyCursor()
    3881
    3882                  if filename is False:
    3883                          gmGuiHelpers.gm_show_error (
    3884                                  title = _('Saving DICOM studies'),
    3885                                  error = _('Unable to save DICOM studies as ZIP.')
    3886                          )
    3887                          return
    3888
    3889                  gmDispatcher.send(signal = 'statustext', msg = _('Successfully saved as [%s].') % filename)
    3890
    3891          #--------------------------------------------------------
    3892 -        def _on_add_pdf_to_study(self, evt):
    3893                  if self.__pacs is None:
    3894                          return
    3895
    3896                  study_data = self._LCTRL_studies.get_selected_item_data(only_one = False)
    3897                  if len(study_data) != 1:
    3898                          gmGuiHelpers.gm_show_info (
    3899                                  title = _('Adding PDF to DICOM study'),
    3900                                  info = _('For adding a PDF file there must be exactly one (1) DICOM study selected.')
    3901                          )
    3902                          return
    3903
    3904                  # select PDF
    3905                  pdf_name = None
    3906                  dlg = wx.FileDialog (
    3907                          parent = self,
    3908                          message = _('Select PDF to add to DICOM study'),
    3909                          defaultDir = os.path.join(gmTools.gmPaths().home_dir, 'gnumed'),
    3910                          wildcard = "%s (*.pdf)|*.pdf|%s (*)|*" % (_('PDF files'), _('all files')),
    3911                          style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST
    3912                  )
    3913                  choice = dlg.ShowModal()
    3914                  pdf_name = dlg.GetPath()
    3915                  dlg.DestroyLater()
    3916                  if choice != wx.ID_OK:
    3917                          return
    3918
    3919                  _log.debug('dicomize(%s)', pdf_name)
    3920                  if pdf_name is None:
    3921                          return
    3922
    3923                  # export one instance as template
    3924                  instance_uuid = study_data[0]['series'][0]['instances'][-1]
    3925                  dcm_instance_template_fname = self.__pacs.get_instance(instance_id = instance_uuid)
    3926                  # dicomize PDF via template
    3927                  _cfg = gmCfg2.gmCfgData()
    3928                  pdf2dcm_fname = gmDICOM.dicomize_pdf (
    3929                          pdf_name = pdf_name,
    3930                          dcm_template_file = dcm_instance_template_fname,
    3931                          title = 'GNUmed',
    3932                          verbose = _cfg.get(option = 'debug')
    3933                  )
    3934                  if pdf2dcm_fname is None:
    3935                          gmGuiHelpers.gm_show_error (
    3936                                  title = _('Adding PDF to DICOM study'),
    3937                                  error = _('Cannot turn PDF file\n\n %s\n\n into DICOM file.')
    3938                          )
    3939                          return
    3940
    3941                  # upload pdf.dcm
    3942                  if self.__pacs.upload_dicom_file(pdf2dcm_fname):
    3943                          gmDispatcher.send(signal = 'statustext', msg = _('Successfully uploaded [%s] to Orthanc DICOM server.') % pdf2dcm_fname)
    3944                          return
    3945
    3946                  gmGuiHelpers.gm_show_error (
    3947                          title = _('Adding PDF to DICOM study'),
    3948                          error = _('Cannot updload DICOM file\n\n %s\n\n into Orthanc PACS.') % pdf2dcm_fname
    3949                  )
    3950                  self._schedule_data_reget()
    3951
    3952          #--------------------------------------------------------
    3953          #--------------------------------------------------------
    3954 -        def __browse_patient(self):
    3955                  if self.__pacs is None:
    3956                          return
    3957
    3958                  gmNetworkTools.open_url_in_browser (
    3959                          self.__pacs.get_url_browse_patient(patient_id = self.__orthanc_patient['ID']),
    3960                          new = 2,
    3961                          autoraise = True
    3962                  )
    3963
    3964          #--------------------------------------------------------
    3965          #--------------------------------------------------------
    3966 -        def __browse_pacs(self):
    3967                  if self.__pacs is None:
    3968                          return
    3969
    3970                  gmNetworkTools.open_url_in_browser (
    3971                          self.__pacs.url_browse_patients,
    3972                          new = 2,
    3973                          autoraise = True
    3974                  )
    3975
    3976          #--------------------------------------------------------
    3977          # reget-on-paint mixin API
    3978          #--------------------------------------------------------
    3979 -        def _populate_with_data(self):
    3980                  if not self.__patient.connected:
    3981                          self.__reset_ui_content()
    3982                          return True
    3983
    3984                  if not self.__refresh_patient_data():
    3985                          return False
    3986
    3987                  return True
    3988
    3989          #--------------------------------------------------------
    3990          # event handling
    3991          #--------------------------------------------------------
    3992 -        def __register_interests(self):
    3993
    3994                  # wxPython signals
    3995                  self._BMP_preview.Bind(wx.EVT_LEFT_DCLICK, self._on_preview_image_leftdoubleclicked)
    3996                  self._BMP_preview.Bind(wx.EVT_RIGHT_UP, self._on_preview_image_rightclicked)
    3997                  self._BTN_browse_study.Bind(wx.EVT_RIGHT_UP, self._on_studies_button_rightclicked)
    3998
    3999                  # client internal signals
    4000                  gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection)
    4001                  gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection)
    4002
    4003                  # generic database change signal
    4004                  gmDispatcher.connect(signal = 'gm_table_mod', receiver = self._on_database_signal)
    4005
    4006          #--------------------------------------------------------
    4007 -        def _on_pre_patient_unselection(self):
    4008                  # only empty out here, do NOT access the patient
    4009                  # or else we will access the old patient while it
    4010                  # may not be valid anymore ...
    4011                  self.__reset_patient_data()
    4012
    4013          #--------------------------------------------------------
    4014 -        def _on_post_patient_selection(self):
    4015                  self._schedule_data_reget()
    4016
    4017          #--------------------------------------------------------
    4018 -        def _on_database_signal(self, **kwds):
    4019
    4020                  if not self.__patient.connected:
    4021                          # probably not needed:
    4022                          #self._schedule_data_reget()
    4023                          return True
    4024
    4025                  if kwds['pk_identity'] != self.__patient.ID:
    4026                          return True
    4027
    4028                  if kwds['table'] == 'dem.lnk_identity2ext_id':
    4029                          self._schedule_data_reget()
    4030                          return True
    4031
    4032                  return True
    4033
    4034          #--------------------------------------------------------
    4035          # events: lists
    4036          #--------------------------------------------------------
    4037 -        def _on_series_list_item_selected(self, event):
    4038
    4039                  event.Skip()
    4040                  if self.__pacs is None:
    4041                          return
    4042
    4043                  study_data = self._LCTRL_studies.get_selected_item_data(only_one = True)
    4044                  if study_data is None:
    4045                          return
    4046
    4047                  series = self._LCTRL_series.get_selected_item_data(only_one = True)
    4048                  if series is None:
    4049                          self.__set_button_states()
    4050                          return
    4051
    4052                  if len(series['instances']) == 0:
    4053                          self.__refresh_image()
    4054                          self.__refresh_details()
    4055                          self.__set_button_states()
    4056                          return
    4057
    4058                  # set first image
    4059                  self.__refresh_image(0)
    4060                  self.__refresh_details()
    4061                  self.__set_button_states()
    4062                  self._BTN_previous_image.Disable()
    4063
    4064          #--------------------------------------------------------
    4065 -        def _on_series_list_item_deselected(self, event):
    4066                  event.Skip()
    4067
    4068                  self.__refresh_image()
    4069                  self.__refresh_details()
    4070                  self.__set_button_states()
    4071
    4072          #--------------------------------------------------------
    4073 -        def _on_studies_list_item_selected(self, event):
    4074                  event.Skip()
    4075                  if self.__pacs is None:
    4076                          return
    4077
    4078                  study_data = self._LCTRL_studies.get_item_data(item_idx = event.Index)
    4079                  series_list_items = []
    4080                  series_list_data = []
    4081                  for series in study_data['series']:
    4082
    4083                          series_time = ''
    4084                          if series['time'] is None:
    4085                                  series['time'] = study_data['time']
    4086                          if series['time'] is None:
    4087                                  series_time = '?'
    4088                          else:
    4089                                  series_time = '%s:%s:%s' % (
    4090                                          series['time'][:2],
    4091                                          series['time'][2:4],
    4092                                          series['time'][4:6]
    4093                                  )
    4094
    4095                          series_desc_parts = []
    4096                          if series['description'] is not None:
    4097                                  if series['protocol'] is None:
    4098                                          series_desc_parts.append(series['description'].strip())
    4099                                  else:
    4100                                          if series['description'].strip() not in series['protocol'].strip():
    4101                                                  series_desc_parts.append(series['description'].strip())
    4102                          if series['protocol'] is not None:
    4103                                  series_desc_parts.append('[%s]' % series['protocol'].strip())
    4104                          if series['performed_procedure_step_description'] is not None:
    4105                                  series_desc_parts.append(series['performed_procedure_step_description'].strip())
    4106                          if series['acquisition_device_processing_description'] is not None:
    4107                                  series_desc_parts.append(series['acquisition_device_processing_description'].strip())
    4108                          series_desc = ' / '.join(series_desc_parts)
    4109                          if len(series_desc) > 0:
    4110                                  series_desc = ': ' + series_desc
    4111                          series_desc = _('%s image(s)%s') % (len(series['instances']), series_desc)
    4112
    4113                          series_list_items.append ([
    4114                                  series_time,
    4115                                  gmTools.coalesce(series['modality'], ''),
    4116                                  gmTools.coalesce(series['body_part'], ''),
    4117                                  series_desc
    4118                          ])
    4119                          series_list_data.append(series)
    4120
    4121                  self._LCTRL_series.set_string_items(items = series_list_items)
    4122                  self._LCTRL_series.set_data(data = series_list_data)
    4123                  self._LCTRL_series.SortListItems(0)
    4124
    4125                  self.__refresh_image()
    4126                  self.__refresh_details()
    4127                  self.__set_button_states()
    4128
    4129          #--------------------------------------------------------
    4130 -        def _on_studies_list_item_deselected(self, event):
    4131                  event.Skip()
    4132
    4133                  self._LCTRL_series.remove_items_safely()
    4134                  self.__refresh_image()
    4135                  self.__refresh_details()
    4136                  self.__set_button_states()
    4137
    4138          #--------------------------------------------------------
    4139          # events: buttons
    4140          #--------------------------------------------------------
    4141 -        def _on_connect_button_pressed(self, event):
    4142                  event.Skip()
    4143
    4144                  if not self.__connect():
    4145                          self.__reset_patient_data()
    4146                          self.__set_button_states()
    4147                          return False
    4148
    4149                  if not self.__refresh_patient_data():
    4150                          self.__set_button_states()
    4151                          return False
    4152
    4153                  self.__set_button_states()
    4154                  return True
    4155
    4156          #--------------------------------------------------------
    4157 -        def _on_upload_button_pressed(self, event):
    4158                  event.Skip()
    4159                  if self.__pacs is None:
    4160                          return
    4161
    4162                  dlg = wx.DirDialog (
    4163                          self,
    4164                          message = _('Select the directory from which to recursively upload DICOM files.'),
    4165                          defaultPath = os.path.join(gmTools.gmPaths().home_dir, 'gnumed')
    4166                  )
    4167                  choice = dlg.ShowModal()
    4168                  dicom_dir = dlg.GetPath()
    4169                  dlg.DestroyLater()
    4170                  if choice != wx.ID_OK:
    4171                          return True
    4172                  wx.BeginBusyCursor()
    4173                  try:
    4174                          uploaded, not_uploaded = self.__pacs.upload_from_directory (
    4175                                  directory = dicom_dir,
    4176                                  recursive = True,
    4177                                  check_mime_type = False,
    4178                                  ignore_other_files = True
    4179                          )
    4180                  finally:
    4181                          wx.EndBusyCursor()
    4182                  if len(not_uploaded) == 0:
    4183                          q = _('Delete the uploaded DICOM files now ?')
    4184                  else:
    4185                          q = _('Some files have not been uploaded.\n\nDo you want to delete those DICOM files which have been sent to the PACS successfully ?')
    4186                          _log.error('not uploaded:')
    4187                          for f in not_uploaded:
    4188                                  _log.error(f)
    4189
    4190                  delete_uploaded = gmGuiHelpers.gm_show_question (
    4191                          title = _('Uploading DICOM files'),
    4192                          question = q,
    4193                          cancel_button = False
    4194                  )
    4195                  if not delete_uploaded:
    4196                          return
    4197                  wx.BeginBusyCursor()
    4198                  for f in uploaded:
    4199                          gmTools.remove_file(f)
    4200                  wx.EndBusyCursor()
    4201
    4202          #--------------------------------------------------------
    4203 -        def _on_modify_orthanc_content_button_pressed(self, event):
    4204                  event.Skip()
    4205                  if self.__pacs is None:
    4206                          return
    4207
    4208                  title = _('Working on: Orthanc "%s" (AET "%s" @ %s:%s, Version %s)') % (
    4209                          self.__pacs.server_identification['Name'],
    4210                          self.__pacs.server_identification['DicomAet'],
    4211                          self._TCTRL_host.Value.strip(),
    4212                          self._TCTRL_port.Value.strip(),
    4213                          self.__pacs.server_identification['Version']
    4214                  )
    4215                  dlg = cModifyOrthancContentDlg(self, -1, server = self.__pacs, title = title)
    4216                  dlg.ShowModal()
    4217                  dlg.DestroyLater()
    4218                  self._schedule_data_reget()
    4219
    4220          #--------------------------------------------------------
    4221          # - image menu and image buttons
    4222          #--------------------------------------------------------
    4223 -        def _on_show_image_as_dcm(self, event):
    4224                  self.__show_image(as_dcm = True)
    4225
    4226          #--------------------------------------------------------
    4227 -        def _on_show_image_as_png(self, event):
    4228                  self.__show_image(as_png = True)
    4229
    4230          #--------------------------------------------------------
    4231 -        def _on_copy_image_to_export_area(self, event):
    4232                  self.__copy_image_to_export_area()
    4233
    4234          #--------------------------------------------------------
    4235 -        def _on_save_image_as_png(self, event):
    4236                  self.__save_image(as_png = True, nice_filename = True)
    4237
    4238          #--------------------------------------------------------
    4239 -        def _on_save_image_as_dcm(self, event):
    4240                  self.__save_image(as_dcm = True, nice_filename = True)
    4241
    4242          #--------------------------------------------------------
    4243          #--------------------------------------------------------
    4244 -        def _on_preview_image_leftdoubleclicked(self, event):
    4245                  self.__show_image()
    4246
    4247          #--------------------------------------------------------
    4248 -        def _on_preview_image_rightclicked(self, event):
    4249                  if self.__image_data is None:
    4250                          return False
    4251
    4252                  self.PopupMenu(self.__thumbnail_menu)
    4253
    4254          #--------------------------------------------------------
    4255 -        def _on_next_image_button_pressed(self, event):
    4256                  if self.__image_data is None:
    4257                          return
    4258
    4259                  self.__refresh_image(idx = self.__image_data['idx'] + 1)
    4260                  self.__refresh_details()
    4261
    4262          #--------------------------------------------------------
    4263 -        def _on_previous_image_button_pressed(self, event):
    4264                  if self.__image_data is None:
    4265                          return
    4266                  self.__refresh_image(idx = self.__image_data['idx'] - 1)
    4267                  self.__refresh_details()
    4268
    4269          #--------------------------------------------------------
    4270 -        def _on_button_image_show_pressed(self, event):
    4271                  self.__show_image()
    4272
    4273          #--------------------------------------------------------
    4274 -        def _on_button_image_export_pressed(self, event):
    4275                  self.__copy_image_to_export_area()
    4276
    4277          #--------------------------------------------------------
    4278          # - study menu and buttons
    4279          #--------------------------------------------------------
    4280 -        def _on_browse_study_button_pressed(self, event):
    4281                  self.__browse_studies()
    4282
    4283          #--------------------------------------------------------
    4284 -        def _on_studies_show_button_pressed(self, event):
    4285                  self.__show_studies()
    4286
    4287          #--------------------------------------------------------
    4288 -        def _on_studies_export_button_pressed(self, event):
    4289                  self.__copy_selected_studies_to_export_area()
    4290
    4291          #--------------------------------------------------------
    4292 -        def _on_studies_button_rightclicked(self, event):
    4293                  self.PopupMenu(self.__studies_menu)
    4294
    4295          #--------------------------------------------------------
    4296 -        def _on_copy_selected_studies_to_export_area(self, event):
    4297                  self.__copy_selected_studies_to_export_area()
    4298
    4299          #--------------------------------------------------------
    4300 -        def _on_copy_all_studies_to_export_area(self, event):
    4301                  self.__copy_all_studies_to_export_area()
    4302
    4303          #--------------------------------------------------------
    4304 -        def _on_copy_zip_of_selected_studies_to_export_area(self, event):
    4305                  self.__copy_zip_of_selected_studies_to_export_area()
    4306
    4307          #--------------------------------------------------------
    4308 -        def _on_copy_zip_of_all_studies_to_export_area(self, event):
    4309                  self.__copy_zip_of_all_studies_to_export_area()
    4310
    4311          #--------------------------------------------------------
    4312 -        def _on_save_selected_studies(self, event):
    4313                  self.__save_selected_studies()
    4314
    4315          #--------------------------------------------------------
    4316 -        def _on_save_zip_of_selected_studies(self, event):
    4317                  self.__save_zip_of_selected_studies()
    4318
    4319          #--------------------------------------------------------
    4320 -        def _on_save_all_studies(self, event):
    4321                  self.__save_all_studies()
    4322
    4323          #--------------------------------------------------------
    4324 -        def _on_save_zip_of_all_studies(self, event):
    4325                  self.__save_zip_of_all_studies()
    4326
    4327          #--------------------------------------------------------
    4328          # - patient buttons (= all studies)
    4329          #--------------------------------------------------------
    4330 -        def _on_browse_patient_button_pressed(self, event):
    4331                  self.__browse_patient()
    4332
    4333          #--------------------------------------------------------
    4334 -        def _on_verify_patient_data_button_pressed(self, event):
    4335                  if self.__pacs is None:
    4336                          return None
    4337
    4338                  if self.__orthanc_patient is None:
    4339                          return None
    4340
    4341                  patient_id = self.__orthanc_patient['ID']
    4342                  wx.BeginBusyCursor()
    4343                  try:
    4344                          bad_data = self.__pacs.verify_patient_data(patient_id)
    4345                  finally:
    4346                          wx.EndBusyCursor()
    4347                  if len(bad_data) == 0:
    4348                          gmDispatcher.send(signal = 'statustext', msg = _('Successfully verified DICOM data of patient.'))
    4349                          return
    4350
    4351                  gmGuiHelpers.gm_show_error (
    4352                          title = _('DICOM data error'),
    4353                          error = _(
    4354                                  'There seems to be a data error in the DICOM files\n'
    4355                                  'stored in the Orthanc server.\n'
    4356                                  '\n'
    4357                                  'Please check the inbox.'
    4358                          )
    4359                  )
    4360
    4361                  msg = _('Checksum error in DICOM data of this patient.\n\n')
    4362                  msg += _('DICOM server: %s\n\n') % bad_data[0]['orthanc']
    4363                  for bd in bad_data:
    4364                          msg += _('Orthanc patient ID [%s]\n %s: [%s]\n') % (
    4365                                  bd['patient'],
    4366                                  bd['type'],
    4367                                  bd['instance']
    4368                          )
    4369                  prov = self.__patient.primary_provider
    4370                  if prov is None:
    4371                          prov = gmStaff.gmCurrentProvider()
    4372                  report = gmProviderInbox.create_inbox_message (
    4373                          message_type = _('error report'),
    4374                          message_category = 'clinical',
    4375                          patient = self.__patient.ID,
    4376                          staff = prov['pk_staff'],
    4377                          subject = _('DICOM data corruption')
    4378                  )
    4379                  report['data'] = msg
    4380                  report.save()
    4381
    4382          #--------------------------------------------------------
    4383 -        def _on_browse_pacs_button_pressed(self, event):
    4384                  self.__browse_pacs()
    4385
    4386  #------------------------------------------------------------
    4387  from Gnumed.wxGladeWidgets.wxgModifyOrthancContentDlg import wxgModifyOrthancContentDlg
    4388
    4389 -class cModifyOrthancContentDlg(wxgModifyOrthancContentDlg):
    4390 -        def __init__(self, *args, **kwds):
    4391                  self.__srv = kwds['server']
    4392                  del kwds['server']
    4393                  title = kwds['title']
    4394                  del kwds['title']
    4395                  wxgModifyOrthancContentDlg.__init__(self, *args, **kwds)
    4396                  self.SetTitle(title)
    4397                  self._LCTRL_patients.set_columns( [_('Patient ID'), _('Name'), _('Birth date'), _('Gender'), _('Orthanc')] )
    4398
    4399          #--------------------------------------------------------
    4400 -        def __refresh_patient_list(self):
    4401                  self._LCTRL_patients.set_string_items()
    4402                  search_term = self._TCTRL_search_term.Value.strip()
    4403                  if search_term == '':
    4404                          return
    4405                  pats = self.__srv.get_patients_by_name(name_parts = search_term.split(), fuzzy = True)
    4406                  if len(pats) == 0:
    4407                          return
    4408                  list_items = []
    4409                  list_data = []
    4410                  for pat in pats:
    4411                          mt = pat['MainDicomTags']
    4412                          try:
    4413                                  gender = mt['PatientSex']
    4414                          except KeyError:
    4415                                  gender = ''
    4416                          try:
    4417                                  dob = mt['PatientBirthDate']
    4418                          except KeyError:
    4419                                  dob = ''
    4420                          list_items.append([mt['PatientID'], mt['PatientName'], dob, gender, pat['ID']])
    4421                          list_data.append(mt['PatientID'])
    4422                  self._LCTRL_patients.set_string_items(list_items)
    4423                  self._LCTRL_patients.set_column_widths()
    4424                  self._LCTRL_patients.set_data(list_data)
    4425
    4426          #--------------------------------------------------------
    4427 -        def _on_search_patients_button_pressed(self, event):
    4428                  event.Skip()
    4429                  self.__refresh_patient_list()
    4430
    4431          #--------------------------------------------------------
    4432 -        def _on_suggest_patient_id_button_pressed(self, event):
    4433                  event.Skip()
    4434                  pat = gmPerson.gmCurrentPatient()
    4435                  if not pat.connected:
    4436                          return
    4437                  self._TCTRL_new_patient_id.Value = pat.suggest_external_id(target = 'PACS')
    4438
    4439          #--------------------------------------------------------
    4440 -        def _on_set_patient_id_button_pressed(self, event):
    4441                  event.Skip()
    4442                  new_id = self._TCTRL_new_patient_id.Value.strip()
    4443                  if new_id == '':
    4444                          return
    4445                  pats = self._LCTRL_patients.get_selected_item_data(only_one = False)
    4446                  if len(pats) == 0:
    4447                          return
    4448                  really_modify = gmGuiHelpers.gm_show_question (
    4449                          title = _('Modifying patient ID'),
    4450                          question = _(
    4451                                  'Really modify %s patient(s) to have the new patient ID\n\n'
    4452                                  ' [%s]\n\n'
    4453                                  'stored in the Orthanc DICOM server ?'
    4454                          ) % (
    4455                                  len(pats),
    4456                                  new_id
    4457                          ),
    4458                          cancel_button = True
    4459                  )
    4460                  if not really_modify:
    4461                          return
    4462                  all_modified = True
    4463                  for pat in pats:
    4464                          success = self.__srv.modify_patient_id(old_patient_id = pat, new_patient_id = new_id)
    4465                          if not success:
    4466                                  all_modified = False
    4467                  self.__refresh_patient_list()
    4468
    4469                  if not all_modified:
    4470                          gmGuiHelpers.gm_show_warning (
    4471                                  aTitle = _('Modifying patient ID'),
    4472                                  aMessage = _(
    4473                                          'I was unable to modify all DICOM patients.\n'
    4474                                          '\n'
    4475                                          'Please refer to the log file.'
    4476                                  )
    4477                          )
    4478                  return all_modified
    4479
    4480  #------------------------------------------------------------
    4481  # outdated:
    4482 -def upload_files():
    4483          event.Skip()
    4484          dlg = wx.DirDialog (
    4485                  self,
    4486                  message = _('Select the directory from which to recursively upload DICOM files.'),
    4487                  defaultPath = os.path.join(gmTools.gmPaths().home_dir, 'gnumed')
    4488          )
    4489          choice = dlg.ShowModal()
    4490          dicom_dir = dlg.GetPath()
    4491          dlg.DestroyLater()
    4492          if choice != wx.ID_OK:
    4493                  return True
    4494          wx.BeginBusyCursor()
    4495          try:
    4496                  uploaded, not_uploaded = self.__pacs.upload_from_directory (
    4497                          directory = dicom_dir,
    4498                          recursive = True,
    4499                          check_mime_type = False,
    4500                          ignore_other_files = True
    4501                  )
    4502          finally:
    4503                  wx.EndBusyCursor()
    4504          if len(not_uploaded) == 0:
    4505                  q = _('Delete the uploaded DICOM files now ?')
    4506          else:
    4507                  q = _('Some files have not been uploaded.\n\nDo you want to delete those DICOM files which have been sent to the PACS successfully ?')
    4508                  _log.error('not uploaded:')
    4509                  for f in not_uploaded:
    4510                          _log.error(f)
    4511                  delete_uploaded = gmGuiHelpers.gm_show_question (
    4512                  title = _('Uploading DICOM files'),
    4513                  question = q,
    4514                  cancel_button = False
    4515          )
    4516          if not delete_uploaded:
    4517                  return
    4518          wx.BeginBusyCursor()
    4519          for f in uploaded:
    4520                  gmTools.remove_file(f)
    4521          wx.EndBusyCursor()
    4522
    4523  #============================================================
    4524  # main
    4525  #------------------------------------------------------------
    4526  if __name__ == '__main__':
    4527
    4528          if len(sys.argv) < 2:
    4529                  sys.exit()
    4530
    4531          if sys.argv[1] != 'test':
    4532                  sys.exit()
    4533
    4534          from Gnumed.business import gmPersonSearch
    4535          from Gnumed.wxpython import gmPatSearchWidgets
    4536
    4537          #----------------------------------------------------------------
    4538 -        def test_document_prw():
    4539                  app = wx.PyWidgetTester(size = (180, 20))
    4540                  #pnl = cEncounterEditAreaPnl(app.frame, -1, encounter=enc)
    4541                  prw = cDocumentPhraseWheel(app.frame, -1)
    4542                  prw.set_context('pat', 12)
    4543                  app.frame.Show(True)
    4544                  app.MainLoop()
    4545
    4546          #----------------------------------------------------------------
    4547          test_document_prw()
    4548