Saltar a contenido

URLs

Django básico

Cuando Django recibe una petición HTTP lo primero que hace es intentar encontrar el patrón que coincide con la URL solicitada:

flowchart TD
    client[Client] -->|"https://myblog.com/posts/django-is-awesome/"| django[Django]
    subgraph "URLs de primer nivel"
    main_urls[main/urls.py]
    end
    django -->|"/posts/django-is-awesome/"| main_urls
    subgraph "URLs de segundo nivel"
    main_urls -->|"django-is-awesome/"| post_urls[posts/urls.py]
    end
    post_urls -->|"'django-is-awesome'"| post_views[posts/views.py]
    subgraph "Vistas"
    post_views --> post_detail["post_detail('django-is-awesome')"]
    end

En esta sección veremos cómo configurar estos patrones para lanzar las acciones oportunas.

URLs de primer nivel

Si hemos creado el proyecto Django con la carpeta base main podremos encontrar las URLs de primer nivel en el fichero main/urls.py.

El contenido (por defecto) de este fichero es el siguiente:

main/urls.py
from django.contrib import admin#(1)!
from django.urls import path#(2)!


urlpatterns = [#(3)!
    path('admin/', admin.site.urls),#(4)!
]

  1. Este módulo contiene las funcionalidades de la interfaz administrativa de Django.
  2. Esta función nos permite definir las rutas URL correspondientes.
  3. Las URLs deben almacenarse en una lista con nombre urlpatterns.
    • Cada URL viene definida por la función path que vincula (en general) una ruta con una vista.
    • En este caso se indica que si la URL de entrada es /admin/ se pase el control al módulo admin.site.urls.

URLs de segundo nivel

Cada aplicación en un proyecto Django puede tener sus propias URLs que definen el comportamiento de la misma.

Supongamos por ejemplo que estamos desarrollando una aplicación llamada posts y queremos que las siguientes URLs cobren vida:

  • /posts/#(1)!
  • /posts/this-is-a-new-post/#(2)!
  1. Listado de todos los «posts» del «blog».
  2. Detalle de un «post» en concreto con el slug this-is-a-new-post.

Lo primero será modificar el fichero de configuración de las URLs de primer nivel para añadir la delegación a la aplicación correspondiente:

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


urlpatterns = [
    path('admin/', admin.site.urls),
    path('posts/', include('posts.urls')),#(2)!
]

  1. Importamos la función include().
    • Indicamos que las URLs que comiencen por /posts/ deben delegarse a las URLs de segundo nivel que están en posts/urls.py.
    • Las urls se especifican usando una cadena de texto «cualificada».
    • Cabe la posibilidad de especificar un argumento namespace para sobreescribir el valor asignado a app_name (urls.py).

Barra final

Para evitar problemas, recuerda siempre acabar las URLs con la barra / del final.
Por ejemplo 'comments/' en vez de 'comments

Ahora ya podemos definir las URLs de segundo nivel en la aplicación posts creando el fichero posts/urls.py con el siguiente contenido:

posts/urls.py
from django.urls import path

from . import views


app_name = 'posts'#(1)!

urlpatterns = [
    path('', views.post_list, name='post-list'),#(2)!
    path('<slug:post_slug>/', views.post_detail, name='post-detail'),#(3)!
]

  1. La variable app_name define el espacio de nombres de las URLs de cada aplicación.
  2. Analicemos cada parámetro de la función path por separado:
    1. '' Si concatenamos /posts/ con la cadena vacía, obtenemos que la URL resultante es: /posts/
    2. views.post_list vista que se lanzará si la URL casa con este patrón.
    3. name='post-list' nombre de la URL, que unido al espacio de nombres, lo identifican unívocamente en todo el proyecto. Por tanto será: posts:post-list.
  3. Analicemos cada parámetro de la función path por separado:
    1. '<post_slug>' El uso de ángulos nos indica que se trata de un parámetro variable. Casa con cualquier entrada. Si concatenamos /posts/ con <post_slug> obtenemos que la URL resultante es /posts/this-is-a-new-post/
    2. views.post_detail vista que se lanzará si la URL casa con este patrón.
    3. name='post-detail' nombre de la URL, que unido al espacio de nombres, lo identifican unívocamente en todo el proyecto. Por tanto será: posts:post-detail.

Nombres de URLs

Es habitual usar «slugs» en los nombres de URLs. Es decir, cuando utilizamos el parámetro name de la función path. En vez de name='post_list' suele ser de buen estilo escribir name='post-list'.

Agrupar URLs

En el caso de tener distintos patrones de URLs en un mismo fichero urls.py se aconseja agrupar los patrones por similitud.

Un pequeño ejemplo en el que se plantea este escenario:

urls.py
urlpatterns = [
    path('shop/', ...),
    path('api/', ...),
    path('shop/product/{int:product_pk}/', ...),
    path('api/apparel/{int:product_pk}/', ...),
    path('shop/purchase/{slug:article_slug}/', ...),
    path('api/goods/{slug:good_slug}/', ...),
]
urls.py
urlpatterns = [
    path('api/', ...),
    path('api/apparel/{int:product_pk}/', ...),
    path('api/goods/{slug:good_slug}/', ...),
    path('shop/', ...),
    path('shop/product/{int:product_pk}/', ...),
    path('shop/purchase/{slug:article_slug}/', ...),
]

Conversores de rutas

En las rutas dinámicas (aquellas que contienen parámetros variables) es posible indicar el tipo de cada parámetro para que Django realice una conversión «implícita» al tipo de dato correspondiente.

La sintaxis de un conversor de ruta es la siguiente: path(<param:converter>, ...)

Conversores predefinidos

Veamos una tabla resumen con los conversores de rutas predefinidos en Django:

Conversor Ejemplo Explicación
path('<username>', ...) /guido/ Equivalente al conversor str
path('<str:query>', ...) /django+python+dev/ Casa con cualquier cadena de caracteres excluyendo el separador / y retorna un str
path('<int:post_id>', ...) /4673/ Casa con 0 o un entero positivo y retorna int
path('<slug:product_slug>', ...) /display-23-inches/ Casa con un «slug» y retorna str
path('<uuid:token>', ...) /075194d3-6885-417e-a8a8-6c931e272f00/ Casa con cualquier UUID y retorna un objeto UUID
path('<path:resource_path>', ...) /products/tech/logitech-keyboard/ Casa con cualquier cadena de caracteres incluyendo el separador / y retorna un str

Conversores personalizados

Django intermedio

Django permite crear registrar conversores de rutas personalizados de tal forma que obtenemos un objeto del tipo (clase) deseado directamente en la vista.

Y además es muy fácil de implementar. Lo único que necesitamos es escribir una clase con dos métodos concretos y registrarla convenientemente.

Veamos un ejemplo donde creamos un conversor personalizado para un «post» de un «blog» a partir de su slug:

posts/converters.py
from django.shortcuts import get_object_or_404

from .models import Post#(1)!


class PostConverter:#(2)!
    regex = r'[\w-]+'#(3)!

    def to_python(self, post_slug: str) -> Post:#(4)!
        return get_object_or_404(Post, slug=post_slug)#(5)!

    def to_url(self, post: Post) -> str:#(6)!
        return post.slug

  1. Importamos el modelo sobre el que vamos a trabajar.
  2. Aunque sólo es una convención, si el modelo es Model llamamos ModelConverter al conversor.
  3. Hay que especificar la expresión regular que captura el patrón en la URL.
    • Este método convierte el patrón capturado str en el objeto correspondiente.
    • URL Python
  4. Ver consulta no encontrada.
    • Este método convierte el objeto a la subruta correspondiente de la URL.
    • Python URL

posts/urls.py
from django.urls import path, register_converter#(1)!

from . import converters, views#(2)!


app_name = 'posts'
register_converter(converters.PostConverter, 'post')#(3)!

urlpatterns = [
    path('', views.post_list, name='post-list')
    path('<post:post>/', views.post_detail, name='post-detail')#(4)!
]

  1. Necesitamos importar la función register_converter() para registrar el conversor.
  2. Necesitamos importar el módulo converters de conversores personalizados.
  3. Registramos el conversor asignándole un identificador que utilizaremos en la ruta.
  4. Indicamos que estamos capturando un objeto de tipo «post» con el conversor previamente registrado.

posts/views.py
from django.shortcuts import render

from .models import Post


def post_detail(request, post: Post):#(1)!
    return render(request, 'posts/post/detail.html', {'post': post})

  1. Directamente la vista está recibiendo un objeto de modelo Post.

posts/post/detail.html
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>

<a href="{% url 'posts:delete-post' post %}">Delete post</a><!--(1)!-->

  1. Ahora ya no pasamos el «slug» del «post», sino directamente un objeto de tipo Post que Django sabrá como convertir a URL.

Redirección

Se considera una mala práctica «hardcodear»1 las URLs directamente (tanto en vistas como en plantillas) ya que, ante un determinado cambio de una URL en el futuro, tendremos que localizar todas las ocurrencias de dicha URL en el código y modificarlas.

Para resolver esta problemática, Django nos «anima» a utilizar nombres de URLs en la función path() dentro de los distintos ficheros urls.py.

En el ejemplo anterior del «blog» se han definido las siguientes URLs:

posts/urls.py
# ...
app_name = 'posts'

urlpatterns = [
    path('', views.post_list, name='post-list'),
    path('<slug:post_slug>/', views.post_detail, name='post-detail'),
]

Así las cosas, Django nos permite identificar cada URL mediante <app_name>:<url_name>:

  • 'posts:post-list' identifica la URL del listado de «posts».
  • 'posts:post-detail' identifica la URL del detalle de un «post».

En una vista usaremos la función redirect() para hacer redirecciones y pasar el control a otra URL:

from django.shortcuts import redirect    


def my_view(request):
    # ...
    return redirect('posts:post-list')
from django.shortcuts import redirect    


def my_view(request):
    # ...
    return redirect('posts:post-detail', post_slug=post.slug)
Redirección permanente

Django aplica por defecto una redirección temporal 302. Si lo que se quiere es realizar una redirección permanente 301 habrá que usar el argumento permanent=True en la función redirect().

Como era esperable, también podremos redirigir a cualquier otra URL externa que queramos: redirect('https://python.org').

URL desde nombre

Django intermedio

Hay ocasiones en las que nos interesa obtener una URL a partir de su nombre (alias). Para ello, Django proporciona la función reverse().

Continuando con el ejemplo previo del «blog», supongamos que queremos obtener el nombre de ciertas URLs. El uso de la función reverse() depende de si la URL tiene o no parámetros:

>>> from django.urls import reverse

>>> reverse('posts:post-list')
'/posts/'

>>> from django.urls import reverse

>>> reverse('posts:post-detail', args=['test'])#(1)!
'/posts/test/'

>>> reverse('posts:post-detail', kwargs={'post_slug': 'test'})#(2)!
'/posts/test/'

  1. Aproximación usando parámetros posicionales.
  2. Aproximación usando parámetros nominales.

Accesos directos en primer nivel

Django intermedio

En las URLs de primer nivel podemos ir más allá del típico «include». En este sentido se abren varias posibilidades:

1⃣ Apuntar a vistas.
2⃣ Redireccionar a URLs.
3⃣ Renderizar plantillas.

Apuntar a vistas

Es posible que queramos «apuntar» una determinada URL en main/urls.py a una cierta vista de una aplicación concreta.

Supongamos por ejemplo que disponemos de una vista de contacto (información acerca de la web) en la aplicación shared y que queremos apuntar directamente a dicha vista desde las URLs de primer nivel:

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

import shared.views#(1)!

urlpatterns = [
    path('admin/', admin.site.urls),
    path('posts/', include('posts.urls'))
    path('contact/', shared.views.contact, name='contact')#(2)!
]

  1. En main/urls.py se recomienda importar las vistas de esta forma para evitar «colisiones» con otros espacios de nombres.
  2. Se apunta a la vista de «contact» de la aplicación shared.

Caso de uso

Este enfoque es más adecuado cuando la plantilla a renderizar requiere de datos que sean procesados desde la vista.

Redireccionar a URLs

Es posible que queramos redireccionar una determinada URL en main/urls.py a otra URL (habitualmente a través de su nombre).

Supongamos por ejemplo un escenario en el que queremos redirigir la URL raíz de nuestro blog / al listado de «posts» que hay en la plataforma:

main/urls.py
from django.contrib import admin
from django.shortcuts import redirect
from django.urls import path, include


urlpatterns = [
    path('', lambda _: redirect('posts:post-list'), name='index')#(1)!
    path('admin/', admin.site.urls),
    path('posts/', include('posts.urls'))
]

    • Simulamos una vista mediante una función lambda.
    • Como no usamos el supuesto parámetro request escribimos _ como primer argumento.
    • Poner la redirección «lambda» en primer lugar es una buena práctica para visualizar más claramente las URLs.

Caso de uso

Este enfoque es más adecuado cuando queramos que la URL (del navegador) cambie en la propia redirección y pase el control a otra vista.

Renderizar plantillas

Es posible que queramos renderizar una plantilla directamente desde main/urls.py.

En un ejemplo donde tengamos una plantilla «estática» que no dependa del contexto, podemos aplicar esta técnica de manera sencilla. Supongamos que queremos renderizar una página de índice:

main/urls.py
from django.contrib import admin
from django.shortcuts import render
from django.urls import path, include


urlpatterns = [
    path('', lambda r: render(r, 'index.html'), name='index')#(1)!
    path('admin/', admin.site.urls),
    path('posts/', include('posts.urls'))
]

    • Simulamos una vista mediante una función lambda.
    • Necesitamos el parámetro request por eso escribimos r como primer argumento.
    • Poner la redirección «lambda» en primer lugar es una buena práctica para visualizar más claramente las URLs.

Caso de uso

Este enfoque es más adecuado cuando la plantilla a renderizar no requiere de datos que sean procesados desde la vista.

Pasar argumentos a una vista

Django intermedio

Hay ocasiones en las que interesa pasar argumentos a una vista desde la propia URL.

Supongamos un ejemplo donde cambiamos el estado de publicación de un determinado «post» en un «blog»:

posts/urls.py
from django.urls import path

from . import views


urlpatterns = [
    path(
        '<slug:post_slug>/status/private/',
        views.change_post_status,
        name='change-post-status',
        kwargs={'post_private': True},#(1)!
    ),
    path(
        '<slug:post_slug>/status/public/',
        views.change_post_status,
        name='change-post-status',
        kwargs={'post_private': False},#(2)!
    ),
]

  1. Pasamos los argumentos que recibe la vista como nominales.
  2. Pasamos los argumentos que recibe la vista como nominales.

posts/views.py
from django.http import HttpResponse

from .models import Post


def change_post_status(request, post_slug: str, post_private: bool):#(1)!
    try:
        post = Post.objects.get(slug=post_slug)
        post.private = post_private
        post.save()
    except Post.DoesNotExist:
        return HttpResponse('Post does not exist')

  1. El parámetro post_private vendrá establecido desde posts/urls.py.

Expresiones regulares

Django avanzado

A la hora de definir los patrones en las URLs, Django nos permite utilizar expresiones regulares. Es una técnica muy potente ya que permite ir más allá de los formatos «básicos» y definir reglas más específicas.

En este escenario, en vez de utilizar la función path() usaremos la función re_path() que, como su propio nombre indica, nos permite definir rutas (URLs) mediante expresiones regulares (re).

Planteamos un ejemplo en el que queremos mostrar los «posts» de un «blog» con una determinada categoría, pero con el matiz de que el código de categoría es un «string» de 3 letras mayúsculas:

from django.db import models


class Post(models.Model):
    DEFAULT_CATEGORY = 'GEN'

    title = models.CharField(max_length=256)
    slug = models.SlugField(max_length=256)
    content = models.TextField()
    category = models.CharField(max_length=3, default=DEFAULT_CATEGORY)

    def __str__(self):
        return self.title

from django.urls import re_path#(1)!

from . import views

app_name = 'posts'


urlpatterns = [
    re_path(#(2)!
        r'^(?P<category_code>[A-Z]{3})/$',#(3)!
        views.post_by_category,
        name='post-by-category',
    )
]

  1. Importamos la función.
  2. Utilizamos la función como el resto de patrones.
    • Es conveniente usar cadenas en crudo para las expresiones regulares.
    • También es recomendable empezar la cadena con ^ (comienzo de línea) y acabarla con $ (final de línea) para delimitar el patrón.
    • Se utiliza un grupo de captura nominal (?P<name>) para el parámetro correspondiente.
    • La expresión regular viene a continuación. En este caso [A-Z]{4} indica cuatro apariciones de cualquier letra en mayúsculas.
from django.shortcuts import render

from .models import Post


def post_by_category(request, category_code: str):
    posts = Post.objects.filter(category=category_code)
    return render(request, 'posts/post/list.html', {'posts': posts})
Mezclando patrones

Cuando usamos re_path() tenemos que utilizar expresiones regulares en toda la URL. No es posible mezclar patrones «convencionales» con patrones expresión regular.


  1. «Hardcodear» significa escribir literales/valores directamente en el código.