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
  • cerebros y objetos

  • tables

Ahora no queremos para proporcionar información sobre un elemento específico, sino en varios elementos. ¿Y ahora qué? No podemos mirar a varios elementos al mismo tiempo que el contexto.

Usando 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.

Es la forma más rápida de obtener el contenido que existe en el sitio y hacer algo con él. A partir de los resultados del catálogo podemos conseguir los mismos objetos, pero a menudo no lo necesita, pero sólo las propiedades que los resultados ya tienen.

browser/configure.zcml

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

browser/views.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from Products.Five.browser import BrowserView
from plone import api
from plone.dexterity.browser.view import DefaultView


class DemoView(BrowserView):
    """ This does nothing so far
    """


class TalkView(DefaultView):
    """ The default view for talks
    """


class TalkListView(BrowserView):
    """ A list of talks
    """

    def talks(self):
        results = []
        portal_catalog = api.portal.get_tool('portal_catalog')
        current_path = "/".join(self.context.getPhysicalPath())

        brains = portal_catalog(portal_type="talk",
                                path=current_path)
        for brain in brains:
            talk = brain.getObject()
            results.append({
                'title': brain.Title,
                'description': brain.Description,
                'url': brain.getURL(),
                'audience': ', '.join(talk.audience),
                'type_of_talk': talk.type_of_talk,
                'speaker': talk.speaker,
                'uuid': brain.UID,
                })
        return results

Nosotros consultamos el catálogo para dos cosas:

  • portal_type = "talk"
  • path = "/".join(self.context.getPhysicalPath())

Obtenemos la ruta del contexto actual para consultar sólo para los objetos en la ruta actual. De lo contrario nos íbamos a todas las charlas en todo el sitio. Si nos trasladamos algunas charlas a una parte diferente del lugar (por ejemplo, un sub-conferencia de universidades con una lista especial charla) puede ser que no quiera ver así en nuestra lista.

Nos iterar sobre la lista de resultados que el catálogo nos devuelve.

Creamos un diccionario que contiene toda la información que queremos mostrar en la plantilla. De esta manera no tiene que poner ninguna lógica compleja en la plantilla.

cerebros y objetos

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
  • relativamente pocos, ya que no tenemos miles de charlas en nuestra conferencia

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 to weigh the pros and cons:

  • charlas son importantes y por lo tanto más probable es que siempre este en memoria

  • prevenir la inflamación de catálogo con índices

Nota

El código para agregar este índice se vería así:

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

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

Tendríamos que registrar esta función factory como un adaptador llamado en el archivo configure.zcml. Suponiendo que haya puesto el código anterior en un archivo llamado indexers.py

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

Vamos a agregar algunos indices más adelante.

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.

Por razones históricas, algunos atributos de los cerebros y los objetos se escriben de manera diferente:

>>> obj = brain.getObject()

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

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

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

¿Quién puede adivinar lo que brain.title volverán ya que el cerebro no tiene ese atributo?

Nota

Respuesta:. Adquisición obtendrá el atributo de servidor principal más cercano. brain.__parent__ es <CatalogTool at /Plone/portal_catalog>. El atributo title del portal_catalog es ‘Indixado en todo el contenido en el sitio’.

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>>

Lo mismo es cierto para los métodos:

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

Consultando el catálogo

Los muchos índices de catálogo para consultar. He aquí algunos ejemplos:

>>> 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 ...

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

1
2
3
4
5
6
7
8
def get_news(self):

    portal_catalog = api.portal.get_tool('portal_catalog')
    return portal_catalog(
        portal_type='News Item',
        review_state='published',
        sort_on='effective',
    )

Exercise 2

Add a method that returns all published keynotes as objects.

Solution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def keynotes(self):

    portal_catalog = api.portal.get_tool('portal_catalog')
    brains = portal_catalog(
        portal_type='Talk',
        review_state='published')
    results = []
    for brain in brains:
        # There is no catalog-index for type_of_talk so we must check
        # the objects themselves.
        talk = brain.getObject()
        if talk.type_of_talk == 'Keynote':
            results.append(talk)
    return results

La plantilla para la lista

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:

Legibilidad:
It’s much easier to read python than complex tal-structures
Velocidad:

El código Python es más rápido que el código ejecutado en las plantillas. También es fácil de añadir almacenamiento en caché a los métodos.

DRY:
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.

El esquema MVC no se aplica directamente a Plone pero míralo de esta manera:

Modelo:

el objeto

Vista:

la plantilla

Controlador:

la vista

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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<table class="listing">
    <thead>
        <tr>
            <th>
                Title
            </th>
            <th>
                Speaker
            </th>
            <th>
                Audience
            </th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>
               The 7 sins of plone development
            </td>
            <td>
                Philip Bauer
            </td>
            <td>
                Advanced
            </td>
        </tr>
    </tbody>
</table>

Afterwards you transform it into a listing. Here we use one of many nice features built into Plone. The class="pat-tablesorter" (before Plone 5 that was class="listing") gives the table a nice style and makes the table sortable with some javascript.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<table class="listing pat-tablesorter" id="talks">
    <thead>
        <tr>
            <th>
                Title
            </th>
            <th>
                Speaker
            </th>
            <th>
                Audience
            </th>
        </tr>
    </thead>
    <tbody>
        <tr tal:repeat="talk view/talks">
            <td>
                <a href=""
                   tal:attributes="href talk/url;
                                   title talk/description"
                   tal:content="talk/title">
                   The 7 sins of plone-development
                </a>
            </td>
            <td tal:content="talk/speaker">
                Philip Bauer
            </td>
            <td tal:content="talk/audience">
                Advanced
            </td>
        </tr>
        <tr tal:condition="not:view/talks">
            <td colspan=3>
                No talks so far :-(
            </td>
        </tr>
    </tbody>
</table>

There are some some things that need explanation:

tal:repeat="talk view/talks"
This iterates over the list of dictionaries returned by the view. view/talks calls the method talks of our view and each talk is in turn one of the dictionaries that are returned by this method. Since TAL’s path expressions for the lookup of values in dictionaries is the same as the attributes of objects we can write talk/somekey as we could view/somemethod. 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. It would be a good practice to write tal:repeat="talk python:view.talks()".
tal:content="talk/speaker"

‘speaker’ es una clave en el diccionario ‘talk’. También podríamos escribir tal:content="python:talk['speaker']"

tal:condition="not:view/talks"
This is a fallback if no talks are returned. It then returns an empty list (remember results = []?)

Exercise

Modify the view to only python expressions.

Solution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<table class="listing pat-tablesorter" id="talks">
    <thead>
        <tr>
            <th>
                Title
            </th>
            <th>
                Speaker
            </th>
            <th>
                Audience
            </th>
        </tr>
    </thead>
    <tbody tal:define="talks python:view.talks()">
        <tr tal:repeat="talk talks">
            <td>
                <a href=""
                   tal:attributes="href python:talk['url'];
                                   title python:talk['description']"
                   tal:content="python:talk['title']">
                   The 7 sins of plone-development
                </a>
            </td>
            <td tal:content="python:talk['speaker']">
                Philip Bauer
            </td>
            <td tal:content="python:talk['audience']">
                Advanced
            </td>
        </tr>
        <tr tal:condition="python:not talks">
            <td colspan=3>
                No talks so far :-(
            </td>
        </tr>
    </tbody>
</table>

To follow the mantra “don’t repeat yourself” we define talks instead of calling the method twice.

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.

Si agregamos /manage_propertiesForm podemos establecer la propiedad “layout” para la vista 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 if for all folders we add a new file profiles/default/types/Folder.xml

1
2
3
4
5
6
<?xml version="1.0"?>
<object name="Folder">
 <property name="view_methods" purge="False">
  <element value="talklistview"/>
 </property>
</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.

Agregando un poco de javascript (collective.js.datatables)

Advertencia

We’ll skip this section since the integration of js in Plone 5 is still not finished while it is still an alpha!

We could improve that table further by using a nice javascript library called “datatables”. It might even become part of the Plone core at some point.

Like for many js libraries there is already a python package that does the plone integration for us: collective.js.datatables. Like all python packages you can find it on pypi: http://pypi.python.org/pypi/collective.js.datatables

We already added the add-on to our buildout, you just have to activate it in our template.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
      metal:use-macro="context/main_template/macros/master"
      i18n:domain="ploneconf.site">
<body>

<metal:head fill-slot="javascript_head_slot">
    <link rel="stylesheet" type="text/css" media="screen" href="++resource++jquery.datatables/media/css/jquery.dataTables.css">

    <script type="text/javascript" src="++resource++jquery.datatables.js"></script>
    <script type="text/javascript">
        $(document).ready(function(){
            var oTable = $('#talks').dataTable({
            });
        })
    </script>
</metal:head>

<metal:content-core fill-slot="content-core">

    <table class="" id="talks">
        <thead>
            <tr>
                <th>
                    Title
                </th>
                <th>
                    Speaker
                </th>
                <th>
                    Audience
                </th>
            </tr>
        </thead>
        <tbody>
            <tr tal:repeat="talk view/talks">
                <td>
                    <a href=""
                       tal:attributes="href talk/url;
                                       title talk/description"
                       tal:content="talk/title">
                       The 7 sins of plone-development
                    </a>
                </td>
                <td tal:content="talk/speaker">
                    Philip Bauer
                </td>
                <td tal:content="talk/audience">
                    Advanced
                </td>
            </tr>
            <tr tal:condition="not:view/talks">
                <td colspan=3>
                    No talks so far :-(
                </td>
            </tr>
        </tbody>
    </table>

</metal:content-core>
</body>
</html>

We don’t need the css class listing anymore since it might clash with datatables (it does not but still...).

La documentación de la biblioteca datatables está más allá de nuestro entrenamiento.

We use METAL again but this time to fill a different slot. The “javascript_head_slot” is part of the html’s <head> area in Plone and can be extended this way. We could also just put the code inline but having nicely ordered html is a good practice.

Hagamos una prueba: http://localhost:8080/Plone/talklistview

Nota

We add the jquery.datatables.js file directly to the HEAD slot of the HTML without using Plone JavaScript registry (portal_javascript). By using the registry you could enable merging of js files and advanced caching. A GenericSetup profile is included in the collective.js.datatables package.

Resumen

Hemos creado un bonito listado, que se puede llamar en cualquier lugar en el sitio web.