0

I'm using Django filters (django-filter) in my project. I have the models below, where a composition (Work) has a many-to-many instrumentations field with a through model. Each instrumentation has several instruments within it.

models.py:

class Work(models.Model):

    instrumentations = models.ManyToManyField(Instrument,
        through='Instrumentation',
        blank=True)

class Instrument(models.Model):

    name = models.CharField(max_length=100)

class Instrumentation(models.Model):

    players = models.IntegerField(validators=[MinValueValidator(1)])
    work = models.ForeignKey(Work, on_delete=models.CASCADE)
    instrument = models.ForeignKey(Instrument, on_delete=models.CASCADE)

views.py:

import django_filters

class WorkFilter(django_filters.FilterSet):

    instrument = django_filters.ModelMultipleChoiceFilter(
        field_name="instrumentation__instrument",
        queryset=Instrument.objects.all())

My filter works fine: it grabs all the pieces where there is the instrument selected by the user in the filter form.

However, I'd like to add the possibility of filtering the compositions with those exact instruments. For instance, if a piece contains violin, horn and cello and nothing else, I'd like to get that, but not a piece written for violin, horn, cello, and percussion. Is it possible to achieve that?

I'd also like the user to choose, from the interface, whether to perform an exact search or not, but that's a secondary issue for now, I suppose.


Update: type_of_search using ChoiceFilter

I made some progress; with the code below, I can give the user a choice between the two kinds of search. Now, I need to find which query would grab only the compositions with that exact set of instruments.

class WorkFilter(django_filters.FilterSet):

    # ...

    CHOICES = {
        ('exact', 'exact'), ('not_exact', 'not_exact')
    }

    type_of_search = django_filters.ChoiceFilter(label="Exact match?", choices=CHOICES, method="filter_instruments")

    def filter_instruments(self, queryset, name, value):
        if value == 'exact':
            return queryset.??
        elif value == 'not_exact':
            return queryset.??

I know that the query I want is something like:

Work.objects.filter(instrumentations__name='violin').filter(instrumentations__name='viola').filter(instrumentations__name='horn')

I just don't know how to 'translate' it into the django_filters language.


Update 2: 'exact' query using QuerySet.annotate

Thanks to this question, I think this is the query I'm looking for:

from django.db.models import Count

instrument_list = ['...'] # How do I grab them from the form?
instruments_query = Work.objects.annotate(count=Count('instrumentations__name')).filter(count=len(instrument_list))

for instrument in instrument_list:
    instruments_query = instruments_query.filter(instrumentations__name=instrument_list)

I feel I'm close, I just don't know how to integrate this with django_filters.


Update 3: WorkFilter that returns empty if the search is exact

class WorkFilter(django_filters.FilterSet):

    genre = django_filters.ModelChoiceFilter(
        queryset=Genre.objects.all(),
        label="Filter by genre")

    instrument = django_filters.ModelMultipleChoiceFilter(
        field_name="instrumentation__instrument",
        queryset=Instrument.objects.all(),
        label="Filter by instrument")

    CHOICES = {
        ('exact', 'exact'), ('not_exact', 'not_exact')
    }

    type_of_search = django_filters.ChoiceFilter(label="Exact match?", choices=CHOICES, method="filter_instruments")

    def filter_instruments(self, queryset, name, value):
        instrument_list = self.data.getlist('instrumentation__instrument')

        if value == 'exact':
            queryset = queryset.annotate(count=Count('instrumentations__name')).filter(count=len(instrument_list))

            for instrument in instrument_list:
                queryset = queryset.filter(instrumentations__name=instrument)

        elif value == 'not_exact':
            pass  # queryset = ...

        return queryset

    class Meta:
        model = Work
        fields = ['genre', 'title', 'instrument', 'instrumentation']
aaron
  • 39,695
  • 6
  • 46
  • 102
HBMCS
  • 686
  • 5
  • 25

1 Answers1

2

You can grab instrument_list with self.data.getlist('instrument').

This is how you would use instrument_list for the 'exact' query:

type_of_search = django_filters.ChoiceFilter(label="Exact match?", choices=CHOICES, method=lambda queryset, name, value: queryset)

instrument = django_filters.ModelMultipleChoiceFilter(
    field_name="instrumentation__instrument",
    queryset=Instrument.objects.all(),
    label="Filter by instrument",
    method="filter_instruments")

def filter_instruments(self, queryset, name, value):
    if not value:
        return queryset

    instrument_list = self.data.getlist('instrument')  # [v.pk for v in value]
    type_of_search = self.data.get('type_of_search')

    if type_of_search == 'exact':
        queryset = queryset.annotate(count=Count('instrumentations')).filter(count=len(instrument_list))

        for instrument in instrument_list:
            queryset = queryset.filter(instrumentations__pk=instrument)
    else:
        queryset = queryset.filter(instrumentations__pk__in=instrument_list).distinct()

    return queryset
aaron
  • 39,695
  • 6
  • 46
  • 102
  • I've invited to a repository with the minimal code for running a Django app, including sqlite sample data :) – HBMCS Jun 13 '20 at 16:51
  • _Now_ it's doing exactly what it needs to do. THANK YOU again, great solution. – HBMCS Jun 14 '20 at 08:45