Lo explicado en este tema debería ser suficiente para crear un proyecto Django simple que interactúe con una base de datos. Aquí se van a ir exponiendo los conceptos y los distintos elementos que intervienen. En el tema siguiente, donde explicaré el trabajo del curso, podréis ver un proyecto Django al completo y comprender mejor como se unen todas las piezas del curso: HTML + CSS + Python + Django.
Índice
- Introducción.
- Vistas.
- Configuración de las urls.
- Añadimos una vista y url algo más compleja.
- El sistema de plantillas de Django.
- El modelo de datos.
- Procesamiento de formularios.
- Seguridad.
- El sitio de administración de Django.
1. Introducción
Django es un entorno de desarrollo web escrito en Python que fomenta el desarrollo rápido y el diseño limpio y pragmático. Es un framework web de código abierto que permite construir aplicaciones web más rápido y con menos código.
Django fue inicialmente desarrollado para gestionar aplicaciones web de páginas orientadas a noticias de World Online, más tarde se liberó bajo licencia BSD. Django se centra en automatizar todo lo posible y se adhiere al principio DRY (Don’t Repeat Yourself).
Filosofía
Para comprender el funcionamiento del framework imaginemos que un usuario realiza una petición a nuestra web mediante una url (dirección web). Debemos responder a esa petición mostrando la página que corresponda a esa dirección, para eso Django primero consulta qué debe hacer con esa dirección entrante (urls.py) y posteriormente le pasa la petición a la vista correspondiente (views.py) que es quien realizará las consultas y operaciones pertienentes a la base de datos (models.py), puede que utilice la definición de alguno de los formularios (forms.py), y finalmente devuelve el resultado formateado según se exprese en la plantilla que debe utilizar (templates).
Todo ese proceso se expresa en el siguiente diagrama:
Estructura de un proyecto
Un proyecto Django es una colección de configuraciones para una instancia de Django (no penséis en instancias de OO), incluyendo configuración de base de datos, opciones específicas de Django y configuraciones específicas de aplicaciones.
Cada proyecto Django puede contener distintas aplicaciones que pueden considerarse como elementos relativamente independientes de un mismo proyecto. Por ejemplo si tenemos un proyecto llamado empresa podríamos tener dos aplicaciones dentro como son tienda y nominas, y que una implemente la tienda virtual y la otra la gestión de nóminas.
Está organizado por directorios donde, como acabamos de decir, un mismo proyecto engloba una serie de aplicaciones Django, con una configuración común. El proyecto podemos estructurarlo como queramos, aunque una buena estructura podría ser la siguiente:
projecto/ __init__.py manage.py settings.py urls.py aplicacion/ __init__.py models.py views.py templates/ aplicacion/ pagina.html (por ejemplo) ... static/ ...
Donde:
- __init__.py Un archivo requerido para que Python trate a este directorio como un paquete.
- manage.py Una utilidad de línea de comandos suministrada por Django que permite interactuar con el proyecto de varias formas.
- settings.py Opciones/configuraciones para este proyecto de Django.
- urls.py La declaración de las URL que serán accesibles para este proyecto de Django y qué vista se debe invocar.
- models.py Contiene el modelo de datos de la aplicación.
- views.py Contiene las vistas que se utilizarán en función de la url a la que se esté accediendo.
- templates/ Directorio que contiene las plantillas de cada aplicación.
- pagina.html Plantilla HTML que se renderizará según lo pasado por la vista correspondiente y que se mostrará en el navegador del cliente.
- static/ Directorio con el contenido estático y públicamente accesible del proyecto: CSS, imágenes, documentos, etc.
A modo de ejemplo según lo anterior podríamos tener el proyecto:
empresa/ __init__.py manage.py settings.py urls.py tienda/ __init__.py models.py views.py nominas/ __init__.py models.py views.py templates/ tienda/ index.html ver_producto.html carrito.html nominas/ index.html ver_empleado.html nuevo_contrato.html static/ images/ css/ js/
Por tanto, los pasos básicos para crear una aplicación web en Django son los siguientes:
- Definir el modelo de datos (models.py).
- Definir el acceso a la información mediante la definición de las urls (urls.py) que serán accesibles.
- Implementar las vistas (views.py).
- Escribir el código de las plantillas HTML (*.html).
En este tema vamos a ir viendo los pasos de creación de un proyecto Django, aunque no en ese orden, para mayor claridad.
Configuración de un proyecto Django
A lo largo de este tema se irá mencionando el fichero de configuración del proyecto (settings.py), en él es donde indicamos: la base de datos a utilizar, las distintas aplicaciones que componen el proyecto, cual es la carpeta donde están las plantillas (generalmente templates), en qué carpeta están los archivos multimedia (como imágenes y css), etc.
No vamos a ver en este tema este fichero para no distraer con temas de configuración, cuando veamos el trabajo del curso ya explicaremos en qué consiste y cómo se define.
Nota sobre el código en este tema
Algunas líneas de los ejemplos de código por problemas de espacio vendrán separadas por el caracter \ que en Django se utiliza para continuar una línea más abajo. Así, es lo mismo escribir:
texto = "Buenas tardes"
Que:
texto = "Buenas \ tardes"
2. Vistas
Es el controlador en el modelo vista/controlador, contiene el grueso del código del proyecto, interactúa con la base de datos y devuelve normalmente una página web, rellenando una de las plantillas [1].
Comenzaremos viendo un ejemplo de vista cuto objetivo es mostrar el día y hora actual, por lo que es un buen ejemplo al ser una página dinámica que no requiere de uso de bases de datos.
Para ello crearemos una función de vista que recibe la petición web (request la solemos llamar siempre) y devuelve una respuesta web, en nuestro caso un documento HTML, aunque podría haber sido una redirección, una imagen, un documento, etc.
En principio el código puede encontrarse donde queramos, mientras que se encuentre dentro del Python path, pero como hemos comentado lo coherente es colocarlo en el archivo views.py de la aplicación.
from django.http import HttpResponse import datetime def hora_actual(request): ahora = datetime.datetime.now() html = u"<html><body>Día y hora actual: %s.</body></html>" % ahora return HttpResponse(html)
Explicando el código:
- Importamos la clase HttpResponse, la cual pertenece al módulo django.http.
- Importamos el módulo datetime de la biblioteca estándar de Python visto en el tema anterior para trabajar con fechas.
- Definimos la vista hora_actual. Cada vista tiene como primer argumento un objeto HttpRequest, que solemos llamar request. En la siguiente sección (urls.py) explicaremos cómo Django invoca a esta funcion.
- Almacenamos la fecha/hora actual, como un objeto datetime.datetime, en la variable local ahora.
- La segunda línea de código dentro de la función construye la respuesta HTML usando el formato de cadena de caracteres de Python. El %s dentro de la cadena de caracteres es un marcador de posición, y el signo porcentaje después de la cadena de caracteres, significa «Reemplaza el %s por el valor de la variable ahora.». Usamos un código HTML inválido por reducir su tamaño en el ejemplo.
- Por último, retorna un objeto HttpResponse que contiene la respuesta generada. Toda vista debe retornar un objeto HttpResponse.
3. Configuración de las urls
Como hemos comentado en la introducción es en las URLconf donde le decimos a Django qué vista debe invocar para una url dada [2]. Recuerda que estas funciones de vista deben estar en el Python path, en el fichero urls.py.
En nuestro ejemplo:
from django.conf.urls.defaults import patterns, url from aplicacion.views import hora_actual urlpatterns = patterns('', url(r'^hora/$', hora_actual), )
- La primera línea importa todos los objetos desde el módulo django.conf.urls.defaults, incluyendo una función llamada patterns.
- La segunda importa la vista anterior, hora_actual, que está dentro de las vistas de la aplicacion aplicacion (aplicacion/views.py).
- La tercera línea llama a la función patterns() y guarda el resultado en una variable llamada urlpatterns. La función patterns() recibe un primer argumento, una cadena de caracteres que puede ser usada para proveer un prefijo común para las funciones de vista, el resto de las líneas especifican las reglas de mapeo.
- En la línea «(r’^hora/$’, hora_actual),» especificamos que cualquier petición a la URL /hora invoque la vista hora_actual. Esta línea hace referencia a un URLpattern, una tupla de Python en dónde el primer elemento es una expresión regular simple y el segundo elemento es la función de vista que usa para ese patrón.
- Pasamos la función de vista hora_actual como un objeto, sin llamar a la función (hora_actual(…)). Esto es una característica de Python y otros lenguajes dinámicos: las funciones son objetos de primera clase, lo cual significa que pueden pasarse como cualquier otra variable.
- La r en r’^hora/$’ significa que ‘^hora/$ es una cadena de caracteres en crudo de Python. Esto permite que las expresiones regulares sean escritas sin demasiadas sentencias de escape.
- Se puede excluir la barra al comienzo de la expresión ‘^hora/$’ para que coincida con /hora/. Django automáticamente agrega una barra antes de toda expresión. A primera vista esto parece raro, pero una URLconf puede ser incluida en otra URLconf, y no incluir la barra simplifica mucho las cosas.
- El acento circunflejo (^) significa que «requiere que el patrón concuerde con el inicio de la cadena de caracteres», y el signo de dólar ($) significa que «exige que el patrón concuerde con el fin de la cadena». Este concepto se explica mejor con un ejemplo. Si hubiéramos utilizado el patrón ‘^hora/’ (sin el signo de dólar al final), entonces cualquier URL que comience con hora/ concordaría, así como /hora/foo y /hora/bar, no sólo /hora/. Del mismo modo, si dejamos de lado el carácter acento circunflejo inicial (‘hora/$’), Django concordaría con cualquier URL que termine con hora/, así como /foo/bar/hora/. Por lo tanto, usamos tanto el acento circunflejo como el signo de dólar para asegurarnos que sólo la URL /hora/ concuerde. Nada más y nada menos. Si se accede a /hora, ocurre lo esperado, que se convierte en /hora/ (a través de un redireccionamiento) siempre y cuando APPEND_SLASH tenga asignado el valor True.
Errores 404
En las URLconf anteriores, hemos definido un solo patrón URL: el que maneja la petición para la URL /hora. ¿Qué pasaría si se solicita una URL diferente? Pues que se lanzaría un error 404 de página no encontrada.
Si estamos en la fase de depuración (debug mode) los errores en Django muestran una serie de información útil. En el caso de un error 404 la utilidad de esta página va más allá del mensaje básico de error 404; nos dice también, qué URLconf utilizó Django y todos los patrones de esa URLconf. Con esa información, tendríamos que ser capaces de establecer porqué la URL solicitada lanzó un error 404.
Naturalmente, esta información no se mostraría en un sitio en producción.
4. Añadimos una vista y url algo más compleja
En la vista hora_actual, el contenido de la página era dinámico, pero la URL (/hora) era estática, siempre la misma. En la mayoría de las aplicaciones Web, sin embargo, la URL contiene parámetros que influyen en la salida de la página.
Vamos a crear una segunda vista que nos muestre la fecha y hora actual con un adelanto de ciertas horas. El objetivo es montar un sitio en la que la página /hora/mas/1/ muestre la fecha/hora actual con una hora más, la página /hora/mas/2/ muestre la fecha/hora con dos horas más, etc.
En otros lenguajes esto lo haríamos enviando por GET el parámetro horas, obteniendo una URL del tipo: /hora/mas?horas=3. Sin embargo en Django las urls son más “bonitas” y simples, por tanto mejor valoradas por los motores de búsqueda. Vamos a invocar a la vista con una URL del tipo /hora/mas/3/.
Para ello indicamos el patrón que especifica el número de las horas en el archivo urls.py, usamos el patrón de expresión regular \d+ para que coincida con uno o más dígitos:
from django.conf.urls.defaults import patterns, url from mysite.views import hora_actual, hora_actual_mas urlpatterns = patterns("", url(r'^hora/$', hora_actual), url(r'^hora/mas/\d+/$', hora_actual_mas), )
Este patrón coincidirá con cualquier URL que sea como /hora/mas/2/, /hora/mas/25/, o también /hora/mas/100000000000/. Por lo que podríamos limitar el número máximo de horas en dos dígitos, lo que nos quedaría así: \d{1,2}.
url(r'^hora/mas/\d{1,2}/$', hora_actual_mas),
Le asignaremos un nombre a ese patrón, en nuestro caso son las horas a sumar, para ello indicamos: (?P<horas>\d{1,2}). El archivo urls.py queda entonces como:
from django.conf.urls.defaults import patterns, url from mysite.views import hora_actual, hora_actual_mas urlpatterns = patterns("", url(r'^hora/$', hora_actual), url(r'^hora/mas/(?P<horas>\d{1,2})/$', hora_actual_mas), )
Y definiríamos la vista hora_actual_mas muy parecida a la de antes pero con un argumento horas a aumentar:
import django.http.HttpResponse import datetime def hora_actual_mas(request, horas): horas = int(horas) tiempo = datetime.datetime.now() + datetime.timedelta(hours=horas) html = "<html><body>Dentro de %s hora(s), serán las %s. \ </body></html>" % (horas, tiempo) return HttpResponse(html)
Convertimos el argumento horas en entero para poder sumarlo a la fecha/hora actual.
También podríamos haber utilizado una única vista, de manera que una url llame a la vista pasando número de horas a sumar y la otra deje el valor por defecto, cero.
from django.conf.urls.defaults import patterns, url from mysite.views import hora_actual, hora_actual_mas urlpatterns = patterns("", url(r'^hora/$', hora_actual), url(r'^hora/mas/(?P<horas>\d{1,2})/$', hora_actual), )
Y la vista:
import django.http.HttpResponse import datetime def hora_actual(request, horas=0): horas = int(horas) tiempo = datetime.datetime.now() + datetime.timedelta(hours=horas) if horas: html = "<html><body>Dentro de %s hora(s), serán las %s. \ </body></html>" % (horas, tiempo) else: html = u"<html><body>Día y hora actual: %s.</body></html>" \ % tiempo return HttpResponse(html)
5. El sistema de plantillas de Django
Hasta ahora, en los ejemplos anteriores, hemos incluído el código HTML en la vista. algo totalmente desaconsejado ya que:
- Cualquier cambio en el diseño de la página requiere un cambio en el código de Python.
- Escribir código Python y diseñar HTML son dos disciplinas diferentes, y deben estar lo más separado posible.
- No permite que programadores y diseñadores trabajen a la vez.
Por esas razones, es mucho más limpio y mantenible separar diseño y programación, para lo que usaremos el sistema de plantillas de Django. Éste factor es muy importante en el desarrollo de aplicaciones web y que Django implementa de una manera muy efectiva.
Sistema básico de plantillas
Una plantilla de Django es una cadena de texto que pretende separar la presentación de un documento de sus datos [3]. Una plantilla define rellenos y diversos bits de lógica básica (esto es, etiquetas de plantillas) que regulan como debe ser mostrado el documento. Normalmente, las plantillas son usadas para producir HTML, pero las plantillas de Django son igualmente capaces de generar cualquier formato basado en texto.
Veamos un ejemplo de respuesta de una web ante un pedido de productos:
<html> <head> <title>Datos del pedido</title> </head> <body> <p>{{ nombre }}, gracias por su pedido a {{ empresa }}.</p> <p>Envío estimado: {{ fecha_envio|date:"d/m/Y H:i" }}.</p> <p>Productos del pedido:</p> <ul> {% for producto in productos %} <li>{{ producto }}</li> {% endfor %} </ul> {% if en_garantia %} <p>La documentación sobre la garantía va en el paquete.</p> {% endif %} <p>Atentamente, {{ empresa }}.</p> </body> </html>
Esta plantilla es un HTML básico con algunas variables y etiquetas de plantillas agregadas. Vamos paso a paso a través de ésta:
- Cualquier texto encerrado por un par de llaves (por ej. {{ nombre }}) es una variable. Esto significa “insertar el valor de la variable con ese nombre”. Esa variable la habremos pasado previamente desde la vista.
- Cualquier texto que esté rodeado por llaves y signos de porcentaje (por ej. {% if en_garantia %}) es una etiqueta de plantilla [4], que nos permiten dotar a las plantillas de algunos mecanismos simples de programación para simplificar la maquetación.
- Este ejemplo de plantilla contiene dos etiquetas: la etiqueta {% for producto in productos %} (muy similar al for de Python) y la etiqueta {% if en_garantia %} (similar a la sentencia if).
- Si el valor de la variable en_garantia se evalúa como True el sistema de plantillas mostrará todo lo que hay entre {% if en_garantia %} y {% endif %}. Si no, el sistema de plantillas no mostrará esto. El sistema de plantillas también admite {% else %} y otras cláusulas lógicas.
- Finalmente, el segundo párrafo de esta plantilla tiene un ejemplo de filtro, con el cual puedes alterar la exposición de una variable. En este ejemplo, {{ fecha_envio|date:»d/m/Y H:i» }}, estamos pasando la variable fecha_envio por el filtro date, tomando el filtro date el argumento «d/m/Y H:i» (día/mes/año horas:minutos). El filtro date formatea fechas en el formato dado, especificado por ese argumento. Los filtros se encadenan mediante el uso de un caracter pipe (|), como una referencia a las tuberías de Unix. Por tanto podríamos poner unas a continuación de otras.
Cada plantilla de Django tiene acceso a varias etiquetas y filtros incorporados, algunos de los cuales serán tratados en la sección que sigue. Existe una serie de etiquetas y filtros proporcionado por Django aunque también es posible crear tus propios filtros y etiquetas [5], cuya explicación queda fuera de este tema.
Uso de plantillas en las vistas
Recordemos la vista hora_actual en aplicacion.views:
from django.http import HttpResponse import datetime def hora_actual(request): ahora = datetime.datetime.now() html = u"<html><body>Día y hora actual: %s.</body></html>" % ahora return HttpResponse(html)
Vamos a cambiar esta vista usando el sistema de plantillas de Django, por lo que el contenido de la plantilla estará, en nuestro ejemplo, en templates/aplicacion/hora_actual.html.
from django.template.loader import get_template from django.template import Context from django.http import HttpResponse import datetime def hora_actual(request): ahora = datetime.datetime.now() t = get_template('templates/aplicacion/hora_actual.html') html = t.render(Context({'fechahora_actual': ahora})) return HttpResponse(html)
Usamos la función django.template.loader.get_template() en vez de cargar la plantilla desde el sistemas de archivos manualmente. La función get_template() toma el nombre de la plantilla como argumento y retorna un objeto Template compilado.
Si get_template() no puede encontrar la plantilla con el nombre pasado, esta levanta una excepción TemplateDoesNotExist.
Debido a que es común cargar una plantilla, rellenar un Context, y retornar un objeto HttpResponse con el resultado de la plantilla renderizada, Django provee un atajo más cómodo de usar, la función render_to_response(), la cual se encuentra en el módulo django.shortcuts.
Aquí está el ejemplo actual hora_actual reescrito utilizando render_to_response():
from django.shortcuts import render_to_response import datetime def hora_actual(request): ahora = datetime.datetime.now() return render_to_response('aplicacion/hora_actual.html', {'fechahora_actual': ahora})
Ahora sí que vamos observando la simplicidad que Django hereda de Python, en solo dos líneas tenemos todo el código necesario. Esta vez, con render_to_response no indicamos la carpeta templates ya que la habremos configurado en el proyecto para indicar cual es la carpeta donde están las plantillas. Envíamos además la “fechahora_actual” para que se pueda mostrar en la plantilla. El segundo argumento no es obligatorio en la llamada.
El contenido de la plantilla podría ser:
<!DOCTYPE html> <html> <head> <title>Hora actual</title> </head> <body> <h1>Bienvenidos</h1> <p>Día y hora actual: {{ fechahora_actual }}</p> <p>Gracias por su visita.</p> </body> </html>
La etiqueta de plantilla include
La etiqueta include, al igual que en otros lenguajes de programación, permite incluir el contenido de una plantilla dentro de otra y evitar así tener código repetido. El argumento será el nombre de la plantilla a incluir, entre simples o dobles comillas.
Así incluiríamos el contenido de la plantilla nav.html:
{% include "nav.html" %}
O bien el contenido de la plantilla includes/nav.html:
{% include 'includes/nav.html' %}
Este ejemplo incluye el contenido de la plantilla cuyo nombre se encuentra en la variable nombre_plantilla:
{% include nombre_plantilla %}
Como en render_to_response, la plantilla se buscará dentro del directorio de plantillas (TEMPLATE_DIRS del settings.py), generalmente se le llama templates.
Herencia de plantillas
Sin duda esta es una de las características que más os va a gustar de Django, gracias a la herencia evitamos repetir el código HTML de las ditintas páginas (como la cabecera, el menú, el pie, etc.).
Una forma clásica de solucionar este problema es usar includes, insertando dentro de las páginas HTML a “incluir” una página dentro de otra. Es más, Django admite esta aproximación, con la etiqueta {% include %} anteriormente descrita. Pero la herencia de plantillas es una solución mucho más elegante.
En esencia, con la herencia de plantillas podemos construir una plantilla base “esqueleto” que contenga todas las partes comunes del sitio y definir “bloques” que los hijos puedan sobreescribir.
Veamos un ejemplo de esto creando una plantilla completa para nuestra ya conocidísima vista hora_actual, editando el archivo hora_actual.html:
<!DOCTYPE html> <html> <head> <title>Mostramos la hora actual</title> </head> <body> <h1>Bienvenidos</h1> <p>Día y hora actual: {{ fechahora_actual }}</p> <p>Gracias por su visita.</p> </body> </html>
Esto se ve bien, pero ¿Qué sucede cuando queremos crear una plantilla para otra vista, digamos, ¿la vista hora_actual_mas anterior? Si queremos hacer nuevamente una agradable, válida, y completa plantilla HTML, crearíamos algo como:
<!DOCTYPE html> <html> <head> <title>Mostramos la hora actual</title> </head> <body> <h1>Bienvenidos</h1> <p> {% if horas %} En {{ horas }} hora(s) serán las {{ fechahora }}. {% else %} Son las {{ fechahora }}. {% endif %} </p> <p>Gracias por su visita.</p> </body> </html>
Claramente, estaríamos duplicando una cantidad de código HTML, y eso que este ejemplo es pequeño, imagina si tuviéramos barra de navegación, hojas de estilo, JavaScript…
La solución a este problema usando includes en el servidor es sacar factor común de ambas plantillas y guardarlas en recortes de plantillas separados, que luego son incluidos en cada plantilla. Quizás quieras guardar la parte superior de la plantilla en un archivo llamado header.html:
<!DOCTYPE htlm> <html> <head>
Y quizás quieras guardar la parte inferior en un archivo llamado footer.html:
<p>Gracias por su visita.</p> </body> </html>
Con una estrategia basada en includes, la cabecera y la parte de abajo son fáciles. Es el medio el que queda desordenado. En este ejemplo, ambas páginas contienen un título <h1>, pero ese título no puede encajar dentro de header.html porque el <title> en las dos páginas es diferente. Si incluimos <h1> en la cabecera, tendríamos que incluir <title>, lo cual no permitiría personalizar este en cada página.
El sistema de herencia de Django soluciona estos problemas, en vez de definir los pedazos que son comunes, definimos los pedazos que son diferentes.
El primer paso es definir una plantilla base, un “esqueleto” de tu página que las plantillas hijas llenaran luego. Aquí hay una platilla para nuestro ejemplo actual:
<!DOCTYPE html> <html> <head> <title>{% block title %}Título por defecto{% endblock %}</title> </head> <body> <h1>{% block titulo %}{% endblock %}</h1> {% block contenido %}{% endblock %} {% block pie %}<p>Gracias por su visita.</p>{% endblock %} </body> </html>
Esta plantilla, que vamos a llamar base.html, define un documento esqueleto HTML simple que usaremos para todas las páginas del sitio. Es trabajo de las plantillas hijas sobreescribir, agregar o dejar vacío el contenido de los bloques.
Para ello usamos la etiqueta {% block %}, que le indica al motor de plantillas que una plantilla hijo quizás sobreescriba esa porción de la plantilla.
Ahora que tenemos una plantilla base, modificamos la plantilla hora_actual.html:
{% extends "base.html" %} {% block title %}Mostramos la hora actual{% endblock %} {% block titulo %}Bienvenidos{% endblock %} {% block contenido %} <p>Día y hora actual: {{ fechahora_actual }}</p> {% endblock %}
Cada plantilla contiene sólo el código que es único para esa plantilla. No necesita redundancia, así si se quiere hacer un cambio grande en el diseño del sitio, sólo cambiaríamos base.html, y todas las otras plantillas reflejarán el efecto inmediatamente.
Veamos cómo trabaja. Cuando cargamos una plantilla hora_actual.html, el motor de plantillas al encontrar {% extends %} carga la plantilla padre, en este caso base.html.
El motor plantillas reemplaza los cuatro bloques de base.html por el contenido de la plantilla hija.
En la plantilla hija no hemos redefinido el bloque pie, por tanto se mostrará el que tiene en base.html.
La herencia no afecta el funcionamiento del contexto, y puedes usar tantos niveles de herencia como necesites. Una forma común de utilizar la herencia es el siguiente enfoque de tres niveles:
- Crear una plantilla base.html que contenga el aspecto principal de tu sitio. Esto rara vez cambiará.
- Crear una plantilla base_seccion.html para cada “sección” de tu sitio (por ej. base_facturas.html y base_tienda.html). Esas plantillas heredan de base.html e incluyen secciones específicas en estilo y diseño.
- Crear una plantilla individual para cada tipo de página, tales como listados de facturas o catálogo de productos. Estas plantillas heredan de la plantilla de la sección apropiada.
Esta aproximación maximiza la reutilización de código y simplifica la creación de páginas nuevas.
Algunos consejos con respecto a la herencia de plantillas:
- Si usas {% extends %} en la plantilla ésta debe ser la primera etiqueta de esa plantilla. En otro caso, la herencia no funcionará.
- Generalmente, cuanto más etiquetas {% block %} tengas en tus plantillas, mejor. Recuerda que las plantillas hijas no tienen que definir todos los bloques del padre, por lo que se puede rellenar un número razonable de bloques por omisión, y luego definir sólo lo que necesiten las plantillas hijas. Es mejor tener más conexiones que menos.
- Si encuentras código duplicado en un número de plantillas, esto probablemente signifique que debes mover ese código a un {% block %} en la plantilla padre.
- Si necesitas obtener el contenido de un bloque padre desde la plantilla hija, se puede con {{ block.super }}. Esto es útil si quieres agregar contenido al bloque padre en vez de sobreescribirlo completamente.
- No puedes definir múltiples etiquetas {% block %} con el mismo nombre en la misma plantilla.
- El nombre de plantilla pasado a {% extends %} es cargado usando el mismo método que get_template(), el nombre de la plantilla es agregado a la variable TEMPLATE_DIRS.
- Al igual que en {% include %} el argumento para {% extends %} normalmente será una cadena pero también puede ser una variable. Esto te permite hacer cálculos dinámicos.
6. El modelo de datos
Una de las piezas imprescindibles que aún no habíamos presentado es el modelo de datos, su definición y uso. Como veremos la inserción, modificación, borrado y consulta de datos en Django es muy potente [6, 7].
Introducción
Como ejemplo nos enfocaremos en el clásico ejemplo que siempre utilizan los de Django, la reliación entre libro, autor y editor. Ya que, tal y como comentamos al principio, ese fue el uso primero del proyecto Django.
Supondremos los siguientes conceptos, campos y relaciones:
- Un autor tiene un título (ej.: Sr. o Sra.), nombre, apellidos, dirección de correo electrónico y una foto tipo carné.
- Un editor tiene un nombre, dirección, ciudad, localidad, país y una página web.
- Un libro tiene un título y una fecha de publicación. También tiene uno o más autores (una relación muchos-a-muchos con autores) y un único editor (una relación uno a muchos, también conocida como clave externa, con los editores).
La definición del modelo de datos se realiza, tal y como introdujimos, en el archivo models.py de la aplicación. Consideremos que nuestra aplicación se llama libros y que éste es el contenido de libros/models.py:
from django.db import models class Editor(models.Model): nombre = models.CharField(max_length=30) direccion = models.CharField(max_length=50) ciudad = models.CharField(max_length=60) provincia = models.CharField(max_length=30) pais = models.CharField(max_length=50) web = models.URLField() class Autor(models.Model): titulo = models.CharField(max_length=10) nombre = models.CharField(max_length=30) apellidos = models.CharField(max_length=40) email = models.EmailField() foto = models.ImageField(upload_to='/fotos') class Libro(models.Model): titulo = models.CharField(max_length=100) autores = models.ManyToManyField(Autor) editor = models.ForeignKey(Editor) fecha_publicacion = models.DateField()
El modelo de datos se expresa como código Python, en vez de SQL por ejemplo, porque así se expresará de la misma manera para múltiples motores de bases de datos como PostgreSQL, MySQL, SQLite u Oracle.
Cada modelo es representado como una clase Python subclase de django.db.models.Model. La clase antecesora, Model, contiene toda la maquinaria necesaria para hacer que estos objetos sean capaces de interactuar con la base de datos. Esto permite definir nuestro modelo de una manera tan compacta.
Generalmente cada clase se transforma en una tabla de la base de datos, aunque hay veces, como en las relaciones muchos-a-muchos, que Django crea otras tablas auxiliares para establecer la relación. Cada atributo la clase corresponde a una columna en esa tabla, el nombre corresponde al nombre de la columna, y el tipo (ej.: CharField) corresponde al tipo de columna de la base de datos (ej.: varchar).
Así, el modelo Editor es equivalente a la siguiente tabla en la sintaxis de PostgreSQL:
CREATE TABLE "libros_editor" ( "id" serial NOT NULL PRIMARY KEY, "nombre" varchar(30) NOT NULL, "direccion" varchar(50) NOT NULL, "ciudad" varchar(60) NOT NULL, "provincia" varchar(30) NOT NULL, "pais" varchar(50) NOT NULL, "web" varchar(200) NOT NULL );
Django genera la sentencia CREATE TABLE automáticamente con la sintaxis adecuada según el motor de BBDD que utilice.
Habrás notado que no hemos definido explícitamente una clave primaria en ninguno de estos modelos. A no ser que se indique lo contrario, Django asigna a cada modelo un campo entero autoincremental como clave primaria llamado id.
El prefijo “libros_” lo añade Django a las tablas para referirse a la aplicación a la que pertenecen.
Insertar y actualizar datos
Ahora empezaréis a ver las ventajas de no usar directamente SQL. Para manejar los datos utilizamos las clases definidas, así para realizar una inserción instanciamos cada atributo:
p = Editor(nombre='Apress', direccion='2855 Telegraph Ave.', ciudad='Berkeley', provincia='CA', pais='U.S.A.', web='http://www.apress.com/')
Hasta ahora no hemos realizado ningún cambio en la BD. Para guardar el registro en la base de datos, para realizar el INSERT, invocamos al método save() del objeto:
p.save()
En SQL sería:
INSERT INTO libros_editor (nombre, direccion, ciudad, provincia, pais, web) VALUES ('Apress', '2855 Telegraph Ave.', 'Berkeley', 'CA', 'U.S.A.', 'http://www.apress.com/');
Las subsecuentes llamadas a save() guardarán el registro en su lugar, sin crear un nuevo registro, es decir, ejecutarán un UPDATE en lugar de INSERT:
p.nombre = 'Apress Publishing' p.save()
La sentencia save() del párrafo anterior resulta aproximadamente en la sentencia SQL:
UPDATE libros_editor SET nombre = 'Apress Publishing' WHERE id = 52;
Realizar consultas
La API de consultas de Django permiten realizar consultas complejas en una sola línea facilmente comprensible.
Consulta genérica
Podríamos probar en el shell de Python lo siguiente:
>>> from libros.models import Editor >>> Editor.objects.all() [<Editor: Editor object>, <Editor: Editor object>]
Con Editor.objects nos referimos a los objetos de la base de datos y con la sentencia Editor.objects.all() los obtenemos todos, en este caso solo dos. Esto Django lo traducirá a una sentencia SQL SELECT.
Sin embargo la lista de editores que hemos obtenido no nos facilita mucha información ya que en todos los elementos indica “Editor object”. Para indicar a Django como debe mostrar un objeto de la base de datos definimos el método __unicode__ del modelo.
En nuestro ejemplo:
class Editor(models.Model): ... def __unicode__(self): return self.nombre class Autor(models.Model): ... def __unicode__(self): return '%s %s' % (self.nombre, self.apellidos) class Libro(models.Model): ... def __unicode__(self): return self.titulo
Como se puede ver, un método __unicode__() devuelve una representación textual. Los métodos __unicode__() de Editor y Libro devuelven simplemente el nombre y título del objeto respectivamente y el __unicode__() del Autor junta los campos nombre y apellidos. El único requerimiento para __unicode__() es que devuelva una cadena, en caso contrario se generaría un error TypeError con un mensaje como «__unicode__ returned non-string».
Ahora la lista de objetos Editor es más fácil de entender:
>>> Editor.objects.all() [<Editor: Addison-Wesley>, <Editor: O'Reilly>]
Que en SQL sería:
SELECT id, nombre, direccion, ciudad, provincia, pais, web FROM libros_editor;
Filtrar la consulta
Aunque obtener todos los objetos es algo que ciertamente tiene su utilidad, la mayoría de las veces lo que vamos a necesitar es manejarnos sólo con un subconjunto de los datos. Para ello usaremos el método filter():
>>> Editor.objects.filter(nombre="Apress Publishing") [<Editor: Apress Publishing>]
Filter toma argumentos de palabra clave que son traducidos en las cláusulas SQL WHERE apropiadas. El ejemplo anterior sería traducido en algo como:
SELECT id, nombre, direccion, ciudad, provincia, pais, web FROM libros_editor WHERE nombre = 'Apress Publishing';
Se le puede pasar a filter() múltiples argumentos para filtrar aún más:
>>> Editor.objects.filter(pais="U.S.A.", provincia="CA") [<Editor: Apress Publishing>]
Esos múltiples argumentos son traducidos a cláusulas SQL AND. Por lo tanto el ejemplo en el fragmento de código se traduce en:
SELECT id, nombre, direccion, ciudad, provincia, pais, web FROM libros_editor WHERE pais = 'U.S.A.' AND provincia = 'CA';
Nótese que por omisión la búsqueda usa el operador SQL = para realizar búsquedas exactas. Existen también otros tipos de búsquedas:
>>> Editor.objects.filter(nombre__contains="press") [<Editor: Apress Publishing>]
Indicamos un doble guión bajo entre nombre y contains, la parte __contains es traducida por Django en una sentencia SQL LIKE:
SELECT id, nombre, direccion, ciudad, provincia, pais, web FROM libros_editor WHERE nombre LIKE '%press%';
Veamos un último ejemplo que seguro os gustará, esta vez queremos obtener los libros cuyo editor tiene el nombre “Apress”. Si utilizásemos SQL tendríamos que hacer un join entre las tablas Libro y Editor, esto se hace en Django simplemente con:
>>> Libro.objects.filter(editor__name="Apress")
Con el doble subrayado sobre un campo ForeignKey estamos haciendo referencia a los campos del modelo destino, en este caso el campo nombre del modelo Editor. Creo que se empieza a entender la potencia en las consultas.
Si quisiésemos todos los libros que no son de ese editor:
>>> Libro.objects.exclude(editor__name="Apress")
O bien, de manera separada:
>>> e = Editor.objects.filter(name="Apress")[0] >>> Libro.objects.exclude(editor=e)
Ordenar datos
Hasta ahora no le hemos indicado a la base de datos cómo ordenar sus resultados, de manera que simplemente estamos recibiendo datos con algún orden arbitrario seleccionado por la base de datos. No quisiéramos que una página Web que muestra una lista de editores estuviera ordenada aleatoriamente. Así que, en la práctica, probablemente querremos usar order_by() para reordenar nuestros datos en listas más útiles:
>>> Editor.objects.order_by("nombre") [<Editor: Addison-Wesley>, <Editor: Apress Publishing>, <Editor: O'Reilly>]
Podemos ordenar por cualquier campo que deseemos, por múltiples campos o podemos especificar un ordenamiento inverso antecediendo al nombre del campo un prefijo -:
>>> Editor.objects.order_by("state_provice", "-nombre") [<Editor: Apress Publishing>, <Editor: O'Reilly>, <Editor: Addison-Wesley>]
También podemos especificar un ordenamiento inverso antecediendo al nombre del campo un prefijo – (el símbolo menos): Aunque esta flexibilidad es útil, usar order_by() todo el tiempo puede ser demasiado repetitivo. La mayor parte del tiempo tendrás un campo particular por el que usualmente desearás ordenar. Es esos casos Django te permite anexar al modelo un ordenamiento por omisión para el mismo:
class Editor(models.Model): ... def __unicode__(self): return self.nombre class Meta: ordering = ["nombre"]
Este fragmento ordering = [«nombre»] le indica a Django que a menos que se proporcione un ordenamiento mediante order_by(), todos los editores deberán ser ordenados, por defecto, por su nombre.
Encadenar búsquedas
Has visto cómo puedes filtrar datos y has visto cómo ordenarlos. En ocasiones, por supuesto, vas a desear realizar ambas cosas. En esos casos simplemente “encadenas” las búsquedas entre sí:
>>> Editor.objects.filter(pais="U.S.A.").order_by("-nombre") [<Editor: O'Reilly>, <Editor: Apress Publishing>, <Editor: Addison-Wesley>]
Eliminar datos
Para eliminar objetos, simplemente se llama al método delete() del objeto:
>>> p = Editor.objects.get(nombre="Addison-Wesley") >>> p.delete() >>> Editor.objects.all() [<Editor: Apress Publishing>, <Editor: O'Reilly>]
Puedes también borrar varios objetos llamando a delete() en el resultado de una búsqueda:
>>> editores = Editor.objects.all() >>> editores.delete() >>> Editor.objects.all() []
7. Procesamiento de formularios
Una de las características que más agilizan el desarrollo es el procesamiento automático que Django realiza sobre los formularios. Permite crear formularios de los modelos automáticamente y validar los distintos campos según el tipo de los atributos del modelo [8].
Imaginemos que tenemos este modelo:
class Libro(models.Model): titulo = models.CharField(maxlength=100) autores = models.ManyToManyField(Autor) editor = models.ForeignKey(Editor) fecha_publicacion = models.DateField()
Crear un formulario para esa tabla es tan fácil como:
class LibroForm(ModelForm): class Meta: model = Libro
Ese formulario lo crearíamos en el archivo forms.py de la aplicación.
Solo con eso ya disponemos de un formulario con los campos del modelo y que realiza automáticamente la comprobación de tipos. Además al ser un formulario de un modelo nos permite realizar inserciones y ediciones de manera inmediata. La vista de inserción de libros sería algo tan simple como:
from libros.forms import LibroForm def nuevo_libro(request): libro = LibroForm() if request.POST: libro = LibroForm(request.POST) if libro.is_valid() libro.save() libro = LibroForm() return render_to_response('libros/nuevo_libro.html', {'formulario':libro})
Lo que queremos conseguir es que cuando el usuario entre en, por ejemplo, /libros/nuevo/ que estará definido en urls.py, se invoque a la vista nuevo_libro anterior. La primera vez que se accede se mostrará un formulario vacío y al rellenar el usuario los datos y darle al botón Insertar se dirige de nuevo a la url /libros/nuevo/ pero esta vez con los datos del formulario enviados por POST. La vista valida esos datos y realiza el INSERT.
Expliquemos el código de la vista:
- Con la primera línea instanciamos el formulario LibroForm en la variable libro.
- El if es el que recoge los datos enviados, la primera vez no entraremos en él, ya que no hay datos enviados por POST, sino que realizaremos el render_to_response para mostrar el formulario.
La plantilla, maquetación aparte, podría ser:
{% block contenido %} <form action="/libros/nuevo/" method="post"> {% for campo in formulario %} {{ campo.label }}: {{ campo }} {{ campo.help_text }} <br /> {% if campo.errors %} {{ campo.errors }} {% endif %} {% endfor %} <input type="submit" name="insertar" value="Insertar" /> </form> {% endblock %}
Una vez rellenos los datos, al dar a Insertar volvemos a entrar en la vista pero esta vez sí que entramos en el if:
- Con la línea LibroForm(request.POST) obtenemos el formulario relleno con los datos pasados por POST.
- Con «is_valid» comprobamos si los datos son válidos, en caso contrario volveríamos al formulario remarcando los errores producidos (campo.errors en la plantilla).
- Si los datos son válidos hacemos el «save» sobre el formulario y esto hace el INSERT y creamos una entrada para la tabla «Libro» con los campos suministrados. Si lo hiciésemos sobre un objeto que ya existe, como hemos visto antes, se modificarían sus datos.
- Volvemos a dejar el formulario en blanco y llamamos de nuevo a render_to_response para mostrarlo.
En este ejemplo hemos visto un formulario creado a partir de un modelo pero también se pueden crear formularios independientes de cualquier modelo, como podría ser un formulario de búsqueda, por ejemplo.
8. Seguridad
Usuarios y grupos
En Django existe siempre por defecto la tabla de usuarios y la de grupos. Un usuario puede pertenecer a varios grupos y se pueden asignar permisos a un usuario o grupo, de manera que un usuario dado tiene los permisos que se le hayan dado a él más los que sume de todos los grupos a los que pertenezca [9].
Autenticación e inicio de sesión
La autenticación y control de sesiones de usuario en Django es muy simple, sino observad lo que habría que modificar en la vista anterior para requerir que se tenga que haber iniciado sesión para insertar un libro:
from libros.forms import LibroForm @login_required def nuevo_libro(request): libro = LibroForm() if request.POST: libro = LibroForm(request.POST) if libro.is_valid() libro.save() libro = LibroForm() return render_to_response('libros/nuevo_libro.html', {'formulario':libro})
Solo con añadir el decorador [9, 10, 11] login_required a la función ya estamos indicando a Django que compruebe si se ha iniciado sesión, sino redirige a la página de login que hayamos indicado en la configuración de Django (settings.py) y una vez introducido el usuario y contraseña volveríamos al formulario de inserción de un libro, ya sí podríamos entrar en la vista.
El sistema de permisos
En Django la administración de permisos está bien integrada, cada tabla dispone siempre al menos de tres permisos: add, change y delete, que aplicados a un usuario o grupo de usuarios especifican si puede insertar, modificar o eliminar datos de esa tabla. Además de estos tres se pueden definir otros personalizados para el proyecto.
Estos permisos pueden ser comprobados en las vistas (enviando un 403 por ejemplo si no se dispone de ellos), en las plantillas (mostrando o no determinada información) y se tienen en cuenta a la hora de administrar desde la Zona de Administración, como veremos más adelante.
Así en nuestro ejemplo anterior podríamos haber comprobado si el usuario dispone o no de permisos simplemente:
from libros.forms import LibroForm @login_required def nuevo_libro(request): if not request.user.has_perm("libros.add_libro"): return HttpResponseRedirect("/forbidden/") libro = LibroForm() if request.POST: libro = LibroForm(request.POST) if libro.is_valid() libro.save() libro = LibroForm() return render_to_response('libros/nuevo_libro.html', {'formulario':libro})
Y en una plantilla podríamos mostrar o no el enlace de “Añadir libro” dependiendo de si dispone o no del permiso:
{% if perms.libros.add_libro %} <a href="/libros/nuevo">Añadir libro</a> {% endif %}
9. El sitio de administración de Django
El sitio de administración de Django (desde ahora admin) es otra de las buenas características que aporta este framework. Permite, de manera casi inmediata, disponer de una interfaz cómoda de administración de la información almacenada en la BD.
Y diréis, ¿no es lo mismo que otras interfaces como PHPMyAdmin? Pues no, digamos que esas otras interfaces están pensadas para administradores de bases de datos, sin embargo el admin de Django está pensado para administradores del sitio web. Permite obtener listados, filtrar, ordenar, realizar búsquedas, insertar, modificar y eliminar.
No es propósito de este tema exponer con detalle todas las opciones y configuración del admin, aunque sí se dará nociones de su uso. Para profundizar sobre su uso puede consultar [12].
Acceso
El acceso está restringido a los usuarios que tengan el boolean is_staff a True y éstos solo podrán editar la información según sus privilegios sobre cada tabla (add, change y delete).
Contenido
El contenido del admin está configurado por el desarrollador ya que es éste quien indica qué tablas se podrán ver y editar, y por qué datos se puede consultar y filtrar.
Estructura
Básicamente en el admin se diferencian tres interfaces:
Pantalla inicial o home
Donde se muestran las distintas tablas sobre las que puede actuar.
Listado de una tabla
Muestra los distintos objetos de una tabla, permitiendo realizar búsquedas, ordenaciones o filtrar el contenido según algunos campos.
Edición de un objeto
Es la pantalla donde se inserta o edita un objeto de la tabla, se accede al pulsar sobre él en el listado, por ejemplo.
Dependiendo del tipo de cada campo se muestra de un modo u otro. Por ejemplo:
- Fecha: habilita un icono donde seleccionar la fecha de un calendario.
- Clave externa a otra tabla: desplegable con los valores de la otra tabla.
- Etc.
Permite también crear objetos relacionados con el objeto que se está editando (por claves externas). Así podríamos, por ejemplo, ver y editar tanto un Editor como los Libros que ha editado.
Referencias
- [1] Escribiendo vistas.
- [2] Gestión de URLs.
- [3] El lenguaje de plantillas.
- [4] Etiquetas de plantillas.
- [5] Etiquetas de plantillas personalizadas.
- [6] Modelos de datos.
- [7] Consultas al modelo de datos.
- [8] Trabajando con formularios.
- [9] Autenticación de usuarios.
- [10] Blog de Juanjo Conti donde explica de manera fácil lo que es un decorador en Python.
- [11] Decoradores de vista disponibles en Django.
- [12] El sitio de administración de Django.
muy buen aporte gracias
Gran aporte, me gustaria tener un ejemplo de settings.py bien configurado.
Muchas gracias
Que tal disculpa conoces programadores que me puedas recomendar que sepan desarrollar plataformas web con DJANGO.
Saludos.
Claro que conozco, incluso puedo ayudarte yo mismo. Por favor, contáctame y me cuentas más detalles. Gracias.