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.limit = limit
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:
@ -162,7 +162,8 @@ class String(Type):
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;
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
# non-mandatory fields.
# First, remove any non-digit char

View file

@ -116,9 +116,6 @@ class ArchetypeFieldDescriptor:
self.widgetParams['size'] = 50
if 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:
self.fieldType = 'TextField'
self.widgetType = 'TextAreaWidget'
@ -132,9 +129,6 @@ class ArchetypeFieldDescriptor:
else:
self.fieldType = 'StringField'
self.widgetType = 'StringWidget'
# Manage searchability
if self.appyType.searchable:
self.fieldParams['searchable'] = True
def walkComputed(self):
'''How to generate a computed field? We generate an Archetypes String
@ -206,11 +200,9 @@ class ArchetypeFieldDescriptor:
self.fieldParams['default'] = self.appyType.default
# - required?
if self.appyType.multiplicity[0] >= 1:
if self.appyType.type != 'Ref':
# Indeed, if it is a ref appy will manage itself field updates
# in at_post_create_script, so Archetypes must not enforce
# required=True
self.fieldParams['required'] = True
if self.appyType.type != 'Ref': self.fieldParams['required'] = True
# Indeed, if it is a ref appy will manage itself field updates in
# onEdit, so Archetypes must not enforce required=True
# - optional ?
if self.appyType.optional:
Flavour._appy_addOptionalField(self)
@ -222,14 +214,20 @@ class ArchetypeFieldDescriptor:
methodName = 'getDefaultValueFor%s' % self.fieldName
self.fieldParams['default_method'] = methodName
self.classDescr.addDefaultMethod(methodName, self)
# - searchable ?
if self.appyType.searchable and (self.appyType.type != 'String'):
# - 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 ?
if self.appyType.searchable: self.fieldParams['searchable'] = True
# - slaves ?
if self.appyType.slaves:
self.widgetParams['visible'] = False # Archetypes will believe the
# field is invisible; we will display it ourselves (like for Ref
# fields)
if self.appyType.slaves: self.widgetParams['visible'] = False
# Archetypes will believe the field is invisible; we will display it
# ourselves (like for Ref fields)
# - need to generate a field validator?
# In all cases, add an i18n message for the validation error for this
# field.
@ -490,6 +488,14 @@ class ArchetypesClassDescriptor(ClassDescriptor):
else: res.append(search)
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):
m = self.methods
spaces = TABS

View file

@ -660,13 +660,17 @@ class Generator(AbstractGenerator):
'implements': implements, 'baseSchema': baseSchema,
'register': register, 'toolInstanceName': self.toolInstanceName})
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.produceNiceDefault()
self.labels.append(poMsg)
poMsgDescr = PoMessage('%s_edit_descr' % classDescr.name, '', ' ')
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):
poMsg = PoMessage('%s_%d' % (classDescr.name, i), '',
classDescr.klass.__name__)
@ -675,7 +679,11 @@ class Generator(AbstractGenerator):
poMsgDescr = PoMessage('%s_%d_edit_descr' % (classDescr.name, i),
'', ' ')
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):
searchLabelId = '%s_search_%s' % (classDescr.name, search.name)
searchDescrId = '%s_descr' % searchLabelId

View file

@ -1,9 +1,10 @@
# ------------------------------------------------------------------------------
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.FlavourMixin import FlavourMixin
from appy.gen.plone25.wrappers import AbstractWrapper
from appy.gen.plone25.descriptors import ArchetypesClassDescriptor
_PY = 'Please specify a file corresponding to a Python interpreter ' \
'(ie "/usr/bin/python").'
@ -97,24 +98,46 @@ class ToolMixin(AbstractMixin):
def showPortlet(self):
return not self.portal_membership.isAnonymousUser()
def executeQuery(self, queryName, flavourNumber):
if queryName.find(',') != -1:
def executeQuery(self, contentType, flavourNumber=1, searchName=None,
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
portalTypes = queryName.split(',')
portalTypes = contentType.split(',')
if flavourNumber != 1:
portalTypes = ['%s_%d' % (pt, flavourNumber) \
for pt in portalTypes]
else:
portalTypes = queryName
portalTypes = contentType
params = {'portal_type': portalTypes, 'batch': True}
res = self.portal_catalog.searchResults(**params)
batchStart = self.REQUEST.get('b_start', 0)
res = self.getProductConfig().Batch(res,
self.getNumberOfResultsPerPage(), int(batchStart), orphan=0)
return res
# Manage additional criteria from a search when relevant
if searchName:
# In this case, contentType must contain a single content type.
appyClass = self.getAppyClass(contentType)
# 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):
contentTypes = queryName.strip(',').split(',')
def getResultColumnsNames(self, contentType):
contentTypes = contentType.strip(',').split(',')
resSet = None # Temporary set for computing intersections.
res = [] # Final, sorted result.
flavour = None
@ -137,12 +160,12 @@ class ToolMixin(AbstractMixin):
res.append(fieldName)
return res
def getResultColumns(self, anObject, queryName):
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 a FieldDescr instance.'''
res = []
for fieldName in self.getResultColumnsNames(queryName):
for fieldName in self.getResultColumnsNames(contentType):
if fieldName == 'workflowState':
# We do not return a FieldDescr instance if the attributes is
# not a *real* attribute but the workfow state.
@ -297,4 +320,20 @@ class ToolMixin(AbstractMixin):
for msg in jsMessages:
res += 'var %s = "%s";\n' % (msg, self.translate(msg))
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
from appy.gen import String
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.utils import updateRolesForPermission, getAppyRequest
@ -210,7 +210,7 @@ class AbstractMixin:
for uid in toDelete:
sortedUids.remove(uid)
# Prepare the result
res = RefObjects()
res = SomeObjects()
res.totalNumber = res.batchSize = len(sortedUids)
if batchNeeded:
res.batchSize = appyType['maxPerPage']
@ -218,7 +218,7 @@ class AbstractMixin:
res.startNumber = startNumber
# Get the needed referred objects
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?
while i < (res.startNumber + res.batchSize):
if i >= res.totalNumber: break
@ -245,7 +245,7 @@ class AbstractMixin:
startNumber=startNumber).__dict__
else:
# 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):
'''Gets the position of p_obj within Ref field named p_fieldName.'''

View file

@ -541,131 +541,28 @@
</script>
</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">
<!--
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:result condition="objs">
function sortRows(fieldName, ascending) {
var queryRows = cssQuery('#query_row');
// 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);">
<tal:comment replace="nothing">Appy (top) navigation</tal:comment>
<metal:nav use-macro="here/skyn/macros/macros/appyNavigate"/>
<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,
excepted for workflow state (which is not a field): in this case it is simply the
string "workflowState".</tal:comment>
@ -677,7 +574,7 @@
onClick python:'javascript:onSort(\'title\')';"
id="arrow_title" style="cursor:pointer"/>
<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>
<tal:comment replace="nothing">Columns corresponding to other fields</tal:comment>
@ -695,19 +592,19 @@
<tal:workflowState condition="python: fieldName == 'workflow_state'">
<span tal:replace="python: tool.translate('workflow_state')"/>
</tal:workflowState>
<input type="text" size="10"
<input type="text" size="5"
tal:attributes="id python: 'filter_%s' % fieldName;
onkeyup python:'javascript:onTextEntered(\'%s\')' % fieldName"/>
</th>
</tal:columnHeader>
<tal:comment replace="nothing">Column "Object type", shown if we are on tab "consult all"</tal:comment>
<th tal:condition="mainTabSelected"><img
<tal:comment replace="nothing">Column "Object type", shown if instances of several types are shown</tal:comment>
<th tal:condition="severalTypes"><img
tal:attributes= "src string: $portal_url/arrowDown.gif;
onClick python:'javascript:onSort(\'root_type\')';"
id = "arrow_root_type" style="cursor:pointer"/>
<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\')'"/>
</th>
@ -716,11 +613,10 @@
</tr>
<tal:comment replace="nothing">Results</tal:comment>
<tr tal:repeat="brain queryResult" id="query_row">
<tal:row define="obj brain/getObject">
<tr tal:repeat="obj objs" id="query_row">
<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:otherFields repeat="fieldDescr fieldDescrs">
@ -744,8 +640,8 @@
</tal:workflowState>
</tal:otherFields>
<tal:comment replace="nothing">Column "Object type", shown if we are on tab "consult all"</tal:comment>
<td tal:condition="mainTabSelected" id="field_root_type"
<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"
tal:content="python: tool.translate(obj.portal_type)"></td>
<tal:comment replace="nothing">Column "Actions"</tal:comment>
@ -766,12 +662,18 @@
</tr>
</table>
</td>
</tal:row>
</tr>
</table>
<div metal:use-macro="context/batch_macros/macros/navigation" />
</div>
<tal:comment replace="nothing">Appy (bottom) navigation</tal:comment>
<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">
<tal:comment replace="nothing">This macro displays phases defined for a given content type,
@ -840,10 +742,19 @@
</form>
</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>
<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"
tal:condition="python: member.has_role('Manager')"
tal:attributes="onClick python: 'href: window.location=\'%s/skyn/view\'' % tool.absolute_url();
@ -851,15 +762,59 @@
src string:$portal_url/skyn/appyConfig.gif"/>
</dt>
<tal:comment replace="nothing">Links to flavours</tal:comment>
<dt class="portletAppyItem" tal:repeat="flavourInfo tool/getFlavoursInfo">
<tal:comment replace="nothing">TODO: implement a widget for selecting the needed flavour.</tal:comment>
<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;
rootTypes python: test(flavourNumber==1, rootClasses, ['%s_%s' % (rc, flavourNumber) for rc in rootClasses]);
rootClassesQuery python:','.join(rootTypes)"
tal:content="flavourInfo/title"
tal:attributes="title python: tool.translate('query_consult_all');
href python:'%s/skyn/query?query=%s&flavourNumber=%d' % (appFolder.absolute_url(), rootClassesQuery, flavourNumber)"></a>
</dt>
href python:'%s?type_name=%s&flavourNumber=%d' % (queryUrl, rootClassesQuery, flavourNumber)"></a>
</dt-->
<dt class="portletAppyItem" tal:define="contextObj tool/getPublishedObject"
tal:condition="python: contextObj.meta_type in rootClasses">
@ -867,12 +822,11 @@
</dt>
</metal:portletContent>
<div metal:define-macro="appyNavigate" tal:condition="python: totalNumber &gt; batchSize" align="right">
<tal:comment replace="nothing">
Buttons for navigating among a list of elements (next, back, first, last, etc).
</tal:comment>
<metal:appyNavigate define-macro="appyNavigate" tal:condition="python: totalNumber &gt; batchSize">
<table cellpadding="0" cellspacing="0" align="right" class="appyNav"
tal:define="baseUrl python: contextObj.getUrl('showRef', **{'fieldName': fieldName, 'isBack': isBack, 'innerRef': innerRef, 'labelId': labelId, 'descrId': descrId}) + '&%s_startNumber=' % ajaxHookId">
<table cellpadding="0" cellspacing="0" class="appyNav">
<tr>
<tal:comment replace="nothing">Go to the first page</tal:comment>
<td><img style="cursor:pointer" tal:condition="python: (startNumber != 0) and (startNumber != batchSize)"
@ -886,10 +840,11 @@
title python: tool.translate('goto_previous');
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl+str(sNumber))"/></td>
<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"/>
<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>
<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>
</tr>
</table>
</metal:appyNavigate>
</div>

View file

@ -15,62 +15,146 @@
tal:define="appFolder context/getParentNode;
appName appFolder/id;
tool python: portal.get('portal_%s' % appName.lower());
queryName python:context.REQUEST.get('query');
flavourNumber python:context.REQUEST.get('flavourNumber');
rootClasses tool/getRootClasses;
rootTypes python: test(flavourNumber=='1', rootClasses, ['%s_%s' % (rc, flavourNumber) for rc in rootClasses]);
rootClassesQuery python:','.join(rootClasses);
mainTabSelected python: queryName.find(',') != -1">
contentType python:context.REQUEST.get('type_name');
flavourNumber python:int(context.REQUEST.get('flavourNumber'));
searchName python:context.REQUEST.get('search', '');
searchLabel python: '%s_search_%s' % (contentType, searchName);
searchDescr python: '%s_descr' % searchLabel;
severalTypes python: contentType and (contentType.find(',') != -1)">
<div metal:use-macro="here/skyn/macros/macros/pagePrologue"/>
<span tal:condition="python: queryName and (queryName != 'none')">
<span tal:define="queryResult python: tool.executeQuery(queryName, int(flavourNumber));
batch queryResult">
<script language="javascript">
<!--
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>
<ul class="contentViews appyTabs">
<tal:comment replace="nothing">Tab "All objects"</tal:comment>
<li tal:define="selected python:mainTabSelected"
tal:attributes="class python:test(selected, 'selected', 'plain')"
tal:condition="python: len(rootClasses)>1">
function sortRows(fieldName, ascending) {
var queryRows = cssQuery('#query_row');
// 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);
}
};
<a tal:content="python: tool.translate('query_consult_all')"
tal:attributes="href python: '%s/skyn/query?query=%s&flavourNumber=%s' % (appFolder.absolute_url(), rootClassesQuery, flavourNumber)"></a>
</li>
<tal:comment replace="nothing">One tab for each root content type</tal:comment>
<tal:tab repeat="rootContentType rootTypes">
<li tal:define="selected python:queryName == rootContentType;
addPermission python: '%s: Add %s' % (appName, rootContentType);
userMayAdd python: member.has_permission(addPermission, appFolder);
createMeans python: tool.getCreateMeans(rootContentType)"
tal:attributes="class python:test(selected, 'selected', 'plain')">
<a tal:content="python: tool.translate(rootContentType)"
tal:attributes="href python: '%s/skyn/query?query=%s&flavourNumber=%s' % (appFolder.absolute_url(), rootContentType, flavourNumber)"/>
<tal:comment replace="nothing">Create a new object from a web form</tal:comment>
<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/>
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>
<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>
<span tal:condition="queryResult">
<span metal:use-macro="here/skyn/macros/macros/queryResult"></span>
</span>
<span tal:condition="not: queryResult"
tal:content="python: tool.translate('query_no_result')">No result.</span>
</span>
</span>
<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>
</body>
</html>

View file

@ -107,7 +107,8 @@
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);
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.
@ -144,12 +145,11 @@
<fieldset tal:attributes="class python:test(innerRef, 'innerAppyFieldset', '')">
<legend tal:condition="python: not innerRef or showPlusIcon">
<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"/>
</legend>
<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: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" />
<dl tal:define="rootClasses tool/getRootClasses;
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"/>
</dl>
</div>

View file

@ -212,12 +212,27 @@ fieldset {
margin: 0 0.2em 0.2em 0;
}
/* Portlet elements */
.portletHeader {
text-transform: none;
padding: 1px 0.5em;
}
.portletAppyItem {
margin: 0;
padding: 1px 0.5em;
border-left: 1px solid #8cacbb;
border-right: 1px solid #8cacbb;
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 */

View file

@ -172,13 +172,22 @@ class AppyRequest:
return res
# ------------------------------------------------------------------------------
class RefObjects:
'''Represents a bunch of objects retrieved from a reference.'''
def __init__(self, objects=None):
class SomeObjects:
'''Represents a bunch of objects retrieved from a reference or a query in
portal_catalog.'''
def __init__(self, objects=None, batchSize=None, startNumber=0):
self.objects = objects or [] # The objects
self.totalNumber = len(self.objects) # self.objects may only represent a
# part of all available objects.
self.batchSize = self.totalNumber # The max length of self.objects.
self.startNumber = 0 # The index of first object in self.objects in
# the whole list.
self.batchSize = batchSize or self.totalNumber # The max length of
# self.objects.
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]
# ------------------------------------------------------------------------------