Usando mezclas con vistas basadas en clases

Prudencia

Este es un tema avanzado. Se aconseja tener una buena comprensión de las vistas basadas en clases (vistas basadas en clases de Django) antes de explorar estas técnicas.

Django proporciona vistas basadas en clases con una gran cantidad de funcionalidad integrada, pero es posible que desees usar algunas de ellas por separado. Por ejemplo, puede que quieras escribir una vista que renderice un template para generar la respuesta HTTP, pero no puedes utilizar TemplateView; quizás necesitas renderizar un template solo en POST, mientras que con GET se hace algo completamente diferente. Si bien podrías usar directamente TemplateResponse, lo más probable es que te lleve a duplicar código.

Django también proporciona un número de mezclas que ofrecen funcionalidades más discretas. La renderización de plantillas, por ejemplo, se encapsula en la TemplateResponseMixin. La documentación de referencia de Django contiene la documentación completa de todas las mezclas.

Contenido y respuestas de plantilla

Dos mezclas centrales se proporcionan que ayudan en la tarea de brindar una interfaz consistente para trabajar con plantillas en vistas basadas en clase.

TemplateResponseMixin

Cada vista integrada que devuelve una TemplateResponse llamará al método render_to_response() proporcionado por TemplateResponseMixin. La mayoría de las veces esto se llamará para ti (por ejemplo, se llama por el método get() implementado tanto por TemplateView como por DetailView); de manera similar, es poco probable que necesites sobrescribirlo, aunque si quieres que tu respuesta devuelva algo no renderizado mediante un template Django entonces lo querrás hacer. Para un ejemplo de esto, consulta el JSONResponseMixin example.

render_to_response() llama a get_template_names(), que por defecto buscará template_name en la vista basada en clase; dos otras mixins (SingleObjectTemplateResponseMixin y MultipleObjectTemplateResponseMixin) sobrescriben esto para proporcionar valores por defecto más flexibles cuando se tratan de objetos reales.

ContextMixin

Cada vista integrada que necesita datos de contexto, como para renderizar un template (incluyendo TemplateResponseMixin arriba), debería llamar a get_context_data() pasando cualquier dato que quieran asegurarse esté allí como argumentos clave. get_context_data() devuelve un diccionario; en ContextMixin devuelve sus argumentos clave, pero es común sobrescribir esto para agregar más miembros al diccionario. También puedes utilizar el atributo extra_context.

Creación de vistas basadas en clase genericas de Django

Vamos a ver cómo dos de las vistas basadas en clase genericas de Django están construidas a partir de mixins que proporcionan funcionalidad discreta. Consideraremos DetailView, que renderiza una «vista de detalle» de un objeto, y ListView, que renderizará una lista de objetos, típicamente desde un queryset, y opcionalmente los paginará. Esto nos introducirá a cuatro mixins que entre ellos proporcionan funcionalidad útil cuando se trabaja con un solo objeto Django o múltiples objetos.

También hay mixins involucrados en las vistas genericas de edición (FormView, y las vistas específicas del modelo CreateView, UpdateView y DeleteView), y en las vistas genericas basadas en fecha. Estas se cubren en la documentación de referencia de mixins <ref/class-based-views/mixins>.

DetailView: trabajando con un solo objeto Django

Para mostrar el detalle de un objeto, básicamente necesitamos hacer dos cosas: necesitamos buscar el objeto y luego hacer una TemplateResponse con un template adecuado, y ese objeto como contexto.

To get the object, DetailView relies on SingleObjectMixin, which provides a get_object() method that figures out the object based on the URL of the request (it looks for pk and slug keyword arguments as declared in the URLConf, and looks the object up either from the model attribute on the view, or the queryset attribute if that’s provided). SingleObjectMixin also overrides get_context_data(), which is used across all Django’s built in class-based views to supply context data for template renders.

Para luego hacer una TemplateResponse, DetailView utiliza SingleObjectTemplateResponseMixin, que extiende TemplateResponseMixin, sobrescribiendo get_template_names() como se discutió anteriormente. De hecho, proporciona un conjunto bastante sofisticado de opciones, pero la principal que la mayoría de las personas van a utilizar es <app_label>/<model_name>_detail.html. La parte _detail se puede cambiar estableciendo template_name_suffix en una subclase para algo diferente. (Por ejemplo, las vistas genericas de edición utilizan _form para crear y actualizar vistas, y _confirm_delete para eliminar vistas.)

ListView: trabajando con muchos objetos Django

Las listas de objetos siguen un patrón similar: necesitamos una (posiblemente paginada) lista de objetos, típicamente una QuerySet, y luego debemos hacer una TemplateResponse con un template adecuado utilizando esa lista de objetos.

Para obtener los objetos, ListView utiliza MultipleObjectMixin, que proporciona tanto get_queryset() como paginate_queryset(). A diferencia de con SingleObjectMixin, no hay necesidad de basarse en partes de la URL para determinar el conjunto de objetos a trabajar, por lo que el valor predeterminado utiliza el atributo queryset o model en la clase del view. Una razón común para sobrescribir get_queryset() aquí sería variar dinámicamente los objetos, como dependiendo del usuario actual o para excluir publicaciones futuras en un blog.

MultipleObjectMixin también sobrescribe get_context_data() para incluir variables de contexto apropiadas para la paginación (proporcionando dummies si la paginación está deshabilitada). Depende de object_list que se pasa como argumento de palabra clave, lo que ListView arregla.

Para hacer una TemplateResponse, ListView luego utiliza MultipleObjectTemplateResponseMixin; al igual que con SingleObjectTemplateResponseMixin anteriormente, esto sobrescribe get_template_names() para proporcionar una variedad de opciones, con la más utilizada siendo <app_label>/<model_name>_list.html, con la parte _list nuevamente tomada del atributo template_name_suffix. (Las vistas genericas de fecha utilizan sufijos como _archive, _archive_year y así sucesivamente para utilizar diferentes plantillas para las diversas vistas de lista especializadas por fechas.)

Usando mixins de vistas basadas en clases de Django

Ahora que hemos visto cómo las vistas genericas de Django basadas en clases utilizan los mixins proporcionados, vamos a ver otras formas de combinarlos. Aún estamos combinándolos con vistas basadas en clases o vistas genericas de Django, pero hay una variedad de problemas raros que se pueden resolver que no están proporcionados por defecto.

Advertencia

No todos los mixins se pueden utilizar juntos y no todas las vistas genericas de Django se pueden usar con todos los otros mixins. Aquí presentamos algunos ejemplos que funcionan; si deseas combinar otras funcionalidades entonces tendrás que considerar las interacciones entre atributos y métodos que se superponen entre las diferentes clases que estás utilizando, y cómo afectará la resolución de método a qué versiones de los métodos se llamarán en qué orden.

La referencia documentada para las vistas basadas en clases de Django (vistas basadas en clases y mezclas de vistas basadas en clases) te ayudará a entender qué atributos y métodos son probables que causen conflictos entre diferentes clases y mezclas.

Si tienes alguna duda, es mejor retroceder y basar tu trabajo en View o TemplateView, quizás con SingleObjectMixin y MultipleObjectMixin. Aunque probablemente terminarás escribiendo más código, es más probable que sea claramente comprensible para alguien más que lo vaya a leer después, y con menos interacciones de las que te preocupas ahorrarás algunas reflexiones. (Por supuesto, siempre puedes sumergirte en la implementación de Django de las vistas basadas en clases genericas para obtener inspiración sobre cómo abordar problemas.)

Usando SingleObjectMixin con View

Si queremos escribir una vista basada en clase que responda solo a POST, heredaremos de View y escribiremos un método post() en la subclase. Sin embargo, si queremos que nuestro procesamiento funcione sobre un objeto particular, identificado desde la URL, querremos la funcionalidad proporcionada por SingleObjectMixin.

Demostraremos esto con el modelo Author que usamos en la introducción a las vistas basadas en clases genericas (vistas basadas en clases genericas).

views.py
from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.urls import reverse
from django.views import View
from django.views.generic.detail import SingleObjectMixin
from books.models import Author


class RecordInterestView(SingleObjectMixin, View):
    """Records the current user's interest in an author."""

    model = Author

    def post(self, request, *args, **kwargs):
        if not request.user.is_authenticated:
            return HttpResponseForbidden()

        # Look up the author we're interested in.
        self.object = self.get_object()
        # Actually record interest somehow here!

        return HttpResponseRedirect(
            reverse("author-detail", kwargs={"pk": self.object.pk})
        )

En la práctica, probablemente querrías registrar el interés en un almacén de valores clave-valor en lugar de en una base de datos relacional, por lo que hemos dejado ese bit fuera. La única parte de la vista que necesita preocuparse de utilizar SingleObjectMixin es donde queremos buscar el autor en el que estamos interesados, y lo hace con un llamado a self.get_object(). Todo lo demás se toma cuidado por nosotros mediante la mezcla.

Podemos conectar esto fácilmente en nuestras URL:

urls.py
from django.urls import path
from books.views import RecordInterestView

urlpatterns = [
    # ...
    path(
        "author/<int:pk>/interest/",
        RecordInterestView.as_view(),
        name="author-interest",
    ),
]

Nota el grupo de nombre pk, que get_object() utiliza para buscar la instancia de Author. También podrías usar un slug o cualquier otra característica de SingleObjectMixin.

Usando SingleObjectMixin con ListView

ListView proporciona paginación integrada, pero quizás querrías paginar una lista de objetos que están todos vinculados (por una clave foránea) a otro objeto. En nuestro ejemplo de publicación, podrías querer paginar por todas las obras de un editor particular.

Una forma de hacer esto es combinar ListView con SingleObjectMixin, de modo que el conjunto de resultados para la lista paginada de libros pueda estar asociado al editor encontrado como objeto único. Para hacer esto, necesitamos tener dos conjuntos de resultados diferentes:

Book conjunto de resultados para uso por ListView

Dado que tenemos acceso al Publisher cuyos libros queremos listar, sobrescribimos get_queryset() y utilizamos el administrador de claves foráneas reversas del Publisher<backwards-related-objects>.

Conjunto de resultados Publisher para uso en get_object()

Nosotros nos apoyaremos en la implementación predeterminada de get_object() para obtener el objeto correcto Publisher. Sin embargo, necesitamos pasar explícitamente un argumento queryset porque de otra manera la implementación predeterminada de get_object() llamaría a get_queryset() que hemos sobrescrito para devolver objetos Book en lugar de los Publisher.

Nota

Tenemos que pensar cuidadosamente sobre get_context_data(). Dado que tanto SingleObjectMixin como ListView agregarán cosas al contexto bajo el valor de context_object_name si está configurado, en su lugar aseguraremos explícitamente que el Publisher esté en el contexto. ListView agregará los objetos adecuados page_obj y paginator para nosotros siempre y cuando recordemos llamar a super().

Ahora podemos escribir una nueva vista de detalles del editor PublisherDetailView:

from django.views.generic import ListView
from django.views.generic.detail import SingleObjectMixin
from books.models import Publisher


class PublisherDetailView(SingleObjectMixin, ListView):
    paginate_by = 2
    template_name = "books/publisher_detail.html"

    def get(self, request, *args, **kwargs):
        self.object = self.get_object(queryset=Publisher.objects.all())
        return super().get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["publisher"] = self.object
        return context

    def get_queryset(self):
        return self.object.book_set.all()

Observa cómo establecemos self.object dentro de get() para que podamos usarlo nuevamente más tarde en get_context_data() y get_queryset(). Si no configuras template_name, el template por defecto será la elección normal ListView, que en este caso sería "books/book_list.html" porque se trata de una lista de libros; ListView no sabe nada sobre SingleObjectMixin, así que no tiene ninguna idea de que esta vista tenga algo que ver con un Publisher.

La configuración paginate_by está deliberadamente pequeña en el ejemplo para que no tengas que crear muchos libros para ver la paginación funcionando. Aquí tienes el template que querrías usar:

{% extends "base.html" %}

{% block content %}
    <h2>Publisher {{ publisher.name }}</h2>

    <ol>
      {% for book in page_obj %}
        <li>{{ book.title }}</li>
      {% endfor %}
    </ol>

    <div class="pagination">
        <span class="step-links">
            {% if page_obj.has_previous %}
                <a href="?page={{ page_obj.previous_page_number }}">previous</a>
            {% endif %}

            <span class="current">
                Page {{ page_obj.number }} of {{ paginator.num_pages }}.
            </span>

            {% if page_obj.has_next %}
                <a href="?page={{ page_obj.next_page_number }}">next</a>
            {% endif %}
        </span>
    </div>
{% endblock %}

Evita cualquier cosa más compleja

Generally you can use TemplateResponseMixin and SingleObjectMixin when you need their functionality. As shown above, with a bit of care you can even combine SingleObjectMixin with ListView. However things get increasingly complex as you try to do so, and a good rule of thumb is:

Consejo

Cada una de tus vistas debe utilizar solo mixins o vistas de uno de los grupos de vistas basadas en clases genéricas: detalle, lista, edición y fecha. Por ejemplo, está bien combinar TemplateView (vista integrada) con MultipleObjectMixin (lista genérica), pero es probable que tengas problemas combinando SingleObjectMixin (detalle genérico) con MultipleObjectMixin (lista genérica).

Para mostrar qué sucede cuando intentas ser más sofisticado, mostramos un ejemplo que sacrifica la legibilidad y la mantenibilidad cuando hay una solución más simple. Primero, veamos un intento ingenuo de combinar DetailView con FormMixin para permitirnos enviar un formulario Django Form a la misma URL que estamos mostrando un objeto utilizando DetailView.

Usar FormMixin con DetailView

Piensa en nuestro ejemplo anterior de usar View y SingleObjectMixin juntos. Estábamos registrando el interés del usuario en un autor determinado; digamos ahora que queremos permitirles dejar un mensaje diciendo por qué les gustan. De nuevo, supongamos que no vamos a almacenar esto en una base de datos relacional sino en algo más esotérico que aquí no nos preocuparemos.

En este punto es natural llegar a un Form para encapsular la información enviada desde el navegador del usuario hasta Django. Digamos también que estamos muy comprometidos con REST, así que queremos usar la misma URL para mostrar al autor como para capturar el mensaje del usuario. Vamos a reescribir nuestro AuthorDetailView para hacerlo.

Seguiremos utilizando el manejo de GET de DetailView, aunque tendremos que agregar un Form en los datos de contexto para poder renderizarlo en la plantilla. También queremos aprovechar el procesamiento de formularios de FormMixin, y escribir un poco de código para que, al enviar POST, se llame a la forma correctamente.

Nota

Usamos FormMixin e implementamos post() nosotros mismos en lugar de intentar mezclar DetailView con FormView (que proporciona un post() adecuado ya que) porque ambas vistas implementan get(), y las cosas se volverían mucho más confusas.

Nuestra nueva vista AuthorDetailView se ve así:

# CAUTION: you almost certainly do not want to do this.
# It is provided as part of a discussion of problems you can
# run into when combining different generic class-based view
# functionality that is not designed to be used together.

from django import forms
from django.http import HttpResponseForbidden
from django.urls import reverse
from django.views.generic import DetailView
from django.views.generic.edit import FormMixin
from books.models import Author


class AuthorInterestForm(forms.Form):
    message = forms.CharField()


class AuthorDetailView(FormMixin, DetailView):
    model = Author
    form_class = AuthorInterestForm

    def get_success_url(self):
        return reverse("author-detail", kwargs={"pk": self.object.pk})

    def post(self, request, *args, **kwargs):
        if not request.user.is_authenticated:
            return HttpResponseForbidden()
        self.object = self.get_object()
        form = self.get_form()
        if form.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

    def form_valid(self, form):
        # Here, we would record the user's interest using the message
        # passed in form.cleaned_data['message']
        return super().form_valid(form)

get_success_url() proporciona un lugar a donde redirigirse, lo cual se utiliza en la implementación predeterminada de form_valid(). Tenemos que proporcionar nuestro propio post() como se indicó anteriormente.

Una solución mejor

El número de interacciones sutiles entre FormMixin y DetailView ya está poniendo a prueba nuestra capacidad para gestionar las cosas. Es poco probable que desee escribir este tipo de clase usted mismo.

En este caso, podría escribir el método post() usted mismo, manteniendo DetailView como la única funcionalidad genérica, aunque escribir código de manipulación de formularios con Form implica una gran cantidad de duplicado.

Alternativamente, aún sería menos trabajo que el enfoque anterior tener una vista separada para procesar el formulario, que podría utilizar FormView distinto de DetailView sin preocupaciones.

Una solución mejor alternativa

En realidad, lo que estamos tratando de hacer aquí es usar dos vistas basadas en clase diferentes desde la misma URL. ¿Por qué no hacer exactamente eso? Tenemos una división muy clara aquí: las solicitudes GET deben obtener DetailView (con el formulario Form agregado a los datos de contexto), y las solicitudes POST deben obtener FormView. Vamos a configurar esas vistas primero.

La vista AuthorDetailView es casi la misma que cuando introdujimos por primera vez AuthorDetailView; debemos escribir nuestro propio get_context_data() para hacer disponible el formulario AuthorInterestForm a la plantilla. Nos saltaremos la sobrescritura de get_object() del antes para claridad:

from django import forms
from django.views.generic import DetailView
from books.models import Author


class AuthorInterestForm(forms.Form):
    message = forms.CharField()


class AuthorDetailView(DetailView):
    model = Author

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["form"] = AuthorInterestForm()
        return context

Luego, la vista AuthorInterestFormView es una FormView, pero debemos traer SingleObjectMixin para poder encontrar al autor con el que estamos hablando, y debemos recordar establecer template_name para asegurarnos de que los errores del formulario se rendericen la misma plantilla que AuthorDetailView está utilizando en GET

from django.http import HttpResponseForbidden
from django.urls import reverse
from django.views.generic import FormView
from django.views.generic.detail import SingleObjectMixin


class AuthorInterestFormView(SingleObjectMixin, FormView):
    template_name = "books/author_detail.html"
    form_class = AuthorInterestForm
    model = Author

    def post(self, request, *args, **kwargs):
        if not request.user.is_authenticated:
            return HttpResponseForbidden()
        self.object = self.get_object()
        return super().post(request, *args, **kwargs)

    def get_success_url(self):
        return reverse("author-detail", kwargs={"pk": self.object.pk})

Finalmente, traemos esto juntos en una nueva vista AuthorView. Ya sabemos que llamar a as_view() sobre una vista basada en clase nos da algo que se comporta exactamente como una función basada en vistas, por lo que podemos hacer eso en el punto en el que elegimos entre las dos subvistas.

Puedes pasar argumentos de palabra clave a as_view() del mismo modo en que lo harías en tu URLconf, como si deseabas que el comportamiento AuthorInterestFormView también apareciera en otra URL pero utilizando una plantilla diferente:

from django.views import View


class AuthorView(View):
    def get(self, request, *args, **kwargs):
        view = AuthorDetailView.as_view()
        return view(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        view = AuthorInterestFormView.as_view()
        return view(request, *args, **kwargs)

Esta aproximación también se puede utilizar con cualquier otra clase de vistas basadas en clases generales o con tus propias vistas basadas en clases que hereden directamente de View o TemplateView, ya que mantiene las diferentes vistas lo más separadas posible.

Más que solo HTML

Donde las vistas basadas en clases brillan es cuando quieres hacer la misma cosa muchas veces. Supongamos que estás escribiendo una API, y cada vista debe devolver JSON en lugar de HTML renderizado.

Podemos crear una clase mixin para utilizar en todas nuestras vistas, manejando la conversión a JSON una vez.

Por ejemplo, un mixin de JSON podría verse algo así:

from django.http import JsonResponse


class JSONResponseMixin:
    """
    A mixin that can be used to render a JSON response.
    """

    def render_to_json_response(self, context, **response_kwargs):
        """
        Returns a JSON response, transforming 'context' to make the payload.
        """
        return JsonResponse(self.get_data(context), **response_kwargs)

    def get_data(self, context):
        """
        Returns an object that will be serialized as JSON by json.dumps().
        """
        # Note: This is *EXTREMELY* naive; in reality, you'll need
        # to do much more complex handling to ensure that arbitrary
        # objects -- such as Django model instances or querysets
        # -- can be serialized as JSON.
        return context

Nota

Consulte la documentación Serializando objetos Django para obtener más información sobre cómo transformar correctamente los modelos y conjuntos de consultas Django en JSON.

Esta clase mixin proporciona el método render_to_json_response() con la misma firma que render_to_response(). Para utilizarlo, debemos mezclarlo en una TemplateView por ejemplo, y sobrescribir render_to_response() para llamar a render_to_json_response() en su lugar:

from django.views.generic import TemplateView


class JSONView(JSONResponseMixin, TemplateView):
    def render_to_response(self, context, **response_kwargs):
        return self.render_to_json_response(context, **response_kwargs)

Igualmente podríamos utilizar nuestra clase mixin con alguna de las vistas generales. Podemos crear nuestra propia versión de DetailView mezclando JSONResponseMixin con la BaseDetailView – (la DetailView antes del comportamiento de renderizado de plantillas se ha mezclado):

from django.views.generic.detail import BaseDetailView


class JSONDetailView(JSONResponseMixin, BaseDetailView):
    def render_to_response(self, context, **response_kwargs):
        return self.render_to_json_response(context, **response_kwargs)

Esta vista luego puede ser desplegada de la misma manera que cualquier otra DetailView, con exactamente el mismo comportamiento – excepto por el formato de la respuesta.

Si quieres ser realmente aventurero, podrías incluso mezclar una subclase de DetailView capaz de devolver ambos contenido HTML y JSON, dependiendo de alguna propiedad de la solicitud HTTP, como un argumento de consulta o un encabezado HTTP. Mezcla en ambas el JSONResponseMixin y una SingleObjectTemplateResponseMixin, y sobrescribe la implementación del método render_to_response() para delegar en el método de renderizado apropiado dependiendo del tipo de respuesta que el usuario solicitó:

from django.views.generic.detail import SingleObjectTemplateResponseMixin


class HybridDetailView(
    JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView
):
    def render_to_response(self, context):
        # Look for a 'format=json' GET argument
        if self.request.GET.get("format") == "json":
            return self.render_to_json_response(context)
        else:
            return super().render_to_response(context)

Debido a la forma en que Python resuelve el sobrecargamiento de métodos, la llamada a super().render_to_response(context) termina llamando a la implementación de render_to_response() de TemplateResponseMixin.