skip to Main Content

I’m a beginner building the backend API for a social media clone using DRF. The frontend will be built later and not in Django. I’m currently using Postman to interact with the API.

I’m trying to implement a "like" feature as you would have on Facebook or Instagram. I cannot send the correct data with Postman to update the fields which bear the many-to-many relationship.

Here is some of my code:

models.py

class User(AbstractUser):
    liked_haikus = models.ManyToManyField('Haiku', through='Likes')
    pass

class Haiku(models.Model):
    user = models.ForeignKey(User, related_name='haikus', on_delete=models.CASCADE)
    body = models.CharField(max_length=255)
    liked_by = models.ManyToManyField('User', through='Likes') 
    created_at = models.DateTimeField(auto_now_add=True)

class Likes(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    haiku = models.ForeignKey(Haiku, on_delete=models.CASCADE)

serializers.py

class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = ['username', 'password', 'url', 'liked_haikus']
        extra_kwargs = { 'password' : {'write_only': True}}

    def create(self, validated_data):
        password = validated_data.pop('password')
        user = User(**validated_data)
        user.set_password(password)
        user.save()
        token = Token.objects.create(user=user)
        return user

class HaikuSerializer(serializers.ModelSerializer):
    class Meta:
        model = Haiku
        fields = ['user', 'body', 'liked_by', 'created_at']

class LikesSerializer(serializers.ModelSerializer):
    model = Likes
    fields = ['haiku_id', 'user_id']

views.py

class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [permissions.IsAuthenticated]

    @action(detail=True, methods=['get'])
    def haikus(self, request, pk=None):
        user = self.get_object()
        serializer = serializers.HaikuSerializer(user.haikus.all(), many=True)
        return Response(serializer.data)

class UserCreateViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [permissions.AllowAny]


class HaikuViewSet(viewsets.ModelViewSet):
    queryset = Haiku.objects.all()
    serializer_class = HaikuSerializer
    permission_classes = [permissions.IsAuthenticated]


class LikesViewSet(viewsets.ModelViewSet):
    queryset = Likes.objects.all()
    serializer_class = LikesSerializer
    permission_classes = [permissions.IsAuthenticated]

urls.py

router = routers.DefaultRouter(trailing_slash=False)
router.register('users', views.UserViewSet)
router.register('haikus', views.HaikuViewSet)
router.register('register', views.UserCreateViewSet)
router.register('likes', views.LikesViewSet)

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include(router.urls)),
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
    path('api-auth-token', obtain_auth_token, name='api_token_auth')
]

Using the Django Admin I can manually set users to like posts and the fields in the db will update and reflect in API requests.

With Postman, I’ve tried sending both PUT and PATCH to, for example:

http://127.0.0.1:8000/haikus/2

with "form data" where key ="liked_by" and value="3" (Where 3 is a user_id). I got a 200 response and JSON data for the endpoint back, but there was no change in the data.

I’ve tried GET and POST to http://127.0.0.1:8000/likes and I receive the following error message:

AttributeError: ‘list’ object has no attribute ‘values’

I’ve looked at nested-serializers in the DRF docs, but they don’t seem to be quite the same use-case.

How can I correct my code and use Postman to properly update the many-to-many fields?

I think I need to probably write an update function to one or several of the ViewSets or Serializers, but I don’t know which one and don’t quite know how to go about it.

All guidance, corrections and resources appreciated.

2

Answers


  1. Chosen as BEST ANSWER

    adnan kaya has provided the correct code and I have upvoted him and checked him off as the correct answer. I want go through his solution to explain it for future readers of this question.

    liked_by = serializers.PrimaryKeyRelatedField(
            many=True,
            queryset=User.objects.all())
    

    You can read about PrimaryKeyRelatedField here: https://www.django-rest-framework.org/api-guide/relations/

    Since liked_by is a ManyToManyField it has special properties in that ManyToMany relations create a new table in the DB that relates pks to each other. This line tells Django that this field is going to refer to one of these tables via its primary key. It tells it that liked by is going to have multiple objects in it and it tells it that these objects are going to come from a particular queryset.

    def update(self, instance, validated_data):
            liked_by = validated_data.pop('liked_by')
            for i in liked_by:
                instance.liked_by.add(i)
            instance.save()
            return instance
    

    ModelSerializers is a class that provides its own built in create and update functions that are fairly basic and operate in a straightforward manner. Update, for example, will just update the field. It will take the incoming data and use it to replace the existing data in the field it is directed at.

    You can read more about ModelSerializers here: https://www.django-rest-framework.org/api-guide/serializers/#modelserializer

    You can overwrite these functions and specify custom functions by declaring them. I have declared update here. Update is a function that takes 3 arguments. The first is self. You can call this whatever you want, but there is a strong convention to call it self for readability. Essentially this is importing the class the function belongs, into the function so you can utilize all that classes functions and variables. Next is instance. Instance is the data that is currently in the entry you are trying to update. It is a dictionary like object. Finally, there is validated_data. This is the data you are trying to send to the entry to update it. When using form data, for example, to update a database, this will be a dictionary.

    liked_by = validated_data.pop('liked_by')
    

    Because validated_data is a dictionary you can use the .pop() method on it. Pop can take the key of the dictionary and "pop it off" leaving you with the value (more formally, .pop('key') will return its 'value'). This is nice because, at least in my case, it is the value that you want added to the entry.

      for i in liked_by:
                instance.liked_by.add(i)
    

    this is a simple python for-loop. A for loop is here because in my use-case the value of the validated_data dictionary is potentially a list.

    The .add() method is a special method that can be used with ManytoMany relationships. You can read about the special methods for ManytoMany relations here: https://docs.djangoproject.com/en/3.1/ref/models/relations/

    It does what it advertises. It will add the value you send send to it to data you call it for, instead of replacing that data. In this case it is instance.liked_by (the current contents of the entry).

    instance.save()
    

    This saves the new state of the instance.

    return instance
    

    returns the new instance, now with the validated data appended to it.

    I'm not sure if this is the most ideal, pythonic, or efficient way implementing a like feature to a social media web app, but it is a straightforward way of doing it. This code can be repurposed to add all sorts of many-to-many relationships into your models (friends lists/followers and tags for example).

    This is my understanding of what is going on here and I hope it can help make sense of the confusing topic of ManytoMany relationships for clearer.


  2. To update the liked_by Many2Many field, the serializer expect you to provide primary key(s).

    Just edit your HaikuSerializer like the following. It will work.

    class HaikuSerializer(serializers.ModelSerializer):
        liked_by = serializers.PrimaryKeyRelatedField(
            many=True,
            queryset=User.objects.all())
    
        class Meta:
            model = models.Haiku
            fields = ['created_by', 'body', 'liked_by', 'created_at']
    
        def update(self, instance, validated_data):
            liked_by = validated_data.pop('liked_by')
            for i in liked_by:
                instance.liked_by.add(i)
            instance.save()
            return instance
    
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search