Skip to content

Commit

Permalink
Merge pull request #26 from andela/ft-rate-article-160617674
Browse files Browse the repository at this point in the history
#160617674 rating articles
  • Loading branch information
Walukagga Patrick committed Oct 12, 2018
2 parents b57c200 + dc79d42 commit d3f8f22
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 29 deletions.
2 changes: 1 addition & 1 deletion authors/apps/articles/admin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from django.contrib import admin
from authors.apps.articles.models import Article
from authors.apps.articles.models import Article, RateArticle
admin.site.register(Article)
33 changes: 26 additions & 7 deletions authors/apps/articles/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from authors.apps.authentication.models import User
from django.conf import settings
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.db.models import Avg
from django.utils import timezone
from django.utils.text import slugify
from django.contrib.postgres.fields import ArrayField
from authors.apps.authentication.models import User
from .utils import get_unique_slug,time

from .utils import get_unique_slug, time

# Create your models here.

Expand Down Expand Up @@ -41,17 +44,33 @@ def __str__(self):
This string is used when a `Article` is printed in the console.
"""
return self.title

def save(self, *args, **kwargs):
if not self.slug:
self.slug = get_unique_slug(self, 'title', 'slug')
return super().save(*args, **kwargs)

def read(self):
read_time=time(self.body)
read_time = time(self.body)
return read_time


class Meta:
ordering = ['-created_at']


def average_rating(self):
ratings = RateArticle.objects.filter(
article=self).aggregate(Avg('rating'))
if ratings['rating__avg'] == None:
return 0
else:
return int(ratings['rating__avg'])


class RateArticle(models.Model):
rated_by = models.ForeignKey(User, blank=False, on_delete=models.CASCADE)

date_created = models.DateTimeField(auto_now_add=True)

article = models.ForeignKey(Article, blank=False, on_delete=models.CASCADE)

rating = models.IntegerField(blank=False, null=False, default=0)
26 changes: 26 additions & 0 deletions authors/apps/articles/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,29 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
return json.dumps({
'articles': data
})

class RateUserJSONRenderer(JSONRenderer):
charset = 'utf-8'

def render(self, data, media_type=None, renderer_context=None):
# If the view throws an error (such as the user can't be authenticated
# or something similar), `data` will contain an `errors` key. We want
# the default JSONRenderer to handle rendering errors, so we need to
# check for this case.
if type(data) != ReturnList:
errors = data.get('errors', None)
if errors is not None:
return super(RateUserJSONRenderer, self).render(data)

if type(data) == ReturnDict:
# single article
return json.dumps({
'rate': data
})

else:
# Finally, we can render our data under the "article" namespace.
return json.dumps({
'rate': data

})
35 changes: 29 additions & 6 deletions authors/apps/articles/serializers.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,38 @@
from authors.apps.articles.models import Article,RateArticle
from authors.apps.authentication.models import User
from rest_framework import serializers
from authors.apps.articles.models import Article


class ArticleSerializer(serializers.ModelSerializer):
"""Serializer for articles."""
author = serializers.ReadOnlyField(source='author.username')
read_time=serializers.ReadOnlyField(source='read')
read_time = serializers.ReadOnlyField(source='read')

class Meta:
model = Article
""" List all of the fields that could possibly be included in a request
or response, including fields specified explicitly above."""

fields = ('author','title','slug','description','body','created_at','updated_at','read_time')
read_only_fields = ('slug','author_id',)

fields = ('author', 'title', 'slug', 'description',
'body', 'created_at', 'updated_at', 'read_time','average_rating')
read_only_fields = ('slug', 'author_id',)


class RateArticleSerializer(serializers.ModelSerializer):
rated_by = serializers.ReadOnlyField(source='rated_by.username')
article = serializers.ReadOnlyField(source='article.slug')

class Meta:
model = RateArticle
fields = ['rated_by', 'date_created', 'rating', 'article']

def validate(self, data):
rating = data.get('rating')
if not rating in range(1, 6):
raise serializers.ValidationError(
"Your rating should be in range of 1 to 5."
)

return {
"rating": rating,
}
3 changes: 2 additions & 1 deletion authors/apps/articles/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from django.urls import path

from .views import ArticleAPIView, ArticleAPIDetailsView
from .views import ArticleAPIView, ArticleAPIDetailsView, RateArticleView

app_name = "articles"

urlpatterns = [
path("articles/", ArticleAPIView.as_view()),
path('articles/<str:slug>/', ArticleAPIDetailsView.as_view(),name='retrieveUpdateDelete'),
path('articles/<str:slug>/rating/', RateArticleView.as_view())
]
53 changes: 41 additions & 12 deletions authors/apps/articles/views.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
from django.shortcuts import render
from rest_framework import generics
from rest_framework import status
from rest_framework.permissions import IsAuthenticated,IsAuthenticatedOrReadOnly
from rest_framework.response import Response
import json
from authors.apps.articles.renderers import ArticleJSONRenderer
from authors.apps.articles.serializers import ArticleSerializer
from rest_framework.exceptions import PermissionDenied
from .models import Article
from rest_framework import serializers
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.shortcuts import get_object_or_404, render
from rest_framework import generics, serializers, status
from rest_framework.exceptions import PermissionDenied
from rest_framework.generics import ListCreateAPIView
from rest_framework.permissions import (IsAuthenticated,
IsAuthenticatedOrReadOnly)
from rest_framework.response import Response
from .models import Article, RateArticle
from .renderers import ArticleJSONRenderer, RateUserJSONRenderer
from .serializers import ArticleSerializer, RateArticleSerializer


class ArticleAPIView(generics.ListCreateAPIView):
Expand All @@ -25,7 +29,7 @@ def create(self, request, *args, **kwargs):
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

def perform_create(self, serializer):
serializer.save(author=self.request.user)

Expand All @@ -37,21 +41,46 @@ class ArticleAPIDetailsView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = ArticleSerializer
lookup_field = "slug"
queryset = Article.objects.all()

def destroy(self, request, *args, **kwargs):
instance = self.get_object()
if instance.author != request.user:
raise PermissionDenied
self.perform_destroy(instance)
return Response({"message":"article deleted"},status=status.HTTP_200_OK)
return Response({"message": "article deleted"}, status=status.HTTP_200_OK)

def update(self, request, *args, **kwargs):
article_dict = request.data.get("article", {})
partial = kwargs.pop('partial', False)
instance = self.get_object()
if instance.author != request.user:
raise PermissionDenied
serializer = self.get_serializer(instance, data=article_dict, partial=partial)
serializer = self.get_serializer(
instance, data=article_dict, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data, status=status.HTTP_201_CREATED)


class RateArticleView(ListCreateAPIView):
permission_classes = (IsAuthenticated,)
renderer_classes = (RateUserJSONRenderer,)
queryset = RateArticle.objects.all()
serializer_class = RateArticleSerializer

def create(self, request, *args, **kwargs):
article_slug = get_object_or_404(Article, slug=self.kwargs['slug'])
get_rated_article = RateArticle.objects.filter(
article_id=article_slug.id, rated_by_id=request.user.id)
if article_slug.author_id == request.user.id:
return Response({"msg": "you can not rate your own article"})
if get_rated_article:
return Response({"msg": "you have already rated this article"})
rating = request.data.get('rate', {})
serializer = self.serializer_class(data=rating)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer, article_slug)
return Response(serializer.data, status=status.HTTP_201_CREATED)

def perform_create(self, serializer, article_slug):
serializer.save(rated_by=self.request.user, article=article_slug)
64 changes: 64 additions & 0 deletions tests/test_articles/test_rate_and_like.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from rest_framework import status
from rest_framework.test import APIClient, APITestCase
from rest_framework import status
from ..test_authentication.test_base import BaseTest
from authors.apps.articles.models import Article
from authors.apps.authentication.models import User


class ArticlesTest(APITestCase, BaseTest):
def setUp(self):
BaseTest.__init__(self)
self.client = APIClient()

def create_login_user(self):
user = User.objects.create_user(
self.username, self.email, self.password)
User.is_verified = True
token = str(user.token(1))
self.loginresponse = self.client.post(
"/api/users/login/", self.user_login, format="json")
self.addcredentials(token)
self.client.post('/api/articles/', self.create_article, format="json")

def create_login_user2(self):
user = User.objects.create_user('mim', 'mim@gmail.com', 'Mim123@ghjo')
User.is_verified = True
token = str(user.token(1))
self.loginresponse = self.client.post(
"/api/users/login/", self.user_login, format="json")
self.addcredentials(token)

def addcredentials(self, response):
self.client.credentials(
HTTP_AUTHORIZATION='Token ' + response)

def test_valid_rating(self):
self.create_login_user()
self.create_login_user2()
response = self.client.post(
f'/api/articles/{self.slug}/rating/', self.rate_data, format="json")
self.assertEqual(response.status_code, 201)

def test_rating_own_article(self):
self.create_login_user()
response = self.client.post(
f'/api/articles/{self.slug}/rating/', self.rate_data, format="json")
self.assertIn("can not rate your own article", str(response.data))

def test_invalid_rating(self):
self.create_login_user()
self.create_login_user2()
self.rate_data['rate']['rating'] = 9
response = self.client.post(
f'/api/articles/{self.slug}/rating/', self.rate_data, format="json")
self.assertIn("range of 1 to 5.", str(response.data))

def test_rate_article_twice(self):
self.create_login_user()
self.create_login_user2()
self.client.post(
f'/api/articles/{self.slug}/rating/', self.rate_data, format="json")
response = self.client.post(
f'/api/articles/{self.slug}/rating/', self.rate_data, format="json")
self.assertIn("already rated this article", str(response.data))
13 changes: 11 additions & 2 deletions tests/test_authentication/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,14 @@ def __init__(self):
"body": "this is edited",
}
}
self.slug='how-to-tnnrain-your-flywwwwwwwwwwf'

self.slug = 'how-to-tnnrain-your-flywwwwwwwwwwf'
self.rate_data = {
"rate": {
"rating": 2,
}
}
self.like_data = {
"like": {
"likes": True,
}
}

0 comments on commit d3f8f22

Please sign in to comment.