[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