0

Here I have a serializer and in that serializer is a get_is_liked method. This method is going to return a boolean, wheter the post has liked by the current user or not.

Now, I want to get the result of this method in the format of a json attribute like other fields. There's suppose to be a mobile application that sends request for the logged in user to show if the post has liked before or not.

serializer.py

class BookSerializer(serializers.ModelSerializer):
    user = serializers.SlugRelatedField(slug_field='username', read_only=True)

    class Meta:
        fields = (
            'id',
            'name',
            'description',
            'user',
            'likes'
        )
        model = models.Book

    def get_is_liked(self, obj):
        requestUser = self.context['request'].user
        return models.BookLike.objects.filter(
            book=obj, 
            liker=requestUser
        ).exists()

views.py

class ListBookView(generics.ListCreateAPIView):
    permission_classes = (IsAuthenticated, )
    queryset = models.Book.objects.all()
    serializer_class = serializers.BookSerializer

    def perform_create(self, serializer):
       serializer.save(user=self.request.user)

class DetailBookView(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = (IsOwnerOrReadOnly, )
    queryset = models.Book.objects.all()
    serializer_class = serializers.BookSerializer

EDITED:

models.py

class Book(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    name = models.CharField(max_length=125)
    description = models.CharField(max_length=225)
    likes = models.PositiveIntegerField(default=0)

def __str__(self):
   return self.name


class BookLike(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE)
    liker = models.ForeignKey(User, on_delete=models.CASCADE)

def __str__(self):
   return 'Post: {0}, Liked by {1}'.format(self.book, self.liker)

Now, I don't know how to do it?!

artick
  • 175
  • 1
  • 13

1 Answers1

1

Use SerializerMethodField to create a read-only field and gets its value by calling a method on the serializer class.

Create a field and a method on the serializer called get_<field_name>. This method have two arguments: self and the given object being serialized (just like your get_is_liked() method)

serializers.py

class BookSerializer(serializers.ModelSerializer):
    is_liked = serializers.SerializerMethodField()
    likes = serializers.SerializerMethodField()    

    def get_is_liked(self, obj):
        '''
        Returns a boolean that represents whether the book has 
        already been liked by the user
        '''
        return models.BookLike.objects.filter(
            book=obj, 
            liker=self.context['request'].user
        ).exists()

   def get_likes(self, obj):
       '''
       Returns the numer of likes of the book
       '''
       return models.BookLike.objects.filter(book=obj).count()

Implement an endpoint for Liking

get_is_liked() will always return False until the API allows the user to give a like for a book. Will be necessary create a new endpoint to do so.

I suggest you merge your views into a single viewset and add a extra action (see documentation) to perform the like funcionality. Something like:

from rest_framework.decorators import action


class BookViewSet(generics.ModelViewSet):
    permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
    queryset = models.Book.objects.all()
    serializer_class = serializers.BookSerializer

    @action(methods=['post'], detail=True)
    def like(self, request, pk=None):
        book = self.get_object()

        # Create a like for the book or get an existent one
        like, created = BookLike.objects.get_or_create(
            book=book, 
            liker=request.user
        )

        # User never gave a like for this book
        if created:
            return Response({
                'detail': 'Your like was registered with success.'
            })

        # Book already liked by the user (dislike or error?)
        return Response({
            'detail': 'Only one like per book is allowed.'
        }, status.HTTP_400_BAD_REQUEST)

urls.py

from rest_framework.routers import DefaultRouter

from django.urls import path, include

from . import views


router = DefaultRouter()
router.register(r'books', views.BookViewSet, base_name='book')

urlpatterns = [
    path('', include(router.urls)),
]

With this configuration, the endpoint for like is

POST /books/{bookId}/like/
Lucas Weyne
  • 1,107
  • 7
  • 17
  • Now, It shows is_liked field but, it's always false. my model fieldscode is: ` class Book(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) name = models.CharField(max_length=125) likes = models.PositiveIntegerField(default=0) class BookLike(models.Model): book = models.ForeignKey(BookSuggest, on_delete=models.CASCADE) liker = models.ForeignKey(User, on_delete=models.CASCADE) – artick Jan 02 '19 at 06:15
  • `is_liked` field is always `False` because your queryset is not finding any `BookLike` instance for logged user on database. Where are you put the code to perform a like on the book, i.e. create a `BookLike` instance for the logged user? Please, put your models (`Book` and `BookLike`) in the question description for easy viewing – Lucas Weyne Jan 02 '19 at 10:50
  • I did add that into the question. – artick Jan 02 '19 at 11:20
  • I use the User, and also have a foreign key, named liker in BookLike class. – artick Jan 02 '19 at 12:11
  • But, there is some endpoint to create a `BookLike` for the user? Something like `BookLike.objects.create(liker=request.user, book=book)` – Lucas Weyne Jan 02 '19 at 13:04
  • No, I mean, how can I do that? I just have a ListCreateAPIView to show them (fields) and send post request for creating a Book instance. – artick Jan 02 '19 at 13:17
  • I add a snippet to your viewset in this anwser, check if is util – Lucas Weyne Jan 02 '19 at 14:34
  • Another way is create a viewset and a serializer for `BookLike` to mange theese votes – Lucas Weyne Jan 02 '19 at 14:48
  • Thank you. but I have a question. Is this a new view that I should add into my code, or jush add the action to my own view? And another question is, I want the like field increase each time a user vote, how is this possible? And the last one is, how the url path is looks like for the `BookViewSet`. Thank you again. – artick Jan 02 '19 at 15:05
  • The action you put in your own viewset class. I see you have a `likes` attribute in your `Book` model, but you already have the count of likes for a book (you don't need increment it). Just call the reverse attribute of `Book` to list all `BookLike` for a specific book. Something like `book.booklike_set.count()`. You can change the name of the reverse attribute in book adding a attribute called `related_name` inside the `ForeignKey` definition for the `book` field of `BookLike`. Check [this](https://stackoverflow.com/a/2642645/7727181) – Lucas Weyne Jan 02 '19 at 16:15
  • The `BookViewSet` urls path depends on your viewset definition. For a `ModelViewSet` you will have all CRUD actions. The URL paths also depends of your [routers](https://www.django-rest-framework.org/api-guide/routers/) and URL definitions in you `urls.py` file. Check the [viewsets](https://www.django-rest-framework.org/api-guide/viewsets/) documentation to – Lucas Weyne Jan 02 '19 at 16:22
  • But I don't have any viewset. I've added all of my views.py in the question description. – artick Jan 02 '19 at 17:30
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/186071/discussion-between-lucas-weyne-and-artick). – Lucas Weyne Jan 02 '19 at 22:01