Contenido

Django lo hizo un mago

En las películas, a menudo, se encuentran errores imposibles de explicar: un tarro que se derrama y aparece lleno, gafas que aparecen y desaparecen, espadas que se transforman en martillos… En esos casos decimos que “lo hizo un mago”.

En esta ocasión no es que un mago haya hecho Django, sino que hay un equipo impresionante detrás. Pero sí es cierto que Django hace mucha magia.

Me gustaría comenzar aquí un pequeño tutorial rápido para usar Django, utilizando toda la magia. Este tutorial no valdrá en todos los casos, pero dará un buen punto de comienzo para aplicaciones más avanzadas.

Este artículo utiliza el mismo ejemplo que el que escribí hace tiempo Django: Creación de un sitio básico, pero creo que lo encontraréis muy mejorado.

Django

Creación de un blog

Como ejemplo, vamos a crear un blog personal. No tendrá grandes cosas: nos basta con que sea funcional. Tendrá lo típico: posts, tags y paginación. Por no complicarnos la vida, no tendrá ni comentarios, aunque no sería difícil añadirlos.

Ése es nuestro objetivo, y vamos a conseguirlo lanzando “balas trazadoras”, es decir, definiendo una funcionalidad e implementándola de pincipio a fin. Veremos así cómo la aplicación va obteniendo la forma…

Me gustaría comenzar por la vista, pero en este caso creo que es mejor comenzar por el modelo. Empezar por la vista tiene sus ventajas: evita añadir cosas que uno no necesita. Sin embargo, en este caso, comenzar por el modelo nos va a dar mejor perspectiva de lo que estamos haciendo.

1. Entorno

Vamos a crear el entorno. Para ello voy a crear un entorno aislado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ virtualenv venv
New python executable in venv/bin/python
Installing setuptools, pip...done.
$ . venv/bin/activate
(venv)$ pip install Django==1.7.1
Downloading/unpacking django
Installing collected packages: django
Successfully installed django
Cleaning up...
(venv)$

Con esto tenemos nuestro entorno aislado. No voy a hablar aquí sobre Virtualenv, pip ni otras herramientas. Voy a centrarme en Django.

Ahora voy a crear la estructura básica de la aplicación. Para evitar problemas, voy a llamar al proyecto app:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(venv)$ django-admin startproject app
(venv)$ tree  app
app
├── app
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

1 directory, 5 files
(venv)$

Bien. Con esto tenemos lo justo para empezar. A partir de ahora, todo lo que hagamos estará dentro del directorio app (el de nivel superior, se entiende).

No voy a generar más archivos. A partir de ahora todo lo escribiremos nosotros. De todas maneras, os dejo con “los planos” de lo que vamos a hacer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
app
├── app
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── blog
│   ├── admin.py
│   ├── __init__.py
│   ├── models.py
│   └── views.py
├── db.sqlite3
├── manage.py
└── templates
    └── blog
        └── post_list.html

Si no os apetece ir creando los archivos, podéis descargaros el código así:

1
2
$ git clone git@github.com:magmax/django-blog.git
$ git checkout 0.1.0

2. Aplicación

Lo primero es crear una aplicación. En Python un paquete es cualquier directorio con el fichero __init__.py. En Django, una aplicación es un paquete Python con un archivo models.py. Así que vamos a crear una aplicación blog:

1
2
3
4
(venv)app$ mkdir blog
(venv)app$ touch blog/__init__.py
(venv)app$ touch blog/models.py
(venv)app$

Y ahora hay que registrarla. Para ello se edita el archivo app/settings.py. Veremos que es un archivo Python normal, definiendo variables. Buscamos INSTALLED_APPS y añadimos 'blog':

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
"""
Django settings for app project.

For more information on this file, see
https://docs.djangoproject.com/en/1.6/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.6/ref/settings/
"""

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '*8l=t4nnb4ns)7d$ckxejxi&i1%5knrwm9+sw6-p4-58q4d(p='

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

TEMPLATE_DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',
)

MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
)

ROOT_URLCONF = 'app.urls'

WSGI_APPLICATION = 'app.wsgi.application'


# Database
# https://docs.djangoproject.com/en/1.6/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

# Internationalization
# https://docs.djangoproject.com/en/1.6/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.6/howto/static-files/

STATIC_URL = '/static/'

TEMPLATE_DIRS = [
    os.path.join(BASE_DIR, 'templates'),
]

Ya hemos creado una aplicación.

Podemos crear la base de datos mediante:

1
$(env)app$ python manage.py syncdb

Y siguiendo los pasos. Con eso vamos a crear una base de datos sqlite con las tablas necesarias para las aplicaciones instaladas.

Veámoslo:

1
$(env)app$ python manage.py runserver 8000

Tendréis la app funcionando en https://localhost:8000 y también podréis acceder a la interfaz de administración en https://localhost:8000/admin (luego lo explico).

Bien… Suficiente para empezar.

3. El modelo

Al grano:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from django.db import models


class Tag(models.Model):
    name = models.CharField(max_length=100)

    def __unicode__(self):
        return self.name


class Post(models.Model):
    title = models.CharField(max_length=100)
    teaser = models.TextField()
    body = models.TextField()

    tags = models.ManyToManyField(Tag, related_name="posts",
                                  null=True, blank=True)

    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    def __unicode__(self):
        return self.title

Lo explico: Tenemos dos clases que heredan de django.db.models.Model: Tag y Post.

La clase Tag se sencilla: tiene un único campo (field) llamado name, de longitud 100. Si trato de pintar la clase se ejecutará el método __unicode__, mostrando sólo el título.

La clase Post es más compleja: Tiene un título, un teaser y un body. Además puede contener cero o más Tags:

  • related_name: permite una relación inversa. En este caso, dado un objeto de tipo Tag podría acceder a todos los Post relacionados mediante tag.posts.
  • null: Permite valores nulos.
  • blank: Permite insertar valores vacíos (sutil diferencia con null).

Además tenemos los campos created y updated de los que no nos preocuparemos. Son de tipo fecha y se gestionan automáticamente.

Aplicamos:

1
(venv)app$ python manage.py migrate

Pero yo quiero crear posts….

4. Admin

Ahora vamos a acrear el archivo app/admin.py:

1
2
3
4
5
6
7
from django.contrib import admin

from . import models


admin.site.register(models.Tag)
admin.site.register(models.Post)

Y si volvemos a lanzar el servidor:

1
$(env)app$ python manage.py runserver 8000

Y nos vamos a la interfaz de administración en https://localhost:8000/admin, veremos que Django nos ha creado todo lo que necesitamos para añadir posts. Adelante, cread alguno :D

5. Enrutado

Todo está funcionando, pero nos falta la interfaz de usuario. Temo que en este apartado vamos a romper lo poco que llevamos.

Editamos el archivo app/urls.py y añadimos una ruta nueva:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from django.conf.urls import patterns, include, url

from django.contrib import admin
admin.autodiscover()

from blog import views


urlpatterns = patterns('',
    url(r'^$', views.PostListView.as_view(), name='home'),
    url(r'^admin/', include(admin.site.urls)),
)

(Ojo, que hay varios cambios). En general, se parece a una de las líneas que nos ofrecen…

En Django hay dos maneras de crear una vista: mediante una clase o mediante una función. En este caso he optado por la clase, ya que pretendo aprovechar el sistema de herencia.

Y esto no funciona porque aún no existe la clase views.PostListView, así que vamos a crearla:

6. El controlador (Views)

Por desgracia, en Django llaman “view” al controlador. Así que tenemos que escribir el archivo blog/views.py:

1
2
3
4
5
6
7
from django.views.generic.list import ListView

from . import models


class PostListView(ListView):
    model = models.Post

Y podemos intentar acceder a https://localhost:8000/ para ver un bonito error: nos falta el archivo blog/post_list.html.

Lo primero será editar el archivo app/settings.py para indicar dónde estarán las plantillas (templates), por lo que añadiremos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
"""
Django settings for app project.

For more information on this file, see
https://docs.djangoproject.com/en/1.6/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.6/ref/settings/
"""

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '*8l=t4nnb4ns)7d$ckxejxi&i1%5knrwm9+sw6-p4-58q4d(p='

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

TEMPLATE_DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',
)

MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
)

ROOT_URLCONF = 'app.urls'

WSGI_APPLICATION = 'app.wsgi.application'


# Database
# https://docs.djangoproject.com/en/1.6/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

# Internationalization
# https://docs.djangoproject.com/en/1.6/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.6/howto/static-files/

STATIC_URL = '/static/'

TEMPLATE_DIRS = [
    os.path.join(BASE_DIR, 'templates'),
]

Y después editaremos el archivo templates/blog/post_list.html por algo muy básico:

1
2
3
4
5
<ul>
  {% for post in post_list %}
  <li>{{ post.title }}</li>
  {% endfor %}
</ul>

Magia. En este momento todo cuadra y funciona.

El truco

Ahora es cuando contamos secretos de magos: ¿Dónde está el truco?

Bien… lo que hemos usado son un montón de convenciones.

Cuando en el navegador se indica una URL, Django comprueba cada expresión regular del archivo app/url.py hasta que una de ellas se cumple. Entonces invoca a la función que tenga asociada. En nuestro caso, esta función es el wrapper .as_view(), que devuelve una función.

Esta función as_view(), sabe que debe invocar ciertos métodos de esa clase. En nuestro caso hemos dejado las implementaciones por defecto de la clase django.views.generic.list.ListView. Ésta necesita un modelo, que le hemos indicado en la variable PostListView.model y con esto se pone a funcionar:

  • obtiene la lista de elementos del modelo
  • genera una variable de “contexto”
  • asigna la lista de elementos a una variable <model>_list en el contexto
  • busca la plantilla <application>/<model>_list.html
  • renderiza la plantilla con el contexto.

Django nos ofrece la posibilidad de sobreescribir cualquiera de esos puntos, pero estamos aprendiendo y, en este momento, lo mejor es dejarle hacer a él. Si este tutorial tiene tirón, veremos cómo hacerlo.

Más información

Podéis encontrar toda la información en el tutorial de Django.

Este artículo continúa en Django lo hizo un mago: paginación y detalle, donde creamos el detalle de cada post y paginamos la página de índice. Después sigue en Django lo hizo un mago: plantillas y contextos, donde se crea un menú con las categorías y se mejoran las plantillas.