多字段的 Django 过滤器完全匹配:ManyToManyField 使用 ModelMultipleChoiceFilter

Django filter exact match for multi field: ManyToManyField using ModelMultipleChoiceFilter

我在我的项目中使用 Django 过滤器 (django-filter)。我有下面的模型,其中组合 (Work) 具有多对多 instrumentations 字段和 through 模型。每个仪器中都有几个仪器。

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())

我的过滤器工作正常:它抓取用户在过滤器表单中选择的乐器的所有部分。

但是,我想添加用那些确切的乐器过滤乐曲的可能性。例如,如果一首曲子只包含小提琴、圆号和大提琴而没有其他任何东西,我想得到它,但不是为小提琴、圆号、大提琴和打击乐器而写的曲子。有可能实现吗?

我还希望用户从界面中选择是否执行精确搜索,但我想这是目前的次要问题。


更新type_of_search 使用 ChoiceFilter

我取得了一些进步;使用下面的代码,我可以让用户在两种搜索之间进行选择。现在,我需要找到哪个查询将只获取具有那组确切乐器的乐曲。

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.??

我知道我想要的查询是这样的:

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

我只是不知道如何 'translate' 将其翻译成 django_filters 语言。


更新 2'exact' 使用 QuerySet.annotate

查询

感谢 this question,我认为这就是我正在寻找的查询:

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)

我觉得我很接近,我只是不知道如何将它与 django_filters 集成。


更新 3WorkFilter 如果搜索准确 returns 为空

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']

你可以用 self.data.getlist('instrument') 抢到 instrument_list

这就是您将 instrument_list 用于 'exact' 查询的方式:

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