diff --git a/authors/apps/articles/migrations/0001_initial.py b/authors/apps/articles/migrations/0001_initial.py index e7ca05c..12aba67 100644 --- a/authors/apps/articles/migrations/0001_initial.py +++ b/authors/apps/articles/migrations/0001_initial.py @@ -1,5 +1,6 @@ -# Generated by Django 2.1.5 on 2019-03-20 11:50 +# Generated by Django 2.1.5 on 2019-03-21 09:14 +from django.conf import settings import django.contrib.postgres.fields from django.db import migrations, models import django.db.models.deletion @@ -11,6 +12,7 @@ class Migration(migrations.Migration): dependencies = [ ('profiles', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -18,10 +20,10 @@ class Migration(migrations.Migration): name='Article', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=100)), + ('title', models.CharField(max_length=255)), ('body', models.TextField()), ('slug', models.SlugField(blank=True, unique=True)), - ('description', models.CharField(max_length=100)), + ('description', models.CharField(max_length=255)), ('likes', models.IntegerField(default=0)), ('dislikes', models.IntegerField(default=0)), ('created_at', models.DateTimeField(auto_now_add=True)), @@ -31,6 +33,8 @@ class Migration(migrations.Migration): ('tagList', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, default=list, size=None)), ('favorited', models.BooleanField(default=False)), ('favoritesCount', models.IntegerField(default=0)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='author_articles', to='profiles.Profile')), + ('favorites', models.ManyToManyField(blank=True, related_name='favorited_articles', to='profiles.Profile')), ], options={ 'ordering': ['-created_at'], @@ -43,6 +47,17 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('likes', models.BooleanField(default=False, null=True)), ('created_at', models.DateTimeField(auto_now_add=True)), + ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='articles.Article')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='profiles.Profile')), + ], + ), + migrations.CreateModel( + name='Bookmark', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('bookmarked_at', models.DateTimeField(auto_now_add=True)), + ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='articles.Article')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], ), migrations.CreateModel( @@ -52,6 +67,7 @@ class Migration(migrations.Migration): ('rated_on', models.DateTimeField(auto_now_add=True)), ('score', models.DecimalField(decimal_places=2, max_digits=5)), ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ratings', to='articles.Article')), + ('rated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scores', to=settings.AUTH_USER_MODEL)), ], options={ 'ordering': ['-score'], diff --git a/authors/apps/articles/migrations/0002_auto_20190320_1150.py b/authors/apps/articles/migrations/0002_auto_20190320_1150.py deleted file mode 100644 index dc9eb21..0000000 --- a/authors/apps/articles/migrations/0002_auto_20190320_1150.py +++ /dev/null @@ -1,44 +0,0 @@ -# Generated by Django 2.1.5 on 2019-03-20 11:50 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('profiles', '0001_initial'), - ('articles', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='rating', - name='rated_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scores', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='articlelikesdislikes', - name='article', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='articles.Article'), - ), - migrations.AddField( - model_name='articlelikesdislikes', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='profiles.Profile'), - ), - migrations.AddField( - model_name='article', - name='author', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='author_articles', to='profiles.Profile'), - ), - migrations.AddField( - model_name='article', - name='favorites', - field=models.ManyToManyField(blank=True, related_name='favorited_articles', to='profiles.Profile'), - ), - ] diff --git a/authors/apps/articles/models.py b/authors/apps/articles/models.py index 5587cd1..2b8691a 100644 --- a/authors/apps/articles/models.py +++ b/authors/apps/articles/models.py @@ -3,6 +3,8 @@ from django.contrib.postgres.fields import ArrayField from rest_framework.response import Response from rest_framework import status + +from authors.apps.authentication.models import User from authors.apps.profiles.models import Profile from authors.settings import WORD_LENGTH, WORD_PER_MINUTE @@ -135,3 +137,8 @@ class Meta: def __str__(self): return self.reason +class Bookmark(models.Model): + """Model for creating bookmarks of an article by a user.""" + user = models.ForeignKey(User, on_delete=models.CASCADE, blank=False) + article = models.ForeignKey(Article, on_delete=models.CASCADE) + bookmarked_at = models.DateTimeField(auto_now_add=True) diff --git a/authors/apps/articles/serializers.py b/authors/apps/articles/serializers.py index 9e29455..0c09d4e 100644 --- a/authors/apps/articles/serializers.py +++ b/authors/apps/articles/serializers.py @@ -2,6 +2,8 @@ from authors.apps.comments.models import Comment from .models import Article, ArticleLikesDislikes, Rating, ReportedArticle +from .models import Article, ArticleLikesDislikes, Rating, Bookmark + from ..profiles.serializers import ProfileSerializer @@ -114,3 +116,9 @@ class ReportedArticleSerializer(serializers.ModelSerializer): class Meta: model = ReportedArticle fields = ('id', 'reporter', 'article', 'reason',) +class BookmarkSerializer(serializers.ModelSerializer): + article = ArticleSerializer(read_only=True) + + class Meta: + model = Bookmark + fields = ('id', 'article', 'bookmarked_at',) diff --git a/authors/apps/articles/tests/test_bookmarks.py b/authors/apps/articles/tests/test_bookmarks.py new file mode 100644 index 0000000..554e96d --- /dev/null +++ b/authors/apps/articles/tests/test_bookmarks.py @@ -0,0 +1,99 @@ +from django.urls import reverse +from authors.apps.articles.tests.base_class import ArticlesBaseTest +from ..models import Article + + +class TestBookmarks(ArticlesBaseTest): + """Bookmark tests""" + def setUp(self): + super().setUp() + + def test_bookmark_your_own_article(self): + """Test for bookmarking your own article an article""" + self.add_article() + article = Article.objects.all().first() + url = reverse("articles:bookmark_article", kwargs={'slug': article.slug}) + response = self.client.post(url) + self.assertEqual(response.status_code, 403) + self.assertIn(response.data['error'], 'You can not bookmark your own article') + + def test_bookmark_another_authors_article(self): + """Test for bookmarking another authors article""" + self.add_article() + self.register_and_login_new_user() + article = Article.objects.all().first() + url = reverse("articles:bookmark_article", kwargs={'slug': article.slug}) + response = self.client.post(url) + self.assertEqual(response.status_code, 201) + self.assertIn(response.data['message'], 'Article has been bookmarked') + + def test_bookmark_article_that_does_not_exist(self): + """Test for bookmarking an article that does not exist""" + self.add_article() + self.register_and_login_new_user() + url = reverse("articles:bookmark_article", kwargs={'slug': 't990'}) + response = self.client.post(url) + self.assertEqual(response.status_code, 404) + self.assertIn(response.data['detail'], 'Not found.') + + def test_bookmark_article_that_has_already_been_bookmarked(self): + """Test bookmarking an article that has already been bookmarked""" + self.add_article() + self.register_and_login_new_user() + article = Article.objects.all().first() + self.client.post(reverse("articles:bookmark_article", kwargs={'slug': article.slug})) + url = reverse("articles:bookmark_article", kwargs={'slug': article.slug}) + response = self.client.post(url) + self.assertEqual(response.status_code, 400) + self.assertIn(response.data['error'], 'You have already bookmarked this article') + + def test_unbookmark_article(self): + """Test for unbookmarking an article""" + self.add_article() + self.register_and_login_new_user() + article = Article.objects.all().first() + self.client.post(reverse("articles:bookmark_article", kwargs={'slug': article.slug})) + url = reverse("articles:bookmark_article", kwargs={'slug': article.slug}) + response = self.client.delete(url) + self.assertEqual(response.status_code, 200) + self.assertIn(response.data['message'], 'Article has been unbookmarked') + + def test_unbookmark_article_that_has_already_been_unbookmarked(self): + """Test for unbookmarking an article that has already been unbookmarked""" + self.add_article() + self.register_and_login_new_user() + article = Article.objects.all().first() + self.client.post(reverse("articles:bookmark_article", kwargs={'slug': article.slug})) + self.client.delete(reverse("articles:bookmark_article", kwargs={'slug': article.slug})) + url = reverse("articles:bookmark_article", kwargs={'slug': article.slug}) + response = self.client.delete(url) + self.assertEqual(response.status_code, 400) + self.assertIn(response.data['error'], 'Article does not exist in your bookmarks list') + + def test_get_bookmarked_articles(self): + """Test to get bookmarked articles by a user""" + self.add_article() + self.register_and_login_new_user() + article = Article.objects.all().first() + self.client.post(reverse("articles:bookmark_article", kwargs={'slug': article.slug})) + url = reverse('articles:articles_bookmarked') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + def test_get_no_articles_bookmarked(self): + """Test to get empty bookmarks list""" + self.add_article() + self.register_and_login_new_user() + url = reverse('articles:articles_bookmarked') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertIn(response.data['message'], 'You have not bookmarked any articles yet') + + def test_unbookmark_article_that_does_not_exist(self): + """Test unbookmark article that does not exist in the bookmarks list""" + self.add_article() + self.register_and_login_new_user() + url = reverse("articles:bookmark_article", kwargs={'slug': 't990'}) + response = self.client.delete(url) + self.assertEqual(response.status_code, 404) + self.assertIn(response.data['detail'], 'Not found.') diff --git a/authors/apps/articles/tests/test_data.py b/authors/apps/articles/tests/test_data.py index 97d73bb..8ad25ae 100644 --- a/authors/apps/articles/tests/test_data.py +++ b/authors/apps/articles/tests/test_data.py @@ -3,7 +3,8 @@ "title": "When ooh in oo updated ocean", "description": "this is the description,.", - "body": "this is the body" + "body": "this is the body", + "tagList": ["python", "java"] } diff --git a/authors/apps/articles/urls.py b/authors/apps/articles/urls.py index 858d44e..7abf34d 100644 --- a/authors/apps/articles/urls.py +++ b/authors/apps/articles/urls.py @@ -3,9 +3,8 @@ from .views import ( ArticlesApiView, ArticleDetailApiView, ArticleLikeApiView, RateArticleView, - FavoriteHandlerView, ArticleTagsApiView, ReportArticleView - ) - + FavoriteHandlerView, ArticleTagsApiView, ReportArticleView, + BookmarksApiView,BookmarksListView) urlpatterns = [ path('articles/', ArticlesApiView.as_view(), name='articles'), @@ -31,4 +30,6 @@ ), path('articles//favorite', FavoriteHandlerView.as_view(), name='article-favorite'), + path('articles//bookmark/', BookmarksApiView.as_view(), name="bookmark_article"), + path('bookmarks/', BookmarksListView.as_view(), name="articles_bookmarked"), ] diff --git a/authors/apps/articles/views.py b/authors/apps/articles/views.py index d20e2f3..9e15fd3 100644 --- a/authors/apps/articles/views.py +++ b/authors/apps/articles/views.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from rest_framework import status, filters from rest_framework import generics, permissions + from . import ( serializers, permissions as app_permissions @@ -11,7 +12,7 @@ from .pagination import ArticlesLimitOffsetPagination from .renderers import ArticleJSONRenderer from .utils import Utils -from authors.apps.articles.models import Article, ArticleLikesDislikes, Rating +from authors.apps.articles.models import Article, ArticleLikesDislikes, Rating, Bookmark from ..profiles.models import Profile from authors.apps.core.utils import Utilities @@ -69,15 +70,10 @@ def get_queryset(self): return queryset def post(self, request): - serializer = self.serializer_class( - data=request.data, - context={"request": request} - ) + serializer = self.serializer_class(data=request.data, context={"request": request}) serializer.is_valid(raise_exception=True) - serializer.save( - author=Profile.objects.filter(user=request.user).first(), - slug=Utils.create_slug(request.data['title']) - ) + serializer.save(author=Profile.objects.filter(user=request.user).first(), + slug=Utils.create_slug(request.data['title'])) return Response(serializer.data, status=status.HTTP_201_CREATED) @@ -91,11 +87,8 @@ def get(self, request, slug): article = self.get_object(slug) context = {"request": request} if not article: - return Response({ - 'errors': 'that article was not found' - }, status=status.HTTP_404_NOT_FOUND) - serialized_data = self.serializer_class(article, - context=context) + return Response({'errors': 'that article was not found'}, status=status.HTTP_404_NOT_FOUND) + serialized_data = self.serializer_class(article, context=context) return Response(serialized_data.data, status=status.HTTP_200_OK) @@ -106,10 +99,7 @@ def patch(self, request, slug): context = {"request": request} if article: self.check_object_permissions(request, article) - serializer_data = self.serializer_class(article, - article_data, - partial=True, - context=context) + serializer_data = self.serializer_class(article, article_data, partial=True, context=context) serializer_data.is_valid(raise_exception=True) serializer_data.save() return Response(serializer_data.data, @@ -123,13 +113,8 @@ def delete(self, request, slug): if article: self.check_object_permissions(request, article) article.delete() - return Response({ - 'article': 'Article has been deleted'}, - status=status.HTTP_200_OK - ) - return Response({ - 'errors': 'that article was not found' - }, status=status.HTTP_404_NOT_FOUND) + return Response({'article': 'Article has been deleted'}, status=status.HTTP_200_OK) + return Response({'errors': 'that article was not found'}, status=status.HTTP_404_NOT_FOUND) def get_object(self, slug): return Article.objects.filter(slug=slug).first() @@ -142,19 +127,13 @@ class ArticleLikeApiView(generics.GenericAPIView): def post(self, request, slug): """This function enables a user to like or dislike an article.""" article = Article.objects.filter(slug=slug).first() - liked_article = ArticleLikesDislikes.objects.filter( - article_id=article.id, - user_id=request.user.id - ) + liked_article = ArticleLikesDislikes.objects.filter(article_id=article.id, user_id=request.user.id) if len(liked_article) <= 0: # when a user likes the article for the first time # the article is liked. - serializer_data = self.serializer_class(data={ - "user": request.user.id, - "article": article.id, - "likes": True - }) + serializer_data = self.serializer_class(data={"user": request.user.id, "article": article.id, + "likes": True}) serializer_data.is_valid(raise_exception=True) serializer_data.save() data = { @@ -176,10 +155,8 @@ def post(self, request, slug): } } # updates the number of likes and dislikes of a given article - likes = ArticleLikesDislikes.objects.filter( - article_id=article.id, likes=True) - dislikes = ArticleLikesDislikes.objects.filter( - article_id=article.id, likes=False) + likes = ArticleLikesDislikes.objects.filter(article_id=article.id, likes=True) + dislikes = ArticleLikesDislikes.objects.filter(article_id=article.id, likes=False) Article.objects.filter(slug=slug).update( likes=(len(likes)), dislikes=(len(dislikes)), @@ -201,28 +178,19 @@ def post(self, request, slug): score = score_data.get("score", 0) article = get_object_or_404(Article, slug=slug) if score < 0 or score > 5: - return Response( - {"message": "Rating must be between 0 and 5"}, - status=status.HTTP_400_BAD_REQUEST - ) + return Response({"message": "Rating must be between 0 and 5"}, status=status.HTTP_400_BAD_REQUEST) serializer = self.serializer_class(data=score_data) serializer.is_valid(raise_exception=True) if user.username == article.author.user.username: - return Response( - {"message": "You can not rate your own article"}, - status=status.HTTP_403_FORBIDDEN - ) + return Response({"message": "You can not rate your own article"}, status=status.HTTP_403_FORBIDDEN) try: - Rating.objects.get( - rated_by_id=user.pk, - article_id=article.pk, - score=score - ) - return Response( - {"message": "You have already rated the article"}, - status=status.HTTP_200_OK) + Rating.objects.get(rated_by_id=user.pk, + article_id=article.pk, + score=score) + return Response({"message": "You have already rated the article"}, + status=status.HTTP_200_OK) except Rating.DoesNotExist: serializer.save(rated_by=user, article=article) return Response(serializer.data, status=status.HTTP_201_CREATED) @@ -247,18 +215,17 @@ def post(self, request, slug): article = get_object_or_404(Article, slug=slug) if article.author.user.username == user.username: - return Response( - { - "message": "Please favourite another author's article", - }, - status=status.HTTP_403_FORBIDDEN) + return Response( + { + "message": "Please favourite another author's article", + }, + status=status.HTTP_403_FORBIDDEN) if article in user.profile.favorited_articles.all(): return Response({"error": "This article is in your favorites"}, status=status.HTTP_400_BAD_REQUEST) else: - return Article.objects.handle_favorite_an_article( - user_obj=user, slug=article.slug) + return Article.objects.handle_favorite_an_article(user_obj=user, slug=article.slug) def delete(self, request, slug): article = get_object_or_404(Article, slug=slug) @@ -277,12 +244,10 @@ class ReportArticleView(generics.GenericAPIView): def post(self, request, slug): reporter = Profile.objects.get(user=request.user) article = Article.objects.filter(slug=slug).first() - data = { - "reporter": reporter, - "article": article, - "reason": request.data['reason'] if 'reason' in request.data - else '' - } + data = {"reporter": reporter, + "article": article, + "reason": request.data['reason'] if 'reason' in request.data + else ''} if reporter == article.author: return Response({"message": "You cannot report your own article"}, status=status.HTTP_403_FORBIDDEN) @@ -292,7 +257,57 @@ def post(self, request, slug): serializer.save(reporter=reporter, article=article) data = {"subject": "[Article Reported]", "to": serializer.data['article']['author']['email'], - "body": f"Your article was reported,These are the details:\n{data['reason']}"} + "body": f"Your article was reported,These are the details:\n{data['reason']}"} Utilities.send_email(data, 'article_reports') return Response(serializer.data, status=status.HTTP_201_CREATED) + + +class BookmarksApiView(generics.GenericAPIView): + permission_classes = [permissions.IsAuthenticated, ] + renderer_classes = [ArticleJSONRenderer, ] + serializer_class = serializers.BookmarkSerializer + + def post(self, request, slug=None): + """This method creates a bookmark""" + user = request.user + article = get_object_or_404(Article, slug=slug) + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + + is_bookmarked = Bookmark.objects.filter(user_id=user.id, article_id=article.id).exists() + + if not (user.email == article.author.user.email): + + if not is_bookmarked: + + Bookmark.objects.create(article=article, user=user) + + return Response({'message': 'Article has been bookmarked'}, status=status.HTTP_201_CREATED) + + return Response({'error': 'You have already bookmarked this article'}, status=status.HTTP_400_BAD_REQUEST) + + return Response({'error': 'You can not bookmark your own article'}, + status=status.HTTP_403_FORBIDDEN) + + def delete(self, request, slug=None): + user = request.user + article = get_object_or_404(Article, slug=slug) + bookmark = Bookmark.objects.filter(user_id=user.id, article_id=article.id) + + if bookmark: + bookmark.delete() + return Response({'message': 'Article has been unbookmarked'}, status=status.HTTP_200_OK) + return Response({'error': 'Article does not exist in your bookmarks list'}, status=status.HTTP_400_BAD_REQUEST) + + +class BookmarksListView(generics.GenericAPIView): + permission_classes = [permissions.IsAuthenticated, ] + serializer_class = serializers.BookmarkSerializer + + def get(self, request): + bookmarks = Bookmark.objects.filter(user=request.user).all() + if len(bookmarks) < 1: + return Response({'message': 'You have not bookmarked any articles yet'}) + serializer = self.serializer_class(bookmarks, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/authors/apps/comments/migrations/0001_initial.py b/authors/apps/comments/migrations/0001_initial.py index 7c88831..861edc3 100644 --- a/authors/apps/comments/migrations/0001_initial.py +++ b/authors/apps/comments/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.1.5 on 2019-03-20 11:50 +# Generated by Django 2.1.5 on 2019-03-21 09:14 from django.db import migrations, models @@ -10,8 +10,8 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('articles', '0001_initial'), ('profiles', '0001_initial'), - ('articles', '0002_auto_20190320_1150'), ] operations = [ diff --git a/authors/apps/profiles/migrations/0001_initial.py b/authors/apps/profiles/migrations/0001_initial.py index 37f3607..3e699e7 100644 --- a/authors/apps/profiles/migrations/0001_initial.py +++ b/authors/apps/profiles/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.1.5 on 2019-03-19 08:16 +# Generated by Django 2.1.5 on 2019-03-21 09:14 from django.conf import settings from django.db import migrations, models