Ejemplo de un proyecto básico con Python, Django y PyCharm

Django es un Framework para el desarrollo de Aplicaciones Web en Python, que proporciona su propio ORM e implementa un patrón Modelo-Vista-Plantilla (MVT: Model-View-Template) muy similar al popular MVC. En este Post vamos a ver un sencillo ejemplo, paso a paso, para introducirnos con las Plantillas de Django, cómo utilizarlas, utilizar Modelos, veremos el Panel de Administración (Django Admin), Vistas basadas en Clases, Pruebas Unitarias en Django (así como la forma de obtener un informe de cobertura y subirlo a Sonarqube), y como dockerizar la solución con Gunicorn y NGINX.

Continuando con la serie de Posts sobre Python y Django, en esta ocasión vamos a realizar otro ejemplo más completo al que vimos en el Post anterior (Hello World con Python, Django y PyCharm), y disponible también en el siguiente repo público de GitHub: GitHub – ElWillieES – django-basic-example

En esta ocasión vamos a ver a través de un ejemplo paso a paso, varios puntos de interés como los siguientes:

  • Cómo utilizar Plantillas (Templates) para crear páginas Web con Django, incluido la utilización de una plantilla base donde reutilizar la cabezará y pie de nuestras páginas.
  • Cómo crear un Modelo y utilizar el ORM de Django para crear las tablas de base de datos, en este caso, sobre SQLite.
  • Cómo utilizar el Panel de Administración de Django Admin, para tener una zona privada donde poder hacer tareas de alta/baja/modificación de nuestro modelo de datos, con muy poco esfuerzo.
  • Cómo utilizar Vistas basadas en Clases.
  • Cómo crear Pruebas Unitarias en Django, incluyendo la forma de generar un informe de cobertura y de subirlo a Sonar.
  • Cómo dockerizar nuestra aplicación con un NGINX y Gunicorn como servidor de aplicaciones, teniendo en cuenta, que en este caso tenemos que generar los estáticos de Django para que sean servidos por el NGINX.

Si estás comenzando con Python, quizás te interese leer primero el siguiente Post, antes de continuar: Hello World con Python y PyCharm.

Dicho esto, comenzamos.

Creación de un nuevo Proyecto Python con un Proyecto Django en PyCharm

Abrimos PyCharm y creamos un nuevo proyecto de Python en blanco, sin incluir el welcome script (main.py).

Añadimos un fichero requirements.txt a la raiz del proyecto, e incluimos las librerías o paquetes que deseamos utilizar con la versión que queremos, en nuestro caso, django==4.0.0. Click en Install requirement para que PyCharm instale la librería en el Virtual Environment del proyecto, y que la podamos utilizar.

En la ventana de Terminal de PyCharm, ejecutamos los siguientes dos comandos, el primero para comprobar la versión instalada de Django, y el segundo para crear un Proyecto Django, es decir, crear los ficheros que necesita Django para funcionar (manage.py y la carpeta del Proyecto Django, en nuestro caso, la carpeta basic_example).

python -m django --version
django-admin startproject basic_example .

Vamos a ejecutar las migraciones de base de datos. Esto creará una base de datos SQLite con las tablas necesarias por la aplicaciones por defecto (built-in) de Django (ej: el Panel de Administración de Django), ya que por defecto el ORM de Django utiliza SQLite como motor de base de datos por simplicidad, por lo que después de ejecutar las migraciones encontraremos un nuevo fichero db.sqlite3. Para hacerlo, es suficiente con ejecutar el siguiente comando en la ventana Terminal de PyCharm.

python manage.py migrate

Crear una Aplicación Django y utilizarla en nuestro Proyecto Django

Para continuar, vamos a crear una Aplicación Django. Para ello ejecutaremos el siguiente comando desde una ventana de Terminal de PyCharm.

python manage.py startapp basic_example_pages

Aunque hemos creado una Aplicación Django, el Proyecto Django aún no sabe de su existencia. Son dos elementos independientes, que tenemos que relacionar. Para ellos, editaremos el fichero settings.py del Proyecto Django, y añadiremos la siguiente línea al final de la opción INSTALLED_APPS. El valor que añadimos está relacionado con el contenido del fichero apps.py de la Aplicación Django que deseamos ejecutar dentro de nuestro Proyecto Django.

Vamos a crear una carpeta para almacenar las plantillas (Templates) en una ubicación única para todo el Proyecto Django. Para ello, creamos una carpeta llamada «templates», y por otro lado necesitamos modificar una línea del fichero settings.py, de forma similar a como se muestra en la siguiente pantalla, para especificar cuál deseamos que sea la carpeta a utilizar para almacenar las plantillas del Proyecto.

Creamos una Plantilla base para nuestro Proyecto que llamaremos base.html, que heredaremos en el resto de páginas, para centralizar aquí ciertos aspectos como el menú y pie de página.

Creamos tres páginas o Plantillas (home.html, about.html, contact.html) que heredan de la base que acabamos de definir, de forma similar a como se muestra a continuación.

Ahora vamos a escribir las Vistas de nuestra Aplicación Django, dentro del fichero views.py, que consistirá en añadir las siguientes líneas de código (un clase para cada página o Plantilla). En particular, vamos a utilizar Vistas basadas en clases genéricas predefinidas (GCBVs: Generic Class Based Views), en concreto useremos la clase TemplateView, que permite renderizar una plantilla existente.

Ahora que ya tenemos definidas las Vista, llega el momento de definir las URLs que deseamos asociar a cada Vista.

En lugar de incluir esta asociación en el fichero urls.py del Proyecto Django, vamos a crear un fichero urls.py en la Aplicación Django para configurar ahí el enrutamiento de dicha Aplicación, y lo vamos a referenciar (include) en el fichero urls.py del Proyecto.

Para hacer esto, lo primero es crear el fichero urls.py de la Aplicación Django y configurar el enrutamiento que deseamos, tal y como se muestra en el siguiente ejemplo. Por este motivo, necesitamos importar del fichero views del directorio actual (la línea que comienza por from .views), la clases de la Vistas que vamos a utilizar, y que justo acabamos de definir (HomePageView, AboutPageView, ContactPageView). Además, asignaremos nombres a las URLs (home, about, contact).

Por otro lado, en el fichero urls.py del Proyecto Django configuraremos también el enrutamiento, en este caso diremos que cualquier petición a la raiz se resuelva conforme se defina en el fichero urls.py de nuestra Aplicación Django, que acabamo de configurar en el punto anterior. El fichero urls.py del Proyecto Django quedaría así:

Ejecutar y Depurar en PyCharm

En PyCharm, desde la opción de menú “Run -> Edit Configurations“, añadimos una configuración de ejecución, que arranque el servidor Web de Django, tal y como se muestra en la siguiente pantalla.

Hecho esto, podremos tanto ejecutar (Run) como ejecutar en modo depuración (Debug) nuestra aplicación con un sólo click, en este último caso, pudiendo además poner puntos de interrupción, ejecutar paso a paso, evaluar variables, etc. En nuestro caso, vamos a ejecutar (Run) la aplicación, para comprobar si funciona correctamente.

Y afortunadamente, todo funciona como esperábamos.

Utilización de Modelos con Django

Vamos a crear un sencillo modelo de base de datos para almacenar libros y autores (dos tablas, relacionadas, y con varios campos). Para ello editaremos el fichero models.py de nuestra Aplicación Django, y escribiremos ahí nuestro modelo de base de datos, por ejemplo:

from django.db import models

class BookAuthor(models.Model):
    name = models.CharField(max_length=100)
    country = models.CharField(max_length=50)

    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(BookAuthor, on_delete=models.CASCADE)
    genre = models.CharField(max_length=50)
    publish_date = models.DateField()

    def __str__(self):
        return self.title

Ahora que ya hemos definido nuestro modelo, tenemos que activarlo, que consiste en los siguientes dos pasos:

  • Crear un fichero de migraciones, con el comando makemigrations.
  • Aplicar los cambios en el motor de base de datos, con el comando migrate.

A continuación se muestran los dos comandos que tendríamos que ejecutar en nuestro ejemplo.

python manage.py makemigrations basic_example_pages
python manage.py migrate

Utilización de Django Admin: el Panel de Administración de Django

Django Admin es de gran utilidad, ya que nos ofrece un Panel de Administración super sencillo, de tal modo, que una vez definimos nuestros modelos, podemos utilizar Django Admin para tener una zona privada en nuestra Aplicación Web, desde la que podamos hacer tareas básicas como añadir, modificar, o eliminar registros. Lo interesante, es que Django Admin nos proporciona de forma automática todos esos formulario y pantallas, independientemente del motor de base de datos que deseemos utilizar, simplemente a partir de la definición de nuestros modelos. Mola.

Para poder utilizar Django Admin, lo primero que deberemos hacer es crear un superusuario con el comando createsuperuser.

python manage.py createsuperuser

Lo siguiente que tendremos que hacer, es decirle a Django Admin qué modelos deseamos que se puedan gestionar desde Django Admin, en nuestro caso, serían los dos que hemos creado: BookAuthor y Book. Esto lo haremos en el fichero admin.py.

from django.contrib import admin
from .models import BookAuthor, Book

admin.site.register(BookAuthor)
admin.site.register(Book)

Hecho todo esto, vamos a ejecutar nuestra aplicación Django, y acceder con un navegador, pero accederemos directamente a Django Admin. Tendremos que iniciar sesión con el superuser que acabamos de crear.

Una vez dentro, podemos ver que podemos administrar los modelos que hemos definido en nuestra Aplicación Django, en nuestro caso, tanto libros como autores.

En el siguiente ejemplo vemos cómo hemos podido dar de alta un Autor (ej: Miguel Delibes) y un Libro (ej: El Camino), de forma muy sencilla, utilizando Django Admin. Aprovecharemos y daremos de alta un par de libros más (ej: La Celestina, Don Quijote de la Mancha) y sus autores, para tener algunos datos de prueba, y poder trastear.

Utilizar los datos del Modelo desde la Vista

Ahora que ya hemos definido nuestro Modelo y lo podemos gestionar desde Django Admin, vamos a modificar la Home para que muestre los datos de nuestra base de datos, en particular, que nos liste los libros que tenemos.

Para ellos modificaremos las Vistas (Views) de nuestra Aplicación Django, de tal modo que para la Home vamos a utilizar una de tipo ListView sobre el modelo Book, que nos permitirá iterar sobre los datos del Modelo. Esto lo podemos representar con el siguiente código fuente en views.py.

from django.views.generic import TemplateView, ListView
from .models import Book

class HomePageView(ListView):
    model = Book
    template_name = "home.html"


class AboutPageView(TemplateView):
    template_name = "about.html"


class ContactPageView(TemplateView):
    template_name = "contact.html"

Lo siguiente es actualizar la Plantilla de la Home, para que sea capaz de iterar por los datos del Modelo, para mostrar el contenido que deseamos en la respuesta HTTP, por ejemplo, de esta forma.

<ul>
    {% for book in book_list %}
        <li>{{ book.title }}</li>
    {% endfor %}
</ul>

Hecho esto, llega el momento de probarlo. Ejecutamos nuestra aplicación, y al acceder a la Home, vemos como ya muestra el contenido que deseamos desde nuestra base de datos, utilizando el ORM de Django.

Ejecución de Pruebas Unitarias (Unit Testing) con Django

Python proporciona un framework para la ejecución de Pruebas Unitarias (Unit Testing) a través de la librería unittest (basada en jUnit), que utiliza la definición de Casos de Prueba y el uso de Aserciones, para su implementación.

Django proporciona su propio framework de Pruebas Unitarias, por encima de unittest, que incluye un Cliente que realiza peticiones dummy HTTP, sus propias Aserciones, y sus propios Casos de Prueba, como:

  • SimpleTestCase. Util cuando no se utiliza una base de datos.
  • TestCase. Util cuando si se utiliza una base de datos.
  • TransactionTestCase. Util cuando hay transacciones de base de datos.
  • LiveServerTestCase. Util cuando se utilizan herramientas como Selenium.

Además, al crear una Aplicación Django, también nos proporciona un fichero test.py donde deberemos escribir nuestras Pruebas Unitarias de Django. Una vez hecho, las podemos ejecutar con un comando como el siguiente:

python manage.py test

En el caso de tener que escribir Pruebas Unitarias de páginas estáticas servidas desde una Plantilla sin acceso a base de datos, como las páginas About y Contact de este ejemplo, podemos utilizar un Caso de Prueba de tipo SimpleTestCase y probar por ejemplo:

  • Una petición HTTP a la URL de la página (ej: /about/), devuelve un 200.
  • Una petición al nombre de la URL de la página (ej: about), devuelve un 200.
  • Una petición a la página, es resuelta utilizando la Plantilla (template) correcta (ej: about.html).
  • Una petición a la página, devuelve una respuesta que contiene un contenido determinado (ej: <p>This is the About page</p>).

A continuación se puede ver un ejemplo, para las páginas de About y Contact.

En el caso de tener que escribir Pruebas Unitarias del Modelo así como de páginas servidas desde una Plantilla con acceso a base de datos, como es el caso de la Home, podemos utilizar un Caso de Prueba de tipo TestCase, tanto para probar el modelo como la página. Necesitaremos definir un método setUpTestData(), que permite crear los datos una vez para cada caso de pruebas, por lo que suele ser mejor opción y más eficiente que el métido setUp() el cual crea los datos para cada Test. A continuación se puede ver un ejemplo.

Con todo esto, el total de las Pruebas Unitarias escritas en el fichero tests.py queda así:

from django.test import SimpleTestCase, TestCase
from django.urls import reverse

from .models import BookAuthor, Book

class BookAuthorTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.author = BookAuthor.objects.create(name="Miguel Delibes", country="España")

    def test_model_content(self):
        self.assertEqual(self.author.name, "Miguel Delibes")


class BookTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.author = BookAuthor.objects.create(name="Miguel Delibes", country="España")
        cls.book = Book.objects.create(title="El Camino", author=cls.author, genre="Fiction", publish_date="2006-03-16")

    def test_model_content(self):
        self.assertEqual(self.book.title, "El Camino")

    def test_url_exists_at_correct_location(self):
        response = self.client.get("/")
        self.assertEqual(response.status_code, 200)

    def test_url_available_by_name(self):
        response = self.client.get(reverse("home"))
        self.assertEqual(response.status_code, 200)

    def test_template_name_correct(self):
        response = self.client.get(reverse("home"))
        self.assertTemplateUsed(response, "home.html")

    def test_template_content(self):
        response = self.client.get(reverse("home"))
        self.assertContains(response, "<p>This is the Home page</p>")


class AboutpageTests(SimpleTestCase):
    def test_url_exists_at_correct_location(self):
        response = self.client.get("/about/")
        self.assertEqual(response.status_code, 200)

    def test_url_available_by_name(self):
        response = self.client.get(reverse("about"))
        self.assertEqual(response.status_code, 200)

    def test_template_name_correct(self):
        response = self.client.get(reverse("about"))
        self.assertTemplateUsed(response, "about.html")

    def test_template_content(self):
        response = self.client.get(reverse("about"))
        self.assertContains(response, "<p>This is the About page</p>")


class ContactpageTests(SimpleTestCase):
    def test_url_exists_at_correct_location(self):
        response = self.client.get("/contact/")
        self.assertEqual(response.status_code, 200)

    def test_url_available_by_name(self):
        response = self.client.get(reverse("contact"))
        self.assertEqual(response.status_code, 200)

    def test_template_name_correct(self):
        response = self.client.get(reverse("contact"))
        self.assertTemplateUsed(response, "contact.html")

    def test_template_content(self):
        response = self.client.get(reverse("contact"))
        self.assertContains(response, "<p>This is the Contact page</p>")

Ya sólo queda ejecutar las Pruebas Unitarias, y comprobar su resultado. Vamos a ello.

Productivizar nuestro Proyecto Django con Gunicorn, Docker, y NGINX

Ahora vamos a ver cómo podríamos llevarlo a Producción, es decir, que se ejecute con servidor Web más profesional como Gunicorn, de forma dockerizada, y añadiendo un Proxy Inverso como NGINX (también dockerizado).

Editaremos el fichero requirements.txt y añadiremos una línea para incluir el paquete de gunicorn, en la versión que deseemos, de forma similar a como se muestra a continuación. También aprovechamos para subir la versión de Django a la última LTS disponible. Instalamos ambas librerías en el Virtual Environment de PyCharm.

Como además de los propios ficheros de nuestro Proyecto Python, también necesitamos tener otros ficheros como Dockerfiles, vamos a reorganizar mejor los ficheros y carpetas de nuestro proyecto Python. Básicamente creamos un carpeta app en la que movemos los ficheros de nuestro Proyecto Python, todo excepto requirements.txt, que lo mantenemos en la raíz. A continuación se muestra como quedaría.

Ahora que ya tenemos Gunicorn y hemos reorganizado nuestro proyecto, vamos a dockerizarlo. Para ello añadiremos un nuevo fichero Dockerfile en la raíz de nuestro proyecto Python, en el que añadiremos el siguiente contenido. Básicamente, partiendo de una imagen oficial de Python, copiamos los ficheros de nuestra aplicación, instalamos las librerías que necesitamos (requirements.txt) y ejecutamos gunicorn.

FROM python:3.9.15-slim-bullseye

ENV TZ="Europe/Madrid"

RUN mkdir -p /usr/src/app
COPY app /usr/src/app/
COPY requirements.txt /usr/src/app/

WORKDIR /usr/src/app

RUN pip3 install --upgrade pip
RUN pip3 install -r requirements.txt

CMD cd /usr/src/app && gunicorn basic_example.wsgi:application --bind 0.0.0.0:8000

Para probarlo, desde una ventana de Terminal podemos construir la imagen Docker (docker build) y arrancar un contenedor con nuestra imagen (docker run). Hecho eso, podemos comprobar que el contenedor ha arrancado correctamente (docker ps) y probar el acceso a nuestra aplicación con un navegador. Cuando acabemos, podemos parar el contenedor (docker stop).

docker build -t django-basic-example .
docker run -it --rm -d -p 8000:8000 --name django-basic-example django-basic-example
docker stop django-basic-example

Sin embargo, al acceder al Panel de Administración de Django Admin, resulta que no se visualiza correctamente, ya que no es capaz de descargarse los estáticos (ej: css, js, etc). Esto es debido a que estamos utilizando Gunicorn, que es un servidor de aplicaciones para la ejecución de código de servidor, y como tal, Gunicorn no se encarga de la entrega de ficheros estáticos.

Para solucionarlo tenemos que hacer varias cosas:

  • Configurar el Proyecto Django, en el fichero settings.py, la ruta que deseamos utilizar para almacenar los estáticos (tanto el path de la URL como el directorio que almacenará los estáticos, utilizando las opciones STATIC_URL y STATIC_ROOT).
  • Necesitaremos ejecutar un comando (collectstatic) para generar los estáticos, ya sea en tiempo de construcción de la imagen Docker, o bien durante el arranque del contenedor (esta última opción es la que vamos a aplicar).
  • Arrancar un contenedorer NGINX, y configurar un volumen docker compartido entre ambos contenedores (NGINX y Django), para que así, al generar los estáticos desde Django, sean visibles por el NGINX para poder servirlos. Esto implica configurar NGINX para la entrega de los estáticos.

Vamos a comenzar con la configuración del Proyecto Django, indicando en el fichero settings.py la ruta que deseamos utilizar para la entrega de los estáticos (STATIC_URL y STATIC_ROOT).

Seguidamente, configuramos el Dockerfile para ejecutar la generación de estáticos (comando collectstatic) justo antes de arrancar el servidor Gunicorn.

Hecho esto, ahora vamos a dejar listo el fichero docker-compose-yml, para poder arrancar nuestra aplicación Django junto con un NGINX, compartiendo un volumen que almacenará los estáticos. Para ello, creamos el fichero docker-compose.yml con el siguiente contenido.

version: '3.4'

services:
  django-basic-example:
    image: django-basic-example
    container_name: django-basic-example
    restart: unless-stopped
    build:
      context: .
      dockerfile: ./Dockerfile
    ports:
      - "8000:8000"
    volumes:
      - ./docker-disks/django_staticfiles:/usr/src/app/staticfiles/
  nginx:
    image: nginx:1.23.4
    container_name: nginx
    restart: unless-stopped
    environment:
      TZ: ${TZ}
    ports:
      - 80:80
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./docker-disks/django_staticfiles:/usr/src/app/staticfiles/

Lo siguiente es crear el fichero de configuración del NGINX (nginx.conf) para que actúe como Proxy Inverso escuchando en el puerto tcp-80, que utilizamos en el Docker Compose, incluyendo la entrega de estáticos desde el volumen compartido.

events {
}

http {
   include /etc/nginx/mime.types;

   server {
      listen        80;

      client_max_body_size 1G;

      location / {
         proxy_pass http://django-basic-example:8000;

         proxy_set_header Host $host;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
      }

      location /static/ {
         alias /usr/src/app/staticfiles/;
      }
   }

}

Si ahora volvemos construir la imagen Docker, arrancamos el Docker Compose, y probamos, veremos que todo funciona correctamente.

docker-compose up --build -d
docker ps
docker-compose down

Con esto, ya hemos prácticamente acabado, ya sólo quedaría añadirlo a Git, pasarlo por Sonarqube (crear un fichero sonar-project.properties), crear un README.md, etc.

Ejecutar Pruebas Unitarias, informe de cobertura, y pasar por Sonar

Por último, aprovechando que tenemos Pruebas Unitarias, vamos a aprovechar para ejecutarlas generando el informe de cobertura, y pasar por Sonar.

Para ello, lo primero necesitamos instalar la librería Coverage.py, pero al no ser un requisito para nuestra aplicación, ya que sólo lo necesitamos para ejecutar las pruebas unitarias o obtener el informe de cobertura, no lo vamos a añadir al requirements.txt, en su lugar, nos vamos a limitar a instalarlo en el Virtual Environment de PyCharm (pestaña Python Packages). Buscamos la librería, la seleccionamos, y click en Install.

Hecho esto ya podemos:

  • Ejecutar las pruebas unitarias con el comando coverage run, y así obtener la cobertura. Lo haremos desde la ventana de Terminal de PyCharm.
  • Generar el informe de cobertura con el comando coverage xml.
  • Crea o revisar el fichero sonar-project.properties, y asegurarnos de que especificamos en el la ubicación exacta del informe de cobertura, ya que lo necesita para recogerlo y enviarlo al servidor Sonar, además del resto de opciones que podamos necesitar, como las exclusiones.
  • Ejecutar el Sonar-Scanner, para realizar el análisis de código y enviarlo a Sonar junto con el informe de cobertura, para su procesamiento.

En resumidas cuentas, se trata de ejecutar unos comandos similares a los siguientes.

coverage run --source='.' manage.py test
coverage xml
sonar-scanner

Y con esto, ya tendremos en nuestro servidor de Sonarqube nuestro análisis de código incluyendo la cobertura, que a vista de pájaro, con un 91%, tiene buena pinta.

Despedida y Cierre

Hasta aquí llega este nuevo Post de Django, en el que hemos visto varias cosas cómo por ejemplo:

  • Cómo utilizar Plantillas (Templates) para crear páginas Web con Django, incluido la utilización de una plantilla base donde reutilizar la cabezará y pie de nuestras páginas.
  • Cómo crear un Modelo y utilizar el ORM de Django para crear las tablas de base de datos, en este caso, sobre SQLite.
  • Cómo utilizar el Panel de Administración de Django Admin, para tener una zona privada donde poder hacer tareas de alta/baja/modificación de nuestro modelo de datos.
  • Cómo utilizar Vistas basadas en Clases.
  • Cómo crear Pruebas Unitarias en Django, incluyendo la forma de generar un informe de cobertura y de subirlo a Sonar.
  • Cómo dockerizar nuestra aplicación con un NGINX y Gunicorn como servidor de aplicaciones, teniendo en cuenta, que en este caso tenemos que generar los estáticos de Django para que sean servidos por el NGINX.

Además, tienes disponible este ejemplo en el siguiente repo público de GitHub: GitHub – ElWillieES – django-basic-example

Poco más por hoy. Como siempre, confío que la lectura resulte de interés.