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('''