El libro de Django 1.0

7.1. Búsquedas

Los buscadores son unos de los principales protagonistas de la web. Casos de éxito como Google y Yahoo, han construido sus empresas multimillonarias alrededor de las búsquedas. Casi todos los sitios obtienen un gran porcentaje de tráfico viniendo desde y hacia sus páginas de búsqueda. A menudo, la diferencia entre el éxito y el fracaso de un sitio, lo determina la calidad de su búsqueda. Así que sería mejor que agreguemos un poco de búsqueda a nuestro pequeño sitio de libros, ¿no?

Comenzaremos agregando la vista para la búsqueda a nuestro URLconf (mysite.urls). Recuerda que esto se hace agregando algo como (r'^search/$', 'mysite.books.views.search') al conjunto de URL patterns (patrones).

A continuación, escribiremos la vista search en nuestro módulo de vistas (mysite.books.views):

from django.db.models import Q
from django.shortcuts import render_to_response
from models import Book

def search(request):
    query = request.GET.get('q', '')
    if query:
        qset = (
            Q(title__icontains=query) |
            Q(authors__first_name__icontains=query) |
            Q(authors__last_name__icontains=query)
        )
        results = Book.objects.filter(qset).distinct()
    else:
        results = []
    return render_to_response("books/search.html", {
        "results": results,
        "query": query
    })

Aquí han surgido algunas cosas que todavía no hemos vimos. La primera, ese request.GET. Así es cómo accedes a los datos del GET desde Django; Los datos del POST se acceden de manera similar, a través de un objeto llamado request.POST. Estos objetos se comportan exactamente como los diccionarios estándar de Python, y tienen además otras características que se explican en el apéndice H.

Así que la línea:

query = request.GET.get('q', '')

busca un parámetro del GET llamado q y devuelve una cadena de texto vacía si este parámetro no fue suministrado. Observa que estamos usando el método get() de request.GET, algo potencialmente confuso. Este método get() es el mismo que posee cualquier diccionario de Python. Lo estamos usando aquí para ser precavidos: no es seguro asumir que request.GET tiene una clave 'q', así que usamos get('q', '') para proporcionar un valor por defecto, que es '' (el string vacío). Si hubiéramos intentado acceder a la variable simplemente usando request.GET['q'], y q no hubiese estado disponible en los datos del GET, se habría lanzado un KeyError.

Segundo, ¿qué es ese Q? Los objetos Q se utilizan para ir construyendo consultas complejas — en este caso, estamos buscando los libros que coincidan en el título o en el nombre con la consulta. Técnicamente, estos objetos Q consisten de un QuerySet, y puede leer más sobre esto en el apéndice C.

En estas consultas, icontains es una búsqueda en la que no se distinguen mayúsculas de minúsculas (case-insensitive), y que internamente usa el operador LIKE de SQL en la base de datos.

Dado que estamos buscando en campos de muchos-a-muchos, es posible que un libro se obtenga más de una vez (por ej: un libro que tiene dos autores, y los nombres de ambos concuerdan con la consulta). Al agregar .distinct() en el filtrado, se eliminan los resultados duplicados.

Todavía no hay una plantilla para esta vista. Esto lo solucionará:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="en">
<head>
    <title>Search{% if query %} Results{% endif %}</title>
</head>
<body>
  <h1>Search</h1>
  <form action="." method="GET">
    <label for="q">Search: </label>
    <input type="text" name="q" value="{{ query|escape }}">
    <input type="submit" value="Search">
  </form>

  {% if query %}
    <h2>Results for "{{ query|escape }}":</h2>

    {% if results %}
      <ul>
      {% for book in results %}
        <li>{{ book|escape }}</l1>
      {% endfor %}
      </ul>
    {% else %}
      <p>No books found</p>
    {% endif %}
  {% endif %}
</body>
</html>

A esta altura, lo que esto hace debería ser obvio. Sin embargo, hay algunos detalles que vale la pena resaltar:

  • action s . en el formulario, esto significa "la URL actual". Esta es una buena práctica estándar: no utilices vistas distintas para la página que contiene el formulario y para la página con los resultados; usa una página única para las dos cosas.
  • Volvemos a insertar el texto de la consulta en el <input>. Esto permite a los usuarios refinar fácilmente sus búsquedas sin tener que volver a teclear todo nuevamente.
  • En todo lugar que aparece query y book, lo pasamos por el filtro escape para asegurarnos de que cualquier búsqueda potencialmente maliciosa sea descartada antes de que se inserte en la página

    ¡Es vital hacer esto con todo el contenido suministrado por el usuario! De otra forma el sitio se abre a ataques de cross-site scripting (XSS). El Capítulo 19 explica XSS y la seguridad con más detalle.

  • En cambio, no necesitamos preocuparnos por el contenido malicioso en las búsquedas de la base de datos — podemos pasar directamente la consulta a la base de datos. Esto es posible gracias a que la capa de base de datos de Django se encarga de manejar este aspecto de la seguridad por ti.

Ahora ya tenemos la búsqueda funcionando. Se podría mejorar más el sitio colocando el formulario de búsqueda en cada página (esto es, en la plantilla base). Dejaremos esto de tarea para el hogar.

A continuación veremos un ejemplo más complejo. Pero antes de hacerlo, discutamos un tema más abstracto: el "formulario perfecto".