[pod+gen] Added POD rendering based on ODS templates. Integrated with gen.

This commit is contained in:
Gaetan Delannay 2013-02-05 08:51:25 +01:00
parent d5d99b67eb
commit 43261fde60
8 changed files with 144 additions and 92 deletions

View file

@ -272,6 +272,12 @@ class ZopeInstaller:
appyType.template)
if os.path.exists(fileName):
setattr(appyTool, attrName, fileName)
# If the template is ods, set the default format to ods
# (because default is odt)
if fileName.endswith('.ods'):
formats = appyTool.getAttributeName('formats',
appyClass, appyType.name)
setattr(appyTool, formats, ['ods'])
appyTool.log('Imported "%s" in the tool in ' \
'attribute "%s"'% (fileName, attrName))
else:

View file

@ -94,6 +94,8 @@ appyLabels = [
('pdf', 'PDF'),
('doc', 'DOC'),
('rtf', 'RTF'),
('ods', 'ODS'),
('xls', 'XLS'),
('front_page_text', 'Welcome to this Appy-powered site.'),
('captcha_text', 'Please type "${text}" (without the double quotes) in the ' \
'field besides, but without the character at position ' \

BIN
gen/ui/ods.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

BIN
gen/ui/xls.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

View file

@ -54,7 +54,7 @@ class ToolWrapper(AbstractWrapper):
(user.o.absolute_url(), user.title,access))
return res + '\n'.join(rows) + '</table>'
podOutputFormats = ('odt', 'pdf', 'doc', 'rtf')
podOutputFormats = ('odt', 'pdf', 'doc', 'rtf', 'ods', 'xls')
def getPodOutputFormats(self):
'''Gets the available output formats for POD documents.'''
return [(of, self.translate(of)) for of in self.podOutputFormats]
@ -185,4 +185,36 @@ class ToolWrapper(AbstractWrapper):
except Exception, e:
failed.append(startObject)
return nb, failed
def validate(self, new, errors):
'''Validates that uploaded POD templates and output types are
compatible.'''
page = self.request.get('page', 'main')
if page == 'documents':
# Check that uploaded templates and output formats are compatible.
for fieldName in dir(new):
# Ignore fields which are not POD templates.
if not fieldName.startswith('podTemplate'): continue
# Get the file name, either from the newly uploaded file or
# from the existing file stored in the database.
if getattr(new, fieldName):
fileName = getattr(new, fieldName).filename
else:
fileName = getattr(self, fieldName).name
# Get the extension of the uploaded file.
ext = os.path.splitext(fileName)[1][1:]
# Get the chosen output formats for this template.
formatsFieldName = 'formatsFor%s' % fieldName[14:]
formats = getattr(new, formatsFieldName)
error = False
if ext == 'odt':
error = ('ods' in formats) or ('xls' in formats)
elif ext == 'ods':
error = ('odt' in formats) or ('pdf' in formats) or \
('doc' in formats) or ('rtf' in formats)
if error:
msg = 'This (these) format(s) cannot be used with ' \
'this template.'
setattr(errors, formatsFieldName, msg)
return self._callCustom('validate', new, errors)
# ------------------------------------------------------------------------------

View file

@ -51,15 +51,15 @@ DOC_NOT_FOUND = 'Document "%s" was not found.'
URL_NOT_FOUND = 'Doc URL "%s" is wrong. %s'
BAD_RESULT_TYPE = 'Bad result type "%s". Available types are %s.'
CANNOT_WRITE_RESULT = 'I cannot write result "%s". %s'
CONNECT_ERROR = 'Could not connect to OpenOffice on port %d. UNO ' \
'(OpenOffice API) says: %s.'
CONNECT_ERROR = 'Could not connect to LibreOffice on port %d. UNO ' \
'(LibreOffice API) says: %s.'
# Some constants ---------------------------------------------------------------
DEFAULT_PORT = 2002
# ------------------------------------------------------------------------------
class Converter:
'''Converts a document readable by OpenOffice into pdf, doc, txt, rtf...'''
'''Converts a document readable by LibreOffice into pdf, doc, txt, rtf...'''
exeVariants = ('soffice.exe', 'soffice')
pathReplacements = {'program files': 'progra~1',
'openoffice.org 1': 'openof~1',
@ -72,9 +72,9 @@ class Converter:
self.resultType = resultType
self.resultFilter = self.getResultFilter()
self.resultUrl = self.getResultUrl()
self.ooContext = None
self.oo = None # The OpenOffice application object
self.doc = None # The OpenOffice loaded document
self.loContext = None
self.oo = None # The LibreOffice application object
self.doc = None # The LibreOffice loaded document
def getInputUrls(self, docPath):
'''Returns the absolute path of the input file. In fact, it returns a
@ -100,7 +100,7 @@ class Converter:
return res
def getResultUrl(self):
'''Returns the path of the result file in the format needed by OO. If
'''Returns the path of the result file in the format needed by LO. If
the result type and the input type are the same (ie the user wants to
refresh indexes or some other action and not perform a real
conversion), the result file is named
@ -126,7 +126,7 @@ class Converter:
raise ConverterError(CANNOT_WRITE_RESULT % (res, ioe))
def connect(self):
'''Connects to OpenOffice'''
'''Connects to LibreOffice'''
if os.name == 'nt':
import socket
import uno
@ -138,17 +138,17 @@ class Converter:
resolver = localContext.ServiceManager.createInstanceWithContext(
"com.sun.star.bridge.UnoUrlResolver", localContext)
# Connect to the running office
self.ooContext = resolver.resolve(
self.loContext = resolver.resolve(
'uno:socket,host=localhost,port=%d;urp;StarOffice.' \
'ComponentContext' % self.port)
# Is seems that we can't define a timeout for this method.
# I need it because, for example, when a web server already listens
# to the given port (thus, not a OpenOffice instance), this method
# to the given port (thus, not a LibreOffice instance), this method
# blocks.
smgr = self.ooContext.ServiceManager
smgr = self.loContext.ServiceManager
# Get the central desktop object
self.oo = smgr.createInstanceWithContext(
'com.sun.star.frame.Desktop', self.ooContext)
'com.sun.star.frame.Desktop', self.loContext)
except NoConnectException, nce:
raise ConverterError(CONNECT_ERROR % (self.port, nce))
@ -220,7 +220,7 @@ class Converter:
raise ConverterError(URL_NOT_FOUND % (self.docPath, iae))
def convertDocument(self):
'''Calls OO to perform a document conversion. Note that the conversion
'''Calls LO to perform a document conversion. Note that the conversion
is not really done if the source and target documents have the same
type.'''
properties = []
@ -238,7 +238,7 @@ class Converter:
self.doc.storeToURL(self.resultUrl, tuple(properties))
def run(self):
'''Connects to OO, does the job and disconnects.'''
'''Connects to LO, does the job and disconnects.'''
self.connect()
self.loadDocument()
self.convertDocument()
@ -257,12 +257,12 @@ class ConverterScript:
' and outputType is the output format, that must be one of\n' \
' %s.\n' \
' "python" should be a UNO-enabled Python interpreter (ie the ' \
' one which is included in the OpenOffice.org distribution).' % \
' one which is included in the LibreOffice distribution).' % \
str(FILE_TYPES.keys())
def run(self):
optParser = OptionParser(usage=ConverterScript.usage)
optParser.add_option("-p", "--port", dest="port",
help="The port on which OpenOffice runs " \
help="The port on which LibreOffice runs " \
"Default is %d." % DEFAULT_PORT,
default=DEFAULT_PORT, metavar="PORT", type='int')
(options, args) = optParser.parse_args()

View file

@ -23,7 +23,7 @@ from UserDict import UserDict
import appy.pod, time, cgi
from appy.pod import PodError
from appy.shared import mimeTypesExts
from appy.shared import mimeTypes, mimeTypesExts
from appy.shared.xml_parser import XmlElement
from appy.shared.utils import FolderDeleter, executeCommand
from appy.shared.utils import FileWrapper
@ -40,10 +40,10 @@ RESULT_FILE_EXISTS = 'Result file "%s" exists.'
CANT_WRITE_RESULT = 'I cannot write result file "%s". %s'
CANT_WRITE_TEMP_FOLDER = 'I cannot create temp folder "%s". %s'
NO_PY_PATH = 'Extension of result file is "%s". In order to perform ' \
'conversion from ODT to this format we need to call OpenOffice. ' \
'conversion from ODT to this format we need to call LibreOffice. ' \
'But the Python interpreter which runs the current script does ' \
'not know UNO, the library that allows to connect to ' \
'OpenOffice in server mode. If you can\'t install UNO in this ' \
'LibreOffice in server mode. If you can\'t install UNO in this ' \
'Python interpreter, you can specify, in parameter ' \
'"pythonWithUnoPath", the path to a UNO-enabled Python ' \
'interpreter. One such interpreter may be found in ' \
@ -57,12 +57,11 @@ BLANKS_IN_PATH = 'Blanks were found in path "%s". Please use the DOS-names ' \
BAD_RESULT_TYPE = 'Result "%s" has a wrong extension. Allowed extensions ' \
'are: "%s".'
CONVERT_ERROR = 'An error occurred during the conversion. %s'
BAD_OO_PORT = 'Bad OpenOffice port "%s". Make sure it is an integer.'
BAD_OO_PORT = 'Bad LibreOffice port "%s". Make sure it is an integer.'
XHTML_ERROR = 'An error occurred while rendering XHTML content.'
WARNING_INCOMPLETE_ODT = 'Warning: your ODT file may not be complete (ie ' \
'imported documents may not be present). This is ' \
'because we could not connect to OpenOffice in ' \
'server mode: %s'
WARNING_INCOMPLETE_OD = 'Warning: your OpenDocument file may not be complete ' \
'(ie imported documents may not be present). This is because we could not ' \
'connect to LibreOffice in server mode: %s'
DOC_NOT_SPECIFIED = 'Please specify a document to import, either with a ' \
'stream (parameter "content") or with a path (parameter ' \
'"at")'
@ -92,6 +91,8 @@ STYLES_POD_FONTS = '<@style@:font-face @style@:name="PodStarSymbol" ' \
# ------------------------------------------------------------------------------
class Renderer:
templateTypes = ('odt', 'ods') # Types of POD templates
def __init__(self, template, context, result, pythonWithUnoPath=None,
ooPort=2002, stylesMapping={}, forceOoCall=False,
finalizeFunction=None, overwriteExisting=False,
@ -416,20 +417,9 @@ class Renderer:
FolderDeleter.delete(self.tempFolder)
raise po
def reportProblem(self, msg, resultType):
'''When trying to call OO in server mode for producing ODT
(=forceOoCall=True), if an error occurs we still have an ODT to
return to the user. So we produce a warning instead of raising an
error.'''
if (resultType == 'odt') and self.forceOoCall:
print WARNING_INCOMPLETE_ODT % msg
else:
raise msg
def callOpenOffice(self, resultOdtName, resultType):
'''Call Open Office in server mode to convert or update the ODT
result.'''
ooOutput = ''
def callLibreOffice(self, resultName, resultType):
'''Call LibreOffice in server mode to convert or update the result.'''
loOutput = ''
try:
if (not isinstance(self.ooPort, int)) and \
(not isinstance(self.ooPort, long)):
@ -437,7 +427,7 @@ class Renderer:
try:
from appy.pod.converter import Converter, ConverterError
try:
Converter(resultOdtName, resultType, self.ooPort).run()
Converter(resultName, resultType, self.ooPort).run()
except ConverterError, ce:
raise PodError(CONVERT_ERROR % str(ce))
except ImportError:
@ -449,35 +439,54 @@ class Renderer:
raise PodError(BLANKS_IN_PATH % self.pyPath)
if not os.path.isfile(self.pyPath):
raise PodError(PY_PATH_NOT_FILE % self.pyPath)
if resultOdtName.find(' ') != -1:
qResultOdtName = '"%s"' % resultOdtName
if resultName.find(' ') != -1:
qResultName = '"%s"' % resultName
else:
qResultOdtName = resultOdtName
qResultName = resultName
convScript = '%s/converter.py' % \
os.path.dirname(appy.pod.__file__)
if convScript.find(' ') != -1:
convScript = '"%s"' % convScript
cmd = '%s %s %s %s -p%d' % \
(self.pyPath, convScript, qResultOdtName, resultType,
(self.pyPath, convScript, qResultName, resultType,
self.ooPort)
ooOutput = executeCommand(cmd)
loOutput = executeCommand(cmd)
except PodError, pe:
# When trying to call OO in server mode for producing
# ODT (=forceOoCall=True), if an error occurs we still
# have an ODT to return to the user. So we produce a
# warning instead of raising an error.
if (resultType == 'odt') and self.forceOoCall:
print WARNING_INCOMPLETE_ODT % str(pe)
# When trying to call LO in server mode for producing ODT or ODS
# (=forceOoCall=True), if an error occurs we have nevertheless
# an ODT or ODS to return to the user. So we produce a warning
# instead of raising an error.
if (resultType in self.templateTypes) and self.forceOoCall:
print WARNING_INCOMPLETE_OD % str(pe)
else:
raise pe
return ooOutput
return loOutput
def getTemplateType(self):
'''Identifies the type of the pod template in self.template
(ods or odt). If self.template is a string, it is a file name and we
simply get its extension. Else, it is a binary file in a StringIO
instance, and we seek the mime type from the first bytes.'''
if isinstance(self.template, basestring):
res = os.path.splitext(self.template)[1][1:]
else:
# A StringIO instance
self.template.seek(0)
firstBytes = self.template.read(90)
firstBytes = firstBytes[firstBytes.index('mimetype')+8:]
if firstBytes.startswith(mimeTypes['ods']):
res = 'ods'
else:
# We suppose this is ODT
res = 'odt'
return res
def finalize(self):
'''Re-zip the result and potentially call OpenOffice if target format is
not ODT or if forceOoCall is True.'''
for odtFile in ('content.xml', 'styles.xml'):
shutil.copy(os.path.join(self.tempFolder, odtFile),
os.path.join(self.unzipFolder, odtFile))
'''Re-zip the result and potentially call LibreOffice if target format
is not among self.templateTypes or if forceOoCall is True.'''
for innerFile in ('content.xml', 'styles.xml'):
shutil.copy(os.path.join(self.tempFolder, innerFile),
os.path.join(self.unzipFolder, innerFile))
# Insert dynamic styles
contentXml = os.path.join(self.unzipFolder, 'content.xml')
f = file(contentXml)
@ -493,27 +502,28 @@ class Renderer:
self.finalizeFunction(self.unzipFolder)
except Exception, e:
print WARNING_FINALIZE_ERROR % str(e)
# Re-zip the result.
resExt = os.path.splitext(self.template)[1]
resultOdtName = os.path.join(self.tempFolder, 'result%s' % resExt)
# Re-zip the result, first as an OpenDocument file of the same type as
# the POD template (odt, ods...)
resultExt = self.getTemplateType()
resultName = os.path.join(self.tempFolder, 'result.%s' % resultExt)
try:
resultOdt = zipfile.ZipFile(resultOdtName,'w', zipfile.ZIP_DEFLATED)
resultZip = zipfile.ZipFile(resultName, 'w', zipfile.ZIP_DEFLATED)
except RuntimeError:
resultOdt = zipfile.ZipFile(resultOdtName,'w')
resultZip = zipfile.ZipFile(resultName,'w')
# Insert first the file "mimetype" (uncompressed), in order to be
# compliant with the OpenDocument Format specification, section 17.4,
# that expresses this restriction. Else, libraries like "magic", under
# Linux/Unix, are unable to detect the correct mimetype for a pod result
# (it simply recognizes it as a "application/zip" and not a
# "application/vnd.oasis.opendocument.text)".
resultOdt.write(os.path.join(self.unzipFolder, 'mimetype'),
resultZip.write(os.path.join(self.unzipFolder, 'mimetype'),
'mimetype', zipfile.ZIP_STORED)
for dir, dirnames, filenames in os.walk(self.unzipFolder):
for f in filenames:
folderName = dir[len(self.unzipFolder)+1:]
# Ignore file "mimetype" that was already inserted.
if (folderName == '') and (f == 'mimetype'): continue
resultOdt.write(os.path.join(dir, f),
resultZip.write(os.path.join(dir, f),
os.path.join(folderName, f))
if not dirnames and not filenames:
# This is an empty leaf folder. We must create an entry in the
@ -521,35 +531,34 @@ class Renderer:
folderName = dir[len(self.unzipFolder):]
zInfo = zipfile.ZipInfo("%s/" % folderName,time.localtime()[:6])
zInfo.external_attr = 48
resultOdt.writestr(zInfo, '')
resultOdt.close()
resultType = os.path.splitext(self.result)[1]
resultZip.writestr(zInfo, '')
resultZip.close()
resultType = os.path.splitext(self.result)[1].strip('.')
try:
if (resultType == '.odt') and not self.forceOoCall:
if (resultType in self.templateTypes) and not self.forceOoCall:
# Simply move the ODT result to the result
os.rename(resultOdtName, self.result)
os.rename(resultName, self.result)
else:
if resultType.startswith('.'): resultType = resultType[1:]
if not resultType in FILE_TYPES.keys():
if resultType not in FILE_TYPES:
raise PodError(BAD_RESULT_TYPE % (
self.result, FILE_TYPES.keys()))
# Call OpenOffice to perform the conversion or document update
output = self.callOpenOffice(resultOdtName, resultType)
# I (should) have the result. Move it to the correct name
resPrefix = os.path.splitext(resultOdtName)[0] + '.'
if resultType == 'odt':
# Call LibreOffice to perform the conversion or document update.
output = self.callLibreOffice(resultName, resultType)
# I (should) have the result. Move it to the correct name.
resPrefix = os.path.splitext(resultName)[0]
if resultType in self.templateTypes:
# converter.py has (normally!) created a second file
# suffixed .res.odt
resultName = resPrefix + 'res.odt'
if not os.path.exists(resultName):
resultName = resultOdtName
# suffixed .res.[resultType]
finalResultName = '%s.res.%s' % (resPrefix, resultType)
if not os.path.exists(finalResultName):
finalResultName = resultName
# In this case OO in server mode could not be called to
# update indexes, sections, etc.
else:
resultName = resPrefix + resultType
if not os.path.exists(resultName):
finalResultName = '%s.%s' % (resPrefix, resultType)
if not os.path.exists(finalResultName):
raise PodError(CONVERT_ERROR % output)
os.rename(resultName, self.result)
os.rename(finalResultName, self.result)
finally:
FolderDeleter.delete(self.tempFolder)
# ------------------------------------------------------------------------------

View file

@ -4,20 +4,23 @@ import os.path
# ------------------------------------------------------------------------------
appyPath = os.path.realpath(os.path.dirname(appy.__file__))
mimeTypes = {'odt': 'application/vnd.oasis.opendocument.text',
od = 'application/vnd.oasis.opendocument'
mimeTypes = {'odt': '%s.text' % od,
'ods': '%s.spreadsheet' % od,
'doc': 'application/msword',
'rtf': 'text/rtf',
'pdf': 'application/pdf'
}
mimeTypesExts = {
'application/vnd.oasis.opendocument.text': 'odt',
'application/msword' : 'doc',
'text/rtf' : 'rtf',
'application/pdf' : 'pdf',
'image/png' : 'png',
'image/jpeg' : 'jpg',
'image/pjpeg' : 'jpg',
'image/gif' : 'gif'
'%s.text' % od: 'odt',
'%s.spreadsheet' % od: 'ods',
'application/msword': 'doc',
'text/rtf': 'rtf',
'application/pdf': 'pdf',
'image/png': 'png',
'image/jpeg': 'jpg',
'image/pjpeg': 'jpg',
'image/gif': 'gif'
}
# ------------------------------------------------------------------------------