Escribiendo tu primera Django app, parte 3

Este tutorial empieza donde quedó el Tutorial 2. Continuamos con la aplicación web de preguntas y nos concentraremos en la creación de la interfaz pública – “views”.

Filosofía

Una view es un “tipo” de página web en nuestra aplicación Django que generalmente provee una función específica y tiene un template particular. Por ejemplo, en una aplicación de blog, uno podría tener las siguientes views:

  • Blog homepage – muestra las últimas entradas.

  • Detalle de una entrada – página correspondiente a una entrada o post.

  • Archivo anual – muestra los meses de una año con sus correspondientes entradas.

  • Archivo mensual – muestra los días de un mes con las correspondientes entradas.

  • Archivo diario – muestra todas las entradas en un día dado.

  • Acción de comentar – maneja el posteo de comentarios en una entrada dada.

En nuestra aplicación de preguntas, vamos a tener las siguientes cuatro views:

  • Página índice – muestra las últimas preguntas.

  • Detalle de pregunta – muestra el texto de la pregunta, sin resultados, junto con un form para votar.

  • Página de resultados – muestra los resultados para una pregunta particular.

  • Acción de votar – maneja el voto por una opción particular en una pregunta dada.

En Django, las páginas web y otro contenido se manejan mediante views. Cada view se representa por una simple función Python (o método, en el caso de views basadas en clases). Django elige una view examinando la URL que se pidió (para ser precisos, la parte de la URL después del nombre de dominio).

En la web puede que te hayas encontrado con cosas como “ME2/Sites/dirmod.asp?sid=&type=gen&mod=Core+Pages&gid=A6CD4967199A42D9B65B1B”. Estarás encantado de saber que Django permite definir patrones de URL mucho más elegantes.

Un patrón de URL es simplemente la forma genérica de una URL - por ejemplo: /newsarchive/<year>/<month>/.

Para llegar de una URL a una view, Django usa lo que se conoce como ‘URLconfs’. Un URLconf mapea patrones de URL (descriptos como expresiones regulares) a views.

Este tutorial provee las instrucciones básicas para el uso de URLconfs, para más información referirse a django.core.urlresolvers.

Escribiendo nuestra primera view

Escribamos nuestra primera view. Abrimos el archivo polls/views.py y ponemos el siguiente código Python:

polls/views.py
from django.http import HttpResponse


def index(request):
    return HttpResponse("Hello, world. You're at the polls index.")

Esta es la view más simple posible en Django. Para llamar a esta view, necesitamos mapearla a una URL - y para ello necesitamos un URLconf.

Para crear un URLconf, en el directorio polls creamos un archivo llamado urls.py. La estructura de directorios de nuestra app ahora debería verse así:

polls/
    __init__.py
    admin.py
    models.py
    tests.py
    urls.py
    views.py

En el archivo polls/urls.py incluimos el siguiente código:

polls/urls.py
from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^$', views.index, name='index'),
]

El próximo paso es apuntar el URLconf raíz al módulo polls.urls. En mysite/urls.py insertamos un include(), con lo que nos quedaría:

mysite/urls.py
from django.conf.urls import include, url
from django.contrib import admin

urlpatterns = [
    url(r'^polls/', include('polls.urls')),
    url(r'^admin/', include(admin.site.urls)),
]

No coincide con lo que ves?

Si estás viendo admin.autodiscover() antes de la definición de urlpatterns, probablemente estás usando una versión de Django que no coincide con la de este tutorial. Deberías cambiar a una versión anterior del tutorial o a una versión más nueva de Django.

Hemos conectado una view index en el URLconf. Si vamos a la dirección http://localhost:8000/polls/ en nuestro browser, deberíamos ver el texto “Hello, world. You’re at the polls index.”, que definimos la view index.

La función url() toma cuatro argumentos, dos requeridos: regex y view, y dos opcionales: kwargs, y name. En este punto vale la pena revisar para que es cada uno de ellos.

url(): argumento regex

El término “regex” se usa comúnmente como abreviatura para expresión regular (regular expression, en inglés), que es una forma de describir patrones que se verifiquen en un string, o en este caso patrones de url. Django comienza en la primera expresión regular y va recorriendo la lista hacia abajo, comparando la URL solicitada contra cada expresión regular hasta encontrar una cuyo patrón coincida.

Notar que estas expresiones regulares no chequean parámetros de GET o POST, o el nombre de dominio. Por ejemplo, en un pedido por http://www.example.com/myapp/, el URLconf buscará por myapp/. En un pedido por http://www.example.com/myapp/?page=3, el URLconf también buscará por myapp/.

Si necesitaras ayuda con las expresiones regulares, podés ver Wikipedia y la documentación del módulo re. También es fantástico, el libro de O’Reilly “Mastering Regular Expressions”, de Jeffrey Friedl. En la práctica, sin embargo, no hace falta ser un experto en expresiones regulares, ya que lo que realmente es necesario saber es cómo capturar patrones simples. De hecho, expresiones regulares complejas pueden afectar la performance de búsqueda, por lo que uno no debería confiar en todo el poder de las expresiones regulares.

Finalmente, una nota sobre performance: estas expresiones regulares se compilan la primera vez que el módulo URLconf se carga. Son súper rápidas (mientras que no sean complejas como se menciona arriba).

url(): argumento view

Cuando Django encuentra una expresión regular que coincide, se llama a la función view especificada, con un objeto HttpRequest como primer argumento y los valores que se hayan “capturado” a partir de la expresión regular como argumentos restantes. Si la regex usa capturas simples (sin nombre), los valores se pasan como argumentos posicionales; si se usan capturas con nombre, los valores se pasan como argumentos nombrados. Veremos un ejemplo en breve.

url(): argumento kwargs

Se pueden pasar argumentos arbitrarios en un diccionario a la view de destino. No vamos a usar esta opción en el tutorial.

url(): argumento name

Nombrar las URL nos permite referirnos a ellas de forma unívoca desde distintas partes del código, especialmente en los templates. Esta característica nos permite hacer cambios globales a los patrones de url del proyecto cambiando tan sólo un archivo (ya que el nombre permanece sin cambios, y nos referimos a una URL por su nombre).

Escribiendo más views

Ahora agreguemos algunas views más a polls/views.py. Estas views van a ser un poco diferentes porque van a tomar un argumento:

polls/views.py
def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)

def results(request, question_id):
    response = "You're looking at the results of question %s."
    return HttpResponse(response % question_id)

def vote(request, question_id):
    return HttpResponse("You're voting on question %s." % question_id)

Conectemos estas nuevas views en el módulo polls.urls agregando las siguientes llamadas a url():

polls/urls.py
from django.conf.urls import url

from . import views

urlpatterns = [
    # ex: /polls/
    url(r'^$', views.index, name='index'),
    # ex: /polls/5/
    url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
    # ex: /polls/5/results/
    url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'),
    # ex: /polls/5/vote/
    url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]

Veamos en nuestro browser “/polls/34/”. Se ejecutará la función detail() y nos mostrará el ID que pasamos en la URL. Probemos también “/polls/34/results/” y “/polls/34/vote/” – esto nos debería mostrar los placeholder que definimos para las páginas de resultados y para votar.

Cuando alguien pide por una página de nuestro sitio – supongamos “/polls/34/”, Django va a cargar el módulo mysite.urls, al que apunta el setting ROOT_URLCONF. Encuentra la variable urlpatterns y recorre las expresiones regulares en orden. Las llamadas a include() simplemente referencian otros URLconf. Notar que las expresiones regulares para los include() no tienen un $ (caracter que indica el fin de string en un patrón), sino que terminan en una barra. Cada vez que Django encuentra un include(), recorta la parte de la URL que coincide hasta ese punto y envía el string restante al URLconf relacionado para continuar el proceso.

La idea detrás de include() es hacer fácil tener URLs plug-and-play. Como polls tiene su propio URLconf (polls/urls.py), las URLs de la app se pueden poner bajo “/polls/”, o bajo “/fun_polls/”, o bajo “/content/polls/”, o cualquier otro camino, y la app seguirá funcionando.

Esto es lo que pasa si un usuario va a “/polls/34/” en este sistema:

  • Django encontrará coincidencia en '^polls/'

  • Entonces, Django va a recortar el texto que coincide ("polls/") y enviar el texto restante – "34/" – al URLconf ‘polls.urls’ para seguir el proceso, donde coincidirá con r'^(?P<question_id>[0-9]+)/$', resultando en una llamada a la view detail() de la forma:

    detail(request=<HttpRequest object>, question_id='34')
    

La parte question_id='34' surge de (?P<question_id>[0-9]+). Usando paréntesis alrededor de un patrón se “captura” el texto que coincida con el patrón y ese valor se pasas como argumento a la función view; ?P<question_id> define el nombre que se usará para identificar la coincidencia; y [0-9]+ es una expresión regular para buscar una secuencia de dígitos (i.e., un número).

Como los patrones de URL son expresiones regulares, no hay realmente un límite de lo que se puede hacer con ellos. Y no hay necesidad de agregar cosas como .html – a menos que uno quisiera, en cuyo caso nos quedaría algo como:

url(r'^polls/latest\.html$', views.index),

Pero no hagan esto. No tiene sentido.

Escribiendo views que hacen algo

Cada view es responsable de hacer una de dos cosas: devolver un objeto HttpResponse con el contenido de la página solicitada, o levantar una excepción, por ejemplo Http404. El resto depende de uno.

Una view puede leer registros de una base de datos, o no. Puede usar un sistema de templates como el de Django – o algún otro basado en Python –, o no. Puede generar un archivo PDF, una salida XML, crear un archivo ZIP, cualquier cosa que uno quiera, usando cualquier librería Python que uno quiera.

Todo lo que Django espera es un HttpResponse. O una excepción.

Por ser conveniente, vamos a usar la API de Django para base de datos, que vimos en el Tutorial 1. Aquí tenemos una aproximación a la view index() que muestra las 5 preguntas más recientes en el sistema, separadas por comas, de acuerdo a la fecha de publicación:

polls/views.py
from django.http import HttpResponse

from .models import Question


def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    output = ', '.join([p.question_text for p in latest_question_list])
    return HttpResponse(output)

# Leave the rest of the views (detail, results, vote) unchanged

Pero tenemos un problema: el diseño de la página está escrito explícitamente en la view. Si uno quisiera cambiar cómo se ve la página, debería editar el código Python. Entonces vamos a usar el sistema de templates de Django para separar el diseño del código Python, creando un template que la view pueda usar.

Primero, creamos un directorio llamado templates en nuestro directorio polls. Django va a buscar los templates allí.

El setting TEMPLATES de nuestro proyecto describe cómo Django va a cargar y renderizar templates. Por defecto se configura como backend DjangoTemplates, con la opción APP_DIRS en True. Por convención DjangoTemplates busca por un subdirectorio “templates” en cada una de las aplicaciones en INSTALLED_APPS. Esta es la manera en que Django sabe encontrar los templates de nuestra aplicación polls sin tener que modificar la opción DIRS, como hicimos en Tutorial 2.

Organizando los templates

Podríamos tener todos nuestros templates juntos, en un gran directorio templates y funcionaría perfectamente bien. Sin embargo, este template pertenece a la aplicación polls entonces, a diferencia del template del admin que creamos en la parte anterior, vamos a poner este en el directorio de templates de la aplicación (polls/templates) en lugar de aquel del proyecto (templates). Discutiremos más en detalle en el tutorial de apps reusables el por qué de esto.

En el directorio templates que acabamos de crear, creamos otro directorio llamado polls, y allí creamos un archivo index.html. En otras palabras, nuestro template debería estar en polls/templates/polls/index.html. Como la carga de templates basada en app_directories funciona como se describió arriba, nos podemos referir a este template en Django simplemente como polls/index.html.

Espacio de nombre en templates

Ahora podríamos continuar poniendo nuestros templates directamente en polls/templates (en lugar de crear otro subdirectorio polls), pero no sería una buena idea. Django elegirá el primer template que encuentre tal que el nombre coincida, y si tenemos un template con el mismo nombre en una aplicación diferente, Django no podrá distinguir entre ellos. Necesitamos apuntar a Django al correcto, y la forma más fácil de asegurarse de esto es usando espacio de nombres para ellos. Esto es, poner los templates dentro de otro directorio llamado como la aplicación correspondiente.

Pongamos el siguiente código en ese template:

polls/templates/polls/index.html
{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

Ahora actualicemos nuestra view index en polls/views.py para usar el template:

polls/views.py
from django.http import HttpResponse
from django.template import RequestContext, loader

from .models import Question


def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    template = loader.get_template('polls/index.html')
    context = RequestContext(request, {
        'latest_question_list': latest_question_list,
    })
    return HttpResponse(template.render(context))

Ese código carga el template llamado polls/index.html y le pasa un contexto. El contexto es un diccionario que mapea nombres de variable a objetos Python.

Carguemos la página apuntando el browser a “/polls/”, y deberíamos ver una lista conteniendo la pregunta “What’s up” del Tutorial 1. El link apunta a la página de detall de la pregunta.

Un atajo: render()

La acción de cargar un template, armar un contexto y devolver un objeto HttpResponse con el resultado de renderizar el template es muy común. Django provee un atajo. Aquí está la view index() reescrita:

polls/views.py
from django.shortcuts import render

from .models import Question


def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    context = {'latest_question_list': latest_question_list}
    return render(request, 'polls/index.html', context)

Una vez que hayamos hecho esto para todas estas views, ya no necesitamos importar loader, RequestContext y HttpResponse (habrá que mantener HttpResponse si es que todavía tenemos métodos stub para detail, results y vote).

La función render() toma un objeto request como primer un argumento, un nombre de template como segundo argumento y un diccionario como tercer argumento opcional. Devuelve un objeto HttpResponse con el template renderizado con el contexto dado.

Levantando un error 404

Ahora veamos la view de detalle de una pregunta – la página que muestra el texto de la pregunta. Aquí está la view:

polls/views.py
from django.http import Http404
from django.shortcuts import render

from .models import Question
# ...
def detail(request, question_id):
    try:
        question = Question.objects.get(pk=question_id)
    except Question.DoesNotExist:
        raise Http404("Question does not exist")
    return render(request, 'polls/detail.html', {'question': question})

El nuevo concepto aquí: la view levanta una excepción Http404 si no existe una pregunta con el ID dado.

Veremos qué podríamos poner en el template polls/detail.html luego, pero si quisiéramos tener el ejemplo arriba funcionando rápidamente, esto alcanza para empezar:

polls/templates/polls/detail.html
{{ question }}

para ponernos en marcha por ahora.

Un atajo: get_object_or_404()

Es muy común usar el método get() y levantar un Http404 si el objeto no existe. Django provee un atajo. Esta la view detail(), actualizada:

polls/views.py
from django.shortcuts import get_object_or_404, render

from .models import Question
# ...
def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})

La función get_object_or_404() toma un modelo Django como primer argumento y un número arbitrario de argumentos nombrados, que se pasan a la función get() del manager del modelo. Levanta un Http404 si el objeto no existe.

Filosofía

Por qué usamos una función get_object_or_404() en lugar de manejar automáticamente la excepción ObjectDoesNotExist a un nivel más arriba, o hacer que la API a nivel modelo levante Http404 en lugar de ObjectDoesNotExist?

Porque esto acoplaría la capa de modelos a la capa de views. Uno de los objetivos de diseño de Django es mantener bajo acoplamiento. Cierto acoplamiento controlado se introduce en el módulo django.shortcuts.

Existe también una función get_list_or_404(), que funciona como get_object_or_404() – excepto que usa filter() en lugar de get(). Levanta un Http404 si la lista es vacía.

Usando el sistema de templates

Volvamos a la view detail() de nuestra aplicación. Dada la variable de contexto question, veamos como podría lucir el template polls/detail.html:

polls/templates/polls/detail.html
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>

El sistema de templates usa sintaxis de punto para acceder a los atributos de variable. En el ejemplo de {{ question.question_text }}, Django primero hace una búsqueda de diccionario sobre el objeto question. Si eso falla, intenta una búsqueda de atributo – que en este caso, funciona. Si hubiera fallado, se hubiera intentado una búsqueda por índice de lista.

Una llamada de método se da en el loop {% for %}: question.choice_set.all se interpreta como el código Python question.choice_set.all(), que devuelve un iterable de objetos Choice y usable para el tag {% for %}.

Para más detalles sobre templates, se puede ver template guide.

Borrando URLs escritas explícitamente en templates

Recordemos que cuando escribimos el link a una encuesta en el template polls/index.html, el link estaba parcialmente escrito “a mano”:

<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>

El problema con esto, es que es una aproximación muy acoplada que hace que sea un desafío cambiar las URLs en un proyecto con muchos templates. Sin embargo, como definimos el argumento name en las llamadas a url() en el módulo polls.urls, podemos eliminar la dependencia de URLs fijas usando el template tag {% url %}:

<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

La forma en que esto funciona es buscando la definición de la URL especificada en el módulo polls.urls. Podemos ver dónde se define el nombre ‘detail’ de la URL aquí:

...
# the 'name' value as called by the {% url %} template tag
url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
...

Si uno quisiera cambiar la URL de la view de detalle de pregunta a algo distinto, tal vez algo como polls/specifics/12/, en lugar de hacerlo en el template (o templates), bastaría con cambiarlo en polls/urls.py:

...
# added the word 'specifics'
url(r'^specifics/(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
...

Espacio de nombres en URLs

El proyecto de este tutorial tiene sólo una app, polls. En proyectos Django reales, podría haber cinco, diez, veinte o más apps. Cómo Django distingue los nombres de las URLs entre todas las apps? Por ejemplo, la app polls tiene una view detail, y podría ser que otra app para un blog en el mismo proyecto también. Cómo hace Django para saber la view de qué app usar al resolver un template tag {% url %}?

La respuesta es agregar espacios de nombres al URLconf raíz. Cambiamos el archivo mysite/urls.py para incluir espacios de nombres:

mysite/urls.py
from django.conf.urls import include, url
from django.contrib import admin

urlpatterns = [
    url(r'^polls/', include('polls.urls', namespace="polls")),
    url(r'^admin/', include(admin.site.urls)),
]

Ahora cambiamos el template polls/index.html de:

polls/templates/polls/index.html
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

a que apunte a la view de detalle con el espacio de nombres:

polls/templates/polls/index.html
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>

Una vez que estés cómodo escribiendo views, podés pasar a la parte 4 del tutorial para aprender sobre procesamiento simple de forms y views genéricas.