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 -*-
# ------------------------------------------------------------------------------
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.'''

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.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

View file

@ -7,7 +7,7 @@
<label tal:attributes="for chekboxId" class="discreet"
tal:content="python: tool.translate(doLabel)"></label>
</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;
onClick python: 'generatePodDocument(\'%s\',\'%s\',\'%s\',\'%s\')' % (contextObj.UID(), name, podFormat, tool.getQueryInfo());
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 ' \
'2-tuple (fileName, fileContent) or a 3-tuple (fileName, fileContent, ' \
'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:
@ -205,6 +209,41 @@ class AbstractWrapper:
ploneObj.reindexObject()
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):
'''Deletes myself.'''
self.o.delete()