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]

fix typo and formatting
  • Loading branch information
kevpy committed Mar 18, 2019
1 parent 090c1fd commit 9e75a12
Show file tree
Hide file tree
Showing 6 changed files with 348 additions and 74 deletions.
90 changes: 80 additions & 10 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,8 +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 @@ -47,17 +51,17 @@ 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 Rate(models.Model):
"""
Rate model schema
Expand All @@ -74,3 +78,69 @@ class Rate(models.Model):
)

your_rating = models.FloatField(null=False)


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 single Article
: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)
88 changes: 70 additions & 18 deletions authors/apps/article/serializers.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
from rest_framework import serializers
from .models import Article
from django.core.validators import MinValueValidator, MaxValueValidator
from .models import Rate
from django.db.models import Avg

from rest_framework import serializers

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


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)
rates = serializers.SerializerMethodField()
author = UserSerializer(read_only=True)
likes = serializers.SerializerMethodField()
dislikes = serializers.SerializerMethodField()
likes_count = serializers.SerializerMethodField()
dislikes_count = serializers.SerializerMethodField()

def get_rates(self, obj):
"""
Expand All @@ -35,12 +40,60 @@ def get_rates(self, obj):
return average_rating

return average['your_rating__avg']


class Meta:
model = Article
fields = ['title', 'description',
'body', 'created_at', 'updated_at', 'slug', 'favourited','rates', '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()


class RateSerializer(serializers.ModelSerializer):
"""
Rating model serializers
Expand All @@ -60,7 +113,7 @@ class RateSerializer(serializers.ModelSerializer):
)
],
error_messages={
'required':'rating is required'
'required': 'rating is required'
}
)
article = serializers.SerializerMethodField()
Expand Down Expand Up @@ -92,4 +145,3 @@ def get_rate_count(self, obj):
class Meta:
model = Rate
fields = ('article', 'average_rating', 'rate_count', 'your_rating')

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)
7 changes: 5 additions & 2 deletions authors/apps/article/urls.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from django.urls import path
from .views import (ArticlesAPIView, RetrieveArticleAPIView, RateAPIView)

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

app_name = "articles"

urlpatterns = [
path('articles/', ArticlesAPIView.as_view()),
path('articles/<slug>', RetrieveArticleAPIView.as_view()),
path('rate/<slug>/', RateAPIView.as_view(), name='rate'),

path('articles/<slug>/like', LikeAPIView.as_view()),
path('articles/<slug>/dislike', DislikeAPIView.as_view()),
]


Loading

0 comments on commit 9e75a12

Please sign in to comment.