Merge pull request #16757 from appsembler/omar/search-for-courses-api
Add search for courses API to allow building a mobile native search view
This commit is contained in:
@@ -48,6 +48,7 @@ class CourseListGetForm(UsernameValidatorMixin, Form):
|
||||
"""
|
||||
A form to validate query parameters in the course list retrieval endpoint
|
||||
"""
|
||||
search_term = CharField(required=False)
|
||||
username = CharField(required=False)
|
||||
org = CharField(required=False)
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ class TestCourseListGetForm(FormTestMixin, UsernameTestMixin, SharedModuleStoreT
|
||||
'username': user.username,
|
||||
'org': '',
|
||||
'mobile': None,
|
||||
'search_term': '',
|
||||
'filter_': None,
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@ from hashlib import md5
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from nose.plugins.attrib import attr
|
||||
from search.tests.test_course_discovery import DemoCourse
|
||||
from search.tests.tests import TEST_INDEX_NAME
|
||||
from search.tests.utils import SearcherMixin
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase
|
||||
|
||||
@@ -223,3 +227,85 @@ class CourseDetailViewTestCase(CourseApiTestViewMixin, SharedModuleStoreTestCase
|
||||
request.user = self.staff_user
|
||||
response = CourseDetailView().dispatch(request, course_key_string='a:b:c')
|
||||
self.assertEquals(response.status_code, 400)
|
||||
|
||||
|
||||
@override_settings(ELASTIC_FIELD_MAPPINGS={
|
||||
'start_date': {'type': 'date'},
|
||||
'enrollment_start': {'type': 'date'},
|
||||
'enrollment_end': {'type': 'date'}
|
||||
})
|
||||
@override_settings(SEARCH_ENGINE="search.tests.mock_search_engine.MockSearchEngine")
|
||||
@override_settings(COURSEWARE_INDEX_NAME=TEST_INDEX_NAME)
|
||||
class CourseListSearchViewTest(CourseApiTestViewMixin, ModuleStoreTestCase, SearcherMixin):
|
||||
"""
|
||||
Tests the search functionality of the courses API.
|
||||
|
||||
Similar to search.tests.test_course_discovery_views but with the course API integration.
|
||||
"""
|
||||
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
super(CourseListSearchViewTest, self).setUp()
|
||||
DemoCourse.reset_count()
|
||||
self.searcher.destroy()
|
||||
|
||||
self.courses = [
|
||||
self.create_and_index_course('OrgA', 'Find this one with the right parameter'),
|
||||
self.create_and_index_course('OrgB', 'Find this one with another parameter'),
|
||||
self.create_and_index_course('OrgC', 'This course has a unique search term'),
|
||||
]
|
||||
|
||||
self.url = reverse('course-list')
|
||||
self.staff_user = self.create_user(username='staff', is_staff=True)
|
||||
self.honor_user = self.create_user(username='honor', is_staff=False)
|
||||
|
||||
def create_and_index_course(self, org_code, short_description):
|
||||
"""
|
||||
Add a course to both database and search.
|
||||
|
||||
Warning: A ton of gluing here! If this fails, double check both CourseListViewTestCase and MockSearchUrlTest.
|
||||
"""
|
||||
|
||||
search_course = DemoCourse.get({
|
||||
'org': org_code,
|
||||
'run': '2010',
|
||||
'number': 'DemoZ',
|
||||
# Using the slash separated course ID bcuz `DemoCourse` isn't updated yet to new locator.
|
||||
'id': '{org_code}/DemoZ/2010'.format(org_code=org_code),
|
||||
'content': {
|
||||
'short_description': short_description,
|
||||
},
|
||||
})
|
||||
|
||||
DemoCourse.index(self.searcher, [search_course])
|
||||
|
||||
org, course, run = search_course['id'].split('/')
|
||||
|
||||
db_course = self.create_course(
|
||||
mobile_available=False,
|
||||
org=org,
|
||||
course=course,
|
||||
run=run,
|
||||
short_description=short_description,
|
||||
)
|
||||
|
||||
return db_course
|
||||
|
||||
def test_list_all(self):
|
||||
"""
|
||||
Test without search, should list all the courses.
|
||||
"""
|
||||
res = self.verify_response()
|
||||
self.assertIn('results', res.data)
|
||||
self.assertNotEqual(res.data['results'], [])
|
||||
self.assertEqual(res.data['pagination']['count'], 3) # Should list all of the 3 courses
|
||||
|
||||
def test_list_all_with_search_term(self):
|
||||
"""
|
||||
Test with search, should only the course that matches the search term.
|
||||
"""
|
||||
res = self.verify_response(params={'search_term': 'unique search term'})
|
||||
self.assertIn('results', res.data)
|
||||
self.assertNotEqual(res.data['results'], [])
|
||||
self.assertEqual(res.data['pagination']['count'], 1) # Should list a single course
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
Course API Views
|
||||
"""
|
||||
|
||||
import search
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from rest_framework.generics import ListAPIView, RetrieveAPIView
|
||||
|
||||
@@ -137,6 +139,8 @@ class CourseListView(DeveloperErrorViewMixin, ListAPIView):
|
||||
Body comprises a list of objects as returned by `CourseDetailView`.
|
||||
|
||||
**Parameters**
|
||||
search_term (optional):
|
||||
Search term to filter courses (used by ElasticSearch).
|
||||
|
||||
username (optional):
|
||||
The username of the specified user whose visible courses we
|
||||
@@ -193,6 +197,11 @@ class CourseListView(DeveloperErrorViewMixin, ListAPIView):
|
||||
pagination_class = NamespacedPageNumberPagination
|
||||
serializer_class = CourseSerializer
|
||||
|
||||
# Return all the results, 10K is the maximum allowed value for ElasticSearch.
|
||||
# We should use 0 after upgrading to 1.1+:
|
||||
# - https://github.com/elastic/elasticsearch/commit/8b0a863d427b4ebcbcfb1dcd69c996c52e7ae05e
|
||||
results_size_infinity = 10000
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Return a list of courses visible to the user.
|
||||
@@ -201,9 +210,24 @@ class CourseListView(DeveloperErrorViewMixin, ListAPIView):
|
||||
if not form.is_valid():
|
||||
raise ValidationError(form.errors)
|
||||
|
||||
return list_courses(
|
||||
db_courses = list_courses(
|
||||
self.request,
|
||||
form.cleaned_data['username'],
|
||||
org=form.cleaned_data['org'],
|
||||
filter_=form.cleaned_data['filter_'],
|
||||
)
|
||||
|
||||
if not settings.FEATURES['ENABLE_COURSEWARE_SEARCH'] or not form.cleaned_data['search_term']:
|
||||
return db_courses
|
||||
|
||||
search_courses = search.api.course_discovery_search(
|
||||
form.cleaned_data['search_term'],
|
||||
size=self.results_size_infinity,
|
||||
)
|
||||
|
||||
search_courses_ids = {course['data']['id']: True for course in search_courses['results']}
|
||||
|
||||
return [
|
||||
course for course in db_courses
|
||||
if unicode(course.id) in search_courses_ids
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user