Contenido

Django lo hizo un mago: paginación y detalle

En el artículo anterior Django lo hizo un mago creamos un entorno básico con un listado de posts para un blog. En esta ocasión vamos a añadir más funcionalidad.

En concreto, veremos cómo visualizar cada post (lo que llamaremos detalle), y añadiremos paginación.

Pero antes un extra: Añadir elementos aleatorios de pruebas fácilmente con sampledatahelper.

Django

Cómo leer este artículo

Hay tres maneras de leer este artículo.

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

Acordáos de crear el entorno Virtualenv e instalar las dependencias.

Y dicho esto, vamos al lío.

Documentos a cholón

Personalmente encuentro bastante fastidioso tener que escribir muchos artículos, así que vamos a instalarnos una librería llamada sampledatahelper y a configurarla para que nos genere documentos aleatorios.

Instalamos el paquete:

1
(env)$ pip install django-sampledatahelper==0.2.1

Instalamos la aplicación, añadiéndola a la variable INSTALLED_APPS del archivo app/settings.py, además de la configuración necesaria:

 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
"""
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, },
]

y ejecutamos:

1
python manage.py sampledatafiller

Y tendremos escritos 100 posts pertenecientes a 5 categorías diferentes. Podéis ejecutar varias veces para generar más o jugar con los parámetros :D

Paginación

Con tantos documentos no hay quien lea nada.

Vamos a paginar. Para ello, editamos nuestro controlador, que en Django se llama vista(archivo blog/views.py):

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

from . import models


class PostListView(ListView):
    model = models.Post
    paginate_by = 8

Y con eso se nos reducirán los documentos a 8. Ahora editamos la plantilla, para saltar al anterior y al siguiente:

 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
<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>

No creo que tengáis problemas en entenderlo, más allá de encontrar el truco de magia: Django nos ha inyectado una variable llamada page_obj. Esto es lo que se denomina contexto y que veremos en mayor detalle en el artículo de plantillas y estáticos.

Qué difícil ¿no?

Si os sentís artistas podéis darle un estilillo más chulo. De todas maneras hablaremos más en algún artículo dedicado a las plantillas y estáticos.

Viendo los posts

Ha llegado el momento de ver el contenido de los posts. Esto tiene bastantes modificaciones. Vayamos por partes:

El controlador

No vamos a modificar el modelo, así que vamos directamente con el controlador (archivo blog/views.py, acordaos):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView

from . import models


class PostListView(ListView):
    model = models.Post
    paginate_by = 8


class PostDetailView(DetailView):
    model = models.Post

Poco que explicar aquí: importamos django.views.generic.detail.DetailView y creamos una clase de ese tipo, asociándola con nuestro modelo blog.models.Post.

El enrutador

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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'^post/(?P<pk>\d+)$', views.PostDetailView.as_view(), name='post_detail'),
    url(r'^admin/', include(admin.site.urls)),
)

Sólo he añadido una línea, la segunda url. Aquí estoy usando una expresión regular((?P<pk>\d+)) que puede parecer compleja, pero no lo es. Tan sólo cojo uno o más números y le doy un nombre: pk (de primary key). Utilizo esta expresión regular para componer las URLs que identificarán el detalle de un post, asociándolo con la clase que creamos en el paso anterior. Además, le doy un nombre.

Enlazando

Ahora podemos enlazar en la lista de posts, 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
<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>

La vista

Si lanzamos el servidor y pulsamos sobre estos enlaces, nos dará un error porque no encuentra el archivo blog/post_detail.html. Vamos a crearlo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<h1>{{ post.title }}</h1>

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

<hr />

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

<a href="{% url 'home' %}">Post list</a>

Y, ahora sí, podemos ver el detalle.

Conclusión

Si hacéis un show de los cambios de esta versión, descubriréis que los cambios que hemos hecho hoy son muy pequeños:

1
6 files changed, 49 insertions(+), 1 deletion(-)

Creo que esto explica el motto de Django:

Django makes it easier to build better Web apps more quickly and with less code.

Si os ha gustado, podéis continuar con Django lo hizo un mago: plantillas y contextos, donde mejoraremos el aspecto dándole una calidad profesional con muy poco esfuerzo.