diff --git a/gen/installer.py b/gen/installer.py index cd60316..22e65d4 100644 --- a/gen/installer.py +++ b/gen/installer.py @@ -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: diff --git a/gen/po.py b/gen/po.py index 44ceb01..f4301b1 100644 --- a/gen/po.py +++ b/gen/po.py @@ -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 ' \ diff --git a/gen/ui/ods.png b/gen/ui/ods.png new file mode 100644 index 0000000..04e71a5 Binary files /dev/null and b/gen/ui/ods.png differ diff --git a/gen/ui/xls.png b/gen/ui/xls.png new file mode 100644 index 0000000..af1e82d Binary files /dev/null and b/gen/ui/xls.png differ diff --git a/gen/wrappers/ToolWrapper.py b/gen/wrappers/ToolWrapper.py index 74fa49a..578e0de 100644 --- a/gen/wrappers/ToolWrapper.py +++ b/gen/wrappers/ToolWrapper.py @@ -54,7 +54,7 @@ class ToolWrapper(AbstractWrapper): (user.o.absolute_url(), user.title,access)) return res + '\n'.join(rows) + '' - 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) # ------------------------------------------------------------------------------ diff --git a/pod/converter.py b/pod/converter.py index b4a50e3..d44e64f 100644 --- a/pod/converter.py +++ b/pod/converter.py @@ -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() diff --git a/pod/renderer.py b/pod/renderer.py index ea3ff3c..3aaf4a4 100644 --- a/pod/renderer.py +++ b/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) # ------------------------------------------------------------------------------ diff --git a/shared/__init__.py b/shared/__init__.py index 816699c..bf93f3e 100644 --- a/shared/__init__.py +++ b/shared/__init__.py @@ -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' } # ------------------------------------------------------------------------------