Apuntes de py4web

Work in progress

Apuntes de py4web, muy incompletos.

De momento son una copia descarada del curso de Luca de Alfaro (que está genial para aprender)

Referencias

¿Que aplicación queremos crear?

Una aplicación de inventario que llamaremos Cornucopia.

Instalación de py4web

Clonando py4web desde github

  1. Como de costumbre creamos un entorno virtual para trabajar. Con un entorno virtual independiente podremos instalar todas las bibliotecas python que queramos sin interferir con el python del sistema. Personalmente uso pyenv para gestionar los entornos virtuales en mi sistema por que me permite usar cualquier versión de Python. (Echa un ojo a los apuntes de python si quieres saber mas de pyenv)

    1
    
    pyenv virtualenv 3.9.7 ve_py4web
  2. Creamos un directorio de trabajo y asignamos el entorno virtual. Es decir que usando otra vez pyenv vamos a asociar el entorno virtual que creamos en el paso anterior a un directorio de nuestro disco (como entorno virtual local). Nuestro sistema, (gracias a pyenv sabrá que hay que activar el virtualenv local si estamos trabajando en este directorio o en cualquiera de sus descendientes. En otras palabras estamos activan el virtualenv automáticamente si estamos en algún directorio del proyecto.

    1
    2
    3
    4
    
    mkdir webdev
    cd webdev
    pyenv local ve_py4web
    myve                   # Esta macro se encarga de instalar lsp en mi entorno python

    El alias myve en mi ordenador equivale a ejecutar las lineas de abajo. Es decir instala en nuestro nuevo entorno virtual algunas bibliotecas básicas para gestionar entornos virtuales e instalar otras bibliotecas. Además también instala el Language Server Protocol server, que nos valdrá para tener facilidades adicionales al editar ficheros de Python:

    1
    2
    
    pip install --upgrade pip setuptools wheel pipx virtualenv virtualenvwrapper
    pip install 'python-lsp-server[all]'
  3. Clonamos el py4web:

    1
    
    git clone https://github.com/web2py/py4web.git
  4. Instalamos las dependencias de py4web, es decir las bibliotecas de python que el programa py4web necesita para funcionar.

    1
    2
    
    cd py4web
    pip install -r ./requirements.txt
  5. Ya estamos listos para arrancar nuestra nueva instancia de py4web, podemos comprobar que funciona con ./py4web.py version. Para arrancar la aplicación, establecemos la password de administración y lanzamos la aplicación especificando en que directorio residen las aplicaciones.

1
2
./py4web.py set_password
./py4web.py run apps

Instalando py4web con pip

Seguimos las instrucciones del github de py4web, coincide con lo que comenta Luca de Alfaro en este segundo video.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  pyenv virtualenv 3.11.4 ve_p4w_311
  amd p4w_311
  pyenv local ve_p4w_311
  myve                 # esta macro se encarga de instalar LSP en mis entornos python
  pip install --upgrade --no-cache-dir py4web

  py4web setup apps    # Instalar todas contestando a todo que si
  py4web set_password  # Configurar password de administración
  py4web run apps      # Arrancar el sistema

  py4web -h            # resumen de comandos

Siempre podemos añadir el scaffold_bulma como comentamos en el apartado anterior.

Preparando la primera app

Pasos a seguir:

  1. Crear la app dentro de nuestro py4web

  2. Decidir la base de datos

  3. Decidir el tipo de session (fichero settings.py)

  4. Cambiar el SESSION_SECRET_KEY en el fichero settings.py

    login con username

    Si queremos activar el uso de usernames (un nombre de usuario abreviado para el login) tenemos que cambiar la opción auth.use_username en el fichero common.py.

    Si hacemos este cambio y usamos sqlite3 como base de datos py4web no será capaz de cambiar la estructura de la tabla “al vuelo”. Tendremos que cambiarla a mano en la base de datos si ya está creada, o directamente borrar la base de datos y permitir que py4web la genere de nuevo.

  5. Configurar el modelo de nuestra app (la estructura de la base de datos)

Crear la app dentro de nuestro py4web

En la instalación que hemos propuesto, tendremos py4web instalado en el directorio ..../webdev/py4web, podemos crear una nueva aplicación desde el dashboard del propio py4web o crearla nosotros a mano en el directorio ..../webdev/py4web/apps

Tenemos varios templates para escoger a la hora de crear una nueva aplicación:

minimal

es un template que contiene lo mínimo imprescindible para empezar

scaffold

Un template creado por Maximo Di Pietro, mucho más completa que minimal. Basado en no.css un framework CSSCascade Style Sheets extremadamente simple.

scaffold_bulma

Un template creado por Luca de Alfaro, basado en Bulma otro framework CSS (con la particularidad de que no necesita JavaScript). Este template no venía por defecto con py4web lo he descargado del Bitbucket de Luca de Alfaro.

Vamos a usar como punto de partida la propuesta de Luca De Alfaro, un Template basado en Bulma para la parte CSS. Podemos pasarle la dirección del github al Dashboard y crear una nueva aplicación o clonarlo con git con:

1
2
cd py4web/apps
git clone git@bitbucket.org:luca_de_alfaro/scaffold_bulma.git cornucopia

Con esto tendremos un nuevo directorio ..../webdev/py4web/apps/cornucopia que contendrá nuestra nueva aplicación. Como queremos tener nuestra aplicación en nuestro propio git tenemos que cambiar la URL asociada a nuestra propia dirección (p.ej. <git@git.comacero.com:py4web/cornucopia.git>) para ello:

1
2
3
4
5
cd cornucopia
git remote -v
git remote set-url origin git@git.comacero.com:py4web/cornucopia.git
git remote -v
git push --set-upstream --all

Así conseguimos que el directorio ..../webdev/py4web esté apuntando al git original de py4web (para poder actualizar fácilmente), y el directorio de nuestra app apuntando a nuestro propio git para tener controlada el desarrollo del software.

Estructura de nuestra aplicación

En el nuevo directorio de nuestra aplicación tenemos los siguientes subdirectorios:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
tree -d cornucopia
cornucopia
├── databases
├── __pycache__
├── static
│   ├── css
│   ├── font-awesome-4.7.0
│   │   ├── css
│   │   ├── fonts
│   │   ├── less
│   │   └── scss
│   └── js
├── templates
├── translations
└── uploads
static

Almacena el contenido estático, aquí nos encontraremos imágenes (como logos por ejemplo), ficheros CSS y JavaScript.

databases

En este directorio se almacena la base de datos, se usa principalmente en el desarrollo con motores de base de datos como SQLite, en producción lo normal es usar motores de base de datos más potentes; como MariaDB o Postgresql que no almacenarán sus datos en este directorio.

templates

Aquí residen las plantillas de las páginas web de nuestra aplicación

translations

En este directorio tenemos los ficheros de I18n para nuestra aplicación.

uploads

Aquí se almacenan los contenidos de tipo upload que usemos en nuestra applicación (si es que los usamos, claro)

py4web va a cargar nuestra aplicación como un módulo Python. Por eso en el directorio cornucopia tenemos un fichero __init__.py Si vemos el contenido del fichero veremos que hace tres cosas:

  • Carga el módulo general py4web
  • Importa la definición de db desde el fichero models.py
  • Importa los controladores desde el fichero controllers.py

En el fichero controllers.py es donde definimos todas las rutas que tendrá nuestra aplicación. Un ejemplo chorras de ruta sería:

1
2
3
4
@action('sample-page')
@action.uses('spage.html')
def serve_sample_page():
  return dict()
  • El decorador @action define la ruta, es decir la URL asociada que en nuestro caso con el servidor local sería https://localhost:8000/cornucopia/sample-page
  • El decorador @action.uses define los recursos necesarios para esta nueva ruta, en este caso solo especificamos un html template que tiene que existir en el directorio cornucopia/templates
  • Por último definimos la función que implementa el controlador de la ruta, es imprescindible que esta función devuelva un diccionario. El diccionario sirve para pasar valores al html template, pero en nuestro caso no son necesarios así que devolvemos un diccionario vacío.

Otros ficheros de la aplicación:

settings.py

contiene declaraciones de valores usados por la aplicación. Por ejemplo que base de datos usamos, etc. etc.

models.py

contiene las definiciones de las tablas de la base de datos

common.py

Define varias valores fundamentales para el funcionamiento de la aplicación. Entre otros:

  • La conexión a la base de datos
  • El tipo de sesión que usaremos
  • El mecanismo de autenticación que usará la aplicación

El flujo de trabajo durante el desarrollo

Nos vamos a pasar la mayor parte del tiempo escribiendo controllers y templates para cada ruta que necesitemos en nuestra aplicación vamos a tener que codificar un controller y casi con toda seguridad su correspondiente template html

Para la parte de los templates vamos a usar un lenguage de templates: YATLYet Another Template Language (ver documentación de YATL)aunque py4web soporta también Renoir (aparentemente usa los dos tras las bambalinas y de forma transparente para nosotros)

Lenguajes de template

Los lenguajes de template permiten definir plantillas de documentos (de cualquier tipo), donde se hacen operaciones de sustitución de parámetros o incluso secciones de código de programa, para generar el documento final.

El concepto ha demostrado ser tan potente que hay implementaciones en practicamente todos los lenguajes de programación (cuando no se implementa directamente en el propio lenguaje)

Hay muchos lenguajes de template para Python, probablemente Jinja sea el más conocido. En py4web se usa YATL.

Decidir que motor de base de datos vamos a usar.

Por defecto py4web va a usar SQLite como motor de base de datos, pero soporta muchos más.

Podemos seguir con nuestro desarrollo en SQLite sin más. En ese caso puedes saltarte la instalación de MariaDB.

Para el caso de que alguien quiera hacer el desarrollo con un motor de base de datos más potente que SQLite vamos a describir como usar MariaDB (instrucciones también válidas para MySQL) para tener al menos dos opciones y por si alguien quiere experimentar con un motor de base de datos más potente que SQLite.

Yo voy a instalar MariaDB como un contenedor Docker para el desarrollo va perfecto.

Cuando se pase la aplicación a producción habrá que ver cual es la mejor opción. Se podría seguir con el servidor “dockerizado” o decidir si se quiere una instalación de MariaDB en el servidor real (el host si hablamos la jerga de contenedores) o incluso podría ser que tuviéramos un servidor de base de datos en nuestra red y quisieramos usarlo para nuestra aplicación. Hay muchas posibilidades.

Usando MariaDB como base de datos

MySQL
Si queremos usar MySQL en lugar de MariaDB el procedimiento sería exactamente el mismo que el descrito.

Como ya comenté, me voy a instalar la base de datos en Docker, prefiero tenerla lo más aislada posible en el portatil.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Bajamos la imagen del hub de Docker
docker pull mariadb:10.7.3

# Lanzamos el servidor de base de datos:
docker run --detach --name my-mariadb --publish 3306:3306  \
--env MARIADB_USER=pyuser --env MARIADB_PASSWORD=secreto \
--env MARIADB_ROOT_PASSWORD=secreto mariadb:10.7.3

# Instalamos el cliente de mariadb en nuestro linux
sudo apt install mariadb-client

# Nos conectamos a la base de datos (para probar el acceso)
mariadb -h 127.0.0.1 -u pyuser -p

Ya tenemos un servidor de bases de datos MariaDB accesible desde nuestro PC. Al estar dockerizada tenemos que hacer todas las conexiones con la dirección IP, como si fuera un servidor independiente en nuestra red, aunque usamos la IP local: 127.0.0.1 como dirección del servidor MariaDB.

También tenemos que crear manualmente la base de datos que vamos a usar. Crearemos la base de datos cornucopiadb y daremos todos los privilegios de acceso sobre esta base de datos al usuario pyuser. El comando de conexion a la base de datos sería:

1
mariadb -h 127.0.0.1 -u root -p

Y para crear la base de datos y dar privilegios:

1
2
3
4
create database cornucopiadb;
grant all privileges on cornucopiadb.* to pyuser@'%';
flush privileges;
quit

Es decir:

  • Creamos la base de datos cornucopiadb
  • Concedemos todos los privilegios al usario pyuser conectado desde cualquier IP a todas las tablas de cornucopiadb
  • Hacemos un flush para que los privilegios se activen de inmediato

El siguiente paso es instalar alguna biblioteca Python de conexión a la base de datos. Cualquiera de los conectores disponibles para MySQL debería funcionar correctamente con MariaDB. En nuestro caso vamos a instalar pymysql. Nos aseguramos de tener activado el entorno virtual del proyecto e instalamos con pip:

1
2
pyenv which pip
pip install pymysql

Sabiendo que biblioteca de conexión a MariaDB tenemos instalada ya podemos definir la conexión a la base de datos en nuestra aplicación py4web. Tenemos que editar el parámetro DB_URI en el fichero settings.py de nuestra aplicación.

El DB_URI que viene configurado por defecto es el de sqlite3, en la documentación oficial podemos comprobar que necesitamos algo como: mysql://pyuser:secreto@127.0.0.1/cornucopiadb?set_encoding =utf8mb4.

Una vez editado el fichero settings.py y cambiado el DB_URI podemos arrancar nuestra aplicación con ./py4web run apps

Otros motores de base de datos
Los pasos descritos deberían ser muy parecidos al margen del motor de base de datos que usemos. Solo tendremos que instalar la biblioteca python adecuada a la base de datos elegida y ajustar el DB_URI a ese motor de base de datos.

Decidir el tipo de sesión y cambio del SESSION_SECRET_KEY

En el video-curso de Luca de Alfaro, nos explican cuales son las funciones de una session, y por qué es imprescindible tener un mecanismo para gestionar sesiones de usuario en nuestra aplicación. Si no lo tienes claro mira los videos:

Editamos el fichero settings.py de nuestra nueva aplicación y fijamos el tipo de sesión (recomendable usar database)

Cambiamos el parámetro SESSION_SECRET_KEY que viene por defecto en el template por uno nuevo. Podemos usar un generador de _uuid: como este.

Definiendo nuestro modelo

Una vez arrancado py4web podemos conectarnos a la base de datos (da igual que sea sqlite3 o MariaDB) y comprobar que py4web ha creado las tablas necesarias para la gestión de usuarios y sesiones. Podemos comprobarlo desde nuestro cliente de base de datos con el comando .tables si usamos sqlite3 o con el comando show tables si estamos usando MariaDB. También podemos comprobarlo desde el propio cuadro de mando del py4web.

Veremos que py4web ha creado las tablas:

  • auth_user
  • auth_user_tag_groups
  • py4web_session

Todas las tablas estarán vacías, puesto que aun no hemos creado usuarios de la aplicación (lo haremos más adelante). Pero ya vemos que py4web es capaz de ir creando nuevas tablas en la base de datos que hayamos definido. Así, a medida que vayamos definiendo el modelo de nuestra aplicación, veremos que automáticamente se crean (y/o modifican) las tablas correspondientes en nuestra base de datos.

Definiendo tablas

El objetivo de nuestra aplicación es mantener un inventario de “cosas”. Parece lógico que nuestra primera tabla valga para almacenar “cosas”. Así que en el fichero cornucopia/models.py añadimos las siguientes lineas (en la sección indicada) y salvamos el fichero:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
### Define your table below
#
# db.define_table('thing', Field('name'))
#
## always commit your models to avoid problems later

db.define_table('thing',
    Field('id', 'integer'),
    Field('name', 'string')
    Field('description', 'string'),
    migrate = True)

Ya tenemos creada nuestra nueva tabla thing. La hemos definido de forma explícita con dos campos: id y description y un parámetro adicional: migrate = True

Lo cierto es que se recomienda no crear explicitamente el campo id. py4web se va a encargar de crear siempre este campo para todas las tablas. Es un campo entero autoincremental (generalmente empezando por 1); esto quiere decir que cada vez que se inserta un nuevo registro en la tabla, la base de datos va a asignar un entero en el campo id incrementando un contador interno de la tabla.

El parámetro migrate = True hace que py4web intente mantener la definición de la base de datos real (en el motor de base de datos que estemos usando) alineada con el modelo que definamos en el fichero models.py. Si cambiamos la definición, se ejecutarán los comandos de base de datos necesarios para cambiar la definición de la tabla en la base de datos.

En realidad se ha definido migrate = True por defecto para todas las tablas en el fichero settings.py así que la definición de nuestra tabla podria quedar commo:

1
2
3
Table('thing',
      Field('name', 'string')
      Field('descriptor', 'string'))

Si recargamos el fichero models.py (desde el Dashboard), podremos comprobar que se ha creado la correspondiente tabla en la base de datos y podríamos crear algunos registros (lineas de la tabla) en ella a través del propio Dashboard de py4web.

Laa tabla es demasiado simple, evidentemente tenemos que mejorar nuestro modelo. Pero antes de profundizar vamos a echar un vistazo a los formularios.

El primer formulario

Un formulario simple

Los formularios (html forms) son una parte importantísima de nuestra aplicación. Gran parte de las interacciones con los usuarios se harán via formularios html.

Ya tenemos definida nuestra tabla thing, vamos a ver como crear un formulario (html form) para añadir nuevos objetos thing a nuestra base de datos.

En la parte del template definimos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[[extend 'layout.html']]

<div class="section">
  Add a new Thing

  <div>
    <form action="[[=URL('add') ]]" method="POST">
      <div class="field">
        <input class="input" name="thing_name" type="text" placeholder="Thing name here" />
        <input class="button" name="" type="submit" value="Submit"/>
      </div>
    </form>
  </div>
</div>

y en la parte del controller vamos a añadir una ruta add:

1
2
3
4
5
6
7
8
9
@action('add', method=['GET', 'POST'])
@action.uses('add.html', db, auth.user)
def add():
    if request.method == 'GET':
        return dict()
    else:
        print(request.params.get("thing_name"))
        db.thing.insert(name=request.params.get("thing_name"))
        redirect(URL('add'))

El primer decorador de nuestro controller (en la primera línea) define la ruta asociada, que será <server_url>/<app_name>/add, en nuestro caso podría quedar como http://127.0.0.1:8000/cornucopia/add. Además especificamos que el controlador atiende peticiones (html requests) de tipo GET y POST.

El segundo decorador del controller (@action.uses... en la segunda línea) define los fixtures asociados al controller:

  • En primer lugar asocia el template a la ruta, en nuestro caso add.html. Es importante que sea el primero por que los que vengan a continuación a menudo inyectarán funciones en el template para que las podamos usar al generar el código html
  • Después un objeto db que encapsula la conexión a la base de datos. Esta conexión está definida en el fichero common.py y en nuestro caso nos permite acceder a una base de datos en el servidor MariaDB.
  • Aunque no está declarado especificamente también se asigna el session fixture. @ El auth fixture nos permite gestionar el login del usuario, los permisos del usuario, etc. Hay varias opciones para configurar el comportamiento de auth en el fichero common.py. Al especificar auth.user será necesario ser un usuario autenticado para acceder a esta ruta.
Fixtures

Un objeto de clase Fixture en py4web implementa los siguientes métodos:

  • on_request que se invoca al recibir un html request
  • on_error que se invoca si hay un error al procesar la request
  • on_success que se invoca una vez procesada con éxito la request
  • transform que se invoca tras procesar la request para transformar la salida de la misma

Por ejemplo el DAL fixture:

  • on_request: hace una reconexión con la base de datos (es configurable)
  • on_error: hace un rollback de la transacción pendiente en la base de datos
  • on_success: hace un commit de la transacción pendiente en la base de datos

Cuando visitamos la URL <server_url>/cornucopia/add estamos haciendo un GET así que nuestro controller devuelve un diccionario vacío, py4web renderiza el template y lo envía como respuesta al navegador del usuario que verá en su pantalla el formulario html.

Cuando el usuario hace un Submit con el botón correspondiente, lo que enviamos al servidor web es una POST request, así que nuestro controller va a imprimir en la consola el nombre de nuestra thing y la va a insertar en la base de datos.

En el código podemos ver como consultar los parámetros del payload de una POST request, py4web está basado en Bottle.py así que podemos consultar la documentación de Bottle para cualquier duda.

También podemos ver como se hace una inserción en la base de datos con ayuda del DALDatabase Abstraction Layer de py4web. Vamos a hacer un uso intensivo del DAL si quieres puedes echar un ojo a la documentación

De todas formas esto no es más que un ejemplo para ver un formulario básico, en realidad jamás los vamos a implementar así por que por un lado es inseguro, y por otro lado py4web nos ofrece facilidades mucho más potentes para hacerlos.

Un formulario usando las “facilidades” de py4web

El formulario simple que hemos definido en el punto anterior, además de dar bastante trabajo es inseguro cualquiera podría crear “cosas” en nuestra base de datos enviando POST Requests a nuestro servidor.

1
2
3
4
5
6
7
8
9
@action('add', method=['GET', 'POST'])
@action.uses('add.html', db, auth.user)
def add():
    form = Form(db.thing, csrf_session=session, formstyle=FormStyleBulma)
    if form.accepted:
        # We simply redirect, the insertion already happened
        redirect(URL('index'))
    # This is a GET or a POST with errors
    return(dict(form=form))

Este es nuestro formulario re-escrito. La parte importante es que ahora usamos Form() (es imprescindible hacer un from py4web.utils.form import Form, FormStyleBulma al principio de nuestro módulo controllers.py)

Form() genera el formulario html a partir de la definición de la tabla en la base de datos. A mayores le pasamos dos parámetros: uno para que genere un formulario con estilo “Bulma” y otro para que el formulario vaya protegido con una clave basada en la sesión del usuario.

Además Form() nos ofrece un método para saber si el formulario ha sido recibido con datos válidos y aceptado. En ese caso simplemente dirigimos al usuario a la página principal (de momento).

El fichero de template quedaría tan simple como:

1
2
3
4
5
6
7
8
9
[[extend 'layout.html']]

<div class="section">
  Add a new Thing

  <div>
    [[=form]]
  </div>
</div>

Refinar la base de datos

Para comprobar como nuestro formulario se adapta automáticamente a la definición de la tabla en la base de datos vamos a añadir un par de campos a la definición. En el fichero models.py vamos a añadir también una helper function:

1
2
3
def get_user_username():
    """Return user username if we have an auth_user."""
    return auth.current_user.get('username') if auth.current_user else None

Esta función nos devuelve el username del usuario logueado en el servidor (si es que se ha logueado claro) En general todo el template Bulma que nos propone Luca De Alfaro se orienta a usar el correo del usuario como identidad del mismo (y tiene sus ventajas), pero a mi me gusta usar el login.

La nueva definición de la tabla queda:

1
2
3
4
5
6
7
db.define_table('thing',
                Field('id', 'integer'),
                Field('name', 'string', requires=IS_NOT_EMPTY()),
                Field('description', 'string'),
                Field('created_by', default=get_user_username),
                Field('creation_date', 'datetime', default=get_time),
                migrate=True)

Hemos añadido dos campos de tal manera que el propio py4web se va a encargar de poner el valor cuando creemos una nueva “cosa” usando las helper functions get_time y get_username que tenemos en el fichero models.py.

También hemos especificado el parámetro migrate=True, este es el valor por defecto así que no vamos a cambiar nada por especificarlo. El parámetro a True hace que py4web cree o modifique las tablas en la base de datos cuando modifiquemos el fichero models.py (ver documentación

Si ahora comprobamos nuestra aplicación en el py4web veremos que:

a) La estructura de la tabla en la base de datos se ha actualizado y ahora tenemos los dos campos nuevos b) El formulario de la página add también se ha actualizado para mostrar los nuevos campos a la hora de crear una nueva “cosa”

De todas formas los dos nuevos campos no deberían ser actualizables o iniciados por el usuario de la aplicación, es mejor reservarlos para que se inicien con los valores por defecto, para eso nos basta con cambiar el modelo y dejarlo así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
db.define_table('thing',
                Field('id', 'integer'),
                Field('name', 'string', requires=IS_NOT_EMPTY()),
                Field('description', 'string'),
                Field('created_by', default=get_user_username),
                Field('creation_date', 'datetime', default=get_time),
                migrate=True)


db.thing.id.readable = db.thing.id.writable = False
db.thing.created_by.readable = db.thing.created_by.writable = False
db.thing.creation_date.readable = db.thing.creation_date.writable = False

Con esto comprobaremos que al añadir una “cosa” ya no nos aparecen los campos en el formulario.

Listado de “cosas”

Vamos a añadir un listado de “cosas” a nuestra aplicación, de momento lo añadimos en la página principal (una chapuza, pero lo corregiremos)

Para empezar cambiamos el controller que se ocupa de la página principal:

1
2
3
4
5
@action('index')
@action.uses('index.html', db, auth)
def index():
    rows = db(db.thing).select()
    return dict(rows=rows)

Con esto hemos usado por primera vez el DALDatabase Abstraction Layer de py4web. En la línea 4 estamos haciendo un select de todas las “cosas”" que hay en la tabla things. Y lo pasamos al template a través de la variable rows.

En el template tenemos el siguiente código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[[extend 'layout.html']]

<div class="section">
  <div class="container">
    <table class="table is-stripped is-fullwidth">
      <tr>
        <th>Id</th>
        <th>Name</th>
        <th>Description</th>
        <th>Created by</th>
        <th>Created on</th>
      </tr>
      [[for row in rows:]]
      <tr>
        <td>[[=row.id]]</td>
        <td>[[=row.name]]</td>
        <td>[[=row.description]]</td>
        <td>[[=row.created_by]]</td>
        <td>[[=row.creation_date]]</td>
      </tr>
      [[pass]]
    </table>
  </div>
</div>

Las líneas 1 ~ 12 definen una página web con una tabla (vamos a presentar una “cosa” por cada linea de la tabla) En las lineas 13 ~ 21 tenemos un bucle for que itera sobre todas las rows que hemos pasado al template generando una linea de la tabla para cada row, es decir para cada “cosa” almacenada en la base de datos.

Esta tabla es bastante chorras y no nos vale de mucho. Vamos a darle un poco más de funcionalidad

Listado de “cosas” interactivo

Vamos a cambiar el código html para que en cada linea de la tabla (cada “cosa”) tengamos un botón de edición y un botón de borrado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
[[extend 'layout.html']]

<div class="section">
  <div class="container">
    <table class="table is-stripped is-fullwidth">
      <tr>
        <th>Id</th>
        <th>Name</th>
        <th>Description</th>
        <th>Created by</th>
        <th>Created on</th>
        <th></th>
        <th></th>
      </tr>
      [[for row in rows:]]
      <tr>
        <td>[[=row.id]]</td>
        <td>[[=row.name]]</td>
        <td>[[=row.description]]</td>
        <td>[[=row.created_by]]</td>
        <td>[[=row.creation_date]]</td>
        <td>
          <a class="button" href="[[=URL('edit', row.id)]]">
            <span class="icon"><i class="fa fa-fw fa-pencil"></i></span> <span>Edit</span>
          </a>
        </td>
        <td>
          <a class="button" href="[[=URL('delete', row.id, signer=url_signer)]]">
            <span class="icon"><i class="fa fa-fw fa-trash"></i></span>
          </a>
        </td>
      </tr>
      [[pass]]
    </table>
  </div>
</div>

Hemos añadido un par de títulos de columna vacíos para que la tabla quede bonita, y en cada linea definimos los botones (lineas 22 ~ 32)

Para el botón Edit definimos un botón con un icono y un texto.

Para el botón Delete solo ponemos el icono de la papelera.

Evidentemente tendremos que definir los correspondientes controllers para las acciones de Edición y Borrado de los objetos “cosa” de nuestra base de datos. Pero hay que fijarse también en como generamos las URL asociadas a los botones:

Edición

La url la generamos con URL('edit', row.id) eso nos va a generar una url de la forma: http://<host>:<port>/<appName>/edit/<id> en nuestro caso sería algo como http://127.0.0.1:8000/cornucopia/edit/83. Y la tradución sería: quiero editar la “cosa” con id=83

Para que el controller lea correctamente este URL tenemos que informar de las estructura del path que añadimos a la url. En nuestro caso el path es sencillamente /<thing_id> pero podría ser mucho más largo y con más parámetros.

Nuestro controller sería:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@action('edit/<thing_id:int>', method=['GET', 'POST'])
@action.uses('edit.html', url_signer, db, session, auth.user)
def edit(thing_id=None):
    assert(thing_id is not None)
    # my_thing = db(db.thing.id == thing.id).select().first()
    my_thing = db.thing[thing_id]
    if my_thing is None:
        # Nothing found to edit
        redirect(URL('index'))
    form = Form(db.thing, record=my_thing,
                csrf_session=session, formstyle=FormStyleBulma)

    if form.accepted:
        # The update has been done
        redirect(URL('index'))
    return dict(form=form)
  • En la línea 1 el decorador @action define la interpretación del path, como thing_id que será un entero
  • En la línea 3, el controller toma el thing_id como parámetro, además si no nos lo pasan (hay que ser siempre paranoico) lo ponemos a None y provocamos un fallo en la línea 4
  • Podríamos seleccionar nuestra “cosa” en la base de datos explicitamente como en la linea 5, pero esto se hace con tanta frecuencia que py4web nos ofrece una forma abreviada, que usamos en la linea 6
  • Por fin, si no encontramos la “cosa” en la base de datos (siempre paranoicos), nos vamos a la página principal sin hacer nada. Si la encontramos generamos un formulario para editarla especificando el parámetro recorden la llamada a Form()
  • Las lineas 13 ~ 15 se encargan de redirigir a la página principal si tenemos un POST request y el formulario ha completado una edición
  • En caso contrario, aun tenemos que preguntar al usuario que quiere editar así que pasamos el formulario al template y generamos la página de eición para el usuario.

Borrado

Para el botón de borrado la url generada es ligeramente diferente, usamos el comando: URL('delete', row.id, signer=url_signer), donde especificamos que la html request debe ir firmada.

La acción de borrado se va a ejecutar directamente en el controller en cuando el botón lance la request así que necesitamos asegurarnos de la autenticidad. La firma va a depender tanto de la sesión como del contenido de la propia request para que un atacante no pueda falsificar peticiones de borrado.

El controller nos quedaría como especificamos a continuación:

1
2
3
4
5
6
@action('delete/<thing_id:int>')
@action.uses(db, session, auth.user, url_signer.verify())
def delete(product_id=None):
    assert product_id is not None
    db(db.product.id == thing_id).delete()
    redirect(URL('index'))

Vemos que simplemente procede al borrado de nuestra “cosa” en la base de datos y redirige de nuevo a la página principal. Ni siquiera necesitamos un template ya que no tenemos una página web dedicada a la acción de Borrar.



Apuntes de web2py
Lo que sigue a continuación de este aviso son mis antiguos apuntes de web2py

web2py

web2py es un framework para facilitar el desarrollo de aplicaciones web escrito en Python.

web2py funciona correctamente en Python 3. Su curva de aprendizaje no es tan empinada como la de Django (que es el framework de aplicaciones web de referencia en Python) y en muchos sentidos es más moderno que Django.

web2py tiene una documentación muy completa y actualizada (disponible también en castellano) y sobre todo una comunidad de usuarios y desarrolladores muy activa y que responden con rapidez a las dudas que puedas plantear.

web2py está basado en el modelo MVC

web2py incorpora Bootstrap 4

Empezar rápido

Instalación

Vamos a ver el proceso de instalación de una instancia de web2py en modo standalone. web2py instalado de esta forma es ideal para entornos de desarrollo. Para un entorno de producción puede ser más conveniente instalar web2py tras un servidor web como Apache o Nginx, pero dependiendo de la carga de trabajo y de como administres tus sistemas puede ser mejor opción usarlo standalone también en producción.

  1. Creamos un entorno virtual

    Como ya hemos comentado web2py funciona ya en Python 3. Y en cualquier caso, con Python nunca está de mas encapsular nuestras pruebas y desarrollos en un entorno virtual.^[Los siguientes comandos asumen que tienes instalado virtualenvwrapper como recomendamos en la guía de postinstalación de Linux Mint, si no lo tienes te recomendamos crear un virtualenv con los comandos tradicionales] Así que creamos el virtualenv que llamaremos web2py:

    1
    
    mkvirtualenv -p `which python3` web2py
  2. Bajamos el programa de la web de Web2py y descomprimimos el framework:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    # creamos un directorio (cambia el path a tu gusto)
    mkdir web2py_test
    cd web2py_test
    
    # bajamos el programa de la web y descomprimimos
    wget https://mdipierro.pythonanywhere.com/examples/static/web2py_src.zip
    
    # opcionalmente borramos el zip, aunque sería mejor guardarlo
    # por si queremos hacer nuevas instalaciones
    rm web2py_src.zip
  3. Generamos certificados para el protocolo ssl:

    Para usar con comodidad web2py conviene que nos generemos unos certificados para gestionar el ssl:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    # nos movemos al directorio de web2py
    cd web2py
    
    openssl genrsa -out server.key 2048
    openssl req -new -key server.key -out server.csr
    
    Country Name (2 letter code) [AU]:ES
    State or Province Name (full name) [Some-State]:A Coruna
    Locality Name (eg, city) []:A Coruna
    Organization Name (eg, company) [Internet Widgits Pty Ltd]:BricoLabs
    Organizational Unit Name (eg, section) []:Division de Hackeo
    Common Name (e.g. server FQDN or YOUR name) []:testServer@bricolabs.cc
    Email Address []:contacto@bricolabs.cc
    
    Please enter the following 'extra' attributes
    to be sent with your certificate request
    A challenge password []:secret1t05
    An optional company name[]:Asociacion BricoLabs

    Y ahora ejecutamos:

    1
    
    openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
  4. Servidor de base de datos.

    Para usar web2py es imprescindible tener acceso a un servidor de base de datos. Podemos usar MySQL o MariaDB por ejemplo. Pero para empezar rápidamente vamos a tirar de SQLite, un servidor fácil de instalar potente y versátil. Es importante usar la versión 3 que introduce grandes mejoras sobre el antiguo SQLite

    1
    
    sudo apt install sqlite3
  5. Arrancamos el servidor:

    Deberíamos tener los ficheros generados en el paso anterior: server.key, server.csr y server.crt, en el directorio raiz de web2py. Podemos arrancar el servidor con los siguientes parámetros (recuerda activar el entorno virtual si no lo tienes activo):

    1
    
    python web2py.py -a 'admin_password' -c server.crt -k server.key -i 0.0.0.0 -p 8000

    Y ya podemos acceder nuestro server web2py, con nuestro navegador favorito, visitando la dirección https://localhost:8000

Y ahora si que ya tenemos todo listo para empezar a usar web2py. Ya podemos crear nuestra primera aplicación.

Los detalles tenebrosos (del arranque)

Si tienes mucha prisa por aprender web2py puedes saltarte esta sección e ir directamente a la sección siguiente

Si por el contrario quieres entender exactamente que hemos hecho para poder arrancar el web2py continuar leyendo puede ser el primer paso.

¿Qué es un virtualenv?

Python nos permite definir virtualenv. Un virtualenv es un entorno python aislado. Todos los virtualenvs están aislados entre si y mejor todavía son independientes del python del sistema. Esto te permite tener multiples entornos de desarrollo (o producción) cada uno con distintas versiones de python y diferentes librerias python instaladas en cada uno de ellos, o quizás diferentes versiones de las mismas librerias.

¿Que es virtualenvwrapper?

Es un frontend para usar virtualenv, la herramienta nativa de python para gestionar virtualenvs. Es completamente opcional, aunque a mi me parece muy cómoda.

¿Qué es todo eso de los certificados?

web2py viene preparado para usar https (estas siglas tienen varias interpretaciones: HTTP over TLS, HTTP over SSL o HTTP Secure). https usa comunicaciones cifradas entre tu navegador y el servidor web para garantizar dos cosas: que estás accediendo al auténtico servidor y que nadie este interceptando la comunicación entre navegador y servidor. En particular web2py exige que se use https para conectarse a las páginas de administración. Así que si no generas los certificados podrás arrancar y conectar con web2py pero no podrás hacer demasiadas cosas.

Para usar https hay que hacer varias cosas:

  • Generar un CSR (Certificate Signing Request)
  • Obtener con ese CSR un certificado SSL de una autoridad certificadora (CA)
  • O alternativamente generar nosotros un certificado a partir del CSR

Lo que hemos hecho con los comandos openssl ha sido:

  • Generar un par de claves (privada y pública) para nuestro servidor (server.key)
  • Generar con esa clave un CSR (el CSR lleva la información que le hemos metido de nuestro servidor y la clave pública)
  • Generar un certificado firmándolo nosotros mismos con esa misma clave como si fueramos la autoridad certificadora.

Esto nos vale para arrancar web2py aunque nuestro navegador nos dará una alerta de riesgo de seguridad por que no reconoce a la CA.

Más info de openssl

¿Qué es un motor de base de datos?

web2py usa un motor (o gestor) de base de datos relacional. Puede usar muchos, incluyendo los más populares como por ejemplo MySQL, Postgres o MariaDB.

Las bases de datos relacionales se basan en relaciones. Las relaciones primarias son tablas que almacenan registros (filas) con atributos comunes (columnas). Las relaciones derivadas se establecen entre distintas tablas mediante consultas (queries) o vistas (views)

web2py te permite gestionar y utilizar las bases de datos a muy alto nivel, así que podras usarlo sin saber practicamente de bases de datos; pero no es demasiado difícil aprender los conceptos básicos y compensa ;-) Todo lo que puedas aprender de bases de datos te ayudará a hacer mejores aplicaciones web.

Nuestra primera aplicación

Vamos a crear nuestra primera aplicación en web2py.

Si has seguido los pasos de la sección anterior ya tienes el web2py funcionando y puedes seguir cualquiera de los tutoriales que hay en la red para aprender. El capítulo 3 del libro de web2py es muy recomendable, y está disponible en castellano, puedes ventilarte los ejemplos que trae explicados en una tarde y son muy ilustrativos.

En esta guía vamos a ver la creación de una aplicación paso a paso.

Crearemos una aplicación de inventario para el material de la Asociación BricoLabs, pero lo haremos de manera que también nos valga para uso particular y tener controladas todas nuestras cacharradas.

Este no es un tutorial de diseño profesional de aplicaciones, sólo pretendemos demostrar lo fácil que es iniciarse con web2py.

De hecho, no seguiremos un orden lógico en el diseño de la aplicación, si no que intentaremos seguir un orden que facilite conocer el framework.

Sin más rollo, vamos a comenzar con nuestra aplicación:

Crea una aplicación desde el interfaz de administración, en nuestro caso la llamaremos cornucopia.

Nuestro web2py “viene de serie” con algunas aplicaciones de ejemplo. La propia pantalla inicial es una de ellas la aplicación “Welcome” o “Bienvenido” (dependerá del lenguaje por defecto de tu navegador).

Para crear nuestra aplicación cornucopia:

  • Vamos al botón admin en la pantalla principal.
  • Metemos la password de administración (con la que hemos arrancado el web2py en la linea de comandos).
  • Desde la ventana de administración creamos nuestra nueva aplicación

Inmediatamente nos encontraremos en la ventana de diseño de nuestra nueva aplicación. web2py nos permite diseñar completamente nuestra aplicación desde aquí, ni siquiera necesitaremos un editor de texto (aunque nada impide usar uno, desde luego).

private/appconfig.ini

El primer fichero que vamos a examinar es private/appconfig.ini La sección private debería estar abajo de todo en la ventana de diseño.

En la sección [app] del fichero podemos configurar el nombre de la aplicación y los datos del desarrollador.

En la sección [db] fichero configuramos el motor de base de datos que vamos a usar en nuestra aplicación. Por defecto viene configurado sqlite así que no vamos a tener que cambiar nada en este sentido.

En la seccion [smtp] podemos configurar el gateway de correo que usará la aplicación para enviar correos a los usuarios. Por defecto viene viene la configuración para usar una cuenta de gmail como gateway, solo tenemos que cubrir los valores de usuario y password y la dirección de correo.^[Es aconsejable crear una cuenta de gmail, o cualquier otro servicio de correo que nos guste, para pruebas. Usar tu cuenta de correo personal podría ser muy mala idea]

El Modelo

En la parte superior de la ventana de diseño (o edición) de nuestra aplicación tenemos la sección Models

web2py se encarga de crear las tablas necesarias en la base de datos que le hayamos indicado que use.

Al crear la aplicación _web2py ha creado en la base de datos todas las tablas relacionadas con la gestión de usuarios y sus privilegios.

Si echamos un ojo al modelo gráfico (Graphs Models) veremos las tablas que web2py ha creado por defecto y las relaciones entre ellas. Estas tablas que ha creado el framework son las que se encargan de la gestión de usuarios, sus privilegios y el acceso de los mismos al sistema, es decir la capa de seguridad.

Si vemos el log de comandos de sql (sql.log) veremos los comandos que web2py ha ejecutado en el motor de base de datos.

Y por último si vemos database administration podremos ver las tablas creadas en la base de datos, e incluso crear nuevos registros en esas tablas (de momento no lo hagas)

También podemos echar un ojo al contenido del fichero db.py o menu.py pero por el momento no vamos a modificar nada en esos ficheros.

Ahora tenemos que ampliar el modelo y añadir todo lo que consideremos necesario para nuestra aplicación.

Diseñando el modelo

Build fat models and thin controllers es uno de los lemas del modelo MVC, no vamos a entrar en detalles de momento pero un modelo bien diseñado nos va a ahorrar muchísimo trabajo al construir la aplicación.

El diseño de bases de datos es una rama de la ingeniería en si mismo, hay camiones de libros escritos sobre el tema y todo tipo de herramientas para ayudar al diseñador. Pero nosotros nos vamos a centrar en usar sólo lo que nos ofrece web2py.

Además como estamos aprendiendo vamos a ver algunas facilidades que nos da web2py sin proponer ningún proceso de diseño del modelo (recuerda, esto no es un curso de diseño de aplicaciones)

Vamos a definir el modelo (concretamente las tablas) de nuestra aplicación en un nuevo fichero de la sección Models, que llamaremos db_custom así que pulsamos en el botón Create, y creamos el fichero db_custom.

web2py parsea todos los ficheros de la sección Models por orden alfabético. Esto nos permite separar nuestro código del que viene originalmente con la aplicación. Pero es importante que db.py sea siempre el primero alfabeticamente para que se ejecute antes que el resto.

web2py se encarga también de añadir la extensión .py al nuevo fichero que estamos creando así que teclea sólo el nombre db_custom.

El objetivo de nuestra aplicación es mantener un inventario de “cosas”. Parece lógico que nuestra primera tabla valga para almacenar “cosas”. Así que en el fichero db_custom.py añadimos las siguientes lineas y salvamos el fichero:

1
2
3
4
db.define_table('thing',
    Field('id', 'integer'),
    Field('desc', 'string'),
    migrate = True);
Tickets de error

Ya hemos salvado nuestro fichero, vamos a echar un ojo a nuestra base de datos con el botón Graph Model.

¡Tenemos un horror! ¿Qué ha pasado?. Si pinchamos en el link del Ticket se abrirá una nueva pestaña en nuestro navegador:

En el ticket tenemos mucha información acerca del error, afortunadamente en este caso es facilito. El nombre del campo desc que hemos añadido a nuestra tabla thing es una palabra reservada en todas las variedades de SQL (es el comando para ver la definición de una tabla: desc tablename)

Editamos de nuevo nuestro fichero db_custom.py y corregimos el contenido:

1
2
3
4
db.define_table('thing',
    Field('id', 'integer'),
    Field('description', 'string'),
    migrate = True);

¡Ahora si! Si pulsamos en el botón de Graph Model (después de salvar el nuevo contenido) veremos que web2py ha creado la nueva tabla en la base de datos. Incluso podríamos empezar a añadir filas (cosas) a nuestra tabla desde el database administration

El campo id es casi obligatorio en todas las tablas que definamos en web2py, siempre será un valor único para cada fila en una tabla y se usará internamente como clave primaria. Podemos usar otros campos como clave primaria pero de momento mantendremos las cosas símples.

Además el campo id se añade por defecto a la definición de una tabla, aunque no lo especifiques en la definición. Si además tenemos en cuenta que string es el tipo por defecto si no especificas un tipo, podríamos haber escrito nuestra tabla como:

1
2
3
db.define_table('thing',
    Field('description'),
    migrate = True);

Pero de momento vamos a dejar todas las definiciones explícitas para no liarnos.

Si visitas ahora la sección de administración de la base de datos puedes añadir algunas “cosas” a la nueva tabla.

Mejorando la tabla

Evidentemente nuestro modelo de “cosa” es demasiado simple, tenemos que añadirle nuevos atributos de distintos tipos para que sea funcional. Pero antes de ir a por todas vamos a ver algunas funciones que nos ofrece web2py para construir los Modelos.

Vamos a añadir algunos campos más de distintos tipos a nuestro modelo y verlos con un poco de calma.

1
2
3
4
5
6
7
db.define_table('thing',
    Field('id', 'integer'),
    Field('name', 'string'),
    Field('description', 'string'),
    Field('picture', 'upload'),
    Field('created_on, 'datetime'),
    migrate = True);

Hemos añadido a nuestra “cosa” un nombre (name), una foto (_picture), que seguro que nos será muy útil, y una fecha de creación (created_on) que será la fecha en que añadimos esta “cosa” concreta a nuestro inventario.

Si ahora volvemos al administrador de base de datos podemos comprobar que:

  • No hemos perdido las “cosas” que añadimos antes, web2py ha añadido las nuevas columnas pero ha conservado los valores de las antiguas.

  • Podemos editar las “cosas” que habíamos añadido sin mas que hacer click en el id

  • Si queremos editar (o añadir) una “cosa”, web2py nos ofrece un diálogo para subir la foto de nuestro objeto. Sabe que los atributos de tipo upload son fichero que subiremos al servidor.

    De la misma forma nos ofrece un menú inteligente para añadir el campo datetime

Este es el tipo de facilidades que ofrecen los frameworks para acelerar el trabajo de crear una aplicación.

Sigamos refinando nuestra definición de “cosa” añadiendo características más sofisticadas nuestra tabla thing:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
db.define_table('thing',
    Field('id', 'integer'),
    Field('name', 'string',
          required = True,
          requires = IS_NOT_EMPTY(error_message='cannot be empty')),
    Field('description', 'string'),
    Field('qty', 'integer',
          default=1,
          label=T('Quantity')),
    Field('picture', 'upload'),
    Field('created_on', 'datetime'),
    format='%(name)s',
    migrate = True);

En la linea del name hemos añadido un VALIDATOR. Se trata de funciones auxiliares que nos permiten comprobar multitud de condiciones y que son extremadamente útiles (iremos viendo casos de uso). En este caso exigimos que el campo name no puede estar vacío y además especificamos el mensaje de error que debe aparecer si sucede.

Hemos añadido un atributo qty (cantidad), hemos especificado que tenga un valor por defecto de una unidad, y además hemos especificado el label.

El label se usará en los formularios en lugar del nombre del campo en la base de datos. Si vamos a añadir una nueva “cosa” veremos que en el formulario no aparece qty sino que nos pregunta Quantity. Además, y esto es muy importante, hemos asignado el valor de la etiqueta con la función T().

web2py incorpora un sistema completo de internacionalización. Al usar la función T() la cadena Quantity se ha añadido a todos los diccionarios de traducción (si es que no estaba ya) y solo tenemos que añadir la traducción en el diccionario correspondiente (p.ej. a es.py) para que funcione la i18n. Una vez añadida si el idioma por defecto de nuestro navegador es el castellano, en el formulario aparecerá “Cantidad” en lugar de Quantity.

Por último hemos añadido el format a la definición de la tabla, format especifica que cuando nos refiramos a un objeto “cosa” se represente por defecto con su atributo name.

Relaciones entre tablas

Supongamos ahora que queremos tener registrado en nuestro inventario al proveedor de cada una de nuestras cosas. ¿cómo se hace eso?

Vamos a añadir los proveedores a nuestro modelo:

1
2
3
4
5
6
7
db.define_table('provider',
    Field('id', 'integer'),
    Field('name, 'string'),
    Field('CIF', 'string'),
    Field('email', 'string'),
    Field('phone', 'string'),
    migrate = True);

Y ahora, a la tabla thing, le añadimos la referencia a proveedores:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

db.define_table('thing',
    Field('id', 'integer'),
    Field('name', 'string',
          required = True,
          requires = IS_NOT_EMPTY(error_message='cannot be empty')),
    Field('description', 'string'),
    Field('qty', 'integer', default=1, label=T('Quantity')),
    Field('picture', 'upload'),
    Field('created_on', 'datetime'),
    Field('provider_id', 'reference provider',
          requires=IS_EMPTY_OR(IS_IN_DB(db, 'provider.id', '%(name)s'))),
    format='%(name)s',
    migrate = True);

Si ahora creamos un proveedor (o varios):

{width=75%}

A la hora de crear una nueva “cosa” web2py se encargará de ofrecernos un desplegable para escoger el proveedor.

Detalles tenebrosos (del modelo inicial)

Si ya has leido el capítulo 3 del libro de web2py, los detalles tenebrosos serán menos tenebrosos para ti.

Lo primero que tenemos que comentar es que mientras hemos estado definiendo el modelo nos hemos pasado casi todo el tiempo usando el DAL que viene empaquetado con web2py.

El DAL (Database Abstraction Layer) es una biblioteca de alto nivel para tratar con bases de datos relacionales. Y podemos usarla por separado en cualquier proyecto que queramos sin tener que usar web2py.

En segundo lugar, merece la pena subrayar que hay dos tipos de restricciones que podemos aplicar a los atributos (o campos) de una tabla.

Cuando usamos default=1, o required=True, este tipo de directivas se traducen directamente en la definición de la tabla en el motor de bases de datos.

Si marcásemos el campo name como required=True e intentásemos crear un registro con el campo vacío, python nos daría una excepción de base de datos. El motor de base de datos sería el que diera el error, por que se violaría la estrutura de tabla que tiene definida.

En cambio los VALIDATORS funcionan de una forma totalmente distinta. Se aplican a nivel de formulario, las comprobaciones y el error (si procede) se harían en web2py, no sería la base de datos la que daría el error.

Por último, quizás hayais notado que hay un fallo grave (deliberado) en el modelo propuesto como ejemplo. Está claro que una cosa puede ser comprada a varios proveedores. Y un proveedor puede vendernos muchas cosas. Eso significa que entre las tablas thing y provider tenemos una relación n:n (many to many relationship), varias cosas pueden estar relacionadas con varios proveedores. Este tipo de relaciones siempre son un problema en las bases de datos relacionales y más adelante veremos como implementarlas correctamente. De momento nos quedamos con el modelo simplificado.

Checklist

Hay que editar private/appconfig.ini:

1
2
3
4
5
6
7
8
[app]
name        = cornucopia
author      = salvari <salvari@protonmail.com>
description = una aplicación de inventario
keywords    = web2py, python, framework, bricolabs
generator   = Web2py Web Framework
production  = false
toolbar     = false

Secciones en el futuro

web2py y git

Instalación con nginx

Certificados let’s encrypt

Notas sueltas de Bases de Datos

Truncar tablas en MariaDB

Los comandos de TRUNCATE se tratan internamente como un borrado y creado de la tabla.

Cuando tenemos tablas con relaciones tenemos dos caminos posibles:

Borrar toda la tabla y resetear el contador:

1
2
DELETE FROM COUNTRY;
ALTER TABLE COUNTRY AUTO_INCREMENT = 1;

Desactivar los chequeos y volverlos a activar al final (ojito que no estoy seguro de que la desactivación sea por sesión)

1
2
3
4
5
6
SET FOREIGN_KEY_CHECKS = 0;

TRUNCATE TABLE COUNTRY;
TRUNCATE TABLE STATE;

SET FOREIGN_KEY_CHECKS = 1;
0%