-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(rating-articles): Users can rate articles
- 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
Showing
17 changed files
with
591 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
authors/apps/articles/migrations/0002_auto_20190130_1803.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)), | ||
], | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
24
authors/apps/ratings/migrations/0002_auto_20190131_0806.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!' | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -54,6 +54,7 @@ | |
'authors.apps.core', | ||
'authors.apps.profiles', | ||
'authors.apps.articles', | ||
'authors.apps.ratings' | ||
|
||
] | ||
|
||
|
Oops, something went wrong.