From 32a8a3a8509cf0a64d1db2335d8ca219da1847bc Mon Sep 17 00:00:00 2001 From: Erastus Ruiru Date: Thu, 31 Jan 2019 10:14:33 +0300 Subject: [PATCH] ft(user-read-stat):Get the articles read by user - user should be able to see the articles they have read - write tests for the funcionality [Delivers #162948931] --- authors/apps/articles/tests/test_articles.py | 60 ++++- authors/apps/articles/views.py | 13 ++ authors/apps/authentication/messages.py | 6 + authors/apps/reading_stats/__init__.py | 0 authors/apps/reading_stats/admin.py | 3 + authors/apps/reading_stats/apps.py | 5 + .../apps/reading_stats/migrations/__init__.py | 0 authors/apps/reading_stats/models.py | 20 ++ authors/apps/reading_stats/serializers.py | 22 ++ authors/apps/reading_stats/tests/__init__.py | 0 .../reading_stats/tests/test_read_stats.py | 219 ++++++++++++++++++ authors/apps/reading_stats/urls.py | 12 + authors/apps/reading_stats/views.py | 48 ++++ authors/settings.py | 1 + authors/urls.py | 2 +- 15 files changed, 401 insertions(+), 10 deletions(-) create mode 100644 authors/apps/reading_stats/__init__.py create mode 100644 authors/apps/reading_stats/admin.py create mode 100644 authors/apps/reading_stats/apps.py create mode 100644 authors/apps/reading_stats/migrations/__init__.py create mode 100644 authors/apps/reading_stats/models.py create mode 100644 authors/apps/reading_stats/serializers.py create mode 100644 authors/apps/reading_stats/tests/__init__.py create mode 100644 authors/apps/reading_stats/tests/test_read_stats.py create mode 100644 authors/apps/reading_stats/urls.py create mode 100644 authors/apps/reading_stats/views.py diff --git a/authors/apps/articles/tests/test_articles.py b/authors/apps/articles/tests/test_articles.py index 3d76c43..018e803 100644 --- a/authors/apps/articles/tests/test_articles.py +++ b/authors/apps/articles/tests/test_articles.py @@ -4,6 +4,7 @@ from ..models import Article from ...authentication.models import User +from authors.apps.authentication.messages import read_stats_message class ArticleTestCase(APITestCase): @@ -19,6 +20,7 @@ def setUp(self): self.all_article_url = reverse("articles:articles") self.login_url = reverse("auth:login") self.register = reverse("auth:register") + self.read_stats = reverse("read:user_read_stats") self.client = APIClient() self.valid_article_data = { "article": { @@ -77,6 +79,13 @@ def get_slug_from_title(self, title): ) return specific_article_url + def get_slug(self, title): + """ + Get slug + """ + slug = slugify(title) + return slug + def create_article(self, data): """ Create an article @@ -117,22 +126,55 @@ def test_get_one_article(self): ) self.assertEqual(response.status_code, 200) - - def test_read_time(self): + def test_get_stat(self): """ Test GET /api/v1/article// """ token = self.login(self.user_data) self.create_article(self.valid_article_data['article']) - url = self.get_slug_from_title( - self.valid_article_data['article']['title']) response = self.client.get( - url, - format="json", - HTTP_AUTHORIZATION="Bearer {}".format(token) - ) + self.read_stats, + content_type='application/json', + HTTP_AUTHORIZATION="Bearer {}".format(token) + ) self.assertEqual(response.status_code, 200) - self.assertIn(response.data['read_time'],'0:01:00') + + def test_read(self): + """ + Test reading an article that doesnot exist + """ + token = self.login(self.user_data) + self.create_article(self.valid_article_data['article']) + slug = "its-a-test-article" + response = self.client.get( + reverse("read:article_read", kwargs={ + "slug":slug + }), + content_type='application/json', + HTTP_AUTHORIZATION="Bearer {}".format(token) + ) + self.assertEqual(response.status_code, 404) + self.assertIn(response.data['message'], + read_stats_message['read_error']) + + def test_read_new(self): + """ + + """ + token = self.login(self.user_data) + self.create_article(self.valid_article_data['article']) + slug = self.get_slug(self.valid_article_data['article']['title']) + response = self.client.get( + reverse("read:article_read", kwargs={ + "slug":slug + }), + content_type='application/json', + HTTP_AUTHORIZATION="Bearer {}".format(token) + ) + self.assertEqual(response.status_code, 404) + self.assertIn(response.data['message'], + read_stats_message['read_error']) + def test_remove_one_article(self): """ diff --git a/authors/apps/articles/views.py b/authors/apps/articles/views.py index 8a84f9b..de76e78 100644 --- a/authors/apps/articles/views.py +++ b/authors/apps/articles/views.py @@ -24,6 +24,8 @@ from .serializers import ArticleSerializer, TagSerializers from .messages import error_msgs, success_msg +from authors.apps.reading_stats.models import ReadStats + class ArticleAPIView(generics.ListCreateAPIView): """ @@ -92,6 +94,17 @@ def get(self, request, slug, *args, **kwargs): raise exceptions.NotFound({ "message": error_msgs['not_found'] }) + #this checks if an istance of read exists + #if it doesn't then it creates a new one + if request.user.id: + if not ReadStats.objects.filter(user=request.user, article=article).exists(): + user_stat = ReadStats( + user = request.user, + article = article + ) + user_stat.article_read = True + user_stat.save() + serializer = ArticleSerializer( article, context={ diff --git a/authors/apps/authentication/messages.py b/authors/apps/authentication/messages.py index dfcd892..fa55202 100644 --- a/authors/apps/authentication/messages.py +++ b/authors/apps/authentication/messages.py @@ -63,3 +63,9 @@ 'Dislike': "Disliked", 'Null': "Null", } + +read_stats_message = { + "read_status":"Article has been read", + "read_update":"You have already read this article", + "read_error":"Article doesnot exist check details again" +} \ No newline at end of file diff --git a/authors/apps/reading_stats/__init__.py b/authors/apps/reading_stats/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/authors/apps/reading_stats/admin.py b/authors/apps/reading_stats/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/authors/apps/reading_stats/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/authors/apps/reading_stats/apps.py b/authors/apps/reading_stats/apps.py new file mode 100644 index 0000000..3f1c517 --- /dev/null +++ b/authors/apps/reading_stats/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ReadingStartsConfig(AppConfig): + name = 'reading_starts' diff --git a/authors/apps/reading_stats/migrations/__init__.py b/authors/apps/reading_stats/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/authors/apps/reading_stats/models.py b/authors/apps/reading_stats/models.py new file mode 100644 index 0000000..02089a0 --- /dev/null +++ b/authors/apps/reading_stats/models.py @@ -0,0 +1,20 @@ +from django.db import models + +from authors.apps.authentication.models import User +from authors.apps.articles.models import Article + + +class ReadStats(models.Model): + """ + The model for user read starts + """ + user = models.ForeignKey( + User, on_delete=models.CASCADE, null=False, blank=False) + article = models.ForeignKey( + Article, on_delete=models.CASCADE, null=False, blank=False) + article_read = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.article.title diff --git a/authors/apps/reading_stats/serializers.py b/authors/apps/reading_stats/serializers.py new file mode 100644 index 0000000..de367bd --- /dev/null +++ b/authors/apps/reading_stats/serializers.py @@ -0,0 +1,22 @@ +from rest_framework import serializers + +from .models import ReadStats + + +class ReadStatsSerializers(serializers.ModelSerializer): + """" + Serializer class for our ReadStats model + """ + + article = serializers.SerializerMethodField() + + class Meta: + model = ReadStats + + fields = '__all__' + + def get_article(self, stats): + return { + "article": stats.article.title, + "slug": stats.article.slug + } diff --git a/authors/apps/reading_stats/tests/__init__.py b/authors/apps/reading_stats/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/authors/apps/reading_stats/tests/test_read_stats.py b/authors/apps/reading_stats/tests/test_read_stats.py new file mode 100644 index 0000000..575f8e4 --- /dev/null +++ b/authors/apps/reading_stats/tests/test_read_stats.py @@ -0,0 +1,219 @@ +from django.test import TestCase, Client +from django.urls import reverse +from rest_framework import status + +from ..models import ReadStats +from authors.apps.articles.models import Article +from django.template.defaultfilters import slugify +from authors.apps.authentication.messages import read_stats_message + + +class ReadStatsTestCase(TestCase): + + def setUp(self): + client = Client() + self.login_url = reverse("auth:login") + self.register_url = reverse("auth:register") + self.read_stats = reverse("read:user_read_stats") + self.all_article_url = reverse("articles:articles") + + self.valid_article_data = { + "article": { + "image_path": "", + "title": "Its a test article", + "body": "Its a test article body" + } + } + + self.valid_user_credentials = { + "user": { + "username": "Alpha", + "email": "alphaandela@gmail.com", + "password": "@Alpha254" + } + } + + self.user_credentials = { + "user": { + "username": "Alpha", + "email": "alphaandela@gmail.com", + "password": "@Alpha254" + } + } + + self.valid_article_data = { + "article": { + "image_path": "......", + "title": "Its a test article", + "body": "Its a test article body" + } + } + + def user_registration(self, data): + # register a user + response = self.client.post( + self.register_url, + self.valid_user_credentials, + content_type='application/json' + ) + return response + + def login(self, data): + # login user + response = self.client.post( + self.login_url, + self.valid_user_credentials, + content_type='application/json' + ) + return response + + def get_slug_from_title(self, title): + """ + Get slug + """ + specific_article_url = reverse( + "articles:specific_article", + kwargs={ + "slug": slugify(title) + } + ) + return specific_article_url + + # def test_create_article(self): + # """ + # Create an article + # """ + # self.user_registration(self.valid_user_credentials) + # token = self.login(self.valid_user_credentials).data['token'] + + # response = self.client.post( + # self.all_article_url, + # self.valid_article_data, + # format="json", + # HTTP_AUTHORIZATION="Bearer " + token + # ) + # import pdb; pdb.set_trace() + # return response.data + + def test_user_read_starts(self): + """ + test to get list of users read articles + """ + self.user_registration(self.valid_user_credentials) + self.login(self.valid_user_credentials) + res = self.login(self.valid_user_credentials) + token = res.data['token'] + response = self.client.get( + self.read_stats, + content_type='application/json', + HTTP_AUTHORIZATION='Bearer ' + token + ) + self.assertEqual(response.status_code, 200) + + def test_read_error(self): + """ + test response given to user + """ + self.user_registration(self.valid_user_credentials) + self.login(self.valid_user_credentials) + res = self.login(self.valid_user_credentials) + slug = "slug" + token = res.data['token'] + response = self.client.get( + reverse("read:article_read", kwargs={ + "slug":slug + }), + content_type='application/json', + HTTP_AUTHORIZATION="Bearer {}".format(token) + ) + self.assertEqual(response.status_code, 404) + self.assertIn(response.data['message'], + read_stats_message['read_error']) + + + + def test_user_start(self): + """ + test length of data returned + """ + self.user_registration(self.valid_user_credentials) + self.login(self.valid_user_credentials) + res = self.login(self.valid_user_credentials) + token = res.data['token'] + response = self.client.get( + self.read_stats, + content_type='application/json', + HTTP_AUTHORIZATION='Bearer ' + token + ) + data = response.data + + self.assertTrue(len(data) == 4) + + def test_user_results_with_no_read(self): + """ + test length of result data returned + """ + self.user_registration(self.valid_user_credentials) + self.login(self.valid_user_credentials) + res = self.login(self.valid_user_credentials) + token = res.data['token'] + response = self.client.get( + self.read_stats, + content_type='application/json', + HTTP_AUTHORIZATION='Bearer ' + token + ) + data = response.data + self.assertTrue(len(data['results']) == 0) + + + def test_user_read_article_that_doesnot_exist(self): + """ + test if a user tires to read a non existent article + """ + self.user_registration(self.valid_user_credentials) + self.login(self.valid_user_credentials) + res = self.login(self.valid_user_credentials) + slug = "slug" + token = res.data['token'] + response = self.client.get( + "api/v1/read," + slug + "/", + content_type='application/json', + HTTP_AUTHORIZATION='Bearer ' + token + ) + self.assertEqual(response.status_code, 404) + + def test_stat_without_being_authenticated(self): + """ + test to ensure authentication is required + """ + self.login(self.valid_user_credentials) + token = "new" + response = self.client.get( + self.read_stats, + content_type='application/json', + HTTP_AUTHORIZATION='Bearer ' + token + ) + self.assertEqual(response.status_code, 401) + + def test_read_user_response(self): + """ + test response given to user + """ + self.user_registration(self.valid_user_credentials) + self.login(self.valid_user_credentials) + res = self.login(self.valid_user_credentials) + slug = "slug" + token = res.data['token'] + response = self.client.get( + reverse("read:article_read", kwargs={ + "slug":slug + }), + content_type='application/json', + HTTP_AUTHORIZATION="Bearer {}".format(token) + ) + self.assertEqual(response.status_code, 404) + self.assertIn(response.data['message'], + read_stats_message['read_error']) + + + diff --git a/authors/apps/reading_stats/urls.py b/authors/apps/reading_stats/urls.py new file mode 100644 index 0000000..7e6cd94 --- /dev/null +++ b/authors/apps/reading_stats/urls.py @@ -0,0 +1,12 @@ +from django.urls import path + +from .views import UserReadStatsView, UserCompleteStatView + + + +app_name = "read" + +urlpatterns = [ + path("read-stats/", UserReadStatsView.as_view(), name="user_read_stats"), + path("read//", UserCompleteStatView.as_view(), name="article_read"), +] \ No newline at end of file diff --git a/authors/apps/reading_stats/views.py b/authors/apps/reading_stats/views.py new file mode 100644 index 0000000..7e69261 --- /dev/null +++ b/authors/apps/reading_stats/views.py @@ -0,0 +1,48 @@ +from rest_framework import status +from rest_framework.permissions import IsAuthenticated +from .serializers import ReadStatsSerializers +from .models import ReadStats +from rest_framework.generics import ListAPIView, RetrieveAPIView +from rest_framework.response import Response +from authors.apps.authentication.messages import read_stats_message + + +class UserReadStatsView(ListAPIView): + serializer_class = ReadStatsSerializers + + def get_queryset(self): + """" + This gets all the articles the user has read + """ + return ReadStats.objects.filter(user=self.request.user) + + +class UserCompleteStatView(RetrieveAPIView): + + permission_classes = (IsAuthenticated,) + + def get(self, request, slug): + """ + This method checks if the articlle an user is accesing is available + article__slug specidies that the slug is from the articles object + if a no article with such slug exists then it throws an error + """ + try: + user_stat = ReadStats.objects.get(article__slug=slug) + except ReadStats.DoesNotExist: + return Response({ + "message": read_stats_message['read_error']}, + status=status.HTTP_404_NOT_FOUND) + if user_stat.article_read: + return Response({ + "message": read_stats_message['read_update'] + }, status=status.HTTP_403_FORBIDDEN) + + user_stat.article_read = True + user_stat.save() + + return Response( + { + "message": read_stats_message['read_status'] + }, status.HTTP_200_OK + ) diff --git a/authors/settings.py b/authors/settings.py index 14fd37d..3ca50ea 100644 --- a/authors/settings.py +++ b/authors/settings.py @@ -60,6 +60,7 @@ 'authors.apps.core', 'authors.apps.comments', 'authors.apps.profiles', + 'authors.apps.reading_stats', 'rest_framework_swagger', # credits --> https://github.com/axnsan12/drf-yasg 'drf_yasg', diff --git a/authors/urls.py b/authors/urls.py index 891d2e1..f2bfc71 100644 --- a/authors/urls.py +++ b/authors/urls.py @@ -49,5 +49,5 @@ path('api/v1/', include('authors.apps.articles.urls')), path('api/v1/', include('authors.apps.rating.urls')), path('api/v1/', include('authors.apps.comments.urls')), - path('api/v1/', include('authors.apps.rating.urls')), + path('api/v1/', include('authors.apps.reading_stats.urls')) ]