Leveraging the Component Architecture

Background

If you’re not familiar with component architectures, and zope.component in particular, please take a few minutes to read An Introduction to the Zope Component Architecture.

Key Objects

Three of the the standard names available in templates play a particularly important part in the way templates, macros and viewlets are used.

Those names are context, request and view. These names are inherited from the Zope family of Web frameworks, but they have equal relevance here. We’ll begin by discussing each of the objects individually, and then in Component Lookup we will explore how they come together.

Context

The context is the object being rendered. It is most commonly a Nikola post (represented by IPost or IMathJaxPost), but it can also be a IPostList on index pages (or IMathJaxPostList), or IGallery, IListing or ISlide on their respective pages.

Tip

The template variables for specific template pages are available from the context object, in addition to being available from the options dictionary. Using context can result templates that are more clear in their intent.

Request

The request (also known as the layer for the role it plays) expresses the intent of the render. It tells us the purpose of our rendering. It will always provide the IPageKind interface. Indeed, it will actually always provide one of the more specific interfaces that better identifies what we’re trying to render, such as ITagsPageKind.

In the Zope web framework, various interfaces are applied to the request to determine the “skin”, or appearance, of web pages. Layers are also used to determine available functionality such as the contents of menus. IPageKind is used for similar purposes.

View

Finally, the view is the code that initiated the rendering. In the web frameworks, this typically corresponds to the last portion of the URL. It determines the overall output by choosing the template to render and providing additional functionality. For example, if the context is a JPEG photograph, a URL like /photo.jpg/full_screen may use a view called full_screen and result in a full-screen view of the photograph, while for the same photo, a URL like /photo.jpg/thumbnail may produce a small thumbnail using a view called thumbnail

In Nikola, it is always the Nikola engine that initiated the rendering process, and so the view object is always the same, an instance of View. This object provides a templates attribute to access the ChameleonTemplates object, through which the Nikola site object can be found.

In addition, the view also has “layer” interfaces applied to it to express various aspects of system configuration. Currently the only such layer is the ICommentKind. More specifically, a particular subinterface identifying whether comments are disabled (ICommentKindNone) or enabled (ICommentKindAllowed) and if they are enabled what specific comment system is being used (ICommentKindDisqus).

Caution

The specific layer interfaces applied to requests and views are subject to change in future versions. The division now is somewhat arbitrary, but the intent is to allow for registering macros for a request layer of IStoryPageKind when comments are ICommentKindDisqus; to do that we need to be able to separate those interfaces on different objects, or introduce a bunch of unified interfaces (IStoryPageKindCommendKindDisqus), which is intractable.

Component Lookup

Now that we know about the three key objects, we can talk about exactly why they are so key and how they are used. In short, they are used for component lookup through the system, and it is this layer of indirection that permits us flexibility and allows for easy Theme Inheritance. Or to put it more pragmatically, they allow us to declaratively configure how templates are composed and used, instead of creating a mess of spaghetti code.

Throughout, we will use the example of Nikola rendering a page (not a blog post) in a system that has Disqus comments enabled.

Templates

Let’s begin at the beginning. When Nikola asks to find a template, say page.tmpl, we begin by determining the correct context, request and view to use, applying the appropriate layers to each object.

At this point we have a context implementing IPost, a request implementing IPostPageKind, and a view implementing ICommentKindDisqus.

These three objects together are used to ask the component registry for an adapter to IContentTemplate. This means that, in addition to the name, we have three degrees of freedom for finding a template. This is a great way to keep programattic logic out of your templates, streamlining them, reducing the chances of error, and shifting runtime checks to faster declarative configuration.

By default, if a page.tmpl.pt file exists on disk, it will be found registered for the page.tmpl template name. All such default templates are registered with the lowest priority, least-specific interfaces possible. This means that you can easily provide a more specific template customized exactly for pages that have disqus comments enabled.

In your theme.zcml, you would include a z3c.template directive to register this template. (We’ll say that this custom template file is named disqus_page.pt. Remember that the template we’re replacing, the one Nikola is asking for, is named page.tmpl)

<configure  xmlns="http://namespaces.zope.org/zope"
            xmlns:i18n="http://namespaces.zope.org/i18n"
            xmlns:zcml="http://namespaces.zope.org/zcml"
            xmlns:z3c="http://namespaces.zope.org/z3c"
            xmlns:browser="http://namespaces.zope.org/browser"
            >
  <include package="z3c.template" file="meta.zcml" />

  <z3c:template
     template="disqus_page.pt"
     name="page.tmpl"
     context=".interfaces.IPost"
     layer=".interfaces.IPostPageKind"
     for=".interfaces.ICommentKindDisqus"
     />

</configure>

Note

In the z3c:template directive, layer means the request object, and for means the view object.

Although this is certainly possible, and may work well for extremely special cases, or for keeping your .pt files as full HTML that can be edited in a visual editor, replacing an entire template like this is rare, though. There are more effective ways to control the content on portions of a page with macros and viewlets.

Adding Templates

Another way to use this lookup of templates in the component registry is to add templates that don’t exist on disk (and hence wouldn’t be registered by default). This is useful when templates are similar enough that they can be collapsed into a single file, with all of what needs to be different between them managed by macros and viewlets registered for specific interfaces. The other templating systems Nikola supports don’t have this flexibility, so Nikola asks for a different template name in almost every situation; we can instead share a template and simply supply it under different names.

Again, your theme.zcml would include z3c.template directives to do this:

<configure  xmlns="http://namespaces.zope.org/zope"
            xmlns:i18n="http://namespaces.zope.org/i18n"
            xmlns:zcml="http://namespaces.zope.org/zcml"
            xmlns:z3c="http://namespaces.zope.org/z3c"
            xmlns:browser="http://namespaces.zope.org/browser"
            >
   <include package="z3c.template" file="meta.zcml" />


    <!--
    We don't have files on disk that match all the template names
    that Nikola likes to use by default. So lets set up some aliases
    to the files that we *do* have that implement the required
    functionality.
    -->
    <z3c:template
      template="index.tmpl.pt"
      name="archiveindex.tmpl"
      layer=".interfaces.IArchiveIndexPageKind"
      />

    <z3c:template
      template="generic_post_list.pt"
      name="tag.tmpl" />

    <z3c:template
      template="generic_post_list.pt"
      name="author.tmpl" />
  </configure>

Macros

When you use the macro: expression type in a template, the registered macro is also looked up based on the current context, request, and view. Suppose we are in our disqus_page.pt rendering with our IPost context, IPostPageKind request layer, and ICommentKindDisqus view.

<div metal:use-macro="macro:comments" />

The registered macro named comments will be looked up for those three objects.

Just as with templates, the macros that are found in .macro.pt files are registered for the most generic, least specific interfaces possible. They can be augmented or replaced in your theme.zcml using the z3c.macro directives:

<configure xmlns="http://namespaces.zope.org/zope"
           xmlns:i18n="http://namespaces.zope.org/i18n"
           xmlns:zcml="http://namespaces.zope.org/zcml"
           xmlns:z3c="http://namespaces.zope.org/z3c"
           xmlns:browser="http://namespaces.zope.org/browser"
        >

  <include package="z3c.macro" />
  <include package="z3c.macro" file="meta.zcml" />

  <!-- Extra macros -->
  <z3c:macro
     name="comments"
     template="comment_helper.pt"
     for=".interfaces.IPost"
     view=".interfaces.ICommentKindDisqus"
     layer=".interfaces.IPostPageKind" />

</configure>

By default, the name of the macro in the template file is the same as the name it is registered with: the name attribute in the ZCML element. If they need to be different, you can supply the macro attribute in ZCML.

For example, if we have two different kinds of comment systems in comment_helper.pt:

<tal:block>

   <metal:block metal:define-macro="comments-facebook">
      <!-- Stuff for Facebook comments -->
   </metal:block>
   <metal:block metal:define-macro="comments-disqus">
      <!-- Stuff for Disqus comments -->
   </metal:block>
</tal:block>

We could register them and use them both for different comment systems with this ZCML:

<configure xmlns="http://namespaces.zope.org/zope"
           xmlns:i18n="http://namespaces.zope.org/i18n"
           xmlns:zcml="http://namespaces.zope.org/zcml"
           xmlns:z3c="http://namespaces.zope.org/z3c"
           xmlns:browser="http://namespaces.zope.org/browser"
        >

  <include package="z3c.macro" />
  <include package="z3c.macro" file="meta.zcml" />

  <!-- Extra macros -->
  <z3c:macro
     name="comments"
     macro="comments-disqus"
     template="comment_helper.pt"
     for=".interfaces.IPost"
     view=".interfaces.ICommentKindDisqus"
     layer=".interfaces.IPostPageKind" />
  <z3c:macro
     name="comments"
     macro="comments-facebook"
     template="comment_helper.pt"
     for=".interfaces.IPost"
     view=".interfaces.ICommentKindFacebook"
     layer=".interfaces.IPostPageKind" />

</configure>

For more on using macros (and in particular, how to find a macro for a different context), see Using Macros.

Viewlets

As you might have guessed, viewlets are handled the same way. When you use the provider: tales expression in a template, the resulting provider is found in the component registry using the current context, request and view.

Note

Although the provider: expression type supports generic content providers, this package exclusively uses viewlets. This is because viewlets can be developed using only templates and ZCML, without any Python code.

Unlike macros and templates, there is no automatic registration for viewlet managers and viewlets. Instead, they must all be registered in ZCML.

Suppose we have a template that uses a viewlet:

<head>
  ...
  <!--! The extensible viewlet manager; anyone can add things based
       on view, context and request/layer to it. -->
  <tal:block content="structure provider:extra_head" />
</head>

We would set up and fill that viewlet with this ZCML:

<configure xmlns="http://namespaces.zope.org/zope"
           xmlns:i18n="http://namespaces.zope.org/i18n"
           xmlns:zcml="http://namespaces.zope.org/zcml"
           xmlns:z3c="http://namespaces.zope.org/z3c"
           xmlns:browser="http://namespaces.zope.org/browser"
        >

  <include package="zope.viewlet" file="meta.zcml" />

  <!-- Extra head -->
  <!-- The normal extra head for a page is called 'default_extra_head' -->
  <browser:viewletManager
      name="extra_head"
      provides=".interfaces.IHtmlHeadViewletManager"
      class="zope.viewlet.manager.WeightOrderedViewletManager"
      permission="zope.Public"
      />

  <browser:viewlet
      name="default_extra_head"
      manager=".interfaces.IHtmlHeadViewletManager"
      template="v_index_extra_head.pt"
      permission="zope.Public"
      layer=".interfaces.IIndexPageKind"
      weight="0"
      />

  <browser:viewlet
      name="archive_index_extra_head"
      manager=".interfaces.IHtmlHeadViewletManager"
      template="v_archiveindex_extra_head.pt"
      permission="zope.Public"
      layer=".interfaces.IArchiveIndexPageKind"
      weight="1"
      />

  <!-- And so on as needed -->

</configure>

For much more about viewlets, see Using Viewlets.

Views

When you use the @@view_name syntax in a path expression, the previous path element becomes the context and an adapter from just context and request are looked up by that name.

Example template:

<html metal:use-macro="context/@@base.tmpl/index/macros/base"
      xmlns:tal="http://xml.zope.org/namespaces/tal"
      xmlns:metal="http://xml.zope.org/namespaces/metal">

   ...
</html>

The macro to use is looked for beginning with the expression context/@@base.tmpl. This takes the context object of this template and uses it to find the view (for the current request/layer) named @@base.tmpl. All the .tmpl.pt files are registered as most generic, least specific named views automatically. You can add your own view registrations for specific combinations of context and request if desired, though this usually requires writing Python code.

Tip

Any callable that accepts a context and request can be registered as a view. This package provides several other helpful views.