Jessie Liauw A Fong
Jessie Liauw A Fong

Reputation: 200

Django exclude filter on multiple foreign keys to the same model

I am trying to filter a query set based upon a nested contains on multiple fields. One field works but the other doesn't

Here is a short version of my model:

class Shift(models.Model):
    users = models.ManyToManyField(User, blank=True)
    potential_users = models.ManyToManyField(User, blank=True, related_name='potential_users')

I want to filter it so that a user is NOT in users and NOT in potential users attribute. I use this exclude function on the queryset:

queryset = Shift.objects.exclude(users__id__contains=self.request.user.id, potential_users__id__contains=self.request.user.id)

I also tried chaining the exclude:

queryset = Shift.objects.exclude(users__id__contains=self.request.user.id).exclude(potential_users__id__contains=self.request.user.id)

When the user is in the users attribute of a shift I don't get any shifts which is expected. BUT when a user is in the potential user attribute I do get the shift.

In potential users

When the user is in potential_users. I run this in my debug executor after the queryset is made:

self.request.user.id == queryset[0].potential_users.all()[0].id

I get True which should be impossible since it is in the exclude. I suspect it has something to do with both of the attributes referring to the same foreign key model

In users

When I do the same filtering with the user in the users attribute I get a index out of range which is good because it means it did not retrieve the shift. Which is expected. This is the check I run:

self.request.user.id == queryset[0].users.all()[0].id

Query

This is the full queryset:

Shift.objects.annotate(amount_users=Count('users')).filter(show_on_market=True, amount_users__lt=F('amount_of_employees'), start__week=self.request.query_params['week'], start__year=self.request.query_params['year'], start__gt=datetime.datetime.now()).exclude(users__id__contains=self.request.user.id, potential_users__id__contains=self.request.user.id)

This is the query that django runs

SELECT `shift_shift`.`id`, `shift_shift`.`title`, `shift_shift`.`start`, `shift_shift`.`end`, `shift_shift`.`amount_of_minutes`, `shift_shift`.`amount_of_employees`, `shift_shift`.`employment_agency_id`, `shift_shift`.`client_id`, `shift_shift`.`store_id`, `shift_shift`.`description`, `shift_shift`.`show_on_market`, `shift_shift`.`repeat_shift_id`, `shift_shift`.`is_repeat`, COUNT(`shift_shift_users`.`user_id`) AS `amount_users` FROM `shift_shift` LEFT OUTER JOIN `shift_shift_users` ON (`shift_shift`.`id` = `shift_shift_users`.`shift_id`) WHERE (`shift_shift`.`is_repeat` = False AND `shift_shift`.`show_on_market` = True AND `shift_shift`.`start` > 2019-02-11 14:54:28.462725 AND WEEK(`shift_shift`.`start`, 3) = 7 AND `shift_shift`.`start` BETWEEN 2019-01-01 00:00:00 AND 2019-12-31 23:59:59.999999 AND NOT (`shift_shift`.`id` IN (SELECT U1.`shift_id` FROM `shift_shift_users` U1 WHERE U1.`user_id` LIKE BINARY %df6c3f22-b3c2-40af-81c9-9a689083bd15%)) AND NOT (`shift_shift`.`id` IN (SELECT U1.`shift_id` FROM `shift_shift_potential_users` U1 WHERE U1.`user_id` LIKE BINARY %df6c3f22-b3c2-40af-81c9-9a689083bd15%))) GROUP BY `shift_shift`.`id`, `shift_shift`.`amount_of_employees` HAVING COUNT(`shift_shift_users`.`user_id`) < (`shift_shift`.`amount_of_employees`) ORDER BY `shift_shift`.`start` ASC

Does anybody know what I am doing wrong.

Upvotes: 0

Views: 1631

Answers (3)

Tobias Ernst
Tobias Ernst

Reputation: 4634

Try this:

from django.db.models import Q
queryset = Shift.objects.exclude(Q(users__id__in=[self.request.user.id]) & Q(potential_users__id__in=[self.request.user.id]))
  • Use __contains only for string checks. Try __in=[Array] syntax to check if an element exists on the many side.
  • With Q() you can combine multiple queries (|=OR, &=AND)
  • You should not chain multiple filter()/exclude() as this is less efficient to compute.

Upvotes: 0

Marto
Marto

Reputation: 21

The simple answer for your problem is:

from django.db.models import Q

user_id = self.request.user.id
queryset = Shift.objects.exclude(
    Q(users__id__contains=user_id) | Q(potential_users__id__contains=user_id
)

Upvotes: 2

mfrackowiak
mfrackowiak

Reputation: 1304

If I understand your intentions properly, that is: not (in users) and not (in potential users) which can be changed to: not (in users or in potential users), you should use the exclude chaining instead of single call, i.e.:

queryset = Shift.objects.exclude(
    users__id__contains=self.request.user.id,
).exclude(
    potential_users__id__contains=self.request.user.id
)

Upvotes: 0

Related Questions