2

My current project has me building a page where the output can be filtered by category. Meaning by default you get a full list of items but you can narrow the output by clicking on ( multiple or a single) check boxes. The idea would be if you only want to see the results from two categories you can choose those two and just see items from those two categories. (think like an Amazon page where you can can search for an item but filter the results)

This works through a form using the get method. So the url will look something like:

ip/?family=category_one&family=category_two

My code is as below:

Please note the Firmware model is related to ProductModel through a ManyToManyField and ProductModel is related to Family through a ForeignKey relation

*models

class Family(models.Model):
    family_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    slug = models.SlugField(null=False, unique=True)
    family_name = models.CharField(max_length=50, editable=True, unique=True)

class ProductModel(models.Model):
    model_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    model_number = models.CharField(max_length=25, editable=True, unique=True)
    family_id = models.ForeignKey(Family, on_delete=models.DO_NOTHING, editable=True, null=False, blank=False)

class Firmware(models.Model):
    firmware_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    version = models.CharField(max_length=50, help_text='The \'version number\' of the firmware',
                               editable=True)
    model_id = models.ManyToManyField(ProductModel, editable=True)

*View:

class FirmwareListView(ListView):
    model = Firmware
    context_object_name = 'firmwares'
    qs = QuerySet()

    def get(self, request, *args, **kwargs):
        requested_families = request.GET.getlist("family")
        if requested_families:
            query = Q(model_id__family_id__family_name__iexact=requested_families[0])
            for f in range(1, len(requested_families)):
                query = query | Q(model_id__family_id__family_name__iexact=requested_families[f])
            self.qs = Firmware.objects.filter(query)
        else:
            self.qs = Firmware.objects.all()
        return super().get(request, *args, **kwargs)

    def get_context_data(self, *, object_list=None, **kwargs):
        context = super().get_context_data()
        context['firmware_list'] = self.qs
        return context

The most important parts here are:

        requested_families = request.GET.getlist("family")
        if requested_families:
            query = Q(model_id__family_id__family_name__iexact=requested_families[0])
            for f in range(1, len(requested_families)):
                query = query | Q(model_id__family_id__family_name__iexact=requested_families[f])
            self.qs = Firmware.objects.filter(query)

This query will work (in that it runs and returns results but the results are wrong. The results seem to be as if they ignore the exact requirement and only run a contains query. In other words, if I filter by the category foo I will get results for food and foo. If I run the query by doing the below:

self.qs = Firmware.objects.filter(Q(model_id__family_id__family_name__iexact=requested_families[0]) | Q(model_id__family_id__family_name__iexact=requested_families[1]))

and I choose two categories the result is perfect. Of course I cannot use the above method in production since I will never know how many categories a user will choose.

I have seen this post ( Django query with variable number of filter arguments ) and tried all of the suggestions there with no luck.

I believe the issue is I am trying to filter on a field from a model that runs through a many to many field then a foreign key. There is no way to get around this (that I can think of at least)

Any help offered is greatly appreciated. I am a bit stuck on this one but there must be a way to do it. For now the only method I can think of is to perform a separate query on each term and then combine the results at the end.

EFiore
  • 105
  • 1
  • 9

2 Answers2

1

Figured this one out after much effort. Well I caused my own problem. First I as passing my query result through another method (I did not mention it in my original question since I thought it was irrelevant) which changed the query.

This second method sorted the output in a unique way. When I started to look closer at that method I noticed the queryset changed all of sudden in the middle of it. I found where I made the mistake and voila! It works fine now.

Bottom line is the method of creating a variable amount of query parameters using the aforementioned method (and re-shown below) work perfect.

To make a variable number of query parameters in Django you can use:

class ViewName():
    qs = QuerySet()

    def get(self, request, *args, **kwargs):
        get_queries = request.GET.getlist('named_get')
        if get_queries:
            query = Q(query_type=get_queries[0])
                for t in range(1, len(filtered_types)):
                    query = query | Q(query_type=get_queries[t]) #obviously if you need an and here just substitute in & where | currently is
            Model.objects.filter(query)
EFiore
  • 105
  • 1
  • 9
0

I had to do something similar. I ended up using a queryset that chained .exclude()'s to those not selected. I used a little javascript in the form to pass the right (those I didn't want) values.

Michael
  • 102
  • 2
  • 10
  • hmmm, interesting solutions. Thanks. I think I'm going to go with a different filtering system. That is not filtering by family but by the ProductModel model. – EFiore May 31 '20 at 23:32