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:
Albert (AJ) St. Aubin
2018-03-26 07:31:36 -04:00
committed by GitHub
4 changed files with 113 additions and 1 deletions

View File

@@ -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)

View File

@@ -66,6 +66,7 @@ class TestCourseListGetForm(FormTestMixin, UsernameTestMixin, SharedModuleStoreT
'username': user.username,
'org': '',
'mobile': None,
'search_term': '',
'filter_': None,
}

View File

@@ -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

View File

@@ -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
]