Search code examples
pythondjangodjango-filterdjango-filters

Associate classes with django-filters


Bonjour, I have a question regarding django-filters. My problem is: I have two classes defined in my models.py that are:

class Volcano(models.Model):
    vd_id = models.AutoField("ID, Volcano Identifier (Index)",
                         primary_key=True)
    [...]

class VolcanoInformation(models.Model):

    # Primary key
    vd_inf_id = models.AutoField("ID, volcano information identifier (index)",
                             primary_key=True)

    # Other attributes
    vd_inf_numcal = models.IntegerField("Number of calderas")
    [...]

    # Foreign key(s)
    vd_id = models.ForeignKey(Volcano, null=True, related_name='vd_inf_vd_id',
                          on_delete=models.CASCADE)

The two of them are linked throught the vd_id attribute.

I want to develop a search tool that allows the user to search a volcano by its number of calderas (vd_inf_numcal).

I am using django-filters and for now here's my filters.py:

from .models import *
import django_filters

class VolcanoFilter(django_filters.FilterSet):

    vd_name = django_filters.ModelChoiceFilter(
                                      queryset=Volcano.objects.values_list('vd_name', flat=True),
                                 widget=forms.Select, label='Volcano name',
                                 to_field_name='vd_name',
                                 )

    vd_inf_numcal = django_filters.ModelChoiceFilter(
                                   queryset=VolcanoInformation.objects.values_list('vd_inf_numcal', flat=True),
                                 widget=forms.Select, label='Number of calderas',
                                 )


    class Meta:
        model = Volcano
        fields = ['vd_name', 'vd_inf_numcal']

My views.py is:

def search(request):
    feature_list = Volcano.objects.all()
    feature_filter = VolcanoFilter(request.GET, queryset = feature_list)

    return render(request, 'app/search_list.html', {'filter' : feature_filter, 'feature_type': feature_type})

In my application, a dropdown list of the possible number of calderas appears but the search returns no result which is normal because there is no relation between VolcanoInformation.vd_inf_numcal, VolcanoInformation.vd_id and Volcano.vd_id.

It even says "Select a valid choice. That choice is not one of the available choices."

My question is how could I make this link using django_filters ? I guess I should write some method within the class but I have absolutely no idea on how to do it.

If anyone had the answer, I would be more than thankful !


Solution

  • In general, you need to answer two questions:

    • What field are we querying against & what query/lookup expressions need to be generated.
    • What kinds of values should we be filtering with.

    These answers are essentially the left hand and right hand side of your .filter() call.

    In this case, you're filtering across the reverse side of the Volcano-Volcano Information relationship (vd_inf_vd_id), against the number of calderas (vd_inf_numcal) for a Volcano. Additionally, you want an exact match.

    For the values, you'll need a set of choices containing integers.

    AllValuesFilter will look at the DB column and generate the choices from the column values. However, the downside is that the choices will not include any missing values, which look weird when rendered. You could either adapt this field, or use a plain ChoiceFilter, generating the values yourself.

    def num_calderas_choices():
        # Get the maximum number of calderas
        max_count = VolcanoInformation.objects.aggregate(result=Max('vd_inf_numcal'))['result']
    
        # Generate a list of two-tuples for the select dropdown, from 0 to max_count
        # e.g, [(0, 0), (1, 1), (2, 2), ...]
        return zip(range(max_count), range(max_count))
    
    class VolcanoFilter(django_filters.FilterSet):
        name = ...
        num_calderas = django_filters.ChoiceFilter(
            # related field traversal (note the connecting '__')
            field_name='vd_inf_vd_id__vd_inf_numcal',
            label='Number of calderas',
            choices=num_calderas_choices
        )
    
    class Meta:
        model = Volcano
        fields = ['name', 'num_calderas']
    

    Note that I haven't tested the above code myself, but it should be close enough to get you started.