Photo by Photoholgic on Unsplash

Django Tricks: How to pass querysets from a View to a Form

Adrien Van Thong

--

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!

--

--