(Viewlets)

Description

Viewlets are parts of the page in Plone page rendering process. You can create, hide and shuffle them freely.

はじめに

Viewlets are view snippets which will render a part of the HTML page. Viewlets provide conflict-free way to contribute new user-interface actions and HTML snippets to Plone pages.

  • Viewlets are managed using /@@manage-viewlets page
  • Viewlets can shown and hidden through-the-web
  • Viewlets can be reordered (limited to reordering within container in Plone 3.x)
  • Viewlets can be registered and overridden in theme specific manner using layers
  • Viewlets have update() and render() methods
  • Viewlets should honour zope.contentprovider.interfaces.IContentProvider call contract.

Creating a viewlet

Viewlet consists of

  • Python class
  • Page template (.pt) file
  • A browser layer defining which add-on product must be installed, so that the viewlet is rendered

Example Python code for viewlets.py:

"""

    Facebook like viewlet for Plone.

    http://mfabrik.com

"""

__license__ = "GPL 2"
__copyright__ = "2010 mFabrik Research Oy"
__author__ = "Mikko Ohtamaa <mikko.ohtamaa@mfabrik.com>"
__docformat__ = "epytext"

import urllib

from plone.app.layout.viewlets import common as base

class LikeViewlet(base.ViewletBase):
    """ Add a Like button

    http://developers.facebook.com/docs/reference/plugins/like
    """

    def contructParameters(self):
        """ Create HTTP GET query parameters send to Facebook used to render the button.

        href=http%253A%252F%252Fexample.com%252Fpage%252Fto%252Flike&amp;layout=standard&amp;show_faces=true&amp;width=450&amp;action=like&amp;font&amp;colorscheme=light&amp;height=80
        """


        context = self.context.aq_inner
        href = context.absolute_url()

        params = {
                  "href" : href,
                  "layout" : "standard",
                  "show_faces" : "true",
                  "width" : "450",
                  "height" : "40",
                  "action" : "like",
                  "colorscheme" : "light",
        }

        return params

    def getIFrameSource(self):
        """
        @return: <iframe src=""> string
        """
        params = self.contructParameters()
        return "http://www.facebook.com/plugins/like.php" + "?" + urllib.urlencode(params)


    def getStyle(self):
        """ Construct CSS style for Like-button IFRAME.

        Use width and height from contstructParameters()

        style="border:none; overflow:hidden; width:450px; height:80px;"

        @return: style="" for <iframe>
        """
        params = self.contructParameters()
        return "margin-left: 10px; border:none; overflow:hidden; width:%spx; height:%spx;" % (params["width"], params["height"])

Then a sample page template (like.pt). You can use TAL template variable view to refer to your viewlet class instance:

<iframe scrolling="no"
        frameborder="0"
        allowTransparency="true"
        tal:attributes="src view/getIFrameSource; style view/getStyle"
        >
</iframe>

Registering a viewlet

Example configuration ZCML snippets below. You usually <viewlet> to browser/configure.zcml folder.

<configure
    xmlns="http://namespaces.zope.org/zope"
    xmlns:five="http://namespaces.zope.org/five"
    xmlns:browser="http://namespaces.zope.org/browser"
    xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
    i18n_domain="mfabrik.like">

    <browser:viewlet
      name="mfabrik.like"
      manager="Plone.App.Layout.Viewlets.Interfaces.IBelowContent"
      template="like.pt"
      layer="mfabrik.like.interfaces.IAddOnInstalled"
      permission="zope2.View"
      />

</configure>

Viewlet behavior

Viewlets have two important methods

  1. update() - set up all variables
  2. render() - generate the resulting HTML code by evaluating the template with context variables set up in update()

These methods should honour zope.contentprovider.interfaces.IContentProvider call contract.

See

Conditionally rendering viewlets

There are two primary methods to render viewlets only on some pages

  • Register viewlet against some marker interface or content type class - the viewlet is rendered on this content type only. You can use dynamic marker interfaces to toggle interface on some individual pages through ZMI
  • Hard-code a condition to your viewlet in Python code.

Below is an example of overriding a render() method to conditionally render your viewlet:

import Acquisition
from zope.component import getUtility

from plone.app.layout.viewlets import common as base
from plone.registry.interfaces import IRegistry


class LikeViewlet(base.ViewletBase):
    """ Add a Like button

    http://developers.facebook.com/docs/reference/plugins/like
    """

    def isEnabledOnContent(self):
        """
        @return: True if the current content type supports Like-button
        """
        registry = getUtility(IRegistry)
        content_types = registry['mfabrik.like.content_types']

        # Don't assume that all content items would have portal_type attribute
        # available (might be changed in the future / very specialized content)
        current_content_type =  portal_type = getattr(Acquisition.aq_base(self.context), 'portal_type', None)

        # Note that plone.registry keeps values as unicode strings
        # make sure that we have one also
        current_content_type = unicode(current_content_type)

        return current_content_type in content_types


    def render(self):
        """ Render viewlet only if it is enabled.

        """

        # Perform some condition check
        if self.isEnabledOnContent():
            # Call parent method which performs the actual rendering
            return super(LikeViewlet, self).render()
        else:
            # No output when the viewlet is disabled
            return ""

Rendering viewlet by name

Below is a complex example how to expose viewlets without going through a viewlet manager.

See collective.fastview for updates and more information.

from Acquisition import aq_inner
import zope.interface

from plone.app.customerize import registration

from Products.Five.browser import BrowserView

from zope.traversing.interfaces import ITraverser, ITraversable
from zope.publisher.interfaces import IPublishTraverse
from zope.publisher.interfaces.browser import IBrowserRequest
from zope.viewlet.interfaces import IViewlet
from zExceptions import NotFound

class Viewlets(BrowserView):
    """ Expose arbitary viewlets to traversing by name.

    Exposes viewlets to templates by names.

    Example how to render plone.logo viewlet in arbitary template code point::

        <div tal:content="context/@@viewlets/plone.logo" />

    """
    zope.interface.implements(ITraversable)

    def getViewletByName(self, name):
        """ Viewlets allow through-the-web customizations.

        Through-the-web customization magic is managed by five.customerize.
        We need to think of this when looking up viewlets.

        @return: Viewlet registration object
        """
        views = registration.getViews(IBrowserRequest)

        for v in views:

            if v.provided == IViewlet:
                # Note that we might have conflicting BrowserView with the same name,
                # thus we need to check for provided
                if v.name == name:
                    return v

        return None


    def setupViewletByName(self, name):
        """ Constructs a viewlet instance by its name.

        Viewlet update() and render() method are not called.

        @return: Viewlet instance of None if viewlet with name does not exist
        """
        context = aq_inner(self.context)
        request = self.request

        # Perform viewlet regisration look-up
        # from adapters registry
        reg = self.getViewletByName(name)
        if reg == None:
            return None

        # factory method is responsible for creating the viewlet instance
        factory = reg.factory

        # Create viewlet and put it to the acquisition chain
        # Viewlet need initialization parameters: context, request, view
        try:
            viewlet = factory(context, request, self, None).__of__(context)
        except TypeError:
            # Bad constructor call parameters
            raise RuntimeError("Unable to initialize viewlet %s. Factory method %s call failed." % (name, str(factory)))

        return viewlet

    def traverse(self, name, further_path):
        """
        Allow travering intoviewlets by viewlet name.

        @return: Viewlet HTML output

        @raise: RuntimeError if viewlet is not found
        """

        viewlet = self.setupViewletByName(name)
        if viewlet is None:
            raise NotFound("Viewlet does not exist by name %s for theme layer %s" % name)

        viewlet.update()
        return viewlet.render()

Rendering viewlets with accurate layout

Default viewlet managers render viewlets as HTML code string concatenation, in the order of appearance. This is unsuitable to build complex layouts.

Below is an example which defines master viewlet HeaderViewlet which will place other viewlets into the manually tuned HTMLable.

header.py:

from Acquisition import aq_inner

# Use template files with acquisition support
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile

# Import default Plone viewlet classes
from plone.app.layout.viewlets import common as base

# Import our customized viewlet classes
import plonetheme.something.browser.viewlets.common as something

def render_viewlet(factory, context, request):
    """ Helper method to render a viewlet """

    context = aq_inner(context)
    viewlet = factory(context, request, None, None).__of__(context)
    viewlet.update()
    return viewlet.render()


class HeaderViewlet(base.ViewletBase):
    """ Render header with special     le layout.

    Though we render viewlets internally we not inherit from the viewlet manager,
    since we do not offer the option for the site manager or integrator
    shuffle viewlets - they are fixed to our templates.
    """

    index = ViewPageTemplateFile('header_items.pt')

    def update(self):

        base.ViewletBase.update(self)

        # Dictionary containing all viewlets which are rendered inside this viewlet.
        # This is populated during render()
        self.subviewlets = {}

    def renderViewlet(self, viewlet_class):
        """ Render one viewlet

        @param viewlet_class: Class which manages the viewlet
        @return: Resulting HTML as string
        """
        return render_viewlet(viewlet_class, self.context, self.request)


    def render(self):

        self.subviewlets["logo"] = self.renderViewlet(something.SomethingLogoViewlet) # Customized viewlet
        self.subviewlets["second_level_navigation"] = self.renderViewlet(something.SecondLevelSectionsViewlet)
        self.subviewlets["sections"] = self.renderViewlet(something.SomethingGlobalSectionsViewlet)
        self.subviewlets["search"] = self.renderViewlet(base.SearchBoxViewlet) # Default Plone viewlet
        self.subviewlets["selector"] = self.renderViewlet(something.SomethingTransla    leLanguageSelector)
        self.subviewlets["site_actions"] = self.renderViewlet(something.SiteActionsViewlet)

        # Call template to perform rendering
        return self.index()

header_items.pt

<table class="something-header">
    <tbody>
        <tr class="upper">
            <td class="left">
                <div tal:replace="structure view/subviewlets/logo" />
            </td>

            <td>
                <    le class="right">
                    <tbody>
                        <tr>
                            <td>
                                <div tal:replace="structure view/subviewlets/search" />
                            </td>
                        </tr>

                        <tr>

                            <td>
                                <a href="http://www.something.fi">
                                    <img tal:attributes="src string:${view/site_url}/++resource++plonetheme.something/something_logo.gif" />
                                </a>
                            </td>

                            <td>
                                <div tal:replace="structure view/subviewlets/selector" />
                            </td>
                        </tr>

                    </tbody>
                </    le>
            </td>
        </tr>

        <tr class="lower">
            <td class="left">
                <div tal:replace="structure view/subviewlets/sections" />
            </td>
            <td class="right">
                <div tal:replace="structure view/subviewlets/site_actions" />
            </td>
        </tr>
    </tbody>
</    le>

configure.zcml

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

    <!--

        Public localizable site header

        See viewlets.xml for order/hidden
    -->


    <!-- Header viewlet which replaces the standard plone.header-->
    <browser:viewlet
        name="plone.header"
        manager="plone.app.layout.viewlets.interfaces.IPortalTop"
        class=".header.HeaderViewlet"
        permission="zope2.View"
        layer="..interfaces.IThemeSpecific"
        />


     <!-- Site actions-->
     <browser:viewlet
         name="plonetheme.something.site_actions"
         class=".common.SiteActionsViewlet"
         permission="zope2.View"
         template="site_actions.pt"
         layer="..interfaces.IThemeSpecific"
         allowed_attributes="site_actions"
         manager="..interfaces.IHeader"
         />

    <!-- The logo -->
    <browser:viewlet
        name="plonetheme.something.logo"
        class=".common.SomethingLogoViewlet"
        permission="zope2.View"
        layer="..interfaces.IThemeSpecific"
        template="logo.pt"
        manager="..interfaces.IHeader"
        />

    <!-- Searchbox -->
     <browser:viewlet
         name="plone.searchbox"
         for="*"
         class="plone.app.layout.viewlets.common.SearchBoxViewlet"
         permission="zope2.View"
         template="searchbox.pt"
         layer="..interfaces.IThemeSpecific"
         manager="..interfaces.IHeader"
         />

     <!-- First level navigation -->
     <browser:viewlet
         name="plonetheme.something.global_sections"
         for="*"
         class=".common.SomethingGlobalSectionsViewlet"
         permission="zope2.View"
         template="sections.pt"
         layer="..interfaces.IThemeSpecific"
         manager="..interfaces.IHeader"
         />

    <!-- Second level navigation -->
     <browser:viewlet
         name="plonetheme.something.second_level_sections"
         class=".common.SecondLevelSectionsViewlet"
         permission="zope2.View"
         template="second_level_sections.pt"
         layer="..interfaces.IThemeSpecific"
         manager="..interfaces.IHeader"
         />


    <!-- Language selector-->
        <browser:viewlet
          name="something.languageselector"
          class=".common.SomethingTransla    leLanguageSelector"
          permission="zope2.View"
          layer="..interfaces.IThemeSpecific"
          manager="..interfaces.IHeader"
         />
</configure>

portal_header.pt

<div id="portal-header">
    <div tal:replace="structure provider:something.header" />
</div>

Viewlets for one page only

Viewlets can be registered to one special page only using a marker interface. This allow loading a page specific CSS files.

<head> viewlets

You can register custom Javascript or CSS files to HTML <head> section using viewlets.

Below is an head.pt which will be injected in <head>. This examples shows how to dynamically generate <script> elements. Example is taken from mfabrik.like add-on.

<script type="text/javascript" tal:attributes="src view/getConnectScriptSource"></script>
<script tal:replace="structure view/getInitScriptTag" />

Then you register it against viewlewt manager plone.app.layout.viewlets.interfaces.IHtmlHead in configure.zcml

<browser:viewlet
   name="mfabrik.like.facebook-connect-head"
   class=".viewlets.FacebookConnectJavascriptViewlet"
   manager="plone.app.layout.viewlets.interfaces.IHtmlHead"
   template="facebook-connect-head.pt"
   layer="mfabrik.like.interfaces.IAddOnInstalled"
   permission="zope2.View"
   />

viewlet.py code:

class FacebookConnectJavascriptViewlet(LikeButtonOnConnectFacebookBaseViewlet):
    """ This will render Facebook Javascript load in <head>.

    <head> section is retrofitted only if the viewlet is enabled.

    """

    def getConnectScriptSource(self):
        base = "http://static.ak.connect.facebook.com/connect.php/"
        return base + self.getLocale()

    def getInitScriptTag(self):
        """ Get <script> which boostraps Facebook stuff.
        """
        return '<script type="text/javascript">FB.init("%s");</script>' % self.settings.api_key

    def isEnabled(self):
        """
        @return: Should this viewlet be rendered on this page.
        """
        # Some logic based self.context here whether Javascript should be included on this page or not
        return True


    def render(self):
        """ Render viewlet only if it is enabled.

        """

        # Perform some condition check
        if self.isEnabled():
            # Call parent method which performs the actual rendering
            return super(LikeButtonOnConnectFacebookBaseViewlet, self).render()
        else:
            # No output when the viewlet is disabled
            return ""

Finding viewlets programmatically

Occasionaly, you may need to get hold of your viewlets in python code, perhaps in tests. Since the availability of a viewlet is ultimately controlled by the viewlet manager to which it has been registered, using that manager is a good way to go

from zope.component import queryMultiAdapter
from zope.viewlet.interfaces import IViewletManager

from Products.Five.browser import BrowserView as View

from my.package.tests.base import MyPackageTestCase

class TestMyViewlet(MyPackageTestCase):
    """ test demonstrates that registration variables worked
    """

    def test_viewlet_is_present(self):
        """ looking up and updating the manager should list our viewlet
        """
        # we need a context and request
        request = self.app.REQUEST
        context = self.portal

        # viewlet managers also require a view object for adaptation
        view = View(context, request)

        # finally, you need the name of the manager you want to find
        manager_name = 'plone.portalfooter'

        # viewlet managers are found by Multi-Adapter lookup
        manager = queryMultiAdapter((context, request, view), IViewletManager, manager_name, default=None)
        self.failUnless(manager)

        # calling update() on a manager causes it to set up its viewlets
        manager.update()

        # now our viewlet should be in the list of viewlets for the manager
        # we can verify this by looking for a viewlet with the name we used
        # to register the viewlet in zcml
        my_viewlet = [v for v in manager.viewlets if v.__name__ == 'mypackage.myviewlet']

        self.failUnlessEqual(len(my_viewlet), 1)

Since it is possible to register a viewlet for a specific content type and for a browser layer, you may also need to use these elements in looking up your viewlet

from zope.component import queryMultiAdapter
from zope.viewlet.interfaces import IViewletManager
from Products.Five.browser import BrowserView as View
from my.package.tests.base import MyPackageTestCase

# this time, we need to add an interface to the request
from zope.interface import alsoProvides

# we also need our content type and browser layer
from my.package.content.mytype import MyType
from my.package.interfaces import IMyBrowserLayer

class TestMyViewlet(MyPackageTestCase):
    """ test demonstrates that zcml registration variables worked properly
    """

    def test_viewlet_is_present(self):
        """ looking up and updating the manager should list our viewlet
        """
        # our viewlet is registered for a browser layer.  Browser layers
        # are applied to the request during traversal in the publisher.  We
        # need to do the same thing manually here
        request = self.app.REQUEST
        alsoProvides(request, IMyBrowserLayer)

        # we also have to make our context an instance of our content type
        content_id = self.folder.invokeFactory('MyType', 'my-id')
        context = self.folder[content_id]

        # and that's it.  Everything else from here out is identical to the
        # example above.