diff --git a/gen/__init__.py b/gen/__init__.py index f659aad..1e781e6 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -90,7 +90,8 @@ class Group: def __init__(self, name, columns=['100%'], wide=True, style='section2', hasLabel=True, hasDescr=False, hasHelp=False, 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 # 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 @@ -127,6 +128,8 @@ class Group: self.colspan = colspan self.align = align self.valign = valign + self.cellpadding = cellpadding + self.cellspacing = cellspacing if style == 'tabs': # Group content will be rendered as tabs. In this case, some # param combinations have no sense. @@ -289,8 +292,9 @@ class Search: self.group = group # Searches may be visually grouped in the portlet self.sortBy = sortBy self.limit = limit - self.fields = fields # This is a dict whose keys are indexed field - # names and whose values are search values. + # In the dict below, keys are indexed field names and values are + # search values. + self.fields = fields @staticmethod def getIndexName(fieldName, usage='search'): '''Gets the name of the technical index that corresponds to field named @@ -1466,7 +1470,8 @@ class Ref(Type): searchable=False, specificReadPermission=False, specificWritePermission=False, width=None, height=5, colspan=1, master=None, masterValue=None, focus=False, - historized=False): + historized=False, queryable=False, queryFields=None, + queryNbCols=1): self.klass = klass self.attribute = attribute # May the user add new objects through this ref ? @@ -1503,6 +1508,15 @@ class Ref(Type): self.maxPerPage = maxPerPage # Specifies sync 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, editDefault, show, page, group, layouts, move, indexed, False, specificReadPermission, specificWritePermission, diff --git a/gen/plone25/descriptors.py b/gen/plone25/descriptors.py index ca188a5..1c39291 100644 --- a/gen/plone25/descriptors.py +++ b/gen/plone25/descriptors.py @@ -535,7 +535,6 @@ class UserClassDescriptor(ClassDescriptor): self.klass = klass self.customized = True def isFolder(self, klass=None): return True - def isRoot(self): return False def generateSchema(self): ClassDescriptor.generateSchema(self, configClass=True) diff --git a/gen/plone25/generator.py b/gen/plone25/generator.py index 402b719..898c11b 100644 --- a/gen/plone25/generator.py +++ b/gen/plone25/generator.py @@ -98,7 +98,6 @@ class Generator(AbstractGenerator): msg('max_ref_violated', '', msg.MAX_REF_VIOLATED), msg('no_ref', '', msg.REF_NO), msg('add_ref', '', msg.REF_ADD), - msg('ref_name', '', msg.REF_NAME), msg('ref_actions', '', msg.REF_ACTIONS), msg('move_up', '', msg.REF_MOVE_UP), msg('move_down', '', msg.REF_MOVE_DOWN), @@ -303,7 +302,7 @@ class Generator(AbstractGenerator): repls['addPermissions'] = addPermissions # Compute root classes rootClasses = '' - for classDescr in self.classes: + for classDescr in self.getClasses(include='allButTool'): if classDescr.isRoot(): rootClasses += "'%s'," % classDescr.name repls['rootClasses'] = rootClasses @@ -617,9 +616,20 @@ class Generator(AbstractGenerator): if self.user.customized: 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 # 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 childDescr in classDescr.getChildren(): childFieldName = fieldName % childDescr.name @@ -644,16 +654,6 @@ class Generator(AbstractGenerator): repls['wrapperClass'] = '%s_Wrapper' % self.tool.name 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): '''Is called each time an Appy class is found in the application, for generating the corresponding Archetype class and schema.''' diff --git a/gen/plone25/mixins/ToolMixin.py b/gen/plone25/mixins/ToolMixin.py index 60a77e8..10b98e1 100644 --- a/gen/plone25/mixins/ToolMixin.py +++ b/gen/plone25/mixins/ToolMixin.py @@ -164,16 +164,25 @@ class ToolMixin(BaseMixin): res.append((appyType.name, self.translate(appyType.labelId))) return res - def getSearchableFields(self, contentType): - '''Returns, among the list of all searchable fields (see method above), - the list of fields that the user has configured as being effectively - used in the search screen.''' - res = [] - fieldNames = getattr(self.appy(), 'searchFieldsFor%s' % contentType, ()) + def getSearchInfo(self, contentType, refInfo=None): + '''Returns a tuple: + 1) The list of searchable fields (ie fields among all indexed fields) + 2) The number of columns for layouting those fields.''' + fields = [] + 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: appyType = self.getAppyType(name, asDict=True,className=contentType) - res.append(appyType) - return res + fields.append(appyType) + return fields, nbOfColumns def getImportElements(self, contentType): '''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, search=None, remember=False, brainsOnly=False, 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 with commas) in Plone's portal_catalog. If p_searchName is specified, it corresponds to: @@ -256,7 +266,10 @@ class ToolMixin(BaseMixin): If p_filterKey is given, it represents an additional search parameter 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 ? if contentType.find(',') != -1: portalTypes = contentType.split(',') @@ -276,6 +289,9 @@ class ToolMixin(BaseMixin): if search: # Add additional search criteria 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 # name of the corresponding index. attrName = Search.getIndexName(fieldName) @@ -306,7 +322,9 @@ class ToolMixin(BaseMixin): # Return brains only. if not maxResults: return brains 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 res = SomeObjects(brains, maxResults, startNumber,noSecurity=noSecurity) res.brainsToObjects() @@ -327,14 +345,17 @@ class ToolMixin(BaseMixin): s['search_%s' % searchName] = uids return res.__dict__ - def getResultColumnsNames(self, contentType): + def getResultColumnsNames(self, contentType, refField): contentTypes = contentType.strip(',').split(',') resSet = None # Temporary set for computing intersections. res = [] # Final, sorted result. fieldNames = None appyTool = self.appy() for cType in contentTypes: - fieldNames = getattr(appyTool, 'resultColumnsFor%s' % cType) + if refField: + fieldNames = refField.shownInfo + else: + fieldNames = getattr(appyTool, 'resultColumnsFor%s' % cType) if not resSet: resSet = set(fieldNames) else: @@ -346,27 +367,6 @@ class ToolMixin(BaseMixin): res.append(fieldName) 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): '''Truncates the p_value according to p_appyType width.''' maxWidth = appyType['width'] @@ -452,14 +452,13 @@ class ToolMixin(BaseMixin): return pythonClass.maySearch(self.appy()) return True - def userMayNavigate(self, someClass): + def userMayNavigate(self, obj): '''This method checks if the currently logged user can display the navigation panel within the portlet. This is done by calling method - "mayNavigate" on the class whose currently shown object is an - instance of. If no such method exists, we return True.''' - pythonClass = self.getAppyClass(someClass) - if 'mayNavigate' in pythonClass.__dict__: - return pythonClass.mayNavigate(self.appy()) + "mayNavigate" on the currently shown object. If no such method + exists, we return True.''' + appyObj = obj.appy() + if hasattr(appyObj, 'mayNavigate'): return appyObj.mayNavigate() return True def onImportObjects(self): @@ -485,8 +484,9 @@ class ToolMixin(BaseMixin): return False 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")?''' + if (',' in className) or (name == 'workflowState'): return False appyType = self.getAppyType(name, className=className) return appyType.isSortable(usage=usage) @@ -591,10 +591,14 @@ class ToolMixin(BaseMixin): oper = ' %s ' % rq.form.get(operKey, 'or').upper() attrValue = oper.join(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 # Go to the screen that displays search results 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) def getJavascriptMessages(self): @@ -605,6 +609,18 @@ class ToolMixin(BaseMixin): res += 'var %s = "%s";\n' % (msg, self.translate(msg)) 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): '''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 @@ -650,8 +666,9 @@ class ToolMixin(BaseMixin): on p_contentType.''' baseUrl = self.getAppFolder().absolute_url() + '/skyn' baseParams = 'type_name=%s' % contentType - # Manage start number rq = self.REQUEST + if rq.get('ref'): baseParams += '&ref=%s' % rq.get('ref') + # Manage start number if startNumber != None: baseParams += '&startNumber=%s' % startNumber elif rq.has_key('startNumber'): diff --git a/gen/plone25/mixins/__init__.py b/gen/plone25/mixins/__init__.py index 9356ca7..f6f6189 100644 --- a/gen/plone25/mixins/__init__.py +++ b/gen/plone25/mixins/__init__.py @@ -490,9 +490,25 @@ class BaseMixin: if tjs not in js: js.append(tjs) return css, js - def getAppyTypesFromNames(self, fieldNames, asDict=True): - '''Gets the Appy types names p_fieldNames.''' - return [self.getAppyType(name, asDict) for name in fieldNames] + def getAppyTypesFromNames(self, fieldNames, asDict=True, addTitle=True): + '''Gets the Appy types named p_fieldNames. If 'title' is not among + 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): '''Returns information about the states that are related to p_phase. diff --git a/gen/plone25/model.py b/gen/plone25/model.py index fbefdc4..7ae7583 100644 --- a/gen/plone25/model.py +++ b/gen/plone25/model.py @@ -71,7 +71,7 @@ class User(ModelClass): _appy_attributes = ['title', 'name', 'firstName', 'login', 'password1', 'password2', 'roles'] # 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)} name = 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 # application. users = Ref(None, multiplicity=(0,None), add=True, link=False, - back=Ref(attribute='toTool'), page='users', - shownInfo=('login', 'title', 'roles'), showHeaders=True) + back=Ref(attribute='toTool'), page='users', queryable=True, + queryFields=('login',), showHeaders=True, + shownInfo=('login', 'title', 'roles')) enableNotifications = Boolean(default=True, page='notifications') def validPythonWithUno(self, value): pass # Real method in the wrapper unoEnabledPython = String(group="connectionToOpenOffice", diff --git a/gen/plone25/skin/macros.pt b/gen/plone25/skin/macros.pt index 15ae643..fdf70ad 100644 --- a/gen/plone25/skin/macros.pt +++ b/gen/plone25/skin/macros.pt @@ -1,6 +1,9 @@ + newSearchUrl python: '%s/skyn/search?type_name=%s&ref=%s' % (tool.getAppFolder().absolute_url(), contentType, refInfo);"> @@ -42,44 +45,23 @@ - - 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". - +
Headers, with filters and sort arrows - Mandatory column "Title"/"Name" - - - - Columns corresponding to other fields - - - - - Column "Object type", shown if instances of several types are shown + + Object type, shown if instances of several types are shown - - Column "Actions" + Actions @@ -88,31 +70,31 @@ - Mandatory column "Title"/"Name" - + + Title + - Columns corresponding to other fields - - - - - - - - - + Workflow state + + + Any other field + + Column "Object type", shown if instances of several types are shown
- - - - Display header for a "standard" field - - - - Display header for the workflow state - - - - + + + + - +
+ + - - - - Field - not found. - + + + + + - + - +
diff --git a/gen/plone25/skin/query.pt b/gen/plone25/skin/query.pt index da0b763..357fcd8 100644 --- a/gen/plone25/skin/query.pt +++ b/gen/plone25/skin/query.pt @@ -15,8 +15,8 @@ tal:define="appFolder context/getParentNode; appName appFolder/id; tool python: portal.get('portal_%s' % appName.lower()); - contentType python:context.REQUEST.get('type_name'); - searchName python:context.REQUEST.get('search', '')"> + contentType request/type_name; + searchName request/search|python:'';">
Query result diff --git a/gen/plone25/skin/search.pt b/gen/plone25/skin/search.pt index a2b5320..567c070 100644 --- a/gen/plone25/skin/search.pt +++ b/gen/plone25/skin/search.pt @@ -15,8 +15,11 @@ + searchInfo python: tool.getSearchInfo(contentType, refInfo); + searchableFields python: searchInfo[0]; + numberOfColumns python: searchInfo[1]"> Search title

— @@ -26,9 +29,9 @@
+ - +
diff --git a/gen/plone25/skin/widgets/ref.pt b/gen/plone25/skin/widgets/ref.pt index 6b8c8d2..2dc4e63 100644 --- a/gen/plone25/skin/widgets/ref.pt +++ b/gen/plone25/skin/widgets/ref.pt @@ -78,7 +78,7 @@ ref field according to the field that corresponds to this column. + 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')"> @@ -159,6 +159,10 @@ () + The search icon if field is queryable + + Object description @@ -185,44 +189,34 @@
Show forward reference(s) - - - Object title, shown here if not specified somewhere - else in appyType.shownInfo. - - Additional fields that must be shownActions
- - - - - - - + +
- - + - - - - + + + + + + + + @@ -232,7 +226,6 @@
-

diff --git a/gen/plone25/skin/widgets/show.pt b/gen/plone25/skin/widgets/show.pt index 209f373..52283f9 100644 --- a/gen/plone25/skin/widgets/show.pt +++ b/gen/plone25/skin/widgets/show.pt @@ -140,7 +140,9 @@ + class widget/css_class; + cellspacing widget/cellspacing; + cellpadding widget/cellpadding"> Display the title of the group if it is not rendered a fieldset.