djangodjango-templatesdjango-filters

How to create filter form page with Django filters with object count per category shown in the option labels


I am new to Django and trying to create a page that shows a list of products with couple of filters. The page works fine using standard method and shows filter and update the page as selected. What I want to do is to add a count of products available based on the seelcted filters in; in this case just for category.

models.py

class Category(index.Indexed, models.Model):
    name = models.CharField(max_length=1024)

    class Meta:
        verbose_name = "Category"
        verbose_name_plural = "Categories"

class Product(models.Model):
    title = models.CharField(max_length=1024, verbose_name="Product title")
    last_modified = models.DateTimeField(null=True, blank=True)
    category = models.ForeignKey("Category", on_delete=models.SET_NULL, null=True, blank=True)

filters.py


class ProductFilter(django_filters.FilterSet):
    category = django_filters.ModelChoiceFilter(
        queryset=Category.objects.annotate(num_product=Count("product"))
    ) 
    last_modified = django_filters.DateRangeFilter(label="Date_Range")

    class Meta:
        model = Product
        fields = ["category", "last_modified"]

views.py


def product_explorer_view(request):
    products = Product.objects.all()
    product_filter = ProductFilter(request.GET, queryset=products)
    products = product_filter.qs

    context = {
        "products": products,
        "product_filter": product_filter,
      
    } 
    return render(request, "/products_index.html", context)

products_index.html

  <form class="mb-1">
        <div class="row g-2">
          <div class="col-md-2 col-sm-4">
            Category:
            <div class="form-floating">
              <!-- <label for="searchTerm" class="form-label">Category</label> -->
              {{ product_filter.form.category }}
            </div>
          </div>
          <div class="col-md-2 col-sm-4">
            Date:
            <div class="form-floating">
              <!-- <label for="searchTerm" class="form-label">Category</label> -->
              {{ chart_filter.form.last_modified }}
            </div>
          </div>
          <div class="col-md-1 col-sm-4 pt-1">
            </br>

            <button class="btn btn-secondary btn-lg" type="submit">Search</button>
          </div>
        </div>
        Number of available products: {{ products|length }}
      </form>

Right now this Category option field in the form is filled as below. Output on page

<th><label for="id_category">Category:</label></th>

<td><select name="category" id="id_category">

<option value="">---------</option>
<option value="11" selected>Category1</option>
<option value="12">Category2</option>
<option value="13">Category3</option>
<option value="14">Category4</option>
<option value="16">Category5</option>
<option value="17">Category6</option>
</select></td>

What I want is to get

<th><label for="id_category">Category:</label></th>
<td>

<select name="category" id="id_category">

<option value="">---------</option>
<option value="11" selected>Category1 (3)</option>
<option value="12">Category2 (5)</option>
<option value="13">Category3 (7)</option>
<option value="14">Category4 (18)</option>
<option value="16">Category5 (20)</option>
<option value="17">Category6 (0)</option>
</select></td>

The added annotation in the filters.py add that extra variable in the queryset, but it does not help include that in the <option> labels.

Is there some configuration/parameter to change that labels? Or in which other way can I make that other than manually building the select options and then linking it back to product filters?

I assume there must be some Django way of doing it, as it is/should be quite a standard requirement.


Solution

  • Attempt #2 :)

    By overriding the form @property you can override the labels of the select

    class ProductFilter(django_filters.FilterSet):
        category = django_filters.ModelChoiceFilter(
            queryset=Category.objects.annotate(num_product=Count("product"))
        )
        last_modified = django_filters.DateRangeFilter(label="Date_Range")
    
        class Meta:
            model = Product
            fields = ["category", "last_modified"]
    
        @property
        def form(self):
            if not hasattr(self, "_form"):
                Form = self.get_form_class()
                if self.is_bound:
                    self._form = Form(self.data, prefix=self.form_prefix)
                else:
                    self._form = Form(prefix=self.form_prefix)
                # This line
                self._form.fields['category'].label_from_instance = lambda obj: f'{obj.name} ({obj.num_product})'
            # <- we DO NOT place it here, else it runs 3 times,
            #  we want only during the init!
            return self._form
    

    Thought process of finding this out

    1. Pull django-filter from Github
    2. Find FilterSet Object, CTRL-SHIFT-F, it's inside django_filters/filterset.py
    3. Look Look for any form init or field overrides
      • found def form(self)
    4. Paste entire block into ProductFilter, Overriding default.
    5. Start print()-ing stuff out
      • vars() is handy

    Structure (trial and error):

    print(self._form.fields)
    # {
    #     'category': <django_filters.fields.ModelChoiceField object at 0x7fd960c26890>,
    #     'last_modified': <django_filters.fields.ChoiceField object at 0x7fd960c269b0>
    # }
    
    1. Google django override modelchoicefield display
    2. Choose the lambda answer because i'm not creating a custom ModelChoiceField