Skip to content

Commit

Permalink
feat(stats): implement user can view read stats
Browse files Browse the repository at this point in the history
 - implement read count is logged when authenticated users read an article
 - implement authenticated users can view read count
 - implement article owners can view people who have read their articles
 - read count and readers returned when getting an article
[Delivers #165305284]
  • Loading branch information
jkamz committed May 16, 2019
1 parent 197272c commit 09c591a
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 6 deletions.
24 changes: 24 additions & 0 deletions authors/apps/articles/migrations/0030_readstatsmodel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 2.2 on 2019-05-15 12:10

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', '0029_auto_20190513_1142'),
]

operations = [
migrations.CreateModel(
name='ReadStatsModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='read_article', to='articles.ArticleModel')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='read_user', to=settings.AUTH_USER_MODEL)),
],
),
]
9 changes: 9 additions & 0 deletions authors/apps/articles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,12 @@ class BookmarkArticleModel(models.Model):
to_field='slug')
bookmarked_at = models.DateTimeField(
auto_created=True, auto_now=False, default=timezone.now)


class ReadStatsModel(models.Model):
"""Reading statistics model"""
user = models.ForeignKey(
User, related_name="read_user", on_delete=models.CASCADE)

article = models.ForeignKey(
ArticleModel, related_name="read_article", on_delete=models.CASCADE)
45 changes: 44 additions & 1 deletion authors/apps/articles/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from rest_framework import serializers
from django.apps import apps
from .models import (ArticleModel, FavoriteArticleModel,
BookmarkArticleModel, TagModel)
BookmarkArticleModel, TagModel, ReadStatsModel)
from fluent_comments.models import FluentComment
from .utils import user_object, configure_response, TagField
from django.contrib.auth.models import AnonymousUser
Expand Down Expand Up @@ -51,6 +51,8 @@ class ArticleSerializer(serializers.ModelSerializer):
many=True,
required=False
)
read_count = serializers.SerializerMethodField()
article_readers = serializers.SerializerMethodField()

class Meta:
model = TABLE
Expand Down Expand Up @@ -78,6 +80,8 @@ class Meta:
'mail',
'readtime',
'highlights',
'read_count',
'article_readers',
)
lookup_field = 'slug'
extra_kwargs = {'url': {'lookup_field': 'slug'}}
Expand Down Expand Up @@ -143,6 +147,37 @@ def get_highlights(self, obj):

return None

def get_read_count(self, obj):
"""
Return the number of people who have read an article
This is visible to all logged in users
"""
if not self.check_anonymous():
read = ReadStatsModel.objects.filter(article=obj).count()

if read:
return read
return 0
return None

def get_article_readers(self, obj):
"""
Get the usernames of people who have read an article
This is only visible to the author of the article
"""
if self.check_anonymous():
return None

request = self.context.get('request')

if request.user != obj.author:
return None

read = ReadStatsModel.objects.filter(article=obj)
users = [x.user.username for x in read]

return users


class FavoriteArticleSerializer(serializers.ModelSerializer):
"""Favorite article serializer"""
Expand Down Expand Up @@ -193,3 +228,11 @@ class Meta:

def to_representation(self, instance):
return instance.tagname


class ReadStatsSerializer(serializers.ModelSerializer):
"""Article read statistics serializer"""

class Meta:
Model = ReadStatsModel
fields = '__all__'
17 changes: 14 additions & 3 deletions authors/apps/articles/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
from django.db.models import Q
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from .models import User
from .models import ArticleModel, TagModel
from .models import (User, ArticleModel, TagModel, ReadStatsModel)


def ImageUploader(image):
Expand Down Expand Up @@ -103,6 +102,18 @@ def add_social_share(request):
return request


def save_read_stat(request, article):
""" Save a read statitic to db if it does not exist"""
if not request.user.is_anonymous:
existing_read = ReadStatsModel.objects.all().filter(
user=request.user, article=article
)

if not existing_read:
ReadStatsModel.objects.create(
user=request.user, article=article)


class ArticleFilter(FilterSet):
"""
Custom filter class for articles
Expand Down Expand Up @@ -145,14 +156,14 @@ def to_representation(self, value):
"""
Return the representation that should be used to serialize the field
"""

return value.tagname

def to_internal_value(self, data):
"""
Validate data and restore it back into its internal
python representation
"""

if data:
if not re.match(r'^[a-zA-Z0-9][ A-Za-z0-9_-]*$', data):
raise ValidationError(
Expand Down
7 changes: 5 additions & 2 deletions authors/apps/articles/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
CommentSerializer, FavoriteArticleSerializer,
BookmarkArticleSerializer, TagSerializer)
from .models import (ArticleModel, FavoriteArticleModel,
BookmarkArticleModel, TagModel)
BookmarkArticleModel, TagModel, ReadStatsModel)
from .utils import (ImageUploader, user_object,
configure_response, add_social_share, ArticleFilter)
configure_response, add_social_share, ArticleFilter, save_read_stat)


class ArticleView(viewsets.ModelViewSet):
Expand Down Expand Up @@ -111,6 +111,9 @@ def retrieve(self, request, slug=None):
return JsonResponse({"status": 404,
"error": "Article with slug {} not found".format(slug)},
status=404)

save_read_stat(request, article)

serializer = ArticleSerializer(article,
context={'request': request})
response = Response(serializer.data)
Expand Down
92 changes: 92 additions & 0 deletions authors/tests/data/test_read_stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""
Reader statistics tests
"""
import json
from .base_test import BaseTest
from .data import Data


class HighlightArticleTestcase(BaseTest):
"""
This class defines the test suite for like and dislike
cases.
"""

def setUp(self):
""" Define the test client and required test variables. """

BaseTest.setUp(self)
data = self.base_data.user_data2
signup = self.signup_user()
signup2 = self.signup_user(data)

uid = signup.data.get('data')['id']
token = signup.data.get('data')['token']
uid_2 = signup2.data.get('data')['id']
token_2 = signup2.data.get('data')['token']

self.activate_user(uid=uid, token=token)
self.activate_user(uid=uid_2, token=token_2)
self.base_data = Data()

login = self.login_user()
self.token = login.data['token']
self.control_token = self.login_user_and_get_token(data)

def test_can_add_article_read_count_if_user_authenticated(self):
"""
Test that a reading count is added after getting
an article by an authenticated user
"""
article = self.create_article()
slug = article.data['data']['slug']

response = self.client.get('/api/articles/{}/'.format(slug),
HTTP_AUTHORIZATION='Bearer ' +
self.token)
res = json.loads(response.content.decode('utf-8'))

self.assertEqual(res['data']['read_count'], 1)

def test_cannot_add_article_read_count_if_user_not_authenticated(self):
"""
Test that a reading count is not added after getting
an article by an unauthenticated user
"""
article = self.create_article()
slug = article.data['data']['slug']

response = self.client.get('/api/articles/{}/'.format(slug))
res = json.loads(response.content.decode('utf-8'))

self.assertEqual(res['data']['read_count'], None)

def test_can_get_article_readers_if_owner(self):
"""
Test that the author can view the people who have
read the article
"""
article = self.create_article()
slug = article.data['data']['slug']

response = self.client.get('/api/articles/{}/'.format(slug),
HTTP_AUTHORIZATION='Bearer ' +
self.token)
res = json.loads(response.content.decode('utf-8'))

self.assertEqual(res['data']['article_readers'][0], 'Alpha')

def test_canot_get_article_readers_if_not_owner(self):
"""
Test that one cannot view the people who have
read an article if they are not the author
"""
article = self.create_article()
slug = article.data['data']['slug']

response = self.client.get('/api/articles/{}/'.format(slug),
HTTP_AUTHORIZATION='Bearer ' +
self.control_token)
res = json.loads(response.content.decode('utf-8'))

self.assertEqual(res['data']['article_readers'], None)

0 comments on commit 09c591a

Please sign in to comment.