skip to content

How do you combine Django's SingleObjectMixin with ListView to paginate one podcast's episodes, and which pitfalls must you handle?

level: seniorimportance: nice to knowfreq 22%

answer

  1. two querysets, two models
  2. set self.object inside get()
  3. pass queryset to get_object()
  4. context names collide

basics

~10 s

Subclass SingleObjectMixin and ListView, set self.object = self.get_object(queryset=Podcast.objects.all()) in get() before super().get(), return that podcast's episodes from get_queryset(), and add the podcast to the context yourself.

solid answer

~30 s

`class PodcastEpisodesView(SingleObjectMixin, ListView)` gets `ListView`'s pagination for the episodes and `SingleObjectMixin`'s lookup for the podcast. Both mixins define `get_queryset()`, and you override it to return `self.object.episodes.all()`, so `get_object()` must be given `queryset=Podcast.objects.all()` explicitly or it would search episodes. `self.object` must be set in `get()` before `super().get()`, because `BaseListView.get()` calls `get_queryset()` straight away. Both mixins also claim `context_object_name`, so leave it unset and add `context['podcast'] = self.object` in `get_context_data()`; the episodes arrive as `page_obj` and `object_list`, not `episode_list`. The template defaults to the episode list template. It works, but the Django docs themselves advise keeping each view within one generic group.

code

python · 21 lines
python
from django.views.generic import ListView
from django.views.generic.detail import SingleObjectMixin

from .models import Podcast


class PodcastEpisodesView(SingleObjectMixin, ListView):
    paginate_by = 20
    template_name = 'podcasts/podcast_episodes.html'

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

    def get_queryset(self):
        return self.object.episodes.order_by('-published_at')

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

go deeper

for a junior

Recall that ListView can be combined with SingleObjectMixin to list objects that belong to one parent object.

for a middle

Explain the recipe: set self.object in get() via get_object(queryset=...), return the children from get_queryset(), add the parent in get_context_data().

for a senior

Name each clash (one get_queryset for two models, ordering in BaseListView.get, the shared context name) and when to prefer a plain ListView instead.

for a principal

Treat cross-group mixin combinations as a code smell in review; the docs' one-group rule keeps views readable for the next maintainer.

## The goal A podcast page shows the podcast's details and a paginated list of its episodes. `ListView` brings pagination; `SingleObjectMixin` brings the URL-based lookup of one object. The Django documentation shows this combination, and it is a favourite interview probe because every hook clashes once. ## The two querysets problem Both `SingleObjectMixin` and `MultipleObjectMixin` (inside `ListView`) use `get_queryset()`: | Needs a queryset | For | Model | |---|---|---| | `SingleObjectMixin.get_object()` | finding the podcast from the URL | `Podcast` | | `BaseListView.get()` | the list to paginate | `Episode` | There is only one `get_queryset()` on the class, and you override it to return episodes. So `get_object()` must be called with an explicit `queryset=Podcast.objects.all()`; otherwise it would look for an episode with the podcast's pk or slug. ## The ordering problem `BaseListView.get()` starts with `self.object_list = self.get_queryset()`. Your `get_queryset()` reads `self.object`, so the podcast has to be loaded first: 1. Override `get()`. 2. Set `self.object = self.get_object(queryset=Podcast.objects.all())`. 3. Call `super().get(request, *args, **kwargs)`, which builds the list, paginates it and renders. Setting `self.object` anywhere later (in `get_context_data()`, say) is too late. ## The context-name problem Both mixins implement `get_context_object_name()` and both read `context_object_name`: - With `SingleObjectMixin` first in the bases, its version of `get_context_object_name()` wins for both calls. For the list, it receives a queryset, which is not a model instance, and returns `None`, so **no `episode_list` key is added**. - The episodes are still available as `object_list` and, when paginating, `page_obj`. - Setting `context_object_name` would make both mixins write the same key. Leave it unset and add `context['podcast'] = self.object` yourself after calling `super().get_context_data(**kwargs)`, which also keeps `paginator` and `page_obj`. ## The template problem `ListView` knows nothing about podcasts. Its template naming uses the list's model, so the default template is the episode list template in the episodes' app. Set `template_name` explicitly if the page is really about the podcast. ## Checking it works Because every clash in this view is silent, a short test pins the behaviour down: - request the podcast's URL and assert the response uses the intended template and has `podcast` in its context; - assert that `page_obj` holds only that podcast's episodes, and that a second page exists when there are more episodes than `paginate_by`; - request an unknown slug or pk and assert a 404, which proves `get_object()` searched podcasts rather than episodes. If someone later sets `context_object_name`, reorders the bases, or moves the `self.object` assignment, one of these assertions fails instead of a template quietly rendering an empty list. ## When not to do this The Django docs close the section with a hint: use mixins from one group of generics per view (detail, list, editing or date), because combining a single-object mixin with a multiple-object mixin gets complex quickly. Simpler alternatives: - a plain `ListView` of episodes whose `get_queryset()` looks up the podcast with `get_object_or_404()` and filters by it, adding the podcast to the context; - a `DetailView` of the podcast that paginates the episodes itself in `get_context_data()`. Both avoid the double `get_queryset()` and the context-name collision, at the cost of a few explicit lines.

  • Why is episode_list missing from the context in this combined view?
    `SingleObjectMixin` is first in the bases, so its `get_context_object_name()` answers for the list too. Given a queryset rather than a model instance, it returns `None`, and `MultipleObjectMixin` skips the named key. Use `page_obj` or `object_list` in the template.
  • What simpler design gives the same page without mixing generic groups?
    A plain `ListView` of episodes: its `get_queryset()` fetches the podcast with `get_object_or_404()`, stores it on `self` and filters episodes by it, and `get_context_data()` adds the podcast. One queryset hook, one context name, same pagination.

saying these in an interview costs you the question

  • get_object() automatically uses the podcast model despite the overridden get_queryset
  • self.object can be set later in get_context_data()
  • The episodes will be in the context as episode_list
  • Setting context_object_name = 'podcast' cleanly separates the two
  • Combining any two generic mixins is always safe