diff --git a/gen/__init__.py b/gen/__init__.py index eafb160..2287f95 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -506,7 +506,8 @@ class Type: search results (p_usage="search") or when sorting reference fields (p_usage="ref")?''' if usage == 'search': - return self.indexed and not self.isMultiValued() + return self.indexed and not self.isMultiValued() and not \ + ((self.type == 'String') and self.isSelection()) elif usage == 'ref': return self.type in ('Integer', 'Float', 'Boolean', 'Date') or \ ((self.type == 'String') and (self.format == 0)) @@ -514,7 +515,6 @@ class Type: def isShowable(self, obj, layoutType): '''When displaying p_obj on a given p_layoutType, must we show this field?''' - isEdit = layoutType == 'edit' # Do not show field if it is optional and not selected in tool if self.optional: tool = obj.getTool().appy() @@ -524,7 +524,7 @@ class Type: return False # Check if the user has the permission to view or edit the field user = obj.portal_membership.getAuthenticatedMember() - if isEdit: + if layoutType == 'edit': perm = self.writePermission else: perm = self.readPermission @@ -535,14 +535,9 @@ class Type: res = self.callMethod(obj, self.show) else: res = self.show - # Take into account possible values 'view' and 'edit' for 'show' param. - if res == 'view': - if isEdit: res = False - else: res = True - elif res == 'edit': - if isEdit: res = True - else: res = False - return res + # Take into account possible values 'view', 'edit', 'search'... + if res in ('view', 'edit', 'result'): return res == layoutType + return bool(res) def isClientVisible(self, obj): '''This method returns True if this field is visible according to @@ -1641,6 +1636,16 @@ class Ref(Type): toDelete.append(uid) for uid in toDelete: uids.remove(uid) + if not uids: + # Maybe is there a default value? + defValue = Type.getValue(self, obj) + if defValue: + # I must prefix call to function "type" with "__builtins__" + # because this name was overridden by a method parameter. + if __builtins__['type'](defValue) in sequenceTypes: + uids = [o.o.UID() for o in defValue] + else: + uids = [defValue.o.UID()] # Prepare the result: an instance of SomeObjects, that, in this case, # represent a subset of all referred objects res = SomeObjects() @@ -1751,6 +1756,10 @@ class Computed(Type): self.method = method # Does field computation produce plain text or XHTML? self.plainText = plainText + if isinstance(method, basestring): + # When field computation is done with a macro, we know the result + # will be HTML. + self.plainText = False Type.__init__(self, None, multiplicity, index, default, optional, False, show, page, group, layouts, move, indexed, False, specificReadPermission, specificWritePermission, width, @@ -1758,10 +1767,30 @@ class Computed(Type): sync) self.validable = False + def callMacro(self, obj, macroPath): + '''Returns the macro corresponding to p_macroPath. The base folder + where we search is "skyn".''' + # Get the special page in Appy that allows to call a macro + macroPage = obj.skyn.callMacro + # Get, from p_macroPath, the page where the macro lies, and the macro + # name. + names = self.method.split('/') + # Get the page where the macro lies + page = obj.skyn + for name in names[:-1]: + page = getattr(page, name) + macroName = names[-1] + return macroPage(obj, contextObj=obj, page=page, macroName=macroName) + def getValue(self, obj): '''Computes the value instead of getting it in the database.''' if not self.method: return - return self.callMethod(obj, self.method, raiseOnError=False) + if isinstance(self.method, basestring): + # self.method is a path to a macro that will produce the field value + return self.callMacro(obj, self.method) + else: + # self.method is a method that will return the field value + return self.callMethod(obj, self.method, raiseOnError=False) def getFormattedValue(self, obj, value): if not isinstance(value, basestring): return str(value) @@ -1864,7 +1893,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): + askAction=False, stylesMapping=None): # 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 @@ -1876,6 +1905,8 @@ class Pod(Type): # 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 Type.__init__(self, None, (0,1), index, default, optional, False, show, page, group, layouts, move, indexed, searchable, specificReadPermission, diff --git a/gen/plone25/descriptors.py b/gen/plone25/descriptors.py index e49497a..d06ea4d 100644 --- a/gen/plone25/descriptors.py +++ b/gen/plone25/descriptors.py @@ -434,7 +434,7 @@ class ToolClassDescriptor(ClassDescriptor): self.addField(fieldName, fieldType) # Add the field that will store the output format(s) fieldName = 'formatsFor%s_%s' % (className, fieldDescr.fieldName) - fieldType = String(validator=('odt', 'pdf', 'doc', 'rtf'), + fieldType = String(validator=Selection('getPodOutputFormats'), multiplicity=(1,None), default=('odt',), **pg) self.addField(fieldName, fieldType) diff --git a/gen/plone25/generator.py b/gen/plone25/generator.py index 6b30792..e2a4992 100644 --- a/gen/plone25/generator.py +++ b/gen/plone25/generator.py @@ -18,8 +18,14 @@ COMMON_METHODS = ''' def getTool(self): return self.%s def getProductConfig(self): return Products.%s.config def skynView(self): - """Redirects to skyn/view.""" - return self.REQUEST.RESPONSE.redirect(self.getUrl()) + """Redirects to skyn/view. Transfers the status message if any.""" + rq = self.REQUEST + msg = rq.get('portal_status_message', '') + if msg: + url = self.getUrl(portal_status_message=msg) + else: + url = self.getUrl() + return rq.RESPONSE.redirect(url) ''' # ------------------------------------------------------------------------------ class Generator(AbstractGenerator): @@ -140,6 +146,10 @@ class Generator(AbstractGenerator): msg('field_invalid', '', msg.FIELD_INVALID), msg('file_required', '', msg.FILE_REQUIRED), msg('image_required', '', msg.IMAGE_REQUIRED), + msg('odt', '', msg.FORMAT_ODT), + msg('pdf', '', msg.FORMAT_PDF), + msg('doc', '', msg.FORMAT_DOC), + msg('rtf', '', msg.FORMAT_RTF), ] # Create a label for every role added by this application for role in self.getAllUsedRoles(): @@ -153,7 +163,7 @@ class Generator(AbstractGenerator): if self.config.frontPage: self.generateFrontPage() self.copyFile('Install.py', self.repls, destFolder='Extensions') - self.copyFile('configure.zcml', self.repls) + self.generateConfigureZcml() self.copyFile('import_steps.xml', self.repls, destFolder='profiles/default') self.copyFile('ProfileInit.py', self.repls, destFolder='profiles', @@ -304,6 +314,17 @@ class Generator(AbstractGenerator): if isBack: res += '.back' return res + def generateConfigureZcml(self): + '''Generates file configure.zcml.''' + repls = self.repls.copy() + # Note every class as "deprecated". + depr = '' + for klass in self.getClasses(include='all'): + depr += '\n' % \ + (klass.name, klass.name) + repls['deprecated'] = depr + self.copyFile('configure.zcml', repls) + def generateConfig(self): repls = self.repls.copy() # Get some lists of classes diff --git a/gen/plone25/mixins/ToolMixin.py b/gen/plone25/mixins/ToolMixin.py index ad2942b..f9e8d4f 100644 --- a/gen/plone25/mixins/ToolMixin.py +++ b/gen/plone25/mixins/ToolMixin.py @@ -45,6 +45,7 @@ class ToolMixin(BaseMixin): res['title'] = self.translate(appyType.labelId) res['context'] = appyType.context res['action'] = appyType.action + res['stylesMapping'] = appyType.stylesMapping return res def getSiteUrl(self): @@ -68,7 +69,7 @@ class ToolMixin(BaseMixin): template = podInfo['template'].content podTitle = podInfo['title'] if podInfo['context']: - if type(podInfo['context']) == types.FunctionType: + if callable(podInfo['context']): specificPodContext = podInfo['context'](appyObj) else: specificPodContext = podInfo['context'] @@ -76,16 +77,38 @@ class ToolMixin(BaseMixin): # Temporary file where to generate the result tempFileName = '%s/%s_%f.%s' % ( getOsTempFolder(), obj.UID(), time.time(), format) - # Define parameters to pass to the appy.pod renderer + # 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: @@ -105,7 +128,13 @@ class ToolMixin(BaseMixin): f = file(tempFileName, 'rb') res = f.read() # Identify the filename to return - fileName = u'%s-%s' % (obj.Title().decode('utf-8'), podTitle) + 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]) @@ -189,6 +218,19 @@ class ToolMixin(BaseMixin): return {'fields': fields, 'nbOfColumns': nbOfColumns, 'fieldDicts': fieldDicts} + queryParamNames = ('type_name', 'search', 'sortKey', 'sortOrder', + 'filterKey', 'filterValue') + def getQueryInfo(self): + '''If we are showing search results, this method encodes in a string all + the params in the request that are required for re-triggering the + search.''' + rq = self.REQUEST + res = '' + if rq.has_key('search'): + res = ';'.join([rq.get(key,'').replace(';','') \ + for key in self.queryParamNames]) + return res + def getImportElements(self, contentType): '''Returns the list of elements that can be imported from p_path for p_contentType.''' @@ -863,4 +905,10 @@ class ToolMixin(BaseMixin): os.remove(fileName) return content return 'File does not exist' + + def getResultPodFields(self, contentType): + '''Finds, among fields defined on p_contentType, which ones are Pod + fields that need to be shown on a page displaying query results.''' + return [f.__dict__ for f in self.getAllAppyTypes(contentType) \ + if (f.type == 'Pod') and (f.show == 'result')] # ------------------------------------------------------------------------------ diff --git a/gen/plone25/mixins/__init__.py b/gen/plone25/mixins/__init__.py index e1e0f7c..e08e138 100644 --- a/gen/plone25/mixins/__init__.py +++ b/gen/plone25/mixins/__init__.py @@ -3,7 +3,7 @@ - mixins/ToolMixin is mixed in with the generated application Tool class.''' # ------------------------------------------------------------------------------ -import os, os.path, sys, types, mimetypes +import os, os.path, sys, types, mimetypes, urllib import appy.gen from appy.gen import Type, String, Selection, Role from appy.gen.utils import * @@ -67,13 +67,15 @@ class BaseMixin: initiator.appy().link(fieldName, obj) # Call the custom "onEdit" if available + msg = None # The message to display to the user. It can be set by onEdit if obj.wrapperClass: appyObject = obj.appy() - if hasattr(appyObject, 'onEdit'): appyObject.onEdit(created) + if hasattr(appyObject, 'onEdit'): + msg = appyObject.onEdit(created) # Manage "add" permissions and reindex the object obj._appy_managePermissions() obj.reindexObject() - return obj + return obj, msg def delete(self): '''This methods is self's suicide.''' @@ -219,10 +221,21 @@ class BaseMixin: return self.skyn.edit(self) # Create or update the object in the database - obj = self.createOrUpdate(isNew, values) + obj, msg = self.createOrUpdate(isNew, values) # Redirect the user to the appropriate page - msg = obj.translate('Changes saved.', domain='plone') + if not msg: msg = obj.translate('Changes saved.', domain='plone') + # If the object has already been deleted (ie, it is a kind of transient + # object like a one-shot form and has already been deleted in method + # onEdit), redirect to the main site page. + if not getattr(obj.getParentNode(), obj.id, None): + obj.unindexObject() + return self.goto(tool.getSiteUrl(), msg) + # If the user can't access the object anymore, redirect him to the + # main site page. + user = self.portal_membership.getAuthenticatedMember() + if not user.has_permission('View', obj): + return self.goto(tool.getSiteUrl(), msg) if rq.get('buttonOk.x', None) or saveConfirmed: # Go to the consult view for this object obj.plone_utils.addPortalMessage(msg) @@ -321,8 +334,10 @@ class BaseMixin: if previousData: self.addDataChange(previousData) - def goto(self, url, addParams=False): + def goto(self, url, msg=None): '''Brings the user to some p_url after an action has been executed.''' + if msg: + url += '?' + urllib.urlencode([('portal_status_message',msg)]) return self.REQUEST.RESPONSE.redirect(url) def showField(self, name, layoutType='view'): diff --git a/gen/plone25/skin/callMacro.pt b/gen/plone25/skin/callMacro.pt new file mode 100644 index 0000000..9e50f64 --- /dev/null +++ b/gen/plone25/skin/callMacro.pt @@ -0,0 +1,13 @@ + + This page allows to call any macro from Python code, for example. + + + + diff --git a/gen/plone25/skin/page.pt b/gen/plone25/skin/page.pt index b4b5002..820548b 100644 --- a/gen/plone25/skin/page.pt +++ b/gen/plone25/skin/page.pt @@ -141,7 +141,7 @@ params['filterValue'] = filterWidget.value; } } - askAjaxChunk(hookId,'GET',objectUrl,'macros','queryResult',params); + askAjaxChunk(hookId,'GET',objectUrl, 'result', 'queryResult', params); } function askObjectHistory(hookId, objectUrl, startNumber) { @@ -244,12 +244,13 @@ createCookie(cookieId, newState); } // Function that allows to generate a document from a pod template. - function generatePodDocument(contextUid, fieldName, podFormat) { + function generatePodDocument(contextUid, fieldName, podFormat, queryData) { var theForm = document.getElementsByName("podTemplateForm")[0]; theForm.objectUid.value = contextUid; theForm.fieldName.value = fieldName; theForm.podFormat.value = podFormat; theForm.askAction.value = "False"; + theForm.queryData.value = queryData; var askActionWidget = document.getElementById(contextUid + '_' + fieldName); if (askActionWidget && askActionWidget.checked) { theForm.askAction.value = "True"; @@ -376,6 +377,7 @@ + @@ -536,6 +538,7 @@
Single message from portal_status_message request key
+ tal:condition="msg" class="portalMessage" tal:content="structure msg" i18n:translate="">
Messages added via plone_utils diff --git a/gen/plone25/skin/macros.pt b/gen/plone25/skin/result.pt similarity index 92% rename from gen/plone25/skin/macros.pt rename to gen/plone25/skin/result.pt index 2793594..96ce1a1 100644 --- a/gen/plone25/skin/macros.pt +++ b/gen/plone25/skin/result.pt @@ -34,9 +34,20 @@ + Display here POD templates if required. + + +
+     +
+
+ tal:condition="python: searchName and descr.strip()">

diff --git a/gen/plone25/skin/view.pt b/gen/plone25/skin/view.pt index 1812b15..95f411d 100644 --- a/gen/plone25/skin/view.pt +++ b/gen/plone25/skin/view.pt @@ -25,8 +25,7 @@ appName appFolder/getId; phaseInfo python: contextObj.getAppyPhases(currentOnly=True, layoutType='view'); page request/page|python:'main'; - phase phaseInfo/name; - showWorkflow python: tool.getAttr('showWorkflowFor' + contextObj.meta_type)"> + phase phaseInfo/name;"> diff --git a/gen/plone25/skin/widgets/pod.pt b/gen/plone25/skin/widgets/pod.pt index 8ae42d5..a5b42f1 100644 --- a/gen/plone25/skin/widgets/pod.pt +++ b/gen/plone25/skin/widgets/pod.pt @@ -9,7 +9,7 @@ diff --git a/gen/plone25/skin/widgets/ref.pt b/gen/plone25/skin/widgets/ref.pt index 7f4c42e..2135eb3 100644 --- a/gen/plone25/skin/widgets/ref.pt +++ b/gen/plone25/skin/widgets/ref.pt @@ -239,8 +239,9 @@ refUids python: [o.UID() for o in contextObj.getAppyRefs(name)['objects']]; isBeingCreated python: contextObj.isTemporary() or ('/portal_factory/' in contextObj.absolute_url())"> -