23. Tipos Dexterity - Parte II: Creciendo

Los tipos de contenidos talks existentes todavía le faltan algunas funcionalidades que queremos utilizar.

En esta parte vamos a tratar:

  • Añadir una interfaz marker para nuestro tipo de contenido talk.

  • Crear catálogo de índices personalizados.

  • Hacer consultas al catálogo a estos.

  • Permitir a algunas características más por defecto para nuestro tipo de contenido.

Agregar una interfaz marker para el tipo de contenido talk

Interfaces Marcador

El tipo de contenido Talk no es aun ciudadano de primera clase porque no implementa su propia interfaz. Las interfaces son como etiquetas de nombre, diciendo a otros elementos quien y que eres y que puedes hacer. Una interfaz marker es como una etiqueta. Los tipo de contenido talks actualmente tienen una interfaz marker auto generado en plone.dexterity.schema.generated.Plone_0_talk.

El problema es que el nombre de la instancia Plone es parte del nombre de la interfaz. si tu ahora mueves estos tipos a un sitio con otro nombre, el código que usa en estas interfaces no podrán ser capaces de encontrar estos objetos.

Para solventar esto agregamos un nuevo archivo interfaces.py:

1
2
3
4
5
6
from zope.interface import Interface


class ITalk(Interface):
    """Marker interface for Talks
    """

ITalk es una interfaz marker. Podemos anexar Vistas y Viewlets al contenido que provee estas interfaces. Ahora veremos como proveer esta interfaz. Hay dos soluciones para esto.

  1. Dejemos que sean las instancias de una clase que implementa esta interfaz.

  2. Registra esta interfaz como un comportamiento y habilite este en el tipo de contenido Talk.

La primera opción tiene un inconveniente importante: Sólo nuevas charlas pueden ser instancias de una nueva clase. Nos gustaría o podríamos tal vez tener que migrar a las charlas existentes o eliminarlas.

Así que vamos a registrar la interfaz como un comportamiento en el archivo behaviors/configure.zcml

<plone:behavior
  title="Talk"
  description="Marker interface for talks to be able to bind views to."
  provides="..interfaces.ITalk"
  />

Y habilite ese comportamiento en la definición de tipo de contenido en su configuración GenericSetup en el archivo profiles/default/types/talk.xml

1
2
3
4
5
6
<property name="behaviors">
 <element value="plone.app.dexterity.behaviors.metadata.IDublinCore"/>
 <element value="plone.app.content.interfaces.INameFromTitle"/>
 <element value="ploneconf.site.behaviors.social.ISocial"/>
 <element value="ploneconf.site.interfaces.ITalk"/>
</property>

Vuelva a instalar el complemento, aplicar el comportamiento a mano o ejecutar una paso de actualización GenericSetup (ver más abajo) y la interfaz estará allí.

Entonces podemos con seguridad unir la vista talkview a la nueva interfaz marker, agregando el siguiente código ZCML:

<browser:page
  name="talkview"
  for="ploneconf.site.interfaces.ITalk"
  layer="zope.interface.Interface"
  class=".views.TalkView"
  template="templates/talkview.pt"
  permission="zope2.View"
  />

Ahora la vista /talkview puede solamente se usada en objetos que implementan dicha interfaz. Ahora también podemos consultar el catálogo para los objetos que proporcionan esta interfaz catalog(object_provides="ploneconf.site.interfaces.ITalk"). Las vistas talklistview y demoview no reciben esta restricción, ya que no sólo son utilizados en los tipos de contenidos talks.

Nota

Sólo para completarlo, esto es lo que tendría que pasar por la primera opción:

  • Crear una nueva clase que herede desde plone.dexterity.content.Container e implementa la interfaz marker.

    from plone.dexterity.content import Container
    from ploneconf.site.interfaces import ITalk
    from zope.interface import implements
    
    class Talk(Container):
        implements(ITalk)
    
  • Modificar la clase para los nuevos tipos de contenidos talks en el archivo profiles/default/types/talk.xml

    1
    2
    3
    4
    5
    ...
    <property name="add_permission">cmf.AddPortalContent</property>
    <property name="klass">ploneconf.site.content.talk.Talk</property>
    <property name="behaviors">
    ...
    
  • Crear un paso de actualización para modificar la clase de tipo de contenido existente. Un código ejemplo de como hacer esto lo encuentra en el paquete ftw.upgrade.

Pasos de actualización

Cuando los proyectos se desarrollan a veces tendrá que modificar varias cosas mientras que el sitio ya este arriba, lleno de contenido y usuarios. Los pasos de actualización son piezas de código que se ejecutan al actualizar de una versión de un complemento a una más reciente. Ellos pueden hacer casi cualquier cosa.

Nosotros crearemos un paso de actualización que:

  • Ejecute el paso typeinfo (es decir, carga los almacenes de configuración GenericSetup en profiles/default/types.xml y profiles/default/types/... por lo que no tiene que volver a instalar el complemento para tener nuestros cambios hechos) y

  • haga una limpieza de algún contenido que pudiera estar esparcidos alrededor del sitio en las primeras etapas de su creación. Nos movemos todas los tipos de contenidos talks en una carpeta llamadas talks (a menos que ya están allí).

Los pasos de actualización son usualmente registrados en en su propio archivo ZCML. Cree el archivo upgrades.zcml, agregando el siguiente código ZCML:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<configure
  xmlns="http://namespaces.zope.org/zope"
  xmlns:i18n="http://namespaces.zope.org/i18n"
  xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
  i18n_domain="ploneconf.site">

  <genericsetup:upgradeStep
    title="Update and cleanup talks"
    description="Updates typeinfo and moves talks to a folder 'talks'"
    source="1"
    destination="1001"
    handler="ploneconf.site.upgrades.upgrade_site"
    sortkey="1"
    profile="ploneconf.site:default"
    />

</configure>

El paso de actualización cambia la versión número del perfil GenericSetup del complemento ploneconf.site de 1 a 1001. La versión se almacena en profiles/default/metadata.xml. Cámbielo a:

<version>1001</version>

Incluir el nuevo archivo upgrades.zcml en nuestro archivo configure.zcml, agregando el siguiente código ZCML:

<include file="upgrades.zcml" />

Ahora GenericSetup espera el código como un método upgrade_site en el archivo upgrades.py. Vamos a crearlo, agregando el siguiente código Python:

 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 plone import api
import logging

default_profile = 'profile-ploneconf.site:default'
logger = logging.getLogger('ploneconf.site')


def upgrade_site(setup):
    setup.runImportStepFromProfile(default_profile, 'typeinfo')
    catalog = api.portal.get_tool('portal_catalog')
    portal = api.portal.get()
    if 'the-event' not in portal:
        theevent = api.content.create(
            container=portal,
            type='Folder',
            id='the-event',
            title='The event')
    else:
        theevent = portal['the-event']
    if 'talks' not in theevent:
        talks = api.content.create(
            container=theevent,
            type='Folder',
            id='talks',
            title='Talks')
    else:
        talks = theevent['talks']
    talks_url = talks.absolute_url()
    brains = catalog(portal_type='talk')
    for brain in brains:
        if talks_url in brain.getURL():
            continue
        obj = brain.getObject()
        logger.info('Moving %s to %s' % (obj.absolute_url(), talks.absolute_url()))
        api.content.move(
            source=obj,
            target=talks,
            safe_id=True)

Después de iniciar de nuevo el sitio, nosotros podemos ejecutar el paso de actualización:

  • Valla al panel control Complementos en la dirección URL http://localhost:8080/Plone/prefs_install_products_form. Ahora debe haber una advertencia This add-on has been upgraded. Old profile version was 1. New profile version is 1001 y un botón al lado de él.

  • Ejecutar el paso de actualización haciendo clic en el.

En la consola debería ver mensajes de registro como este:

INFO ploneconf.site Moving http://localhost:8080/Plone/old-talk1 to http://localhost:8080/Plone/the-event/talks

Alternativamente, usted puede seleccionar los pasos de actualización para ejecutarlo de la siguiente manera:

  • En la ZMI valla a la herramienta portal_setup

  • Valla a la pestaña Upgrades

  • Seleccione ploneconf.site desde la lista desplegable y haga clic Choose profile

  • Ejecutar el paso de actualización.

Nota

Actualizando desde una versión anterior de Plone a uno más reciente también puede ejecutar los pasos de actualización del paquete plone.app.upgrade. Usted debe ser capaz de actualizar un sitio limpio de Plone 2.5 a Plone 5.0a2 con un solo clic.

Para un ejemplo ver el paso de actualización para Plone 5.0a1 https://github.com/plone/plone.app.upgrade/blob/master/plone/app/upgrade/v50/alphas.py#L23

Agregar una browserlayer

Una BrowserLayer es otro ejemplo de la interfaz marker. Las BrowserLayers nos permiten fácilmente habilitar y deshabilitar opiniones y funcionalidad de otro sitio basado en complementos y temas instalados.

Como queremos que las características que escribimos sólo estén disponible cuando el paquete ploneconf.site actualmente este instalado podemos asociarlos a una BrowserLayer.

En el archivo interfaces.py nosotros agregamos el siguiente código Python:

class IPloneconfSiteLayer(Interface):
    """Marker interface for the Browserlayer
    """

Nosotros registramos la BrowserLayer en GenericSetup en el archivo profiles/default/browserlayer.xml

<?xml version="1.0"?>
<layers>
  <layer name="ploneconf.site"
    interface="ploneconf.site.interfaces.IPloneconfSiteLayer" />
</layers>

Después de volver a instalar el complemento podemos obligar a las vistas talkview, demoview y talklistview a que sean parte de nuestra layer. Aquí hay un ejemplo usando la vista talkview.

<browser:page
  name="talklistview"
  for="ploneconf.site.interfaces.ITalk"
  layer="..interfaces.IPloneconfSiteLayer"
  class=".views.TalkListView"
  template="templates/talklistview.pt"
  permission="zope2.View"
  />

Tenga en cuenta el ruta relativa Python ..interfaces.IPloneconfSiteLayer. Es equivalente a la ruta absoluta ploneconf.site.interfaces.IPloneconfSiteLayer.

Ejercicio

¿Es necesario vincular el viewlet social desde el capítulo 20 de este nuevo Browser-Layer?

Solución

No, no haría ninguna diferencia desde el viewlet ya está asociada a la interfaz marker ploneconf.site.behaviors.social.ISocial.

Agregar índices del catálogo

En la vista talklistview nosotros tuvimos que despertar todos los objetos para acceder a algunos de sus atributos. Eso está bien si no tenemos muchos objetos y ellos son objetos Dexterity ligeros. Si tuviéramos miles de objetos que esto podría no ser una buena idea.

En lugar de cargar a todos en la memoria usaremos índices del catálogo para obtener los datos que queremos mostrar.

Agregar un nuevo archivo profiles/default/catalog.xml, con el siguiente código XML:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?xml version="1.0"?>
<object name="portal_catalog">
  <index name="type_of_talk" meta_type="FieldIndex">
    <indexed_attr value="type_of_talk"/>
  </index>
  <index name="speaker" meta_type="FieldIndex">
    <indexed_attr value="speaker"/>
  </index>
  <index name="audience" meta_type="KeywordIndex">
    <indexed_attr value="audience"/>
  </index>

  <column value="audience" />
  <column value="type_of_talk" />
  <column value="speaker" />
</object>

Esto agrega los nuevos índices para los tres campos que queremos mostrar en el listado. No es que audience es un KeywordIndex porque el campo es de varios valores, pero queremos una entrada de índice independiente para cada valor a un objeto.

La entrada column .. nos permite visualizar estos valores de estos índices en la vista tableview de colecciones.

Nota

Hasta Plone 4.3.2 agregar índices en el archivo catalog.xml era perjudicial porque ¡al volver a instalar el complemento purgaba los índices! Ver mas detalles en la dirección URL https://www.starzel.de/blog/a-reminder-about-catalog-indexes.

Para ejecutar código personalizado adicional al volver instalar un complemento que puedes usar un archivo setuphandler.py.

Consulta para índices personalizados

Los nuevos índices se comportan como los que Plone tiene construido en:

>>> (Pdb) from Products.CMFCore.utils import getToolByName
>>> (Pdb) catalog = getToolByName(self.context, 'portal_catalog')
>>> (Pdb) catalog(type_of_talk='Keynote')
[<Products.ZCatalog.Catalog.mybrains object at 0x10737b9a8>, <Products.ZCatalog.Catalog.mybrains object at 0x10737b9a8>]
>>> (Pdb) catalog(audience=('Advanced', 'Professionals'))
[<Products.ZCatalog.Catalog.mybrains object at 0x10737b870>, <Products.ZCatalog.Catalog.mybrains object at 0x10737b940>, <Products.ZCatalog.Catalog.mybrains object at 0x10737b9a8>]
>>> (Pdb) brain = catalog(type_of_talk='Keynote')[0]
>>> (Pdb) brain.speaker
u'David Glick'

Ahora podemos usar los nuevos índices para mejorar la vista talklistview así que no tenemos que despertar los objetos nunca más.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class TalkListView(BrowserView):
    """ A list of talks
    """

    def talks(self):
        results = []
        portal_catalog = getToolByName(self.context, 'portal_catalog')
        current_path = "/".join(self.context.getPhysicalPath())

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

La plantilla no necesita ser cambiado y el resultado no cambió también.

Agregar criterio de colección

Para ser capaz de buscar contenido en tipo de contenidos Colecciones utilizando los nuevos índices tendríamos que registrarlos como criterios para el widget de cadena de consulta que usan las Colecciones.

Agregar un nuevo archivo profiles/default/registry.xml, agregando el siguiente código XML:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<registry>
  <records interface="plone.app.querystring.interfaces.IQueryField"
           prefix="plone.app.querystring.field.audience">
    <value key="title">Audience</value>
    <value key="description">A custom speaker index</value>
    <value key="enabled">True</value>
    <value key="sortable">False</value>
    <value key="operations">
      <element>plone.app.querystring.operation.string.is</element>
    </value>
    <value key="group">Metadata</value>
  </records>
  <records interface="plone.app.querystring.interfaces.IQueryField"
           prefix="plone.app.querystring.field.type_of_talk">
    <value key="title">Type of Talk</value>
    <value key="description">A custom index</value>
    <value key="enabled">True</value>
    <value key="sortable">False</value>
    <value key="operations">
      <element>plone.app.querystring.operation.string.is</element>
    </value>
    <value key="group">Metadata</value>
  </records>
</registry>

Añadir más características a través de GenericSetup

Habilitar el control de versiones y una visión del diff para los tipos de contenidos talks a través de GenericSetup.

Agregar nuevo archivo profiles/default/repositorytool.xml, con el siguiente código XML:

1
2
3
4
5
6
7
8
9
<?xml version="1.0"?>
<repositorytool>
  <policymap>
    <type name="talk">
      <policy name="at_edit_autoversion"/>
      <policy name="version_on_revert"/>
    </type>
  </policymap>
</repositorytool>

Agregar nuevo archivo profiles/default/diff_tool.xml, con el siguiente código XML:

1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<object>
  <difftypes>
    <type portal_type="talk">
      <field name="any" difftype="Compound Diff for Dexterity types"/>
    </type>
  </difftypes>
</object>