Inter-object navigation.

This commit is contained in:
Gaetan Delannay 2009-11-17 10:05:19 +01:00
parent 7cdc3c1ed6
commit d9484b104e
10 changed files with 247 additions and 48 deletions

View file

@ -124,6 +124,7 @@ class Generator(AbstractGenerator):
msg('goto_previous', '', msg.GOTO_PREVIOUS),
msg('goto_next', '', msg.GOTO_NEXT),
msg('goto_last', '', msg.GOTO_LAST),
msg('goto_source', '', msg.GOTO_SOURCE),
]
# Create basic files (config.py, Install.py, etc)
self.generateTool()

View file

@ -98,9 +98,15 @@ class ToolMixin(AbstractMixin):
def showPortlet(self):
return not self.portal_membership.isAnonymousUser()
def getObject(self, uid, appy=False):
'''Allows to retrieve an object from its p_uid.'''
res = self.uid_catalog(UID=uid)
if res: return res[0].getObject()
return None
_sortFields = {'title': 'sortable_title'}
def executeQuery(self, contentType, flavourNumber=1, searchName=None,
startNumber=0, search=None):
startNumber=0, search=None, remember=False):
'''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
@ -147,6 +153,21 @@ class ToolMixin(AbstractMixin):
brains = self.portal_catalog.searchResults(**params)
res = SomeObjects(brains, self.getNumberOfResultsPerPage(), startNumber)
res.brainsToObjects()
# In some cases (p_remember=True), we need to keep some information
# about the query results in the current user's session, allowing him
# to navigate within elements without re-triggering the query every
# time a page for an element is consulted.
if remember:
if not searchName:
# It is the global search for all objects pf p_contentType
searchName = contentType
s = self.REQUEST.SESSION
uids = {}
i = -1
for obj in res.objects:
i += 1
uids[startNumber+i] = obj.UID()
s['search_%s_%s' % (flavourNumber, searchName)] = uids
return res.__dict__
def getResultColumnsNames(self, contentType):
@ -373,12 +394,122 @@ class ToolMixin(AbstractMixin):
if cookieValue: return cookieValue.value
return default
def getQueryUrl(self, contentType, flavourNumber, searchName):
def getQueryUrl(self, contentType, flavourNumber, searchName, ajax=True,
startNumber=None):
'''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)
on p_contentType from flavour numbered p_flavourNumber. If p_ajax
is False, it returns the non-ajax URL.'''
baseUrl = self.getAppFolder().absolute_url() + '/skyn'
baseParams = 'type_name=%s&flavourNumber=%s'%(contentType,flavourNumber)
# Manage start number
rq = self.REQUEST
if startNumber != None:
baseParams += '&startNumber=%s' % startNumber
elif rq.has_key('startNumber'):
baseParams += '&startNumber=%s' % rq['startNumber']
# Manage search name
if searchName or ajax: baseParams += '&search=%s' % searchName
if ajax:
return '%s/ajax?objectUid=%s&page=macros&macro=queryResult&%s' % \
(baseUrl, self.UID(), baseParams)
else:
return '%s/query?%s' % (baseUrl, baseParams)
def computeStartNumberFrom(self, currentNumber, totalNumber, batchSize):
'''Returns the number (start at 0) of the first element in a list
containing p_currentNumber (starts at 0) whose total number is
p_totalNumber and whose batch size is p_batchSize.'''
startNumber = 0
res = startNumber
while (startNumber < totalNumber):
if (currentNumber < startNumber + batchSize):
return startNumber
else:
startNumber += batchSize
return startNumber
def getNavigationInfo(self):
'''Extracts navigation information from request/nav and returns a dict
with the info that a page can use for displaying object
navigation.'''
res = {}
t,d1,d2,currentNumber,totalNumber = self.REQUEST.get('nav').split('.')
res['currentNumber'] = int(currentNumber)
res['totalNumber'] = int(totalNumber)
newNav = '%s.%s.%s.%%d.%s' % (t, d1, d2, totalNumber)
# Among, first, previous, next and last, which one do I need?
previousNeeded = False # Previous ?
previousIndex = res['currentNumber'] - 2
if (previousIndex > -1) and (res['totalNumber'] > previousIndex):
previousNeeded = True
nextNeeded = False # Next ?
nextIndex = res['currentNumber']
if nextIndex < res['totalNumber']: nextNeeded = True
firstNeeded = False # First ?
firstIndex = 0
if previousIndex > 0: firstNeeded = True
lastNeeded = False # Last ?
lastIndex = res['totalNumber'] - 1
if (nextIndex < lastIndex): lastNeeded = True
# Get the list of available UIDs surrounding the current object
if t == 'ref': # Manage navigation from a reference
fieldName = d2
masterObj = self.getObject(d1)
batchSize = masterObj.getAppyType(fieldName)['maxPerPage']
uids = getattr(masterObj, '_appy_%s' % fieldName)
# In the case of a reference, we retrieve ALL surrounding objects.
# Display the reference widget at the page where the current object
# lies.
startNumberKey = '%s%s_startNumber' % (masterObj.UID(), fieldName)
startNumber = self.computeStartNumberFrom(res['currentNumber']-1,
res['totalNumber'], batchSize)
res['sourceUrl'] = '%s?%s=%s' % (masterObj.getUrl(),
startNumberKey, startNumber)
else: # Manage navigation from a search
contentType, flavourNumber = d1.split(':')
flavourNumber = int(flavourNumber)
searchName = keySuffix = d2
batchSize = self.getNumberOfResultsPerPage()
if not searchName: keySuffix = contentType
s = self.REQUEST.SESSION
searchKey = 'search_%s_%s' % (flavourNumber, keySuffix)
if s.has_key(searchKey): uids = s[searchKey]
else: uids = {}
# In the case of a search, we retrieve only a part of all
# surrounding objects, those that are stored in the session.
if (previousNeeded and not uids.has_key(previousIndex)) or \
(nextNeeded and not uids.has_key(nextIndex)):
# I do not have this UID in session. I will need to
# retrigger the query by querying all objects surrounding
# this one.
newStartNumber = (res['currentNumber']-1) - (batchSize / 2)
if newStartNumber < 0: newStartNumber = 0
self.executeQuery(contentType, flavourNumber,
searchName=searchName, startNumber=newStartNumber,
remember=True)
uids = s[searchKey]
# For the moment, for first and last, we get them only if we have
# them in session.
if not uids.has_key(0): firstNeeded = False
if not uids.has_key(lastIndex): lastNeeded = False
# Compute URL of source object
startNumber = self.computeStartNumberFrom(res['currentNumber']-1,
res['totalNumber'], batchSize)
res['sourceUrl'] = self.getQueryUrl(contentType, flavourNumber,
searchName, ajax=False, startNumber=startNumber)
# Compute URLs
for urlType in ('previous', 'next', 'first', 'last'):
exec 'needIt = %sNeeded' % urlType
urlKey = '%sUrl' % urlType
res[urlKey] = None
if needIt:
exec 'index = %sIndex' % urlType
brain = self.uid_catalog(UID=uids[index])
if brain:
baseUrl = brain[0].getObject().getUrl()
navUrl = baseUrl + '/?nav=' + newNav % (index + 1)
res['%sUrl' % urlType] = navUrl
return res
# ------------------------------------------------------------------------------

View file

@ -1079,11 +1079,15 @@ class AbstractMixin:
'''This method returns various URLs about this object.'''
baseUrl = self.absolute_url()
params = ''
rq = self.REQUEST
for k, v in kwargs.iteritems(): params += '&%s=%s' % (k, v)
params = params[1:]
if t == 'showRef':
chunk = '/skyn/ajax?objectUid=%s&page=ref&' \
'macro=showReferenceContent&' % self.UID()
startKey = '%s%s_startNumber' % (self.UID(), kwargs['fieldName'])
if rq.has_key(startKey) and not kwargs.has_key(startKey):
params += '&%s=%s' % (startKey, rq[startKey])
return baseUrl + chunk + params
else: # We consider t=='view'
return baseUrl + '/skyn/view' + params

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

View file

@ -475,7 +475,7 @@
<div metal:define-macro="showPageHeader"
tal:define="appyPages python: contextObj.getAppyPages(phase);
showCommonInfo python: not isEdit"
tal:condition="python: not contextObj.portal_factory.isTemporary(contextObj)">
tal:condition="not: contextObj/isTemporary">
<tal:comment replace="nothing">Information that is common to all tabs (object title, state, etc)</tal:comment>
<table width="100%" tal:condition="showCommonInfo" class="appyCommonInfo">
@ -499,7 +499,7 @@
</td>
</tr>
<tr tal:define="descrLabel python: contextObj.translate('%s_edit_descr' % contextObj.portal_type)"
tal:condition="descrLabel" >
tal:condition="descrLabel/strip" >
<tal:comment replace="nothing">Content type description</tal:comment>
<td colspan="2" class="discreet" tal:content="descrLabel"/>
</tr>
@ -526,6 +526,7 @@
</td>
</tr>
</table>
<metal:nav use-macro="here/skyn/macros/macros/objectNavigate"/>
<tal:comment replace="nothing">Tabs</tal:comment>
<ul class="contentViews appyTabs" tal:condition="python: len(appyPages)&gt;1">
@ -586,23 +587,38 @@
<metal:queryResults define-macro="queryResult"
tal:define="tool python: contextObj;
contentType request/contentType;
contentType request/type_name;
flavourNumber python: int(request['flavourNumber']);
startNumber python:test(request['startNumber']=='', '0', request['startNumber']);
startNumber request/startNumber|python:'0';
startNumber python: int(startNumber);
searchName request/searchName;
searchName request/search;
searchLabel python: '%s_search_%s' % (contentType, searchName);
searchDescr python: '%s_descr' % searchLabel;
severalTypes python: contentType and (contentType.find(',') != -1);
queryResult python: tool.executeQuery(contentType, flavourNumber, searchName, startNumber);
queryResult python: tool.executeQuery(contentType, flavourNumber, searchName, startNumber, remember=True);
objs queryResult/objects;
totalNumber queryResult/totalNumber;
batchSize queryResult/batchSize;
ajaxHookId python:'queryResult';
baseUrl python: tool.getQueryUrl(contentType, flavourNumber, searchName)">
baseUrl python: tool.getQueryUrl(contentType, flavourNumber, searchName, startNumber='**v**')">
<tal:result condition="objs">
<fieldset>
<legend>
<span tal:replace="structure python: test(searchName, tool.translate(searchLabel), test(severalTypes, tool.translate(tool.getAppName()), tool.translate('%s_plural' % contentType)))"/>
(<span tal:replace="totalNumber"/>)
</legend>
<table cellpadding="0" cellspacing="0" width="100%"><tr>
<td><span class="discreet" tal:define="descr python: tool.translate(searchDescr)"
tal:condition="python: searchName and descr" tal:content="descr"></span><br/><br/>
</td>
<td align="right">
<tal:comment replace="nothing">Appy (top) navigation</tal:comment>
<metal:nav use-macro="here/skyn/macros/macros/appyNavigate"/>
</td>
</tr></table>
<table tal:define="fieldDescrs python: tool.getResultColumns(objs[0], contentType)"
class="vertical listing" width="100%" cellpadding="0" cellspacing="0">
@ -662,7 +678,9 @@
<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="obj/Title" tal:attributes="href obj/getUrl"></a></td>
<td id="field_title"><a
tal:define="navInfo python:'nav=search.%s:%d.%s.%d.%d' % (contentType, flavourNumber, searchName, repeat['obj'].number()+startNumber, totalNumber);"
tal:content="obj/Title" tal:attributes="href python: obj.getUrl() + '/?' + navInfo"></a></td>
<tal:comment replace="nothing">Columns corresponding to other fields</tal:comment>
<tal:otherFields repeat="fieldDescr fieldDescrs">
@ -713,6 +731,7 @@
<tal:comment replace="nothing">Appy (bottom) navigation</tal:comment>
<metal:nav use-macro="here/skyn/macros/macros/appyNavigate"/>
</fieldset>
</tal:result>
<span tal:condition="not: objs"
@ -932,13 +951,13 @@
<td><img style="cursor:pointer" tal:condition="python: (startNumber != 0) and (startNumber != batchSize)"
tal:attributes="src string: $portal_url/skyn/arrowLeftDouble.png;
title python: tool.translate('goto_first');
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl+'0')"/></td>
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl.replace('**v**', '0'))"/></td>
<tal:comment replace="nothing">Go to the previous page</tal:comment>
<td><img style="cursor:pointer" tal:condition="python: startNumber != 0"
tal:define="sNumber python: startNumber - batchSize"
tal:attributes="src string: $portal_url/skyn/arrowLeftSimple.png;
title python: tool.translate('goto_previous');
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl+str(sNumber))"/></td>
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl.replace('**v**', str(sNumber)))"/></td>
<tal:comment replace="nothing">Explain which elements are currently shown</tal:comment>
<td class="discreet" valign="middle">&nbsp;
<span tal:replace="python: startNumber+1"/>
@ -946,13 +965,12 @@
<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>
<td><img style="cursor:pointer" tal:condition="python: sNumber &lt; totalNumber"
tal:define="sNumber python: startNumber + batchSize"
tal:attributes="src string: $portal_url/skyn/arrowRightSimple.png;
title python: tool.translate('goto_next');
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl+str(sNumber))"/></td>
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl.replace('**v**', str(sNumber)))"/></td>
<tal:comment replace="nothing">Go to the last page</tal:comment>
<td><img style="cursor:pointer" tal:condition="python: (startNumber != sNumber) and (startNumber != sNumber-batchSize)"
tal:define="lastPageIsIncomplete python: totalNumber % batchSize;
@ -961,7 +979,51 @@
sNumber python: (nbOfCountedPages*batchSize)"
tal:attributes="src string: $portal_url/skyn/arrowRightDouble.png;
title python: tool.translate('goto_last');
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl+str(sNumber))"/></td>
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl.replace('**v**', str(sNumber)))"/></td>
</tr>
</table>
</div>
<div metal:define-macro="objectNavigate" tal:condition="request/nav|nothing" align="right">
<tal:comment replace="nothing">
Buttons for going to next/previous elements if this one is among bunch of referenced or searched objects.
currentNumber starts with 1.
</tal:comment>
<table cellpadding="0" cellspacing="0"
tal:define="navInfo tool/getNavigationInfo;
currentNumber navInfo/currentNumber;
totalNumber navInfo/totalNumber;
firstUrl navInfo/firstUrl;
previousUrl navInfo/previousUrl;
nextUrl navInfo/nextUrl;
lastUrl navInfo/lastUrl;
sourceUrl navInfo/sourceUrl">
<tr>
<tal:comment replace="nothing">Go to the source URL (search or referred object)</tal:comment>
<td><a tal:condition="sourceUrl" tal:attributes="href sourceUrl"><img style="cursor:pointer"
tal:attributes="src string: $portal_url/skyn/gotoSource.png;
title python: tool.translate('goto_source')"/></a></td>
<tal:comment replace="nothing">Go to the first page</tal:comment>
<td><a tal:condition="firstUrl" tal:attributes="href firstUrl"><img style="cursor:pointer"
tal:attributes="src string: $portal_url/skyn/arrowLeftDouble.png;
title python: tool.translate('goto_first')"/></a></td>
<tal:comment replace="nothing">Go to the previous page</tal:comment>
<td><a tal:condition="previousUrl" tal:attributes="href previousUrl"><img style="cursor:pointer"
tal:attributes="src string: $portal_url/skyn/arrowLeftSimple.png;
title python: tool.translate('goto_previous')"/></a></td>
<tal:comment replace="nothing">Explain which element is currently shown</tal:comment>
<td class="discreet" valign="middle">&nbsp;
<span tal:replace="python: currentNumber"/>&nbsp;<b>//</b>
<span tal:replace="python: totalNumber"/>&nbsp;&nbsp;
</td>
<tal:comment replace="nothing">Go to the next page</tal:comment>
<td><a tal:condition="python: nextUrl" tal:attributes="href nextUrl"><img style="cursor:pointer"
tal:attributes="src string: $portal_url/skyn/arrowRightSimple.png;
title python: tool.translate('goto_next')"/></a></td>
<tal:comment replace="nothing">Go to the last page</tal:comment>
<td><a tal:condition="lastUrl" tal:attributes="href lastUrl"><img style="cursor:pointer"
tal:attributes="src string: $portal_url/skyn/arrowRightDouble.png;
title python: tool.translate('goto_last')"/></a></td>
</tr>
</table>
</div>

View file

@ -17,10 +17,7 @@
tool python: portal.get('portal_%s' % appName.lower());
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)">
searchName python:context.REQUEST.get('search', '')">
<div metal:use-macro="here/skyn/macros/macros/pagePrologue"/>
<script language="javascript">
@ -143,11 +140,6 @@
-->
</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>

View file

@ -4,9 +4,12 @@
<metal:objectTitle define-macro="objectTitle">
<tal:comment replace="nothing">Displays the title of a referenced object, with a link on
it to reach the consult view for this object. If we are on a back reference, the link
allows to reach the correct page where the forward reference is defined.</tal:comment>
allows to reach the correct page where the forward reference is defined. If we are
on a forward reference, the "nav" parameter is added to the URL for allowing to navigate
from one object to the next/previous on skyn/view.</tal:comment>
<a tal:define="viewUrl obj/getUrl;
fullUrl python: test(isBack, viewUrl + '/?pageName=%s&phase=%s' % (appyType['page'], appyType['phase']), viewUrl)"
navInfo python:'nav=ref.%s.%s.%d.%d' % (contextObj.UID(), fieldName, repeat['obj'].number()+startNumber, totalNumber);
fullUrl python: test(isBack, viewUrl + '/?pageName=%s&phase=%s' % (appyType['page'], appyType['phase']), viewUrl + '/?' + navInfo)"
tal:attributes="href fullUrl" tal:content="obj/Title"></a>
</metal:objectTitle>
@ -72,7 +75,7 @@
- descrId (string) the i18n id of the reference field description
</tal:comment>
<div metal:define-macro="showReference"
tal:define="ajaxHookId python: contextObj.UID()+fieldName;
tal:define="ajaxHookId python: contextObj.UID() + fieldName;
ajaxUrl python: contextObj.getUrl('showRef', **{'fieldName': fieldName, 'isBack': isBack, 'innerRef': innerRef, 'labelId': labelId, 'descrId': descrId})"
tal:attributes="id ajaxHookId">
<script language="javascript"
@ -108,7 +111,7 @@
atMostOneRef python: (multiplicity[1] == 1) and (len(objs)&lt;=1);
label python: tool.translate(labelId);
description python: tool.translate(descrId);
baseUrl python: contextObj.getUrl('showRef', **{'fieldName': fieldName, 'isBack': isBack, 'innerRef': innerRef, 'labelId': labelId, 'descrId': descrId}) + '&%s_startNumber=' % ajaxHookId">
baseUrl python: contextObj.getUrl('showRef', **{'fieldName': fieldName, 'isBack': isBack, 'innerRef': innerRef, 'labelId': labelId, 'descrId': descrId, '%s_startNumber' % ajaxHookId: '**v**'})">
<tal:comment replace="nothing">This macro displays the Reference widget on a "consult" page.
@ -130,7 +133,7 @@
<tal:comment replace="nothing">If there is an object...</tal:comment>
<tal:objectIsPresent condition="python: len(objs) == 1">
<tal:obj define="obj python:objs[0]">
<tal:obj repeat="obj objs">
<td><metal:showObjectTitle use-macro="here/skyn/ref/macros/objectTitle" /></td>
<td tal:condition="not: isBack">
<metal:showObjectActions use-macro="here/skyn/ref/macros/objectActions" />
@ -145,6 +148,7 @@
<fieldset tal:attributes="class python:test(innerRef, 'innerAppyFieldset', '')">
<legend tal:condition="python: not innerRef or showPlusIcon">
<span tal:condition="not: innerRef" tal:content="label"/>
(<span tal:replace="totalNumber"/>)
<metal:plusIcon use-macro="here/skyn/ref/macros/plusIcon"/>
</legend>

View file

@ -113,7 +113,7 @@
background-color: &dtml-evenRowBackgroundColor;;
border-style: solid;
border-width: 2px;
margin-bottom: 1em;
margin-bottom: 0.5em;
}
.appyWorkflow {

View file

@ -9,4 +9,8 @@ class ToolWrapper:
if initiatorUid:
res = self.o.uid_catalog(UID=initiatorUid)[0].getObject().appy()
return res
def getObject(self, uid):
'''Allow to retrieve an object from its unique identifier p_uid.'''
return self.o.getObject(uid, appy=True)
# ------------------------------------------------------------------------------

View file

@ -85,6 +85,7 @@ class PoMessage:
GOTO_PREVIOUS = 'Go to previous'
GOTO_NEXT = 'Go to next'
GOTO_LAST = 'Go to end'
GOTO_SOURCE = 'Go back'
def __init__(self, id, msg, default, fuzzy=False, comments=[]):
self.id = id