From fd896aebdc7e742fed5357167b40c7aa149521b8 Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Wed, 16 Feb 2011 13:43:58 +0100 Subject: [PATCH] appy.gen: added the possibility to freeze, within Pod fields, documents that are normally generated with appy.pod. --- gen/__init__.py | 173 +++++++++++++++++++++++++++++-- gen/plone25/mixins/ToolMixin.py | 138 ++++-------------------- gen/plone25/skin/widgets/pod.pt | 2 +- gen/plone25/wrappers/__init__.py | 39 +++++++ 4 files changed, 222 insertions(+), 130 deletions(-) diff --git a/gen/__init__.py b/gen/__init__.py index 96d9c72..cad3295 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -1,13 +1,15 @@ # -*- coding: utf-8 -*- # ------------------------------------------------------------------------------ -import re, time, copy, sys, types, os, os.path, mimetypes -from appy.shared.utils import Traceback +import re, time, copy, sys, types, os, os.path, mimetypes, StringIO from appy.gen.layout import Table from appy.gen.layout import defaultFieldLayouts from appy.gen.po import PoMessage from appy.gen.utils import sequenceTypes, GroupDescr, Keywords, FileWrapper, \ getClassName, SomeObjects +import appy.pod +from appy.pod.renderer import Renderer from appy.shared.data import languages +from appy.shared.utils import Traceback, getOsTempFolder # Default Appy permissions ----------------------------------------------------- r, w, d = ('read', 'write', 'delete') @@ -1421,6 +1423,28 @@ class File(Type): width, height, colspan, master, masterValue, focus, historized, True) + @staticmethod + def getFileObject(filePath, fileName=None, zope=False): + '''Returns a File instance as can be stored in the database or + manipulated in code, filled with content from a file on disk, + located at p_filePath. If you want to give it a name that is more + sexy than the actual basename of filePath, specify it in + p_fileName. + + If p_zope is True, it will be the raw Zope object = an instance of + OFS.Image.File. Else, it will be a FileWrapper instance from Appy.''' + f = file(filePath, 'rb') + if not fileName: + fileName = os.path.basename(filePath) + fileId = 'file.%f' % time.time() + import OFS.Image + res = OFS.Image.File(fileId, fileName, f) + res.filename = fileName + res.content_type = mimetypes.guess_type(fileName)[0] + f.close() + if not zope: res = FileWrapper(res) + return res + def getValue(self, obj): value = Type.getValue(self, obj) if value: value = FileWrapper(value) @@ -1505,14 +1529,7 @@ class File(Type): elif isinstance(value, FileWrapper): setattr(obj, self.name, value._atFile) elif isinstance(value, basestring): - f = file(value) - fileName = os.path.basename(value) - fileId = 'file.%f' % time.time() - zopeFile = OFSImageFile(fileId, fileName, f) - zopeFile.filename = fileName - zopeFile.content_type = mimetypes.guess_type(fileName)[0] - setattr(obj, self.name, zopeFile) - f.close() + setattr(obj, self.name, File.getFileObject(value, zope=True)) elif type(value) in sequenceTypes: # It should be a 2-tuple or 3-tuple fileName = None @@ -1913,6 +1930,9 @@ class Pod(Type): '''A pod is a field allowing to produce a (PDF, ODT, Word, RTF...) document from data contained in Appy class and linked objects or anything you want to put in it. It uses appy.pod.''' + POD_ERROR = 'An error occurred while generating the document. Please ' \ + 'contact the system administrator.' + DELETE_TEMP_DOC_ERROR = 'A temporary document could not be removed. %s.' def __init__(self, validator=None, index=None, default=None, optional=False, editDefault=False, show='view', page='main', group=None, layouts=None, move=0, indexed=False, @@ -1920,7 +1940,7 @@ class Pod(Type): specificWritePermission=False, width=None, height=None, colspan=1, master=None, masterValue=None, focus=False, historized=False, template=None, context=None, action=None, - askAction=False, stylesMapping=None): + askAction=False, stylesMapping={}, freezeFormat='pdf'): # The following param stores the path to a POD template self.template = template # The context is a dict containing a specific pod context, or a method @@ -1934,6 +1954,8 @@ class Pod(Type): self.askAction = askAction # A global styles mapping that would apply to the whole template self.stylesMapping = stylesMapping + # Freeze format is by PDF by default + self.freezeFormat = freezeFormat Type.__init__(self, None, (0,1), index, default, optional, False, show, page, group, layouts, move, indexed, searchable, specificReadPermission, @@ -1941,6 +1963,135 @@ class Pod(Type): masterValue, focus, historized, False) self.validable = False + def isFrozen(self, obj): + '''Is there a frozen document for p_self on p_obj?''' + value = getattr(obj.o, self.name, None) + return isinstance(value, obj.o.getProductConfig().File) + + def getToolInfo(self, obj): + '''Gets information related to this field (p_self) that is available in + the tool: the POD template and the available output formats. If this + field is frozen, available output formats are not available anymore: + only the format of the frozen doc is returned.''' + tool = obj.tool + appyClass = tool.o.getAppyClass(obj.o.meta_type) + # Get the output format(s) + if self.isFrozen(obj): + # The only available format is the one from the frozen document + fileName = getattr(obj.o, self.name).filename + formats = (os.path.splitext(fileName)[1][1:],) + else: + # Available formats are those which are selected in the tool. + name = tool.getAttributeName('formats', appyClass, self.name) + formats = getattr(tool, name) + # Get the POD template + name = tool.getAttributeName('podTemplate', appyClass, self.name) + template = getattr(tool, name) + return (template, formats) + + def getValue(self, obj): + '''Gets, on_obj, the value conforming to self's type definition. For a + Pod field, if a file is stored in the field, it means that the + field has been frozen. Else, it means that the value must be + retrieved by calling pod to compute the result.''' + rq = getattr(obj, 'REQUEST', None) + res = getattr(obj, self.name, None) + if res and res.size: + print 'RETURNING FROZEN DOC' + return FileWrapper(res) # Return the frozen file. + # If we are here, it means that we must call pod to compute the file. + # A Pod field differs from other field types because there can be + # several ways to produce the field value (ie: output file format can be + # odt, pdf,...; self.action can be executed or not...). We get those + # precisions about the way to produce the file from the request object + # and from the tool. If we don't find the request object (or if it does + # not exist, ie, when Zope runs in test mode), we use default values. + obj = obj.appy() + tool = obj.tool + # Get POD template and available formats from the tool. + template, availFormats = self.getToolInfo(obj) + # Get the output format + defaultFormat = 'pdf' + if defaultFormat not in availFormats: defaultFormat = availFormats[0] + outputFormat = getattr(rq, 'podFormat', defaultFormat) + # Get or compute the specific POD context + specificContext = None + if callable(self.context): + specificContext = self.callMethod(obj, self.context) + else: + specificContext = self.context + # Temporary file where to generate the result + tempFileName = '%s/%s_%f.%s' % ( + getOsTempFolder(), obj.uid, time.time(), outputFormat) + # Define parameters to give to the appy.pod renderer + podContext = {'tool': tool, 'user': obj.user, 'self': obj, + 'now': obj.o.getProductConfig().DateTime(), + 'projectFolder': tool.getDiskFolder()} + # If the POD document is related to a query, get it from the request, + # execute it and put the result in the context. + isQueryRelated = rq.get('queryData', None) + if isQueryRelated: + # Retrieve query params from the request + cmd = ', '.join(tool.o.queryParamNames) + cmd += " = rq['queryData'].split(';')" + exec cmd + # (re-)execute the query, but without any limit on the number of + # results; return Appy objects. + objs = tool.o.executeQuery(type_name, searchName=search, + sortBy=sortKey, sortOrder=sortOrder, filterKey=filterKey, + filterValue=filterValue, maxResults='NO_LIMIT') + podContext['objects'] = [o.appy() for o in objs['objects']] + if specificContext: + podContext.update(specificContext) + # Define a potential global styles mapping + if callable(self.stylesMapping): + stylesMapping = self.callMethod(obj, self.stylesMapping) + else: + stylesMapping = self.stylesMapping + rendererParams = {'template': StringIO.StringIO(template.content), + 'context': podContext, 'result': tempFileName, + 'stylesMapping': stylesMapping} + if tool.unoEnabledPython: + rendererParams['pythonWithUnoPath'] = tool.unoEnabledPython + if tool.openOfficePort: + rendererParams['ooPort'] = tool.openOfficePort + # Launch the renderer + try: + renderer = Renderer(**rendererParams) + renderer.run() + except appy.pod.PodError, pe: + if not os.path.exists(tempFileName): + # In some (most?) cases, when OO returns an error, the result is + # nevertheless generated. + obj.log(str(pe), type='error') + return Pod.POD_ERROR + # Give a friendly name for this file + fileName = obj.translate(self.labelId) + if not isQueryRelated: + # This is a POD for a single object: personalize the file name with + # the object title. + fileName = '%s-%s' % (obj.title, fileName) + fileName = tool.normalize(fileName) + '.' + outputFormat + # Get a FileWrapper instance from the temp file on the filesystem + res = File.getFileObject(tempFileName, fileName) + # Execute the related action if relevant + doAction = getattr(rq, 'askAction', False) in ('True', True) + if doAction and self.action: self.action(obj, podContext) + # Returns the doc and removes the temp file + try: + os.remove(tempFileName) + except OSError, oe: + obj.log(Pod.DELETE_TEMP_DOC_ERROR % str(oe), type='warning') + except IOError, ie: + obj.log(Pod.DELETE_TEMP_DOC_ERROR % str(ie), type='warning') + return res + + def store(self, obj, value): + '''Stores (=freezes) a document (in p_value) in the field.''' + if isinstance(value, FileWrapper): + value = value._atFile + setattr(obj, self.name, value) + # Workflow-specific types ------------------------------------------------------ class Role: '''Represents a role.''' diff --git a/gen/plone25/mixins/ToolMixin.py b/gen/plone25/mixins/ToolMixin.py index cb91915..6aa02bf 100644 --- a/gen/plone25/mixins/ToolMixin.py +++ b/gen/plone25/mixins/ToolMixin.py @@ -1,9 +1,7 @@ # ------------------------------------------------------------------------------ -import re, os, os.path, time, Cookie, StringIO, types +import re, os, os.path, time, Cookie, types from appy.shared import mimeTypes from appy.shared.utils import getOsTempFolder -import appy.pod -from appy.pod.renderer import Renderer import appy.gen from appy.gen import Type, Search, Selection from appy.gen.utils import SomeObjects, sequenceTypes, getClassName @@ -12,9 +10,6 @@ from appy.gen.plone25.wrappers import AbstractWrapper from appy.gen.plone25.descriptors import ClassDescriptor # Errors ----------------------------------------------------------------------- -DELETE_TEMP_DOC_ERROR = 'A temporary document could not be removed. %s.' -POD_ERROR = 'An error occurred while generating the document. Please ' \ - 'contact the system administrator.' jsMessages = ('no_elem_selected', 'delete_confirm') # ------------------------------------------------------------------------------ @@ -32,127 +27,34 @@ class ToolMixin(BaseMixin): res = '%s%s' % (elems[1], elems[4]) return res - def getPodInfo(self, ploneObj, fieldName): - '''Returns POD-related information about Pod field p_fieldName defined - on class whose p_ploneObj is an instance of.''' - res = {} - appyClass = self.getAppyClass(ploneObj.meta_type) - appyTool = self.appy() - n = appyTool.getAttributeName('formats', appyClass, fieldName) - res['formats'] = getattr(appyTool, n) - n = appyTool.getAttributeName('podTemplate', appyClass, fieldName) - res['template'] = getattr(appyTool, n) - appyType = ploneObj.getAppyType(fieldName) - res['title'] = self.translate(appyType.labelId) - res['context'] = appyType.context - res['action'] = appyType.action - res['stylesMapping'] = appyType.stylesMapping - return res - def getSiteUrl(self): '''Returns the absolute URL of this site.''' return self.portal_url.getPortalObject().absolute_url() + def getPodInfo(self, obj, name): + '''Gets the available POD formats for Pod field named p_name on + p_obj.''' + podField = self.getAppyType(name, className=obj.meta_type) + return podField.getToolInfo(obj.appy()) + def generateDocument(self): '''Generates the document from field-related info. UID of object that is the template target is given in the request.''' rq = self.REQUEST - appyTool = self.appy() - # Get the object - objectUid = rq.get('objectUid') - obj = self.uid_catalog(UID=objectUid)[0].getObject() - appyObj = obj.appy() - # Get information about the document to render. - specificPodContext = None + # Get the object on which a document must be generated. + obj = self.getObject(rq.get('objectUid'), appy=True) fieldName = rq.get('fieldName') - format = rq.get('podFormat') - podInfo = self.getPodInfo(obj, fieldName) - template = podInfo['template'].content - podTitle = podInfo['title'] - if podInfo['context']: - if callable(podInfo['context']): - specificPodContext = podInfo['context'](appyObj) - else: - specificPodContext = podInfo['context'] - doAction = rq.get('askAction') == 'True' - # Temporary file where to generate the result - tempFileName = '%s/%s_%f.%s' % ( - getOsTempFolder(), obj.UID(), time.time(), format) - # Define parameters to give to the appy.pod renderer - currentUser = self.portal_membership.getAuthenticatedMember() - podContext = {'tool': appyTool, 'user': currentUser, 'self': appyObj, - 'now': self.getProductConfig().DateTime(), - 'projectFolder': appyTool.getDiskFolder(), - } - # If the POD document is related to a query, get it from the request, - # execute it and put the result in the context. - if rq['queryData']: - # Retrieve query params from the request - cmd = ', '.join(self.queryParamNames) - cmd += " = rq['queryData'].split(';')" - exec cmd - # (re-)execute the query, but without any limit on the number of - # results; return Appy objects. - objs = self.executeQuery(type_name, searchName=search, - sortBy=sortKey, sortOrder=sortOrder, filterKey=filterKey, - filterValue=filterValue, maxResults='NO_LIMIT') - podContext['objects'] = [o.appy() for o in objs['objects']] - if specificPodContext: - podContext.update(specificPodContext) - # Define a potential global styles mapping - stylesMapping = None - if podInfo['stylesMapping']: - if callable(podInfo['stylesMapping']): - stylesMapping = podInfo['stylesMapping'](appyObj) - else: - stylesMapping = podInfo['stylesMapping'] - rendererParams = {'template': StringIO.StringIO(template), - 'context': podContext, 'result': tempFileName} - if stylesMapping: - rendererParams['stylesMapping'] = stylesMapping - if appyTool.unoEnabledPython: - rendererParams['pythonWithUnoPath'] = appyTool.unoEnabledPython - if appyTool.openOfficePort: - rendererParams['ooPort'] = appyTool.openOfficePort - # Launch the renderer - try: - renderer = Renderer(**rendererParams) - renderer.run() - except appy.pod.PodError, pe: - if not os.path.exists(tempFileName): - # In some (most?) cases, when OO returns an error, the result is - # nevertheless generated. - appyTool.log(str(pe), type='error') - appyTool.say(POD_ERROR) - return self.goto(rq.get('HTTP_REFERER')) - # Open the temp file on the filesystem - f = file(tempFileName, 'rb') - res = f.read() - # Identify the filename to return - if rq['queryData']: - # This is a POD for a bunch of objects - fileName = podTitle - else: - # This is a POD for a single object: personalize the file name with - # the object title. - fileName = '%s-%s' % (obj.Title(), podTitle) - fileName = appyTool.normalize(fileName) - response = obj.REQUEST.RESPONSE - response.setHeader('Content-Type', mimeTypes[format]) - response.setHeader('Content-Disposition', 'inline;filename="%s.%s"' % \ - (fileName, format)) - f.close() - # Execute the related action if relevant - if doAction and podInfo['action']: - podInfo['action'](appyObj, podContext) - # Returns the doc and removes the temp file - try: - os.remove(tempFileName) - except OSError, oe: - appyTool.log(DELETE_TEMP_DOC_ERROR % str(oe), type='warning') - except IOError, ie: - appyTool.log(DELETE_TEMP_DOC_ERROR % str(ie), type='warning') - return res + res = getattr(obj, fieldName) + if isinstance(res, basestring): + # An error has occurred, and p_res contains the error message + obj.say(res) + return self.goto(rq.get('HTTP_REFERER')) + # res contains a FileWrapper instance. + response = rq.RESPONSE + response.setHeader('Content-Type', res.mimeType) + response.setHeader('Content-Disposition', + 'inline;filename="%s"' % res.name) + return res.content def getAttr(self, name): '''Gets attribute named p_attrName. Useful because we can't use getattr diff --git a/gen/plone25/skin/widgets/pod.pt b/gen/plone25/skin/widgets/pod.pt index a5b42f1..f09db67 100644 --- a/gen/plone25/skin/widgets/pod.pt +++ b/gen/plone25/skin/widgets/pod.pt @@ -7,7 +7,7 @@ -