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())">
-