Contenido

Django lo hizo un mago: plantillas y contextos

En el tutorial Django lo hizo un mago expliqué los conceptos básicos de Django]. En Django lo hizo un mago: paginación y detalle añadimos paginación y la ventana de detalle. En esta ocasión vamos a ponerlo bonito mediante plantillas, y crearemos cierto estilo añadiendo también contextos.

Para los que sepan algo de Django, no es necesario seguir los tutoriales anteriores, aunque sí es recomendable para los más novatos :D

Django

Cómo leer este artículo

Nuevamente, hay tres maneras de leer este artículo.

  1. Continuando tras la lectura de Django lo hizo un mago: paginación y detalle.
  2. Descargándose el tag 0.2.0 del repositorio GitHub git@github.com:magmax/django-blog.git
  3. Viendo los resultados descargándose el tag 0.3.0 del repositorio GitHub git@github.com:magmax/django-blog.git

Seguiremos usando el entorno Virtualen] que creamos en Django lo hizo un mago.

Plantillas (Templates)

Vamos a ahondar en el uso de plantillas, también conocidas como templates. Ya hemos visto lo básico: Las variables se rodean con doble llave {{ variable }} y los tags con llave porcentaje: {% tag [arguments] %}. Algunos tags tienen que cerrarse con {% endTAG %}. Finalmente, hay filtros (en inglés filters) que permiten modificar variables.

Los tags más importantes son if/else/endif y for/endfor. El primero nos permite añadir condiciones y el segundo, bucles. Tenéis ejemplos de ambos en el archivo templates/blog/post_list.html.

Pero no me voy a centrar en los tags o filters: Veamos la herencia.

Una plantilla puede heredar de otra. De esta manera, la plantilla padre define una estructura y la hija sobreescribe bloques. Es importante que definamos bien los bloques para que quede una buena estructura.

Veamos un ejemplo que hace uso de Bootstra] usando el CDN (archivo templates/base.html):

 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
<!DOCTYPE html>
<html lang="en">
  <head>
    {% load static %}
    <meta charset="utf-8" />

    <!-- Always force latest IE rendering engine (even in intranet) & Chrome Frame
         Remove this if you use the .htaccess -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
    <title>{% block title %}{% endblock %}</title>
    <link rel="stylesheet" href="/static/css/bootstrap.min.css" type="text/css" media="screen" />
    <style>
      body { padding-top: 70px; padding-bottom: 70px; }
      {% block css %}{% endblock %}
    </style>
  </head>

  <body>
    <div class="container">
      <div class="row">
        {% block head %}{% endblock %}
      </div>
      <div class="row">
        <div class="col-sm-8">
          {% block content %}{% endblock %}
        </div>
      </div>
    </div>
    <script type="text/javascript">
      {% block javascript %}{% endblock %}
    </script>
  </body>

  <foot>
    {% block foot %}{% endblock %}
  </foot>
</html>

Como se puede observar, hemos creado los bloques title, css, head, content, javascript-load, javascript y foot.

Ahora vamos a realizar unas pequeñas modificaciones en templates/blog/post_list.html:

 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
{% extends "base.html"%}

{% block content %}
<ul>
  {% for post in post_list %}
  <li><a href="{% url 'post_detail' post.id %}">{{ post.title }}</a></li>
  {% endfor %}
</ul>

<div>
    {% if page_obj.has_previous %}
      <a href="{% url 'home' %}?page={{ page_obj.previous_page_number }}">
        <span>&laquo; Previous</span>
      </a>
    {% else %}
      <span>&laquo; Previous</span>
    {% endif %}

      <span> | <span>

    {% if page_obj.has_next %}
      <a href="{% url 'home' %}?page={{ page_obj.next_page_number }}">
        <span>Next &raquo;</span>
      </a>
    {% else %}
      <span>Next &raquo;</span>
    {% endif %}
</div>
{% endblock %}

Básicamente, indicamos que la plantilla extiende de base.html y metemos lo que ya teníamos en el bloque content. Esto reemplazará al bloque content de la plantilla base.html.

Y con estas pequeñas modificaciones, veremos el cambio producido en la visualización de la página. Vamos a darle un poco más de estilo (templates/blog/post_list.html de nuevo):

 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
{% extends "base.html"%}

{% block content %}
<ul class="list-unstyled">
  {% for post in post_list %}
  <li><a href="{% url 'post_detail' post.id %}">{{ post.title }}</a></li>
  {% endfor %}
</ul>

<div>
  <nav>
    <ul class="pager">
      <li>
        {% if page_obj.has_previous %}
        <a href="{% url 'home' %}?page={{ page_obj.previous_page_number }}">
          <span>&laquo; Previous</span>
        </a>
        {% else %}
        <span>&laquo; Previous</span>
        {% endif %}
      </li>

      <li>
        {% if page_obj.has_next %}
        <a href="{% url 'home' %}?page={{ page_obj.next_page_number }}">
          <span>Next &raquo;</span>
        </a>
        {% else %}
        <span>Next &raquo;</span>
        {% endif %}
      </li>
    </ul>
  </nav>
</div>
{% endblock %}

Y ya que nos ponemos, hacemos lo mismo en templates/blog/post_detail.html:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
{% extends "base.html"%}

{% block content %}
<ol class="breadcrumb">
  <li><a href="{% url 'home' %}">Posts</a></li>
  <li class="active">{{ post.title }}</li>
</ol>

<h1>{{ post.title }}</h1>

<div>
  {{ post.teaser }}
</div>

<hr />

<div>
  {{ post.body }}
</div>

{% endblock %}

Esto ya va pareciendo un blog en condiciones :D Si queréis podéis darle un estilo mejor, añadir una barra de navegación, etc. De la sidebar nos ocuparemos a continuación.

Context Processors

Según la documentación:

Un context processor tiene una interfaz muy simple: Es tan sólo una función que toma un argumento, un objecto HttpRequest, y devuelve un diccionario que se añade al contexto de plantillas. Cada context processor debe devolver un diccionario.

Pero… ¿Qué es el contexto? El contexto es el ámbito donde se resuelven las variables. El contexto sea el conjunto de variables que podemos usar desde las plantillas sin definirlas en la misma.

Dicho de otro modo: Los context processors nos permiten definir variables que después podremos usar desde cualquier plantilla.

Y cómo no, vamos a verlo con un ejemplo.

Vamos a crear el archivo blog/context_processors.py con el siguiente contenido:

1
2
3
4
5
6
7
from . import models


def tags(request):
    return {
        'tags': models.Tag.objects.order_by('name')
    }

Añadimos nuestro nuevo context_processor a la lista de context processors a cargar en el archivo app/settings.py (como estamos usando el módulo auth, hay que cargar sus context processors también):

  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
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
"""
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',
    'sampledatahelper',
)

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'),
]

SAMPLEDATAHELPER_MODELS  = [
    { 'model': 'blog.Post', 'number': 100, },
    { 'model': 'blog.Tag', 'number': 5, },
]

TEMPLATE_CONTEXT_PROCESSORS = (
    'django.contrib.auth.context_processors.auth',
    'blog.context_processors.tags',
)

USE_CDN = False

Y modificamos ligeramente el archivo templates/base.html, añadiendo una sidebar con el menú de Tags:

 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
<!DOCTYPE html>
<html lang="en">
  <head>
    {% load static %}
    <meta charset="utf-8" />

    <!-- Always force latest IE rendering engine (even in intranet) & Chrome Frame
         Remove this if you use the .htaccess -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
    <title>{% block title %}{% endblock %}</title>
    <link rel="stylesheet" href="/static/css/bootstrap.min.css" type="text/css" media="screen" />
    <style>
      body { padding-top: 70px; padding-bottom: 70px; }
      {% block css %}{% endblock %}
    </style>
  </head>

  <body>
    <div class="container">
      <div class="row">
        {% block head %}{% endblock %}
      </div>
      <div class="row">
        <div class="col-sm-8">
          {% block content %}{% endblock %}
        </div>
        <div class="col-sm-3 col-sm-offset-1">
          {% if tags %}
          <div class="sidebar-module">
            <h4>Tags</h4>
            <ol class="list-unstyled">
              {% for tag in tags %}
              <li><a href="#">{{ tag }}</a></li>
              {% endfor %}
            </ol>
          </div>
          {% endif %}
        </div>
      </div>
    </div>
    <script type="text/javascript" src="{% static 'jquery.js' %}"></script>
    <script type="text/javascript" src="{% static 'jquery-ui.js' %}"></script>
    <script type="text/javascript" src="{% static 'js/bootstrap.min.js' %}"></script>
    <script type="text/javascript">
      {% block javascript %}{% endblock %}
    </script>
  </body>

  <foot>
    {% block foot %}{% endblock %}
  </foot>
</html>

Y veremos aparecer el menú con todos los tags en todas nuestras páginas.

EJERCICIO: Dejo al lector un ejercicio de repaso: añadir una DetailView para los blog.models.Tag, con su plantilla correspondiente, y enlazarlos desde este menú que acabamos de crear. Para que os quede más chulo podéis usar la librería Bootstra]; Podéis aprender lo asico en Bootstrap en 5 minutos ¿La solución? En el tag 0.3.0-exercise del repositorio git@github.com:magmax/django-blog.git.