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 multiple 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 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
1 2 3 4 5 6 7 8 | <configure
...>
...
<include package=".behavior" />
...
</configure>
|
Luego, cree el archivo behavior/configure.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 marcador
Hay una fabrica
La fábrica es una clase que proporciona la lógica de comportamiento y da acceso a los atributos que ofrecemos. Las fábricas 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 marcador de los objetos proporcionando mi comportamiento, yo debo proporcionar uno, para esto usamos el atributo marcador. Mi objeto implementa IVotable y debido a esto, podemos escribir vistas y viewlets sólo para este tipo de contenido.
Las interfaces necesitan ser escritas, en nuestro caso en un archivo interfaces.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 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 viewlets y browser views. Permite agregar aquí mismo. La interfaz IVotable es la interfaz marcador simple. Sólo se utiliza para enlazar las 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 nosotros anotar esta información adicional para que los auto-formularios renderizados pueden 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 mínimos esquema 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 charla 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 viewlets.
La última línea se asegura de que los campos de esquema son conocidos por otros paquetes. Siempre que algún código quiere que 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 behavior/voting.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 | # 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 clave específica.
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 única propiedad, ya que no nos definimos manipuladores de escritura. Nosotros no los necesitamos para que no nos agregarlos.
Como se ha visto en la declaración de 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 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.