Gnumed.wxpython.gmDocumentWidgets

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