Michał Strumecki asks:
I just want to filter select field in a form, regarding a currently logged user. Every user has own categories and budgets. I want to display only a models related with a currently logged user. I’ve tried stuff with filtering beforeis_valid
field, but with no result.
Answer
This is a very common use case when dealing with ModelForm
s. The problem is that in the form fields ModelChoice
and ModelMultipleChoiceField
, which are used respectively for the model fields ForeignKey
and ManyToManyField
,
it defaults the queryset to the Model.objects.all()
.
If the filtering was static, you could simply pass a filtered queryset in the form definition, like
Model.objects.filter(status='pending')
.
When the filtering parameter is dynamic, we need to do a few tweaks in the form to get the right queryset.
Let’s simplify the scenario a little bit. We have the Django User
model, a Category
model and Product
model. Now
let’s say it’s a multi-user application. And each user can only see the products they create, and naturally only use
the categories they own.
models.py
from django.contrib.auth.models import User
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=30)
user = models.ForeignKey(User, on_delete=models.CASCADE)
class Product(models.Model):
name = models.CharField(max_length=30)
price = models.DecimalField(decimal_places=2, max_digits=10)
category = models.ForeignKey(Category)
user = models.ForeignKey(User, on_delete=models.CASCADE)
Here is how we can create a ModelForm
for the Product
model, using only the currently logged-in user:
forms.py
from django import forms
from .models import Category, Product
class ProductForm(forms.ModelForm):
class Meta:
model = Product
fields = ('name', 'price', 'category', )
def __init__(self, user, *args, **kwargs):
super(ProductForm, self).__init__(*args, **kwargs)
self.fields['category'].queryset = Category.objects.filter(user=user)
That means now the ProductForm
has a mandatory parameter in its constructor. So, instead of initializing the form
as form = ProductForm()
, you need to pass a user instance: form = ProductForm(user)
.
Here is a working example of view handling this form:
views.py
from django.shortcuts import render, redirect
from .forms import ProductForm
@login_required
def new_product(request):
if request.method == 'POST':
form = ProductForm(request.user, request.POST)
if form.is_valid():
product = form.save(commit=False)
product.user = request.user
product.save()
return redirect('products_list')
else:
form = ProductForm(request.user)
return render(request, 'products/product_form.html', {'form': form})
Using ModelFormSet
The machinery behind the modelformset_factory
is not very flexible, so we can’t add extra parameters in the
form constructor. But we certainly can play with the available resources.
The difference here is that we will need to change the queryset on the fly.
Here is what we can do:
models.py
@login_required
def edit_all_products(request):
ProductFormSet = modelformset_factory(Product, fields=('name', 'price', 'category'), extra=0)
data = request.POST or None
formset = ProductFormSet(data=data, queryset=Product.objects.filter(user=request.user))
for form in formset:
form.fields['category'].queryset = Category.objects.filter(user=request.user)
if request.method == 'POST' and formset.is_valid():
formset.save()
return redirect('products_list')
return render(request, 'products/products_formset.html', {'formset': formset})
The idea here is to provide a screen where the user can edit all his products at once. The product form involves handling a list of categories. So for each form in the formset, we need to override the queryset with the proper list of values.
The big difference here is that each of the categories list is filtered by the categories of the logged in user.
Get the Code
I prepared a very detailed example you can explore to get more insights.
The code is available on GitHub: github.com/sibtc/askvitor.