Está en la página 1de 79

Tutorial Django Documentation

Publicacin 1.0

Salvador Nicolas<snicoper@gmail.com>

10 de September de 2015

ndice general

1. Tabla de contenidos:
1.1. Antes de empezar . . . . . . .
1.2. Instalacin Django . . . . . . .
1.3. Creacin de un proyecto Django
1.4. Crear nuestra primera app . . .
1.5. Creacin de la Vista . . . . . .
1.6. URLs . . . . . . . . . . . . . .
1.7. Templates . . . . . . . . . . . .
1.8. Archivos static . . . . . . . . .
1.9. Creacin accounts . . . . . . .
1.10. Creacion blog . . . . . . . . . .
1.11. Pagina About . . . . . . . . . .
1.12. Pagina Contacto . . . . . . . .
1.13. Notas finales . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.

1
1
1
2
7
8
8
11
13
16
35
71
72
74

II

CAPTULO 1

Tabla de contenidos:

1.1 Antes de empezar


Esta es un pequeo tutorial para empezar a usar Django, no pretende ser una manual tcnico y la mejor manera de
dominar el Framework es usarlo y mojarse con la ayuda de Documentacin Django y/o si tienes dudas puedes buscar
o exponer tus dudas en stackoverflow y como no, con San Google.
Los requisitos mnimos para poder seguir sin problemas el tutorial seria tener conocimientos bsicos de Python, Html.
He usado para el tutorial Python 3, Django 1.8 y Fedora 22.
Puedes descargar el cdigo del tutorial en Github
Para el tutorial, vamos a crear un pequeo Blog con las siguientes caractersticas.
Un registro de usuarios.
Login y Logout.
Editar informacin del usuario.
Aadir y editar las entradas.
Ver en detalles las entradas de manera individual.
Editar y eliminar las entradas.
Aadir Disqus en las entradas.
Pagina Sobre mi.
Pagina de contacto.
Cualquier sugerencia o correccin, puedes contactar con migo a travs de www.snicoper.com
Nota: La gua esta en proceso de revisin, puede contener errores tipogrficos y de programacin.
Ya sin mas, comencemos a crear nuestro Blog :)

1.2 Instalacin Django


Antes de instalar Django, se ha de tener Python en el sistema, para no repetirme dejo aqu unas guas para instalar
Python y Django en varios sistemas.
Instalacin Python en Windows

Tutorial Django Documentation, Publicacin 1.0

Instalacin Python en Fedora


Instalacin Python en Ubuntu
Nota: Uso Python 3 para el tutorial.

1.2.1 Crear un entorno virtual


La mejor manera de desarrollar con Python, es usando entornos virtuales, de esta manera podemos tener versiones de
paquetes sin que se mezclen.
Para crear un entorno virtual, abrimos la terminal y escribimos:
mkvirtualenv tutorial_django

Ahora en la terminal, podemos ver que pone (terminal_django), eso nos indica que entorno estamos usando y
el que siempre usaremos en todo el tutorial.
Para salir del entorno virtual, simplemente con deactivate en la terminal, saldremos y para entrar o cambiar en
entorno, usamos workon nombre_entorno.

1.2.2 Instalacin de Django


Para instalar Django, usando el entorno virtual, escribimos:
Nota: Omito (terminal_django)snicoper@lxmaq1 ~/projects/tutorial_django en la terminal cuando pongo los comandos, asumo que a partir de ahora siempre se estar usando el entorno virtual
terminal_django.
pip install django

Una vez instalado, comprobamos si todo ha salido bien:


python -c 'import django; print(django.get_version())'
# 1.8.2

Si nos muestra la versin, es que todo ha salido correcto y ahora podemos crear nuestro primer proyecto.

1.3 Creacin de un proyecto Django


Vamos a crear un proyecto, nos desplazamos desde la terminal hasta el directorio donde vayamos a crear el proyecto
y ejecutamos:
django-admin startproyect tutorial_django

Nota: Si el comando anterior da algn error, probar con python django-admin.py startproyect
tutorial_django
Esto nos crea un directorio con la siguiente estructura:
tutorial_django/
-- manage.py
-- tutorial_django

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

-----

__init__.py
settings.py
urls.py
wsgi.py

1 directory, 5 files

tutorial_django es la raz del proyecto y este directorio se puede renombrar tranquilamente, para diferenciarlo
con el que hay en el interior con el mismo nombre, lo renombramos a src.
mv tutorial_django src

manage.py es un archivo Python que se encarga de poner el proyecto en sys.path, establecer la variable de
entorno DJANGO_SETTINGS_MODULE para que apunte al archivo settings.py y llama a django.setup().
Nota: En linux, en principio manage.py tiene permisos de ejecucin, en caso de no tenerlos chmod +x
manage.py
Todos los comandos (argumentos) (poco a poco veremos unos cuantos) que se pasan a manage.py son pasados a
django-admin estableciendo los parmetros del prrafo anterior, por lo tanto es un wrapper de django-admin.
Nota: Para ver la lista de argumentos de manage.py usar ./manage.py help
Dentro de src hay otro directorio con el mismo nombre tutorial_django y cuatro archivos mas, el directorio es
posible cambiarle el nombre, pero abra que cambiar algunos parmetros en manage.py, nosotros lo dejaremos tal
y como esta.

__init__.py es para decirle que trate tutorial_django como un paquete Python.


settings.py aqu pondremos/cambiaremos las configuraciones del sitio.
urls.py archivo URLconf, aqu es donde manejaremos las rutas que apuntan a las vistas.
wsgi.py WSGI en pocas palabras es una interfaz entre un servidor web y la propia aplicacin, no hablaremos mas
sobre este tema en este tutorial.

1.3.1 Primera migracin


Una de las primeras cosas que se hace despus de crear un proyecto Django, es elegir el motor de base de datos en la que almacenaremos todos nuestros datos, decir a Django que RDBMS vamos a usar, en
tutorial_django/settings.py, buscamos la zona donde esta la configuracin DATABASES.
Para este tutorial usaremos el mas simple, SQLite que es la que viene por defecto, pero si quieres usar otro distinto
puedes ver de la documentacin
La configuracin por defecto:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}

Ahora, tenemos que hacer la primera migracin y la creacin de un administrador, para la migracin usamos el siguiente comando.

1.3. Creacin de un proyecto Django

Tutorial Django Documentation, Publicacin 1.0

./manage.py migrate

Esto nos crea las tablas de algunas aplicaciones que vienen por defecto en Django (si quieres ver las apps que se usan
en Django, puedes mirar en el archivo settings.py en la tupla INSTALLED_APPS).
Si usas el gestor de la base de datos que hayas elegido, puedes ver que se han creado varias tablas en la base de datos,
tambin has podido ver que tablas se han creado, con la salida de ./manage.py migrate
Operations to perform:
Synchronize unmigrated apps: staticfiles, messages
Apply all migrations: sessions, admin, auth, contenttypes
Synchronizing apps without migrations:
Creating tables...
Running deferred SQL...
Installing custom SQL...
Running migrations:
Rendering model states... DONE
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying sessions.0001_initial... OK

1.3.2 Creacin del super usuario


Ahora que tenemos nuestra primera migracin en nuestra base de datos y por consiguiente la tabla auth_user,
vamos a crear el super usuario, desde la terminal ejecutamos ./manage.py createsuperuser
$ ./manage.py createsuperuser
Username (leave blank to use 'snicoper'):
Email address: snicoper@gmail.com
Password:
Password (again):
Superuser created successfully.

Vamos a ver si todo ha salido bien, o como se esperaba. Para ello se usa el comando runserver
./manage.py runserver

Advertencia: Django tiene un servidor escrito en Python exclusivamente para el desarrollo, cada vez que se
modifica un archivo, el servidor se reinicia y recompila los archivos, ahorrndonos mucho tiempo y molestias, pero
eso solo eso, un servidor para desarrollo, que soporta una o unas pocas peticiones, no lo uses para un servidor de
produccin! para eso tienes Nginx, Apache entre otros.
Accedemos en el navegador a la url http://127.0.0.1:8000 y si todo ha salido bien, veras una pantalla como la siguiente:

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

Tambin podemos ver la administracin que viene incorporada con Django y que ahorra muchas horas de trabajo, genera un sistema CRUD simplemente genial, para acceder a la administracin vamos a la url http://127.0.0.1:8000/admin/

1.3. Creacin de un proyecto Django

Tutorial Django Documentation, Publicacin 1.0

Eso es todo!, ya tenemos un proyecto creado, nuestra primera migracin y creado el super usuario (administrador) del
sitio con uno pocos pasos.
Mas adelante, modificaremos settings.py y urls.py y los veremos mas en detalle, ahora vamos a crear nuestra
primera aplicacin.

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

1.4 Crear nuestra primera app


Una aplicacin (app), no es mas que un paquete Python con una estructura bsica para hacer algo concreto, por ejemplo
blog, about, contact seria cada una de ellas una aplicacin. Las aplicaciones en Django, estn (en un principio)
totalmente desacopladas y puedes tener apps en varios proyectos sin modificar ni una sola linea de cdigo, el nico
requisito es que este en la variable de entorno PYTHONPATH
Nota: Cuando ejecutamos un archivo .py Python aade el directorio actual en PYTHONPATH, por lo tanto, durante
el desarrollo, cuando levantamos el servidor, al hacerlo a travs de manage.py src estar en PYTHONPATH.
Vamos a crear nuestra primera app, crearemos el home de nuestro sitio, para crear nuestra app, abriremos la terminal
e iremos hasta src y ejecutaremos:
./manage.py startapp home

Nota: Si el comando anterior da algun error (en Windows creo), usar python manage.py startapp home o
en windows .\manage.py startapp home.
El comando anterior es posible crearlo con django-admin, muy til cuando se crean apps en otros directorios
que no sean en la raz del proyecto. Lo que el comando manage.py startapp hace es crear una estructura, que
tranquilamente se podra crear los directorios y archivos a mano. El comando anterior crea la siguiente estructura:
home/
-- migrations
|
-- __init__.py
-- __init__.py
-- admin.py
-- models.py
-- tests.py
-- views.py

Examinemos los distintos archivos que ha creado el comando anterior, nos crea un directorio migrations que
contiene un archivo __init__.py (para decirle a Python que es un paquete), migrations
Las migraciones son manera de propagar los cambios que realice en sus modelos (la adicin de un campo,
la eliminacin de un modelo , etc.) en el esquema de base de datos de Django. Estn diseados para ser
en su mayora automtica.
fuente
admin.py donde crearemos la forma que se vera la app en la administracin de Django. models.py donde crearemos los modelos y es el puente entre Django y la base de datos. test.py para test unitarios, no se trata el tema
en el tutorial. views.py donde crearemos las vistas (o controladores, para los que vengan de otros Frameworks).
Con todo esto en mente, vamos a crear el primer Hello world :), en primer lugar, tenemos que decirle a Django que
incluya la aplicacin, para ello, editamos tutotial_django/settings.py
Vamos a INSTALLED_APPS y aadimos la siguiente linea:
# tutotial_django/settings.py
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',

1.4. Crear nuestra primera app

Tutorial Django Documentation, Publicacin 1.0

'home',
)

Ahora, Django conocer que tenemos una aplicacin y buscara templates (plantillas), archivos de migracin, etc,
dentro de esa app.
En la siguiente seccin, modificaremos las urls para poder acceder a las vistas.

1.5 Creacin de la Vista


Las views (controladores en otros Frameworks), es el puente entre los templates (vistas en otros Frameworks) y el
modelo.
En este primer contacto, no vamos a usar los modelos para mantener las cosas lo mas simple posible, as que vamos a
escribir la vista mas sencilla posible.
Abrimos home/views.py y escribimos lo siguiente:
# home/views.py
from django.http import HttpResponse

def index_view(request):
return HttpResponse("Hello world")

Tenemos que escribir una url para que al acceder ella, Django y el sistema de enrrutamiento sepa a que view ha de
acceder.

1.6 URLs
Se
puede
ver
en
tutotial_django/settings.py
una
variable
ROOT_URLCONF =
tutorial_django.urls, esto es el punto de entrada a las rutas en la barra de navegacin del navegador. Cuando insertamos una URI en el navegador (o pinchamos algn link), Django obtiene la URI y la va
comparando con expresiones regulares con el archivo tutorial_django/urls.py y si hay coincidencia, cagara
la vista asociada al patrn, en caso contrario mostrara un error 404.
Este archivo es solo un punto de entrada y algo cmodo (que ademas mantiene al mximo el desacoplamiento), es
crear un archivo urls.py en cada app y en tutorial_django/urls.py indicar que se ha creado otro archivo
urls para que siga comprobando.
Primero decimos a tutorial_django/urls.py que vamos a crear un archivo con urls y donde encontrar el
archivo, a parte pondremos parte de un patrn.
Echemos un ojo al archivo que crea por defecto Django al crear proyecto.
# tutorial_django/urls.py
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
]

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

urlpatterns es una lista que contiene un conjunto de funciones url(), en este caso, se le pasan dos parmetros
r^admin/ que es el patrn, una expresin regular que luego comparara con la URI que insertamos en el navegador
e include(admin.site.urls) que es una funcin para incluir otros URLconf.
En el argumento de regex, es una expresin regular pura en Python, por lo que no debemos aprender otros tipo de
patrones!.
Para entender como funciona esto (a ver si yo me se explicar, que es lo difcil de verdad :P), vamos a crear nuestro
archivo home/urls.py y le insertamos el siguiente cdigo.
# home/urls.py
from django.conf.urls import url
from . import views
urlpatterns = [
# /home/
url(regex=r'^$', view=views.index_view, name='home'),
]

Tenemos que incluir este archivo URLconf al URLconf principal tutorial_django/urls.py


# tutorial_django/urls.py
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^$', include('home.urls')),
url(r'^home/', include('home.urls')),
url(r'^admin/', include(admin.site.urls)),
]

En este ultimo URLconf, hemos aadido dos url(), con una expresin regular ^$ que coincidir con la URI /
(vaci) y la otra que coincidir con la URI home/, e incluirn home/urls.py y sera nuestra pagina principal.
Ahora, imaginamos que ponemos en el navegador http://127.0.0.1:8000/home/about/, como funcionan
las comparaciones?, en primer lugar Django separa la parte del dominio con el resto, es decir se queda con la URI, que
en este caso es /home/about/ y empieza a compararlas con tutorial_django/urls.py, va de arriba hacia
abajo en las url().
En primer lugar compara /home/about/ con el patrn r^home/ y ya encuentra una coincidencia, que es
home/, ahora, parte el string y se queda con el resto, en este caso about/. Pero ahora, continua las comparaciones
con los patrones en home/urls.py. En home/urls.py, no encontrara ninguna coincidencia, por lo que volver
a tutorial_django/urls.py, cuando termine, no abra encontrado nada, as que devolver un error 404.
En cambio si la URI, es /home/, encuentra una coincidencia con r^home/, cuando parte el string se queda con
/ y cuando pasa a home/urls.py encuentra una coincidencia con r^$ (vacio) y devuelve la vista asociada.
Si nos fijamos en el archivo URLconf de home/urls.py y examinamos el resto de la url()
url(regex=r'^$', view=views.index_view, name='home.index')

Podemos ver que tiene tres argumentos (aunque a la funcin se le pueden pasar mas), regex ya lo conocemos,
view es simplemente la vista a cargar en caso de coincidencia, que en este caso el modulo views y la funcin
index_view, el name=home se usa mucho en los templates para generar links, mas adelante los iremos usando.
Entonces para mostrar el contenido de nuestra vista creada en la seccin anterior, hay dos URIs validas para llegar a la
vista, / y /home/, si vamos al navegador (tenemos que tener el servidor en ejecucin) http://127.0.0.1:8000

1.6. URLs

Tutorial Django Documentation, Publicacin 1.0

o tambin http://127.0.0.1:8000/home/ veremos que nos muestra Hello world que es justo lo que se esperaba.
Si probamos la URL de antes http://127.0.0.1:8000/home/about/, veremos que nos muestra una pagina
igual a esta:

Nota: Ese mensaje tan feo para el usuario, pero tan til para nosotros, solo lo muestra cuando se esta en modo
desarrollo, por defecto en tutorial_django/settings.py la variable de configuracin DEBUG, dice a Django
el modo en el que estamos, por defecto al crear un proyecto se establece en True.
Advertencia: En produccin JAMAS usar DEBUG = True, siempre DEBUG = False, a parte de que las
peticiones son mas lentas, expone informacin que puede poner en peligro la seguridad del sitio.
Para recordar la vista
from django.http import HttpResponse

def index_view(request):
return HttpResponse("Hello world")

Toda vista, obtiene un objeto HttpRequest y devuelve un objeto HttpResponse, en este caso dentro de
HttpResponse es solo un string, si vamos al navegador y vemos el cdigo, veremos que es limpiamente el string
devuelto por la funcin de la vista index_view y aunque podemos poner html dentro del HttpResponse, no
seria muy til crear paginas de este modo!, tardaramos mas, eso sin contar con lo horrible que seria el cdigo...
Para evitarlo, la mejor manera es por medio de las plantillas o templates de Django.

10

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

1.7 Templates
Hasta ahora, ya sabemos como crear un proyecto, una app, configurar URLconfs y enlazarlas a las vistas, ahora es
hora de renderizar desde las vistas a los templates o plantillas e incluir los archivos.
Empezamos con el archivo de configuracin tutorial_django/settings.py, buscamos la lista TEMPLATES
y vemos que contiene un diccionario, un elemento de ese diccionario es DIRS: [], una clave DIRS con un valor
que es una lista vaca. Dentro de la lista vamos a decir donde guardaremos los archivos de plantillas, quedando de la
siguiente manera.
# tutorial_django/settings.py
TEMPLATES = [
{
# ...
'DIRS': [os.path.join(BASE_DIR, 'templates')],
# ...
}
]

Ahora, tenemos que crear el directorio templates en el directorio raz del proyecto
mkdir templates

Como puedes ver dentro del diccionario dentro de TEMPLATES hay un elemento APP_DIRS: True, que le dice
si buscar dentro de las apps un directorio templates donde contendrn plantillas que pertenecen a esa **a**pp (mas
adelante lo veremos como funciona) y terminare de explicar como busca las plantillas Django, por ahora, qudate con
APP_DIRS: True,.
Creamos dentro del directorio templates un archivo .html que sera el archivo base, el que se leer en todas nuestras
paginas con el nombre base.html y aadimos el siguiente cdigo html.
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<h1>Pagina principal</h1>
</body>
</html>

Volvemos al archivo de vista en la app home y cambiamos todo el cdigo por el siguiente:
# home/views.py
from django.shortcuts import render

def index_view(request):
return render(request, 'base.html')

Como se pude ver, se ha cambiado el from... y el return..., ahora vemos que en vez de devolver HttpResponse como
hacamos antes, ahora devolvemos render que es un atajo donde volvemos a pasar el HttpRequest, el nombre de
plantilla a usar y mas adelante, veremos como pasar un diccionario con datos (un diccionario con pares clave/valor)
para poderlos mostrar desde las plantillas.
1.7. Templates

11

Tutorial Django Documentation, Publicacin 1.0

Ahora si iniciamos el servidor ./manage.py runserver y vamos a http:/127.0.0.1:8000 veremos que la salida,
es la del html creado.
Vamos a empezar con la herencia de plantillas, para ver como podemos crear bloques, vamos a editar el archivo
base.html y creamos un par de bloques.
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>{% block title %}{% endblock title %}</title>
</head>
<body>
<h1>Pagina principal</h1>
{% block content %}{% endblock content %}
</body>
</html>

Podemos observar que hemos creado dos bloques, para que sirven?, pues bien, cuando una plantilla extiende de otra,
la plantilla que llama sustituye el contenido del bloque por la de la plantilla padre, para verlo, dentro de home,
creamos un directorio templates y dentro creamos otro directorio con el nombre home y dentro un archivo .html
con el nombre index.html.
mkdir -p home/templates/home
touch home/templates/home/index.html

y ponemos el siguiente cdigo:


<!-- home/templates/home/index.html -->
{% extends 'base.html' %}
{% block title %}home{% endblock title %}
{% block content %}
<h2>Home page</h2>
{% endblock content %}

La manera de extender una plantilla es con la etiqueta (tag) { % extends base.html %} donde le estamos
diciendo que extienda la plantilla a base.html y a partir de hay, los bloques (block) de cdigo, cambiaran los datos
de la plantilla actual con la de la plantilla extendida, en este caso base.html.
Como se puede apreciar, puede parecer un poco folln crear un directorio template y dentro otro con el mismo
nombre de la app, en este caso home, porque este lo?, sencillo, imagina que creas diez apps en un proyecto y tres
de ellos tienen una plantilla index.html, como sabe Django que plantilla cargar?, de hay que se crea siempre un
directorio con el nombre de la app (los nombres de las apps, son siempre nicos).
Otra manera o estructura de crear las plantillas, es dentro del directorio templates de la raz del proyecto, es crear
directorios con el mismo nombre que la apps y dentro las plantillas, pero yo por costumbre, siempre los creo en los
directorios de la app.
Bien, continuemos... ahora, vamos a cambiar en la vista index_view la plantilla que requiere.
def index_view(request):
return render(request, 'home/index.html')

Vemos que hemos aadido la ruta home/index.html y lo mejor de todo, vemos que ahora imprime lo de

12

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

base.html y lo de index.html, ahora la pagina muestra un titulo y debajo de Pagina principal inserta el contenido que hay dentro de block en home/index.html.
Para terminar con las plantillas (seguiremos a lo largo del tutorial), vamos a ver como pasar un contexto de la vista a
la plantilla.
Volvemos a editar home/views.py
# home/views.py
from django.shortcuts import render
from django.utils import timezone
def index_view(request):
context = {
'ahora': timezone.now()
}
return render(request, 'home/index.html', context)

y ahora, en home/templates/home/index.html
<!-- home/templates/home/index.html -->
{% extends 'base.html' %}
{% block title %}home{% endblock title %}
{% block content %}
<h2>Home page</h2>
<p>Ahora es {{ ahora }}</p>
{% endblock content %}

Y vemos que hemos pasado la fecha y hora de la maquina en la que estamos. Para imprimir el valor de una variable, se
usa dobles llaves de apertura y cierre {{ nombre_variable }}, los espacios son opcionales, pero aconsejables.
Nota:
Si te sale la fecha en ingles y una hora que no corresponde a la de tu sistema, ves a
tutorial_django/settings.py para editar las variables de configuracin LANGUAGE_CODE = es-es
ver identificadores y TIME_ZONE = Europe/Madrid ver timezones
En la siguiente seccin, veremos como incluir archivos estticos a nuestro proyecto.

1.8 Archivos static


Ahora es el momento de ver como incluir los archivos estticos (css, js, imagenes) al proyecto.
Si abres el archivo de configuracin tutorial_django/settings.py y bajas al final del archivo, vemos una
variable de configuracin STATIC_URL = /static/, de esta manera le esta diciendo que, cuando en las plantillas llamamos a un archivo esttico, anteponga en la URI /static/ (ahora veremos como funciona), a parte tenemos
que decirle a Django donde estarn esos archivos estticos, as que, debajo de STATIC_URL ponemos lo siguiente:
# tutorial_django/settings.py
STATIC_URL = '/static/'
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),
)

1.8. Archivos static

13

Tutorial Django Documentation, Publicacin 1.0

STATICFILES_DIRS es una tupla, que dice las rutas absolutas del sistema para buscar archivos estticos, en nuestro
caso, solo hemos aadido uno.
Ahora, ya sabr Django que los archivos estarn en la src/static (con una ruta absoluta), pero ese directorio no
existe, as que lo creamos y dentro creamos tres directorios mas, js, css e img
mkdir -p static/{js,css,img}

dentro de css creamos un archivo main.css y dentro de js un archivo main.js. Ya para terminar, los incluimos
en templates/base.html
<!-- templates/base.html -->
<!DOCTYPE html>
{% load staticfiles %}
<html lang="es">
<head>
<meta charset="utf-8">
<title>{% block title %}{% endblock title %}</title>
<link rel="stylesheet" href="{% static 'css/main.css' %}">
</head>
<body>
<h1>Pagina principal</h1>
{% block content %}{% endblock content %}
<script src="{% static 'js/main.js' %}"></script>
</body>
</html>

Hemos incluido una tag o etiqueta { % load staticfiles %} (si no, dar error al cargar la pagina),
cuando llama un archivo esttico como { % static js/main.js %}, Django cambiara { % static
js/main.js %} por el valor de settings.STATIC_URL + js/main.js que en este caso sera <script
src="/static/js/main.js"></script>
Dentro de static/css/main.css aadimos:
/* static/css/main.css */
body { background-color: blue; }

Ahora, reiniciamos el servidor (por si acaso) y vamos a http://127.0.0.1:8000/ y vemos que el fondo es azul,
lo importante es ver el cdigo html generado, si nos fijamos ha puesto la ruta <link rel="stylesheet"
href="/static/css/main.css">, eso significa que podramos cambiar el directorio a otro ruta y cambiando
en settings la ruta, sin modificar nada mas, seguiremos teniendo a nuestra disposicin los archivos estticos.
Para el resto del tutorial, vamos a poner Bootstrap y JQuery, descargamos ambas libreras y las descomprimimos
dentro del directorio static (jquery no lo usaremos), quedando de esta manera la estructura.
static/
-- bootstrap
|
-- css
|
|
-- bootstrap.css
|
|
-- bootstrap.css.map
|
|
-- bootstrap.min.css
|
|
-- bootstrap-theme.css
|
|
-- bootstrap-theme.css.map
|
|
-- bootstrap-theme.min.css
|
-- fonts
|
|
-- glyphicons-halflings-regular.eot
|
|
-- glyphicons-halflings-regular.svg

14

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

|
|
|
|
|
|
|
-|
--|
--

|
-- glyphicons-halflings-regular.ttf
|
-- glyphicons-halflings-regular.woff
|
-- glyphicons-halflings-regular.woff2
-- js
-- bootstrap.js
-- bootstrap.min.js
-- npm.js
css
-- main.css
img
jquery
-- jquery.js
js
-- main.js

8 directories, 18 files

E incluimos en nuestro templates/base.html. Ademas aadimos el tpico navbar superior de Bootstrap, quedando de la siguiente manera nuestro archivo base.html
<!-- templates/base.html -->
<!DOCTYPE html>
{% load staticfiles %}
<html lang="es">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% endblock title %}</title>
<link rel="stylesheet" href="{% static 'bootstrap/css/bootstrap.css' %}">
<link rel="stylesheet" href="{% static 'css/main.css' %}">
</head>
<body>

<nav class="navbar navbar-inverse navbar-fixed-top">


<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Project name</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="#">Home</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
</div><!--/.nav-collapse -->
</div>
</nav>
<div class="container">

1.8. Archivos static

15

Tutorial Django Documentation, Publicacin 1.0

<div class="starter-template">
{% block content %}{% endblock content %}
</div>
</div><!-- /.container -->
<script src="{% static 'jquery/jquery.js' %}"></script>
<script src="{% static 'bootstrap/js/bootstrap.js' %}"></script>
<script src="{% static 'js/main.js' %}"></script>
</body>
</html>

El archivo css tambin le vamos a quitar ese color azul tan moln :P y ponerle el margen de la barra.
/* static/css/main.css */
body {
margin: 70px 0 20px 0;
}

Si actualizamos la pagina, ahora se ve que la cosa cambia :) y es hora de empezar a profundizar mas en todos los
componentes que hasta ahora hemos tocado, a parte de que queda mucho por ver como los modelos.

1.9 Creacin accounts


Vamos a ver como crear un pequeo sistema para los usuarios, como poder registrar, hacer login, logout y como editar
datos del perfil de usuarios.

1.9.1 Creacin de registros de usuarios


Una de las primeras cosas que se suele hacer, es el manejo de usuarios, como el registro, login, logout, etc.
Django tiene un sistema de usuarios, recuerda que nosotros ya tenemos un usuario registrado y ademas es administrador del sitio, es decir que tambin maneja permisos ademas de sesiones.
Vamos a crear una manera para registrar usuarios en el sitio, primero vamos a crear una app accounts donde muestre
el formulario para registrar usuario. Un modelo para extender los datos del usuario, donde el usuario podr aadir una
imagen/avatar, dentro de esta app, aadiremos la manera para que el usuario pueda hacer login y logout.
Django, como he comentado, incorpora un sistema de manejo de usuarios, si vemos en la base de datos las tablas
que cre cuando usamos la primera vez el comando ./manage.py migrate, vemos que cre las tablas auth_*
y otra para las sesiones, django_session. Si nos fijamos en la tabla auth_user vemos las columnas id,
password, is_superuser, username, etc, eso significa que Django tiene un modelo que hace de puente entre Django y la base de datos.
Vamos a crear nuestro primer modelo que tenga una relacin One to One donde una campo del modelo que crearemos
sera una relacin con el modelo User en la base de datos, la tabla es auth_user.
En primer lugar, vamos a crear la app accounts
./manage.py startapp accounts

Ahora, en tutorial_django/settings.py al final del archivo aadimos


# tutorial_django/settings.py
LOGIN_URL = '/accounts/login/'
LOGOUT_URL = '/accounts/logout/'

16

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

LOGIN_URL y LOGOUT_URL, los pongo para mostrarlos, pero usa los mismos valores que tienen por defecto, cuando
una pagina requiera que el usuario este logueado (por medio de decoradores en las vistas, por ejemplo), lo redireccionara automticamente a LOGIN_URL y cuando un usuario haga logout lo redireccionara al valor de LOGOUT_URL
El siguiente paso es decirle a Django que hemos creado una app, en
tutorial_django/settings.py, en INSTALLED_APPS aadimos accounts

el

mismo

archivo

# tutorial_django/settings.py
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'home',
'accounts',
)

Ahora, vamos a crear nuestro modelo, para ello, editamos accounts/models.py con el siguiente cdigo
# accounts.models.py
from django.db import models
from django.conf import settings

class UserProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL)
photo = models.ImageField(upload_to='profiles', blank=True, null=True)

Cada modelo es una subclase de django.db.models.Model y cada atributo de la clase UserProfile representa una columna en la base de datos.
Cuando hagamos una migracin (ahora, dentro de un rato), veremos que los nombres de tablas en la base de datos,
por defecto las crea de la siguiente manera, nombreapp_nombremodelo, convierte todo a minusculas y pone un guion
bajo _ entre el nombre de la app y el nombre del modelo, es decir, cuando cree la tabla en la base de datos nuestro
modelo UserProfile, creara una tabla con el nombre accounts_userprofile.
Cada propiedad en el modelo UserProfile, es una clase en el modulo models y aqu tienes una lista con todos
los campos y opciones en los argumentos de cada campo.
Otra cosa a comentar, es la linea from django.conf import settings, tambin podamos a ver puesto
from django.contrib.auth import User, pero tal y como lo hemos puesto, nos aseguramos que siempre
leemos el modelo User que estamos usando, ya que todo esto lo podemos personalizar y crear nuestros propios
modelos User, por defecto settings.AUTH_USER_MODEL es auth.User.
Antes de continuar, tenemos que instalar un paquete Python Pillow para manejar imgenes.
# Dentro del entorno virtual (tutorial_django)
pip install Pillow

Ahora vamos a crear nuestra primera migracin de una app creada, para ello, en la terminal usamos el comando
makemigrations nombre_app
./manage.py makemigrations accounts

1.9. Creacin accounts

17

Tutorial Django Documentation, Publicacin 1.0

De momento, si vemos en el explorador de archivos, dentro de src/accounts/migrations se ha creado un archivo 0001_initial.py y si lo abrimos, podemos ver que creara tres campos, id, que lo crea de manera implcita
(todos lo modelos crea un campo id de manera implcita a no ser que se diga de manera explicita), photo y user.
Pero si vemos en la base de datos, aun no ha creado nada y antes de que lo cree, vamos a usar el comando
sqlmigrate nombre_app nombre_migracion. En este caso, el nombre_app es accounts y el nombre_migracion 0001_initial (omitimos el .py)(con solo la numeracin es suficiente, en este caso 0001), de
esta manera podemos ver la sentencia SQL que ejecutara cuando usemos el comando migrate.
./manage.py sqlmigrate accounts 0001_initial
BEGIN;
CREATE TABLE "accounts_userprofile" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"photo" varchar(100) NULL,
"user_id" integer NOT NULL UNIQUE REFERENCES "auth_user" ("id")
);

Nota: La sentencia SQL puede variar segn el RDBMS elegido. La mostrada es la que usara con SQLite.
Si nos parece bien lo que va hacer, ejecutamos migrate y ya si que los cambios se reflejaran en la base de datos.
./manage.py migrate

Vamos a ir a la administracin de Django http://127.0.0.1:8000/admin/ y podemos observar que no hay nada diferente!,
donde configuramos los nuevos perfiles de los usuarios? :), hay que decirle a la administracin Django, que nos
muestre el modelo recin creado.
Cuando creamos las apps, un archivo que nos crea en la estructura es admin.py, vamos a editarlo y poner lo
siguiente.
# accounts/admin.py
from django.contrib import admin
from .models import UserProfile
admin.site.register(UserProfile)

Y ahora si nos sale el modelo User Profile

y si pinchamos, podemos ver que no sale ningn campo

18

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

Si pinchamos en Aadir user profile, podemos aadir datos a usuarios, por que como se puede ver, el campo User:,
nos muestra los usuarios que tenemos registrados, en nuestro caso, solo uno y podemos incluir una imagen o no,
ya que el campo, cuando lo creemos como parmetros, pusimos blank=True, null=True, blank es para los
formularios, con True decimos que permitimos que los campos podrn estar vacos y con null es para las base de
datos, con True decimos el el campo permite datos nulos (por defecto, ambos campos son False).
Tambin, podemos observar algo no deseado, si vamos al gestor de archivos, podemos ver que nos ha creado un directorio profiles en la raz del proyecto, lo ideal es contener los archivos media, dentro de un directorio, para configurar
donde almacenar los archivos media, vamos al archivo de configuracin tutorial_django/settings.py y al
final del archivo aadimos:
# tutorial_django/settings.py
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

Donde
MEDIA_ROOT
indica
la
ruta
fsica
del
directorio
(en
este
caso
/path/directorio/proyecto/src/media, que lo genera dinamicamente con os.path.join()) y
MEDIA_URL es lo mismo que STATIC_URL, en las plantillas, antepondr en la URI el valor, en este caso tambin
/media/.
Ahora vamos a crear el directorio media en la raz del proyecto y a mover profiles dentro de media.
mkdir media
mv profiles/ media/

Ahora, si cambias la imagen, veras que la nueva imagen la sube a src/media/profiles.


Si nos fijamos en la administracin, podemos ver que ahora tenemos una entrada

Pero, el nombre que muestra UserProfile object no es muy intuitivo, ahora solo hay uno, pero si hubiesen 100,
a ver como averiguamos que elemento pertenece a X usuario...
Vamos a solucionar esto, vamos a editar el modelo accounts/models.py y aadimos el siguiente mtodo
# accounts/models.py
class UserProfile(models.Model):
# ...
def __str__(self):
return self.user.username

Aqu podemos observar, primero, lo fcil que es acceder los campos de las columnas relacionales, en este caso,
obtenemos el campo username de la clase django.contrib.auth.models.User que tenga relacin con el
objeto actual UserProfile, esto es gracias al ORM que incorpora Django y en segundo lugar, si actualizamos la
pagina de administracin, ahora observamos que nos muestra el username al que pertenece la fila de UserProfile

1.9. Creacin accounts

19

Tutorial Django Documentation, Publicacin 1.0

Con __str__ obtenemos algo y no el objeto en si, que era lo que antes nos mostraba. (Se puede pensar en __str__
como toString en otros lenguajes)
Siguiente paso, crear formularios .py para la representacin .html, creamos un archivo forms.py dentro de la app
accounts y le aadimos el siguiente cdigo:
# accounts/forms.py
from django import forms
from django.contrib.auth.models import User

class RegistroUserForm(forms.Form):
username = forms.CharField(min_length=5)
email = forms.EmailField()
password = forms.CharField(min_length=5, widget=forms.PasswordInput())
password2 = forms.CharField(widget=forms.PasswordInput())
photo = forms.ImageField(required=False)
def clean_username(self):
"""Comprueba que no exista un username igual en la db"""
username = self.cleaned_data['username']
if User.objects.filter(username=username):
raise forms.ValidationError('Nombre de usuario ya registrado.')
return username
def clean_email(self):
"""Comprueba que no exista un email igual en la db"""
email = self.cleaned_data['email']
if User.objects.filter(email=email):
raise forms.ValidationError('Ya existe un email igual en la db.')
return email
def clean_password2(self):
"""Comprueba que password y password2 sean iguales."""
password = self.cleaned_data['password']
password2 = self.cleaned_data['password2']
if password != password2:
raise forms.ValidationError('Las contraseas no coinciden.')
return password2

Los formularios son muy parecidos a los modelos, pero en vez de usar un objeto model usa forms.
Puedes ver aqu la lista completa de campos y widgets para los formularios.
A groso modo, podemos ver que username requiere de al menos 5 caracteres y es un campo tipo text, email es
un campo tipo email, password y password2 son campos tipo password y requiere de al menos 5 caracteres y
photo es un campo tipo file que ademas sera tratado como un archivo de imagen (comprobara que sea un tipo de
imagen).
Con clean_nombre_campo, donde nombre_campo es un campo de propiedad, lo que hace, es una validacin
personalizada cuando el usuario le da al botn del formulario, en este caso, comprueba que password y password2
sean iguales (por eso password2 no le puse min_length=5, por que aqu han de ser iguales y si password no
cumple con los requisitos, lanzara un forms.ValidationError).
20

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

Se puede ver que clean_username y clean_email comprueba si existe un username o email en la base de
datos, si existe lanzara un forms.ValidationError().
Ahora, nos queda ver como implementar esto para que lo muestre en un archivo html, primero vamos a crear la vista
para el registro (de momento, simplificada).
# accounts/views.py
from django.shortcuts import render
from .forms import RegistroUserForm

def registro_usuario_view(request):
if request.method == 'POST':
form = RegistroUserForm(request.POST, request.FILES)
else:
form = RegistroUserForm()
context = {
'form': form
}
return render(request, 'accounts/registro.html', context)

Lo que hacemos es importar el formulario que acabamos de crear RegistroUserForm, despus creamos la vista
para manejar los datos y dentro de la vista esto es lo que hace.
Comprueba si method de la solicitud (request), es POST, es decir, si le a dado al botn del formulario, en
caso de afirmativo, crea una instancia de RegistroUserForm y lo rellena con los datos request.POST,
request.FILES, que son los datos del formulario (si no tuviera un tipo file, no hara falta request.FILES),
en caso contrario, es decir la primera carga de la pagina que el method seria GET, simplemente instanciaria
RegistroUserForm sin datos.
Por ultimo almacenos el formulario en el contexto y renderizamos la pagina, devolviendo la respuesta, el ruta/nombre
plantilla y el contexto.
Es necesario crear una url() en el archivo URLconf, primero, vamos a tutorial_django/urls.py y aadimos la siguiente url dentro de la lista urlpatterns:
# tutorial_django/urls.py
urlpatterns = [
# ...
url(r'^accounts/', include('accounts.urls')),
]

Creamos el archivo accounts/urls.py y aadimos lo siguiente


# accounts/urls.py
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^registro/$', views.registro_usuario_view, name='accounts.registro'),
]

Ya como paso final, creamos el directorio accounts/templates/accounts y dentro creamos el archivo html
registro.html con el siguiente contenido:

1.9. Creacin accounts

21

Tutorial Django Documentation, Publicacin 1.0

# accounts/templates/accounts/registro.html
{% extends 'base.html' %}
{% block title %}Registro de usuario{% endblock title %}
{% block content %}
<div class="row">
<div class="col-md-6 col-sm-offset-3">
<div class="page-header">
<h2>Registro Usuario</h2>
</div>
<form method="post" action="" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Registrar</button>
</form>
</div>
</div>
{% endblock content %}

Una vez mas extendemos la plantilla usando base.html, le damos un <title></title> dentro del bloque { %
block title %}Registro de usuario{ % endblock title %} y le aadimos el contenido dentro del
bloque { % block content %}{ % endblock content %}.
Nota: { % block content %}{ % endblock content %} se puede escribir { % block content %}{ %
endblock %} pero a mi personalmente me gusta aadir en el endblock el nombre al que pertenece el bloque, por
claridad.
En cuanto al formulario hay una tag nueva { % csrf_token %} (wikipedia y documentacion Django lectura obligatoria) la etiqueta es obligatoria por defecto en formularios con method="post".
{{ form.as_p }} form es la variable de contexto que pasamos desde la vista (un objeto RegistroUserForm,
que a su vez es subclase de django.forms.Form), muestra una representacin en html de los campos. Al usar
as_p, rodea los elementos del formulario en etiquetas <p>.
A parte de form.as_p hay dos opciones mas form.as_table y form.as_ul, todos hacen los mismo, lo nico a tener en cuanta es que as_ul y as_table insertan las propiedades del form en
<tr><td>label</td><input></tr> es decir, omite <table> y </table> (as_ul omite <ul> y
</ul>), por otro lado, tambin saber que {{ form.as_X }} no aade las etiquetas html <form></form>
ni lo botones.
Si vamos al navegador con la url http://127.0.0.1:8000/accounts/registro/ podemos ver el siguiente resultado:

22

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

Realmente sencillo, ahora se puede aadir o quitar campos de una manera muy sencilla o usar este formulario en otras
vistas/plantillas sin cambiar nada.
Vamos a poner un poco de estilo con los widgets, volvemos al archivo accounts/forms.py y aadimos los
widgets.
# accounts/forms.py
# ...
username = forms.CharField(
min_length=5,
widget=forms.TextInput(attrs={'class': 'form-control'}))
email = forms.EmailField(
widget=forms.EmailInput(attrs={'class': 'form-control'}))
password = forms.CharField(
min_length=5,
widget=forms.PasswordInput(attrs={'class': 'form-control'}))
password2 = forms.CharField(
min_length=5,
widget=forms.PasswordInput(attrs={'class': 'form-control'}))
photo = forms.ImageField(required=False)
# ...

Para ver y comprender en mas profundidad los formularios, te recomiendo visitar las documentacion de Django.
Ahora falta Que hacer cuando se han validado los datos?, para ello abrimos el archivo accounts/views.py y
lo re escribimos de la siguiente manera.
# accounts/views.py
from
from
from
from

django.shortcuts import render


django.contrib.auth.models import User
django.shortcuts import redirect
django.core.urlresolvers import reverse

1.9. Creacin accounts

23

Tutorial Django Documentation, Publicacin 1.0

from .forms import RegistroUserForm


from .models import UserProfile

def registro_usuario_view(request):
if request.method == 'POST':
# Si el method es post, obtenemos los datos del formulario
form = RegistroUserForm(request.POST, request.FILES)
# Comprobamos si el formulario es valido
if form.is_valid():
# En caso de ser valido, obtenemos los datos del formulario.
# form.cleaned_data obtiene los datos limpios y los pone en un
# diccionario con pares clave/valor, donde clave es el nombre del campo
# del formulario y el valor es el valor si existe.
cleaned_data = form.cleaned_data
username = cleaned_data.get('username')
password = cleaned_data.get('password')
email = cleaned_data.get('email')
photo = cleaned_data.get('photo')
# E instanciamos un objeto User, con el username y password
user_model = User.objects.create_user(username=username, password=password)
# Aadimos el email
user_model.email = email
# Y guardamos el objeto, esto guardara los datos en la db.
user_model.save()
# Ahora, creamos un objeto UserProfile, aunque no haya incluido
# una imagen, ya quedara la referencia creada en la db.
user_profile = UserProfile()
# Al campo user le asignamos el objeto user_model
user_profile.user = user_model
# y le asignamos la photo (el campo, permite datos null)
user_profile.photo = photo
# Por ultimo, guardamos tambien el objeto UserProfile
user_profile.save()
# Ahora, redireccionamos a la pagina accounts/gracias.html
# Pero lo hacemos con un redirect.
return redirect(reverse('accounts.gracias', kwargs={'username': username}))
else:
# Si el mthod es GET, instanciamos un objeto RegistroUserForm vacio
form = RegistroUserForm()
# Creamos el contexto
context = {'form': form}
# Y mostramos los datos
return render(request, 'accounts/registro.html', context)

def gracias_view(request, username):


return render(request, 'accounts/gracias.html', {'username': username})

El cdigo ya esta comentado, pero me gustara comentar el redireccionamiento que se ha hecho cuando el formulario
ha sido valido.
Las partes de cdigo importantes son las siguiente:
Importar los mdulos a usar, mientras redirect redirecciona a otra pagina (como si lo escribiera en la barra de
navegacin del explorador), reverse obtiene a que URI rediseccionar, el primer parmetro de reverse es el
name= que se pone en los URLconf y los kwargs son los parmetros dinmicos que espera en el patrn de

24

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

url().
# accounts/urls.py
url(
r'gracias/(?P<username>[\w]+)/$',
views.gracias_view,
name='accounts.gracias'
),

Esta es la url que se ha aadido al URLconf y se pude ver (?P<username>[\w]+) que es una simple expresin
regular, donde <username> es la variable que pasara a la vista con el valor [\w]+.
Ahora queda la plantilla accounts/templates/accounts/gracias.html con el siguiente cdigo
# accounts/templates/accounts/gracias.html
{% extends 'base.html' %}
{% block title %}Gracias por registrarte{% endblock title %}
{% block content %}
<h2>{{ username }} gracias por registrarte!</h2>
{% endblock content %}

Ya tenemos un registro bsico en la base de datos, podemos ir a la administracin y ver que se han creado 2 filas, una
en la tabla Usuarios y otra en User profiles.
Esto ha sido una manera de hacer un registro de usuario con un profile, hay varias maneras de hacer las cosas e incluso
paquetes de terceros, el objetivo era ver los formularios con forms.Form, pero tambin es posible crear formularios
obteniendo los datos de los modelos forms.ModelForm que veremos mas adelante. Tambin hemos visto como
podemos validar los datos de los campos y lanzar errores forms.ValidationError().
En la siguiente seccin veremos como hacer login y logout :)

1.9.2 Login usuario


Django ya incorpora funciones para hacer login (e incluso vistas y formularios, pero los vamos a omitir en este
tutorial).
En primer lugar creamos la vista
# accounts/views.py
# Aadimos
from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required

@login_required
def index_view(request):
return render(request, 'accounts/index.html')

def login_view(request):
# Si el usuario esta ya logueado, lo redireccionamos a index_view
if request.user.is_authenticated():
return redirect(reverse('accounts.index'))

1.9. Creacin accounts

25

Tutorial Django Documentation, Publicacin 1.0

mensaje = ''
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
user = authenticate(username=username, password=password)
if user is not None:
if user.is_active:
login(request, user)
return redirect(reverse('accounts.index'))
else:
# Redireccionar informando que la cuenta esta inactiva
# Lo dejo como ejercicio al lector :)
pass
mensaje = 'Nombre de usuario o contrasea no valido'
return render(request, 'accounts/login.html', {'mensaje': mensaje})

La vista, lo primero que comprueba es si esta autenticado, si lo esta, mostrar el formulario es tontera y lo redirecciona
a la pagina principal index_view, despus comprueba si la respuesta viene por el mtodo POST, si no lo es,
renderiza la plantilla accounts/login.html, que seria la primea vez que se accede a la pagina.
En caso contrario, significa que la respuesta es por mtodo POST, entonces obtiene los valores de los campos
username y password del formulario con request.POST.get(nombre_campo)
Ahora, comprueba una autenticacin, que lo que hace es comprobar si username y password coinciden con un usuario
en la base de datos y en caso de xito devolver un objeto con el usuario, en caso contrario devolver None con lo que
podemos redirecionar otra vez al formulario con el mensaje Nombre de usuario o contrasea no valido.
Si user no es None, significa que es un objeto User y comprueba si el usuario tiene una cuanta activa con if
user.is_active, si no la tiene, se debera cambiar el mensaje, para que el usuario sea consciente de por que no
puede hacer login. De lo contrario, si todo ha ido bien, el usuario se loguea y lo redirecciona a la vista index_view.
La vista index_view, tiene un decorador que comprueba si esta logueado o no, si no lo esta, redireccionara a la
pagina de login, de lo contrario, podr acceder a la vista.
Nota: el decorador acepta un parmetro (entre otros) @login_required(login_url=), en caso de omitir
el parmetro redireccionara al valor de tutorial_django.settings.LOGIN_URL.
Ahora vamos a crear las dos plantillas, la del formulario para hacer login y la pagina index de accounts, esta ultima
pagina, simplemente mostrara un mensaje de bienvenida y la foto del usuario.
touch accounts/templates/accounts/{index.html,login.html}
# accounts/templates/accounts/index.html
{% extends 'base.html' %}
{% block title %}Perfil de usuario{% endblock title %}

{% block content %}
<div class="page-header"><h2><h2>Perfil de usuario</h2></div>
Hola de nuevo {{ user.username }}<br>
<div>
<img src="{{ MEDIA_URL }}{{ user.userprofile.photo }}" alt="Imagen de {{ user.username }}" />
</div>
{% endblock content %}

Se puede observar que cuando queremos mostrar un archivo media, lo hacemos con {{ MEDIA_URL }}, con esto,
obtiene la url de settings.MEDIA_URL, el resto, lo obtiene con {{ user.userprofile.photo }}. El

26

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

objeto user esta accesible en todos las plantillas, accede a UserProfile con el mismo nombre pero en minsculas
y finalmente accede a la propiedad photo
# accounts/templates/accounts/login.html
{% extends 'base.html' %}
{% block title %}Login{% endblock title %}
{% block content %}
<div class="row">
<div class="col-md-4 col-md-offset-4">
<div class="page-header"><h2>Login</h2></div>
{% if mensaje %}
<div class="alert alert-danger">
{{ mensaje }}
</div>
{% endif %}

<form method="post" action="">


{% csrf_token %}
<div class="form-group">
<label class="control-label" for="username">Nombre de usuario</label>
<input type="text" id="username" name="username" class="form-control" value="{{ u
</div>
<div class="form-group">
<label for="password">Contrasea</label>
<input type="password" name="password" id="password" class="form-control">
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
</div>
</div>
{% endblock content %}

Por ultimo, tenemos que aadir las dos urls en el URLconf.


# accounts/urls.py
# Aadir dentro de urlpatterns
urlpatterns = [
url(r'^$', views.index_view, name='accounts.index'),
url(r'^login/$', views.login_view, name='accounts.login'),
# ...
]

Vamos a ver si todo funciona mas o menos bien :P, para ello, si estas logueado (hasta ahora, la nica manera de
hacerlo era a travs de la administracin) y entras a http://127.0.0.1:8000/accounts/login/, veras que te redirecciona a
http://127.0.0.1:8000/accounts/ (as que, deslogueate desde la administracin y prueba de nuevo).
Y si lo haces al revs, si no estas logueado e intentas acceder a http://127.0.0.1:8000/accounts/, te redireccionara a
http://127.0.0.1:8000/accounts/login/.
No puedes ver la imagen?, :), primero en tutorial_django/settings en la lista TEMPLATES, hay otra
lista context_processors, asegrate que django.template.context_processors.media, esta
incluido en la lista (en Django 1.8, no viene por defecto), a parte, cuando ests con el servidor de desarrollo, en
totorial_django/urls.py inserta lo siguiente:

1.9. Creacin accounts

27

Tutorial Django Documentation, Publicacin 1.0

# tutorial_django/urls.py
# Aade esto, al inicio del documento
from django.conf import settings
# Aade esto, al final del documento
if settings.DEBUG:
# static files (images, css, javascript, etc.)
urlpatterns.append(
# /media/:<mixed>path/
url(
regex=r'^media/(?P<path>.*)$',
view='django.views.static.serve',
kwargs={'document_root': settings.MEDIA_ROOT}))

Prueba ahora a ver si puedes ver la imagen!


Pues ya tenemos casi terminado el sistema de usuarios, queda el contrario, poder hacer logout, que sera en la prxima
seccin y la manera de que el usuario pueda modificar sus datos.

1.9.3 Logout de un usuario


Ahora nos queda que el usuario pueda desloguearse del sitio, de lo que llevamos hecho, esto es lo mas fcil y sobre
todo, como ya debes entender el funcionamiento de las vistas y plantillas, pues todo sera rpido y sin dolor!.
Como siempre, se ha de crear una vista y aadir la url() al URLconf, pero en esta ocasin, no vamos a crear una
plantilla, a cambio, lo mostraremos con un mensaje para as echar un ojo al sistema django.contrib.messages
(hasta los haremos un poco bonitos :P).
Empecemos con la vista, es tan simple que ni la comentare.
# accounts/views.py
# Aadir import logout y messages
from django.contrib.auth import authenticate, login, logout
from django.contrib import messages

def logout_view(request):
logout(request)
messages.success(request, 'Te has desconectado con exito.')
return redirect(reverse('accounts.login'))

Antes de continuar, vamos a crear un pequeo sistema que muestra los mensajes con un poco de estilo CSS gracias a
Bootstrap.
<!-- templates/base.html ->
<!-- Antes de {% block content %}{% endblock content %} aadimos -->
{% include '_messages.html' %}
{% block content %}{% endblock content %}

Con la etiqueta { % include _nombre_plantilla.html %} importa un archivo html y lo vuelca en el lugar donde es
llamado.
El archivo _messages.html es una plantilla que ahora vamos a crear y lo creamos en templates de la raz del
proyecto.

28

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

<!-- templates/_messages.html -->

{% if messages %}
<div class="row dj-messages">
<div class="col-md-6 col-md-offset-3">
{% for message in messages %}
{% if message.tags == 'error' %}
<div class="alert alert-danger" role="alert">
{% else %}
<div class="alert alert-{{ message.tags }}" role="alert">
{% endif %}
{{ message }}<button type="button" class="close" data-dismiss="alert">&times;</bu
</div>
{% endfor %}
</div>
</div>
{% endif %}

Primero comprueba si existe una variable messages y en caso de existir, mostrara el mensaje con un estilo (usando
Bootstrap), con un diseo decente, si no existe messages, no insertara nada.
Tambin vamos a poner en la barra de navegacin, un link para que un usuario pueda hacer login/logout.
<!-- templates/base.html -->
<!DOCTYPE html>
{% load staticfiles %}
<html lang="es">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% endblock title %}</title>
<link rel="stylesheet" href="{% static 'bootstrap/css/bootstrap.css' %}">
<link rel="stylesheet" href="{% static 'css/main.css' %}">
</head>
<body>

<nav class="navbar navbar-inverse navbar-fixed-top">


<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Project name</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="#">Home</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
{% if user.is_authenticated %}
<li><a href="{% url 'accounts.index' %}">{{ user.username }}</a></li>
<li><a href="{% url 'accounts.logout' %}">Logout</a></li>

1.9. Creacin accounts

29

Tutorial Django Documentation, Publicacin 1.0

{% else %}
<li><a href="{% url 'accounts.registro' %}">Registro</a></li>
<li><a href="{% url 'accounts.login' %}">Login</a></li>
{% endif %}
</ul>
</div><!--/.nav-collapse -->
</div>
</nav>
<div class="container">
{% include "_messages.html" %}
{% block content %}{% endblock content %}
</div><!-- /.container -->
<script src="{% static 'jquery/jquery.js' %}"></script>
<script src="{% static 'bootstrap/js/bootstrap.js' %}"></script>
<script src="{% static 'js/main.js' %}"></script>
</body>
</html>

Resalto la parte aadida:


<!-- templates/base.html -->
<ul class="nav navbar-nav navbar-right">
{% if user.is_authenticated %}
<li><a href="{% url 'accounts.index' %}">{{ user.username }}</a></li>
<li><a href="{% url 'accounts.logout' %}">Logout</a></li>
{% else %}
<li><a href="{% url 'accounts.registro' %}">Registro</a></li>
<li><a href="{% url 'accounts.login' %}">Login</a></li>
{% endif %}
</ul>

Como se vio anteriormente, el objeto user, esta disponible en las plantillas y un mtodo es is_autenthicate
(como tambin se vio en una vista), por lo que si esta autenticado, mostrara el botn para acceder al indice de la cuenta
con el nombre del usuario y Logout para darle la posibilidad de desloguearse del sistema, en caso contrario, mostrara
los botones Registro y Login para que pueda registrarse o loguearse respectivamente.
Tambin se puede ver, que cuando haces logout, mostrara el mensaje Te has desconectado con xito con
una x para cerrar el mensaje, el mensaje solo lo mostrara una vez (si actualizas la pagina, no aparecer) y solo lo ve el
usuario que ha hecho logout.
Ya tenemos casi terminado el sistema de usuarios (una versin simple), en la siguiente seccin, veremos como actualizar algunos datos del usuario.

1.9.4 Editar perfil de usuario


Ahora tenemos que darle la oportunidad al usuario de poder modificar datos, como cambiar el email, contrasea y la
foto.
Para empezar, vamos a modificar la pagina principal de accounts para aadir links a la edicin por separado de los
datos.
Vamos a aadir un men lateral, para poner los links y despus, aadiremos
editar_contrasena.html, editar_foto.html, editar_email.html.

tres

plantillas,

Como las tres paginas (a parte de index.html), incluirn el men lateral, seria repetirse cuatro veces la parte del men,
por lo que vamos a crear una plantilla _menu.html.
30

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

Aparte, necesitamos una plantilla para representa el men en un lado y el contenido, en el resto de la pagina, crearemos
tambin una platilla base_accounts.html que represente ambos espacios.
Vamos a crear los archivos:

touch accounts/templates/accounts/{base_accounts.html,editar_contrasena.html,editar_foto.html,editar_

Editamos accounts/templates/accounts/base_accounts.html
<!-- accounts/templates/accounts/base_accounts.html -->
{% extends 'base.html' %}
{% block content %}
<div class="row">
<div class="col-md-2">
<div class="list-group">
<a href="#" class="list-group-item">Editar email</a>
<a href="#" class="list-group-item">Editar contrasea</a>
<a href="#" class="list-group-item">Editar foto</a>
</div>
</div>
<div class="col-md-10">
{% block accounts_content %}{% endblock accounts_content %}
</div>
</div>
{% endblock content %}

Mas tarde, ya aadiremos los links, por ahora, para hacerse una idea ya vale.
Editamos accounts/templates/accounts/index.html
<!-- accounts/templates/accounts/index.html -->
{% extends 'accounts/base_accounts.html' %}
{% block title %}Perfil de usuario{% endblock title %}

{% block accounts_content %}
<div class="page-header"><h2><h2>Perfil de usuario</h2></div>
Hola de nuevo {{ user.username }}<br>
<div>
<img src="{{ MEDIA_URL }}{{ user.userprofile.photo }}" alt="Imagen de {{ user.username }}" />
</div>
{% endblock accounts_content %}

Los cambios ({ % extends accounts/base_accounts.html %} y los blocks { % block


accounts_content %} y { % endblock accounts_content %}), son fciles de entender, en vez
de usar extends de base.html (base_accounts.html usa extends a base.html, por lo que no
perderemos el diseo anterior) usara uno para que las cuatro paginas, tengan el mismo aspecto.
Editar Email
Empecemos con editar email, necesitamos un formulario de un solo campo, despus el email ha de ser nico en la base
de datos (si lo ha cambiado, comprobar que no exista uno igual), en caso de xito, redireccionamos otra vez a index,
en caso contrario, se lo notificaremos a usuario.
El formulario

1.9. Creacin accounts

31

Tutorial Django Documentation, Publicacin 1.0

# accounts/forms.py
class EditarEmailForm(forms.Form):
email = forms.EmailField(
widget=forms.EmailInput(attrs={'class': 'form-control'}))
def __init__(self, *args, **kwargs):
"""Obtener request"""
self.request = kwargs.pop('request')
return super().__init__(*args, **kwargs)
def clean_email(self):
email = self.cleaned_data['email']
# Comprobar si ha cambiado el email
actual_email = self.request.user.email
username = self.request.user.username
if email != actual_email:
# Si lo ha cambiado, comprobar que no exista en la db.
# Exluye el usuario actual.
existe = User.objects.filter(email=email).exclude(username=username)
if existe:
raise forms.ValidationError('Ya existe un email igual en la db.')
return email

La vista
# accounts/views.py
@login_required
def editar_email(request):
if request.method == 'POST':
form = EditarEmailForm(request.POST, request=request)
if form.is_valid():
request.user.email = form.cleaned_data['email']
request.user.save()
messages.success(request, 'El email ha sido cambiado con exito!.')
return redirect(reverse('accounts.index'))
else:
form = EditarEmailForm(
request=request,
initial={'email': request.user.email})
return render(request, 'accounts/editar_email.html', {'form': form})

Es la misma rutina de siempre, comprueba el mtodo, y acta en consecuencia, lo nico distinto, es ver como pasamos
el objeto HttpRequest al instanciar el formulario y como rellenamos datos en el formulario con el argumento
initial.
La plantilla
<!-- accounts/templates/accounts/editar_email.html -->
{% extends 'accounts/base_accounts.html' %}
{% block title %}Editar email{% endblock title %}
{% block accounts_content %}
<h2 class="page-header">Editar Email</h2>
<form method="post" action="">

32

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

{% csrf_token %}
{{ form.as_p }}
<button class="btn btn-primary" type="submit">Actualizar Email</button>
<a href="{% url 'accounts.index' %}" class="btn btn-warning" type="submit">Cancelar</a>
</form>
{% endblock accounts_content %}

URLconf
# accounts/urls.py
# Aadimos en urlpatterns
url(r'^editar_email/$', views.editar_email, name='accounts.editar_email'),

Y actualizamos el link en accounts/templates/accounts/base_accounts.html


<!-- accounts/templates/accounts/base_accounts.html -->
<a href="{% url 'accounts.editar_email' %}" class="list-group-item">Editar email</a>

Editar contrasea
La contrasea requiere de tres campos, uno con la contrasea actual, otro para insertar nueva contrasea y un ultimo
para repetir de nueva contrasea.
El formulario
# accounts/forms.py
class EditarContrasenaForm(forms.Form):
actual_password = forms.CharField(
label='Contrasea actual',
min_length=5,
widget=forms.PasswordInput(attrs={'class': 'form-control'}))
password = forms.CharField(
label='Nueva contrasea',
min_length=5,
widget=forms.PasswordInput(attrs={'class': 'form-control'}))
password2 = forms.CharField(
label='Repetir contrasea',
min_length=5,
widget=forms.PasswordInput(attrs={'class': 'form-control'}))
def clean_password2(self):
"""Comprueba que password y password2 sean iguales."""
password = self.cleaned_data['password']
password2 = self.cleaned_data['password2']
if password != password2:
raise forms.ValidationError('Las contraseas no coinciden.')
return password2

La vista
# accounts/views.py

1.9. Creacin accounts

33

Tutorial Django Documentation, Publicacin 1.0

# Aadir al inicio
from django.contrib.auth.hashers import make_password
# Modificar al inicio
from .forms import (
RegistroUserForm, EditarEmailForm, EditarContrasenaForm)
# Aadir al final
@login_required
def editar_contrasena(request):
if request.method == 'POST':
form = EditarContrasenaForm(request.POST)
if form.is_valid():
request.user.password = make_password(form.cleaned_data['password'])
request.user.save()
messages.success(request, 'La contrasea ha sido cambiado con exito!.')
messages.success(request, 'Es necesario introducir los datos para entrar.')
return redirect(reverse('accounts.index'))
else:
form = EditarContrasenaForm()
return render(request, 'accounts/editar_contrasena.html', {'form': form})

Observa como usamos make_password() para generar un password con hash (no se traducir esto, lo siento :)),
es muy importante, ya que si no, guardara la contrasea en texto plano y es un gran error por motivos de seguridad!.

(Lo pongo aqu, aunque seria parte del EditarContrasenaForm), tambin hay una funcin
check_password() <https://docs.djangoproject.com/en/1.4/topics/auth/#django.contrib.auth.ha
que podramos a ver comprobado en un mtodo clean_actual_password() y comprobar
si actual_password es igual a password informar al usuario que esta usando la misma contrasea que la actual
(lo dejo como ejercicio para el lector).
La plantilla
<!-- accounts/templates/accounts/editar_contrasena.html -->
{% extends 'accounts/base_accounts.html' %}
{% block title %}Editar email{% endblock title %}
{% block accounts_content %}
<h2 class="page-header">Editar contrasea</h2>
<form method="post" action="">
{% csrf_token %}
{{ form.as_p }}
<button class="btn btn-primary" type="submit">Actualizar contrasea</button>
<a href="{% url 'accounts.index' %}" class="btn btn-warning" type="submit">Cancelar</a>
</form>
{% endblock accounts_content %}

El URLconf
# accounts/urls.py
# Aadir a urlpatterns
url(r'^editar_contrasena/$', views.editar_contrasena, name='accounts.editar_contrasena'),

Actualizar base_accounts.html

34

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

<!-- accounts/templates/accounts/base_accounts.html -->


<a href="{% url 'accounts.editar_contrasena' %}" class="list-group-item">Editar contrasea</a>

Ya solo queda editar la imagen, pero lo dejo como ejercicio para el lector, ademas, tambin dejo como ejercicio, si nos
fijamos, las plantillas editar_x son prcticamente iguales, es decir nos repetimos demasiado!!, intenta que con una
plantilla muestra los datos que quieres mostrar. Como pista podras crear una sola pagina de formulario y dentro de la
pagina aadir variables de contexto {{ titulo }}, etc y pasarlas de las vistas a las plantillas.

1.10 Creacion blog


En esta ocasin, vamos a crear un blog, donde se explicara en mas detalles partes como los modelos y vistas, que se
vio en accounts, pero se profundizara mas.
En esta parte del tutorial, se usaran CBV y ModelForm para mostrar otros conceptos del Framework.

1.10.1 Creacin APP Blog


Hasta ahora, tenemos creado toda la parte de gestin de usuario, registro, login, logout el usuario puede cambiar
algunos datos de su perfil.
Comprendemos como estructurar las plantillas, como incluir archivos media y static, comprendemos las relaciones
model, view, template MVT (o MVC), como aadir patrones en el router, etc.
A partir de ahora, cambiare un poco las maneras de hacer el tutorial, ya que si no, seria repetirse un poco.
En vez de usar vistas basadas en funciones (Function Based Views FBV), usaremos vistas basadas en clases (Class
Based Views CBV), intentare explicar los fundamentos del ORM de Django, los fundamentos de las plantillas,
inclusion tags y otras cosas.
De momento, solo vamos a crear la app y aadirla en INSTALLED_APPS y e incluir las urls.
./manage.py startapp blog
touch blog/urls.py
mkdir -p blog/templates/blog
# tutorial_django/settings.py
INSTALLED_APPS = (
# ...
'home',
'accounts',
'blog',
)

La app ya esta creada, en la siguiente seccin, empezamos con los modelos.

1.10.2 Creacin del modelo


Una aplicacin siempre que use una base de datos, se comienza por el modelo, la representacin de la base de datos,
sus tablas y sus campos (clases y propiedades en Python), el blog se compone de dos tablas, una para las etiquetas Tag
y otra para los artculos Article.
1.10. Creacion blog

35

Tutorial Django Documentation, Publicacin 1.0

Tag tendra tres campos


id Primary Key creado de manera implcita.
name string, nico.
slug string, nombre amigable de name, nico.
Article tendr mas campos, el id, titulo, slug, owner (propietario del articulo)
id Primary Key creado de manera implcita.
title string, nico.
slug string, nombre amigable de title, nico.
body text, cuerpo del articulo.
owner ForeignKey (User), propietario del articulo.
tags ManyToMany (Tag), lista de etiquetas.
create_at DateTime, fecha y hora de creacin.
update_at DateTime, fecha y hora de ultima modificacin.
Los Primary Key, los crea de manera implcita a no ser que se cree explcitamente, owner es una relacin un
usuario sera propietario de muchos artculos o muchos artculos pueden pertenecer a un mismo propietario,
entonces desde la perspectiva Article es una relacin muchos a uno y desde la perspectiva User uno a muchos
(corrjanme si estoy equivocado). En cuanto a tags es una relacin muchos a muchos, una articulo puede pertenecer
a muchas Tag y un Tag pueden contener muchos Article.
Cuando se crea un campo Foreign Key aade un campo extra en la base de datos, en el caso de owner la tabla
article creara un campo owner_id que sera la relacin con auth_user.id, en cuanto a tags, creara una
nueva tabla blog_article_tag con tres columnas, id, article_id y tag_id
Con esto en mente, vamos a crear el modelo en blog/models.py, lo mas simple posible, ya mas adelante iremos
aadiendo mas cdigo.
from django.db import models
from django.contrib.auth.models import User

class Tag(models.Model):
name = models.CharField(max_length=100, unique=True)
slug = models.CharField(max_length=100, unique=True)

class Article(models.Model):
title = models.CharField(max_length=100, unique=True)
slug = models.CharField(max_length=100, unique=True)
body = models.TextField()
owner = models.ForeignKey(User, related_name='article_owner')
tags = models.ManyToManyField(Tag, related_name='article_tags')
create_at = models.DateTimeField(auto_now_add=True)
# update_at = models.DateTimeField(auto_now=True)

Nota: He dejado un campo comentado a propsito update_at = models.DateTimeField(auto_now=True)


que mas tarde aadiremos, lo hago para mostrar como actan las migraciones.

36

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

Creo que con lo que se comento anteriormente, mas lo que llevamos hecho con accounts, es bastante claro lo que
hace, pero quiz haya un par de cosas a comentar, los argumentos de las propiedades, algunos argumentos como
max_length en los clases CharField son obligatorios, y especifica el tamao del campo en la base de datos,
ademas, en los formularios, comprobara que el texto insertado por el usuario, no pase de la cantidad puesta. En cuanto
a unique, declaramos que el campo sera nico.
Otro apunte son los campos owner y tags de la clase Article, el primer argumento, hace referencia a la relacin, en este caso la clase a la que tiene relacin y related_name es un nombre al que podremos acceder cuando
hagamos relacin inversa, en el caso de Tag, cuando queramos acceder al titulo, lo haremos de la siguiente manera tag_object.article_tags.title. related_name, es opcional y en caso de omitirlo, para acceder al
campo title se haria tag_object.article.title, es decir, nombre de clase relacional en minsculas.
auto_now_add y auto_now, argumentos dice: auto_now_add inserta de manera implcita la fecha y hora (en
este caso DateTime) cuando se crea un articulo y auto_now, actualiza la fecha y hora, cuando se actualice un
articulo.
Nota: En este caso, La clase Article esta debajo de Tag en el cdigo Python y por eso tags =
models.ManyToManyField(Tag) es posible poner Tag, es decir el literal de clase, en caso de que la clase
Tag estuviera debajo de Article, para decirle que la relacin es la clase Tag, se deber usar entre comillas, tags
= models.ManyToManyField(Tag).
El siguiente paso es crear una migracin (preparar los cambios) y migrar (crear los cambios en la base de datos).
./manage.py makemigrations blog
Migrations for 'blog':
0001_initial.py:
- Create model Article
- Create model Tag
- Add field tags to article

Antes de crear la migracin, vamos a ver que cdigo SQL nos va a generar (el cdigo es el generado para SQLite).
./manage.py sqlmigrate blog 0001_initial
BEGIN;
CREATE TABLE "blog_article" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"title" varchar(100) NOT NULL UNIQUE,
"slug" varchar(100) NOT NULL UNIQUE,
"body" text NOT NULL,
"create_at" datetime NOT NULL,
"owner_id" integer NOT NULL REFERENCES "auth_user" ("id")
);
CREATE TABLE "blog_tag" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" varchar(100) NOT NULL UNIQUE,
"slug" varchar(100) NOT NULL UNIQUE
);
CREATE TABLE "blog_article_tags" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"article_id" integer NOT NULL REFERENCES "blog_article" ("id"),
"tag_id" integer NOT NULL REFERENCES "blog_tag" ("id"), UNIQUE ("article_id", "tag_id")
);
CREATE INDEX "blog_article_5e7b1936" ON "blog_article" ("owner_id");
CREATE INDEX "blog_article_tags_a00c1b00" ON "blog_article_tags" ("article_id");
CREATE INDEX "blog_article_tags_76f094bc" ON "blog_article_tags" ("tag_id");

1.10. Creacion blog

37

Tutorial Django Documentation, Publicacin 1.0

COMMIT;

Si nos parece bien, ejecutamos la migracin para hacer los cambios (en este caso crear las tablas) en la base de datos.
./manage.py migrate
Operations to perform:
Synchronize unmigrated apps: messages, staticfiles
Apply all migrations: accounts, contenttypes, admin, auth, blog, sessions
Synchronizing apps without migrations:
Creating tables...
Running deferred SQL...
Installing custom SQL...
Running migrations:
Rendering model states... DONE
Applying blog.0001_initial... OK

Como se puede ver en la base de datos, ahora se han creado las tres tablas. Antes comentemos un campo (propiedad)
en la clase Article, la descomentamos y ejecutamos makemigrations blog y migrate.
./manage.py makemigrations blog

You are trying to add a non-nullable field 'update_at' to article without a default; we can't do that
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows)
2) Quit, and let me add a default in models.py
Select an option:

Nos esta diciendo que el campo update_at es un campo not null por lo que no puede aadir un campo sin datos
(en este caso no hay filas, pero bueno :)), as que nos pregunta si queremos aadir un dato ahora o cambiar el modelo
y aadir un dato por defecto.
En este caso, vamos a aadir la opcion 1 y aadimos timezone.now().
Select an option: 1
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now()
>>> timezone.now()
Migrations for 'blog':
0002_article_update_at.py:
- Add field update_at to article

Y hacemos la migracin para actualizar la base de datos:


./manage.py migrate

Ahora el campo update_at ya esta en la base de datos :)


Antes de continuar, explicar que las clases de modelo creadas Tag y Article son subclases de
django.db.models.Model, que tienen una propiedad objects (por lo tanto Tag y Article tambin tienen la propiedad objects), que es una clase django.db.models.Manager. Es una interface a travs de la
cual las operaciones de consulta de base de datos se proporcionan a los modelos de Django, en resumen, el manager
(gestor) de un modelo es un objeto a travs del cual los modelos de Django realizan consultas de bases de datos. Cada
modelo de Django tiene al menos un manager, y puedes crear managers personalizados con el fin de personalizar el
acceso de base de datos.
Django tiene un argumento con ./manage.py que es shell y es lo mismo que la consola interactiva de Python,
pero que aade al path el proyecto (ejecuta internamente django.setup()) y tenemos a nuestra disposicin los
modelos del proyecto.

38

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

Desde la consola vamos a ver 4 cosillas para interactuar con el ORM incorporado de Django y aadir, actualizar, etc
filas en la base de datos.
./manage.py shell

Ahora, como si estuviramos en un archivo .py, vamos a crear artculos, primero importamos los mdulos y creamos
un par de tags, despus creamos algunos artculos.
Ir # comentando paso a paso las instrucciones, >>> es indicativo de que es una instruccin y se ha de omitir en la
escritura en la terminal, por ultimo tambin mostrare la salida.
>>> from blog.models import Tag, Article
# Comprobar cuantas etiquetas hay, para ello se usa el metodo all()
# que obtiene una lista con todos los elementos existentes en la db (si los hay)
>>> Tag.objects.all()
[]
# Creamos un objeto Tag, insertamos datos y guardamos con save() el objeto
# Con save(), guardara en la base de datos la fila
>>> tag = Tag()
>>> tag.name = 'Linux'
>>> tag.slug = 'linux'
>>> tag.save()
>>> Tag.objects.all()
[<Tag: Tag object>]

Como se puede ver, nos devuelve [<Tag: Tag object>], una lista con un elemento, vamos a modificar
blog/models.py
# blog/models.py
class Tag(models.Model):
# .....
def __str__(self):
return self.name

class Article(models.Model):
# .....
def __str__(self):
return self.title

Al a ver realizado un cambio en el archivo models.py, se ha de salir del interprete


>>> exit()
./manage.py shell
>>> from blog.models import Tag, Article
>>> Tag.objects.all()
[<Tag: Linux>]
# Vamos a crear 2 tags mas, pasando los datos en el 'constructor' de Tag
>>> tag1 = Tag(name='Windows', slug='windows')
>>> tag1.save()

1.10. Creacion blog

39

Tutorial Django Documentation, Publicacin 1.0

>>> Tag.objects.all()
[<Tag: Linux>, <Tag: Windows>]
>>> tag2 = Tag(name='Mac OS X', slug='mac-os-x')
>>> tag2.save()
>>> Tag.objects.all()
[<Tag: Linux>, <Tag: Windows>, <Tag: Mac OS X>]

Ahora vamos a modificar un elemento, primero obtendremos el elemento que queremos modificar con
filter(nombre_campo=valor_campo), donde nombre_campo, es el nombre de la propiedad Tag y
valor_campo es un valor, por ejemplo Windows. Nos devolver siempre, una lista con 0 o mas elementos, tantos
como campos tengan un valor Windows (en este caso, la propiedad name es unique, por lo que obtendremos 0 o 1
elemento) y en este caso, si no existe, no lanzara una excepcin (mas tarde veremos lo de en este caso)
Despus de obtener el elemento (que sabemos de antemano, que sera 0 o 1 elemento), lo modificaremos y por ultimo
actualizaremos los datos en la base de datos.
>>> Tag.objects.all()
[<Tag: Linux>, <Tag: Windows>, <Tag: Mac OS X>]
# Obtener el primer elemento
>>> w = Tag.objects.filter(name='Windows')[0]
>>> w
<Tag: Windows>
>>> type(w)
<class 'blog.models.Tag'>
>>> w.name = 'Microsoft Windows'
>>> w.slug = 'microsoft-windows'
>>> w.save()
>>> w
<Tag: Microsoft Windows>
>>> Tag.objects.all()
[<Tag: Linux>, <Tag: Microsoft Windows>, <Tag: Mac OS X>]

Si observamos en el filtro Tag.objects.filter(name=Windows)[0] el [0], es puro Python, la manera


de obtener x elementos, Django lo traduce como:
Tag.objects.filter(field=valor)[0] LIMIT 1 El elemento que corresponda a X, el elemento
0 es el primer elemento.
Tag.objects.filter(field=valor)[:5] LIMIT 5 Los 5 primeros elementos
Tag.objects.filter(field=valor)[5:10] OFFSET 5 LIMIT 5 Cinco elementos a partir del
5 elemento.
Por lo tanto, solo devuelve 1 elemento y es el objeto, en caso de no utilizar [x] o [x:x], siempre devolver una lista.
As que w es un objeto Tag por lo que se puede acceder a sus propiedades y mtodos directamente y lo que hacemos
es modificar el name y slug, por ultimo, guardamos los cambios en la base de datos.
Vamos primero a modificar el modelo, la clase django.db.models.Model tiene un mtodo save(), que
se ejecuta justo antes de guardar/actualizar datos en la base de datos. Vamos a aprovecharlo para cambiar el
slug dinamicamente, asi solo sera necesario cambiar/poner el name y justo antes de guardar/actualizar, Django(Python), nos cambiara el slug. A la vez, Django tiene una funcin para generar slugs validos que se encuentra
en django.utils.text.slugify.
# blog/models.py
# Aadir al inicio
from django.utils.text import slugify
class Tag(models.Model):

40

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

# ...
def save(self, *args, **kwargs):
self.slug = slugify(self.name)
return super().save(*args, **kwargs)

class Article(models.Model):
# ...
def save(self, *args, **kwargs):
self.slug = slugify(self.title)
return super().save(*args, **kwargs)

Vamos a cambiar de nuevo Microsoft Windows por Windows y a ver que pasa
>>> w = Tag.objects.filter(name='Microsoft Windows')[0]
>>> w
<Tag: Microsoft Windows>
>>> w.name = 'Windows'
>>> w.save()
>>> Tag.objects.filter(name='Windows')[0].slug
'windows'

Se puede observar, que ahora el slug es generado dinamicamente :)


Otro mtodo de Manager, es get(**kwargs), como argumentos, acepta pares clave/valor, como
filter() a excepcin que siempre devuelve un solo elemento y si hay mas de un elemento lanzara
MultipleObjectsReturned y que si no hay coincidencia, lanzara DoesNotExist.
>>> Tag.objects.get(pk=1)
<Tag: Linux>
>>> Tag.objects.get(slug='windows')
<Tag: Windows>
>>> Tag.objects.get(id=1)
<Tag: Linux>

>>> Tag.objects.get(id=10)
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/home/snicoper/.virtualenvs/tutorial_django/lib/python3.4/site-packages/django/db/models/mana
return getattr(self.get_queryset(), name)(*args, **kwargs)
File "/home/snicoper/.virtualenvs/tutorial_django/lib/python3.4/site-packages/django/db/models/quer
self.model._meta.object_name
blog.models.DoesNotExist: Tag matching query does not exist.

>>> Tag.objects.get(name__icontains='i')
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/home/snicoper/.virtualenvs/tutorial_django/lib/python3.4/site-packages/django/db/models/mana
return getattr(self.get_queryset(), name)(*args, **kwargs)
File "/home/snicoper/.virtualenvs/tutorial_django/lib/python3.4/site-packages/django/db/models/quer
(self.model._meta.object_name, num)
blog.models.MultipleObjectsReturned: get() returned more than one Tag -- it returned 2!

Aunque el campo de clave primaria pk es id, tambin es posible usar pk en los campos y Django, usara el nombre del
campo que sea PRIMARY KEY (que por defecto es siempre id).

1.10. Creacion blog

41

Tutorial Django Documentation, Publicacin 1.0

name__icontains es un Field lookups (Bsquedas de campo), contiene el nombre de la propiedad y


__tipodebusqueda=valor(dos guiones bajos), en este caso icontains y la i de insensitive, hace una bsqueda insensitiva (me van a matar los puristas del castellano, sry ^^), es decir en SQL seria
algo as:
Tag.objects.filter(name__icontains=algo) se traducira a SELECT * FROM blog_tag
WHERE name ILIKE %algo %
Hay muchas bsquedas de campo y le puedes echar un ojo en la documentacin de Django .
Ahora, vamos a crear algunas entradas.
# Importar Tag y Article y el usuario creado
>>> from blog.models import Tag, Article
>>> from django.contrib.auth.models import User
>>> u = User.objects.get(pk=1)
>>> u
<User: snicoper>
>>> u.email
'snicoper@gmail.com'
# Obtenemos 2 de 3 elementos tags que hay en la db
>>> ts = Tag.objects.all()[1:]
>>> ts
[<Tag: Windows>, <Tag: Mac OS X>]
# Creamos un articulo
>>> a = Article()
>>> a.title = 'Primer articulo'
>>> a.body = 'Contenido del articulo'
>>> a.owner = u
# Es necesario guardar el objeto antes de aadir relaciones many to many
>>> a.save()
# y aadimos las relaciones, una lista con 2 elementos
>>> a.tags.add(*ts)
# Guardamos los cambios
>>> a.save()

# Comprobamos los resultados


>>> article = Article.objects.get(title='Primer articulo')
>>> article.owner
<User: snicoper>
>>> article.tags
<django.db.models.fields.related.create_many_related_manager.<locals>.ManyRelatedManager object at 0x
>>> article.tags.all()
[<Tag: Windows>, <Tag: Mac OS X>]
>>> article.slug
'primer-articulo'

Ahora, vamos a ver, cuantos artculos ha publicado el usuario


>>> from django.contrib.auth.models import User
>>> u = User.objects.get(pk=1)

# Accedemos al modelo Article, con el nombre de related_name que pusimos


>>> u.article_owner
<django.db.models.fields.related.create_foreign_related_manager.<locals>.RelatedManager object at 0x7
>>> u.article_owner.all()
[<Article: Primer articulo>]
>>> u.article_owner.all()[0].title

42

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

'Primer articulo'
# Rizar el rizo
# obtener el primer articulo de todos los que haya publicado el usuario
# obtener la primera tag, de todas las que tenga el articulo y mostrar el name
>>> u.article_owner.all()[0].tags.all()[0].name
'Windows'

Poco a poco iremos viendo y comentado nuevos mtodos que tiene Manager, pero para empezar, creo que se hace
uno una idea del tema, para ver mas sobre el tema, te recomiendo la documentacin
En la siguiente seccin, veremos un poco por encima la administracin Django.

1.10.3 Administracin
La administracin de Django es un aadido espectacular, genera dinamicamente un sistema CRUD que nos ahorra
cientos de horas, que incluso usando otros lenguajes como PHP o ASP.NET MVC 6 (vnext), yo al menos incluira el
sistema al menos para la parte del desarrollo por lo cmodo que es.
Lo vimos con Creacin accounts, donde mostrbamos como podamos editar, aadir o ver los perfiles de usuario.
Para empezar vamos a hacer lo mismo, en primer lugar arrancamos el servidor ./manage.py runserver y vamos
a ir a la administracin http://127.0.0.1:8000/admin/

Se observa (y ahora, se esperaba) que no muestra los modelos creados en el blog, para ello, como con accounts,
editamos el archivo blog/admin.py para incluir los modelos a la administracin.
# blog/admin.py
from django.contrib import admin
from .models import Article, Tag
admin.site.register(Article)
admin.site.register(Tag)

Ahora si actualizamos veremos que muestra ambos modelos (tambin, algo que ya esperbamos).

1.10. Creacion blog

43

Tutorial Django Documentation, Publicacin 1.0

Lo primero que vamos hacer es cambiar los nombres que muestra de los modelos, por defecto (usa reglas del ingles),
aade s o es al final del nombre del modelo, Article lo cambia a Articles y Country lo cambiara a Countries y no
siempre es lo que desearamos, a parte, si vemos en User profiles que pertenece al modelo UserProfile, se intuye
que parte las palabras en las letras maysculas y lo convierte en minsculas excepto el primer carcter.
Para cambiar este comportamiento por defecto, vamos a crear unas clases dentro de las clases del modelo llamada
Meta y dentro, aadiremos meta informacin.
Primero cambiamos el modelo UserProfile
# accounts/models.py
class UserProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL)
photo = models.ImageField(upload_to='profiles', blank=True, null=True)
class Meta:
verbose_name = 'Perfil de usuario'
verbose_name_plural = 'Perfiles de usuarios'
# ...

Y ahora, el modelo del blog


# blog/models.py
class Tag(models.Model):
# ...
class Meta:
verbose_name = 'Etiqueta'
verbose_name_plural = 'Etiquetas'
# ...

class Article(models.Model):
# ...
class Meta:
verbose_name = 'Articulo'

44

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

verbose_name_plural = 'Articulos'
# ...

Realmente fcil como cambiar el nombre que mostrara los modelos.


Ahora si pinchamos en Articulos vemos que nos muestra un elemento

Siendo sinceros, no es mucha informacin, para ver quien lo creo y cuando, abra que pinchar para editarlo y ver la
informacin (que las fechas ni las podramos ver en un principio).
Nota:
Las fechas en los modelos en los campos pasamos los argumentos auto_now=True y
auto_now_add=True (se aadirn o modificaran automticamente) por lo que en la administracin no muestra los campos. Si queremos modificarlos a mano, en los campos del modelo, deberemos aadir el argumento editable=True de esta manera create_at = models.DateTimeField(auto_now_add=True,
editable=True)
De igual manera, los slugs podramos hacer que no muestre el campo en la administracin (recuerda que se generan automticamente antes de guardar el objeto) de esta manera slug = models.SlugField(max_length=100,
editable=False)
Vamos a modificar el comportamiento, para que nos muestre los campos que consideremos que nos de una informacin
til.
# blog/admin.py
class ArticleAdmin(admin.ModelAdmin):
list_display = ('title', 'owner', 'create_at', 'update_at')
admin.site.register(Article, ArticleAdmin)

Hemos creado una clase ArticleAdmin que es subclase de django.contrib.admin.ModelAdmin, hemos aadido una propiedad list_display con una tupla (podra ser una lista), con las propiedades del modelo
Article que queremos mostrar.
1.10. Creacion blog

45

Tutorial Django Documentation, Publicacin 1.0

Despus aadimos (registramos) ArticleAdmin en admin.site.register(Article, ArticleAdmin)


donde el primer argumento es el modelo, y el segundo la subclase de ModelAdmin y el resultado podemos ver en la
siguiente captura.

Podemos crear nuestros propios mtodos en el modelo para mostrar informacin. Por ejemplo, vamos a obtener un
string con las etiquetas con las Tags, que tiene el articulo.
# blog/models.py
# Aadimos el metodo en la clase(modelo) Article
def get_string_tags(self):
return ', '.join([tag.name for tag in self.tags.all()])
# blog/admin.py
# Modificamos el list_display
list_display = ('title', 'owner', 'create_at', 'update_at', 'get_string_tags')

Simplemente aadiendo como string get_string_tags (no podemos aadir un mtodo que acepte parmetros),
obtenemos la devolucin del mtodo (debe devolver valores simples, strings, fechas, int, etc). Se puede ver el resultado en la siguiente captura.

Ahora, si pinchamos dentro de Primer articulo podemos ver lo siguiente.

Vamos solo a cambiar una cosa aqu (aunque se podran hacer muchas cosas), si nos fijamos en Tags:, muestra las 2
tags que tiene el articulo.
Vamos a cambiar la forma en que muestra las etiquetas, para que seas mas fciles de seleccionar, usando la propiedad filter_horizontal, aadimos en una tupla o lista, las propiedades del modelo ManyToManyField que
queremos que muestre dos cajas los elementos.

46

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

# blog/admin.py
class ArticleAdmin(admin.ModelAdmin):
list_display = ('title', 'owner', 'create_at', 'update_at', 'get_string_tags')
filter_horizontal = ('tags',)

Y el resultado:

Esta seccin la damos por terminada, aunque esto es solo la punta del iceberg, te recomiendo la documentacin para
una lista completa de lo que se puede hacer en la administracin.
En las siguientes secciones, vamos a generar un sistema CRUD (aunque teniendo la administracin, no es necesario,
pero para demostrar lo fcil que es crear, editar y eliminar elementos, lo haremos)

1.10.4 Preparar plantilla base_blog.html


Al igual que hicimos con las plantillas en accounts, cuando creemos accounts/base_accounts.html como
base de para el perfil de usuario, aqu haremos lo mismo, en realidad algo parecido.
Primero una idea de lo que queremos hacer

1.10. Creacion blog

47

Tutorial Django Documentation, Publicacin 1.0

La barra de navegacin superior es la misma que en todo el sitio, el bloque central, sera donde mostraremos la lista
de articulo o un articulo en detalle (que sern las plantillas normales con extends base.html) y los bloques
laterales, donde, por ejemplo, podemos crear una lista de ultimas entradas o lista de etiquetas o cualquier cosa que se
nos ocurra.
Los bloques laterales es lo que hay de diferente en el sitio hasta el momento y aadirlos en todas las platillas seria un
curro grande, sin contar que una modificacin, significara modificar todas las paginas que tengan esos bloques.
A diferencia de accounts, que incluamos el bloque con contenido esttico con un { % include
_nombre_plantilla.html %}, aqu puede ser dinmico, intento explicarme...
Hay dos maneras de pasar contenido, la primera es a travs del contexto cuando renderizamos una plantilla a travs
del mtodo render(), eso significara estar pasando los datos de ultimas entradas y lista de etiquetas y eso seria
molesto, seria incluir ese contexto en todas las vistas.
Hay una manera para solucionar esto y es con inclusion_tag. Con una inclusion tag podemos tener acceso a una
plantilla que generara los datos de manera dinmica sin importarnos el contexto pasado en las vistas.
Vamos a empezar por el principio, creamos el base_blog.html para crear la estructura de la app blog.
touch blog/templates/blog/base_blog.html
# <!-- blog/templates/blog/base_blog.html -->
{% extends 'base.html' %}
{% block content %}
<div class="row">
<div class="col-md-9">
{% block blog_content %}{% endblock blog_content %}
</div>
<div class="col-md-3">
{% include 'blog/_menus.html' %}
</div>
</div>
{% endblock content %}

48

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

Ahora, vamos a crear el index para el blog, que sera el que muestra la lista de artculos.
touch blog/templates/blog/article_list.html
# <!-- blog/templates/blog/article_list.html -->
{% extends 'blog/article_list.html' %}
{% block title %}Blog - Lista de articulos{% endblock title %}
{% block blog_content %}
<h2>Aqui mostrar la lista de articulos</h2>
{% endblock blog_content %}

Creamos un archivo de plantilla, para crear los mens laterales


touch blog/templates/blog/_menus.html
<p>Menus laterales</p>

De momento, aunque sin funcionalidad, ya esta mas o menos preparado, separando los componentes de plantilla, para
hacerlos mas fciles de modificar y que un cambio, se refleje en varias paginas.
Vamos a crear la vista, a modo de demostracin, vamos a crear una clase basada en funcin, pero que mas tarde
cambiaremos a una vista basada en clase.
# blog/views.py
from django.shortcuts import render
from .models import Article

def article_list_view(request):
articles = Article.objects.all()
context = {'articles': articles}
return render(request, 'blog/article_list.html', context)

Nada nuevo, obtenemos todos los artculos del modelo Article los aadimos a un diccionario para el contexto y
renderizamos la pagina blog/article_list.html
Ahora article_list.html, para mostrar los artculos tpico de los blogs, titulo y el contenido, as que modificamos el
archivo, quedando de esta manera.
# <!-- blog/templates/blog/article_list.html -->
{% extends 'blog/base_blog.html' %}
{% block title %}Blog - Lista de articulos{% endblock title %}
{% block blog_content %}
{% for article in articles %}
<h2>{{ article.title }}</h2>
<p>{{ article.body }}</p>
{% endfor %}
{% endblock blog_content %}

Se puede ver que es posible iterar sobre elementos en las plantillas, una variable de contexto es articles, un objeto
django.db.models.query.QuerySet iterable, por lo que podemos recorrer sus elementos (filas de datos), es

1.10. Creacion blog

49

Tutorial Django Documentation, Publicacin 1.0

igual que un for Python, lo nico diferente es la forma de crearlos en las plantillas con los caracteres { % %} (tags) y
que contiene una etiqueta de cierre { % enfor %} obligatoria.
Recorre todos sus elementos y podemos acceder a las propiedades con las sintaxis de punto . como lo haramos en
Python.
Como ya vimos en accounts, la forma de imprimir una variable es encerrando la variable entre llaves {{ }}.
Para probarlo, tenemos que aadir en el URLconf principal tutorial_django/urls.py las urls del blog, haremos un pequeo cambio, pondremos el blog como pagina principal.
# tutorial_django/urls.py
# ...
urlpatterns = [
url(r'^$', include('blog.urls')),
url(r'^blog/', include('blog.urls')),
url(r'^home/', include('home.urls')),
url(r'^accounts/', include('accounts.urls')),
url(r'^admin/', include(admin.site.urls)),
]
# ...

Ahora insertamos en blog/urls.py la url para la vista creada.


# blog/urls.py
from django.conf.urls import url
from . import views
urlpatterns = [
# /blog/ | /
url(r'^$', views.article_list_view, name='blog.article_list'),
]

Vamos tambin a modificar templates/base.html para poner los links.


<!-- templates/base.html -->
<!-- buscamos la parte -->
<a class="navbar-brand" href="#">Project name</a>
<!-- y la remplazamos por -->
<a class="navbar-brand" href="{% url 'blog.article_list' %}">Tutorial Django</a>
<!-- buscamos la parte -->
<ul class="nav navbar-nav">
<li class="active"><a href="#">Home</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
<!-- y la remplazamos por -->
<ul class="nav navbar-nav">
<li><a href="{% url 'home' %}">Home</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>

50

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

</ul>

Vemos que para crear links vasta con usar el tag { % url nombre_url %} donde nombre_url es el
name=nombre_url de las funciones url() en urlpatterns = [] de los archivos urls.py, de esta manera, si cambiamos una vista a otra, no tendremos que cambiar el link html en todas las plantillas (un lujo) como se
podr observar mas tarde cuando cambiemos la vista article_list_view.
Si probamos ahora, con el servidor en marcha, vamos a http://127.0.0.1:8000 vemos que ahora muestra los artculos
en un lado y un espacio para luego insertar los mens en el lado lateral (en este caso a la derecha).

El siguiente paso, seria crear la inclusion tag, para ello, vamos a crear un directorio templatetags dentro de la raz
de la app blog y dentro, el archivo __init__.py para que Python lo trate como un modulo y otro blog_tags.py
donde crearemos nuestra inclusion tag.
mkdir blog/templatetags
touch blog/templatetags/{__init__.py,blog_tags.py}
# blog/templatetags/blog_tags.py
from django import template
from ..models import Article, Tag
register = template.Library()

@register.inclusion_tag('blog/_menus.html')
def get_menus_blog():
context = {
# Obtener lista completa de etiquetas
'lista_tags': Tag.objects.all(),
# Obtener las 5 ultimas entradas
'ultimos_articulos': Article.objects.order_by('-create_at')[:5]
}
return context

Importamos el modulo template y los modelos Article y Tag, creamos una instancia de Library para
mas tarde usarlo como decorador @register.inclusion_tag(blog/_menus.html) pasndole como
argumento la plantilla que usara para la representacin de los datos devueltos por la funcin get_menus_blog,
dentro generamos un contexto con dos elementos, lista_tags con todas las etiquetas en la base de datos y
ultimos_articulos con los ltimos 5 artculos.
Nota: order_by() obtiene todos los elementos ordenados por el argumento, en este caso create_at, por defecto,
se obtienen de manera ascendente, al incluir un guion -, los datos obtenidos sern de manera descendente.
Ahora nos queda leer los datos y representarlos, modificamos la plantilla blog/templates/blog/_menu.html
teniendo en mente que tendremos acceso al contexto devuelto por get_menus_blog
1.10. Creacion blog

51

Tutorial Django Documentation, Publicacin 1.0

<!-- blog/templates/blog/_menu.html -->


<div class="bloque-menu">
<h4>Ultimos articulos</h4>
<hr>
{% for articulo in ultimos_articulos %}
<a href="#">{{ articulo.title }}</a><br>
{% endfor %}
</div>
<div class="bloque-menu">
<h4>Lista de etiquetas</h4>
<hr>
{% for tag in lista_tags %}
<a href="#">{{ tag.name }}</a><br>
{% endfor %}
</div>

Si actualizamos la pagina, vemos que no muestra los resultados, debido a que debemos incluir en el template
blog_tags

Lo incluimos en blog/templates/blog/base_blog.html
<!-- blog/templates/blog/base_blog.html -->
<!-- Aadimos al inicio del documento -->
{% extends 'base.html' %}
{% load blog_tags %} <!-- aadir -->
<!-- Eliminamos {% include 'blog/_menus.html' %} y lo cambiamos por -->
{% get_menus_blog %

Si ahora, actualizamos la pagina, veremos lo siguiente:

Primero se ha de leer las tags { % load blog_tags %}, da igual donde se ponga, lo importante es ponerlas antes
de la llamada a la funcin que queremos usar, en este caso { % get_menus_blog %} con sintaxis de tag { % %}.

52

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

Vemos a editar el articulo Primer articulo desde la administracin de Django, el titulo lo dejamos igual y el contenido
el campo body, le aadimos 2 lorem ipsum separndolos con un espacio (2 prrafos)
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.</p>
Si nos fijamos en la imagen:

se puede observar, como en realidad lo muestra como un solo prrafo, eso es porque por defecto, Django, no permite
html por razones de seguridad, si miras el cdigo generado en el navegador, veras que <p> lo traduce a &lt;p&gt;
(entidades html), pero en este caso (hemos creado nosotros la entrada, no un usuario desconocido!, nunca te fes de
un usuario, puede comenter errores involuntarios... o no...), vamos a permitir html en las entradas. Para ello, usaremos
uno de lo muchos filtros que tiene por defecto Django (a parte nosotros podemos crear nuestros propios filtros). El
filtro que usaremos es safe <https://docs.djangoproject.com/en/1.8/ref/templates/builtins/#safe>.
Volvemos a la plantilla y aadimos el filtro
<!-- blog/templates/blog/article_list.html -->
<!-- aadimos el filtro a {{ article.body }} -->
<p>{{ article.body|safe }}</p>

Los filtros se aaden con los pipers? | al elemento que quieres que se aplique, en este caso es al article.body,
con safe no convierte el html a entidades.
Si ahora actualizamos la pagina, vemos que ahora esta mucho mejor!

Nota: Otra opcin, que es la que prefiero, es usar el filtro linebreaksbr y en el articulo puedo omitir poner <p>
y </p>, lo que hace es sustituir el final de linea \n por <br>.
Yo utilizo markdown para crear el contenido, puedes ver un articulo que cree para aadir markdown a Django.
Ahora es el momento para ver las CBV, pero lo dejamos para la siguiente seccin!

1.10. Creacion blog

53

Tutorial Django Documentation, Publicacin 1.0

1.10.5 Clases basada en Vista ListView


Las clases basadas en vista CBV aporta lo que aporta la programacin orientada a objetos, herencia, mixins, etc.
A parte de clases predefinidas que con solo 3 lineas de cdigo es capaz de mostrar los datos de un modelo, y dar
funcionalidad para actualizarlos.
Un mixin no es mas que una clase que ofrece una funcionalidad comn a otras clases, pero que no pertenece a un tipo
concreto.
A diferencia de las funciones FBV, una clase tiene mtodos para las respuestas, por ejemplo, tiene un mtodo get()
y otro post() y dependiendo el mtodo de la respuesta, usa uno u otro.
Las clases al tener herencia, podemos crear clases personalizadas con la misma funcionalidad (o similar) o crear
nuestras CBV, pero por defecto tiene clases para las cosas mas comunes, como un sistema CRUD con, ListView,
UpdateView, CreateView, DeleteView e incluso otras como FormView para formularios, entre otras.
ListView
Creo que lo mejor es mostrarlo, vamos a editar la nica funcin de vista que tenemos en blog/views.py y la
vamos a cambiar por una CBV.
# blog/views.py
from django.views import generic
from .models import Article

class ArticleListView(generic.ListView):
template_model = 'blog/article_list.html'
model = Article
context_object_name = 'articles'

En nuestra clase se ha aadido tres propiedades, pero por defecto solo es obligatorio uno, model = MiModel que
es el modelo sobre el que va a trabajar, pero para ser explicito se han aadido template_model que es la plantilla
a renderizar y context_objects_name que sera el nombre de contexto con los elementos importados de la base
de datos, en este caso un lista de objectos Article.
El siguiente paso es editar blog/urls.py, eliminando la url() que contiene y cambindola por
# blog/urls.py
from django.conf.urls import url
from . import view
urlpatterns = [
# /blog/ | /
url(r'^$', views.ArticleListView.as_view(), name='blog.article_list'),
]

regex es lo mismo, al igual que name, lo nico que cambia es la llamada a la vista
views.ArticleListView.as_view(), donde views es el modulo con las vistas, ArticleListView es
la clase de la vista, que a su vez, es subclase de django.views.generic.ListView, todas las vistas heredan
de django.views.generic.View, que es la CBV mas simple de todas y provee de un mtodo (entre otros),
as_view() que devuelve un objeto HttpResponse.

54

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

Si probamos el sitio y vamos a la pagina principal, veremos que muestra exactamente lo mismo hacia con la funcin
article_list_view.
Posiblemente, en cuanto a lineas de cdigo, pueda parecer que no valga la pena, pero este ejemplo ha sido simple y si
conoces la programacin orientada a objetos, conocers sus ventajas.
Paginacin
Para continuar, vamos primero a aadir 10 artculos, as que, ves a la administracin y crea 9 artculos mas, ponle los
ttulos que quieras y usar un par de lorem ipsum (con etiquetas <p></p> en cada prrafo) en cada articulo (o los que
quieras), en la parte de tags, ponle de 1 a 3, a tu gusto!.

Los 10 artculos creados desde la administrador.

Como se puede ver, la pagina donde muestra los articulo, muestra los 10 artculos, si hubieran 50, mostrara los 50,
algo que pondra mas lenta las cargas de la pagina y gasto de ancho de banda.
Vamos a aadir una propiedad a la vista creada anteriormente, paginate_by, que mandara como contexto a la
plantilla informacin sobre paginacin que usaremos mas tarde.
# blog/views.py
class ArticleListView(generic.ListView):

1.10. Creacion blog

55

Tutorial Django Documentation, Publicacin 1.0

# ...
paginate_by = 3

Ahora editamos la plantilla


<!-- blog/templates/blog/article_list.html -->
<!-- Aadir debajo del {% endfor %} -->
<nav>
{% if is_paginated %}
<ul class="pagination">
{% if page_obj.has_previous %}
<li><a href="?page={{ page_obj.previous_page_number }}">&laquo;</a></li>
{% endif %}
{% for i in paginator.page_range %}
<li {% if page_obj.number == i %} class="active" {% endif %}>
<a href="?page={{i}}">{{ i }}</a>
</li>
{% endfor %}
{% if page_obj.has_next %}
<li><a href="?page={{ page_obj.next_page_number }}">&raquo;</a></li>
{% endif %}
</ul>
{% endif %}
</nav>

La vista pasa una variable de contexto is_paginated, si devuelve True, entonces es que hay paginacin.
Tambin pasa un objeto page_obj con mtodos para obtener datos como si tiene mas paginas respecto a
la actual page_obj.has_next o si tiene mas paginas previas a la actual page_obj.has_previous.
page_obj.number es la pagina actual, por lo que se puede comparar con la iteracin de
paginator.page_range que es el rango de paginas disponibles y as cambiar el CSS para marcar en la
pagina que se encuentra..

Y podemos ver, que ha generado un Query string en la URI, en este caso ?page=2

Con muy pocas lineas de cdigo, hemos generado mucho, eso es innegable :)
Tambin podemos observar que los artculos los muestra por orden de creacin (por orden de id), vamos a cambiar el
orden.
# blog/views.py
class ArticleListView(generic.ListView):

56

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

# ...
ordering = '-create_at'

Ahora, el orden es inverso al que mostraba antes los artculos :), tan solo con la propiedad ordering y como valor
una propiedad del modelo.
Informacin del articulo
Ahora, vamos aadir debajo del titulo del articulo, la fecha y autor. Toda esa informacin ya tenemos acceso desde la
plantilla, as que vamos aadir el siguiente cdigo.
<!-- blog/templates/blog/article_list.html -->
<!-- Modificar el interior del {% for article in articles %} -->
{% for article in articles %}
<h2>{{ article.title }}</h2>
<div class="article-info">
<small>
<strong>Por: </strong>{{ article.owner.username }}
<strong>Hace: </strong>{{ article.create_at|timesince }}
<strong>El: </strong>{{ article.create_at|date:'d F Y' }}
</small><hr>
</div>
<p>{{ article.body|safe }}</p>
{% endfor %}

Lo hago simple y no genero CSS para centrarnos en lo importante.


En primer lugar, se puede ver lo facil que es acceder a los modelos relaciones con article.owner.username, la propiedad article.owner devuelve django.contrib.auth.models.User, que tiene acceso a username,
simplemente lo mostramos.
Por motivos de demostracin, he aadido las 2 siguientes lineas, que maneja la fecha y hora de la propiedad
create_at con filtros, el primer filtro timesince es una funcin que nos devuelve el tiempo que ha pasado
desde la publicacin hasta la fecha y hora actual. Con el filtro date se le pasa un argumento, que es un string y le
decimos como representar la fecha y hora, puedes ver una lista de caracteres en la documentacin de PHP

Por ultimo, vamos a aadir, el footer del articulo con las etiquetas que tiene el articulo:

1.10. Creacion blog

57

Tutorial Django Documentation, Publicacin 1.0

<!-- blog/templates/blog/article_list.html -->


<!-- Modificar el interior del {% for article in articles %} -->
{% for article in articles %}
<h2>{{ article.title }}</h2>
<div class="article-info">
<small>
<strong>Por: </strong>{{ article.owner.username }}
<strong>Hace: </strong>{{ article.create_at|timesince }}
<strong>El: </strong>{{ article.create_at|date:'d F Y' }}
</small><hr>
</div>
<p>{{ article.body|safe }}</p>
<div class="article-footer">
<strong>Etiquetas: </strong>{{ article.get_string_tags }}
</div>
{% endfor %}

Se puede observar, que es posible llamar a funciones del modelo (siempre que no requieran de parmetros) y el
resultado es el siguiente:

En la siguiente seccin, se aadir una plantilla que muestra un nico articulo seleccionado por el usuario (detalles)
con sus comentarios.

1.10.6 Detalles articulo DetailView


Ahora, vamos a aadir la pagina de detalles del articulo, donde mostrar el articulo en una pagina separada.
Los pasos, los de siempre, la vista, la plantilla y crear la url en URLconf.
La vista, al igual que hicimos con ArticleListView que es una subclase de ListView, ahora vamos hacer
prcticamente lo mismo, ArticleDetailView que sera una subclase de DetailView.
# blog/views.py
class ArticleDetailView(generic.DetailView):
template_model = 'blog/article_detail.html'
model = Article
context_object_name = 'article'

El template

58

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

<!-- blog/templates/blog/article_detail.html -->


{% extends 'blog/base_blog.html' %}
{% block title %}{{ article.title }}{% endblock title %}
{% block blog_content %}
<h2>{{ article.title }}</h2>
<div class="article-info">
<small>
<strong>Por: </strong>{{ article.owner.username }}
<strong>Hace: </strong>{{ article.create_at|timesince }}
<strong>El: </strong>{{ article.create_at|date:'d F Y' }}
</small><hr>
</div>
<p>{{ article.body|safe }}</p>
<div class="article-footer">
<strong>Etiquetas: </strong>{{ article.get_string_tags }}
</div>
<div class="comentarios">
<!-- Aqui los comentarios -->
</div>
{% endblock blog_content %}

Se puede ver que es prcticamente igual que article_list.html omitiendo la paginacin, dentro de un rato,
volveremos con este tema (para no repetirnos) y lo modificaremos.
en URLconf aadimos la siguiente url
# blog/urls.py
urlpatterns = [
# ...

# /blog/detalles/:<string>slug/
url(r'^detalle/(?P<slug>[-\w]+)/$', views.ArticleDetailView.as_view(), name='blog.article_detail'
]

Por ultimo, tenemos que aadir un link en la lista de artculos para acceder a los detalles del articulo.
<!-- blog/templates/blog/article_list.html
<!-- Cambiar <h2>{{ article.title }}</h2> por -->
<h2><a href="{% url 'blog.article_detail' article.slug %}">{{ article.title }}</a></h2>

Como se puede observar, en { % url blog.article_detail article.slug %}, le pasamos el slug como parmetro, que lo requiere en la URLconf (?P<slug>[-\w]+). Por defecto, la vista ArticleDetailView,
para obtener el item a obtener del modelo exige la id o el slug (se puede cambiar con slug_url_kwarg,
slug_field y pk_url_kwarg)
Si vamos a la pagina, podemos ver que la lista de artculos, el titulo es un link que nos mandara al detalle del articulo.
Si recordamos, la platilla se repite, prcticamente es igual a la plantilla article_list.html, la nica diferencia
es el link para ver el articulo en detalles.
Primero vamos a crear una nueva plantilla, _article.html y aadimos.
<!-- blog/templates/blog/_article.html -->

1.10. Creacion blog

59

Tutorial Django Documentation, Publicacin 1.0

{% if articles %}
<h2><a href="{% url 'blog.article_detail' article.slug %}">{{ article.title }}</a></h2>
{% else %}
<h2>{{ article.title }}</h2>
{% endif %}
<div class="article-info">
<small>
<strong>Por: </strong>{{ article.owner.username }}
<strong>Hace: </strong>{{ article.create_at|timesince }}
<strong>El: </strong>{{ article.create_at|date:'d F Y' }}
</small><hr>
</div>
<p>{{ article.body|safe }}</p>
<div class="article-footer">
<strong>Etiquetas: </strong>{{ article.get_string_tags }}
</div>

Y modificamos article_detail.html y article_list.html


<!-- blog/templates/blog/article_list.html -->
<!-- la parte del {% for article in articles %} -->
{% for article in articles %}
{% include 'blog/_article.html' %}
{% endfor %}
<!-- blog/templates/blog/article_detail.html -->
{% extends 'blog/base_blog.html' %}
{% block title %}{{ article.title }}{% endblock title %}
{% block blog_content %}
{% include 'blog/_article.html' %}
<div class="comentarios">
<!-- Aqu los comentarios -->
</div>
{% endblock blog_content %}

Como funciona?, cuando aadimos { % include blog/_article.html %} importamos parte de un documento html en el mismo punto donde lo incluimos, por lo tanto, la plantilla incluida, tiene acceso al mismo
contexto, por lo tanto en article_list.html tiene una variable de contexto articles mientras que en
article_detail, no. Ambos contextos, tienen la variable article que es el objeto de un modelo Article,
en el caso de article_list.html se genera dinamicamente dentro de for por lo que incluira tantas plantillas
_article.html como artculos muestre y el objeto Article varia en cada loop
De esta manera, podemos tener una plantilla y un cambio se reflejara en ambas plantillas article_list.html y
article_detail.
Puedes observar, que he creado un comentario <!-- Aqu los comentarios --> en article_detail,
que seria para aadir el sistema Disqus, pero no lo voy a incluir en el tutorial, te recomiendo un articulo que cree en
mi blog www.snicoper.com
En la siguiente seccin, vamos a crear el tpico leer mas... y as crearemos nuestro primer filtro.

60

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

1.10.7 Creacin de un filtro


Vamos a crear un filtro personalizado, vamos a crear el tpico leer mas..., que sera un link para leer el articulo entero.
En la lista de artculos, los artculos se cortaran donde pongamos una marca <!-- read_more -->. El filtro comprobara si tiene un <!-- read_more -->, si lo tiene, cambiara ese texto por un link hacia detalles del articulo.
Primero, vamos aadir un mtodo al modelo Article, get_absolute_url
# blog/models.py
# Aadir al inicio
from django.core.urlresolvers import reverse
class Article(models.Model):
# ...
def get_absolute_url(self):
return reverse('blog.article_detail', kwargs={'slug': self.slug})

Primero importamos del modulo urlresolvers la funcin reverse, que generara una URI en base al archivo
URLconf, el primer parmetro es el name de url() y el segundo parmetro es un diccionario kwargs con los
argumentos de regex (?P<slug>[-\w]+), en este caso requiere un slug, por lo tanto se le pasa el nombre (el
parmetro de regex) y el valor, en este caso self.slug.
La funcin, como veremos, no es una invencin o convencin nuestra, pertenece a django.db.models.Model y
es utilizada a menudo como iremos viendo.
Los filtros, se crean en el mismo archivo que las inclusion_tag, es decir, en el archivo
nombre_app/templatetags/app_tags.py, en este caso en blog/templatetags/blog_tags.py.
# blog/templatetags/blog_tags.py
@register.filter
def read_more(article):
pattern = '<!-- read_more -->'
body = article.body
if pattern in body:
pos = body.index(pattern)
replace = '<a href="{}" class="small">Sigue leyendo...</a>'.format(
article.get_absolute_url())
body_return = body.replace(pattern, replace)
return body_return[:pos + len(replace)]
return body

Y modificamos la plantilla _article.html


<!-- blog/templates/blog/_article.html -->
<!-- cambiamos <p>{{ article.body|safe }}</p> por -->
{% if articles %}
<p>{{ article|read_more|safe }}</p>
{% else %}
<p>{{ article.body|safe }}</p>
{% endif %}

Como funciona?, el filtro read_more es una simple funcin Python que requiere de un parmetro article, espera
un objeto blog.models.Article.
La funcin (o filtro), lo nico que hace es buscar un substring <!-- read_more -->, si lo encuentra sustitu-

1.10. Creacion blog

61

Tutorial Django Documentation, Publicacin 1.0

ye <!-- read_more --> por un link. El link es generado gracias al mtodo get_absolute_url del objeto
Article.
La plantilla, usamos la misma tcnica anterior, si existe un contexto articles, significa que estamos en la plantilla
article_list.html y llamamos al filtro de la siguiente manera {{ article|read_more|safe }}.
read_more, como hemos comentado, requiere un parmetro, un objeto Article, los filtros se aplican a la parte
izquierda del filtro se_aplica|filtro. En este caso el se_aplica es pasado como primer argumento (en este caso,
un objeto Article).
Ademas, podemos observar que los filtros pueden a su vez tener otros filtros, a la devolucin de
article|read_more que ahora es un string, le aplicamos el filtro safe anteriormente comentado.
Si estamos llamando a la plantilla desde article_detail.html, la variable de contexto articles no existe,
por lo que se ejecutara {{ article.body|safe }}.
Aqu podemos ver el resultado

En la siguiente seccin, vamos a ver como crear artculos.

1.10.8 Creacin de artculos CreateView


A modo de demostracin, vamos a crear en esta seccin y las siguientes un sistema para aadir, editar y eliminar
artculos. Con la administracin Django, en este caso no seria necesario (la administracin tambin controla permisos).
Vamos a empezar con la creacin de artculos y comprobaremos si el usuario tiene permisos para aadir nuevos
artculos, si no tiene permisos, crearemos una plantilla informando que no tiene acceso.
Va a ser rutinario, aadir la vista, crear la plantilla y aadir el url, as que por partes...
Creamos la vista, en esta ocasin, creamos una clase que deriva de CreateView
62

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

# blog/views.py
class ArticleCreateView(generic.CreateView):
template_model = 'blog/article_form.html'
model = Article

En este ocasin no aadimos la propiedad context_object_name que por defecto es form en una clase
CreateView.
La plantilla es article_form.html, ya que la reutilizaremos con la vista de edicin de artculos. Por este motivo,
luego aadiremos un mtodo mas a ArticleCreateView para ver como podemos aadir contexto personalizado.
<!-- blog/templates/blog/article_form.html -->
{% extends 'blog/base_blog.html' %}
{% block title %}{{ title }}{% endblock title %}
{% block blog_content %}
<h2 class="page-header">{{ title }}</h2>
<form action="" method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">{{ nombre_btn }}</button>
</form>
{% endblock blog_content %}

Se puede observar variables de contexto {{ title }} que esta en 2 partes y {{ nombre_btn }}, dentro de un
rato volveremos a esto.
Vamos con al URLconf y aadir la url
# blog/urls.py
urlpatterns = [
# ...
# /blog/crear/
url(r'^crear/$', views.ArticleCreateView.as_view(), name='blog.crear'),
]

Y por ultimo, vamos a crear un botn en la barra de navegacin superior, si tiene permisos para crear un articulo,
saldr el link.
<!-- templates/base.html -->
<!-- debajo de <li><a href="#contact">Contact</a></li> aadimos -->
{% if perms.article.can_add %}
<li><a href="#contact">Crear articulo</a></li>
{% endif %}

perms tambien esta disponible en las plantillas, y la forma de saber si un usuario tiene permisos es:
perms.nombre_modelo.permiso, donde nombre_modelo es el nombre del modelo en minsculas y
permiso es uno de los siguientes:
can_add: Puede aadir
can_change: Puede cambiar (editar)

1.10. Creacion blog

63

Tutorial Django Documentation, Publicacin 1.0

can_delete: Puede eliminar


Si probamos y vamos primero a http://127.0.0.1:8000/ <http://127.0.0.1:8000/>, si estamos logueados, veremos un
link para crear un articulo.

Y si estamos deslogueados, no mostrara el link

Si pinchamos encima de Crear articulo veremos que nos mostrara un error! http://127.0.0.1:8000/blog/crear/ el error
es por que Django 1.8 obliga aadir la propiedad fields (en este caso dentro de la vista, pero podra ser tambin en
un ModelForm), con los campos que vamos a mostrar.
As que, vamos a aadir en la vista la siguiente linea:
# blog/views.py
class ArticleCreateView(generic.CreateView):
template_model = 'blog/article_form.html'
model = Article
fields = ('title', 'body')

Si actualizamos la pagina, veremos que nos muestra un formulario con los campos que queramos que mostrase.

Podemos ver varias cosas, primero lo fcil que ha sido crear un formulario a partir de un modelo!, con la clase

64

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

CreateView, segundo, donde esta el titulo de la pagina?, y un botn sin texto? :)


Vamos aadir contexto en ArticleCreateView, para ello, vamos a usar un mtodo get_context_data()
# blog/views.py
class ArticleCreateView(generic.CreateView):
# ...
def get_context_data(self, **kwargs):
# Obtenemos el contexto de la clase base
context = super().get_context_data(**kwargs)
# aadimos nuevas variables de contexto al diccionario
context['title'] = 'Crear articulo'
context['nombre_btn'] = 'Crear'
# devolvemos el contexto
return context

y podemos ver los resultados

De esta manera, podemos mas tarde reutilizar la plantilla cuando hagamos la de edicin.
Y el slug y el propietario (owner)? bueno, el slug, si recuerdas, se genera automticamente cuando se crea/guarda el
objeto y el owner, lo recuperaremos mas tarde del usuario que esta logueado (que actualmente tenemos un problema,
por que cualquiera puede crear un articulo, luego lo arreglamos)

1.10. Creacion blog

65

Tutorial Django Documentation, Publicacin 1.0

Otra cosa que podemos apreciar, es el diseo de los elementos del formulario, no tienen el diseo de Bootstrap! como
antes :(, a parte los campos tienen un nombre no muy til para los usuarios.
Empezaremos por lo nombres de los campos (labels), en principio, aade el nombre del campo del modelo, en este
caso de blog.models.Article, pero es posible cambiar el nombre en el mismo modelo.
# blog/models.py
class Article(models.Model):
# Remplazamos
title = models.CharField(max_length=100, unique=True)
# por
title = models.CharField(verbose_name='titulo', max_length=100, unique=True)

El del body, lo dejamos para el formulario, para demostrar otra posibilidad de hacerlo.
En accounts creamos los formularios con subclases de django.forms.Form, pero hay otra manera cuando se
crean formularios que son parte de un modelo (como es el caso), vamos a crear un formulario ArticleCreateForm
que es subclase de django.forms.ModelForm.
touch blog/forms.py
# blog/forms.py
from django import forms
from .models import Article
class ArticleCreateForm(forms.ModelForm):
class Meta:
model = Article
fields = ('title', 'body')
labels = {
'body': 'Texto'
}
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'body': forms.Textarea(attrs={'class': 'form-control'})
}

Como se puede apreciar, las opciones de formulario estn en la clase Meta, donde el model es el modelo, en este
caso Article, fields una tupla o lista con los campos a mostrar, labels, al igual que en el modelo usbamos
verbose_name, en los formularios es labels, un diccionario con la clave (el nombre del campo/elemento) y el
valor (el texto a mostrar). Por ultimo vemos los widgets, tambin un diccionario donde la clave es el nombre del
campo y el valor es el tipo de campo que queremos con un argumento attrs con los atributos que insertara en el
elemento del formulario.
Si actualizamos la pagina, nos dar un error diciendo que no podemos tener fields en 2 sitios, as que quitamos
fields = (title, body) en la vista ArticleCreateView.
Vamos aadir un mtodo en la vista, el mtodo se ejecuta despus de instanciar la clase, para determinar el tipo
method de la solicitud.
# blog/views.py
# Aadimos al inicio
from django.shortcuts import redirect

66

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

from django.conf import settings


class ArticleCreateView(generic.CreateView):
# ...
def dispatch(self, request, *args, **kwargs):
# Nota: Dejo para el lector como ejercicio que cambie este
# comportamiento, actualmente, solo comprueba si es usuario
# tiene permisos para aadir un articulo, si no lo tiene, lo
# redirecciona a login, pero, y si esta logueado y simplemente
# no tiene permisos?
# El compportamiento logico seria:
#
Estas logueado?
#
Tiene permisos?
#
Continuar con la ejecucin
#
No tienes permisos?
#
Redireccionar a una pagina informando que no tiene permisos
#
para aadir un articulo.
#
Si no estas logueado:
#
Redireccionar a pagina de login
if not request.user.has_perms('blog.add_article'):
return redirect(settings.LOGIN_URL)
return super().dispatch(request, *args, **kwargs)

has_perms(blog.add_article) a diferencia de las plantillas, se le pasa un argumento como string con el


nombre_app.tipopermiso_nombremodelo donde tipopermiso es add_, change_ y delete_ (por defecto).
Si vamos a la web, estando logueados y le damos al botn de Crear, vemos el formulario se valida, y si no pasa la
validacin, recarga otra vez el formulario indicando los errores.
La validacin va en este orden, ModelForm sobrescribe a Model (otra cosa es que luego nos mande un error el
modelo por no cumplir los requisitos).

1.10. Creacion blog

67

Tutorial Django Documentation, Publicacin 1.0

Vamos a crear un articulo, ponemos un Titulo y ponemos algo en Text y le damos al botn Crear

OPS!, se nos ha olvidado asignar a owner al articulo!


CreateView tiene dos mtodos form_valid(self, form) y form_invalid(self, form), que podemos tomar
decisiones si el formulario se ha validado con xito o no.
En este caso, queremos hacer algo cuando sea valido, asignar el usuario actual a owner.

68

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

# blog/views.py
class ArticleCreateView(generic.CreateView):
# ...
def form_valid(self, form):
form.instance.owner = self.request.user
return super().form_valid(form)

form.instance es un objeto del modelo (una propiedad de FormModel), pero aun no se ha guardado. (Puede
parece un lo), despus de ejecutar CreateView.form_valid, se ejecutara FormModel.save() que a su vez,
ejecutara Model.save()
Por ultimo, cuando todo lo anterior termina bien, ArticleCreateView tiene una propiedad success_url y un
mtodo get_success_url(), si no tiene datos, ni el mtodo ni la propiedad (es la url de redireccin en caso de
xito), intentara obtener Model.get_absolute_url(), si no se tuviera declarado al menos 1 de las 3 opciones,
lanzara un error, por que no sabe donde queremos ir despus de crear el articulo. En este caso, tenemos declarada en
el modelo get_absolute_url(), sera donde redireccionara cuando todo haya salido bien, que sera a detalles del
articulo.
En la siguiente seccin, vamos a poner la opcin de editar el articulo

1.10.9 Editar articulo UpdateView


Con editar ya si que no vamos a ver nada nuevo, prcticamente es lo mismo que la creacin, lo nico que cambia es
(internamente), es que el formulario, muestra los datos de un articulo a modificar.
Tampoco necesitamos saber que usuario lo ha creado (no lo cambiaremos), as que pondr los pasos e ir mas rpido
y pondr las capturas para una vista rpida.
La vista
# blog/views.py
class ArticleUpdateView(generic.UpdateView):
template_model = 'blog/article_form.html'
model = Article
form_class = ArticleCreateForm
def dispatch(self, request, *args, **kwargs):
# Al igual que con ArticleCreateView, dejo al lector
# que cambie el comportamiento de este mtodo para saber
# si esta logueado y tiene permisos.
# Ver el comentario de ArticleCreateView en el mtodo
# dispatch
if not request.user.has_perms('blog.change_article'):
return redirect(settings.LOGIN_URL)
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
# Obtenemos el contexto de la clase base
context = super().get_context_data(**kwargs)
# aadimos nuevas variables de contexto al diccionario
context['title'] = 'Editar articulo'
context['nombre_btn'] = 'Editar'

1.10. Creacion blog

69

Tutorial Django Documentation, Publicacin 1.0

# devolvemos el contexto
return context

dispatch cambia el tipo de permisos, ahora comprueba que el usuario pueda editar y en el contexto
get_context_data, cambia el title y nombre_btn.
La plantilla es la misma que la de crear articulo, as que pasamos a la URLconf
# blog/urls.py
# Aadir
urlspatterns = [
# ...
url(r'^editar/(?P<slug>[-\w]+)/$', views.ArticleUpdateView.as_view(), name='blog.editar'),
]

Vamos a aadir un link en los detalles del articulo y probar si puede editar, que muestre el botn para editar.
<!-- blog/templates/blog/article_detail.html -->
<!-- aadimos debajo de {% include 'blog/_article.html' %} -->
{% if perms.article.can_change %}
<a href="{% url 'blog.editar' article.slug %}" class="btn btn-primary">Editar</a>
{% endif %}

Y ya esta!! nada mas :)


Como ejercicio para el lector: Te propongo que cuando creas y editas un articulo, si el formulario es valido (si
se ha creado o editado un articulo), muestre un mensaje al usuario como lo hace la funcin logout_view de
accounts/views.py cuando te deslogueas.
En la siguiente seccin ya terminamos el sistema CRUD del blog y prcticamente terminamos este pequeo tutorial.

1.10.10 Eliminar articulo DeleteView


Eliminar un articulo, tambin es muy fcil:
La vista:
# blog/views.py
# Aadir al inicio
from django.core.urlresolvers import reverse_lazy
class ArticleDeleteView(generic.DeleteView):
template_name = 'blog/confirmar_eliminacion.html'
success_url = reverse_lazy('blog.article_list')
model = Article
def dispatch(self, request, *args, **kwargs):
if not request.user.has_perms('blog.delete_article'):
return redirect(settings.LOGIN_URL)
return super().dispatch(request, *args, **kwargs)

Aqu se puede ver que hemos usado success_url, por que si lo dejamos sin poner como anteriormente, cuando lea
del modelo get_absolute_url, dar un error, por que el articulo no existe (lo acabamos de eliminar).

70

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

reverse_lazy es igual que reverse que ya hemos usado varias veces, la diferencia, es que reverse_lazy es
til usarla cuando URLconf aun no se ha cargado.
URLconf:
# blog/urls.py
urlpatterns = [
url(r'^eliminar/(?P<slug>[-\w]+)/$', views.ArticleDeleteView.as_view(), name='blog.eliminar'),
]

La plantila:
<!-- blog/templates/blog/confirmar_eliminacion.html -->
{% extends 'blog/base_blog.html' %}
{% block title %}Confirmar la eliminacin{% endblock title %}
{% block blog_content %}
<h2 class="page-header">Confirmacion para eliminar {{ article.title }}</h2>
<form method="post" action="">
{% csrf_token %}
<p>Seguro que quieres eliminar el articulo {{ article.title }}?</p>
<button type="submit" class="btn btn-danger">Si, eliminar</button>
<a href="{% url 'blog.article_detail' article.slug %} class="btn btn-success">Cancelar</a>
</form>
{% endblock blog_content %}

Y aadimos un enlace en detalles del articulo.


<!-- blog/templates/blog/article_detail.html -->
<!-- aadir antes de <div class="comentarios"> -->
{% if perms.article.can_delete %}
<a href="{% url 'blog.eliminar' article.slug %}" class="btn btn-danger">Eliminar</a>
{% endif %}

Ta hemos terminado el blog, un blog bsico, pero funcional, espero que te haya servido para aprender los fundamentos
de temas como las CBV y los ModelForm.
Ahora, nos queda la pagina about y contact que veremos en las prximas secciones.

1.11 Pagina About


About (Sobre mi o Acerca de), la vamos a crear con la vista mas simple, View para mostrarla, tanto about como
contact, crearemos las vistas en la app home.
Vamos a empezar como siempre, si, la vista...
# home/views.py
# Aadimos al inicio
from django.views import generic
# Creamos la vista
class AboutView(generic.View):

1.11. Pagina About

71

Tutorial Django Documentation, Publicacin 1.0

def get(self, request, *args, **kwargs):


return render(request, 'home/about.html')

La clase View es la clase de vista mas simple, la que menos propiedades y mtodos tiene, 2 de los mtodos que podemos usar es get() y post() que ejecutara uno u otro dependiendo del method. En este caso, get() simplemente
renderiza una pagina html (una simple funcin lo podra a ver hecho, pero quera mostrar la clase View).
Ahora ponemos la url en URLconf
# home/urls.py
urlpatterns = [
# ...
url(regex=r'^about/$', view=views.AboutView.as_view(), name='about'),
]

la plantilla
<!-- blog/templates/blog/about.html -->
{% extends 'base.html' %}
{% block title %}About{% endblock title %}
{% block content %}
<h2 class="page-header">About</h2>
{% endblock content %}

Y aadimos en base.html el link


<!-- templates/base.html -->
<!-- buscar -->
<li><a href="#about">About</a></li>
<!-- cambiar por -->
<li><a href="{% url 'about' %}">About</a></li>

El contenido de about es cosa de cada uno :)


Bueno, pues ya solo queda contact donde daremos la posibilidad a los usuarios que puedan contactar con nosotros,
eso sera en la prxima seccin.

1.12 Pagina Contacto


Django proporciona una envoltura de smtplib para que el envi de correos electrnicos sea una tarea muy sencilla.
Para empezar, es necesario aadir variables en el archivo de configuracin tutorial_django/settings.py
Al igual que about, lo haremos en la app home. Aadimos al final:
# tutorial_django/settings.py
EMAIL_USE_TLS = True
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = 'usuario@gmail.com'
EMAIL_HOST_PASSWORD = 'contrasea'

72

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

Esta es la configuracin tpica para usar Gmail como SMTP, modifica los datos de tu cuenta Gmail.
Vamos a empezar con los formularios. La idea es la siguiente, si un usuario esta logueado (tenemos su email de
contacto), no mostrara el campo de email, en caso contrario, le pediremos su email por si tenemos que contactar con
el.
touch home/forms.py
from django import forms

class ContactUsuarioAnonimoForm(forms.Form):
email = forms.EmailField(
label='Email',
widget=forms.EmailInput(attrs={'class': 'form-control'})
)
subject = forms.CharField(
label='Asunto',
widget=forms.TextInput(attrs={'class': 'form-control'})
)
body = forms.CharField(
label='Mensaje',
widget=forms.Textarea(attrs={'class': 'form-control'})
)

class ContactUsuarioLoginForm(forms.Form):
subject = forms.CharField(
label='Asunto',
widget=forms.TextInput(attrs={'class': 'form-control'})
)
body = forms.CharField(
label='Mensaje',
widget=forms.Textarea(attrs={'class': 'form-control'})
)

Creamos 2 formularios, uno para el usuario logueado y otro para el usuario annimo.
La vista, en esta ocasin, vamos a usar otra CBV que es un FormView
# home/views.py
# Aadimos en el inicio
from django.contrib import messages
from django.core.urlresolvers import reverse_lazy
from django.core.mail import send_mail
from .forms import ContactUsuarioAnonimoForm, ContactUsuarioLoginForm
# Creamos una funcin para el envi de email
# (es muy simple, solo para demostrar como enviar un email)
def send_email_contact(email_usuario, subject, body):
body = '{} ha enviado un email de contacto\n\n{}\n\n{}'.format(email_usuario, subject, body)
send_mail(
subject='Nuevo email de contacto',
message=body,
from_email='contact@example.com',
recipient_list=['usuario@example.com']
)

1.12. Pagina Contacto

73

Tutorial Django Documentation, Publicacin 1.0

class ContactView(generic.FormView):
template_name = 'home/contact.html'
success_url = reverse_lazy('home')
def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated():
self.form_class = ContactUsuarioLoginForm
else:
self.form_class = ContactUsuarioAnonimoForm
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
subject = form.cleaned_data.get('subject')
body = form.cleaned_data.get('body')
if self.request.user.is_authenticated():
email_usuario = self.request.user.email
send_email_contact(email_usuario, subject, body)
else:
email_usuario = form.cleaned_data.get('email')
send_email_contact(email_usuario, subject, body)
messages.success(self.request, 'Email enviado con exito')
return super().form_valid(form)

Por defecto, un FormView requiere una propiedad form_class, en este caso, como no sabemos que formulario
vamos a usar, en el mtodo dispatch (es el mtodo encargado en saber el method del tipo de respuesta) comprobamos si el usuario esta logueado o no y dependiendo de si lo esta o no, asignaremos a form_class un formulario u
otro.
Despus ya la clase ContactView se encarga de renderizar la plantilla, que la lee de template_name.
Ahora, tenemos que generar el mtodo form_valid y decirle que
django.core.mail.send_mail, una funcin que acepta los siguientes parmetros

envi

el

email

con

send_mail(subject, message, from_email, recipient_list, fail_silently=False, auth_user=None, auth_pas

Volvemos hacer la comprobacin de si es un usuario autenticado, si lo esta, recuperamos el email con


self.request.user.email y si no lo es, entonces el usuario ha introducido el email en el formulario y lo
recuperamos con form.cleaned_data.get(email).
Despus pasamos los datos a la funcin send_email_contact, que prepara el email (de una manera muy simple, solo para mostrar como funciona) y lo enva de una manera muy sencilla (recuerda configurar las variables de
configuracin en tutorial_django/settings.py)
Pues yo creo que ya tenemos un blog (bsico) creado, con la funcionalidad bsica de cualquier formulario, y por ahora
ya lo dejamos, espero que lo hayas disfrutado y te haya servido de algo el tutorial.
Ya solo unas palabras de despedida en la siguiente seccin.

1.13 Notas finales


Espero que te haya sido til y hayas aprendido al menos los conceptos bsicos de Django, que a partir de ahora te
cueste un poco menos crear algo con Django.
Seguir mejorando y actualizando este pequeo tutorial, al menos que sea compatible con la ultima versin de Django.
Si quieres seguir mis trabajos, opiniones u ojear mis apuntes, visita mi blog personal en http://www.snicoper.com

74

Captulo 1. Tabla de contenidos:

Tutorial Django Documentation, Publicacin 1.0

Si has llegado hasta aqu, por favor, mndame tus impresiones desde http://www.snicoper.com/contact/, cualquier cosa,
desde un error, que estoy equivocado o simplemente las gracias (ayuda despus de las horas que le he dedicado).
Y si crees que lo ha merecido la pena, por favor, considera en hacer una pequea donacin

1.13. Notas finales

75

También podría gustarte