From 1ad3405865329a197f3927785f9b7e902127bc68 Mon Sep 17 00:00:00 2001 From: Kimbugwe Simon Peter Date: Wed, 17 Oct 2018 18:59:24 +0300 Subject: [PATCH] ft(escalating): add feature to report article - add model for reported articles - add serializer for reported articles - add view to report articles - add url to report articles - perform flake8 corrections [DELIVERS #160617687] ft(escalating): correct migrations -correct migrations [FIXES #160617687] ft(escalating): escalations - add admin access [DELIVERS #160617687] ft(escalating): escalations - add times article - add report article serializer - add report article urls - add report view - add migrations - add tests for the feature [DELIVERS #160617687] --- README.md | 17 ++ authors/apps/articles/admin.py | 6 +- .../apps/articles/migrations/0006_reported.py | 25 +++ .../migrations/0007_reported_created_at.py | 20 +++ .../migrations/0008_auto_20181018_1517.py | 17 ++ .../0009_reported_times_reported.py | 18 ++ .../migrations/0010_article_times_reported.py | 18 ++ .../0011_remove_reported_times_reported.py | 17 ++ authors/apps/articles/models.py | 21 ++- authors/apps/articles/renderers.py | 4 + authors/apps/articles/serializers.py | 43 +++-- authors/apps/articles/urls.py | 22 ++- authors/apps/articles/utils.py | 25 ++- authors/apps/articles/views.py | 127 ++++++++------ authors/apps/comments/admin.py | 3 +- authors/apps/comments/models.py | 4 + authors/apps/comments/views.py | 48 +++--- tests/test_articles/test_articles.py | 161 ++++++++++++------ tests/test_authentication/test_base.py | 5 + 19 files changed, 448 insertions(+), 153 deletions(-) create mode 100644 authors/apps/articles/migrations/0006_reported.py create mode 100644 authors/apps/articles/migrations/0007_reported_created_at.py create mode 100644 authors/apps/articles/migrations/0008_auto_20181018_1517.py create mode 100644 authors/apps/articles/migrations/0009_reported_times_reported.py create mode 100644 authors/apps/articles/migrations/0010_article_times_reported.py create mode 100644 authors/apps/articles/migrations/0011_remove_reported_times_reported.py diff --git a/README.md b/README.md index 19fd67a..f0a49ff 100644 --- a/README.md +++ b/README.md @@ -421,6 +421,23 @@ No additional parameters required `GET /api/tags` +### Report an article + +`POST /api/articles/:slug/report/` + +Authentication and reason for reporting required +Request Body +```source-json +{ + "report": { + "reason": "this is not the best tense to use" + + } +} +``` + + + diff --git a/authors/apps/articles/admin.py b/authors/apps/articles/admin.py index 2620c09..53d25af 100644 --- a/authors/apps/articles/admin.py +++ b/authors/apps/articles/admin.py @@ -1,6 +1,10 @@ from django.contrib import admin -from authors.apps.articles.models import Article, RateArticle, LikeArticle, Category + +from authors.apps.articles.models import (Article, Category, LikeArticle, + RateArticle, Reported) + admin.site.register(Article) admin.site.register(RateArticle) admin.site.register(LikeArticle) admin.site.register(Category) +admin.site.register(Reported) diff --git a/authors/apps/articles/migrations/0006_reported.py b/authors/apps/articles/migrations/0006_reported.py new file mode 100644 index 0000000..2e8ab66 --- /dev/null +++ b/authors/apps/articles/migrations/0006_reported.py @@ -0,0 +1,25 @@ +# Generated by Django 2.1.1 on 2018-10-17 16:57 + +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', '0005_merge_20181016_2114'), + ] + + operations = [ + migrations.CreateModel( + name='Reported', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('reason', models.TextField()), + ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='articles.Article', to_field='slug')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, to_field='email')), + ], + ), + ] diff --git a/authors/apps/articles/migrations/0007_reported_created_at.py b/authors/apps/articles/migrations/0007_reported_created_at.py new file mode 100644 index 0000000..e40d992 --- /dev/null +++ b/authors/apps/articles/migrations/0007_reported_created_at.py @@ -0,0 +1,20 @@ +# Generated by Django 2.1.1 on 2018-10-18 07:30 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('articles', '0006_reported'), + ] + + operations = [ + migrations.AddField( + model_name='reported', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + ] diff --git a/authors/apps/articles/migrations/0008_auto_20181018_1517.py b/authors/apps/articles/migrations/0008_auto_20181018_1517.py new file mode 100644 index 0000000..3d93ced --- /dev/null +++ b/authors/apps/articles/migrations/0008_auto_20181018_1517.py @@ -0,0 +1,17 @@ +# Generated by Django 2.1.1 on 2018-10-18 12:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('articles', '0007_reported_created_at'), + ] + + operations = [ + migrations.AlterModelOptions( + name='reported', + options={'ordering': ['-created_at']}, + ), + ] diff --git a/authors/apps/articles/migrations/0009_reported_times_reported.py b/authors/apps/articles/migrations/0009_reported_times_reported.py new file mode 100644 index 0000000..7fd11b5 --- /dev/null +++ b/authors/apps/articles/migrations/0009_reported_times_reported.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.1 on 2018-10-18 13:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('articles', '0008_auto_20181018_1517'), + ] + + operations = [ + migrations.AddField( + model_name='reported', + name='times_reported', + field=models.IntegerField(default=0), + ), + ] diff --git a/authors/apps/articles/migrations/0010_article_times_reported.py b/authors/apps/articles/migrations/0010_article_times_reported.py new file mode 100644 index 0000000..c2b9ecb --- /dev/null +++ b/authors/apps/articles/migrations/0010_article_times_reported.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.1 on 2018-10-18 13:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('articles', '0009_reported_times_reported'), + ] + + operations = [ + migrations.AddField( + model_name='article', + name='times_reported', + field=models.IntegerField(default=0), + ), + ] diff --git a/authors/apps/articles/migrations/0011_remove_reported_times_reported.py b/authors/apps/articles/migrations/0011_remove_reported_times_reported.py new file mode 100644 index 0000000..51860f1 --- /dev/null +++ b/authors/apps/articles/migrations/0011_remove_reported_times_reported.py @@ -0,0 +1,17 @@ +# Generated by Django 2.1.1 on 2018-10-18 13:53 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('articles', '0010_article_times_reported'), + ] + + operations = [ + migrations.RemoveField( + model_name='reported', + name='times_reported', + ), + ] diff --git a/authors/apps/articles/models.py b/authors/apps/articles/models.py index ea14abc..82f16a2 100644 --- a/authors/apps/articles/models.py +++ b/authors/apps/articles/models.py @@ -4,9 +4,10 @@ from django.db.models import Avg, Count from django.utils import timezone from django.utils.text import slugify -from taggit.managers import TaggableManager from authors.apps.authentication.models import User +from authors.apps.profiles.models import Profile +from taggit.managers import TaggableManager from .utils import get_unique_slug, time @@ -15,7 +16,7 @@ class Category(models.Model): title = models.CharField(max_length=100, default="general") slug = models.SlugField(max_length=100, unique=True) - class Meta: + class Meta: verbose_name_plural = "Categories" def __str__(self): @@ -57,8 +58,9 @@ class Article(models.Model): tags = TaggableManager() - category = models.ForeignKey(Category, + category = models.ForeignKey(Category, on_delete=models.CASCADE) + times_reported = models.IntegerField(default=0) def __str__(self): """ @@ -116,3 +118,16 @@ class LikeArticle(models.Model): article = models.ForeignKey(Article, blank=False, on_delete=models.CASCADE) likes = models.NullBooleanField() + + +class Reported(models.Model): + article = models.ForeignKey( + Article, blank=False, on_delete=models.CASCADE, to_field='slug') + user = models.ForeignKey( + User, blank=False, on_delete=models.CASCADE, to_field='email') + reason = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + + class Meta: + ordering = ['-created_at'] diff --git a/authors/apps/articles/renderers.py b/authors/apps/articles/renderers.py index b679865..861be58 100644 --- a/authors/apps/articles/renderers.py +++ b/authors/apps/articles/renderers.py @@ -27,6 +27,10 @@ class CategoryJSONRenderer(AhJSONRenderer): charset = 'utf-8' object_label = 'category' +class ReportArticleJSONRenderer(AhJSONRenderer): + charset = 'utf-8' + object_label = 'support' + class ShareArticleJSONRenderer(AhJSONRenderer): charset = 'utf-8' diff --git a/authors/apps/articles/serializers.py b/authors/apps/articles/serializers.py index c50d75e..eec2baa 100644 --- a/authors/apps/articles/serializers.py +++ b/authors/apps/articles/serializers.py @@ -1,11 +1,12 @@ -from authors.apps.articles.models import Article, RateArticle, LikeArticle, Category -from authors.apps.authentication.models import User from django.core.exceptions import ObjectDoesNotExist from rest_framework import serializers -from taggit_serializer.serializers import (TagListSerializerField, - TaggitSerializer) - + +from authors.apps.articles.models import (Article, Category, LikeArticle, + RateArticle, Reported) +from authors.apps.authentication.models import User from taggit.models import Tag +from taggit_serializer.serializers import (TaggitSerializer, + TagListSerializerField) class ArticleSerializer(TaggitSerializer, serializers.ModelSerializer): @@ -13,33 +14,34 @@ class ArticleSerializer(TaggitSerializer, serializers.ModelSerializer): author = serializers.ReadOnlyField(source='author.username') read_time = serializers.ReadOnlyField(source='read') tags = TagListSerializerField() - + 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', 'average_rating', 'likes', 'dislikes', + 'author', 'title', 'slug', 'description', 'body', 'created_at', + 'updated_at', 'read_time', 'average_rating', 'likes', 'dislikes', 'tags', 'category', 'favorites_count', 'image') read_only_fields = ('slug', 'author_id',) + class TagSerializer(serializers.ModelSerializer): - + class Meta: model = Tag fields = ('name',) + class CategorySerializer(serializers.ModelSerializer): - class Meta: model = Category fields = ('id', 'title', 'slug') read_only_fields = ('id', 'slug',) - + class RateArticleSerializer(serializers.ModelSerializer): rated_by = serializers.ReadOnlyField(source='rated_by.username') article = serializers.ReadOnlyField(source='article.slug') @@ -79,7 +81,6 @@ def validate(self, data): return { "likes": likes} - def get_favorites_count(self, instance): return instance.favorited_by.count() @@ -94,3 +95,21 @@ def check(self, data): raise serializers.ValidationError( 'An email is required to share.' ) +class ReportArticleSerializer(serializers.ModelSerializer): + article_title = serializers.ReadOnlyField(source='article.title') + article_slug = serializers.ReadOnlyField(source='article.slug') + article_author = serializers.ReadOnlyField(source='article.author.email') + reported_by = serializers.ReadOnlyField(source='user.email') + + class Meta: + model = Reported + fields = ['article_title', 'reason', 'article_slug', + 'article_author', 'reported_by'] + +class ReportSerializer(ReportArticleSerializer): + times_reported = serializers.ReadOnlyField(source='article.times_reported') + + class Meta: + model = Reported + fields = ['article_title','article_slug', + 'article_author','times_reported','reason'] diff --git a/authors/apps/articles/urls.py b/authors/apps/articles/urls.py index dd0025e..223f786 100644 --- a/authors/apps/articles/urls.py +++ b/authors/apps/articles/urls.py @@ -1,11 +1,11 @@ from django.urls import path -from .views import ( - ArticleAPIView, ArticleAPIDetailsView, - RateArticleView, LikeArticleView, LikeAPIDetailsView, - TagListAPIView, TagRetrieveAPIView, CategoryListCreateAPIView, - CategoryRetrieveAPIView, FavoriteArticleView, UnFavoriteArticleView, - ShareArticleAPIView,) +from .views import (ArticleAPIDetailsView, ArticleAPIView, + CategoryListCreateAPIView, CategoryRetrieveAPIView, + FavoriteArticleView, LikeAPIDetailsView, LikeArticleView, + RateArticleView, ReportArticle, ReportArticleListView, + ReportView, ShareArticleAPIView, TagListAPIView, + TagRetrieveAPIView, UnFavoriteArticleView) app_name = "articles" @@ -24,8 +24,14 @@ path("tags//", TagRetrieveAPIView.as_view()), path("categories/", CategoryListCreateAPIView.as_view()), path("categories//", CategoryRetrieveAPIView.as_view()), - path('articles//favorite/', FavoriteArticleView.as_view(), name="favorite"), - path('articles//unfavorite/', UnFavoriteArticleView.as_view(), name="unfavorite") + path('articles//favorite/', + FavoriteArticleView.as_view(), name="favorite"), + path('articles//unfavorite/', + UnFavoriteArticleView.as_view(), name="unfavorite"), + path('articles//report/', ReportArticle.as_view(), name="escalate"), + path('escalations/', ReportArticleListView.as_view(), name="escalated"), + path('escalations/report/', ReportView.as_view(), name="escalatation report") + ] diff --git a/authors/apps/articles/utils.py b/authors/apps/articles/utils.py index 84e7b4e..071b3d2 100644 --- a/authors/apps/articles/utils.py +++ b/authors/apps/articles/utils.py @@ -1,8 +1,11 @@ -from django.utils.text import slugify -import re import datetime import math - +import re + +from django.core.mail import EmailMessage +from django.utils.text import slugify + + def get_unique_slug(model_instance, slugable_field_name, slug_field_name): """ Takes a model instance, sluggable field name (such as 'title') of that @@ -34,3 +37,19 @@ def time(string, image=0): for image in range(image): readtime += image_time return str(datetime.timedelta(seconds=readtime)) + +def shareArticleMail(share, request, share_data, article): + user_instance = request.user + host = request.get_host() + user = user_instance.username + subject = article.title + share_slug = article.slug + body = 'Click on the link below to view Article! \n\n \ + {}/api/articles/{}/ \n\n shared by [ {} ]'.format( + host, share_slug, user) + to_email = [share['email']] + email = EmailMessage(subject, body, to=to_email,) + email.send() + share_data.update({ + 'message': 'Article shared succesfully' + }) diff --git a/authors/apps/articles/views.py b/authors/apps/articles/views.py index 5c16d3d..36b9176 100644 --- a/authors/apps/articles/views.py +++ b/authors/apps/articles/views.py @@ -1,27 +1,26 @@ from django.core.exceptions import ObjectDoesNotExist -from django.core.mail import EmailMessage from django.http import JsonResponse -from django.shortcuts import get_object_or_404, render -from rest_framework import filters, generics, serializers, status +from django.shortcuts import get_object_or_404 +from rest_framework import filters, generics, status from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.generics import ListCreateAPIView -from rest_framework.permissions import (AllowAny, IsAuthenticated, +from rest_framework.permissions import (AllowAny, IsAdminUser, IsAuthenticated, IsAuthenticatedOrReadOnly) from rest_framework.response import Response -from rest_framework.views import APIView -from taggit.models import Tag -from authors.apps.articles.renderers import ArticleJSONRenderer -from authors.apps.articles.serializers import ArticleSerializer, TagSerializer +from taggit.models import Tag from .exceptions import CatHasNoArticles, TagHasNoArticles -from .models import Article, Category, LikeArticle, RateArticle +from .models import Article, Category, LikeArticle, RateArticle, Reported from .renderers import (ArticleJSONRenderer, CategoryJSONRenderer, LikeUserJSONRenderer, RateUserJSONRenderer, - ShareArticleJSONRenderer, TagJSONRenderer) + ReportArticleJSONRenderer, ShareArticleJSONRenderer, + TagJSONRenderer) from .serializers import (ArticleSerializer, CategorySerializer, LikeArticleSerializer, RateArticleSerializer, - ShareEmailSerializer) + ReportArticleSerializer, ReportSerializer, + ShareEmailSerializer, TagSerializer) +from .utils import shareArticleMail class TagListAPIView(generics.ListAPIView): @@ -41,7 +40,8 @@ def retrieve(self, request, *args, **kwargs): tag_name = self.kwargs["tag_name"] tags = Article.objects.filter(tags__name__in=[tag_name]).values() if tags: - return JsonResponse({'articles': list(tags)}, status=status.HTTP_200_OK) + return JsonResponse({'articles': list(tags)}, + status=status.HTTP_200_OK) else: raise TagHasNoArticles("This tag currently has no articles") @@ -86,7 +86,7 @@ class ArticleAPIView(generics.ListCreateAPIView): serializer_class = ArticleSerializer filter_backends = (filters.SearchFilter,) search_fields = ( - 'title', 'author__username', 'description', 'body', 'tags__name',) + 'title', 'author__username', 'description', 'body', 'tags__name',) def create(self, request, *args, **kwargs): article = request.data.get("article", {}) @@ -110,7 +110,7 @@ def get_queryset(self): queryset = queryset.filter(author__username=author) title = self.request.query_params.get('title', None) if title is not None: - queryset = queryset.filter(title__icontains=title) + queryset = queryset.filter(title__icontains=title) return queryset @@ -127,8 +127,8 @@ def destroy(self, request, *args, **kwargs): 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", {}) @@ -178,7 +178,8 @@ def create(self, request, *args, **kwargs): instance = LikeArticle.objects.filter( article_id=article_slug.id, liked_by_id=request.user.id).first() if instance: - return Response({"msg": "you can only like an article once"}, status=status.HTTP_400_BAD_REQUEST) + return Response({"msg": "you can only like an article once"}, + status=status.HTTP_400_BAD_REQUEST) liking = request.data.get('like', {}) serializer = self.serializer_class(data=liking) @@ -220,12 +221,17 @@ def create(self, request, *args, **kwargs): try: article = Article.objects.get(slug=self.kwargs['slug']) if article.author_id == profile.user_id: - return Response({'error': 'You cannot favorite your own article'}, status=status.HTTP_400_BAD_REQUEST) + return Response( + {'error': 'You cannot favorite your own article'}, + status=status.HTTP_400_BAD_REQUEST) if profile.has_favorited(article): - return Response({'error': 'You already favorited this article'}, status=status.HTTP_400_BAD_REQUEST) + return Response( + {'error': 'You already favorited this article'}, + status=status.HTTP_400_BAD_REQUEST) except Article.DoesNotExist: - return Response({'message': 'An article with this slug was not found.'}, - status=status.HTTP_404_NOT_FOUND) + return Response( + {'message': 'An article with this slug was not found.'}, + status=status.HTTP_404_NOT_FOUND) profile.favorite(article) article.favorites_count += 1 @@ -246,12 +252,13 @@ def delete(self, request, *args, **kwargs): try: article = Article.objects.get(slug=self.kwargs['slug']) except Article.DoesNotExist: - return Response({'message': 'An article with this slug was not found.'}, - status=status.HTTP_404_NOT_FOUND) - + return Response( + {'message': 'An article with this slug was not found.'}, + status=status.HTTP_404_NOT_FOUND) if not profile.has_favorited(article): - return Response({'message': 'This article is not among your favorites'}, - status=status.HTTP_400_BAD_REQUEST) + return Response( + {'message': 'This article is not among your favorites'}, + status=status.HTTP_400_BAD_REQUEST) profile.unfavorite(article) if article.favorites_count > 0: @@ -270,28 +277,46 @@ class ShareArticleAPIView(generics.CreateAPIView): serializer_class = ShareEmailSerializer def create(self, request, *args, **kwargs): - article_slug = self.kwargs['slug'] - article = get_object_or_404(Article, slug=article_slug) - share = request.data.get('share', {}) - serializer = self.serializer_class(data=share) - serializer.is_valid(raise_exception=True) - share_data = serializer.data - self.shareArticleMail( - share, request, share_data, article) - return Response(share_data, status=status.HTTP_200_OK) - - def shareArticleMail(self, share, request, share_data, article): - user_instance = self.request.user - host = request.get_host() - user = user_instance.username - subject = article.title - share_slug = article.slug - body = 'Click on the link below to view Article! \n\n \ - {}/api/articles/{}/ \n\n shared by [ {} ]'.format( - host, share_slug, user) - to_email = [share['email']] - email = EmailMessage(subject, body, to=to_email,) - email.send() - share_data.update({ - 'message': 'Article shared succesfully' - }) + article_slug = self.kwargs['slug'] + article = get_object_or_404(Article, slug=article_slug) + share = request.data.get('share', {}) + serializer = self.serializer_class(data=share) + serializer.is_valid(raise_exception=True) + share_data = serializer.data + shareArticleMail(share, request, share_data, article) + return Response(share_data, status=status.HTTP_200_OK) + + +class ReportArticle(generics.CreateAPIView): + permission_classes = (IsAuthenticated,) + serializer_class = ReportArticleSerializer + renderer_classes = (ReportArticleJSONRenderer,) + lookup_field = "slug" + queryset = Article.objects.all() + + def post(self, request, *args, **kwargs): + report_data = request.data.get('report', {}) + article = get_object_or_404(Article, slug=kwargs['slug']) + if article.author_id == request.user.id: + raise PermissionDenied("you cant report your article") + if Reported.objects.filter(article=article, user=request.user): + raise PermissionDenied("you can only report once") + serializer = self.serializer_class(data=report_data) + serializer.is_valid(raise_exception=True) + article.times_reported += 1 + article.save() + serializer.save(article=article, user=request.user) + msg = {'message': 'The article has been reported to the site admin'} + return Response(msg, status=status.HTTP_201_CREATED) + + +class ReportArticleListView(generics.ListAPIView): + permission_classes = (IsAdminUser,) + renderer_classes = (ReportArticleJSONRenderer,) + serializer_class = ReportArticleSerializer + queryset = Reported.objects.all() + + +class ReportView(ReportArticleListView): + queryset = Reported.objects.order_by('article_id').distinct('article_id') + serializer_class = ReportSerializer diff --git a/authors/apps/comments/admin.py b/authors/apps/comments/admin.py index a4cec9f..702cc75 100644 --- a/authors/apps/comments/admin.py +++ b/authors/apps/comments/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import Comment +from .models import Comment,CommentHistory # Register your models here. @@ -13,3 +13,4 @@ class CommentAdmin(admin.ModelAdmin): admin.site.register(Comment, CommentAdmin) +admin.site.register(CommentHistory) \ No newline at end of file diff --git a/authors/apps/comments/models.py b/authors/apps/comments/models.py index f0ca0ee..7b96965 100644 --- a/authors/apps/comments/models.py +++ b/authors/apps/comments/models.py @@ -52,6 +52,10 @@ def create_history(sender, **kwargs): post_save.connect(create_history, sender=Comment) class LikeComment(models.Model): + """ + This model is responsble for creating relationship between user + who likes a comment and the comment + """ liked_by = models.ForeignKey(User, blank=False, on_delete=models.CASCADE) comment = models.ForeignKey(Comment, blank=False, on_delete=models.CASCADE) diff --git a/authors/apps/comments/views.py b/authors/apps/comments/views.py index fc85a4e..2880ef1 100644 --- a/authors/apps/comments/views.py +++ b/authors/apps/comments/views.py @@ -8,26 +8,30 @@ from rest_framework.serializers import ValidationError from authors.apps.articles.models import Article -from .models import Comment, LikeComment, CommentHistory -from .renderer import CommentJSONRenderer, CommentThreadJSONRenderer -from .serializers import CommentChildSerializer, CommentSerializer, LikeCommentSerializer +from .models import Comment, CommentHistory, LikeComment from .renderer import (CommentHistoryJSONRenderer, CommentJSONRenderer, CommentThreadJSONRenderer) from .serializers import (CommentChildSerializer, CommentHistorySerializer, - CommentSerializer) - -# Create your views here. + CommentSerializer, LikeCommentSerializer) class CommentListCreateView(generics.ListCreateAPIView): + """ + View to create comments and retrieve comments + + """ serializer_class = CommentSerializer permission_classes = (IsAuthenticated,) renderer_classes = (CommentJSONRenderer,) queryset = Comment.objects.all().filter(parent=None) + queryset = Comment.objects.all() lookup_field = 'slug' def post(self, request, *args, **kwargs): + """ + method to post a comment to article + """ comment, slug = self.get_comment_input(request) serializer = self.serializer_class(data=comment, partial=True) serializer.is_valid(raise_exception=True) @@ -141,22 +145,23 @@ def get(self, request, *args, **kwargs): class LikeCommentView(generics.ListCreateAPIView): permission_classes = (IsAuthenticated,) renderer_classes = (CommentJSONRenderer,) - serializer_class = LikeCommentSerializer + serializer_class = LikeCommentSerializer lookup_fields = 'id', 'slug' queryset = Comment.objects.all().filter(parent__isnull=True) def create(self, request, *args, **kwargs): - comment=get_object_or_404(Comment,id=self.kwargs['id']) + comment = get_object_or_404(Comment, id=self.kwargs['id']) if comment.author_id == request.user.id: - return Response({"message":"you can't like your own comment"},status=status.HTTP_400_BAD_REQUEST) + return Response({"message": "you can't like your own comment"}, status=status.HTTP_400_BAD_REQUEST) - instance = LikeComment.objects.filter(comment_id=comment.id,liked_by_id=request.user.id) + instance = LikeComment.objects.filter( + comment_id=comment.id, liked_by_id=request.user.id) if instance: return Response({"message": "you have already liked the comment"}, status=status.HTTP_400_BAD_REQUEST) comment.likes_count += 1 - + comment.save() likes = comment.likes_count @@ -164,33 +169,34 @@ def create(self, request, *args, **kwargs): serializer.is_valid(raise_exception=True) self.perform_create(serializer, comment, likes) return Response(serializer.data) - + def perform_create(self, serializer, comment, likes): - serializer.save(liked_by=self.request.user, comment=comment, likes = likes) - + serializer.save(liked_by=self.request.user, + comment=comment, likes=likes) + + class UnLikeCommentView(generics.DestroyAPIView): permission_classes = (IsAuthenticated,) renderer_classes = (CommentJSONRenderer,) - serializer_class = CommentSerializer + serializer_class = CommentSerializer lookup_fields = 'id', 'slug' queryset = Comment.objects.all().filter(parent__isnull=True) - def delete(self, request, *args, **kwargs): - comment=get_object_or_404(Comment,id=self.kwargs['id']) - + comment = get_object_or_404(Comment, id=self.kwargs['id']) + try: - instance = LikeComment.objects.get(comment_id=comment.id,liked_by_id=request.user.id) + instance = LikeComment.objects.get( + comment_id=comment.id, liked_by_id=request.user.id) except: return Response({'message': 'This is not a comment you like.'}, status=status.HTTP_404_NOT_FOUND) if comment.likes_count > 0: - comment.likes_count -=1 + comment.likes_count -= 1 - comment.save() instance.delete() serializer = self.serializer_class(comment) diff --git a/tests/test_articles/test_articles.py b/tests/test_articles/test_articles.py index 20ce6f3..95d9374 100644 --- a/tests/test_articles/test_articles.py +++ b/tests/test_articles/test_articles.py @@ -6,118 +6,119 @@ from authors.apps.articles.models import Article, Category from authors.apps.authentication.models import User -class ArticlesTest(APITestCase,BaseTest): + +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)) + 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) - def addcredentials(self,response): + def addcredentials(self, response): self.client.credentials( - HTTP_AUTHORIZATION='Token ' + response) + HTTP_AUTHORIZATION='Token ' + response) def test_create_article(self): self.create_login_user() - response = self.client.post('/api/articles/',self.create_article, - format="json") + response = self.client.post('/api/articles/', self.create_article, + format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) - + def test_no_user_auth_create(self): - response = self.client.post('/api/articles/',self.create_article, - format="json") + response = self.client.post('/api/articles/', self.create_article, + format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_update_article(self): self.create_login_user() - article = self.client.post('/api/articles/',self.create_article, - format="json") + article = self.client.post('/api/articles/', self.create_article, + format="json") articleslug = article.data["slug"] - response = self.client.put('/api/articles/{}/'.format(articleslug), - self.update_article, format="json") + response = self.client.put('/api/articles/{}/'.format(articleslug), + self.update_article, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) - + def test_wrong_update_article(self): self.create_login_user() - article = self.client.post('/api/articles/',self.create_article, - format="json") + article = self.client.post('/api/articles/', self.create_article, + format="json") articleslug = article.data["slug"] - response = self.client.put('/api/articles/{}/'.format(articleslug), - self.wrong_article_update, format="json") + response = self.client.put('/api/articles/{}/'.format(articleslug), + self.wrong_article_update, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - + def test_view_article(self): self.create_login_user() - article = self.client.post('/api/articles/',self.create_article, - format="json") + article = self.client.post('/api/articles/', self.create_article, + format="json") articleslug = article.data["slug"] - response = self.client.get('/api/articles/{}/'.format(articleslug), - format="json") + response = self.client.get('/api/articles/{}/'.format(articleslug), + format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) - + def test_delete_article(self): self.create_login_user() - article = self.client.post('/api/articles/',self.create_article, - format="json") + article = self.client.post('/api/articles/', self.create_article, + format="json") articleslug = article.data["slug"] - response = self.client.delete('/api/articles/{}/'.format(articleslug), - format="json") + response = self.client.delete('/api/articles/{}/'.format(articleslug), + format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) - + def test_no_auth_user_delete(self): articleslug = "x-x-x" - response = self.client.delete('/api/articles/{}/'.format(articleslug), - format="json") + response = self.client.delete('/api/articles/{}/'.format(articleslug), + format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - + def test_no_user_auth_update(self): articleslug = "x-x-x" - response = self.client.put('/api/articles/{}/'.format(articleslug), - self.wrong_article_update, - format="json") + response = self.client.put('/api/articles/{}/'.format(articleslug), + self.wrong_article_update, + format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - + def test_search(self): self.create_login_user() article = self.client.post( '/api/articles/', self.create_article, format="json") articlestitle = article.data["title"] - response = self.client.get('/api/articles/?search={}/'.format(articlestitle), - format="json") + response = self.client.get('/api/articles/?search={}/'.format(articlestitle), + format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) - + def test_filter_author(self): self.create_login_user() article = self.client.post( '/api/articles/', self.create_article, format="json") author = article.data["author"] - response = self.client.get('/api/articles/?author={}/'.format(author), - format="json") + response = self.client.get('/api/articles/?author={}/'.format(author), + format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) - + def test_filter_tags(self): self.create_login_user() article = self.client.post( '/api/articles/', self.create_article, format="json") tag = article.data["tags"] - response = self.client.get('/api/articles/?tag={}/'.format(tag), - format="json") + response = self.client.get('/api/articles/?tag={}/'.format(tag), + format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) - + def test_filter_title(self): self.create_login_user() article = self.client.post( '/api/articles/', self.create_article, format="json") title = article.data["title"] - response = self.client.get('/api/articles/?title={}/'.format(title), - format="json") + response = self.client.get('/api/articles/?title={}/'.format(title), + format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) def test_share_article(self): @@ -139,12 +140,66 @@ def test_share_wrong_article_slug(self): self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + def test_report_an_article(self): + self.create_login_user() + article = self.client.post('/api/articles/', self.create_article, + format="json") + slug = article.data['slug'] + self.username = 'test2' + self.password = '12345678' + self.email = 'test2@andela.com' + self.create_login_user() + response = self.client.post(f'/api/articles/{slug}/report/', self.report_data, + format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + def test_report_an_article_when_owner(self): + self.create_login_user() + article = self.client.post('/api/articles/', self.create_article, + format="json") + slug = article.data['slug'] + response = self.client.post(f'/api/articles/{slug}/report/', self.report_data, + format="json") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_report_an_article_twice(self): + self.create_login_user() + article = self.client.post('/api/articles/', self.create_article, + format="json") + slug = article.data['slug'] + self.username = 'test2' + self.password = '12345678' + self.email = 'test2@andela.com' + self.create_login_user() + self.client.post(f'/api/articles/{slug}/report/', self.report_data, + format="json") + response = self.client.post(f'/api/articles/{slug}/report/', self.report_data, + format="json") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_report_an_article_with_no_reason(self): + self.create_login_user() + article = self.client.post('/api/articles/', self.create_article, + format="json") + slug = article.data['slug'] + self.username = 'test2' + self.password = '12345678' + self.email = 'test2@andela.com' + self.create_login_user() + response = self.client.post( + f'/api/articles/{slug}/report/', format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_retrieve_reported_articles(self): + user = User.objects.create_superuser( + 'admin', 'admin@admin.com', '12345678') + token = user.token(1) + self.addcredentials(token) + response = self.client.get('/api/escalations/', format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) - + def test_retrieve_reported_articles_when_not_admin(self): + self.create_login_user() + response = self.client.get('/api/escalations/', format="json") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - \ No newline at end of file diff --git a/tests/test_authentication/test_base.py b/tests/test_authentication/test_base.py index 8db615e..1b24a51 100644 --- a/tests/test_authentication/test_base.py +++ b/tests/test_authentication/test_base.py @@ -157,3 +157,8 @@ def __init__(self): "email": self.email } } + self.report_data={ + "report": { + "reason": "plagiarism" + } + }