Formularios en Plone
Tutorial sobre formularios en zope / plone.
No siempre basta con los formularios base que acompañan a los archetypes. Muchas veces queremos hacer formularios para colocarlos en el panel de control, por ejemplo, o para hacer determinadas acciones complejas de manipulación de objetos.
Este pequeño tutorial trata el triplete Controller Page Template, Controller Validator y Controller Python Script. Este mecanismo procede del producto CMF llamado CMFFormController e incluido en Plone.
Antes de empezar existe una mejor documentación en inglés en el directorio www del producto CMFFormController (docs.stx). Este documento no es una traducción de dicho documento, sólo son unas notas básicas sobre el funcionamiento. Al final también he dejado algunas referencias a documentación en internet.
Ejemplo de funcionamiento
El ejemplo que se va a mostrar consiste en un formulario que creé hace tiempo para gestionar la creación de objetos asignatura de mi sitio Plone. La idea es crear un formulario donde un usuario introduce el título de la asignatura que desea añadir en el sistema, Plone comprueba antes de crear el objeto asignatura: (1) el título no se ha dejado en blanco (2) existen asignaturas coincidentes en titulo.
En la figura se resume el funcionamiento donde se vuelve al formulario para listar las coincidencias siempre que el título de la asignatura coincida con el de otras. Finalmente si el título no está vacío y no hay coincidencias se crea la nueva asignatura. En la parte inferior de cada cuadro de color he indicado el nombre del fichero correspondiente.
Crear el formulario
El primer paso es crear el fichero de plantilla con el formulario. La extensión de dicho fichero debe ser .cpt, así se consigue que zope la interprete como un objeto del tipo Controller Page Template. Este tipo de objetos son una plantilla normal de zope pero con una serie de opciones extra, las cuales, se especificarán desde el fichero .metadata que se debe crear en el disco duro con el mismo nombre.
CMFFormController añade una variable denominada options a partir de la cual se trasvasa la información del estado del formulario (errores, mensajes, datos, etc.). Por ejemplo, los errores se devuelven al formulario en un diccionario al que se accede en la plantilla con: options/state/getErrors. Este diccionario contiene una entrada por cada campo del formulario que tenga error.
tal:define="errors options/state/getErrors"
El siguiente aspecto son los nombres de los botones. Si hay varios debemos darles nombres para controlar el funcionamiento según el que se pulse, deben llamarse: form.button.xxx
<input class="context" name="form.button.search" type="submit" value="Siguiente" /> <input class="context" name="form.button.create" type="submit" value="Crear asignatura" />
Muestro el ejemplo completo de la plantilla: create_subject_form.cpt
<form tal:define="errors options/state/getErrors"
tal:attributes="action string:${here/absolute_url}/${template/id};"
method="post">
<div class="formControls">
<input type="hidden" name="form.submitted" value="1" />
<input type="text" name="Title"
size="30"
tabindex=""
tal:attributes="tabindex tabindex/next;"
value="Teclee nombre de la asignatura"
/>
<input class="context" name="form.button.search" type="submit" value="Siguiente" />
<input class="context" name="form.button.create" type="submit" value="Crear asignatura" />
</div>
</form>
Nota: Es imprescindible incluir el siguiente control oculto para el correcto funcionamiento tal y como indican en [1].
<input type="hidden" name="form.submitted" value="1" />
Validación
Cuando se pulsa alguno de los botones se llama automáticamente a los validadores. Éstos son realmente scripts en python que harán las comprobaciones sobre los campos y devolverán si el campo en válido o no. En este simple ejemplo compruebo la existencia de otra asignatura con el mismo nombre o parecido realizando una búsqueda en el portal por título. Los validadores se indican en el archivo de metadatos, es decir, al llamarse el fichero create_subject_form.cpt, hay que crear un fichero de texto con el nombre create_subject_form.cpt.metadata con el contenido:
[validators] validators..search = validate_subject_title, validate_noother_subjects validators..create = validate_subject_title
donde validate_subject_title y validate_noother_subjects serán dos scripts python existentes en el disco duro con una peculiaridad, deben tener la extensión .vpy. Estos validadores comprobarán si el título no está vacío y si existen otras asignaturas con nombres parecidos en el sistema respectivamente.
El validador contiene una variable state para procesar el formulario, la cual se cambiará y se pasará al siguiente script o plantilla. Las operaciones más comunes sobre esta variable son:
- state.setError(campo,cadena): Establece el error para un determinado campo.
- state.getErrors(): Obtiene los errores almacenados en state
- state.set(status='failure'): por defecto status está establecido a success, pero podemos establecer cualquier estado que deseemos.
- state.set(portal_status_message='Error en el formulario'): Mensaje pasado a Plone
El código de validate_noother_subjects.vpy es:
## Controller Script Python "validate_subject_title"
##bind container=container
##bind context=context
##bind namespace=
##bind script=script
##bind state=state
##bind subpath=traverse_subpath
##parameters=title
##title=validates the email adresses
from Products.CMFPlone import PloneMessageFactory as _
plone_utils=context.plone_utils
dest_folder_str=context.portal_properties.uspdi.pdi_folder
dest_folder=context.restrictedTraverse(dest_folder_str)
contentFilter={
'portal_type':'Asignatura',
'Title':title
}
objs=dest_folder.queryCatalog(contentFilter);
if objs:
objs = [i.getObject() for i in objs]
state.set(matchs=objs)
state.set(status='matchs')
return state
Tras la ejecución de los validadores la variable state contiene un valor en status, de forma que, en el fichero .medatada anterior se debe indicar el fichero hacia al que se traspasa la ejecución de la siguiente forma:
Nuevo contenido contenido del fichero create_subject_form.cpt.metadata
[validators] validators..search = validate_subject_title, validate_noother_subjects validators..create = validate_subject_title [actions] action.failure = traverse_to:string:create_subject_form action.success..create = traverse_to:string:create_subject action.success..search = traverse_to:string:create_subject action.matchs..search = traverse_to:string:create_subject_form
La sección [actions] permite especificar una acción en función del resultados y el botón que se ha pulsado (create ó search). En caso de fallo (action.failure) se vuelve al mismo formulario el cual mostrará los errores si se le hacen los arreglos mostrados a continuación. Si hay coincidencias con otras asignaturas, el validador rellena la variable matchs y establece el estado a macth y se vuelve al formulario donde se podrá leer esta variable.
Retocando el formulario para mostrar los errores y leer el resultado de la validación
Cuando ocurren errores en un formulario de Plone, se observa que ilumina en un color rojo aquel campo que es erróneo y acompaña con un mensaje de texto indicando la causa de error. dando un vistazo a los formularios de Plone, se puede hacer algo simular retocando el formulario anterior siguiendo el siguiente procedimiento:
- Rodeamos cada campo de una etiqueta <div>
- Dicha etiqueta cambiará su clase a class=error si el campo correspondiente tiene un error, esto se comprueba mediante el diccionario de errores explicado anteriormente.
En el listado completo de la plantilla create_subject_form.cpt se ha resaltado dos significativos en negrita (detección de error en un campo y lectura de resultados devueltos por el validador)
<metal:fill fill-slot="main"
tal:define="matchs options/state/kwargs/matchs|nothing">
<h1>Creación de nueva asignatura</h1>
<p>
Este sitio Web contiene páginas del personal Docente e Investigador y páginas de asignaturas
Utilice los formularios para buscar un profesor o asignatura.
</p>
<form tal:define="errors options/state/getErrors"
tal:attributes="action string:${here/absolute_url}/${template/id};"
method="post">
<div class="formControls">
<input type="hidden" name="form.submitted" value="1" />
<div class="row"
tal:define="error errors/title| nothing;
title request/title | nothing;"
tal:attributes="class python:error and 'field error' or 'field'">
<label for="title">Nombre de la asignatura</label>
<div class="formHelp">Introduzca el nombre completo de la asignatura</div>
<div tal:content="error">Validation error output</div>
<input type="text" name="title" size="30" tabindex="" value=""
tal:attributes="tabindex tabindex/next;
value title;"/>
</div>
<input class="context" name="form.button.search" type="submit" value="Siguiente" />
<input tal:condition="matchs"
class="context" name="form.button.create" type="submit" value="Crear asignatura" />
</div>
</form>
<tal:matchs tal:condition="matchs">
<h2>Coincidencias de nombres de asignaturas</h2>
<form action="join_to_subject">
<table class="listing">
<thead>
<th>Asignatura</th><th>Titulación</th><th>Profesores</th><th> </th>
</thead>
<tbody>
<tr tal:repeat="item matchs">
<td>
<img src="" tal:attributes="src string:$portal_url/asignatura.png" alt="icono de asignatura" />
<a href="" tal:content="item/Title"
tal:attributes="href item/absolute_url">Asignatura</a>
</td>
<td tal:content="item/getTitulacion"></td>
<td>
<span tal:repeat="prof python:item.getBRefs('pdiasignaturas')" tal:content="prof/Title"/>
</td>
<td> <input class="context" name="" type="submit" value="Incorporarse"
tal:attributes="name item/id" /> </td>
</tr>
</tbody>
</table>
</form>
</tal:matchs>
</metal:fill>
Script final
Cuando el formulario tiene éxito se ejecuta el script create_subject. Según la extensión que tenga el fichero (.py ó .cpy) será un simple script de pyhton o un Controller Script respectivamente. En el segundo de los casos se dispondrá en el script de la variable state y se podrán escribir validadores y acciones para el script. Si el script necesita acceder a la varibale state o realizar diferentes acciones según el botón pulsado, necesitaríamos un fichero .cpy con sus metadatos. En mi caso ya no era necesaria más ejecución de este tipo, finalmente, el script crea una asignatura en una determinada carpeta independientemente del botón pulsado.
El código de create_subject.py es el siguiente:
## Script (Python) "create_subject"
##bind container=container
##bind context=context
##bind namespace=
##bind script=script
##bind subpath=traverse_subpath
##parameters=title=None
##title=Crea una nueva asignatura
##
from Products.CMFPlone import PloneMessageFactory as _
util = context.plone_utils
dest_folder_str=context.portal_properties.uspdi.subjects_folder
dest_folder=context.restrictedTraverse(dest_folder_str)
new_id=title.strip().lower()
dest_folder.invokeFactory('Asignatura',new_id)
asig=getattr(dest_folder.aq_explicit,new_id)
if asig:
asig.setTitle(title)
asig.unmarkCreationFlag()
asig.reindexObject()
util.addPortalMessage(_(u'Asignatura creada satisfactoriamente'))
return context.REQUEST.RESPONSE.redirect(asig.absolute_url()+"/edit")
else:
util.addPortalMessage(_(u'No se ha podido crear la asignatura'),'error')
return context()


Fvaor