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