thomas
thomas

Reputation: 1510

How to realise a dynamic AND combined query on a ManyToMany relation in Django

I have two simple models. Let's say Tags and Posts. They look like this (simplified):

class Tag(models.Model):
    name = models.CharField(max_length=255, blank=False, unique=True)

class Post(models.Model):
    title = models.CharField(max_length=255, blank=False, default='')
    tags = models.ManyToManyField(Tag)

So, each tags can be assigned to n posts and vice versa.

I'm trying to get a list of either all posts where ANY of the given tags are assigned, or where ALL of the given tags are assigned.

So basically I want an OR or an AND combination.

The OR is easy enough. In my view I do (assume "tags" is a list of Tag objects)

queryset = Post.objects.filter(tags__in=tags)

But I can't figure out how to do that for an AND combination. I either get nothing or the same as with OR. I tried a lot of different things, but nothing worked in my scenario, where I have a dynamic list of tags to filter by.

My most promising approach looked like this:

filter = [Q(tags=tag) for tag in tags if tag.enabled == True]
qs = Post.objects.all()
qs = qs.filter(reduce(operator.__and__, filter))

This still returns an empty list. And yes I am 100% sure that I have Post records, with both requested tags assigned to it.

What am I doing wrong?

Upvotes: 0

Views: 43

Answers (2)

Deniz Saner
Deniz Saner

Reputation: 437

In order to achieve your desired filter, we can do something like this:

wanted_tags = [tag_1, tag_2, tag_3,...]

# To keep it as lean as possible, we first minimise our search space by filtering solely the posts with the amount of tags in the first place:
preprocessed_posts = Post.objects.annotate(c=Count('tags')).filter(c=len(wanted_tags))

# Now comes the somewhat pythonic part, which is simple but feels kinda hacky:
for tag in wanted_tags:
    preprocessed_posts.filter(tags=tag)

# now preprocessed_posts contains the wanted entities
wanted_posts = preprocessed_posts

Upvotes: 0

thomas
thomas

Reputation: 1510

Okay, the answer is, as usual, relativly simple:

qs = Post.objects.all()
for tag in tags:
    qs = qs.filter(tags=tag)

Upvotes: 1

Related Questions