Our journey continues with adding the ability to support a fixed order for menu items which are derived from a container which looks, and quacks, like a dict.  We also want to support the ability to change the maintained order of articles.

Since we know that the list of contained articles are not going to grow very large (there's a practical limit), it does not make a great deal of sense to expend much effort in permanently storing and maintaining lists of articles or article references. Instead, directly producing a new list and sorting the list every time we want to produce a sorted list of articles is hardly going to impact performance, while greatly reducing complexity and possibility for error.

The way we will remember the order of articles, is to store a persistent attribute called 'order' with each IArticle.

As we have already seen, our sorter interface looks like this:

class IArticleSorter(Interface):
    ''' This defines an interface into an article sorter.  We use this approach rather
        than using grok.OrderedContainer for articles, in order to demonstrate the
        extensibility of the grok web framework.
    '''
    def sortedItems(self):
        ''' Returns a sorted list of articles, and maintains sorting order'''
        return []

We can see that this specifies that the interface should define a method returning a list of sorted articles.

An adapter which returns an instance of IArticleSorter when given an instance of an IArticle, is defined as:

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)

As you can see, this does nothing more than define a factory which spews out instances of the Sorter() class.  So what is the Sorter() class, and what does it do?  At the very least we know it must implement a method called sortedItems() which according to the interface definition, returns a list of sorted articles:

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

Whoops! Information Overload!

What is that grok.implements(IArticleSorter, ILayout) and grok.provides(IArticleSorter)??? 

We are simply telling the ZCA that a Sorter model implements both IArticleSorter and ILayout interfaces. That is why the Sorter model also defines a title, since the site index view requires it (as per the ILayout interface definition).

As the ZCA now has two interfaces defined for Sorter, one cannot tell which one of the two the model provides. Hence the need for grok.provides(IArticleSorter).

Other than a few shenanigans in the renumber() method, the rest of the Sorter class implementation should be self-explanitory.

The renumber() method is passed a sorted list of [references to] instances of IArticle, as contained in their parent container. We traverse this list in order, and assign sequential numbers to the order attribute.  At the same time, we assign other attributes for prev, next, being the navTitle values for the prior and next article in the list respectively.  This will simplify implementing menu items for the prior or next items in the list while rendering a given article.

So this is why a simple call to sorter=IArticleSorter(context) can return an instance of Sorter for the context, and subsequent calls to sorter.sortedItems() will return the needed list of items- whilst as an added benefit ensuring consistency in things like prev/next links.

Ok, so what happens if we traverse the site and end up on an instance of a Sorter?

Since Sorter implements ILayout, the default view, being index, will be rendered.  This means that viewlets for the Masthead, Footer, Navigation and Content areas will be rendered (if they exist) using the Sorter as a context.

To render the Sorter Content, we define a viewlet to render the list of contained IArticle instances:

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

For viewlets and views, the update() function will be called prior to the render() function or template.  This means that one may perform any processing needed that the template might need later.  In this case, we have an obscure looking ordermenu.need() and an assignment to a class variable containing the sorted items.

The ordermenu is defined in resource.py as:

from fanstatic import Library, Resource

library    = Library('mygrok.grok4noobs', 'static')
favicon    = Resource(library, 'favicon.ico')
style      = Resource(library, 'style.css')
...
jquery     = Resource(library, 'lib/jquery-1.11.1.min.js')
ordermenu  = Resource(library, 'ordermenu.js', depends=[jquery])
...

In the Installation section we described how static resources are served through the fanstatic library.  The definition of resources and their dependencies is generally found in the resource.py file, although there is nothing that enforces that module name.  To include a resource - in this case ordermenu.js - in the <head /> tag of the site, simply include in  the update() function for the view or viewlet, a line such as ordermenu.need().

 Our template for ReOrderViewlet is found in reorderviewlet.pt:

<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>

After styling, the sorter renders as a list of items something like this:

For navigation buttons, we define the following two classes:

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'

which adds two menu items to the sorter.  The first navigates back to the parent article, while the other effectively navigates back to the same URL that got us to the sorter in the first place.  What??? 

Take a closer look at the mclass = 'setItemOrder ...' line.  The trick is there.  A small javascript function in ordermenu.js traps and takes care of handling the button click, and performs the actual sorting function. The operation of the javascript function is described in the comments of ordermenu.js:

//_____________________________________________________________________________________
// 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();
            }
        }
    });
});

So, to be able to reorder a list of <li /> elements, we give them a class of 'movable'.  When we click an 'li.movable', we change the state of the clicked item to 'moving'; the siblings above get an aboveme class, and those below get a belowme class.  Clicking aboveme or belowme items completes the move operation.  The rest is done with css.

The line    $('<div />').load('setOrder', {'new_order':JSON.stringify(setOrder)}, function(response, status, xhr)... is the important bit which actually updates the item order.  The new order is simply an array of navTitle values retrieved from the list items themselves, and already in the now required order.  This code does a JSON call to the server, calling the setOrder(new_order) function.  At the server side, we implement the function within a grok.JSON class:

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'

As stated in the comments, one would not generally need to use JSON.stringify/simplejson.loads() to deal with arguments. However, the fact that arrays are passed with incompatible names makes argument transparency not really doable in Python.

 

Grok 4 Noobs

Ordered menus from a normal grok.Container