Search code examples
pythondjangoperformancedjango-querysetdjango-q

Complex Django queryset filtering, involving Q objects and tricky logic


I have a web-based chat app developed in Django. People can form their own chat groups in it, invite others and chatter away in the said groups.

I want to isolate all users who were recently online AND: (i) have already been sent an invite for a given group, OR (ii) have already participated (i.e. replied) at least once in the same given group. Note that user is a vanilla django.contrib.auth user.

To accomplish this, I'm writing:

online_invited_replied_users = User.objects.filter(id__in=recently_online,(Q()|Q()))

Assume recently_online to be correctly formulated. What should be the two Q objects?

The first Q() ought to refer invitees, the second to users who have replied at least once. I seem to be running into code-writer's block in formulating a well-rounded, efficient db query here. Please advise!


Relevant models are:

class Group(models.Model):
    topic = models.TextField(validators=[MaxLengthValidator(200)])
    owner = models.ForeignKey(User)
    created_at = models.DateTimeField(auto_now_add=True)

class GroupInvite(models.Model):
    #this is a group invite object
    invitee = models.ForeignKey(User, related_name='invitee')
    inviter = models.ForeignKey(User, related_name ='inviter')
    sent_at = models.DateTimeField(auto_now_add=True)
    which_group = models.ForeignKey(Group)

class Reply(models.Model):
    #if a user has replied in a group, count that as participation
    text = models.TextField(validators=[MaxLengthValidator(500)])
    which_group = models.ForeignKey(Group)
    writer = models.ForeignKey(User)
    submitted_on = models.DateTimeField(auto_now_add=True)

Note: feel free to ask for more info; I'm cognizant of the fact that I may have left something out.


Solution

  • For users that are already invited to a group group, follow the GroupInvite.invitee foreign key backwards.

    already_invited = Q(invitee__which_group=group)
    

    The related_name you chose, invitee, doesn't really make sense, because it's linking to an group invite, not a user. Maybe `group_invites_received' would be be better.

    For users that have already replied, follow the Reply.writer foreign key backwards.

    already_replied = Q(reply__which_group=group)
    

    In this case it's reply. because you haven't specified a related name.

    Note that your current pseudo code

    User.objects.filter(id__in=recently_online,(Q()|Q()))
    

    will give an error 'non-keyword arg after keyword arg'.

    You can fix this by either moving the non-keyword argument before the keyword argument,

    User.objects.filter((Q()|Q()), id__in=recently_online)
    

    or put them in separate filters:

    User.objects.filter(id__in=recently_online).filter(Q()|Q())
    

    Finally, note that you might need to use distinct() on your queryset, otherwise users may appear twice.