19

I'm a student studying django rest framework

I'm making a simple sns with django rest framework

I need follower-following system. So, I tried to make it but there is some trouble

First this is my user model with AbstractBaseUser and PermissionsMixin

class User(AbstractBaseUser, PermissionsMixin):
    user_id = models.CharField(max_length=100, unique=True, primary_key=True)
    name = models.CharField(max_length=100)
    created_at = models.DateTimeField(auto_now_add=True)
    is_staff = models.BooleanField(default=False)
    followers = models.ManyToManyField('self', related_name='follower',blank=True)
    following = models.ManyToManyField('self', related_name='following',blank=True)
    profile_image = models.ImageField(blank=True)

the field followers is people who follows me and following is whom i follow

When i add following with this APIView class

class AddFollower(APIView):
    permission_classes = [IsAuthenticated, ]
    def post(self, requset, format=None):
        user = User.objects.get(user_id=self.request.data.get('user_id'))
        follow = User.objects.get(user_id=self.request.data.get('follow'))
        user.following.add(follow)
        user.save()
        follow.followers.add(user)
        follow.save()
        print(str(user) + ", " + str(follow))
        return JsonResponse({'status':status.HTTP_200_OK, 'data':"", 'message':"follow"+str(follow.user_id)})

The user_id is me and the follow is whom i want to follow

I want to add follow to user_id's following field and add user_id to follow's followers field

But it does not work

What i want for result is like this (with user information api)

{
        "followers": [],
        "following": [
            "some user"
        ],
}

some user's user info

{
        "followers": [
            "user above"
        ],
        "following": [
        ],
}

But real result is like this

{      
        "followers": [
            "some user"
        ],
        "following": [
            "some user"
        ],
}

some user's user info

{
        "followers": [
            "user above"
        ],
        "following": [
            "user above"
        ],
}

this is not what i want

I have no idea with this problem i need some help

Thank you

HyeonSeok
  • 589
  • 1
  • 5
  • 18

6 Answers6

34

I would design it in different way.

I would not add the information to the User model but explicitly create another table to store information about "followers" and "following".

Schema of the table would be:

class UserFollowing(models.Model):
    user_id = models.ForeignKey("User", related_name="following")

    following_user_id = models.ForeignKey("User", related_name="followers")

    # You can even add info about when user started following
    created = models.DateTimeField(auto_now_add=True)

Now, in your post method implementation, you would do only this:

UserFollowing.objects.create(user_id=user.id,
                             following_user_id=follow.id)

And then, you can access following and followers easily:

user = User.objects.get(id=1) # it is just example with id 1
user.following.all()
user.followers.all()

And you can then create constraint so user cannot follow the same user twice. But i leave this up to you ( hint: unique_together )

unicdev
  • 300
  • 2
  • 7
Enthusiast Martin
  • 3,041
  • 2
  • 12
  • 20
  • 2
    Wow!! it works successfully. Thanks!! and i block follow same user twice with your hint. Thanks for your kindness. – HyeonSeok Nov 12 '19 at 01:32
  • This is a good solution, thank you, but I wonder if this is the best solution for a really big project. – Opeyemi Odedeyi Nov 21 '19 at 23:35
  • @OpeyemiOdedeyi yes. should be ok for even big project - on db level, with some indexes as needed. Then you need to use the queries to retrieve followers/following carefully when needed. but this is not part of this question. – Enthusiast Martin Nov 22 '19 at 08:22
  • @고현석 please is it possible to share an example where you implemented this. I am still confused about this. I would like to have an idea of how your model, serializer, views were handled. please, I would really appreciate it. – Opeyemi Odedeyi Dec 02 '19 at 00:56
  • @EnthusiastMartin please is it possible to share an example where you implemented this. I am still confused about this. I would like to have an idea of how your model, serializer, views were handled. please, I would really appreciate it. – Opeyemi Odedeyi Dec 02 '19 at 01:00
  • 1
    @OpeyemiOdedeyi I added an answer for you – HyeonSeok Dec 02 '19 at 06:10
  • 1
    It's great. But do I need to write a URL for these or not? If yes, then how? – Aakash Bhaikatti Jan 03 '22 at 04:18
17

The above solutions are fine and optimal, but I would like to supply a detailed solution for anyone who wants to implement such functionality.

The intermediary Model

from django.contrib.auth import get_user_model
UserModel = get_user_model()

class UserFollowing(models.Model):

    user_id = models.ForeignKey(UserModel, related_name="following", on_delete=models.CASCADE)
    following_user_id = models.ForeignKey(UserModel, related_name="followers", on_delete=models.CASCADE)
    created = models.DateTimeField(auto_now_add=True, db_index=True)

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=['user_id','following_user_id'],  name="unique_followers")
        ]

        ordering = ["-created"]

    def __str__(self):
        f"{self.user_id} follows {self.following_user_id}"

THE SERIALIZER for follow and unfollow

Your View for follow and unfollow

class UserFollowingViewSet(viewsets.ModelViewSet):

    permission_classes = (IsAuthenticatedOrReadOnly,)
    serializer_class = UserFollowingSerializer
    queryset = models.UserFollowing.objects.all()

Custom FollowersSerializer and FollowingSerializer

class FollowingSerializer(serializers.ModelSerializer):

    class Meta:
        model = UserFollowing
        fields = ("id", "following_user_id", "created")
class FollowersSerializer(serializers.ModelSerializer):
    class Meta:
        model = UserFollowing
        fields = ("id", "user_id", "created")

Your UserSerializer

from django.contrib.auth import get_user_model

User = get_user_model()

class UserSerializer(serializers.ModelSerializer):

    following = serializers.SerializerMethodField()
    followers = serializers.SerializerMethodField()
    

    class Meta:
        model = User
        fields = (
            "id",
            "email",
            "username",
            "following",
            "followers",
        )
        extra_kwargs = {"password": {"write_only": True}}

    def get_following(self, obj):
        return FollowingSerializer(obj.following.all(), many=True).data

    def get_followers(self, obj):
        return FollowersSerializer(obj.followers.all(), many=True).data
unicdev
  • 300
  • 2
  • 7
  • It works fine but you didn't write the codes of Unfollow structure here .Also do you know how to do request on follow?Like if i want to follow a user,the user has to accept it? –  Jul 21 '20 at 11:06
  • 1
    Yeah, it's very easy, to follow a user you simply send a POST request to /your-following-route/ create the model and to unfollow a user you send a DELETE request to /your-following-route/ID. If you want to like create the pending functionality on Twitter where the user needs to accept your follow request, you can have a flag that does that. So when the user logs in and he accepts your follow, you update the flag in the model. I hope this helps? – unicdev Jul 22 '20 at 12:17
  • that helped.Thank you so much! –  Jul 22 '20 at 15:22
  • I think you missed a `return` statement in `UserFollowing`: ```python def __str__(self): return f"{self.user_id} follows {self.following_user_id}" ``` – mincom May 14 '21 at 09:12
  • Thanks for the help. But there's a issue in it, as I checked in django admin the user can follow himself. How can I send a delete request in ModelViewset DRF. Do I need to create a URL for these? – Aakash Bhaikatti Jan 03 '22 at 04:42
5
models.py

from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    followers = models.ManyToManyField('self', symmetrical=False, 
                blank=True)

    def count_followers(self):
        return self.followers.count()
    
    def count_following(self):
        return User.objects.filter(followers=self).count()
David Buck
  • 3,752
  • 35
  • 31
  • 35
GST Talib
  • 51
  • 1
  • 1
  • This feels like the only sensible solution – Jakob Lindskog Jul 03 '21 at 14:55
  • I tried using this approach, but it's still possible to have yourself as a follower. I'm still working on it, but so far I have no idea why it fails. – Eduardo Gomes Jun 16 '22 at 19:13
  • Reading the docs I understood that `symmetrical` on this approach will only distinguish `source` from `target`, which is a great step, but still doesn't solve the auto-following issue =| – Eduardo Gomes Jun 16 '22 at 19:34
4

I created a follow and unfollow system similar to Instagram.

Functionality:

  • If private account enable --> send follow request.
  • If user in block list --> they can not follow of opposite user.
  • User can check pending request, sended request list,blocked user list,etc.

Let's start :

Models.py:

    class Profile(models.Model):
        user = models.OneToOneField(to = User,on_delete=models.CASCADE,related_name='profile')
          .......
        private_account = models.BooleanField(default = False)
        followers = models.ManyToManyField('self',blank=True,related_name='user_followers',symmetrical=False)
        following = models.ManyToManyField('self',blank=True,related_name='user_following',symmetrical=False)
        panding_request = models.ManyToManyField('self',blank=True,related_name='pandingRequest',symmetrical=False)
        blocked_user = models.ManyToManyField('self',blank=True,related_name='user_blocked',symmetrical=False)
        created_date = models.DateTimeField(auto_now_add=True)
        
        def __str__(self):
            return '%s' %(self.user)
  • Here you can follow, unfollow, send follow request, accept follow request, decline request and you can remove your follower.
  • From the front end side pass opposite user profile id and type of action(follow,unfollow..).

views.py:

    class FollowUnfollowView(APIView):
        permission_classes = [IsAuthenticated]
        
        def current_profile(self):
            try:
                return Profile.objects.get(user = self.request.user)
            except Profile.DoesNotExist:
                raise Http404
            
        def other_profile(self,pk):
            try:
                return Profile.objects.get(id = pk)
            except Profile.DoesNotExist:
                raise Http404
        
        def post(self, request,format=None):    
            
            pk = request.data.get('id')              # Here pk is opposite user's profile ID
            req_type = request.data.get('type')        
            
            current_profile = self.current_profile()
            other_profile = self.other_profile(pk)
            
            
            if req_type == 'follow':
                if other_profile.private_account:
                    other_profile.panding_request.add(current_profile)
                    return Response({"Requested" : "Follow request has been send!!"},status=status.HTTP_200_OK)
                else:
                    if other_profile.blocked_user.filter(id = current_profile.id).exists():
                        return Response({"Following Fail" : "You can not follow this profile becuase your ID blocked by this user!!"},status=status.HTTP_400_BAD_REQUEST)
                    current_profile.following.add(other_profile)
                    other_profile.followers.add(current_profile)
                    return Response({"Following" : "Following success!!"},status=status.HTTP_200_OK) 
            
            elif req_type == 'accept':
                current_profile.followers.add(other_profile)
                other_profile.following.add(current_profile)
                current_profile.panding_request.remove(other_profile)
                return Response({"Accepted" : "Follow request successfuly accespted!!"},status=status.HTTP_200_OK)
            
            elif req_type == 'decline':
                current_profile.panding_request.remove(other_profile)
                return Response({"Decline" : "Follow request successfully declined!!"},status=status.HTTP_200_OK)
            
            elif req_type == 'unfollow':
                current_profile.following.remove(other_profile)
                other_profile.followers.remove(current_profile)
                return Response({"Unfollow" : "Unfollow success!!"},status=status.HTTP_200_OK)
                
            elif req_type == 'remove':     # You can remove your follower
                current_profile.followers.remove(other_profile)
                other_profile.following.remove(current_profile)
                return Response({"Remove Success" : "Successfuly removed your follower!!"},status=status.HTTP_200_OK)

    # Here we can fetch followers,following detail and blocked user,pending request,sended request.. 
    
        def patch(self, request,format=None):
        
            req_type = request.data.get('type')
        
            if req_type == 'follow_detail':
                serializer = FollowerSerializer(self.current_profile())
                return Response({"data" : serializer.data},status=status.HTTP_200_OK)
        
            elif req_type == 'block_pending':
                serializer = BlockPendinSerializer(self.current_profile())
                pf = list(Profile.objects.filter(panding_request = self.current_profile().id).values('id','user__username','profile_pic','overall_pr'))
                return Response({"data" : serializer.data,"Sended Request" :pf},status=status.HTTP_200_OK)

    # You can block and unblock user

        def put(self, request,format=None):
            pk = request.data.get('id')              # Here pk is oppisite user's profile ID
            req_type = request.data.get('type')
        
            if req_type == 'block':
                self.current_profile().blocked_user.add(self.other_profile(pk))
                return Response({"Blocked" : "This user blocked successfuly"},status=status.HTTP_200_OK)
            elif req_type == 'unblock':
                self.current_profile().blocked_user.remove(self.other_profile(pk))
                return Response({"Unblocked" : "This user unblocked successfuly"},status=status.HTTP_200_OK)

serializers.py:

class EachUserSerializer(serializers.ModelSerializer):
    username = serializers.CharField(source='user.username')
    class Meta:
        model = Profile
        fields = ('id','username','profile_pic')
        read_only_fields = ('id','username','profile_pic')

class FollowerSerializer(serializers.ModelSerializer):
    followers = EachUserSerializer(many=True, read_only= True)
    following = EachUserSerializer(many=True,read_only=True)
    
    class Meta:
        model = Profile
        fields = ('followers','following')
        read_only_fields = ('followers','following')

class BlockPendinSerializer(serializers.ModelSerializer):
    panding_request = EachUserSerializer(many=True, read_only= True)
    blocked_user = EachUserSerializer(many=True,read_only=True)

    class Meta:
        model = Profile
        fields = ('panding_request','blocked_user')  
        read_only_fields = ('panding_request','blocked_user')

urls.py:

from django.urls.conf import path
from .views import *


urlpatterns = [
    .....   
    path('follow_unfollow/',FollowUnfollowView.as_view(),name = "follow_unfollow"),
]

If any doubt of any step please comment. I will briefly describe.

Pradip Kachhadiya
  • 2,067
  • 10
  • 28
1

This is how i solved my problem.

there is a good answer above, but someone need a detail for it. so i'm writing this

I removed field followers and following in User model. And Created new model UserFollowing.

You can see this model in Enthusiast Martin's answer. I didn't use any serializer to create object.

Just two views (Follow, UnFollow) were needed.

In Follow View

UserFollowing.objects.create(user_id=user.id, following_user_id=follow.id)

with this we can create Following-Follower relationship.

The way to use this relationship

In User Information View

following_data = UserFollowingSerializer(qs.following.all(), many=True)
followers_data = UserFollowingSerializer(qs.followers.all(), many=True)
return JsonResponse({'status':status.HTTP_200_OK, 'data':{'user':serializer.data, 'following':following_data.data, 'followers':followers_data.data}, "message":"success"})

I usually use JsonResponse for response.

serializer.data and qs are user object

In UserFolowingSerializer

fields = '__all__'

I used this.

HyeonSeok
  • 589
  • 1
  • 5
  • 18
  • one question I have is that, in follow view, what is the user.id, and the follow.id? is the user.id us passing the user that wants to follow, and is the follow.id us passing the user to be followed? – Opeyemi Odedeyi Dec 02 '19 at 07:39
  • (user_id=user.id, following_user_id=follow.id) this part i am referring to – Opeyemi Odedeyi Dec 02 '19 at 07:39
  • if i am User A and you are User B and i (Usre A) want to follow you (User B). In this situation user_id is me and following_user_id is you – HyeonSeok Dec 02 '19 at 10:08
  • bro, sorry if I am disturbing you. please, can you take a look at this: https://github.com/opeodedeyi/creative-api I tried implementing it in that app and still had issues. – Opeyemi Odedeyi Dec 02 '19 at 13:35
  • @OpeyemiOdedeyi Sorry, I have never used slug. I can't help you. – HyeonSeok Dec 03 '19 at 01:00
  • 1
    I finally understand this, it took me a long time but I finally get it today. – Opeyemi Odedeyi Jan 17 '20 at 21:16
0

Very good answers. For me it would be either @GST Talib's answer or a very similar one without using self

    following = models.ManyToManyField('User', blank=True, related_name='followers')

That way when you add user1.following.add(user2) you can see the relationship being reflected in user2.followers

And no need for extra code.

MayTheSchwartzBeWithYou
  • 1,181
  • 1
  • 16
  • 32