Django фильтр, в котором каждый элемент в списке "многие ко многим" объекта включается в предоставленный набор запросов

У меня небольшие проблемы с формулировкой названия моего вопроса, возможно, поэтому я не могу найти ответ. Это не обязательно должен быть набор запросов, это может быть просто список pks или какой-то другой метод. Я объясню на примере и скажу, что мне нужно, а что нет.

У меня есть следующие модели.

class Document(models.Model):
  allowed_groups = models.ManyToManyField(Group, related_name='allowed_documents')

class Person(models.Model):
  permission_groups = models.ManyToManyField(Group, related_name='people')

class Group(models.Model):
  id = models.BigIntegerField()

Я хочу найти все документы, к которым человек может получить доступ с условием, что он должен быть членом всех разрешенных групп.

Я хочу этого: Case

  1. Document(allowed_groups=1,2,7) with Person(permission_groups=1,2,6,7,11,15) -> MATCH
  2. Document(allowed_groups=1,2,7) with Person(permission_groups=1,7) -> NO_MATCH
  3. Document(allowed_groups=1,2,7) with Person(permission_groups=1,2) -> NO_MATCH
  4. Document(allowed_groups=1,2,7) with Person(permission_groups=2) -> NO_MATCH
  5. Document(allowed_groups=1,2,7) with Person(permission_groups=8) -> NO_MATCH
  6. Document(allowed_groups=1,2,7) with Person(permission_groups=1,2,7) -> MATCH

Если я сделаю это:

person = Person.objects.get(pk=1)
Document.objects.filter(allowed_groups__in=person.permission_groups.all())

Я буду соответствовать во всех вышеперечисленных случаях, кроме 8 (не то, что я хочу)

На stack overflow есть много вопросов, которые спрашивают о точном совпадении, т.е. совпадение только в случае 6, но не в случае 1 (также не то, что я хочу)

Итак, мой вопрос в том, как я могу использовать django для этого? Я рассматривал возможность использования SQL, но наверняка есть способ сделать это с помощью Django ORM. Это не кажется таким уж безумным требованием.

Примечание: у меня есть несколько других условий (другие типы групп и уровни доступа к документам), которые я превратил в сложное выражение с цепочкой объектов filter/Q, но у меня все получилось, кроме этой части.

Используется функция np.isin из библиотеки numpy. Она возвращает булев массив при сравнении двух массивов. Описание Здесь.

Я применил values_list и flat=True к объектам, чтобы извлечь их в список для сравнения в numpy. Я сделал генератор списка aaa (генератор списка во много раз быстрее цикла), в котором документ сравнивается с выбранным человеком, если все значения документа совпадают, то all () возвращает True, тогда i.pk записывается в список aaa. Далее данные документа фильтруются по этому списку и передаются в словарь для отображения в шаблоне.

Замените bboard на название папки, в которой находятся ваши шаблоны. У меня это: templates/bboard, которые находятся в папке приложения.

views.py

import numpy as np

def info(request):
    person = Person.objects.get(pk=1).permission_groups.all().values_list('id', flat=True)
    print(person)

    def my_func(x):
        document = Document.objects.get(pk=x).allowed_groups.all().values_list('id', flat=True)

        return document

    aaa = [i.pk for i in Document.objects.all() if np.isin(my_func(i.pk), person).all()]
    document = Document.objects.filter(pk__in=aaa)

    return render(request, 'bboard/templ.html', {'context': document})

templates

{% for a in context %}
<p>{{ a.id }}</p>
{% endfor %}

На основании этого ответа

Решение:

from django.db.models import Count, Q

person = Person.objects.get(pk=1)
permission_groups = set(person.permission_groups.all())
Document.objects.annotate(
    allowed_groups_count=Count('allowed_groups', filter=Q(allowed_groups__in=permission_groups))
).filter(
    allowed_groups_count__gt=0
)

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

SELECT
  document.id,
  COUNT(document_allowed_groups.group_id) FILTER (
    WHERE
      document_allowed_groups.group_id IN (1, 2, 6, 7, 11, 15)
  ) AS allowed_groups_count
FROM
  document
  LEFT OUTER JOIN document_allowed_groups ON (
    document.id = document_allowed_groups.document_id
  )
GROUP BY
  document.id
HAVING
  COUNT(document_allowed_groups.group_id) FILTER (
    WHERE
      (
        document_allowed_groups.group_id IN (1, 2, 6, 7, 11, 15)
      )
  ) > 0
Вернуться на верх