Pre-populate Django ModelForm with Specific Queryset

I just had a situation where I was trying to filter the queryset for a ModelMultipleChoiceField based on the currently logged-on user.  I was going crazy trawling through the Django docs and eventually Google.  It seemed like something which should be so simple, but there was no obvious way to do it.  Eventually I found the answer, and it IS simple!

As an example, let’s say you have the following two models as part of a simple photo gallery app:

?View Code PYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Photo(Model):
    name = CharField(max_length=100)
    caption = CharField(max_length = 150)
    user = ForeignKey(User)
    upload_timestamp = DateTimeField(auto_now_add=True)
    image = ImageField(upload_to="user_images/%Y/%m/%d/")
    thumbnail = ImageField(upload_to="user_images/%Y/%m/%d")
    def __unicode__(self):
        return self.name
 
class Album(Model):
    name = CharField(max_length=100)
    caption = CharField(max_length = 150)
    user = ForeignKey(User)
    photos = ManyToManyField(Photo, related_name="albums", blank=True)
    creation_timestamp = DateTimeField(auto_now_add=True)
    cover_photo = ForeignKey(Photo, related_name="cover_photos")
    def __unicode__(self):
        return self.name

We then define a ModelForm based on the Album model, which allows users to create albums with photos they’ve previously uploaded (pretend we’ve already made that possible). We only expose the “name”, “caption” and “photos” fields because we’ll fill in the others automatically as part of our view:

?View Code PYTHON
1
2
3
4
class AlbumCreationForm(ModelForm):
    class Meta:
        model = Album
        fields = ("name", "caption", "photos")

Now here’s the real magic. Ordinarily, when first showing the form (pre-POST) we would create it like this and pass it to the template:

?View Code PYTHON
form = AlbumCreationForm()

The problem here is that by default we’ll get all photo objects, i.e. the result of “Photo.objects.all()”. That’s a problem because in this case we just want to list the photos belonging to the current user. To do this, just add the following line:

?View Code PYTHON
form.fields["photos"].queryset = Photo.objects.filter(user=request.user)

It turns out that form fields can be accessed as a dictionary attached to the form instance, and that if the field is model-related, like “photos” in the example, you can update its queryset dynamically.

Here’s a partial view which uses the last two code samples, to provide some context:

?View Code PYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
def create_album(request):
    if request.method == "POST":
        # Process form.
    else:
        form = AlbumCreationForm()
        # Get just the photos belonging to this user.
        form.fields["photos"].queryset = Photo.objects.filter(user=request.user)
 
    template_vars = RequestContext(request, {
        "form": form,
        "user": request.user
    })
    return render_to_response("create_album.html", template_vars)

That’s it! Adding the one extra line to the view gives us the filtering we need.

9 responses to Pre-populate Django ModelForm with Specific Queryset

Thanks for posting this! I’ve been googling for the last half hour to find the right way to do this.

One thing to make clear though: this method doesn’t do any validation to stop someone from saving another user’s photo to his own album, so you need to double check that when you process the form to stop people from hijacking others’ photos with malformed POSTs. Unless I’m missing something?

Thanks Brent, I’m glad it helped.

The code in this post is purely a rough example to illustrate the point so there is certainly a lot that should be added to it before it is production-ready.

I believe if you add a custom queryset in the same way when processing the form in the if request.method == "POST": block, then validation should prevent saving another user’s photo.

After stumbling across numerous blog posts, the not-so-specific-on-that-topic-django-docs and a good amount of StackOverlow posts I finally found your brilliant post.

Kudos sir, you made my day and helped to restore a good bunch of faith in Django.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.