Getting the Add… Menu in your custom browser views (content actions)

Background

When I create a new browser view following Professional Plone Development, or using ZopeSkel’s ‘paster addcontent view’ local command, I get (basically) the following code:

Products/MyProduct/browser/myview.py:


from zope.interface import implements, Interface

from Products.Five import BrowserView
from Products.CMFCore.utils import getToolByName

from Products.MyProduct import managementMessageFactory as _

class IMyView(Interface):
    """
    IterationUserstoryAssociation view interface
    """

    def test():
        """ test method"""

class MyView(BrowserView):
    """
    IterationUserstoryAssociation browser view
    """
    implements(IMyView)

    def __init__(self, context, request):
        self.context = context
        self.request = request 

    @property
    def portal_catalog(self):
        return getToolByName(self.context, 'portal_catalog')

    def test():
        """ test method """
        return "OK"

Products/MyProduct/browser/configure.zcml


 <browser:page
      for="*"
      name="myview"
      class=".myview.MyView"
      allowed_interface=".myview.IMyView"
      template="myview_view.pt"
      permission="zope.Public"
      />

Products/MyProduct/browser/myview_view.pt


<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
      lang="en"
      metal:use-macro="here/main_template/macros/master"
      i18n:domain="Products.MyProduct">
<body>
    <div metal:fill-slot="main">Content Here
    </div>
</body>
</html>

This creates a view on any piece of content in the site, accessible from @@myview. It looks like this (applied to the default Events collection that comes with Plone):

So this is great, and for most views, it’s probably all you need. But sometimes, I’d say most times, you want more from this view. Often we want to segregate our view from Plone, maybe even to the point of using a different slot in the template to toss out most of what main_template does.

But honestly, I think what we usually want is what Plone already has. Every “regular” view of a content object, and its folder_contents view, has certain user interface elements that are usually really handy to have available to your user when they are using your view.

Specifically, I’m talking about the content actions, or the “green bar dropdowns”. Here’s a close up so you know what I’m talking about:

In our view, we don’t get these. That really sucks.

After much toil, I’ve found out why, and two different ways to fix it.

Why is this happening?

First, the why. The “content actions” are actually a viewlet within a dedicated viewlet manager. We can see this through the @@manage-viewlets view:

Along for the ride are the “content views”, which are defined via <action> tags in our Generic Setup profile (the profiles/default/types/*.xml files, specifically). This isn’t confusing at all, is it (and the fact that the viewlet manager and content views viewlet are both named “plone.contentviews” is just icing on the cake!)?

Recall that a viewlet is registered such that it can/will only show up on a content object that implements a specific interface. Another potential limiting facotr, it turns out (my Zope 3 book was in another building today), is the specific view that the viewlet is rendered on. This has lots of potential uses, and speaks to the sweetness of the Component Architecture, but was very hard to figure out.

So the culprit in the case of our content-action-less view, is plone.app.layout. In plone.app.layout.viewlets’ configure.zcml file, lies the following code:


<browser:viewlet
    name="plone.contentactions"
    for="*"
    view="plone.app.layout.globals.interfaces.IViewView"
    manager=".interfaces.IContentViews"
    class=".common.ContentActionsViewlet"
    permission="zope2.View"
    />

<browser:viewlet
    name="plone.contentactions"
    for="*"
    view="plone.app.content.browser.interfaces.IFolderContentsView"
    manager=".interfaces.IContentViews"
    class=".common.ContentActionsViewlet"
    permission="zope2.View"
    />

What this is saying, is that the plone.contentactions viewlet will only show up for content objects that implement plone.app.layout.globals.interfaces.IViewView or plone.app.content.browser.interfaces.IFolderContentsView. This is precisely why we don’t get content actions in our view. Our views aren’t implementing either of those interfaces.

The comment a few lines above these declarations very coyly explains things:


<!-- Content actions (menus) 
    The default version is a blank bar; the one with real menus is
    registered for the main view + folder contents only.
-->

So now we get to the two possible solutions. I don’t think one is better than the other, it just depends on how explicit you want to be, and how much you trust plone.app.layout :)

Solution 1: Assign the Content Actions Viewlet Directly to Your Browser View

The first method assigns the viewlet, just as if it was a custom viewlet, to your browser view’s interface. In our case, it would look like this:


<browser:viewlet
    name="plone.contentactions"
    for="*"
    view=".myview.IMyView"
    manager="plone.app.layout.viewlets.interfaces.IContentViews"
    class="plone.app.layout.viewlets.common.ContentActionsViewlet"
    permission="zope2.View"
    />

The advantage here is that the viewlet will render no matter what happens with plone.app.layout. It also gives you more flexibility; you can limit the application of the viewlet in lots of different ways. You could combine the view attribute with the for attribute to limit it to only certain views that are registered to certain content types. You could create a special interface (instead of using the standard issue interface that represents your view class) that could be used as a marker interface to turn the viewlet on or off depending on an arbitrary state of your views. It could get really interesting.

Solution 2: Implement an Already-Registered Interface in Your View Class (This is What You Really Want to Do)

The easiest, and most straight-forward way to do this is to just add one of the interfaces, already registered to the viewlet, to your view class.

plone.app.layout specifies plone.app.layout.globals.interfaces.IViewView and plone.app.content.browser.interfaces.IFolderContentsView. After looking at the interfaces, it becomes obvious that plone.app.layout.globals.interfaces.IViewView is probably what we want to use. Here’s what the code looks like:


from zope.interface import implements, Interface

from Products.Five import BrowserView
from Products.CMFCore.utils import getToolByName

from plone.app.layout.globals.interfaces import IViewView

class IMyView(Interface):
    """
    IterationUserstoryAssociation view interface
    """

    def test():
        """ test method"""

class MyView(BrowserView):
    """
    IterationUserstoryAssociation browser view
    """
    implements(IMyView, IViewView)

    def __init__(self, context, request):
        self.context = context
        self.request = request 

    @property
    def portal_catalog(self):
        return getToolByName(self.context, 'portal_catalog')

    def test():
        """ test method """
        return "OK"

The Final Result

Here you can see it in action:

Wait, theres (probably) another way

Instead of implementing the interface yourself, you may be able to use ZCML to mark your view class with a registered interface… more on that later.

Updates

I mistakenly posted the wrong screen shot in the “The Final Result” section. Fixed 09:00 03/11/2010

Advertisements
This entry was posted in Uncategorized. Bookmark the permalink.

2 Responses to Getting the Add… Menu in your custom browser views (content actions)

  1. Apologies, I didn’t read the whole article, but: you should rarely, if ever, need to explicitly implement IViewView. That marker is applied to the view instance by Plone when the current view is the “default view” of the type. So, if you’d set @@myview as the default (or current, if you allow the user to select multiple via the “display” menu) view for the type, you’d get those actions.

    The fact that it doesn’t show up on other views is deliberate. If you have a use case for having it show up on something that is not “the view” (i.e. the thing you get when clicking the “view” tab), then marking your view with IViewView is the right thing to do. But you probably should ask why you want this.

    Martin

    • jjmojojjmojo says:

      I wasn’t aware of IViewView being used in that way. Nice. Now I wonder how that affects the usefulness of marking my view with it… I’m thinking maybe assigning the viewlet directly in zcml is the best way to go (and as a bonus it can be overridden in zcml).

      I agree, the content actions are hidden for good reasons (I believe this has been discussed before on various mailing lists). In my case, the views I’m creating are very specific, targeted views for a particular content type or purpose within the context of a content type. They’re essentially on the same “level” with the default view or the folder listing view, they just do different things. I’ve got them set as possible alternate display-menu items, and I provide a viewlet and/or portlet with context-aware links to the alternate views where it makes sense.

      So I believe very strongly that in my case having the content views show up enhances the user experience. But if the view was something different, like a settings panel or provided some other non-context-specific function, or it would otherwise confound the user (say, the base_edit form) I can see why hiding them would be a good idea.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s