Package Gnumed :: Package business :: Module gmForms |
[frames] | no frames]

# Source Code for Module Gnumed.business.gmForms

       1  # -*- coding: latin-1 -*-
       2  """GNUmed forms classes
       3
       4  Business layer for printing all manners of forms, letters, scripts etc.
       5
       6  license: GPL v2 or later
       7  """
       8  #============================================================
       9  __version__ = "$Revision: 1.79 $"
      10  __author__ ="Ian Haywood <ihaywood@gnu.org>, karsten.hilbert@gmx.net"
      11
      12
      13  import os, sys, time, os.path, logging
      14  import codecs
      15  import re as regex
      16  import shutil
      17  import random, platform, subprocess
      18  import socket                                                                           # needed for OOo on Windows
      19  #, libxml2, libxslt
      20  import shlex
      21
      22
      23  if __name__ == '__main__':
      24          sys.path.insert(0, '../../')
      25          from Gnumed.pycommon import gmI18N
      26          gmI18N.activate_locale()
      27          gmI18N.install_domain(domain='gnumed')
      28  from Gnumed.pycommon import gmTools
      29  from Gnumed.pycommon import gmDispatcher
      30  from Gnumed.pycommon import gmExceptions
      31  from Gnumed.pycommon import gmMatchProvider
      32  from Gnumed.pycommon import gmBorg
      33  from Gnumed.pycommon import gmLog2
      34  from Gnumed.pycommon import gmMimeLib
      35  from Gnumed.pycommon import gmShellAPI
      36  from Gnumed.pycommon import gmCfg
      37  from Gnumed.pycommon import gmBusinessDBObject
      38  from Gnumed.pycommon import gmPG2
      39
      40  from Gnumed.business import gmPerson
      41  from Gnumed.business import gmStaff
      42  from Gnumed.business import gmPersonSearch
      43  from Gnumed.business import gmSurgery
      44
      45
      46  _log = logging.getLogger('gm.forms')
      47  _log.info(__version__)
      48
      49  #============================================================
      50  # this order is also used in choice boxes for the engine
      51  form_engine_abbrevs = [u'O', u'L', u'I', u'G', u'P', u'A']
      52
      53  form_engine_names = {
      54          u'O': 'OpenOffice',
      55          u'L': 'LaTeX',
      56          u'I': 'Image editor',
      57          u'G': 'Gnuplot script',
      58          u'P': 'PDF forms',
      59          u'A': 'AbiWord'
      60  }
      61
      62  form_engine_template_wildcards = {
      63          u'O': u'*.o?t',
      64          u'L': u'*.tex',
      65          u'G': u'*.gpl',
      66          u'P': u'*.pdf',
      67          u'A': u'*.abw'
      68  }
      69
      70  # is filled in further below after each engine is defined
      71  form_engines = {}
      72
      73  #============================================================
      74  # match providers
      75  #============================================================
      76 -class cFormTemplateNameLong_MatchProvider(gmMatchProvider.cMatchProvider_SQL2):
      77
      78 -        def __init__(self):
      79
      80                  query = u"""
      81                          SELECT
      82                                  name_long AS data,
      83                                  name_long AS list_label,
      84                                  name_long AS field_label
      85                          FROM ref.v_paperwork_templates
      86                          WHERE name_long %(fragment_condition)s
      87                          ORDER BY list_label
      88                  """
      89                  gmMatchProvider.cMatchProvider_SQL2.__init__(self, queries = [query])
      90  #============================================================
      91 -class cFormTemplateNameShort_MatchProvider(gmMatchProvider.cMatchProvider_SQL2):
      92
      93 -        def __init__(self):
      94
      95                  query = u"""
      96                          SELECT
      97                                  name_short AS data,
      98                                  name_short AS list_label,
      99                                  name_short AS field_label
     100                          FROM ref.v_paperwork_templates
     101                          WHERE name_short %(fragment_condition)s
     102                          ORDER BY name_short
     103                  """
     104                  gmMatchProvider.cMatchProvider_SQL2.__init__(self, queries = [query])
     105  #============================================================
     106 -class cFormTemplateType_MatchProvider(gmMatchProvider.cMatchProvider_SQL2):
     107
     108 -        def __init__(self):
     109
     110                  query = u"""
     111                          SELECT DISTINCT ON (list_label)
     112                                  pk AS data,
     113                                  _(name) || ' (' || name || ')' AS list_label,
     114                                  _(name) AS field_label
     115                          FROM ref.form_types
     116                          WHERE
     117                                  _(name) %(fragment_condition)s
     118                                          OR
     119                                  name %(fragment_condition)s
     120                          ORDER BY list_label
     121                  """
     122                  gmMatchProvider.cMatchProvider_SQL2.__init__(self, queries = [query])
     123  #============================================================
     124 -class cFormTemplate(gmBusinessDBObject.cBusinessDBObject):
     125
     126          _cmd_fetch_payload = u'select * from ref.v_paperwork_templates where pk_paperwork_template = %s'
     127
     128          _cmds_store_payload = [
     129                  u"""update ref.paperwork_templates set
     130                                  name_short = %(name_short)s,
     131                                  name_long = %(name_long)s,
     132                                  fk_template_type = %(pk_template_type)s,
     133                                  instance_type = %(instance_type)s,
     134                                  engine = %(engine)s,
     135                                  in_use = %(in_use)s,
     136                                  filename = %(filename)s,
     137                                  external_version = %(external_version)s
     138                          where
     139                                  pk = %(pk_paperwork_template)s and
     140                                  xmin = %(xmin_paperwork_template)s
     141                  """,
     142                  u"""select xmin_paperwork_template from ref.v_paperwork_templates where pk_paperwork_template = %(pk_paperwork_template)s"""
     143          ]
     144
     145          _updatable_fields = [
     146                  u'name_short',
     147                  u'name_long',
     148                  u'external_version',
     149                  u'pk_template_type',
     150                  u'instance_type',
     151                  u'engine',
     152                  u'in_use',
     153                  u'filename'
     154          ]
     155
     156          _suffix4engine = {
     157                  u'O': u'.ott',
     158                  u'L': u'.tex',
     159                  u'T': u'.txt',
     160                  u'X': u'.xslt',
     161                  u'I': u'.img',
     162                  u'P': u'.pdf'
     163          }
     164
     165          #--------------------------------------------------------
     166 -        def _get_template_data(self):
     167                  """The template itself better not be arbitrarily large unless you can handle that.
     168
     169                  Note that the data type returned will be a buffer."""
     170
     171                  cmd = u'SELECT data FROM ref.paperwork_templates WHERE pk = %(pk)s'
     172                  rows, idx = gmPG2.run_ro_queries (queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}], get_col_idx = False)
     173
     174                  if len(rows) == 0:
     175                          raise gmExceptions.NoSuchBusinessObjectError('cannot retrieve data for template pk = %s' % self.pk_obj)
     176
     177                  return rows[0][0]
     178
     179          template_data = property(_get_template_data, lambda x:x)
     180          #--------------------------------------------------------
     181 -        def export_to_file(self, filename=None, chunksize=0):
     182                  """Export form template from database into file."""
     183
     184                  if filename is None:
     185                          if self._payload[self._idx['filename']] is None:
     186                                  suffix = self.__class__._suffix4engine[self._payload[self._idx['engine']]]
     187                          else:
     188                                  suffix = os.path.splitext(self._payload[self._idx['filename']].strip())[1].strip()
     189                                  if suffix in [u'', u'.']:
     190                                          suffix = self.__class__._suffix4engine[self._payload[self._idx['engine']]]
     191
     192                          filename = gmTools.get_unique_filename (
     193                                  prefix = 'gm-%s-Template-' % self._payload[self._idx['engine']],
     194                                  suffix = suffix
     195                          )
     196
     197                  data_query = {
     198                          'cmd': u'SELECT substring(data from %(start)s for %(size)s) FROM ref.paperwork_templates WHERE pk = %(pk)s',
     199                          'args': {'pk': self.pk_obj}
     200                  }
     201
     202                  data_size_query = {
     203                          'cmd': u'select octet_length(data) from ref.paperwork_templates where pk = %(pk)s',
     204                          'args': {'pk': self.pk_obj}
     205                  }
     206
     207                  result = gmPG2.bytea2file (
     208                          data_query = data_query,
     209                          filename = filename,
     210                          data_size_query = data_size_query,
     211                          chunk_size = chunksize
     212                  )
     213                  if result is False:
     214                          return None
     215
     216                  return filename
     217          #--------------------------------------------------------
     218 -        def update_template_from_file(self, filename=None):
     219                  gmPG2.file2bytea (
     220                          filename = filename,
     221                          query = u'update ref.paperwork_templates set data = %(data)s::bytea where pk = %(pk)s and xmin = %(xmin)s',
     222                          args = {'pk': self.pk_obj, 'xmin': self._payload[self._idx['xmin_paperwork_template']]}
     223                  )
     224                  # adjust for xmin change
     225                  self.refetch_payload()
     226          #--------------------------------------------------------
     227 -        def instantiate(self):
     228                  fname = self.export_to_file()
     229                  engine = form_engines[self._payload[self._idx['engine']]]
     230                  form = engine(template_file = fname)
     231                  form.template = self
     232                  return form
     233  #============================================================
     234 -def get_form_template(name_long=None, external_version=None):
     235          cmd = u'select pk from ref.paperwork_templates where name_long = %(lname)s and external_version = %(ver)s'
     236          args = {'lname': name_long, 'ver': external_version}
     237          rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
     238
     239          if len(rows) == 0:
     240                  _log.error('cannot load form template [%s - %s]', name_long, external_version)
     241                  return None
     242
     243          return cFormTemplate(aPK_obj = rows[0]['pk'])
     244  #------------------------------------------------------------
     245 -def get_form_templates(engine=None, active_only=False, template_types=None, excluded_types=None):
     246          """Load form templates."""
     247
     248          args = {'eng': engine, 'in_use': active_only}
     249          where_parts = [u'1 = 1']
     250
     251          if engine is not None:
     252                  where_parts.append(u'engine = %(eng)s')
     253
     254          if active_only:
     255                  where_parts.append(u'in_use IS true')
     256
     257          if template_types is not None:
     258                  args['incl_types'] = tuple(template_types)
     259                  where_parts.append(u'template_type IN %(incl_types)s')
     260
     261          if excluded_types is not None:
     262                  args['excl_types'] = tuple(excluded_types)
     263                  where_parts.append(u'template_type NOT IN %(excl_types)s')
     264
     265          cmd = u"SELECT * FROM ref.v_paperwork_templates WHERE %s ORDER BY in_use desc, name_long" % u'\nAND '.join(where_parts)
     266
     267          rows, idx = gmPG2.run_ro_queries (
     268                  queries = [{'cmd': cmd, 'args': args}],
     269                  get_col_idx = True
     270          )
     271          templates = [ cFormTemplate(row = {'pk_field': 'pk_paperwork_template', 'data': r, 'idx': idx}) for r in rows ]
     272
     273          return templates
     274  #------------------------------------------------------------
     275 -def create_form_template(template_type=None, name_short=None, name_long=None):
     276
     277          cmd = u'insert into ref.paperwork_templates (fk_template_type, name_short, name_long, external_version) values (%(type)s, %(nshort)s, %(nlong)s, %(ext_version)s)'
     278          rows, idx = gmPG2.run_rw_queries (
     279                  queries = [
     280                          {'cmd': cmd, 'args': {'type': template_type, 'nshort': name_short, 'nlong': name_long, 'ext_version': 'new'}},
     281                          {'cmd': u"select currval(pg_get_serial_sequence('ref.paperwork_templates', 'pk'))"}
     282                  ],
     283                  return_data = True
     284          )
     285          template = cFormTemplate(aPK_obj = rows[0][0])
     286          return template
     287  #------------------------------------------------------------
     288 -def delete_form_template(template=None):
     289          rows, idx = gmPG2.run_rw_queries (
     290                  queries = [
     291                          {'cmd': u'delete from ref.paperwork_templates where pk=%(pk)s', 'args': {'pk': template['pk_paperwork_template']}}
     292                  ]
     293          )
     294          return True
     295  #============================================================
     296  # OpenOffice/LibreOffice API
     297  #============================================================
     298  uno = None
     299  cOOoDocumentCloseListener = None
     300  writer_binary = None
     301
     302  #-----------------------------------------------------------
     303 -def __configure_path_to_UNO():
     304
     305          try:
     306                  which = subprocess.Popen (
     307                          args = ('which', 'soffice'),
     308                          stdout = subprocess.PIPE,
     309                          stdin = subprocess.PIPE,
     310                          stderr = subprocess.PIPE,
     311                          universal_newlines = True
     312                  )
     313          except (OSError, ValueError, subprocess.CalledProcessError):
     314                  _log.exception('there was a problem executing [which soffice]')
     315                  return
     316
     317          soffice_path, err = which.communicate()
     318          soffice_path = soffice_path.strip('\n')
     319          uno_path = os.path.abspath ( os.path.join (
     320                  os.path.dirname(os.path.realpath(soffice_path)),
     321                  '..',
     322                  'basis-link',
     323                  'program'
     324          ))
     325
     326          _log.info('UNO should be at [%s], appending to sys.path', uno_path)
     327
     328          sys.path.append(uno_path)
     329  #-----------------------------------------------------------
     330 -def init_ooo():
     331          """FIXME: consider this:
     332
     333          try:
     334              import uno
     335          except:
     336              print "This Script needs to be run with the python from OpenOffice.org"
     337              print "Example: /opt/OpenOffice.org/program/python %s" % (
     338                  os.path.basename(sys.argv[0]))
     339              print "Or you need to insert the right path at the top, where uno.py is."
     340              print "Default: %s" % default_path
     341          """
     342          global uno
     343          if uno is not None:
     344                  return
     345
     346          try:
     347                  import uno
     348          except ImportError:
     349                  __configure_path_to_UNO()
     350                  import uno
     351
     352          global unohelper, oooXCloseListener, oooNoConnectException, oooPropertyValue
     353
     354          import unohelper
     355          from com.sun.star.util import XCloseListener as oooXCloseListener
     356          from com.sun.star.connection import NoConnectException as oooNoConnectException
     357          from com.sun.star.beans import PropertyValue as oooPropertyValue
     358
     359          #----------------------------------
     360          class _cOOoDocumentCloseListener(unohelper.Base, oooXCloseListener):
     361                  """Listens for events sent by OOo during the document closing
     362                     sequence and notifies the GNUmed client GUI so it can
     363                     import the closed document into the database.
     364                  """
     365                  def __init__(self, document=None):
     366                          self.document = document
     367
     368                  def queryClosing(self, evt, owner):
     369                          # owner is True/False whether I am the owner of the doc
     370                          pass
     371
     372                  def notifyClosing(self, evt):
     373                          pass
     374
     375                  def disposing(self, evt):
     376                          self.document.on_disposed_by_ooo()
     377                          self.document = None
     378          #----------------------------------
     379
     380          global cOOoDocumentCloseListener
     381          cOOoDocumentCloseListener = _cOOoDocumentCloseListener
     382
     383          # search for writer binary
     384          global writer_binary
     385          found, binary = gmShellAPI.find_first_binary(binaries = [
     386                  'lowriter',
     387                  'oowriter'
     388          ])
     389          if found:
     390                  _log.debug('OOo/LO writer binary found: %s', binary)
     391                  writer_binary = binary
     392          else:
     393                  _log.debug('OOo/LO writer binary NOT found')
     394                  raise ImportError('LibreOffice/OpenOffice (lowriter/oowriter) not found')
     395
     396          _log.debug('python UNO bridge successfully initialized')
     397
     398  #------------------------------------------------------------
     399 -class gmOOoConnector(gmBorg.cBorg):
     400          """This class handles the connection to OOo.
     401
     402          Its Singleton instance stays around once initialized.
     403          """
     404          # FIXME: need to detect closure of OOo !
     405 -        def __init__(self):
     406
     407                  init_ooo()
     408
     409                  #self.ooo_start_cmd = 'oowriter -invisible -accept="socket,host=localhost,port=2002;urp;"'
     410                  #self.remote_context_uri = "uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext"
     411
     412                  pipe_name = "uno-gm2lo-%s" % str(random.random())[2:]
     413                  _log.debug('pipe name: %s', pipe_name)
     414
     415                  #self.ooo_start_cmd = '%s -invisible -norestore -accept="pipe,name=%s;urp"' % (
     416                  self.ooo_start_cmd = '%s --norestore --accept="pipe,name=%s;urp" &' % (
     417                          writer_binary,
     418                          pipe_name
     419                  )
     420                  _log.debug('startup command: %s', self.ooo_start_cmd)
     421
     422                  self.remote_context_uri = "uno:pipe,name=%s;urp;StarOffice.ComponentContext" % pipe_name
     423                  _log.debug('remote context URI: %s', self.remote_context_uri)
     424
     425                  self.resolver_uri = "com.sun.star.bridge.UnoUrlResolver"
     426                  self.desktop_uri = "com.sun.star.frame.Desktop"
     427
     428                  self.local_context = uno.getComponentContext()
     429                  self.uri_resolver = self.local_context.ServiceManager.createInstanceWithContext(self.resolver_uri, self.local_context)
     430
     431                  self.__desktop = None
     432          #--------------------------------------------------------
     433 -        def cleanup(self, force=True):
     434                  if self.__desktop is None:
     435                          _log.debug('no desktop, no cleanup')
     436                          return
     437
     438                  try:
     439                          self.__desktop.terminate()
     440                  except:
     441                          _log.exception('cannot terminate OOo desktop')
     442          #--------------------------------------------------------
     443 -        def open_document(self, filename=None):
     444                  """<filename> must be absolute"""
     445
     446                  if self.desktop is None:
     447                          _log.error('cannot access OOo desktop')
     448                          return None
     449
     450                  filename = os.path.expanduser(filename)
     451                  filename = os.path.abspath(filename)
     452                  document_uri = uno.systemPathToFileUrl(filename)
     453
     454                  _log.debug('%s -> %s', filename, document_uri)
     455
     456                  doc = self.desktop.loadComponentFromURL(document_uri, "_blank", 0, ())
     457                  return doc
     458          #--------------------------------------------------------
     459          # internal helpers
     460          #--------------------------------------------------------
     461 -        def __get_startup_settle_time(self):
     462                  # later factor this out !
     463                  dbcfg = gmCfg.cCfgSQL()
     464                  self.ooo_startup_settle_time = dbcfg.get2 (
     465                          option = u'external.ooo.startup_settle_time',
     466                          workplace = gmSurgery.gmCurrentPractice().active_workplace,
     467                          bias = u'workplace',
     468                          default = 3.0
     469                  )
     470          #--------------------------------------------------------
     471          # properties
     472          #--------------------------------------------------------
     473 -        def _get_desktop(self):
     474                  if self.__desktop is not None:
     475                          return self.__desktop
     476
     477                  try:
     478                          self.remote_context = self.uri_resolver.resolve(self.remote_context_uri)
     479                  except oooNoConnectException:
     480                          _log.exception('cannot connect to OOo server')
     481                          _log.info('trying to start OOo server')
     482                          os.system(self.ooo_start_cmd)
     483                          self.__get_startup_settle_time()
     484                          _log.debug('waiting %s seconds for OOo to start up', self.ooo_startup_settle_time)
     485                          time.sleep(self.ooo_startup_settle_time)        # OOo sometimes needs a bit
     486                          try:
     487                                  self.remote_context     = self.uri_resolver.resolve(self.remote_context_uri)
     488                          except oooNoConnectException:
     489                                  _log.exception('cannot start (or connect to started) OOo server')
     490                                  return None
     491
     492                  self.__desktop = self.remote_context.ServiceManager.createInstanceWithContext(self.desktop_uri, self.remote_context)
     493                  _log.debug('connection seems established')
     494                  return self.__desktop
     495
     496          desktop = property(_get_desktop, lambda x:x)
     497  #------------------------------------------------------------
     498 -class cOOoLetter(object):
     499
     500 -        def __init__(self, template_file=None, instance_type=None):
     501
     502                  self.template_file = template_file
     503                  self.instance_type = instance_type
     504                  self.ooo_doc = None
     505          #--------------------------------------------------------
     506          # external API
     507          #--------------------------------------------------------
     508 -        def open_in_ooo(self):
     509                  # connect to OOo
     510                  ooo_srv = gmOOoConnector()
     511
     512                  # open doc in OOo
     513                  self.ooo_doc = ooo_srv.open_document(filename = self.template_file)
     514                  if self.ooo_doc is None:
     515                          _log.error('cannot open document in OOo')
     516                          return False
     517
     518                  # listen for close events
     519                  pat = gmPerson.gmCurrentPatient()
     520                  pat.locked = True
     521                  listener = cOOoDocumentCloseListener(document = self)
     522                  self.ooo_doc.addCloseListener(listener)
     523
     524                  return True
     525          #--------------------------------------------------------
     526 -        def show(self, visible=True):
     527                  self.ooo_doc.CurrentController.Frame.ContainerWindow.setVisible(visible)
     528          #--------------------------------------------------------
     529 -        def replace_placeholders(self, handler=None, old_style_too = True):
     530
     531                  # new style embedded, implicit placeholders
     532                  searcher = self.ooo_doc.createSearchDescriptor()
     533                  searcher.SearchCaseSensitive = False
     534                  searcher.SearchRegularExpression = True
     535                  searcher.SearchWords = True
     536                  searcher.SearchString = handler.placeholder_regex
     537
     538                  placeholder_instance = self.ooo_doc.findFirst(searcher)
     539                  while placeholder_instance is not None:
     540                          try:
     541                                  val = handler[placeholder_instance.String]
     542                          except:
     543                                  val = _('error with placeholder [%s]') % placeholder_instance.String
     544                                  _log.exception(val)
     545
     546                          if val is None:
     547                                  val = _('error with placeholder [%s]') % placeholder_instance.String
     548
     549                          placeholder_instance.String = val
     550                          placeholder_instance = self.ooo_doc.findNext(placeholder_instance.End, searcher)
     551
     552                  if not old_style_too:
     553                          return
     554
     555                  # old style "explicit" placeholders
     556                  text_fields = self.ooo_doc.getTextFields().createEnumeration()
     557                  while text_fields.hasMoreElements():
     558                          text_field = text_fields.nextElement()
     559
     560                          # placeholder ?
     561                          if not text_field.supportsService('com.sun.star.text.TextField.JumpEdit'):
     562                                  continue
     563                          # placeholder of type text ?
     564                          if text_field.PlaceHolderType != 0:
     565                                  continue
     566
     567                          replacement = handler[text_field.PlaceHolder]
     568                          if replacement is None:
     569                                  continue
     570
     571                          text_field.Anchor.setString(replacement)
     572          #--------------------------------------------------------
     573 -        def save_in_ooo(self, filename=None):
     574                  if filename is not None:
     575                          target_url = uno.systemPathToFileUrl(os.path.abspath(os.path.expanduser(filename)))
     576                          save_args = (
     577                                  oooPropertyValue('Overwrite', 0, True, 0),
     578                                  oooPropertyValue('FormatFilter', 0, 'swriter: StarOffice XML (Writer)', 0)
     579
     580                          )
     581                          # "store AS url" stores the doc, marks it unmodified and updates
     582                          # the internal media descriptor - as opposed to "store TO url"
     583                          self.ooo_doc.storeAsURL(target_url, save_args)
     584                  else:
     585                          self.ooo_doc.store()
     586          #--------------------------------------------------------
     587 -        def close_in_ooo(self):
     588                  self.ooo_doc.dispose()
     589                  pat = gmPerson.gmCurrentPatient()
     590                  pat.locked = False
     591                  self.ooo_doc = None
     592          #--------------------------------------------------------
     593 -        def on_disposed_by_ooo(self):
     594                  # get current file name from OOo, user may have used Save As
     595                  filename = uno.fileUrlToSystemPath(self.ooo_doc.URL)
     596                  # tell UI to import the file
     597                  gmDispatcher.send (
     598                          signal = u'import_document_from_file',
     599                          filename = filename,
     600                          document_type = self.instance_type,
     601                          unlock_patient = True
     602                  )
     603                  self.ooo_doc = None
     604          #--------------------------------------------------------
     605          # internal helpers
     606          #--------------------------------------------------------
     607
     608  #============================================================
     609 -class cFormEngine(object):
     610          """Ancestor for forms."""
     611
     612 -        def __init__(self, template_file=None):
     613                  self.template = None
     614                  self.template_filename = template_file
     615          #--------------------------------------------------------
     616 -        def substitute_placeholders(self, data_source=None):
     617                  """Parse the template into an instance and replace placeholders with values."""
     618                  raise NotImplementedError
     619          #--------------------------------------------------------
     620 -        def edit(self):
     621                  """Allow editing the instance of the template."""
     622                  raise NotImplementedError
     623          #--------------------------------------------------------
     624 -        def generate_output(self, format=None):
     625                  """Generate output suitable for further processing outside this class, e.g. printing."""
     626                  raise NotImplementedError
     627          #--------------------------------------------------------
     628 -        def process(self, data_source=None):
     629                  """Merge values into the form template.
     630                  """
     631                  pass
     632          #--------------------------------------------------------
     633 -        def cleanup(self):
     634                  """
     635                  A sop to TeX which can't act as a true filter: to delete temporary files
     636                  """
     637                  pass
     638          #--------------------------------------------------------
     639 -        def exe(self, command):
     640                  """
     641                  Executes the provided command.
     642                  If command cotains %F. it is substituted with the filename
     643                  Otherwise, the file is fed in on stdin
     644                  """
     645                  pass
     646          #--------------------------------------------------------
     647 -        def store(self, params=None):
     648                  """Stores the parameters in the backend.
     649
     650                  - link_obj can be a cursor, a connection or a service name
     651                  - assigning a cursor to link_obj allows the calling code to
     652                    group the call to store() into an enclosing transaction
     653                    (for an example see gmReferral.send_referral()...)
     654                  """
     655                  # some forms may not have values ...
     656                  if params is None:
     657                          params = {}
     658                  patient_clinical = self.patient.get_emr()
     659                  encounter = patient_clinical.active_encounter['pk_encounter']
     660                  # FIXME: get_active_episode is no more
     661                  #episode = patient_clinical.get_active_episode()['pk_episode']
     662                  # generate "forever unique" name
     663                  cmd = "select name_short || ': <' || name_long || '::' || external_version || '>' from paperwork_templates where pk=%s";
     664                  rows = gmPG.run_ro_query('reference', cmd, None, self.pk_def)
     665                  form_name = None
     666                  if rows is None:
     667                          _log.error('error retrieving form def for [%s]' % self.pk_def)
     668                  elif len(rows) == 0:
     669                          _log.error('no form def for [%s]' % self.pk_def)
     670                  else:
     671                          form_name = rows[0][0]
     672                  # we didn't get a name but want to store the form anyhow
     673                  if form_name is None:
     674                          form_name=time.time()   # hopefully unique enough
     675                  # in one transaction
     676                  queries = []
     677                  # - store form instance in form_instance
     678                  cmd = "insert into form_instances(fk_form_def, form_name, fk_episode, fk_encounter) values (%s, %s, %s, %s)"
     679                  queries.append((cmd, [self.pk_def, form_name, episode, encounter]))
     680                  # - store params in form_data
     681                  for key in params.keys():
     682                          cmd = """
     683                                  insert into form_data(fk_instance, place_holder, value)
     684                                  values ((select currval('form_instances_pk_seq')), %s, %s::text)
     685                          """
     686                          queries.append((cmd, [key, params[key]]))
     687                  # - get inserted PK
     688                  queries.append(("select currval ('form_instances_pk_seq')", []))
     689                  status, err = gmPG.run_commit('historica', queries, True)
     690                  if status is None:
     691                          _log.error('failed to store form [%s] (%s): %s' % (self.pk_def, form_name, err))
     692                          return None
     693                  return status
     694
     695  #================================================================
     696  # OOo template forms
     697  #----------------------------------------------------------------
     698 -class cOOoForm(cFormEngine):
     699          """A forms engine wrapping OOo."""
     700
     701 -        def __init__(self, template_file=None):
     702                  super(self.__class__, self).__init__(template_file = template_file)
     703
     704                  path, ext = os.path.splitext(self.template_filename)
     705                  if ext in [r'', r'.']:
     706                          ext = r'.odt'
     707                  self.instance_filename = r'%s-instance%s' % (path, ext)
     708
     709  #================================================================
     710  # AbiWord template forms
     711  #----------------------------------------------------------------
     712 -class cAbiWordForm(cFormEngine):
     713          """A forms engine wrapping AbiWord."""
     714
     715          placeholder_regex = r'\$&lt;.+?&gt;\$'
     716
     717 -        def __init__(self, template_file=None):
     718
     719                  super(cAbiWordForm, self).__init__(template_file = template_file)
     720
     721                  # detect abiword
     722                  found, self.abiword_binary = gmShellAPI.detect_external_binary(binary = r'abiword')
     723                  if not found:
     724                          raise ImportError('<abiword(.exe)> not found')
     725          #--------------------------------------------------------
     726 -        def substitute_placeholders(self, data_source=None):
     727                  # should *actually* properly parse the XML
     728
     729                  path, ext = os.path.splitext(self.template_filename)
     730                  if ext in [r'', r'.']:
     731                          ext = r'.abw'
     732                  self.instance_filename = r'%s-instance%s' % (path, ext)
     733
     734                  template_file = codecs.open(self.template_filename, 'rU', 'utf8')
     735                  instance_file = codecs.open(self.instance_filename, 'wb', 'utf8')
     736
     737                  if self.template is not None:
     738                          # inject placeholder values
     739                          data_source.set_placeholder(u'form_name_long', self.template['name_long'])
     740                          data_source.set_placeholder(u'form_name_short', self.template['name_short'])
     741                          data_source.set_placeholder(u'form_version', self.template['external_version'])
     742
     743                  for line in template_file:
     744
     745                          if line.strip() in [u'', u'\r', u'\n', u'\r\n']:
     746                                  instance_file.write(line)
     747                                  continue
     748
     749                          # 1) find placeholders in this line
     750                          placeholders_in_line = regex.findall(cAbiWordForm.placeholder_regex, line, regex.IGNORECASE)
     751                          # 2) and replace them
     752                          for placeholder in placeholders_in_line:
     753                                  try:
     754                                          val = data_source[placeholder.replace(u'&lt;', u'<').replace(u'&gt;', u'>')]
     755                                  except:
     756                                          val = _('error with placeholder [%s]') % gmTools.xml_escape_string(placeholder)
     757                                          _log.exception(val)
     758
     759                                  if val is None:
     760                                          val = _('error with placeholder [%s]') % gmTools.xml_escape_string(placeholder)
     761
     762                                  line = line.replace(placeholder, val)
     763
     764                          instance_file.write(line)
     765
     766                  instance_file.close()
     767                  template_file.close()
     768
     769                  if self.template is not None:
     770                          # remove temporary placeholders
     771                          data_source.unset_placeholder(u'form_name_long')
     772                          data_source.unset_placeholder(u'form_name_short')
     773                          data_source.unset_placeholder(u'form_version')
     774
     775                  return
     776          #--------------------------------------------------------
     777 -        def edit(self):
     778                  enc = sys.getfilesystemencoding()
     779                  cmd = (r'%s %s' % (self.abiword_binary, self.instance_filename.encode(enc))).encode(enc)
     780                  result = gmShellAPI.run_command_in_shell(command = cmd, blocking = True)
     781                  self.re_editable_filenames = []
     782                  return result
     783          #--------------------------------------------------------
     784 -        def generate_output(self, instance_file=None, format=None):
     785                  self.final_output_filenames = [self.instance_filename]
     786                  return self.instance_filename
     787  #----------------------------------------------------------------
     788  form_engines[u'A'] = cAbiWordForm
     789
     790  #================================================================
     791  # LaTeX template forms
     792  #----------------------------------------------------------------
     793 -class cLaTeXForm(cFormEngine):
     794          """A forms engine wrapping LaTeX."""
     795
     796 -        def __init__(self, template_file=None):
     797                  super(self.__class__, self).__init__(template_file = template_file)
     798                  path, ext = os.path.splitext(self.template_filename)
     799                  if ext in [r'', r'.']:
     800                          ext = r'.tex'
     801                  self.instance_filename = r'%s-instance%s' % (path, ext)
     802          #--------------------------------------------------------
     803 -        def substitute_placeholders(self, data_source=None):
     804
     805                  template_file = codecs.open(self.template_filename, 'rU', 'utf8')
     806                  instance_file = codecs.open(self.instance_filename, 'wb', 'utf8')
     807
     808                  if self.template is not None:
     809                          # inject placeholder values
     810                          data_source.set_placeholder(u'form_name_long', self.template['name_long'])
     811                          data_source.set_placeholder(u'form_name_short', self.template['name_short'])
     812                          data_source.set_placeholder(u'form_version', self.template['external_version'])
     813
     814                  for line in template_file:
     815
     816                          if line.strip() in [u'', u'\r', u'\n', u'\r\n']:
     817                                  instance_file.write(line)
     818                                  continue
     819
     820                          # 1) find placeholders in this line
     821                          placeholders_in_line = regex.findall(data_source.placeholder_regex, line, regex.IGNORECASE)
     822                          # 2) and replace them
     823                          for placeholder in placeholders_in_line:
     824                                  try:
     825                                          val = data_source[placeholder]
     826                                  except:
     827                                          val = _('error with placeholder [%s]') % gmTools.tex_escape_string(placeholder)
     828                                          _log.exception(val)
     829
     830                                  if val is None:
     831                                          val = _('error with placeholder [%s]') % gmTools.tex_escape_string(placeholder)
     832
     833                                  line = line.replace(placeholder, val)
     834
     835                          instance_file.write(line)
     836
     837                  instance_file.close()
     838                  template_file.close()
     839
     840                  if self.template is not None:
     841                          # remove temporary placeholders
     842                          data_source.unset_placeholder(u'form_name_long')
     843                          data_source.unset_placeholder(u'form_name_short')
     844                          data_source.unset_placeholder(u'form_version')
     845
     846                  return
     847          #--------------------------------------------------------
     848 -        def edit(self):
     849
     850                  mimetypes = [
     851                          u'application/x-latex',
     852                          u'application/x-tex',
     853                          u'text/plain'
     854                  ]
     855
     856                  for mimetype in mimetypes:
     857                          editor_cmd = gmMimeLib.get_editor_cmd(mimetype, self.instance_filename)
     858                          if editor_cmd is not None:
     859                                  break
     860
     861                  if editor_cmd is None:
     862                          # LaTeX code is text: also consider text *viewers*
     863                          # since pretty much any of them will be an editor as well
     864                          for mimetype in mimetypes:
     865                                  editor_cmd = gmMimeLib.get_viewer_cmd(mimetype, self.instance_filename)
     866                                  if editor_cmd is not None:
     867                                          break
     868
     869                  # last resort
     870                  if editor_cmd is None:
     871                          if os.name == 'nt':
     872                                  editor_cmd = u'notepad.exe %s' % self.instance_filename
     873                          else:
     874                                  editor_cmd = u'sensible-editor %s' % self.instance_filename
     875
     876                  result = gmShellAPI.run_command_in_shell(command = editor_cmd, blocking = True)
     877                  self.re_editable_filenames = [self.instance_filename]
     878
     879                  return result
     880          #--------------------------------------------------------
     881 -        def generate_output(self, instance_file=None, format=None):
     882
     883                  if instance_file is None:
     884                          instance_file = self.instance_filename
     885
     886                  try:
     887                          open(instance_file, 'r').close()
     888                  except:
     889                          _log.exception('cannot access form instance file [%s]', instance_file)
     890                          gmLog2.log_stack_trace()
     891                          return None
     892
     893                  self.instance_filename = instance_file
     894
     895                  _log.debug('ignoring <format> directive [%s], generating PDF', format)
     896
     897                  # create sandbox for LaTeX to play in
     898                  sandbox_dir = os.path.splitext(self.template_filename)[0]
     899                  _log.debug('LaTeX sandbox directory: [%s]', sandbox_dir)
     900
     901                  old_cwd = os.getcwd()
     902                  _log.debug('CWD: [%s]', old_cwd)
     903
     904                  gmTools.mkdir(sandbox_dir)
     905
     906                  os.chdir(sandbox_dir)
     907                  try:
     908                          sandboxed_instance_filename = os.path.join(sandbox_dir, os.path.split(self.instance_filename)[1])
     909                          shutil.move(self.instance_filename, sandboxed_instance_filename)
     910
     911                          # LaTeX can need up to three runs to get cross references et al right
     912                          if platform.system() == 'Windows':
     913                                  draft_cmd = r'pdflatex.exe -draftmode -interaction nonstopmode %s' % sandboxed_instance_filename
     914                                  final_cmd = r'pdflatex.exe -interaction nonstopmode %s' % sandboxed_instance_filename
     915                          else:
     916                                  draft_cmd = r'pdflatex -draftmode -interaction nonstopmode %s' % sandboxed_instance_filename
     917                                  final_cmd = r'pdflatex -interaction nonstopmode %s' % sandboxed_instance_filename
     918                          for run_cmd in [draft_cmd, draft_cmd, final_cmd]:
     919                                  if not gmShellAPI.run_command_in_shell(command = run_cmd, blocking = True, acceptable_return_codes = [0, 1]):
     920                                          _log.error('problem running pdflatex, cannot generate form output')
     921                                          gmDispatcher.send(signal = 'statustext', msg = _('Error running pdflatex. Cannot turn LaTeX template into PDF.'), beep = True)
     922                                          os.chdir(old_cwd)
     923                                          return None
     924                  finally:
     925                          os.chdir(old_cwd)
     926
     927                  sandboxed_pdf_name = u'%s.pdf' % os.path.splitext(sandboxed_instance_filename)[0]
     928                  target_dir = os.path.split(self.instance_filename)[0]
     929                  try:
     930                          shutil.move(sandboxed_pdf_name, target_dir)
     931                  except IOError:
     932                          _log.exception('cannot move sandboxed PDF: %s -> %s', sandboxed_pdf_name, target_dir)
     933                          gmDispatcher.send(signal = 'statustext', msg = _('Sandboxed PDF output file cannot be moved.'), beep = True)
     934                          return None
     935
     936                  final_pdf_name = u'%s.pdf' % os.path.splitext(self.instance_filename)[0]
     937
     938                  try:
     939                          open(final_pdf_name, 'r').close()
     940                  except IOError:
     941                          _log.exception('cannot open target PDF: %s', final_pdf_name)
     942                          gmDispatcher.send(signal = 'statustext', msg = _('PDF output file cannot be opened.'), beep = True)
     943                          return None
     944
     945                  self.final_output_filenames = [final_pdf_name]
     946
     947                  return final_pdf_name
     948  #------------------------------------------------------------
     949  form_engines[u'L'] = cLaTeXForm
     950  #============================================================
     951  # Gnuplot template forms
     952  #------------------------------------------------------------
     953 -class cGnuplotForm(cFormEngine):
     954          """A forms engine wrapping Gnuplot."""
     955
     956          #--------------------------------------------------------
     957 -        def substitute_placeholders(self, data_source=None):
     958                  """Parse the template into an instance and replace placeholders with values."""
     959                  pass
     960          #--------------------------------------------------------
     961 -        def edit(self):
     962                  """Allow editing the instance of the template."""
     963                  self.re_editable_filenames = []
     964                  return True
     965          #--------------------------------------------------------
     966 -        def generate_output(self, format=None):
     967                  """Generate output suitable for further processing outside this class, e.g. printing.
     968
     969                  Expects .data_filename to be set.
     970                  """
     971                  self.conf_filename = gmTools.get_unique_filename(prefix = 'gm2gpl-', suffix = '.conf')
     972                  fname_file = codecs.open(self.conf_filename, 'wb', 'utf8')
     973                  fname_file.write('# setting the gnuplot data file\n')
     974                  fname_file.write("gm2gpl_datafile = '%s'\n" % self.data_filename)
     975                  fname_file.close()
     976
     977                  # FIXME: cater for configurable path
     978                  if platform.system() == 'Windows':
     979                          exec_name = 'gnuplot.exe'
     980                  else:
     981                          exec_name = 'gnuplot'
     982
     983                  args = [exec_name, '-p', self.conf_filename, self.template_filename]
     984                  _log.debug('plotting args: %s' % str(args))
     985
     986                  try:
     987                          gp = subprocess.Popen (
     988                                  args = args,
     989                                  close_fds = True
     990                          )
     991                  except (OSError, ValueError, subprocess.CalledProcessError):
     992                          _log.exception('there was a problem executing gnuplot')
     993                          gmDispatcher.send(signal = u'statustext', msg = _('Error running gnuplot. Cannot plot data.'), beep = True)
     994                          return
     995
     996                  gp.communicate()
     997
     998                  self.final_output_filenames = [
     999                          self.conf_filename,
    1000                          self.data_filename,
    1001                          self.template_filename
    1002                  ]
    1003
    1004                  return
    1005  #------------------------------------------------------------
    1006  form_engines[u'G'] = cGnuplotForm
    1007
    1008  #============================================================
    1009  # fPDF form engine
    1010  #------------------------------------------------------------
    1011 -class cPDFForm(cFormEngine):
    1012          """A forms engine wrapping PDF forms.
    1013
    1014          Johann Felix Soden <johfel@gmx.de> helped with this.
    1015
    1016          http://partners.adobe.com/public/developer/en/pdf/PDFReference16.pdf
    1017
    1018          http://wwwimages.adobe.com/www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/fdf_data_exchange.pdf
    1019          """
    1020
    1021 -        def __init__(self, template_file=None):
    1022
    1023                  super(cPDFForm, self).__init__(template_file = template_file)
    1024
    1025                  # detect pdftk
    1026                  found, self.pdftk_binary = gmShellAPI.detect_external_binary(binary = r'pdftk')
    1027                  if not found:
    1028                          raise ImportError('<pdftk(.exe)> not found')
    1029                          return          # should be superfluous, actually
    1030
    1031                  enc = sys.getfilesystemencoding()
    1032                  self.pdftk_binary = self.pdftk_binary.encode(enc)
    1033
    1034                  base_name, ext = os.path.splitext(self.template_filename)
    1035                  self.fdf_dumped_filename = (u'%s.fdf' % base_name).encode(enc)
    1036                  self.fdf_replaced_filename = (u'%s-replaced.fdf' % base_name).encode(enc)
    1037                  self.pdf_filled_filename = (u'%s-filled.pdf' % base_name).encode(enc)
    1038                  self.pdf_flattened_filename = (u'%s-filled-flattened.pdf' % base_name).encode(enc)
    1039          #--------------------------------------------------------
    1040 -        def substitute_placeholders(self, data_source=None):
    1041
    1042                  # dump form fields from template
    1043                  cmd_line = [
    1044                          self.pdftk_binary,
    1045                          self.template_filename,
    1046                          r'generate_fdf',
    1047                          r'output',
    1048                          self.fdf_dumped_filename
    1049                  ]
    1050                  _log.debug(u' '.join(cmd_line))
    1051                  try:
    1052                          pdftk = subprocess.Popen(cmd_line)
    1053                  except OSError:
    1054                          _log.exception('cannot run <pdftk> (dump data from form)')
    1055                          gmDispatcher.send(signal = u'statustext', msg = _('Error running pdftk. Cannot extract fields from PDF form template.'), beep = True)
    1056                          return False
    1057
    1058                  pdftk.communicate()
    1059                  if pdftk.returncode != 0:
    1060                          _log.error('<pdftk> returned [%s], failed to dump data from PDF form into FDF', pdftk.returncode)
    1061                          return False
    1062
    1063                  # parse dumped FDF file for "/V (...)" records
    1064                  # and replace placeholders therein
    1065                  fdf_dumped_file = open(self.fdf_dumped_filename, 'rbU')
    1066                  fdf_replaced_file = codecs.open(self.fdf_replaced_filename, 'wb')
    1067
    1068                  string_value_regex = r'\s*/V\s*\(.+\)\s*$'
    1069                  for line in fdf_dumped_file:
    1070                          if not regex.match(string_value_regex, line):
    1071                                  fdf_replaced_file.write(line)
    1072                                  continue
    1073
    1074                          # strip cruft around the string value
    1075                          raw_str_val = line.strip()                                                      # remove framing whitespace
    1076                          raw_str_val = raw_str_val[2:]                                           # remove leading "/V"
    1077                          raw_str_val = raw_str_val.lstrip()                                      # remove whitespace between "/V" and "("
    1078                          raw_str_val = raw_str_val[1:]                                           # remove opening "("
    1079                          raw_str_val = raw_str_val[2:]                                           # remove BOM-16-BE
    1080                          raw_str_val = raw_str_val.rstrip()                                      # remove trailing whitespace
    1081                          raw_str_val = raw_str_val[:-1]                                          # remove closing ")"
    1082
    1083                          # work on FDF escapes
    1084                          raw_str_val = raw_str_val.replace('\(', '(')            # remove escaping of "("
    1085                          raw_str_val = raw_str_val.replace('\)', ')')            # remove escaping of ")"
    1086
    1087                          # by now raw_str_val should contain the actual
    1088                          # string value, albeit encoded as UTF-16, so
    1089                          # decode it into a unicode object,
    1090                          # split multi-line fields on "\n" literal
    1091                          raw_str_lines = raw_str_val.split('\x00\\n')
    1092                          value_template_lines = []
    1093                          for raw_str_line in raw_str_lines:
    1094                                  value_template_lines.append(raw_str_line.decode('utf_16_be'))
    1095
    1096                          replaced_lines = []
    1097                          for value_template in value_template_lines:
    1098                                  # find any placeholders within
    1099                                  placeholders_in_value = regex.findall(data_source.placeholder_regex, value_template, regex.IGNORECASE)
    1100                                  for placeholder in placeholders_in_value:
    1101                                          try:
    1102                                                  replacement = data_source[placeholder]
    1103                                          except:
    1104                                                  _log.exception(replacement)
    1105                                                  replacement = _('error with placeholder [%s]') % placeholder
    1106                                          if replacement is None:
    1107                                                  replacement = _('error with placeholder [%s]') % placeholder
    1108                                          value_template = value_template.replace(placeholder, replacement)
    1109
    1110                                  value_template = value_template.encode('utf_16_be')
    1111
    1112                                  if len(placeholders_in_value) > 0:
    1113                                          value_template = value_template.replace(r'(', r'\(')
    1114                                          value_template = value_template.replace(r')', r'\)')
    1115
    1116                                  replaced_lines.append(value_template)
    1117
    1118                          replaced_line = '\x00\\n'.join(replaced_lines)
    1119
    1120                          fdf_replaced_file.write('/V (')
    1121                          fdf_replaced_file.write(codecs.BOM_UTF16_BE)
    1122                          fdf_replaced_file.write(replaced_line)
    1123                          fdf_replaced_file.write(')\n')
    1124
    1125                  fdf_replaced_file.close()
    1126                  fdf_dumped_file.close()
    1127
    1128                  # merge replaced data back into form
    1129                  cmd_line = [
    1130                          self.pdftk_binary,
    1131                          self.template_filename,
    1132                          r'fill_form',
    1133                          self.fdf_replaced_filename,
    1134                          r'output',
    1135                          self.pdf_filled_filename
    1136                  ]
    1137                  _log.debug(u' '.join(cmd_line))
    1138                  try:
    1139                          pdftk = subprocess.Popen(cmd_line)
    1140                  except OSError:
    1141                          _log.exception('cannot run <pdftk> (merge data into form)')
    1142                          gmDispatcher.send(signal = u'statustext', msg = _('Error running pdftk. Cannot fill in PDF form template.'), beep = True)
    1143                          return False
    1144
    1145                  pdftk.communicate()
    1146                  if pdftk.returncode != 0:
    1147                          _log.error('<pdftk> returned [%s], failed to merge FDF data into PDF form', pdftk.returncode)
    1148                          return False
    1149
    1150                  return True
    1151          #--------------------------------------------------------
    1152 -        def edit(self):
    1153                  mimetypes = [
    1154                          u'application/pdf',
    1155                          u'application/x-pdf'
    1156                  ]
    1157
    1158                  for mimetype in mimetypes:
    1159                          editor_cmd = gmMimeLib.get_editor_cmd(mimetype, self.pdf_filled_filename)
    1160                          if editor_cmd is not None:
    1161                                  break
    1162
    1163                  if editor_cmd is None:
    1164                          _log.debug('editor cmd not found, trying viewer cmd')
    1165                          for mimetype in mimetypes:
    1166                                  editor_cmd = gmMimeLib.get_viewer_cmd(mimetype, self.pdf_filled_filename)
    1167                                  if editor_cmd is not None:
    1168                                          break
    1169
    1170                  if editor_cmd is None:
    1171                          return False
    1172
    1173                  result = gmShellAPI.run_command_in_shell(command = editor_cmd, blocking = True)
    1174
    1175                  path, fname = os.path.split(self.pdf_filled_filename)
    1176                  candidate = os.path.join(gmTools.gmPaths().home_dir, fname)
    1177
    1178                  if os.access(candidate, os.R_OK):
    1179                          _log.debug('filled-in PDF found: %s', candidate)
    1180                          os.rename(self.pdf_filled_filename, self.pdf_filled_filename + '.bak')
    1181                          shutil.move(candidate, path)
    1182                  else:
    1183                          _log.debug('filled-in PDF not found: %s', candidate)
    1184
    1185                  self.re_editable_filenames = [self.pdf_filled_filename]
    1186
    1187                  return result
    1188          #--------------------------------------------------------
    1189 -        def generate_output(self, format=None):
    1190                  """Generate output suitable for further processing outside this class, e.g. printing."""
    1191
    1192                  # eventually flatten the filled in form so we
    1193                  # can keep  both a flattened and an editable copy:
    1194                  cmd_line = [
    1195                          self.pdftk_binary,
    1196                          self.pdf_filled_filename,
    1197                          r'output',
    1198                          self.pdf_flattened_filename,
    1199                          r'flatten'
    1200                  ]
    1201                  _log.debug(u' '.join(cmd_line))
    1202                  try:
    1203                          pdftk = subprocess.Popen(cmd_line)
    1204                  except OSError:
    1205                          _log.exception('cannot run <pdftk> (flatten filled in form)')
    1206                          gmDispatcher.send(signal = u'statustext', msg = _('Error running pdftk. Cannot flatten filled in PDF form.'), beep = True)
    1207                          return None
    1208
    1209                  pdftk.communicate()
    1210                  if pdftk.returncode != 0:
    1211                          _log.error('<pdftk> returned [%s], failed to flatten filled in PDF form', pdftk.returncode)
    1212                          return None
    1213
    1214                  self.final_output_filenames = [self.pdf_flattened_filename]
    1215
    1216                  return self.pdf_flattened_filename
    1217  #------------------------------------------------------------
    1218  form_engines[u'P'] = cPDFForm
    1219
    1220  #============================================================
    1221  # older code
    1222  #------------------------------------------------------------
    1223 -class cIanLaTeXForm(cFormEngine):
    1224          """A forms engine wrapping LaTeX.
    1225          """
    1226 -        def __init__(self, id, template):
    1227                  self.id = id
    1228                  self.template = template
    1229
    1230 -        def process (self,params={}):
    1231                  try:
    1232                          latex = Cheetah.Template.Template (self.template, filter=LaTeXFilter, searchList=[params])
    1233                          # create a 'sandbox' directory for LaTeX to play in
    1234                          self.tmp = tempfile.mktemp ()
    1235                          os.makedirs (self.tmp)
    1236                          self.oldcwd = os.getcwd ()
    1237                          os.chdir (self.tmp)
    1238                          stdin = os.popen ("latex", "w", 2048)
    1239                          stdin.write (str (latex)) #send text. LaTeX spits it's output into stdout
    1240                          # FIXME: send LaTeX output to the logger
    1241                          stdin.close ()
    1242                          if not gmShellAPI.run_command_in_shell("dvips texput.dvi -o texput.ps", blocking=True):
    1243                                  raise FormError ('DVIPS returned error')
    1244                  except EnvironmentError, e:
    1245                          _log.error(e.strerror)
    1246                          raise FormError (e.strerror)
    1247                  return file ("texput.ps")
    1248
    1249 -        def xdvi (self):
    1250                  """
    1251                  For testing purposes, runs Xdvi on the intermediate TeX output
    1252                  WARNING: don't try this on Windows
    1253                  """
    1254                  gmShellAPI.run_command_in_shell("xdvi texput.dvi", blocking=True)
    1255
    1256 -        def exe (self, command):
    1257                  if "%F" in command:
    1258                          command.replace ("%F", "texput.ps")
    1259                  else:
    1260                          command  = "%s < texput.ps" % command
    1261                  try:
    1262                          if not gmShellAPI.run_command_in_shell(command, blocking=True):
    1263                                  _log.error("external command %s returned non-zero" % command)
    1264                                  raise FormError ('external command %s returned error' % command)
    1265                  except EnvironmentError, e:
    1266                          _log.error(e.strerror)
    1267                          raise FormError (e.strerror)
    1268                  return True
    1269
    1270 -        def printout (self):
    1271                  command, set1 = gmCfg.getDBParam (workplace = self.workplace, option = 'main.comms.print')
    1272                  self.exe (command)
    1273
    1274 -        def cleanup (self):
    1275                  """
    1276                  Delete all the LaTeX output iles
    1277                  """
    1278                  for i in os.listdir ('.'):
    1279                          os.unlink (i)
    1280                  os.chdir (self.oldcwd)
    1281                  os.rmdir (self.tmp)
    1282
    1283
    1284
    1285
    1286  #================================================================
    1287  # define a class for HTML forms (for printing)
    1288  #================================================================
    1289 -class cXSLTFormEngine(cFormEngine):
    1290          """This class can create XML document from requested data,
    1291          then process it with XSLT template and display results
    1292          """
    1293
    1294          # FIXME: make the path configurable ?
    1295          _preview_program = u'oowriter ' #this program must be in the system PATH
    1296
    1297 -        def __init__(self, template=None):
    1298
    1299                  if template is None:
    1300                          raise ValueError(u'%s: cannot create form instance without a template' % __name__)
    1301
    1302                  cFormEngine.__init__(self, template = template)
    1303
    1304                  self._FormData = None
    1305
    1306                  # here we know/can assume that the template was stored as a utf-8
    1307                  # encoded string so use that conversion to create unicode:
    1308                  #self._XSLTData = unicode(str(template.template_data), 'UTF-8')
    1309                  # but in fact, unicode() knows how to handle buffers, so simply:
    1310                  self._XSLTData = unicode(self.template.template_data, 'UTF-8', 'strict')
    1311
    1312                  # we must still devise a method of extracting the SQL query:
    1313                  # - either by retrieving it from a particular tag in the XSLT or
    1314                  # - by making the stored template actually be a dict which, unpickled,
    1315                  #       has the keys "xslt" and "sql"
    1316                  self._SQL_query = u'select 1'                   #this sql query must output valid xml
    1317          #--------------------------------------------------------
    1318          # external API
    1319          #--------------------------------------------------------
    1320 -        def process(self, sql_parameters):
    1321                  """get data from backend and process it with XSLT template to produce readable output"""
    1322
    1323                  # extract SQL (this is wrong but displays what is intended)
    1324                  xslt = libxml2.parseDoc(self._XSLTData)
    1325                  root = xslt.children
    1326                  for child in root:
    1327                          if child.type == 'element':
    1328                                  self._SQL_query = child.content
    1329                                  break
    1330
    1331                  # retrieve data from backend
    1332                  rows, idx  = gmPG2.run_ro_queries(queries = [{'cmd': self._SQL_query, 'args': sql_parameters}], get_col_idx = False)
    1333
    1334                  __header = '<?xml version="1.0" encoding="UTF-8"?>\n'
    1335                  __body = rows[0][0]
    1336
    1337                  # process XML data according to supplied XSLT, producing HTML
    1338                  self._XMLData =__header + __body
    1339                  style = libxslt.parseStylesheetDoc(xslt)
    1340                  xml = libxml2.parseDoc(self._XMLData)
    1341                  html = style.applyStylesheet(xml, None)
    1342                  self._FormData = html.serialize()
    1343
    1344                  style.freeStylesheet()
    1345                  xml.freeDoc()
    1346                  html.freeDoc()
    1347          #--------------------------------------------------------
    1348 -        def preview(self):
    1349                  if self._FormData is None:
    1350                          raise ValueError, u'Preview request for empty form. Make sure the form is properly initialized and process() was performed'
    1351
    1352                  fname = gmTools.get_unique_filename(prefix = u'gm_XSLT_form-', suffix = u'.html')
    1353                  #html_file = os.open(fname, 'wb')
    1354                  #html_file.write(self._FormData.encode('UTF-8'))
    1355                  html_file = codecs.open(fname, 'wb', 'utf8', 'strict')          # or 'replace' ?
    1356                  html_file.write(self._FormData)
    1357                  html_file.close()
    1358
    1359                  cmd = u'%s %s' % (self.__class__._preview_program, fname)
    1360
    1361                  if not gmShellAPI.run_command_in_shell(command = cmd, blocking = False):
    1362                          _log.error('%s: cannot launch report preview program' % __name__)
    1363                          return False
    1364
    1365                  #os.unlink(self.filename) #delete file
    1366                  #FIXME: under Windows the temp file is deleted before preview program gets it (under Linux it works OK)
    1367
    1368                  return True
    1369          #--------------------------------------------------------
    1370 -        def print_directly(self):
    1371                  #not so fast, look at it first
    1372                  self.preview()
    1373
    1374
    1375  #=====================================================
    1376  #class LaTeXFilter(Cheetah.Filters.Filter):
    1377 -class LaTeXFilter:
    1378 -        def filter (self, item, table_sep= " \\\\\n", **kwds):
    1379                  """
    1380                  Convience function to escape ISO-Latin-1 strings for TeX output
    1381                  WARNING: not all ISO-Latin-1 characters are expressible in TeX
    1382                  FIXME: nevertheless, there are a few more we could support
    1383
    1384                  Also intelligently convert lists and tuples into TeX-style table lines
    1385                  """
    1386                  if type (item) is types.UnicodeType or type (item) is types.StringType:
    1387                          item = item.replace ("\\", "\\backslash") # I wonder about this, do we want users to be able to use raw TeX?
    1388                          item = item.replace ("&", "\\&")
    1389                          item = item.replace ("$", "\\$")
    1390                          item = item.replace ('"', "") # okay, that's not right, but easiest solution for now
    1391                          item = item.replace ("\n", "\\\\ ")
    1392                          if len (item.strip ()) == 0:
    1393                                  item = "\\relax " # sometimes TeX really hates empty strings, this seems to mollify it
    1394                          # FIXME: cover all of ISO-Latin-1 which can be expressed in TeX
    1395                          if type (item) is types.UnicodeType:
    1396                                  item = item.encode ('latin-1', 'replace')
    1397                                  trans = {'ß':'\\ss{}', 'ä': '\\"{a}', 'Ä' :'\\"{A}', 'ö': '\\"{o}', 'Ö': '\\"{O}',      'ü': '\\"{u}', 'Ü': '\\"{U}',
    1398                                           '\x8a':'\\v{S}', '\x8a':'\\OE{}', '\x9a':'\\v{s}', '\x9c': '\\oe{}', '\a9f':'\\"{Y}', #Microsloth extensions
    1399                                           '\x86': '{\\dag}', '\x87': '{\\ddag}', '\xa7':'{\\S}', '\xb6': '{\\P}', '\xa9': '{\\copyright}', '\xbf': '?`',
    1400                                           '\xc0':'\\`{A}', '\xa1': "\\'{A}", '\xa2': '\\^{A}', '\xa3':'\\~{A}', '\\xc5': '{\AA}',
    1401                                           '\xc7':'\\c{C}', '\xc8':'\\`{E}',
    1402                                           '\xa1': '!`',
    1403                                   '\xb5':'$\mu$', '\xa3': '\pounds{}', '\xa2':'cent'}
    1404                                  for k, i in trans.items ():
    1405                                          item = item.replace (k, i)
    1406                  elif type (item) is types.ListType or type (item) is types.TupleType:
    1407                          item = string.join ([self.filter (i, ' & ') for i in item], table_sep)
    1408                  elif item is None:
    1409                          item = '\\relax % Python None\n'
    1410                  elif type (item) is types.IntType or type (item) is types.FloatType:
    1411                          item = str (item)
    1412                  else:
    1413                          item = str (item)
    1414                          _log.warning("unknown type %s, string %s" % (type (item), item))
    1415                  return item
    1416
    1417
    1418  #===========================================================
    1419 -class cHL7Form (cFormEngine):
    1420          pass
    1421
    1422  #============================================================
    1423  # convenience functions
    1424  #------------------------------------------------------------
    1425 -def get_form(id):
    1426          """
    1427          Instantiates a FormEngine based on the form ID or name from the backend
    1428          """
    1429          try:
    1430                  # it's a number: match to form ID
    1431                  id = int (id)
    1432                  cmd = 'select template, engine, pk from paperwork_templates where pk = %s'
    1433          except ValueError:
    1434                  # it's a string, match to the form's name
    1435                  # FIXME: can we somehow OR like this: where name_short=%s OR name_long=%s ?
    1436                  cmd = 'select template, engine, flags, pk from paperwork_templates where name_short = %s'
    1437          result = gmPG.run_ro_query ('reference', cmd, None, id)
    1438          if result is None:
    1439                  _log.error('error getting form [%s]' % id)
    1440                  raise gmExceptions.FormError ('error getting form [%s]' % id)
    1441          if len(result) == 0:
    1442                  _log.error('no form [%s] found' % id)
    1443                  raise gmExceptions.FormError ('no such form found [%s]' % id)
    1444          if result[0][1] == 'L':
    1445                  return LaTeXForm (result[0][2], result[0][0])
    1446          elif result[0][1] == 'T':
    1447                  return TextForm (result[0][2], result[0][0])
    1448          else:
    1449                  _log.error('no form engine [%s] for form [%s]' % (result[0][1], id))
    1450                  raise FormError ('no engine [%s] for form [%s]' % (result[0][1], id))
    1451  #-------------------------------------------------------------
    1452 -class FormError (Exception):
    1453 -        def __init__ (self, value):
    1454                  self.value = value
    1455
    1456 -        def __str__ (self):
    1457                  return repr (self.value)
    1458  #-------------------------------------------------------------
    1459
    1460  test_letter = """
    1461  \\documentclass{letter}
    1462  \\address{ $DOCTOR \\\\
    1463  $DOCTORADDRESS}
    1464  \\signature{$DOCTOR}
    1465
    1466  \\begin{document}
    1467  \\begin{letter}{$RECIPIENTNAME \\\\
    1468  $RECIPIENTADDRESS}
    1469
    1470  \\opening{Dear $RECIPIENTNAME}
    1471
    1472  \\textbf{Re:} $PATIENTNAME, DOB: $DOB, $PATIENTADDRESS \\\\
    1473
    1474  $TEXT
    1475
    1476  \\ifnum$INCLUDEMEDS>0
    1477  \\textbf{Medications List}
    1478
    1479  \\begin{tabular}{lll}
    1480  $MEDSLIST
    1481  \\end{tabular}
    1482  \\fi
    1483
    1484  \\ifnum$INCLUDEDISEASES>0
    1485  \\textbf{Disease List}
    1486
    1487  \\begin{tabular}{l}
    1488  $DISEASELIST
    1489  \\end{tabular}
    1490  \\fi
    1491
    1492  \\closing{$CLOSING}
    1493
    1494  \\end{letter}
    1495  \\end{document}
    1496  """
    1497
    1498
    1499 -def test_au():
    1500          f = open('../../test-area/ian/terry-form.tex')
    1501          params = {
    1502          'RECIPIENT': "Dr. R. Terry\n1 Main St\nNewcastle",
    1503          'DOCTORSNAME': 'Ian Haywood',
    1504          'DOCTORSADDRESS': '1 Smith St\nMelbourne',
    1505          'PATIENTNAME':'Joe Bloggs',
    1506          'PATIENTADDRESS':'18 Fred St\nMelbourne',
    1507          'REQUEST':'echocardiogram',
    1508          'THERAPY':'on warfarin',
    1509          'CLINICALNOTES':"""heard new murmur
    1510          Here's some
    1511  crap to demonstrate how it can cover multiple lines.""",
    1512          'COPYADDRESS':'Karsten Hilbert\nLeipzig, Germany',
    1513          'ROUTINE':1,
    1514          'URGENT':0,
    1515          'FAX':1,
    1516          'PHONE':1,
    1517          'PENSIONER':1,
    1518          'VETERAN':0,
    1519          'PADS':0,
    1520          'INSTRUCTIONS':u'Take the blue pill, Neo'
    1521          }
    1522          form = LaTeXForm (1, f.read())
    1523          form.process (params)
    1524          form.xdvi ()
    1525          form.cleanup ()
    1526
    1527 -def test_au2 ():
    1528          form = LaTeXForm (2, test_letter)
    1529          params = {'RECIPIENTNAME':'Dr. Richard Terry',
    1530                    'RECIPIENTADDRESS':'1 Main St\nNewcastle',
    1531                    'DOCTOR':'Dr. Ian Haywood',
    1532                    'DOCTORADDRESS':'1 Smith St\nMelbourne',
    1533                    'PATIENTNAME':'Joe Bloggs',
    1534                    'PATIENTADDRESS':'18 Fred St, Melbourne',
    1535                    'TEXT':"""This is the main text of the referral letter""",
    1536                    'DOB':'12/3/65',
    1537                    'INCLUDEMEDS':1,
    1538                    'MEDSLIST':[["Amoxycillin", "500mg", "TDS"], ["Perindopril", "4mg", "OD"]],
    1539                    'INCLUDEDISEASES':0, 'DISEASELIST':'',
    1540                    'CLOSING':'Yours sincerely,'
    1541                    }
    1542          form.process (params)
    1543          print os.getcwd ()
    1544          form.xdvi ()
    1545          form.cleanup ()
    1546  #------------------------------------------------------------
    1547 -def test_de():
    1548                  template = open('../../test-area/ian/Formularkopf-DE.tex')
    1549                  form = LaTeXForm(template=template.read())
    1550                  params = {
    1551                                  'PATIENT LASTNAME': 'Kirk',
    1552                                  'PATIENT FIRSTNAME': 'James T.',
    1553                                  'PATIENT STREET': 'Hauptstrasse',
    1554                                  'PATIENT ZIP': '02999',
    1555                                  'PATIENT TOWN': 'Gross Saerchen',
    1556                                  'PATIENT DOB': '22.03.1931'
    1557                  }
    1558                  form.process(params)
    1559                  form.xdvi()
    1560                  form.cleanup()
    1561
    1562  #============================================================
    1563  # main
    1564  #------------------------------------------------------------
    1565  if __name__ == '__main__':
    1566
    1567          if len(sys.argv) < 2:
    1568                  sys.exit()
    1569
    1570          if sys.argv[1] != 'test':
    1571                  sys.exit()
    1572
    1573          from Gnumed.pycommon import gmDateTime
    1574          gmDateTime.init()
    1575
    1576          #--------------------------------------------------------
    1577          # OOo
    1578          #--------------------------------------------------------
    1579 -        def test_init_ooo():
    1580                  init_ooo()
    1581          #--------------------------------------------------------
    1582 -        def test_ooo_connect():
    1583                  srv = gmOOoConnector()
    1584                  print srv
    1585                  print srv.desktop
    1586          #--------------------------------------------------------
    1587 -        def test_open_ooo_doc_from_srv():
    1588                  srv = gmOOoConnector()
    1589                  doc = srv.open_document(filename = sys.argv[2])
    1590                  print "document:", doc
    1591          #--------------------------------------------------------
    1592 -        def test_open_ooo_doc_from_letter():
    1593                  doc = cOOoLetter(template_file = sys.argv[2])
    1594                  doc.open_in_ooo()
    1595                  print "document:", doc
    1596                  raw_input('press <ENTER> to continue')
    1597                  doc.show()
    1598                  #doc.replace_placeholders()
    1599                  #doc.save_in_ooo('~/test_cOOoLetter.odt')
    1600  #               doc = None
    1601  #               doc.close_in_ooo()
    1602                  raw_input('press <ENTER> to continue')
    1603          #--------------------------------------------------------
    1604 -        def play_with_ooo():
    1605                  try:
    1606                          doc = open_uri_in_ooo(filename=sys.argv[1])
    1607                  except:
    1608                          _log.exception('cannot open [%s] in OOo' % sys.argv[1])
    1609                          raise
    1610
    1611                  class myCloseListener(unohelper.Base, oooXCloseListener):
    1612                          def disposing(self, evt):
    1613                                  print "disposing:"
    1614                          def notifyClosing(self, evt):
    1615                                  print "notifyClosing:"
    1616                          def queryClosing(self, evt, owner):
    1617                                  # owner is True/False whether I am the owner of the doc
    1618                                  print "queryClosing:"
    1619
    1620                  l = myCloseListener()
    1621                  doc.addCloseListener(l)
    1622
    1623                  tfs = doc.getTextFields().createEnumeration()
    1624                  print tfs
    1625                  print dir(tfs)
    1626                  while tfs.hasMoreElements():
    1627                          tf = tfs.nextElement()
    1628                          if tf.supportsService('com.sun.star.text.TextField.JumpEdit'):
    1629                                  print tf.getPropertyValue('PlaceHolder')
    1630                                  print "  ", tf.getPropertyValue('Hint')
    1631
    1632  #               doc.close(True)         # closes but leaves open the dedicated OOo window
    1633                  doc.dispose()           # closes and disposes of the OOo window
    1634          #--------------------------------------------------------
    1635 -        def test_cOOoLetter():
    1636                  pat = gmPersonSearch.ask_for_patient()
    1637                  if pat is None:
    1638                          return
    1639                  gmPerson.set_active_patient(patient = pat)
    1640
    1641                  doc = cOOoLetter(template_file = sys.argv[2])
    1642                  doc.open_in_ooo()
    1643                  print doc
    1644                  doc.show()
    1645                  #doc.replace_placeholders()
    1646                  #doc.save_in_ooo('~/test_cOOoLetter.odt')
    1647                  doc = None
    1648  #               doc.close_in_ooo()
    1649                  raw_input('press <ENTER> to continue')
    1650          #--------------------------------------------------------
    1651          # other
    1652          #--------------------------------------------------------
    1653 -        def test_cFormTemplate():
    1654                  template = cFormTemplate(aPK_obj = sys.argv[2])
    1655                  print template
    1656                  print template.export_to_file()
    1657          #--------------------------------------------------------
    1658 -        def set_template_from_file():
    1659                  template = cFormTemplate(aPK_obj = sys.argv[2])
    1660                  template.update_template_from_file(filename = sys.argv[3])
    1661          #--------------------------------------------------------
    1662 -        def test_latex_form():
    1663                  pat = gmPersonSearch.ask_for_patient()
    1664                  if pat is None:
    1665                          return
    1666                  gmPerson.set_active_patient(patient = pat)
    1667
    1668                  gmStaff.gmCurrentProvider(provider = gmStaff.cStaff())
    1669
    1670                  path = os.path.abspath(sys.argv[2])
    1671                  form = cLaTeXForm(template_file = path)
    1672
    1673                  from Gnumed.wxpython import gmMacro
    1674                  ph = gmMacro.gmPlaceholderHandler()
    1675                  ph.debug = True
    1676                  instance_file = form.substitute_placeholders(data_source = ph)
    1677                  pdf_name = form.generate_output(instance_file = instance_file)
    1678                  print "final PDF file is:", pdf_name
    1679          #--------------------------------------------------------
    1680 -        def test_pdf_form():
    1681                  pat = gmPersonSearch.ask_for_patient()
    1682                  if pat is None:
    1683                          return
    1684                  gmPerson.set_active_patient(patient = pat)
    1685
    1686                  gmStaff.gmCurrentProvider(provider = gmStaff.cStaff())
    1687
    1688                  path = os.path.abspath(sys.argv[2])
    1689                  form = cPDFForm(template_file = path)
    1690
    1691                  from Gnumed.wxpython import gmMacro
    1692                  ph = gmMacro.gmPlaceholderHandler()
    1693                  ph.debug = True
    1694                  instance_file = form.substitute_placeholders(data_source = ph)
    1695                  pdf_name = form.generate_output(instance_file = instance_file)
    1696                  print "final PDF file is:", pdf_name
    1697          #--------------------------------------------------------
    1698 -        def test_abiword_form():
    1699                  pat = gmPersonSearch.ask_for_patient()
    1700                  if pat is None:
    1701                          return
    1702                  gmPerson.set_active_patient(patient = pat)
    1703
    1704                  gmStaff.gmCurrentProvider(provider = gmStaff.cStaff())
    1705
    1706                  path = os.path.abspath(sys.argv[2])
    1707                  form = cAbiWordForm(template_file = path)
    1708
    1709                  from Gnumed.wxpython import gmMacro
    1710                  ph = gmMacro.gmPlaceholderHandler()
    1711                  ph.debug = True
    1712                  instance_file = form.substitute_placeholders(data_source = ph)
    1713                  form.edit()
    1714                  final_name = form.generate_output(instance_file = instance_file)
    1715                  print "final file is:", final_name
    1716          #--------------------------------------------------------
    1717          #--------------------------------------------------------
    1718          # now run the tests
    1719          #test_au()
    1720          #test_de()
    1721
    1722          # OOo
    1723          #test_init_ooo()
    1724          #test_ooo_connect()
    1725          #test_open_ooo_doc_from_srv()
    1726          #test_open_ooo_doc_from_letter()
    1727          #play_with_ooo()
    1728          #test_cOOoLetter()
    1729
    1730          #test_cFormTemplate()
    1731          #set_template_from_file()
    1732          #test_latex_form()
    1733          #test_pdf_form()
    1734          test_abiword_form()
    1735
    1736  #============================================================
    1737