Gnumed.wxpython.gmVisualProgressNoteWidgets

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                  if not self:
    390                          # our C/C++ part may be dead already, due to async behaviour
    391                          return
    392
    393                  if document_folder is None:
    394                          self.clear()
    395                          self.GetParent().Layout()
    396                          return
    397
    398                  soap_docs = document_folder.get_visual_progress_notes(episodes = episodes, encounter = encounter)
    399                  if len(soap_docs) == 0:
    400                          self.clear()
    401                          self.GetParent().Layout()
    402                          return
    403
    404                  if not do_async:
    405                          cookie, parts_list = self._worker__export_doc_parts(docs = soap_docs)
    406                          self.__show_exported_parts(parts_list = parts_list)
    407                          return
    408
    409                  self.__worker_cookie = '%sCookie-%s' % (self.__class__.__name__, random.random())
    410                  _log.debug('starting worker thread, cookie: %s', self.__worker_cookie)
    411                  gmWorkerThread.execute_in_worker_thread (
    412                          payload_function = self._worker__export_doc_parts,
    413                          payload_kwargs = {'docs': soap_docs, 'cookie': self.__worker_cookie},
    414                          completion_callback = self._forwarder__show_exported_doc_parts,
    415                          worker_name = self.__class__.__name__
    416                  )
    417
    418          #--------------------------------------------------------
    419 -        def clear(self):
    420                  while self._SZR_soap and len(self._SZR_soap.GetChildren()) > 0:
    421                          self._SZR_soap.Detach(0)
    422                  for bmp in self.__bitmaps:
    423                          bmp.Unbind(wx.EVT_LEFT_UP)
    424                          bmp.DestroyLater()
    425                  self.__bitmaps = []
    426
    427          #--------------------------------------------------------
    428          # event handlers
    429          #--------------------------------------------------------
    430 -        def _on_bitmap_leftclicked(self, evt):
    431                  wx.CallAfter (
    432                          edit_visual_progress_note,
    433                          doc_part = evt.GetEventObject().doc_part,
    434                          discard_unmodified = True
    435                  )
    436
    437          #--------------------------------------------------------
    438          # internal API
    439          #--------------------------------------------------------
    440 -        def _worker__export_doc_parts(self, docs=None, cookie=None):
    441                  # this is used as the worker thread payload
    442                  _log.debug('cookie [%s]', cookie)
    443                  conn = gmPG2.get_connection(readonly = True, connection_name = threading.current_thread().name, pooled = False)
    444                  parts_list = []
    445                  for soap_doc in docs:
    446                          parts = soap_doc.parts
    447                          if len(parts) == 0:
    448                                  continue
    449                          parts_counter = ''
    450                          if len(parts) > 1:
    451                                  parts_counter = _(' [part 1 of %s]') % len(parts)
    452                          part = parts[0]
    453                          fname = part.save_to_file(conn = conn)
    454                          if fname is None:
    455                                  continue
    456                          tt_header = _('Created: %s%s') % (gmDateTime.pydt_strftime(part['date_generated'], '%Y %b %d'), parts_counter)
    457                          tt_footer = gmTools.coalesce(part['doc_comment'], '').strip()
    458                          parts_list.append([fname, part, tt_header, tt_footer])
    459                  conn.close()
    460                  _log.debug('worker finished')
    461                  return (cookie, parts_list)
    462
    463          #--------------------------------------------------------
    464 -        def _forwarder__show_exported_doc_parts(self, worker_result):
    465                  # this is the worker thread completion callback
    466                  cookie, parts_list = worker_result
    467                  # worker still the one we are interested in ?
    468                  if cookie != self.__worker_cookie:
    469                          _log.debug('received results from old worker [%s], I am [%s], ignoring', cookie, self.__worker_cookie)
    470                          return
    471                  if len(parts_list) == 0:
    472                          return
    473                  wx.CallAfter(self.__show_exported_parts, parts_list = parts_list)
    474
    475          #--------------------------------------------------------
    476 -        def __show_exported_parts(self, parts_list=None):
    477                  self.clear()
    478                  for part_def in parts_list:
    479                          fname, part, tt_header, tt_footer = part_def
    480                          #_log.debug(tt_header)
    481                          #_log.debug(tt_footer)
    482                          img = gmGuiHelpers.file2scaled_image (
    483                                  filename = fname,
    484                                  height = 30
    485                          )
    486                          bmp = wx_genstatbmp.GenStaticBitmap(self, -1, img, style = wx.NO_BORDER)
    487                          bmp.doc_part = part
    488                          img = gmGuiHelpers.file2scaled_image (
    489                                  filename = fname,
    490                                  height = 150
    491                          )
    492                          tip = agw_stt.SuperToolTip (
    493                                  '',
    494                                  bodyImage = img,
    495                                  header = tt_header,
    496                                  footer = tt_footer
    497                          )
    498                          tip.SetTopGradientColor('white')
    499                          tip.SetMiddleGradientColor('white')
    500                          tip.SetBottomGradientColor('white')
    501                          tip.SetTarget(bmp)
    502                          bmp.Bind(wx.EVT_LEFT_UP, self._on_bitmap_leftclicked)
    503                          # FIXME: add context menu for Delete/Clone/Add/Configure
    504                          self._SZR_soap.Add(bmp, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM | wx.EXPAND, 3)
    505                          self.__bitmaps.append(bmp)
    506                  self.GetParent().Layout()
    507
    508  #============================================================
    509  # main
    510  #------------------------------------------------------------
    511  if __name__ == '__main__':
    512
    513          if len(sys.argv) < 2:
    514                  sys.exit()
    515
    516          if sys.argv[1] != 'test':
    517                  sys.exit()
    518
    519          gmI18N.activate_locale()
    520          gmI18N.install_domain(domain = 'gnumed')
    521
    522          #----------------------------------------
    523