Escribiendo tu primera Django app, parte 2

Este tutorial comienza donde dejó Tutorial 1. Continuaremos con la aplicación de encuestas y nos concentraremos en la interfaz de administración (que llamaremos ‘admin’) que Django genera automáticamente.

Filosofía

Generar sitios de administración para que gente de staff o clientes agreguen, cambien y borren contenido es un trabajo tedioso que no requiere mucha creatividad. Por esta razón, Django automatiza completamente la creación de una interfaz de administración para los modelos.

Django fue escrito en el ambiente de una sala de noticias, con una separación muy clara entre “administradores de contenido” y el sitio “público”. Los administradores del sitio usan el sistema para agregar noticias, eventos, resultados deportivos, etc., y el contenido se muestra en el sitio público. Django resuelve el problema de crear una interfaz unificada para que los administradores editen contenido.

El admin no está pensado para ser usado por los visitantes de un sitio, sino para los administradores del mismo.

Creando un usuario admin

Primero vamos a necesitar crear un usuario que pueda loguearse al sitio de administración. Corremos el siguiente comando:

$ python manage.py createsuperuser

Ingresamos el nombre usuario que querramos y presionamos enter.

Username: admin

Deberemos ingresar la dirección de email:

Email address: admin@example.com

El último paso es ingresar el password. Deberemos ingresarlo dos veces, la segunda como una confirmación de la primera.

Password: **********
Password (again): *********
Superuser created successfully.

Levantar el servidor de desarrollo

El sitio de admin de Django está habilitado por defecto. Levantemos el servidor de desarrollo y exploremoslo.

Recordemos del Tutorial 1 que para empezar el servidor de desarrollo debemos correr:

$ python manage.py runserver

Ahora, abrimos un browser y vamos a “/admin/” en el dominio local – e.g., http://127.0.0.1:8000/admin/. Deberíamos ver la pantalla de login del admin:

Django admin login screen

Dado que translation está activado por defecto, la pantalla de login podría mostrarse en tu propio idioma, dependiendo de la configuración de tu browser y de si Django tiene las traducciones para dicho idioma.

No coincide con lo que ves?

Si en este punto en lugar de la pantalla de login de arriba obtenés una página de error reportando algo como:

ImportError at /admin/
cannot import name patterns
...

entonces probablemente estás usando una versión de Django que no coincide con la de este tutorial. Deberías cambiar a una versión anterior del tutorial o a una versión más nueva de Django.

Ingresar al admin

Ahora intentemos loguearnos con la cuenta de superusuario que creamos en el paso anterior. Deberíamos ver la página inicial del admin de Django:

Django admin index page

Deberíamos ver unos pocos tipos de contenido editable: grupos y usuarios. Éstos vienen provistos por django.contrib.auth, el framework de autenticación que viene con Django.

Hacer la app poll modificable en el admin

Dónde está nuestra app poll? No se muestra en la página del admin.

Hay una cosa que hacer: necesitamos decirle al admin que los objetos Question tengan una interfaz de administración. Para esto abrimos el archivo polls/admin.py, y lo editamos para que tenga el siguiente contenido:

polls/admin.py
from django.contrib import admin

from .models import Question

admin.site.register(Question)

Explorar la funcionalidad del admin

Ahora que registramos Question, Django sabe que se debe mostrar en la página del admin:

Django admin index page, now with polls displayed

Hacemos click en “Questions”. Ahora estamos en la página de listado (change list) de preguntas. Esta página muestra todas las preguntas en la base de datos y nos permite elegir una para modificarla. Está la pregunta “What’s up” que creamos en la primera parte:

Polls change list page

Cliqueamos en ella para editarla:

Editing form for question object

Cosas para notar aquí:

  • El form es generado automáticamente a partir del modelo Question.

  • Los diferentes tipos de campo del modelo (DateTimeField, CharField) se corresponden con el widget HTML apropiado. Cada tipo de campo sabe cómo mostrarse en el admin de Django.

  • Cada DateTimeField tiene atajos JavaScript. La fecha tienen un atajo para “Hoy” (Today) y un popup de calendario, y la hora un “Ahora” (Now) y un popup que lista las horas más comunes.

El cierre de la página nos da algunas opciones:

  • Guardar (Save) – Guarda los cambios y nos devuelve a la página listado para este tipo de objeto.

  • Grabar y continuar editando (Save and continue editing) – Guarda los cambios y recarga la página del admin de este objeto.

  • Grabar y agregar otro (Save and add another) – Guarda los cambios y carga un form nuevo, en blanco, que permite agregar un nuevo objeto de este tipo.

  • Eliminar (Delete) – Muestra una página de confirmación de borrado.

Si el valor de “Date published” no coincide con el establecido durante la creación del objeto en Tutorial 1, probablemente sea que olvidaste setear el valor correcto para el TIME_ZONE. Revisalo, recargá la página y chequeá que se muestra el valor correcto.

Cambiamos el valor de “Date published” haciendo click en “Today” y “Now”. Luego hacemos click en “Save and continue editing”. Si hacemos click en “History” arriba a la derecha, podemos ver una página que lista todos los cambios hechos a este objeto a través del admin, con la fecha/hora y el nombre de usuario de la persona que hizo el cambio:

History page for question object

Personalizar el form del admin

Lleva unos pocos minutos maravillarse por todo el código que no tuvimos que escribir. Sólo registrando el modelo Question con admin.site.register(Question), Django fue capaz de construir una representación con un form por defecto. A menudo uno querrá personalizar cómo este form se ve y funciona. Esto lo hacemos pasando algunas opciones a Django cuando registramos el modelo.

Veamos cómo funciona reordenando los campos en el form de edición. Reemplazamos la línea admin.site.register(Question) por:

polls/admin.py
from django.contrib import admin

from .models import Question


class QuestionAdmin(admin.ModelAdmin):
    fields = ['pub_date', 'question_text']

admin.site.register(Question, QuestionAdmin)

Seguiremos este patrón – creamos un objeto model admin, que luego pasamos como segundo argumento de admin.site.register() – cada vez que necesitemos cambiar alguna opción del admin para un modelo.

Este cambio en particular hace que “Publication date” se muestre antes que el campo “Question”:

Fields have been reordered

No es muy impresionante con sólo dos campos, pero para forms con docenas de campos, elegir un orden intuitivo es un detalle de usabilidad importante.

Y hablando de forms con docenas de campos, podríamos querer dividir el form en fieldsets:

polls/admin.py
from django.contrib import admin

from .models import Question


class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        (None,               {'fields': ['question_text']}),
        ('Date information', {'fields': ['pub_date']}),
    ]

admin.site.register(Question, QuestionAdmin)

El primer argumento de cada tupla en fieldsets es el título del fieldset. Ahora nuestro form se vería así:

Form has fieldsets now

Podemos asignar clases HTML arbitrarias a cada fieldset. Django provee una clase "collapse" que muestra el fieldset inicialmente plegado. Esto es útil cuando uno tiene un form largo que contiene ciertos campos que no se usan normalmente:

polls/admin.py
from django.contrib import admin

from .models import Question


class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        (None,               {'fields': ['question_text']}),
        ('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
    ]

admin.site.register(Question, QuestionAdmin)
Fieldset is initially collapsed

Personalizar la página listado

Ahora que la página de admin de Question se ve bien, hagamos algunos tweaks a la página de listado – la que lista todas las preguntas en el sistema.

Así se ve ahora:

Polls change list page

Por defecto, Django muestra el str() de cada objeto. Pero algunas veces es más útil si podemos mostrar campos individuales. Para eso usamos la opción list_display del admin, que es una tupla de los nombres de campo a mostrar, como columnas, en la página de listado:

polls/admin.py
class QuestionAdmin(admin.ModelAdmin):
    # ...
    list_display = ('question_text', 'pub_date')

Sólo por si acaso, incluyamos también el método was_published_recently que definimos en la primera parte:

polls/admin.py
class QuestionAdmin(admin.ModelAdmin):
    # ...
    list_display = ('question_text', 'pub_date', 'was_published_recently')

Ahora la página de listado de encuestas se vería así:

Polls change list page, updated

Podemos hacer click en los encabezados de las columnas para ordenar por los respectivos valores – salvo en el caso de was_published_recently, porque ordenar por la salida de un método arbitrario no está soportado. Notemos también que el encabezado para la columna de was_published_recently es por defecto el nombre del método (reemplazando guiones bajos por espacios), y que cada línea contiene la representación como string del valor devuelto por el método.

Esto se puede mejorar definiendo un par de atributos del método (en polls/models.py) como sigue:

polls/models.py
class Question(models.Model):
    # ...
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
    was_published_recently.admin_order_field = 'pub_date'
    was_published_recently.boolean = True
    was_published_recently.short_description = 'Published recently?'

Para más información sobre estas propiedades, ver list_display.

Editamos el archivo polls/admin.py de nuevo y agregamos una mejora a la página de listado de preguntas: filtros usando list_filter. Agregamos la siguiente línea a QuestionAdmin:

list_filter = ['pub_date']

Esto agrega un “Filtro” en la barra lateral que permite filtrar el listado por el campo pub_date:

Polls change list page, updated

El tipo de filtro depende del tipo de campo sobre el cual se filtra. Como pub_date es un DateTimeField, Django sabe darnos opciones de filtro apropiadas: “Any date”, “Today”, “Past 7 days”, “This month”, “This year”.

Esto está tomando buena forma. Agreguemos campacidad de búsqueda:

search_fields = ['question_text']

Esto agrega un campo de búsqueda al tope del listado. Cuando alguien ingresa términos de búsqueda, Django va a buscar sobre el campo question_text. Se pueden usar tantos campos como uno quiera – aunque como por detrás se trata de una consulta del tipo LIKE hay que ser razonables para hacérselo simple a la base de datos.

Es un buen momento para observar también que el listado es paginado. Por defecto se muestran 100 items por página. Paginación, búsqueda, filtros, jerarquía de fechas, y ordenamiento desde columnas funcionan como uno esperaría.

Personalizar el “look and feel” del admin

Claramente tener el título “Django administration” al tope de cada página del admin es ridículo. Es un “placeholder”.

Es fácil de cambiar usando el sistema de templates de Django. El admin de Django funciona gracias a Django mismo, y la interfaz provista usa el sistema de templates de Django.

Personalizando los templates de tu proyecto

Creamos un directorio templates en el directorio del proyecto (el que contiene manage.py). Los templates pueden estar en cualquier lugar de nuestro sistema de archivos al que Django tenga acceso (Django corre como el usuario que corra el servidor). Sin embargo, mantener los templates dentro del proyecto es una buena convención a seguir.

Abrimos el archivo de settings (recordemos, mysite/settings.py) y agregamos la opción DIRS en el setting TEMPLATES:

mysite/settings.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

DIRS es una lista de directorios en los que Django chequea para cargar templates; es un path de búsqueda.

Ahora creamos un directorio llamado admin dentro de templates, y copiamos el template admin/base_site.html del directorio de templates del admin de Django por defecto, del código fuente de Django mismo (django/contrib/admin/templates), a ese directorio.

Dónde están los archivos del código fuente de Django?

Si tenés problemas encontrando dónde están los archivos de Django, podés correr el siguiente comando:

$ python -c "
import sys
sys.path = sys.path[1:]
import django
print(django.__path__)"

Luego, basta editar el archivo y reemplazar {{ site_header|default:_('Django administration') }} (incluyendo las llaves) por el nombre de nuestro sitio. Deberías terminar con una sección de código como:

{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">Polls Administration</a></h1>
{% endblock %}

Usamos esta metodología para enseñarte la manera en que se personalizan templates. En un proyecto real, probablemente usaríamos el atributo django.contrib.admin.AdminSite.site_header para cambiar este valor en particular.

Este archivo de template contiene varios textos de la forma {% block branding %} y and {{ title }}. Los tags {% y {{ son parte del lenguaje de templates de Django. Cuando Django renderiza admin/base_site.html, este lenguaje se evalúa para producir la página HTML final. No nos preocupemos de esto por ahora – veremos en detalle el lenguaje de templates de Django en Tutorial 3.

Notemos que cualquier template del admin de Django se puede “sobreescribir”. Para esto sólo basta repetir lo que hicimos con base_site.html – copiar el template desde el directorio de Django a nuestro directorio de templates y aplicar los cambios.

Personalizando los templates de tu aplicación

Los lectores astutos se estarán preguntando: si DIRS estaba vacío por defecto, cómo es que Django encuentra los templates del admin? La respuesta es que, como APP_DIRS está seteado en True, Django automáticamente busca un subdirectorio templates/ en cada paquete de aplicación, para usar como fallback (no olvidarse que django.contrib.admin es una aplicación).

Nuestra aplicación no es muy compleja y no necesita personalizar templates del admin. Pero si creciera de forma más sofisticada y requiriera cambios a los templates base del admin para alguna funcionalidad, sería más razonable modificar los templates a nivel aplicación en lugar de proyecto. De esta manera, se podría incluir la aplicación polls en cualquier nuevo proyecto y asegurarse de que los templates personalizados estuvieran disponibles.

Ver la documentación de carga de templates para más información sobre cómo Django busca los templates.

Personalizar la página inicial del admin

De forma similar uno podría querer personalizar el look and feel de la página inicial del admin.

Por defecto, muestra todas las apps en INSTALLED_APPS que se registraron con la aplicación admin, en orden alfabético. Uno podría querer hacer cambios significativos al layout. Después de todo, la inicial es probablemente la página más importante del admin, y debería ser fácil de usar.

El template a personalizar es admin/index.html (habría que hacer lo mismo que hicimos en la sección anterior con admin/base_site.html – copiar el template a nuestro directorio de templates). Editamos el archivo, y veremos que usa una variable de template llamada app_list. Esta variable contiene cada app instalada. En lugar de usarla, uno podría escribir los links de manera explícita a las páginas de objetos específicas del admin de la manera en que a uno le parezca mejor. De nuevo, no preocuparse si no se entiende el lenguaje de templates – lo cubriremos en detalle en Tutorial 3.

Una vez que te sientas cómodo con el sitio de admin, podés empezar con la parte 3 del tutorial donde comenzamos a trabajar con las vistas públicas de nuestra aplicación.