appy.gen: added the possibility to freeze, within Pod fields, documents that are normally generated with appy.pod.

This commit is contained in:
Gaetan Delannay 2011-02-16 13:43:58 +01:00
parent a18be357f5
commit fd896aebdc
4 changed files with 222 additions and 130 deletions

View file

@ -1,13 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import re, time, copy, sys, types, os, os.path, mimetypes import re, time, copy, sys, types, os, os.path, mimetypes, StringIO
from appy.shared.utils import Traceback
from appy.gen.layout import Table from appy.gen.layout import Table
from appy.gen.layout import defaultFieldLayouts from appy.gen.layout import defaultFieldLayouts
from appy.gen.po import PoMessage from appy.gen.po import PoMessage
from appy.gen.utils import sequenceTypes, GroupDescr, Keywords, FileWrapper, \ from appy.gen.utils import sequenceTypes, GroupDescr, Keywords, FileWrapper, \
getClassName, SomeObjects getClassName, SomeObjects
import appy.pod
from appy.pod.renderer import Renderer
from appy.shared.data import languages from appy.shared.data import languages
from appy.shared.utils import Traceback, getOsTempFolder
# Default Appy permissions ----------------------------------------------------- # Default Appy permissions -----------------------------------------------------
r, w, d = ('read', 'write', 'delete') r, w, d = ('read', 'write', 'delete')
@ -1421,6 +1423,28 @@ class File(Type):
width, height, colspan, master, masterValue, focus, width, height, colspan, master, masterValue, focus,
historized, True) 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): def getValue(self, obj):
value = Type.getValue(self, obj) value = Type.getValue(self, obj)
if value: value = FileWrapper(value) if value: value = FileWrapper(value)
@ -1505,14 +1529,7 @@ class File(Type):
elif isinstance(value, FileWrapper): elif isinstance(value, FileWrapper):
setattr(obj, self.name, value._atFile) setattr(obj, self.name, value._atFile)
elif isinstance(value, basestring): elif isinstance(value, basestring):
f = file(value) setattr(obj, self.name, File.getFileObject(value, zope=True))
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()
elif type(value) in sequenceTypes: elif type(value) in sequenceTypes:
# It should be a 2-tuple or 3-tuple # It should be a 2-tuple or 3-tuple
fileName = None fileName = None
@ -1913,6 +1930,9 @@ class Pod(Type):
'''A pod is a field allowing to produce a (PDF, ODT, Word, RTF...) document '''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 from data contained in Appy class and linked objects or anything you
want to put in it. It uses appy.pod.''' 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, def __init__(self, validator=None, index=None, default=None,
optional=False, editDefault=False, show='view', optional=False, editDefault=False, show='view',
page='main', group=None, layouts=None, move=0, indexed=False, page='main', group=None, layouts=None, move=0, indexed=False,
@ -1920,7 +1940,7 @@ class Pod(Type):
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False, colspan=1, master=None, masterValue=None, focus=False,
historized=False, template=None, context=None, action=None, 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 # The following param stores the path to a POD template
self.template = template self.template = template
# The context is a dict containing a specific pod context, or a method # The context is a dict containing a specific pod context, or a method
@ -1934,6 +1954,8 @@ class Pod(Type):
self.askAction = askAction self.askAction = askAction
# A global styles mapping that would apply to the whole template # A global styles mapping that would apply to the whole template
self.stylesMapping = stylesMapping self.stylesMapping = stylesMapping
# Freeze format is by PDF by default
self.freezeFormat = freezeFormat
Type.__init__(self, None, (0,1), index, default, optional, Type.__init__(self, None, (0,1), index, default, optional,
False, show, page, group, layouts, move, indexed, False, show, page, group, layouts, move, indexed,
searchable, specificReadPermission, searchable, specificReadPermission,
@ -1941,6 +1963,135 @@ class Pod(Type):
masterValue, focus, historized, False) masterValue, focus, historized, False)
self.validable = 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 ------------------------------------------------------ # Workflow-specific types ------------------------------------------------------
class Role: class Role:
'''Represents a role.''' '''Represents a role.'''

View file

@ -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 import mimeTypes
from appy.shared.utils import getOsTempFolder from appy.shared.utils import getOsTempFolder
import appy.pod
from appy.pod.renderer import Renderer
import appy.gen import appy.gen
from appy.gen import Type, Search, Selection from appy.gen import Type, Search, Selection
from appy.gen.utils import SomeObjects, sequenceTypes, getClassName 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 from appy.gen.plone25.descriptors import ClassDescriptor
# Errors ----------------------------------------------------------------------- # 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') jsMessages = ('no_elem_selected', 'delete_confirm')
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -32,127 +27,34 @@ class ToolMixin(BaseMixin):
res = '%s%s' % (elems[1], elems[4]) res = '%s%s' % (elems[1], elems[4])
return res 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): def getSiteUrl(self):
'''Returns the absolute URL of this site.''' '''Returns the absolute URL of this site.'''
return self.portal_url.getPortalObject().absolute_url() 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): def generateDocument(self):
'''Generates the document from field-related info. UID of object that '''Generates the document from field-related info. UID of object that
is the template target is given in the request.''' is the template target is given in the request.'''
rq = self.REQUEST rq = self.REQUEST
appyTool = self.appy() # Get the object on which a document must be generated.
# Get the object obj = self.getObject(rq.get('objectUid'), appy=True)
objectUid = rq.get('objectUid')
obj = self.uid_catalog(UID=objectUid)[0].getObject()
appyObj = obj.appy()
# Get information about the document to render.
specificPodContext = None
fieldName = rq.get('fieldName') fieldName = rq.get('fieldName')
format = rq.get('podFormat') res = getattr(obj, fieldName)
podInfo = self.getPodInfo(obj, fieldName) if isinstance(res, basestring):
template = podInfo['template'].content # An error has occurred, and p_res contains the error message
podTitle = podInfo['title'] obj.say(res)
if podInfo['context']: return self.goto(rq.get('HTTP_REFERER'))
if callable(podInfo['context']): # res contains a FileWrapper instance.
specificPodContext = podInfo['context'](appyObj) response = rq.RESPONSE
else: response.setHeader('Content-Type', res.mimeType)
specificPodContext = podInfo['context'] response.setHeader('Content-Disposition',
doAction = rq.get('askAction') == 'True' 'inline;filename="%s"' % res.name)
# Temporary file where to generate the result return res.content
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
def getAttr(self, name): def getAttr(self, name):
'''Gets attribute named p_attrName. Useful because we can't use getattr '''Gets attribute named p_attrName. Useful because we can't use getattr

View file

@ -7,7 +7,7 @@
<label tal:attributes="for chekboxId" class="discreet" <label tal:attributes="for chekboxId" class="discreet"
tal:content="python: tool.translate(doLabel)"></label> tal:content="python: tool.translate(doLabel)"></label>
</tal:askAction> </tal:askAction>
<img tal:repeat="podFormat python: tool.getPodInfo(contextObj, name)['formats']" <img tal:repeat="podFormat python: tool.getPodInfo(contextObj, name)[1]"
tal:attributes="src string: $portal_url/skyn/${podFormat}.png; tal:attributes="src string: $portal_url/skyn/${podFormat}.png;
onClick python: 'generatePodDocument(\'%s\',\'%s\',\'%s\',\'%s\')' % (contextObj.UID(), name, podFormat, tool.getQueryInfo()); onClick python: 'generatePodDocument(\'%s\',\'%s\',\'%s\',\'%s\')' % (contextObj.UID(), name, podFormat, tool.getQueryInfo());
title podFormat/capitalize" title podFormat/capitalize"

View file

@ -14,6 +14,10 @@ from appy.shared.csv_parser import CsvMarshaller
WRONG_FILE_TUPLE = 'This is not the way to set a file. You can specify a ' \ WRONG_FILE_TUPLE = 'This is not the way to set a file. You can specify a ' \
'2-tuple (fileName, fileContent) or a 3-tuple (fileName, fileContent, ' \ '2-tuple (fileName, fileContent) or a 3-tuple (fileName, fileContent, ' \
'mimeType).' 'mimeType).'
FREEZE_ERROR = 'Error while trying to freeze a "%s" file in POD field ' \
'"%s" (%s).'
FREEZE_FATAL_ERROR = 'A server error occurred. Please contact the system ' \
'administrator.'
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class AbstractWrapper: class AbstractWrapper:
@ -205,6 +209,41 @@ class AbstractWrapper:
ploneObj.reindexObject() ploneObj.reindexObject()
return appyObj return appyObj
def freeze(self, fieldName, doAction=False):
'''This method freezes a POD document. TODO: allow to freeze Computed
fields.'''
rq = self.request
field = self.o.getAppyType(fieldName)
if field.type != 'Pod': raise 'Cannot freeze non-Pod field.'
# Perform the related action if required.
if doAction: self.request.set('askAction', True)
# Set the freeze format
rq.set('podFormat', field.freezeFormat)
# Generate the document.
doc = field.getValue(self.o)
if isinstance(doc, basestring):
self.log(FREEZE_ERROR % (field.freezeFormat, field.name, doc),
type='error')
if field.freezeFormat == 'odt': raise FREEZE_FATAL_ERROR
self.log('Trying to freeze the ODT version...')
# Try to freeze the ODT version of the document, which does not
# require to call OpenOffice/LibreOffice, so the risk of error is
# smaller.
self.request.set('podFormat', 'odt')
doc = field.getValue(self.o)
if isinstance(doc, basestring):
self.log(FREEZE_ERROR % ('odt', field.name, doc), type='error')
raise FREEZE_FATAL_ERROR
field.store(self.o, doc)
def unFreeze(self, fieldName):
'''This method un freezes a POD document. TODO: allow to unfreeze
Computed fields.'''
rq = self.request
field = self.o.getAppyType(fieldName)
if field.type != 'Pod': raise 'Cannot unFreeze non-Pod field.'
field.store(self.o, None)
def delete(self): def delete(self):
'''Deletes myself.''' '''Deletes myself.'''
self.o.delete() self.o.delete()