Skip to content

Commit

Permalink
feat(rating-articles): Users can rate articles
Browse files Browse the repository at this point in the history
- create ratings app in apps folder
- Add ratings app to settings.py file
- Add model for ratings in models.py
- Create rating serializer in serializer.py
- Create corresponding views in views.py
- Add urls.py file to register the views
- Include the rating urls file in main urls.py file

[Starts #162949218]
  • Loading branch information
Allan690 committed Jan 31, 2019
1 parent 133f300 commit c9f1b3f
Show file tree
Hide file tree
Showing 17 changed files with 591 additions and 9 deletions.
2 changes: 1 addition & 1 deletion authors/apps/articles/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 2.1.4 on 2019-01-30 06:35
# Generated by Django 2.1.4 on 2019-01-30 15:26

from django.conf import settings
from django.db import migrations, models
Expand Down
18 changes: 18 additions & 0 deletions authors/apps/articles/migrations/0002_auto_20190130_1803.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.1.4 on 2019-01-30 18:03

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('articles', '0001_initial'),
]

operations = [
migrations.AlterField(
model_name='article',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
]
25 changes: 25 additions & 0 deletions authors/apps/articles/migrations/0003_rating.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 2.1.4 on 2019-01-31 08:06

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', '0002_auto_20190130_1803'),
]

operations = [
migrations.CreateModel(
name='Rating',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('rating', models.FloatField(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)),
],
),
]
13 changes: 10 additions & 3 deletions authors/apps/articles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
from django.db.models.signals import pre_save

from .utils import get_unique_slug
from django.db.models.signals import pre_save

# local imports
from ..authentication.models import User

Expand Down Expand Up @@ -59,7 +57,7 @@ class Article(models.Model):
description = models.CharField(max_length=230, blank=False)
body = models.TextField(blank=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateField(auto_now=True)
updated_at = models.DateTimeField(auto_now=True)
votes = GenericRelation(LikeDislike, related_query_name='articles')

def __str__(self):
Expand Down Expand Up @@ -102,3 +100,12 @@ class FavoriteArticle(models.Model):

def __str__(self):
return "{}".format(self.article)


class Rating(models.Model):
"""
Rating model
"""
rating = models.FloatField(null=False, default=0)
article = models.ForeignKey(Article, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
13 changes: 11 additions & 2 deletions authors/apps/articles/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,28 @@
from authors.apps.profiles.models import UserProfile
from authors.apps.articles.models import FavoriteArticle
from authors.apps.profiles.serializers import ProfileSerialiazer
from ..utils import get_article_rating


class ArticleSerializer(serializers.ModelSerializer):
author = serializers.SerializerMethodField()
body = serializers.CharField(required=True)
title = serializers.CharField(required=True)
description = serializers.CharField(required=True)
rating = serializers.SerializerMethodField(read_only=True)

def get_author(self, article):
author = ProfileSerialiazer(article.author.profiles)
return author.data

def get_rating(self, article):
return get_article_rating(article)

class Meta:
model = Article

fields = ['slug', 'title', 'description', 'body', 'created_at',
'updated_at', 'author']
'updated_at', 'author', 'rating']

def validate(self, data):

Expand Down Expand Up @@ -49,15 +54,19 @@ class UpdateArticleSerializer(serializers.ModelSerializer):
body = serializers.CharField(required=True)
title = serializers.CharField(required=True)
description = serializers.CharField(required=True)
rating = serializers.SerializerMethodField()

def get_author(self, article):
author = ProfileSerialiazer(article.author.profiles)
return author.data

def get_rating(self, article):
return get_article_rating(article)

class Meta:
model = Article
fields = ['slug', 'title', 'description', 'body', 'created_at',
'updated_at', 'author']
'updated_at', 'author', 'rating']


class CommentSerializer(serializers.ModelSerializer):
Expand Down
27 changes: 27 additions & 0 deletions authors/apps/ratings/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 2.1.4 on 2019-01-30 15:26

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.CreateModel(
name='Rating',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('rating', models.FloatField(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)),
],
),
]
24 changes: 24 additions & 0 deletions authors/apps/ratings/migrations/0002_auto_20190131_0806.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 2.1.4 on 2019-01-31 08:06

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('ratings', '0001_initial'),
]

operations = [
migrations.RemoveField(
model_name='rating',
name='article',
),
migrations.RemoveField(
model_name='rating',
name='user',
),
migrations.DeleteModel(
name='Rating',
),
]
Empty file.
26 changes: 26 additions & 0 deletions authors/apps/ratings/responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
error_messages = {
'maximum_rating': 'The maximum allowed rating is 5',
'minimum_rating': 'The minimum allowed rating is 1',
'not_exist': {
'message': 'Article not found'
},
'unauthorized': {
'error': 'You cannot rate your own article'
},
'not_rated': 'Sorry, there are no ratings yet',
'required': 'Please provide a valid rating',
'login': 'Please login to rate an article'
}

success_messages = {
'message': 'article rating',
'data': {
'article': 'another-post',
'average_rating': 3.0,
'rating': 3
}
}

successful_submission = {
'message': 'Rating submitted successfully!'
}
41 changes: 41 additions & 0 deletions authors/apps/ratings/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""
Serializer module for the ratings model
"""
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db.models import Avg
from rest_framework import serializers

from .responses import error_messages
from ..articles.models import Rating


class RatingSerializer(serializers.ModelSerializer):
"""
Serializer for the rating model
"""
rating = serializers.IntegerField(
required=True,
validators=[
MaxValueValidator(5, message=error_messages['maximum_rating']),
MinValueValidator(1, message=error_messages['minimum_rating'])
],
error_messages={
'required': 'Please provide a valid rating'
}
)
article = serializers.SerializerMethodField()
average_rating = serializers.SerializerMethodField()

def get_average_rating(self, rating_object):
"""Returns an average rating for an article"""
average_rating = Rating.objects.filter(
article=rating_object.article.id).aggregate(Avg('rating'))
return round(average_rating['rating__avg'], 1)

def get_article(self, rating_object):
"""Returns the slug of the article from the ratings table"""
return rating_object.article.slug

class Meta:
model = Rating
fields = ('article', 'average_rating', 'rating')
11 changes: 11 additions & 0 deletions authors/apps/ratings/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.urls import path
from .views import ArticleRatingAPIView

app_name = 'ratings'

article_rating = ArticleRatingAPIView.as_view()

urlpatterns = [
path('articles/<slug>/rate-article', article_rating,
name='article_rating'),
]
101 changes: 101 additions & 0 deletions authors/apps/ratings/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from rest_framework import status
from rest_framework.exceptions import NotFound
from rest_framework.generics import (
GenericAPIView
)
from rest_framework.permissions import (
IsAuthenticatedOrReadOnly
)
from rest_framework.response import Response

from .responses import (
error_messages, successful_submission, success_messages
)
from .serializers import RatingSerializer
from ..articles.models import Article, Rating
from ..utils import get_article_rating


class ArticleRatingAPIView(GenericAPIView):
queryset = Rating.objects.all()
serializer_class = RatingSerializer
permission_classes = (IsAuthenticatedOrReadOnly,)

@staticmethod
def get_article(slug):
"""Returns the first record in the articles table with the slug"""
article = Article.objects.all().filter(slug=slug).first()
return article

def post(self, request, slug):
"""POST Request to rate an article"""
rating = request.data
article = self.get_article(slug)

if check_article_exists(article):
return check_article_exists(article)

if request.user.id == article.author.id:
return Response(
error_messages['unauthorized'],
status=status.HTTP_403_FORBIDDEN
)
try:
current_article_rating = Rating.objects.get(
user=request.user.id,
article=article.id
)
serializer = self.serializer_class(
current_article_rating, data=rating)
except Rating.DoesNotExist:
serializer = self.serializer_class(data=rating)

serializer.is_valid(raise_exception=True)
serializer.save(user=request.user, article=article)
return Response({
'message': successful_submission['message'],
'data': serializer.data
}, status=status.HTTP_201_CREATED)

def get(self, request, slug):
"""Returns an article's ratings"""
article = self.get_article(slug)
rating = None

# check if the article exists
if check_article_exists(article):
return check_article_exists(article)

# if the user is authenticated fetch their ratings
if request.user.is_authenticated:
try:
rating = Rating.objects.get(
user=request.user, article=article
)
except Rating.DoesNotExist:
raise NotFound(
detail=error_messages['not_rated']
)
# for unauthenticated users
if rating is None:
average_rating = get_article_rating(article)

if request.user.is_authenticated is False:
return Response({
'article': article.slug,
'average_rating': average_rating,
'rating': error_messages['login']
}, status=status.HTTP_200_OK)
serializer = self.serializer_class(rating)
return Response({
'message': success_messages['message'],
'data': serializer.data
}, status=status.HTTP_200_OK)


def check_article_exists(article):
if not article:
return Response(
error_messages['not_exist'],
status=status.HTTP_404_NOT_FOUND
)
20 changes: 20 additions & 0 deletions authors/apps/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from authors.apps.articles.models import Rating
from django.db.models import Avg


def get_article_rating(article):
"""
Returns the average rating of an article
"""

# searches for the article with the given slug
# and returns the average ratings
average_rating = Rating.objects.filter(
article__slug=article.slug).aggregate(Avg('rating'))
response = average_rating['rating__avg']

# set the average rating to 0 if the article has not been rated
if average_rating['rating__avg'] is None:
response = average_rating['rating__avg'] = 0
return response
return round(response, 1)
1 change: 1 addition & 0 deletions authors/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
'authors.apps.core',
'authors.apps.profiles',
'authors.apps.articles',
'authors.apps.ratings'

]

Expand Down
Loading

0 comments on commit c9f1b3f

Please sign in to comment.