Nodes are now orderable via the ZMI

git-svn-id: svn://svn.cy55.de/Zope3/src/loops/trunk@1005 fd906abe-77d9-0310-91a1-e0d9ade77398
This commit is contained in:
helmutm 2006-01-13 10:02:51 +00:00
parent d61d424009
commit 4962810373
9 changed files with 419 additions and 22 deletions

View file

@ -71,6 +71,7 @@ We can now ask our concepts for their related concepts:
TODO: Work with views... TODO: Work with views...
Resources and what they have to do with Concepts Resources and what they have to do with Concepts
================================================ ================================================
@ -107,8 +108,17 @@ below) via the getClients() method:
>>> conc[0] is zope >>> conc[0] is zope
True True
Views: Menus, Menu Items, Listings, Simple Content, etc
======================================================= Views/Nodes: Menus, Menu Items, Listings, Pages, etc
====================================================
Note: the term "view" here is not directly related to the special
Zop 3 term "view" (a multiadapter for presentation purposes) but basically
bears the common sense meaning: an object (that may be persistent or
created on the fly) that provides a view to content of whatever kind.
Views (or nodes - that's the only type of views existing at the moment)
thus provide the presentation space to concepts and resources.
We first need a view manager: We first need a view manager:
@ -196,6 +206,59 @@ view class's target attribute:
>>> m111.target is zope3 >>> m111.target is zope3
True True
Node views
----------
>>> from loops.browser.node import NodeView
>>> view = NodeView(m1, TestRequest())
>>> view.menu()
{'url': 'http://127.0.0.1/loops/views/m1',
'items': [{'url': 'http://127.0.0.1/loops/views/m1/m11', 'items': [],
'selected': False, 'title': u'Zope'}],
'selected': True, 'title': u'Menu'}
>>> view.content()
{'url': 'http://127.0.0.1/loops/views/m1', 'body': u'', 'items': [],
'title': u'Menu'}
>>> view = NodeView(m11, TestRequest())
>>> view.menu()
{'url': 'http://127.0.0.1/loops/views/m1',
'items': [{'url': 'http://127.0.0.1/loops/views/m1/m11', 'items': [],
'selected': True, 'title': u'Zope'}],
'selected': False, 'title': u'Menu'}
>>> view.content()
{'url': 'http://127.0.0.1/loops/views/m1/m11', 'body': u'',
'items': [{'url': 'http://127.0.0.1/loops/views/m1/m11/m112',
'body': u'', 'items': [], 'title': u'Zope 3'}],
'title': u'Zope'}
Ordering Nodes
--------------
Let's add some more nodes and reorder them:
>>> m113 = Node()
>>> m11['m113'] = m113
>>> m114 = Node()
>>> m11['m114'] = m114
>>> m11.keys()
['m111', 'm112', 'm113', 'm114']
>>> m11.moveSubNodesByDelta(['m113'], -1)
>>> m11.keys()
['m111', 'm113', 'm112', 'm114']
A special management view provides methods for moving objects down, up,
to the bottom, and to the top
>>> from loops.browser.node import OrderedContainerView
>>> view = OrderedContainerView(m11, TestRequest())
>>> view.moveToBottom(('m113',))
>>> m11.keys()
['m111', 'm112', 'm114', 'm113']
>>> view.moveUp(('m114',), 1)
>>> m11.keys()
['m111', 'm114', 'm112', 'm113']
Fin de partie Fin de partie
============= =============

View file

@ -7,7 +7,7 @@
<!-- resources --> <!-- resources -->
<resource name="view.css" file="view.css" /> <resource name="node.css" file="node.css" />
<!-- macros --> <!-- macros -->
@ -199,13 +199,6 @@
view="AddLoopsViewManager.html" view="AddLoopsViewManager.html"
/> />
<containerViews
for="loops.interfaces.IViewManager"
index="zope.View"
contents="zope.ManageContent"
add="zope.ManageContent"
/>
<!-- node --> <!-- node -->
<addform <addform
@ -256,10 +249,25 @@
name="node.html" name="node.html"
/> />
<!-- move nodes up and down -->
<containerViews <containerViews
for="loops.interfaces.INode" for="loops.interfaces.IBaseNode"
contents="zope.ManageContent" index="zope.ManageContent"
add="zope.ManageContent" add="zope.ManageContent"
/> />
<pages
for="loops.interfaces.IBaseNode"
class=".node.OrderedContainerView"
permission="zope.ManageContent">
<page name="contents.html" template="contents.pt"
menu="zmi_views" title="Contents"
/>
<page name="move_down" attribute="moveDown" />
<page name="move_up" attribute="moveUp" />
<page name="move_bottom" attribute="moveToBottom" />
<page name="move_top" attribute="moveToTop" />
</pages>
</configure> </configure>

216
browser/contents.pt Normal file
View file

@ -0,0 +1,216 @@
<html metal:use-macro="context/@@standard_macros/view"
i18n:domain="zope">
<body>
<div metal:fill-slot="body">
<div metal:define-macro="contents">
<tal:checkmove define="isMoveAction view/checkMoveAction">
<tal:showlisting condition="not:isMoveAction">
<form name="containerContentsForm" method="post" action="."
tal:attributes="action request/URL"
tal:define="container_contents view/listContentInfo">
<input type="hidden" name="type_name" value=""
tal:attributes="value request/type_name"
tal:condition="request/type_name|nothing"
/>
<input type="hidden" name="retitle_id" value=""
tal:attributes="value request/retitle_id"
tal:condition="request/retitle_id|nothing"
/>
<div class="page_error"
tal:condition="view/error"
tal:content="view/error"
i18n:translate="">
Error message
</div>
<table id="sortable" class="listing" summary="Content listing"
i18n:attributes="summary">
<thead>
<tr>
<th>&nbsp;</th>
<th i18n:translate="">Name</th>
<th i18n:translate="">Title</th>
<th i18n:translate="">Size</th>
<th i18n:translate="">Created</th>
<th i18n:translate="">Modified</th>
</tr>
</thead>
<tbody>
<metal:block tal:condition="view/hasAdding">
<tr tal:define="names_required context/@@+/nameAllowed"
tal:condition="python:names_required and request.has_key('type_name')">
<td></td>
<td><input name="new_value" id="focusid" value="" /></td>
<td></td>
<td></td>
<td></td>
</tr>
</metal:block>
<metal:block tal:define="supportsRename view/supportsRename"
tal:repeat="item container_contents">
<tr tal:define="oddrow repeat/item/odd; url item/url;
id_quoted item/id/url:quote"
tal:attributes="class python:oddrow and 'even' or 'odd'" >
<td>
<input type="checkbox" class="noborder" name="ids:list" id="#"
value="#"
tal:attributes="value item/id;
id item/cb_id;
checked request/ids_checked|nothing;"/>
</td>
<td><a href="#"
tal:attributes="href
string:${url}/@@SelectedManagementView.html"
tal:content="structure item/icon|default">
</a
><span tal:condition="item/rename"
><input name="new_value:list"
tal:attributes="value item/id"
/><input type="hidden" name="rename_ids:list" value=""
tal:attributes="value item/rename"
/></span
><span tal:condition="not:item/rename">
<a href="#"
tal:attributes="href
string:${url}/@@SelectedManagementView.html"
tal:content="item/id"
i18n:translate=""
>foo</a
><a href="#"
tal:attributes="href
string:${request/URL}?rename_ids:list=${id_quoted}"
tal:condition="supportsRename"
>&nbsp;&nbsp;</a
></span
></td>
<td>
<input name="new_value" id="focusid"
tal:attributes="value item/title|nothing"
tal:condition="item/retitle"
/>
<a href="#"
tal:attributes="href
string:${request/URL}?retitle_id=${id_quoted}"
tal:condition="item/retitleable"
tal:content="item/title|default"
i18n:translate=""
>&nbsp;&nbsp;&nbsp;&nbsp;</a>
<span
tal:condition="item/plaintitle"
tal:content="item/title|default"
i18n:translate=""
>&nbsp;&nbsp;&nbsp;&nbsp;</span>
</td>
<td><span tal:content="item/size/sizeForDisplay|nothing"
i18n:translate="">
&nbsp;</span></td>
<td><span tal:define="created item/created|default"
tal:content="created"
i18n:translate="">&nbsp;</span></td>
<td><span tal:define="modified item/modified|default"
tal:content="modified"
i18n:translate="">&nbsp;</span></td>
</tr>
</metal:block>
</tbody>
</table>
<div tal:condition="view/normalButtons">
<input type="submit" name="container_rename_button" value="Rename"
i18n:attributes="value container-rename-button"
tal:condition="view/supportsRename"
/>
<input type="submit" name="container_cut_button" value="Cut"
i18n:attributes="value container-cut-button"
tal:condition="view/supportsCut"
/>
<input type="submit" name="container_copy_button" value="Copy"
i18n:attributes="value container-copy-button"
tal:condition="view/supportsCopy"
/>
<input type="submit" name="container_paste_button" value="Paste"
tal:condition="view/hasClipboardContents"
i18n:attributes="value container-paste-button"
/>
<input type="submit" name="container_delete_button" value="Delete"
i18n:attributes="value container-delete-button"
tal:condition="view/supportsDelete"
i18n:domain="zope"
/>
<div tal:condition="view/orderable">
<input type="submit" name="move_top" value="Top"
i18n:attributes="value container-movetop-button"
i18n:domain="zope" />
<input type="submit" name="move_up" value="Up"
i18n:attributes="value container-moveup-button"
i18n:domain="zope" />
<input type="submit" name="move_down" value="Down"
i18n:attributes="value container-moveup-button"
i18n:domain="zope" />
<input type="submit" name="move_bottom" value="Bottom"
i18n:attributes="value container-movebottom-button"
i18n:domain="zope" />
<input type="hidden" name="delta" value="1" />
</div>
<div tal:condition="view/hasAdding" tal:omit-tag="">
<div tal:omit-tag=""
tal:define="adding nocall:context/@@+;
addingInfo adding/addingInfo;
has_custom_add_view adding/hasCustomAddView;
names_required adding/nameAllowed"
tal:condition="adding/isSingleMenuItem">
<input type="submit" name="container_add_button" value="Add"
i18n:attributes="value add-button"
i18n:domain="zope"
/>
<input type="text" name="single_new_value" id="focusid"
tal:condition="python:names_required and not has_custom_add_view"
i18n:domain="zope"
/>
<input type="hidden" name="single_type_name"
value=""
tal:attributes="value python:addingInfo[0]['action']"
/>
</div>
</div>
</div>
<div tal:condition="view/specialButtons">
<input type="submit" value="Apply"
i18n:attributes="value container-apply-button"
/>
<input type="submit" name="container_cancel_button" value="Cancel"
i18n:attributes="value container-cancel-button"
/>
</div>
</form>
</tal:showlisting>
</tal:checkmove>
<script type="text/javascript"><!--
if (document.containerContentsForm.new_value)
document.containerContentsForm.new_value.focus();
//-->
</script>
</div>
</div>
</body>
</html>

View file

@ -6,14 +6,14 @@
<metal:css fill-slot="style_slot"> <metal:css fill-slot="style_slot">
<style type="text/css" media="all" <style type="text/css" media="all"
tal:content="string:@import url(${context/++resource++view.css});"> tal:content="string:@import url(${context/++resource++node.css});">
@import url(view.css); @import url(view.css);
</style> </style>
</metal:css> </metal:css>
<metal:body fill-slot="body"> <metal:body fill-slot="body">
<tal:content define="item view/getContent; <tal:content define="item view/content;
level level|python: 1"> level level|python: 1">
<metal:block define-macro="content"> <metal:block define-macro="content">
@ -36,7 +36,7 @@
<div class="box" metal:fill-slot="navigators"> <div class="box" metal:fill-slot="navigators">
<h4>Navigation</h4> <h4>Navigation</h4>
<tal:menu define="item view/getMenu; <tal:menu define="item view/menu;
level level|python: 1" level level|python: 1"
condition="item"> condition="item">

View file

@ -24,6 +24,7 @@ $Id$
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from zope.app import zapi from zope.app import zapi
from zope.app.container.browser.contents import JustContents
from zope.app.dublincore.interfaces import ICMFDublinCore from zope.app.dublincore.interfaces import ICMFDublinCore
from zope.proxy import removeAllProxies from zope.proxy import removeAllProxies
from zope.security.proxy import removeSecurityProxy from zope.security.proxy import removeSecurityProxy
@ -32,6 +33,10 @@ from loops.interfaces import IConcept
class NodeView(object): class NodeView(object):
def __init__(self, context, request):
self.context = context
self.request = request
def render(self, text): def render(self, text):
if not text: if not text:
return u'' return u''
@ -49,7 +54,7 @@ class NodeView(object):
d = dc.modified or dc.created d = dc.modified or dc.created
return d and d.strftime('%Y-%m-%d %H:%M') or '' return d and d.strftime('%Y-%m-%d %H:%M') or ''
def getContent(self, item=None): def content(self, item=None):
if item is None: if item is None:
item = self.context.getPage() item = self.context.getPage()
if item is None: if item is None:
@ -57,11 +62,11 @@ class NodeView(object):
result = {'title': item.title, result = {'title': item.title,
'url': zapi.absoluteURL(item, self.request), 'url': zapi.absoluteURL(item, self.request),
'body': self.render(item.body), 'body': self.render(item.body),
'items': [self.getContent(child) 'items': [self.content(child)
for child in item.getTextItems()]} for child in item.getTextItems()]}
return result return result
def getMenu(self, item=None): def menu(self, item=None):
if item is None: if item is None:
item = self.context.getMenu() item = self.context.getMenu()
if item is None: if item is None:
@ -69,7 +74,53 @@ class NodeView(object):
result = {'title': item.title, result = {'title': item.title,
'url': zapi.absoluteURL(item, self.request), 'url': zapi.absoluteURL(item, self.request),
'selected': item == self.context, 'selected': item == self.context,
'items': [self.getMenu(child) 'items': [self.menu(child)
for child in item.getMenuItems()]} for child in item.getMenuItems()]}
return result return result
class OrderedContainerView(JustContents):
""" A view providing the necessary methods for moving sub-objects
within an ordered container.
"""
@Lazy
def url(self):
return zapi.absoluteURL(self.context, self.request)
@Lazy
def orderable(self):
return len(self.context) > 1
def checkMoveAction(self):
request = self.request
for var in request:
if var.startswith('move_'):
params = []
if 'delta' in request:
params.append('delta=' + request['delta'])
if 'ids' in request:
for id in request['ids']:
params.append('ids:list=' + id)
request.response.redirect('%s/%s?%s'
% (self.url, var, '&'.join(params)))
return True
return False
def moveDown(self, ids, delta=1):
self.context.moveSubNodesByDelta(ids, int(delta))
self.request.response.redirect(self.url + '/contents.html')
def moveUp(self, ids, delta=1):
self.context.moveSubNodesByDelta(ids, -int(delta))
self.request.response.redirect(self.url + '/contents.html')
def moveToBottom(self, ids):
self.context.moveSubNodesByDelta(ids, len(self.context))
self.request.response.redirect(self.url + '/contents.html')
def moveToTop(self, ids):
self.context.moveSubNodesByDelta(ids, -len(self.context))
self.request.response.redirect(self.url + '/contents.html')

View file

@ -170,8 +170,12 @@ class IView(Interface):
target = Attribute('Target object that is referenced by this view') target = Attribute('Target object that is referenced by this view')
class IBaseNode(IOrderedContainer):
""" Common abstract base class for different types of nodes
"""
class INode(IView, IOrderedContainer):
class INode(IView, IBaseNode):
""" A node is a view that may contain other views, thus building a """ A node is a view that may contain other views, thus building a
menu or folder hierarchy. menu or folder hierarchy.
@ -227,8 +231,14 @@ class INode(IView, IOrderedContainer):
a menu). a menu).
""" """
def moveSubNodesByDelta(names, delta):
""" Move the sub-nodes specified by the list of names up
(negative delta) or down (positive delta) by the number of places
specified by delta.
"""
class IViewManager(IContainer):
class IViewManager(IBaseNode):
""" A manager/container for views. """ A manager/container for views.
""" """
contains(IView) contains(IView)

46
util.py Normal file
View file

@ -0,0 +1,46 @@
#
# Copyright (c) 2006 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
"""
Utility functions.
$Id$
"""
def moveByDelta(objs, toMove, delta):
""" Return the list given by objs resorted in a way that the elements
of toMove (which must be in the objs list) have been moved by delta.
"""
result = [obj for obj in objs if obj not in toMove]
if delta < 0:
objs = list(reversed(objs))
result.reverse()
toMove = sorted(toMove, lambda x,y: cmp(objs.index(x), objs.index(y)))
for element in toMove:
newPos = min(len(result), objs.index(element) + abs(delta))
result.insert(newPos, element)
if delta < 0:
result.reverse()
return result
def nl2br(text):
if not text: return text
if '\n' in text: # Unix or DOS line endings
return '<br />\n'.join(x.replace('\r', '') for x in text.split('\n'))
else: # gracefully handle Mac line endigns
return '<br />\n'.join(text.split('\r'))

View file

@ -34,6 +34,7 @@ from cybertools.relation.registry import IRelationsRegistry, getRelations
from interfaces import IView, INode from interfaces import IView, INode
from interfaces import IViewManager, INodeContained from interfaces import IViewManager, INodeContained
from interfaces import ILoopsContained from interfaces import ILoopsContained
from util import moveByDelta
class View(object): class View(object):
@ -122,8 +123,10 @@ class Node(View, OrderedContainer):
def getTextItems(self): def getTextItems(self):
return self.getChildNodes(['text']) return self.getChildNodes(['text'])
def moveSubNodesByDelta(self, names, delta):
self.updateOrder(moveByDelta(self.keys(), names, delta))
class ViewManager(BTreeContainer): class ViewManager(OrderedContainer):
implements(IViewManager, ILoopsContained) implements(IViewManager, ILoopsContained)