Formularios y Validadores

Existen cuatros maneras distintas de construir formularios en web2py:

  • FORM provee una implementación de bajo nivel en términos de ayudantes HTML. Un objeto FORM puede serializarse en HTML y está al tanto de los campos que contiene. Un objeto FORM sabe como validar los valores que se han agregado al formulario.
  • SQLFORM provee una API de alto nivel para construir los formularios crear, actualizar y borrar desde una tabla de base de datos existente.
  • SQLFORM.factory es una capa de abstracción en el tope de SQLFORM con el fin de tomar ventaja de las características de la generación de formularios aún cuando no exista una base de datos. Esta genera un formulario muy similar al formulario SQLFORM a partir de la descripción de una tabla pero sin la necesidad de crear la tabla de base de datos.
  • Métodos CRUD. Estos son funcionalmente equivalentes a SQLFORM y se basan en SQLFORM, pero proveen una notación más compacta.

Todas estos formularios están consientes de si mismas y, si una entrada no pasa la validación, ellas pueden modificarse a si mismas y agregar mensajes de error. Los formularios pueden ser solicitados en búsqueda de las variables validadas y los mensajes de error que pueden ser generados por la validación.

Código arbitrario HTML puede insertarse o extraerse del formulario usando ayudantes.

FORM

Considere como ejemplo una aplicación test con el siguiente controlador “default.py”:

1
2
def display_form():
    return dict()

Y la vista asociada “default/display_form.html”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{{extend 'layout.html'}}
<h2>Input form</h2>
<form enctype="multipart/form-data"
      action="{{=URL()}}" method="post">
Your name:
<input name="name" />
<input type="submit" />
</form>
<h2>Submitted variables</h2>
{{=BEAUTIFY(request.vars)}}

Este es un formulario regular HTML que pide el nombre del usuario. Cuando se llena el formulario y se le da click al botón de enviar (submit), el formulario se auto envía, y la variable request.vars.name y su valor es mostrado al final.

La misma formulario puede generarse usando ayudantes. Esto puede hacerse tanto en la vista como en la acción. Como web2py procesó el formulario en la acción, está bien definir el formulario en la acción.

He aquí el nuevo controlador:

1
2
3
def display_form():
   form=FORM('Your name:', INPUT(_name='name'), INPUT(_type='submit'))
   return dict(form=form)

Y la vista asociada “default/display_form.html”:

1
2
3
4
5
{{extend 'layout.html'}}
<h2>Input form</h2>
{{=form}}
<h2>Submitted variables</h2>
{{=BEAUTIFY(request.vars)}}

Hasta el momento el código es equivalente al código previo, pero el formulario se genera por la declaración {{=form}} la cual serializa el objeto FORM.

Ahora se le agrega un nivel de complejidad agregando validación y procesamiento del formulario.

Se cambia el controlador como sigue:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def display_form():
    form=FORM('Your name:',
              INPUT(_name='name', requires=IS_NOT_EMPTY()),
              INPUT(_type='submit'))
    if form.accepts(request.vars, session):
        response.flash = 'form accepted'
    elif form.errors:
        response.flash = 'form has errors'
    else:
        response.flash = 'please fill the form'
    return dict(form=form)

Y la vista asociada “default/display_form.html”:

1
2
3
4
5
6
7
8
9
{{extend 'layout.html'}}
<h2>Input form</h2>
{{=form}}
<h2>Submitted variables</h2>
{{=BEAUTIFY(request.vars)}}
<h2>Accepted variables</h2>
{{=BEAUTIFY(form.vars)}}
<h2>Errors in form</h2>
{{=BEAUTIFY(form.errors)}}

Observe que:

  • En la acción, se agregó el validador requires=IS_NOT_EMPTY() para el campo de entrada “name”.
  • En la acción, se agregó una llamada a form.accepts(...)
  • En la vista, se está imprimiendo form.vars y form.errors así como el formulario y request.vars.

Todo el trabajo lo hace el método accepts del objeto form. Éste filtra request.vars de acuerdo a los requerimientos declarados (expresados por los validadores). accepts guarda esas variables que pasan la validación en form.vars. Si un valor de un campo no cumple un requerimiento , el validador que falla devuelve un error y ese error es guardado en form.errors. Tanto form.vars como form.errors son objetos gluon.storage.Storage similares a request.vars. El primero contiene los valores que pasan la validación, como por ejemplo:

1
form.vars.name = "Max"

El último contiene los errores, como por ejemplo:

1
form.errors.name = "Cannot be empty!"

La firma total del método accepts es la siguiente:

form.accepts(vars, session=None, formname='default',
             keepvalues=False, onvalidation=None,
             dbio=True, hideerror=False):

El significado de los parámetros opcionales se explica en las siguientes subsecciones.

La función accepts devuelve True si el formulario es aceptado y False en caso contrario. Un formulario no se acepta si tiene errores o cuando no ha sido enviado (por ejemplo la primera vez que se muestra).

He aquí como la página se ve la primera vez que se despliega:

MVC

Y aquí como se ve cuando es enviada de forma inválida:

MVC

Y así se ve cuando es enviada de forma válida:

MVC

Campos ocultos

Cuando el objeto form visto arriba es serializado por {{=form}}, y debido a la llamada previa al método accepts, ahora se ve así:

1
2
3
4
5
6
7
<form enctype="multipart/form-data" action="" method="post">
your name:
<input name="name" />
<input type="submit" />
<input value="783531473471" type="hidden" name="_formkey" />
<input value="default" type="hidden" name="_formname" />
</form>

Note la presencia de dos campos ocultos: “_formkey” y “_formname”. Su presencia se activa por la llamada a accepts y ellas juegan dos roles diferentes e importantes:

  • El campo oculto llamado “_formkey” es una ficha única que web2py usa para prevenir el envío doble de formularios. El valor de esta clave se genera cuando el formulario se serializa y se guarda en session. Cuando el formulario se envía, este valor debe coincidir, de lo contrario accepts devuelve False sin errores como si el formulario nunca hubiese sido enviado. Esto es porque web2py no puede determinar si el formulario fué enviado correctamente.
  • El campo oculto llamado “_formname” es generado por web2py como un nombre para el formulario, pero el nombre puede ser sobreescrito. Este campo es necesario para permitir páginas que contienen y procesan formularios múltiples. web2py distingue los diferentes formularios enviados por su nombre.
  • Campos ocultos opcionales especificados como FORM(..,hidden=dict(...)).

El papel de estos campos ocultos y su uso en formularios y páginas personalizadas con formularios múltiples se discute con más detalle más adelante en este capítulo.

Si el formulario de arriba se envía con el campo “name” vacío, el formulario no pasa la validación. Cuando el formulario se serializa otra vez aparece como:

1
2
3
4
5
6
7
8
<form enctype="multipart/form-data" action="" method="post">
your name:
<input value="" name="name" />
<div class="error">cannot be empty!</div>
<input type="submit" />
<input value="783531473471" type="hidden" name="_formkey" />
<input value="default" type="hidden" name="_formname" />
</form>

Observe la presencia de un DIV de clase “error” en el formulario serializado. web2py inserta este mensaje de error en el formulario para notificar el visitante respecto al campo que no pasó la validación . El método accepts, después del envío, determina que el formulario fué enviado, verifica si el campo “name” está vacío y si se requiere, y eventualmente inserta el mensaje de error del validador dentro del formulario.

La vista base “layout.html” se espera que maneje DIVs de la clase “error”. La distribución por defecto usa efectos jQuery para hacer que los errores aparezcan y se deslicen en modo cascada con un fondo rojo. Ver el Capítulo 10 para más detalles. keepvalues

keepvalues

El argumento opcional keepvalues le dice a web2py que hacer cuando un formulario es aceptado y no hay redireccionamiento, por lo que el mismo formulario es mostrado otra vez. Por defecto el formulario se vacía. Si a keepvalues se le asigna True, el formulario se pre-llena con los valores que tenía anteriormente. Esto resulta útil cuando se tiene un formulario que se supone va a ser usado de manera repetitiva para insertar múltiples registros similares. Si al argumento dbio se le asigna False, web2py no hará ninguna inserción/actualización en la DB después de aceptar el formulario. Si a hideerror se le asigna True y el formulario contiene errores, estos no se verán cuando el formulario se presenta (dependerá del diseñador si se despliegan desde form.errors de alguna manera). El argumento onvalidation se explica abajo.

onvalidation

El argumento onvalidation puede ser None o puede ser una función que toma el formulario y no devuelve nada. Una función así sería ser llamada y pasada al formulario, inmediatamente después de la validación (si pasa la validación) y antes de que pase nada más. El propósito de esta función es múltiple. Se puede usar, por ejemplo, para hacer chequeos adicionales del formulario y eventualmente agregar errores al mismo. También podría ser usado para calcular los valores de algunos campos basados en los valores de otros. También puede usarse para disparar algunas acciones (como enviar un email) antes de que un registro sea creado/actualizado.

Un ejemplo:

db.define_table('numbers',
    Field('a', 'integer'),
    Field('b', 'integer'),
    Field('c', 'integer', readable=False, writable=False))
def my_form_processing(form):
    c = form.vars.a * form.vars.b
    if c < 0:
       form.errors.b = 'a*b cannot be negative'
    else:
       form.vars.c = c
def insert_numbers():
   form = SQLFORM(db.numbers)
   if form.accepts(request.vars, session,
                   onvalidation=my_form_processing)
       session.flash = 'record inserted'
       redirect(URL())
   return dict(form=form)

Formularios y Redireccionamiento

La manera más común de usar formularios es a través del auto-envío, de manera que las variables de los campos enviados sean procesadas por la misma acción que genera el formulario. Una vez que el formulario se acepta, no es usual mostrar de nuevo la página actual (algo que se hace aquí solo por simplicidad). Lo más común es redireccionar al visitante a la “próxima”página.

El nuevo controlador del ejemplo es:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def display_form():
    form = FORM('Your name:',
              INPUT(_name='name', requires=IS_NOT_EMPTY()),
              INPUT(_type='submit'))
    if form.accepts(request.vars, session):
        session.flash = 'form accepted'
        redirect(URL('next'))
    elif form.errors:
        response.flash = 'form has errors'
    else:
        response.flash = 'please fill the form'
    return dict(form=form)
def next():
    return dict()

Con la finalidad de fijar un flash en la próxima página en vez de la actual, se debe usar session.flash en vez de response.flash. web2py mueve el primero dentro del segundo después del redireccionamiento. Observe que usar session.flash requiere que no se use session.forget().

Múltiples formulario por página

El contenido de esta sección aplica tanto para objetos FORM como para objetos SQLFORM.

Es posible tener múltiples formularios por página, pero debe permitirse que web2py las diferencie. Si son derivadas de SQLFORM desde diferentes tablas, entonces web2py les da nombres diferentes automáticamente; de lo contrario se necesita darles diferentes nombres a los formularios de manera explícita. Más aún, cuando hay múltiples formularios presentes en la misma página, el mecanismo para evitar que se envíen dos veces los formularios deja de funcionar, y se debe omitir el argumento session cuando se llama al método accepts. Por ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def two_forms():
    form1 = FORM(INPUT(_name='name', requires=IS_NOT_EMPTY()),
               INPUT(_type='submit'))
    form2 = FORM(INPUT(_name='name', requires=IS_NOT_EMPTY()),
               INPUT(_type='submit'))
    if form1.accepts(request.vars, formname='form_one'):
        response.flash = 'form one accepted'
    if form2.accepts(request.vars, formname='form_two'):
        response.flash = 'form two accepted'
    return dict(form1=form1, form2=form2)

Y la salida que produce:

MVC

Cuando el visitante envía un form1 vacío, solo form1muestra un error; si el visitante envía un form2, vacía, solo form2 despliega un mensaje de error.

Compartiendo formularios

El contenido de esta sección aplica tanto para objetos FORM como para objetos SQLFORM. Lo que aquí se discute es posible pero no se recomienda, ya que siempre es una buena práctica tener formularios que se envíen a si mismos. Algunas veces, sin embargo, no hay alternativa, porque la acción que envía el formulario y la acción que lo recibe pertenecen a diferentes aplicaciones.

Es posible generar un formulario que haga envío a una acción diferente. Esto se hace especificando el URL de la acción que procesa en los atributos de los objetos FORM o SQLFORM. Por ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
form = FORM(INPUT(_name='name', requires=IS_NOT_EMPTY()),
        INPUT(_type='submit'), _action=URL('page_two'))
def page_one():
    return dict(form=form)
def page_two():
    if form.accepts(request.vars, formname=None):
         response.flash = 'form accepted'
    else:
         response.flash = 'there was an error in the form'
    return dict()

Note que ya que “page_one” y “page_two” usan el mismo formulario form, lo hemos definido solo una vez colocándolo afuera de todas las acciones, con el fin de no repetirnos. La parte común del código al comienzo del controlador se ejecuta cada vez antes de darle control a la acción llamada.

Como “page_one” no llama accepts, el formulario no tiene nombre ni clave, por lo cual no debe pasarse session y debe colocarse formname=None en accepts, o de lo contrario el formulario no se validará cuando “page_two” lo reciba.

SQLFORM

Nos movemos ahora al próximo nivel, asignándole a la aplicación un archivo modelo:

1
2
db = DAL('sqlite://storage.sqlite')
db.define_table('person', Field('name', requires=IS_NOT_EMPTY()))

Se modifica el controlador como sigue:

1
2
3
4
5
6
7
8
9
def display_form():
   form = SQLFORM(db.person)
   if form.accepts(request.vars, session):
       response.flash = 'form accepted'
   elif form.errors:
       response.flash = 'form has errors'
   else:
       response.flash = 'please fill out the form'
   return dict(form=form)

La vista no necesita ser cambiada.

En el controlador nuevo no se necesita construir un FORM, ya que el constructor SQLFORM construye uno desde la tabla db.person definida en el modelo. El nuevo formulario, cuando se serializa, aparece como:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<form enctype="multipart/form-data" action="" method="post">
  <table>
    <tr id="person_name__row">
       <td><label id="person_name__label"
                  for="person_name">Your name: </label></td>
       <td><input type="text" class="string"
                  name="name" value="" id="person_name" /></td>
       <td></td>
    </tr>
    <tr id="submit_record__row">
       <td></td>
       <td><input value="Submit" type="submit" /></td>
       <td></td>
    </tr>
  </table>
  <input value="9038845529" type="hidden" name="_formkey" />
  <input value="person" type="hidden" name="_formname" />
</form>

El formulario automáticamente generado es más complejo que el anterior formulario de bajo nivel. Antes que todo, ésta contiene una tabla de filas, y cada fila tiene tres columnas. La primera columna contiene las etiquetas de los campos (tal como se determina desde db.person), la segunda columna contiene los campos de entrada (y eventualmente los mensajes de error), y la tercera columna es opcional y por lo tanto está vacía (puede llenarse con los campos del constructor SQLFORM).

Todas las etiquetas en el formulario tienen nombres derivados de la tabla y del nombre del campo. Esto permite una fácil personalización del formulario usando CSS y JavaScript. Esta capacidad se discute con más detalle en el Capítulo 10.

Más importante es que ahora el método accepts hace más trabajo para uno. Como en el caso previo, valida la entrada, pero adicionalmente, si la entrada pasa la validación, también hace una inserción en la base de datos del nuevo registro y guarda en form.vars.id el identificador único “id” del nuevo registro.

Un objeto SQLFORM también es capaz de trabajar automáticamente con campos “upload” guardando archivos cargados en la carpeta “uploads” (luego de renombrarlos de forma segura para evitar conflictos y prevenir ataques de recorrido del directorio) y guardar sus nombres (los nuevos) en el campo apropiado de la base de datos.

La SQLFORM muestra valores “boolean” con checkbox, valores “text” con áreas de texto, valores que se requiere estén en un conjunto definido o una base de datos con drop-boxes, y campos “upload” con enlaces que permitan a los usuarios descargar los archivos cargados. Éste esconde campos “blob”, ya que se supone que ellos se manejan de manera diferente, tal como se discutirá más adelante.

Por ejemplo, considere el siguiente modelo:

1
2
3
4
5
6
db.define_table('person',
    Field('name', requires=IS_NOT_EMPTY()),
    Field('married', 'boolean'),
    Field('gender', requires=IS_IN_SET(['Male', 'Female', 'Other'])),
    Field('profile', 'text'),
    Field('image', 'upload'))

En este caso, SQLFORM(db.person) genera el formulario que se muestra abajo:

MVC

El constructor SQLFORM permite varias personalizaciones, tal como desplegar solo un subconjunto de los campos, cambiar las etiquetas, agregar valores a la tercera columna opcional, o crear formularios UPDATE y DELETE, contrario a formularios INSERT tal como el actual. SQLFORM es el más grande objeto ahorrador de tiempo en web2py.

La clase SQLFORM se define en “gluon/sqlhtml.py”. Puede extenderse fácilmente sobrecargando su método xml, el método que serializa los objetos, para cambiar su salida.

La firma para el constructor SQLFORM es la siguiente:

1
2
3
4
5
6
SQLFORM(table, record=None, deletable=False,
        linkto=None, upload=None, fields=None, labels=None, col3={},
        submit_button='Submit', delete_label='Check to delete:',
        id_label='Record id: ', showid=True,
        readonly=False, comments=True, keepopts=[],
       ignore_rw=False, formstyle='table3cols',**attributes)
  • El segundo argumento opcional cambia el formulario INSERT en un formulario UPDATE para el registro específico (ver la próxima subsección).

Si a deletable se le asigna True, el formulario UPDATE despliega un checkbox “Check to delete” (Marque para borrar). El valor de la etiqueta de este campo se asigna por medio del argumento delete_label.

  • submit_button le asigna el valor al botón de enviar.

  • id_label asigna la etiqueta del registro “id”

  • El “id” del registro no se muestra si a showid se le asigna False.

  • fields es una lista opcional de nombres de campos que se quieren mostrar. Si se provee una lista, solo los valores en la lista son mostrados. Por ejemplo:

    1
    fields = ['name']
    
  • labels es un diccionario de etiquetas de campos. La clave del diccionario es el nombre del campo y su valor correspondiente es lo que se muestra como su etiqueta. Si no se provee una etiqueta, web2py la deduce del nombre del campo (pone en mayúsculas el nombre del campo y reemplaza subrayados con espacios). Por ejemplo:

    1
    labels = {'name':'Your Full Name:'}
    
  • col3 es un diccionario de valores para la tercera columna. Por ejemplo:

    1
    2
    col3 = {'name':A('what is this?',
          _href='http://www.google.com/search?q=define:name')}
    
  • linkto y upload son URLs opcionales a controladores definidos por el usuario que permiten al formulario manejar campos de referencia. Esto se discute con más detalle en la próxima sección.

  • readonly. Si se le asigna True, muestra el formulario como de sólo lectura.

  • comments. Si se le asigna False, no muestra los comentarios de la col3

  • ignore_rw. Normalmente, para un formulario crear/actualizar, solo los campos marcados como writable=True se muestran, y para formularios de sólo lectura, sólo campos marcados como readable=True se muestran. Asignar ignore_rw=True hace que estas restricciones se ignoren, y todos los campos se muestran. Esto se usa más que todo en la interfaz appadmin para presentar todos los campos por cada tabla, anulando lo que indica el modelo.

  • formstyle determina el estilo que se usará cuando se serializa el formulario en html. Éste puede ser “table3cols” (por defecto), “table2cols” (una fila para etiqueta y comentario, y una fila para la entrada), “ul” (hace una lista no ordenada de campos de entrada), “divs” (representa el formulario usando divs para css amigable, para personalización arbitraria). formystyle puede también una función que toma (record_id, field_label, field_widget, field_comment) como atributos y retorna un objeto TR().

  • attributes opcionales son argumentos que comienzan con un subrayado que se quieren pasar a la etiqueta FORM que presenta el objeto SQLFORM. Son ejemplos:

    1
    2
    _action = '.'
    _method = 'POST'
    

Hay un atributo especial hidden. Cuando se pasa un diccionario como hidden, sus elementos se traducen en campos de ENTRADA “ocultos” (ver el ejemplo para el ayudante FORM en el Capítulo 5).

SQLFORM e insert/update/delete

Si se pasa un registro como segundo argumento opcional al constructor SQLFORM, el formulario se convierte en un formulario UPDATE para ese registro. Esto significa que cuando el formulario es enviado, el registro existente se actualiza y no se inserta un nuevo registro. Si el argumento se asigna deletable=True, el formulario UPDATE despliega un checkbox “check to delete” . Si se verifica, se borra el registro.

Se puede, por ejemplo, modificar el controlador del ejemplo previo de tal forma que cuando se pase un argumento adicional entero en el camino de la URL, como en:

  1. /test/default/display_form/2

Y si hay un registro con la id correspondiente, el formulario SQLFORM genera un formulario UPDATE/DELETE para el registro:

1
2
3
4
5
6
7
8
def display_form():
   record = db.person(request.args(0)) or redirect(URL('index'))
   form = SQLFORM(db.person, record)
   if form.accepts(request.vars, session):
       response.flash = 'form accepted'
   elif form.errors:
       response.flash = 'form has errors'
   return dict(form=form)

La línea 3 encuentra el registro, la línea 5 hace un formulario actualización y borrado UPDATE/DELETE, y la línea 7 hace una formulario de inserción INSERT. La línea 8 hace todo el procesamiento de los formularios.

Un formulario de actualización es muy similar a un formulario de creación con la excepción de que está pre-llenado con el registro actual y sus imágenes previas. Por defecto se tiene que deletable = True, lo cual significa que el formulario de actualización mostrará una opción “borrar un registro”.

Los formularios de edición también contienen un campo de entrada oculto INPUT con name=”id” lo cual se usa para identificar el registro. Este id también se guarda del lado del servidor por seguridad adicional y, si el visitante interfiere con el valor de este campo, la actualización no se realiza y web2py produce un Error de Sintaxis, “el usuario esta tratando de interferir con el formulario”.

Cuando un Campo se marca con writable=False, el campo no se muestra en los formularios de creación, y está en modo de sólo lectura en los formularios de actualización. Si un campo se marca como writable=False y readable=False, entonces el campo no se muestra para nada, ni siquiera en los formularios de actualización.

Los formularios creados con

1
form = SQLFORM(...,ignore_rw=True)

Ignoran los atributos readable y writable y muestran siempre todos los campos. Los formularios en appadmin los ignora por defecto.

Los formularios creados con

1
form = SQLFORM(table,record_id,readonly=True)

Siempre muestran todos los campos en modo de sólo lectura, y ellos no pueden ser aceptados.

SQLFORM en HTML

Algunas veces se quiere usar SQLFORM para beneficiarse de su generación y procesamiento de formularios, pero se requiere un nivel de personalización de los formularios en HTML que no puede alcanzarse con los parámetros del objeto SQLFORM, por lo que se tiene que diseñar el formulario usando HTML. Ahora, se edita el controlador previo y se añade una nueva acción:

def display_manual_form():
   form = SQLFORM(db.person)
   if form.accepts(request.vars, formname='test'):
       response.flash = 'form accepted'
   elif form.errors:
       .flash = 'form has errors'
   else:
       response.flash = 'please fill the form'
   return dict()

Y se inserta el formulario en la vista asociada “default/display_manual_form.html”:

1
2
3
4
5
6
7
8
{{extend 'layout.html'}}
<form>
<ul>
  <li>Your name is <input name="name" /></li>
</ul>
  <input type="submit" />
  <input type="hidden" name="_formname" value="test" />
</form>

Observe que la acción no devuelve el formulario porque ésta no necesita pasarlo a la vista. La vista contiene un formulario creado manualmente en HTML. El formulario contiene un campo oculto “_formname” que debe ser el mismo nombre de formulario especificado como el argumento de accepts en la acción. web2py usa el nombre del formulario en caso de que existan múltiples formularios en la misma página, para determinar cual fué enviado. Si la página contiene un sóla formulario, se puede fijar formname=None y omitir el campo oculto en la vista.

SQLFORM y Cargas

Los campos del tipo “upload” (cargas) son especiales. Ellos se presentan como campos de entrada de tipo type=”file”. A menos que se especifique lo contrario, el archivo cargado se deja fluir usando un buffer, y se guarda en la carpeta “uploads” de la aplicación usando un nuevo nombre seguro, asignado de manera automática. El nombre de este archivo se guarda entonces en el campo de tipo “uploads”.

Como ejemplo, considere el siguiente modelo:

1
2
3
db.define_table('person',
    Field('name', requires=IS_NOT_EMPTY()),
    Field('image', 'upload'))

Se puede usar la misma acción del controlador “display_form” que se muestra arriba.

Cuando se inserta un nuevo registro, el formulario permite buscar un archivo. Se escoge, por ejemplo, una imagen jpg. El archivo se carga y se guarda como:

  1. applications/test/uploads/person.image.XXXXX.jpg
“XXXXXX” es un identificador aleatorio para el archivo asignado por web2py.
Note que, por defecto, el nombre del archivo original del archivo cargado es codificado y usado para construir el nuevo nombre del archivo. Este nombre es recuperado por la acción “download” por defecto y se usa para poner el encabezado de disposición de contenido al archivo original.

Solo se conserva su extensión. Esto es un requerimiento de seguridad ya que el nombre del archivo puede contener caracteres especiales que podrían permitir al visitante hacer ataques de recorrido de directorio u otras operaciones maliciosas.

El nuevo nombre del archivo se guarda también en form.vars.image.

Cuando se edita un registro usando el formulario de actualización, sería bueno desplegar un enlace al archivo cargado existente, y web2py provee una forma de hacerlo.

Si se pasa un URL al constructor SQLFORM a través del argumento upload, web2py usa la acción en ese URL para bajar el archivo. Considere las siguientes acciones:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def display_form():
   record = db.person(request.args(0)) or redirect(URL('index'))
   form = SQLFORM(db.person, record, deletable=True,
                  upload=URL('download'))
   if form.accepts(request.vars, session):
       response.flash = 'form accepted'
   elif form.errors:
       response.flash = 'form has errors'
   return dict(form=form)
def download():
    return response.download(request, db)

Ahora inserte un registro nuevo en el URL:

http://127.0.0.1:8000/test/default/display_form

Se carga una imagen, se envía el formulario, y entonces se edita el nuevo registro creado cuando se visita:

http://127.0.0.1:8000/test/default/display_form/3

(se asume que el id del último registro es 3). El formulario desplegara una vista previa de la imagen, como se muestra a continuación:

MVC

Este formulario, cuando se serializa, genera el siguiente HTML:

1
2
3
4
5
6
7
<td><label id="person_image__label" for="person_image">Image: </label></td>
<td><div><input type="file" id="person_image" class="upload" name="image"
/>[<a href="/test/default/download/person.image.0246683463831.jpg">file</a>|
<input type="checkbox" name="image__delete" />delete]</div></td><td></td></tr>
<tr id="delete_record__row"><td><label id="delete_record__label" for="delete_record"
>Check to delete:</label></td><td><input type="checkbox" id="delete_record"
class="delete" name="delete_this_record" /></td>

Lo cual contiene un enlace para permitir bajar el archivo cargado, y un checkbox para remover el archivo del registro de la base de datos, por lo tanto guardando NULL en el campo “image”.

¿Porqué se expone este mecanismo? ¿Porqué se necesita escribir la función de “bajar” archivos? Porque se querría reenforzar un mecanismo de autorización para la función de bajar archivos. Ver el Capítulo 8 para un ejemplo.

Guardando el nombre original del archivo

web2py guarda automáticamente el nombre original del archivo dentro del nuevo nombre de archivo UUID y lo recupera cuando se baja el archivo. Al bajarse el archivo, el nombre original se guarda en en encabezado del contenido de disposición de la respuesta HTTP. Todo esto se hace de manera transparente sin la necesidad de programar.

Ocasionalmente se querría guardar el nombre original en un campo de la base de datos. En este caso, se requiere modificar el modelo y agregar un campo para guardarlo allí:

1
2
3
4
db.define_table('person',
    Field('name', requires=IS_NOT_EMPTY()),
    Field('image_filename'),
    Field('image', 'upload'))

Entonces, se necesita modificar el controlador para poder manejarlo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def display_form():
    record = db.person(request.args(0)) or redirect(URL('index'))
    url = URL('download')
    form = SQLFORM(db.person, record, deletable=True,
                   upload=url, fields=['name', 'image'])
    if request.vars.image:
        form.vars.image_filename = request.vars.image.filename
    if form.accepts(request.vars, session):
        response.flash = 'form accepted'
    elif form.errors:
        response.flash = 'form has errors'
    return dict(form=form)

Observe que SQLFORM no muestra el campo “image_filename”. La acción “display_form” mueve el nombre del archivo de request.vars.image a form.vars.image_filename, de tal forma que éste sea procesado por accepts y guardado en la base de datos. La función de “descarga”, antes de ofrecer el archivo, verifica en la base de datos por el nombre del archivo original y lo usa en el encabezado de disposición de contenido.

autodelete

El SQLFORM, al momento de borrar un registro, no borra los archivos(s) referenciados por el registro cargados físicamente. La razón es que web2py no sabe si el mismo archivo es usado/enlazado por otras tablas o usado para otros propósitos. Si se sabe que es seguro borrar los archivos cuando se borra el correspondiente registro, se puede hacer lo siguiente:

1
2
3
db.define_table('image',
    Field('name', requires=IS_NOT_EMPTY()),
    Field('file','upload',autodelete=True))

El atributo autodelete es False por defecto. Cuando se pone a True se asegura que el archivo se borra cuando se borra el registro.

Enlaces a Referenciar Registros

Consideremos el caso de dos tablas enlazadas por un campo de referencia. Por ejemplo:

1
2
3
4
5
6
db.define_table('person',
    Field('name', requires=IS_NOT_EMPTY()))
db.define_table('dog',
    Field('owner', db.person),
    Field('name', requires=IS_NOT_EMPTY()))
db.dog.owner.requires = IS_IN_DB(db,db.person.id,'%(name)s')

Una persona tiene perros, y cada perro tiene un dueño, el cual es una persona. El dueño del perro se requiere para referenciar un db.person.id válido por ‘%(name)s’.

Usemos la interfaz appadmin para esta aplicación agregando unas cuantas personas y sus perros.

Cuando se edita una persona que ya existe, el formulario appadmin de actualización UPDATE muestra un enlace a una página que lista los perros que pertenecen a la persona. Este comportamiento puede imitarse usando el argumento linkto de la SQLFORM. linkto tiene que apuntar a la URL de una nueva acción que recibe una cadena de una solicitud del SQLFORM y lista los registros correspondientes. He aquí un ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def display_form():
   record = db.person(request.args(0)) or redirect(URL('index'))
   url = URL('download')
   link = URL('list_records')
   form = SQLFORM(db.person, records, deletable=True,
                  upload=url, linkto=link)
   if form.accepts(request.vars, session):
       response.flash = 'form accepted'
   elif form.errors:
       response.flash = 'form has errors'
   return dict(form=form)

He aquí la página:

MVC

Hay un enlace llamado “dog.owner”. El nombre del enlace puede ser cambiado a través del argumento labels del SQLFORM, por ejemplo:

  1. labels = {‘dog.owner’:”This person’s dogs”}

Al darle clic al enlace se redirecciona a:

  1. /test/default/list_records/dog?query=dog.owner%3D5

“list_records” es la acción especificada, con request.args(0) asignado al nombre de la tabla referenciada y request.vars.query asignado a la cadena de solicitud SQL. La cadena de solicitud en el URL contiene el valor “dog.owner=5” apropiadamente codificado para la url (web2py decodifica esto automáticamente cuando la URL se analiza).

Se puede implementar fácilmente una acción “list_records” muy genérica, tal como sigue:

1
2
3
4
5
def list_records():
    table = request.args(0)
    query = request.vars.query
    records = db(query).select(db[table].ALL)
    return dict(records=records)

Con la vista asociada “default/list_records.html”:

1
2
{{extend 'layout.html'}}
{{=records}}

Cuando un conjunto de registro es devuelto por una selección y serializado en una vista, primero se convierte a un objeto SQLTABLE (no es lo mismo que una tabla) y entonces se serializa a una tabla HTML, donde cada campo corresponde a la columna de la tabla.

Pre-llenando el formulario

Siempre es posible pre-llenar el formulario usando la siguiente sintaxis:

1
form.vars.name = 'fieldvalue'

Declaraciones como la de arriba deben ser insertadas luego de declarar el formulario y antes de que este sea aceptado, así el campo (“name” en este ejemplo) sea o no visualizado explícitamente en el formulario.

SQLFORM sin entrada y salida a la base de datos

Hay momentos cuando se quiere generar un formulario desde una base de datos usando SQLFORM y se quiere validar un formulario enviado de manera acorde, pero no se quiere ninguna acción automática de INSERT/UPDATE/DELETE en la base de datos. Este es el caso, por ejemplo, cuando uno de los campos necesita ser calculado con el valor de otros campos de entrada. Este es también el caso cuando se requiere realizar validaciones adicionales en los datos insertados, cosa que no puede lograrse con los validadores estándares.

Esto se puede hacer convirtiendo:

1
2
3
form = SQLFORM(db.person)
if form.accepts(request.vars, session):
    response.flash = 'record inserted'

en:

1
2
3
4
5
form = SQLFORM(db.person)
if form.accepts(request.vars, session, dbio=False):
    ### deal with uploads explicitly
    form.vars.id = db.person.insert(**dict(form.vars))
    response.flash = 'record inserted'

Lo mismo puede hacerse para los formularios de UPDATE/DELETE transformando:

1
2
3
form = SQLFORM(db.person,record)
if form.accepts(request.vars, session):
     response.flash = 'record updated'

en:

1
2
3
4
5
6
7
form = SQLFORM(db.person,record)
if form.accepts(request.vars, session, dbio=False):
    if form.vars.get('delete_this_record', False):
        db(db.person.id==record.id).delete()
    else:
        record.update_record(**dict(form.vars))
    response.flash = 'record updated'

En ambos casos web2py se ocupa del almacenamiento y renombrado del archivo cargado como si dbio=True, lo que es el comportamiento por defecto. El nombre del archivo cargado está en:

1
form.vars.fieldname

SQLFORM.factory

Hay casos en que se quiere generar formularios como si se tuviese una tabla de base de datos pero realmente no se quiere una tabla de base de datos. Lo que se quiere es simplemente sacarle provecho a la capacidad de SQLFORM de generar un formulario amigable con CSS de buen aspecto y quizás llevar a cabo cargas de archivos y renombramiento.

Esto puede hacerse por medio de form_factory. Aquí hay un ejemplo donde el formulario se genera, se hace la validación, se carga un archivo y se guarda todo en la session

def form_from_factory()
    form = SQLFORM.factory(
        Field('your_name', requires=IS_NOT_EMPTY()),
        Field('your_image', 'upload'))
    if form.accepts(request.vars, session):
        response.flash = 'form accepted'
        session.your_name = form.vars.your_name
        session.filename = form.vars.your_image
    elif form.errors:
        response.flash = 'form has errors'
    return dict(form=form)

Esta es la vista “default/form_from_factory.html”:

1
2
{{extend 'layout.html'}}
{{=form}}

Necesita usarse un subrayado en vez de un espacio para etiquetas de campos, o pasar explícitamente un diccionario de labels a form_factory, tal como se haría para una SQLFORM. Por defecto SQLFORM.factory genera el formulario usando atributos “id” html generados como si el formulario fué generado de una tabla llamada “no_table”. Para cambiar el nombre a esta tabla artificial, se usa el atributo table_name para la factory:

1
form = SQLFORM.factory(...,table_name='other_dummy_name')

Cambiar el table_name se hace necesario si se necesita colocar dos formularios generados con factory en la misma tabla y se quiere evitar conflictos CSS.

CRUD

Uno de los elementos agregados recientemente a web2py es la API Crear/Leer/Actualizar/Borrar (Create/Read/Update/Delete - CRUD) en la cima de SQLFORM. CRUD crea un SQLFORM, pero simplifica el código porque incorpora la creación del formulario, el procesamiento del formulario, la notificación y la redirección, todo en una simple función.

Lo primero que hay que observar es que CRUD difiere de las otras APIs de wep2py que hemos usado hasta los momentos porque no se ha expuesto. Debe importarse. También debe ser enlazada a una base de datos específica. Por ejemplo:

1
2
from gluon.tools import Crud
crud = Crud(globals(), db)

El primer argumento del constructor es el contexto actual globals() de tal forma que CRUD pueda acceder el requerimiento, respuesta y sesión local. El segundo argumento es un objeto de conexión a base de datos, db.

El objeto crud definido arriba provee las siguientes API:

  • crud.tables() devuelve una lista de tablas definidas en la base de datos.
  • crud.create(db.tablename) devuelve un formulario de creación para la tabla y su nombre.
  • crud.read(db.tablename, id) devuelve un formulario de sólo lectura para el nombre de la tabla y el registro id.
  • crud.update(db.tablename, id) devuelve un formulario de actualización para el nombre de la tabla y el registro id.
  • crud.delete(db.tablename, id) borra el registro.
  • crud.select(db.tablename, query) devuelve una lista de registros seleccionados de la tabla.
  • crud.search(db.tablename) devuelve una tupla (formulario, registros) donde form es un formulario de búsqueda y records es una lista de registros basada en el formulario de búsqueda enviado.
  • crud() devuelve uno de los de a basado en request.args().

Por ejemplo, la siguiente acción:

def data: return dict(form=crud())

Expondría los siguientes URLs:

http://.../[app]/[controller]/data/tables

http://.../[app]/[controller]/data/create/[tablename]

http://.../[app]/[controller]/data/read/[tablename]/[id]

http://.../[app]/[controller]/data/update/[tablename]/[id]

http://.../[app]/[controller]/data/delete/[tablename]/[id]

http://.../[app]/[controller]/data/select/[tablename]

http://.../[app]/[controller]/data/search/[tablename]

Sin embargo, la siguiente acción:

def create_tablename:
    return dict(form=crud.create(db.tablename))

Sólo expondría el método create

http://.../[app]/[controller]/create_tablename

Mientras la siguiente acción:

def update_tablename:
    return dict(form=crud.update(db.tablename, request.args(0)))

Solo expondría el método update

http://.../[app]/[controller]/update_tablename/[id]

y así sucesivamente.

El comportamiento de CRUD puede personalizarse de dos formas: estableciendo algunos atributos del objeto crud o pasándo parámetros extra a cada uno de sus métodos.

Configuración

He aquí la lista completa de todos los atributos CRUD existentes, sus valores por defecto y significado:

Para reforzar la autenticación de todos los formularios crud:

1
crud.settings.auth = None

El uso se explica en el Capítulo 8.

Para especificar el controlador que define la función data la cual devuelve el objeto crud

crud.settings.controller = ‘default’

Para especificar la URL a redireccionar después de una “creación” de registro exitosa:

1
crud.settings.create_next = URL('index')

Para especificar la URL a redireccionar después de una “actualización” de registro exitosa:

1
crud.settings.update_next = URL('index')

Para especificar la URL a redireccionar después de un “borrado” de registro exitoso

1
crud.settings.delete_next = URL('index')

Para especificar la URL a ser usada para enlazar archivos cargados:

1
crud.settings.download_url = URL('download')

Para especificar funciones extras a ser ejecutadas después de la los procedimientos de validación estándar por los formularios crud.create:

1
crud.settings.create_onvalidation = StorageList()

StorageList es lo mismo que el objeto Storage, ambos están definidos en el archivo “gluon/storage.py”, pero su valor por defecto es [] al contrario que None. Permite la siguiente sintáxis:

crud.settings.create_onvalidation.mytablename.append(lambda form:....)

Para especificar funciones extras a ser ejecutadas después de los procedimientos de validación estándar para los formularios crud.update:

1
crud.settings.update_onvalidation = StorageList()

Para especificar funciones extras a ser ejecutadas después de completados los formularios crud.create:

1
crud.settings.create_onaccept = StorageList()

Para especificar funciones extras a ser ejecutadas después de completados los formularios crud.update:

1
crud.settings.update_onaccept = StorageList()

Para especificar funciones extras a ser ejecutadas después de completado crud.update si se borra registro:

1
crud.settings.update_ondelete = StorageList()

Para especificar funciones extras a ser ejecutadas después de completado crud.delete:

1
crud.settings.delete_onaccept = StorageList()

Para determinar si los formularios de “actualización” deben tener un botón “borrar”:

1
crud.settings.update_deletable = True

Para determinar si los formularios de “actualización” deben mostrar el id del registro editado:

1
crud.settings.showid = False

Para determinar si los formularios deben mantener los valores insertados previamente o volver a los valores por defecto después de un envío exitoso:

1
crud.settings.keepvalues = False

Se puede cambiar el estilo del formulario usando:

1
crud.settings.formstyle = 'table3cols' or 'table2cols' or 'divs' or 'ul'

Se puede agregar captcha a los formularios, usando la misma convención que para auth, con:

1
2
3
crud.settings.create_captcha = None
crud.settings.update_captcha = None
crud.settings.captcha = None

Mensajes

He aquí una lista de mensajes personalizables:

1
crud.messages.submit_button = 'Submit'

Asigna el texto para el botón “submit” tanto para el formulario crear como el formulario actualizar.

1
crud.messages.delete_label = 'Check to delete:'

Asigna la etiqueta del botón “delete” en los formularios “update”.

1
crud.messages.record_created = 'Record Created'

Coloca un mensaje flash cuando se crean registros exitosamente.

1
crud.messages.record_updated = 'Record Updated'

Coloca un mensaje flash cuando se actualiza un registro de manera exitosa.

1
crud.messages.record_deleted = 'Record Deleted'

Coloca un mensaje flash cuando se borra un registro de manera exitosa .

1
crud.messages.update_log = 'Record %(id)s updated'

Asigna el mensaje log cuando se actualiza un registro de manera exitosa.

1
crud.messages.create_log = 'Record %(id)s created'

Asigna el mensaje log cuando se crea un registro de manera exitosa.

1
crud.messages.read_log = 'Record %(id)s read'

Asigna el mensaje log cuando se produce acceso de lectura a un registro de manera exitosa.

1
crud.messages.delete_log = 'Record %(id)s deleted'

Asigna el mensaje log al borrarse un registro de manera exitosa.

Observe que los crud.messages pertenecen a la clase gluon.storage.Message la cual es similar a gluon.storage.Storage pero ésta traduce automáticamente sus valores, sin necesidad del operador T.

Los mensajes log se usan si y solo si CRUD se conecta a Auth como se discute en el Capítulo 8. Los eventos son registrados en la tabla Auth “auth_events”.

Métodos

El comportamiento de los métodos CRUD también pueden ser personalizado por cada llamada en particular. Aquí están sus firmas:

1
2
3
4
5
6
7
crud.tables()
crud.create(table, next, onvalidation, onaccept, log, message)
crud.read(table, record)
crud.update(table, record, next, onvalidation, onaccept, ondelete, log, message, deletable)
crud.delete(table, record_id, next, message)
crud.select(table, query, fields, orderby, limitby, headers, **attr)
crud.search(table, query, queries, query_labels, fields, field_labels, zero)
  • table es una tabla DAL o un nombre de tabla sobre la cual el método debe actuar.
  • record y record_id son los id del registro sobre el cual el método debe actuar.
  • next es la URL a redireccionar después del éxito de la operación. Si el URL contiene la subcadena “[id]”, éste será reemplazado por el id del registro que actualmente se está creando/actualizando.
  • onvalidation tiene la misma función que SQLFORM(..., onvalidation)
  • onaccept es una función a llamar después que el envío del formulario se acepta y es trabajado, pero antes del redireccionamiento.
  • log es el registro de mensajes. El registro de mensajes en CRUD ve variables en el diccionario form.vars tal como “%(id)s”.
  • message es el mensaje flash generado cuando se acepta el formulario.
  • ondelete se llama en lugar de onaccept cuando un registro se borra usando un formulario de “actualización”.
  • deletable determina si el formulario de “actualización” debe tener una opción de borrar.
  • query es la consulta a ser usada para seleccionar registros.
  • fields es una lista de los campos a ser seleccionados.
  • orderby determina el orden en el cual los registros deben ser seleccionados (ver Chapter 6).
  • limitby determina el rango de los registros seleccionados que deben ser mostrados (ver Capítulo 6).
  • headers es un diccionario con los nombres de los encabezados de la tabla.
  • queries es una lista como [‘equals’, ‘not equal’, ‘contains’] que contiene los métodos permitidos en el formulario de búsqueda.
  • query_labels es un diccionario como query_labels=dict(equals=’Equals’) que les da nombres a los métodos de búsqueda.
  • fields es una lista de campos a ser listados en el widget de búsqueda.
  • field_labels es un diccionario que asigna los nombres de los campos a etiquetas.
  • zero es por defecto “choose one” y se usa como la opción por defecto para la lista desplegable en el widget de búsqueda.

He aquí un ejemplo de una función controladora:

# assuming db.define_table('person', Field('name'))
def people():
    form = crud.create(db.person, next=URL('index'),
           message=T("record created"))
    persons = crud.select(db.person, fields=['name'],
           headers={'person.name', 'Name'})
    return dict(form=form, persons=persons)

He aquí otra función genérica controladora que permite buscar, crear y editar cualquier registro de cualquier tabla donde el nombre de tabla es pasado a request.args(0):

1
2
3
4
5
6
7
def manage():
    table=db[request.args(0)]
    form = crud.update(table,request.args(1))
    table.id.represent = lambda id: \
       A('edit:',id,_href=URL(args=(request.args(0),id)))
    search, rows = crud.select(table)
    return dict(form=form,search=search,rows=rows)

Observe la línea table.id.represent=... que le dice a web2py que cambie la representación del campo id y muestre un enlace en vez de la propia página y pase el id como request.args(1) lo cual convierte la página de creación en una página de actualización.

Versionando registros

CRUD provee una utilidad para versionar registros de bases de datos:

Si se tiene una tabla (db.mytable) que necesita una historia de revisión completa, se puede simplemente:

1
form=crud.update(db.mytable,myrecord,onaccept=crud.archive)

crud.archive define una nueva tabla llamada db.mytable_history (el nombre se deriva de la tabla de arriba) y se guarda al hacer la actualización, guarda una copia del registro (antes de la actualización) en la nueva tabla creada, incluyendo una referencia al registro actual.

Debido a que el registro se actualiza realmente (solo se archiva su estado previo), las referencias nunca se rompen.

Esto se hace de manera transparente pero se puede necesitar acceder a la tabla histórica. Si se necesita acceder a esa tabla, tiene que ser definida en el modelo:

1
2
3
db.define_table('mytable_history',
   Field('current_record',db.mytable),
   db.mytable)

Observe que la tabla extiende db.mytable, es decir, incluye todos sus campos, y tiene una referencia al registro current_record.

crud.archive no les pone un sello de tiempo a los registros guardados a menos que la tabla original tenga un campo de sello de tiempo, por ejemplo:

db.define_table('mytable',
    Field('saved_on','datetime',
          default=request.now,update=request.now,writable=False),
    Field('saved_by',auth.user,
          default=auth.user_id,update=auth.user_id,writable=False),

No hay nada especial respecto a estos campos, se les puede dar cualquier nombre. Ellos se llenan antes de que el registro se archive, y son archivados con cada copia del registro.

Si se quiere cambiar el nombre de la tabla histórica y/o el nombre del campo de referencia, se puede hacer de esta manera:

1
2
3
4
5
6
7
8
db.define_table('myhistory',
    Field('parent_record',db.mytable),
    db.mytable)
# ...
form = crud.update(db.mytable,myrecord,
                   onaccept=lambda form:crud.archive(form,
                   archive_table=db.myhistory,
                   current_record='parent_record'))

Formularios personalizados

Si un formulario se crea con SQLFORM, SQLFORM.factory o CRUD, hay múltiples maneras de que éste pueda incrustarse en una vista permitiendo múltiples niveles de personalización. Considere por ejemplo el siguiente modelo:

1
2
3
db.define_table('image',
    Field('name'),
    Field('file', 'upload'))

Una acción de carga

1
2
def upload_image():
    return dict(form=crud.create(db.image))

La manera más simple de incrustar el formulario en la vista para upload_image es

1
{{=form}}

Esto resulta en un diseño estándar de tabla. Si se quiere usar un diseño diferente, puede dividirse el formulario en sus componentes:

1
2
3
4
5
{{=form.custom.begin}}
Image name: <div>{{=form.custom.widget.name}}</div>
Image file: <div>{{=form.custom.widget.file}}</div>
Click here to upload: {{=form.custom.submit}}
{{=form.custom.end}}

Donde form.custom.widget[fieldname] se serializa en el widget adecuado para el campo. Si el formulario se envía y contiene errores, se anexan debajo de los widgets, como siempre.

El formulario ejemplo de arriba se muestra en la imagen de abajo.

MVC

Note que un resultado similar se habría obtenido con:

1
crud.settings.formstyle='table2cols'

Sin usar un formulario personalizado. Otros formstyles* posibles son “table3cols” (el predeterminado), “divs” y “ul”.

Si no se desea usar los widgets serializados por web2py, se pueden reemplazar con HTML. Hay algunas variables que pueden ser útiles para esto:

  • form.custom.label[fieldname] contiene la etiqueta para el campo.
  • form.custom.dspval[fieldname] representación del campo a mostrar dependiente del tipo de formulario o del tipo de campo.
  • form.custom.inpval[fieldname] valores dependientes del tipo de formulario y del tipo de campo para ser usados en el código de campo.

Es importante seguir las convenciones descritas abajo.

Convenciones CSS

Etiquetas en formularios generados por SQLFORM, SQLFORM.factory y CRUD siguen una estricta convención CSS para poner nombres que puede ser usada para personalizar más aún los formularios.

Dada una tabla “mytable”, un campo “myfield” del tipo “string”, este se presenta por defecto por un

1
SQLFORM.widgets.string.widget

Que luce como:

1
2
<input type="text" name="myfield" id="mytable_myfield"
       class="string" />

Observe que:

  • la clase de la etiqueta INPUT es la misma que el tipo de campo. Esto es muy importante para que el código jQuery en “web2py_ajax.html” trabaje. Esto se asegura de que se pueda tener sólo números en campos “integer” y “double”, y que los campos “time”, “date” y “datetime” desplieguen el calendario pop-up.
  • El id es el nombre de la clase más el nombre del campo, unidos por un subrayado. Esto permite referirse de una manera única al campo por medio de, por ejemplo, jQuery(‘#mytable_myfield’) y manipular el estilo de la hoja del campo o enlazar acciones asociadas a los eventos del campo (focus, blur, keyup, etc.).
  • name es, como podría esperarse, el nombre del campo.

Esconder Errores

Ocasionalmente, se puede querer deshabilitar el posicionamiento de errores automático y desplegar el formulario de los mensajes de error en algún lugar distinto al asignado por defecto. Esto puede hacerse fácilmente.

  • En el caso de FORM o SQLFORM, pasar hiderror=True al método accepts.
  • En el caso de CURD, asignar crud.settings.hiderror=True

También se puede querer modificar las vistas para mostrar los errores (ya que no se muestran más de manera automática).

Aquí hay un ejemplo donde los errores son mostrados arriba del formulario y no en el.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{{if form.errors:}}
  Your submitted form contains the following errors:
  <ul>
  {{for fieldname in form.errors:}}
    <li>{{=fieldname}} error: {{=form.errors[fieldname]}}</li>
  {{pass}}
  </ul>
  {{form.errors.clear()}}
{{pass}}
{{=form}}

Los errores se mostrarán como se ve en la imagen de abajo.

MVC

Este mecanismo también funciona con formularios personalizados.

Validadores

Validadores son clases usadas para validar campos de entrada (incluyendo formularios generados desde tablas de bases de datos).

He aquí un ejemplo del uso de un validador con un FORM:

1
INPUT(_name='a', requires=IS_INT_IN_RANGE(0, 10))

He aquí un ejemplo de como requerir un validador para un campo de una tabla:

1
2
db.define_table('person', Field('name'))
db.person.name.requires = IS_NOT_EMPTY()

Validadores siempre son asignados usando el atributo requires de un campo. Un campo puede tener un único validador o múltiples validadores. Validadores múltiples se hacen parte de una lista:

1
2
db.person.name.requires = [IS_NOT_EMPTY(),
                           IS_NOT_IN_DB(db, 'person.name')]

Los validadores son llamados por la función accepts en un FORM u otro objeto ayudante HTML que contiene un formulario. Se llaman en el orden en el cual están listados.

Los validadores incorporados tienen constructores que toman un argumento opcional:

1
IS_NOT_EMPTY(error_message=T('cannot be empty'))

error_message permite anular el mensaje de error por defecto para cualquier validador.

Este es un ejemplo de un validador de una tabla de base de datos:

1
db.person.name.requires = IS_NOT_EMPTY(error_message=T('fill this!'))

Donde se ha usado el operador de traducción T para permitir la internacionalización. Note que los mensajes de errores por defecto no se traducen.

Validadores

IS_ALPHANUMERIC

Este validador verifica que un campo contiene solo caracteres en los rangos a-z, A-Z, o 0-9.

1
requires = IS_ALPHANUMERIC(error_message=T('must be alphanumeric!'))

IS_DATE

Este validador confirma que un campo contiene una fecha válida en el formato especificado. Es bueno especificar el formato usando el operador de traducción, a fin de soportar diferentes formatos locales.

1
2
requires = IS_DATE(formulariot=T('%Y-%m-%d'),
                   error_message=T('must be YYYY-MM-DD!'))

Para una descripción completa de las directivas % ver el validador IS_DATETIME.

IS_DATE_IN_RANGE

Trabaja muy parecido al validador anterior pero permite especificar un rango:

1
2
3
4
requires = IS_DATE_IN_RANGE(formulariot=T('%Y-%m-%d'),
                   minimum=datetime.date(2008,1,1),
                   maximum=datetime.date(2009,12,31),
                   error_message=T('must be YYYY-MM-DD!'))

Para una descripción completa de las directivas % ver el validador IS_DATETIME.

IS_DATETIME

Este validador verifica que el valor de un campo contiene una fecha/hora válida en el formato especificado. Es bueno especificar el formato usando el operador de traducción, con el fin de soportar diferentes formatos locales.

1
2
requires = IS_DATETIME(formulariot=T('%Y-%m-%d %H:%M:%S'),
                       error_message=T('must be YYYY-MM-DD HH:MM:SS!'))

Los siguientes símbolos pueden ser usados para el formato string:

as a decimal number [00,53]. All days in a new year preceding
        the first Sunday are considered to be in week 0.
        as a decimal number [00,53]. All days in a new year preceding
        the first Monday are considered to be in week 0.

IS_DATETIME_IN_RANGE

Trabaja muy parecido al validador anterior pero permite especificar un rango:

1
2
3
4
requires = IS_DATETIME(formulariot=T('%Y-%m-%d %H:%M:%S'),
                       minimum=datetime.datetime(2008,1,1,10,30),
                       maximum=datetime.datetime(2009,12,31,11,45),
                       error_message=T('must be YYYY-MM-DD HH:MM::SS!'))

Para una descripción completa de las directivas % ver el validador IS_DATETIME.

IS_DECIMAL_IN_RANGE

1
INPUT(_type='text', _name='name', requires=IS_DECIMAL_IN_RANGE(0, 10))

Convierte la entrada en un Decimal Python o genera un error si el decimal no cae dentro de un rango inclusivo especificado. La comparación se hace con aritmética Python Decimal.

Los límites mínimos y máximos pueden ser None, significando que no hay límite superior ni inferior respectivamente.

IS_EMAIL

Verifica que el valor del campo se ve como una dirección de correo electrónico. No trata de enviar un email para confirmar.

1
requires = IS_EMAIL(error_message=T('invalid email!'))

IS_EQUAL_TO

verifica si el valor validado es igual a un valor dado (el cual puede ser una variable):

1
2
requires = IS_EQUAL_TO(request.vars.password,
                       error_message=T('passwords do not match'))

IS_EXPR

Su primer argumento es una cadena conteniendo una expresión lógica en términos del valor de una variable. Este valida el valor de una variable si el valor de la expresión es True. Por ejemplo:

1
2
requires = IS_EXPR('int(value)%3==0',
                   error_message=T('not divisible by 3'))

Se debería verificar primero que el valor es un entero de tal forma que la excepción no ocurra .

1
requires = [IS_INT_IN_RANGE(0, 100), IS_EXPR('value%3==0')]

IS_FLOAT_IN_RANGE

Verifica que el valor del campo es un número de punto flotante dentro de un rango definido , 0 < value < 100 en el siguiente ejemplo:

1
2
requires = IS_FLOAT_IN_RANGE(0, 100,
         error_message=T('too small or too large!'))

IS_INT_IN_RANGE

Verifica que el valor del campo es un número entero dentro de un rango definido, 0 < value < 100 en el siguiente ejemplo:

1
2
requires = IS_INT_IN_RANGE(0, 100,
         error_message=T('too small or too large!'))

IS_IN_SET

Verifica que el valor del campo pertenece a un conjunto:

1
2
requires = IS_IN_SET(['a', 'b', 'c'],zero=T('choose one'),
         error_message=T('must be a or b or c'))

El argumento zero es opcional y determina el texto de la opción asignada por defecto, una opción que no es aceptada por el validador IS_IN_SET por si mismo. Si no se quiere una opción de “escoger uno” se debe asignar zero=False

La opción zero se introdujo en la revisión (1.67.1). No perturbó la compatibilidad con versiones anteriores en el sentido de que no perturbó aplicaciones pero si cambia su comportamiento dado que antes no existía la opción zero.

Los elementos del conjunto deben ser siempre cadenas a menos que su validador se preceda por IS_INT_IN_RANGE (lo cual convierte el valor a entero) o IS_FLOAT_IN_RANGE (lo cual convierte el valor a punto flotante). Por ejemplo:

1
2
requires = [IS_INT_IN_RANGE(0, 8), IS_IN_SET([2, 3, 5, 7],
          error_message=T('must be prime and less than 10'))]

IS_IN_SET y Etiquetamiento

El validador IS_IN_SET tiene un atributo opcional multiple=False. Si se le asigna True, pueden guardarse múltiples valores en un campo. El campo debe ser del tipo list:integer o list:string. Multiple: las referencias múltiples se manejan automáticamente al crear y actualizar formularios, pero estas son transparentes al DAL. Se insiste en sugerir el uso del plugin jQuery multiselect para presentar múltiples campos.

IS_LENGTH

Verifica si la longitud del valor del campo cabe dentro de unos límites dados . Trabaja tanto para entradas de texto como para entradas de archivos.

Sus argumentos son:

  • maxsize: el tamaño/longitud máximo(a) permitido
  • minsize: el tamaño/longitud mínimo(a) permitido

Ejemplos: Verificar si la cadena de texto es más corta que 33 caracteres:

1
INPUT(_type='text', _name='name', requires=IS_LENGTH(32))

Verificar si la cadena de la clave es más corta que 5 caracteres:

1
INPUT(_type='password', _name='name', requires=IS_LENGTH(minsize=6))

Verificar si el archivo cargado tiene un tamaño entre 1KB y 1MB:

1
INPUT(_type='file', _name='name', requires=IS_LENGTH(1048576, 1024))

Para todos los tipos de campo, excepto para archivos, se verifica la longitud del valor. En el caso de archivos, el valor es una cookie.FieldStorage, de tal forma que este valida la longitud de los datos en el archivo, lo cual es el comportamiento que intuitivamente se podría esperar.

IS_LIST_OF

Esto no es exactamente un validador. Su uso específico es permitir la validación de los campos que devuelven múltiples valores. Se usa en los raros casos cuando un formulario contiene múltiples campos con el mismo nombre o una caja de selección múltiple. Su único argumento es otro validador, y todo lo que hace es aplicar el otro validador a cada elemento de la lista. Por ejemplo, la siguiente expresión verifica que cada ítem en una lista es un entero en el rango 0-10:

1
requires = IS_LIST_OF(IS_INT_IN_RANGE(0, 10))

Nunca retorna un error y no contiene un mensaje de error. El validador interno controla la generación de errores.

IS_LOWER

Este validador nunca devuelve un error. Solo convierte el valor a minúscula.

1
requires = IS_LOWER()

IS_MATCH

Este validador compara el valor con una expresión regular y devuelve un error si no coinciden. He aquí un ejemplo de uso para validar un código postal de los US:

1
2
requires = IS_MATCH('^\d{5}(-\d{4})?$',
         error_message='not a zip code')

Ejemplo de uso para validar una dirección IPv4:

1
2
requires = IS_MATCH('^\d{1,3}(\.\d{1,3}){3}$',
         error_message='not an IP address')

Ejemplo de uso para validar un número de teléfono en los US:

1
2
requires = IS_MATCH('^1?((-)\d{3}-?|\(\d{3}\))\d{3}-?\d{4}$',
         error_message='not a phone number')

Para más información en expresiones regulares de Python, referirse a la documentación oficial Python.

IS_NOT_EMPTY

Este validador verifica que el contenido del valor de un campo no es una cadena vacía.

1
requires = IS_NOT_EMPTY(error_message='cannot be empty!')

IS_TIME

Este validador verifica que el valor del campo contiene una hora válida en el formato especificado.

1
requires = IS_TIME(error_message=T('must be HH:MM:SS!'))

IS_URL

Rechaza una cadena URL si algo de lo que sigue se cumple:

  • La cadena está vacía o es None
  • La cadena usa caracteres que no se permiten en una URL
  • La cadena rompe cualquiera de las reglas de sintaxis de HTTP
  • El esquema especificado URL (si se especifica alguno) no es ‘http’ o ‘https’
  • El dominio de alto nivel (si se especifica un nombre de host) no existe

(Estas reglas se basan en RFC 2616 65)

Esta función solo verifica la sintaxis del URL. No verifica si la URL apunta a un documento real, por ejemplo, o que tenga sentido semántico. Esta función automáticamente anexa ‘http://‘ en frente de una URL en el caso de una URL abreviada(por ejemplo ‘google.ca’).

Si se usa el parámetro mode=’generic’, entonces el comportamiento de esta función cambia. Rechaza entonces una cadena URL si se cumple cualquiera de lo siguiente:

  • La cadena está vacía o es None
  • La cadena usa caracteres que no se permiten en una URL
  • El esquema especificado de la URL (si se especifica uno) no es válido

(Estas reglas se basan en RFC 2396 66)

La lista de los esquemas permitidos es personalizable con el parámetro allowed_schemes. Si se excluye None de la lista, entonces URLs abreviadas (que carecen de esquema tal como ‘http’) se rechazarán.

El esquema anexado por defecto puede personalizarse con el parámetro prepend_scheme. Si se asigna None a prepend_scheme, se deshabilitará el anexo de esquemas. Las URLs que requieren el anexo para ser analizadas serán aceptadas, pero el valor devuelto no será modificado.

IS_URL es compatible con el estándar Nombre de Dominio Internacionalizado (IDN por sus siglas en Inglés) especificado en RFC 34 67. Como resultado, URLs pueden ser cadenas regulares o unicode. Si el componente domino de la URL (ejemplo google.ca) contiene letras que no son US-ASCII, entonces el domino se convertirá a Punycode (definido en RFC 349 68. IS_URL va un poco más allá de los estándares, y permite caracteres que no son US-ASCII estar presentes tanto en los componentes de ruta como en los componentes de solicitud de la URL. Estos caracteres que no son US-ASCII se codificarán. Por ejemplo, los espacios serán codificados como ‘%20’. El caracter unicode con código hexadecimal 0x4e86 se convertirá en ‘%4e%86’.

Ejemplos:

requires = IS_URL())
requires = IS_URL(mode='generic')
requires = IS_URL(allowed_schemes=['https'])
requires = IS_URL(prepend_scheme='https')
requires = IS_URL(mode='generic',
                  allowed_schemes=['ftps', 'https'],
                  prepend_scheme='https')

IS_SLUG

1
requires = IS_SLUG(maxlen=80, check=False, error_message='must be slug')

Si a check se le asigna True verifica si el valor validado es un slug (permitiendo solo caracteres alfanuméricos y guiones que no se repiten).

Si a checks se le asigna False (por defecto) éste convierte el valor de entrada a slug.

IS_STRONG

Hace cumplir requerimientos de complejidad en un campo (usualmente el campo clave)

Ejemplo:

1
requires = IS_STRONG(min=10, special=2, upper=2)

donde

  • min es la longitud mínima del valor
  • special es el número mínimo de caracteres especiales requeridos que están entre estos !@#$%^&*(){}[]-+
  • upper es el número mínimo de caracteres en mayúsculas

IS_IMAGE

Este validador verifica si un archivo cargado a través de un archivo de entrada fué guardado en uno de los formatos de imágenes seleccionados y tiene dimensiones (ancho y alto) dentro de límites dados.

No verifica por el tamaño máximo del archivo (se usa IS_LENGTH para eso). Éste devuelve una falla en la validación si no se han cargado datos. Soporta los formatos de archivo BMP, GIF, JPEG, PNG, Y no requiere la Librería de Imágenes de Python.

El código fué tomado de la ref. 69

Toma los siguientes argumentos:

  • extensions: iterativo que contiene las extensiones de imágenes permitidas en minúsculas
  • maxsize: iterativo que contiene el ancho y altura máximo de la imagen
  • minsize: iterativo que contiene el ancho y altura mínimo de la imagen

Se usa (-1, -1) como minsize para saltarse la verificación de tamaño de la imagen.

Aquí algunos ejemplos:

  • verifica si el archivo cargado está en alguno de los formatos de imágenes permitidos:

    1
    requires = IS_IMAGE()
    
  • verifica si el archivo cargado es JPEG o PNG:

    1
    requires = IS_IMAGE(extensions=('jpeg', 'png'))
    
  • verifica si el archivo cargado es PNG con un tamaño máximo de 200x200 pixels:

    1
    requires = IS_IMAGE(extensions=('png'), maxsize=(200, 200))
    

IS_UPLOAD_FILENAME

El validador verifica si el nombre y la extensión del archivo cargado a través del archivo de entrada coincide con cierto criterio.

No se asegura del tipo de archivo de ninguna forma. Devuelve falla en la validación si no hay ningún dato cargado.

Sus argumentos son:

  • filename: expresión regular del nombre del archivo (antes del punto).
  • extension: expresión regular de la extensión (después del punto).
  • lastdot: que punto debe ser usado como separador de nombre de archivo / extensión: True indica último punto (por ejemplo, “file.tar.gz” se dividirá en “file.tar”+”gz”) mientras False significa el primer punto (por ejemplo, “file.tar.gz” se dividirá en “file”+”tar.gz).
  • case: 0 significa conservar el estilo; 1 significa transformar la cadena a minúsculas (por defecto); 2 significa trasformar la cadena a mayúsculas.

Si no hay puntos, la verificación de la extensión se hará contra cadenas vacías y la verificación del nombre de archivo se hará contra el valor completo.

Ejemplos:

verifica si un archivo tiene una extensión pdf (no sensible a mayúsculas/minúsculas):

1
requires = IS_UPLOAD_FILENAME(extension='pdf')

verifica si el archivo tiene una extensión tar.gz y nombre comenzando por backup:

1
requires = IS_UPLOAD_FILENAME(filename='backup.*', extension='tar.gz', lastdot=False)

verifica si un archivo no tiene extensión y un nombre README (sensible a mayúsculas/minúsculas):

1
requires = IS_UPLOAD_FILENAME(filename='^README$', extension='^$', case=0)

IS_IPV4

El validador verifica si el valor del campo es una dirección IP versión 4 en forma decimal. Se puede fijar para forzar direcciones de cierto rango.

La expresión regular de IPv4 tomada de la ref. 70 Sus argumentos son:

  • minip dirección más baja permitida; acepta: str, por ejemplo, 192.168.0.1; iteración de los números, por ejemplo, [192, 168, 0, 1]; int, por ejemplo, 3232235521
  • maxip dirección más alta permitida, acepta (igual que para minip)

Todos los valores de los tres ejemplos son iguales, ya que las direcciones se convierten a enteros para la verificación de inclusión con la siguiente función:

1
number = 16777216 * IP[0] + 65536 * IP[1] + 256 * IP[2] + IP[3]

Ejemplos:

Verificación de una dirección IPv4 válida:

1
requires = IS_IPV4()

Verificación de una dirección IPv4 de una red privada válida:

1
requires = IS_IPV4(minip='192.168.0.1', maxip='192.168.255.255')

IS_LOWER

Este validador nunca devuelve un error. Convierte el valor a minúsculas.

IS_UPPER

Este validador nunca devuelve un error. Convierte el valor a mayúsculas.

1
requires = IS_UPPER()

IS_NULL_OR

Es obsoleto, es un alias por IS_EMPTY_OR el cual se describe abajo.

IS_EMPTY_OR

Algunas veces se necesita permitir valores vacíos en un campo junto con otros requerimientos. Por ejemplo un campo puede ser una fecha pero también puede estar vacío. El validador IS_EMPTY_OR permite esto:

1
requires = IS_NULL_OR(IS_DATE())

CLEANUP

Esto es un filtro. Nunca falla. Solo remueve todos los caracteres cuyos códigos decimales ASCII no están en la lista [10, 13, 32-127].

1
requires = CLEANUP()

CRYPT

Esto también es un filtro. Esta desarrolla un hash seguro en la entrada y se usa para prevenir claves sean pasadas sin ser encriptadas a la base de datos.

1
requires = CRYPT()

Si una clave no se especifica, esta usa el algoritmo MD5. Si se especifica una clave, CRYPT usa el algoritmo HMAC. La clave puede contener un prefijo que determina el algoritmo a usar con HMAC, por ejemplo SHA512:

1
requires = CRYPT(key='sha512:thisisthekey')

Esta es la sintaxis recomendada. La clave tiene que ser una cadena única asociada a la base de datos usada. La clave nunca puede cambiarse. Si se pierde la clave, el hash hecho previamente se vuelve inútil.

Validadores de base de datos

IS_NOT_IN_DB

Considere el siguiente ejemplo:

1
2
db.define_table('person', Field('name'))
db.person.name.requires = IS_NOT_IN_DB(db, 'person.name')

Requiere que cuando se inserta una persona nueva, su nombre no esté ya en la base de datos, db, en el campo person.name. Como con todos los validadores este requerimiento se hace cumplir al nivel de procesamiento del formulario, no a nivel de la base de datos. Esto significa que hay la pequeña probabilidad de que, si dos visitantes tratan de insertar registros de manera concurrente con el mismo person.name, esto resulta en una condición de carrera y se aceptan ambos registros. Es por lo tanto más seguro informar también a la base de datos que este campo debe tener un valor único:

1
2
db.define_table('person', Field('name', unique=True))
db.person.name.requires = IS_NOT_IN_DB(db, 'person.name')

Ahora, si la condición de carrera ocurre, la base de datos levanta un error ‘OperationalError’ y uno de los dos registro insertados es rechazado.

El primer argumento de IS_NOT_IN_DB puede ser una conexión de base de datos o un conjunto. En el segundo caso, se estará verificando solo por el conjunto definido.

El siguiente código, por ejemplo, no permite el registro de dos personas con el mismo nombre en un intervalo de 10 días entre los dos:

1
2
3
4
5
6
7
import datetime
now = datetime.datetime.today()
db.define_table('person',
    Field('name'),
    Field('registration_stamp', 'datetime', default=now))
recent = db(db.person.registration_stamp>now-datetime.timedelta(10))
db.person.name.requires = IS_NOT_IN_DB(recent, 'person.name')

IS_IN_DB

Considere las siguientes tablas y requerimiento:

db.define_table('person', Field('name', unique=True))
db.define_table('dog', Field('name'), Field('owner', db.person)
db.dog.owner.requires = IS_IN_DB(db, 'person.id', '%(name)s',
                                 zero=T('choose one'))

Esto se hace cumplir al nivel los formularios INSERT/UPDATE/DELETE del perro. Requiere que un dog.owner sea un id válido en el campo person.id en la base de datos db. Debido a este validador, el campo dog.owner se representa como un listado desplegable. El tercer argumento del validador es una cadena que describe los elementos del listado desplegable. En el ejemplo se quiere ver la persona %(name)s en vez de la persona %(id)s. %(...)s se reemplaza por el valor del campo en corchetes por cada registro.

La opción zero trabaja muy parecido a como lo hace con el validador IS_IN_SET.

Si se quiere validar el campo, pero no se quiere una lista desplegable, se debe poner el validador en una lista.

1
db.dog.owner.requires = [IS_IN_DB(db, 'person.id', '%(name)s')]

El primer argumento del validador debe ser una conexión a la base de datos o a un conjunto DAL, como en IS_NOT_IN_DB.

Ocasionalmente se puede querer el listado desplegable (por lo cual no se quiere usar la lista de sintaxis que se menciona arriba) y aún así no se quieren usar validadores adicionales. Para esto el validador IS_IN_DB toma el argumento extra _and que puede apuntar a la lista de otros validadores aplicados si el valor validado pasa la validación IS_IN_DB. Por ejemplo para validar todos los dueños de perros en db que no están en un subconjunto:

1
2
3
subset=db(db.person.id>100)
db.dog.owner.requires = IS_IN_DB(db, 'person.id', '%(name)s',
                                 _and=IS_NOT_IN_DB(subset,'person.id'))

IS_IN_DB y Etiquetamiento

El validador IS_IN_DB tiene un atributo opcional multiple=False. Si se le asigna True, múltiples valores pueden ser guardados en un campo. Este campo debe ser del tipo list:reference como se discutió en el Capítulo 6. Un ejemplo explícito de etiquetamiento se discute allí. Referencias multiple se manejan automáticamente en formularios de creación y actualización, pero son transparentes al DAL. Se insiste en sugerir el plugin jQuery multiselect para hacer varios registros.

Validadores personalizados

Todos los validadores siguen el prototipo que se muestra:

class sample_validator:
    def __init__(self, *a, error_message='error'):
        self.a = a
        self.e = error_message
    def __call__(value):
        if validate(value):
            return (parsed(value), None)
        return (value, self.e)
    def formulariotter(self, value):
        return formulariot(value)

Es decir, cuando se llama para validar un valor, un validador devuelve una tupla (x, y). Si y es None, entonces el valor pasa la validación y x contiene un valor ya analizado. Por ejemplo, si el validador requiere que el valor sea un entero, x se convierte a int(value). Si el valor no pasa la validación, entonces x contiene el valor de entrada e y contiene un mensaje de error que explica la validación fallida. Este mensaje de error se usa para reportar el error en formularios que no validan.

El validador puede también contener un método formatter. Éste debe llevar a cabo la conversión opuesta a la que __call__ hace. Por ejemplo, considere el código fuente para IS_DATE:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class IS_DATE(object):
    def __init__(self, formulariot='%Y-%m-%d', error_message='must be YYYY-MM-DD!'):
        self.formulariot = formulariot
        self.error_message = error_message
    def __call__(self, value):
        try:
            y, m, d, hh, mm, ss, t0, t1, t2 = time.strptime(value, str(self.formulariot))
            value = datetime.date(y, m, d)
            return (value, None)
        except:
            return (value, self.error_message)
    def formulariotter(self, value):
        return value.strftime(str(self.formulariot))

Si es exitoso, el método __call__ lee una cadena de fecha del formulario y la convierte en un objeto datetime.date usando el formato de cadena especificado en el constructor. El objeto formatter toma el objeto datetime.date y lo convierte a un representación en cadena usando el mismo formato. El formatter se llama automáticamente en formularios, pero también puede llamarse explícitamente para convertir objetos a su correcta representación. Por ejemplo:

1
2
3
4
5
6
7
>>> db = DAL()
>>> db.define_table('atable',
       Field('birth', 'date', requires=IS_DATE('%m/%d/%Y')))
>>> id = db.atable.insert(birth=datetime.date(2008, 1, 1))
>>> row = db.atable[id]
>>> print db.atable.formulariotter(row.birth)
01/01/2008

Cuando se requieren múltiples validadores (y son guardados en una lista), ellos se ejecutan en orden y la salida de uno se pasa como entrada del siguiente. La cadena se rompe cuando uno de los validadores falla.

A la inversa, cuando se llama al método formatter de un campo, los formatters de los validadores asociados se encadenan también, pero en orden inverso.

Validadores con Dependencias

De vez en cuando, se necesita validar un campo y el validador depende del valor de otro campo. Esto puede hacerse, pero requiere la asignación del validador en el controlador, cuando el valor de los otros campos es conocido. Por ejemplo, aquí está una página que genera un formulario de inscripción que pregunta por un nombre de usuario y una clave dos veces. Ninguno de los campos puede estar vacío, y las dos claves deben coincidir:

1
2
3
4
5
6
7
8
9
def index():
    form = SQLFORM.factory(
        Field('username', requires=IS_NOT_EMPTY()),
        Field('password', requires=IS_NOT_EMPTY()),
        Field('password_again',
              requires=IS_SAME_AS(request.vars.password)))
    if form.accepts(request.vars, session):
        pass # or take some action
    return dict(form=form)

El mismo mecanismo se puede aplicar a objetos FORM y SQLFORM.

Widgets

Aquí una lista de los widgets web2py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
SQLFORM.widgets.string.widget
SQLFORM.widgets.text.widget
SQLFORM.widgets.password.widget
SQLFORM.widgets.integer.widget
SQLFORM.widgets.double.widget
SQLFORM.widgets.time.widget
SQLFORM.widgets.date.widget
SQLFORM.widgets.datetime.widget
SQLFORM.widgets.upload.widget
SQLFORM.widgets.boolean.widget
SQLFORM.widgets.options.widget
SQLFORM.widgets.multiple.widget
SQLFORM.widgets.radio.widget
SQLFORM.widgets.checkboxes.widget
SQLFORM.widgets.autocomplete

Los primeros diez de ellos son los predeterminados para los tipos de campos correspondientes. El widget “options” se usa cuando los requerimientos de un campo son IS_IN_SET o IS_IN_DB con multiple=False (comportamiento por defecto). El widget “multiple” se usa cuando los requerimientos de un campo son IS_IN_SET o IS_IN_DB con multiple=True. Los widgets “radio” y “checkboxes” nunca se usan por defecto, pero pueden asignarse manualmente. El widget autocomplete es especial y se discute en la próxima sección.

Por ejemplo, para tener un campo “string” representado por una área de texto:

1
Field('comment', 'string', widget=SQLFORM.widgets.text.widget)

Se pueden crear widget nuevos o extender widgets existentes.

SQLFORM.widgets[type] es una clase y SQLFORM.widgets[type].widget es una función de miembro estática de la correspondiente clase. Cada función widget toma dos argumentos: el objeto campo, y el valor actual de ese campo. Devuelve la representación del widget. Como ejemplo, el widget string puede ser recodificado como sigue:

1
2
3
4
5
6
7
def my_string_widget(field, value):
    return INPUT(_name=field.name,
                 _id="%s_%s" % (field._tablename, field.name),
                 _class=field.type,
                 _value=value,
                 requires=field.requires)
Field('comment', 'string', widget=my_string_widget)

Los valores de id y class deben seguir la convención descrita más adelante en este capítulo. Un widget puede contener sus propios validadores, pero es una buena práctica asociar los validadores al atributo “requires” del campo y hacer que el widget los tome desde allí.

Widget de autocompletar

Hay dos usos posibles para el widget de autocompletar: autocompletar un campo que toma un valor de una lista o autocompletar un campo de referencia (donde la cadena a autocompletarse es una representación de la referencia implementada como un id ).

El primer caso es fácil:

db.define_table('category',Field('name'))
db.define_table('product',Field('name'),Field('category')
db.product.category.widget = SQLHTML.widgets.autocomplete(
     request, db.category.name, limitby=(0,10), min_length=2)

Donde limitby intruye el widget para que despliegue no más de 10 sugerencias a la vez, y min_length instruye al widget a realizar una llamada Ajax para obtener sugerencias solo después de que el usuario ha escrito al menos dos caracteres en el campo de búsqueda.

El segundo caso es más complejo:

db.define_table('category',Field('name'))
db.define_table('product',Field('name'),Field('category')
db.product.category.widget = SQLHTML.widgets.autocomplete(
     request, db.category.name, id_field=db.category.id)

En este caso el valor de id_field le dice al widget que aún cuando el valor a ser autocompletado es un db.category.name, el valor a ser guardado es el correspondiente db.category.id. Un parámetro opcional es orderby que instruye al widget en como ordenar las sugerencias (por defecto alfabéticamente).

Este widget trabaja por medio de Ajax. ¿Donde está la llamada Ajax? Este widget trae algo de magia. La llamada es un método del objeto widget por si mismo. ¿Cómo se expone? En web2py cualquier pedazo de código puede generar una respuesta levantando una excepción HTTP. Este widget explota esta posibilidad de la siguiente forma: El widget envía la llamada Ajax a la misma URL que generó el widget y pone una ficha especial en request.vars. El widget genera una instancia de nuevo, encuentra el token y levanta la excepción HTTP que responde a la solicitud. Todo esto se hace de manera transparente al desarrollador.