Worflow state can not be used in ref fields; mayNavigate is not static anymore, but an instance method.

This commit is contained in:
Gaetan Delannay 2010-12-06 11:11:40 +01:00
parent e62e00d367
commit 2124cffa5e
14 changed files with 187 additions and 161 deletions

View file

@ -90,7 +90,8 @@ class Group:
def __init__(self, name, columns=['100%'], wide=True, style='section2', def __init__(self, name, columns=['100%'], wide=True, style='section2',
hasLabel=True, hasDescr=False, hasHelp=False, hasLabel=True, hasDescr=False, hasHelp=False,
hasHeaders=False, group=None, colspan=1, align='center', hasHeaders=False, group=None, colspan=1, align='center',
valign='top', css_class='', master=None, masterValue=None): valign='top', css_class='', master=None, masterValue=None,
cellpadding=1, cellspacing=1):
self.name = name self.name = name
# In its simpler form, field "columns" below can hold a list or tuple # In its simpler form, field "columns" below can hold a list or tuple
# of column widths expressed as strings, that will be given as is in # of column widths expressed as strings, that will be given as is in
@ -127,6 +128,8 @@ class Group:
self.colspan = colspan self.colspan = colspan
self.align = align self.align = align
self.valign = valign self.valign = valign
self.cellpadding = cellpadding
self.cellspacing = cellspacing
if style == 'tabs': if style == 'tabs':
# Group content will be rendered as tabs. In this case, some # Group content will be rendered as tabs. In this case, some
# param combinations have no sense. # param combinations have no sense.
@ -289,8 +292,9 @@ class Search:
self.group = group # Searches may be visually grouped in the portlet self.group = group # Searches may be visually grouped in the portlet
self.sortBy = sortBy self.sortBy = sortBy
self.limit = limit self.limit = limit
self.fields = fields # This is a dict whose keys are indexed field # In the dict below, keys are indexed field names and values are
# names and whose values are search values. # search values.
self.fields = fields
@staticmethod @staticmethod
def getIndexName(fieldName, usage='search'): def getIndexName(fieldName, usage='search'):
'''Gets the name of the technical index that corresponds to field named '''Gets the name of the technical index that corresponds to field named
@ -1466,7 +1470,8 @@ class Ref(Type):
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=5, specificWritePermission=False, width=None, height=5,
colspan=1, master=None, masterValue=None, focus=False, colspan=1, master=None, masterValue=None, focus=False,
historized=False): historized=False, queryable=False, queryFields=None,
queryNbCols=1):
self.klass = klass self.klass = klass
self.attribute = attribute self.attribute = attribute
# May the user add new objects through this ref ? # May the user add new objects through this ref ?
@ -1503,6 +1508,15 @@ class Ref(Type):
self.maxPerPage = maxPerPage self.maxPerPage = maxPerPage
# Specifies sync # Specifies sync
sync = {'view': False, 'edit':True} sync = {'view': False, 'edit':True}
# If param p_queryable is True, the user will be able to perform queries
# from the UI within referenced objects.
self.queryable = queryable
# Here is the list of fields that will appear on the search screen.
# If None is specified, by default we take every indexed field
# defined on referenced objects' class.
self.queryFields = queryFields
# The search screen will have this number of columns
self.queryNbCols = queryNbCols
Type.__init__(self, validator, multiplicity, index, default, optional, Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, layouts, move, indexed, editDefault, show, page, group, layouts, move, indexed,
False, specificReadPermission, specificWritePermission, False, specificReadPermission, specificWritePermission,

View file

@ -535,7 +535,6 @@ class UserClassDescriptor(ClassDescriptor):
self.klass = klass self.klass = klass
self.customized = True self.customized = True
def isFolder(self, klass=None): return True def isFolder(self, klass=None): return True
def isRoot(self): return False
def generateSchema(self): def generateSchema(self):
ClassDescriptor.generateSchema(self, configClass=True) ClassDescriptor.generateSchema(self, configClass=True)

View file

@ -98,7 +98,6 @@ class Generator(AbstractGenerator):
msg('max_ref_violated', '', msg.MAX_REF_VIOLATED), msg('max_ref_violated', '', msg.MAX_REF_VIOLATED),
msg('no_ref', '', msg.REF_NO), msg('no_ref', '', msg.REF_NO),
msg('add_ref', '', msg.REF_ADD), msg('add_ref', '', msg.REF_ADD),
msg('ref_name', '', msg.REF_NAME),
msg('ref_actions', '', msg.REF_ACTIONS), msg('ref_actions', '', msg.REF_ACTIONS),
msg('move_up', '', msg.REF_MOVE_UP), msg('move_up', '', msg.REF_MOVE_UP),
msg('move_down', '', msg.REF_MOVE_DOWN), msg('move_down', '', msg.REF_MOVE_DOWN),
@ -303,7 +302,7 @@ class Generator(AbstractGenerator):
repls['addPermissions'] = addPermissions repls['addPermissions'] = addPermissions
# Compute root classes # Compute root classes
rootClasses = '' rootClasses = ''
for classDescr in self.classes: for classDescr in self.getClasses(include='allButTool'):
if classDescr.isRoot(): if classDescr.isRoot():
rootClasses += "'%s'," % classDescr.name rootClasses += "'%s'," % classDescr.name
repls['rootClasses'] = rootClasses repls['rootClasses'] = rootClasses
@ -617,9 +616,20 @@ class Generator(AbstractGenerator):
if self.user.customized: if self.user.customized:
Tool.users.klass = self.user.klass Tool.users.klass = self.user.klass
# Generate the User class
self.user.generateSchema()
self.labels += [ Msg(self.userName, '', Msg.USER),
Msg('%s_edit_descr' % self.userName, '', ' '),
Msg('%s_plural' % self.userName, '',self.userName+'s')]
repls = self.repls.copy()
repls['fields'] = self.user.schema
repls['methods'] = self.user.methods
repls['wrapperClass'] = '%s_Wrapper' % self.user.name
self.copyFile('UserTemplate.py', repls,destName='%s.py' % self.userName)
# Before generating the Tool class, finalize it with query result # Before generating the Tool class, finalize it with query result
# columns, with fields to propagate, workflow-related fields. # columns, with fields to propagate, workflow-related fields.
for classDescr in self.classes: for classDescr in self.getClasses(include='allButTool'):
for fieldName, fieldType in classDescr.toolFieldsToPropagate: for fieldName, fieldType in classDescr.toolFieldsToPropagate:
for childDescr in classDescr.getChildren(): for childDescr in classDescr.getChildren():
childFieldName = fieldName % childDescr.name childFieldName = fieldName % childDescr.name
@ -644,16 +654,6 @@ class Generator(AbstractGenerator):
repls['wrapperClass'] = '%s_Wrapper' % self.tool.name repls['wrapperClass'] = '%s_Wrapper' % self.tool.name
self.copyFile('ToolTemplate.py', repls, destName='%s.py'% self.toolName) self.copyFile('ToolTemplate.py', repls, destName='%s.py'% self.toolName)
# Generate the User class
self.user.generateSchema()
self.labels += [ Msg(self.userName, '', Msg.USER),
Msg('%s_edit_descr' % self.userName, '', ' ')]
repls = self.repls.copy()
repls['fields'] = self.user.schema
repls['methods'] = self.user.methods
repls['wrapperClass'] = '%s_Wrapper' % self.user.name
self.copyFile('UserTemplate.py', repls,destName='%s.py' % self.userName)
def generateClass(self, classDescr): def generateClass(self, classDescr):
'''Is called each time an Appy class is found in the application, for '''Is called each time an Appy class is found in the application, for
generating the corresponding Archetype class and schema.''' generating the corresponding Archetype class and schema.'''

View file

@ -164,16 +164,25 @@ class ToolMixin(BaseMixin):
res.append((appyType.name, self.translate(appyType.labelId))) res.append((appyType.name, self.translate(appyType.labelId)))
return res return res
def getSearchableFields(self, contentType): def getSearchInfo(self, contentType, refInfo=None):
'''Returns, among the list of all searchable fields (see method above), '''Returns a tuple:
the list of fields that the user has configured as being effectively 1) The list of searchable fields (ie fields among all indexed fields)
used in the search screen.''' 2) The number of columns for layouting those fields.'''
res = [] fields = []
fieldNames = getattr(self.appy(), 'searchFieldsFor%s' % contentType, ()) if refInfo:
# The search is triggered from a Ref field.
refField = self.getRefInfo(refInfo)[1]
fieldNames = refField.queryFields or ()
nbOfColumns = refField.queryNbCols
else:
# The search is triggered from an app-wide search.
at = self.appy()
fieldNames = getattr(at, 'searchFieldsFor%s' % contentType,())
nbOfColumns = getattr(at, 'numberOfSearchColumnsFor%s' %contentType)
for name in fieldNames: for name in fieldNames:
appyType = self.getAppyType(name, asDict=True,className=contentType) appyType = self.getAppyType(name, asDict=True,className=contentType)
res.append(appyType) fields.append(appyType)
return res return fields, nbOfColumns
def getImportElements(self, contentType): def getImportElements(self, contentType):
'''Returns the list of elements that can be imported from p_path for '''Returns the list of elements that can be imported from p_path for
@ -220,7 +229,8 @@ class ToolMixin(BaseMixin):
def executeQuery(self, contentType, searchName=None, startNumber=0, def executeQuery(self, contentType, searchName=None, startNumber=0,
search=None, remember=False, brainsOnly=False, search=None, remember=False, brainsOnly=False,
maxResults=None, noSecurity=False, sortBy=None, maxResults=None, noSecurity=False, sortBy=None,
sortOrder='asc', filterKey=None, filterValue=None): sortOrder='asc', filterKey=None, filterValue=None,
refField=None):
'''Executes a query on a given p_contentType (or several, separated '''Executes a query on a given p_contentType (or several, separated
with commas) in Plone's portal_catalog. If p_searchName is specified, with commas) in Plone's portal_catalog. If p_searchName is specified,
it corresponds to: it corresponds to:
@ -256,7 +266,10 @@ class ToolMixin(BaseMixin):
If p_filterKey is given, it represents an additional search parameter If p_filterKey is given, it represents an additional search parameter
to take into account: the corresponding search value is in to take into account: the corresponding search value is in
p_filterValue.''' p_filterValue.
If p_refField is given, the query is limited to the objects that are
referenced through it.'''
# Is there one or several content types ? # Is there one or several content types ?
if contentType.find(',') != -1: if contentType.find(',') != -1:
portalTypes = contentType.split(',') portalTypes = contentType.split(',')
@ -276,6 +289,9 @@ class ToolMixin(BaseMixin):
if search: if search:
# Add additional search criteria # Add additional search criteria
for fieldName, fieldValue in search.fields.iteritems(): for fieldName, fieldValue in search.fields.iteritems():
# Management of searches restricted to objects linked through a
# Ref field: not implemented yet.
if fieldName == '_ref': continue
# Make the correspondance between the name of the field and the # Make the correspondance between the name of the field and the
# name of the corresponding index. # name of the corresponding index.
attrName = Search.getIndexName(fieldName) attrName = Search.getIndexName(fieldName)
@ -306,7 +322,9 @@ class ToolMixin(BaseMixin):
# Return brains only. # Return brains only.
if not maxResults: return brains if not maxResults: return brains
else: return brains[:maxResults] else: return brains[:maxResults]
if not maxResults: maxResults = self.appy().numberOfResultsPerPage if not maxResults:
if refField: maxResults = refField.maxPerPage
else: maxResults = self.appy().numberOfResultsPerPage
elif maxResults == 'NO_LIMIT': maxResults = None elif maxResults == 'NO_LIMIT': maxResults = None
res = SomeObjects(brains, maxResults, startNumber,noSecurity=noSecurity) res = SomeObjects(brains, maxResults, startNumber,noSecurity=noSecurity)
res.brainsToObjects() res.brainsToObjects()
@ -327,13 +345,16 @@ class ToolMixin(BaseMixin):
s['search_%s' % searchName] = uids s['search_%s' % searchName] = uids
return res.__dict__ return res.__dict__
def getResultColumnsNames(self, contentType): def getResultColumnsNames(self, contentType, refField):
contentTypes = contentType.strip(',').split(',') contentTypes = contentType.strip(',').split(',')
resSet = None # Temporary set for computing intersections. resSet = None # Temporary set for computing intersections.
res = [] # Final, sorted result. res = [] # Final, sorted result.
fieldNames = None fieldNames = None
appyTool = self.appy() appyTool = self.appy()
for cType in contentTypes: for cType in contentTypes:
if refField:
fieldNames = refField.shownInfo
else:
fieldNames = getattr(appyTool, 'resultColumnsFor%s' % cType) fieldNames = getattr(appyTool, 'resultColumnsFor%s' % cType)
if not resSet: if not resSet:
resSet = set(fieldNames) resSet = set(fieldNames)
@ -346,27 +367,6 @@ class ToolMixin(BaseMixin):
res.append(fieldName) res.append(fieldName)
return res return res
def getResultColumns(self, anObject, contentType):
'''What columns must I show when displaying a list of root class
instances? Result is a list of tuples containing the name of the
column (=name of the field) and the corresponding appyType (dict
version).'''
res = []
for fieldName in self.getResultColumnsNames(contentType):
if fieldName == 'workflowState':
# We do not return a appyType if the attribute is not a *real*
# attribute, but the workfow state.
res.append(fieldName)
else:
appyType = anObject.getAppyType(fieldName, asDict=True)
if not appyType:
res.append({'name': fieldName, '_wrong': True})
# The field name is wrong.
# We return it so we can show it in an error message.
else:
res.append(appyType)
return res
def truncateValue(self, value, appyType): def truncateValue(self, value, appyType):
'''Truncates the p_value according to p_appyType width.''' '''Truncates the p_value according to p_appyType width.'''
maxWidth = appyType['width'] maxWidth = appyType['width']
@ -452,14 +452,13 @@ class ToolMixin(BaseMixin):
return pythonClass.maySearch(self.appy()) return pythonClass.maySearch(self.appy())
return True return True
def userMayNavigate(self, someClass): def userMayNavigate(self, obj):
'''This method checks if the currently logged user can display the '''This method checks if the currently logged user can display the
navigation panel within the portlet. This is done by calling method navigation panel within the portlet. This is done by calling method
"mayNavigate" on the class whose currently shown object is an "mayNavigate" on the currently shown object. If no such method
instance of. If no such method exists, we return True.''' exists, we return True.'''
pythonClass = self.getAppyClass(someClass) appyObj = obj.appy()
if 'mayNavigate' in pythonClass.__dict__: if hasattr(appyObj, 'mayNavigate'): return appyObj.mayNavigate()
return pythonClass.mayNavigate(self.appy())
return True return True
def onImportObjects(self): def onImportObjects(self):
@ -485,8 +484,9 @@ class ToolMixin(BaseMixin):
return False return False
def isSortable(self, name, className, usage): def isSortable(self, name, className, usage):
'''Is field p_name defined on p_metaType sortable for p_usage purposes '''Is field p_name defined on p_className sortable for p_usage purposes
(p_usage can be "ref" or "search")?''' (p_usage can be "ref" or "search")?'''
if (',' in className) or (name == 'workflowState'): return False
appyType = self.getAppyType(name, className=className) appyType = self.getAppyType(name, className=className)
return appyType.isSortable(usage=usage) return appyType.isSortable(usage=usage)
@ -591,10 +591,14 @@ class ToolMixin(BaseMixin):
oper = ' %s ' % rq.form.get(operKey, 'or').upper() oper = ' %s ' % rq.form.get(operKey, 'or').upper()
attrValue = oper.join(attrValue) attrValue = oper.join(attrValue)
criteria[attrName[2:]] = attrValue criteria[attrName[2:]] = attrValue
# Complete criteria with Ref info if the search is restricted to
# referenced objects of a Ref field.
refInfo = rq.get('ref', None)
if refInfo: criteria['_ref'] = refInfo
rq.SESSION['searchCriteria'] = criteria rq.SESSION['searchCriteria'] = criteria
# Go to the screen that displays search results # Go to the screen that displays search results
backUrl = '%s/query?type_name=%s&&search=_advanced' % \ backUrl = '%s/query?type_name=%s&&search=_advanced' % \
(os.path.dirname(rq['URL']), rq['type_name']) (os.path.dirname(rq['URL']),rq['type_name'])
return self.goto(backUrl) return self.goto(backUrl)
def getJavascriptMessages(self): def getJavascriptMessages(self):
@ -605,6 +609,18 @@ class ToolMixin(BaseMixin):
res += 'var %s = "%s";\n' % (msg, self.translate(msg)) res += 'var %s = "%s";\n' % (msg, self.translate(msg))
return res return res
def getRefInfo(self, refInfo=None):
'''When a search is restricted to objects referenced through a Ref
field, this method returns information about this reference: the
source content type and the Ref field (Appy type). If p_refInfo is
not given, we search it among search criteria in the session.'''
if not refInfo:
criteria = self.REQUEST.SESSION.get('searchCriteria', None)
if criteria and criteria.has_key('_ref'): refInfo = criteria['_ref']
if not refInfo: return ('', None)
sourceContentType, refField = refInfo.split(':')
return refInfo, self.getAppyType(refField, className=sourceContentType)
def getSearches(self, contentType): def getSearches(self, contentType):
'''Returns the list of searches that are defined for p_contentType. '''Returns the list of searches that are defined for p_contentType.
Every list item is a dict that contains info about a search or about Every list item is a dict that contains info about a search or about
@ -650,8 +666,9 @@ class ToolMixin(BaseMixin):
on p_contentType.''' on p_contentType.'''
baseUrl = self.getAppFolder().absolute_url() + '/skyn' baseUrl = self.getAppFolder().absolute_url() + '/skyn'
baseParams = 'type_name=%s' % contentType baseParams = 'type_name=%s' % contentType
# Manage start number
rq = self.REQUEST rq = self.REQUEST
if rq.get('ref'): baseParams += '&ref=%s' % rq.get('ref')
# Manage start number
if startNumber != None: if startNumber != None:
baseParams += '&startNumber=%s' % startNumber baseParams += '&startNumber=%s' % startNumber
elif rq.has_key('startNumber'): elif rq.has_key('startNumber'):

View file

@ -490,9 +490,25 @@ class BaseMixin:
if tjs not in js: js.append(tjs) if tjs not in js: js.append(tjs)
return css, js return css, js
def getAppyTypesFromNames(self, fieldNames, asDict=True): def getAppyTypesFromNames(self, fieldNames, asDict=True, addTitle=True):
'''Gets the Appy types names p_fieldNames.''' '''Gets the Appy types named p_fieldNames. If 'title' is not among
return [self.getAppyType(name, asDict) for name in fieldNames] p_fieldNames and p_addTitle is True, field 'title' is prepended to
the result.'''
res = []
for name in fieldNames:
appyType = self.getAppyType(name, asDict)
if appyType: res.append(appyType)
elif name == 'workflowState':
# We do not return a appyType if the attribute is not a *real*
# attribute, but the workfow state.
res.append({'name': name, 'labelId': 'workflow_state',
'filterable': False})
else:
self.appy().log('Field "%s", used as shownInfo in a Ref, ' \
'was not found.' % name, type='warning')
if addTitle and ('title' not in fieldNames):
res.insert(0, self.getAppyType('title', asDict))
return res
def getAppyStates(self, phase, currentOnly=False): def getAppyStates(self, phase, currentOnly=False):
'''Returns information about the states that are related to p_phase. '''Returns information about the states that are related to p_phase.

View file

@ -71,7 +71,7 @@ class User(ModelClass):
_appy_attributes = ['title', 'name', 'firstName', 'login', 'password1', _appy_attributes = ['title', 'name', 'firstName', 'login', 'password1',
'password2', 'roles'] 'password2', 'roles']
# All methods defined below are fake. Real versions are in the wrapper. # All methods defined below are fake. Real versions are in the wrapper.
title = String(show=False) title = String(show=False, indexed=True)
gm = {'group': 'main', 'multiplicity': (1,1)} gm = {'group': 'main', 'multiplicity': (1,1)}
name = String(**gm) name = String(**gm)
firstName = String(**gm) firstName = String(**gm)
@ -109,8 +109,9 @@ class Tool(ModelClass):
# link to the predefined User class or a custom class defined in the # link to the predefined User class or a custom class defined in the
# application. # application.
users = Ref(None, multiplicity=(0,None), add=True, link=False, users = Ref(None, multiplicity=(0,None), add=True, link=False,
back=Ref(attribute='toTool'), page='users', back=Ref(attribute='toTool'), page='users', queryable=True,
shownInfo=('login', 'title', 'roles'), showHeaders=True) queryFields=('login',), showHeaders=True,
shownInfo=('login', 'title', 'roles'))
enableNotifications = Boolean(default=True, page='notifications') enableNotifications = Boolean(default=True, page='notifications')
def validPythonWithUno(self, value): pass # Real method in the wrapper def validPythonWithUno(self, value): pass # Real method in the wrapper
unoEnabledPython = String(group="connectionToOpenOffice", unoEnabledPython = String(group="connectionToOpenOffice",

View file

@ -1,6 +1,9 @@
<metal:queryResults define-macro="queryResult" <metal:queryResults define-macro="queryResult"
tal:define="tool python: contextObj; tal:define="tool python: contextObj;
contentType request/type_name; contentType request/type_name;
refInfo tool/getRefInfo;
refField python: refInfo[1];
refInfo python: refInfo[0];
startNumber request/startNumber|python:'0'; startNumber request/startNumber|python:'0';
startNumber python: int(startNumber); startNumber python: int(startNumber);
searchName request/search; searchName request/search;
@ -11,13 +14,13 @@
sortOrder request/sortOrder| python:'asc'; sortOrder request/sortOrder| python:'asc';
filterKey request/filterKey| python:''; filterKey request/filterKey| python:'';
filterValue request/filterValue | python:''; filterValue request/filterValue | python:'';
queryResult python: tool.executeQuery(contentType, searchName, startNumber, remember=True, sortBy=sortKey, sortOrder=sortOrder, filterKey=filterKey, filterValue=filterValue); queryResult python: tool.executeQuery(contentType, searchName, startNumber, remember=True, sortBy=sortKey, sortOrder=sortOrder, filterKey=filterKey, filterValue=filterValue, refField=refField);
objs queryResult/objects; objs queryResult/objects;
totalNumber queryResult/totalNumber; totalNumber queryResult/totalNumber;
batchSize queryResult/batchSize; batchSize queryResult/batchSize;
ajaxHookId python:'queryResult'; ajaxHookId python:'queryResult';
navBaseCall python: 'askQueryResult(\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, tool.absolute_url(), contentType, searchName); navBaseCall python: 'askQueryResult(\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, tool.absolute_url(), contentType, searchName);
newSearchUrl python: '%s/skyn/search?type_name=%s&' % (tool.getAppFolder().absolute_url(), contentType);"> newSearchUrl python: '%s/skyn/search?type_name=%s&ref=%s' % (tool.getAppFolder().absolute_url(), contentType, refInfo);">
<tal:result condition="objs"> <tal:result condition="objs">
@ -42,44 +45,23 @@
</td> </td>
</tr></table> </tr></table>
<table tal:define="fieldDescrs python: tool.getResultColumns(objs[0], contentType)" <table tal:define="fieldNames python: tool.getResultColumnsNames(contentType, refField);
widgets python: objs[0].getAppyTypesFromNames(fieldNames);"
class="listing nosort" width="100%" cellpadding="0" cellspacing="0"> class="listing nosort" width="100%" cellpadding="0" cellspacing="0">
<tal:comment replace="nothing">Every item in fieldDescrs is an Appy type (dict version),
excepted for workflow state (which is not a field): in this case it is simply the
string "workflow_state".</tal:comment>
<tal:comment replace="nothing">Headers, with filters and sort arrows</tal:comment> <tal:comment replace="nothing">Headers, with filters and sort arrows</tal:comment>
<tr> <tr>
<tal:comment replace="nothing">Mandatory column "Title"/"Name"</tal:comment> <tal:header repeat="widget widgets">
<th tal:define="sortable python: tool.isSortable(widget['name'], contentType, 'search');
<th tal:define="fieldName python:'title'; sortable python:True; filterable python:True"> filterable widget/filterable|nothing;">
<span tal:replace="structure python: tool.truncateText(tool.translate('ref_name'))"/> <span tal:replace="structure python: tool.truncateText(tool.translate(widget['labelId']))"/>
<metal:sortAndFilter use-macro="here/skyn/navigate/macros/sortAndFilter"/> <metal:icons use-macro="here/skyn/navigate/macros/sortAndFilter"/>
</th> </th>
</tal:header>
<tal:comment replace="nothing">Columns corresponding to other fields</tal:comment> <tal:comment replace="nothing">Object type, shown if instances of several types are shown</tal:comment>
<tal:columnHeader repeat="fieldDescr fieldDescrs">
<th tal:define="fieldName fieldDescr/name|string:workflow_state;
sortable python: tool.isSortable(fieldName, contentType, 'search');
filterable fieldDescr/filterable|nothing;">
<tal:comment replace="nothing">Display header for a "standard" field</tal:comment>
<tal:standardField condition="python: fieldName != 'workflow_state'">
<span tal:replace="structure python: tool.truncateText(tool.translate(fieldDescr['labelId']))"/>
</tal:standardField>
<tal:comment replace="nothing">Display header for the workflow state</tal:comment>
<tal:workflowState condition="python: fieldName == 'workflow_state'">
<span tal:replace="python: tool.translate('workflow_state')"/>
</tal:workflowState>
<metal:sortAndFilter use-macro="here/skyn/navigate/macros/sortAndFilter"/>
</th>
</tal:columnHeader>
<tal:comment replace="nothing">Column "Object type", shown if instances of several types are shown</tal:comment>
<th tal:condition="severalTypes"> <th tal:condition="severalTypes">
<span tal:replace="python: tool.translate('root_type')"/> <span tal:replace="python: tool.translate('root_type')"></span>
</th> </th>
<tal:comment replace="nothing">Actions</tal:comment>
<tal:comment replace="nothing">Column "Actions"</tal:comment>
<th tal:content="python: tool.translate('ref_actions')"></th> <th tal:content="python: tool.translate('ref_actions')"></th>
</tr> </tr>
@ -88,15 +70,22 @@
<tr id="query_row" tal:define="odd repeat/obj/odd" <tr id="query_row" tal:define="odd repeat/obj/odd"
tal:attributes="class python:test(odd, 'even', 'odd')"> tal:attributes="class python:test(odd, 'even', 'odd')">
<tal:comment replace="nothing">Mandatory column "Title"/"Name"</tal:comment> <tal:fields repeat="widget widgets">
<td id="field_title"><a <tal:comment replace="nothing">Title</tal:comment>
tal:define="navInfo python:'search.%s.%s.%d.%d' % (contentType, searchName, repeat['obj'].number()+startNumber, totalNumber);" <td id="field_title"
tal:content="obj/Title" tal:attributes="href python: obj.getUrl(nav=navInfo, page='main')"></a></td> tal:condition="python: widget['name'] == 'title'">
<a tal:define="navInfo python:'search.%s.%s.%d.%d' % (contentType, searchName, repeat['obj'].number()+startNumber, totalNumber);"
tal:content="obj/Title" tal:attributes="href python: obj.getUrl(nav=navInfo, page='main')"></a>
</td>
<tal:comment replace="nothing">Columns corresponding to other fields</tal:comment> <tal:comment replace="nothing">Workflow state</tal:comment>
<tal:otherFields repeat="widget fieldDescrs"> <td id="field_workflow_state"
<tal:standardField condition="python: widget != 'workflowState'"> tal:condition="python: widget['name'] == 'workflowState'"
<td tal:condition="python: '_wrong' not in widget" tal:content="python: tool.translate(obj.getWorkflowLabel())">
</td>
<tal:comment replace="nothing">Any other field</tal:comment>
<td tal:condition="python: widget['name'] not in ('title', 'workflowState')"
tal:attributes="id python:'field_%s' % widget['name']"> tal:attributes="id python:'field_%s' % widget['name']">
<tal:field define="contextObj python:obj; <tal:field define="contextObj python:obj;
layoutType python:'cell'; layoutType python:'cell';
@ -105,14 +94,7 @@
<metal:field use-macro="here/skyn/widgets/show/macros/field"/> <metal:field use-macro="here/skyn/widgets/show/macros/field"/>
</tal:field> </tal:field>
</td> </td>
<td tal:condition="python: '_wrong' in widget" style="color:red">Field </tal:fields>
<span tal:replace="widget/name"/> not found.
</td>
</tal:standardField>
<tal:workflowState condition="python: widget == 'workflowState'">
<td id="field_workflow_state" tal:content="python: tool.translate(obj.getWorkflowLabel())"></td>
</tal:workflowState>
</tal:otherFields>
<tal:comment replace="nothing">Column "Object type", shown if instances of several types are shown</tal:comment> <tal:comment replace="nothing">Column "Object type", shown if instances of several types are shown</tal:comment>
<td tal:condition="severalTypes" id="field_root_type" <td tal:condition="severalTypes" id="field_root_type"

View file

@ -95,7 +95,7 @@
This macro displays up/down arrows in a table header column for sorting a given column. This macro displays up/down arrows in a table header column for sorting a given column.
It requires variables "sortable", 'filterable' and 'fieldName'. It requires variables "sortable", 'filterable' and 'fieldName'.
</tal:comment> </tal:comment>
<metal:sortAndFilter define-macro="sortAndFilter"> <metal:sortAndFilter define-macro="sortAndFilter" tal:define="fieldName widget/name">
<tal:sort condition="sortable"> <tal:sort condition="sortable">
<img tal:attributes="src string: $portal_url/skyn/sortDown.gif; <img tal:attributes="src string: $portal_url/skyn/sortDown.gif;
onClick python: navBaseCall.replace('**v**', '0,\'%s\',\'asc\',\'%s\'' % (fieldName, filterKey))" onClick python: navBaseCall.replace('**v**', '0,\'%s\',\'asc\',\'%s\'' % (fieldName, filterKey))"

View file

@ -28,7 +28,7 @@
</td> </td>
</dt> </dt>
<tal:publishedObject condition="python: contextObj and tool.userMayNavigate(contextObj.meta_type)"> <tal:publishedObject condition="python: contextObj and tool.userMayNavigate(contextObj)">
<dt class="portletAppyItem portletCurrent"><b tal:content="contextObj/Title"></b></dt> <dt class="portletAppyItem portletCurrent"><b tal:content="contextObj/Title"></b></dt>
<dt class="portletAppyItem"><metal:phases use-macro="here/skyn/portlet/macros/phases"/></dt> <dt class="portletAppyItem"><metal:phases use-macro="here/skyn/portlet/macros/phases"/></dt>
</tal:publishedObject> </tal:publishedObject>

View file

@ -15,8 +15,8 @@
tal:define="appFolder context/getParentNode; tal:define="appFolder context/getParentNode;
appName appFolder/id; appName appFolder/id;
tool python: portal.get('portal_%s' % appName.lower()); tool python: portal.get('portal_%s' % appName.lower());
contentType python:context.REQUEST.get('type_name'); contentType request/type_name;
searchName python:context.REQUEST.get('search', '')"> searchName request/search|python:'';">
<div metal:use-macro="here/skyn/page/macros/prologue"/> <div metal:use-macro="here/skyn/page/macros/prologue"/>
<tal:comment replace="nothing">Query result</tal:comment> <tal:comment replace="nothing">Query result</tal:comment>

View file

@ -15,8 +15,11 @@
<metal:fill fill-slot="main" <metal:fill fill-slot="main"
tal:define="appFolder context/getParentNode; tal:define="appFolder context/getParentNode;
contentType request/type_name; contentType request/type_name;
refInfo request/ref|nothing;
tool python: portal.get('portal_%s' % appFolder.id.lower()); tool python: portal.get('portal_%s' % appFolder.id.lower());
searchableFields python: tool.getSearchableFields(contentType)"> searchInfo python: tool.getSearchInfo(contentType, refInfo);
searchableFields python: searchInfo[0];
numberOfColumns python: searchInfo[1]">
<tal:comment replace="nothing">Search title</tal:comment> <tal:comment replace="nothing">Search title</tal:comment>
<h1><span tal:replace="python: tool.translate('%s_plural' % contentType)"/> — <h1><span tal:replace="python: tool.translate('%s_plural' % contentType)"/> —
@ -26,9 +29,9 @@
<form name="search" tal:attributes="action python: appFolder.absolute_url()+'/skyn/do'" method="post"> <form name="search" tal:attributes="action python: appFolder.absolute_url()+'/skyn/do'" method="post">
<input type="hidden" name="action" value="SearchObjects"/> <input type="hidden" name="action" value="SearchObjects"/>
<input type="hidden" name="type_name" tal:attributes="value contentType"/> <input type="hidden" name="type_name" tal:attributes="value contentType"/>
<input tal:condition="refInfo" type="hidden" name="ref" tal:attributes="value refInfo"/>
<table class="no-style-table" cellpadding="0" cellspacing="0" width="100%" <table class="no-style-table" cellpadding="0" cellspacing="0" width="100%">
tal:define="numberOfColumns python: tool.getAttr('numberOfSearchColumnsFor%s' % contentType)">
<tr tal:repeat="searchRow python: tool.tabularize(searchableFields, numberOfColumns)" valign="top"> <tr tal:repeat="searchRow python: tool.tabularize(searchableFields, numberOfColumns)" valign="top">
<td tal:repeat="widget searchRow" tal:attributes="width python:'%d%%' % (100/numberOfColumns)"> <td tal:repeat="widget searchRow" tal:attributes="width python:'%d%%' % (100/numberOfColumns)">
<tal:field condition="widget"> <tal:field condition="widget">

View file

@ -78,7 +78,7 @@
ref field according to the field that corresponds to this column. ref field according to the field that corresponds to this column.
</tal:comment> </tal:comment>
<metal:sortIcons define-macro="sortIcons" <metal:sortIcons define-macro="sortIcons"
tal:define="ajaxBaseCall python: navBaseCall.replace('**v**', '\'%s\',\'SortReference\', {\'sortKey\':\'%s\', \'reverse\':\'**v**\'}' % (startNumber, shownField))" tal:condition="python: canWrite and tool.isSortable(shownField, objs[0].meta_type, 'ref')"> tal:define="ajaxBaseCall python: navBaseCall.replace('**v**', '\'%s\',\'SortReference\', {\'sortKey\':\'%s\', \'reverse\':\'**v**\'}' % (startNumber, widget['name']))" tal:condition="python: canWrite and tool.isSortable(widget['name'], objs[0].meta_type, 'ref')">
<img style="cursor:pointer" <img style="cursor:pointer"
tal:attributes="src string:$portal_url/skyn/sortAsc.png; tal:attributes="src string:$portal_url/skyn/sortAsc.png;
onClick python: ajaxBaseCall.replace('**v**', 'False')"/> onClick python: ajaxBaseCall.replace('**v**', 'False')"/>
@ -159,6 +159,10 @@
<span tal:condition="not: innerRef" tal:content="label"/> <span tal:condition="not: innerRef" tal:content="label"/>
(<span tal:replace="totalNumber"/>) (<span tal:replace="totalNumber"/>)
<metal:plusIcon use-macro="portal/skyn/widgets/ref/macros/plusIcon"/> <metal:plusIcon use-macro="portal/skyn/widgets/ref/macros/plusIcon"/>
<tal:comment replace="nothing">The search icon if field is queryable</tal:comment>
<a tal:condition="appyType/queryable"
tal:attributes="href python: '%s/skyn/search?type_name=%s&ref=%s:%s' % (tool.getAppFolder().absolute_url(), linkedPortalType, contextObj.meta_type, appyType['name'])">
<img src="search.gif" tal:attributes="title python: tool.translate('search_objects')"/></a>
</legend> </legend>
<tal:comment replace="nothing">Object description</tal:comment> <tal:comment replace="nothing">Object description</tal:comment>
@ -185,44 +189,34 @@
</table> </table>
<tal:comment replace="nothing">Show forward reference(s)</tal:comment> <tal:comment replace="nothing">Show forward reference(s)</tal:comment>
<table tal:attributes="class python:test(innerRef, '', 'listing nosort'); <table tal:attributes="class python:test(innerRef, '', 'listing nosort');
width python:test(innerRef, '100%', appyType['layouts']['view']['width']);" width python:test(innerRef, '100%', appyType['layouts']['view']['width']);"
align="right" tal:condition="python: not appyType['isBack'] and objs" cellpadding="0" cellspacing="0"> align="right" tal:condition="python: not appyType['isBack'] and objs" cellpadding="0" cellspacing="0">
<tal:widgets define="widgets python: objs[0].getAppyTypesFromNames(appyType['shownInfo'])"> <tal:widgets define="widgets python: objs[0].getAppyTypesFromNames(appyType['shownInfo'])">
<tr tal:condition="appyType/showHeaders"> <tr tal:condition="appyType/showHeaders">
<th tal:condition="python: 'title' not in appyType['shownInfo']"
tal:define="shownField python:'title'">
<span tal:content="python: tool.translate('ref_name')"></span>
<metal:sortIcons use-macro="portal/skyn/widgets/ref/macros/sortIcons" />
</th>
<th tal:repeat="widget widgets"> <th tal:repeat="widget widgets">
<tal:header define="shownField widget/name">
<span tal:content="python: tool.translate(widget['labelId'])"></span> <span tal:content="python: tool.translate(widget['labelId'])"></span>
<metal:sortIcons use-macro="portal/skyn/widgets/ref/macros/sortIcons" /> <metal:sortIcons use-macro="portal/skyn/widgets/ref/macros/sortIcons" />
</tal:header>
</th> </th>
<th tal:content="python: tool.translate('ref_actions')"></th> <th tal:content="python: tool.translate('ref_actions')"></th>
</tr> </tr>
<tal:row repeat="obj objs"> <tal:row repeat="obj objs">
<tr valign="top" tal:define="odd repeat/obj/odd" <tr valign="top" tal:define="odd repeat/obj/odd"
tal:attributes="class python:test(odd, 'even', 'odd')"> tal:attributes="class python:test(odd, 'even', 'odd')">
<tal:comment replace="nothing">Object title, shown here if not specified somewhere
else in appyType.shownInfo.</tal:comment>
<td tal:condition="python: 'title' not in appyType['shownInfo']"><metal:showObjectTitle
use-macro="portal/skyn/widgets/ref/macros/objectTitle"/>
</td>
<tal:comment replace="nothing">Additional fields that must be shown</tal:comment>
<td tal:repeat="widget widgets"> <td tal:repeat="widget widgets">
<tal:showTitle condition="python: widget['name'] == 'title'"> <tal:title condition="python: widget['name'] == 'title'">
<metal:showObjectTitle use-macro="portal/skyn/widgets/ref/macros/objectTitle"/> <metal:showObjectTitle use-macro="portal/skyn/widgets/ref/macros/objectTitle"/>
</tal:showTitle> </tal:title>
<tal:showOtherField define="contextObj python:obj; <tal:state condition="python: widget['name'] == 'workflowState'"
content="python: tool.translate(obj.getWorkflowLabel())">
</tal:state>
<tal:other condition="python: widget['name'] not in ('title', 'workflowState')">
<tal:field define="contextObj python:obj;
layoutType python: 'cell'; layoutType python: 'cell';
innerRef python:True" innerRef python:True">
condition="python: widget['name'] != 'title'"> <metal:field use-macro="portal/skyn/widgets/show/macros/field" />
<metal:showField use-macro="portal/skyn/widgets/show/macros/field" /> </tal:field>
</tal:showOtherField> </tal:other>
</td> </td>
<tal:comment replace="nothing">Actions</tal:comment> <tal:comment replace="nothing">Actions</tal:comment>
<td align="right"> <td align="right">
@ -232,7 +226,6 @@
</tal:row> </tal:row>
</tal:widgets> </tal:widgets>
</table> </table>
</td></tr> </td></tr>
</table> </table>

View file

@ -140,7 +140,9 @@
<table metal:define-macro="groupContent" <table metal:define-macro="groupContent"
tal:attributes="width python: test(widget['wide'], '100%', ''); tal:attributes="width python: test(widget['wide'], '100%', '');
align widget/align; align widget/align;
class widget/css_class"> class widget/css_class;
cellspacing widget/cellspacing;
cellpadding widget/cellpadding">
<tal:comment replace="nothing">Display the title of the group if it is not rendered a fieldset.</tal:comment> <tal:comment replace="nothing">Display the title of the group if it is not rendered a fieldset.</tal:comment>
<tr tal:condition="python: (widget['style'] != 'fieldset') and widget['hasLabel']"> <tr tal:condition="python: (widget['style'] != 'fieldset') and widget['hasLabel']">
<td tal:attributes="colspan python: len(widget['columnsWidths']); <td tal:attributes="colspan python: len(widget['columnsWidths']);

View file

@ -46,7 +46,6 @@ class PoMessage:
POD_ASKACTION = 'Trigger related action' POD_ASKACTION = 'Trigger related action'
REF_NO = 'No object.' REF_NO = 'No object.'
REF_ADD = 'Add a new one' REF_ADD = 'Add a new one'
REF_NAME = 'Name'
REF_ACTIONS = 'Actions' REF_ACTIONS = 'Actions'
REF_MOVE_UP = 'Move up' REF_MOVE_UP = 'Move up'
REF_MOVE_DOWN = 'Move down' REF_MOVE_DOWN = 'Move down'