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

# Source Code for Module Gnumed.wxpython.gmVisualProgressNoteWidgets

      1  # -*- coding: utf-8 -*-
      2  """GNUmed visual progress notes handling widgets."""
      3  #================================================================
      4  __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>"
      5  __license__ = "GPL v2 or later (details at http://www.gnu.org)"
      6
      7  import sys
      8  import logging
      9  import os
     10  import os.path
     11  import shutil
     12  import random
     13  import threading
     14
     15
     16  import wx
     17  import wx.lib.agw.supertooltip as agw_stt
     18  import wx.lib.statbmp as wx_genstatbmp
     19
     20
     21  if __name__ == '__main__':
     22          sys.path.insert(0, '../../')
     23
     24  from Gnumed.pycommon import gmI18N
     25
     26  if __name__ == '__main__':
     27          gmI18N.activate_locale()
     28          gmI18N.install_domain()
     29
     30  from Gnumed.pycommon import gmDispatcher
     31  from Gnumed.pycommon import gmTools
     32  from Gnumed.pycommon import gmDateTime
     33  from Gnumed.pycommon import gmShellAPI
     34  from Gnumed.pycommon import gmCfg
     35  from Gnumed.pycommon import gmMatchProvider
     36  from Gnumed.pycommon import gmMimeLib
     37  from Gnumed.pycommon import gmWorkerThread
     38  from Gnumed.pycommon import gmPG2
     39
     40  from Gnumed.business import gmPerson
     41  from Gnumed.business import gmEMRStructItems
     42  from Gnumed.business import gmPraxis
     43  from Gnumed.business import gmForms
     44  from Gnumed.business import gmDocuments
     45
     46  from Gnumed.wxpython import gmPhraseWheel
     47  from Gnumed.wxpython import gmGuiHelpers
     48  from Gnumed.wxpython import gmCfgWidgets
     49  from Gnumed.wxpython import gmDocumentWidgets
     50
     51
     52  _log = logging.getLogger('gm.ui')
     53
     54  #============================================================
     55  # visual progress notes
     56  #============================================================
     57 -def configure_visual_progress_note_editor():
     58
     59          def is_valid(value):
     60
     61                  if value is None:
     62                          gmDispatcher.send (
     63                                  signal = 'statustext',
     64                                  msg = _('You need to actually set an editor.'),
     65                                  beep = True
     66                          )
     67                          return False, value
     68
     69                  if value.strip() == '':
     70                          gmDispatcher.send (
     71                                  signal = 'statustext',
     72                                  msg = _('You need to actually set an editor.'),
     73                                  beep = True
     74                          )
     75                          return False, value
     76
     77                  found, binary = gmShellAPI.detect_external_binary(value)
     78                  if not found:
     79                          gmDispatcher.send (
     80                                  signal = 'statustext',
     81                                  msg = _('The command [%s] is not found.') % value,
     82                                  beep = True
     83                          )
     84                          return True, value
     85
     86                  return True, binary
     87
     88          #------------------------------------------
     89          cmd = gmCfgWidgets.configure_string_option (
     90                  message = _(
     91                          'Enter the shell command with which to start\n'
     92                          'the image editor for visual progress notes.\n'
     93                          '\n'
     94                          'Any "%(img)s" included with the arguments\n'
     95                          'will be replaced by the file name of the\n'
     96                          'note template.'
     97                  ),
     98                  option = 'external.tools.visual_soap_editor_cmd',
     99                  bias = 'user',
    100                  default_value = None,
    101                  validator = is_valid
    102          )
    103
    104          return cmd
    105
    106  #============================================================
    107 -def select_file_as_visual_progress_note_template(parent=None):
    108          if parent is None:
    109                  parent = wx.GetApp().GetTopWindow()
    110
    111          dlg = wx.FileDialog (
    112                  parent = parent,
    113                  message = _('Choose file to use as template for new visual progress note'),
    114                  defaultDir = os.path.expanduser('~'),
    115                  defaultFile = '',
    116                  #wildcard = "%s (*)|*|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')),
    117                  style = wx.FD_OPEN | wx.FD_FILE_MUST_EXIST
    118          )
    119          result = dlg.ShowModal()
    120
    121          if result == wx.ID_CANCEL:
    122                  dlg.DestroyLater()
    123                  return None
    124
    125          full_filename = dlg.GetPath()
    126          dlg.Hide()
    127          dlg.DestroyLater()
    128          return full_filename
    129
    130  #------------------------------------------------------------
    131 -def select_visual_progress_note_template(parent=None):
    132
    133          if parent is None:
    134                  parent = wx.GetApp().GetTopWindow()
    135
    136          dlg = gmGuiHelpers.c3ButtonQuestionDlg (
    137                  parent,
    138                  -1,
    139                  caption = _('Visual progress note source'),
    140                  question = _('From which source do you want to pick the image template ?'),
    141                  button_defs = [
    142                          {'label': _('Database'), 'tooltip': _('List of templates in the database.'), 'default': True},
    143                          {'label': _('File'), 'tooltip': _('Files in the filesystem.'), 'default': False},
    144                          {'label': _('Device'), 'tooltip': _('Image capture devices (scanners, cameras, etc)'), 'default': False}
    145                  ]
    146          )
    147          result = dlg.ShowModal()
    148          dlg.DestroyLater()
    149
    150          # 1) select from template
    151          if result == wx.ID_YES:
    152                  _log.debug('visual progress note template from: database template')
    153                  from Gnumed.wxpython import gmFormWidgets
    154                  template = gmFormWidgets.manage_form_templates (
    155                          parent = parent,
    156                          template_types = [gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE],
    157                          active_only = True
    158                  )
    159                  if template is None:
    160                          return (None, None)
    161                  filename = template.save_to_file()
    162                  if filename is None:
    163                          gmDispatcher.send(signal = 'statustext', msg = _('Cannot export visual progress note template for [%s].') % template['name_long'])
    164                          return (None, None)
    165                  return (filename, True)
    166
    167          # 2) select from disk file
    168          if result == wx.ID_NO:
    169                  _log.debug('visual progress note template from: disk file')
    170                  fname = select_file_as_visual_progress_note_template(parent = parent)
    171                  if fname is None:
    172                          return (None, None)
    173                  # create a copy of the picked file -- don't modify the original
    174                  ext = os.path.splitext(fname)[1]
    175                  tmp_name = gmTools.get_unique_filename(suffix = ext)
    176                  _log.debug('visual progress note from file: [%s] -> [%s]', fname, tmp_name)
    177                  shutil.copy2(fname, tmp_name)
    178                  return (tmp_name, False)
    179
    180          # 3) acquire from capture device
    181          if result == wx.ID_CANCEL:
    182                  _log.debug('visual progress note template from: image capture device')
    183                  fnames = gmDocumentWidgets.acquire_images_from_capture_device(device = None, calling_window = parent)
    184                  if fnames is None:
    185                          return (None, None)
    186                  if len(fnames) == 0:
    187                          return (None, None)
    188                  return (fnames[0], False)
    189
    190          _log.debug('no visual progress note template source selected')
    191          return (None, None)
    192
    193  #------------------------------------------------------------
    194 -def edit_visual_progress_note(filename=None, episode=None, discard_unmodified=False, doc_part=None, health_issue=None):
    195          """This assumes <filename> contains an image which can be handled by the configured image editor."""
    196
    197          if doc_part is not None:
    198                  filename = doc_part.save_to_file()
    199                  if filename is None:
    200                          gmDispatcher.send(signal = 'statustext', msg = _('Cannot export visual progress note to file.'))
    201                          return None
    202
    203          dbcfg = gmCfg.cCfgSQL()
    204          editor = dbcfg.get2 (
    205                  option = 'external.tools.visual_soap_editor_cmd',
    206                  workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
    207                  bias = 'user'
    208          )
    209
    210          if editor is None:
    211                  _log.error('no editor for visual progress notes configured, trying mimetype editor')
    212                  gmDispatcher.send(signal = 'statustext', msg = _('Editor for visual progress note not configured.'), beep = False)
    213                  mimetype = gmMimeLib.guess_mimetype(filename = filename)
    214                  editor = gmMimeLib.get_editor_cmd(mimetype = mimetype, filename = filename)
    215                  if editor is None:
    216                          _log.error('no editor for mimetype <%s> configured, trying mimetype viewer', mimetype)
    217                          success, msg = gmMimeLib.call_viewer_on_file(aFile = filename, block = True)
    218                          if not success:
    219                                  _log.debug('problem running mimetype <%s> viewer', mimetype)
    220                                  gmGuiHelpers.gm_show_error (
    221                                          _(      'There is no editor for visual progress notes defined.\n'
    222                                                  'Also, there is no editor command defined for the file type\n'
    223                                                  '\n'
    224                                                  ' [%s].\n'
    225                                                  '\n'
    226                                                  'Therefor GNUmed attempted to at least *show* this\n'
    227                                                  'visual progress note. That failed as well, however:\n'
    228                                                  '\n'
    229                                                  '%s'
    230                                          ) % (mimetype, msg),
    231                                          _('Editing visual progress note')
    232                                  )
    233                                  editor = configure_visual_progress_note_editor()
    234                                  if editor is None:
    235                                          gmDispatcher.send(signal = 'statustext', msg = _('Editor for visual progress note not configured.'), beep = True)
    236                                          return None
    237
    238          if '%(img)s' in editor:
    239                  editor = editor % {'img': filename}
    240          else:
    241                  editor = '%s %s' % (editor, filename)
    242
    243          if discard_unmodified:
    244                  original_stat = os.stat(filename)
    245                  original_md5 = gmTools.file2md5(filename)
    246
    247          success = gmShellAPI.run_command_in_shell(editor, blocking = True)
    248          if not success:
    249                  success, msg = gmMimeLib.call_viewer_on_file(aFile = filename, block = True)
    250                  if not success:
    251                          _log.debug('problem running mimetype <%s> viewer', mimetype)
    252                          gmGuiHelpers.gm_show_error (
    253                                  _(      'There was a problem running the editor\n'
    254                                          '\n'
    255                                          ' [%s] (%s)\n'
    256                                          '\n'
    257                                          'on the visual progress note.\n'
    258                                          '\n'
    259                                          'Therefor GNUmed attempted to at least *show* it.\n'
    260                                          'That failed as well, however:\n'
    261                                          '\n'
    262                                          '%s'
    263                                  ) % (editor, mimetype, msg),
    264                                  _('Editing visual progress note')
    265                          )
    266                          editor = configure_visual_progress_note_editor()
    267                          if editor is None:
    268                                  gmDispatcher.send(signal = 'statustext', msg = _('Editor for visual progress note not configured.'), beep = True)
    269                  return None
    270
    271          try:
    272                  open(filename, 'r').close()
    273          except Exception:
    274                  _log.exception('problem accessing visual progress note file [%s]', filename)
    275                  gmGuiHelpers.gm_show_error (
    276                          _(      'There was a problem reading the visual\n'
    277                                  'progress note from the file:\n'
    278                                  '\n'
    279                                  ' [%s]\n'
    280                                  '\n'
    281                          ) % filename,
    282                          _('Saving visual progress note')
    283                  )
    284                  return None
    285
    286          if discard_unmodified:
    287                  modified_stat = os.stat(filename)
    288                  # same size ?
    289                  if original_stat.st_size == modified_stat.st_size:
    290                          modified_md5 = gmTools.file2md5(filename)
    291                          # same hash ?
    292                          if original_md5 == modified_md5:
    293                                  _log.debug('visual progress note (template) not modified')
    294                                  # ask user to decide
    295                                  msg = _(
    296                                          'You either created a visual progress note from a template\n'
    297                                          'in the database (rather than from a file on disk) or you\n'
    298                                          'edited an existing visual progress note.\n'
    299                                          '\n'
    300                                          'The template/original was not modified at all, however.\n'
    301                                          '\n'
    302                                          'Do you still want to save the unmodified image as a\n'
    303                                          'visual progress note into the EMR of the patient ?\n'
    304                                  )
    305                                  save_unmodified = gmGuiHelpers.gm_show_question (
    306                                          msg,
    307                                          _('Saving visual progress note')
    308                                  )
    309                                  if not save_unmodified:
    310                                          _log.debug('user discarded unmodified note')
    311                                          return
    312
    313          if doc_part is not None:
    314                  _log.debug('updating visual progress note')
    315                  doc_part.update_data_from_file(fname = filename)
    316                  doc_part.set_reviewed(technically_abnormal = False, clinically_relevant = True)
    317                  return None
    318
    319          if not isinstance(episode, gmEMRStructItems.cEpisode):
    320                  if episode is None:
    321                          episode = _('visual progress notes')
    322                  pat = gmPerson.gmCurrentPatient()
    323                  emr = pat.emr
    324                  episode = emr.add_episode(episode_name = episode.strip(), pk_health_issue = health_issue, is_open = False)
    325
    326          doc = gmDocumentWidgets.save_file_as_new_document (
    327                  filename = filename,
    328                  document_type = gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE,
    329                  episode = episode,
    330                  unlock_patient = False,
    331                  pk_org_unit = gmPraxis.gmCurrentPraxisBranch()['pk_org_unit']
    332          )
    333          doc.set_reviewed(technically_abnormal = False, clinically_relevant = True)
    334
    335          return doc
    336
    337  #============================================================
    338 -class cVisualSoapTemplatePhraseWheel(gmPhraseWheel.cPhraseWheel):
    339          """Phrasewheel to allow selection of visual SOAP template."""
    340
    341 -        def __init__(self, *args, **kwargs):
    342
    343                  gmPhraseWheel.cPhraseWheel.__init__ (self, *args, **kwargs)
    344
    345                  query = """
    346  SELECT
    347          pk AS data,
    348          name_short AS list_label,
    349          name_sort AS field_label
    350  FROM
    351          ref.paperwork_templates
    352  WHERE
    353          fk_template_type = (SELECT pk FROM ref.form_types WHERE name = '%s') AND (
    354                  name_long %%(fragment_condition)s
    355                          OR
    356                  name_short %%(fragment_condition)s
    357          )
    358  ORDER BY list_label
    359  LIMIT 15
    360  """     % gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE
    361
    362                  mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
    363                  mp.setThresholds(2, 3, 5)
    364
    365                  self.matcher = mp
    366                  self.selection_only = True
    367          #--------------------------------------------------------
    368 -        def _data2instance(self):
    369                  if self.GetData() is None:
    370                          return None
    371
    372                  return gmForms.cFormTemplate(aPK_obj = self.GetData())
    373
    374  #============================================================
    375  from Gnumed.wxGladeWidgets import wxgVisualSoapPresenterPnl
    376
    377 -class cVisualSoapPresenterPnl(wxgVisualSoapPresenterPnl.wxgVisualSoapPresenterPnl):
    378          """A panel displaying a number of images (visual progress note thumbnails)."""
    379
    380 -        def __init__(self, *args, **kwargs):
    381                  wxgVisualSoapPresenterPnl.wxgVisualSoapPresenterPnl.__init__(self, *args, **kwargs)
    382                  self._SZR_soap = self.GetSizer()
    383                  self.__bitmaps = []
    384
    385          #--------------------------------------------------------
    386          # external API
    387          #--------------------------------------------------------
    388 -        def refresh(self, document_folder=None, episodes=None, encounter=None, do_async=False):
    389
    390                  if document_folder is None:
    391                          self.clear()
    392                          self.GetParent().Layout()
    393                          return
    394
    395                  soap_docs = document_folder.get_visual_progress_notes(episodes = episodes, encounter = encounter)
    396                  if len(soap_docs) == 0:
    397                          self.clear()
    398                          self.GetParent().Layout()
    399                          return
    400
    401                  if not do_async:
    402                          cookie, parts_list = self._worker__export_doc_parts(docs = soap_docs)
    403                          self.__show_exported_parts(parts_list = parts_list)
    404                          return
    405
    406                  self.__worker_cookie = '%sCookie-%s' % (self.__class__.__name__, random.random())
    407                  _log.debug('starting worker thread, cookie: %s', self.__worker_cookie)
    408                  gmWorkerThread.execute_in_worker_thread (
    409                          payload_function = self._worker__export_doc_parts,
    410                          payload_kwargs = {'docs': soap_docs, 'cookie': self.__worker_cookie},
    411                          completion_callback = self._forwarder__show_exported_doc_parts,
    412                          worker_name = self.__class__.__name__
    413                  )
    414
    415          #--------------------------------------------------------
    416 -        def clear(self):
    417                  while len(self._SZR_soap.GetChildren()) > 0:
    418                          self._SZR_soap.Detach(0)
    419                  for bmp in self.__bitmaps:
    420                          bmp.Unbind(wx.EVT_LEFT_UP)
    421                          bmp.DestroyLater()
    422                  self.__bitmaps = []
    423
    424          #--------------------------------------------------------
    425          # event handlers
    426          #--------------------------------------------------------
    427 -        def _on_bitmap_leftclicked(self, evt):
    428                  wx.CallAfter (
    429                          edit_visual_progress_note,
    430                          doc_part = evt.GetEventObject().doc_part,
    431                          discard_unmodified = True
    432                  )
    433
    434          #--------------------------------------------------------
    435          # internal API
    436          #--------------------------------------------------------
    437 -        def _worker__export_doc_parts(self, docs=None, cookie=None):
    438                  # this is used as the worker thread payload
    439                  _log.debug('cookie [%s]', cookie)
    440                  conn = gmPG2.get_raw_connection(readonly = True, connection_name = threading.current_thread().name)
    441                  parts_list = []
    442                  for soap_doc in docs:
    443                          parts = soap_doc.parts
    444                          if len(parts) == 0:
    445                                  continue
    446                          parts_counter = ''
    447                          if len(parts) > 1:
    448                                  parts_counter = _(' [part 1 of %s]') % len(parts)
    449                          part = parts[0]
    450                          fname = part.save_to_file(conn = conn)
    451                          if fname is None:
    452                                  continue
    453                          tt_header = _('Created: %s%s') % (gmDateTime.pydt_strftime(part['date_generated'], '%Y %b %d'), parts_counter)
    454                          tt_footer = gmTools.coalesce(part['doc_comment'], '').strip()
    455                          parts_list.append([fname, part, tt_header, tt_footer])
    456                  conn.close()
    457                  _log.debug('worker finished')
    458                  return (cookie, parts_list)
    459
    460          #--------------------------------------------------------
    461 -        def _forwarder__show_exported_doc_parts(self, worker_result):
    462                  gmPG2.discard_pooled_connection()
    463                  # this is the worker thread completion callback
    464                  cookie, parts_list = worker_result
    465                  # worker still the one we are interested in ?
    466                  if cookie != self.__worker_cookie:
    467                          _log.debug('received results from old worker [%s], I am [%s], ignoring', cookie, self.__worker_cookie)
    468                          return
    469                  if len(parts_list) == 0:
    470                          return
    471                  wx.CallAfter(self.__show_exported_parts, parts_list = parts_list)
    472
    473          #--------------------------------------------------------
    474 -        def __show_exported_parts(self, parts_list=None):
    475                  self.clear()
    476                  for part_def in parts_list:
    477                          fname, part, tt_header, tt_footer = part_def
    478                          #_log.debug(tt_header)
    479                          #_log.debug(tt_footer)
    480                          img = gmGuiHelpers.file2scaled_image (
    481                                  filename = fname,
    482                                  height = 30
    483                          )
    484                          bmp = wx_genstatbmp.GenStaticBitmap(self, -1, img, style = wx.NO_BORDER)
    485                          bmp.doc_part = part
    486                          img = gmGuiHelpers.file2scaled_image (
    487                                  filename = fname,
    488                                  height = 150
    489                          )
    490                          tip = agw_stt.SuperToolTip (
    491                                  '',
    492                                  bodyImage = img,
    493                                  header = tt_header,
    494                                  footer = tt_footer
    495                          )
    496                          tip.SetTopGradientColor('white')
    497                          tip.SetMiddleGradientColor('white')
    498                          tip.SetBottomGradientColor('white')
    499                          tip.SetTarget(bmp)
    500                          bmp.Bind(wx.EVT_LEFT_UP, self._on_bitmap_leftclicked)
    501                          # FIXME: add context menu for Delete/Clone/Add/Configure
    502                          self._SZR_soap.Add(bmp, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM | wx.EXPAND, 3)
    503                          self.__bitmaps.append(bmp)
    504                  self.GetParent().Layout()
    505
    506  #============================================================
    507  # main
    508  #------------------------------------------------------------
    509  if __name__ == '__main__':
    510
    511          if len(sys.argv) < 2:
    512                  sys.exit()
    513
    514          if sys.argv[1] != 'test':
    515                  sys.exit()
    516
    517          gmI18N.activate_locale()
    518          gmI18N.install_domain(domain = 'gnumed')
    519
    520          #----------------------------------------
    521