So far our site has defined pluggable areas for masthead, navigation, footer, sitematter and content.  These areas are filled by rendering their respective viewlet managers.  The masthead and footer areas already contain content as defined by viewlets in the layout.pt source code.

Navigation and content have yet to be defined, and this brings us to the page template menu.pt.

We begin by defining a Menu(grok.Viewlet), and tell it to render into the Navigation slot for instances of ILayout:

class Menu(grok.Viewlet):
    grok.viewletmanager(Navigation)  # Render into the Navigation area
    grok.context(ILayout)            # for all instances of ILayout

The corresponding template lives in menu.pt:

<div class='MenuHeader' tal:condition="context/navTitle | nothing">
    <h5 tal:content="context/navTitle" />
</div>

<div class='buttons' tal:condition="context/navTitle | nothing">
    <a tal:attributes="href python:viewlet.view.url(context,data={'edit':1})">Edit</a>
    <a tal:attributes="href python:viewlet.view.url(context,data={'add':1})">Add</a>
    <a tal:attributes="href python:viewlet.view.url(context)">Cancel</a>
</div>

<ul class='menu'>
    <div tal:replace='structure provider:menuitems' />
</ul>

 The result may be seen on the left, the template renders two div's and an unordered list; The first contains the navTitle for the current context.  Although.it is conditional on the context actually having a navTitle, we expect this to always be the case.  The second div contains three anchors, clearly containing links to edit, add or cancel operations (only shown if you are an authenticated user).  The unorderd list element renders the content of a new provider called 'menuitems'.

So let us take a look at the MenuItems viewlet manager:

class MenuItems(grok.ViewletManager):
    grok.context(ILayout)            # This will be a list of <li /> elements

We can see that it renders for contexts which implement the ILayout interface.

In order to greatly reduce the amount of boilerplate code we would otherwise have, we can define a generic MenuItem which we then may use as a base class for ad-hoc menu items we may wish to later add.  A grok.View or grok.Viewlet may use the grok.baseclass() directive to ensure that the class is treated as a base class and does not register itself with the ZCA.

class MenuItem(grok.Viewlet):
    ''' A base class for ad-hoc navigation menu items.
    '''
    grok.viewletmanager(MenuItems)   # Render into the MenuItems area
    grok.context(ILayout)            # for all instances of ILayout
    grok.baseclass()                 # This is not a concrete class

    title = u''
    link  = u''
    mclass = ''
    dclass = ''
    mtitle = ''
    image = None
    
    def condition(self):
        return True

    def selected(self):
        return self.title == getattr(self.context, 'navTitle', None)

    def href(self):
        return self.view.url(self.link)

    def render(self):
        if self.condition():
            img = ''
            if self.image is not None:
                img = '''<img src="{}" />'''.format(self.image)
                
            return ("""<li class="menuItem%s%s" title="%s"><a href="%s">%s%s</a></li>""" %
                    (' selected' if self.selected() else '',
                     ' %s'%self.mclass if len(self.mclass) else '', self.mtitle,
                     self.href(), img, self.title))
        else:
            return ''

For the first time we have an instance of a view or viewlet which does not have an associated page template.  The MenuItem class defines a render() method, which renders the whole of the viewlet <li /> tag content.  You cannot have both a render method and a page template for the same view or viewlet (a view may have only one way of rendering).

One or two things should be explained about the above code; 

First, menu items are selectively rendered depending on the condition() method.  A subclass may override this method, for example use information available in the context to decide whether or not to render.

Second, the grok.view.url(argument) call may take several kinds of argument.  Text is interpreted as relative to the current view url, if a grok.Model or grok.Container is the argument, the absolute url to the object is returned.

Third, the selected() method returns True if the menu title matches the navTitle of the context.  That is to say, if the current URL has identified a model which has the same navTitle as the currently rendered menu item's title, we give the <li /> tag a selected class.  This is not immediately useful, but in conjunction with CSS, we can style the menu item differently depending on the current state of navigation.

mclass is an extra css class for the menu item, and mtitle is a title for the <li /> tag which is displayed when hovering over the item.  title is displayed as a menu title, and link is interpreted by the href() method as discussed. If an image is specified, it will be rendered in a 20x20 area to the left of the menu title.

In order to move ahead with our discussion here, we will first need to introduce the IArticle interface.  We don't need to know an awful lot about it yet, other than that there is one, and that it represents an article in our wiki.  The reason we introduce IArticle here, is that IArticle is a container which contains other IArticle's, and the navigation menu is generated automatically from the content.  An IArticle also has a title, a navTitle, and text:

class IArticle(Interface):
    ''' Identifies an article, and defines a data schema for it
    '''
    title = TextLine(title=u'Page Title:', description=u'The title of the displayed page')
    navTitle = TextLine(title=u'Nav Title:', description=u'A title for the navigation area')
    text  = Text(title=u'Display Text:', description=u'Text to display on this page')

In the above class definition, the TextLine and Text classes are defined in zope.schema, and such schema classes are the basis for server-side validation and automatic form generation in Grok.  This will be discussed in more detail later.

We define a ContainerMenu viewlet which renders into the MenuItems slot whenever the context is an IArticle:

class ContainerMenu(grok.Viewlet):
    ''' Render the items contained inside self.context.  The container menu must
        always be rendered in the same order
    '''
    grok.viewletmanager(MenuItems)   # Render into the MenuItems area
    grok.context(IArticle)            # for all instances of ILayout

    def sortedItems(self):
        sorter = IArticleSorter(self.context)
        return sorter.sortedItems()

The corresponding containermenu.pt page template loops through the containermenu.sorteditems() and renders a <li /> item tag for each one:

<li class="indent" tal:repeat='item viewlet/sortedItems'>
    <a tal:attributes="href python:viewlet.view.url(item)" tal:content="item/navTitle" />
</li>

Which brings us to IArticleSorter.  Grok defines two kinds of containers; the general grok.Container looks like and quacks like a dict.  The grok.OrderedContainer sort of looks like and quacks like a dict which always appends to the end, and has an updateOrder() method which can be used to change the order of items maintained.  Performance of grok.OrderedContainer is not as good as the grok.Container, due to the need to maintain the sorted order.

Containers in a hierarchical structure may contain other containers, and this forms the basis for site hierarchies in the ZODB, Zope, and Grok, and the ability to locate resources through traversal in that hierarchy.

We decided to use a normal grok.Container and implement our own sorting extension, mostly in order to demonstrate extensibility but also because we wanted a bit more flexibility than a grok.OrderedContainer would have given us out of the box. The IArticleSorter interface is defined as follows:

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

The line...

sorter = IArticleSorter(self.context)

in the listing for the ContainerMenu viewlet finds an adapter which can turn a self.context, which we know to be an instance of IArticle, into an IArticleSorter instance.  This adapter implementation may be found in sortedarticles.py, and this will lead us kicking and screaming to the next part of our narrative  discussion.

 

Grok 4 Noobs

Using Viewlets and Viewlet Managers to build a Navigation Fabric