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
- Document(allowed_groups=1,2,7) with Person(permission_groups=1,2,6,7,11,15) -> MATCH
- Document(allowed_groups=1,2,7) with Person(permission_groups=1,7) -> NO_MATCH
- Document(allowed_groups=1,2,7) with Person(permission_groups=1,2) -> NO_MATCH
- Document(allowed_groups=1,2,7) with Person(permission_groups=2) -> NO_MATCH
- Document(allowed_groups=1,2,7) with Person(permission_groups=8) -> NO_MATCH
- 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