diff --git a/authors/apps/articles/migrations/0001_initial.py b/authors/apps/articles/migrations/0001_initial.py index 9e51004..169cc66 100644 --- a/authors/apps/articles/migrations/0001_initial.py +++ b/authors/apps/articles/migrations/0001_initial.py @@ -1,6 +1,5 @@ -# Generated by Django 2.1.5 on 2019-03-14 13:36 +# Generated by Django 2.1.5 on 2019-03-19 08:16 -from django.conf import settings import django.contrib.postgres.fields from django.db import migrations, models import django.db.models.deletion @@ -11,8 +10,6 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('profiles', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -33,8 +30,6 @@ 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, to='profiles.Profile')), - ('favorites', models.ManyToManyField(blank=True, related_name='favorited_articles', to='profiles.Profile')), ], options={ 'ordering': ['-created_at'], @@ -47,8 +42,6 @@ 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( @@ -58,7 +51,6 @@ 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_20190319_0816.py b/authors/apps/articles/migrations/0002_auto_20190319_0816.py new file mode 100644 index 0000000..d2f1724 --- /dev/null +++ b/authors/apps/articles/migrations/0002_auto_20190319_0816.py @@ -0,0 +1,44 @@ +# Generated by Django 2.1.5 on 2019-03-19 08:16 + +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 f43dee4..992dd49 100644 --- a/authors/apps/articles/models.py +++ b/authors/apps/articles/models.py @@ -42,7 +42,11 @@ def unfavorite_an_article(self, request_user, slug): class Article(models.Model): title = models.CharField(max_length=100) - author = models.ForeignKey(Profile, on_delete=models.CASCADE) + author = models.ForeignKey( + Profile, + on_delete=models.CASCADE, + related_name="author_articles" + ) body = models.TextField() slug = models.SlugField(unique=True, blank=True) description = models.CharField(max_length=100) @@ -52,12 +56,14 @@ class Article(models.Model): updated_at = models.DateTimeField(auto_now=True, null=True) image = models.URLField(blank=True) user_rating = models.CharField(max_length=10, default='0') - tagList = ArrayField(models.CharField( - max_length=200), default=list, blank=True) + tagList = ArrayField( + models.CharField(max_length=200), + default=list, + blank=True, + ) favorites = models.ManyToManyField(Profile, related_name='favorited_articles', blank=True) favorited = models.BooleanField(default=False) favoritesCount = models.IntegerField(default=0) - objects = ArticleManager() class Meta: ordering = ['-created_at'] diff --git a/authors/apps/articles/tests/base_class.py b/authors/apps/articles/tests/base_class.py index 7eb43d0..aad62df 100644 --- a/authors/apps/articles/tests/base_class.py +++ b/authors/apps/articles/tests/base_class.py @@ -11,16 +11,16 @@ def setUp(self): def add_article(self): self.register_and_login_user() - response = self.client.post(self.articles_url, + self.client.post(self.articles_url, data=valid_article, format='json') - self.client.post(self.articles_url, + return self.client.post(self.articles_url, data=valid_article, format='json') def add_tagged_article(self): self.register_and_login_user() - self.client.post(self.articles_url, + return self.client.post(self.articles_url, data=valid_article_with_tags, format='json') diff --git a/authors/apps/articles/tests/test_search.py b/authors/apps/articles/tests/test_search.py new file mode 100644 index 0000000..0b98a1e --- /dev/null +++ b/authors/apps/articles/tests/test_search.py @@ -0,0 +1,37 @@ +from django.urls import reverse + +from rest_framework import status + +from authors.apps.articles.tests.base_class import ArticlesBaseTest +from .test_data import valid_article +from ..models import Article + + +class TestSearchArticle(ArticlesBaseTest): + + def test_search_by_keyword(self): + """ + Tests if a user can search articles by keyword + """ + self.add_article() + response = self.client.get(self.articles_url+"?keyword=the") + self.assertIn("the", response.data['articles'][0]['description']) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_search_by_tag(self): + """ + Tests if a user can search articles by tag + """ + self.add_tagged_article() + response = self.client.get(self.articles_url+"?tag=apps") + self.assertIn("apps", response.data['articles'][0]['tagList']) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_search_by_author(self): + """ + Tests if a user can search articles by an author + """ + self.add_tagged_article() + response = self.client.get(self.articles_url+"?author=Bagzie12") + self.assertIn("Bagzie12", response.data['articles'][0]['author']['username']) + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/authors/apps/articles/views.py b/authors/apps/articles/views.py index f88e4ba..fd3badf 100644 --- a/authors/apps/articles/views.py +++ b/authors/apps/articles/views.py @@ -1,6 +1,8 @@ +from itertools import chain +from django.db.models import Q from django.shortcuts import get_object_or_404 from rest_framework.response import Response -from rest_framework import status +from rest_framework import status, filters from rest_framework import generics, permissions from . import ( serializers, @@ -16,8 +18,54 @@ class ArticlesApiView (generics.ListCreateAPIView): permission_classes = (permissions.IsAuthenticatedOrReadOnly,) serializer_class = serializers.ArticleSerializer - queryset = Article.objects.all() pagination_class = ArticlesLimitOffsetPagination + queryset = Article.objects.all() + search_fields = ( + 'title', + 'body', + 'description', + 'tagList', + 'author__username', + 'favorited_articles' + ) + + def get_queryset(self): + queryset = self.queryset + tag = self.request.query_params.get('tag', None) + keyword = self.request.query_params.get('keyword', None) + favorite = self.request.query_params.get('favorite', None) + author = self.request.query_params.get('author', None) + + queryset_keyword, queryset_tag, queryset_default, \ + queryset_favorite, queryset_author = [], [], [], [], [] + + if keyword: + result = ( + Q(title__icontains=keyword) | + Q(body__icontains=keyword) | + Q(description__icontains=keyword) + ) + queryset_keyword = queryset.filter(result) + + elif tag: + queryset_tag = queryset.filter(tagList__icontains=tag) + elif favorite: + queryset_favorite = \ + queryset.filter(favorites__user__username__icontains=favorite) + elif author: + queryset_author = \ + queryset.filter(author__user__username__icontains=author) + else: + queryset_default = queryset.all() + + queryset = list(chain( + queryset_keyword, + queryset_tag, + queryset_favorite, + queryset_author, + queryset_default + )) + return queryset def post(self, request): data = request.data.get('article') @@ -187,7 +235,8 @@ def get(self, request): for tag in Article.get_all_tags(): merged += tag return Response({"tags": set(merged)}) - + + class FavoriteHandlerView(generics.GenericAPIView): permission_classes = [permissions.IsAuthenticated, ] renderer_classes = [ArticleJSONRenderer, ] diff --git a/authors/apps/authentication/migrations/0001_initial.py b/authors/apps/authentication/migrations/0001_initial.py index 286efc0..373637d 100644 --- a/authors/apps/authentication/migrations/0001_initial.py +++ b/authors/apps/authentication/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.1.5 on 2019-03-14 07:12 +# Generated by Django 2.1.5 on 2019-03-19 08:16 from django.db import migrations, models diff --git a/authors/apps/authentication/views.py b/authors/apps/authentication/views.py index 68e7899..ea49f03 100644 --- a/authors/apps/authentication/views.py +++ b/authors/apps/authentication/views.py @@ -149,6 +149,7 @@ class ChangePasswordAPIView(generics.GenericAPIView): # Allow any user (authenticated or not) to hit this endpoint. # then allows users to set password permission_classes = (AllowAny,) + serializer_class = UserSerializer def patch(self, request): try: diff --git a/authors/apps/profiles/migrations/0001_initial.py b/authors/apps/profiles/migrations/0001_initial.py index e334a14..37f3607 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-14 07:12 +# Generated by Django 2.1.5 on 2019-03-19 08:16 from django.conf import settings from django.db import migrations, models diff --git a/authors/settings.py b/authors/settings.py index 44cccc4..7a25f0a 100644 --- a/authors/settings.py +++ b/authors/settings.py @@ -35,6 +35,7 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django_filters', 'corsheaders', 'django_extensions', @@ -147,6 +148,10 @@ 'DEFAULT_AUTHENTICATION_CLASSES': ( 'authors.apps.authentication.backends.JWTAuthentication', ), + + 'DEFAULT_FILTER_BACKENDS': ( + 'django_filters.rest_framework.DjangoFilterBackend', + ) } SWAGGER_SETTINGS = {