Introducción a web2py

Introducción a web2py



web2py es un framework de código abierto para desarrollar aplicaciones web en Python. Se distingue por su sencillez y su capacidad para montar proyectos funcionales rápidamente; la filosofía de web2py es que todo tenga un comportamiento por defecto, de modo que podemos enfocarnos en tener un prototipo de nuestro proyecto totalmente funcional y luego ocuparnos de los detalles.

Por esta razón incluye una gran cantidad de herramientas muy fáciles de implementar, tales como autenticación de usuarios (registro, inicio de sesión, recuperar contraseña, entre otros), sesiones, una capa de abstracción de base de datos, formularios y grillas inteligentes, helpers para generar código HTML en forma dinámica desde Python, un potente sistema de caché, varias funciones para trabajar con AJAX, soporte para servicios web como REST y SOAP, y muchas otras cosas interesantes. Todas ellas atravezadas por una estricta política de seguridad: en web2py, el programador no debe hacer decisiones de ese tipo. La capa de abstracción de base de datos, por ejemplo, elimina completamente la posibilidad de inyecciones SQL, y los formularios inteligentes previenen ataques CSRF.

Siempre que me intereso por una tecnología me gusta empaparme de unos cuantos códigos de ejemplo para tener una primer impresión del proyecto. Esta introducción pretende justamente abordar web2py desde lo pragmático e ir desarrollando una pequeña aplicación ─completa hacia el final del artículo─ para ir conociendo de qué se trata este web framework.

Preparando el entorno

Antes de comenzar debemos asegurarnos de tener web2py instalado en nuestro ordenador. Para ello vamos a dirigirnos a www.web2py.com/init/default/download y descargarlo. web2py se distribuye tanto como una aplicación independiente (no requiere Python instalado) o bien con su código de fuente. Como presumiblemente ya tienes Python instalado, iremos por la segunda opción.

Descargar web2py

(La sección de descargas erróneamente indica que web2py únicamente soporta Python 2.6 y 2.7, aunque realmente Python 3 es totalmente soportado).

Una vez obtenido, vamos a descomprimir el archivo web2py_src.zip en alguna ubicación cómoda y ejecutar python web2py/web2py.py. Esto abrirá el servidor de desarrollo de web2py (al igual que otros web frameworks, se incluye un servidor HTTP básico para ser empleado durante la ejecución local de una aplicación, y así evitar tener que instalar herramientas de producción como Apache o NGINX), que se ve más o menos así.

web2py server

Podemos opcionalmente configurar la dirección en la que escuchará peticiones el servidor, pero lo que nos interesa aquí es establecer una contraseña de administrador. Cuando la hayas ingresado, presiona start server para iniciar web2py.

Creando una aplicación

web2py incluye dos aplicaciones por defecto, llamadas welcome y admin. La primera es sencillamente una aplicación de ejemplo que sirve como base para desarrollar nuevos proyectos. La segunda es el gestor de aplicaciones, desde donde podemos crear, editar, remover, empaquetar, compilar ─y otras cosas más─ aplicaciones de web2py, y al cual accedemos vía http://127.0.0.1:8000/admin.

Ahora bien, dijimos que toda aplicación creada desde el gestor es una copia de welcome, por lo que ya incluye algunas funcionalidades por defecto. Puesto que aún no hemos visto nada concreto de web2py, será mejor empezar por una aplicación (casi) completamente vacía. Las aplicaciones ─sean creadas por el gestor o manualmente─ están alojadas en web2py/applications/, y no son más que un conjunto de archivos y carpetas.

La pequeña aplicación que estaremos desarrollando en el artículo será un blog. Descarguemos, entonces, la estructura de una aplicación prácticamente en blanco desde este enlace. Acto seguido crearemos una nueva carpeta dentro de web2py/applications/, a la que llamaremos blog, y descomprimiremos allí el contenido del archivo que hemos descargado. (¡Es importante que el contenido del archivo ZIP sea descomprimido directamente dentro de web2py/applications/blog/, no dentro de /blog/emptyapp/!).

La estructura de carpetas de nuestra aplicación debería ser la siguiente.

web2py
  |- applications
       |- blog
            |- cache
            |- controllers
            |- cron
            |- etc.

«Hola mundo»

Ya deberíamos ser capaces de acceder a nuestra aplicación vía http://127.0.0.1:8000/blog. Como no hemos hecho nada aún, simplemente verás en pantalla el siguiente mensaje.

invalid function (default/index)

Un mismo servidor de web2py puede ejecutar múltiples aplicaciones. Para discriminar a cuál de ellas queremos acceder, indicamos su nombre luego del dominio o dirección. Así, accedemos a nuestro blog vía http://127.0.0.1:8000/blog, a la aplicación welcome en http://127.0.0.1:8000/welcome, y al gestor de aplicaciones a través de http://127.0.0.1:8000/admin. No obstante, éste es solo el comportamiento por defecto; podemos configurar web2py para que acceda a alguna aplicación en particular al visitar http://127.0.0.1:8000/ y remover la necesidad de indicar su nombre en la URL.

Para darle una primera funcionalidad a nuestro blog, vamos a editar el archivo blog/controllers/default.py e ingresar el siguiente código.

# -*- coding: utf-8 -*-

def index():
    return "¡Hola, mundo!"

Recarga la página y deberás ver el mensaje en pantalla. ¡Felicitaciones! Hemos hecho nuestro primer código en web2py.

Estructura de las URL

Las direcciones de URL en web2py por defecto están asociadas con una aplicación, un archivo de Python y una función. Ya vimos que http://127.0.0.1:8000/blog invoca a la función index() dentro del archivo controllers/default.py y envía su resultado al navegador. Pero es interesante saber que dicha dirección no es sino un atajo para http://127.0.0.1:8000/blog/default/index. Aquí observamos claramente cuál es la esructura de una URL en web2py: /a/c/f, donde a indica el nombre de una aplicación, c el nombre de un archivo (sin la extensión .py) dentro de la carpeta controllers, y f el de una función dentro este archivo. A cada uno de los archivos dentro de la carpeta controllers se los conoce como controladores.

Esta estructura de las direcciones de URL puede ser alterada para producir URL amigables. Por ejemplo, para que nuestra función index() dentro de controllers/default.py sea accedida vía http://127.0.0.1:8000/hola-mundo, aunque no lo estaremos tratando en esta introducción. Para un artículo sobre el tema véase URL amigables en web2py.

Vistas o plantillas

Si bien podemos retornar código HTML directamente desde nuestra función, es considerado una buena práctica separarlo de la lógica de la aplicación. Para esto vamos a crear un nuevo archivo blog/views/default/index.html con el siguiente código.

<!DOCTYPE html>
<html>
  <h1>¡Hola, mundo!</h1>
</html>

Nótese que el nombre del archivo coincide con el del controlador y de la función con la cual queremos asociar la vista.

Ahora bien, debemos hacer una pequeña modificación a nuestra función index() para indicarle que debe retornar el contenido de la vista.

def index():
    return {}

Ahora vista http://127.0.0.1:8000/blog/default/index y verás el mensaje con el formato que hemos especificado.

A menudo debemos enviar datos desde el controlador a la vista. Por ejemplo, si queremos desplegar un saludo personalizado según el nombre que se indique en la URL, modificamos el controlador así:

def index():
    # Obtener el nombre pasado en la URL o bien la cadena
    # "mundo" si no se ha especificado ninguno.
    name = request.args(0) or "mundo"
    # Enviarlo a la vista como el objeto "name".
    return {"name": name}

Y en la vista:

<!DOCTYPE html>
<html>
  <h1>¡Hola, {{=name}}!</h1>
</html>

De este modo, nuestra vista mostrará “¡Hola, Juan!” al visitar http://127.0.0.1:8000/blog/default/index/Juan y “¡Hola, mundo!” si ingresamos http://127.0.0.1:8000/blog/default/index a secas.

request es un objeto que contiene información sobre la petición HTTP que estamos procesando. Nótese que no es necesario importarlo.

Seguiremos abordando estas cuestiones más adelante.

Modelos

Ya hemos mencionado los controladores, que contienen la lógica de la aplicación y cuyas funciones son puntos de acceso para las direcciones de URL, y las vistas, que permiten separar el código HTML (frontend) del backend. Ahora introduciremos los modelos, quienes se encargarán de definir una base de datos y sus respectivas tablas. Por cuanto los modelos se ocupan principalmente de esta tarea, una aplicación que no requiera persistencia de datos bien puede prescindir de ellos.

web2py incluye una librería llamada pyDAL, que nos evita tener que escribir código SQL manualmente (y, de paso, nos releva de los chequeos de seguridad que le son propios) y en su lugar nos provee una sintaxis más pythonica. pyDAL surge con web2py pero es un proyecto independiente, que puede ser empleando con otros frameworks e incluso en aplicaciones no relacionadas con la web. Por ello tenemos todo un artículo para que lo conozcas: pyDAL – Capa de abstracción de base de datos.

Dado que nuestra intención es desarrollar una web con funcionalidad de blog básica, empecemos por definir una base de datos y una tabla que contendrá las publicaciones (posts) del blog. Por convención, esto se hace en el archivo models/db.py.

# -*- coding: utf-8 -*-

db = DAL("sqlite://storage.db")

En esta línea creamos una instancia de la clase DAL, indicándole qué motor de base de datos queremos utilizar y, por tratarse de SQLite, el nombre del archivo en donde se alojan las tablas. Para otros motores la cadena pasada como argumento tendrá un formato diferente; por ejemplo, mysql://usuario:contraseña@localhost/nombrebd.

Acto seguido definimos la tabla post.

db.define_table(
    "post",
    Field("title", type="string", length=256),
    Field("content", type="text"),
    Field("image", type="upload"),
    Field("comments", type="list:string")
)

El primer argumento pasado a define_table() es el nombre de la propia tabla; a continuación se pasan instancias de Field que representan sus campos o columnas. Primero se indica el nombre, seguido del tipo de dato que contendrá vía el argumento type, y luego le suceden otros argumentos que dependerán del tipo de dato especificado (por ejemplo, el tamaño máximo de la cadena en los campos del tipo "string"). Nótese que type corresponde a nombres de tipos de pyDAL, no del motor de base de datos subyacente.

Ahora bien, ¿qué hacemos con este archivo models/db.py? No hay que ejecutarlo ni nada por el estilo, web2py se ocupa de ello en cada petición. Los modelos son los primeros archivos en ejecutarse, y todos los objetos definidos en ellos son visibles para los controladores. De modo que el archivo storage.db se creará dentro de la carpeta databases y la tabla post definida automáticamente la próxima vez que ingresemos a nuestra web. Si hacemos alguna modificación al esquema de nuestra tabla, pyDAL lo detecta y se encarga de hacer los cambios pertinentes. Esto se conoce como migraciones automáticas y puede ser desactivado en producción para mayor rapidez.

Creando una publicación

Ya tenemos el esquema de nuestra base de datos listo para ser utilizado. Es hora de exponer un formulario para crear nuevas publicaciones en nuestro blog. Lo haremos en el controlador default.py.

def new_post():
    form = SQLFORM(db.post)
    form.process()
    return {"form": form}

SQLFORM es una clase que recibe una tabla de la base de datos y en base a sus campos retorna el formulario correspondiente. El método process() procesa la petición POST que envía los datos desde el navegador y se encarga de añadirlos a la base de datos. Pero aún falta la vista; hagamos eso creando el archivo views/default/new_post.html.

<!DOCTYPE html>
<html>
  <h1>Nueva publicación</h1>
  {{=form}}
</html>

Ahora visita http://127.0.0.1:8000/blog/default/new_post y deberás ver un formulario como el siguiente.

Formulario web2py

Haz la prueba de ingresar algunos datos cualesquiera, no es menester completar todos los campos. Luego abre con algún visor de SQLite el archivo databases/storage.db y verás que efectivamente el formulario ha ingresado los datos a la tabla post. web2py hace de este tipo de tareas algo realmente sencillo.

Ahora bien, todavía debemos ajustar algunas tuercas. En primer lugar, habrás observado que SQLFORM incluye todos los campos de la tabla al generar el formulario. Eso incluye los comentarios, que no debería ser algo a completar al momento de crear una nueva publicación. Podemos indicar específicamente cuáles son los campos para los cuales queremos permitir la entrada.

def new_post():
    form = SQLFORM(db.post, fields=["title", "content", "image"])
    # ...

Por otro lado, por defecto la referencia en el formulario de cada uno de los campos es creada a partir del nombre que tienen en la base de datos (Title, Content, Image). A menudo será mejor indicarlos explícitamente.

db.define_table(
    "post",
    Field("title", type="string", length=256, label="Título"),
    Field("content", type="text", label="Contenido"),
    Field("image", type="upload", label="Imagen"),
    Field("comments", type="list:string")
)

Habrás notado que, hasta ahora, incluso se aceptarán formularios totalmente vacíos, puesto que no hemos establecido ninguna restricción. Haremos que el título y el contenido sean indispensables para enviar el formulario; la imagen bien puede ser opcional.

db.define_table(
    "post",
    Field("title", type="string", length=256, label="Título",
          requires=IS_NOT_EMPTY()),
    Field("content", type="text", label="Contenido",
          requires=IS_NOT_EMPTY()),
    Field("image", type="upload", label="Imagen"),
    Field("comments", type="list:string")
)

Con esta modificación, verás que se mostrará el mensaje “Enter a value” debajo del título y el contenido en caso de estar vacíos. Aun este mensaje es configurable.

    Field("title", type="string", length=256, label="Título",
          requires=IS_NOT_EMPTY(error_message="Ingrese un título.")),
    Field("content", type="text", label="Contenido",
          requires=IS_NOT_EMPTY(error_message="Escriba el contenido.")),

Viendo una publicación

Crearemos una nueva función que nos permita visualizar las publicaciones que hemos creado. La idea es que se acceda a cada uno de ellos vía 127.0.0.1:8000/blog/default/view_post/id, donde id es el identificador numérico de la publicación que queremos mostrar. Este número único es almacenado en el campo id de la tabla post, que es creado y generado automáticamente por web2py en cada inserción.

Definamos, entonces, esta función según lo planeado.

def view_post():
    post_id = request.args(0, cast=int)
    post = db(db.post.id == post_id).select().first()
    return {"post": post}

Y la vista views/default/view_post.html:

<!DOCTYPE html>
<html>
  <h1>{{=post.title}}</h1>
  <p>
    {{=post.content}}
  </p>
  <h3>Comentarios</h3>
  {{if post.comments is not None:}}
      {{for comment in post.comments:}}
          <div class="comment">
            {{=comment}}
          </div>
      {{pass}}
  {{else:}}
      No hay comentarios.
  {{pass}}
</html>

Aquí hemos utilizado un poco de código Python dentro de la vista para mostrar cada uno de los comentarios. La única diferencia es que debemos emplear {{pass}} para delimitar los bloques de código, dado que en las vistas la sangría es opcional.

Ahora, si has creado alguna publicación con el formulario que desarrollamos en el apartado anterior, podrás verla en http://127.0.0.1:8000/blog/default/view_post/1 (o bien chequea con tu visor de base de datos qué identificador numérico le fue asignado, si acaso insertaste formularios vacíos).

Pero claro, será mejor listar todos las publicaciones en el inicio de nuestra web. Reemplacemos nuestra antigua función index() por la siguiente.

def index():
    posts = db(db.post.id > 0).select(db.post.id, db.post.title)
    return {"posts": posts}

En este caso pasamos a select() el nombre de los campos que queremos seleccionar, ya que no estaremos utilizando más que esos dos, para que la consulta sea más rápida.

Y la vista correspondiente:

<!DOCTYPE html>
<html>
  <h1>Publicaciones</h1>
  <ul>
    {{for post in posts:}}
        <li><a href='{{=URL("view_post", args=[post.id])}}'>{{=post.title}}</a></li>
    {{pass}}
  </ul>
</html>

Aquí introdujimos una nueva función, URL(), que debe ser usada siempre que queremos generar direcciones de URL al interior de nuestra aplicación. Se encarga de crear URL relativas independientemente del lugar desde donde sea invocada.

Hasta aquí tenemos una página de entrada que lista todas las publicaciones y provee enlaces de acceso a ellas, una página para crear publicaciones y otra para visualizarlas. Tenemos pendiente aún los comentarios y la imagen. Los abordaremos más adelante; antes me gustaría detenerme en la siguiente cuestión: ¡cualquier persona que acceda a http://127.0.0.1:8000/blog/default/new_post será capaz de crear una publicación en nuestro sitio! Debemos, por ende, incorporar un sistema de autenticación de usuarios y establecer permisos de acceso para dicha función.

Autenticación y autorización

Para empezar a utilizar el sistema de autenticación de web2py debemos conectarlo a la base de datos y definir las tablas en las que se almacenará la información de los usuarios. Para ello vamos a modificar nuestro modelo db.py de la siguiente manera.

from gluon.tools import Auth

db = DAL("sqlite://storage.db")
auth = Auth(db)
auth.define_tables(username=True)

# ...

Vía username=True le indicamos a web2py que los usuarios ingresarán a partir de un nombre de usuario en lugar de direcciones de correo electrónico, que es la configuración por defecto.

Lo siguiente es crear un formulario de registro de usuarios y otro para iniciar sesión. Haremos eso en nuestro controlador default.py.

def user():
    if request.args(0) == "login":
        title = "Iniciar sesión"
        form = auth.login()
    elif request.args(0) == "signup":
        title = "Registrarse"
        form = auth.register()
    else:
        raise HTTP(404)
    return {"form": form, "title": title}

Lo novedoso aquí es la excepción HTTP, que invocamos para indicar que la página no se ha encontrado (404). Aunque bien puede ser configurado de otro modo, por defecto web2py mostrará “404 NOT FOUND” cuando la excepción sea lanzada.

Continuemos con la vista correspondiente para estos dos formularios, views/default/user.py.

<!DOCTYPE html>
<html>
  <h1>{{=title}}</h1>
  {{=form}}
</html>

Por último, aplicamos el decorador auth.requires_login() a cada una de las funciones que lo propicien.

@auth.requires_login()
def new_post():
    # ...

Ahora, al acceder a http://127.0.0.1:8000/blog/default/new_post sin haber iniciado sesión, web2py redireccionará automáticamente a http://127.0.0.1:8000/blog/default/user/login.

El objeto auth tiene muchísimas más funcionalidades de las que podremos exponer aquí; véase el capítulo 9 del libro oficial de web2py para una cobertura completa.

Ahora bien, sería conveniente mostrar algún tipo de mensaje al inicio de cada página para indicar que se ha iniciado sesión (en alguna aplicación más compleja, esto sería una barra de herramientas). Puesto que tenemos múltiples vistas, habría que repetir este código en cada una de ellas. Para evitar esto, haremos uso de la funcionalidad de herencia.

Herencia en las vistas

Además del mensaje de autenticación que tenemos pendiente, habrás observado que todas las vistas tienen una estructura bastante similar. En nuestra pequeña aplicación se trata de la siguiente estructura.

<!DOCTYPE html>
<html>
    <!-- ... -->
</html>

Pero típicamente las aplicaciones reales compartirán una cabecera (header) y un pie de página (footer). Para ello vamos a abstraer esta estructura común en un único archivo, del cual luego heredarán el resto de las vistas.

Creemos entonces el archivo en cuestión, al que le daremos el nombre de views/default/base.html.

<!DOCTYPE html>
<html>
  {{include}}
</html>

Habrás deducido que en donde nosotros escribimos {{include}}, allí web2py colocará el código de las vistas que hereden de ésta. Es correcto.

Modifiquemos ahora nuestras cuatro vistas para que hereden de base.html.

{{extend "default/base.html"}}
<h1>Publicaciones</h1>
<ul>
  {{for post in posts:}}
      <li><a href='{{=URL("view_post", args=[post.id])}}'>{{=post.title}}</a></li>
  {{pass}}
</ul>

{{extend "default/base.html"}}
<h1>{{=title}}</h1>
{{=form}}

{{extend "default/base.html"}}
<h1>Nueva publicación</h1>
{{=form}}

{{extend "default/base.html"}}
<h1>{{=post.title}}</h1>
<p>
  {{=post.content}}
</p>
<h3>Comentarios</h3>
{{if post.comments is not None:}}
    {{for comment in post.comments:}}
        <div class="comment">
          {{=comment}}
        </div>
    {{pass}}
{{else:}}
    No hay comentarios.
{{pass}}

Ahora podemos modificar base.html para que incluya un mensaje cuando se haya iniciadio la sesión, que se verá en todas las vistas.

<!DOCTYPE html>
<html>
{{if auth.user is not None:}}
    <header>
      Has iniciado sesión como {{=auth.user.username}}.
    </header>
{{pass}}
  {{include}}
</html>

Comentarios

Añadiremos un formulario a nuestra función view_post() que permita agregar un comentario a la publicación.

def view_post():
    post_id = request.args(0, cast=int)
    post = db(db.post.id == post_id).select().first()
    comment_form = FORM(
        "Comentario:",
        BR(),
        TEXTAREA(_name="comment", requires=IS_LENGTH(2000, minsize=1)),
        BR(),
        BUTTON("Enviar comentario", _type="submit")
    )
    if comment_form.process().accepted:
        s = db(db.post.id == post_id)
        comments = s.select().first().comments
        if comments is None:
            comments = [comment_form.vars.comment]
        else:
            comments.append(comment_form.vars.comment)
        s.update(comments=comments)
        redirect(URL(args=request.args))
    return {"post": post, "comment_form": comment_form}

En primer lugar, creamos un formulario vía la clase FORM, y le pasamos como argumento algunos controles tal como lo haríamos en HTML. Todas las etiquetas de HTML tienen su correspondiente en web2py (a la etiqueta <br> le corresponde la clase BR; a <textarea>, TEXTAREA; etc.), y los atributos se especifican como argumentos anteponiendo un guión bajo (_name, por ejemplo, se traduce como el atributo name en HTML).

Este tipo de clases que representan etiquetas de HTML, a las que web2py le reserva la convención de escribirlos en mayúsculas, se los denomina helpers. FORM es un helper un tanto particular, puesto que también se encarga de manejar los datos enviados a través del formulario y validarlos según los validadores que hemos indicado a cada control vía el argumento requires.

Luego procesamos el formulario y chequeamos que haya sido validado vía el atributo accepted; en cuyo caso procedemos a agregar el comentario ingresado (form.vars.comment) a la base de datos actualizando (a través del método update()) la información de la publicación actual.

Por último actualizamos la página haciendo una redirección a la URL actual para visualizar el nuevo comentario.

Simplemente nos resta modificar la vista views/default/view_post.html para que despliegue el formulario.

<!-- ... -->
<h3>Comentarios</h3>
<div class="leave-comment">
  {{=comment_form}}
</div>
<!-- ... -->

Imágenes

Nos había quedado pendiente el mostrar la imagen de la publicación. Los archivos cargados a través de formularios en campos del tipo "upload" son almacenados con un nombre especial en la carpeta uploads del directorio de nuestra aplicación. Para poder hacer referencia a ellos a través de una URL ─que es justamente lo que necesitamos para mostrar la imagen─ debemos crear una función que se encargue de leer el archivo especificado de la carpeta uploads y retornar su contenido. La aplicación de ejemplo welcome nos proporciona esa misma función; la agregaremos a nuestro default.py.

@cache.action()
def download():
    """
    allows downloading of uploaded files
    http://..../[app]/default/download/[filename]
    """
    return response.download(request, db)

Por último, editemos el archivo views/default/view_post.html para que muestre la imagen de la publicación, en caso de haber.

<!-- ... -->
<h1>{{=post.title}}</h1>
{{if post.image:}}
    <img src='{{=URL("download", args=post.image)}}' />
{{pass}}
<!-- ... -->

Conclusión

Hemos recorrido las principales características de web2py: cómo responder a una petición HTTP a través de los controladores, conectarse con una base de datos y definir la estructura de las tablas vía pyDAL, crear formularios desde Python a partir de estos modelos o bien usando helpers, usar plantillas para separar la parte gráfica de la lógica de nuestra aplicación, implementar un sistema de autenticación y autorización de usuarios, entre otras.

¡Hay mucho más por descubrir! web2py es un framework pensado, como decíamos al comienzo, para desarrollar aplicaciones web rápidamente. Entre las funcionalidades que no hemos abordado encontrarás un sistema de caché, sesiones, formas de autenticación diversas y más amplias, servicios REST y SOAP, utilidades para trabajar con AJAX, componentes, complementos, herramientas para interactuar con servidores de correo electrónico, y la lista sigue… Todo ello lo encontrarás felizmente documentado en el libro oficial del proyecto.

De esta pequeña introducción esperamos que haya servido para conocer la metodología de web2py y eventualmente te decidas por iniciar tu próximo proyecto con él.



2 comentarios.

Deja un comentario