[pod+gen] Added POD rendering based on ODS templates. Integrated with gen.
This commit is contained in:
parent
d5d99b67eb
commit
43261fde60
|
@ -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:
|
||||
|
|
|
@ -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
BIN
gen/ui/ods.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 731 B |
BIN
gen/ui/xls.png
Normal file
BIN
gen/ui/xls.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 314 B |
|
@ -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)
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
@ -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()
|
||||
|
|
141
pod/renderer.py
141
pod/renderer.py
|
@ -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)
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
@ -4,13 +4,16 @@ 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',
|
||||
'%s.text' % od: 'odt',
|
||||
'%s.spreadsheet' % od: 'ods',
|
||||
'application/msword': 'doc',
|
||||
'text/rtf': 'rtf',
|
||||
'application/pdf': 'pdf',
|
||||
|
|
Loading…
Reference in a new issue