Skip to content

Commit

Permalink
Merge cc366a9 into 1926219
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewhingah committed Jan 27, 2019
2 parents 1926219 + cc366a9 commit ab78745
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 8 deletions.
25 changes: 25 additions & 0 deletions authors/apps/articles/migrations/0010_articlerating.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 2.1.4 on 2019-01-23 13:52

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('articles', '0009_auto_20190121_0754'),
]

operations = [
migrations.CreateModel(
name='ArticleRating',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('rate', models.IntegerField(default=0)),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='articles.Article')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
14 changes: 14 additions & 0 deletions authors/apps/articles/migrations/0013_merge_20190125_1538.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated by Django 2.1.5 on 2019-01-25 15:38

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('articles', '0012_merge_20190125_1317'),
('articles', '0010_articlerating'),
]

operations = [
]
12 changes: 12 additions & 0 deletions authors/apps/articles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,15 @@ class Article(models.Model):
User, related_name='likes', blank=True)
user_id_dislikes = models.ManyToManyField(
User, related_name='dislikes', blank=True)


class ArticleRating(models.Model):
"""
This is a model to handle article ratings
"""
user = models.ForeignKey(User, on_delete=models.CASCADE)
article = models.ForeignKey(Article, on_delete=models.CASCADE)
rate = models.IntegerField(default=0)

def __str__(self):
return self.rate
5 changes: 5 additions & 0 deletions authors/apps/articles/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,8 @@ class DeleteArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ['is_deleted']


class RatingSerializer(serializers.Serializer):
"""validate rating article"""
rate = serializers.IntegerField(required=True)
154 changes: 154 additions & 0 deletions authors/apps/articles/tests/test_article_ratings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import json
from django.urls import reverse
from rest_framework.test import APIClient, APITestCase
from rest_framework import status
from authors.apps.authentication.models import User


class TestArticleRatings(APITestCase):
def setUp(self):
self.client = APIClient()
self.login_url = reverse("authentication:auth-login")

self.article_author = User.objects.create_user(
username="testuser",
email="testuser@gmail.com",
password="this_user123@A")

self.author_data = {"user": {
"email": "testuser@gmail.com",
"password": "this_user123@A"
}}

login_res = self.client.post(
self.login_url, self.author_data, format='json')
self.token = login_res.data["token"]

article_details = {
"title": "your first blog",
"description": "this is your first blog",
"body": "this is your first blog",
"tagList": ["dragons", "training"],
}

self.response = self.client.post(
reverse("articles:articles-listcreate"),
data=json.dumps(article_details),
content_type="application/json",
HTTP_AUTHORIZATION='Token ' + self.token)

self.slug = self.response.data['slug']

self.rating_user = User.objects.create_user(
username="andrew",
email="andrew@gmail.com",
password="this_user123@A")

self.rating_user_data = {"user": {
"email": "andrew@gmail.com",
"password": "this_user123@A"
}}

login_res1 = self.client.post(
self.login_url, self.rating_user_data, format='json')
self.assertEqual(login_res1.status_code, status.HTTP_200_OK)
self.token1 = login_res1.data["token"]

def test_rate_article(self):
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token1)
slug = self.slug
data = {"rate": 4}
response = self.client.post(
"/api/articles/{}/rate/".format(slug), data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data['message'], 'Article successfully rated'
)
self.assertEqual(response.data["rating"]["rate"], 4)

def test_rate_with_number_outside_range(self):
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
slug = self.slug
data = {"rate": 6}
response = self.client.post(
"/api/articles/{}/rate/".format(slug), data=data,)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
'Give a rating between 1 to 5 inclusive', response.data['message'])

def test_rate_non_existing_article(self):
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
slug = "home-away"
data = {"rate": 3}
response = self.client.post(
"/api/articles/{}/rate/".format(slug), data=data)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertIn('Article not found', response.data['detail'])

def test_rating_by_unauthenticated_user(self):
slug = self.slug
data = {"rate": 4}
response = self.client.post(
"/api/articles/{}/rate/".format(slug), data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_rate_own_article(self):
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
slug = self.slug
data = {"rate": 4}
response = self.client.post(
"/api/articles/{}/rate/".format(slug), data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.data["error"],
"You are not allowed to rate your own article"
)

def test_update_ratings(self):
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token1)

# assert that the first rating is made successfully
slug = self.slug
data1 = {"rate": 4}
res1 = self.client.post(
"/api/articles/{}/rate/".format(slug), data=data1, format='json')
self.assertEqual(res1.status_code, status.HTTP_200_OK)

# assert that the rating is 4
res2 = self.client.get(
"/api/articles/rate/{}/".format(slug))
self.assertEqual(res2.status_code, status.HTTP_200_OK)
self.assertEqual(res2.data['ratings'], 4)

# assert that rating can be modified
data2 = {"rate": 3}
res3 = self.client.post(
"/api/articles/{}/rate/".format(slug), data=data2, format='json')
self.assertEqual(res3.status_code, status.HTTP_200_OK)

# assert that the new rating is 3
res4 = self.client.get(
"/api/articles/rate/{}/".format(slug))
self.assertEqual(res4.status_code, status.HTTP_200_OK)
self.assertEqual(res4.data['ratings'], 3)

def test_view_article_rating(self):
# assert that ratings of an article that's not rated is 0
slug = self.slug
res1 = self.client.get(
"/api/articles/rate/{}/".format(slug))
self.assertEqual(res1.status_code, status.HTTP_200_OK)
self.assertEqual(res1.data['ratings'], 0)

# assert that the article is rated
data = {"rate": 4}
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token1)
res2 = self.client.post(
"/api/articles/{}/rate/".format(slug), data=data, format='json')
self.assertEqual(res2.status_code, status.HTTP_200_OK)

# assert that the new rating is 4
res3 = self.client.get(
"/api/articles/rate/{}/".format(slug))
self.assertEqual(res3.status_code, status.HTTP_200_OK)
self.assertEqual(res3.data['ratings'], 4)
18 changes: 14 additions & 4 deletions authors/apps/articles/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from django.urls import path

from .views import (ArticleAPIView, ArticleDetailsView,
DeleteArticle, LikeArticleApiView, DislikeArticleApiView)
DeleteArticle, LikeArticleApiView,
DislikeArticleApiView, PostRatingsAPIView,
AverageRatingsAPIView
)


app_name = "articles"
urlpatterns = [
path('articles/', ArticleAPIView.as_view(), name="articles-listcreate"),
urlpatterns = [path('articles/', ArticleAPIView.as_view(), name="articles-listcreate"),
path('articles/<slug:slug>/',
ArticleDetailsView.as_view(),
name="articles-retrieveupdate"),
Expand All @@ -14,5 +18,11 @@
path('articles/<slug>/like/',
LikeArticleApiView.as_view(), name='likes'),
path('articles/<slug>/dislike/',
DislikeArticleApiView.as_view(), name='dislikes')
DislikeArticleApiView.as_view(), name='dislikes'),
path('articles/<slug>/rate/',
PostRatingsAPIView.as_view(),
name="rate_article"),
path('articles/rate/<slug>/',
AverageRatingsAPIView.as_view(),
name="articles_view_ratings")
]
93 changes: 89 additions & 4 deletions authors/apps/articles/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@
from django.core import serializers
from rest_framework import status
from rest_framework.response import Response
from rest_framework.generics import (ListCreateAPIView,
from rest_framework.generics import (CreateAPIView, ListCreateAPIView,
RetrieveUpdateAPIView, UpdateAPIView)
from rest_framework.views import APIView
from rest_framework.permissions import (IsAuthenticated,
IsAuthenticatedOrReadOnly)
from rest_framework.renderers import JSONRenderer
from rest_framework.exceptions import NotFound

from authors.apps.core.permissions import IsAuthorOrReadOnly
from . import serializers
from .serializers import (ArticleSerializer,
GetArticleSerializer, DeleteArticleSerializer)
from .models import Article
GetArticleSerializer, DeleteArticleSerializer,
RatingSerializer)
from .models import Article, ArticleRating


class ArticleAPIView(ListCreateAPIView):
Expand Down Expand Up @@ -131,3 +133,86 @@ def put(self, request, slug):
context={'request': request},
partial=True)
return Response(serializer.data, status.HTTP_200_OK)


class AverageRatingsAPIView(APIView):
"""
Class for viewing article ratings
"""

serializer_class = RatingSerializer

def get(self, request, slug):
"""method for viewing average ratings"""
rate = 0

try:
article = Article.objects.get(slug=slug)
except ObjectDoesNotExist:
raise NotFound('article not found')

rate_articles = ArticleRating.objects.filter(article=article)

for rate_article in rate_articles:
rate += rate_article.rate
rate_value = 0
if rate:
rate_value = rate / rate_articles.count()
return Response(
data={"ratings": round(rate_value, 1)}, status=status.HTTP_200_OK)


class PostRatingsAPIView(CreateAPIView):
"""
Class for rating an article
"""
permission_classes = (IsAuthenticated,)
serializer_class = RatingSerializer

def post(self, request, slug):
"""method for posting a rating"""
data = request.data
serializer = self.serializer_class(data=data)
serializer.is_valid(raise_exception=True)

if not int(data['rate']) > 0 or not int(data['rate']) <= 5:
return Response(
{"message": "Give a rating between 1 to 5 inclusive"},
status=status.HTTP_400_BAD_REQUEST
)

try:
article = Article.objects.get(slug=slug)
except ObjectDoesNotExist:
raise NotFound('Article not found')

author = article.author.id
if author == request.user.id:
return Response(
{"error": "You are not allowed to rate your own article"},
status=status.HTTP_400_BAD_REQUEST)

user = request.user
response = Response(
{
"message": "Article successfully rated",
"rating": serializer.data
},
status=status.HTTP_200_OK
)

# update the rating if there's one
try:
article_ratings = ArticleRating.objects.get(
user=user, article=article
)
article_ratings.rate = data['rate']
article_ratings.save()
return response
# create new rating if none exists
except Exception:
article_ratings = ArticleRating(
user=user, article=article, rate=data['rate']
)
article_ratings.save()
return response

0 comments on commit ab78745

Please sign in to comment.