Skip to content

Commit

Permalink
ft(rating): implement rating an article 
Browse files Browse the repository at this point in the history
- add RateArticle model
- add rating method to Article model
- add RateArticleSerializer class
- create RateArticleView class
- add rating url

[Delivers #160617674]

ft(rating): implement rating an article

- rename author field in RateArticle to rated by

[Delivers #160617674]

ft(rating): implement rating an article

- rename rating method in Articles
- modify rating method

[Delivers #160617674]

ft(rating): implement rating an article

- add rate article tests to test_rate_and_like.py

[Delivers #160617674]
  • Loading branch information
nakiwuge committed Oct 12, 2018
1 parent b57c200 commit dc79d42
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 dc79d42

Please sign in to comment.