Фильтрация в админке Django для нескольких условий на одной записи

Это связано с моим другим вопросом здесь, который, как мне казалось, я решил. Тем не менее, при применении фильтра по school я получаю любой проект, в котором любой человек принадлежит к этой школе. Вместо этого я хочу отфильтровать только проекты с лицами, которые одновременно являются "главным исследователем" (role) и принадлежат к определенной школе.

Прямо сейчас код ниже выводит проект, в котором есть любой связанный человек, принадлежащий к отфильтрованной школе, независимо от его роли.

Мой models.py:

class School(models.Model):
    name = models.CharField(max_length=200)

class Person(models.Model):

    surname = models.CharField(max_length=100, blank=True, null=True)
    forename = models.CharField(max_length=100, blank=True, null=True)
    school = models.ForeignKey(School, null=True, blank=True, on_delete=models.CASCADE)

class PersonRole(models.Model):
    ROLE_CHOICES = [
        ("Principal investigator", "Principal investigator"),
        [...]
    ]
    project = models.ForeignKey('Project', on_delete=models.CASCADE)
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    person_role = models.CharField(choices=ROLE_CHOICES, max_length=30)


class Project(models.Model):

    title = models.CharField(max_length=200)
    person = models.ManyToManyField(Person, through=PersonRole)

Мой admin.py:

class PISchoolFilter(admin.SimpleListFilter):

   title = 'PI School'
   parameter_name = 'school'

   def lookups(self, request, model_admin):
        return (
                ('Arts & Humanities Admin',('Arts & Humanities Admin')),
              [...]
              )

   def queryset(self, request, queryset):
    if self.value() == 'Arts & Humanities Admin':
        return queryset.filter(
            person__personrole__person_role__contains="Principal investigator",
            person__personrole__person__school=5 #5 is 'Arts & Humanities Admin' school pk.
        ).distinct()
    [...]
    def choices(self, changelist):
        super().choices(changelist)
        return (
            *self.lookup_choices,
            )

class ProjectAdmin(NumericFilterModelAdmin, ImportExportModelAdmin):
   
    list_filter = [PI_SchoolFilter]

Почему (person__personrole__person_role__contains="Principal investigator", person__personrole__person__school=5) не работает на одном экземпляре PersonRole? Я также пробовал с Q(person__personrole__person_role__contains="Principal investigator") & Q(person__personrole__person__school=4).distinct() и получил то же самое, т.е. проект, в котором был либо один человек с этой ролью, либо с этой школой. Я хочу, чтобы и то, и другое было на одном человеке, или ничего.

PS:

Я заметил, что когда я фильтрую по школе (т.е. выбираю школу из выпадающего списка фильтров администратора), адрес загружаемой страницы содержит только ?person__personrole__person__school=5, полностью игнорируя роль. Если я жестко закодирую ?person__personrole__person_role__contains="Principal investigator", то получу

DisallowedModelAdminLookup at /admin/artsdb/project/

Filtering by person__personrole__person_role__contains not allowed

Проблема, с которой вы столкнулись, связана с тем, как Django обрабатывает отношения "многие-ко-многим". Когда вы используете filter() для поля типа "многие-ко-многим", Django создает SQL JOIN для каждого условия фильтрации. Это означает, что ваши условия фильтрации применяются не к одному экземпляру PersonRole, а к любому экземпляру PersonRole, связанному с проектом.

Чтобы гарантировать, что оба условия применяются к одному и тому же экземпляру PersonRole, можно использовать объект Q для создания сложного поиска. Вот как можно модифицировать метод queryset:

from django.db.models import Q

def queryset(self, request, queryset):
    if self.value() == 'Arts & Humanities Admin':
        return queryset.filter(
            Q(person__personrole__person_role__contains="Principal investigator") &
        Q(person__personrole__person__school=5)
    ).distinct()

Это вернет только те проекты, в которых один и тот же человек является и "главным исследователем", и принадлежит школе с идентификатором 5.

Что касается ошибки DisallowedModelAdminLookup, то это связано с тем, что Django не позволяет фильтровать по полю "многие-ко-многим" из-за потенциальных проблем с производительностью. Вы можете отменить это поведение, установив упорядочивание в ModelAdmin:

    class ProjectAdmin(NumericFilterModelAdmin, ImportExportModelAdmin):
    ordering = ('person__personrole__person_role',)
    list_filter = [PI_SchoolFilter]

Это указывает Django на разрешение упорядочивания по person_role, что позволяет фильтровать по этому полю.

Проблема с вашим текущим подходом к фильтрации заключается в том, как вы обходите отношения между моделями. Вот почему ваш код не фильтрует так, как ожидалось, и как это исправить:

Анализ проблемы:

  • person__personrole__person_role__contains="Principal investigator": Проверяется, содержит ли поле person_role строку "Principal investigator". Это может соответствовать проектам, в которых человек имеет другую роль, содержащую эту строку.

Решения:

  1. Точное соответствие роли:

Измените условие, чтобы использовать точное совпадение для роли:

person__personrole__person_role="Principal investigator"
  1. Извлечение связанных объектов:

Используйте prefetch_related для эффективной загрузки связанных объектов за один раз. Это позволяет избежать множества запросов к базе данных и упрощает процесс фильтрации:

from django.db import models

class ProjectAdmin(NumericFilterModelAdmin, ImportExportModelAdmin):
    list_filter = [PI_SchoolFilter]

    def get_queryset(self, request):
        queryset = super().get_queryset(request)
        return queryset.prefetch_related('personrole_set')

    # ... rest of your admin class definition
  1. Пользовательский менеджер или аннотации:

Для более сложной фильтрации рассмотрите возможность создания пользовательского менеджера на модели Project или используйте аннотации для фильтрации на основе атрибутов связанных объектов.

Пояснение:

  • Оптимизация prefetch_related позволяет избежать запроса PersonRole объектов для каждого проекта в отфильтрованном списке.
  • Фильтрация с помощью person__personrole__person_role="Principal investigator" обеспечивает точное совпадение для требуемой роли.

Адресация отброшена Параметр фильтра:

В URL отображается только фильтр школы (person__personrole__person__school=5), что может быть связано с тем, что класс PI_SchoolFilter неправильно обрабатывает фильтр роли. Убедитесь, что метод value() возвращает значение роли, если оно присутствует в строке запроса.

Помни:

  • Сочетание prefetch_related с фильтрацией по атрибутам связанных объектов может потребовать дополнительной логики фильтрации в представлении или шаблоне.

При реализации одного из этих решений ваш фильтр администратора должен точно отфильтровывать проекты с людьми, имеющими роль "Главный исследователь" и принадлежащими к выбранной школе.

Вернуться на верх