From 46170a3782bb4363cd82603084c31572c199ab19 Mon Sep 17 00:00:00 2001 From: Silas Kenneth Date: Tue, 11 Dec 2018 09:58:26 +0300 Subject: [PATCH] feat(rate articles): Allow users rate articles - This delivers code to allow users rate articles in the api [Delivers #161966611] --- authors/apps/articles/models.py | 19 ++++- authors/apps/articles/serializers.py | 23 ++++-- authors/apps/articles/tests/test_ratings.py | 76 +++++++++++++++++ authors/apps/articles/urls.py | 6 +- authors/apps/articles/views.py | 82 +++++++++++++++++-- .../tests/test_token_generation.py | 1 - 6 files changed, 186 insertions(+), 21 deletions(-) create mode 100644 authors/apps/articles/tests/test_ratings.py diff --git a/authors/apps/articles/models.py b/authors/apps/articles/models.py index 25d6f16..424361f 100644 --- a/authors/apps/articles/models.py +++ b/authors/apps/articles/models.py @@ -4,6 +4,7 @@ from ..authentication.models import User + class Article(models.Model): """ Article model class. @@ -19,7 +20,7 @@ class Article(models.Model): # auto_now is updated with change created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) - + def __str__(self): """ Return the article title. @@ -34,10 +35,22 @@ def create_slug(self): while Article.objects.filter(article_slug=slug).exists(): slug = slug + '-' + uuid.uuid4().hex return slug - + def save(self, *args, **kwargs): """ Edit the save function to include the created slug. """ self.article_slug = self.create_slug() - super().save(*args,**kwargs) + super().save(*args, **kwargs) + + +class Rating(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + article = models.ForeignKey(Article, on_delete=models.CASCADE) + value = models.IntegerField(choices=zip(range(1, 6), (1, 6))) + + class Meta: + db_table = "ratings" + # Create a unique key which is a combination of + # two fields + unique_together = ("article", "user") diff --git a/authors/apps/articles/serializers.py b/authors/apps/articles/serializers.py index 68b47c7..6cc611a 100644 --- a/authors/apps/articles/serializers.py +++ b/authors/apps/articles/serializers.py @@ -1,6 +1,5 @@ from rest_framework import serializers - -from .models import Article +from .models import Article, Rating from ..authentication.serializers import UserSerializer @@ -12,7 +11,7 @@ class ArticleSerializer(serializers.ModelSerializer): description = serializers.CharField() body = serializers.CharField() author = serializers.HiddenField( - default = serializers.CurrentUserDefault() + default=serializers.CurrentUserDefault() ) class Meta: @@ -26,13 +25,14 @@ def update(self, instance, data): instance.title = data.get('title', instance.title) instance.description = data.get('description', instance.description) instance.body = data.get('body', instance.body) - instance.author_id = data.get('authors_id',instance.author_id) + instance.author_id = data.get('authors_id', instance.author_id) instance.save() return instance - def get_author(self,Article): + def get_author(self, Article): return Article.author.pk + class ArticleAuthorSerializer(serializers.ModelSerializer): """ Class to serialize article and return the full owner information. @@ -40,8 +40,17 @@ class ArticleAuthorSerializer(serializers.ModelSerializer): title = serializers.CharField(max_length=200) description = serializers.CharField() body = serializers.CharField() - author = UserSerializer(read_only = True) + author = UserSerializer(read_only=True) + class Meta: model = Article fields = '__all__' - \ No newline at end of file + + +class RatingSerializer(serializers.ModelSerializer): + rating = serializers.IntegerField(source='rating.value', + required=True, allow_null=False, ) + + class Meta: + model = Rating + fields = ('rating',) diff --git a/authors/apps/articles/tests/test_ratings.py b/authors/apps/articles/tests/test_ratings.py new file mode 100644 index 0000000..94ce895 --- /dev/null +++ b/authors/apps/articles/tests/test_ratings.py @@ -0,0 +1,76 @@ +import json + +from django.urls import reverse +from rest_framework.test import (APIClient, + APITestCase) +from rest_framework import status + + +class TestRatings(APITestCase): + def setUp(self): + self.token = self.login().get("token", "") + self.user_test = dict( + email='silaskenn@gmail.com', + username='silaskenn', + password='Password@2019' + ) + self.REGISTER_URL = reverse("authentication:user-signup") + self.LOGIN_URL = reverse("authentication:user-login") + self.BASE_URL = 'http://localhost:8000/api/' + # self.RATING_URL = self.BASE_URL + "articles/good-father/rate/" + self.RATING_URL = reverse("articles:rate-article", kwargs={'slug': 'good-father'}) + self.create_article() + self.client = APIClient() + + def test_cannot_rate_without_token(self): + content = self.client.post(self.RATING_URL, data={'rating': 4}) + self.assertEqual(content.status_code, status.HTTP_403_FORBIDDEN) + + def test_cannot_rate_without_rating(self): + content = self.client.post(self.RATING_URL, data={}) + self.assertEqual(content.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(content.data.get('rating'), None) + + def test_cannot_rate_with_bad_range(self): + content = self.client.post(self.RATING_URL, data={'rating': 8}, HTTP_AUTHORIZATION='Token ' + self.token) + self.assertEqual(content.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(content.data.get('errors', {}).get('rating', 'message'), 'Specify a valid rating between 1 and 5 inclusive') + + def test_cannot_rate_with_non_numeric(self): + content = self.client.post(self.RATING_URL, data={'rating': 's0'}, HTTP_AUTHORIZATION='Token '+self.token) + self.assertEqual(content.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(content.data.get('errors', {}).get('rating', 'message'), 'Specify a valid rating between 1 and 5 inclusive') + + def test_can_rate_article(self): + content = self.client.post(self.RATING_URL, data={'rating': 4}, HTTP_AUTHORIZATION='Token ' + self.token) + self.assertEqual(content.status_code, status.HTTP_201_CREATED) + self.assertEqual(content.data.get('article', {}).get('message', 'message')[-1:-5:-1][::-1], '4/5!') + + def login(self): + self.register() + user_test = dict(user=dict( + email='silaskenn@gmail.com', + username='silaskenn', + password='Password@2019' + )) + logged = self.client.post(reverse("authentication:user-login"), data=json.dumps(user_test), + content_type="application/json") + return logged.data + + def register(self): + user_test = dict(user=dict( + email='silaskenn@gmail.com', + username='silaskenn', + password='Password@2019' + )) + self.client.post(reverse("authentication:user-signup"), data=json.dumps(user_test), + content_type="application/json") + + def create_article(self): + article = dict(article=dict( + title="Good father", + description='Something good from someone', + body='Something from the shitty mess' + )) + self.client.post(reverse("articles:articles"), data=json.dumps(article), + content_type="application/json", HTTP_AUTHORIZATION='Token ' + self.token) diff --git a/authors/apps/articles/urls.py b/authors/apps/articles/urls.py index 3cd87b5..8cf5a6e 100644 --- a/authors/apps/articles/urls.py +++ b/authors/apps/articles/urls.py @@ -1,8 +1,8 @@ from django.urls import path - -from .views import ArticleAPIView +from .views import ArticleAPIView, RatingsAPIView urlpatterns = [ path('articles/', ArticleAPIView.as_view(), name='articles'), - path('articles/', ArticleAPIView.as_view(), name='articles') + path('articles/', ArticleAPIView.as_view(), name='articles'), + path('articles//rate/', RatingsAPIView.as_view(), name='rate-article'), ] \ No newline at end of file diff --git a/authors/apps/articles/views.py b/authors/apps/articles/views.py index 36776f7..5803dd4 100644 --- a/authors/apps/articles/views.py +++ b/authors/apps/articles/views.py @@ -1,15 +1,18 @@ -from django.shortcuts import render -from rest_framework import permissions -from rest_framework.exceptions import PermissionDenied +import jwt +from django.conf import settings from rest_framework import status -from rest_framework.views import APIView -from requests.exceptions import HTTPError +from rest_framework.permissions import IsAuthenticatedOrReadOnly from rest_framework.response import Response -from rest_framework.permissions import AllowAny, IsAuthenticated, IsAuthenticatedOrReadOnly +from rest_framework.views import APIView -from .permissions import IsOwnerOrReadOnly +from authors.apps.authentication.models import User from .models import Article +from .models import ( + Rating) +from .permissions import IsOwnerOrReadOnly from .serializers import ArticleSerializer, ArticleAuthorSerializer +from .serializers import ( + RatingSerializer) class ArticleAPIView(APIView): @@ -78,3 +81,68 @@ def delete(self, request, pk): 'message':"article '{}' has been successfully deleted.".format(article.title) } return Response(message, status=status.HTTP_200_OK) + +# Create your views here. + + +class RatingsAPIView(APIView): + # The ratings view for handling http requests + serializer_class = RatingSerializer + + def post(self, request, *args, **kwargs): + slug = kwargs.get("slug", '') + try: + article = Article.objects.get(article_slug=slug) + except Article.DoesNotExist: + article = None + if article is None: + return Response({"message": "The article with slug %s does not exist" % slug}, + status=status.HTTP_404_NOT_FOUND) + else: + pass + try: + valid_user = request.user + # valid_user = User.objects.get(email=user.get('email', 'not_there')) + # First check if the token is really having an associated + # user. Because sometimes an account might be deactivated or deleted + # before the token expires which might cause the application to save + # a rating for an Invalid user + if not valid_user: + return Response({"message": "Your account is not available or might be disabled"}, + status=status.HTTP_401_UNAUTHORIZED) + invalid_rating = False + try: + rating = request.data.get('rating', None) + # Incase the user never provided the rating in the request + # throw an error + if rating is None: + return Response(dict(errors=dict( + message="Missing rating field" + ))) + rating = int(rating) + if not 1 <= rating <= 5: + invalid_rating = True + response = dict(errors= + dict(rating="Specify a valid rating between 1 and 5 inclusive")) + except ValueError: + response = dict(errors= + dict(rating="Specify a valid rating between 1 and 5 inclusive")) + invalid_rating = True + # Check if what was sent by the user is really a number + # if the number if None + if invalid_rating: + return Response(response, status=status.HTTP_400_BAD_REQUEST) + obje, created = Rating.objects.update_or_create(user=valid_user, + article=article, + defaults={"value": rating, + 'user_id': valid_user.id, + 'article_id': article.id}) + obje.save() + response = dict(article={"message": "You successfully rated the article %s %d/5!" + % (article.title, obje.value)}) + return Response(response, status=status.HTTP_201_CREATED) + except Exception as ex: + response = dict(errors={"message": "There was a problem sending the rating" + " try again later."}) + print(ex) + return Response(response, status=status.HTTP_503_SERVICE_UNAVAILABLE) diff --git a/authors/apps/authentication/tests/test_token_generation.py b/authors/apps/authentication/tests/test_token_generation.py index 7d85640..8a2b1d9 100644 --- a/authors/apps/authentication/tests/test_token_generation.py +++ b/authors/apps/authentication/tests/test_token_generation.py @@ -37,7 +37,6 @@ def test_can_get_login_token(self): data=json.dumps(self.user2), content_type="application/json") self.assertEqual(posted.status_code, status.HTTP_201_CREATED) - print(content.data) self.assertEqual(content.status_code, status.HTTP_200_OK) token_from_login = content.data.get('token', '')