Hablando de Django

Me gustaría empezar diciendo que Django es solo Python, mientras más cómodos nos sintamos usando Python mejor nos moveremos en Django.

Realmente no se necesita un conocimiento profundo de Python, pero hay un par de conceptos que creo que es importante mencionar

Módulos

En Python un módulo es un archivo con extensión .py que contiene código de Python.

Paquetes

Es una carpeta que contiene varios módulos, contiene un archivo __init__.py lo cual ayuda al intérprete a identificar la carpeta como un módulo de Python.

Ciclo de solicitud/respuesta

Django (entre otros frameworks web) tiene algo llamado ciclo de solicitud/respuesta, en inglés request/response cicle. El siguiente diagrama

ciclo request/response

En la reunión vimos los principales archivos que posibilitan este flujo en Django y como usarlos. El resto de este artículo está dedicado a explicar ese flujo.

Creando el entorno virtual

Es conveniente tener un entorno virtual. Durante la reunión usamos venv que usualmente viene junto con la instalación de Python. Dependiendo de nuestro sistema operativo utilizaremos uno de los siguientes grupos de comandos:

Windows

Usando la interfaz gráfica de Windows creamos una carpeta para almacenar nuestro proyecto, en este caso la llamaremos miproyecto

Abrimos el CMD y navegamos hasta esa carpeta, suponiendo que creamos las carpeta en el disco local C ejecutariamos los siguiente

cd C:\miproyecto

Creamos el entorno virtual dandole un nombre al entorno

python -m venv mientorno

Activamos el entorno virtual

mientorno\Scripts\activate

Linux

# crea una carpeta en el root del usuario
mkdir ~/miproyecto
# entra en esa carpeta
cd ~/miproyecto
# crea un entorno usando venv
python3 -m venv mientorno
# activa el entorno
source venv/bin/activate

Instalando e Inicializando Django

Para instalar Django ejecuta el siguiente comando

pip install django

Podemos verificar que hemos instalado Django con el siguiente comando

pip list

Deberíamos ver la siguiente salida en la terminal

Package           Version
----------------- -------
asgiref           3.8.1
Django            5.2.1
pip               22.0.2
setuptools        59.6.0
sqlparse          0.5.3
typing_extensions 4.13.2

Inicializamos nuestro proyecto con el siguiente comando

django-admin startproject miproyecto .

El punto al final del comando indica que no queremos que cree una carpeta envolviendo nuestro proyecto. Recordando que ya estamos dentro de una carpeta que envuelve nuestro proyecto.

Revisando los archivos

En este punto deberíamos tener la siguiente estructura de archivos

miproyecto
├── manage.py
└── miproyecto
    ├── asgi.py
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

Entre estos el más importante es settings.py aquí van todas las configuraciones de nuestro proyecto, entre ellas por el momento nos importan 2 en particular, los paquetes instalados y las rutas registradas.

# . . . algunas configuraciones antes
# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

# . . . más configuracinoes en el medio

ROOT_URLCONF = 'miproyecto.urls'

# . . . más configuraciones despues

Django usa el módulo indicado por ROOT_URLCONF para saber a qué rutas responder. Es como la puerta de entrada de nuestro servidor.

# miproyecto/urls.py

from django.contrib import admin
from django.urls import path

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

Las rutas se definen dentro de urlpatterns usando la función path

Por defecto nuestro proyecto tiene solo una ruta registrada que dirige hacia el admin de Django.

Creando Nuestras Rutas

La función path recibe por lo menos 2 parametros:

  • un path (url) válido
  • una función que devuelva una instancia de HttpResponse

Veamos como hacerlo

# miproyecto/urls.py

from django.contrib import admin
from django.urls import path
from django.http import HttpResponse

def mivista(request):
    return HttpResponse("hola mundo")

urlpatterns = [
    path('admin/', admin.site.urls),
    path('inicio', mivista)
]

Guardamos los cambios e iniciamos el servidor de desarrollo de Django desde la terminal con el siguiente comando:

python manage.py runserver

Ahora visitamos la ruta que acabamos de registrar http://127.0.0.1:8000/inicio

primera-ruta-en-django

Tada!!! 🎉 ✨🎊

¿Viste? Es magia... bueno, en realidad es solo Python.

¿Qué acaba de suceder? Creamos la ruta "inicio" y le asociamos la función mivista.

Volvamos a nuestro flujograma inicial para repasar como hemos implementado el ciclo de solicitud y respuesta para la ruta "inicio"

django-step-1

Ok, simplificamos un montón el proceso, pero esto es solo para mostrar el concepto. Ahora que sabemos lo mínimo que necesitamos para generar una respuesta, usemos algunas de las herramientas que vienen con Django.

Creando nuestra primera app

En Django se le llama app a los paquetes que descargamos o a los que nosotros mismos desarrollamos. Desarrollaremos una página que muestre información de algunos platos de comida.

Para comenzar a desarrollar nuestra app usaremos el comando startapp seguido del nombre de la app.

python manage.py startapp comida

Ahora deberíamos tener la siguiente estructura de archivos

miproyecto
├── comida
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── db.sqlite3
├── manage.py
└── miproyecto
    ├── asgi.py
    ├── __init__.py
    ├── __pycache__
    ├── settings.py
    ├── urls.py
    └── wsgi.py

Django solo conoce las apps que están registradas en INSTALLED_APPS . Para registrar nuestra app agregaremos el nombre a la lista.

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'comida', # la agregamos aquí
]

Hay varias cosas que Django buscará en nuestra app cada vez que se inicie el servidor de desarrollo, de eso hablaremos dentro de poco, por ahora lo importante es decir que Django ya sabe que nuestra app existe.

Dentro de nuestra app recién creada hay un archivo destinado a colocar las funciones asociadas a las rutas, démosle un vistazo.

# archivo comida/views.py
from django.shortcuts import render

# Create your views here.

Como nos lo indica el nombre de este módulo a las funciones asociadas a una ruta se les denomina vista.

La función render que vemos importada al comienzo también devuelve una instancia de la clase HttpResponse lo que significa que la podemos utilizar para enviar una respuesta. render recibe los siguientes argumentos:

  1. objeto request
  2. una cadena de texto con el nombre de una plantilla
  3. un diccionario de Python que se pasa como variables a la plantilla
# archivo comida/views.py
from django.shortcuts import render

def lista_comidas(request):
    contexto = {
        "comida": "empanada"
    }
    return render(request, "comida/index.html", contexto)

Ok, nuestra vista está lista, pero falta crear la plantilla "comida/index.html".

Django busca plantillas dentro de todas las apps registradas y espera encontrarlas en una carpeta llamada templates.

Cuando llamamos a render Django buscará en TODAS las apps registradas y nos devolverá la primera plantilla que tenga ese nombre, por esa razón debemos crear una carpeta intermedia con el nombre de la app (hay otro enfoque que veremos en otra oportunidad)

Al crear nuestra plantilla tendremos la siguiente estructura de archivos

├── comida
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── templates # NUEVO django buscará esta carpeta al usar render
│   │   └── comida # NUEVO esta es la carpeta intermedia
│   │       └── index.html # NUEVO nuestra plantilla
│   ├── tests.py
│   └── views.py
├── db.sqlite3
├── manage.py
└── miproyecto
    ├── asgi.py
    ├── __init__.py
    ├── __pycache__
    ├── settings.py
    ├── urls.py
    └── wsgi.py

Colocamos el siguiente contenido en la plantilla

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Comidas</title>
</head>
<body>
    <h1>Lista de Comidas</h1>
    <p>Los mejores platos de Latinoamérica</p>
    <ul>
        <li>{{comida}}</li>
    </ul>
</body>
</html>

Nuestra plantilla parece un archivo HTML común, pero el motor de plantillas de Django tiene la capacidad de inyectar variables, ejecutar bucles, leer condicionales y muchas más cosas. En este caso solo imprimirá la variable que le estamos pasando desde la vista.

Estamos casi listos, ahora tenemos que asociar nuestra vista a una ruta.

Crearemos un módulo llamado urls dedicado a guardar todas las rutas de nuestra app recién creada.

├── comida
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── templates 
│   │   └── comida
│   │       └── index.html
│   ├── tests.py
│   ├── urls.py # NUEVO rutas de la app
│   └── views.py
├── db.sqlite3
├── manage.py
└── miproyecto
    ├── asgi.py
    ├── __init__.py
    ├── __pycache__
    ├── settings.py
    ├── urls.py
    └── wsgi.py

En este módulo colocaremos lo siguiente

# comidas/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('comidas', views.lista_comidas)
]

La única diferencia de este módulo de rutas con el que vimos al principio es que la vista está definida en un módulo diferente.

En la segunda línea podemos ver una importación relativa, que dice algo como "desde este mismo paquete importar el módulo views".

Aquí importamos todo el módulo, por lo que podemos usar la vista que definimos así views.lista_comidas

Este enfoque es útil para desacoplar un poco la lógica de nuestra app de comidas, pero no hay que olvidar que la "puerta de nuestro servidor" sigue siendo el módulo miproyecto/urls.py así que hay que conectar nuestra app con él.

Comentaré el código anterior y agregaré nuevo código al archivo miproyecto/urls.py

# miproyecto/urls.py

from django.contrib import admin
from django.urls import path, include # NUEVO importación
# from django.http import HttpResponse

# def mivista(request):
#     return HttpResponse("hola mundo")

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include("comida.urls")),
    # path('inicio', mivista)
]

Hay varias cosas aquí, lo más importante es que la función include nos permite incluir todo el módulo de rutas de nuestra app de comidas y asociarlo con una ruta. En este caso con una cadena de texto vacía, lo que significa que no usamos un prefijo para nuestras rutas.

Guardamos todos los cambios y visitamos la ruta http://127.0.0.1:8000/comidas recuerda revisar que el servidor de desarrollo esté rodando.

comidas-lista-0

Bravo!! 👏👏👏

Sin duda este tomo muchos más pasos que nuestro primer ejemplo, pero hemos desacoplado la app de comidas del resto del proyecto, y esto nos ayudará a mantener las cosas organizadas, veamos nuevamente nuestro flujograma

django-step-2

Creando Modelos

El corazón de Django está en los modelos. Los modelos nos permiten interactuar con la base de datos sin tener que escribir SQL.

Cada app tiene un modulo dedicadoa los modelos llamado models.py e inicialmente se ve así:

# comida/models.py
from django.db import models

# Create your models here.

Ahora crearemos una clase que nos permita almacenar información de comidas en la base de datos.

# comida/models.py
from django.db import models

class Comida(models.Model):
    nombre = models.CharField(max_length=120)
    descripcion = models.TextField()
    calificacion = models.IntegerField()

    def __str__(self):
        return self.nombre

Aquí creamos una clase que extiende de model.Model, Django usará estos atributos para crear una tabla en la base de datos, si te interesa puedes consultar la documentación de todos los campos disponibles en Django. Finalmente, definimos el método __str__ que establece que texto se mostrará cuando imprimamos una instancia de esta clase (este método es algo propio de las clases en Python).

Migraciones

Las migraciones son el mecanismo que nos permite traducir cambios en el código de nuestros modelos (Python) en cambios a nuestra base de datos (SQL). Este mecanismo se da en un proceso de 2 pasos:

  1. Crear una migración
  2. Ejecutar una migración

Ejecuta el siguiente comando

python manage.py makemigrations

Deberías ver una salida en la terminal como esta

Migrations for 'comida':
  comida/migrations/0001_initial.py
    + Create model Comida

En la carpeta de la app de comidas deberías tener el siguiente archivo

comida
├── admin.py
├── apps.py
├── __init__.py
├── migrations
│   ├── 0001_initial.py # NUEVO
│   ├── __init__.py
│   └── __pycache__
├── models.py
. . .

Démosle una revisada rápida

# Generated by Django 5.2.1 on 2025-05-23 14:38

from django.db import migrations, models

class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Comida',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('nombre', models.CharField(max_length=120)),
                ('descripcion', models.TextField()),
                ('calificacion', models.IntegerField()),
            ],
        ),
    ]

Este archivo es el que Django utilizará para realizar los cambios en la base de datos, normalmente no necesitaremos hacer cambios en el de forma directa a menos que algo salga mal durante el proceso de crear la migración.

En este punto nuestra base de datos está completamente vacía, hay un archivo llamado db.sqlite3 en el directorio base de nuestro proyecto pero este no contiene nada.

Ahora aplicaremos las migraciones con el siguiente comando:

python manage.py migrate

Deberíamos ver la siguiente salida en la terminal

Operations to perform:
  Apply all migrations: admin, auth, comida, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying comida.0001_initial... OK
  Applying sessions.0001_initial... OK

Lo que acaba de suceder es que Django hay recorrido todas las apps registradas buscando migraciones pendientes por ejecutar, casi todo lo que vemos aquí son migraciones que vienen creadas dentro de la instalación de Django, la migración de nuestro modelo está en la penúltima línea.

Ahora que hemos creado una tabla para almacenar datos de nuestras comidas, es hora de llenar esa tabla.

Shell de Django

Django tiene un Shell que nos da acceso al entorno de nuestro proyecto mientras ejecutamos código de Python. Para entrar al Shell usamos el siguiente comando:

python manage.py shell

Iniciamos importando nuestra clase Comida

from comida.models import Comida

Usamos la clase para consultar el contenido de la tabla de comidas (sabemos que está vacía).

Comida.objects.all()
### salida:<QuerySet []>

Esta especie de atributo llamado objects es un Manager. Los managers tienen muchos métodos útiles para interactuar con la tabla a la que la clase está relacionada. El método all que invocamos trae todos los datos de la tabla.

El tipo de la salida es un QuerySet, se parece mucho a una lista en Python, como esperábamos, esta QuerySet está vacía.

A continuación crearemos varias instancias de la clase Comida pasándole los atributos como argumentos:

empanadas = Comida(nombre="Empanadas", descripcion="Platillo típico venezolano hecho a base de masa de maíz precocido (similar a la de las arepas), que se rellena con diversos ingredientes —como carne mechada, pollo, queso, cazón (pescado), o caraotas (frijoles negros)— y luego se fríe hasta obtener una textura crujiente por fuera y suave por dentro.", calificacion=5)
ceviche = Comida(nombre="Ceviche", descripcion="Plato fresco y sabroso preparado con pescado crudo (generalmente corvina o lenguado) marinado en jugo de limón, acompañado de cebolla roja, ají limo, cilantro y sal. Se sirve tradicionalmente con camote (batata dulce), maíz cocido (choclo) y, a veces, cancha (maíz tostado)", calificacion=5)
bandeja_paisa = Comida(nombre="Bandeja Paisa", descripcion="La bandeja paisa es uno de los platos más tradicionales y abundantes de la cocina colombiana, especialmente de la región de Antioquia. Se caracteriza por su generosa variedad de ingredientes servidos juntos en una sola bandeja: arroz blanco, carne molida o en bistec, chicharrón crujiente, huevo frito, plátano maduro, frijoles rojos, arepa, aguacate y morcilla.", calificacion=5)

Podemos revisar los atributos de estas instancias

empanadas.nombre
# salida: 'Empanadas'

bandeja_paisa.calificacion
# salida: 5

ceviche
# salida: <Comida: Ceviche>
# la salida de imprimir la instancia "ceviche" es "Ceviche" porque el método __str__  que definimos devuelve el atributo nombre de la instancia

Muy bien, entonces si consultamos nuestra tabla nuevamente tendremos lo siguiente

Comida.objects.all()
# salida:<QuerySet []>
# Sí, no hay nada en esta tabla

La razón de porque la tabla continúa vacía es porque las instancias se crean "en memoria" y para "enviarlas" a la base de datos es necesario ejecutar el método save de la siguiente forma:

empanadas.save()
bandeja_paisa.save()
ceviche.save()
Comida.objects.all()
# salida: <QuerySet [<Comida: Empanadas>, <Comida: Bandeja Paisa>, <Comida: Ceviche>]>
# Ahora sí hemos guardado las comidas en nuestra base de datos

En ocasiones querremos crear un nuevo registro en una tabla sin tener que primero crear una instancia en memoria, esto se puede hacer usando el método create del manager, de la siguiente forma:

Comida.objects.create(nombre="Asado", calificacion=5, descripcion="El asado es mucho más que una comida: es una tradición y un ritual social en Argentina. Consiste en carne de res (como costillas, vacío, chorizo, morcilla y otros cortes) cocinada lentamente a las brasas en una parrilla. Se suele acompañar con chimichurri (una salsa a base de ajo, perejil, vinagre y especias), ensaladas frescas y pan.")
# salida <Comida: Asado>
# devuelve como salida la instancia (después de guardar el registro en la base de datos)

Para salir del Shell escribimos exit()

Bien, ahora que hemos poblado nuestra tabla podemos recuperar esos datos en la vista y así mostrarlos en la respuesta.

Haciendo Consultas desde la Vista

Editemos comida/views.py de la siguiente forma

from django.shortcuts import render
from .models import Comida # importamos nuestra clase 

def lista_comidas(request):
    contexto = {
        "comidas": Comida.objects.all() # repetimos nuestra consulta asociandola a la clave "comidas" (nótese que la clave ahora está en plural)
    }
    return render(request, "comida/index.html", contexto)

Ahora actualicemos la plantilla para poder imprimir nuestra QuerySet

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Comidas</title>
    <style>
        * {
            margin:0;
            box-sizing: border-box;
        }
        body {
            margin-inline: auto;
            max-width: 56rem;
            text-align: center;
            padding: 1rem;
            font-family: sans-serif;
        }
        
        header {
            margin-bottom: 1.25rem; 
        }

        .subtitulo {
            font-size: 1.25rem;
            margin-top: 0.5rem;
        }

        .comidas {
            list-style: none;
            padding: 0;
            display: flex;
            flex-wrap: wrap;
            gap: 1rem;
        }

        .comida {
            flex: 1 1 calc(50% - 0.75rem);
            display: flex;
            flex-direction:column;
            gap: 1rem;
            min-width: min(100%, 20rem);
            padding: 1rem;
            border-radius: 0.5rem;
            background-color: #8ae1fc;
        }

        .comida-nombre {
            font-size: 1.5rem;
        }

        .comida-descripcion {
            opacity: 0.8;
        }

        .comida-calificacion {
            flex: 1 0;
            display: grid;
            place-items: end center;
        }
</style>
</head>
<body>
    <header>
        <h1>Lista de Comidas</h1>
        <p class="subtitulo"><em>Los mejores platos de latinoamérica</em></p>
    </header>
    <main>
        <ul class="comidas">
            {% for comida in comidas %}
            <li class="comida">
                <p class="comida-nombre">{{ comida }}</p>
                <p class="comida-descripcion">{{ comida.descripcion }}</p>
                <p class="comida-calificacion">Calificación: {{ comida.calificacion }}</p>
            </li>
            {% endfor %}
        </ul>
    </main>
</body>
</html>

Estamos iterando nuestro QuerySet usando una sintaxis similar a como se iteran las colecciones en Python, luego dentro del bucle estamos imprimiendo los atributos de la instancia que se está iterando, al final debemos indicar en donde se cierra el bucle.

Agregue algunos estilos dentro de una tag style para mejorar la apariencia. En otra oportunidad podríamos ver como se cargan los estilos (archivos estáticos) en Django.

Finalmente iniciamos el servidor de desarrollo (si no está rodando) y visitamos nuestra ruta http://127.0.0.1:8000/comidas

comidas-lista-2

¡Lo hemos logrado! 🚀🚀🚀

Echémosle un vistazo a nuestro diagrama de flujo de solicitud y respuesta para la ruta comidas

django-step-3

Ha sido un gusto acompañarte en este proceso. Si tienes alguna duda, consulta o algo que quieras compartir conmigo puedes usar el formulario de contacto que está continuación, te deseo la mejor de las suertes y mucho éxito.

Contacto

Puedes escribirme por Whatsapp, pero si prefieres no usar ese medio tambien puedes contactarme el formulario de contacto en esta sección.

Yo prefiero el contacto por medio de Whatsapp.

Iniciar conversación