Ragav Y
Ragav Y

Reputation: 1872

How to query for an object that has specific number of foreignKey relations and has specific values in those foreinKey values in Django?

I am building a Django GraphQL API to use for a chat app. I have the following 2 models, one for Chat and one for chat members, which is tied in as a foreingKey:-

class Chat(models.Model):
    name = models.CharField(max_length=100, blank=True, null=True)
    members = models.ManyToManyField(User, related_name="privateChats",
                                     through="ChatMember", through_fields=('chat', 'member'), blank=True)
    active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name


class ChatMember(models.Model):
    chat = models.ForeignKey(Chat, on_delete=models.CASCADE)
    member = models.ForeignKey(User, on_delete=models.CASCADE)

    def __str__(self):
        return self.chat

I have a certain method that is supposed to take in 2 user Ids and first it looks to check if an existing chat exists between these 2 members. If it exists, it returns that chat. If not then it creates a new chat for these 2 members and returns the newly created chat.

The tricky part is where it tries to search if an existing chat exists. Because I'm using this same chat model for group chats, it is possible that it could return a group that also has these 2 members. So the condition to check if there is a one on one chat between these 2 members, it is to first check if the chat has only 2 members in it and those 2 members have the ids that I get as the input for that method.

I'm very new to Django and Python, so I don't know to write the query to find if such a chat exists. So far this is what I have done for that:-

class ChatWithMember(graphene.Mutation):

    class Meta:
        description = "Mutation to get into a Chat"

    class Arguments:

        # ***This is the first user's Id.***

        id = graphene.ID(required=True) 

    ok = graphene.Boolean()
    chat = graphene.Field(ChatType)

    @staticmethod
    @login_required
    def mutate(root, info, id):
        ok = True
        
        # ***Second user's id is taken from the request***

        current_user = info.context.user 
        member = User.objects.get(pk=id)

        if member is None:
            return ChatWithMember(ok=False, chat=None)

        # ***First I annotate the number of members in 'individual_chats'***

        individual_chats = Chat.objects.annotate(member_count=Count('members'))

        # ***Then I filter those chats that have only 2 members***

        individual_chats = individual_chats.filter(member_count=2)

        # ***Then I loop through those to check if there's one that has the two members in that chat and if it exists, I return it.***

        for chat in individual_chats.all():
            if current_user.id in chat.members and member.id in chat.members:
                return ChatWithMember(ok=ok, chat=chat)

        chat_instance = Chat()
        chat_instance.save()

        # Adding the creator of the chat and the member
        chat_instance.members.set([current_user.id, id])

        return ChatWithMember(ok=ok, chat=chat_instance)

At the moment I get the error __str__ returned non-string (type NoneType), previously I got TypeError: ManyRelatedManager object is not iterable. I've spent quite a bit of time on this and clearly I could use some help.

I've written this method with my own understanding of how to get it done, but I'm not sure if a simpler approach exists. Please let me know how to go about it.

Update:-

I've modified the method as per suggestions from @Henri:-

    def mutate(root, info, id):
        ok = True
        current_user = info.context.user
        member = User.objects.get(pk=id)
        if member is None:
            return ChatWithMember(ok=False, chat=None)
        individual_chats = Chat.objects.annotate(member_count=Count('members'))
        individual_chats = individual_chats.filter(member_count=2)
        print('Individual chats before try => ',
              individual_chats.values_list('name', 'members__id'))
        try:
            chats_with_these_2_members = individual_chats.get(
                members__in=[member, current_user])
            return ChatWithMember(ok=ok, chat=chats_with_these_2_members)
        except MultipleObjectsReturned:
            print('multiple chats found!')
            return ChatWithMember(ok=False, chat=None)
            # Error because multiple chats found with these 2 members (and only them)
        except Chat.DoesNotExist:

            print('Creating new chat')
            chat_instance = Chat()
            chat_instance.save()

            # Adding the creator of the chat and the member
            chat_instance.members.set([current_user.id, id])

            return ChatWithMember(ok=ok, chat=chat_instance)

With this in place, when I try to trigger this method with two users who already have a chat with them as the only 2 members, I get the following output in the console:-

Individual chats before try => <QuerySet []> Creating new chat

So basically it fails to identify the existence of the chat that qualifies and goes ahead and creates a new chat.

What's worse is if I try to initiate a chat with different member combination after the first chat is created, I get this:-

Individual chats before try => <QuerySet []> multiple chats found!

This means it no longer creates new chats for different member combinations.

There's something very wrong with this part:-

try:
            chats_with_these_2_members = individual_chats.get(
                members__in=[member, current_user])
            return ChatWithMember(ok=ok, chat=chats_with_these_2_members)
        except MultipleObjectsReturned:
            print('multiple chats found!')
            return ChatWithMember(ok=False, chat=None)
            # Error because multiple chats found with these 2 members (and only them)
        except Chat.DoesNotExist:

but I'm not sure how to fix it because I'm so new t Python.

Upvotes: 0

Views: 102

Answers (2)

Ragav Y
Ragav Y

Reputation: 1872

I modified the Chat model to the following:-

class Chat(models.Model):
    name = models.CharField(max_length=100, blank=True, null=True)
    individual_member_one = models.ForeignKey(
        User, related_name="chat_member_one", on_delete=models.CASCADE, blank=True, null=True)
    individual_member_two = models.ForeignKey(
        User, related_name="chat_member_two", on_delete=models.CASCADE, blank=True, null=True)
    # Type Choices

    class TypeChoices(models.TextChoices):
        INDIVIDUAL = "IL", _('Individual')
        GROUP = "GP", _('Group')
    # End of Type Choices

    chat_type = models.CharField(
        max_length=2, choices=TypeChoices.choices, default=TypeChoices.INDIVIDUAL)
    admins = models.ManyToManyField(User, related_name="adminInChats", through="ChatAdmin", through_fields=(
        'chat', 'admin'), blank=True)
    members = models.ManyToManyField(User, related_name="privateChats",
                                     through="ChatMember", through_fields=('chat', 'member'), blank=True)
    active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name

and the new mutation method is as follows:-

class ChatWithMember(graphene.Mutation):

    class Meta:
        description = "Mutation to get into a Chat"

    class Arguments:
        id = graphene.ID(required=True)

    ok = graphene.Boolean()
    chat = graphene.Field(ChatType)

    @staticmethod
    @login_required
    def mutate(root, info, id):
        ok = True
        current_user = info.context.user
        member = User.objects.get(pk=id)
        print('current_user => ', current_user, 'member => ', member)
        if member is None:
            print('There is no member with id ', id, 'member =>', member)
            return ChatWithMember(ok=False, chat=None)
        try:
            first_possibility = Chat.objects.get(
                individual_member_one=current_user.id, individual_member_two=member.id)
            return ChatWithMember(ok=ok, chat=first_possibility)
        except Chat.DoesNotExist:
            try:
                second_possibility = Chat.objects.get(
                    individual_member_one=member.id, individual_member_two=current_user.id)
                return ChatWithMember(ok=ok, chat=second_possibility)
            except:
                pass

            chat_instance = Chat(
                individual_member_one=current_user, individual_member_two=member)
            chat_instance.save()

            return ChatWithMember(ok=ok, chat=chat_instance)

This is not really a direct answer to the original question, but it solves my issue of having group chat and individual chats being stored in a single model.

Upvotes: 0

Henri
Henri

Reputation: 801

The first part seems good :

    individual_chats = Chat.objects.annotate(member_count=Count('members'))

    # ***Then I filter those chats that have only 2 members***

    individual_chats = individual_chats.filter(member_count=2)

I guess you get the right queryset of chats with 2 members at this moment.

Then you can filter with:

    chats_with_these_2_members = individual_chats.filter(members__in=[current_user, member])
    if chats_with_these_2_members.exists():
         return chats_with_these_2_members.first()
    else:
        # Create it

EDIT: with errors, a better version would be

    from django.core.exceptions import MultipleObjectsReturned
    try:
        chats_with_these_2_members = individual_chats.get(members__in=[current_user, member])
    except MultipleObjectsReturned:
        # Error because multiple chats found with these 2 members (and only them)
    except Chat.DoesNotExist:
        # Create it 

EDIT 2: this version is better written and will help you to identify the problem.

def mutate(root, info, id):
    ok = True
    current_user = info.context.user
    try:
        member = User.objects.get(pk=id)
    except User.DoesNotExist:
        return ChatWithMember(ok=False, chat=None)
    
    chats_annotated = Chat.objects.annotate(member_count=Count('members'))
    individual_chats = chats_annotated.filter(member_count=2)
    print(f'Individual chats => {individual_chats.count()} found')
    print(f'Chats with {member} and {current_user} => {individual_chats.filter(members__in=[member, current_user]).count()} found')
    
    chat_instance = None
    try:
        chat_instance = individual_chats.get(members__in=[member, current_user])
    except MultipleObjectsReturned:
        print('multiple chats found!')
        ok = False
        # Error because multiple chats found with these 2 members (and only them)
    except Chat.DoesNotExist:
        print('Creating new chat')
        chat_instance = Chat(name='name of the chat')
        chat_instance.save()
        # Adding the creator of the chat and the member
        chat_instance.members.add(current_user, member)

    return ChatWithMember(ok=ok, chat=chat_instance)

With the 2 first prints, you should get the number of individual chats (should be > 1) and the number of individual chats with member and current user (should be 0 or 1).

By the way, why did you use ChatMember as the through parameter ? Like explained in the doc, it is used when you want to add extra-data to the many-to-many relationship. But in your case, you do not have any extra-data. Just member and chat.

Upvotes: 1

Related Questions