Sobre este documento

Este guión de prácticas ha sido elaborado a partir de los tutoriales de introducción a Django que se pueden encontrar en la página de Django. Han sido adaptados y modificados específicamente para las clases de SAT y SARO de los Grados que se imparten en la Escuela de Ingeniería de Telecomunicación de la Universidad Rey Juan Carlos.

Creando la aplicación cms

Activa el entorno virtual «cursosweb» que creamos en la clase anterior:

$ source cursosweb/bin/activate

Dentro del proyecto «proyecto» que creamos y configuramos en la clase anterior, vamos a crear una nueva aplicación que será un gestor de contenidos (content management system, en inglés). Llamaremos a la aplicación «cms», y será tras calc nuestra segunda aplicación Django.

Para crear tu aplicación, asegúrate de que estás en el mismo directorio que el archivo manage.py y escribe este comando:

$ python3 manage.py startapp cms
...\> py manage.py startapp cms

De la clase anterior sabemos que en el fichero cms/views.py irá el código (en Python) que se ejecutará cuando sea pertinente. En la clase de hoy, manejaremos contenidos con bases de datos, que vendrán definidas en cms/models.py, se podrán ver las diferentes vesiones en cms/migrations/ y las podremos manejar vía web con lo que pongamos en cms/admin.py.

Creando modelos

A continuación definiremos los modelos, que viene a ser la estructura de la base de datos con metadatos adicionales.

Filosofía

Un modelo es la fuente única y definitiva de información sobre los datos de una aplicación. Contiene los campos esenciales y los comportamientos de los datos. Django sigue el Principio DRY (Don't Repeat Yourself). El objetivo es definir el modelo de datos en un solo lugar y derivar cosas de este automáticamente, como veremos más adelante.

Esta filosofía incluye lo que se denominan migraciones. Las migraciones se derivan directamente de los modelos y son básicamente la historia de los modelos, lo que permite evolucionar un esquema de base de datos para que se adapte a los nuevos modelos. En otras palabras, podemos crear ahora un modelo que será la estructura de la base de datos, y en el futuro modificar este modelo (y, por tanto, la estructura de la base de datos). Para migrar de manera más sencilla de la estructura antigua a la nueva, tenemos migraciones.

Para nuestra aplicación cms, crearemos dos modelos: Contenido y Comentario. Un Contenido tiene una clave y un valor, al igual que ya lo tenía en la aplicación web que hicimos en su día y que guardábamos en un diccionario Python. Una Comentario tiene tres campos: el título, el cuerpo y la fecha del comentario. Cada Comentario está asociado a un Contenido.

Estos conceptos vienen representados por clases en Python. Edita el fichero cms/models.py para que el resultado sea el siguiente:

cms/models.py
from django.db import models

class Contenido(models.Model):
    clave = models.CharField(max_length=64)
    valor = models.TextField()

class Comentario(models.Model):
    contenido = models.ForeignKey(Contenido, on_delete=models.CASCADE)
    titulo = models.CharField(max_length=200)
    cuerpo = models.TextField(blank=False)
    fecha = models.DateTimeField('publicado')

Como se puede ver, cada modelo viene representado pro una clase que hereda de django.db.models.Model. Cada modelo tiene una serie de variables de clase, que representa un campo de la base de datos en el modelo.

Veamos un poco más en detalle sobre la sintaxis para definir los modelos:

  • Tablas (clases) y columnas (campos): Puedes ver una base de datos como una hoja de cálculo. Cada clase se corresponde con una tabla, mientras que cada campo se corresponde con una columna. Así, en este caso, tendremos dos tablas, Contenido y Comentario. Contenido tiene dos columnas y Comentario tiene cuatro.
  • Tipos de campos: Cada campo está representado por una instancia de una clase Field. Por ejemplo, CharField para campos de caracteres (limitados en número por el valor de max_length que se pasa como parámetro obligatorio), TextField para campos de texto sin límite de caracters, IntegerField para campos que incluyan enteros o DateTimeField para variables de tiempo y fecha. Esto le dice a Django qué tipo de datos cada campo contiene.
  • Nombres de los campos: El nombre de cada instancia Field (por ejemplo, clave o fecha)` es el nombre del campo, en formato adaptado al lenguaje de la máquina (esto es, tiene que ser una variable Python válida, por lo que no podremos tener espacios en blanco, letras con tilde o la eñe). Usaremos esos nombres en el código Python y la base de datos lo utilizará como el nombre de la columna.
  • Nombres alternativos de los campos: Podemos emplear un primer argumento posicional (que es opcional) a un Field para designar un nombre legible por humanos. Este nombre se utiliza en varias partes de Django, como el interfaz de admin (ver más abajo), y sirve al mismo tiempo como documentación. Si no se proporciona este campo, Django usará el nombre de la variable. En este ejemplo, sólo hemos definido un nombre legible para Question.fecha. Para el resto de los campos en este modelo, el nombre de la variable servirá como el nombre legible por humanos.
  • Argumentos (obligatorios): Algunas clases Field precisan argumentos. La clase CharField, por ejemplo, requiere que le asignes un max_length. Esta se utiliza no sólo en el esquema de la base de datos, sino también en la validación como veremos dentro de poco.
  • Argumentos (opcionales): Una clase Field también puede tener varios argumentos opcionales; en este caso, le hemos fijado que el campo no puede estar vacío (por ello, blank está fijado a None).
  • Relaciones entre tablas: Por último, ten en cuenta que una relación se define usando ForeignKey. Eso le indica a Django que cada Comentarios se relaciona con un solo Contenido. Django es compatible con todas las relaciones de bases de datos comunes. Esta es una relación ene a uno (un contenido puede tener múltiples comentarios). Pero también hay relaciones uno a uno o ene a ene; las veremos más adelante.

Configuración de la base de datos

Una vez que hemos definido el modelo, vamos a configurar la base de datos. La base de datos es utilizada a nivel de proyecto, esto es, todas las aplicaciones comparten una misma base de datos. Dentro de la base de datos, habrá tablas; una aplicación puede o no tener tablas (por ejemplo, nuestra calc de la última semana no tenía tablas), o tener una tabla o bien tener muchas. Las tablas sí son específicas de las aplicaciones.

Para configurar la base de datos abre el archivo proyecto/settings.py. Es un módulo (fichero) normal de Python con variables de nivel de módulo que representan la configuración de Django.

Por defecto la base de datos utilizada SQLite, que es la que utilizaremos en esta asignatura. SQLite es un gestor de base de datos incluido en Python por lo que no tendremos que instalar nada más para soportar nuestras bases de datos. Para proyectos reales, es recomendable utilizar una base de datos más potente como PostgreSQL o MySQL para evitar dolores de cabeza en el futuro al tener que cambiar entre base de datos.

Si desea utilizar otra base de datos, instale los conectores de base de datos apropiados, y cambie las siguientes claves en el ítem DATABASES 'default' para que se ajusten a la configuración de conexión de la base de datos:

  • ENGINE – bien sea 'django.db.backends.sqlite3', 'django.db.backends.postgresql_psycopg2', 'django.db.backends.mysql', o 'django.db.backends.oracle'. Otros backends también están disponibles.
  • NAME –el nombre de tu base de datos. Si estás utilizando SQLite, la base de datos será un archivo en tu ordenador; en ese caso NAME debe ser la ruta absoluta completa, incluyendo el respectivo nombre del archivo. El valor predeterminado, os.path.join(BASE_DIR, 'db.sqlite3'), guardará el archivo en el directorio de tu proyecto.

Si no estás utilizando SQLite como gestor de base de datos, se deben añadir ajustes adicionales tales como USER, PASSWORD, y HOST se deben añadir. Para más información, vea la documentación de referencia para DATABASES.

Ya que estamos editando el archivo proyecto/settings.py, configuremos TIME_ZONE a nuestra zona horaria ("Europe/Madrid").

Activando los modelos

Dentro de proyecto/settings.py, existe una variable llamada INSTALLED_APPS se encuentra en la parte superior del archivo. Esta variable, una lista, contiene los nombres de todas las aplicaciones Django que están activadas en nuestro proyecto.

Por defecto, INSTALLED_APPS contiene las siguientes aplicaciones (Django viene equipado con todas ellas de fábrica):

Es hora de indicarle a nuestro proyecto que la aplicación cms está instalada y lista para ser usada.

Filosofía

Las aplicaciones Django son «conectables»: Podemos utilizar una aplicación en diversos proyectos y se pueden distribuir aplicaciones porque no necesitan estar atadas a una determinado proyecto de Django.

Para incluir la aplicación cms en nuestro proyecto necesitamos agregar una referencia en INSTALLED_APPS. Lo que debemos indicar en INSTALLED_APPS es una clase con la configuración que guardaremos en el fichero cms/apps.py. Esta clase la llamaremos CmsConfig. La ruta para que el proyecto la reconozca será 'cms.apps.CmsConfig', de manera que edita el archivo proyecto/settings.py y agrega esta ruta punteada a INSTALLED_APPS. ¿Y CmsConfig, no lo tenemos que crear? Por ahora, como no necesitamos ninguna configuración específica para nuestra aplicación cms, no hará falta editar el fichero cms/apps.py para añadir una clase CmsConfig. Con la configuración por defecto, o sea sin explícitamente tener CmsConfig, nos valdrá.

El resultado de la variable INSTALLED_APPS en el fichero proyecto/settings.py se verá así:

proyecto/settings.py
INSTALLED_APPS = [
    'cms.apps.CmsConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

Hecho esto, Django es consciente de que existe una aplicación llamada cms que necesita que se cree una base de datos. Nota que por ahora, hemos definido el modelo en lenguage Python. Necesitaremos ejecutar una serie de acciones para que se cree la base de datos a partir de ese model. Para ello, ejecutaremos el siguiente comando:

$ python3 manage.py makemigrations
...\> py manage.py makemigrations

Al ejecutarlo, como es la primera vez que lo hacemos, se crearán no sólo las tablas de nuestra aplicación cms, sino de todas las demás. Veremos por pantalla un montón de información sobre creación de esas tablas. Las líneas relativas a cms serán similares a las siguientes:

   
Migrations for 'cms':
  cms/migrations/0001_initial.py
    - Create model Contenido
    - Create model Comentario

Al ejecutar makemigrations, le estamos indicando a Django que hemos realizado algunos cambios a sus modelos (en este caso, ha realizado cambios nuevos) y que le gustaría que los guarde como una migración.

Las migraciones son cómo Django almacena los cambios en sus modelos (y, por lo tanto, en el esquema de la base de datos) - ahora ya no son clases de Python (los modelos) sino que son archivos (donde guardaremos los datos) en el disco. Puedes ver la migración del nuevo modelo, si lo deseas, en el archivo cms/migrations/0001_initial.py. No te preocupes, no se espera que te los leas cada vez que Django hace uno -- pero están diseñados para ser editados por humanos en caso de que desees modificar manualmente cómo Django cambia las cosas.

Hay un comando que ejecutará las migraciones y que gestionará el esquema de base de datos automáticamente: migrate.

También puedes ejecutar el comando python manage.py check para revisar cualquier problema en tu proyecto sin hacer migraciones o modificar la base de datos.

A continuación, ejecuta de nuevo el comando migrate para crear esas tablas modelos en tu base de datos:

$ python3 manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, cms, sessions
Running migrations:
  Applying cms.0001_initial... OK
...\> py manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, cms, sessions
Running migrations:
  Rendering model states... DONE
  Applying cms.0001_initial... OK

El comando migrate toma todas las migraciones que no han sido aplicadas (Django rastrea cuales se aplican utilizando una tabla especial en su base de datos llamada django_migrations) y las ejecuta contra su base de datos; básicamente, sincronizando los cambios que hayas realizado a tus modelos con el esquema en la base de datos.

Las migraciones son muy potentes y le permiten modificar sus modelos con el tiempo, a medida que desarrolla tu proyecto, sin necesidad de eliminar su base de datos o las tablas y hacer otras nuevas. Este se especializa en la actualización de su base de datos en vivo, sin perder datos. Vamos a hablar de ellas en mayor profundidad más tarde en el tutorial, pero por ahora, recuerde la guía de tres pasos para hacer cambios de modelo:

La razón por la que existen comandos separados para realizar y aplicar migraciones es porque enviarás las migraciones a tu sistema de control de versiones (git) y las enviarás con tu aplicación; no solo facilitan el desarrollo, también pueden ser utilizadas por otros desarrolladores y en producción.

Si has llegado hasta aquí, pon en el chat de Teams: "Estoy justo antes de lo de admin" para que sepamos los profes por dónde vas.

Presentando la interfaz de administración de Django

Filosofía

La generación de sitios administrativos para agregar, modificar y eliminar contenido es un trabajo aburrido que no requiere mucha creatividad. Por esa razón, Django automatiza completamente la creación de interfaces de sitios administrativos para los modelos.

Nótese que Django fue escrito en un entorno de sala de redacción de un periódico, con una separación muy clara entre «los editores de contenido» y el sitio «público». Los administradores del sitio utilizan el sistema de administración para agregar noticias, eventos, resultados deportivos, etc., y ese contenido se muestra en el sitio público. Con la interfaz de administración, Django resuelve el problema de crear una interfaz unificada para los administradores del sitio para editar el contenido.

En resumen, el interfaz de administración, conocido como admin, no está destinado a ser utilizado por los visitantes del sitio. Es para los administradores del sitio.

Creando un usuario para admin

Primero tendremos que crear un usuario, el superusuario, que pueda iniciar sesión en el sitio administrativo. Este superusuario tendrá permisos no sólo para acceder al interfaz de admin vía web, sino que podrá crear otros usuarios, gestionar los permisos, entre otras tareas. Además, como veremos pronto, podrá añadir, modificar o borrar contenidos en la base de datos. Para crear el superusuario, ejecuta el siguiente comando:

$ python3 manage.py createsuperuser
...\> py manage.py createsuperuser

Introduce un nombre de usuario (por ejemplo, admin) y pulsa enter.

Username: admin

A continuación se solicitará una dirección de correo electrónico:

Email address: alumno@alumnos.urjc.es

Y el paso final consiste en introducir una contraseña. Se pedirá que introducir la contraseña dos veces, la segunda vez como confirmación de la primera.

Password: **********
Password (again): *********
Superuser created successfully.

Inicie el servidor de desarrollo

El sitio administrativo de Django se activa de forma predeterminada. Vamos a iniciar el servidor de desarrollo y a explorarlo. Si el servidor no está en marcha, lánzalo de la siguiente forma:

$ python3 manage.py runserver
...\> py manage.py runserver

A continuación, abre un navegador web y ve a «/admin/» en tu dominio local, por ejemplo, http://127.0.0.1:8000/admin/. Deberás ver la pantalla de inicio de sesión del sitio administrativo:

Django admin login screen

Dado que la traducción se activa de forma predeterminada, la pantalla de inicio de sesión puede aparecer en tu propio idioma, dependiendo de la configuración del navegador y de si Django tiene una traducción para este idioma.

Accede al sitio administrativo

Ahora, intenta iniciar sesión con la cuenta de superusuario que has creado en el paso anterior. Deberías ver la página de índice del sitio administrativo de Django:

Django admin index page

Deberías ver algunos tipos de contenidos editables: grupos y usuarios. Ellos son proporcionados por django.contrib.auth, el framework de autenticación enviado por Django. En usuarios ahora mismo sólo encontrarás al superusuario que acabamos de crear, pero verás que se pueden crear nuevos usuarios (y asignarles roles, permisos, etc.) de manera sencilla. Los usuarios se pueden agrupar en grupos, que aúnan a usuarios con características comunes (p.ej., editores, periodistas, diseñadores). Ahora mismo no hay ningún grupo creado, pero podrás observar que su creación y gestión es muy sencilla.

Haciendo que la aplicación cms se pueda modificar desde admin

Pero, ¿dónde está nuestra aplicación cms? No se muestra en la página de índice del sitio administrativo. Para que aparezca, debemos realizar todavía una acción. Necesitamos decirle a admin que los objetos Contenido puedan ser gestionados desde el interfaz de administrador. Para ello, abre el archivo cms/admin.py, y edítalo para que se vea así:

cms/admin.py
from django.contrib import admin

from .models import Contenido

admin.site.register(Contenido)

Explorando la funcionalidad de admin

Ahora que hemos registrado el modelo Contenido, Django desplegará esa tabla en la página índice del sitio administrativo:

Django admin index page, now with cms displayed

Haz clic en «Contenidos» (nota que la interfaz de admin añade automáticamente una ese al final del nombre del modelo), y te llevará a la página «lista de cambios» para los contenidos. Esta página muestra todos los contenidos en la base de datos y permite seleccionar uno para modificarla.

Ahora mismo, no tenemos ninguno, pero podemos crear uno de manera sencilla pulsando sobre el botón gris «Add Contenido» en el margen superior derecho. De esta forma, crea un contenido cuya clave sea «nabucco» y cuyo valor sea «Vuela, pensamiento, en alas doradas».

Django adding stuff

Al guardarlo, podrás observar que has creado un objeto contenido, pero no mucho más. Veremos más adelante cómo cambiar esto.

Cuestiones a tener en cuenta:

  • El formulario se genera automáticamente a partir del modelo Contenido.
  • Los diferentes tipos de campos del modelo (TextField, CharField) corresponden al widget de entrada HTML adecuado. Cada tipo de campo sabe cómo mostrarse en el sitio administrativo de Django.
  • Los formularios se adaptan al tipo de campo. Así, para los campos de fechas, como DateTimeField, existirán atajos de JavaScript: las fechas tienen un atajo «Hoy» y una ventana emergente del calendario, mientras que las horas tienen un atajo «Ahora» y una ventana emergente conveniente que enumera las horas que se registran comúnmente.

La parte inferior de la página nos da unas cuantas opciones:

  • Guardar: Guarda los cambios y retorna a la página de la lista de cambios para este tipo de objeto.
  • Guardar y continuar editando: Guarda los cambios y recarga la página del sitio administrativo para este objeto.
  • Guardar y añadir otro: Guarda los cambios y carga un nuevo formulario vacio para este tipo de objeto.
  • Eliminar: Muestra una página de confirmación de eliminación.

Creando las URLs y la vista

Una vez que hemos introducido algunos contenidos, veamos cómo hacer para que nuestro servidor los muestre. Lo primero que haremos es definir las URLs. Como en el caso de la calculadora, lo que haremos es incluir en cms/urls.py (el URLs del proyecto) una línea indicando que queremos que las URLs las maneje la aplicación:

proyecto/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('cms/', include('cms.urls')),
    path('calc/', include('calc.urls')),
    path('admin/', admin.site.urls),
]

Dentro del subdirector cms (el de la aplicación), crearemos (en cms/urls.py y añadiremos la siguiente regla:

cms/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('<str:llave>', views.get_content),
]

Y ahora vayamos a por la vista, que como hemos especificado en cms/urls.py ha de llamarse get_content. Por eso, en cms/views.py pondremos lo siguiente:

cms/views.py

from django.http import HttpResponse
from .models import Contenido

def get_content(request, llave):
    contenido = Contenido.objects.get(clave=llave)
    return HttpResponse(contenido.valor)

La parte relevante de la vista está en la siguiente línea: Contenido.objects.get(clave=llave). Contenido es el modelo creado en calc/models.py, y Contenido.objects son todos los objetos (datos) que contiene esa tabla. De todos ellos, con get(clave=llave), lo que hacemos es obtener aquél objeto de Contenido cuyo valor de clave sea igual al valor de la variable llave que hemos obtenido como parámetro en la vista. Veremos en la clase de la semana que viene cómo realizar búsquedas más sofisticadas en la base de datos.

Prueba a ir a http://localhost:8000/cms/nabucco con tu navegador... si todo ha ido bien, debería devolver una página web con el contenido (el valor) con lo que canta el coro de los esclavos judíos en la famosa ópera.

La variable contenido es un objeto Contenido. Como ya sabemos de los modelos, los objetos Contenido tienen dos campos: clave y valor. En la línea HttpResponse(contenido.valor), lo que indicamos es que queremos responder con una página web con el campo valor (un string). Prueba con el código HttpResponse(contenido) para cerciorarte de que así estarías enviando el objeto.

Sin embargo, si intentas introducir cualquier otra llave, por ejemplo http://localhost:8000/cms/rigoletto, nos devolverá una página con un error. Resulta que cuando no existe una clave en la tabla de Contenido con el valor de la llave, saltará una excepción DoesNotExist (asociada al modelo Contenido). Para manejarlo correctamente, deberíamos modificar nuestra vista para que sea tal que así:

cms/views.py

from django.http import HttpResponse
from .models import Contenido

def get_content(request, llave):
    try:
        respuesta = Contenido.objects.get(clave=llave).valor
    except Contenido.DoesNotExist:
        respuesta = 'No existe contenido para la clave ' + llave
    return HttpResponse(respuesta)

Y hasta aquí la práctica de hoy. Por favor, pon en el chat de Teams: "He terminado con el guión" para que los profes lo sepamos. Esto nos ayuda a dimensionar las prácticas. Muchas gracias.

Si has llegado hasta aquí y todavía tienes tiempo, prueba a introducir más contenidos en la base de datos mediante el interfaz de administración y prueba con el navegador que los muestre bien.

Introduciendo contenidos con PUT

Para la semana que viene, os pido que incluyáis nuevos contenidos con PUT. Tendréis que modificar la vista, manteniendo la parte de GET y añadiendo lo correspondiente a PUT. Por cuestiones de seguridad, tendremos que añadir @csrf_exempt justo antes de la vista (y que veremos en detalle la semana que viene).

Dentro de la vista, miramos si el método recibido es "PUT". Para ello, tomamos la variable request, que es la variable que contiene la información de la petición, y miramos si su atributo request.method corresponde a un PUT. Si es así, de la petición, cogemos el cuerpo request.body y lo pasamos a UTF-8, ya que nos lo dan en bytes.

A continuación, creamos un objeto Contenido. Contenido es un modelo, que hemos definido en cms/models.py, y tiene dos campos: clave y valor. Al crear el objeto, debemos dar valor a esos dos campos: uno será el recurso (el parámetro llave que hemos cogido vía cms/urls.py) y otro será el valor (el cuerpo de la petición, que hemos cogido de request.body). Una vez creado el objeto, se guardan los datos en la base de datos con el método save().

El fichero cms/views.py resultante quería de la siguiente manera:

cms/views.py

from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt

from .models import Contenido

@csrf_exempt
def get_content(request, llave):
    if request.method == "PUT":
        valor = request.body.decode('utf-8')
        c = Contenido(clave=llave, valor=valor)
        c.save()

    try:
        respuesta = Contenido.objects.get(clave=llave).valor
    except Contenido.DoesNotExist:
        respuesta = 'No existe contenido para la clave ' + llave
    return HttpResponse(respuesta)

Nota: Puedes hacer PUT con tu navegador, añadiendo una extensión. Hay muchas extensiones que lo permiten; una de ellas es RESTer (lo tienes para Mozilla Firefox y para Google Chrome/Chromium).

Nota: verás que tal y como hemos resuelto el problema, si hacemos dos PUT sobre el mismo recurso, nos dará un error. Esto es debido a que objects.get() sólo permite devolver un objeto (o, si no existe, salta la excepción DoesNotExist). Modifica tu código para que, antes de crear un nuevo objeto, compruebe si ya existe uno con esa clave. Y si existe, mira a ver cómo se podría hacer para modificar ese objeto y no crear uno nuevo.