Sortedarticles.pt module:

'''
    This module deals with ordering articles and managing article order.
    Grok does include a OrderedContainer type which could have formed
    a better basis for our IArticle implementation, but the code below
    demonstrates numerous features quite well.
'''
import grok

from interfaces import IArticle, IArticleSorter
from layout import ILayout, Content
from resource import ordermenu
from menu import MenuItem

class SorterBackLink(MenuItem):
    grok.context(IArticleSorter)
    grok.order(0)
    title = u'Back to article'
    link = u'..'
    mclass = 'nav buttons'

class SorterAccept(MenuItem):
    grok.context(IArticleSorter)
    grok.order(1)
    title = u'Accept this menu order'
    link = u''
    mclass = 'setItemOrder buttons'

class Sorter(grok.Model):
    ''' Implements an order for items in a normal grok.Container.  Not terribly efficient
        since it sorts every time we extract the items, but there won't really be that
        many items anyway, and this demonstrates a transient object.
    '''
    grok.implements(IArticleSorter, ILayout)
    grok.provides(IArticleSorter)
    title = u'Ordering the navigation menu'

    def __init__(self, context):
        self.context = context
    def renumber(self, items):
        prev = None
        for i, ob in enumerate(items):
            if prev is not None:             # Simultaneously build a doubly linked list
                prev.next = ob.navTitle      # while assigning an object order
                ob.prev = prev.navTitle
            else:
                ob.prev = None
            ob.next = None
            ob.order = i  # Re-assign order numbers
            prev = ob
    def itemKey(self, value):
        return getattr(value, 'order', 9999)  # by default insert items at the end
    def sortedItems(self):
        items = [v for v in self.context.values()]
        items.sort(key=lambda value: self.itemKey(value))
        self.renumber(items)   # We ensure a renumber of ordered items every time we render
        return items

class LayoutArticleSorter(grok.Adapter):
    ''' Adapts an IArticle and returns an IArticleSorter.  Uses a factory pattern to return
        an instance of a Sorter.  Since the Sorter implements ILayout, the site's index view
        will be rendered as the default view for such objects.  This means that our viewlet
        below will be called for the Content area.
    '''
    grok.context(IArticle)
    grok.implements(IArticleSorter)

    def __new__(cls, context):
        return Sorter(context)

class ReOrderViewlet(grok.Viewlet):
    ''' Renders an interface for re-ordering items in the IArticle container
    '''
    grok.context(IArticleSorter)
    grok.viewletmanager(Content)

    items = []
    def update(self):
        ordermenu.need()                             # Include Javascript
        self.items = self.context.sortedItems()      # Get the items

class OrderSetter(grok.JSON):
    '''  Any method declared below becomes a JSON callable
    '''
    grok.context(IArticleSorter)

    def setOrder(self, new_order=None):
        '''  Accepts an array of navTitle elements in the required order.
            Normally, we would not have to use JSON.stringify and
            simplejson.loads on arguments, but array arguments get
            names which are not compatible with Python.
        '''
        from simplejson import loads
        from urllib import quote_plus

        if new_order is not None:
            new_order = loads(new_order)   # Unescape stringified json
            container = self.context.context
            for nth, navTitle in enumerate(new_order):
                container[quote_plus(navTitle)].order = nth
            return 'ok'

The ReOrderArticle viewlet page template:

<div>
    <ul class="menu">
        <li class="menuItem movable" tal:repeat="item viewlet/items">
            <span class='navTitle' tal:content="item/navTitle" />
            - <span tal:content="item/title" />
        </li>
    </ul>
</div>

The ordermenu.js source:

//_____________________________________________________________________________________
// Javascript to move and re-order menu items.
//   Initial state, are a bunch of ul.menu > li.movable items.
//   When we click on one of them, we remove the .movable from all of them, and set
//   the clicked one to li.moving.  The items above we set to li.aboveme, and the
//   items below, we set to li.belowme.  When an li.aboveme is clicked, we move the
//   li.moving item to be above the clicked element.  The opposite for li.belowme.
//   When the li.moving item has moved, we remove aboveme, belowme and moving, and
//   set all the classes back to movable.

$(document).ready(function(){
    $('li.menuItem.setItemOrder').on('click', function(clickEvent){
        var setOrder = [];
        clickEvent.preventDefault();
        $('span.navTitle', $('li.menuItem.movable')).each(function(){
            setOrder.push($(this).text());
        });

        $('<div />').load('setOrder', {'new_order':JSON.stringify(setOrder)},
                                        function(response, status, xhr){
            console.log('response is ' + response);
            if (status != 'error') {
                document.location.href = '../';
            } else {
                alert(response);
            }
        });
    });

    $('ul.menu').on('click', '> li.movable, >li.aboveMe, >li.belowMe', function(){
        var parent = $(this).parent();
        var siblings = parent.children();

        function resetState(){
            siblings = parent.children();
            siblings.removeClass('moving').addClass('movable');
            siblings.removeClass('aboveMe').removeClass('belowMe');
        }

        if ($(this).hasClass('movable')) {
            var toMove = $(this);
            var idx = toMove.index();

            siblings.removeClass('movable');
            toMove.addClass('moving');

            if (idx > 0) {
                siblings.slice(0, idx).addClass('aboveMe');
            }
            if (idx < siblings.length) {
                siblings.slice(idx+1).addClass('belowMe');
            }
        } else {
            var toMove = $('li.moving', parent);
            if ($(this).hasClass('aboveMe')) {
                toMove.remove();
                $(this).before(toMove);
                resetState();
            }
            if ($(this).hasClass('belowMe')) {
                toMove.remove();
                $(this).after(toMove);
                resetState();
            }
        }
    });
});

The CSS styling for the sort interface(ordermenu.css):

/* css to support moving menu items around */
li.menuItem.movable, li.menuItem.aboveMe, li.menuItem.belowMe {
    margin:2px 0;
	border:1px solid gray;
	border-top-color:darkgray;
	border-left-color:darkgray;
	cursor:pointer;
    padding:1px 0.5em;
}

li.menuItem.movable:hover {
	background-color: #BAEACA;
}

li.menuItem.aboveMe:hover, li.menuItem.belowMe:hover {
	cursor:move;
}

li.menuItem.moving {
	background-color: #CACAEA;
}

li.menuItem.aboveMe:hover {
    border-top:2px solid black;
    margin-top:0px;
}

li.menuItem.belowMe:hover {
    border-bottom:2px solid black;
    margin-bottom:0px;
}

 

Grok 4 Noobs

Full source for SortedArticles Module