Chris Shaw

Chris Shaw

Lead Profile All Articles

I am an ardent software developer and eager lifelong learner of code. My passion is the python language with particular emphasis in the 'django' and  'opencv' packages. I have created this blog to share my e... Read More

Tutorials


Django Forms, Overriding the queryset on a select field to exclude options already used.

May 6, 2019 Chris Shaw 4552 views

In a Django form, It is posibble to override the queryset that produces the results displayed in a 'select field' from a foregin key. In this tutorial, we will pass valuse from the view to the form to build the queryset.

Normally in our model forms, the foreign key is simply displayed as a select field, containing all the values from the foreign table. But what if this list is long and cumbersome. We can modify the queryset to only display results based on our criteria, such as active, belonging to a user or not yet selected.

I this tutorial, I imagine a situation where we have multiple products and multiple price lists. Products can be assigned to price lists with a price. Each product can only be assigned to each price list once. So once the product has been assigned to the price list, I do not want that product to appear in the select field anymore.

Starting with or models.py, we will create 3 tables for: The products, The price lists and Assigning the product to the price lists. This is mostly self explanatory and will not go into a detailed explanation here.

from django.db import models
from django.utils.translation import ugettext_lazy as _


"""Our products available"""
class Product(models.Model):
    title = models.CharField(_('Title'), max_length=64, unique=True)

    def __str__(self):
        return self.title


"""The pricelists"""
class Pricelist(models.Model):
    title = models.CharField(_('Title'), max_length=64, unique=True)

    def __str__(self):
        return self.title


"""Assign products to priceslists"""
class PricelistProduct(models.Model):
    pricelist = models.ForeignKey(Pricelist, on_delete=models.CASCADE)
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    price = models.DecimalField(_('Price'), max_digits=8, decimal_places=2)

    class Meta:
        unique_together = ("pricelist", "product")

    def __str__(self):
        return '{} - {}'.format(self.pricelist, self.product)

To focus on the tutorial, I am not going to show my urls.py or the full views.py. You can view them on the github page if you are interested. In our views.py, we write the view that will display a list of the product and prices on a price list as well as a form to add more products. Here we are using the generic createview. I am selecting the pricelist using a pk in the url. When we come to the form, I will hide the pricelist select field. The key point here is def get_initial(self):. Here we are setting the initial value of the pricelist field from the pk. This is what is sent to the form and the template, so we can use this information in the form.

"""Add our products to a pricelist"""
class PricelistProductView(CreateView):
    template_name = 't190506/pricelistproduct.html'
    model = PricelistProduct
    form_class = PricelistProductForm
    pricelist = None

    # Set the success url to be the current url so the same page is loaded after submission.
    def get_success_url(self):
        return self.request.path

    # Set the pricelist form field from the pricelist pk
    def get_initial(self):
        self.pricelist = get_object_or_404(Pricelist, pk=self.kwargs.get('pk'))
        return {
            'pricelist':self.pricelist,
        }

    #Add pricelist data to the context
    def get_context_data(self, *args, **kwargs):
        context = super(PricelistProductView, self).get_context_data(*args, **kwargs)
        context['pricelist'] = self.pricelist
        return context

So let us create the form. I have already mentioned that I wat to hide the pricelist field and this is done by setting it's widget to a Hidden Input. This way, the field is not available to the user to edit, but is still submitted.

Having access to the pricelist, means that I can get the products already assigned to the pricelist and exclude them from the products available in the product select field. We set the custom queryset in def __init__(self, *args, **kwargs):. Firstly, we get the the pricelist from the initial values. We then get the existing product i the form of a value list as we are only interested in the id's of the products. Finally, we set the queryset on the field to return the products, excluding the items in our existing queryset.

from django import forms

from .models import Product, PricelistProduct


"""Pricelist Product create"""
class PricelistProductForm(forms.ModelForm):

    class Meta:
        model = PricelistProduct
        fields = '__all__'
        widgets = {
            'pricelist': forms.HiddenInput,
        }

    def __init__(self, *args, **kwargs):
        super(PricelistProductForm, self).__init__(*args, **kwargs)

        # Get the inital values set in the view
        inital = kwargs.get('initial')

        # Get the pricelist we are using
        pricelist = inital.get('pricelist')

        # Get a 'value list' of products already in the price list
        existing = PricelistProduct.objects.filter(pricelist=pricelist).values_list('product')

        # Override the product query set with a list of product excluding those already in the pricelist
        self.fields['product'].queryset = Product.objects.exclude(id__in=existing)

Lastly, we create a simple template to display the products assigned to the pricelist as well as the form to add more products.

# templates/t190506/pricelistproduct.html
{% extends 'base.html' %}

{% load static %}

{% block title %}{{ block.super }} | Add Products to Pricelist{% endblock %}

{% block content %}
<h2>Pricelist: {{ pricelist }}</h2>
<ul>
{% for product in pricelist.pricelistproduct_set.all %}
  <li>
    {{forloop.counter}}.
    <strong>Product</strong>: {{ product.product }},
    <strong>Price</strong>: {{ product.price }}
  </li>
{% empty %}
  <li><strong>No products added yet.</strong></li>
{% endfor %}
</ul>

<h3>Add Product</h3>
<form method="post" enctype="multipart/form-data">
  {% csrf_token %}
  {{ form }}
  <input type="submit" value="Submit">
</form>
{% endblock %}

Conclusion:

And that's it. Our select field will only display items not already selected. I struggled to find information to achieve what I was trying to do, so hopefully this will be a help to other developers. The full code is available on github.

You can expand this further, for example;

  • If your products had a boolean field active, you could only display active products.
  • If your products had an user field such as creator, you could send the current user from the view to the form and only display items created by the current user.

blog comments powered by Disqus