[gen] Pod fields can now be configured with several templates.

This commit is contained in:
Gaetan Delannay 2014-03-19 23:13:31 +01:00
parent 889289407f
commit ecc3a8c39b
10 changed files with 175 additions and 122 deletions

View file

@ -28,32 +28,76 @@ WRONG_FILE_TUPLE = 'This is not the way to set a file. You can specify a ' \
'mimeType).'
CONVERSION_ERROR = 'An error occurred. %s'
def guessMimeType(fileName):
'''Try to find the MIME type of file p_fileName.'''
return mimetypes.guess_type(fileName)[0] or File.defaultMimeType
def osPathJoin(*pathElems):
'''Version of os.path.elems that takes care of path elems being empty
strings.'''
return os.path.join(*pathElems).rstrip(os.sep)
# ------------------------------------------------------------------------------
class FileInfo:
'''For a "file" field, its binary content is stored on the filesystem.
Within the database, we store a FileInfo instance that only stores some
metadata.'''
'''A FileInfo instance holds metadata about a file on the filesystem.
For every File field, we will store a FileInfo instance in the dabatase;
the real file will be stored in the Appy/ZODB database-managed
filesystem.
This is the primary usage of FileInfo instances. FileInfo instances can
also be used every time we need to manipulate a file. For example, when
getting the content of a Pod field, a temporary file may be generated and
you will get a FileInfo that represents it.
'''
BYTES = 5000
def __init__(self, fsPath):
# The path on disk (from the root DB folder) where the file will be
# stored.
def __init__(self, fsPath, inDb=True, uploadName=None):
'''p_fsPath is the path of the file on disk.
- If p_inDb is True, this FileInfo will be stored in the database and
will hold metadata about a File field whose content will lie in the
database-controlled filesystem. In this case, p_fsPath is the path
of the file *relative* to the root DB folder. We avoid storing
absolute paths in order to ease the transfer of databases from one
place to the other. Moreover, p_fsPath does not include the
filename, that will be computed later, from the field name.
- If p_inDb is False, this FileInfo is a simple temporary object
representing any file on the filesystem (not necessarily in the
db-controlled filesystem). For instance, it could represent a temp
file generated from a Pod field in the OS temp folder. In this
case, p_fsPath is the absolute path to the file, including the
filename. If you manipulate such a FileInfo instance, please avoid
using methods that are used by Appy to manipulate
database-controlled files (like methods getFilePath, removeFile,
writeFile or copyFile).'''
self.fsPath = fsPath
self.fsName = None # The name of the file in fsPath
self.uploadName = None # The name of the uploaded file
self.uploadName = uploadName # The name of the uploaded file
self.size = 0 # Its size, in bytes
self.mimeType = None # Its MIME type
self.modified = None # The last modification date for this file.
# Complete metadata if p_inDb is False
if not inDb:
self.fsName = '' # Already included in self.fsPath.
# We will not store p_inDb. Checking if self.fsName is the empty
# string is equivalent.
fileInfo = os.stat(self.fsPath)
self.size = fileInfo.st_size
self.mimeType = guessMimeType(self.fsPath)
from DateTime import DateTime
self.modified = DateTime(fileInfo.st_mtime)
def getFilePath(self, obj):
'''Returns the absolute file name of the file on disk that corresponds
to this FileInfo instance.'''
dbFolder, folder = obj.o.getFsFolder()
return os.path.join(dbFolder, folder, self.fsName)
return osPathJoin(dbFolder, folder, self.fsName)
def removeFile(self, dbFolder, removeEmptyFolders=False):
def removeFile(self, dbFolder='', removeEmptyFolders=False):
'''Removes the file from the filesystem.'''
try:
os.remove(os.path.join(dbFolder, self.fsPath, self.fsName))
os.remove(osPathJoin(dbFolder, self.fsPath, self.fsName))
except Exception, e:
# If the current ZODB transaction is re-triggered, the file may
# already have been deleted.
@ -62,7 +106,7 @@ class FileInfo:
# if this removal leaves them empty (unless p_removeEmptyFolders is
# False).
if removeEmptyFolders:
sutils.FolderDeleter.deleteEmpty(os.path.join(dbFolder,self.fsPath))
sutils.FolderDeleter.deleteEmpty(osPathJoin(dbFolder,self.fsPath))
def normalizeFileName(self, name):
'''Normalizes file p_name.'''
@ -118,7 +162,7 @@ class FileInfo:
self.uploadName = name
self.fsName = '%s%s' % (fieldName, os.path.splitext(name)[1].lower())
# Write the file on disk (and compute/get its size in bytes)
fsName = os.path.join(dbFolder, self.fsPath, self.fsName)
fsName = osPathJoin(dbFolder, self.fsPath, self.fsName)
f = file(fsName, 'wb')
if fileType == 'FileUpload':
# Write the FileUpload instance on disk.
@ -150,21 +194,22 @@ class FileInfo:
self.modified = DateTime()
def copyFile(self, fieldName, filePath, dbFolder):
'''Copies the "external" file stored at _filePath in the db-controlled
'''Copies the "external" file stored at p_filePath in the db-controlled
file system, for storing a value for p_fieldName.'''
# Set names for the file
name = self.normalizeFileName(filePath)
self.uploadName = name
self.fsName = '%s%s' % (fieldName, os.path.splitext(name)[1])
# Set mimeType
self.mimeType= mimetypes.guess_type(filePath)[0] or File.defaultMimeType
self.mimeType = guessMimeType(filePath)
# Copy the file
shutil.copyfile(filePath, self.fsName)
fsName = osPathJoin(dbFolder, self.fsPath, self.fsName)
shutil.copyfile(filePath, fsName)
from DateTime import DateTime
self.modified = DateTime()
self.size = os.stat(self.fsName).st_size
self.size = os.stat(fsName).st_size
def writeResponse(self, response, dbFolder):
def writeResponse(self, response, dbFolder=''):
'''Writes this file in the HTTP p_response object.'''
# As a preamble, initialise response headers.
header = response.setHeader
@ -176,7 +221,7 @@ class FileInfo:
#sh('Cachecontrol', 'no-cache')
#sh('Expires', 'Thu, 11 Dec 1975 12:05:05 GMT')
# Write the file in the response
fsName = os.path.join(dbFolder, self.fsPath, self.fsName)
fsName = osPathJoin(dbFolder, self.fsPath, self.fsName)
f = file(fsName, 'rb')
while True:
chunk = f.read(self.BYTES)
@ -282,28 +327,6 @@ class File(Field):
historized, mapping, label, sdefault, scolspan, swidth,
sheight, 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 p_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 = sutils.FileWrapper(res)
return res
def getRequestValue(self, request, requestName=None):
name = requestName or self.name
return request.get('%s_file' % name)
@ -387,7 +410,7 @@ class File(Field):
fileName, fileContent, mimeType = value
if not fileName:
raise Exception(WRONG_FILE_TUPLE)
mimeType = mimeType or mimetypes.guess_type(fileName)[0]
mimeType = mimeType or guessMimeType(fileName)
info.writeFile(self.name, (fileName, fileContent, mimeType),
dbFolder)
# Store the FileInfo instance in the database.

View file

@ -16,10 +16,12 @@
# ------------------------------------------------------------------------------
import time, os, os.path
from file import FileInfo
from appy import Object
from appy.fields import Field
from appy.px import Px
from file import File
from appy.gen.layout import Table
from appy.gen import utils as gutils
from appy.pod import PodError
from appy.pod.renderer import Renderer
from appy.shared import utils as sutils
@ -34,19 +36,28 @@ class Pod(Field):
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.'
NO_TEMPLATE = 'Please specify a pod template in field "template".'
UNAVAILABLE_TEMPLATE = 'You are not allow to perform this action.'
TEMPLATE_NOT_FOUND = 'Template not found at %s.'
pxView = pxCell = Px('''
<!-- Ask action -->
<x if="field.askAction"
var2="doLabel='%s_askaction' % field.labelId;
chekboxId='%s_%s_cb' % (zobj.UID(), name)">
<input type="checkbox" name=":doLabel" id=":chekboxId"/>
<label lfor=":chekboxId" class="discreet">:_(doLabel)"></label>
</x>
<img for="fmt in field.getOutputFormats(zobj)" src=":url(fmt)"
onclick=":'generatePodDocument(%s, %s, %s, %s)' % \
(q(zobj.UID()), q(name), q(fmt), q(ztool.getQueryInfo()))"
title=":fmt.capitalize()" class="clickable"/>''')
<table cellpadding="0" cellspacing="0">
<tr>
<td for="template in field.getVisibleTemplates(obj)">
<table cellpadding="0" cellspacing="0" class="podTable">
<tr>
<td for="fmt in field.getOutputFormats(obj)">
<img src=":url(fmt)" title=":fmt.upper()" class="clickable"
onclick=":'generatePodDocument(%s,%s,%s,%s,%s)' % \
(q(obj.uid), q(name), q(template), q(fmt), \
q(ztool.getQueryInfo()))"/>
</td>
<td class="podName">:field.getTemplateName(obj, template)</td>
</tr>
</table>
</td>
</tr>
</table>''')
pxEdit = pxSearch = ''
@ -56,26 +67,43 @@ class Pod(Field):
specificWritePermission=False, width=None, height=None,
maxChars=None, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None, label=None,
template=None, context=None, action=None, askAction=False,
stylesMapping={}, formats=None, freezeFormat='pdf'):
# The following param stores the path to a POD template
self.template = template
template=None, templateName=None, showTemplate=None,
context=None, stylesMapping={}, formats=None,
freezeFormat='pdf'):
# Param "template" stores the path to the pod template(s).
if not template: raise Exception(Pod.NO_TEMPLATE)
if isinstance(template, basestring):
self.template = [template]
else:
self.template = template
# Param "templateName", if specified, is a method that will be called
# with the current template (from self.template) as single arg and must
# return the name of this template. If self.template stores a single
# template, you have no need to use param "templateName". Simply use the
# field label to name the template. But if you have a multi-pod field
# (with several templates specified as a list or tuple in param
# "template"), you will probably choose to hide the field label and use
# param "templateName" to give a specific name to every template. If
# "template" contains several templates and "templateName" is None, Appy
# will produce names from template filenames.
self.templateName = templateName
# "showTemplate", if specified, must be a method that will be called
# with the current template as single arg and that must return True if
# the template can be seen by the current user. "showTemplate" comes in
# addition to self.show. self.show dictates the visibility of the whole
# field (ie, all templates from self.template) while "showTemplate"
# dictates the visiblity of a specific template within self.template.
self.showTemplate = showTemplate
# The context is a dict containing a specific pod context, or a method
# that returns such a dict.
self.context = context
# Next one is a method that will be triggered after the document has
# been generated.
self.action = action
# If askAction is True, the action will be triggered only if the user
# checks a checkbox, which, by default, will be unchecked.
self.askAction = askAction
# A global styles mapping that would apply to the whole template
self.stylesMapping = stylesMapping
# What are the output formats when generating documents from this pod ?
self.formats = formats
if not formats:
# Compute default ones
if template.endswith('.ods'):
if self.template[0].endswith('.ods'):
self.formats = ('xls', 'ods')
else:
self.formats = ('pdf', 'doc', 'odt')
@ -103,32 +131,53 @@ class Pod(Field):
fileName = getattr(obj.o.aq_base, self.name).filename
return (os.path.splitext(fileName)[1][1:],)
def getTemplateName(self, obj, fileName):
'''Gets the name of a template given its p_fileName.'''
res = None
if self.templateName:
# Use the method specified in self.templateName.
res = self.templateName(obj, fileName)
# Else, deduce a nice name from p_fileName.
if not res:
name = os.path.splitext(os.path.basename(fileName))[0]
res = gutils.produceNiceMessage(name)
return res
def getVisibleTemplates(self, obj):
'''Returns, among self.template, the template(s) that can be shown.'''
if not self.showTemplate: return self.template # Show them all.
res = []
for template in self.template:
if self.showTemplate(obj, template):
res.append(template)
return res
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.aq_base, self.name, None)
if res and res.size:
# Return the frozen file.
return sutils.FileWrapper(res)
# 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.
# 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.
'''For a pod field, getting its value means computing a pod document or
returning a frozen one. A pod field differs from other field types
because there can be several ways to produce the field value (ie:
self.template can hold various templates; output file format can be
odt, pdf,.... We get those precisions about the way to produce the
file from the request object. 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.'''
rq = getattr(obj, 'REQUEST') or Object()
obj = obj.appy()
template = rq.get('template') or self.template[0]
# Security check.
if not self.showTemplate(obj, template):
raise Exception(self.UNAVAILABLE_TEMPLATE)
# Return the frozen document if frozen.
# if ...
# We must call pod to compute a pod document from "template".
tool = obj.tool
diskFolder = tool.getDiskFolder()
# Get the path to the pod template.
templatePath = os.path.join(diskFolder, self.template)
templatePath = os.path.join(diskFolder, template)
if not os.path.isfile(templatePath):
raise Exception('Pod template not found at %s.' % templatePath)
raise Exception(self.TEMPLATE_NOT_FOUND % templatePath)
# Get the output format
outputFormat = getattr(rq, 'podFormat', 'odt')
outputFormat = rq.get('podFormat', 'odt')
# Get or compute the specific POD context
specificContext = None
if callable(self.context):
@ -190,24 +239,20 @@ class Pod(Field):
obj.log(str(pe).strip(), type='error')
return Pod.POD_ERROR
# Give a friendly name for this file
fileName = obj.translate(self.labelId)
fileName = self.getTemplateName(obj, template)
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)
# Get a FileInfo instance to manipulate the temp file on the filesystem.
return FileInfo(tempFileName, inDb=False, uploadName=fileName)
# Returns the doc and removes the temp file
try:
os.remove(tempFileName)
except OSError, oe:
obj.log(Pod.DELETE_TEMP_DOC_ERROR % str(oe).strip(), type='warning')
except IOError, ie:
obj.log(Pod.DELETE_TEMP_DOC_ERROR % str(ie).strip(), type='warning')
except Exception, e:
obj.log(Pod.DELETE_TEMP_DOC_ERROR % str(e).strip(), type='warning')
return res
def store(self, obj, value):

View file

@ -296,12 +296,6 @@ class FieldDescriptor:
label = '%s_%s_addConfirm' % (self.classDescr.name, self.fieldName)
self.i18n(label, po.CONFIRM, nice=False)
def walkPod(self):
# Add i18n-specific messages
if self.appyType.askAction:
label = '%s_%s_askaction' % (self.classDescr.name, self.fieldName)
self.i18n(label, po.POD_ASKACTION, nice=False)
def walkList(self):
# Add i18n-specific messages
for name, field in self.appyType.fields:
@ -361,8 +355,6 @@ class FieldDescriptor:
elif self.appyType.type == 'Action': self.walkAction()
# Manage things which are specific to Ref types
elif self.appyType.type == 'Ref': self.walkRef()
# Manage things which are specific to Pod types
elif self.appyType.type == 'Pod': self.walkPod()
# Manage things which are specific to List types
elif self.appyType.type == 'List': self.walkList()
# Manage things which are specific to Calendar types

View file

@ -120,12 +120,10 @@ class ToolMixin(BaseMixin):
# 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
# res contains a FileInfo instance.
res.writeResponse(rq.RESPONSE)
# (Try to) delete the temp file on disk.
res.removeFile()
def getAppName(self):
'''Returns the name of the application.'''

View file

@ -689,6 +689,7 @@ class BaseMixin:
'''Returns the database value of field named p_name for p_self.'''
if layoutType == 'search': return # No object in search screens.
field = self.getAppyType(name)
if field.type == 'Pod': return
if '*' not in name: return field.getValue(self)
# The field is an inner field from a List.
listName, name, i = name.split('*')

View file

@ -23,7 +23,6 @@ fallbacks = {'en': 'en-us en-ca',
# Default values for i18n labels whose ids are not fixed.
CONFIG = "Configuration panel for product '%s'"
POD_ASKACTION = 'Trigger related action'
EMAIL_SUBJECT = '${siteTitle} - Action \\"${transitionName}\\" has been ' \
'performed on element entitled \\"${objectTitle}\\".'
EMAIL_BODY = 'You can consult this element at ${objectUrl}.'

View file

@ -156,3 +156,5 @@ td.search { padding-top: 8px }
.homeTable th { padding-top: 5px; font-size: 105% }
.first { margin-top: 0px }
.error { margin: 5px }
.podName { font-size: 90%; padding-left: 3px }
.podTable { margin-left: 15px }

View file

@ -521,20 +521,16 @@ function toggleCookie(cookieId) {
}
// Function that allows to generate a document from a pod template.
function generatePodDocument(contextUid, fieldName, podFormat, queryData,
function generatePodDocument(uid, fieldName, template, podFormat, queryData,
customParams) {
var theForm = document.getElementById("podTemplateForm");
theForm.objectUid.value = contextUid;
theForm.objectUid.value = uid;
theForm.fieldName.value = fieldName;
theForm.template.value = template;
theForm.podFormat.value = podFormat;
theForm.askAction.value = "False";
theForm.queryData.value = queryData;
if (customParams) { theForm.customParams.value = customParams; }
else { theForm.customParams.value = ''; }
var askActionWidget = document.getElementById(contextUid + '_' + fieldName + '_cb');
if (askActionWidget && askActionWidget.checked) {
theForm.askAction.value = "True";
}
theForm.submit();
}

View file

@ -93,7 +93,7 @@ class ToolWrapper(AbstractWrapper):
# PXs for graphical elements shown on every page
# --------------------------------------------------------------------------
# Global elements included in every page.
pxPagePrologue = Px('''<x>
pxPagePrologue = Px('''
<!-- Include type-specific CSS and JS. -->
<x if="cssJs">
<link for="cssFile in cssJs['css']" rel="stylesheet" type="text/css"
@ -133,12 +133,11 @@ class ToolWrapper(AbstractWrapper):
action=":ztool.absolute_url() + '/generateDocument'">
<input type="hidden" name="objectUid"/>
<input type="hidden" name="fieldName"/>
<input type="hidden" name="template"/>
<input type="hidden" name="podFormat"/>
<input type="hidden" name="askAction"/>
<input type="hidden" name="queryData"/>
<input type="hidden" name="customParams"/>
</form>
</x>''')
</form>''')
pxPageBottom = Px('''
<script var="info=zobj.getSlavesRequestInfo(page)"

View file

@ -797,14 +797,12 @@ class AbstractWrapper(object):
zopeObj.reindex()
return appyObj
def freeze(self, fieldName, doAction=False):
def freeze(self, fieldName):
'''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.