Skip to content

Commit

Permalink
Merge 2bb5c4d into 5eb3426
Browse files Browse the repository at this point in the history
  • Loading branch information
jkamz committed May 16, 2019
2 parents 5eb3426 + 2bb5c4d commit e29890c
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 24 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)),
],
),
]
12 changes: 11 additions & 1 deletion authors/apps/articles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from vote.models import VoteModel
from fluent_comments.models import FluentComment


class TagModel(models.Model):
"""
Tags for articles. The text for each tag is unique
Expand Down Expand Up @@ -83,11 +84,20 @@ class CommentHistoryModel(models.Model):
updated_comment = models.TextField()
updated_at = models.DateTimeField(auto_now_add=True)


class CommentModel(VoteModel, models.Model):
"""The comment model"""
comment = models.ForeignKey(FluentComment,
related_name='comment_id',
on_delete=models.CASCADE, default='')
user = models.ForeignKey(
User, related_name='user', on_delete=models.CASCADE)


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)
52 changes: 49 additions & 3 deletions authors/apps/articles/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from rest_framework import serializers
from django.apps import apps
from .models import (ArticleModel, FavoriteArticleModel,
BookmarkArticleModel, TagModel, CommentHistoryModel, CommentModel)
BookmarkArticleModel, TagModel,
CommentHistoryModel, CommentModel, ReadStatsModel)
from fluent_comments.models import FluentComment
from .utils import user_object, configure_response, TagField
from django.contrib.auth.models import AnonymousUser
Expand All @@ -22,7 +23,8 @@ def to_representation(self, value):
context=self.context) # pragma: no cover

response = serializer.data # pragma: no cover
response['author'] = user_object(response['user_id']) # pragma: no cover
response['author'] = user_object(
response['user_id']) # pragma: no cover

comment_votes = CommentModel.objects.filter(
comment=response['id']).first()
Expand Down Expand Up @@ -64,6 +66,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 @@ -91,6 +95,8 @@ class Meta:
'mail',
'readtime',
'highlights',
'read_count',
'article_readers',
)
lookup_field = 'slug'
extra_kwargs = {'url': {'lookup_field': 'slug'}}
Expand Down Expand Up @@ -151,11 +157,43 @@ def get_highlights(self, obj):
highlighted_by=self.context['request'].user)

if highlighted:
serializer = HighlightsSerializer(highlighted, many=True) # pragma: no cover
serializer = HighlightsSerializer(
highlighted, many=True) # pragma: no cover
return serializer.data

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 @@ -214,3 +252,11 @@ class CommentHistorySerializer(serializers.ModelSerializer):
class Meta:
model = CommentHistoryModel
fields = ['updated_comment', 'updated_at']


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

class Meta:
Model = ReadStatsModel
fields = '__all__'
29 changes: 19 additions & 10 deletions authors/apps/articles/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,11 @@
import re
import cloudinary.uploader
from django_filters import (
filters, Filter, FilterSet, rest_framework, ModelMultipleChoiceFilter)
from django.db.models import Q
from rest_framework import serializers, status
from .models import User
from rest_framework.response import Response
FilterSet, rest_framework)
from rest_framework import serializers
from fluent_comments.models import FluentComment
from .models import ArticleModel, TagModel, CommentModel
from django.http import JsonResponse
from rest_framework.exceptions import ValidationError, NotFound
from rest_framework import status
from rest_framework.exceptions import (ValidationError, NotFound)
from .models import ArticleModel, TagModel, CommentModel, ReadStatsModel


def ImageUploader(image):
Expand Down Expand Up @@ -115,6 +110,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 @@ -157,14 +164,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 All @@ -173,6 +180,7 @@ def to_internal_value(self, data):
tag, created = TagModel.objects.get_or_create(tagname=data)
return tag


def get_comment_queryset(request, slug):
article = ArticleModel.objects.filter(slug=slug).first()
if article is None:
Expand All @@ -197,6 +205,7 @@ def get_comment_queryset(request, slug):

return queryset


def check_article(slug):
"""
Function that checks if an article exists
Expand Down
17 changes: 7 additions & 10 deletions authors/apps/articles/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,8 @@
from rest_framework.pagination import PageNumberPagination
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from rest_framework.decorators import permission_classes
from django.contrib.auth.models import User
from ..authentication.models import User
from rest_framework.permissions import (
IsAuthenticatedOrReadOnly, IsAuthenticated, IsAdminUser, AllowAny
)
from rest_framework.permissions import (IsAuthenticated, AllowAny)
from django.utils import timezone
from django.http import JsonResponse
from django_filters.rest_framework import DjangoFilterBackend
Expand All @@ -24,13 +20,11 @@
BookmarkArticleSerializer, TagSerializer,
CommentHistorySerializer)
from .models import (ArticleModel, FavoriteArticleModel,
BookmarkArticleModel, TagModel, CommentHistoryModel, CommentModel)
BookmarkArticleModel, TagModel,
CommentHistoryModel, CommentModel)
from .utils import (ImageUploader, user_object,
configure_response, add_social_share, ArticleFilter,
get_comment_queryset, check_article)
from django_filters.rest_framework import DjangoFilterBackend
from django.shortcuts import get_object_or_404
from django_comments import get_model as get_comments_model
get_comment_queryset, check_article, save_read_stat)


class ArticleView(viewsets.ModelViewSet):
Expand Down Expand Up @@ -121,6 +115,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 e29890c

Please sign in to comment.