First version of a searches system, where queries may be defined on every root class. The portlet and query pages have been deeply revised. Ajax is now used for displaying query results and appy-specific ajax-based navigation is used for refs as well as for queries in portal_catalog.

This commit is contained in:
Gaetan Delannay 2009-11-03 15:02:18 +01:00
parent 2b907fee32
commit 1c0744da85
12 changed files with 371 additions and 253 deletions

View file

@ -35,7 +35,7 @@ class Search:
self.sortBy = sortBy self.sortBy = sortBy
self.limit = limit self.limit = limit
self.fields = fields # This is a dict whose keys are indexed field self.fields = fields # This is a dict whose keys are indexed field
# names and whosse values are search values. # names and whose values are search values.
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class Type: class Type:
@ -162,7 +162,8 @@ class String(Type):
character is ignored. If p_complement is True, it does compute the character is ignored. If p_complement is True, it does compute the
complement of modulo 97 instead of modulo 97. p_obj is not used; complement of modulo 97 instead of modulo 97. p_obj is not used;
it will be given by the Appy validation machinery, so it must be it will be given by the Appy validation machinery, so it must be
specified as parameter.''' specified as parameter. The function returns True if the check is
successful.'''
if not value: return True # Plone calls me erroneously for if not value: return True # Plone calls me erroneously for
# non-mandatory fields. # non-mandatory fields.
# First, remove any non-digit char # First, remove any non-digit char

View file

@ -116,9 +116,6 @@ class ArchetypeFieldDescriptor:
self.widgetParams['size'] = 50 self.widgetParams['size'] = 50
if self.appyType.width: if self.appyType.width:
self.widgetParams['size'] = self.appyType.width self.widgetParams['size'] = self.appyType.width
# Manage index
if self.appyType.searchable:
self.fieldParams['index'] = 'FieldIndex'
elif self.appyType.format == String.TEXT: elif self.appyType.format == String.TEXT:
self.fieldType = 'TextField' self.fieldType = 'TextField'
self.widgetType = 'TextAreaWidget' self.widgetType = 'TextAreaWidget'
@ -132,9 +129,6 @@ class ArchetypeFieldDescriptor:
else: else:
self.fieldType = 'StringField' self.fieldType = 'StringField'
self.widgetType = 'StringWidget' self.widgetType = 'StringWidget'
# Manage searchability
if self.appyType.searchable:
self.fieldParams['searchable'] = True
def walkComputed(self): def walkComputed(self):
'''How to generate a computed field? We generate an Archetypes String '''How to generate a computed field? We generate an Archetypes String
@ -206,11 +200,9 @@ class ArchetypeFieldDescriptor:
self.fieldParams['default'] = self.appyType.default self.fieldParams['default'] = self.appyType.default
# - required? # - required?
if self.appyType.multiplicity[0] >= 1: if self.appyType.multiplicity[0] >= 1:
if self.appyType.type != 'Ref': if self.appyType.type != 'Ref': self.fieldParams['required'] = True
# Indeed, if it is a ref appy will manage itself field updates # Indeed, if it is a ref appy will manage itself field updates in
# in at_post_create_script, so Archetypes must not enforce # onEdit, so Archetypes must not enforce required=True
# required=True
self.fieldParams['required'] = True
# - optional ? # - optional ?
if self.appyType.optional: if self.appyType.optional:
Flavour._appy_addOptionalField(self) Flavour._appy_addOptionalField(self)
@ -222,14 +214,20 @@ class ArchetypeFieldDescriptor:
methodName = 'getDefaultValueFor%s' % self.fieldName methodName = 'getDefaultValueFor%s' % self.fieldName
self.fieldParams['default_method'] = methodName self.fieldParams['default_method'] = methodName
self.classDescr.addDefaultMethod(methodName, self) self.classDescr.addDefaultMethod(methodName, self)
# - put an index on this field?
if self.appyType.indexed:
if (self.appyType.type == 'String') and \
(self.appyType.isMultiValued()):
self.fieldParams['index'] = 'ZCTextIndex, lexicon_id=' \
'plone_lexicon, index_type=Okapi BM25 Rank'
else:
self.fieldParams['index'] = 'FieldIndex'
# - searchable ? # - searchable ?
if self.appyType.searchable and (self.appyType.type != 'String'): if self.appyType.searchable: self.fieldParams['searchable'] = True
self.fieldParams['index'] = 'FieldIndex'
# - slaves ? # - slaves ?
if self.appyType.slaves: if self.appyType.slaves: self.widgetParams['visible'] = False
self.widgetParams['visible'] = False # Archetypes will believe the # Archetypes will believe the field is invisible; we will display it
# field is invisible; we will display it ourselves (like for Ref # ourselves (like for Ref fields)
# fields)
# - need to generate a field validator? # - need to generate a field validator?
# In all cases, add an i18n message for the validation error for this # In all cases, add an i18n message for the validation error for this
# field. # field.
@ -490,6 +488,14 @@ class ArchetypesClassDescriptor(ClassDescriptor):
else: res.append(search) else: res.append(search)
return res return res
@staticmethod
def getSearch(klass, searchName):
'''Gets the search named p_searchName.'''
for search in ArchetypesClassDescriptor.getSearches(klass):
if search.name == searchName:
return search
return None
def addGenerateDocMethod(self): def addGenerateDocMethod(self):
m = self.methods m = self.methods
spaces = TABS spaces = TABS

View file

@ -660,13 +660,17 @@ class Generator(AbstractGenerator):
'implements': implements, 'baseSchema': baseSchema, 'implements': implements, 'baseSchema': baseSchema,
'register': register, 'toolInstanceName': self.toolInstanceName}) 'register': register, 'toolInstanceName': self.toolInstanceName})
fileName = '%s.py' % classDescr.name fileName = '%s.py' % classDescr.name
# Remember i18n labels that will be generated in the i18n file # Create i18n labels (class name, description and plural form)
poMsg = PoMessage(classDescr.name, '', classDescr.klass.__name__) poMsg = PoMessage(classDescr.name, '', classDescr.klass.__name__)
poMsg.produceNiceDefault() poMsg.produceNiceDefault()
self.labels.append(poMsg) self.labels.append(poMsg)
poMsgDescr = PoMessage('%s_edit_descr' % classDescr.name, '', ' ') poMsgDescr = PoMessage('%s_edit_descr' % classDescr.name, '', ' ')
self.labels.append(poMsgDescr) self.labels.append(poMsgDescr)
# Remember i18n labels for flavoured variants poMsgPl = PoMessage('%s_plural' % classDescr.name, '',
classDescr.klass.__name__+'s')
poMsgPl.produceNiceDefault()
self.labels.append(poMsgPl)
# Create i18n labels for flavoured variants
for i in range(2,10): for i in range(2,10):
poMsg = PoMessage('%s_%d' % (classDescr.name, i), '', poMsg = PoMessage('%s_%d' % (classDescr.name, i), '',
classDescr.klass.__name__) classDescr.klass.__name__)
@ -675,7 +679,11 @@ class Generator(AbstractGenerator):
poMsgDescr = PoMessage('%s_%d_edit_descr' % (classDescr.name, i), poMsgDescr = PoMessage('%s_%d_edit_descr' % (classDescr.name, i),
'', ' ') '', ' ')
self.labels.append(poMsgDescr) self.labels.append(poMsgDescr)
# Remember i18n labels for searches poMsgPl = PoMessage('%s_%d_plural' % (classDescr.name, i), '',
classDescr.klass.__name__+'s')
poMsgPl.produceNiceDefault()
self.labels.append(poMsgPl)
# Create i18n labels for searches
for search in classDescr.getSearches(classDescr.klass): for search in classDescr.getSearches(classDescr.klass):
searchLabelId = '%s_search_%s' % (classDescr.name, search.name) searchLabelId = '%s_search_%s' % (classDescr.name, search.name)
searchDescrId = '%s_descr' % searchLabelId searchDescrId = '%s_descr' % searchLabelId

View file

@ -1,9 +1,10 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import re, os, os.path import re, os, os.path
from appy.gen.utils import FieldDescr from appy.gen.utils import FieldDescr, SomeObjects
from appy.gen.plone25.mixins import AbstractMixin from appy.gen.plone25.mixins import AbstractMixin
from appy.gen.plone25.mixins.FlavourMixin import FlavourMixin from appy.gen.plone25.mixins.FlavourMixin import FlavourMixin
from appy.gen.plone25.wrappers import AbstractWrapper from appy.gen.plone25.wrappers import AbstractWrapper
from appy.gen.plone25.descriptors import ArchetypesClassDescriptor
_PY = 'Please specify a file corresponding to a Python interpreter ' \ _PY = 'Please specify a file corresponding to a Python interpreter ' \
'(ie "/usr/bin/python").' '(ie "/usr/bin/python").'
@ -97,24 +98,46 @@ class ToolMixin(AbstractMixin):
def showPortlet(self): def showPortlet(self):
return not self.portal_membership.isAnonymousUser() return not self.portal_membership.isAnonymousUser()
def executeQuery(self, queryName, flavourNumber): def executeQuery(self, contentType, flavourNumber=1, searchName=None,
if queryName.find(',') != -1: startNumber=0):
'''Executes a query on a given p_contentType (or several, separated
with commas) in Plone's portal_catalog. Portal types are from the
flavour numbered p_flavourNumber. If p_searchName is specified, it
corresponds to a search defined on p_contentType: additional search
criteria will be added to the query. We will retrieve objects from
p_startNumber.'''
# Is there one or several content types ?
if contentType.find(',') != -1:
# Several content types are specified # Several content types are specified
portalTypes = queryName.split(',') portalTypes = contentType.split(',')
if flavourNumber != 1: if flavourNumber != 1:
portalTypes = ['%s_%d' % (pt, flavourNumber) \ portalTypes = ['%s_%d' % (pt, flavourNumber) \
for pt in portalTypes] for pt in portalTypes]
else: else:
portalTypes = queryName portalTypes = contentType
params = {'portal_type': portalTypes, 'batch': True} params = {'portal_type': portalTypes, 'batch': True}
res = self.portal_catalog.searchResults(**params) # Manage additional criteria from a search when relevant
batchStart = self.REQUEST.get('b_start', 0) if searchName:
res = self.getProductConfig().Batch(res, # In this case, contentType must contain a single content type.
self.getNumberOfResultsPerPage(), int(batchStart), orphan=0) appyClass = self.getAppyClass(contentType)
return res # Find the search
search = ArchetypesClassDescriptor.getSearch(appyClass, searchName)
for fieldName, fieldValue in search.fields.iteritems():
appyType = getattr(appyClass, fieldName)
attrName = fieldName
if (appyType.type == 'String') and appyType.isMultiValued():
attrName = 'get%s%s' % (fieldName[0].upper(), fieldName[1:])
params[attrName] = fieldValue
brains = self.portal_catalog.searchResults(**params)
print 'Number of results per page is', self.getNumberOfResultsPerPage()
print 'StartNumber is', startNumber
res = SomeObjects(brains, self.getNumberOfResultsPerPage(), startNumber)
res.brainsToObjects()
print 'Res?', res.totalNumber, res.batchSize, res.startNumber
return res.__dict__
def getResultColumnsNames(self, queryName): def getResultColumnsNames(self, contentType):
contentTypes = queryName.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.
flavour = None flavour = None
@ -137,12 +160,12 @@ class ToolMixin(AbstractMixin):
res.append(fieldName) res.append(fieldName)
return res return res
def getResultColumns(self, anObject, queryName): def getResultColumns(self, anObject, contentType):
'''What columns must I show when displaying a list of root class '''What columns must I show when displaying a list of root class
instances? Result is a list of tuples containing the name of the instances? Result is a list of tuples containing the name of the
column (=name of the field) and a FieldDescr instance.''' column (=name of the field) and a FieldDescr instance.'''
res = [] res = []
for fieldName in self.getResultColumnsNames(queryName): for fieldName in self.getResultColumnsNames(contentType):
if fieldName == 'workflowState': if fieldName == 'workflowState':
# We do not return a FieldDescr instance if the attributes is # We do not return a FieldDescr instance if the attributes is
# not a *real* attribute but the workfow state. # not a *real* attribute but the workfow state.
@ -297,4 +320,20 @@ class ToolMixin(AbstractMixin):
for msg in jsMessages: for msg in jsMessages:
res += 'var %s = "%s";\n' % (msg, self.translate(msg)) res += 'var %s = "%s";\n' % (msg, self.translate(msg))
return res return res
def getSearches(self, contentType):
'''Returns the searches that are defined for p_contentType.'''
appyClass = self.getAppyClass(contentType)
return [s.__dict__ for s in \
ArchetypesClassDescriptor.getSearches(appyClass)]
def getQueryUrl(self, contentType, flavourNumber, searchName):
'''This method creates the URL that allows to perform an ajax GET
request for getting queried objects from a search named p_searchName
on p_contentType from flavour numbered p_flavourNumber.'''
return self.getAppFolder().absolute_url() + '/skyn/ajax?objectUid=%s' \
'&page=macros&macro=queryResult&contentType=%s&flavourNumber=%s' \
'&searchName=%s&startNumber=' % (self.UID(), contentType,
flavourNumber, searchName)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -10,7 +10,7 @@ import os, os.path, sys, types, mimetypes
import appy.gen import appy.gen
from appy.gen import String from appy.gen import String
from appy.gen.utils import FieldDescr, GroupDescr, PhaseDescr, StateDescr, \ from appy.gen.utils import FieldDescr, GroupDescr, PhaseDescr, StateDescr, \
ValidationErrors, sequenceTypes, RefObjects ValidationErrors, sequenceTypes, SomeObjects
from appy.gen.plone25.descriptors import ArchetypesClassDescriptor from appy.gen.plone25.descriptors import ArchetypesClassDescriptor
from appy.gen.plone25.utils import updateRolesForPermission, getAppyRequest from appy.gen.plone25.utils import updateRolesForPermission, getAppyRequest
@ -210,7 +210,7 @@ class AbstractMixin:
for uid in toDelete: for uid in toDelete:
sortedUids.remove(uid) sortedUids.remove(uid)
# Prepare the result # Prepare the result
res = RefObjects() res = SomeObjects()
res.totalNumber = res.batchSize = len(sortedUids) res.totalNumber = res.batchSize = len(sortedUids)
if batchNeeded: if batchNeeded:
res.batchSize = appyType['maxPerPage'] res.batchSize = appyType['maxPerPage']
@ -218,7 +218,7 @@ class AbstractMixin:
res.startNumber = startNumber res.startNumber = startNumber
# Get the needed referred objects # Get the needed referred objects
i = res.startNumber i = res.startNumber
# Is is possible and more efficient to perform a single query in # Is it possible and more efficient to perform a single query in
# uid_catalog and get the result in the order of specified uids? # uid_catalog and get the result in the order of specified uids?
while i < (res.startNumber + res.batchSize): while i < (res.startNumber + res.batchSize):
if i >= res.totalNumber: break if i >= res.totalNumber: break
@ -245,7 +245,7 @@ class AbstractMixin:
startNumber=startNumber).__dict__ startNumber=startNumber).__dict__
else: else:
# Note Pagination is not yet implemented for backward ref. # Note Pagination is not yet implemented for backward ref.
return RefObjects(self.getBRefs(fieldName)).__dict__ return SomeObjects(self.getBRefs(fieldName)).__dict__
def getAppyRefIndex(self, fieldName, obj): def getAppyRefIndex(self, fieldName, obj):
'''Gets the position of p_obj within Ref field named p_fieldName.''' '''Gets the position of p_obj within Ref field named p_fieldName.'''

View file

@ -541,131 +541,28 @@
</script> </script>
</div> </div>
<div metal:define-macro="queryResult"> <metal:queryResults define-macro="queryResult"
tal:define="tool python: contextObj;
contentType request/contentType;
flavourNumber python: int(request['flavourNumber']);
startNumber python:test(request['startNumber']=='', '0', request['startNumber']);
startNumber python: int(startNumber);
searchName request/searchName;
severalTypes python: contentType and (contentType.find(',') != -1);
queryResult python: tool.executeQuery(contentType, flavourNumber, searchName, startNumber);
objs queryResult/objects;
totalNumber queryResult/totalNumber;
batchSize queryResult/batchSize;
ajaxHookId python:'queryResult';
baseUrl python: tool.getQueryUrl(contentType, flavourNumber, searchName)">
<script language="javascript"> <tal:result condition="objs">
<!--
function getSortValue(row, fieldName) {
// Find, from p_fieldName, the cell that is used for sorting.
var cellId = "field_" + fieldName;
var cells = row.cells;
for (var i=0; i < cells.length; i++) {
if (cells[i].id == cellId) {
// Ok we have the cell on which we must sort.
// Now get the cell content.
// If the cell contains links, content is the 1st link content
var innerLinks = cells[i].getElementsByTagName("a");
if (innerLinks.length > 0) {
return innerLinks[0].innerHTML;
} else {
return cells[i].innerHTML;
}
}
}
}
function sortRows(fieldName, ascending) { <tal:comment replace="nothing">Appy (top) navigation</tal:comment>
var queryRows = cssQuery('#query_row'); <metal:nav use-macro="here/skyn/macros/macros/appyNavigate"/>
// Create a wrapper for sorting
var RowWrapper = function(row, fieldName) {
this.value = getSortValue(row, fieldName);
this.cloned_node = row.cloneNode(true);
this.toString = function() {
if (this.value.toString) {
return this.value.toString();
} else {
return this.value;
}
}
}
// Wrap nodes
var items = new Array();
for (var i=0; i<queryRows.length; i++) {
items.push(new RowWrapper(queryRows[i], fieldName));
}
// Sort nodes
items.sort();
if (!ascending) {
items.reverse();
}
// Reorder nodes
for (var i=0; i<items.length; i++) {
var dest = queryRows[i];
dest.parentNode.replaceChild(items[i].cloned_node, dest);
}
};
function onSort(fieldName){
// First, switch the sort arrow (up->down or down->up)
var arrow = document.getElementById("arrow_" + fieldName);
var sortAscending = (arrow.src.indexOf('arrowDown.gif') != -1);
if (sortAscending){
// Display "up" image
arrow.src = arrow.src.replace('arrowDown.gif', 'arrowUp.gif')
}
else { // Display "down" image
arrow.src = arrow.src.replace('arrowUp.gif', 'arrowDown.gif')
}
// Then, sort the rows on column "fieldName".
sortRows(fieldName, sortAscending);
}
function cellMatches(cell, searchValue) {
// This function returns true if the HTML p_cell contains p_searchValue
var innerLinks = cell.getElementsByTagName("a");
// If the cell contains links, we search within the link contents
for (var i=0; i < innerLinks.length; i++){
var linkContent = innerLinks[i].innerHTML.toLowerCase();
if (linkContent.indexOf(searchValue) != -1) {
return true;
}
}
// If we are here, we still have no match. Let's search directly within
// the cell.
var cellContent = cell.innerHTML.toLowerCase();
if (cellContent.indexOf(searchValue) != -1) {
return true;
}
return false;
}
function onTextEntered(fieldName) {
// Is called whenever text is entered into field named p_fieldName.
var cellId = "field_" + fieldName
var field = document.getElementById("filter_" + fieldName);
var fieldValue = field.value.toLowerCase();
if (fieldValue.length >= 3) {
// Browse all rows and check if it should be visible or not.
var queryRows = cssQuery('#query_row');
for (var i=0; i < queryRows.length; i++) {
// Find the value of the cell.
var queryCells = queryRows[i].cells;
for (var j=0; j < queryCells.length; j++) {
if (queryCells[j].id == cellId) {
if (cellMatches(queryCells[j], fieldValue)) {
queryRows[i].style.display = "";
}
else {
queryRows[i].style.display = "none";
}
}
}
}
}
else {
// Show all rows
var queryRows = cssQuery('#query_row');
for (var i=0; i < queryRows.length; i++) {
queryRows[i].style.display = "";
}
}
}
-->
</script>
<table class="vertical listing" width="100%"
tal:define="fieldDescrs python: tool.getResultColumns(queryResult[0].getObject(), queryName);">
<table tal:define="fieldDescrs python: tool.getResultColumns(objs[0], contentType)"
class="vertical listing" width="100%" cellpadding="0" cellspacing="0">
<tal:comment replace="nothing">Every item in fieldDescr is a FieldDescr instance, <tal:comment replace="nothing">Every item in fieldDescr is a FieldDescr instance,
excepted for workflow state (which is not a field): in this case it is simply the excepted for workflow state (which is not a field): in this case it is simply the
string "workflowState".</tal:comment> string "workflowState".</tal:comment>
@ -677,7 +574,7 @@
onClick python:'javascript:onSort(\'title\')';" onClick python:'javascript:onSort(\'title\')';"
id="arrow_title" style="cursor:pointer"/> id="arrow_title" style="cursor:pointer"/>
<span tal:content="python: tool.translate('ref_name')"/> <span tal:content="python: tool.translate('ref_name')"/>
<input id="filter_title" type="text" size="10" onkeyup="javascript:onTextEntered('title')"/> <input id="filter_title" type="text" size="5" onkeyup="javascript:onTextEntered('title')"/>
</th> </th>
<tal:comment replace="nothing">Columns corresponding to other fields</tal:comment> <tal:comment replace="nothing">Columns corresponding to other fields</tal:comment>
@ -695,19 +592,19 @@
<tal:workflowState condition="python: fieldName == 'workflow_state'"> <tal:workflowState condition="python: fieldName == 'workflow_state'">
<span tal:replace="python: tool.translate('workflow_state')"/> <span tal:replace="python: tool.translate('workflow_state')"/>
</tal:workflowState> </tal:workflowState>
<input type="text" size="10" <input type="text" size="5"
tal:attributes="id python: 'filter_%s' % fieldName; tal:attributes="id python: 'filter_%s' % fieldName;
onkeyup python:'javascript:onTextEntered(\'%s\')' % fieldName"/> onkeyup python:'javascript:onTextEntered(\'%s\')' % fieldName"/>
</th> </th>
</tal:columnHeader> </tal:columnHeader>
<tal:comment replace="nothing">Column "Object type", shown if we are on tab "consult all"</tal:comment> <tal:comment replace="nothing">Column "Object type", shown if instances of several types are shown</tal:comment>
<th tal:condition="mainTabSelected"><img <th tal:condition="severalTypes"><img
tal:attributes= "src string: $portal_url/arrowDown.gif; tal:attributes= "src string: $portal_url/arrowDown.gif;
onClick python:'javascript:onSort(\'root_type\')';" onClick python:'javascript:onSort(\'root_type\')';"
id = "arrow_root_type" style="cursor:pointer"/> id = "arrow_root_type" style="cursor:pointer"/>
<span tal:replace="python: tool.translate('root_type')"/> <span tal:replace="python: tool.translate('root_type')"/>
<input type="text" size="10" id="filter_root_type" <input type="text" size="5" id="filter_root_type"
tal:attributes="onkeyup python:'javascript:onTextEntered(\'root_type\')'"/> tal:attributes="onkeyup python:'javascript:onTextEntered(\'root_type\')'"/>
</th> </th>
@ -716,11 +613,10 @@
</tr> </tr>
<tal:comment replace="nothing">Results</tal:comment> <tal:comment replace="nothing">Results</tal:comment>
<tr tal:repeat="brain queryResult" id="query_row"> <tr tal:repeat="obj objs" id="query_row">
<tal:row define="obj brain/getObject">
<tal:comment replace="nothing">Mandatory column "Title"/"Name"</tal:comment> <tal:comment replace="nothing">Mandatory column "Title"/"Name"</tal:comment>
<td id="field_title"><a tal:content="brain/Title" tal:attributes="href obj/getUrl"></a></td> <td id="field_title"><a tal:content="obj/Title" tal:attributes="href obj/getUrl"></a></td>
<tal:comment replace="nothing">Columns corresponding to other fields</tal:comment> <tal:comment replace="nothing">Columns corresponding to other fields</tal:comment>
<tal:otherFields repeat="fieldDescr fieldDescrs"> <tal:otherFields repeat="fieldDescr fieldDescrs">
@ -744,8 +640,8 @@
</tal:workflowState> </tal:workflowState>
</tal:otherFields> </tal:otherFields>
<tal:comment replace="nothing">Column "Object type", shown if we are on tab "consult all"</tal:comment> <tal:comment replace="nothing">Column "Object type", shown if instances of several types are shown</tal:comment>
<td tal:condition="mainTabSelected" id="field_root_type" <td tal:condition="severalTypes" id="field_root_type"
tal:content="python: tool.translate(obj.portal_type)"></td> tal:content="python: tool.translate(obj.portal_type)"></td>
<tal:comment replace="nothing">Column "Actions"</tal:comment> <tal:comment replace="nothing">Column "Actions"</tal:comment>
@ -766,12 +662,18 @@
</tr> </tr>
</table> </table>
</td> </td>
</tal:row>
</tr> </tr>
</table> </table>
<div metal:use-macro="context/batch_macros/macros/navigation" /> <tal:comment replace="nothing">Appy (bottom) navigation</tal:comment>
</div> <metal:nav use-macro="here/skyn/macros/macros/appyNavigate"/>
</tal:result>
<span tal:condition="not: objs"
tal:content="python: tool.translate('query_no_result')">No result.
</span>
</metal:queryResults>
<metal:phases define-macro="phases"> <metal:phases define-macro="phases">
<tal:comment replace="nothing">This macro displays phases defined for a given content type, <tal:comment replace="nothing">This macro displays phases defined for a given content type,
@ -840,10 +742,19 @@
</form> </form>
</metal:transitions> </metal:transitions>
<metal:portletContent define-macro="portletContent"> <metal:portletContent define-macro="portletContent"
tal:define="queryUrl python: '%s/skyn/query' % appFolder.absolute_url();
currentSearch request/search|nothing;
currentType request/type_name|nothing;">
<tal:comment replace="nothing">Portlet title, with link to tool.</tal:comment> <tal:comment replace="nothing">Portlet title, with link to tool.</tal:comment>
<dt class="portletHeader"> <dt class="portletHeader">
<span tal:replace="python: tool.translate(appName)"/>&nbsp; <tal:comment replace="nothing">If there is only one flavour, clicking on the portlet
title allows to see all root objects in the database.</tal:comment>
<a tal:condition="python: len(flavours)==1"
tal:attributes="href python:'%s?type_name=%s&flavourNumber=1' % (queryUrl, ','.join(rootClasses))"
tal:content="python: tool.translate(appName)"></a>
<span tal:condition="python: len(flavours)&gt;1"
tal:replace="python: tool.translate(appName)"/>&nbsp;
<img style="cursor:pointer" <img style="cursor:pointer"
tal:condition="python: member.has_role('Manager')" tal:condition="python: member.has_role('Manager')"
tal:attributes="onClick python: 'href: window.location=\'%s/skyn/view\'' % tool.absolute_url(); tal:attributes="onClick python: 'href: window.location=\'%s/skyn/view\'' % tool.absolute_url();
@ -851,15 +762,59 @@
src string:$portal_url/skyn/appyConfig.gif"/> src string:$portal_url/skyn/appyConfig.gif"/>
</dt> </dt>
<tal:comment replace="nothing">Links to flavours</tal:comment> <tal:comment replace="nothing">TODO: implement a widget for selecting the needed flavour.</tal:comment>
<dt class="portletAppyItem" tal:repeat="flavourInfo tool/getFlavoursInfo">
<tal:comment replace="nothing">Create a section for every root class.</tal:comment>
<tal:section repeat="rootClass rootClasses" define="flavourNumber python:1">
<tal:comment replace="nothing">Section title, with action icons</tal:comment>
<dt tal:attributes="class python:test(repeat['rootClass'].number()==1, 'portletAppyItem', 'portletAppyItem portletSep')">
<table width="100%" cellspacing="0" cellpadding="0" class="no-style-table">
<tr>
<td>
<a tal:attributes="href python: '%s?type_name=%s&flavourNumber=%s' % (queryUrl, rootClass, flavourNumber);
class python:test(not currentSearch and (currentType==rootClass), 'portletCurrent', '')"
tal:content="python: tool.translate(rootClass + '_plural')"></a>
</td>
<td align="right"
tal:define="addPermission python: '%s: Add %s' % (appName, rootClass);
userMayAdd python: member.has_permission(addPermission, appFolder);
createMeans python: tool.getCreateMeans(rootClass)">
<tal:comment replace="nothing">Create a new object from a web form</tal:comment>
<img style="cursor:pointer"
tal:condition="python: ('form' in createMeans) and userMayAdd"
tal:attributes="onClick python: 'href: window.location=\'%s/skyn/do?action=Create&type_name=%s\'' % (appFolder.absolute_url(), rootClass);
src string: $portal_url/skyn/plus.png;
title python: tool.translate('query_create')"/>
<tal:comment replace="nothing">Create (a) new object(s) by importing data</tal:comment>
<img style="cursor:pointer"
tal:condition="python: ('import' in createMeans) and userMayAdd"
tal:attributes="onClick python: 'href: window.location=\'%s/skyn/import?type_name=%s\'' % (appFolder.absolute_url(), rootClass);
src string: $portal_url/skyn/import.png;
title python: tool.translate('query_import')"/>
</td>
</tr>
</table>
</dt>
<tal:comment replace="nothing">Searches for this content type.</tal:comment>
<dt class="portletAppyItem portletSearch" tal:repeat="search python: tool.getSearches(rootClass)">
<a tal:define="searchLabel python: '%s_search_%s' % (rootClass, search['name']);
searchDescr python: '%s_descr' % searchLabel"
tal:attributes="href python: '%s?type_name=%s&flavourNumber=%s&search=%s' % (queryUrl, rootClass, flavourNumber, search['name']);
title python: tool.translate(searchDescr);
class python: test(search['name'] == currentSearch, 'portletCurrent', '')"
tal:content="structure python: tool.translate(searchLabel)"></a>
</dt>
</tal:section>
<tal:comment replace="nothing">All objects in flavour</tal:comment>
<!--dt class="portletAppyItem" tal:define="flavourInfo python: flavours[0]">
<a tal:define="flavourNumber flavourInfo/number; <a tal:define="flavourNumber flavourInfo/number;
rootTypes python: test(flavourNumber==1, rootClasses, ['%s_%s' % (rc, flavourNumber) for rc in rootClasses]); rootTypes python: test(flavourNumber==1, rootClasses, ['%s_%s' % (rc, flavourNumber) for rc in rootClasses]);
rootClassesQuery python:','.join(rootTypes)" rootClassesQuery python:','.join(rootTypes)"
tal:content="flavourInfo/title" tal:content="flavourInfo/title"
tal:attributes="title python: tool.translate('query_consult_all'); tal:attributes="title python: tool.translate('query_consult_all');
href python:'%s/skyn/query?query=%s&flavourNumber=%d' % (appFolder.absolute_url(), rootClassesQuery, flavourNumber)"></a> href python:'%s?type_name=%s&flavourNumber=%d' % (queryUrl, rootClassesQuery, flavourNumber)"></a>
</dt> </dt-->
<dt class="portletAppyItem" tal:define="contextObj tool/getPublishedObject" <dt class="portletAppyItem" tal:define="contextObj tool/getPublishedObject"
tal:condition="python: contextObj.meta_type in rootClasses"> tal:condition="python: contextObj.meta_type in rootClasses">
@ -867,12 +822,11 @@
</dt> </dt>
</metal:portletContent> </metal:portletContent>
<tal:comment replace="nothing"> <div metal:define-macro="appyNavigate" tal:condition="python: totalNumber &gt; batchSize" align="right">
Buttons for navigating among a list of elements (next, back, first, last, etc). <tal:comment replace="nothing">
</tal:comment> Buttons for navigating among a list of elements (next, back, first, last, etc).
<metal:appyNavigate define-macro="appyNavigate" tal:condition="python: totalNumber &gt; batchSize"> </tal:comment>
<table cellpadding="0" cellspacing="0" align="right" class="appyNav" <table cellpadding="0" cellspacing="0" class="appyNav">
tal:define="baseUrl python: contextObj.getUrl('showRef', **{'fieldName': fieldName, 'isBack': isBack, 'innerRef': innerRef, 'labelId': labelId, 'descrId': descrId}) + '&%s_startNumber=' % ajaxHookId">
<tr> <tr>
<tal:comment replace="nothing">Go to the first page</tal:comment> <tal:comment replace="nothing">Go to the first page</tal:comment>
<td><img style="cursor:pointer" tal:condition="python: (startNumber != 0) and (startNumber != batchSize)" <td><img style="cursor:pointer" tal:condition="python: (startNumber != 0) and (startNumber != batchSize)"
@ -886,10 +840,11 @@
title python: tool.translate('goto_previous'); title python: tool.translate('goto_previous');
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl+str(sNumber))"/></td> onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl+str(sNumber))"/></td>
<tal:comment replace="nothing">Explain which elements are currently shown</tal:comment> <tal:comment replace="nothing">Explain which elements are currently shown</tal:comment>
<td class="discreet">&nbsp; <td class="discreet" valign="middle">&nbsp;
<span tal:replace="python: startNumber+1"/> <span tal:replace="python: startNumber+1"/>
<img tal:attributes="src string: $portal_url/skyn/to.png"/> <img tal:attributes="src string: $portal_url/skyn/to.png"/>
<span tal:replace="python: startNumber+len(objs)"/>&nbsp;&nbsp; <span tal:replace="python: startNumber+len(objs)"/>&nbsp;<b>//</b>
<span tal:replace="python: totalNumber"/>&nbsp;&nbsp;
</td> </td>
<tal:comment replace="nothing">Go to the next page</tal:comment> <tal:comment replace="nothing">Go to the next page</tal:comment>
@ -909,4 +864,4 @@
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl+str(sNumber))"/></td> onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl+str(sNumber))"/></td>
</tr> </tr>
</table> </table>
</metal:appyNavigate> </div>

View file

@ -15,62 +15,146 @@
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());
queryName python:context.REQUEST.get('query'); contentType python:context.REQUEST.get('type_name');
flavourNumber python:context.REQUEST.get('flavourNumber'); flavourNumber python:int(context.REQUEST.get('flavourNumber'));
rootClasses tool/getRootClasses; searchName python:context.REQUEST.get('search', '');
rootTypes python: test(flavourNumber=='1', rootClasses, ['%s_%s' % (rc, flavourNumber) for rc in rootClasses]); searchLabel python: '%s_search_%s' % (contentType, searchName);
rootClassesQuery python:','.join(rootClasses); searchDescr python: '%s_descr' % searchLabel;
mainTabSelected python: queryName.find(',') != -1"> severalTypes python: contentType and (contentType.find(',') != -1)">
<div metal:use-macro="here/skyn/macros/macros/pagePrologue"/> <div metal:use-macro="here/skyn/macros/macros/pagePrologue"/>
<span tal:condition="python: queryName and (queryName != 'none')"> <script language="javascript">
<span tal:define="queryResult python: tool.executeQuery(queryName, int(flavourNumber)); <!--
batch queryResult"> function getSortValue(row, fieldName) {
// Find, from p_fieldName, the cell that is used for sorting.
var cellId = "field_" + fieldName;
var cells = row.cells;
for (var i=0; i < cells.length; i++) {
if (cells[i].id == cellId) {
// Ok we have the cell on which we must sort.
// Now get the cell content.
// If the cell contains links, content is the 1st link content
var innerLinks = cells[i].getElementsByTagName("a");
if (innerLinks.length > 0) {
return innerLinks[0].innerHTML;
} else {
return cells[i].innerHTML;
}
}
}
}
<tal:comment replace="nothing">Tabs</tal:comment> function sortRows(fieldName, ascending) {
<ul class="contentViews appyTabs"> var queryRows = cssQuery('#query_row');
<tal:comment replace="nothing">Tab "All objects"</tal:comment> // Create a wrapper for sorting
<li tal:define="selected python:mainTabSelected" var RowWrapper = function(row, fieldName) {
tal:attributes="class python:test(selected, 'selected', 'plain')" this.value = getSortValue(row, fieldName);
tal:condition="python: len(rootClasses)>1"> this.cloned_node = row.cloneNode(true);
this.toString = function() {
if (this.value.toString) {
return this.value.toString();
} else {
return this.value;
}
}
}
// Wrap nodes
var items = new Array();
for (var i=0; i<queryRows.length; i++) {
items.push(new RowWrapper(queryRows[i], fieldName));
}
// Sort nodes
items.sort();
if (!ascending) {
items.reverse();
}
// Reorder nodes
for (var i=0; i<items.length; i++) {
var dest = queryRows[i];
dest.parentNode.replaceChild(items[i].cloned_node, dest);
}
};
<a tal:content="python: tool.translate('query_consult_all')" function onSort(fieldName){
tal:attributes="href python: '%s/skyn/query?query=%s&flavourNumber=%s' % (appFolder.absolute_url(), rootClassesQuery, flavourNumber)"></a> // First, switch the sort arrow (up->down or down->up)
</li> var arrow = document.getElementById("arrow_" + fieldName);
<tal:comment replace="nothing">One tab for each root content type</tal:comment> var sortAscending = (arrow.src.indexOf('arrowDown.gif') != -1);
<tal:tab repeat="rootContentType rootTypes"> if (sortAscending){
<li tal:define="selected python:queryName == rootContentType; // Display "up" image
addPermission python: '%s: Add %s' % (appName, rootContentType); arrow.src = arrow.src.replace('arrowDown.gif', 'arrowUp.gif')
userMayAdd python: member.has_permission(addPermission, appFolder); }
createMeans python: tool.getCreateMeans(rootContentType)" else { // Display "down" image
tal:attributes="class python:test(selected, 'selected', 'plain')"> arrow.src = arrow.src.replace('arrowUp.gif', 'arrowDown.gif')
<a tal:content="python: tool.translate(rootContentType)" }
tal:attributes="href python: '%s/skyn/query?query=%s&flavourNumber=%s' % (appFolder.absolute_url(), rootContentType, flavourNumber)"/> // Then, sort the rows on column "fieldName".
<tal:comment replace="nothing">Create a new object from a web form</tal:comment> sortRows(fieldName, sortAscending);
<img style="cursor:pointer" class="appyPlusImg" }
tal:condition="python: ('form' in createMeans) and userMayAdd"
tal:attributes="onClick python: 'href: window.location=\'%s/skyn/do?action=Create&type_name=%s\'' % (appFolder.absolute_url(), rootContentType);
src string: $portal_url/skyn/plus.png;
title python: tool.translate('query_create')"/>
<tal:comment replace="nothing">Create (a) new object(s) by importing data</tal:comment>
<img style="cursor:pointer" class="appyPlusImg"
tal:condition="python: ('import' in createMeans) and userMayAdd"
tal:attributes="onClick python: 'href: window.location=\'%s/skyn/import?type_name=%s\'' % (appFolder.absolute_url(), rootContentType);
src string: $portal_url/skyn/import.png;
title python: tool.translate('query_import')"/>
</li>
</tal:tab>
</ul>
<br/>
<tal:comment replace="nothing">Query result</tal:comment> function cellMatches(cell, searchValue) {
<span tal:condition="queryResult"> // This function returns true if the HTML p_cell contains p_searchValue
<span metal:use-macro="here/skyn/macros/macros/queryResult"></span> var innerLinks = cell.getElementsByTagName("a");
</span> // If the cell contains links, we search within the link contents
<span tal:condition="not: queryResult" for (var i=0; i < innerLinks.length; i++){
tal:content="python: tool.translate('query_no_result')">No result.</span> var linkContent = innerLinks[i].innerHTML.toLowerCase();
</span> if (linkContent.indexOf(searchValue) != -1) {
</span> return true;
}
}
// If we are here, we still have no match. Let's search directly within
// the cell.
var cellContent = cell.innerHTML.toLowerCase();
if (cellContent.indexOf(searchValue) != -1) {
return true;
}
return false;
}
function onTextEntered(fieldName) {
// Is called whenever text is entered into field named p_fieldName.
var cellId = "field_" + fieldName
var field = document.getElementById("filter_" + fieldName);
var fieldValue = field.value.toLowerCase();
if (fieldValue.length >= 3) {
// Browse all rows and check if it should be visible or not.
var queryRows = cssQuery('#query_row');
for (var i=0; i < queryRows.length; i++) {
// Find the value of the cell.
var queryCells = queryRows[i].cells;
for (var j=0; j < queryCells.length; j++) {
if (queryCells[j].id == cellId) {
if (cellMatches(queryCells[j], fieldValue)) {
queryRows[i].style.display = "";
}
else {
queryRows[i].style.display = "none";
}
}
}
}
}
else {
// Show all rows
var queryRows = cssQuery('#query_row');
for (var i=0; i < queryRows.length; i++) {
queryRows[i].style.display = "";
}
}
}
-->
</script>
<tal:comment replace="nothing">Query title and description</tal:comment>
<h1 tal:content="structure python: test(searchName, tool.translate(searchLabel), test(severalTypes, tool.translate(appName), tool.translate('%s_plural' % contentType)))"></h1>
<div class="discreet" tal:condition="searchName"
tal:content="structure python: tool.translate(searchDescr)+'<br/><br/>'"></div>
<tal:comment replace="nothing">Query result</tal:comment>
<div id="queryResult"></div>
<script language="javascript"
tal:define="ajaxUrl python: tool.getQueryUrl(contentType, flavourNumber, searchName)"
tal:content="python: 'askAjaxChunk(\'queryResult\',\'%s\')' % ajaxUrl">
</script>
</div> </div>
</body> </body>
</html> </html>

View file

@ -107,7 +107,8 @@
showPlusIcon python:not isBack and appyType['add'] and not maxReached and member.has_permission(addPermission, folder); showPlusIcon python:not isBack and appyType['add'] and not maxReached and member.has_permission(addPermission, folder);
atMostOneRef python: (multiplicity[1] == 1) and (len(objs)&lt;=1); atMostOneRef python: (multiplicity[1] == 1) and (len(objs)&lt;=1);
label python: tool.translate(labelId); label python: tool.translate(labelId);
description python: tool.translate(descrId)"> description python: tool.translate(descrId);
baseUrl python: contextObj.getUrl('showRef', **{'fieldName': fieldName, 'isBack': isBack, 'innerRef': innerRef, 'labelId': labelId, 'descrId': descrId}) + '&%s_startNumber=' % ajaxHookId">
<tal:comment replace="nothing">This macro displays the Reference widget on a "consult" page. <tal:comment replace="nothing">This macro displays the Reference widget on a "consult" page.
@ -144,12 +145,11 @@
<fieldset tal:attributes="class python:test(innerRef, 'innerAppyFieldset', '')"> <fieldset tal:attributes="class python:test(innerRef, 'innerAppyFieldset', '')">
<legend tal:condition="python: not innerRef or showPlusIcon"> <legend tal:condition="python: not innerRef or showPlusIcon">
<span tal:condition="not: innerRef" tal:content="label"/> <span tal:condition="not: innerRef" tal:content="label"/>
<tal:numberOfRefs>(<span tal:replace="totalNumber"/>)</tal:numberOfRefs>
<metal:plusIcon use-macro="here/skyn/ref/macros/plusIcon"/> <metal:plusIcon use-macro="here/skyn/ref/macros/plusIcon"/>
</legend> </legend>
<tal:comment replace="nothing">Object description</tal:comment> <tal:comment replace="nothing">Object description</tal:comment>
<p tal:condition="python: not innerRef and description" <p tal:condition="python: not innerRef and description.strip()"
tal:content="description" class="discreet" ></p> tal:content="description" class="discreet" ></p>
<tal:comment replace="nothing">Appy (top) navigation</tal:comment> <tal:comment replace="nothing">Appy (top) navigation</tal:comment>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 B

After

Width:  |  Height:  |  Size: 211 B

View file

@ -8,7 +8,8 @@
<metal:block metal:use-macro="here/global_defines/macros/defines" /> <metal:block metal:use-macro="here/global_defines/macros/defines" />
<dl tal:define="rootClasses tool/getRootClasses; <dl tal:define="rootClasses tool/getRootClasses;
appName string:<!applicationName!>; appName string:<!applicationName!>;
appFolder tool/getAppFolder" class="portlet"> appFolder tool/getAppFolder;
flavours tool/getFlavoursInfo" class="portlet">
<metal:content use-macro="here/skyn/macros/macros/portletContent"/> <metal:content use-macro="here/skyn/macros/macros/portletContent"/>
</dl> </dl>
</div> </div>

View file

@ -212,12 +212,27 @@ fieldset {
margin: 0 0.2em 0.2em 0; margin: 0 0.2em 0.2em 0;
} }
/* Portlet elements */ /* Portlet elements */
.portletHeader {
text-transform: none;
padding: 1px 0.5em;
}
.portletAppyItem { .portletAppyItem {
margin: 0; margin: 0;
padding: 1px 0.5em; padding: 1px 0.5em;
border-left: 1px solid #8cacbb; border-left: 1px solid #8cacbb;
border-right: 1px solid #8cacbb; border-right: 1px solid #8cacbb;
font-weight: normal; font-weight: normal;
text-transform: none;
}
.portletSep {
border-top: 1px dashed #8cacbb;
}
.portletSearch {
padding: 0 0 0 0.6em;
font-style: italic;
}
.portletCurrent {
font-weight: bold;
} }
/* Uncomment this if you want to hide breadcrumbs */ /* Uncomment this if you want to hide breadcrumbs */

View file

@ -172,13 +172,22 @@ class AppyRequest:
return res return res
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class RefObjects: class SomeObjects:
'''Represents a bunch of objects retrieved from a reference.''' '''Represents a bunch of objects retrieved from a reference or a query in
def __init__(self, objects=None): portal_catalog.'''
def __init__(self, objects=None, batchSize=None, startNumber=0):
self.objects = objects or [] # The objects self.objects = objects or [] # The objects
self.totalNumber = len(self.objects) # self.objects may only represent a self.totalNumber = len(self.objects) # self.objects may only represent a
# part of all available objects. # part of all available objects.
self.batchSize = self.totalNumber # The max length of self.objects. self.batchSize = batchSize or self.totalNumber # The max length of
self.startNumber = 0 # The index of first object in self.objects in # self.objects.
# the whole list. self.startNumber = startNumber # The index of first object in
# self.objects in the whole list.
def brainsToObjects(self):
'''self.objects has been populated from brains from the portal_catalog,
not from True objects. This method turns them (or some of them
depending on batchSize and startNumber) into real objects.'''
start = self.startNumber
brains = self.objects[start:start + self.batchSize]
self.objects = [b.getObject() for b in brains]
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------