Skip to content

Commit

Permalink
165305760-Feature(Rating): Users are able to rate an article
Browse files Browse the repository at this point in the history
- User can rate an article
- fix codeclimate issues
- Return average ratings
- Added a standard response message

starts #165305760
  • Loading branch information
Ogutu-Brian authored and Bakley committed May 10, 2019
1 parent 25641ef commit d1d7f88
Show file tree
Hide file tree
Showing 14 changed files with 324 additions and 124 deletions.
22 changes: 10 additions & 12 deletions authors/apps/articles/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# Generated by Django 2.2 on 2019-05-10 07:26
# Generated by Django 2.2 on 2019-05-10 08:25

import autoslug.fields
import cloudinary.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion

Expand All @@ -12,7 +11,6 @@ class Migration(migrations.Migration):
initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
Expand All @@ -32,12 +30,17 @@ class Migration(migrations.Migration):
('image', cloudinary.models.CloudinaryField(max_length=255, verbose_name='image')),
('favorited', models.BooleanField(default=False)),
('favoritesCount', models.PositiveSmallIntegerField(default=0)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['created_at'],
},
),
migrations.CreateModel(
name='Favorite',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='Tag',
fields=[
Expand All @@ -49,16 +52,11 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
name='Favorite',
name='RatingModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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)),
('rate', models.IntegerField(default=0)),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rated_article', to='articles.Article')),
],
),
migrations.AddField(
model_name='article',
name='tags',
field=models.ManyToManyField(to='articles.Tag'),
),
]
43 changes: 43 additions & 0 deletions authors/apps/articles/migrations/0002_auto_20190510_0825.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated by Django 2.2 on 2019-05-10 08:25

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


class Migration(migrations.Migration):

initial = True

dependencies = [
('articles', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.AddField(
model_name='ratingmodel',
name='rated_by',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rated_by', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='favorite',
name='article',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='articles.Article'),
),
migrations.AddField(
model_name='favorite',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='article',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='article',
name='tags',
field=models.ManyToManyField(to='articles.Tag'),
),
]
24 changes: 24 additions & 0 deletions authors/apps/articles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,27 @@ class Favorite(models.Model):
"""
user = models.ForeignKey(User, on_delete=models.CASCADE)
article = models.ForeignKey(Article, on_delete=models.CASCADE)


class RatingModel(models.Model):
"""
Models for rating an article (0 - 5)
"""
article = models.ForeignKey(
Article, related_name='rated_article', on_delete=models.CASCADE)
rated_by = models.ForeignKey(
User, related_name='rated_by', on_delete=models.CASCADE)
rate = models.IntegerField(default=0)

def get_articles_details(self):
"""
Fetch relevant articles details
"""
return {
"slug": self.article.slug,
"title": self.article.title,
"description": self.article.description,
"body": self.article.body,
"created_at": self.article.created_at,
"updated_at": self.article.updated_at
}
45 changes: 41 additions & 4 deletions authors/apps/articles/serializers.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from rest_framework import serializers
from .models import Article, Favorite
import json
from collections import OrderedDict

from authors.apps.articles.models import Tag
from authors.apps.utils.baseserializer import BaseSerializer
from django.db.models import Avg
from rest_framework import serializers
from rest_framework.pagination import PageNumberPagination
from collections import OrderedDict
from rest_framework.response import Response

from .exceptions import ArticleNotFound
from authors.apps.utils.baseserializer import BaseSerializer
from .models import Article, Favorite, RatingModel


class ArticleSerializer(BaseSerializer):
Expand Down Expand Up @@ -112,3 +115,37 @@ class Meta:
fields = (
'user', 'article'
)
class RatingSerializer(ArticleSerializer):
"""
Serializer class to rate an article
"""

rated_by = serializers.ReadOnlyField()
article = serializers.ReadOnlyField(source='get_articles_details')
rate = serializers.IntegerField(
min_value=1,
max_value=5,
required=True
)
average_rating = serializers.SerializerMethodField(
method_name="calulate_average_rating"
)

class Meta:
model = RatingModel
fields = ('rate',
'average_rating',
'article',
'rated_by'
)

def calulate_average_rating(self, obj):
"""
Calculate the average rating of an article
"""
average_rate = RatingModel.objects.filter(article=obj.article,
).aggregate(rate=Avg('rate'))

if average_rate["rate"]:
return float('%.2f' % (average_rate["rate"]))
return 0
72 changes: 72 additions & 0 deletions authors/apps/articles/tests/basetests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import json

from django.contrib.auth import get_user_model
from rest_framework.test import APITestCase, APIClient
from rest_framework.reverse import reverse
Expand Down Expand Up @@ -257,6 +259,76 @@ def get_favorites(self):
content_type="application/json"
)

def rate_article(self):
"""
Pass slug to the url and post the ratings
"""
slug = str(self.article.slug)
url = reverse("articles:rating_articles", args=[slug])
return self.client.post(
url,
data=json.dumps({
"rate": 4
}),
content_type="application/json"
)

def update_rate_article(self):
"""
Pass slug to the url and post the ratings
"""
slug = str(self.article.slug)
url = reverse("articles:rating_articles", args=[slug])
return self.client.post(
url,
data=json.dumps({
"rate": 3
}),
content_type="application/json"
)

def rate_article_more_than_five(self):
"""
Pass slug to the url and post the ratings
"""
slug = str(self.article.slug)
url = reverse("articles:rating_articles", args=[slug])
return self.client.post(
url,
data=json.dumps({
"rate": 7
}),
content_type="application/json"
)

def rate_article_less_than_one(self):
"""
Pass slug to the url and post the ratings
"""
slug = str(self.article.slug)
url = reverse("articles:rating_articles", args=[slug])
return self.client.post(
url,
data=json.dumps({
"rate": -1
}),
content_type="application/json"
)

def rate_non_existing_article(self):
"""
Pass slug to the url and post the ratings
"""
slug = "how-to-let-go"
url = reverse("articles:rating_articles", args=[slug])
return self.client.post(
url,
data=json.dumps({
"rate": 4
}),
content_type="application/json"
)


class TagsBaseTest(APITestCase):
"""
Expand Down
49 changes: 49 additions & 0 deletions authors/apps/articles/tests/test_ratings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import json

from rest_framework.views import status
from .basetests import BaseTest


class RatingArticlesTest(BaseTest):
"""
Test cases for article ratings
"""
unauthorized = "Authentication credentials were not provided."

def test_rate_an_article_with_authorized_user(self):
"""
Test if an article is rated when no data is passed
"""
rating = self.rate_article()
self.assertEqual(rating.status_code, status.HTTP_201_CREATED)

def test_update_ratings_on_an_article_with_authorized_user(self):
"""
Test if an article is rated when no data is passed
"""
rating = self.update_rate_article()
self.assertEqual(rating.status_code, status.HTTP_201_CREATED)


def test_rate_an_article_with_greater_than_five(self):
"""
Test if an article is rated when the rate is more that 5
"""
rating = self.rate_article_more_than_five()
self.assertEqual(rating.status_code, status.HTTP_400_BAD_REQUEST)


def test_rate_an_article_with_less_than_one(self):
"""
Test if an article is rated when that rate is less than 1
"""
rating = self.rate_article_less_than_one()
self.assertEqual(rating.status_code, status.HTTP_400_BAD_REQUEST)

def test_rate_a_non_existing_article(self):
"""
Test if an article is rated when article doesn't exist
"""

rating = self.rate_non_existing_article()
self.assertEqual(rating.status_code, status.HTTP_404_NOT_FOUND)
16 changes: 8 additions & 8 deletions authors/apps/articles/urls.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
from django.conf.urls import url
from django.urls import path
from .views import (
ListCreateArticleAPIView,
RetrieveUpdateArticleAPIView,
FetchTags,
LikeDislikeView,
FavoritesView,
ListUserFavoriteArticlesView)

from .views import (FavoritesView, FetchTags, LikeDislikeView,
ListCreateArticleAPIView, ListUserFavoriteArticlesView,
RateArticleAPIView, RetrieveUpdateArticleAPIView)

app_name = 'articles'

urlpatterns = [
path('articles/', ListCreateArticleAPIView.as_view(), name='article'),
path('articles/<slug>/', RetrieveUpdateArticleAPIView.as_view(),
path('articles/<slug>', RetrieveUpdateArticleAPIView.as_view(),
name='articles'),
path('tags/', FetchTags.as_view(), name="all_tags"),
path('articles/<str:slug>/<str:vote_type>/vote/',
Expand All @@ -20,4 +18,6 @@
FavoritesView.as_view(), name='favorite'),
path('articles/favorites/me/',
ListUserFavoriteArticlesView.as_view(), name='get_favorite'),
path('articles/<slug>/rate/',
RateArticleAPIView.as_view(), name='rating_articles'),
]
Loading

0 comments on commit d1d7f88

Please sign in to comment.