Introducción
Django es un framework web de alto nivel basado en Python.Instalación
Para la instalación en Windows vamos a necesitar descargar los siguientes archivos:
- Active Python 2.5.2.2 (versión para 64-bits)
- PySQLite3 2.5.0
- Django 1.0
- SlikSubversion 1.5.3 (versión para 64-bits) (opcional)
Instalando dependencias
Antes de instalar Django debemos instalar sus dependencias: Python y Pysqlite. Para ellos instalar Active Python 2.5.2.2 y PySQLite3 2.5.5 siguiendo las instrucciones provistas al ejecutar los instaladores.
Instalando Django
Asumiendo que el archivo Django-1.0.tar.gz se extrajo en C:\Descargas\Django-1.0, debemos proceder de la siguiente forma para instalar Django:
- Abrir una terminal (menú inicio, ejecutar, cmd.exe).
- Cambiar a la carpeta C:\Descargas\Django-1.0 (cd C:\Descargas\Django-1.0).
- Ejecutar el script de instalación (python setup.py install).
- Ajustar la variable de entorno PATH para poder ejecutar el script de administración de Django, django-admin.py. Para hacer esto se debe:
- Hacer click derecho en Mi PC y seleccionar Propiedades.
- Ir a la pestaña Opciones avanzadas y hacer click en el botón Variables de entorno.
- En Variables del sistema, seleccionar Path y hacer click en el botón Modificar.
- Agregar C:\Python25\Scripts al comienzo del campo Valor de la variable y separar con un punto y coma (;) del valor existente en ese campo.
- Apretar el botón Aceptar en los dos cuadros de diálogo abiertos para que los cambios tengan efecto.
- Cerrar las instancias de cmd.exe y volverlas a abrir para que tomen la nueva configuración.
Para probar que la instalación fue exitosa creemos un proyecto
vacío, iniciemos el proceso servidor y corroboremos que podemos
accederlo desde el navegador. Para hacer esta prueba, procedamos de
esta forma:
- Crear la carpeta C:\Projects.
- Abrir una terminal (menú inicio, ejecutar, cmd.exe).
- Cambiar a la carpeta C:\Projects (cd C:\Projects).
- Ejecutar el siguiente comando: django-admin.py startproject mysite
- Cambiar la carpeta C:\Projects\mysite (cd C:\Projects\mysite) y ejecutar: python manage.py runserver
- Apuntar el navegador de internet a la dirección http://127.0.0.1:8000/ y deberíamos ver el mensaje de instalación exitosa.
Mi primera aplicación en Django
Una aplicación en un paquete idealmente reusable que provee una funcionalidad específica (por ejemplo, un weblog). Un proyecto es un conjunto de configuraciones y aplicaciones para un sitio web particular.
En esta sección crearemos una aplicación web para encuestas que permita a los visitantes entrar al sitio y llenar las encuestas. Los administradores del sitio podrán cargar nuevas encuestas.
Creando un proyecto
> cd C:\Projects
> django-admin.py startproject mysite
Esto nos genera esta estructura:
mysite/
__init__.py
manage.py
settings.py
urls.py
Tener en cuenta:
- Los archivos de un proyecto pueden vivir en cualquier parte del sistema. No es buena idea ponerlos en el document root de nuestro servidor web, como es usual en PHP, por ejemplo.
-
Iniciar servidor con python manage.py runserver 8000.
- Configuración de la base de datos en settings.py.
Creando una aplicación
Creamos la aplicación para las encuestas.
> cd mysite
> django-admin.py startapp polls
Esto crea una carpeta polls, que luce de esta forma:
polls/
__init__.py
models.py
views.py
Creando modelos
Modificar mysite/polls/models.py reemplazando todo su contenido por este:
"""Models for our polls application
"""
from django.db import models
class Poll(models.Model):
"""A model for polls"""
question = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
class Choice(models.Model):
"""A model for choice"""
poll = models.ForeignKey(Poll)
choice = models.CharField(max_length=200)
votes = models.IntegerField()
Modificar los valores de las variables DATABASE_ENGINE y DATABASE_NAME en mysite/settings.py de esta forma:
DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
DATABASE_NAME = 'polls.db' # Or path to database file if using sqlite3.
Aprovechemos también para cambiar el idioma de la interface de administración (que veremos más adelante) a español:
LANGUAGE_CODE = 'es-AR'
Ejecutar el siguiente comando para inicializar la base de datos:
> python manage.py syncdb
Creating table auth_permission
Creating table auth_group
Creating table auth_user
Creating table auth_message
Creating table django_content_type
Creating table django_session
Creating table django_site
Creating table polls_poll
Creating table polls_choice
You just installed Django's auth system, which means you don't have any superusers defined.
Would you like to create one now? (yes/no): yes
Username (Leave blank to use 'emanuel'): admin
E-mail address: emanuel@menttes.com
Password:
Password (again):
Superuser created successfully.
Installing index for auth.Permission model
Installing index for auth.Message model
Installing index for polls.Choice model
Con esto Django logra:
- Crear el esquema de la base de datos para esta aplicación
- Crear una API Python de acceso a la base de datos para acceder a los objetos Poll y Choice.
Jugando con la API
Con el siguiente comando iniciamos una sesión Python interactiva que tiene mysite en sys.path.
python manage.py shell
Ahora podemos jugar un poco con la API:
>>> from mysite.polls.models import Poll, Choice # Import the model classes we just wrote.
# No polls are in the system yet.
>>> Poll.objects.all()
[]
# Create a new Poll.
>>> import datetime
>>> p = Poll(question="What's up?", pub_date=datetime.datetime.now())
# Save the object into the database. You have to call save() explicitly.
>>> p.save()
# Now it has an ID. Note that this might say "1L" instead of "1", depending
# on which database you're using. That's no biggie; it just means your
# database backend prefers to return integers as Python long integer
# objects.
>>> p.id
1
# Access database columns via Python attributes.
>>> p.question
"What's up?"
>>> p.pub_date
datetime.datetime(2007, 7, 15, 12, 00, 53)
# Change values by changing the attributes, then calling save().
>>> p.pub_date = datetime.datetime(2007, 4, 1, 0, 0)
>>> p.save()
# objects.all() displays all the polls in the database.
>>> Poll.objects.all()
[<Poll: Poll object>]
Poll: Poll object no es una buena representación de este objeto. Podemos arreglar eso agregado un método en el modelo:
class Poll(models.Model):
# ...
def __unicode__(self):
return self.question
class Choice(models.Model):
# ...
def __unicode__(self):
return self.choice
Notar que estos son métodos normales. Agreguemos un método personalizado con fines demostrativos. En este caso agregaremos un método que nos dice si un objeto Poll fue publicado hoy.
import datetime
# ...
class Poll(models.Model):
# ...
def was_published_today(self):
"""Return True if the Poll was published today"""
return self.pub_date.date() == datetime.date.today()
Ahora podemos jugar un poco más con la API:
>>> from mysite.polls.models import Poll, Choice
# Make sure our __unicode__() addition worked.
>>> Poll.objects.all()
[<Poll: What's up?>]
# Django provides a rich database lookup API that's entirely driven by
# keyword arguments.
>>> Poll.objects.filter(id=1)
[<Poll: What's up?>]
>>> Poll.objects.filter(question__startswith='What')
[<Poll: What's up?>]
# Get the poll whose year is 2007. Of course, if you're going through this
# tutorial in another year, change as appropriate.
>>> Poll.objects.get(pub_date__year=2007)
<Poll: What's up?>
>>> Poll.objects.get(id=2)
Traceback (most recent call last):
...
DoesNotExist: Poll matching query does not exist.
# Lookup by a primary key is the most common case, so Django provides a
# shortcut for primary-key exact lookups.
# The following is identical to Poll.objects.get(id=1).
>>> Poll.objects.get(pk=1)
<Poll: What's up?>
# Make sure our custom method worked.
>>> p = Poll.objects.get(pk=1)
>>> p.was_published_today()
False
# Give the Poll a couple of Choices. The create call constructs a new
# choice object, does the INSERT statement, adds the choice to the set
# of available choices and returns the new Choice object.
>>> p = Poll.objects.get(pk=1)
>>> p.choice_set.create(choice='Not much', votes=0)
<Choice: Not much>
>>> p.choice_set.create(choice='The sky', votes=0)
<Choice: The sky>
>>> c = p.choice_set.create(choice='Just hacking again', votes=0)
# Choice objects have API access to their related Poll objects.
>>> c.poll
<Poll: What's up?>
# And vice versa: Poll objects get access to Choice objects.
>>> p.choice_set.all()
[<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]
>>> p.choice_set.count()
3
# The API automatically follows relationships as far as you need.
# Use double underscores to separate relationships.
# This works as many levels deep as you want; there's no limit.
# Find all Choices for any poll whose pub_date is in 2007.
>>> Choice.objects.filter(poll__pub_date__year=2007)
[<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]
# Let's delete one of the choices. Use delete() for that.
>>> c = p.choice_set.filter(choice__startswith='Just hacking')
>>> c.delete()
Activando el sitio de administración
-
Agregar django.contrib.admin a INSTALLED_APPS en settings.py.
-
Ejecutar python manage.py syncdb. Como hemos agregado una aplicación a INSTALLED_APPS, necesitamos actualizar la base de datos.
-
Editar el archivo mysite/urls.py y descomentar las líneas debajo del comentario “Uncomment the next two lines...”. Este archivo es un URLconf. Por ahora, todo lo que necesitamos saber es que mapea URLs a aplicaciones. Nuestro archivo urls.py debe lucir así:
"""Map URLs to applications for the mysite project
"""
from django.conf.urls.defaults import *
# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
# Example:
# (r'^mysite/', include('mysite.foo.urls')),
# Uncomment the admin/doc line below and add 'django.contrib.admindocs'
# to INSTALLED_APPS to enable admin documentation:
# (r'^admin/doc/', include('django.contrib.admindocs.urls')),
# Uncomment the next line to enable the admin:
(r'^admin/(.*)', admin.site.root),
)(El texto en negritas muestra las líneas que deben ser descomentadas).
usando las credenciales que elegimos la primera vez que ejecutamos python manage.py syncdb.
Sin embargo, los objetos de tipo Poll aún no pueden ser administrados en el sitio de administración. Para activar la modificación de los objetos Poll desde el sitio de administración debemos agregar un nuevo módulo mysite/polls/admin.py con este contenido:
"""The admin user interface for polls
"""
from mysite.polls.models import Poll, Choice
from django.contrib import admin
admin.site.register(Poll)
admin.site.register(Choice)
Reiniciamos el proceso servidor, volvemos a acceder al sitio de administración y veremos que ahora podemos administrar Polls desde el sitio de administración.
Notemos:
- El formulario se genera automáticamente a partir del modelo de Poll.
- Los distintos tipos de campos en el modelo (DateTimeField, CharField) se corresponden con los widgets HTML apropiados. Cada tipo de campo sabe cómo mostrarse a sí mismo en el sitio de administración Django.
- Cada DateTimeField obtiene atajos JavaScript gratuitos. Las fechas obtienen un atajo "Today" y un calendario, por ejemplo.
- En la parte de abajo tengo las opciones: Grabar, Grabar y continuar editando, Grabar y añadir otro y Eliminar.
Modifiquemos admin.py para que luzca como acá:
"""The admin user interface for polls
"""
from mysite.polls.models import Poll, Choice
from django.contrib import admin
class ChoiceInline(admin.TabularInline):
model = Choice
extra = 3
class PollAdmin(admin.ModelAdmin):
fieldsets = [
(None, {'fields': ['question']}),
('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
]
inlines = [ChoiceInline]
list_display = ('question', 'pub_date', 'was_published_today')
list_filter = ['pub_date']
search_fields = ['question']
date_hierarchy = 'pub_date'
admin.site.register(Poll, PollAdmin)
Para que la columna de was_published_today tenga un título personalizado podemos agregar lo siguiente a models.py justo debajo de la definición del método:
was_published_today.short_description = 'Published today?'
Personalizando la apariencia del sitio de administración
Tener "Administración de Django" en la parte superior de cada pantalla de administración es algo que queremos cambiar para adaptarse mejor a los propósitos de nuestro sistema. Para realizar este cambio se procede como sigue:
- Crear una carpeta llamada templates dentro de mysite.
- Dentro de templates crear una carpeta llamada admin.
- Copiar base_site.html a mysite/templates/admin
- Editar mysite/templates/admin/base_site.html cambiando donde dice 'Django administration' por 'Mi sitio de encuestas' (incluyendo las comillas).
- Editar mysite/settings.py y cambiar la variable TEMPLATES_DIRS para que luzca así:
TEMPLATE_DIRS = (
También importar el módulo os al principio del archivo mysite/settings.py:
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
os.path.join(os.path.dirname(__file__), 'templates'),
)
import os
Diseñando nuestros URLs
En nuestra aplicación de encuestas vamos a tener cuatro vistas:
- La página de archivo (index): muestra las últimas encuestas.
- La página con los detalles de una encuesta (detail): muestra la pregunta sin los resultados con un formulario para votar.
- La págna de resultados de una encuesta (results): muestra los resultados para una encuesta en particular.
- La acción de votar (vote): maneja el voto por una opción en particular en una encuesta en particular.
El primer paso para escribir estas vistas es decidir como va a lucir nuestro esquema de URLs.
Editar mysite/urls.py para que luzca así:
"""Map URLs to applications for the mysite project
"""
from django.conf.urls.defaults import *
# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
# Example:
# (r'^mysite/', include('mysite.foo.urls')),
# Uncomment the admin/doc line below and add 'django.contrib.admindocs'
# to INSTALLED_APPS to enable admin documentation:
# (r'^admin/doc/', include('django.contrib.admindocs.urls')),
# Uncomment the next line to enable the admin:
(r'^admin/(.*)', admin.site.root),
(r'^polls/$', 'mysite.polls.views.index'),
(r'^polls/(?P<poll_id>\d+)/$', 'mysite.polls.views.detail'),
(r'^polls/(?P<poll_id>\d+)/results/$', 'mysite.polls.views.results'),
(r'^polls/(?P<poll_id>\d+)/vote/$', 'mysite.polls.views.vote'),
)
Cuando alguien pide una página de nuestro sitio web, digamos "polls/23", Django va a cargar este módulo porque es apuntado por la variable ROOT_URLCONF en settings.py. Luego encuentra la variable llamada urlpatterns y recorre las expresiones regulares en orden. Cuando encuentra una expresión regular que coincide con el pedido (en este caso r'^polls/(?P<poll_id>\d+)/$'), carga el paquete/módulo Python asociado: mysite.polls.views.detail. Esto corresponde a la función detail() en mysite/polls/views.py. Finalmente, la función detail() se llama de esta forma:
detail(request=<HttpRequest object>, poll_id='23')
La parte poll_id='23' viene de (?P<poll_id>\d+). El uso de paréntesis alrededor de un patrón "captura" el texto que coincide con el patrón y lo envía como un argumento a la función de vista; ?P<poll_id> define el nombre que será usado para identificar el patrón que coincide; y \d+ es una expresión regular que coincide con una secuencia de dígitos (i.e., un número).
Nuestra primera vista
Si en este momento apuntamos nuestro navegador a http://127.0.0.1:8000/polls/vamos a obtener un error. Esto ocurre porque aún no existe la vista mysite.polls.views.index. Para arreglar esto, creemos la vista. Editar mysite/polls/views.py para que luzca así:
"""Views for the polls application
"""
from django.http import HttpResponse
def index(request):
return HttpResponse("Hello, world. You're at the poll index.")
Ahora apuntemos nuestro navegador a http://127.0.0.1:8000/polls/ nuevamente y veamos lo que pasa.
Escribiendo vistas que hacen algo
Haciendo que index devuelva las encuestas existentes, hagamos que mysite/polls/views.py luzca así:
"""Views for the polls applicationEl problema con esto es que el diseño de la página está mezclado con el código Python. Usando el sistema de plantillas de Django (templates) se puede separar el código Python de la presentación, hagamos que mysite/polls/views.py luzca así:
"""
from django.http import HttpResponse
from mysite.polls.models import Poll
def index(request):
latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
output = ', '.join([p.question for p in latest_poll_list])
return HttpResponse(output)
"""Views for the polls application
"""
from django.template import Context, loader
from mysite.polls.models import Poll
from django.http import HttpResponse
def index(request):
latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
t = loader.get_template('polls/index.html')
c = Context({
'latest_poll_list': latest_poll_list,
})
return HttpResponse(t.render(c))
Esta vista nos va a dar un error hasta tanto no creemos la plantilla polls/index.html. Para arreglarlo hagamos:
- Crear la carpeta mysite/polls/templates
- Crear la carpeta mysite/polls/templates/polls
- Crear el archivo mysite/polls/templates/polls/index.html con el siguiente contenido:
{% if latest_poll_list %}
<ul>
{% for poll in latest_poll_list %}
<li>{{ poll.question }}</li>
{% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %}
reescrita para usar el atajo render_to_response():
"""Views for the polls application
"""
from django.shortcuts import render_to_response
from mysite.polls.models import Poll
def index(request):
latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
return render_to_response('polls/index.html', {'latest_poll_list': latest_poll_list})
Ahora escribamos la vista de los detalles (detail) agregando lo siguiente a views.py:
from django.http import Http404
# ...
def detail(request, poll_id):
try:
p = Poll.objects.get(pk=poll_id)
except Poll.DoesNotExist:
raise Http404
return render_to_response('polls/detail.html', {'poll': p})
Y su respectiva plantilla mysite/polls/templates/polls/detail.html:
<h1>{{ poll.question }}</h1>
<ul>
{% for choice in poll.choice_set.all %}
<li>{{ choice.choice }}</li>
{% endfor %}
</ul>
Es muy común usar get() y lanzar un Http404 si el objeto no existe. Django provee un atajo. Acá reescribimos la vista detail() usando el atajo get_object_or_404():
from django.shortcuts import render_to_response, get_object_or_404
# ...
def detail(request, poll_id):
p = get_object_or_404(Poll, pk=poll_id)
return render_to_response('polls/detail.html', {'poll': p})
Simplificando y desacoplando URLconfs
Vamos a dejar al paquete polls que maneje los URLs específicos a encuestas. Para esto agregamos el archivo mysite/polls/urls.py con el siguiente contenido:
"""Map URLs to views for the polls application
"""
from django.conf.urls.defaults import *
urlpatterns = patterns('',
(r'^$', 'mysite.polls.views.index'),
(r'^(?P<poll_id>\d+)/$', 'mysite.polls.views.detail'),
(r'^(?P<poll_id>\d+)/results/$', 'mysite.polls.views.results'),
(r'^(?P<poll_id>\d+)/vote/$', 'mysite.polls.views.vote'),
)
Y modificamos mysite/urls.py para que luzca así:
"""Map URLs to applications for the mysite project
"""
from django.conf.urls.defaults import *
# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
# Example:
# (r'^mysite/', include('mysite.foo.urls')),
# Uncomment the admin/doc line below and add 'django.contrib.admindocs'
# to INSTALLED_APPS to enable admin documentation:
# (r'^admin/doc/', include('django.contrib.admindocs.urls')),
# Uncomment the next line to enable the admin:
(r'^admin/(.*)', admin.site.root),
(r'^polls/', include('mysite.polls.urls')),
)
Se puede simplificar mysite/polls/urls.py de esta forma:
"""Map URLs to views for the polls application
"""
from django.conf.urls.defaults import *
urlpatterns = patterns('mysite.polls.views',
(r'^$', 'index'),
(r'^(?P<poll_id>\d+)/$', 'detail'),
(r'^(?P<poll_id>\d+)/results/$', 'results'),
(r'^(?P<poll_id>\d+)/vote/$', 'vote'),
)
Escribiendo nuestro primer formulario
Reemplacemos la plantilla polls/detail.html por la siguiente:
<h1>{{ poll.question }}</h1>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
<form action="/polls/{{ poll.id }}/vote/" method="post">
{% for choice in poll.choice_set.all %}
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
<label for="choice{{ forloop.counter }}">{{ choice.choice }}</label><br />
{% endfor %}
<input type="submit" value="Vote" />
</form>
Con esto ya podremos ver un formulario, pero no hemos programado la lógica aún. Para arreglar esto devemos agregar la funcón vote en views.py:
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
#...
def vote(request, poll_id):
p = get_object_or_404(Poll, pk=poll_id)
try:
selected_choice = p.choice_set.get(pk=request.POST['choice'])
except (KeyError, Choice.DoesNotExist):
# Redisplay the poll voting form.
return render_to_response('polls/detail.html', {
'poll': p,
'error_message': "You didn't select a choice.",
})
else:
selected_choice.votes += 1
selected_choice.save()
# Always return an HttpResponseRedirect after successfully dealing
# with POST data. This prevents data from being posted twice if a
# user hits the Back button.
return HttpResponseRedirect(reverse('mysite.polls.views.results', args=(p.id,)))
Aquí se hace referencia a una plantilla de resultados, mysite.polls.views.resutls, que aún no existe. Para crearla agregamos la siguiente función a views.py:
def results(request, poll_id):
p = get_object_or_404(Poll, pk=poll_id)
return render_to_response('polls/results.html', {'poll': p})
Y la plantilla asociada mysite/polls/templates/polls/results.html:
<h1>{{ poll.question }}</h1>
<ul>
{% for choice in poll.choice_set.all %}
<li>{{ choice.choice }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>
Usando vistas genéricas
Las vistas results() y details() son muy simples y redundantes. Las vistas genéricas abstraen patrones genéricos al punto en que no necesitaremos escribir código Python.
Cambiemos mysite/polls/urls.py para que luzca así:
"""Map URLs to views for the polls applicationAquí estamos usando dos vistas genéricas: object_list() y object_detail(). Respectivamente, esas vistas abstraen los conceptos de "mostrar una lista de objetos" y "mostras una páginas con detalles de un tipo particular de objeto".
"""
from django.conf.urls.defaults import *
from mysite.polls.models import Poll
info_dict = {
'queryset': Poll.objects.all(),
}
urlpatterns = patterns('',
(r'^$', 'django.views.generic.list_detail.object_list', info_dict),
(r'^(?P<object_id>\d+)/$', 'django.views.generic.list_detail.object_detail', info_dict),
url(r'^(?P<object_id>\d+)/results/$', 'django.views.generic.list_detail.object_detail',
dict(info_dict, template_name='polls/results.html'), 'poll_results'),
(r'^(?P<poll_id>\d+)/vote/$', 'mysite.polls.views.vote'),
)
- Cada vista genérica necesita saber sobre qué datos estará actuando. Estos datos son provistos en un diccionario. La clave queryset en este diccionario apunta a la lista de objetos que serán manipulados por la vista genérica.
- La vista genérica object_detail() espera el valor del ID capturado del URL, a ser llamado "object_id",
por lo tanto hemos cambiado poll_id a object_id para las vistas genéricas.
- Hemos agregado un nombre, poll_results, a la vista de resultados así tenemos una forma de referirnos al URL más tarde. También estamos usando la función url() desde django.conf.urls.defaults. Usar url() es un buen hábito cuando proveemos un nombre al patrón como en este caso.
Por defecto la vista genérica object_detail() usa una plantilla llamada <app name>/<model name>_detail.html. En nuestro caso usará la plantilla llamada "polls/poll_detail.html". Por lo tanto debemos renombrar polls/detail.html a polls/poll_detail.html, y cambiar la línea render_to_response() en vote().
De forma similar, la vista genérica object_list() usa una plantilla llamada <app name>/<model name>_list.html. Por lo tanto, renombremos polls/index.html a polls/poll_list.html.
Como tenemos más de una entrada en URLconf que usa object_detail() para la applicación poll, especificamos manualmente una plantilla para la vista de resutados: template_name='polls/results.html'. En caso contrario, ambas vistas usarían la misma plantilla.
Anteriormente las plantillas eran provistas de un contexto que contenía variables de context poll y latest_poll_list. Sin embargo, las vistas genéricas proveen como contexto las variables object y object_list. Por lo tanto necesitamos cambiar nuestras plantillas para que coincidan con esas nuevas variables de contexto. Entonces recorramos nuestras plantillas reemplazando latest_poll_list por object_list y poll por object.
Podemos borrar las vistas index(), detail() y results() de polls/views.py. Ya no las necesitamos, las hemos reemplazado por vistas genéricas.
La vista vote() es requerida aún. Sin embargo, necesitamos modificarla para que coincida con las nuevas variables de contexto. En la llamada render_to_response(), renombrar la variable de contexto poll a object.
La última cosa por hacer es arreglar el manejo de los URL de acuerdo al uso de vistas genéricas. En la vista vote de arriba usamos la función reverse() para evitar ingresar directamente los URLs. Ahora que cambiamos a vistas genéricas necesitamos cambiar la llamada a reverse() para que apunte a nuestra vista genérica. Ya no podemos simplemente usar la función de la vista, pero podemos usar el nombre que le dimos a la vista:
return HttpResponseRedirect(reverse('poll_results', args=(p.id,))
Ejecutemos el servidor y usemos nuestra nueva aplicación de encuestas basada en vistas genéricas.
Ejercicios
- Crear una vista que muestre todas las encuestas ordenadas de acuerdo a la catidad de votos recibidos, colocando primero la encuesta que más votos recibió.
- Modificar la vista de resultados para que muestre porcentajes en lugar de cantidad de votos.
- Crear un proyecto con nombre "agenda" que tenga una aplicación para almacenar "contactos".
El sistema de plantillas de Django
Motivación
¿Cuál es el problema de programar las vistas sin usar plantillas? Es decir, escribiendo el código HTML dentro de la función Python correspondiente a la vista.
- Obviamente, cualquier cambio en el diseño de la página conllevará un cambio del código. El diseño de un sitio tiende a cambiar mucho más rápidamente que el código subyacente, así que sería conveniente si los cambios del HTML estuvieran separados de los cambios del código.
- Segundo, escribir código de backend en Python y diseñar/escribir HTML son dos disciplinas diferentes, y la mayoría de los entornos profesionales de desarrollo Web dividen estas responsabilidades entre personas distintas (o inclusos departamentos distintos). Los diseñadores y programadores de HTML/CSS no deberían tener que editar código en Python para hacer su trabajo; sólo deberían tratar con HTML.
- Asimismo, es más eficiente que los programadores puedan trabajar en su código en Python y los diseñadores en sus plantillas de forma simultánea, en lugar de que una persona tenga que esperar que la otra acabe de editar un único fichero que contiene tanto Python como HTML.
Por estas razones, es mucho más limpio y mantenible separar el diseño de la página del propio código en Python. Y podemos conseguirlo con el sistema de plantillas de Django.
Un ejemplo
Una plantilla de Django es una cadena de texto pensada para separar la presentación de un documento de sus datos. Una plantilla define variables y varios trozos de lógica básica (tags) que regulan la manera en que debería mostrarse el documento. Normalmente, las plantillas se usan para producir HTML, pero las plantillas de Django son igualmente capaces de generar cualquier formato basado en texto.
Echémosle un amplio vistazo mediante una sencilla plantilla de ejemplo. Esta plantilla describe una página de HTML que da las gracias a una persona por realizar un pedido a una compañía. Piense en ella como una carta:
<html>
<head><title>Albarán de pedido</title></head>
<body>
<p>Estimado/a {{ nombre_persona }},</p>
<p>Gracias por hacer su pedido a {{ company }}. Será preparada para
envío el {{ fecha_envio|date:"j de F de Y" }}.</p>
<p>Estos son los artículos que ha solicitado:</p>
<ul>
{% for item in lista_items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
{% if pidio_garantia %}
<p>
<p>Se incluirá información sobre la garantía en el paquete.</p>
{% endif %}
<p>Sinceramente,<br />{{ company }}</p>
</body>
</html>
Esta plantilla es HTML básico al que se han añadido algunas variables y etiquetas (tags) de plantilla. Veámoslo paso a paso:
-
Cualquier texto rodeado por llaves (p.ej. {{ nombre_persona }}) es una variable. Significa «inserta el valor de la variable con este nombre» (¿y cómo especificamos el valor de esas variables? Lo vemos en un momento).
-
Cualquier texto rodeado por llaves y símbolos de porcentaje (p.ej. {% if pidio_garantia %}) es una etiqueta de bloque (block tag). La definición de una etiqueta de bloque es muy amplia: simplemente es algo que le dice al sistema de etiquetas que haga algo.
Esta plantilla de ejemplo contiene dos etiquetas de bloque: la etiqueta {% for item in lista_items %} (etiqueta "for") y la etiqueta {% if pidio_garantia %} (etiqueta "if"). Una etiqueta "for" actúa como estructura simple de repetición, permitiéndole iterar sobre cada elemento de una secuencia. Una etiqueta "if", como podría esperarse, actúa como una sentencia lógica "si". En este caso particular, la etiqueta comprueba si el valor de la variable pidio_garantia se evalúa a True. si lo hace, el sistema de plantillas mostrará todo lo que hay entre {% if pidio_garantia %} y {% endif %}. Si no, no se mostrará. El sistema de plantillas también admite {% else %} y otros tipos de sentencia lógica.
Cada plantilla de Django tiene acceso a varias etiquetas de bloque incluidas de serie. Además, puede escribir sus propias etiquetas.
-
Por último, el segundo párrafo de esta plantilla tiene un ejemplo de filtro. Los filtros son una manera de alterar el aspecto de una variable. En este ejemplo ({{ fecha_envio|date:"j de F de Y" }}) estamos pasando la variable fecha_envio al filtro date, dándole a date``el argumento ``"j de F de Y". Este filtro da formato a fechas según lo especificado por el argumento. Los filtros se adjuntan usando el caracter tubería (|), como referencia a los pipe de Unix.
Cada plantilla de Django tiene acceso a varios filtros de serie. Además, puede escribir los suyos propios.
Usando el sistema de plantillas
>>> from django.template import Template, Context
>>> raw_template = """<p>Dear {{ person_name }},</p>
...
... <p>Thanks for ordering {{ product }} from {{ company }}. It's scheduled
... to ship on {{ ship_date|date:"F j, Y" }}.</p>
...
... {% if ordered_warranty %}
... <p>Your warranty information will be included in the packaging.</p>
... {% endif %}
...
... <p>Sincerely,<br />{{ company }}</p>"""
>>> t = Template(raw_template)
>>> import datetime
>>> c = Context({'person_name': 'John Smith',
... 'product': 'Super Lawn Mower',
... 'company': 'Outdoor Equipment',
... 'ship_date': datetime.date(2009, 4, 2),
... 'ordered_warranty': True})
>>> t.render(c)
"<p>Dear John Smith,</p>\n\n<p>Thanks for ordering Super Lawn Mower from
Outdoor Equipment. It's scheduled \nto ship on April 2, 2009.</p>\n\n\n
<p>Your warranty information will be included in the packaging.</p>\n\n\n
<p>Sincerely,<br />Outdoor Equipment</p>"
En los ejemplos que llevamos vistos hemos pasado valores sencillos en los contextos de la plantilla (mayormente cadenas, más un ejemplo con datetime.date). Sin embargo, el sistema de plantillas maneja con elegancia estructuras de datos más complejas como listas, diccionarios y objetos arbitrarios.
La clave del recorrido de estructuras de datos complejas en plantillas de Django es el caracter '.' (el punto). Usamos un punto para acceder a claves de diccionarios, atributos, índices o métodos de un objeto.
Lo ilustramos mejor con unos pocos ejemplos. Para empezar, digamos que pasamos un diccionario de Python a la plantilla. Para acceder a los valores de ese diccionario mediante sus claves, usamos un punto:
>>> from django.template import Template, Context
>>> persona = {'nombre': 'Sally', 'edad': '43'}
>>> t = Template('{{ persona.nombre }} tiene {{ persona.edad }} años.')
>>> c = Context({'persona': persona})
>>> t.render(c)
'Sally tiene 43 años.'
De forma similar, los puntos nos permiten acceder a los atributos de los objetos. Por ejemplo, un objeto datetime.date de Python tiene atributos year, month y day, y podemos usar un punto para acceder a esos atributos en una plantilla de Django:
>>> from django.template import Template, Context
>>> import datetime
>>> d = datetime.date(1993, 5, 2)
>>> d.year
1993
>>> d.month
5
>>> d.day
2
>>> t = Template('El mes es {{ fecha.month }} y el año es {{ fecha.year }}.')
>>> c = Context({'fecha': d})
>>> t.render(c)
'El mes es 5 y el año es 1993.'
Este ejemplo usa una clase hecha por el usuario:
>>> from django.template import Template, Context
>>> class Persona(object):
... def __init__(self, nombre, apellido):
... self.nombre, self.apellido = nombre, apellido
>>> t = Template('Hola, {{ persona.nombre }} {{ persona.apellido }}.')
>>> c = Context({'persona': Persona('John', 'Smith')})
>>> t.render(c)
'Hola, John Smith.'
También usamos los puntos para acceder a índices de listas. Por ejemplo:
>>> from django.template import Template, Context
>>> t = Template('El elemento 2 es {{ items.2 }}.')
>>> c = Context({'items': ['manzanas', 'plátanos', 'zanahorias']})
>>> t.render(c)
'El elemento 2 es zanahorias.'
No se admiten índices negativos en las listas. Por ejemplo, la variable de plantilla {{ items.-1 }} produciría una TemplateSyntaxError.
Por último, también podemos usar los puntos para invocar métodos de objetos. Por ejemplo, cada cadena de Python tiene los métodos upper() e isdigit() y podemos invocarlos desde las plantillas de Django usando la misma sintaxis de puntos:
>>> from django.template import Template, Context
>>> t = Template('{{ var }} -- {{ var.upper }} -- {{ var.isdigit }}')
>>> t.render(Context({'var': 'hola'}))
'hola -- HOLA -- False'
>>> t.render(Context({'var': '123'}))
'123 -- 123 -- True'
Observe que en la invocación a métodos no incluimos los paréntesis. Además, no es posible pasar argumentos a los métodos.
Etiquetas y filtros básicos
Como ya hemos mencionado, el sistema de plantillas incluye una serie de etiquetas y filtros. Aquí tiene un listado de las más comunes. El Apéndice 6 incluye un listado completo de todas las etiquetas y filtros incluidos en Django, y es buena idea que se familiarice con dicha lista para hacerse una idea de lo que es posible.
if/else
La etiqueta {% if %} evalúa una variable, y si esa variable es "verdadera" (es decir, existe, no está vacía, y no es un valor booleano falso), el sistema mostrará todo lo que haya entre {% if %} y {% endif %}. Por ejemplo:
{% if hoy_es_fin_de_semana %}
<p>¡Bienvenido al fin de semana!</p>
{% endif %}
La etiqueta {% else %} es opcional:
{% if hoy_es_fin_de_semana %}
<p>¡Bienvenido al fin de semana!</p>
{% else %}
<p>Vuelve al trabajo.</p>
{% endif %}
La etiqueta {% if %} acepta and, or o not para comprobar múltiples variables, o para negar una dada. Por ejemplo:
{% if lista_atletas and lista_entrenadores %}
Disponemos de atletas y entrenadores.
{% endif %}
{% if not lista_atletas %}
No hay atletas.
{% endif %}
{% if lista_atletas or lista_entrenadores %}
Hay algunos atletas o algunos entrenadores.
{% endif %}
{% if not lista_atletas or lista_entrenadores %}
No hay atletas o no hay entrenadores (OK, traducir a
idioma humano la lógica booleana suena estúpido; pero
no es nuestra culpa).
{% endif %}
{% if lista_atletas and not lista_entrenadores %}
Hay algunos atletas y absolutamente ningún entrenador.
{% endif %}
Las etiquetas {% if %} no admiten cláusulas and y or de forma simultánea en la misma etiqueta, ya que el orden lógico sería ambiguo. Por ejemplo, esto no es válido:
{% if lista_atletas and lista_entrenadores or lista_animadoras %}
Si necesita combinar and y or para conseguir lógica avanzada, basta con anidar etiquetas {% if %}. Por ejemplo:
{% if lista_atletas %}
{% if lista_entrenadores or lista_animadoras %}
Tenemos atletas y ¡entradores (o animadoras)!
{% endif %}
{% endif %}
Se puede usar varias veces un operador lógico, siempre que sea el mismo. Por ejemplo, esto es válido:
{% if lista_atletas or lista_entrenadores or lista_padres or lista_profesores %}
No hay etiqueta {% elif %}. Utilice etiquetas {% if %} anidadas para conseguir el mismo efecto:
{% if lista_atletas %}
<p>Aquí tiene a los atletas: {{ lista_atletas }}.</p>
{% else %}
<p>No disponemos de atletas.</p>
{% if lista_entrenadores %}
<p>Aquí tiene a los entrenadores: {{ lista_entrenadores }}.</p>
{% endif %}
{% endif %}
Asegúrese de cerrar cada {% if %} con un {% endif %}. En caso contrario Django lanzará un TemplateSyntaxError.
for
La etiqueta {% for %} le permite iterar sobre los elementos de una secuencia. Al igual que para la sentencia for de Python, la sintaxis es for X in Y, siendo Y la secuencia sobre la que hay que iterar y X es el nombre de la variable a usar durante un ciclo particular del bucle. En cada paso por el bucle, el sistema de plantillas producirá todo lo que haya entre {% for %} y {% endfor %}.
Por ejemplo, para mostrar una lista de atletas dada la variable lista_atletas:
<ul>
{% for atleta in lista_atletas %}
<li>{{ atleta.nombre }}</li>
{% endfor %}
</ul>
Añada reversed a la etiqueta para iterar sobre la lista en orden inverso:
{% for atleta in lista_atletas reversed %}
...
{% endfor %}
Es posible anidar etiquetas {% for %}:
{% for pais in paises %}
<h1>{{ pais.nombre }}</h1>
<ul>
{% for ciudad in pais.lista_ciudades %}
<li>{{ ciudad }}</li>
{% endfor %}
</ul>
{% endfor %}
No hay manera de salir fuera de un bucle antes de que termine al estilo de una sentencia "break". Si quiere hacer esto, cambie la variable sobre la que itera para que sólo incluya los valores con los que quiere trabajar. De la misma manera, no hay soporte para una sentencia "continue" que instruya al procesador de bucles para que vuelva inmediatamente al principio del bucle (vea "Filosofías y limitaciones" más adelante en este capítulo si quiere conocer el razonamiento tras esta decisión de diseño).
La etiqueta {% for %} crea una variable "mágica" forloop dentro del bucle. Esta variable tiene unos pocos atributos que le dan información sobre el progreso del bucle:
-
forloop.counter siempre es igual a un entero que representa el número de veces que se ha entrado al bucle. El índice tiene el uno como base, así que la primera vez que se entre al bucle forloop.counter tendrá el valor 1. Ejemplo:
{% for item in lista_todo %}
<p>{{ forloop.counter }}: {{ item }}</p>
{% endfor %} -
forloop.counter0 es como forloop.counter, excepto que tiene el cero como base. Su valor será 0 la primera vez que se entre al bucle.
-
forloop.revcounter es siempre igual a un entero que representa el número de elementos que quedan en el bucle. La primera vez que se entra al bucle, forloop.revcounter será igual al número total de elementos de la secuencia que se está recorriendo. La última vez que se recorra el bucle, forloop.revcounter tendrá el valor 1.
-
forloop.revcounter0 es como forloop.revcounter, excepto que tiene cero como base. La primera vez que se entra en el bucle, forloop.revcounter0 será igual al número de elementos de la secuencia menos uno. La última vez que se entre al bucle, tendrá valor 0.
-
forloop.first es un valor booleano igual a True si es la primera vez que se recorre el bucle. Es conveniente para casos especiales:
{% for objeto in objetos %}
{% if forloop.first %}<li class="first">{% else %}<li>{% endif %}
{{ objecto }}
</li>
{% endfor %} -
forloop.last es un valor booleano igual a True si es la última vez que se recorre el bucle. Un ejemplo de uso para esto sería poner el caracter tubería por entre una lista de enlaces:
{% for link in links %}{{ link }}{% if not forloop.last %} | {% endif %}{% endfor %} -
forloop.parentloop es una referencia al objeto forloop del bucle de nivel superior, en el caso de bucles anidados. Por ejemplo:
{% for pais in paises %}
<table>
{% for ciudad in pais.lista_ciudades %}
<tr>
<td>País #{{ forloop.parentloop.counter }}</td>
<td>Ciudad #{{ forloop.counter }}</td>
<td>{{ ciudad }}</td>
</tr>
{% endfor %}
</table>
{% endfor %}
La variable mágica forloop sólo está disponible dentro de bucles. forloop desaparece después de que el intérprete de plantillas haya llegado a {% endfor %}.
Si nuestro contexto de plantilla ya contiene una variable llamada forloop, Django la ocultará dentro de las etiquetas {% for %}. En el resto de partes de la plantilla que no sean bucles, su forloop seguirá disponible e intacta. Le advertimos en contra del uso de variables de plantilla con el nombre forloop, pero si lo necesita y quiere acceso a su variable desde dentro de una etiqueta {% for %}, puede usar el forloop.parentloop descrito anteriormente.
ifequal/ifnotequal
El sistema de plantillas de Django no tiene la potencia de todo un lenguaje de programación y esto es deliberado, de manera que no le permita ejecutar sentencias arbitrarias de Python (más sobre esto en "Filosofías y limitaciones"). Sin embargo, es un requisito bastante común en las plantillas comparar dos valores y mostrar algo si son iguales (y Django proporciona la etiqueta {% ifequal %} para ese propósito).
La etiqueta {% ifequal %} compara dos valores y muestra todo lo que haya entre {% ifequal %} y {% endifequal %} si los valores son iguales.
Este ejemplo compara las variables de plantilla user y currentuser:
{% ifequal user currentuser %}
<h1>¡Bienvenido!</h1>
{% endifequal %}
Los argumentos pueden ser cadenas prefijadas, usando comillas dobles o simples de manera que lo siguiente es válido:
{% ifequal section 'sitenews' %}
<h1>Noticias del sitio</h1>
{% endifequal %}
{% ifequal section "community" %}
<h1>Comunidad</h1>
{% endifequal %}
Al igual que {% if %}, la etiqueta {% ifequal %} soporta una {% else %} opcional:
{% ifequal section 'sitenews' %}
<h1>Noticias del sitio</h1>
{% else %}
<h1>Aquí no hay noticias</h1>
{% endifequal %}
Sólo se admiten variables de plantilla, cadenas, enteros y números decimales como argumentos a {% ifequal %}. Los siguientes son ejemplos válidos:
{% ifequal variable 1 %}
{% ifequal variable 1.23 %}
{% ifequal variable 'foo' %}
{% ifequal variable "foo" %}
No se puede codificar de forma prefijada en {% ifequal %} ningún otro tipo de valor de Python, como diccionarios, listas o booleanos. Estos ejemplos no son válidos:
{% ifequal variable True %}
{% ifequal variable [1, 2, 3] %}
{% ifequal variable {'key': 'value'} %}
Si necesita evaluar si algo es cierto o falso, use etiquetas {% if %} en lugar de {% ifequal %}.
Comentarios
Al igual que en el HTML o en un lenguaje de programación como Python, el lenguaje de plantillas de Django admite comentarios. Para indicar un comentario utilice {# #}. Por ejemplo:
{# Esto es un comentario #}
El comentario no aparecerá en el producto final de la plantilla.
Un comentario no puede extenderse por varias líneas. En la siguiente plantilla, la salida mostrada aparecerá exactamente de la misma manera que en la plantilla (es decir, la etiqueta de comentario no será tratada como tal):
Esto es una {# el comentario va aquí
y salta a otra línea #}
prueba.
Filtros
Como explicamos anteriormente en este capítulo, los filtros de las plantillas son una manera sencilla de alterar el valor de las variables antes de que se muestren.
Los filtros se parecen a esto:
{{ nombre|lower }}
Esto muestra el valor de la variable {{ nombre }} tras pasarla a través del filtro lower, que convierte el texto a minúsculas. Utilice una tubería (|) para aplicar el filtro.
Los filtros se pueden encadenar (o sea, la salida de un filtro se aplica al siguiente). Aquí tiene una expresión común usada para "escapar" el contenido del texto, convirtiendo luego los saltos de línea en etiquetas <p>:
{{ mi_texto|escape|linebreaks }}
Algunos filtros toman argumentos. Los argumentos de los filtros se indican así:
{{ bio|truncatewords:"30" }}
Esto muestra las primeras 30 palabras de la variable bio. Los argumentos de los filtros siempre se escriben entre comillas dobles.
Aquí están algunos de los filtros más importantes:
-
addslashes: Añade una barra invertida antes de cualquier otra barra invertida, comilla simple o doble. Esto es útil si estamos produciendo texto dentro de una cadena de JavaScript.
-
date: Da formato a un objeto date o datetime de acuerdo a la cadena de formato dada como parámetro. Por ejemplo:
{{ fecha_pub|date:"F j, Y" }}Las cadenas de formato se definen en el Apéndice 6.
-
escape: Escapa ampersands, comillas y signos mayor/menor en la cadena dada. Esto es útil para limpiar los datos enviados por el usuario y para asegurarse de que los datos son XML o XHTML válido. En particular, escape realiza las siguientes conversiones: - Convierte & en & - Convierte < en < - Convierte > en > - Convierte " (comillas dobles) en " - Convierte ' (comilla simple) en '
-
length: Devuelve la longitud del valor. Se puede usar con una lista o una cadena, o con cualquier objeto de Python que sepa cómo determinar su longitud (es decir, cualquier objeto que tenga un método __len__()).
Filosofías y limitaciones
El sistema de plantillas tiene sus raíces en la manera en que se hace el desarrollo Web en World Online y la experiencia combinada de los creadores de Django. Aquí tiene unas pocas de esas filosofías:-
La lógica de negocio debería estar separada de la lógica de presentación. Vemos el sistema de plantillas como una herramienta que controla la lógica relacionada con la presentación (y eso es todo). El sistema de plantillas no debería ofrecer funcionalidad más allá de su objetivo básico.
Por esta razón, es imposible ejecutar código Python directamente desde las plantillas de Django. Toda la "programación" está limitada fundamentalmente por el ámbito de lo que pueden hacer las etiquetas de plantilla. Es posible escribir etiquetas personalizadas que hagan cosas arbitrarias, pero las que ofrece Django de serie no admiten la ejecución de código arbitrario de Python.
-
La sintaxis debería desacoplarse de HTML/XML. Aunque el sistema de plantillas de Django se usa principalmente para emitir HTML, está pensado para ser igual de útil para formatos diferentes a HTML, tal como el texto plano. Algunos otros lenguajes de plantillas se basan en XML, poniendo la lógica de la plantilla dentro de etiquetas o atributos de XML, pero Django evita esta limitación de forma deliberada. Requerir XML válido para escribir plantillas introduce un mundo de errores humanos y mensajes de error difíciles de entender, y usar un motor XML para interpretar las plantillas incurre en un nivel inaceptable de carga en el proceso de las plantillas.
-
Se asume que los diseñadores tienen que estar cómodos con el código HTML. El sistema de plantillas no está diseñado de manera que las plantillas tengan necesariamente que verse bien en editores WYSIWYG tales como Dreamweaver. Esta limitación es demasiado importante y no permitiría que la sintaxis fuera tan buena como es. Django espera de los autores de plantillas que estén cómodos editando HTML de forma directa.
-
Se asume que los diseñadores no son programadores de Python. Los autores del sistema de plantillas reconocen que las plantillas de páginas Web las escriben más a menudo diseñadores, no programadores, y por tanto no debería asumírseles conocimientos de Python.
Sin embargo, el sistema también pretende acomodarse a equipos pequeños en los que las plantillas las crean programadores de Python. Ofrece una manera de extender la sintaxis del sistema escribiendo código en Python (más sobre esto en el Capítulo 10).
-
El objetivo es no inventar un lenguaje de programación. El objetivo es ofrecer sólo la funcionalidad programática necesaria, tal como la disyunción y los bucles, que son esenciales para para tomar decisiones relacionadas con la presentación.
Como resultado de estas filosofías de diseño, el sistema de plantillas de Django tiene las siguientes limitaciones:
- Una plantilla no puede establecer o cambiar el valor de una variable. Es posible escribir etiquetas de plantilla que consigan este objetivo (véase el capítulo 10), pero las etiquetas base de Django no lo permiten.
- Una plantilla no puede invocar código de Python. No hay manera de "entrar en modo Python" o usar estructuras de Python directamente. De nuevo, es posible escribir etiquetas de plantilla que lo hagan, pero las que incorpora el propio Django no lo admiten.
La etiqueta include
Esta etiqueta le permite incluir el contenido de otra plantilla. El argumento de la etiqueta debería ser el nombre de la plantilla a incluir, y éste a su vez puede ser una variable o una cadena prefijada, usando comillas dobles o simples.
Estos dos ejemplos incluyen el contenido de la plantilla nav.html. Los ejemplos son equivalentes e ilustran que se permiten ambos tipos de comilla:
{% include 'nav.html' %}
{% include "nav.html" %}
Este ejemplo incluye el contenido de la plantilla includes/nav.html:
{% include 'includes/nav.html' %}
Este ejemplo incluye el contenido de la plantilla cuyo nombre esté contenido en la variable nombre_plantilla:
{% include nombre_plantilla %}
El nombre del fichero se determina añadiendo el nombre de la plantilla al directorio de plantillas que se obtiene de TEMPLATE_DIRS.
Si una plantilla contiene algún tipo de código (etiquetas o variables) éste será evaluado dentro del contexto de la plantilla que lo incluye.
Si no se encuentra una plantilla con el nombre solicitado, Django hará una de dos cosas:
- Si la opción DEBUG es True, veremos aparecer la excepción TemplateDoesNotExist en una página de error de Django.
- Si la opción DEBUG es False, la etiqueta no emitirá ningún error, y no se mostrará nada en su lugar.
Herencia de plantillas
Hasta ahora nuestros ejemplos de plantillas han sido pequeños trozos de HTML, pero en el mundo real usaremos el sistema de plantillas de Django para producir páginas completas. Esto nos lleva a un problema común en el desarrollo Web: ¿cómo reducir la duplicación y redundancia de áreas comunes, tales como la navegación por el sitio, en las páginas de un sitio web?
Una manera clásica de resolver este problema es usar server-side includes, directivas que se pueden insertar en las páginas HTML para "incluir" una página Web dentro de otra. De hecho, Django admite este enfoque usando la etiqueta {% include %} que describimos antes. Pero la manera preferente de resolver este problema con Django es usar una estrategia más elegante llamada herencia de plantillas.
En esencia, la herencia de plantillas nos permite construir una plantilla básica "esqueleto" que contiene todas las partes comunes del sitio y define "bloques" que las plantillas descendientes pueden sustituir.
Veamos un ejemplo de esto creando una plantilla más completa para nuestra vista current_datetime, editando el fichero current_datetime.html:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="es">
<head>
<title>La hora actual</title>
</head>
<body>
<h1>Mi sitio de fecha/hora</h1>
<p>El momento actual es {{ fecha_actual }}.</p>
<hr>
<p>Gracias por visitar mi sitio.</p>
</body>
</html>
Tiene buena pinta, pero ¿qué sucede cuando queremos crear una plantilla para otra vista (digamos, la hours_ahead del Capítulo 3)? Si de nuevo queremos tener una bonita plantilla llena de HTML válido, crearemos algo así:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="es">
<head>
<title>Tiempo futuro</title>
</head>
<body>
<h1>Mi sitio de fecha/hora</h1>
<p>En {{ hour_offset }} hora(s), será {{ next_time }}.</p>
<hr>
<p>Gracias por visitar mi sitio.</p>
</body>
</html>
Está claro que hemos duplicado bastante HTML. Imagine si tuviéramos que incluir unas pocas páginas de estilo en cada página, quizá también una barra de navegación, a lo mejor algo de JavaScript... Terminaríamos incluyendo todo tipo de HTML redundante en cada plantilla.
La solución server-side include a este problema sería sacar los trozos comunes de cada plantilla y guardarlos como trozos separados, que podríamos incluir en cada una. Quizá almacenaríamos la parte superior de la plantilla en un fichero llamado cabecera.html:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="es">
<head>
Y a lo mejor pondríamos la parte inferior en un fichero llamado pie.html:
<hr>
<p>Gracias por visitar mi sitio.</p>
</body>
</html>
Con una estrategia basada en inclusiones es fácil hacer cabeceras y pies. Pero lo de en medio se vuelve complicado. En este ejemplo ambas páginas tienen el mismo título (<h1>My helpful timestamp site</h1>), que no podemos incluir en cabecera.html porque el <title> de cada página es diferente. Si incluyéramos el <h1> en la cabecerá tendríamos que incluir también el <title>, lo que no nos permitiría personalizarlo en cada página. ¿Ve a dónde lleva esto?
El sistema de herencia de plantillas de Django resuelve estos problemas. Puede pensar en esto como un server-side includes al revés. En lugar de definir los trozos que son comunes, definimos los que son diferentes.
El primer paso es crear una plantilla base (un esqueleto de la página que rellenarán las plantillas hijas más adelante). Aquí tenemos una plantilla base para nuestro ejemplo:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="es">
<head>
<title>{% block title %}{% endblock %}</title>
</head>
<body>
<h1>Mi sitio de fecha/hora</h1>
{% block content %}{% endblock %}
{% block footer %}
<hr>
<p>Gracias por visitar mi sitio.</p>
{% endblock %}
</body>
</html>
Esta plantilla, a la que llamaremos base.html, define un sencillo documento HTML esqueleto que usaremos para todas las páginas del sitio. La tarea de las plantillas hijas será sustituir, o añadir algo a, o dejar sin tocar el contenido de los bloques (si va a seguir con este ejemplo, guarde el fichero en su directorio de plantillas).
Estamos usando una etiqueta de plantilla que no hemos visto anteriormente (la {% blogk %}). Todo lo que hacen las etiquetas {% block %} es decirle al sistema que una plantilla hija puede sustituir esas porciones de plantilla por otra cosa.
Ahora que tenemos esta plantilla base podemos definir nuestra plantilla current_datetime.html para que la use:
{% extends "base.html" %}
{% block title %}La hora actual{% endblock %}
{% block content %}
<p>El momento actual es {{ fecha_actual }}.</p>
{% endblock %}
Ya que estamos a ello, vamos a crear una plantilla para la vista hours_ahead del Capítulo 3 (si tiene escrito el código, le dejaremos a usted que modifique hours_ahead para usar el sistema de plantillas). El aspecto de la plantilla sería éste:
{% extends "base.html" %}
{% block title %}Tiempo futuro{% endblock %}
{% block content %}
<p>En {{ hour_offset }} hora(s), será {{ next_time }}.</p>
{% endblock %}
¿No es hermoso? Cada plantilla contiene sólo el código que la diferencia. No hace falta redundancia. Si necesitamos un cambio global del sitio, lo hacemos en base.html y el resto de las plantillas reflejarán el cambio inmediatamente.
Así funciona:
-
Cuando se carga la plantilla current_datetime.html, el sistema ve la etiqueta {% extends %} y se da cuenta de que trata con una plantilla que hereda de otra. Carga inmediatamente la plantilla madre (base.html, en este caso).
-
Llegado aquí, el sistema encuentra las tres etiquetas {% block %} dentro de base.html y las sustituye con el contenido de la plantilla hija. De esta manera, se usará el título que hemos definido en {% block title %}, así como lo que hay en {% block content %}.
Observe que como la plantilla hija no define un bloque footer, el sistema utiliza el valor proporcionado por la plantilla madre. Siempre se usa como valor por omisión el contenido de una etiqueta {% block %} definida en una plantilla ascendiente.
Se pueden usar tantos niveles de herencia como se necesiten. Una manera común de usar la herencia es la siguiente técnica de tres niveles:
- Cree una plantilla base.html que contenga el aspecto principal del sitio. Esto es algo que se cambia raramente (si es que cambia).
- Cree una plantilla base_SECCION.html para cada "sección" del sitio. Por ejemplo, base_fotos.html, base_forum.html. Todas estas plantillas extienden a base.html e incluyen estilo o diseño específicos de cada sección.
- Cree plantillas individuales para cada tipo de página, como pueda ser una página de foro o una galería de fotos. Estas plantillas extienden a las de la sección apropiada.
Este enfoque maximiza la reutilización de código y es sencillo añadir elementos a las áreas compartidas, como por ejemplo una barra de navegación para la sección.
Algunos consejos para trabajar ocn herencia de plantillas:
- Si usa {% extends %} en una plantilla, debe ser la primera etiqueta de todas la plantilla. En caso contrario la herencia no funcionará.
- Por lo general, cuantas más etiquetas {% block %} haya en las plantillas base, mejor. Recuerde que las plantillas hijas no tienen por qué redefinir todos los bloques de las que heredan, así que podemos introducir valores razonables por omisión en todos los bloques que queramos, y luego definir sólamente los que necesitamos en las plantillas herederas. Es mejor tener más enganches que menos.
- Si se encuentra ducplicando código en varias plantillas, posiblemente eso signifique que debería mover ese código a un {% block %} en una plantilla madre.
- Si necesita obtener el contenido del bloque de la plantilla madre, lo podrá hacer usando la variable {{ block.super }}. Esto es útil si sólo quiere añadir algo al contenido del bloque heredado en lugar de sustituirlo por completo.
- No puede definir varias etiquetas {% block %} con el mismo nombre dentro de la misma plantilla. Esta limitación existe debido a que la etiqueta block funciona en "ambas" direcciones. Es decir, una etiqueta block no sólo ofrece a las plantillas hijas un hueco que llenar, sino que también define el contenido que llenará el hueco en la plantilla madre. Si hubiera dos etiquetas {% block %} de igual nombre en la misma plantilla, la ascendiente no sabría cual de ellas usar para sustituir a la suya.
- El nombre de plantilla que se pasa a {% extends %} se carga usando el mismo método de get_template(). O sea, el nombre de la plantilla se añade a lo que haya en TEMPLATE_DIRS.
- En la mayoría de los casos el argumento de {% extends %} será una cadena, pero también puede ser una variable si es que no vamos a saber el nombre de la plantilla madre hasta que vayamos a mostrarla. Esto permite hacer algunas cosas dinámicas bastante interesantes.
Aplicando lo aprendido a nuestra aplicación de encuestas
Crear mysite/polls/templates/polls/base.html con este contenido:
Queremos mostrar las encuestas más votadas y las más recientes en la página principal del sitio. Para ello vamos a tener que refactorizar varias partes de nuestra aplicación. En particular, la vista genérica que teníamos para listar las encuestas ya no nos sirve. Modificar mysite/polls/urls.py para que luzca así:
"""Map URLs to views for the polls application
"""
import os
from django.conf.urls.defaults import *
from mysite.polls.models import Poll
info_dict = {
'queryset': Poll.objects.all(),
}
urlpatterns = patterns('',
(r'^$', 'mysite.polls.views.index'),
(r'^(?P<object_id>\d+)/$', 'django.views.generic.list_detail.object_detail', info_dict, 'poll_detail'),
url(r'^(?P<object_id>\d+)/results/$', 'django.views.generic.list_detail.object_detail',
dict(info_dict, template_name='polls/results.html'), 'poll_results'),
(r'^(?P<poll_id>\d+)/vote/$', 'mysite.polls.views.vote'),
(r'^media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': os.path.join(os.path.dirname(__file__), 'media')}),
)
Algo que también estamos haciendo acá es declarar una carpeta donde colocaremos archivos estáticos (media). Usualmente esto se hace sólo a los fines del desarrollo porque es lento e inseguro. A la hora de poner el sistema en producción, los archivos estáticos deben ser manejados por Apache o el servidor web que se utilice. Con esto podemos crear la carpeta mysite/polls/media y colocar allí el logo.
Ahora hagamos las modificaciones necesarias para tener la página principal lista. Modificar mysite/polls/views.py para que luzca así:
"""Views for the polls application
"""
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.shortcuts import render_to_response, get_object_or_404
from mysite.polls.models import Poll, Choice
def vote(request, poll_id):
"""View that handles the poll voting
"""
p = get_object_or_404(Poll, pk=poll_id)
try:
selected_choice = p.choice_set.get(pk=request.POST['choice'])
except (KeyError, Choice.DoesNotExist):
# Redisplay the poll voting form.
return render_to_response('polls/poll_detail.html', {
'object': p,
'error_message': "You didn't select a choice.",
})
else:
selected_choice.votes += 1
selected_choice.save()
# Always return an HttpResponseRedirect after successfully dealing
# with POST data. This prevents data from being posted twice if a
# user hits the Back button.
return HttpResponseRedirect(reverse('poll_results', args=(p.id,)))
def index(request):
"""A view that shows the most popular polls. Popupar as in more
voted.
"""
popular = Poll.objects.all()
popular = list(popular)
popular.sort(key=Poll.votes)
popular.reverse()
latest = Poll.objects.all().order_by('-pub_date')
return render_to_response('polls/index.html', {'latest': latest,
'popular': popular})
Renombrar poll_list.html a index.html y modificar su contenido para que luzca así:
{% extends 'polls/base.html'%}
{% block navigation %}
{% if latest %}
<h2>Ãltimas encuestas</h2>
<ul>
{% for poll in latest %}
<li><a href="{% url poll_detail poll.id%}">{{ poll.question }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %}
{% endblock%}
{% block extra %}
{% if popular %}
<h2>Encuestas más votadas</h2>
<ul>
{% for poll in popular %}
<li><a href="{% url poll_detail poll.id%}">{{ poll.question }}</a>
{{ poll.votes }} vote{{ poll.votes|pluralize }}</li>
{% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %}
{% endblock%}
Finalmente actualizamos las demás plantillas para que extiendan a base.html. results.html debe lucir así:
{% extends 'polls/base.html'%}
{% block content%}
<h2>{{ object.question }}</h2>
<ul>
{% for choice in object.choice_set.all %}
{% ifequal choice.percentage '-'%}
<li>{{ choice.choice }} -- datos no disponibles</li>
{% else %}
<li>{{ choice.choice }} -- {{ choice.percentage }} %</li>
{% endifequal %}
{% endfor %}
</ul>
{% endblock %}
poll_detail.html debe lucir así:
{% extends 'polls/base.html'%}
{% block content%}
<h2>{{ object.question }}</h2>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
<form action="/polls/{{ object.id }}/vote/" method="post">
{% for choice in object.choice_set.all %}
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
<label for="choice{{ forloop.counter }}">{{ choice.choice }}</label><br />
{% endfor %}
<input type="submit" value="Vote" />
</form>
<h2>Resultados</h2>
<ul>
{% for choice in object.choice_set.all %}
{% ifequal choice.percentage '-'%}
<li>{{ choice.choice }} -- datos no disponibles</li>
{% else %}
<li>{{ choice.choice }} -- {{ choice.percentage }} %</li>
{% endifequal %}
{% endfor %}
</ul>
{% endblock%}
Y solucionamos un problema en el método que calcula el porcentaje de cada opción en models.py:
def percentage(self):
"""Returns the percentage of votes for this choice"""
if self.poll.votes() > 0:
return '%.2f' % (self.votes*100.0/self.poll.votes(),)
else:
return '-'
Ejercicios
- Crear un formulario de búsqueda que busque de acuerdo al texto
de las preguntas. Crear también la plantilla que muestre los resultados.
Ayuda:
>>> from django.db.models import Q
>>> from mysite.polls.models import Poll
>>> qset = (Q(question__icontains='color'))
>>> results = Poll.objects.filter(qset).distinct()
>>> results
[<Poll: ¿Cuál es tu color favorito?>]
>>>results tiene todas las encuestas que tengan la palabra 'color' en la pregunta.
- Integrar el formulario de búsqueda y la plantilla de resultados con el resto del sistema de encuestas.