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.
Exercise 2#
Add a method that returns all published keynotes as objects.
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 writeview/talks
as we couldview/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.
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