Chapter 3: Resumen

Resumen

Inicio

Linux
Mac
Windows

web2py viene con paquetes binarios para Windows y Mac OS X. Estos paquetes incluyen el intérprete de Python para que no necesites tenerlo preinstalado. También hay una versión de código fuente que corre en Mac, Linux, y otros sistemas Unix. Se asume para el paquete de código fuente que se cuenta con el intérprete de Python instalado en la computadora.

web2py no requiere instalación. Para iniciarlo, descomprime el archivo zip descargado para tu sistema operativo específico y ejecuta el archivo web2py correspondiente.

En Unix y Linux (distribución de código fuente), ejecuta el siguiente comando:

python web2py.py

En OS X (distribución binaria), debes correr:

open web2py.app

En Windows (distribución binaria), corre el siguiente comando:

web2py.exe

En Windows (distribución de código fuente), ejecuta:

c:/Python27/python.exe web2py.exe

Importante: para correr la distribución de código fuente de web2py en Windows debes instalar primero las extensiones de Python para Windows de Mark Hammond desde http://sourceforge.net/projects/pywin32/

El programa web2py acepta varias opciones de la línea de comandos que se describen más adelante.

Por defecto, al iniciar, web2py muestra una pantalla de inicio y luego un widget de la GUI que pide que elijas una una contraseña temporaria de administrador, la dirección IP de la interfaz de red a utilizar para el servidor web, y un número de puerto desde el cual se servirán las solicitudes. Por defecto, web2py corre su servidor web en 127.0.0.1:8000 (puerto 8000 en localhost), pero puedes correrlo en cualquier dirección IP y puerto disponible. Puedes consultar la dirección IP de tu interfaz de red abriendo una consola y escribiendo ipconfig en Windows o ifconfig en OS X y Linux. En adelante asumiremos que web2py está corriendo en localhost (127.0.0.1:8000). Utiliza 0.0.0.0:80 para correr web2py públicamente en cualquiera de tus interfaces de red.

imagen

Si no especificas una contraseña administrativa, la interfaz administrativa se deshabilita. Esta es una medida de seguridad para evitar que la interfaz admin quede expuesta públicamente.

El acceso a la interfaz administrativa, admin, sólo es posible desde localhost a menos que estés corriendo web2py detrás de Apache con mod_proxy. Si admin detecta un proxy, la cookie de la sesión se establece como segura y el login de admin no funciona a menos que la comunicación entre el cliente y el proxy sea sobre HTTPS; esta es una medida de seguridad. Todas las comunicaciones entre el cliente y admin deben siempre ser o locales o cifradas; de lo contrario un atacante podría realizar un ataque tipo "man-in-the-middle" o "replay attack" y ejecutar código arbitrario en el servidor.

Luego de que la contraseña administrativa ha sido creada, web2py inicia automáticamente el navegador con la dirección:

http://127.0.0.1:8000/

Si la computadora no tiene un navegador por defecto, abre un navegador web e ingresa la URL.

imagen

Al hacer clic sobre "interfaz administrativa" se abre la página de login de esa interfaz.

imagen

La contraseña del administrador es la seleccionada al inicio del programa. Ten en cuenta que sólo hay un administrador, y por lo tanto sólo una contraseña administrativa. Por razones de seguridad, se pide al desarrollador que ingrese una nueva contraseña cada vez que web2py inicie a menos que se especifique la opción <recycle>. Esto es diferente a cómo se manejan las contraseñas en las aplicaciones de web2py.

Luego de que el administrador se autentica en web2py, el navegador es redirigido a la página "site".

imagen

Esta página lista todas las aplicaciones de web2py instaladas y permite su administración. web2py viene con tres aplicaciones:

admin
examples
welcome
scaffolding

  • Una aplicación llamada admin, la que estás utilizando en este momento.
  • La aplicación examples, con la documentación interactiva en línea y una réplica del sitio oficial de web2py.
  • Una aplicación llamada welcome. Esta es la plantilla básica para cualquier otra aplicación. Se dice que es la aplicación de andamiaje. Esta es también la aplicación que da la bienvenida al usuario al inicio del programa.
appliances

Las aplicaciones de web2py listas para usar son llamadas "appliances" (artefactos). Hay muchas appliances disponibles para descarga gratuita en [appliances] . Invitamos a los usuarios de web2py a que suban nuevas appliances, tanto de código abierto como de código cerrado (compiladas y empaquetadas).

Desde la página "site" de la aplicación admin, puedes realizar las siguientes operaciones:

  • instalar una aplicación completando el formulario en la parte inferior derecha de la página. Especifica el nombre de la aplicación, selecciona el archivo conteniendo una aplicación empaquetada o la URL donde se ubica la aplicación, y haz clic en "submit".
  • desinstalar una aplicación haciendo clic en el botón correspondiente. Se muestra una página de confirmación.
  • crear una nueva aplicación especificando un nombre y haciendo clic en "crear".
  • empaquetar una aplicación para su distribución haciendo clic en el botón correspondiente. La descarga de la aplicación es un archivo .tar conteniendo todo, incluyendo la base de datos. No deberías descomprimir este archivo; es automáticamente descomprimido por web2py cuando se instala a través de admin.
  • limpiar los archivos temporarios de la aplicación, como sesiones, errores y archivos de caché.
  • habilitar/deshabilitar cada aplicación. Cuando una aplicación se deshabilita, no es posible utilizarla en forma remota pero no se deshabilitará en forma local (para llamadas desde el mismo sistema). Esto quiere decir que aún es posible acceder a las aplicaciones deshabilitadas detrás de un servidor proxy. Las aplicaciones se deshabilitan creando un archivo llamado "DISABLED" en la carpeta de la aplicación. Los usuarios que intenten acceder a una aplicación deshabilitada recibirán un error 503 de HTTP. Puedes usar routes_onerror para personalizar las páginas de error.
  • EDITAR la aplicación.

Cuando creas una nueva aplicación utilizando admin, esta se instala como un clon de la app de andamiaje "welcome" con un archivo "models/db.py" que crea una base de datos SQLite, conecta a ella, instancia Auth, Crud, y Service, y los configura. Además provee un archivo "controllers/default.py", que expone las acciones "index", "download", "user" para el manejo de usuarios, y "call" para los servicios. En adelante, asumiremos que estos archivos se han eliminado; vamos a crear apps de cero.

web2py también viene con un asistente, descripto más adelante en este capítulo, que puede escribir código alternativo de andamiaje automáticamente basado en plantillas y plugin disponibles en la web y basándose en descripciones de alto nivel de los modelos.

Ejemplos sencillos

Di hola

index

Aquí, como ejemplo, crearemos una app web simple que muestre el mensaje "Hola desde MiApp" al usuario. Llamaremos a esta aplicación "miapp". Además añadiremos un contador que realice un conteo de cuántas veces el mismo usuario visita la página.

Puedes crear una nueva aplicación con sólo escribir su nombre en el formulario de la parte superior derecha de la página site en admin.

imagen

Luego de que presiones el botón de [crear], la aplicación se genera como copia de la aplicación welcome incorporada.

imagen

Para correr la aplicación, visita:

http://127.0.0.1:8000/miapp

Ahora tienes una copia de la aplicación welcome.

Para editar una aplicación, haz clic en el botón editar para la aplicación recién creada.

La página para editar te muestra lo que contiene la aplicación. Cada aplicación de web2py consiste de ciertos archivos, que en su mayoría caen en las siguientes categorías:

  • modelos: describen la representación de los datos.
  • controladores: describen el workflow o flujo de trabajo de la aplicación.
  • vistas: describen la presentación de los datos.
  • idiomas: describen como se debe traducir la presentación de la aplicación a otros idiomas.
  • módulos: módulos de Python que pertenecen a la aplicación.
  • archivos estáticos: imágenes estáticas, archivos CSS [css-w,css-o,css-school] , archivos JavaScript [js-w,js-b] , etc.
  • plugin: grupos de archivos diseñados para trabajar en conjunto.

Todo está prolijamente organizado según el patrón de diseño Modelo-Vista-Controlador. Cada sección en la página editar corresponde a una subcarpeta en la carpeta de la aplicación.

Nótese que haciendo clic en los encabezados de sección las secciones se contraen o expanden. Los nombres de las carpetas bajo archivos estáticos también tienen esta propiedad.

Cada archivo listado en la sección corresponde a un archivo con una ubicación física en la subcarpeta. Toda operación realizada en un archivo a través de la interfaz admin (crear, editar, eliminar) se puede realizar directamente desde la shell utilizando nuestro editor favorito.

La aplicación contiene otros tipos de archivos (bases de datos, archivos de sesiones, archivos de errores, etc.), pero no se listan en la página editar porque no son creadas o modificadas por el administrador; esos archivos son creados y modificados por la aplicación misma.

Los controladores contienen la lógica y flujo de trabajo de la aplicación. Cada URL se asocia a una llamada a una de las funciones en los controladores (acciones). Hay dos controladores por defecto: "appadmin.py" y "default.py". appadmin provee de una interfaz administrativa para la base de datos; por ahora no la usaremos. "default.py es el controlador que debes editar, que es el que se va a llamar por defecto cuando no se especifique el controlador en la URL. Modifica la función "index" de esta forma:

def index():
    return "Hola desde MiApp"

Así es como se ve el editor web:

imagen

Guárdalo y regresa a la página de editar. Haz clic en el link de index para visitar la nueva página creada.

Cuando visitas la URL

http://127.0.0.1:8000/myapp/default/index

se llama a la acción index en el controlador por defecto (default). Este devuelve la cadena que el navegador va a mostrarnos. Debería verse algo como esto:

imagen

Ahora, modifica la función "index" como sigue:

def index():
    return dict(mensaje="Hola desde MiApp")

También desde la página editar, modifica la vista "default/index.html" (el archivo de vista asociado a la acción) y reemplaza completamente los contenidos de ese archivo con lo siguiente:

<html>
   <head></head>
   <body>
      <h1>{{=mensaje}}</h1>
   </body>
</html>

Ahora la acción devuelve un diccionario que define un mensaje. Cuando una acción devuelve un diccionario, web2py busca una vista con el nombre

[controller]/[function].[extension]

y la ejecuta. Aquí [extension] es la extensión de la solicitud. Si no se especifica la extensión, se toma "html" por defecto, que es la extensión que utilizaremos para este caso. Con estas especificaciones, la vista es un archivo con HTML que incrusta o embebe código Python utilizando etiquetas especiales con {{ }}. En particular, en el ejemplo, el {{=mensaje}} ordena a web2py que reemplace el código entre etiquetas con el valor de message devuelto por la acción. Nótese que mensaje aquí no es una palabra especial (keyword) sino que se definió en la acción. Hasta aquí no hemos utilizado keyword de web2py.

Si web2py no encuentra la vista solicitada, utiliza una vista llamada "generic.html" que viene con cada aplicación.

Mac Mail
Google Maps
jsonp
Si se especifica una extensión que no sea "html" (por ejemplo "json"), y el archivo de la vista "[controlador]/[función].json" no se encuentra, web2py busca la vista genérica "generic.json". web2py viene con generic.html, generic.json, generic.jsonp, generic.xml, generic.rss, generic.ics (para Mac Mail Calendar), generic.map (para incrustar Google Maps), y generic.pdf (basado en fpdf). Estas vistas genéricas se pueden modificar para cada aplicación individualmente, y se pueden agregar vistas adicionales fácilmente.

Las vistas genéricas son herramientas de desarrollo. En producción cada acción debería tener su propia vista. De hecho, por defecto, las vistas genéricas sólo están habilitadas para localhost.

También puedes especificar una vista con response.view = 'default/something.html'

Consulta más detalles sobre este tema en el Capítulo 10.

Si regresas a EDITAR y haces clic en index, verás la nueva página HTML:

imagen

La barra de herramientas para depuración

toolbar

Con fines de depuración es posible añadir

{{=response.toolbar()}}

al código de la vista y te mostrará algunos datos útiles, incluyendo los objetos request, response y session, y una lista de todas las consultas a la base de datos cronometradas.

Contemos

session

Ahora vamos a agregar un contador a esta página que llevará la cuenta de cuántas veces el mismo visitante muestra la página.

web2py automática y transparentemente registra a los usuarios utilizando sesiones y cookie. Para cada nuevo visitante, crea una sesión y le asigna un "session_id" único. El objeto session es un contenedor para variables que se almacenan del lado del servidor. La clave única id es enviada al navegador a través de una cookie. Cuando el visitante solicita otra página desde la misma aplicación, el navegador envía la cookie de regreso, es recuperada por web2py, y se reactiva la sesión correspondiente.

Para utilizar el session, modifica el controlador por defecto (default):

def index():
    if not session.contador:
        session.contador = 1
    else:
        session.contador += 1
    return dict(mensaje="Hola desde MiApp", contador=session.contador)

Ten en cuenta que contador no es una palabra especial de web2py pero sí lo es session. Le pedimos a web2py que revise si existe una variable contador en session y, si no es así, que cree una con un valor inicial de 1. Si el contador ya existe, le pedimos a web2py que lo aumente en una unidad. Finalmente le pasamos el valor del contador a la vista.

Una forma más compacta de declarar la misma función es:

def index():
    session.contador = (session.contador or 0) + 1
    return dict(mensaje="Hola desde MiApp", contador=session.contador)

Ahora modifica la vista para agregar una línea que muestre el valor del contador:

<html>
   <head></head>
   <body>
      <h1>{{=mensaje}}</h1>
      <h2>Cantidad de visitas: {{=contador}}</h2>
   </body>
</html>

Cuando visitas la página index nuevamente (y otra vez), deberías obtener la siguiente página HTML:

imagen

El contador está asociado a cada visitante, y se incrementa cada vez que el visitante vuelve a cargar la página. Cada visitante ve un contador distinto.

Di mi nombre

form
request.vars

Ahora crea dos páginas (primera y segunda), donde la primera página crea un formulario, le pide el nombre al visitante, y redirige a la segunda página, que crea un mensaje de bienvenida personalizado.

diagrama yUML

Escribe las acciones correspondientes en el controlador default:

def primera():
    return dict()

def segunda():
    return dict()

Luego crea una vista "default/primera.html" para la primera acción, e ingresa:

{{extend 'layout.html'}}
<h1>¿Cuál es tu nombre?</h1>
<form action="segunda">
  <input name="nombre_del_visitante" />
  <input type="submit" />
</form>

Finalmente, crea una vista "default/segunda.html" para la segunda acción:

{{extend 'layout.html'}}
<h1>Hello {{=request.vars.nombre_del_visitante}}</h1>
layout

En ambas vistas hemos extendido el "layout.html" básico que viene con web2py. La vista layout (plantilla general) mantiene la apariencia/estilo entre dos páginas coherente. El archivo de layout se puede modificar y reemplazar fácilmente, ya que principalmente contiene código HTML.

Si ahora visitas la primera página, e ingresa tu nombre:

imagen

y aceptas el formulario, recibirás un mensaje de bienvenida:

imagen

Postback

redirect
URL
postback

El mecanismo para envío de formularios que hemos utilizado antes es muy común, pero no es una buena práctica de programación. Todos los datos ingresados deberían validarse y, en el ejemplo anterior, la tarea de validación recaería en la segunda acción. De esa forma, la acción que realiza la validación es distinta a la que ha generado el formulario. Esto tiende a la redundancia en el código.

Un patrón más apropiado para el envío de formularios es enviarlos a la misma acción que los generó, en nuestro caso "primera". La acción "primera" debería recibir las variables, procesarlas, almacenarlas del lado del servidor, y redirigir al visitante a la página "segunda", que recupera las variables. Este mecanismo se denomina "postback".

diagrama yUML

Modifica el controlador default para implementar el auto-envío:

def primera():
    if request.vars.nombre_del_visitante:
        session.visitor_name = request.vars.nombre_del_visitante
        redirect(URL('segunda'))
    return dict()

def segunda():
    return dict()

Y ahora modifica la vista "default/primera.html":

{{extend 'layout.html'}}
¿Cuál es tu nombre?
<form>
  <input name="nombre_del_visitante" />
  <input type="submit" />
</form>

y la vista "default/segunda.html" necesita recuperar la información de session en lugar de "request.vars":

{{extend 'layout.html'}}
<h1>Hola {{=session.nombre_del_visitante or "anónimo"}}</h1>

Desde el punto de vista del visitante, el auto-envío se comporta exactamente igual que con la implementación anterior. No hemos añadido validación todavía, pero ahora está claro que la validación debería realizarla la primera acción.

Esta estrategia es mejor además porque el nombre del visitante se mantiene en la sesión, y permanece accesible en toda acción o vista de la aplicación sin que sea necesario pasarlo explícitamente.

Ten en cuenta que si la acción "segunda" se llamara antes de establecer el nombre del visitante, mostrará "Hola anónimo" porque session.nombre_del_visitante devuelve None. Como alternativa podríamos haber agregado el siguiente código en el controlador (en la función "segunda"):

if not request.function=='primera' and not session.nombre_del_visitante:
    redirect(URL('primera'))

Este es un mecanismo ad hoc que puedes utilizar para un mayor control de la autorización en los controladores, aunque puedes consultar un método más eficiente en el Capítulo 9.

FORM
INPUT
requires
IS_NOT_EMPTY
accepts

Con web2py podemos avanzar aun más y pedirle que genere los formularios por nosotros, incluyendo la validación. web2py provee de ayudantes (FORM, INPUT, TEXTAREA, y SELECT/OPTION) con nombres equivalentes a las etiquetas de HTML. Estos ayudantes pueden utilizarse para crear formularios tanto en el controlador como en la vista.

Por ejemplo, aquí se muestra una posible forma de reescribir la primera acción:

def primera():
    formulario = FORM(INPUT(_name='nombre_del_visitante', requires=IS_NOT_EMPTY()),
              INPUT(_type='submit'))
    if formulario.process().accepted:
        session.nombre_del_visitante = formulario.vars.nombre_del_visitante
        redirect(URL('segunda'))
    return dict(formulario=formulario)

donde decimos que la etiqueta FORM contiene dos etiquetas INPUT. Los atributos de las etiquetas se especifican con pares nombre/argumento (named arguments) que comienzan con subguión ("_"). El argumento requires no es un atributo de etiqueta (porque no comienza con subguión) pero establece un validador para el valor de nombre_del_visitante.

Aquí tenemos una forma todavía mejor de crear el mismo formulario:

def primera():
    formulario = SQLFORM.factory(Field('nombre_del_visitante',
                     label='¿Cuál es tu nombre?',
                     requires=IS_NOT_EMPTY()))
    if formulario.process().accepted:
        session.nombre_del_visitante = form.vars.nombre_del_visitante
        redirect(URL('segunda'))
    return dict(formulario=formulario)

El objeto form puede serializarse fácilmente en HTML al incrustarlo en la vista "default/primera.html".

{{extend 'layout.html'}}
{{=formulario}}

El método formulario.process() aplica los validadores y devuelve el formulario en sí. La variable formulario.accepted se establece como True si el formulario se procesó y pasó la validación. Si el formulario auto-enviado pasa la validación, almacena las variables en la sesión y redirige como antes. Si el formulario no pasa la validación, se insertan mensajes de error en él y se muestran al usuario, como se muestra a continuación:

imagen

En la próxima sección vamos a mostrar como los formularios pueden crearse automáticamente desde el modelo.

En todos los ejemplos hemos usado la sesión para pasar el nombre del usuario entre la primera y la segunda acción. Podríamos haber usado un mecanismo diferente pasando los datos como parte del URL de redirección:

def primera():
    formulario = SQLFORM.factory(Field('nombre_del_visitante', requires=IS_NOT_EMPTY()))
    if formulario.process().accepted:
    nombre = form.vars.nombre_del_visitante
    redirect(URL('segunda', vars=dict(nombre=nombre)))
    return dict(formulario=formulario)

def segunda():
   nombre = request.vars.nombre_del_visitante or redirect(URL('primera'))
   return dict(nombre=nombre)

Ten en cuenta que en general no es buena idea pasar datos entre acciones por medio de los URL. Esto hace más difícil la protección de los datos. Es más seguro almacenar los datos en la sesión.

Internacionalización

Probablemente tu código incluya parámetros fijos (hardcoded), como la cadena "¿Cuál es tu nombre?". Deberías poder personalizar esas cadenas sin tener que editar el código fuente y en especial agregar traducciones en diferentes idiomas. De esa forma, si un visitante tuviera como preferencia el idioma italiano en su navegador, web2py usaría la traducción al italiano para las cadenas, si estuvieran disponibles. Esta característica de web2py se llama internationalization y se describe con más detalle en el próximo capítulo.

Aquí solo nos interesa aclarar que para poder usar esta funcionalidad debes marcar las cadenas que se deben traducir. Esto se hace envolviendo las cadenas entre comillas como por ejemplo

"¿Cuál es tu nombre?"

con el operador T:

T("¿Cuál es tu nombre?")

Además puedes marcar para traducción cadenas fijas en las vistas. Por ejemplo

<h1>¿Cuál es tu nombre?</h1>

se convierte en

<h1>{{=T("¿Cuál es tu nombre?")}}</h1>

Es una buena práctica el hacerlo para toda cadena en el código (etiquetas de campos, mensajes emergentes, etc.) salvo para los nombres de tablas o campos de la base de datos.

Una vez que identifiquemos y marquemos las cadenas para traducción, web2py se encargara de prácticamente todo lo demás. Además, la interfaz administrativa provee de una página donde puedes realizar las traducciones de cada cadena en el idioma para el que quieras dar soporte.

web2py incluye un potente motor para pluralización que se describe en el próximo capítulo. Se integra tanto al motor de internacionalización como al conversor para markmin.

Un blog de imágenes

upload

Aquí, como nuevo ejemplo, queremos crear una aplicación web que permita al administrador publicar imágenes y asignarles un nombre, y que permita a los usuarios del sitio web ver las imágenes rotuladas y enviar comentarios (publicaciones).

Como antes, desde la página site en admin, crea una nueva aplicación llamada imagenes, y navega hasta la página de "editar":

imagen

Comenzamos creando un modelo, una representación de los datos permanentes en la aplicación (las imágenes a subir, sus nombres y los comentarios). Primero, necesitas crear/modificar un archivo de modelo que, por falta de imaginación, llamaremos "db.py". Suponemos que el código a continuación reemplazará todo código existente en "db.py". Los modelos y los controladores deben .py como extensión porque son código fuente de Python. Si la extensión no se provee, web2py la agrega automáticamente. Las vistas en cambio tienen .html como extensión ya que principalmente contienen código HTML.

Modifica el archivo "db.py" haciendo clic en el botón "editar" correspondiente:

imagen

e ingresa lo siguiente:

IS_EMAIL
IS_NOT_EMPTY
IS_IN_DB

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

db.define_table('imagen',
   Field('titulo', unique=True),
   Field('archivo', 'upload'),
   format = '%(titulo)s')

db.define_table('publicacion',
   Field('imagen_id', 'reference imagen'),
   Field('autor'),
   Field('email'),
   Field('cuerpo', 'text'))

db.imagen.titulo.requires = IS_NOT_IN_DB(db, db.imagen.titulo)
db.publicacion.imagen_id.requires = IS_IN_DB(db, db.imagen.id, '%(titulo)s')
db.publicacion.autor.requires = IS_NOT_EMPTY()
db.publicacion.email.requires = IS_EMAIL()
db.publicacion.cuerpo.requires = IS_NOT_EMPTY()

db.publicacion.imagen_id.writable = db.publicacion.imagen_id.readable = False

Analicemos esto línea a línea.

La línea 1 define una variable global llamada db que representa la conexión de la base de datos. En este caso es una conexión a una base de datos SQLite almacenada en el archivo "applications/imagenes/databases/storage.sqlite". Si se usa SQLite y el archivo de la base de datos no existe, se crea uno nuevo. Puedes cambiar el nombre del archivo, así como también el nombre de la variable global db, pero es conveniente que se les dé el mismo nombre, para que sea más fácil recordarlos.

Las líneas 3-6 definen una tabla "imagen". define_table es un método del objeto db. El primer argumento "imagen", es el nombre de la tabla que estamos definiendo. Los otros argumentos son los campos que pertenecen a la tabla. Esta tabla tiene un campo denominado "titulo", otro llamado "archivo", y un campo llamado "id" que sirve como clave primaria ("id" no se declara explícitamente porque todas las tablas tienen un campo id por defecto). El campo "titulo" es una cadena, y el campo "archivo" es de tipo "upload". "upload" es un tipo de campo especial usado por la Capa de Abstracción de Datos (DAL) de web2py para almacenar los nombres de los archivos subidos. web2py sabe como subir archivos (por medio de streaming si son grandes), cambiarles el nombre por seguridad, y almacenarlos.

Cuando se define una tabla, web2py realiza una de muchas acciones posibles:

  • Si la tabla no existe, la tabla es creada;
  • Si la tabla existe y no se corresponde con la definición, la tabla se modifica apropiadamente, y si un campo tiene un tipo distinto, web2py intenta la conversión de sus contenidos.
  • Si la tabla existe y se corresponde con la definición, web2py no realiza ninguna acción.

Este comportamiento se llama "migración" (migration). En web2py las migraciones son automáticas, pero se pueden deshabilitar por tabla pasando migrate=False como último argumento de define_table.

La línea 6 define una cadena de formato para la tabla. Esto determina cómo un registro debería representarse como cadena. Nótese que el argumento format también puede ser una función que toma un registro y devuelve una cadena. Por ejemplo:

format=lambda registro: registro.titulo

Las líneas 8-12 definen otra tabla llamada "publicacion". Una publicación tiene un "autor", un "email" (vamos a guardar la dirección de correo electrónico del autor de la publicación), un "cuerpo" de tipo "text" (queremos utilizarlo para guardar el texto en sí publicado por el autor), y un campo "imagen_id" de tipo reference que apunta a db.imagen por medio del campo "id".

En la línea 14, db.imagen.titulo representa el campo "titulo" de la tabla "imagen". El atributo requires te permite configurar requerimientos/restricciones que se controlarán por medio de los formularios de web2py. Aquí requerimos que "titulo" no se repita:

IS_NOT_IN_DB(db, db.imagen.titulo)

Ten en cuenta que esto es opcional porque se configura automáticamente siempre que se especifique Field('titulo', unique=True).

Los objetos que representan estas restricciones se llaman validadores (validators). Se pueden agrupar múltiples validadores en una lista. Los validadores se ejecutan en el orden en que se especifican. IS_NOT_IN_DB(a, b) es un validador especial que comprueba que el valor de un campo b para un nuevo registro no exista previamente en a.

La línea 15 requiere que el campo "imagen_id" de la tabla "publicacion" esté en db.imagen.id. En lo que respecta a la base de datos, ya lo habíamos declarado al definir la tabla "publicacion". Ahora estamos diciendo explícitamente al modelo que esta condición debería ser controlada por web2py también en el nivel del procesamiento de los formularios cuando se publica un comentario, para que los datos inválidos no se propaguen desde los formularios de ingreso a la base de datos. Además requerimos que la "imagen_id" se represente por el "titulo", '%(titulo)s', del registro correspondiente.

La línea 20 indica que el campo "imagen_id" de la tabla "publicacion" no debería mostrarse en formularios, writable=False y tampoco en formularios de sólo-lectura, readable=False.

El significado de los validadores en las líneas 17-18 debería de ser obvio.

format

Ten en cuenta que el validador

db.publicacion.imagen_id.requires = IS_IN_DB(db, db.imagen.id, '%(titulo)s')

se puede omitir (y se configuraría automáticamente) si especificáramos un formato para una tabla referenciada:

db.define_table('imagen', ..., format='%(titulo)s')

donde el formato puede ser una cadena o una función que toma un registro y devuelve una cadena.

appadmin

Una vez que el modelo se ha definido, si no hay errores, web2py crea una interfaz de la aplicación para administrar la base de datos. Puedes acceder a ella a través del link "administración de la base de datos" en la página "editar" o directamente:

http://127.0.0.1:8000/imagenes/appadmin

Aquí se muestra una captura de la interfaz appadmin:

imagen

Esta interfaz se escribe en el controlador llamado "addpadmin.py" y la vista "appadmin.html" correspondiente. En adelante, nos referiremos a esta interfaz como "appadmin.py". Esta interfaz permite al administrador insertar nuevos registros de la base de datos, editar y eliminar registros existentes, examinar las tablas y hacer consultas tipo join en la base de datos.

La primera vez que se accede a appadmin, se ejecuta el modelo y se crean las tablas. La DAL de web2py traduce el código Python en comandos SQL específicos del motor de base de datos implementado (en este ejemplo SQLite). Puedes ver el SQL generado desde la página "editar" haciendo clic en el link "sql.log" debajo de "modelos". Ten en cuenta que el link no está disponible si no se crean las tablas.

imagen

Si editas el modelo y accedes a appadmin de nuevo, web2py generará SQL para alterar las tablas existentes. El SQL generado se registra en "sql.log".

Regresa a appadmin y prueba insertando un nuevo registro:

imagen

web2py ha traducido el campo tipo "upload" db.imagen.archivo en un formulario para subir el archivo. Cuando el formulario se acepta y se sube una imagen, el archivo se cambia de nombre por seguridad conservando la extensión, se guarda con el nuevo nombre en la carpeta "uploads" de la aplicación, y se almacena el nuevo nombre el campo db.imagen.archivo. Este mecanismo está diseñado para prevenir los ataques de tipo directory traversal.

Ten en cuenta que cada tipo de campo es procesado por un "widget". Los widget por defecto se pueden reemplazar (override).

Cuando haces clic en un nombre de tabla en appadmin, web2py realiza un select de todos los registros de la tabla elegida, encontrados por la consulta a la base de datos o query

db.imagen.id > 0

y convierte el resultado (render).

imagen

Puedes seleccionar un conjunto distinto de registros editando la consulta de DAL y presionando [Enviar].

Para modificar o eliminar un solo registro, haz clic en su número id.

Por causa del validador IS_IN_DB, el campo de referencia "imagen_id" se convierte en un menú desplegable (drop-down). Los ítems en el menú son claves (db.imagen.id), pero se representan con el campo db.imagen.titulo, como se especificó al crear el validador.

Los validadores son objetos muy potentes que saben como representar campos, filtrar sus valores, generar errores, y dar formato a los datos extraídos del campo.

La siguiente figura muestra qué pasa cuando se acepta un formulario que no pasa la validación:

imagen

Los mismos formularios que se generan automáticamente en appadmin pueden también generarse en forma programática a través del ayudante SQLFORM e embebidos en aplicaciones del usuario. Estos formularios son aptos para CSS (CSS-friendly), y se pueden personalizar.

Cada aplicación tiene su propio appadmin; por lo tanto, el mismo appadmin puede modificarse sin que se afecten otras aplicaciones.

Hasta aquí, la aplicación sabe cómo almacenar la información, y hemos visto cómo acceder a la base de datos a través de appadmin. El acceso a appadmin está restringido al administrador, y no está pensado como una interfaz web de producción para la aplicación; De ahí la próxima parte de este tutorial paso a paso. Específicamente queremos crear:

  • Una página "index" que liste todas las imágenes disponibles ordenadas por título y link a páginas con detalles para las imágenes.
  • Una página "mostrar/[id]" que muestre al visitante la imagen solicitada y le permita ver y publicar comentarios.
  • Una acción "download/[nombre]" para la descarga de las imágenes subidas.

Esto se muestra esquemáticamente aquí:

diagrama yUML

Regresa a la página "editar" y modifica el controlador "default.py", reemplazando sus contenidos con lo que sigue:

select
def index():
    imagenes = db().select(db.imagen.ALL, orderby=db.imagen.titulo)
    return dict(imagenes=imagenes)

Esta acción devuelve un diccionario. Las claves de los ítems en el diccionario se interpretan como variables pasadas a la vista asociada a la acción. Durante el desarrollo, si no hay una vista, la acción es convertida (render) por la vista "generic.html" que se provee con cada aplicación de web2py.

La acción de index realiza una consulta select de todos los campos (db.imagen.ALL) de la tabla imagen, ordenados por db.imagen.titulo. El resultado del select es un objeto Rows que contiene los registros. Se asigna a una variable local llamada imagenes devuelta por la acción a la vista. imagenes es iterable y sus elementos son los registros consultados. Para cada registro (row) las columnas se pueden examinar como diccionarios: imagenes[0]['titulo'] o imagenes[0].titulo con igual resultado.

Si no escribes una vista, el diccionario es convertido por "views/generic.html" y una llamada a la acción index se vería de esta forma:

imagen

No has creado una vista por esta acción todavía, así que web2py convierte el set de registros en un formulario tabular simple.

Ahora crea una vista para la acción index. Regresa a admin, edita "default/index.html" y reemplaza su contenido con lo que sigue:

{{extend 'layout.html'}}
<h1>Imágenes registradas</h1>
<ul>
{{for imagen in imagenes:}}
{{=LI(A(imagen.titulo, _href=URL("mostrar", args=imagen.id)))}}
{{pass}}
</ul>

Lo primero a tener en cuenta es que una vista es HTML puro con etiquetas {{...}} especiales. El código incrustado en {{...}} es código Python puro con una salvedad: la indentación o espaciado no es relevante. Los bloques de código comienzan con líneas que terminan en dos puntos (:) y terminan en líneas que comienzan con la palabra clave pass. En algunos casos el final de un bloque es obvio teniendo en cuenta el contexto y no es necesario el uso de pass.

Las líneas 5-7 recorren los registros de las imágenes y para cada registro muestran:

LI(A(imagen.titulo, _href=URL('mostrar', args=imagen.id))

Se trata de una etiqueta <li>...</li> que contiene una etiqueta <a href="...">...</a> que contiene el campo imagen.titulo. El valor del hipervínculo o hypertext reference (atributo href) es:

URL('mostrar', args=imagen.id)

Es decir, el URL en el ámbito de la misma aplicación y controlador que la solicitud (request) actual que llama a una función llamada "mostrar", pasándole un argumento único args=imagen.id a esa función. LI, A, etc. son ayudantes de web2py que están asociados a las etiquetas HTML correspondientes. Sus argumentos sin nombre se interpretan como objetos a serializar e insertar en el HTML incluido en la etiqueta. Los argumentos por nombre que comienzan con subguión (por ejemplo _href) son interpretados como atributos de la etiqueta sin el subguión. Por ejemplo _href es el atributo href, _class es el atributo class, etc.

Por ejemplo, el siguiente comando:

{{=LI(A('algo', _href=URL('mostrar', args=123))}}

se convierte en:

<li><a href="/imagenes/default/mostrar/123">algo</a></li>

Algunos ayudantes (INPUT, TEXTAREA, OPTION y SELECT) también aceptan algunos atributos especiales que no comienzan con subguión (value, y requires). Estos parámetros son importantes para la creación de formularios y se tratarán más adelante.

Vuelve a la página "editar". Ahora nos indica que "default.py" expone "index". Haciendo clic en "index", puedes visitar la nueva página creada:

http://127.0.0.1:8000/imagenes/default/index

que se ve así:

imagen

Si haces clic en el link del nombre de la imagen, el navegador abre la dirección:

http://127.0.0.1:8000/imagenes/default/mostrar/1

y esto resulta en un error, ya que todavía no has creado una acción llamada "mostrar" en el controlador "default.py".

Editemos el controlador "default.py" y reemplazando su contenido con:

SQLFORM
accepts
response.flash
request.args

response.download
def index():
    imagenes = db().select(db.imagen.ALL, orderby=db.imagenes.titulo)
    return dict(imagenes=imagenes)

def mostrar():
    imagen = db.imagen(request.args(0, cast=int)) or redirect(URL('index'))
    db.comentario.imagen_id.default = imagen.id
    formulario = SQLFORM(db.comentario)
    if formulario.process().accepted:
        response.flash = 'tu comentario se ha publicado'
    comentarios = db(db.comentario.imagen_id==image.id).select()
    return dict(imagen=imagen, comentarios=comentarios, formulario=formulario)

def download():
    return response.download(request, db)

El controlador contiene dos acciones: "mostrar" y "download". La acción "mostrar" selecciona la imagen con el id leído (parsed) de request args y todos los comentarios asociados a la imagen. a continuación, "mostrar" pasa todo a la vista "default/mostrar.html".

El id de la imagen en la referencia de:

URL('mostrar', args=imagen.id)

en "default/index.html", se puede consultar como:

request.args(0,cast=int)

desde la acción "mostrar". El argumento cast=int es opcional pero muy importante. Intenta convertir (cast) la cadena pasada en PATH_INFO a un int. En caso de fallar genera la excepción apropiada en lugar de generar un ticket. También se puede hacer una redirección en caso de un fallo al convertir el dato:

request.args(0, cast=int, otherwise=URL('error'))

Además, db.imagen(...) es un atajo para

db(db.imagen.id==...).select().first()

La acción "download" espera un nombre de archivo en request.args(0), arma la ruta a la ubicación donde se supone que se ha almacenado el archivo, y lo devuelve al cliente. Si el archivo es demasiado grande, lo transfiere por medio de un stream sin sobrecargar la memoria (overhead).

Ten en cuenta los siguientes comandos:

  • La línea 7 crea un formulario de inserción SQLFORM para la tabla db.publicacion utilizando sólo los campos especificados.
  • La línea 8 configura el valor del campo de referencia, que no es parte del formulario de ingreso de datos porque no está en la lista de campos especificados arriba.
  • La línea 9 procesa el formulario enviado (las variables del formulario están en request.vars) en el contexto de la sesión actual (la sesión se usa para prevenir envíos duplicados y para facilitar la navegación). Si las variables del formulario se validan, el nuevo comentario se inserta en la tabla db.publicacion; de lo contrario el formulario se modifica para incluir los mensajes de error (por ejemplo, si el email del autor no es válido). ¡Todo esto lo hace la línea 9!
  • La línea 10 se ejecuta únicamente cuando el formulario se acepta, luego de que el registro se inserte en la base de datos. response.flash es una variable que se muestra en las vistas y es utilizada para notificar al visitante cuando se detecta un evento en la aplicación.
  • La línea 11 selecciona todos los comentarios que refieren o apuntan a la imagen actual.

La acción "download" (descargar) ya está definida en el controlador "default.py" de la aplicación de andamiaje.

La acción "download" no devuelve un diccionario, por lo que no necesita una vista. La acción "mostrar", sin embargo, debería tener una vista, entonces regresa al admin y crea una nueva vista llamada "default/mostrar.html"

Modifica este archivo y reemplaza su contenido con lo siguiente:

{{extend 'layout.html'}}
<h1>Imagen: {{=imagen.titulo}}</h1>
<center>
<img width="200px"
     src="{{=URL('download', args=imagen.archivo)}}" />
</center>
{{if len(comentarios):}}
  <h2>Comentarios</h2><br /><p>
  {{for publicacion in comentarios:}}
    <p>{{=publicacion.autor}} dice <i>{{=publicacion.cuerpo}}</i></p>
  {{pass}}</p>
{{else:}}
  <h2>Todavía no se han publicado comentarios</h2>
{{pass}}
<h2>Publica un comentario</h2>
{{=formulario}}

Esta vista muestra el imagen.archivo llamando a la acción "download" dentro de una etiqueta <img .../>. Si hay comentarios, los recorre en un bucle y muestra cada uno.

Así es como se verá para el visitante:

imagen

Cuando un visitante envía un comentario a través de esta página, el comentario se almacena en la base de datos y se agrega al final de la página.

Agregando la autenticación

La API de web2py para el Control de Acceso Basado en Roles es bastante sofisticado, pero por ahora vamos a limitarnos a restringir el acceso a la acción mostrar a los usuarios autenticados, dejando una descripción más detallada para el capítulo 9.

Para limitar el acceso a los usuarios autenticados, debemos completar tres pasos. En un modelo, por ejemplo "db.py", debemos añadir:

from gluon.tools import Auth
auth = Auth(db)
auth.define_tables(username=True)

En nuestro controlador, tenemos que agregar una acción:

def user():
    return dict(formulario=auth())

Esto es suficiente para habilitar el login (autenticación), logout (cerrar sesión), etc. La plantilla general del diseño (layout) además mostrará opciones en las que requieran autenticación en la parte superior derecha del navegador.

imagen

Ahora podemos decorar las funciones que queremos restringir, por ejemplo:

@auth.requires_login()
def mostrar():
...

Todo intento de acceder a

http://127.0.0.1:8000/imagenes/default/show/[imagen_id]

requerirá autenticación. Si el usuario no se autentica (login), será redirigido a

http://127.0.0.1:8000/imagenes/default/user/login

imagen

La función user además expone, entre otras, las siguientes acciones:

http://127.0.0.1:8000/imagenes/default/user/logout
http://127.0.0.1:8000/imagenes/default/user/register
http://127.0.0.1:8000/imagenes/default/user/profile
http://127.0.0.1:8000/imagenes/default/user/change_password
http://127.0.0.1:8000/imagenes/default/user/request_reset_password
http://127.0.0.1:8000/imagenes/default/user/retrieve_username
http://127.0.0.1:8000/imagenes/default/user/retrieve_password
http://127.0.0.1:8000/imagenes/default/user/verify_email
http://127.0.0.1:8000/imagenes/default/user/impersonate
http://127.0.0.1:8000/imagenes/default/user/not_authorized

Ahora, un usuario nuevo debe registrarse para poder autenticarse, leer y publicar comentarios.

Tanto el objeto auth como la función user están definidos por defecto en la aplicación de andamiaje. El objeto auth es altamente personalizable y puede manejar aspectos como verificación por email, confirmación de registro, CAPTCHA, y métodos alternativos de autenticación por medio de los plugin.

Agregando los grid

Podemos añadir mejoras a lo realizado hasta aquí usando los gadget SQLFORM.grid y SQLFORM.smartgrid para crear una interfaz de administración para nuestra aplicación:

@auth.requires_membership('administrador')
def administrar():
    grid = SQLFORM.smartgrid(db.imagen, linked_tables=['comentario'])
    return dict(grid=grid)

con su "views/default/administrar.html" asociado

{{extend 'layout.html'}}
<h2>Interfaz de administración</h2>
{{=grid}}

Por medio de appadmin crea un grupo "administrador" y agrega algunos miembros al grupo. Ellos podrán acceder a la interfaz administrativa

http://127.0.0.1:8000/imagenes/default/manage

con funcionalidad para la navegación y búsqueda:

imagen

y opciones para crear, actualizar y eliminar imágenes y sus comentarios:

imagen

Configurando la plantilla general del diseño

Puedes configurar la plantilla general por defecto editando "views/layout.html" pero además puedes configurarla sin editar el HTML. De hecho, la plantilla de estilo "static/base.css" está documentada y descripta en detalle en el capítulo 5. Puedes cambiar el color, las columnas, el tamaño, bordes y fondo sin editar el HTML. Si deseas modificar el menú, el título o el subtítulo, puedes hacerlo en cualquier archivo del modelo. La aplicación de andamiaje, configura los valores por defecto de estos parámetros en el archivo "models/menu.py":

response.title = request.application
response.subtitle = '¡Modifícame!'
response.meta.author = 'tú'
response.meta.description = 'describe tu app'
response.meta.keywords = 'bla bla bla'
response.menu = [ [ 'Inicio', False, URL('index') ] ]

Una wiki simple

wiki
RSS
Ajax
XMLRPC

En esta sección, armamos una wiki simple y desde cero usando únicamente las API de bajo nivel (a diferencia del uso de la característica de wiki incorporada de web2py que se detalla en la próxima sección). El visitante podrá crear páginas, realizar búsquedas de páginas por título y editarlas. El visitante además podrá publicar comentarios (de la misma forma que en las aplicaciones anteriores), y además publicar documentos (adjuntos con las páginas) y enlazarlos desde las páginas. Como convención, adoptaremos la sintaxis markmin para la sintaxis de nuestra wiki. Además implementaremos una página de búsqueda con Ajax, una fuente de RSS para las páginas, y un servicio para la búsqueda de páginas a través de XML-RPC[xmlrpc] . El siguiente diagrama lista las acciones que necesitamos implementar y los enlaces que queremos establecer entre ellos.

diagrama yUML

Comienza creando una nueva app de andamiaje, con nombre "miwiki".

El modelo debe contener tres tablas: página, comentario, y documento. Tanto comentario como documento hacen referencia a página porque pertenecen a ella. Un documento contiene un campo archivo de tipo upload como en la anterior aplicación de imágenes.

Aquí se muestra el modelo completo:

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

from gluon.tools import *
auth = Auth(db)
auth.define_tables()
crud = Crud(db)

db.define_table('pagina',
    Field('titulo'),
    Field('cuerpo', 'text'),
    Field('creada_en', 'datetime', default=request.now),
    Field('creada_por', 'reference auth_user', default=auth.user_id),
    format='%(titulo)s')

db.define_table('comentario',
    Field('pagina_id', 'reference pagina'),
    Field('cuerpo', 'text'),
    Field('creado_en', 'datetime', default=request.now),
    Field('creado_por', 'reference auth_user', default=auth.user_id))

db.define_table('documento',
    Field('pagina_id', 'reference pagina'),
    Field('nombre'),
    Field('archivo', 'upload'),
    Field('creado_en', 'datetime', default=request.now),
    Field('creado_por', 'reference auth_user', default=auth.user_id),
    format='%(name)s')

db.pagina.titulo.requires = IS_NOT_IN_DB(db, 'page.title')
db.pagina.cuerpo.requires = IS_NOT_EMPTY()
db.pagina.creada_por.readable = db.pagina.creada_por.writable = False
db.pagina.creada_en.readable = db.pagina.creada_en.writable = False

db.comentario.cuerpo.requires = IS_NOT_EMPTY()
db.comentario.pagina_id.readable = db.comentario.page_id.writable = False
db.comentario.creado_por.readable = db.comentario.creado_por.writable = False
db.comentario.creado_en.readable = db.comentario.creado_en.writable = False

db.documento.nombre.requires = IS_NOT_IN_DB(db, 'documento.nombre')
db.documento.pagina_id.readable = db.documento.pagina_id.writable = False
db.documento.creado_por.readable = db.documento.creado_por.writable = False
db.documento.creado_en.readable = db.documento.creado_en.writable = False

Modifica el controlador "default.py" y crea las siguientes acciones:

  • index: listar todas las páginas wiki
  • crear: publicar una página wiki nueva
  • mostrar: mostrar una página wiki, listar sus comentarios y agregar comentarios nuevos
  • editar: modificar una página existente
  • documentos: administrar los documentos adjuntos de una página
  • download: descargar un documento (como en el ejemplo de las imágenes)
  • buscar: mostrar un campo de búsqueda y, a través de una llamada de Ajax, devolver los títulos a medida que el visitante escribe
  • callback: una función callback de Ajax. Devuelve el HTML que se embebe en la página de búsqueda mientras el visitante escribe.

Aquí se muestra el controlador "default.py":

def index():
     """ Este controlador devuelve un diccionario convertido
     por la vista

     Lista todas las páginas wiki
     index().has_key('pages')
     True
     """
     paginas = db().select(db.pagina.id, db.pagina.titulo, orderby=db.pagina.titulo)
     return dict(paginas=paginas)

@auth.requires_login()
def crear():
     """crea una nueva página wiki en blanco"""
     formulario = SQLFORM(db.pagina).process(next=URL('index'))
     return dict(formulario=formulario)

def mostrar():
     """muestra una página wiki"""
     esta_pagina = db.pagina(request.args(0, cast=int)) or redirect(URL('index'))
     db.comentario.pagina_id.default = esta_pagina.id
     formulario = SQLFORM(db.comentario).process() if auth.user else None
     comentariospagina = db(db.comentario.pagina_id==esta_pagina.id).select()
     return dict(pagina=esta_pagina, comentarios=comentariospagina, formulario=formulario)

@auth.requires_login()
def editar():
     """editar una página existente"""
     esta_pagina = db.pagina(request.args(0, cast=int)) or redirect(URL('index'))
     formulario = SQLFORM(db.pagina, esta_pagina).process(
                      next = URL('mostrar', args=request.args))
     return dict(formulario=formulario)

@auth.requires_login()
def documentos():
     """lista y edita los comentarios asociados a una página determinada"""
     pagina= db.pagina(request.args(0, cast=int)) or redirect(URL('index'))
     db.documento.pagina_id.default = pagina.id
     db.documento.pagina_id.writable = False
     grid = SQLFORM.grid(db.documento.pagina_id==pagina.id, args=[pagina.id])
     return dict(pagina=pagina, grid=grid)

def user():
     return dict(formulario=auth())

def download():
     """permite la descarga de documentos"""
     return response.download(request, db)

def buscar():
     """una página de búsqueda de wikis via ajax"""
     return dict(formulario=FORM(INPUT(_id='palabra',_name='palabra',
              _onkeyup="ajax('callback', ['palabra'], 'target');")),
              target_div=DIV(_id='target'))

def callback():
     """un callback de ajax que devuelve un <ul> de link a páginas wiki"""
     consulta = db.pagina.titulo.contains(request.vars.palabra)
     paginas = db(consulta).select(orderby=db.page.title)
     direcciones = [A(p.titulo, _href=URL('mostrar',args=p.id)) for p in paginas]
     return UL(*links)

Las líneas 2-6 consisten en un comentario de la acción index. Las líneas 4-5 dentro de los comentarios son interpretadas por Python como código de doctest. Los tests se pueden ejecutar a través de la interfaz admin. En este caso el test verifica que la acción index corra sin errores.

Las líneas 18, 27 y 35 tratan de recuperar el registro pagina con el id en request.args(0).

Las líneas 13 y 20 definen y procesan formularios de creación para una nueva página y un nuevo comentario.

La línea 28 define y procesa un formulario de modificación para una página wiki.

La línea 38 crea un objeto grid que permite visualizar, agregar y actualizar comentarios asociados a una página.

Cierta magia ocurre en la línea 51. Se configura el atributo onkeyup de la etiqueta "palabra". Cada vez que el visitante deja de presionar una tecla, el código JavaScript dentro del atributo onkeyup se ejecuta, del lado del cliente. Este es el código JavaScript:

ajax('callback', ['palabra'], 'target');

ajax es una función JavaScript definida en el archivo "web2py.js" que se incluye por defecto en "layout.html". Toma tres parámetros: el URL de la acción que realiza el callback asíncrono, una lista de los IDs de variables a ser enviadas al callback (["palabra"]), y el ID donde la respuesta se debe insertar ("target").

Tan pronto como escribas algo en el campo de búsqueda y dejes de presionar una tecla, el cliente llama al servidor y envía el contenido del campo "palabra", y, cuando el servidor responde, la respuesta se embebe en la misma página como HTML incluido en la etiqueta de destino ('target').

La etiqueta 'target' es un DIV definido en la línea 52. Podría haberse definido en la vista también.

Aquí se muestra el código para la vista "default/crear.html":

{{extend 'layout.html'}}
<h1>Crear una nueva página wiki</h1>
{{=formulario}}

Suponiendo que ya te has registrado y autenticado, si visitas la página crear, verás lo siguiente:

imagen

Aquí se muestra el código para la vista "default/index.html":

{{extend 'layout.html'}}
<h1>Páginas wiki disponibles</h1>
[ {{=A('buscar', _href=URL('buscar'))}} ]<br />
<ul>{{for pagina in paginas:}}
     {{=LI(A(pagina.titulo, _href=URL('mostrar', args=pagina.id)))}}
{{pass}}</ul>
[ {{=A('crear página', _href=URL('crear'))}} ]

Esto crea la siguiente página:

imagen

Aquí se puede ver el código para la vista "default/mostrar.html":

markdown
MARKMIN

{{extend 'layout.html'}}
<h1>{{=pagina.titulo}}</h1>
[ {{=A('editar', _href=URL('editar', args=request.args))}}
| {{=A('documentos', _href=URL('documentos', args=request.args))}} ]<br />
{{=MARKMIN(pagina.cuerpo)}}
<h2>Comentarios</h2>
{{for comentario in comentarios:}}
  <p>{{=db.auth_user[comentario.creado_por].first_name}} on {{=comentario.creado_en}}
          dice <I>{{=comentario.cuerpo}}</i></p>
{{pass}}
<h2>Publicar un comentario</h2>
{{=formulario}}

Si deseas utilizar la sintaxis markdown en lugar de markmin:

from gluon.contrib.markdown import WIKI as MARKDOWN

y usa MARKDOWN en lugar del ayudante MARKMIN. Alternativamente, puedes elegir aceptar HTML puro en lugar de la sintaxis markmin. En ese caso deberías reemplazar:

{{=MARKMIN(pagina.cuerpo)}}

con:

{{=XML(pagina.cuerpo)}}
sanitize

(para que no se realice el "escapado" del XML, que es el comportamiento por defecto de web2py por razones de seguridad).

Esto es mejor hacerlo con:

{{=XML(pagina.cuerpo, sanitize=True)}}

Al configurar sanitize=True, le dices a web2py que "escape" las etiquetas XML delicadas como "<script>", y que de esa forma se puedan prevenir las vulnerabilidades de tipo XSS.

Ahora si, desde la página index, haces clic en el título de una página, puedes ver la página que has creado:

imagen

Aquí está el código para la vista "default/edit.html":

{{extend 'layout.html'}}
<h1>Edición de la página wiki</h1>
[ {{=A('mostrar', _href=URL('mostrar', args=request.args))}} ]<br />
{{=formulario}}

Genera una página que se ve prácticamente idéntica a la de crear.

Aquí se muestra el código de la vista "default/documentos.html":

{{extend 'layout.html'}}
<h1>Documentos para la página: {{=pagina.titulo}}</h1>
[ {{=A('mostrar', _href=URL('mostrar', args=request.args))}} ]<br />
<h2>Documentos</h2>
{{=grid}}

Si, desde la página "mostrar", haces clic en documentos, ahora puedes administrar los documentos asociados a la página.

imagen

Por último, aquí está el código para la vista "default/buscar.html":

{{extend 'layout.html'}}
<h1>Buscar páginas wiki</h1>
[ {{=A('listar todo', _href=URL('index'))}}]<br />
{{=formulario}}<br />{{=target_div}}

que genera el siguiente formulario Ajax de búsqueda:

imagen

Además puedes probar llamando a la acción callback directamente visitando, por ejemplo, el siguiente URL:

http://127.0.0.1:8000/miwiki/default/callback?palabra=wiki

Si ahora examinas el código fuente verás el HTML devuelto por el callback:

<ul><li><a href="/miwiki/default/mostrar/4">He creado una wiki</a></li></ul>
rss

Es fácil generar una fuente de RSS para las páginas de la wiki utilizando web2py porque incluye gluon.contrib.rss2. Sólo debes añadir la siguiente acción al controlador default:

def noticias():
    """genera una fuente de rss a partir de las páginas wiki"""
    response.generic_patterns = ['.rss']
    paginas = db().select(db.pagina.ALL, orderby=db.pagina.titulo)
    return dict(
       titulo = 'fuente rss de miwiki',
       link = 'http://127.0.0.1:8000/miwiki/default/index',
       description = 'noticias de miwiki',
       creada_en = request.now,
       elementos = [
          dict(titulo = registro.titulo,
               link = URL('mostrar', args=registro.id),
               descripcion = MARKMIN(registro.cuerpo).xml(),
               creada_en = registro.creada_en
               ) for registro in paginas])

y cuando visitas la página

http://127.0.0.1:8000/miwiki/default/news.rss

verás la fuente (la salida exacta depende del lector de fuentes rss). Observa cómo el dict se convierte automáticamente a RSS, gracias a la extensión en el URL.

imagen

web2py también incluye un intérprete (parser) de fuentes para leer fuentes de terceros.

Observa que la línea:

response.generic_patterns = ['.rss']

le indica a web2py que debe usar vistas genéricas (para nuestro ejemplo "views/generic.css") cuando la terminación del URL coincide con el patrón glob ".rss". Por defecto las vistas genéricas solo están habilitadas para el desarrollo desde localhost.

XMLRPC

Por último, agreguemos un manejador de XML-RPC que permita la búsqueda de wiki en forma programática:

service = Service()

@service.xmlrpc
def buscar_por(palabra):
     """busca páginas que contengan la palabra para XML-RPC"""
     return db(db.pagina.titulo.contains(palabra)).select().as_list()

def call():
    """expone todos los servicios registrados, incluyendo XML-RPC"""
    return service()

Aquí, la acción de manejo (handler action) simplemente publica (via XML-RPC), las funciones especificadas en la lista. En este caso, buscar_por no es una acción (porque toma un argumento). Consulta a la base de datos con .select() y luego extrae los registros en forma de lista con .response y devuelve la lista.

Aquí hay un ejemplo de cómo se accede al manejador de XML-RPC desde un programa externo en Python.

>>> import xmlrpclib
>>> servidor = xmlrpclib.ServerProxy(
    'http://127.0.0.1:8000/mywiki/default/call/xmlrpc')
>>> for item in servidor.buscar_por('wiki'):
        print item['creada_en'], item['title']

Se puede acceder al manejador desde distintos lenguajes de programación que hablen XML-RPC, incluyendo C, C++, C# y Java.

Acerca de los formatos date, datetime y time

Existen tres distintas representaciones para cada uno de los tipos date, datetime y time:

  • la representación de la base de datos
  • la representación interna de web2py
  • la representación como cadena en formularios y tablas

La representación de la base de datos es una cuestión interna y no afecta al código. Internamente, en el nivel de web2py, se almacenan como objetos datetime.date, datetime.datetime y datetime.time respectivamente y pueden ser manipulados según las clases mencionadas:

for pagina in db(db.pagina).select():
    print pagina.title, pagina.day, pagina.month, pagina.year

Cuando las fechas se convierten a cadenas en los formularios son convertidas utilizando la representación ISO

%Y-%m-%d %H:%M:%S

de todas formas esta representación está internacionalizada y puedes usar la página de traducción de la aplicación administrativa para cambiar el formato por uno alternativo. Por ejemplo:

%m/%b/%Y %H:%M:%S

Ten en cuenta que por defecto el idioma Inglés no se traduce porque web2py asume que las aplicaciones ya vienen escritas en ese idioma. Si quieres que la internacionalización funcione con el idioma Inglés debes crear el archivo de traducción (utilizando admin) y declarar que el lenguaje actual de la aplicación es otro distinto del Inglés, por ejemplo:

T.current_languages = ['null']

La wiki incorporada de web2py

Ahora puedes olvidarte del código que hemos creado en la sección anterior (pero no de lo que has aprendido sobre las API de web2py, sólo el código específico de ese ejemplo), porque vamos a presentar un ejemplo de la wiki incorporada de web2py.

En realidad, web2py incluye la funcionalidad de wiki con características como el soporte para adjuntar archivos y recursos multimedia (media attachments), etiquetas y nubes de etiquetas, permisología de páginas, el uso de componentes (capítulo 14) y el protocolo oembed [oembed]. Esta wiki se puede utilizar con cualquier aplicación de web2py.

Ten en cuenta que la API de la wiki incorporada todavía está considerada como experimental y por lo tanto es posible que se le hagan modificaciones menores.

Aquí vamos a suponer que comenzaremos desde cero con un simple clon de la aplicación "welcome" que llamaremos "wikidemo". Modifica el controlador y reemplaza la acción "index" con

def index(): return auth.wiki()

¡Hecho! Tienes una wiki completamente funcional. Hasta ahora no se han agregado páginas y para poder hacerlo debes estar autenticado y ser miembro del grupo llamado "wiki_editor" o del grupo "wiki_author" (autores de wiki). Si te has autenticado como administrador el grupo "wiki_editor" se creará automáticamente y serás agregado como miembro. La diferencia entre editores y autores es que los editores pueden crear, modificar y borrar cualquier página, mientras que los autores pueden crear y modificar páginas (con restricciones opcionales) y pueden modificar o eliminar las páginas que han creado.

La función auth.wiki() devuelve un diccionario con una clave content que es automáticamente detectada por la vista "default/index.html". Puedes crear tu propia vista para esa acción:

{{extend 'layout.html'}}
{{=content}}

y agregar HTML adicional o el código que necesites. No es obligatorio que la acción se llame "index" para exponer la wiki. Puedes usar una acción con otro nombre.

Para probar la wiki, simplemente accede a la interfaz admin, y luego visita la página

http://127.0.0.1:8000/wikidemo/default/index

Luego elige un título abreviado o slug (en el oficio gráfico, el término del inglés slug es un nombre corto que se usa para títulos de artículos en producción) y serás redirigido a una página vacía donde puedes editar los contenidos usando la sintaxis de wiki markmin. Un nuevo menú llamado "[wiki]" te permitirá crear, buscar y modificar las páginas. Las páginas wiki tienen URL como el siguiente:

http://127.0.0.1:8000/wikidemo/default/index/[slug]

Las páginas de los servicios tienen nombres que comienzan con guión bajo:

http://127.0.0.1:8000/wikidemo/default/index/_create
http://127.0.0.1:8000/wikidemo/default/index/_search
http://127.0.0.1:8000/wikidemo/default/index/_could
http://127.0.0.1:8000/wikidemo/default/index/_recent
http://127.0.0.1:8000/wikidemo/default/index/_edit/...
http://127.0.0.1:8000/wikidemo/default/index/_editmedia/...
http://127.0.0.1:8000/wikidemo/default/index/_preview/...

Prueba a crear más páginas como "index", "quienes-somos" y "contactenos" Luego prueba a modificarlas

El método wiki tiene la siguiente lista de parámetros o signature:

def wiki(self, slug=None, env=None, render='markmin',
 manage_permissions=False, force_prefix='',
 restrict_search=False, resolve=True,
 extra=None, menugroups=None)

Acepta los siguientes argumentos:

  • render, que es por defecto 'markmin' pero que puede establecerse como 'html'. Determina la sintaxis de la wiki. Daremos más detalles de la sintaxis markmin más adelante. Si cambias este parámetro a HTML podrías necesitar agregar un editor WYSIWYG JavaScript como TinyMCE o NicEdit.
  • manage_permissions. Esta opción tiene el valor False por defecto y sólo reconocerá la permisología para "wiki_editor" y "wiki_author". Si lo cambias a True, la página de creación y modificación dará la opción de especificar por nombres los grupos cuyos miembros tienen permiso para leer y modificar la página. Se puede usar el grupo "everybody" para otorgar permisos a todo usuario.
  • force_prefix. Si se especifica algo como '%(id)s' restringirá la creación de páginas de los autores (no editores) con prefijos "[id del usuario]-[nombre de página]". El prefijo puede contener el id ("%(id)s") o el nombre del usuario ("%(username)s") o cualquier otro campo de la tabla auth_user, siempre y cuando la columna correspondiente contenga una cadena capaz de pasar la validación del URL.
  • restrict_search. Por defecto es False e implica que todo usuario autenticado puede hacer búsquedas de páginas para toda la wiki (aunque no necesariamente tendrá acceso de lectura o escritura a toda página). Si se especifica True, los autores pueden buscar únicamente entre sus propias páginas, los editores podrán buscar cualquier página, y el resto de los usuarios no tendrán acceso a la búsqueda.
  • menu_groups. Si se establece como None (el valor por defecto), el menú de administración de la wiki (para búsqueda, creación, modificación, etc.) estará siempre disponible. Puedes especificar esta opción como una lista de nombres de grupos para los cuales será visible el menú, por ejemplo ['wiki_editor', 'wiki_author']. Ten en cuenta que incluso cuando el menú se exponga a todos los usuarios, esto no implica que todo usuario tenga acceso a todos los comandos del menú, ya que estos permisos están regulados por el sistema de control de acceso.

El método wiki tiene algunos parámetros adicionales que se explicarán más adelante: slug, env y extra.

Elementos básicos de MARKMIN

La sintaxis MARKMIN te permite marcar un texto como negrita usando **negrita**, el texto con letra itálica con ''itálica'', y el texto con código fuente se debe delimitar con comillas invertidas (`) dobles. Para los títulos se debe anteponer con un #, para las secciones ## y para secciones menores ###. Usa el signo menos (-) como prefijo en listas sin orden de elementos y un más (+) como prefijo en listas ordenadas. Los URL se convierten automáticamente en link. He aquí un ejemplo de texto markmin:

# Este es un título
## Este es un título de sección
### Este es un título de una sección menor

El texto puede tener **negrita**, ''itálica'', ``código`` etc.
Puedes encontrar más información en:

http://web2py.com

Puedes usar el parámetro extra de auth.wiki para pasar reglas adicionales de conversión al ayudante MARKMIN. Encontrarás más información sobre la sintaxis MARKMIN en el capítulo 5.

auth.wiki es más potente que el ayudante MARKMIN por sí solo, ya que, por ejemplo, tiene soporte para oembed y componentes.

Puedes usar el parámetro env de auth.wiki para exponer funciones en tu wiki.

Por ejemplo:

auth.wiki(env=dict(unir=lambda a, b, c:"%s-%s-%s" % (a, b, c)))

Te permite usar el siguiente lenguaje de marcado o markup syntax:

@(join:1,2,3)

Esto llama a la función unir que se pasó como parámetro en extra y especifica los argumentos a, b, c=1, 2, 3 y se convertirá como 1-2-3.

El protocolo oembed

Puedes agregar(o copiar y pegar) cualquier URL en una página wiki y normalmente se convertirá en un link a ese URL. Hay algunas excepciones:

  • Si el URL tiene una extensión de imagen, el link se incrustará como imagen, <img/>.
  • Si el URL tiene una extensión de audio, el link se incrustará como audio HTML5 <audio/>.
  • Si el URL tiene una extensión de MS Office o PDF, se embebe un Google Doc Viewer, que muestra el contenido del documento (sólo funciona para documentos públicos).
  • Si el URL está vinculado a una página de YouTube, Vimeo o Flickr, web2py conecta con el servicio correspondiente y consulta la forma apropiada en que se debe incrustar el contenido. Esto se hace utilizando el protocolo oembed.

Esta es una lista completa de los formatos soportados:

Imagen (.PNG, .GIF, .JPG, .JPEG)
Audio (.WAV, .OGG, .MP3)
Video (.MOV, .MPE, .MP4, .MPG, .MPG2, .MPEG, .MPEG4, .MOVIE)

Los formatos soportados a través del servicio de Google Doc Viewer:

Microsoft Excel (.XLS and .XLSX)
Microsoft PowerPoint 2007 / 2010 (.PPTX)
Apple Pages (.PAGES)
Adobe PDF (.PDF)
Adobe Illustrator (.AI)
Adobe Photoshop (.PSD)
Autodesk AutoCad (.DXF)
Scalable Vector Graphics (.SVG)
PostScript (.EPS, .PS)
TrueType (.TTF)
XML Paper Specification (.XPS)

Soportados por oembed:

flickr.com
youtube.com
hulu.com
vimeo.com
slideshare.net
qik.com
polleverywhere.com
wordpress.com
revision3.com
viddler.com

La implementación pertenece al archivo de web2py gluon.contrib.autolinks más específicamente en la función exand_one. Puedes extender el soporte para oembed registrando más servicios. Esto se hace agregando una entrada a la lista EMBED_MAPS:

from gluon.contrib.autlinks import EMBED_MAPS
EMBED_MAPS.append((re.compile('http://vimeo.com/\S*'),
 'http://vimeo.com/api/oembed.json'))

Creando hipervínculos para contenidos de la wiki

Si creas una página wiki con el título "contactenos", puedes hacer referencia a ella como

@////contactenos

Aquí @//// reemplaza a

@/app/controlador/funcion/

per "app", "controlador" y "función" se omiten usando los valores por defecto.

En forma similar, puedes usar el menú de la wiki para subir archivos multimedia (por ejemplo una imagen) asociados a la página. La página "manage media" (administración de archivos multimedia) mostrará todos los archivos que se hayan subido y mostrará la notación apropiada con el hipervínculo al archivo multimedia. Si, por ejemplo, subes un archivo "prueba.jpg", con el título "playa", la notación del hipervínculo será algo como:

@////15/playa.jpg

@//// es el mismo prefijo que se describió anteriormente. 15 es el id del registro que almacena el archivo multimedia. playa es el título. .jpg es la extensión del archivo original.

Si cortas y pegas @////15/playa.jpg en la página wiki incrustarás la imagen.

Ten en cuenta que los archivos multimedia están asociados a las páginas y heredan sus permisos de acceso.

Menús de la wiki

Si creas una página con el slug "wiki-menu" será interpretado como la descripción del menú. He aquí un ejemplo:

- Inicio > @////index
- Informacion > @////info
- web2py > http://www.web2py.com
- - Quiénes somos > @////quienes-somos
- - Contáctenos > @////contactenos

Cada línea es un ítem del menú. Para los menús anidados se usa el guión doble. El signo > separa el título del ítem de menú del link.

Ten en cuenta que el menú se agrega al objeto response.menu, no lo reemplaza. Los ítems del menú [wiki] que dan acceso a los servicios de la wiki se agregan automáticamente.

Las funciones de servicios

Si por ejemplo, quieres usar la wiki para crear una barra lateral editable, puedes crear una página con slug="sidebar" y luego embeberla en el layout.html con

{{=auth.wiki(slug='sidebar')}}

Ten en cuenta que la palabra "sidebar" aquí no tiene un significado especial. Toda página de wiki se puede recuperar y embeber en cualquier instancia de tu código fuente. Esto permite entrelazar funcionalidades de la wiki con las funcionalidades comunes de las aplicaciones de web2py.

También debes tener en cuenta que

auth.wiki('sidebar')
es lo mismo que
auth.wiki(slug='sidebar')
, ya que el argumento slug es el primero en la lista de argumentos del método. El primer comando provee de una sintaxis un tanto más simple.

Además puedes incrustar las funciones especiales de la wiki como la búsqueda por etiquetas:

{{=auth.wiki('_search')}}

o la nube de etiquetas:

{{=auth.wiki('_cloud')}}

Ampliación de la funcionalidad auth.wiki

Cuando tu app con la wiki incorporada se torne más complicada, quizás quieras personalizar los registros de la base de datos para la wiki que son administrados por la interfaz Auth o exponer formularios personalizados para las altas, bajas y modificaciones (ABM o CRUD). Por ejemplo, podrías querer personalizar la representación de un registro de una tabla de la wiki o agregar un nuevo validador de campo. Esto no es posible por defecto, ya que el modelo de la wiki se define únicamente luego de que la interfaz wiki se inicie utilizando el método auth.wiki(). Para permitir el acceso a la configuración específica de la base de datos para la wiki en el modelo de la app, debes agregar el siguiente comando a tu modelo (por ejemplo, en db.py)

# Asegúrate de que la función se llame luego de crear el objeto auth
# y antes de cualquier cambio en las tablas de la wiki
auth.wiki(resolve=False)

Al utilizra la línea de arriba en tu modelo, podrás acceder a las tablas de la wiki (por ejemplo, wiki_page) para operaciones personalizadas en la base de datos.

Ten en cuenta que de todas formas debes usar auth.wik() en el controlador o vista para poder exponer la interfaz wiki, ya que el parámetro resolve=False sólo le indica al objeto auth que debe configurar el modelo de la wiki sin realizar otra acción de configuración específica.

Además, al establecer resolve como False en el llamado al método, se podrá acceder a las tablas y los registros de la wiki través de la interfaz por defecto para la base de datos en <app>/appadmin.

Otra personalización posible es la de agregar campos adicionales a los estándar de la wiki (de igual forma que para la tabla auth_user, como se describe en el capítulo 9). Esto se hace de la siguiente forma:

# coloca este código luego de la inicialización del objeto auth
auth.settings.extra_fields["wiki_page"] = [Field("unblob", "blob"),]

La línea de arriba agrega un campo blob a la tabla wiki_page. No hay necesidad de llamar a

auth.wiki(resolve=False)
para esta opción, a menos que se requiera el acceso al modelo de la wiki para otras personalizaciones.

Componentes

Una de las funciones más potentes del nuevo web2py consiste de la habilidad para embeber una acción dentro de otra acción. A esta característica la llamamos componentes.

Consideremos el siguiente modelo:

db.define_table('cosas',Field('nombre', requires=IS_NOT_EMPTY()))

y la siguiente acción:

@auth.requires_login()
def administrar_cosas():
 return SQLFORM.grid(db.cosa)

Esta acción es especial porque devuelve un widget/ayudante y no un diccionario de objetos. Ahora podemos incrustar la acción administrar_cosas en la vista, con

{{=LOAD('default','manage_things', ajax=True)}}

Esto permite que un visitante pueda interactuar con el componente a través de Ajax sin actualizar la página anfitrión que contiene el widget. La acción es llamada por medio de Ajax, hereda el estilo de la página anfitrión y captura todos los envíos de formularios y mensajes emergentes para que sean manejados en la página actual. Como complemento de todo lo anterior, el widget SQLFORM.grid usa direcciones URL firmadas digitalmente para restringir el acceso. Se puede encontrar información más detallada sobre componentes en el capítulo 13.

Los componentes como el que se describe arriba se pueden incrustar en páginas wiki usando la sintaxis MARKMIN:

@{component:default/administrar_cosas}

Esto sencillamente le indica a web2py que queremos incluir la acción "administrar_cosas" definida en el controlador "default" a través de Ajax.

La mayoría de los usuarios podrán crear aplicaciones relativamente complejas simplemente usando auth.wiki para crear páginas y menús, e incrustar componentes personalizados en páginas wiki. Wiki puede ser pensado como un mecanismo para permitir crear páginas a los miembros de un grupo determinado, pero también puede entenderse como una metodología modular para el desarrollo de aplicaciones.

Más sobre admin

admin

La interfaz administrativa provee de funcionalidad adicional que se tratará brevemente en esta sección.

Site

site

Esta página es la interfaz administrativa principal de web2py. A la izquierda tiene una lista de todas las aplicaciones instaladas y a la derecha tiene algunos formularios especiales.

El primero de esos formularios muestra la versión de web2py y da la opción de hacer un upgrade si existe una nueva versión disponible. Por supuesto, antes de actualizar, ¡asegúrate de hacer una copia de seguridad completa!

Luego hay dos formularios que permiten la creación de una nueva aplicación (en un solo paso o usando un ayudante en línea) especificando su nombre.

Instant Press
Movuca

El formulario que sigue permite subir una aplicación existente tanto desde una ubicación local del sistema como desde un URL remoto. Cuando subes una aplicación, debes especificar su nombre (el uso de distintos nombres te permite instalar múltiples copias de una misma aplicación). Puedes probar, por ejemplo, a subir la aplicación de redes sociales Movuca creada por Bruno Rocha:

https://github.com/rochacbruno/Movuca

o el CMS Instant Press creado por Martín Mulone:

http://code.google.com/p/instant-press/

o uno de los muchos ejemplos disponibles en:

http://web2py.com/appliances

Las apps de web2py se empaquetan como archivos .w2p, que son archivos tar comprimidos con gzip. web2py utiliza la extensión .w2p en lugar de .tgz para evitar que el navegador intente descomprimirlos al descargarlos. Se pueden descomprimir manualmente con tar zxvf [nombredearchivo] aunque esto normalmente no es necesario.

imagen

Si la subida es exitosa, web2py muestra la suma de verificación (checksum) del archivo subido. Puedes utilizarla para verificar que el archivo no se dañó durante la subida. El nombre InstantPress aparecerá en la lista de aplicaciones instaladas.

Si corres la distribución de web2py de código fuente y tienes gitpython instalado (si es necesario, puedes configurarlo con 'easy_install gitpython'), puedes instalar aplicaciones en forma directa desde los repositorios git utilizando el URL con extensión .git del formulario para subir aplicaciones. En ese caso también podrás usar la interfaz admin para aplicar los cambios en el repositorio remoto, pero esa funcionalidad es experimental. Por ejemplo, puedes instalar en forma local la aplicación que muestra este libro en el sitio de web2py con el URL:

https://github.com/mdipierro/web2py-book.git

Ese repositorio aloja la versión actual de este libro (que puede diferir de la versión estable que ves en el sitio web). Las mejoras, reparaciones de fallos y correcciones son bienvenidas y se pueden enviar como solicitud de cambios de git (pull request)

En función de cada aplicación instalada puedes usar la página site para:

  • Acceder directamente a la aplicación haciendo clic en su nombre.
  • Desinstalar la aplicación
  • Ir a la página "acerca de" (ver abajo)
  • Ir a la página de "editar" (ver abajo)
  • Ir a la página de "errores" (ver abajo)
  • Eliminar archivos temporarios (sesiones, errores, y archivos cache.disk)
  • Empaquetar todo. Esto devuelve un archivo .tar que contiene una copia completa de la aplicación. Te sugerimos que elimines los archivos temporarios antes de empaquetar una aplicación.
  • Compilar la aplicación. Si no hay errores, esta opción generará código bytecode-compiled de todos los módulos, controladores y vistas. Como las vistas pueden extender e incluir a otras vistas en una estructura jerárquica, antes de la compilación, el "árbol" de vistas se condensa en un archivo único. El efecto de este procedimiento es que una aplicación compilada es más rápida, porque se omite la lectura de plantillas (parse) y sustituciones de cadenas durante la ejecución.
  • Empaquetar compilados. Esta opción sólo está disponible para aplicaciones compiladas. Permite empaquetar la aplicación sin el código fuente para su distribución "closed source". Ten en cuenta que Python (como cualquier otro lenguaje de programación) puede ser descompilado en la práctica; por lo tanto la compilación no garantiza la protección del código fuente. Sin embargo, la descompilación puede ser puede ser una tarea difícil e incluso ilegal.
  • Eliminar compilados. Simplemente elimina todos los archivos de los modelos, vistas y controladores bytecode-compiled de la aplicación. Si la aplicación se empaquetó con código fuente o se editó localmente, no hay peligro al eliminar los archivos compilados, y la aplicación funcionará de todas formas. Si la aplicación se instaló desde un paquete compilado, entonces la operación no es segura, porque hay un código fuente hacia el cual se puedan revertir los cambios, y la aplicación dejará de funcionar.
admin.py

Todas las funcionalidades disponibles desde el sitio admin de web2py también se pueden utilizar programáticamente a través de la API definida en el módulo gluon/admin.py. Basta con abrir una consola con el intérprete de Python e importar ese módulo.

Si se ha instalado el SDK de Google App Engine la página de la interfaz administrativa site muestra un botón para desplegar tu aplicación en el servicio remoto de GAE. Si se instaló python-git, entonces también encontrarás un botón para aplicar los cambios en Open Shift. Para instalar aplicaciones en Heroku u otro sistema de alojamiento puedes buscar el programa correspondiente en la carpeta "scripts".

Acerca de'

about
license

La sección "acerca de" (about) permite editar la descripción de la aplicación y su licencia. Estas últimas están escritas respectivamente en los archivos ABOUT y LICENSE en la carpeta de la aplicación.

imagen

Se pueden utilizar las sintaxis MARKMIN, o gluon.contrib.markdown.WIKI para estos archivos como se describe en ref.[markdown2] .

Editar

EDIT

Ya has utilizado la página "editar" o design en otra ocasión en este capítulo. Aquí queremos señalar algunas funcionalidades más de esta página.

  • Si haces clic en cualquier nombre de archivo, puedes visualizar sus contenidos con resaltado de código fuente.
  • Si haces clic en editar, puedes editar el archivo a través de la interfaz web.
  • Si haces clic en eliminar, puedes eliminar el archivo (en forma permanente).
  • Si haces clic en test (pruebas), web2py correrá los tests. Los tests son creados por el desarrollador utilizando doctests, y cada función debería tener sus propios tests.
  • Puedes agregar archivos de idiomas, buscar en el dominio de la aplicación para detectar todas las cadenas y editar sus traducciones a través de la interfaz web.
  • Si los archivos estáticos se organizan en carpetas y subcarpetas, las jerarquías de las carpetas se pueden colapsar o desplegar haciendo clic en el nombre de la carpeta.

La imagen a continuación muestra la salida de la página de tests para la aplicación welcome.

imagen

La imagen que sigue muestra la sección de idiomas para la aplicación welcome.

imagen

La siguiente imagen muestra cómo se edita un archivo de idioma, en este caso el idioma "it" (Italiano) para la aplicación welcome.

imagen

El depurador incorporado

(requiere Python 2.6 o superior)

La app admin de web2py incluye un depurador para navegador. Usando el editor en línea puedes agregar los breakpoint (instrucciones para detener el flujo de operación) y, desde la consola de depuración asociada al editor, examinar las variables en esos breakpoint y continuar la ejecución. Se puede ver un ejemplo de este proceso en la imagen que sigue:

imagen

La funcionalidad se basa en el depurador Qdb creado por Mariano Reingart. Esta aplicación utiliza el módulo multiprocessing.connection para la intercomunicación entre backend y frontend, por medio de un protocolo de transmisión similar a JSON-RPC. [qdb]

Shell o consola

Si haces clic en el link "shell" debajo de la sección de controladores en "editar", web2py abrirá una consola de Python para web y ejecutará los modelos para la aplicación actual. Esto te permite comunicarte con la aplicación en forma interactiva.

imagen

Ten cuidado cuando usas la shell para navegador - porque las distintas solicitudes de la consola se ejecutarán en diferentes hilos. Esto puede fácilmente generar errores, en especial si estás realizando operaciones y estás haciendo pruebas para crear o conectar a bases de datos. Para este tipo de actividades (por ejemplo, si necesitas almacenamiento permanente) es preferible usar la línea de comandos de Python.

Crontab

También debajo de la sección de controladores en "editar" hay un link a "crontab". Haciendo clic en este link podrás editar el archivo de crontab de web2py. El crontab de web2py sigue la misma sintaxis que el crontab para Unix, pero no requiere un sistema Unix. En realidad, sólo requiere web2py, y funciona en Windows. Te permite registrar acciones que se tienen que ejecutar en segundo plano en horarios programados.

Para más detalles sobre este tema, consulta el siguiente capítulo.

Errores

errors

Mientras programes con web2py, inevitablemente cometerás errores e introducirás fallas o bugs. web2py te ayuda en dos formas: 1) te permite crear tests para cada función que pueden ejecutarse en el navegador desde la página "editar"; y 2) cuando se produce un error, se devuelve un ticket al visitante y el error se reporta/almacena (log).

Introduce intencionalmente un error en la aplicación de imágenes como se muestra abajo:

def index():
    imagenes = db().select(db.imagen.ALL,orderby=db.imagen.titulo)
    1/0
    return dict(imagenes=imagenes)

Cuando accedas a la acción de index, obtendrás el siguiente ticket:

imagen

Sólo el administrador puede visualizar el detalle del ticket:

imagen

El ticket muestra el traceback (traza o trayectoria del error), y el contenido del archivo que causó el problema, y el estado completo del sistema (variables, objetos request, session, etc.). Si el error se produce en la vista, web2py muestra la vista convertida de HTML a código Python. Esto permite una identificación más fácil de la estructura lógica del archivo.

Por defecto los ticket (o tiques) se almacenan en el sistema de archivos y se muestran agrupados según la traza del error o traceback. La interfaz administrativa provee de vistas con estadística (tipo de traceback y número de repeticiones) y una vista detallada (todos los ticket se listan por id). El administrador puede intercambiar los dos tipos de vistas.

Observa que admin siempre muestra código fuente resaltado (por ejemplo en los reportes de errores, las palabras especiales de web2py se muestran en naranja). Si haces clic en una keyword de web2py, eres redirigido a la página con la documentación sobre esa palabra.

Si reparas el bug de división por cero en la acción index e introduces otro en la vista de index:

{{extend 'layout.html'}}

<h1>Imágenes registradas</h1>
<ul>
{{for imagen in imagenes:}}
{{1/0}}
{{=LI(A(imagen.titulo, _href=URL("mostrar", args=imagen.id)))}}
{{pass}}
</ul>

obtienes el siguiente ticket:

imagen

Nótese que web2py ha convertido la vista de HTML a un archivo Python, y el error descripto en el ticket se refiere al código Python generado y NO a la vista original:

imagen

Esto puede resultar confuso al principio, pero en la práctica hace el trabajo de depuración más sencillo, porque el espaciado (o indentación) de Python resalta la estructura lógica del código que has embebido en las vistas.

El código se muestra al final de la misma página.

Todos los ticket se listan bajo la aplicación admin en la página "errores" de cada aplicación:

imagen

Mercurial

Mercurial

Si corres la distribución de código fuente, la interfaz administrativa muestra un ítem de menú llamado Versioning (Control de versiones).

images

Si ingresas un comentario y presionas el botón de aplicar cambios (commit) en la página asociada a ese botón, se aplicarán los cambios de la aplicación actual. Al aplicar los cambios por primera vez, se creará un repositorio Mercurial específico de la aplicación.

En forma transparente para el usuario, Mercurial almacena información sobre los cambios que se hacen en el código en una carpeta oculta ".hg" ubicada en la subcarpeta de tu aplicación. Cada aplicación tiene su carpeta ".hg" y su propio archivo ".hgignore" (que le dice a Mercurial qué archivos debe ignorar). Para poder usar esta característica, debes tener instaladas las librerías para control de versiones de Mercurial (versión 1.9 o superior):

easy_install mercurial

La interfaz web de mercurial no te permite navegar por los cambios previos y sus archivos diff pero te recomendamos el uso de Mercurial directamente desde la consola o alguno de los numerosos clientes GUI para Mercurial que son más potentes. Por ejemplo te permiten sincronizar tu aplicación con un repositorio remoto:

imágenes

Puedes leer más sobre Mercurial aquí:

http://mercurial.selenic.com/

El Asistente de admin (experimental)

La intefaz admin incluye un asistente que puede ayudarte en la creación de nuevas aplicaciones. Puedes acceder al asistente desde la página "sites" como se muestra en la imagen de abajo.

imagen

El asistente te guiará a través de una serie de pasos para la creación de una nueva aplicación:

  • Elegir un nombre para la aplicación
  • Configurar la aplicación y elegir los plugin necesarios
  • Armar los modelos requeridos (creará páginas de ABM/CRUD para cada modelo)
  • Te permitirá editar las vistas de esas páginas utilizando la sintaxis MARKMIN

La imagen de abajo muestra la segunda fase del proceso.

imagen

Se puede ver un menú desplegable para la elección de un plugin de plantilla general (desde web2py.com/layouts), un menú de opción múltiple para agregar un conjunto de plugin extra (desde web2py.com/plugins) y un campo "login config" para ingresar una "domain:key" de Janrain.

Las demás fases son un tanto más simples, por lo que obviamos su explicación.

El asistente es eficaz para su objetivo pero es considerado una "funcionalidad experimental" por dos razones:

  • Las aplicaciones creadas con el asistente y editadas manualmente ya no pueden ser más modificadas por el asistente.
  • La interfaz del asistente cambiará eventualmente para incluir soporte para más características y un desarrollo visual más fácil.

En todo caso el asistente es una herramienta útil para crear prototipos velozmente y puede servir como plantilla (bootstrap) de una aplicación con un diseño alternativo y otro conjunto de plugin.

Configurando admin

Normalmente no hay necesidad de modificar ninguna configuración en admin aunque son posibles algunas personalizaciones. Luego de autenticarte en admin puedes editar la configuración a través la URL:

http://127.0.0.1:8000/admin/default/edit/admin/models/0.py

Ten en cuenta cómo admin puede utilizarse para auto-modificarse. De hecho admin es una app como cualquier otra.

El archivo "0.py" contiene suficientemente documentación. De todas formas, aquí se muestran las personalizaciones más importantes que puedes necesitar:

GAE_APPCFG = os.path.abspath(os.path.join('/usr/local/bin/appcfg.py'))

Esto debería apuntar a la ubicación del archivo "appcfg.py" que viene con el SDK de Google App Engine. Si tienes el SDK quizás quieras cambiar estos parámetros de configuración a los valores correctos. Esto te permitirá desplegar en GAE desde la interfaz de admin.

DEMO_MODE

Además puedes configurar la app admin de web2py en modo demo:

DEMO_MODE = True
FILTER_APPS = ['welcome']

Y sólo las aplicaciones listadas en FILTER_APPS estarán disponibles y sólo se podrá acceder a ellas en modo de solo-lectura.

MULTI_USER_MODE
virtual laboratory

Si eres docente y quieres exponer la interfaz administrativa a estudiantes para que puedan compartir una interfaz administrativa para sus proyectos (piensa en un laboratorio virtual), lo puedes hacer configurando:

MULTI_USER_MODE = True

De esa forma los estudiantes deberán autenticarse y sólo podrán acceder a sus propias aplicaciones a través de admin. Tú, como el usuario principal/maestro, tendrás acceso a todas.

En modo multiusuario, puedes registrar estudiantes usando un link para el registro múltiple o bulk register y administrarlos usando el link correspondiente (manage students''). El sistema además llevará un registro de los accesos de los estudiantes y de la cantidad de líneas que agregan o eliminan de su código fuente. Esta información puede ser consultada por el administrador por medio de gráficos en la página "acerca de" de la aplicación.

Ten en cuenta que este mecanismo de todas formas asume que todos los usuarios son confiables. Todas las aplicaciones creadas bajo admin corren con las mismas credenciales en el mismo sistema de archivos. Es posible para una aplicación creada por un estudiante el acceso a los datos y el código fuente de una app de otro estudiante. Además sería posible para un estudiante crear una app que prohíba el acceso al servidor.

admin móvil

Ten en cuenta que la aplicación admin incluye el plugin "plugin_jqmobile", que incluye la librería jQuery Mobile. Cuando se accede a la app admin desde un dispositivo móvil, web2py lo detectará y mostrará una interfaz gráfica apta para el dispositivo.

imagen

Más acerca de appadmin

appadmin

appadmin no está pensada para ser expuesta al público. Está diseñada para ayudarte como forma de fácil acceso a la base de datos. Consiste de sólo dos archivos: un controlador "appadmin.py" y una vista "appadmin.html", que son utilizados por todas las acciones en el controlador.

El controlador de appadmin es relativamente pequeño y legible; sirve además como ejemplo para el diseño de una interfaz de acceso a la base de datos.

appadmin muestra cuales bases de datos están disponibles y qué tablas existen en cada base de datos. Puedes insertar registros y listar todos los registros para cada tabla individualmente. appadmin hace una separación en páginas de la salida por cada 100 registros.

Una vez que se selecciona un set de registros, el encabezado de las páginas cambia, permitiéndote actualizar o eliminar los registros devueltos por la consulta.

Para actualizar los registros, ingresa un criterio SQL en el campo para la cadena de la consulta:

title = 'prueba'

donde los valores de la cadena deben estar entre comillas simples. Los criterios múltiples se pueden separar con comas.

Para eliminar un registro, haz clic en el checkbox para confirmar que estás seguro.

appadmin también puede realizar consultas tipo join si el FILTRO de SQL contiene una instrucción condicional de SQL que tenga dos o más tablas. Por ejemplo, prueba con:

db.imagen.id == db.publicacion.imagen_id

web2py le pasa el comando a la DAL, que entiende que la consulta asocia dos tablas; así, las dos tablas se consultan con un INNER JOIN. Esta es la salida:

imagen

Si haces clic en el número de un campo id, obtienes una página de edición para el registro correspondiente.

Si haces clic en el número de un campo tipo reference, obtienes una página de edición para el registro de referencia.

No se pueden actualizar o eliminar registros consultados con un join, porque implicaría la modificación de registros de múltiples tablas y podría resultar confuso.

Aparte de sus funciones para administración de la base de datos, appadmin además te permite visualizar el detalle de los contenidos del cache de la aplicación (en /tuapp/appadmin/cache) así como también los contenidos de los objetos request, response, y session (en /tuapp/appadmin/state).

appadmin reemplaza response.menu con su propio menú, que incluye, para la app actual, accesos a la página edit en admin, la página db (administración de la base de datos), la página state, y la página cache. Si la plantilla general de tu aplicación no genera un menú usando response.menu, entonces no verás el menú de appadmin. En este caso, puedes modificar el archivo appadmin.html y agregar {{=MENU(response.menu)}} para mostrar el menú.

 top