31. Más comportamientos complejos

Usando Anotaciones

Vamos a almacenar la información en una anotación. No porque se necesita, sino porque se encuentra el código que utiliza anotaciones y la necesidad de entender las implicaciones.

Annotations en Zope / Plone significa que los datos no serán almacenados directamente en un objeto, sino de una manera indirecta y con multiples espacios de nombres para que varios paquetes pueden almacenar información bajo el mismo atributo, sin colisionar.

Así que usando anotaciones evita los conflictos de nombres. El costo es una indirección. El diccionario es persistente por lo que tiene que ser almacenado por separado. Además, se podría dar atributos un nombre que contiene un prefijo de espacio de nombres para evitar colisiones de nombres.

Usando Esquema

El atributo donde almacenamos nuestros datos será declarado como un campo de esquema. Marcamos el campo como un campo omitido, porque no vamos a crear widgets del paquete z3c.form para mostrarlos. Nosotros ofrecemos un esquema, porque muchos otros paquetes utilizan la información de esquema para obtener el conocimiento de los campos pertinentes.

Por ejemplo, cuando los archivos se han migrado a blobs, los nuevos objetos tuvieron que ser creados y cada campo de esquema fue copiado. El código no puede saber acerca de nuestro campo, excepto si proporcionamos información de esquema.

Escribiendo código

Para iniciar, creamos un directorio behavior con un archivo vació behavior/__init__.py.

Luego debemos, como siempre, registrar nuestro ZCML.

Primero, agrega la información de que existe otro archivo ZCML en configure.zcml, con el siguiente código ZCML:

1
2
3
4
5
6
7
8
<configure
      ...>

  ...
  <include package=".behavior" />
  ...

</configure>

Luego, cree el archivo behavior/configure.zcml, agregando el siguiente código ZCML:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<configure
    xmlns="http://namespaces.zope.org/zope"
    xmlns:plone="http://namespaces.plone.org/plone">

  <plone:behavior
      title="Voting"
      description="Allow voting for an item"
      provides="starzel.votable_behavior.interfaces.IVoting"
      factory=".voting.Vote"
      marker="starzel.votable_behavior.interfaces.IVotable"
      />

</configure>

Hay algunas diferencias importantes de nuestro primer comportamiento:

  • Hay una interfaz marker.

  • Hay una factory.

La factory es una clase que proporciona la lógica de comportamiento y da acceso a los atributos que ofrecemos. Las factories en la tierra de Plone / Zope se recuperan mediante la adaptación de un objeto con una interfaz. Si usted quiere su comportamiento, usted escribiría IVoting(object)

Pero para que esto funcione, el objeto no puede estar implementando la interfaz IVoting, porque si lo haría, ¡ IVoting(object) devolvería el objeto en sí mismo!. Si yo necesito una interfaz marker de los objetos proporcionando mi comportamiento, yo debo proporcionar uno, para esto usamos el atributo marker. Mi objeto implementa IVotable y debido a esto, podemos escribir views y viewlets sólo para este tipo de contenido.

Las interfaces necesitan ser escritas, en nuestro caso en un archivo interfaces.py, 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# encoding=utf-8
from plone import api
from plone.autoform import directives as form
from plone.autoform.interfaces import IFormFieldProvider
from plone.supermodel import model
from plone.supermodel import directives
from zope import schema
from zope.interface import alsoProvides
from zope.interface import Interface

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

# Ivotable is the marker interface for contenttypes who support this behavior
class IVotable(Interface):
    pass

# This is the behaviors interface. When doing IVoting(object),you receive an
# adapter
class IVoting(model.Schema):
    if not api.env.debug_mode():
        form.omitted("votes")
        form.omitted("voted")

    directives.fieldset(
        'debug',
        label=u'debug',
        fields=('votes', 'voted'),
    )

    votes = schema.Dict(title=u"Vote info",
                        key_type=schema.TextLine(title=u"Voted number"),
                        value_type=schema.Int(title=u"Voted so often"),
                        required=False)
    voted = schema.List(title=u"Vote hashes",
                        value_type=schema.TextLine(),
                        required=False)

    def vote(request):
        """
        Store the vote information, store the request hash to ensure
        that the user does not vote twice
        """

    def average_vote():
        """
        Return the average voting for an item
        """

    def has_votes():
        """
        Return whether anybody ever voted for this item
        """

    def already_voted(request):
        """
        Return the information wether a person already voted.
        This is not very high level and can be tricked out easily
        """

    def clear():
        """
        Clear the votes. Should only be called by admins
        """

alsoProvides(IVoting, IFormFieldProvider)

Se trata de una gran cantidad de código. El IVotableLayer nosotros lo necesitaremos más tarde para las viewlets y los browser views. Permite agregar aquí mismo. La interfaz IVotable es la interfaz marker simple. Sólo se utiliza para enlazar los browser views y viewlets a tipos de contenido que proporcionan nuestro comportamiento, por lo que no hay código necesario.

La clase IVoting es más compleja, como se puede ver. Mientras IVoting es sólo una interfaz, utilizamos plone.supermodel.model.Schema para las características avanzadas Dexterity. El paquete zope.schema no proporciona medios para ocultar campos. La directivas form.omitted de plone.autoform nos permite a nosotros anotar esta información adicional para que los auto-formularios renderizados puedan utilizar la información adicional.

Hacemos esta omitir condicional. Si ejecutamos Plone en modo de depuración, seremos capaces de ver los datos internos en el formulario de edición.

Creamos los campos esquema mínimos para nuestras estructuras de datos internas. Por una pequeña prueba, yo le quité las directivas omitida de formulario y abrí la vista de edición de un tipo de contenido talks que utiliza el comportamiento. Después de ver la fealdad, yo decidí que debía proporcionar al menos mínimo de información. Los títulos y requerido son puramente opcional, pero muy útil si no se omitirán los campos, algo que puede ser útil al depurar el comportamiento. Más tarde, cuando ponemos en práctica el comportamiento, los atributos votes y voted se apliquen de tal manera que no se puede simplemente modificar estos campos, que son de sólo lectura.

Luego definimos la API que vamos a usar en los browser views y las viewlets.

La última línea se asegura de que los campos esquema sean conocidos por otros paquetes. Siempre que algún código quiere todos los esquemas de un objeto, que recibe el esquema definido directamente sobre el objeto y los esquemas adicional. Los esquemas adicionales se compilan mediante la búsqueda de comportamientos y si ofrecen la funcionalidad IFormFieldProvider. Sólo entonces nuestros campos son conocidos como campos de esquema.

Ahora la única cosa que falta es el comportamiento, la cual debemos colocar en archivo behavior/voting.py, con 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
# encoding=utf-8
from hashlib import md5
from persistent.dict import PersistentDict
from persistent.list import PersistentList
from zope.annotation.interfaces import IAnnotations

KEY = "starzel.votable_behavior.behavior.voting.Vote"


class Vote(object):
    def __init__(self, context):
        self.context = context
        annotations = IAnnotations(context)
        if KEY not in annotations.keys():
            annotations[KEY] = PersistentDict({
                "voted": PersistentList(),
                'votes': PersistentDict()
                })
        self.annotations = annotations[KEY]

    @property
    def votes(self):
        return self.annotations['votes']

    @property
    def voted(self):
        return self.annotations['voted']

En nuestro método __init__ obtenemos las anotaciones del objeto. Buscamos los datos con una específica clave.

La clave en este ejemplo es el mismo que lo que obtendría con __name__+Vote.__name__. Pero no vamos a crear un nombre dinámico, esto sería muy inteligente y hábil es malo.

Al declarar un nombre estático, no vamos a tener problemas si reestructuramos el código.

Usted puede ver, que inicializamos los datos si no existe. Trabajamos con PersistentDict y PersistentList. Para entender por qué hacemos esto, es importante entender cómo funciona el ZODB.

Ver también

El ZODB puede almacenar objetos. Tiene un objeto raíz especial que usted nunca toca. Cualquier cosa que usted almacena donde, formará parte del objeto raíz, excepto si se trata de un sublclassing objeto persistent.Persistent. Entonces se almacenará de forma independiente.

Tenga cuenta que los objetos persistentes Zope/ZODB cuando se cambia un atributo en él y marcar como cambiado. Los objetos modificados se guardarán en la base de datos. Esto sucede automáticamente. Cada request inicia una transacción y después de nuestro código corrió y el servidor Zope está preparando para enviar de nuevo la respuesta que nosotros generamos, la transacción sera enviada y todo lo cambiamos, será salvo.

Ahora, si tienen un diccionario normal sobre un objeto persistente, y usted sólo va a cambiar el diccionario, el objeto persistente no tiene manera de saber, si el diccionario se ha cambiado. Esto sucede de vez en cuando.

Así que una solución es cambiar el atributo especial _p_changed a True en el objeto persistente, o utilizar un PersistentDict. Eso es lo que estamos haciendo aquí.

Puede encontrar más información en la documentación de la ZODB, en particular, Reglas para Clases Persistentes

A continuación ofrecemos los campos internos a través de propiedades. El uso de este formulario de propiedad, hace que lean únicamente la propiedad, ya que no nos definimos manipuladores de escritura. Nosotros no los necesitamos entonces para que nosotros no los agregaremos.

Como se ha visto en la declaración del esquema, si ejecuta su sitio en modo de depuración, verá un campo de edición para estos campos. Pero si tratar de cambiar estos campos abra una excepción.

Continuemos con este archivo:

 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
    def _hash(self, request):
        """
        This hash can be tricked out by changing IP addresses and might allow
        only a single person of a big company to vote
        """
        hash_ = md5()
        hash_.update(request.getClientAddr())
        for key in ["User-Agent", "Accept-Language",
                    "Accept-Encoding"]:
            hash_.update(request.getHeader(key))
        return hash_.hexdigest()

    def vote(self, vote, request):
        if self.already_voted(request):
            raise KeyError("You may not vote twice")
        vote = int(vote)
        self.annotations['voted'].append(self._hash(request))
        votes = self.annotations['votes']
        if vote not in votes:
            votes[vote] = 1
        else:
            votes[vote] += 1

    def average_vote(self):
        if not has_votes(self):
            return 0
        total_votes = sum(self.annotations['votes'].values())
        total_points = sum([vote * count for (vote, count) in
                            self.annotations['votes'].items()])
        return float(total_points) / total_votes

    def has_votes(self):
        return len(self.annotations.get('votes', [])) != 0

    def already_voted(self, request):
        return self._hash(request) in self.annotations['voted']

    def clear(self):
        annotations = IAnnotations(self.context)
        annotations[KEY] = PersistentDict({'voted': PersistentList(),
                                           'votes': PersistentDict()})
        self.annotations = annotations[KEY]

Empezamos con un poco del método helper que no está expuesta a través de la interfaz. No queremos que la gente vote dos veces. Hay muchas formas para asegurar esto y cada uno tiene defectos.

Elegimos esta manera de mostrar cómo acceder a la información de la request del navegador del usuario que nos envió. En primer lugar, tenemos la dirección IP del usuario, entonces podemos acceder a un pequeño conjunto de encabezados desde el navegador de los usuarios y generar una suma de comprobación MD5 de esto.

El método de votación, quiere un voto y una request. Comprobamos las condiciones previas, a continuación, convertimos el voto a un número entero, almacenar la request que tiene voted y los votos en el diccionario votes. Sólo contamos allí, con qué frecuencia se ha dado ninguna votación.

Todo lo demás es simplemente código Python aburrido.