Django Tricks: How to pass querysets from a View to a Form
In a previous article, I showcased how to create a bulk edit form in Django using class based views.
Recently, the use case came up where I needed to change the queryset for the FormSet such that it could filter based on the currently logged in user.
The problem I then ran into is that the information about the currently logged in user is available within the View (via self.request
), but the Form
and FormSet
classes have no such access to this information.
Previous Example
Recall what our code structure looked like in the previous article — we were creating a Django app which helped summer camp administrators keep track of their student cohorts and each student’s favorite activities.
from django.views.generic import DetailView, UpdateView
from django import forms
from django.urls import reverse_lazy
from .models import Cohort, Student
class StudentForm(forms.ModelForm):
class Meta:
model = Student
fields = ['fav_snack', 'fav_color', 'fav_activity', 'ride']
class StudentFormSet(forms.BaseInlineFormSet):
def get_queryset(self):
return super().get_queryset().filter(active=True)
class CohortBulkUpdateView(UpdateView):
template_name = 'cohort_bulk_edit.html'
model = Cohort
form_class = forms.inlineformset_factory(Cohort, Student, StudentForm, formset=StudentFormSet, extra=0)
slug_field = 'id'
slug_url_kwarg = 'id'
def get_success_url(self):
return reverse_lazy('cohort-detail', kwargs={'id': self.get_object().id})
The CohortBulkUpdateView
served as an UpdateView
which allowed the user to bulk update multiple Student
records at once, for all active students in the same Cohort
. The StudentForm
defined which fields were editable for each student, while the StudentFormSet
defined the queryset to filter which students would show up on the form (i.e. only the active ones).
What if we wanted to alter our logic in the StudentFormSet
to only include students who have the current logged in user as their chaperone? We run into a dilemma as neither the FormSet
nor the Form
classes have the ability to determine who is currently logged in.
Solution
To solve this, we’ll need to generate the QuerySet
within the View, which does have the context about the currently logged in user, then create a means to pass that queryset over to the FormSet
class.
First, let’s update the constructor for the FormSet to take in a new kwarg queryset
, which we then use to overwrite the FormSet’s current queryset:
class StudentFormSet(forms.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
self._queryset = kwargs.get('queryset', None)
if 'queryset' in kwargs:
del kwargs['queryset']
super().__init__(*args, **kwargs)
self._queryset
is where Django’s FormSet
classes store their querysets internally. Regardless of what it was set to before, this FormSet
is now using whatever was passed in via kwarg
as the new queryset.
Notice how we take care to delete the queryset
entry from the kwargs
dict before we call the super’s __init__
method — we do this because super
isn’t expecting this kwarg
and doesn’t know how to handle it.
Next, let’s update our CohortBulkUpdateView
to insert in its own queryset to the FormSet’s kwargs
when it is instantiated, using our modified constructor on the FormSet class:
class CohortBulkUpdateView(UpdateView):
template_name = 'cohort_bulk_edit.html'
model = Cohort
form_class = forms.inlineformset_factory(Cohort, Student, StudentForm, formset=StudentFormSet, extra=0)
slug_field = 'id'
slug_url_kwarg = 'id'
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
# Override the default queryset for the results to only show the ones assigned to current user:
students = self.get_object().student_set.filter(chaperone=self.request.user)
kwargs['queryset'] = students
return kwargs
We choose to overwrite the get_form_kwargs
method because this is the method tasked with instantiating the FormSet
object and thus passing in the appropriate kwargs
to its constructor . Inside this method, we first get the current dict of kwargs
, then modify this dict by inserting a new entry for queryset
, which filters only for Student
records that have self.request.user
(i.e. the currently logged in user) as their chaperone. Finally, we return the modified kwargs
dict so the framework can pass it in to the FormSet
object when instantiating it.
And voila! Our queryset was generated within the View, using information from self.request
, then passed along to the FormSet
. A bit of a workaround, but it gives us a semi-elegant solution without adding too much code or complexity.
One variation on this solution would be to pass in just the User
object or user’s pk
to the FormSet
via the same mechanism, and override the get_queryset
method to write a new QuerySet
purely within the FormSet
which would leverage that extra information passed in via the constructor.
Do you have any alternative solutions for this? Sound off in the comments!