[Erp5-report] r6328 - in /erp5/trunk/products/ERP5OOo: ./ dtml/

nobody at svn.erp5.org nobody at svn.erp5.org
Tue Mar 28 12:08:48 CEST 2006


Author: fab
Date: Tue Mar 28 12:08:43 2006
New Revision: 6328

URL: http://svn.erp5.org?rev=6328&view=rev
Log:
Added inclusion of OLE documents and pictures.
See readme.txt for details.

Modified:
    erp5/trunk/products/ERP5OOo/OOoTemplate.py
    erp5/trunk/products/ERP5OOo/OOoUtils.py
    erp5/trunk/products/ERP5OOo/dtml/OOoTemplate_add.dtml
    erp5/trunk/products/ERP5OOo/readme.txt

Modified: erp5/trunk/products/ERP5OOo/OOoTemplate.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5OOo/OOoTemplate.py?rev=6328&r1=6327&r2=6328&view=diff
==============================================================================
--- erp5/trunk/products/ERP5OOo/OOoTemplate.py (original)
+++ erp5/trunk/products/ERP5OOo/OOoTemplate.py Tue Mar 28 12:08:43 2006
@@ -40,6 +40,13 @@
 from Globals import InitializeClass, DTMLFile, get_request
 from AccessControl import ClassSecurityInfo
 from OOoUtils import OOoBuilder
+from zipfile import ZipFile, ZIP_DEFLATED
+try:
+    from cStringIO import StringIO
+except ImportError:
+    from StringIO import StringIO
+import re
+import itertools
 
 from zLOG import LOG
 
@@ -62,6 +69,10 @@
     """
     # add actual object
     id = self._setObject(id, OOoTemplate(id, title))
+    file = REQUEST.form.get('file')
+    if file.filename:
+        # Get the template in the associated context and upload the file
+        getattr(self,id).pt_upload(REQUEST, file)
     # respond to the add_and_edit button if necessary
     add_and_edit(self, id, REQUEST)
     return ''
@@ -91,6 +102,14 @@
     meta_type = "ERP5 OOo Template"
     icon = "www/OOo.png"
 
+    # NOTE: 100 is just pure random starting number
+    # it won't influence the code at all
+    document_counter = itertools.count(100)
+    # Every linked OLE document is in a directory starting with 'Obj'
+    _OLE_directory_prefix  = 'Obj'
+    # every OOo document have a content-type starting like this
+    _OOo_content_type_root = 'application/vnd.sun.xml.'
+
     # Declarative Security
     security = ClassSecurityInfo()
 
@@ -120,7 +139,12 @@
     formSettings = PageTemplateFile('www/formSettings', globals(),
                                      __name__='formSettings')
     formSettings._owner = None
-    
+
+    def __init__(self,*args,**kw):
+        ZopePageTemplate.__init__(self,*args,**kw)
+        # we store the attachments of the uploaded document
+        self.OLE_documents_zipstring = None
+
     def pt_upload(self, REQUEST, file=''):
       """Replace the document with the text in file."""
       if SUPPORTS_WEBDAV_LOCKS and self.wl_isLocked():
@@ -133,6 +157,25 @@
       if file.startswith("PK") : # FIXME: this condition is probably not enough
         # this is a OOo zip file, extract the content
         builder = OOoBuilder(file)
+        attached_files_list = [n for n in builder.getNameList()
+            if n.startswith(self._OLE_directory_prefix)
+            or n.startswith('Pictures')
+            or n == 'META-INF/manifest.xml' ]
+        # destroy a possibly pre-existing OLE document set
+        if self.OLE_documents_zipstring:
+            self.OLE_documents_zipstring = None
+        # create a zip archive and store it
+        if attached_files_list:
+            memory_file = StringIO()
+            try:
+              zf = ZipFile(memory_file, mode='w', compression=ZIP_DEFLATED)
+            except RuntimeError:
+              zf = ZipFile(memory_file, mode='w')
+            for attached_file in attached_files_list:  
+                zf.writestr(attached_file, builder.extract(attached_file) )
+            zf.close()
+            memory_file.seek(0)
+            self.OLE_documents_zipstring = memory_file.read()
         self.content_type = builder.getMimeType()
         file = builder.prepareContentXml()
         
@@ -154,6 +197,183 @@
                   % '<br>'.join(self._v_warnings))
       return self.formSettings(manage_tabs_message=message)
 
+    def _resolvePath(self, path):
+        return self.getPortalObject().unrestrictedTraverse(path)
+
+    def renderIncludes(self, text, sub_document=None):
+        attached_files_dict = {}
+        arguments_re = re.compile('(\w+)\s*=\s*"(.*?)"\s*',re.DOTALL)
+
+        def getLengthInfos( opts_dict, opts_names ):
+            ret = []
+            for opt_name in opts_names:
+                try:
+                    val = opts_dict[opt_name]
+                    if val.endswith('cm'):
+                        val = val[:-2]
+                    val = float( val )
+                except (ValueError, KeyError):
+                    val = None
+                ret.append(val)
+            return ret
+
+
+        def replaceIncludes(match):
+            options_dict = dict( style="fr1", x="0cm", y="0cm" )
+            options_dict.update( dict(arguments_re.findall( match.group(1) )) )
+            document = self._resolvePath( options_dict['path'] )
+            document_text = document.read()
+
+            if 'type' not in options_dict:
+                options_dict['type'] = document.content_type
+            else: # type passed in short form as an attribute
+                options_dict['type'] = self._OOo_content_type_root + options_dict['type']
+
+            w, h, x, y = getLengthInfos( options_dict , ('width', 'height', 'x', 'y') )
+            # Set defaults
+            if w is None:
+                w = 10.0
+            if h is None:
+                h = 10.0
+            if  x is None:
+                x = 0.0
+            if  y is None:
+                y = 0.0
+
+            actual_idx = self.document_counter.next()
+            dir_name = '%s%d'%(self._OLE_directory_prefix,actual_idx)
+
+            if sub_document: # sub-document means sub-directory
+                dir_name = sub_document+'/'+dir_name
+
+            try:
+                temp_builder = OOoBuilder(getattr(document,document.ooo_stylesheet))
+                stylesheet = temp_builder.extract('styles.xml')
+            except AttributeError:
+                stylesheet = None
+
+            sub_attached_files_dict = {}
+            if 'office:include' in document_text: # small optimisation to avoid recursion if possible
+                (document_text, sub_attached_files_dict ) = self.renderIncludes(document_text, dir_name)
+
+            # View*    = writer
+            # Visible* = calc
+            settings_text = """<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE office:document-settings PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "office.dtd">
+<office:document-settings xmlns:office="http://openoffice.org/2000/office"
+xmlns:xlink="http://www.w3.org/1999/xlink"
+xmlns:config="http://openoffice.org/2001/config" office:version="1.0">
+<office:settings>
+<config:config-item-set config:name="view-settings">
+<config:config-item config:name="ViewAreaTop" config:type="int">0</config:config-item>
+<config:config-item config:name="ViewAreaLeft" config:type="int">0</config:config-item>
+<config:config-item config:name="ViewAreaWidth" config:type="int">%(w)d</config:config-item>
+<config:config-item config:name="ViewAreaHeight" config:type="int">%(h)d</config:config-item>
+<config:config-item config:name="VisibleAreaTop" config:type="int">0</config:config-item>
+<config:config-item config:name="VisibleAreaLeft" config:type="int">0</config:config-item>
+<config:config-item config:name="VisibleAreaWidth" config:type="int">%(w)d</config:config-item>
+<config:config-item config:name="VisibleAreaHeight" config:type="int">%(h)d</config:config-item>
+</config:config-item-set>
+</office:settings>
+</office:document-settings>"""%dict( w=int(w*1000) , h=int(h*1000) ) # convert from 10^-2 (centimeters) to 10^-5
+            attached_files_dict[dir_name] = dict(document = document_text,
+                    doc_type = options_dict['type'], stylesheet = stylesheet )
+            attached_files_dict[dir_name+'/settings.xml'] = dict( document = settings_text,
+                    doc_type = 'text/xml' )
+            attached_files_dict.update(sub_attached_files_dict )
+
+            # add a paragraph with the OLE document in it
+            # The dir_name is relative here, extract the last path component
+            replacement = """
+            <draw:object draw:style-name="%s" draw:name="ERP5IncludedObject%d"
+            text:anchor-type="paragraph" svg:x="%.3fcm" svg:y="%.3fcm"
+            svg:width="%.3fcm" svg:height="%.3fcm" xlink:href="#./%s"
+            xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"/>
+            """%(options_dict['style'], actual_idx, x, y, w, h, dir_name.split('/')[-1])
+            if not self.content_type.endswith('draw'):
+                replacement = '<text:p text:style-name="Standard">'+replacement+'</text:p>'
+            return replacement
+
+        def replaceIncludesImg(match):
+            options_dict = dict( x='0cm', y='0cm', style="fr1" )
+            options_dict.update( dict(arguments_re.findall( match.group(1) )) )
+            picture = self._resolvePath( options_dict['path'] )
+
+            # "standard" filetype == Image or File , for ERP objects the
+            # manipulations are different
+            is_standard_filetype = True
+
+            if not hasattr(picture,'data') or callable(picture.content_type):
+                is_standard_filetype = False
+
+            if is_standard_filetype:
+                picture_data = picture.data
+            else:
+                picture_data = picture.Base_download()
+
+            # fetch the content-type of the picture (generally guessed by zope)
+            if 'type' not in options_dict:
+                if is_standard_filetype:
+                    options_dict['type'] = picture.content_type
+                else:
+                    options_dict['type'] = picture.content_type()
+
+            if '/' not in options_dict['type']:
+                options_dict['type'] = 'image/' + options_dict['type']
+
+            w, h, maxwidth, maxheight = getLengthInfos( options_dict, ('width','height','maxwidth','maxheight') )
+
+            try:
+                aspect_ratio = float(picture.width) / float(picture.height)
+            except TypeError:
+                aspect_ratio = float(picture.width()) / float(picture.height())
+
+            # fix a default value and correct the aspect
+            if h in None:
+                if w is None:
+                    w = 10.0
+                h = w / aspect_ratio
+            elif w is None:
+                w = h * aspect_ratio
+
+            # picture is too large
+            if maxwidth and maxwidth < w:
+                w = maxwidth
+                h = w / aspect_ratio
+            if maxheight and maxheight < h:
+                h = maxheight
+                w = h * aspect_ratio
+
+            actual_idx = self.document_counter.next()
+            pic_name = 'Pictures/picture%d.%s'%(actual_idx, options_dict['type'].split('/')[-1])
+
+            if sub_document: # sub-document means sub-directory
+                pic_name = sub_document+'/'+pic_name
+
+            attached_files_dict[pic_name] = dict(
+                document = picture_data,
+                doc_type = options_dict['type']
+            )
+            # XXX: Pictures directory not managed (seems facultative)
+            #  <manifest:file-entry manifest:media-type="" manifest:full-path="ObjBFE4F50D/Pictures/"/>
+            replacement = """<draw:image draw:style-name="%s" draw:name="ERP5Image%d"
+            text:anchor-type="paragraph" svg:x="%s" svg:y="%s"
+            svg:width="%.3fcm" svg:height="%.3fcm" xlink:href="#Pictures/%s"
+            xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"/>
+            """%(options_dict['style'], actual_idx,
+                        options_dict['x'], options_dict['y'],
+                        w, h,
+                        pic_name.split('/')[-1] )
+            if not self.content_type.endswith('draw'):
+                replacement = '<text:p text:style-name="Standard">'+replacement+'</text:p>'
+            return replacement
+
+        # NOTE: (?s) at the end is for including '\n' when matching '.'
+        # It's an equivalent to DOTALL option passing (but sub can't get options parameter)
+        text = re.sub('<\s*office:include_img\s+(.*?)\s*/\s*>(?s)', replaceIncludesImg, text)
+        text = re.sub('<\s*office:include\s+(.*?)\s*/\s*>(?s)', replaceIncludes, text)
+        return (text, attached_files_dict)
+
     # Proxy method to PageTemplate
     def pt_render(self, source=0, extra_context={}):
       # Get request
@@ -170,6 +390,31 @@
       # And render page template
       doc_xml = ZopePageTemplate.pt_render(self, source=source, extra_context=extra_context)
 
+      # Replace the includes
+      (doc_xml,attachments_dict) = self.renderIncludes(doc_xml)
+
+      try:
+          default_styles_text = ooo_builder.extract('styles.xml')
+      except AttributeError:
+          default_styles_text = None
+      # Add the associated files
+      for dir_name, document_dict in attachments_dict.iteritems():
+          # Special case : the document is an OOo one
+          if document_dict['doc_type'].startswith(self._OOo_content_type_root):
+              ooo_builder.addFileEntry(full_path = dir_name,
+                      media_type = document_dict['doc_type'] )
+              ooo_builder.addFileEntry(full_path = dir_name+'/content.xml',
+                      media_type = 'text/xml',content = document_dict['document'] )
+              styles_text = default_styles_text
+              if document_dict.has_key('stylesheet') and document_dict['stylesheet']:
+                styles_text = document_dict['stylesheet']
+              if styles_text:
+                  ooo_builder.addFileEntry(full_path = dir_name+'/styles.xml',
+                          media_type = 'text/xml',content = styles_text )
+          else: # Generic case
+              ooo_builder.addFileEntry(full_path=dir_name,
+                      media_type=document_dict['doc_type'], content = document_dict['document'] )
+
       # Get request and batch_mode
       batch_mode = extra_context.get('batch_mode', 0)
 
@@ -179,6 +424,15 @@
 
       # Replace content.xml in master openoffice template
       ooo_builder.replace('content.xml', doc_xml)
+
+      # If the file has embedded OLE documents, restore it
+      if self.OLE_documents_zipstring:
+          additional_builder = OOoBuilder( self.OLE_documents_zipstring )
+          for name in additional_builder.getNameList():
+              ooo_builder.replace(name, additional_builder.extract(name) )
+
+      # Update the META informations
+      ooo_builder.updateManifest()
 
       # Produce final result
       ooo = ooo_builder.render(self.title or self.id)
@@ -218,5 +472,5 @@
 InitializeClass(FSOOoTemplate)
 
 registerFileExtension('ooot', FSOOoTemplate)
-registerMetaType('ERP5 OOo Template', FSOOoTemplate)
-
+registerMetaType(OOoTemplate.meta_type, FSOOoTemplate)
+

Modified: erp5/trunk/products/ERP5OOo/OOoUtils.py
URL: http://svn.erp5.org/erp5/trunk/products/ERP5OOo/OOoUtils.py?rev=6328&r1=6327&r2=6328&view=diff
==============================================================================
--- erp5/trunk/products/ERP5OOo/OOoUtils.py (original)
+++ erp5/trunk/products/ERP5OOo/OOoUtils.py Tue Mar 28 12:08:43 2006
@@ -34,7 +34,10 @@
 from AccessControl import ClassSecurityInfo
 from Globals import InitializeClass, get_request
 from zipfile import ZipFile, ZIP_DEFLATED
-from StringIO import StringIO
+try:
+  from cStringIO import StringIO
+except ImportError:
+  from StringIO import StringIO
 from zLOG import LOG
 import imghdr
 import random
@@ -72,6 +75,7 @@
     else :
       self._document = StringIO(document)
     self._image_count = 0    
+    self._manifest_additions_list = []
 
   security.declarePublic('replace')
   def replace(self, filename, stream):
@@ -83,6 +87,13 @@
       zf = ZipFile(self._document, mode='a', compression=ZIP_DEFLATED)
     except RuntimeError:
       zf = ZipFile(self._document, mode='a')
+    try:
+	    # remove the file first if it exists
+	    fi = zf.getinfo(filename)
+	    zf.filelist.remove( fi )
+    except KeyError:
+	    # This is a new file
+	    pass
     zf.writestr(filename, stream)
     zf.close()
   
@@ -97,6 +108,16 @@
       zf = ZipFile(self._document, mode='r')
     return zf.read(filename)
   
+  security.declarePublic('getNameList')
+  def getNameList(self):
+    try:
+      zf = ZipFile(self._document, mode='r', compression=ZIP_DEFLATED)
+    except RuntimeError:
+      zf = ZipFile(self._document, mode='r')
+    li = zf.namelist()
+    zf.close()
+    return li
+
   security.declarePublic('getMimeType')
   def getMimeType(self):
     return self.extract('mimetype')
@@ -124,6 +145,40 @@
           tal:attributes='dummy python:request.RESPONSE.setHeader("Content-Type", "text/html;; charset=utf-8")'
          office:version='1.0'""")
 
+    """
+    """
+
+  security.declarePublic('addFileEntry')
+  def addFileEntry(self, full_path, media_type, content=None):
+      """ Add a file entry to the manifest and possibly is content """
+      self.addManifest(full_path, media_type)
+      if content:
+          self.replace(full_path, content)
+
+  security.declarePublic('addManifest')
+  def addManifest(self, full_path, media_type):
+    """ Add a path to the manifest """
+    li = '<manifest:file-entry manifest:media-type="%s" manifest:full-path="%s"/>'%(media_type, full_path)
+    self._manifest_additions_list.append(li)
+
+  security.declarePublic('updateManifest')
+  def updateManifest(self):
+    """ Add a path to the manifest """
+    MANIFEST_FILENAME = 'META-INF/manifest.xml'
+    meta_infos = self.extract(MANIFEST_FILENAME)
+
+    # prevent some duplicates
+    for meta_line in meta_infos.split('\n'):
+        for new_meta_line in self._manifest_additions_list:
+            if meta_line.strip() == new_meta_line:
+                self._manifest_additions_list.remove(new_meta_line)
+
+    # add the new lines
+    self._manifest_additions_list.append('</manifest:manifest>')
+    meta_infos = meta_infos.replace( self._manifest_additions_list[-1], '\n'.join(self._manifest_additions_list) )
+    self.replace(MANIFEST_FILENAME, meta_infos)
+    self._manifest_additions_list = []
+
   security.declarePublic('addImage')
   def addImage(self, image, format='png'):
     """
@@ -296,14 +351,18 @@
     # List all embedded spreadsheets
     emb_objects = self.oo_content_dom.getElementsByTagName("draw:object")
     for embedded in emb_objects:
-      document = embedded.getAttributeNS(self.ns["xlink"], "href")
-      if document:
-        try:
-          object_content = self.reader.fromString(self.oo_files[document[3:] + '/content.xml'])
-          for table in object_content.getElementsByTagName("table:table"):
-            spreadsheets.append(table)
-        except:
-          pass
+        document = embedded.getAttributeNS(self.ns["xlink"], "href")
+        if document:
+            try:
+                object_content = self.reader.fromString(self.oo_files[document[3:] + '/content.xml'])
+                tables = object_content.getElementsByTagName("table:table")
+                if tables:
+                    for table in tables:
+                        spreadsheets.append(table)
+                else: # XXX: insert the link to OLE document ?
+                    pass
+            except:
+                pass
     return spreadsheets
 
 

Modified: erp5/trunk/products/ERP5OOo/dtml/OOoTemplate_add.dtml
URL: http://svn.erp5.org/erp5/trunk/products/ERP5OOo/dtml/OOoTemplate_add.dtml?rev=6328&r1=6327&r2=6328&view=diff
==============================================================================
--- erp5/trunk/products/ERP5OOo/dtml/OOoTemplate_add.dtml (original)
+++ erp5/trunk/products/ERP5OOo/dtml/OOoTemplate_add.dtml Tue Mar 28 12:08:43 2006
@@ -9,7 +9,7 @@
 on templates.
 </p>
 
-<form action="addOOoTemplate" method="POST">
+<form action="addOOoTemplate" enctype="multipart/form-data" method="POST">
 
 <table cellspacing="0" cellpadding="2" border="0">
   <tr>
@@ -36,6 +36,17 @@
 
   <tr>
     <td align="left" valign="top">
+      <div class="form-optional">
+        File
+      </div>
+    </td>
+    <td align="left" valign="top">
+      <input type="file" name="file" size="25" value="" />
+    </td>
+  </tr>
+
+  <tr>
+    <td align="left" valign="top">
     </td>
     <td align="left" valign="top">
     <div class="form-element">

Modified: erp5/trunk/products/ERP5OOo/readme.txt
URL: http://svn.erp5.org/erp5/trunk/products/ERP5OOo/readme.txt?rev=6328&r1=6327&r2=6328&view=diff
==============================================================================
--- erp5/trunk/products/ERP5OOo/readme.txt (original)
+++ erp5/trunk/products/ERP5OOo/readme.txt Tue Mar 28 12:08:43 2006
@@ -9,6 +9,34 @@
 
   Rendering consists in replacing content.xml of the original OOo document
   with the content.xml generated by the OOo Template.
+
+  Special tags for including content:
+
+    - <office:include>
+        Allow you to include another document in the current template (as an OLE attachment)
+        You must specify at least the path (can be either a single name or a path name using "/")
+        and the type of the document (generally calc or writer), the default is zope's content-type.
+        The size is always specified in centimeters (with attached "cm" suffix or not).
+        You can specify the style (defined in the sylesheet) with the "style" option.
+        You can also pass "x" and "y" attributes for positioning, it's mainly useful for draw documents.
+
+       Example:
+           <office:include type="calc" width="10.100cm" height="16.000cm" path="agenda" />
+           <office:include width="15" height="20" path="/reports/my_report" />
+           <office:include path="foo" />
+
+    - <office:include_img>
+        Not unlike <office:include>, allows you to include a picture document, refer to
+        the <office:include> part for details.
+        The optional "type" attribute specifies the picture format ; you can either
+        pass a full value ("image/jpeg") or the short version ("jpeg").
+        You can also pass position parameters with "x" and "y" attributes.
+        The maxwidth and maxheight parameters are useful to set constraints.
+        The aspect ratio information try to be kept (if you set only one size
+        or if a constraint is applied).
+
+       Example:
+           <office:include x="5cm" y="1cm" path="foo" />
 
   Tips:
 




More information about the Erp5-report mailing list