El libro de Django 1.0

3.6. Tu segunda Vista: URLs dinámicas

En la primer vista de ejemplo, el contenido de la página — la fecha/hora actual — eran dinámicas, pero la URL (/time) era estática. En la mayoría de las aplicaciones Web, sin embargo, la URL contiene parámetros que influyen en la salida de la página.

Vamos a crear una segunda vista que nos muestre la fecha y hora actual con un adelanto de ciertas horas. El objetivo es montar un sitio en la que la página /time/plus/1/ muestre la fecha/hora una hora más adelantada, la página /time/plus/2/ muestre la fecha/hora dos horas más adelantada, la página /time/plus/3/ muestre la fecha/hora tres horas más adelantada, y así.

A un novato se le ocurriría escribir una función de vista distinta para cada adelanto de horas, lo que resultaría una URLconf como esta:

urlpatterns = patterns('',
    (r'^time/$', current_datetime),
    (r'^time/plus/1/$', one_hour_ahead),
    (r'^time/plus/2/$', two_hours_ahead),
    (r'^time/plus/3/$', three_hours_ahead),
    (r'^time/plus/4/$', four_hours_ahead),
)

Claramente, esta línea de pensamiento es incorrecta. No sólo porque producirá redundancia entre las funciones de vista, sino también la aplicación estará limitada a admitir sólo el rango horario definido — uno, dos, tres o cuatro horas. Si, de repente, quisiéramos crear una página que mostrara la hora cinco horas adelantada, tendríamos que crear una vista distinta y una línea URLconf, perpetuando la duplicación y la demencia. Aquí necesitamos algo de abstracción.

3.6.1. Algunas palabras acerca de las URLs bonitas

Si tienes experiencia en otra plataforma de diseño Web, como PHP o Java, es posible que estés pensado, "¡Oye, usemos un parámetro cadena de consulta!", algo como /time/plus?hours=3, en la cual la hora será designada por el parámetro hours de la cadena de consulta de la URL (la parte a continuación de ?).

Con Django puedes hacer eso (pero te diremos cómo más adelante, si es que realmente quieres saberlo), pero una de las filosofías del núcleo de Django es que las URLs deben ser bonitas. La URL /time/plus/3 es mucho más limpia, más simple, más legible, más fácil de dictarse a alguien y ... justamente más bonita que su homóloga forma de cadena de consulta. Las URLs bonitas son un signo de calidad en las aplicaciones Web.

El sistema de URLconf que usa Django estimula a generar URLs bonitas, haciendo más fácil el usarlas que el no usarlas.

3.6.2. Comodines en los patrones URL

Continuando con nuestro ejemplo hours_ahead, pongámosle un comodín al patrón URL. Como ya se mencionó antes, un patrón URL es una expresión regular; de aquí, es que usamos el patrón de expresión regular \d+ para que coincida con uno o más dígitos:

from django.conf.urls.defaults import *
from mysite.views import current_datetime, hours_ahead

urlpatterns = patterns('',
    (r'^time/$', current_datetime),
    (r'^time/plus/\d+/$', hours_ahead),
)

Este patrón coincidirá con cualquier URL que sea como /time/plus/2/, /time/plus/25/, o también /time/plus/100000000000/. Ahora que lo pienso, podemos limitar el lapso máximo de horas en 99. Eso significa que queremos tener números de uno o dos dígitos en la sintaxis de las expresiones regulares, con lo que nos quedaría así \d{1,2}:

(r'^time/plus/\d{1,2}/$', hours_ahead),

Nota Cuando construimos aplicaciones Web, siempre es importante considerar el caso más descabellado posible de entrada, y decidir si la aplicación admitirá o no esa entrada. Aquí hemos limitado a los exagerados reconociendo lapsos de hasta 99 horas. Y, por cierto, Los Limitadores exagerados, aunque largo, sería un nombre fantástico para una banda musical.

Ahora designaremos el comodín para la URL, necesitamos una forma de pasar esa información a la función de vista, así podremos usar una sola función de vista para cualquier adelanto de hora. Lo haremos colocando paréntesis alrededor de los datos en el patrón URL que querramos guardar. En el caso del ejemplo, queremos guardar cualquier número que se anotará en la URL, entonces pongamos paréntesis alrededor de \d{1,2}:

(r'^time/plus/(\d{1,2})/$', hours_ahead),

Si estás familiarizado con las expresiones regulares, te sentirás como en casa aquí; estamos usando paréntesis para capturar los datos del texto que coincide.

La URLconf final, incluyendo la vista anterior current_datetime, nos quedará algo así:

from django.conf.urls.defaults import *
from mysite.views import current_datetime, hours_ahead

urlpatterns = patterns('',
    (r'^time/$', current_datetime),
    (r'^time/plus/(\d{1,2})/$', hours_ahead),
)

Con cuidado, vamos a escribir la vista hours_ahead.

hours_ahead es muy similar a current_datetime, vista que escribimos antes, sólo que con una diferencia: tomará un argumento extra, el número de horas que debemos adelantar. Agrega al archivo views.py lo siguiente:

import django.http.HttpResponse
import datetime

def hours_ahead(request, offset):
    offset = int(offset)
    dt = datetime.datetime.now() + datetime.timedelta(hours=offset)
    html = "<html><body>In %s hour(s), it will be %s.</body></html>" % (offset, dt)
    return HttpResponse(html)

Repasemos el código anterior línea a línea:

  • Tal como hicimos en la vista current_datetime, importamos la clase django.http.HttpResponse y el módulo datetime.
  • La función de vista hours_ahead, toma dos parámetros: request y offset.
  • request es un objeto HttpRequest, al igual que en current_datetime. Lo diremos nuevamente: cada vista siempre toma un objeto HttpRequest como primer parámetro.
  • offset es la cadena de caracteres capturada por los paréntesis en el patrón URL. Por ejemplo, si la petición URL fuera /time/plus/3/, entonces el offset debería ser la cadena de caracteres '3'. Si la petición URL fuera /time/plus/21/, entonces el offset debería ser la cadena de caracteres '21'. Notar que la cadena de caracteres capturada siempre es una cadena de caracteres, no un entero, incluso si se compone sólo de dígitos, como en el caso '21'.
Decidimos llamar a la variable `offset`, pero puedes asignarle el
nombre que quieras, siempre que sea un identificador válido para
Python. El nombre de la variable no importa; todo lo que importa es lo
que contiene el segundo parámetro de la función (luego de `request`).
Es posible también usar untienes que hacer esto. No es una buena idea 
poner cualquier código Python en la carpeta  raíz del servia palabra 
clave, en lugar de posición, como argumentos en la URLconf. Eso lo veremos 
en detalle en el Capítulo 8.
  • Lo primero que hacemos en la función es llamar a int() sobre offset. Esto convierte el valor de la cadena de caracteres a entero. Tener en cuenta que Python lanzará una excepción ValueError si se llama a la función int() con un valor que no puede convertirse a un entero, como lo sería la cadena de caracteres 'foo'. Sin embargo, en este ejemplo no debemos preocuparnos de atrapar la excepción, porque tenemos la certeza que la variable offset será una cadena de caracteres conformada sólo por dígitos. Sabemos esto, por el patrón URL de la expresión regular en el URLconf — (\d{1,2})— captura sólo dígitos. Esto ilustra otra ventaja de tener un URLconf: nos provee un primer nivel de validación de entrada.
  • La siguiente línea de la función muestra la razón por la que se llamó a la función int() con offset. En esta línea, calculamos la hora actual más las hora que tiene offset, almacenando el resultado en la variable dt. La función datetime.timedelta requiere que el parámetro hours sea un entero.
  • A continuación, construimos la salida HTML de esta función de vista, tal como lo hicimos en la vista current_datetime. Una pequeña diferencia en esta línea, es que usamos el formato de cadenas de Python con dos valores, no sólo uno. Por lo tanto, hay dos símbolos %s en la cadena de caracteres y la tupla de valores a insertar sería: (offset, dt).
  • Finalmente, retornamos el HttpResponse del HTML — de nuevo, tal como hicimos en la vista current_datetime.

Con esta función de vista y la URLconf escrita, ejecuta el servidor de desarrollo de Django (si no está corriendo), y visita http://127.0.0.1:8000/time/plus/3/ para verificar que lo que hicimos funciona. Luego prueba http://127.0.0.1:8000/time/plus/5/. Para terminar visita http://127.0.0.1:8000/time/plus/100/ para verificar que el patrón en la URLconf sólo acepta número de uno o dos dígitos, Django debería mostrar un error en este caso como "Page not found", tal como vimos anteriorment en la sección "Errores 404". La URL http://127.0.0.1:8000/time/plus/ (sin horas designadas) debería también mostrar un error 404.

Si estás siguiendo el libro y programando al mismo tiempo, notarás que el archivo views.py ahora contiene dos vistas. (Omitimos la vista current_datetime del ejemplo anterior sólo por claridad). Poniéndolas juntas, veríamos algo similar a esto:

from django.http import HttpResponse
import datetime

def current_datetime(request):
    now = datetime.datetime.now()
    html = "<html><body>It is now %s.</body></html>" % now
    return HttpResponse(html)

def hours_ahead(request, offset):
    offset = int(offset)
    dt = datetime.datetime.now() + datetime.timedelta(hours=offset)
    html = "<html><body>In %s hour(s), it will be %s.</body></html>" % (offset, dt)
    return HttpResponse(html)