Skip to content

Commit

Permalink
feat(searching): Add Search Functionality and Custom filtering based …
Browse files Browse the repository at this point in the history
…on parameters

- Enable user to filter articles basing on author, title and tag name

[Finishes  #159952016]
  • Loading branch information
reiosantos authored and Innocent Asiimwe committed Sep 23, 2018
1 parent e6d51e7 commit 01667cc
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 13 deletions.
52 changes: 52 additions & 0 deletions authors/apps/articles/filter_search_extras.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""
Define extra methods to handle extra work for
making search functionality
"""


def get_response(attrs):
"""
:param attrs:
:return:
"""
all_fields, author_and_tag, author_and_title, author_only, queryset, tag_only, \
title_and_tag, title_only, author_query, title_query, tag_query = attrs

boolean_mapping = {
"all_fields": all_fields,
"author_and_tag": author_and_tag,
"author_and_title": author_and_title,
"title_and_tag": title_and_tag,
"author_only": author_only,
"title_only": title_only,
"tag_only": tag_only,
}
function_mapping = {
"all_fields": lambda: queryset.filter(author_query & title_query & tag_query),
"author_and_tag": lambda: queryset.filter(author_query & tag_query),
"author_and_title": lambda: queryset.filter(author_query & title_query),
"title_and_tag": lambda: queryset.filter(title_query & tag_query),
"author_only": lambda: queryset.filter(author_query),
"title_only": lambda: queryset.filter(title_query),
"tag_only": lambda: queryset.filter(tag_query),
}
for key, val in boolean_mapping.items():
if val:
return function_mapping.get(key)()
return queryset.all()


def extra_vars(all_fields, author, tag, title):
"""
:param all_fields:
:param author:
:param tag:
:param title:
:return:
"""
title_and_tag = (title and tag and not author)
author_only = (author and not all_fields)
title_only = (title and not author and not tag)
tag_only = (tag and not title and not author)

return author_only, tag_only, title_and_tag, title_only
39 changes: 39 additions & 0 deletions authors/apps/articles/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""
Implements article search and filter
"""
from urllib.parse import unquote

from django.db import models
from django.db.models import Q

from authors.apps.articles.filter_search_extras import extra_vars, get_response


class ArticleManager(models.Manager):
"""
define custom manager for articles
"""

def search(self, params):
"""
customised search functionality
"""
author = unquote(params.get("author", ""))
title = unquote(params.get("title", ""))
tag = unquote(params.get("tag", ""))

author_query = (Q(author__username__icontains=author) | Q(author__email__exact=author))
tag_query = Q(tags__tag_name__exact=tag)
title_query = Q(title__icontains=title)

all_fields = (author and title and tag)
author_and_title = (author and title and not tag)
author_and_tag = (author and tag and not title)
author_only, tag_only, title_and_tag, title_only = extra_vars(all_fields, author, tag, title)

queryset = self.get_queryset()

attrs = (all_fields, author_and_tag, author_and_title, author_only, queryset, tag_only,
title_and_tag, title_only, author_query, title_query, tag_query)

return get_response(attrs)
3 changes: 3 additions & 0 deletions authors/apps/articles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class to declare an article
from django.db.models import Avg
from django.utils import timezone

from authors.apps.articles.filters import ArticleManager
from authors.apps.articles.utils import generate_slug
from authors.apps.authentication.models import User

Expand All @@ -24,6 +25,8 @@ class Article(models.Model):
"""
A model for an article
"""
objects = ArticleManager()

slug = models.SlugField(max_length=100, unique=True)

author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="articles", null=True)
Expand Down
11 changes: 0 additions & 11 deletions authors/apps/articles/tests/test_articles.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,6 @@ def test_post_new_article(self):
self.assertIn('article', response.json())
self.assertIsInstance(response.json().get("article"), dict)

def test_article_slug(self):
val = 0
while val < 10:
response = self.client.post(
"/api/articles/", data=json.dumps(
self.post_article), content_type='application/json')
self.assertEqual(201, response.status_code)
self.assertIn('article', response.json())
self.assertIsInstance(response.json().get("article"), dict)
val += 1

def test_post_article_missing_data(self):
response = self.client.post(
"/api/articles/", data=json.dumps(
Expand Down
9 changes: 9 additions & 0 deletions authors/apps/articles/tests/test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,12 @@ class TestData:
user_name = "iroq"
user_email = "iroq@sims.andela"
password = "teamiroq1"

post_article_tags = {
"article": {
"title": "Yet another Sand Blog",
"description": "Sand is m testing",
"body": "another that am doin test",
"tags": ["python", "software", "english"]
}
}
114 changes: 114 additions & 0 deletions authors/apps/articles/tests/test_filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""
tests for search filters
"""
import json
from urllib.parse import quote

from django.test import TestCase
from rest_framework import status
from rest_framework.test import APIClient

from authors.apps.articles.models import Article
from authors.apps.articles.tests.test_data import TestData
from authors.apps.authentication.models import User


class Tests(TestCase, TestData):

def setUp(self):
"""
setup tests
"""
User.objects.all().delete()
Article.objects.all().delete()

self.login_data = {"user": {"email": self.user_email, "password": self.password,
}
}
self.user_data = {"user": {"username": self.user_name, "email": self.user_email,
"password": self.password,
}
}
self.client = APIClient()

self.response = self.client.post(
"/api/users/",
self.user_data,
format="json")
self.user = User.objects.get(email=self.user_email)
self.user.is_active = True
self.user.is_email_verified = True
self.user.save()
self.response = self.client.post(
"/api/users/login/",
self.login_data,
format="json")
self.assertEqual(status.HTTP_200_OK, self.response.status_code)
self.assertIn('token', self.response.data)
token = self.response.data.get("token", None)
self.client.credentials(HTTP_AUTHORIZATION="Token {0}".format(token))

def test_searching_with_all_params(self):
val = 0
while val < 10:
response = self.client.post(
"/api/articles/", data=json.dumps(
self.post_article_tags), content_type='application/json')
self.assertEqual(201, response.status_code)
self.assertIn('article', response.json())
self.assertIsInstance(response.json().get("article"), dict)
val += 1

response = self.client.get(
"/api/articles/?author=iroq", content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertIn('articles', response.json())
self.assertIn('results', response.json().get("articles"))

response = self.client.get(
"/api/articles/?author=iroq&title={0}&tag=python".format(quote('Yet another Sand Blogs')),
content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertIn('article', response.json())

response = self.client.get(
"/api/articles/?author=iroq&title={0}&tag=python".format(quote('Yet another Sand Blogs')),
content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertIn('article', response.json())

response = self.client.get(
"/api/articles/?author=iroq&tag=python",
content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertIn('articles', response.json())

response = self.client.get(
"/api/articles/?title={0}&tag=python".format(quote('Yet another Sand Blogs')),
content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertIn('article', response.json())

response = self.client.get(
"/api/articles/?title={0}&author=iroq".format(quote('Yet another Sand Blogs')),
content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertIn('article', response.json())

response = self.client.get(
"/api/articles/?author=iroq".format(quote('Yet another Sand Blogs')),
content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertIn('articles', response.json())

response = self.client.get(
"/api/articles/?title={0}".format(quote('Yet another Sand Blog')),
content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertIn('articles', response.json())

response = self.client.get(
"/api/articles/?tag=python".format(quote('Yet another Sand Blogs')),
content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertIn('articles', response.json())
4 changes: 2 additions & 2 deletions authors/apps/articles/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ def list(self, request):
:param request:
:return:
"""
author = request.query_params.get("author", None)
limit = request.query_params.get("limit", 20)
offset = request.query_params.get("offset", 0)

Expand All @@ -53,7 +52,8 @@ def to_int(val):
except ValueError:
raise InvalidQueryParameterException()

queryset = Article.objects.all()
queryset = Article.objects.search(request.query_params)

if queryset.count() > 0:
queryset = queryset[offset:]

Expand Down

0 comments on commit 01667cc

Please sign in to comment.