Skip to content

Commit

Permalink
feature(search and filtering): Add search and custom filtering
Browse files Browse the repository at this point in the history
- Add search functionality
- Add custom filtering based on parameters
- Add tests

[Finishes #164047075]
  • Loading branch information
kevpy committed Mar 26, 2019
1 parent 81345d4 commit 672283c
Show file tree
Hide file tree
Showing 4 changed files with 347 additions and 163 deletions.
25 changes: 24 additions & 1 deletion authors/apps/article/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from cloudinary.models import CloudinaryField
from functools import reduce
import operator

from django.contrib.postgres.search import SearchVector, SearchQuery, SearchVectorField
from django.db import models
from django.shortcuts import get_object_or_404
from django.urls import reverse
Expand All @@ -24,6 +27,7 @@ class Article(models.Model):
updated_at = models.DateTimeField(auto_now=True)
author = models.ForeignKey(
User, related_name="articles", on_delete=models.CASCADE)
search_vector = SearchVectorField(null=True)

def __str__(self):
return self.title
Expand All @@ -45,6 +49,25 @@ def save(self, *args, **kwargs):
self.slug = unique_slug
super().save(*args, **kwargs)

def search_articles(self, args):
""" This method is used to search for articles.
Given a list of arguments, it performs a full text search query.
:params *args: list of arguments to query against
:returns: a queryset
"""
query_args = []

for val in args:
query_args.append(SearchQuery(val))

search_vector = SearchVector('title', 'body', 'slug',
'author__username', 'tag_list__name')

query = Article.objects.annotate(search=search_vector).filter(
search=reduce(operator.and_, query_args, SearchQuery('')))

return query


class ArticleImage(models.Model):
article = models.ForeignKey(
Expand Down
144 changes: 144 additions & 0 deletions authors/apps/article/tests/test_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import json

from rest_framework.test import APIClient, APITestCase
from rest_framework.views import status


class TestSearch(APITestCase):
""" This class tests the Article Search and Filtering feature
"""

client = APIClient()

def setUp(self):
""" This is the set-up method for all feature tests
"""

self.user = {
"user": {
"username": "kibet",
"email": "kibet@olympians.com",
"password": "qwerty12"
}
}

self.profile = {
"bio": "am fantastic",
"interests": "football",
"favorite_quote": "Yes we can",
"mailing_address": "P.O BOX 1080",
"website": "http://www.web.com",
"active_profile": True
}

self.article1 = {
"title": "Andela",
"description": "be epic",
"body": "powering todays teams",
"images": ""
}
self.article2 = {
"title": "Learning some vim",
"description": "Vim is awesome",
"body": "Just random text to test search",
"images": "",
"tag_list": ['programming', 'code']
}

# create user
self.client.post('/api/users/', self.user, format='json')

response = self.client.post(
'/api/users/login/', self.user, format='json')

result = json.loads(response.content)

self.client.credentials(
HTTP_AUTHORIZATION='Token ' + result["user"]["token"])

profile = self.client.post(
'/api/profile/create_profile/', self.profile, format='json')
prof_result = json.loads(profile.content)

article = self.client.post(
'/api/articles/', self.article1, format='json')

article = self.client.post(
'/api/articles/', self.article2, format='json')

def test_empty_search(self):
""" Test search when no query params are provided
"""
response = self.client.get('/api/search/articles', format='json')
result = json.loads(response.content)

self.assertIn('Please provide a search phrase', str(result))
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_search_not_found(self):
""" Test search for non-existing article
"""
response = self.client.get(
'/api/search/articles?search=going&home',
format='json')
result = json.loads(response.content)

self.assertIn('Sorry we could not find what you are looking for.',
str(result))
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

def test_search_found(self):
""" Test search found and article
"""
response = self.client.get(
'/api/search/articles?search=learn&vim',
format='json')
result = json.loads(response.content)

self.assertIn('Just random text to test search', str(result))
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_search_on_article_author(self):
""" Test article search with title and author
"""
response = self.client.get(
'/api/search/articles?search=learn&vim&author=kibet',
format='json')
result = json.loads(response.content)

self.assertIn('Just random text to test search', str(result))
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_search_with_author(self):
""" Test article search with title and author
"""
response = self.client.get(
'/api/search/articles?author=kibet',
format='json')
result = json.loads(response.content)

self.assertIn('Just random text to test search', str(result))
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_search_with_tag(self):
""" Test article search with title and author
"""
response = self.client.get(
'/api/search/articles?tag=code',
format='json')
result = json.loads(response.content)

self.assertIn('Just random text to test search', str(result))
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_mismatch_tag_title(self):
""" Test article search with title and author
"""
response = self.client.get(
'/api/search/articles?title=andela&tag=code',
format='json')
result = json.loads(response.content)

self.assertIn('Sorry we could not find what you are looking for.',
str(result))
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
23 changes: 12 additions & 11 deletions authors/apps/article/urls.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from django.urls import path
from .views import (ArticlesAPIView, RetrieveArticleAPIView, LikeAPIView, DislikeAPIView, RateAPIView, FavouriteAPIView, CommentsAPIView,
RetrieveCommentsAPIView, SubCommentAPIView, LikeUnlikeAPIView, CommentDislikeAPIView, BookmarkAPIView,
BookmarksAPIView, ReportArticlesView, GetSingleReportView, GetAllReportsViews, SocialShareArticle)

from .views import (ArticlesAPIView, RetrieveArticleAPIView, LikeAPIView, DislikeAPIView, RateAPIView, FavouriteAPIView, CommentsAPIView,
RetrieveCommentsAPIView, SubCommentAPIView, LikeUnlikeAPIView, CommentDislikeAPIView, BookmarkAPIView, BookmarksAPIView)
from .views import (ArticlesAPIView, RetrieveArticleAPIView, LikeAPIView, DislikeAPIView, RateAPIView, FavouriteAPIView,
CommentsAPIView,
RetrieveCommentsAPIView, SubCommentAPIView, LikeUnlikeAPIView, CommentDislikeAPIView,
BookmarkAPIView,
BookmarksAPIView, ReportArticlesView, GetSingleReportView, GetAllReportsViews, SocialShareArticle,
SearchArticles)


app_name = "articles"

urlpatterns = [
Expand All @@ -19,12 +19,13 @@
path('articles/<slug>/comments/<pk>', RetrieveCommentsAPIView.as_view()),
path('articles/<slug>/comments/<pk>/subcomment', SubCommentAPIView.as_view()),
path('articles/<slug>/like_comment/<pk>', LikeUnlikeAPIView.as_view()),
path('articles/<slug>/dislike_comment/<pk>',CommentDislikeAPIView.as_view()),
path('articles/<slug>/dislike_comment/<pk>', CommentDislikeAPIView.as_view()),
path('articles/<slug>/favorite', FavouriteAPIView.as_view()),
path('articles/<slug>/bookmark', BookmarkAPIView.as_view()),
path('bookmarks/', BookmarksAPIView.as_view()),
path('report/<slug>/',ReportArticlesView.as_view()),
path('reports/<slug>/',GetSingleReportView.as_view()),
path('reports/',GetAllReportsViews.as_view()),
path("articles/<str:slug>/share/<str:provider>", SocialShareArticle.as_view() , name="share_article")
path('report/<slug>/', ReportArticlesView.as_view()),
path('reports/<slug>/', GetSingleReportView.as_view()),
path('reports/', GetAllReportsViews.as_view()),
path("articles/<str:slug>/share/<str:provider>", SocialShareArticle.as_view(), name="share_article"),
path('search/articles', SearchArticles.as_view())
]
Loading

0 comments on commit 672283c

Please sign in to comment.