Skip to content

Commit 4dc3e35

Browse files
committed
fixup and adding test
1 parent bb4e5aa commit 4dc3e35

File tree

9 files changed

+205
-24
lines changed

9 files changed

+205
-24
lines changed

conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22

33
import pytest
4+
from rest_framework.test import APIClient
45

56

67
def pytest_addoption(parser):
@@ -17,3 +18,8 @@ def pytest_configure(config):
1718
@pytest.fixture(autouse=True)
1819
def settings_modification(settings):
1920
settings.CELERY_ALWAYS_EAGER = True
21+
22+
23+
@pytest.fixture
24+
def api_client():
25+
return APIClient()

readthedocs/restapi/urls.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from readthedocs.restapi.views import (
1212
core_views, footer_views, search_views, task_views, integrations
1313
)
14+
from readthedocs.search.api import PageSearchAPIView
1415

1516
from .views.model_views import (BuildViewSet, BuildCommandViewSet,
1617
ProjectViewSet, NotificationViewSet,
@@ -85,19 +86,15 @@
8586
name='api_webhook'),
8687
]
8788

89+
api_search_urls = [
90+
url(r'^docsearch/$', PageSearchAPIView.as_view(), name='doc_search'),
91+
]
92+
8893
urlpatterns += function_urls
89-
urlpatterns += search_urls
9094
urlpatterns += task_urls
95+
urlpatterns += search_urls
9196
urlpatterns += integration_urls
92-
93-
try:
94-
from readthedocsext.search.docsearch import DocSearch
95-
api_search_urls = [
96-
url(r'^docsearch/$', DocSearch.as_view(), name='doc_search'),
97-
]
98-
urlpatterns += api_search_urls
99-
except ImportError:
100-
pass
97+
urlpatterns += api_search_urls
10198

10299
try:
103100
from readthedocsext.donate.restapi.urls import urlpatterns as sustainability_urls

readthedocs/search/api.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ class PageSearchAPIView(generics.ListAPIView):
1212
serializer_class = PageSearchSerializer
1313

1414
def get_queryset(self):
15-
query = self.request.query_params.get('query')
15+
"""Return Elasticsearch DSL Search object instead of Django Queryset.
16+
17+
Django Queryset and elasticsearch-dsl ``Search`` object is similar pattern.
18+
So for searching, its possible to return ``Search`` object instead of queryset.
19+
The ``filter_backends`` and ``pagination_class`` is compatible with ``Search``
20+
"""
21+
query = self.request.query_params.get('query', '')
1622
queryset = PageDocument.search(query=query)
1723
return queryset

readthedocs/search/documents.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,12 @@ def faceted_search(cls, query, projects_list=None, versions_list=None, using=Non
8787
return FileSearch(**kwargs)
8888

8989
@classmethod
90-
def search(cls, using=None, index=None, **kwargs):
91-
es_search = super(PageDocument, cls).search(using=using, index=index)
90+
def search(cls, *args, **kwargs):
9291
query = kwargs.pop('query')
92+
es_search = super(PageDocument, cls).search(*args, **kwargs)
9393
es_query = cls.get_es_query(query=query)
9494

95-
es_search = es_search.query(es_query)
95+
es_search = es_search.query(es_query).highlight('content')
9696
return es_search
9797

9898
@classmethod

readthedocs/search/filters.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@
44

55

66
class SearchFilterBackend(filters.BaseFilterBackend):
7-
"""
8-
Filter search result with project
9-
"""
107

11-
def filter_queryset(self, request, queryset, view):
8+
"""Filter search result with project"""
9+
10+
def filter_queryset(self, request, es_search, view):
11+
"""Overwrite the method to compatible with Elasticsearch DSL Search object."""
1212
project_slug = request.query_params.get('project')
13+
version_slug = request.query_params.get('version')
1314
project_slug_list = get_project_slug_list_or_404(project_slug=project_slug,
1415
user=request.user)
15-
return queryset.filter('terms', project=project_slug_list)
16+
# Elasticsearch ``terms`` query can take multiple values as list,
17+
# while ``term`` query takes single value.
18+
filtered_es_search = (es_search.filter('terms', project=project_slug_list)
19+
.filter('term', version=version_slug))
20+
return filtered_es_search

readthedocs/search/serializers.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
class PageSearchSerializer(serializers.Serializer):
55
title = serializers.CharField()
6-
headers = serializers.ListField()
7-
content = serializers.CharField()
86
path = serializers.CharField()
7+
highlight = serializers.SerializerMethodField()
8+
9+
def get_highlight(self, obj):
10+
highlight = getattr(obj.meta, 'highlight', None)
11+
if highlight:
12+
return highlight.to_dict()

readthedocs/search/tests/test_api.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import pytest
2+
from django.core.urlresolvers import reverse
3+
4+
from readthedocs.search.tests.utils import get_search_query_from_project_file
5+
6+
7+
@pytest.mark.django_db
8+
@pytest.mark.search
9+
class TestPageSearch(object):
10+
url = reverse('doc_search')
11+
12+
@pytest.mark.parametrize('data_type', ['content', 'headers', 'title'])
13+
@pytest.mark.parametrize('page_num', [0, 1])
14+
def test_search_works(self, api_client, project, data_type, page_num):
15+
query = get_search_query_from_project_file(project_slug=project.slug, page_num=page_num,
16+
data_type=data_type)
17+
18+
version = project.versions.all()[0]
19+
url = reverse('doc_search')
20+
resp = api_client.get(url, {'project': project.slug, 'version': version.slug, 'query': query})
21+
data = resp.data
22+
assert len(data['results']) == 1
23+
24+
# @pytest.mark.parametrize('case', ['upper', 'lower', 'title'])
25+
# def test_file_search_case_insensitive(self, client, project, case):
26+
# """Check File search is case insensitive
27+
#
28+
# It tests with uppercase, lowercase and camelcase
29+
# """
30+
# query_text = get_search_query_from_project_file(project_slug=project.slug)
31+
#
32+
# cased_query = getattr(query_text, case)
33+
# query = cased_query()
34+
#
35+
# result, _ = self._get_search_result(url=self.url, client=client,
36+
# search_params={'q': query, 'type': 'file'})
37+
#
38+
# assert len(result) == 1
39+
# # Check the actual text is in the result, not the cased one
40+
# assert query_text in result.text()
41+
#
42+
# def test_file_search_exact_match(self, client, project):
43+
# """Check quoted query match exact phrase
44+
#
45+
# Making a query with quoted text like ``"foo bar"`` should match
46+
# exactly ``foo bar`` phrase.
47+
# """
48+
#
49+
# # `Github` word is present both in `kuma` and `pipeline` files
50+
# # But the phrase Github can is available only in kuma docs.
51+
# # So search with this phrase to check
52+
# query = r'"GitHub can"'
53+
#
54+
# result, _ = self._get_search_result(url=self.url, client=client,
55+
# search_params={'q': query, 'type': 'file'})
56+
#
57+
# assert len(result) == 1
58+
#
59+
# def test_page_search_not_return_removed_page(self, client, project):
60+
# """Check removed page are not in the search index"""
61+
# query = get_search_query_from_project_file(project_slug=project.slug)
62+
# # Make a query to check it returns result
63+
# result, _ = self._get_search_result(url=self.url, client=client,
64+
# search_params={'q': query, 'type': 'file'})
65+
# assert len(result) == 1
66+
#
67+
# # Delete all the HTML files of the project
68+
# HTMLFile.objects.filter(project=project).delete()
69+
# # Run the query again and this time there should not be any result
70+
# result, _ = self._get_search_result(url=self.url, client=client,
71+
# search_params={'q': query, 'type': 'file'})
72+
# assert len(result) == 0
73+
#
74+
# def test_file_search_show_projects(self, client, all_projects):
75+
# """Test that search result page shows list of projects while searching for files"""
76+
#
77+
# # `Github` word is present both in `kuma` and `pipeline` files
78+
# # so search with this phrase
79+
# result, page = self._get_search_result(url=self.url, client=client,
80+
# search_params={'q': 'GitHub', 'type': 'file'})
81+
#
82+
# # There should be 2 search result
83+
# assert len(result) == 2
84+
#
85+
# # there should be 2 projects in the left side column
86+
# content = page.find('.navigable .project-list')
87+
# assert len(content) == 2
88+
# text = content.text()
89+
#
90+
# # kuma and pipeline should be there
91+
# assert 'kuma' and 'pipeline' in text
92+
#
93+
# def test_file_search_filter_by_project(self, client):
94+
# """Test that search result are filtered according to project"""
95+
#
96+
# # `Github` word is present both in `kuma` and `pipeline` files
97+
# # so search with this phrase but filter through `kuma` project
98+
# search_params = {'q': 'GitHub', 'type': 'file', 'project': 'kuma'}
99+
# result, page = self._get_search_result(url=self.url, client=client,
100+
# search_params=search_params)
101+
#
102+
# # There should be 1 search result as we have filtered
103+
# assert len(result) == 1
104+
# content = page.find('.navigable .project-list')
105+
#
106+
# # kuma should should be there only
107+
# assert 'kuma' in result.text()
108+
# assert 'pipeline' not in result.text()
109+
#
110+
# # But there should be 2 projects in the left side column
111+
# # as the query is present in both projects
112+
# content = page.find('.navigable .project-list')
113+
# if len(content) != 2:
114+
# pytest.xfail("failing because currently all projects are not showing in project list")
115+
# else:
116+
# assert 'kuma' and 'pipeline' in content.text()
117+
#
118+
# @pytest.mark.xfail(reason="Versions are not showing correctly! Fixme while rewrite!")
119+
# def test_file_search_show_versions(self, client, all_projects, es_index, settings):
120+
# # override the settings to index all versions
121+
# settings.INDEX_ONLY_LATEST = False
122+
#
123+
# project = all_projects[0]
124+
# # Create some versions of the project
125+
# versions = [G(Version, project=project) for _ in range(3)]
126+
#
127+
# query = get_search_query_from_project_file(project_slug=project.slug)
128+
#
129+
# result, page = self._get_search_result(url=self.url, client=client,
130+
# search_params={'q': query, 'type': 'file'})
131+
#
132+
# # There should be only one result because by default
133+
# # only latest version result should be there
134+
# assert len(result) == 1
135+
#
136+
# content = page.find('.navigable .version-list')
137+
# # There should be total 4 versions
138+
# # one is latest, and other 3 that we created above
139+
# assert len(content) == 4
140+
#
141+
# project_versions = [v.slug for v in versions] + [LATEST]
142+
# content_versions = []
143+
# for element in content:
144+
# text = element.text_content()
145+
# # strip and split to keep the version slug only
146+
# slug = text.strip().split('\n')[0]
147+
# content_versions.append(slug)
148+
#
149+
# assert sorted(project_versions) == sorted(content_versions)
150+
#
151+
# def test_file_search_subprojects(self, client, all_projects, es_index):
152+
# """File search should return results from subprojects also"""
153+
# project = all_projects[0]
154+
# subproject = all_projects[1]
155+
# # Add another project as subproject of the project
156+
# project.add_subproject(subproject)
157+
#
158+
# # Now search with subproject content but explicitly filter by the parent project
159+
# query = get_search_query_from_project_file(project_slug=subproject.slug)
160+
# search_params = {'q': query, 'type': 'file', 'project': project.slug}
161+
# result, page = self._get_search_result(url=self.url, client=client,
162+
# search_params=search_params)
163+
#
164+
# assert len(result) == 1

readthedocs/search/utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,8 @@ def parse_sections(documentation_type, content):
313313
# TODO: Rewrite all the views using this in Class Based View,
314314
# and move this function to a mixin
315315
def get_project_slug_list_or_404(project_slug, user):
316-
"""Return list of subproject's slug including own slug.
316+
"""
317+
Return list of subproject's slug including own slug.
317318
If the project is not available to user, redirect to 404
318319
"""
319320
queryset = Project.objects.api(user).only('slug')

readthedocs/urls.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
do_not_track,
2323
)
2424
from readthedocs.search import views as search_views
25-
from readthedocs.search.api import PageSearchAPIView
2625

2726
v1_api = Api(api_name='v1')
2827
v1_api.register(UserResource())
@@ -67,7 +66,6 @@
6766
url(r'^api/', include(v1_api.urls)),
6867
url(r'^api/v2/', include('readthedocs.restapi.urls')),
6968
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
70-
url(r'^api/search/', PageSearchAPIView.as_view()),
7169
]
7270

7371
i18n_urls = [

0 commit comments

Comments
 (0)