Django

Vistas

Vistas

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. 

Vistas

Por ejemplo, en una aplicación de blog, uno podría tener las siguientes vistas:

  • 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.

Vistas

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

  • Página inicial – muestra las últimas encuestas.
  • Detalle de encuesta – muestra la pregunta de la encuesta, sin resultados, junto con un form para votar.
  • Página de resultados – muestra los resultados para una encuesta particular.
  • Acción de votar – maneja el voto por una opción particular en una encuesta dada.

En Django, cada view se representa mediante una simple función Python (o método, en el caso de las vistas basadas en clases). Django elegirá la vista analizando la url solicitada

URLs

URLs del tipo “ME2/Sites/dirmod.asp?sid=&type=gen&mod=Core+Pages&gid=A6CD4967199A42D9B65B1B” no son necesarias en django, ya que se pueden escribir patrones de URL más elegantes.

Un patrón de URL es la forma general de una url, por ejemplo: /archivados/<anio>/<mes>/.

Para obtener una vista desde una URL, Django utiliza ‘URLconfs’.

Una URLconf mapea patrones de URL patterns (como expresiones regulares) a vistas.

Para más info ver django.core.urlresolvers.

Escribiendo nuestra primera vista

Abrimos el archivo encuestas/views.py y ponemos el siguiente código Python:

# -*- coding: utf-8 -*-
from django.http import HttpResponse


def index(request):
    return HttpResponse(u"Hola mundo. Estás en el index de encuestas.")

Esta es la view más simple posible en Django. Para llamar a la view, necesitamos mapearla a una URL, y para esto necesitamos el URLconf.

Mapeando vistas

En la carpeta encuestas, crear un archivo llamado urls.py. El directorio de tu aplicación debería quedar así:

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

En nuestro nuevo archivo, escribimos:

from django.conf.urls import patterns, url

from encuestas import views

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

Mapeando vistas

Apuntar el URLconf raíz al módulo encuestas.urls. En curso/urls.py insertamos un include(), con lo que nos quedaría:

from django.conf.urls import patterns, include, url
from django.contrib import admin

urlpatterns = patterns('',
    url(r'^encuestas/', include('encuestas.urls')),
    url(r'^admin/', include(admin.site.urls)),
)

Hemos conectado una view index en el URLconf. Si vamos a la dirección http://localhost:8000/encuestas/ en nuestro browser, deberíamos ver el texto “Hola mundo. Estás en el index de encuestas.”, que definimos la view index.

Argumentos de URLConf

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

  • 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/.

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 encuestas/views.py. Estas views van a ser un poco diferentes porque van a tomar un argumento:

# -*- coding: utf-8 -*-

def detalle(request, pregunta_id):
    return HttpResponse(u"Estás viendo la pregunta %s." % pregunta_id)

def resultados(request, pregunta_id):
    response = u"Estás viendo los resultados de la pregunta %s."
    return HttpResponse(response % pregunta_id)

def votar(request, pregunta_id):
    return HttpResponse(u"Estás votando en la pregunta %s." % pregunta_id)

Escribiendo más views

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

# -*- coding: utf-8 -*-

from django.conf.urls import patterns, url

from encuestas import views

urlpatterns = patterns('',
    # ej: /encuestas/
    url(r'^$', views.index, name='index'),
    # ej: /encuestas/5/
    url(r'^(?P<pregunta_id>\d+)/$', views.detalle, name='detalle'),
    # ej: /encuestas/5/resultadoss/
    url(r'^(?P<pregunta_id>\d+)/resultados/$', views.resultados, name='resultados'),
    # ej: /encuestas/5/votar/
    url(r'^(?P<pregunta_id>\d+)/votar/$', views.votar, name='votar'),
)

Por qué usar includes?

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

¿Qué pasa internamente?

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

  • Django encontrará coincidencia en '^encuestas/'

  • Entonces, Django va a recortar el texto que coincide ("encuestas/") y enviar el texto restante –"34/" – al URLconf ‘encuestas.urls’ para seguir el proceso, donde coincidirá con r'^(?P<pregunta_id>\d+)/$', resultando en una llamada a la view detalle() de la forma:

detalle(request=<HttpRequest object>, pregunta_id='34')

¿Qué pasa internamente?

  • pregunta_id='34' surge de (?P<pregunta_id>\d+).
  • 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<pregunta_id> define el nombre que se usará para identificar la coincidencia;
  • \d+ es una expresión regular para buscar una secuencia de dígitos (i.e., un número).

 

¿Qué pasa internamente?

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:

(r'^encuestas/ultimas\.html$', 'encuestas.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.

Escribiendo views que hacen algo

  • 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.

Escribiendo views que hacen algo

Utilizando nuestra API de base de datos, haremos un nuevo index, que muestre las últimas 5 preguntas:

from django.http import HttpResponse

from encuestas.models import Pregunta


def index(request):
    lista_ultimas_preguntas = Pregunta.objects.order_by('-pub_date')[:5]
    salida = ', '.join([p.texto_pregunta for p in lista_ultimas_preguntas])
    return HttpResponse(salida)

# Deja las otras vistas tal como estaban

Escribiendo views que hacen algo

Hay un problema!: el diseño de la página está escrito explícitamente en la view.

Vamos a usar el sistema de templates de Django para separar el diseño del código Python.

Crear un directorio llamado templates en tu directorio encuestas.

Django Buscará las plantillas ahí.

Dentro de ese directorio, crear otro llamado encuestas y ahí dentro crea un archivo llamado index.html. Debería verse: encuestas/templates/encuestas/index.html. Puedes referiste a esta plantilla como encuestas/index.html.

Creando una plantilla

Escribir lo siguiente en nuestra plantilla index.html

{% if lista_ultimas_preguntas %}
    <ul>
    {% for pregunta in lista_ultimas_preguntas %}
        <li>
          <a href="/encuestas/{{ pregunta.id }}/">{{ pregunta.texto_pregunta }}</a>
        </li>
    {% endfor %}
    </ul>
{% else %}
    <p>No hay encuestas disponibles.</p>
{% endif %}

Actualizando nuestra vista

Actualizamos la función index de encuestas/views.py:

from django.http import HttpResponse
from django.template import RequestContext, loader

from encuestas.models import Pregunta


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

Context es un diccionario de variables pasados a la plantilla.

Un atajo: render()

Las acciones anteriores son algo muy común. Django provee un atajo. Aquí está la view index() reescrita:

from django.shortcuts import render

from encuestas.models import Pregunta


def index(request):
    lista_ultimas_preguntas = Pregunta.objects.all().order_by('-pub_date')[:5]
    context = {'lista_ultimas_preguntas': lista_ultimas_preguntas}
    return render(request, 'encuestas/index.html', context)

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 del template renderizado con el contexto dado.

Levantando un error 404

Ahora veamos la view de detalle de una encuesta – la página que muestra la pregunta de una encuesta:

from django.http import Http404
from django.shortcuts import render

from encuestas.models import Pregunta
# ...
def detalle(request, pregunta_id):
    try:
        pregunta = Pregunta.objects.get(pk=pregunta_id)
    except Pregunta.DoesNotExist:
        raise Http404
    return render(request, 'encuestas/detalle.html', {'pregunta': pregunta})

Por ahora pongamos lo siguiente en encuestas/detalle.html:

{{ pregunta }}

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:

from django.shortcuts import get_object_or_404, render

from encuestas.models import Pregunta
# ...
def detalle(request, pregunta_id):
    pregunta = get_object_or_404(Pregunta, pk=pregunta_id)
    return render(request, 'encuestas/detalle.html', {'pregunta': pregunta})

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.

Usando el sistema de templates

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

<h1>{{ pregunta.texto_pregunta }}</h1>
<ul>
{% for eleccion in pregunta.eleccion_set.all %}
    <li>{{ eleccion.texto_opcion}}</li>
{% endfor %}
</ul>

Usando el sistema de templates

El sistema de templates usa sintaxis de punto para acceder a los atributos de variable.

En {{ pregunta.texto_pregunta }}, Django primero hace una búsqueda de diccionario sobre pregunta. 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 bucle {% for %}: pregunta.eleccion_set.all se interpreta como el código Python pregunta.eleccion_set.all(), que devuelve un iterable de objeto Eleccion y usable para el tag {% for %}.

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

Borrando URLs harcodeadas en templates

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

Con esto es 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 encuestas.urls, podemos eliminar la dependencia de URLs fijas usando el template tag {% url %}:

<li><a href="{% url 'detalle' pregunta.id %}">{{ pregunta.texto_pregunta }}</a></li>

Espacio de nombres en URLs

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? 

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

from django.conf.urls import patterns, include, url
from django.contrib import admin

urlpatterns = patterns('',
    url(r'^encuestas/', include('encuestas.urls', namespace="encuestas")),
    url(r'^admin/', include(admin.site.urls)),
)

Ahora, en encuestas/index.html:

<li><a href="{% url 'encuestas:detalle' pregunta.id %}">{{ pregunta.texto_pregunta }}</a></li>

Espacio de nombres en URLs

Made with Slides.com