18. Vistas - Parte III: Una lista de Charlas

Ahora no queremos 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

Digamos que queremos mostrar una lista de todas las charlas que se presentaron durante la conferencia. Sólo podemos ir a la carpeta y seleccione un método de visualización que nos convenga. Pero ninguno lo hace porque queremos para mostrar al público objetivo en nuestro listado.

Así que tenemos que conseguir todas las charlas. Para ello utilizamos la vista de la clase Python para consultar en el catálogo las charlas.

El catálogo es como un motor de búsqueda por el contenido de nuestro sitio. Contiene información sobre todos los objetos, así como algunos de sus atributos como título, descripción, desde estado de flujo de trabajo, palabras clave que fueron etiquetadas, autor, tipo de contenido, su ruta en el sitio, etc. Sin embargo, no se mantiene el contenido de campos “pesado” como imágenes o archivos, campos texto enriquecidos y el campo que acabamos de definir nosotros mismos.

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.

El archivo 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"
   />

El archivo 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 íbamos a obtener todas las charlas en todo el sitio. Si nos trasladamos algunas charlas a una parte diferente del sitio (por ejemplo, un sub-conferencia para universidades con una especial lista charla) puede ser que no quiera ver así en nuestra lista.

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

Los objetos normalmente no se cargan en la memoria, pero permanecen latentes en el base de datos ZODB. Despertar objetos hasta puede ser lento, especialmente si usted está despertando una gran cantidad de objetos. Afortunadamente nuestros tipos de contenidos no son especialmente pesados, ya que son:

  • objetos dexterity que son más ligeros que sus hermanos archetypes

  • relativamente pocos, ya que no tenemos miles de charlas en nuestra conferencia

Queremos mostrar la audiencia destinada de la charla, pero esos atributos de las charlas no está en el catálogo. Esto es por qué necesitamos llegar a los objetos mismos.

También podríamos agregar un nuevo índice al catálogo que agregará ‘audience’ a las propiedades de los cerebros. Tenemos que ponderar los pros y contras:

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

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

Nota

El código para agregar este índice se vería como el siguiente:

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 fuente anterior en un archivo llamado indexers.py

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

Vamos a agregar algunos índices más adelante.

¿Por qué utilizar el catálogo en absoluto?, comprueba los permisos, y sólo devuelve las charlas que el usuario actual puede ver. Puede ser que sean privados u ocultos a usted, ya que son parte de una súper conferencia secreta para los desarrolladores del núcleo de Plone (¡no hay tal cosa!).

La mayoría de los objetos en Plone son como diccionarios, por lo que podían hacer context.values() para obtener todos sus contenidos.

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: La 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 ‘Indexado en todo el contenido en el sitio’.

La Adquisición puede ser nocivo. Los cerebros no tienen el atributo ‘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

Hay 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')
[]

Llamando al catálogo sin parámetros devuelven todo el sitio:

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

Ejercicios

Ya que ahora saben cómo consultar el catálogo es el momento para un poco de ejercicio.

Ejercicio 1

Agregue un método get_news a TalkListView que devuelve una lista de los cerebros de todos los elementos de Noticias que se publican y ordenarlos en el orden de su publicación fecha.

Solución

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',
    )

Ejercicio 2

Agregar un método que regresa todas los tipos de contenidos Talk de tipo keynotes publicadas como objetos.

Solución

 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

A continuación, crear una plantilla en la usa los resultados del método ‘talks’.

Trate de mantener la lógica mayormente en Python. Esto es por dos razones:

Legibilidad:

Es mucho más fácil de leer Python que complejas estructuras de TAL.

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.

Don’t Repeat Yourself (DRY):

En Python se puede volver a utilizar métodos y refactorizar código fácilmente. Refactorizando TAL generalmente significa tener que hacer grandes cambios en la estructura HTML que se traduce en diffs incomprensible.

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

Modelo:

el objeto

Vista:

la plantilla

Controlador:

la vista

La vista y el controlador están muy mezclados en Plone. Especialmente cuando nos fijamos en algunos de los códigos más antiguos de Plone verás que la política de mantener la lógica en Python y representación en las plantillas no siempre se cumple.

¡Pero usted debe, no obstante, que lo haga! Usted va a terminar con más que suficiente lógica en las plantillas de todos modos.

Agregar esta sencilla tabla al archivo 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>

Después usted lo transforma dentro de una lista:

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

Hay algunas cosas que necesitan explicación:

tal:repeat="talk view/talks"

Este itera sobre la lista de diccionarios devueltos por la vista. La vista view/talks llama al método talks de nuestro vista y cada talk es a su vez regresa uno de los diccionarios que son devueltos por este método. Desde las expresiones de ruta TAL para la búsqueda de los valores en los diccionarios es la misma que los atributos de los objetos se puede escribir talk/somekey como pudimos view/somemethod. Práctico, pero a veces irritante ya de mirar la page template solos a menudo no tenemos forma de saber si algo es un atributo, un método o el valor de un diccionario. Sería una buena práctica para escribir 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"

Esta es una alternativa en caso de no se devuelven las charlas. A continuación, devolver una lista vacía (¿recuerda results = []?)

Ejercicio

Modificar la vista para solamente expresiones Python.

Solución

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

Para seguir el principio “don’t repeat yourself” nosotros definimos talks vez de llamar al método dos veces.

Configuración de una vista personalizada como vista por defecto en un objeto

No queremos tener siempre que a anexar /@@talklistview a nuestra carpeta para obtener la vista. Hay una manera muy fácil de configurar la vista de la carpeta con el ZMI.

Si agregamos /manage_propertiesForm podemos establecer la propiedad “layout” para la vista talklistview.

Para hacer vistas configurables para que los editores pueden elegir entonces tenemos que registrar la vista para el tipo de contenido que nos ocupa, en que es FTI. Para activar si todas las carpetas tiene esta vista se añade un nuevo archivo profiles/default/types/Folder.xml

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

Después de volver a aplicar el perfil typeinfo del complemento (o, simplemente, volver a instalarlo) el tipo de contenido “Carpeta” se amplía con nuestro método de vista adicional y aparece en la lista desplegable Mostrar.

La opción purge="False" anexa a la vista de los ya existentes en lugar de reemplazarlos.

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

Aquí se utiliza una de las muchas agradables característica construida en Plone. La class=”listing” da la tabla un buen estilo y hace que la tabla se puede ordenar con un poco de Javascript.

Pero podríamos mejorar esa tabla aún más mediante el uso de una buena librería Javascript llamada “datatables”. Incluso podría llegar a ser parte del core de Plone en algún momento.

Al igual que para muchas librerías Javascript, ya hay un paquete que hace la integración de Plone por nosotros: collective.js.datatables. Al igual que todos los paquetes Python usted lo puede encontrar en PYPI: https://pypi.python.org/pypi/collective.js.datatables

Ya hemos añadido el complemento a nuestra buildout, sólo tienes que activarlo en nuestra plantilla.

 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="listing" 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>

Nosotros no necesitamos la clase CSS listing nunca más, ya que podría entrar en conflicto con la librería datatables (no es así, pero aún así ...).

La documentación de la librería datatables está más allá de nuestro entrenamiento.

Utilizamos METAL de nuevo pero esta vez para llenar un slot diferente. El “javascript_head_slot” es parte de área <head> del HTML en Plone y se puede ampliar de esta manera. También podríamos simplemente poner el código fuente en línea, pero con HTML muy bien ordenado es una buena práctica.

Hagamos una prueba con la siguiente dirección URL: http://localhost:8080/Plone/talklistview

Nota

Añadimos el archivo jquery.datatables.js directamente a la ranura HEAD del HTML sin utilizar un registro en la herramienta Plone JavaScript registry (portal_javascript). Al utilizar el registro podría permitir la fusión de los archivos js y almacenamiento en caché avanzado. Un perfil GenericSetup está incluido en el paquete collective.js.datatables.

Resumen

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