Skip to content

Commit

Permalink
feature(like/dislike articles): Like and dislike articles by users
Browse files Browse the repository at this point in the history
- user can like article
- user can dislike article
- return liked/disliked article after like/dislike
- user can undo a like /dislike
- test the feature works

[Finishes #164047068]
  • Loading branch information
kevpy committed Mar 18, 2019
1 parent afe909f commit 89068fc
Show file tree
Hide file tree
Showing 6 changed files with 295 additions and 28 deletions.
87 changes: 78 additions & 9 deletions authors/apps/article/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from cloudinary.models import CloudinaryField
from django.db import models
from django.utils.text import slugify
from django.shortcuts import get_object_or_404
from django.urls import reverse
from cloudinary.models import CloudinaryField
from django.utils.text import slugify
from rest_framework import status
from rest_framework.exceptions import APIException
from rest_framework.response import Response

from authors.apps.authentication.models import User

Expand All @@ -15,7 +19,8 @@ class Article(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
favourited = models.BooleanField(default=False)
author = models.ForeignKey(User, related_name="articles", on_delete=models.CASCADE)
author = models.ForeignKey(
User, related_name="articles", on_delete=models.CASCADE)

def __str__(self):
return self.title
Expand Down Expand Up @@ -46,14 +51,78 @@ class ArticleImage(models.Model):
Article,
related_name='article_images',
on_delete=models.CASCADE,
verbose_name='Image associated with the article'
)
image = CloudinaryField('image', default="image/upload/v1551960935/books.png")
verbose_name='Image associated with the article')
image = CloudinaryField(
'image', default="image/upload/v1551960935/books.png")
created = models.DateTimeField(
auto_now=True,
verbose_name='When was this image saved'
)
auto_now=True, verbose_name='When was this image saved')
description = models.CharField(db_index=True, max_length=255)

class Meta:
ordering = ('created',)


class ArticleLikes(models.Model):
"""This class creates a model for Article likes and dislikes"""
user = models.ForeignKey(
User, related_name='liked_by', on_delete=models.CASCADE)
article = models.ForeignKey(
Article,
to_field='slug',
db_column='article',
on_delete=models.CASCADE,
related_name='liked')
likes = models.IntegerField(null=True)
dislikes = models.IntegerField(null=True)
created = models.DateTimeField(auto_now=True)

class Meta:
ordering = ('created',)

@staticmethod
def get_article(slug):
""" This method gets a sin gle Articel
:param slug: The Article's unique slug
:return: returns a serialized Article
"""
article = get_object_or_404(Article, slug=slug)
return article

@staticmethod
def user_likes(user, slug, ArticleSerializer, value):
"""This method creates an Article like or dislike.
:param user: The user liking or disliking an Article
:param slug: The Article unique slug
:param value: The value for like or dislike.
Takes in a +1 for like or -1 for dislike.
:return: success message or fail
"""
try:
likes = ArticleLikes.objects.filter(user=user, article=slug)
article = ArticleLikes.get_article(slug=slug)
except:
APIException.status_code = status.HTTP_404_NOT_FOUND
raise APIException(
{'article': {
'message': 'Article requested does not exist'
}})
if not likes:
if value == 1:
ArticleLikes.objects.create(
user=user, article=article, likes=value)
return Response(
{'message': 'Successfully liked: {} article'.format(slug),
'article': ArticleSerializer(article).data},
status=status.HTTP_201_CREATED)
ArticleLikes.objects.create(
user=user, article=article, dislikes=value)
return Response(
{'message': 'Successfully disliked: {} article'.format(slug),
'article': ArticleSerializer(article).data},
status=status.HTTP_201_CREATED)
likes.delete()
return Response({
'message':
'Successfully undid (dis)like on {} article'.format(slug),
'article': ArticleSerializer(article).data
}, status=status.HTTP_202_ACCEPTED)
74 changes: 63 additions & 11 deletions authors/apps/article/serializers.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,77 @@
from rest_framework import serializers
from .models import Article

from authors.apps.authentication.serializers import UserSerializer
from .models import Article, ArticleLikes


class LikesSerializer(serializers.ModelSerializer):
"""
This class serializes data from ArticleLikes model
"""
user = UserSerializer()

class Meta:
model = ArticleLikes
fields = ('user', )


class ArticleSerializer(serializers.ModelSerializer):
"""
converts the model into JSON format
"""

title = serializers.CharField(required=True)
slug = serializers.SlugField(required=False)
description = serializers.CharField(required=True)
body = serializers.CharField(required=True)
created_at = serializers.DateTimeField(read_only=True)
updated_at = serializers.DateTimeField(read_only=True)
favourited = serializers.BooleanField(required=False)
author = UserSerializer(read_only=True)
likes = serializers.SerializerMethodField()
dislikes = serializers.SerializerMethodField()
likes_count = serializers.SerializerMethodField()
dislikes_count = serializers.SerializerMethodField()

class Meta:
model = Article
fields = ['title', 'description',
'body', 'created_at', 'updated_at', 'slug', 'favourited', 'author']
fields = [
'title',
'description',
'body',
'created_at',
'updated_at',
'slug',
'favourited',
'author',
'likes_count',
'dislikes_count',
'likes',
'dislikes',
]

def get_likes(self, obj):
"""
This method returns users who liked an Article
:param obj: This is the Article object
:return: users who liked an article
"""
query = obj.liked.filter(likes=1)
return LikesSerializer(query, many=True).data

def get_dislikes(self, obj):
"""
This method returns users who disliked an Article
:param obj: This is the Article object
:return: users who liked an article
"""
query = obj.liked.filter(dislikes=-1)
return LikesSerializer(query, many=True).data

def get_likes_count(self, obj):
"""
This method returns number of users who liked an Article
:param obj: This is the Article object
:return: count of users who liked an article
"""
return obj.liked.filter(likes=1).count()

def get_dislikes_count(self, obj):
"""
This method returns number of users who disliked an Article
:param obj: This is the Article object
:return: count of users who disliked an article
"""
return obj.liked.filter(dislikes=-1).count()
4 changes: 2 additions & 2 deletions authors/apps/article/tests/test_article.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@ def test_same_slug(self):
format='json')
result = json.loads(response.content)

self.assertIn('the slug is already used', str(result))
self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
self.assertIn('article with this slug already exists.', str(result))
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_view_all_articles(self):
"""
Expand Down
112 changes: 112 additions & 0 deletions authors/apps/article/tests/test_article_likes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import json

from rest_framework.test import APIClient, APITestCase
from rest_framework.views import status


class TestArticleLikes(APITestCase):
""" This class tests the Article like and dislike feature
"""

client = APIClient()

def setUp(self):
""" This is the set-up method for all tests
"""
self.user = {"user": {
"username": "kibet",
"email": "kibet@olympians.com",
"password": "qwerty12"
}}

self.profile = {
"bio": "am fantastic",
"interests": "football",
"favorite_quote": "Yes we can",
"mailing_address": "P.O BOX 1080",
"website": "http://www.web.com",
"active_profile": True
}

self.article = {
"title": "Andela",
"description": "sdsd",
"body": "dsd",
"images": ""
}

# create user
self.client.post(
'/api/users/', self.user, format='json')

response = self.client.post(
'/api/users/login/', self.user, format='json')

result = json.loads(response.content)

self.client.credentials(
HTTP_AUTHORIZATION='Token ' + result["user"]["token"])

profile = self.client.post(
'/api/profile/create_profile/', self.profile, format='json')
prof_result = json.loads(profile.content)

article = self.client.post('/api/articles/', self.article,
format='json')

article_result = json.loads(article.content)
self.slug = article_result["article"]["slug"]

def test_like_article(self):
"""
Test like an article
"""
#
response = self.client.post('/api/articles/' + self.slug + '/like',
format='json')
result = json.loads(response.content)

self.assertIn('Successfully liked: {} article'.format(self.slug),
str(result))
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

def test_dislike_article(self):
"""
Test like an article
"""
#
response = self.client.post('/api/articles/' + self.slug + '/dislike',
format='json')
result = json.loads(response.content)

self.assertIn('Successfully disliked: {} article'.format(self.slug),
str(result))
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

def test_like_non_existing_article(self):
"""
Test like an article
"""
#
response = self.client.post('/api/articles/random/like',
format='json')
result = json.loads(response.content)

self.assertIn('Article requested does not exist'.format(self.slug),
str(result))
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

def test_unlike_article(self):
"""
Test like an article
"""
#
self.client.post('/api/articles/' + self.slug + '/like',
format='json')
response = self.client.post('/api/articles/' + self.slug + '/like',
format='json')
result = json.loads(response.content)

self.assertIn('Successfully undid (dis)like on {} article'.format(self.slug),
str(result))
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
4 changes: 3 additions & 1 deletion authors/apps/article/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from django.urls import path

from .views import (ArticlesAPIView, RetrieveArticleAPIView)
from .views import (ArticlesAPIView, RetrieveArticleAPIView, LikeAPIView, DislikeAPIView)

app_name = "articles"

urlpatterns = [
path('articles/', ArticlesAPIView.as_view()),
path('articles/<slug>', RetrieveArticleAPIView.as_view()),
path('articles/<slug>/like', LikeAPIView.as_view()),
path('articles/<slug>/dislike', DislikeAPIView.as_view()),
]
42 changes: 37 additions & 5 deletions authors/apps/article/views.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from django.db.utils import IntegrityError
from rest_framework import status
from rest_framework.exceptions import APIException
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status, mixins, viewsets
from rest_framework.exceptions import APIException
from rest_framework.views import APIView

from .models import Article, ArticleImage
from .serializers import ArticleSerializer
from .models import Article, ArticleImage, ArticleLikes
from .renderer import ArticleJSONRenderer
from .serializers import ArticleSerializer


class ArticlesAPIView(APIView):
Expand Down Expand Up @@ -69,7 +69,7 @@ def get(self, request, slug):
try:
article = Article.objects.get(slug=slug)
serializer = ArticleSerializer(article, many=False)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response({'article': serializer.data}, status=status.HTTP_200_OK)
except Article.DoesNotExist:
return Response({"message": "The article requested does not exist"}, status=status.HTTP_404_NOT_FOUND)

Expand Down Expand Up @@ -106,3 +106,35 @@ def delete(self, request, slug):
return Response(response, status=status.HTTP_401_UNAUTHORIZED)
response = {"message": "article deleted"}
return Response(response, status=status.HTTP_202_ACCEPTED)


class LikeAPIView(APIView):
""" This class proviedes a view class to like and unlike an Article
:return: http Response mesage
"""
permission_classes = (IsAuthenticated,)

def post(self, request, slug):
"""This method provides a POST view to like an article.
:param request: http request object
:param slug: Article slug field
:return: http Response message
"""
message = ArticleLikes.user_likes(request.user, slug, ArticleSerializer, 1)
return message


class DislikeAPIView(APIView):
""" This class proviedes a view class to like and unlike an Article
:return: http Response mesage
"""
permission_classes = (IsAuthenticated,)

def post(self, request, slug):
"""This method provides a POST view to dislike an article.
:param request: http request object
:param slug: Article slug field
:return: http Response message
"""
message = ArticleLikes.user_likes(request.user, slug, ArticleSerializer, -1)
return message

0 comments on commit 89068fc

Please sign in to comment.