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:
parent
2b907fee32
commit
1c0744da85
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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¯o=queryResult&contentType=%s&flavourNumber=%s' \
|
||||
'&searchName=%s&startNumber=' % (self.UID(), contentType,
|
||||
flavourNumber, searchName)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
@ -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.'''
|
||||
|
|
|
@ -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)"/>
|
||||
<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)>1"
|
||||
tal:replace="python: tool.translate(appName)"/>
|
||||
<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>
|
||||
|
||||
<tal:comment replace="nothing">
|
||||
<div metal:define-macro="appyNavigate" tal:condition="python: totalNumber > 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 > 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">
|
||||
</tal:comment>
|
||||
<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">
|
||||
<td class="discreet" valign="middle">
|
||||
<span tal:replace="python: startNumber+1"/>
|
||||
<img tal:attributes="src string: $portal_url/skyn/to.png"/>
|
||||
<span tal:replace="python: startNumber+len(objs)"/>
|
||||
<span tal:replace="python: startNumber+len(objs)"/> <b>//</b>
|
||||
<span tal:replace="python: totalNumber"/>
|
||||
</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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)<=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 |
|
@ -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>
|
||||
|
|
|
@ -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 */
|
||||
|
|
21
gen/utils.py
21
gen/utils.py
|
@ -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]
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
Loading…
Reference in a new issue