From e89eda4838d07c8b76a306909763fe3e9aae2ffc Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Thu, 17 Dec 2009 21:14:52 +0100 Subject: [PATCH] More input and output formats for OO conversion in converter.py and bugfix in skyn edit. --- gen/__init__.py | 2 +- gen/plone25/mixins/__init__.py | 12 +- gen/plone25/skin/edit.pt | 28 ++-- gen/plone25/skin/macros.pt | 2 +- gen/plone25/wrappers/__init__.py | 41 +++++- pod/converter.py | 212 +++++++++++++++++++------------ pod/renderer.py | 6 +- 7 files changed, 190 insertions(+), 113 deletions(-) diff --git a/gen/__init__.py b/gen/__init__.py index e1825c6..ca4d2bb 100755 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -355,7 +355,7 @@ class Ref(Type): class Computed(Type): def __init__(self, validator=None, multiplicity=(0,1), index=None, - default=None, optional=False, editDefault=False, show=True, + default=None, optional=False, editDefault=False, show='view', page='main', group=None, move=0, indexed=False, searchable=False, specificReadPermission=False, specificWritePermission=False, width=None, height=None, diff --git a/gen/plone25/mixins/__init__.py b/gen/plone25/mixins/__init__.py index 8afe3ed..98c608a 100644 --- a/gen/plone25/mixins/__init__.py +++ b/gen/plone25/mixins/__init__.py @@ -33,9 +33,13 @@ class AbstractMixin: # creates the final object from the temp object. previousData = None if not created: previousData = self.rememberPreviousData() - # We do not process form data (=real update on the object) if the tool - # itself is being created. - if obj._appy_meta_type != 'tool': obj.processForm() + # Perform the change on the object, unless self is a tool being created. + if (obj._appy_meta_type == 'tool') and created: + # We do not process form data (=real update on the object) if the + # tool itself is being created. + pass + else: + obj.processForm() if previousData: # Keep in history potential changes on historized fields self.historizeData(previousData) @@ -389,7 +393,7 @@ class AbstractMixin: fieldDescr = fieldDescr.__dict__ appyType = fieldDescr['appyType'] if isEdit and (appyType['type']=='Ref') and appyType['add']:return False - if isEdit and (appyType['type'] in ('Action', 'Computed')): return False + if isEdit and (appyType['type'] == 'Action'): return False if (fieldDescr['widgetType'] == 'backField') and \ not self.getBRefs(fieldDescr['fieldRel']): return False # Do not show field if it is optional and not selected in flavour diff --git a/gen/plone25/skin/edit.pt b/gen/plone25/skin/edit.pt index 92df734..8fbd5d4 100644 --- a/gen/plone25/skin/edit.pt +++ b/gen/plone25/skin/edit.pt @@ -3,23 +3,19 @@ errors request/errors | python:{}; Iterator python:modules['Products.Archetypes'].IndexIterator; schematas contextObj/Schemata; - fieldsets python:[key for key in schematas.keys() if (key != 'metadata') and (schematas[key].editableFields(here, visible_only=True))]; + fieldsets python:[key for key in schematas.keys() if (key != 'metadata') and (schematas[key].editableFields(contextObj, visible_only=True))]; default_fieldset python:(not schematas or schematas.has_key('default')) and 'default' or fieldsets[0]; fieldset request/fieldset|options/fieldset|default_fieldset; - fields python:schematas[fieldset].editableFields(here); - dummy python:here.at_isEditable(fields); - portal_type python:here.getPortalTypeName().lower().replace(' ', '_'); - type_name here/getPortalTypeName|here/archetype_name; - lockable python:hasattr(here, 'wl_isLocked'); - isLocked python:lockable and here.wl_isLocked(); - tabindex tabindex|python:Iterator(pos=7000); + fields python:schematas[fieldset].editableFields(contextObj); + lockable python:hasattr(contextObj, 'wl_isLocked'); + isLocked python:lockable and contextObj.wl_isLocked(); isEdit python:True; tool contextObj/getTool; flavour python: tool.getFlavour(contextObj); appFolder tool/getAppFolder; appName appFolder/id; - css python:here.getUniqueWidgetAttr(fields, 'helper_css'); - js python:here.getUniqueWidgetAttr(fields, 'helper_js'); + css python:contextObj.getUniqueWidgetAttr(fields, 'helper_css'); + js python:contextObj.getUniqueWidgetAttr(fields, 'helper_js'); phaseInfo python: contextObj.getAppyPhases(fieldset=fieldset, forPlone=True); phase request/phase|phaseInfo/name; pageName python: contextObj.getAppyPage(isEdit, phaseInfo);"> @@ -91,24 +87,20 @@ + tal:attributes="disabled python:test(isLocked, 'disabled', None);"/> + tal:attributes="disabled python:test(isLocked, 'disabled', None);"/> + tal:attributes="disabled python:test(isLocked, 'disabled', None);"/> + i18n:attributes="value label_cancel;" i18n:domain="plone"/>
diff --git a/gen/plone25/skin/macros.pt b/gen/plone25/skin/macros.pt index c5429ef..98bbf5b 100644 --- a/gen/plone25/skin/macros.pt +++ b/gen/plone25/skin/macros.pt @@ -202,7 +202,7 @@ - + diff --git a/gen/plone25/wrappers/__init__.py b/gen/plone25/wrappers/__init__.py index 164633f..9046391 100644 --- a/gen/plone25/wrappers/__init__.py +++ b/gen/plone25/wrappers/__init__.py @@ -2,7 +2,8 @@ developer the real classes used by the underlying web framework.''' # ------------------------------------------------------------------------------ -import time, os.path, mimetypes, unicodedata, random +import os, os.path, time, mimetypes, unicodedata, random +import appy.pod from appy.gen import Search from appy.gen.utils import sequenceTypes from appy.shared.utils import getOsTempFolder @@ -236,15 +237,23 @@ class AbstractWrapper: wfTool.doActionFor(self.o, transitionName, comment=comment) del self.o._v_appy_do - def log(self, message, logLevel='info'): + def log(self, message, type='info'): '''Logs a message in the log file. p_logLevel may be "info", "warning" or "error".''' logger = self.o.getProductConfig().logger - if logLevel == 'warning': logMethod = logger.warn - elif logLevel == 'error': logMethod = logger.error + if type == 'warning': logMethod = logger.warn + elif type == 'error': logMethod = logger.error else: logMethod = logger.info logMethod(message) + def say(self, message, type='info'): + '''Prints a message in the user interface. p_logLevel may be "info", + "warning" or "error".''' + mType = type + if mType == 'warning': mType = 'warn' + elif mType == 'error': mType = 'stop' + self.o.plone_utils.addPortalMessage(message, type=mType) + def normalize(self, s): '''Returns a version of string p_s whose special chars have been replaced with normal chars.''' @@ -338,11 +347,16 @@ class FileWrapper: raise 'Impossible to set attribute %s. "Settable" attributes ' \ 'are "name", "content" and "mimeType".' % name - def dump(self, filePath=None): + def dump(self, filePath=None, format=None, tool=None): '''Writes the file on disk. If p_filePath is specified, it is the path name where the file will be dumped; folders mentioned in it must exist. If not, the file will be dumped in the OS temp folder. - The absolute path name of the dumped file is returned.''' + The absolute path name of the dumped file is returned. + If an error occurs, the method returns None. If p_format is + specified, OpenOffice will be called for converting the dumped file + to the desired format. In this case, p_tool, a Appy tool, must be + provided. Indeed, any Appy tool contains parameters for contacting + OpenOffice in server mode.''' if not filePath: filePath = '%s/file%f.%s' % (getOsTempFolder(), time.time(), self.name) @@ -358,5 +372,20 @@ class FileWrapper: # Only one chunk f.write(self.content) f.close() + if format: + if not tool: return + # Convert the dumped file using OpenOffice + convScript = '%s/converter.py' % os.path.dirname(appy.pod.__file__) + cmd = '%s %s "%s" %s -p%d' % (tool.unoEnabledPython, convScript, + filePath, format, tool.openOfficePort) + res = os.system(cmd) + os.remove(filePath) + if res != 0: return + # Return the name of the converted file. + baseName, ext = os.path.splitext(filePath) + if (ext == '.%s' % format): + filePath = '%s.res.%s' % (baseName, format) + else: + filePath = '%s.%s' % (baseName, format) return filePath # ------------------------------------------------------------------------------ diff --git a/pod/converter.py b/pod/converter.py index cfcbc02..d093412 100755 --- a/pod/converter.py +++ b/pod/converter.py @@ -20,15 +20,26 @@ import sys, os, os.path, time, signal from optparse import OptionParser -ODT_FILE_TYPES = {'doc': 'MS Word 97', # Could be 'MS Word 2003 XML' - 'pdf': 'writer_pdf_Export', - 'rtf': 'Rich Text Format', - 'txt': 'Text', - 'html': 'HTML (StarWriter)', - 'htm': 'HTML (StarWriter)', - 'odt': 'ODT'} -# Conversion to ODT does not make any conversion; it simply updates indexes and -# linked documents. +htmlFilters = {'odt': 'HTML (StarWriter)', + 'ods': 'HTML (StarCalc)', + 'odp': 'impress_html_Export'} + +FILE_TYPES = {'odt': 'writer8', + 'ods': 'calc8', + 'odp': 'impress8', + 'htm': htmlFilters, 'html': htmlFilters, + 'rtf': 'Rich Text Format', + 'txt': 'Text', + 'csv': 'Text - txt - csv (StarCalc)', + 'pdf': {'odt': 'writer_pdf_Export', 'ods': 'calc_pdf_Export', + 'odp': 'impress_pdf_Export', 'odg': 'draw_pdf_Export'}, + 'swf': 'impress_flash_Export', + 'doc': 'MS Word 97', + 'xls': 'MS Excel 97', + 'ppt': 'MS PowerPoint 97', +} +# Conversion from odt to odt does not make any conversion, but updates indexes +# and linked documents. # ------------------------------------------------------------------------------ class ConverterError(Exception): pass @@ -46,7 +57,7 @@ DEFAULT_PORT = 2002 # ------------------------------------------------------------------------------ class Converter: - '''Converts an ODT document into pdf, doc, txt or rtf.''' + '''Converts an document readable by OpenOffice into pdf, doc, txt or rtf.''' exeVariants = ('soffice.exe', 'soffice') pathReplacements = {'program files': 'progra~1', 'openoffice.org 1': 'openof~1', @@ -54,41 +65,64 @@ class Converter: } def __init__(self, docPath, resultType, port=DEFAULT_PORT): self.port = port - self.docUrl, self.docPath = self.getDocUrls(docPath) - self.resultFilter = self.getResultFilter(resultType) - self.resultUrl = self.getResultUrl(resultType) + self.docUrl, self.docPath = self.getInputUrls(docPath) + self.inputType = os.path.splitext(docPath)[1][1:].lower() + self.resultType = resultType + self.resultFilter = self.getResultFilter() + self.resultUrl = self.getResultUrl() self.ooContext = None - self.oo = None # OpenOffice application object - self.doc = None # OpenOffice loaded document - def getDocUrls(self, docPath): + self.oo = None # The OpenOffice application object + self.doc = None # The OpenOffice loaded document + + def getInputUrls(self, docPath): + '''Returns the absolute path of the input file. In fact, it returns a + tuple with some URL version of the path for OO as the first element + and the absolute path as the second element.''' import uno if not os.path.exists(docPath) and not os.path.isfile(docPath): raise ConverterError(DOC_NOT_FOUND % docPath) docAbsPath = os.path.abspath(docPath) # Return one path for OO, one path for me. return uno.systemPathToFileUrl(docAbsPath), docAbsPath - def getResultFilter(self, resultType): - if ODT_FILE_TYPES.has_key(resultType): - res = ODT_FILE_TYPES[resultType] + + def getResultFilter(self): + '''Based on the result type, identifies which OO filter to use for the + document conversion.''' + if FILE_TYPES.has_key(self.resultType): + res = FILE_TYPES[self.resultType] + if isinstance(res, dict): + res = res[self.inputType] else: - raise ConverterError(BAD_RESULT_TYPE % (resultType, - ODT_FILE_TYPES.keys())) + raise ConverterError(BAD_RESULT_TYPE % (self.resultType, + FILE_TYPES.keys())) return res - def getResultUrl(self, resultType): + + def getResultUrl(self): + '''Returns the path of the result file in the format needed by OO. 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 + .res.. + + Else, the result file is named like the input file but with a + different extension: + . + ''' import uno baseName = os.path.splitext(self.docPath)[0] - if resultType != 'odt': - res = '%s.%s' % (baseName, resultType) + if self.resultType != self.inputType: + res = '%s.%s' % (baseName, self.resultType) else: - res = '%s.res.%s' % (baseName, resultType) + res = '%s.res.%s' % (baseName, self.resultType) try: f = open(res, 'w') f.write('Hello') f.close() os.remove(res) return uno.systemPathToFileUrl(res) - except OSError, oe: - raise ConverterError(CANNOT_WRITE_RESULT % (res, oe)) + except (OSError, IOError), ioe: + raise ConverterError(CANNOT_WRITE_RESULT % (res, ioe)) + def connect(self): '''Connects to OpenOffice''' if os.name == 'nt': @@ -115,73 +149,90 @@ class Converter: 'com.sun.star.frame.Desktop', self.ooContext) except NoConnectException, nce: raise ConverterError(CONNECT_ERROR % (self.port, nce)) - def disconnect(self): - self.doc.close(True) - # Do a nasty thing before exiting the python process. In case the - # last call is a oneway call (e.g. see idl-spec of insertString), - # it must be forced out of the remote-bridge caches before python - # exits the process. Otherwise, the oneway call may or may not reach - # the target object. - # I do this here by calling a cheap synchronous call (getPropertyValue). - self.ooContext.ServiceManager - def loadDocument(self): - from com.sun.star.lang import IllegalArgumentException, \ - IndexOutOfBoundsException + + def updateOdtDocument(self): + '''If the input file is an ODT document, we will perform 2 tasks: + 1) Update all annexes; + 2) Update sections (if sections refer to external content, we try to + include the content within the result file) + ''' + from com.sun.star.lang import IndexOutOfBoundsException # I need to use IndexOutOfBoundsException because sometimes, when # using sections.getCount, UNO returns a number that is bigger than # the real number of sections (this is because it also counts the # sections that are present within the sub-documents to integrate) + # Update all indexes + indexes = self.doc.getDocumentIndexes() + indexesCount = indexes.getCount() + if indexesCount != 0: + for i in range(indexesCount): + try: + indexes.getByIndex(i).update() + except IndexOutOfBoundsException: + pass + # Update sections + self.doc.updateLinks() + sections = self.doc.getTextSections() + sectionsCount = sections.getCount() + if sectionsCount != 0: + for i in range(sectionsCount-1, -1, -1): + # I must walk into the section from last one to the first + # one. Else, when "disposing" sections, I remove sections + # and the remaining sections other indexes. + try: + section = sections.getByIndex(i) + if section.FileLink and section.FileLink.FileURL: + section.dispose() # This method removes the + #
tags without removing the content + # of the section. Else, it won't appear. + except IndexOutOfBoundsException: + pass + + def loadDocument(self): + from com.sun.star.lang import IllegalArgumentException, \ + IndexOutOfBoundsException from com.sun.star.beans import PropertyValue try: - # Load the document to convert in a new hidden frame + # Loads the document to convert in a new hidden frame prop = PropertyValue() prop.Name = 'Hidden' prop.Value = True self.doc = self.oo.loadComponentFromURL(self.docUrl, "_blank", 0, (prop,)) - # Update all indexes - indexes = self.doc.getDocumentIndexes() - indexesCount = indexes.getCount() - if indexesCount != 0: - for i in range(indexesCount): - try: - indexes.getByIndex(i).update() - except IndexOutOfBoundsException: - pass - # Update sections - self.doc.updateLinks() - sections = self.doc.getTextSections() - sectionsCount = sections.getCount() - if sectionsCount != 0: - for i in range(sectionsCount-1, -1, -1): - # I must walk into the section from last one to the first - # one. Else, when "disposing" sections, I remove sections - # and the remaining sections other indexes. - try: - section = sections.getByIndex(i) - if section.FileLink and section.FileLink.FileURL: - section.dispose() # This method removes the - #
tags without removing the content - # of the section. Else, it won't appear. - except IndexOutOfBoundsException: - pass + if self.inputType == 'odt': + # Perform additional tasks for odt documents + self.updateOdtDocument() + try: + self.doc.refresh() + except AttributeError: + pass except IllegalArgumentException, iae: raise ConverterError(URL_NOT_FOUND % (self.docPath, iae)) + def convertDocument(self): - if self.resultFilter != 'ODT': - # I must really perform a conversion - from com.sun.star.beans import PropertyValue - prop = PropertyValue() - prop.Name = 'FilterName' - prop.Value = self.resultFilter - self.doc.storeToURL(self.resultUrl, (prop,)) - else: - self.doc.storeToURL(self.resultUrl, ()) + '''Calls OO to perform a document conversion. Note that the conversion + is not really done if the source and target documents have the same + type.''' + properties = [] + from com.sun.star.beans import PropertyValue + prop = PropertyValue() + prop.Name = 'FilterName' + prop.Value = self.resultFilter + properties.append(prop) + if self.resultType == 'csv': + # For CSV export, add options (separator, etc) + optionsProp = PropertyValue() + optionsProp.Name = 'FilterOptions' + optionsProp.Value = '59,34,76,1' + properties.append(optionsProp) + self.doc.storeToURL(self.resultUrl, tuple(properties)) + def run(self): + '''Connects to OO, does the job and disconnects.''' self.connect() self.loadDocument() self.convertDocument() - self.disconnect() + self.doc.close(True) # ConverterScript-related messages --------------------------------------------- WRONG_NB_OF_ARGS = 'Wrong number of arguments.' @@ -191,12 +242,13 @@ ERROR_CODE = 1 class ConverterScript: usage = 'usage: python converter.py fileToConvert outputType [options]\n' \ ' where fileToConvert is the absolute or relative pathname of\n' \ - ' the ODT file you want to convert;\n'\ + ' the file you want to convert (or whose content like\n' \ + ' indexes need to be refreshed);\n'\ ' 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\n' \ - ' which is included in the OpenOffice.org distribution).' % \ - str(ODT_FILE_TYPES.keys()) + ' "python" should be a UNO-enabled Python interpreter (ie the ' \ + ' one which is included in the OpenOffice.org distribution).' % \ + str(FILE_TYPES.keys()) def run(self): optParser = OptionParser(usage=ConverterScript.usage) optParser.add_option("-p", "--port", dest="port", diff --git a/pod/renderer.py b/pod/renderer.py index f520277..6864c83 100755 --- a/pod/renderer.py +++ b/pod/renderer.py @@ -25,7 +25,7 @@ import appy.pod from appy.pod import PodError from appy.shared.xml_parser import XmlElement from appy.pod.pod_parser import PodParser, PodEnvironment, OdInsert -from appy.pod.converter import ODT_FILE_TYPES +from appy.pod.converter import FILE_TYPES from appy.pod.buffers import FileBuffer from appy.pod.xhtml2odt import Xhtml2OdtConverter from appy.pod.doc_importers import OdtImporter, ImageImporter, PdfImporter @@ -423,9 +423,9 @@ class Renderer: os.rename(resultOdtName, self.result) else: if resultType.startswith('.'): resultType = resultType[1:] - if not resultType in ODT_FILE_TYPES.keys(): + if not resultType in FILE_TYPES.keys(): raise PodError(BAD_RESULT_TYPE % ( - self.result, ODT_FILE_TYPES.keys())) + self.result, FILE_TYPES.keys())) # Call OpenOffice to perform the conversion or document update self.callOpenOffice(resultOdtName, resultType) # I have the result. Move it to the correct name