diff --git a/fields/file.py b/fields/file.py
index e61dc0f..263058f 100644
--- a/fields/file.py
+++ b/fields/file.py
@@ -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.
diff --git a/fields/pod.py b/fields/pod.py
index 426b99e..807bf23 100644
--- a/fields/pod.py
+++ b/fields/pod.py
@@ -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('''
-
-
-
-
-
- ''')
+
+
+
+
+
+
+
+
+
:field.getTemplateName(obj, template)
+
+
+
+
+
''')
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):
diff --git a/gen/descriptors.py b/gen/descriptors.py
index 982e72c..fe5ff49 100644
--- a/gen/descriptors.py
+++ b/gen/descriptors.py
@@ -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
diff --git a/gen/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py
index 726d256..6607524 100644
--- a/gen/mixins/ToolMixin.py
+++ b/gen/mixins/ToolMixin.py
@@ -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.'''
diff --git a/gen/mixins/__init__.py b/gen/mixins/__init__.py
index 7d0d291..2cc3215 100644
--- a/gen/mixins/__init__.py
+++ b/gen/mixins/__init__.py
@@ -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('*')
diff --git a/gen/po.py b/gen/po.py
index 3df2149..a1c0152 100644
--- a/gen/po.py
+++ b/gen/po.py
@@ -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}.'
diff --git a/gen/ui/appy.css b/gen/ui/appy.css
index b576811..2947e18 100644
--- a/gen/ui/appy.css
+++ b/gen/ui/appy.css
@@ -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 }
diff --git a/gen/ui/appy.js b/gen/ui/appy.js
index 6731d38..2972951 100644
--- a/gen/ui/appy.js
+++ b/gen/ui/appy.js
@@ -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();
}
diff --git a/gen/wrappers/ToolWrapper.py b/gen/wrappers/ToolWrapper.py
index 7206f29..845b42c 100644
--- a/gen/wrappers/ToolWrapper.py
+++ b/gen/wrappers/ToolWrapper.py
@@ -93,7 +93,7 @@ class ToolWrapper(AbstractWrapper):
# PXs for graphical elements shown on every page
# --------------------------------------------------------------------------
# Global elements included in every page.
- pxPagePrologue = Px('''
+ pxPagePrologue = Px('''
+
-
-
- ''')
+ ''')
pxPageBottom = Px('''