22. Views III: A Talk List – Mastering Plone 5 development

22. Views III: A Talk List#

In this part you will:

  • Write a Python class to get all talks from the catalog

  • Write a template to display the talks

  • Improve the table

Topics covered:

  • BrowserView

  • plone.api

  • portal_catalog

  • brains and objects

  • Acquisition

Now we don't want to provide information about one specific item but on several items. What now? We can't look at several items at the same time as context.

22.1. Using portal_catalog#

Let's say we want to show a list of all the talks that were submitted for our conference. We can just go to the folder and select a display method that suits us. But none does because we want to show the target audience in our listing.

So we need to get all the talks. For this we use the Python class of the view to query the catalog for the talks.

The catalog is like a search engine for the content on our site. It holds information about all the objects as well as some of their attributes like title, description, workflow_state, keywords that they were tagged with, author, content_type, its path in the site etc. But it does not hold the content of "heavy" fields like images or files, richtext fields and fields that we just defined ourselves.

It is the fast way to get content that exists in the site and do something with it. From the results of the catalog we can get the objects themselves but often we don't need them, but only the properties that the results already have.

browser/configure.zcml

1<browser:page
2   name="talklistview"
3   for="*"
4   layer="zope.interface.Interface"
5   class=".views.TalkListView"
6   template="templates/talklistview.pt"
7   permission="zope2.View"
8   />

browser/views.py

 1from Products.Five.browser import BrowserView
 2from plone import api
 3from plone.dexterity.browser.view import DefaultView
 4
 5[...]
 6
 7class TalkListView(BrowserView):
 8    """ A list of talks
 9    """
10
11    def talks(self):
12        results = []
13        brains = api.content.find(context=self.context, portal_type='talk')
14        for brain in brains:
15            talk = brain.getObject()
16            results.append({
17                'title': brain.Title,
18                'description': brain.Description,
19                'url': brain.getURL(),
20                'audience': ', '.join(talk.audience),
21                'type_of_talk': talk.type_of_talk,
22                'speaker': talk.speaker,
23                'room': talk.room,
24                'uuid': brain.UID,
25                })
26        return results

We query the catalog with two parameters. The catalog returns only items for which both apply:

  • context=self.context

  • portal_type='talk'

We pass a object as context to query only for content in the current path. Otherwise we'd get all talks in the whole site. If we moved some talks to a different part of the site (e.g. a sub-conference for universities with a special talk list) we might not want so see them in our listing. We also query for the portal_type so we only find talks.

Note

We use the method find() in plone.api to query the catalog. It is one of many convenience-methods provided as a wrapper around otherwise more complex api's. If you query the catalog direcly you'd have to first get the catalog, and pass it the path for which you want to find items:

portal_catalog = api.portal.get_tool('portal_catalog')
current_path = '/'.join(self.context.getPhysicalPath())
brains = portal_catalog(path=current_path, portal_type='talk')

We iterate over the list of results that the catalog returns.

We create a dictionary that holds all the information we want to show in the template. This way we don't have to put any complex logic into the template.

22.2. brains and objects#

Objects are normally not loaded into memory but lie dormant in the ZODB database. Waking objects up can be slow, especially if you're waking up a lot of objects. Fortunately our talks are not especially heavy since they are:

  • Dexterity objects which are lighter than their Archetypes brothers

  • relatively few since we don't have thousands of talks at our conference

We want to show the target audience but that attribute of the talk content type is not in the catalog. This is why we need to get to the objects themselves.

We could also add a new index to the catalog that will add 'audience' to the properties of brains, but we should weigh the pros and cons:

  • talks are important and thus most likely always in memory

  • prevent bloating of catalog with indexes

Note

The code to add such an index would look like this:

from plone.indexer.decorator import indexer
from ploneconf.site.talk import ITalk

@indexer(ITalk)
def talk_audience(object, **kw):
     return object.audience

We'd have to register this factory function as a named adapter in the configure.zcml. Assuming you've put the code above into a file named indexers.py

<adapter name="audience" factory=".indexers.talk_audience" />

We will add some indexers later on.

Why use the catalog at all? It checks for permissions, and only returns the talks that the current user may see. They might be private or hidden to you since they are part of a top secret conference for core developers (there is no such thing!).

Most objects in Plone act like dictionaries, so you can do context.values() to get all its contents.

For historical reasons some attributes of brains and objects are written differently.

>>> obj = brain.getObject()

>>> obj.title
u'Talk submission is open!'

>>> brain.Title == obj.title
True

>>> brain.title == obj.title
False

Who can guess what brain.title will return since the brain has no such attribute?

Note

Answer: Acquisition will get the attribute from the nearest parent. brain.__parent__ is <CatalogTool at /Plone/portal_catalog>. The attribute title of the portal_catalog is 'Indexes all content in the site'.

Acquisition can be harmful. Brains have no attribute 'getLayout' brain.getLayout():

>>> brain.getLayout()
'folder_listing'

>>> obj.getLayout()
'newsitem_view'

>>> brain.getLayout
<bound method PloneSite.getLayout of <PloneSite at /Plone>>

The same is true for methods:

>>> obj.absolute_url()
'http://localhost:8080/Plone/news/talk-submission-is-open'
>>> brain.getURL() == obj.absolute_url()
True
>>> brain.getPath() == '/'.join(obj.getPhysicalPath())
True

22.3. Querying the catalog#

The are many catalog indexes to query. Here are some examples:

>>> portal_catalog = getToolByName(self.context, 'portal_catalog')
>>> portal_catalog(Subject=('cats', 'dogs'))
[]
>>> portal_catalog(review_state='pending')
[]

Calling the catalog without parameters returns the whole site:

>>> portal_catalog()
[<Products.ZCatalog.Catalog.mybrains object at 0x1085a11f0>, <Products.ZCatalog.Catalog.mybrains object at 0x1085a12c0>, <Products.ZCatalog.Catalog.mybrains object at 0x1085a1328>, <Products.ZCatalog.Catalog.mybrains object at 0x1085a13 ...

22.4. Exercises#

Since you now know how to query the catalog it is time for some exercise.

Exercise 1#

Add a method get_news() to TalkListView that returns a list of brains of all News Items that are published and sort them in the order of their publishing date.

Solution
1def get_news(self):
2
3    portal_catalog = api.portal.get_tool('portal_catalog')
4    return portal_catalog(
5        portal_type='News Item',
6        review_state='published',
7        sort_on='effective',
8    )

Exercise 2#

Add a method that returns all published keynotes as objects.

Solution
 1def keynotes(self):
 2
 3    portal_catalog = api.portal.get_tool('portal_catalog')
 4    brains = portal_catalog(
 5        portal_type='talk',
 6        review_state='published')
 7    results = []
 8    for brain in brains:
 9        # There is no catalog index for type_of_talk so we must check
10        # the objects themselves.
11        talk = brain.getObject()
12        if talk.type_of_talk == 'Keynote':
13            results.append(talk)
14    return results

22.5. The template for the listing#

Next you create a template in which you use the results of the method 'talks'.

Try to keep logic mostly in Python. This is for two* reasons (and by "two", we mean "three"):

Readability:

It's much easier to read Python than complex TAL structures

Speed:

Python code is faster than code executed in templates. It's also easy to add caching to methods.

DRY, or "Don't Repeat Yourself":

In Python you can reuse methods and easily refactor code. Refactoring TAL usually means having to do big changes in the HTML structure which results in incomprehensible diffs.

The MVC schema does not directly apply to Plone but look at it like this:

Model:

the object

View:

the template

Controller:

the view

The view and the controller are very much mixed in Plone. Especially when you look at some of the older code of Plone you'll see that the policy of keeping logic in Python and representation in templates was not always enforced.

But you should nevertheless do it! You'll end up with more than enough logic in the templates anyway.

Add this simple table to templates/talklistview.pt:

 1<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
 2      metal:use-macro="context/main_template/macros/master"
 3      i18n:domain="ploneconf.site">
 4<body>
 5  <metal:content-core fill-slot="content-core">
 6  <table class="listing"
 7         id="talks"
 8         tal:define="talks python:view.talks()">
 9    <thead>
10      <tr>
11        <th>Title</th>
12        <th>Speaker</th>
13        <th>Audience</th>
14      </tr>
15    </thead>
16    <tbody>
17      <tr tal:repeat="talk talks">
18        <td>
19          <a href=""
20             tal:attributes="href python:talk['url'];
21                             title python:talk['description']"
22             tal:content="python:talk['title']">
23             The 7 sins of Plone development
24          </a>
25        </td>
26        <td tal:content="python:talk['speaker']">
27            Philip Bauer
28        </td>
29        <td tal:content="python:talk['audience']">
30            Advanced
31        </td>
32        <td tal:content="python:talk['room']">
33            101
34        </td>
35      </tr>
36      <tr tal:condition="python: not talks">
37        <td colspan=4>
38            No talks so far :-(
39        </td>
40      </tr>
41    </tbody>
42  </table>
43
44  </metal:content-core>
45</body>
46</html>

Again we use class="listing" to give the table a nice style.

There are some things that need explanation:

tal:define="talks python:view.talks()"

This defines the variable talks. We do this since we reuse it later and don't want to call the same method twice. Since TAL's path expressions for the lookup of values in dictionaries is the same as for the attributes of objects and methods of classes we can write view/talks as we could view/someattribute. Handy but sometimes irritating since from looking at the page template alone we often have no way of knowing if something is an attribute, a method or the value of a dict.

tal:repeat="talk talks"

This iterates over the list of dictionaries returned by the view. Each talk is one of the dictionaries that are returned by this method.

tal:content="python:talk['speaker']"

'speaker' is a key in the dict 'talk'. We could also write tal:content="talk/speaker"

tal:condition="python: not talks"

This is a fallback if no talks are returned. It then returns an empty list (remember results = []?)

Exercise#

Modify the view to only use path expressions. This is not best practice but there is plenty of code in Plone and in add-ons so you have to know how to use them.

Solution
 1<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
 2      metal:use-macro="context/main_template/macros/master"
 3      i18n:domain="ploneconf.site">
 4<body>
 5  <metal:content-core fill-slot="content-core">
 6  <table class="listing" id="talks"
 7         tal:define="talks view/talks">
 8    <thead>
 9      <tr>
10        <th>Title</th>
11        <th>Speaker</th>
12        <th>Audience</th>
13      </tr>
14    </thead>
15    <tbody>
16      <tr tal:repeat="talk talks">
17        <td>
18          <a href=""
19             tal:attributes="href talk/url;
20                             title talk/description"
21             tal:content="talk/title">
22             The 7 sins of Plone development
23          </a>
24        </td>
25        <td tal:content="talk/speaker">
26            Philip Bauer
27        </td>
28        <td tal:content="talk/audience">
29            Advanced
30        </td>
31      </tr>
32      <tr tal:condition="not:talks">
33        <td colspan=3>
34            No talks so far :-(
35        </td>
36      </tr>
37    </tbody>
38  </table>
39
40  </metal:content-core>
41</body>
42</html>

22.6. Setting a custom view as default view on an object#

We don't want to always have to append /@@talklistview to our folder to get the view. There is a very easy way to set the view to the folder using the ZMI.

If we append /manage_propertiesForm we can set the property "layout" to talklistview.

To make views configurable so that editors can choose them we have to register the view for the content type at hand in its FTI. To enable it for all folders we add a new file profiles/default/types/Folder.xml

1<?xml version="1.0"?>
2<object name="Folder">
3 <property name="view_methods" purge="False">
4  <element value="talklistview"/>
5 </property>
6</object>

After re-applying the typeinfo profile of our add-on (or simply reinstalling it) the content type "Folder" is extended with our additional view method and appears in the display dropdown.

The purge="False" appends the view to the already existing ones instead of replacing them.

22.7. Summary#

  • You created a nice listing, that can be called at any place in the website

  • You wrote your first fully grown BrowserView that combines a template, a class and a method in that class

  • You learned about portal_catalog, brains and how they are related to objects

  • You learned about acquisition and how it can have unintended effects

  • You extended the FTI of an existing content type to allow editors to configure the new view as default