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