--- weight: 4 title: "Apuntes de py4web" date: 2022-03-30T18:41:45+0200 draft: false summary: "Apuntes de py4web, creando webapps con Python" resources: - name: "featured-image-preview" src: "featured-image-preview" categories: - notes tags: - python - programacion - web - py4web - web2py - bulma --- {{< admonition type=warning title="Work in progress" open=true >}} Apuntes de py4web, muy incompletos. De momento son una copia descarada del [curso de Luca de Alfaro](https://learn-py4web.github.io/) (que está genial para aprender) {{< /admonition >}} {{< admonition type=info title="Referencias" open=true >}} - [El tutorial de Luca de Alfaro](https://learn-py4web.github.io/) Pero mejor usa los videos de la siguiente referencia. - [El video-tutorial de Luca de Alfaro](https://www.youtube.com/watch?v=hv3aEaT6ulI&list=PLAVb3DQlAH4tIvdAsmese0_xRkvZYlox0 "El video-tutorial de Luca de Alfaro") (muy bueno) - [El video-tutorial en Invidious](https://invidious.namazso.eu/playlist?list=PLAVb3DQlAH4tIvdAsmese0_xRkvZYlox0) - [Bulma CSS crash course](https://www.youtube.com/watch?v=IiPQYQT2-wg) by Traversy Media {{< /admonition >}} ## ¿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 en este blog si quieres saber mas de `pyenv`) ```bash pyenv virtualenv 3.9.7 ve_py4web ``` 1. 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. ```bash 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 **L**anguage **S**erver **Protocol** server, que nos valdrá para tener facilidades adicionales al editar ficheros de Python: ```bash pip install --upgrade pip setuptools wheel pipx virtualenv virtualenvwrapper pip install 'python-lsp-server[all]' ``` 1. Clonamos el `py4web`: ```bash git clone https://github.com/web2py/py4web.git ``` 1. Instalamos las dependencias de ___py4web___, es decir las bibliotecas de python que el programa ___py4web___ necesita para funcionar. ```bash cd py4web pip install -r ./requirements.txt ``` 1. 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. ```bash ./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. ```bash 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___ 1. Decidir la base de datos 1. Decidir el tipo de _session_ (fichero `settings.py`) 2. Cambiar el `SESSION_SECRET_KEY` en el fichero `settings.py` {{< admonition type=tip title="login con username" open=true >}} 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. {{< /admonition >}} 3. 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](https://github.com/mdipierro/no.css/) un framework [CSS]^(Cascade Style Sheets) extremadamente simple. _scaffold\_bulma_ : Un _template_ creado por Luca de Alfaro, basado en [Bulma](https://bulma.io) 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](https://bitbucket.org/luca_de_alfaro/workspace/projects/PY4WEB). 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: ```bash 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. ) para ello: ```bash 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: ```bash 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: ```python3 @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 - 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_: [YATL]^(Yet Another Template Language) (ver [documentación de YATL](https://py4web.com/_documentation/static/en/chapter-09.html))aunque ___py4web___ soporta también _Renoir_ (aparentemente usa los dos tras las bambalinas y de forma transparente para nosotros) {{< admonition type=tip title="Lenguajes de _template_" open=true >}} 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___. {{< /admonition >}} ### 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 {{< admonition type=tip title="MySQL" open=true >}} Si queremos usar _MySQL_ en lugar de _MariaDB_ el procedimiento sería exactamente el mismo que el descrito. {{< /admonition >}} Como ya comenté, me voy a instalar la base de datos en Docker, prefiero tenerla lo más aislada posible en el portatil. ```bash # 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: ```bash mariadb -h 127.0.0.1 -u root -p ``` Y para crear la base de datos y dar privilegios: ```sql 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`: ```bash 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](https://py4web.com/_documentation/static/en/chapter-07.html#connection-strings-the-uri-parameter) 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` {{< admonition type=tip title="Otros motores de base de datos" open=true >}} 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. {{< /admonition >}} ### 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: - [Simple Form](https://www.youtube.com/watch?v=3nkdtnvFfdw&list=PLAVb3DQlAH4tIvdAsmese0_xRkvZYlox0&index=20) - [Form Attacks](https://www.youtube.com/watch?v=qvWVFy8pRxY&list=PLAVb3DQlAH4tIvdAsmese0_xRkvZYlox0&index=21) - [Py4web Form Processing](https://www.youtube.com/watch?v=_prMUT6EpUc&list=PLAVb3DQlAH4tIvdAsmese0_xRkvZYlox0&index=22) 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](https://www.uuidtools.com/generate/v5). ### 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: ~~~~python ### 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: ```python 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: ```yatl [[extend 'layout.html']]
Add a new Thing
``` y en la parte del controller vamos a añadir una ruta `add`: ```python3 @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á `//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_](https://py4web.com/_documentation/static/en/chapter-06.html) 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. {{< admonition type=tip title="Fixtures" open=true >}} 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 {{< /admonition >}} Cuando visitamos la 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_](https://bottlepy.org/docs/dev/api.html) para cualquier duda. También podemos ver como se hace una inserción en la base de datos con ayuda del [DAL]^(Database Abstraction Layer) de ___py4web___. Vamos a hacer un uso intensivo del DAL si quieres puedes echar un ojo a la [documentación](https://py4web.com/_documentation/static/en/chapter-07.html) 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. ```python3 @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: ```html [[extend 'layout.html']]
Add a new Thing
[[=form]]
``` #### 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_: ```python3 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: ```python3 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](https://py4web.com/_documentation/static/en/chapter-07.html#migrations) 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í: ```python3 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: ```python3 @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 [DAL]^(Database 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: ```html [[extend 'layout.html']]
[[for row in rows:]] [[pass]]
Id Name Description Created by Created on
[[=row.id]] [[=row.name]] [[=row.description]] [[=row.created_by]] [[=row.creation_date]]
``` 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. ```html [[extend 'layout.html']]
[[for row in rows:]] [[pass]]
Id Name Description Created by Created on
[[=row.id]] [[=row.name]] [[=row.description]] [[=row.created_by]] [[=row.creation_date]] Edit
``` 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://://edit/` 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 `/` pero podría ser mucho más largo y con más parámetros. Nuestro _controller_ sería: ```python3 @action('edit/', 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 `record`en 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: ```python3 @action('delete/') @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_. --- --- {{< admonition type=warning title="Apuntes de web2py" open=true >}} __Lo que sigue a continuación de este aviso son mis antiguos apuntes de web2py__ {{< /admonition >}} ## web2py [web2py](http://www.web2py.com/) 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](https://www.djangoproject.com/) (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](http://www.web2py.com/init/default/documentation) 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](https://es.wikipedia.org/wiki/Modelo%E2%80%93vista%E2%80%93controlador) __web2py__ incorpora _Bootstrap 4_ {{< admonition type=info title="Referencias web2py" open=false >}} * [Página de documentación y recursos de web2py, con enlaces a grupos de usuarios y tutoriales](http://www.web2py.com/init/default/documentation) * [Evolución del modelo MVC](https://martinfowler.com/eaaDev/uiArchs.html) * [Fat models and thin controllers](https://nomadphp.com/blog/60/working-with-the-thin-controller-and-fat-model-concept-in-laravel) * [Crítica del mantra](https://nomadphp.com/blog/60/working-with-the-thin-controller-and-fat-model-concept-in-laravel) * [Video tutorial en Python Weekly](https://yewtu.be/playlist?list=PL5E2E223FE3777851) * [Web2py Tutorial by Terry Toy](https://vid.puffyan.us/playlist?list=PLmavdTilTrxLJ-8vdx7iDvVtiyC0NjTy-) {{< /admonition >}} ### 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](https://www.apache.org/) o [Nginx](https://www.nginx.com/), 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_: ~~~~ mkvirtualenv -p `which python3` web2py ~~~~ 1. Bajamos el programa de la web de Web2py y descomprimimos el framework: ~~~~bash # 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 ~~~~ 1. Generamos certificados para el protocolo _ssl_: Para usar con comodidad web2py conviene que nos generemos unos certificados para gestionar el ssl: ~~~~bash # 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: ~~~~ openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt ~~~~ 1. 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](https://www.sqlite.org/version3.html), 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_ ~~~~bash sudo apt install sqlite3 ~~~~ 1. 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): ~~~~bash 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](https://www.mozilla.org/en-US/firefox/developer/), visitando la dirección 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](#nuestra-primera-aplicación) 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_](https://www.digitalocean.com/community/tutorials/openssl-essentials-working-with-ssl-certificates-private-keys-and-csrs) ¿Qué es un motor de base de datos? : ___web2py___ usa un motor (o gestor) de [base de datos relacional](https://es.wikipedia.org/wiki/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](#instalación) ya tienes el ___web2py___ funcionando y puedes seguir cualquiera de los tutoriales que hay en la red para aprender. El [capítulo 3](http://web2py.com/books/default/chapter/29/03/overview) del libro de ___web2py___ es muy recomendable, y está disponible [en castellano](http://web2py.com/books/default/chapter/41/03/resumen), 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` ![Menú Modelos](src/img/models_menu.jpg) ___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`. ![Crear fichero](src/img/create_db_custom.png) ___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: ~~~~python 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_. ![Error interno](src/img/internal_error.jpg) ¡Tenemos un horror! ¿Qué ha pasado?. Si pinchamos en el link del _Ticket_ se abrirá una nueva pestaña en nuestro navegador: ![Error palabra reservada SQL](src/img/error_reserved_sql.jpg) 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: ~~~~python 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: ~~~~python 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. ![Añadir destornillador](src/img/add_thing_a.jpg) ###### Mejorando la tabla Evidentemente nuestro modelo de "cosa" es demasiado simple, tenemos que añadirle nuevos atributos de [distintos tipos](http://web2py.com/books/default/chapter/29/06/the-database-abstraction-layer#Field-types) 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. ~~~~python 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`: ~~~~python 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](http://web2py.com/books/default/chapter/29/07/forms-and-validators) 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: ~~~~python 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: ~~~~python 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): ![Un proveedor](src/img/great_provider.jpg){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](http://web2py.com/books/default/chapter/29/03/overview) 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`: ~~~~python [app] name = cornucopia author = salvari 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: ```sql 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) ```sql SET FOREIGN_KEY_CHECKS = 0; TRUNCATE TABLE COUNTRY; TRUNCATE TABLE STATE; SET FOREIGN_KEY_CHECKS = 1; ```