diff --git a/common/test/acceptance/pages/lms/dashboard_search.py b/common/test/acceptance/pages/lms/dashboard_search.py
new file mode 100644
index 0000000000..ff8cfaaa66
--- /dev/null
+++ b/common/test/acceptance/pages/lms/dashboard_search.py
@@ -0,0 +1,40 @@
+"""
+Dashboard search
+"""
+
+from bok_choy.page_object import PageObject
+from . import BASE_URL
+
+
+class DashboardSearchPage(PageObject):
+ """
+ Dashboard page featuring a search form
+ """
+
+ search_bar_selector = '#dashboard-search-bar'
+ url = "{base}/dashboard".format(base=BASE_URL)
+
+ @property
+ def search_results(self):
+ """ search results list showing """
+ return self.q(css='#dashboard-search-results')
+
+ def is_browser_on_page(self):
+ """ did we find the search bar in the UI """
+ return self.q(css=self.search_bar_selector).present
+
+ def enter_search_term(self, text):
+ """ enter the search term into the box """
+ self.q(css=self.search_bar_selector + ' input[type="text"]').fill(text)
+
+ def search(self):
+ """ execute the search """
+ self.q(css=self.search_bar_selector + ' [type="submit"]').click()
+ self.wait_for_element_visibility('.search-info', 'Search results are shown')
+
+ def search_for_term(self, text):
+ """
+ Search and return results
+ """
+ self.enter_search_term(text)
+ self.search()
diff --git a/common/test/acceptance/pages/lms/tab_nav.py b/common/test/acceptance/pages/lms/tab_nav.py
index 861ee5dd8b..410caff2b3 100644
--- a/common/test/acceptance/pages/lms/tab_nav.py
+++ b/common/test/acceptance/pages/lms/tab_nav.py
@@ -33,6 +33,7 @@ class TabNavPage(PageObject):
else:
self.warning("No tabs found for '{0}'".format(tab_name))
+ self.wait_for_page()
self._is_on_tab_promise(tab_name).fulfill()
def is_on_tab(self, tab_name):
diff --git a/common/test/acceptance/tests/lms/test_lms_dashboard_search.py b/common/test/acceptance/tests/lms/test_lms_dashboard_search.py
new file mode 100644
index 0000000000..23daf5d9e5
--- /dev/null
+++ b/common/test/acceptance/tests/lms/test_lms_dashboard_search.py
@@ -0,0 +1,193 @@
+"""
+Test dashboard search
+"""
+import os
+import json
+
+from bok_choy.web_app_test import WebAppTest
+from ..helpers import generate_course_key
+from ...pages.common.logout import LogoutPage
+from ...pages.studio.utils import add_html_component, click_css, type_in_codemirror
+from ...pages.studio.auto_auth import AutoAuthPage
+from ...pages.studio.overview import CourseOutlinePage
+from ...pages.studio.container import ContainerPage
+from ...pages.lms.dashboard_search import DashboardSearchPage
+from ...fixtures.course import CourseFixture, XBlockFixtureDesc
+
+
+class DashboardSearchTest(WebAppTest):
+ """
+ Test dashboard search.
+ """
+ USERNAME = 'STUDENT_TESTER'
+ EMAIL = 'student101@example.com'
+
+ STAFF_USERNAME = "STAFF_TESTER"
+ STAFF_EMAIL = "staff101@example.com"
+
+ TEST_INDEX_FILENAME = "test_root/index_file.dat"
+
+ def setUp(self):
+ """
+ Create the search page and courses to search.
+ """
+ # create test file in which index for this test will live
+ with open(self.TEST_INDEX_FILENAME, "w+") as index_file:
+ json.dump({}, index_file)
+
+ super(DashboardSearchTest, self).setUp()
+ self.dashboard = DashboardSearchPage(self.browser)
+
+ self.courses = {
+ 'A': {
+ 'org': 'test_org',
+ 'number': self.unique_id,
+ 'run': 'test_run_A',
+ 'display_name': 'Test Course A '
+ },
+ 'B': {
+ 'org': 'test_org',
+ 'number': self.unique_id,
+ 'run': 'test_run_B',
+ 'display_name': 'Test Course B '
+ },
+ 'C': {
+ 'org': 'test_org',
+ 'number': self.unique_id,
+ 'run': 'test_run_C',
+ 'display_name': 'Test Course C '
+ }
+ }
+
+ # generate course fixtures and outline pages
+ self.course_outlines = {}
+ self.course_fixtures = {}
+ for key, course_info in self.courses.iteritems():
+ course_outline = CourseOutlinePage(
+ self.browser,
+ course_info['org'],
+ course_info['number'],
+ course_info['run']
+ )
+
+ course_fix = CourseFixture(
+ course_info['org'],
+ course_info['number'],
+ course_info['run'],
+ course_info['display_name']
+ )
+
+ course_fix.add_children(
+ XBlockFixtureDesc('chapter', 'Section 1').add_children(
+ XBlockFixtureDesc('sequential', 'Subsection 1').add_children(
+ XBlockFixtureDesc('problem', 'dashboard search')
+ )
+ )
+ ).add_children(
+ XBlockFixtureDesc('chapter', 'Section 2').add_children(
+ XBlockFixtureDesc('sequential', 'Subsection 2')
+ )
+ ).install()
+
+ self.course_outlines[key] = course_outline
+ self.course_fixtures[key] = course_fix
+
+ def tearDown(self):
+ """
+ Remove index file
+ """
+ super(DashboardSearchTest, self).tearDown()
+ os.remove(self.TEST_INDEX_FILENAME)
+
+ def _auto_auth(self, username, email, staff):
+ """
+ Logout and login with given credentials.
+ """
+ LogoutPage(self.browser).visit()
+ AutoAuthPage(self.browser, username=username, email=email, staff=staff).visit()
+
+ def _studio_add_content(self, course_outline, html_content):
+ """
+ Add content to first section on studio course page.
+ """
+ # create a unit in course outline
+ course_outline.visit()
+ subsection = course_outline.section_at(0).subsection_at(0)
+ subsection.expand_subsection()
+ subsection.add_unit()
+
+ # got to unit and create an HTML component and save (not publish)
+ unit_page = ContainerPage(self.browser, None)
+ unit_page.wait_for_page()
+ add_html_component(unit_page, 0)
+ unit_page.wait_for_element_presence('.edit-button', 'Edit button is visible')
+ click_css(unit_page, '.edit-button', 0, require_notification=False)
+ unit_page.wait_for_element_visibility('.modal-editor', 'Modal editor is visible')
+ type_in_codemirror(unit_page, 0, html_content)
+ click_css(unit_page, '.action-save', 0)
+
+ def _studio_publish_content(self, course_outline):
+ """
+ Publish content in first section on studio course page.
+ """
+ course_outline.visit()
+ subsection = course_outline.section_at(0).subsection_at(0)
+ subsection.expand_subsection()
+ unit = subsection.unit_at(0)
+ unit.publish()
+
+ def test_page_existence(self):
+ """
+ Make sure that the page exists.
+ """
+ self._auto_auth(self.USERNAME, self.EMAIL, False)
+ self.dashboard.visit()
+
+ def test_search(self):
+ """
+ Make sure that you can search courses.
+ """
+
+ search_string = "dashboard"
+ html_content = "dashboard search"
+
+ # Enroll student in courses A & B, but not C
+ for course_info in [self.courses['A'], self.courses['B']]:
+ course_key = generate_course_key(
+ course_info['org'],
+ course_info['number'],
+ course_info['run']
+ )
+ AutoAuthPage(
+ self.browser,
+ username=self.USERNAME,
+ email=self.EMAIL,
+ course_id=course_key
+ ).visit()
+
+ # Create content in studio without publishing.
+ self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
+ self._studio_add_content(self.course_outlines['A'], html_content)
+ self._studio_add_content(self.course_outlines['B'], html_content)
+ self._studio_add_content(self.course_outlines['C'], html_content)
+
+ # Do a search, there should be no results shown.
+ self._auto_auth(self.USERNAME, self.EMAIL, False)
+ self.dashboard.visit()
+ self.dashboard.search_for_term(search_string)
+ assert search_string not in self.dashboard.search_results.html[0]
+
+ # Publish in studio to trigger indexing.
+ self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
+ self._studio_publish_content(self.course_outlines['A'])
+ self._studio_publish_content(self.course_outlines['B'])
+ self._studio_publish_content(self.course_outlines['C'])
+
+ # Do the search again, this time we expect results from courses A & B, but not C
+ self._auto_auth(self.USERNAME, self.EMAIL, False)
+ self.dashboard.visit()
+
+ self.dashboard.search_for_term(search_string)
+ assert self.dashboard.search_results.html[0].count(search_string) == 2
+ assert self.dashboard.search_results.html[0].count(self.courses['A']['display_name']) == 1
+ assert self.dashboard.search_results.html[0].count(self.courses['B']['display_name']) == 1
diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py
index 7826ea2138..0f7918f1a8 100644
--- a/lms/envs/acceptance.py
+++ b/lms/envs/acceptance.py
@@ -179,7 +179,7 @@ YOUTUBE['API'] = "127.0.0.1:{0}/get_youtube_api/".format(YOUTUBE_PORT)
YOUTUBE['TEST_URL'] = "127.0.0.1:{0}/test_youtube/".format(YOUTUBE_PORT)
YOUTUBE['TEXT_API']['url'] = "127.0.0.1:{0}/test_transcripts_youtube/".format(YOUTUBE_PORT)
-if FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
+if FEATURES.get('ENABLE_COURSEWARE_SEARCH') or FEATURES.get('ENABLE_DASHBOARD_SEARCH'):
# Use MockSearchEngine as the search engine for test scenario
SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine"
diff --git a/lms/envs/aws.py b/lms/envs/aws.py
index 9d21ce3760..bff557b189 100644
--- a/lms/envs/aws.py
+++ b/lms/envs/aws.py
@@ -565,7 +565,7 @@ PDF_RECEIPT_COBRAND_LOGO_HEIGHT_MM = ENV_TOKENS.get(
'PDF_RECEIPT_COBRAND_LOGO_HEIGHT_MM', PDF_RECEIPT_COBRAND_LOGO_HEIGHT_MM
)
-if FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
+if FEATURES.get('ENABLE_COURSEWARE_SEARCH') or FEATURES.get('ENABLE_DASHBOARD_SEARCH'):
# Use ElasticSearch as the search engine herein
SEARCH_ENGINE = "search.elastic.ElasticSearchEngine"
diff --git a/lms/envs/bok_choy.py b/lms/envs/bok_choy.py
index 0070b1513f..3d389ca505 100644
--- a/lms/envs/bok_choy.py
+++ b/lms/envs/bok_choy.py
@@ -120,6 +120,10 @@ PASSWORD_COMPLEXITY = {}
# Enable courseware search for tests
FEATURES['ENABLE_COURSEWARE_SEARCH'] = True
+
+# Enable dashboard search for tests
+FEATURES['ENABLE_DASHBOARD_SEARCH'] = True
+
# Use MockSearchEngine as the search engine for test scenario
SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine"
# Path at which to store the mock index
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 9cf4766782..d349d691e9 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -357,6 +357,9 @@ FEATURES = {
# Courseware search feature
'ENABLE_COURSEWARE_SEARCH': False,
+ # Dashboard search feature
+ 'ENABLE_DASHBOARD_SEARCH': False,
+
# log all information from cybersource callbacks
'LOG_POSTPAY_CALLBACKS': True,
@@ -1103,7 +1106,7 @@ courseware_js = (
for pth in ['courseware', 'histogram', 'navigation', 'time']
] +
['js/' + pth + '.js' for pth in ['ajax-error']] +
- ['js/search/main.js'] +
+ ['js/search/course/main.js'] +
sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/modules/**/*.js'))
)
@@ -1135,7 +1138,10 @@ main_vendor_js = base_vendor_js + [
'js/vendor/URI.min.js',
]
-dashboard_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/dashboard/**/*.js'))
+dashboard_js = (
+ sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/dashboard/**/*.js')) +
+ ['js/search/dashboard/main.js']
+)
discussion_js = sorted(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/discussion/**/*.js'))
rwd_header_footer_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/common_helpers/rwd_header_footer.js'))
staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.js'))
@@ -2215,6 +2221,8 @@ PDF_RECEIPT_COBRAND_LOGO_HEIGHT_MM = 12
SEARCH_ENGINE = None
# Use the LMS specific result processor
SEARCH_RESULT_PROCESSOR = "lms.lib.courseware_search.lms_result_processor.LmsSearchResultProcessor"
+# Use the LMS specific filter generator
+SEARCH_FILTER_GENERATOR = "lms.lib.courseware_search.lms_filter_generator.LmsSearchFilterGenerator"
### PERFORMANCE EXPERIMENT SETTINGS ###
# CDN experiment/monitoring flags
diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py
index 10cff1c70d..8d229d05dd 100644
--- a/lms/envs/devstack.py
+++ b/lms/envs/devstack.py
@@ -121,6 +121,10 @@ FEATURES['ENABLE_COURSEWARE_SEARCH'] = True
SEARCH_ENGINE = "search.elastic.ElasticSearchEngine"
+########################## Dashboard Search #######################
+FEATURES['ENABLE_DASHBOARD_SEARCH'] = True
+
+
########################## Certificates Web/HTML View #######################
FEATURES['CERTIFICATES_HTML_VIEW'] = True
diff --git a/lms/envs/test.py b/lms/envs/test.py
index ab098bad30..ff0946b288 100644
--- a/lms/envs/test.py
+++ b/lms/envs/test.py
@@ -461,6 +461,10 @@ FEATURES['ENTRANCE_EXAMS'] = True
# Enable courseware search for tests
FEATURES['ENABLE_COURSEWARE_SEARCH'] = True
+
+# Enable dashboard search for tests
+FEATURES['ENABLE_DASHBOARD_SEARCH'] = True
+
# Use MockSearchEngine as the search engine for test scenario
SEARCH_ENGINE = "search.tests.mock_search_engine.MockSearchEngine"
diff --git a/lms/lib/courseware_search/lms_filter_generator.py b/lms/lib/courseware_search/lms_filter_generator.py
new file mode 100644
index 0000000000..adfbaf6ca2
--- /dev/null
+++ b/lms/lib/courseware_search/lms_filter_generator.py
@@ -0,0 +1,22 @@
+"""
+This file contains implementation override of SearchFilterGenerator which will allow
+ * Filter by all courses in which the user is enrolled in
+"""
+from student.models import CourseEnrollment
+
+from search.filter_generator import SearchFilterGenerator
+
+
+class LmsSearchFilterGenerator(SearchFilterGenerator):
+ """ SearchFilterGenerator for LMS Search """
+
+ def field_dictionary(self, **kwargs):
+ """ add course if provided otherwise add courses in which the user is enrolled in """
+ field_dictionary = super(LmsSearchFilterGenerator, self).field_dictionary(**kwargs)
+ if not kwargs.get('user'):
+ field_dictionary['course'] = []
+ elif not kwargs.get('course_id'):
+ user_enrollments = CourseEnrollment.enrollments_for_user(kwargs['user'])
+ field_dictionary['course'] = [unicode(enrollment.course_id) for enrollment in user_enrollments]
+
+ return field_dictionary
diff --git a/lms/lib/courseware_search/lms_result_processor.py b/lms/lib/courseware_search/lms_result_processor.py
index 0b8d7c2e88..c19be41b24 100644
--- a/lms/lib/courseware_search/lms_result_processor.py
+++ b/lms/lib/courseware_search/lms_result_processor.py
@@ -20,6 +20,7 @@ class LmsSearchResultProcessor(SearchResultProcessor):
""" SearchResultProcessor for LMS Search """
_course_key = None
+ _course_name = None
_usage_key = None
_module_store = None
_module_temp_dictionary = {}
@@ -61,6 +62,16 @@ class LmsSearchResultProcessor(SearchResultProcessor):
kwargs={"course_id": self._results_fields["course"], "location": self._results_fields["id"]}
)
+ @property
+ def course_name(self):
+ """
+ Display the course name when searching multiple courses - retain result for subsequent uses
+ """
+ if self._course_name is None:
+ course = self.get_module_store().get_course(self.get_course_key())
+ self._course_name = course.display_name_with_default
+ return self._course_name
+
@property
def location(self):
"""
diff --git a/lms/lib/courseware_search/test/__init__.py b/lms/lib/courseware_search/test/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/lms/lib/courseware_search/test/test_lms_filter_generator.py b/lms/lib/courseware_search/test/test_lms_filter_generator.py
new file mode 100644
index 0000000000..d3f4bcfed5
--- /dev/null
+++ b/lms/lib/courseware_search/test/test_lms_filter_generator.py
@@ -0,0 +1,72 @@
+"""
+Tests for the lms_filter_generator
+"""
+from xmodule.modulestore.tests.factories import CourseFactory
+from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
+from student.tests.factories import UserFactory
+from student.models import CourseEnrollment
+
+from lms.lib.courseware_search.lms_filter_generator import LmsSearchFilterGenerator
+
+
+class LmsSearchFilterGeneratorTestCase(ModuleStoreTestCase):
+ """ Test case class to test search result processor """
+
+ def build_courses(self):
+ """
+ Build up a course tree with multiple test courses
+ """
+
+ self.courses = [
+ CourseFactory.create(
+ org='ElasticsearchFiltering',
+ course='ES101F',
+ run='test_run',
+ display_name='Elasticsearch Filtering test course',
+ ),
+
+ CourseFactory.create(
+ org='FilterTest',
+ course='FT101',
+ run='test_run',
+ display_name='FilterTest test course',
+ )
+ ]
+
+ def setUp(self):
+ super(LmsSearchFilterGeneratorTestCase, self).setUp()
+ self.build_courses()
+ self.user = UserFactory.create(username="jack", email="jack@fake.edx.org", password='test')
+ for course in self.courses:
+ CourseEnrollment.enroll(self.user, course.location.course_key)
+
+ def test_course_id_not_provided(self):
+ """
+ Tests that we get the list of IDs of courses the user is enrolled in when the course ID is null or not provided
+ """
+ field_dictionary, filter_dictionary = LmsSearchFilterGenerator.generate_field_filters(user=self.user)
+
+ self.assertTrue('start_date' in filter_dictionary)
+ self.assertIn(unicode(self.courses[0].id), field_dictionary['course'])
+ self.assertIn(unicode(self.courses[1].id), field_dictionary['course'])
+
+ def test_course_id_provided(self):
+ """
+ Tests that we get the course ID when the course ID is provided
+ """
+ field_dictionary, filter_dictionary = LmsSearchFilterGenerator.generate_field_filters(
+ user=self.user,
+ course_id=unicode(self.courses[0].id)
+ )
+
+ self.assertTrue('start_date' in filter_dictionary)
+ self.assertEqual(unicode(self.courses[0].id), field_dictionary['course'])
+
+ def test_user_not_provided(self):
+ """
+ Tests that we get empty list of courses in case the user is not provided
+ """
+ field_dictionary, filter_dictionary = LmsSearchFilterGenerator.generate_field_filters()
+
+ self.assertTrue('start_date' in filter_dictionary)
+ self.assertEqual(0, len(field_dictionary['course']))
diff --git a/lms/lib/courseware_search/test/test_lms_result_processor.py b/lms/lib/courseware_search/test/test_lms_result_processor.py
index fee8d582b9..c07352cbeb 100644
--- a/lms/lib/courseware_search/test/test_lms_result_processor.py
+++ b/lms/lib/courseware_search/test/test_lms_result_processor.py
@@ -85,6 +85,17 @@ class LmsSearchResultProcessorTestCase(ModuleStoreTestCase):
self.assertEqual(
srp.url, "/courses/{}/jump_to/{}".format(unicode(self.course.id), unicode(self.html.scope_ids.usage_id)))
+ def test_course_name_parameter(self):
+ srp = LmsSearchResultProcessor(
+ {
+ "course": unicode(self.course.id),
+ "id": unicode(self.html.scope_ids.usage_id),
+ "content": {"text": "This is the html text"}
+ },
+ "test"
+ )
+ self.assertEqual(srp.course_name, self.course.display_name)
+
def test_location_parameter(self):
srp = LmsSearchResultProcessor(
{
diff --git a/lms/static/js/fixtures/search/course_search_form.html b/lms/static/js/fixtures/search/course_search_form.html
new file mode 100644
index 0000000000..f9f53acf00
--- /dev/null
+++ b/lms/static/js/fixtures/search/course_search_form.html
@@ -0,0 +1,12 @@
+
+
+
diff --git a/lms/static/js/fixtures/search/dashboard_search_form.html b/lms/static/js/fixtures/search/dashboard_search_form.html
new file mode 100644
index 0000000000..18973767cf
--- /dev/null
+++ b/lms/static/js/fixtures/search/dashboard_search_form.html
@@ -0,0 +1,14 @@
+
diff --git a/lms/static/js/fixtures/search_form.html b/lms/static/js/fixtures/search_form.html
deleted file mode 100644
index 77c9a1dad8..0000000000
--- a/lms/static/js/fixtures/search_form.html
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
diff --git a/lms/static/js/search/collections/search_collection.js b/lms/static/js/search/base/collections/search_collection.js
similarity index 78%
rename from lms/static/js/search/collections/search_collection.js
rename to lms/static/js/search/base/collections/search_collection.js
index 282be95087..2d32cabd27 100644
--- a/lms/static/js/search/collections/search_collection.js
+++ b/lms/static/js/search/base/collections/search_collection.js
@@ -2,7 +2,7 @@
define([
'backbone',
- 'js/search/models/search_result'
+ 'js/search/base/models/search_result'
], function (Backbone, SearchResult) {
'use strict';
@@ -11,6 +11,7 @@ define([
model: SearchResult,
pageSize: 20,
totalCount: 0,
+ latestModelsCount: 0,
accessDeniedCount: 0,
searchTerm: '',
page: 0,
@@ -20,17 +21,15 @@ define([
initialize: function (models, options) {
// call super constructor
Backbone.Collection.prototype.initialize.apply(this, arguments);
- if (options && options.course_id) {
- this.url += options.course_id;
+ if (options && options.courseId) {
+ this.url += options.courseId;
}
},
performSearch: function (searchTerm) {
this.fetchXhr && this.fetchXhr.abort();
this.searchTerm = searchTerm || '';
- this.totalCount = 0;
- this.accessDeniedCount = 0;
- this.page = 0;
+ this.resetState();
this.fetchXhr = this.fetch({
data: {
search_string: searchTerm,
@@ -71,20 +70,34 @@ define([
cancelSearch: function () {
this.fetchXhr && this.fetchXhr.abort();
- this.page = 0;
- this.totalCount = 0;
- this.accessDeniedCount = 0;
+ this.resetState();
},
parse: function(response) {
+ this.latestModelsCount = response.results.length;
this.totalCount = response.total;
this.accessDeniedCount += response.access_denied_count;
this.totalCount -= this.accessDeniedCount;
- return _.map(response.results, function(result){ return result.data; });
+ return _.map(response.results, function (result) {
+ return result.data;
+ });
+ },
+
+ resetState: function () {
+ this.page = 0;
+ this.totalCount = 0;
+ this.latestModelsCount = 0;
+ this.accessDeniedCount = 0;
+ // empty the entire collection
+ this.reset();
},
hasNextPage: function () {
return this.totalCount - ((this.page + 1) * this.pageSize) > 0;
+ },
+
+ latestModels: function () {
+ return this.last(this.latestModelsCount);
}
});
diff --git a/lms/static/js/search/models/search_result.js b/lms/static/js/search/base/models/search_result.js
similarity index 100%
rename from lms/static/js/search/models/search_result.js
rename to lms/static/js/search/base/models/search_result.js
diff --git a/lms/static/js/search/search_router.js b/lms/static/js/search/base/routers/search_router.js
similarity index 52%
rename from lms/static/js/search/search_router.js
rename to lms/static/js/search/base/routers/search_router.js
index 65683b3f1e..fc4a7adf21 100644
--- a/lms/static/js/search/search_router.js
+++ b/lms/static/js/search/base/routers/search_router.js
@@ -4,9 +4,12 @@ define(['backbone'], function (Backbone) {
'use strict';
return Backbone.Router.extend({
- routes: {
- 'search/:query': 'search'
- }
+ routes: {
+ 'search/:query': 'search'
+ },
+ search: function(query) {
+ this.trigger('search', query);
+ }
});
});
diff --git a/lms/static/js/search/views/search_form.js b/lms/static/js/search/base/views/search_form.js
similarity index 94%
rename from lms/static/js/search/views/search_form.js
rename to lms/static/js/search/base/views/search_form.js
index 3eb407ac6a..5b666cf6af 100644
--- a/lms/static/js/search/views/search_form.js
+++ b/lms/static/js/search/base/views/search_form.js
@@ -5,7 +5,7 @@ define(['jquery', 'backbone'], function ($, Backbone) {
return Backbone.View.extend({
- el: '#courseware-search-bar',
+ el: '',
events: {
'submit form': 'submitForm',
'click .cancel-button': 'clearSearch',
@@ -40,9 +40,13 @@ define(['jquery', 'backbone'], function ($, Backbone) {
}
},
- clearSearch: function () {
+ resetSearchForm: function () {
this.$searchField.val('');
this.setInitialStyle();
+ },
+
+ clearSearch: function () {
+ this.resetSearchForm();
this.trigger('clear');
},
diff --git a/lms/static/js/search/views/search_item_view.js b/lms/static/js/search/base/views/search_item_view.js
similarity index 57%
rename from lms/static/js/search/views/search_item_view.js
rename to lms/static/js/search/base/views/search_item_view.js
index 30a5565385..57ddaeb226 100644
--- a/lms/static/js/search/views/search_item_view.js
+++ b/lms/static/js/search/base/views/search_item_view.js
@@ -7,11 +7,12 @@ define([
'gettext',
'logger'
], function ($, _, Backbone, gettext, Logger) {
- 'use strict';
+ 'use strict';
return Backbone.View.extend({
tagName: 'li',
+ templateId: '',
className: 'search-results-item',
attributes: {
'role': 'region',
@@ -19,16 +20,22 @@ define([
},
events: {
- 'click .search-results-item a': 'logSearchItem',
+ 'click': 'logSearchItem',
},
initialize: function () {
- var template_name = (this.model.attributes.content_type === "Sequence") ? '#search_item_seq-tpl' : '#search_item-tpl';
- this.tpl = _.template($(template_name).html());
+ this.tpl = _.template($(this.templateId).html());
},
render: function () {
- this.$el.html(this.tpl(this.model.attributes));
+ var data = _.clone(this.model.attributes);
+ // Drop the preview text and result type if the search term is found
+ // in the title/location in the course hierarchy
+ if (this.model.get('content_type') === 'Sequence') {
+ data.excerpt = '';
+ data.content_type = '';
+ }
+ this.$el.html(this.tpl(data));
return this;
},
@@ -44,24 +51,23 @@ define([
event.preventDefault();
var self = this;
var target = this.model.id;
- var link = $(event.target).attr('href');
+ var link = this.model.get('url');
var collection = this.model.collection;
var page = collection.page;
var pageSize = collection.pageSize;
var searchTerm = collection.searchTerm;
var index = collection.indexOf(this.model);
- Logger.log("edx.course.search.result_selected",
- {
- "search_term": searchTerm,
- "result_position": (page * pageSize + index),
- "result_link": target
- }).always(function() {
- self.redirect(link);
- });
+ Logger.log('edx.course.search.result_selected', {
+ 'search_term': searchTerm,
+ 'result_position': (page * pageSize + index),
+ 'result_link': target
+ }).always(function() {
+ self.redirect(link);
+ });
}
+
});
});
})(define || RequireJS.define);
-
diff --git a/lms/static/js/search/views/search_list_view.js b/lms/static/js/search/base/views/search_results_view.js
similarity index 59%
rename from lms/static/js/search/views/search_list_view.js
rename to lms/static/js/search/base/views/search_results_view.js
index 7a8d2c45e5..6235ef799a 100644
--- a/lms/static/js/search/views/search_list_view.js
+++ b/lms/static/js/search/base/views/search_results_view.js
@@ -5,39 +5,39 @@ define([
'underscore',
'backbone',
'gettext',
- 'js/search/views/search_item_view'
-], function ($, _, Backbone, gettext, SearchItemView) {
+], function ($, _, Backbone, gettext) {
'use strict';
return Backbone.View.extend({
- el: '#courseware-search-results',
- events: {
- 'click .search-load-next': 'loadNext'
- },
- spinner: '.icon',
+ // these should be defined by subclasses
+ el: '',
+ contentElement: '',
+ resultsTemplateId: '',
+ loadingTemplateId: '',
+ errorTemplateId: '',
+ events: {},
+ spinner: '.search-load-next .icon',
+ SearchItemView: function () {},
initialize: function () {
- this.courseName = this.$el.attr('data-course-name');
- this.$courseContent = $('#course-content');
- this.listTemplate = _.template($('#search_list-tpl').html());
- this.loadingTemplate = _.template($('#search_loading-tpl').html());
- this.errorTemplate = _.template($('#search_error-tpl').html());
- this.collection.on('search', this.render, this);
- this.collection.on('next', this.renderNext, this);
- this.collection.on('error', this.showErrorMessage, this);
+ this.$contentElement = $(this.contentElement);
+ this.resultsTemplate = _.template($(this.resultsTemplateId).html());
+ this.loadingTemplate = _.template($(this.loadingTemplateId).html());
+ this.errorTemplate = _.template($(this.errorTemplateId).html());
},
render: function () {
- this.$el.html(this.listTemplate({
+ this.$el.html(this.resultsTemplate({
totalCount: this.collection.totalCount,
totalCountMsg: this.totalCountMsg(),
pageSize: this.collection.pageSize,
hasMoreResults: this.collection.hasNextPage()
}));
this.renderItems();
- this.$courseContent.hide();
+ this.$el.find(this.spinner).hide();
+ this.$contentElement.hide();
this.$el.show();
return this;
},
@@ -53,11 +53,12 @@ define([
},
renderItems: function () {
- var items = this.collection.map(function (result) {
- var item = new SearchItemView({ model: result });
+ var latest = this.collection.latestModels();
+ var items = latest.map(function (result) {
+ var item = new this.SearchItemView({ model: result });
return item.render().el;
- });
- this.$el.find('.search-results').html(items);
+ }, this);
+ this.$el.find('ol').append(items);
},
totalCountMsg: function () {
@@ -67,25 +68,26 @@ define([
clear: function () {
this.$el.hide().empty();
- this.$courseContent.show();
+ this.$contentElement.show();
},
showLoadingMessage: function () {
this.$el.html(this.loadingTemplate());
this.$el.show();
- this.$courseContent.hide();
+ this.$contentElement.hide();
},
showErrorMessage: function () {
this.$el.html(this.errorTemplate());
this.$el.show();
- this.$courseContent.hide();
+ this.$contentElement.hide();
},
loadNext: function (event) {
event && event.preventDefault();
this.$el.find(this.spinner).show();
this.trigger('next');
+ return false;
}
});
diff --git a/lms/static/js/search/course/main.js b/lms/static/js/search/course/main.js
new file mode 100644
index 0000000000..9d8bd2759c
--- /dev/null
+++ b/lms/static/js/search/course/main.js
@@ -0,0 +1,22 @@
+RequireJS.require([
+ 'jquery',
+ 'backbone',
+ 'js/search/course/search_app',
+ 'js/search/base/routers/search_router',
+ 'js/search/course/views/search_form',
+ 'js/search/base/collections/search_collection',
+ 'js/search/course/views/search_results_view'
+], function ($, Backbone, SearchApp, SearchRouter, CourseSearchForm, SearchCollection, CourseSearchResultsView) {
+ 'use strict';
+
+ var courseId = $('#courseware-search-results').data('courseId');
+ var app = new SearchApp(
+ courseId,
+ SearchRouter,
+ CourseSearchForm,
+ SearchCollection,
+ CourseSearchResultsView
+ );
+ Backbone.history.start();
+
+});
diff --git a/lms/static/js/search/course/search_app.js b/lms/static/js/search/course/search_app.js
new file mode 100644
index 0000000000..992c5fdf56
--- /dev/null
+++ b/lms/static/js/search/course/search_app.js
@@ -0,0 +1,50 @@
+;(function (define) {
+
+define(['backbone'], function(Backbone) {
+ 'use strict';
+
+ return function (courseId, SearchRouter, SearchForm, SearchCollection, SearchListView) {
+
+ var router = new SearchRouter();
+ var form = new SearchForm();
+ var collection = new SearchCollection([], { courseId: courseId });
+ var results = new SearchListView({ collection: collection });
+ var dispatcher = _.clone(Backbone.Events);
+
+ dispatcher.listenTo(router, 'search', function (query) {
+ form.doSearch(query);
+ });
+
+ dispatcher.listenTo(form, 'search', function (query) {
+ results.showLoadingMessage();
+ collection.performSearch(query);
+ router.navigate('search/' + query, { replace: true });
+ });
+
+ dispatcher.listenTo(form, 'clear', function () {
+ collection.cancelSearch();
+ results.clear();
+ router.navigate('');
+ });
+
+ dispatcher.listenTo(results, 'next', function () {
+ collection.loadNextPage();
+ });
+
+ dispatcher.listenTo(collection, 'search', function () {
+ results.render();
+ });
+
+ dispatcher.listenTo(collection, 'next', function () {
+ results.renderNext();
+ });
+
+ dispatcher.listenTo(collection, 'error', function () {
+ results.showErrorMessage();
+ });
+
+ };
+
+});
+
+})(define || RequireJS.define);
diff --git a/lms/static/js/search/course/views/search_form.js b/lms/static/js/search/course/views/search_form.js
new file mode 100644
index 0000000000..8bee186a24
--- /dev/null
+++ b/lms/static/js/search/course/views/search_form.js
@@ -0,0 +1,14 @@
+;(function (define) {
+
+define([
+ 'js/search/base/views/search_form'
+], function (SearchForm) {
+ 'use strict';
+
+ return SearchForm.extend({
+ el: '#courseware-search-bar'
+ });
+
+});
+
+})(define || RequireJS.define);
diff --git a/lms/static/js/search/course/views/search_item_view.js b/lms/static/js/search/course/views/search_item_view.js
new file mode 100644
index 0000000000..cd130b2240
--- /dev/null
+++ b/lms/static/js/search/course/views/search_item_view.js
@@ -0,0 +1,14 @@
+;(function (define) {
+
+define([
+ 'js/search/base/views/search_item_view'
+], function (SearchItemView) {
+ 'use strict';
+
+ return SearchItemView.extend({
+ templateId: '#course_search_item-tpl'
+ });
+
+});
+
+})(define || RequireJS.define);
diff --git a/lms/static/js/search/course/views/search_results_view.js b/lms/static/js/search/course/views/search_results_view.js
new file mode 100644
index 0000000000..917e7e960b
--- /dev/null
+++ b/lms/static/js/search/course/views/search_results_view.js
@@ -0,0 +1,26 @@
+;(function (define) {
+
+define([
+ 'js/search/base/views/search_results_view',
+ 'js/search/course/views/search_item_view'
+], function (SearchResultsView, CourseSearchItemView) {
+ 'use strict';
+
+ return SearchResultsView.extend({
+
+ el: '#courseware-search-results',
+ contentElement: '#course-content',
+ resultsTemplateId: '#course_search_results-tpl',
+ loadingTemplateId: '#search_loading-tpl',
+ errorTemplateId: '#search_error-tpl',
+ events: {
+ 'click .search-load-next': 'loadNext',
+ },
+ SearchItemView: CourseSearchItemView
+
+ });
+
+});
+
+
+})(define || RequireJS.define);
diff --git a/lms/static/js/search/dashboard/main.js b/lms/static/js/search/dashboard/main.js
new file mode 100644
index 0000000000..0a6574caa0
--- /dev/null
+++ b/lms/static/js/search/dashboard/main.js
@@ -0,0 +1,19 @@
+RequireJS.require([
+ 'backbone',
+ 'js/search/dashboard/search_app',
+ 'js/search/base/routers/search_router',
+ 'js/search/dashboard/views/search_form',
+ 'js/search/base/collections/search_collection',
+ 'js/search/dashboard/views/search_results_view'
+], function (Backbone, SearchApp, SearchRouter, DashSearchForm, SearchCollection, DashSearchResultsView) {
+ 'use strict';
+
+ var app = new SearchApp(
+ SearchRouter,
+ DashSearchForm,
+ SearchCollection,
+ DashSearchResultsView
+ );
+ Backbone.history.start();
+
+});
diff --git a/lms/static/js/search/dashboard/search_app.js b/lms/static/js/search/dashboard/search_app.js
new file mode 100644
index 0000000000..d60d604750
--- /dev/null
+++ b/lms/static/js/search/dashboard/search_app.js
@@ -0,0 +1,54 @@
+;(function (define) {
+
+define(['backbone'], function(Backbone) {
+ 'use strict';
+
+ return function (SearchRouter, SearchForm, SearchCollection, SearchListView) {
+
+ var router = new SearchRouter();
+ var form = new SearchForm();
+ var collection = new SearchCollection([]);
+ var results = new SearchListView({ collection: collection });
+ var dispatcher = _.clone(Backbone.Events);
+
+ dispatcher.listenTo(router, 'search', function (query) {
+ form.doSearch(query);
+ });
+
+ dispatcher.listenTo(form, 'search', function (query) {
+ results.showLoadingMessage();
+ collection.performSearch(query);
+ router.navigate('search/' + query, { replace: true });
+ });
+
+ dispatcher.listenTo(form, 'clear', function () {
+ collection.cancelSearch();
+ results.clear();
+ router.navigate('');
+ });
+
+ dispatcher.listenTo(results, 'next', function () {
+ collection.loadNextPage();
+ });
+
+ dispatcher.listenTo(results, 'reset', function () {
+ form.resetSearchForm();
+ });
+
+ dispatcher.listenTo(collection, 'search', function () {
+ results.render();
+ });
+
+ dispatcher.listenTo(collection, 'next', function () {
+ results.renderNext();
+ });
+
+ dispatcher.listenTo(collection, 'error', function () {
+ results.showErrorMessage();
+ });
+
+ };
+
+});
+
+})(define || RequireJS.define);
diff --git a/lms/static/js/search/dashboard/views/search_form.js b/lms/static/js/search/dashboard/views/search_form.js
new file mode 100644
index 0000000000..8c3a4e62e4
--- /dev/null
+++ b/lms/static/js/search/dashboard/views/search_form.js
@@ -0,0 +1,14 @@
+;(function (define) {
+
+define([
+ 'js/search/base/views/search_form'
+], function (SearchForm) {
+ 'use strict';
+
+ return SearchForm.extend({
+ el: '#dashboard-search-bar'
+ });
+
+});
+
+})(define || RequireJS.define);
diff --git a/lms/static/js/search/dashboard/views/search_item_view.js b/lms/static/js/search/dashboard/views/search_item_view.js
new file mode 100644
index 0000000000..d577e2f1f0
--- /dev/null
+++ b/lms/static/js/search/dashboard/views/search_item_view.js
@@ -0,0 +1,14 @@
+;(function (define) {
+
+define([
+ 'js/search/base/views/search_item_view'
+], function (SearchItemView) {
+ 'use strict';
+
+ return SearchItemView.extend({
+ templateId: '#dashboard_search_item-tpl'
+ });
+
+});
+
+})(define || RequireJS.define);
diff --git a/lms/static/js/search/dashboard/views/search_results_view.js b/lms/static/js/search/dashboard/views/search_results_view.js
new file mode 100644
index 0000000000..b94826e0cf
--- /dev/null
+++ b/lms/static/js/search/dashboard/views/search_results_view.js
@@ -0,0 +1,33 @@
+;(function (define) {
+
+define([
+ 'js/search/base/views/search_results_view',
+ 'js/search/dashboard/views/search_item_view'
+], function (SearchResultsView, DashSearchItemView) {
+ 'use strict';
+
+ return SearchResultsView.extend({
+
+ el: '#dashboard-search-results',
+ contentElement: '#my-courses, #profile-sidebar',
+ resultsTemplateId: '#dashboard_search_results-tpl',
+ loadingTemplateId: '#search_loading-tpl',
+ errorTemplateId: '#search_error-tpl',
+ events: {
+ 'click .search-load-next': 'loadNext',
+ 'click .search-back-to-courses': 'backToCourses'
+ },
+ SearchItemView: DashSearchItemView,
+
+ backToCourses: function () {
+ this.clear();
+ this.trigger('reset');
+ return false;
+ }
+
+ });
+
+});
+
+
+})(define || RequireJS.define);
diff --git a/lms/static/js/search/main.js b/lms/static/js/search/main.js
deleted file mode 100644
index f97d3eb95c..0000000000
--- a/lms/static/js/search/main.js
+++ /dev/null
@@ -1,12 +0,0 @@
-RequireJS.require([
- 'jquery',
- 'backbone',
- 'js/search/search_app'
-], function ($, Backbone, SearchApp) {
- 'use strict';
-
- var course_id = $('#courseware-search-results').attr('data-course-id');
- var app = new SearchApp(course_id);
- Backbone.history.start();
-
-});
diff --git a/lms/static/js/search/search_app.js b/lms/static/js/search/search_app.js
deleted file mode 100644
index bf69ac3930..0000000000
--- a/lms/static/js/search/search_app.js
+++ /dev/null
@@ -1,37 +0,0 @@
-;(function (define) {
-
-define([
- 'backbone',
- 'js/search/search_router',
- 'js/search/views/search_form',
- 'js/search/views/search_list_view',
- 'js/search/collections/search_collection'
-], function(Backbone, SearchRouter, SearchForm, SearchListView, SearchCollection) {
- 'use strict';
-
- return function (course_id) {
-
- var self = this;
-
- this.router = new SearchRouter();
- this.form = new SearchForm();
- this.collection = new SearchCollection([], { course_id: course_id });
- this.results = new SearchListView({ collection: this.collection });
-
- this.form.on('search', this.results.showLoadingMessage, this.results);
- this.form.on('search', this.collection.performSearch, this.collection);
- this.form.on('search', function (term) {
- self.router.navigate('search/' + term, { replace: true });
- });
- this.form.on('clear', this.collection.cancelSearch, this.collection);
- this.form.on('clear', this.results.clear, this.results);
- this.form.on('clear', this.router.navigate, this.router);
-
- this.results.on('next', this.collection.loadNextPage, this.collection);
- this.router.on('route:search', this.form.doSearch, this.form);
-
- };
-
-});
-
-})(define || RequireJS.define);
diff --git a/lms/static/js/spec/search/search_spec.js b/lms/static/js/spec/search/search_spec.js
index adc3260515..ebe315684a 100644
--- a/lms/static/js/spec/search/search_spec.js
+++ b/lms/static/js/spec/search/search_spec.js
@@ -4,136 +4,37 @@ define([
'backbone',
'logger',
'js/common_helpers/template_helpers',
- 'js/search/views/search_form',
- 'js/search/views/search_item_view',
- 'js/search/views/search_list_view',
- 'js/search/models/search_result',
- 'js/search/collections/search_collection',
- 'js/search/search_router',
- 'js/search/search_app'
+ 'js/search/base/models/search_result',
+ 'js/search/base/collections/search_collection',
+ 'js/search/base/routers/search_router',
+ 'js/search/course/views/search_item_view',
+ 'js/search/dashboard/views/search_item_view',
+ 'js/search/course/views/search_form',
+ 'js/search/dashboard/views/search_form',
+ 'js/search/course/views/search_results_view',
+ 'js/search/dashboard/views/search_results_view',
+ 'js/search/course/search_app',
+ 'js/search/dashboard/search_app'
], function(
$,
Sinon,
Backbone,
Logger,
TemplateHelpers,
- SearchForm,
- SearchItemView,
- SearchListView,
SearchResult,
SearchCollection,
SearchRouter,
- SearchApp
+ CourseSearchItemView,
+ DashSearchItemView,
+ CourseSearchForm,
+ DashSearchForm,
+ CourseSearchResultsView,
+ DashSearchResultsView,
+ CourseSearchApp,
+ DashSearchApp
) {
'use strict';
- describe('SearchForm', function () {
-
- beforeEach(function () {
- loadFixtures('js/fixtures/search_form.html');
- this.form = new SearchForm();
- this.onClear = jasmine.createSpy('onClear');
- this.onSearch = jasmine.createSpy('onSearch');
- this.form.on('clear', this.onClear);
- this.form.on('search', this.onSearch);
- });
-
- it('trims input string', function () {
- var term = ' search string ';
- $('.search-field').val(term);
- $('form').trigger('submit');
- expect(this.onSearch).toHaveBeenCalledWith($.trim(term));
- });
-
- it('handles calls to doSearch', function () {
- var term = ' search string ';
- $('.search-field').val(term);
- this.form.doSearch(term);
- expect(this.onSearch).toHaveBeenCalledWith($.trim(term));
- expect($('.search-field').val()).toEqual(term);
- expect($('.search-field')).toHaveClass('is-active');
- expect($('.search-button')).toBeHidden();
- expect($('.cancel-button')).toBeVisible();
- });
-
- it('triggers a search event and changes to active state', function () {
- var term = 'search string';
- $('.search-field').val(term);
- $('form').trigger('submit');
- expect(this.onSearch).toHaveBeenCalledWith(term);
- expect($('.search-field')).toHaveClass('is-active');
- expect($('.search-button')).toBeHidden();
- expect($('.cancel-button')).toBeVisible();
- });
-
- it('clears search when clicking on cancel button', function () {
- $('.search-field').val('search string');
- $('.cancel-button').trigger('click');
- expect($('.search-field')).not.toHaveClass('is-active');
- expect($('.search-button')).toBeVisible();
- expect($('.cancel-button')).toBeHidden();
- expect($('.search-field')).toHaveValue('');
- });
-
- it('clears search when search box is empty', function() {
- $('.search-field').val('');
- $('form').trigger('submit');
- expect(this.onClear).toHaveBeenCalled();
- expect($('.search-field')).not.toHaveClass('is-active');
- expect($('.cancel-button')).toBeHidden();
- expect($('.search-button')).toBeVisible();
- });
-
- });
-
-
- describe('SearchItemView', function () {
-
- beforeEach(function () {
- TemplateHelpers.installTemplate('templates/courseware_search/search_item');
- TemplateHelpers.installTemplate('templates/courseware_search/search_item_seq');
- this.model = {
- attributes: {
- location: ['section', 'subsection', 'unit'],
- content_type: 'Video',
- excerpt: 'A short excerpt.',
- url: 'path/to/content'
- }
- };
- this.item = new SearchItemView({ model: this.model });
- });
-
- it('has useful html attributes', function () {
- expect(this.item.$el).toHaveAttr('role', 'region');
- expect(this.item.$el).toHaveAttr('aria-label', 'search result');
- });
-
- it('renders correctly', function () {
- var href = this.model.attributes.url;
- var breadcrumbs = 'section ▸ subsection ▸ unit';
-
- this.item.render();
- expect(this.item.$el).toContainHtml(this.model.attributes.content_type);
- expect(this.item.$el).toContainHtml(this.model.attributes.excerpt);
- expect(this.item.$el).toContain('a[href="'+href+'"]');
- expect(this.item.$el).toContainHtml(breadcrumbs);
- });
-
- it('log request on follow item link', function () {
- this.model.collection = new SearchCollection([this.model], { course_id: 'edx101' });
- this.item.render();
- // Mock the redirect call
- spyOn(this.item, 'redirect').andCallFake( function() {} );
- spyOn(this.item, 'logSearchItem').andCallThrough();
- spyOn(Logger, 'log').andReturn($.Deferred().resolve());
- var link = this.item.$el.find('a');
- expect(link.length).toBe(1);
- link.trigger('click');
- expect(this.item.redirect).toHaveBeenCalled();
- });
-
- });
-
describe('SearchResult', function () {
@@ -171,9 +72,16 @@ define([
this.server.restore();
});
- it('appends course_id to url', function () {
- var collection = new SearchCollection([], { course_id: 'edx101' });
- expect(collection.url).toEqual('/search/edx101');
+ it('sends a request without a course ID', function () {
+ var collection = new SearchCollection([]);
+ collection.performSearch('search string');
+ expect(this.server.requests[0].url).toEqual('/search/');
+ });
+
+ it('sends a request with course ID', function () {
+ var collection = new SearchCollection([], { courseId: 'edx101' });
+ collection.performSearch('search string');
+ expect(this.server.requests[0].url).toEqual('/search/edx101');
});
it('sends a request and parses the json result', function () {
@@ -195,6 +103,7 @@ define([
expect(this.onSearch).toHaveBeenCalled();
expect(this.collection.totalCount).toEqual(1);
+ expect(this.collection.latestModelsCount).toEqual(1);
expect(this.collection.accessDeniedCount).toEqual(1);
expect(this.collection.page).toEqual(0);
expect(this.collection.first().attributes).toEqual(response.results[0].data);
@@ -216,8 +125,9 @@ define([
});
it('sends correct paging parameters', function () {
- this.collection.performSearch('search string');
+ var searchString = 'search string';
var response = { total: 52, results: [] };
+ this.collection.performSearch(searchString);
this.server.respondWith('POST', this.collection.url, [200, {}, JSON.stringify(response)]);
this.server.respond();
this.collection.loadNextPage();
@@ -225,11 +135,9 @@ define([
spyOn($, 'ajax');
this.collection.loadNextPage();
expect($.ajax.mostRecentCall.args[0].url).toEqual(this.collection.url);
- expect($.ajax.mostRecentCall.args[0].data).toEqual({
- search_string : 'search string',
- page_size : this.collection.pageSize,
- page_index : 2
- });
+ expect($.ajax.mostRecentCall.args[0].data.search_string).toEqual(searchString);
+ expect($.ajax.mostRecentCall.args[0].data.page_size).toEqual(this.collection.pageSize);
+ expect($.ajax.mostRecentCall.args[0].data.page_index).toEqual(2);
});
it('has next page', function () {
@@ -266,18 +174,23 @@ define([
beforeEach(function () {
this.collection.page = 2;
this.collection.totalCount = 35;
+ this.collection.latestModelsCount = 5;
});
it('resets state when performing new search', function () {
this.collection.performSearch('search string');
+ expect(this.collection.models.length).toEqual(0);
expect(this.collection.page).toEqual(0);
expect(this.collection.totalCount).toEqual(0);
+ expect(this.collection.latestModelsCount).toEqual(0);
});
it('resets state when canceling a search', function () {
this.collection.cancelSearch();
+ expect(this.collection.models.length).toEqual(0);
expect(this.collection.page).toEqual(0);
expect(this.collection.totalCount).toEqual(0);
+ expect(this.collection.latestModelsCount).toEqual(0);
});
});
@@ -285,167 +198,368 @@ define([
});
- describe('SearchListView', function () {
-
- beforeEach(function () {
- setFixtures(
- '' +
- ''
- );
-
- TemplateHelpers.installTemplates([
- 'templates/courseware_search/search_item',
- 'templates/courseware_search/search_item_seq',
- 'templates/courseware_search/search_list',
- 'templates/courseware_search/search_loading',
- 'templates/courseware_search/search_error'
- ]);
-
- var MockCollection = Backbone.Collection.extend({
- hasNextPage: function (){}
- });
- this.collection = new MockCollection();
-
- // spy on these methods before they are bound to events
- spyOn(SearchListView.prototype, 'render').andCallThrough();
- spyOn(SearchListView.prototype, 'renderNext').andCallThrough();
- spyOn(SearchListView.prototype, 'showErrorMessage').andCallThrough();
-
- this.listView = new SearchListView({ collection: this.collection });
- });
-
- it('shows loading message', function () {
- this.listView.showLoadingMessage();
- expect($('#course-content')).toBeHidden();
- expect(this.listView.$el).toBeVisible();
- expect(this.listView.$el).not.toBeEmpty();
- });
-
- it('shows error message', function () {
- this.listView.showErrorMessage();
- expect($('#course-content')).toBeHidden();
- expect(this.listView.$el).toBeVisible();
- expect(this.listView.$el).not.toBeEmpty();
- });
-
- it('returns to content', function () {
- this.listView.clear();
- expect($('#course-content')).toBeVisible();
- expect(this.listView.$el).toBeHidden();
- expect(this.listView.$el).toBeEmpty();
- });
-
- it('handles events', function () {
- this.collection.trigger('search');
- this.collection.trigger('next');
- this.collection.trigger('error');
-
- expect(this.listView.render).toHaveBeenCalled();
- expect(this.listView.renderNext).toHaveBeenCalled();
- expect(this.listView.showErrorMessage).toHaveBeenCalled();
- });
-
- it('renders a message when there are no results', function () {
- this.collection.reset();
- this.listView.render();
- expect(this.listView.$el).toContainHtml('no results');
- expect(this.listView.$el.find('ol')).not.toExist();
- });
-
- it('renders search results', function () {
- var searchResults = [{
- location: ['section', 'subsection', 'unit'],
- url: '/some/url/to/content',
- content_type: 'text',
- excerpt: 'this is a short excerpt'
- }];
- this.collection.set(searchResults);
- this.collection.totalCount = 1;
-
- this.listView.render();
- expect(this.listView.$el.find('ol')[0]).toExist();
- expect(this.listView.$el.find('li').length).toEqual(1);
- expect(this.listView.$el).toContainHtml('Search Results');
- expect(this.listView.$el).toContainHtml('this is a short excerpt');
-
- searchResults[1] = searchResults[0]
- this.collection.set(searchResults);
- this.collection.totalCount = 2;
- this.listView.renderNext();
- expect(this.listView.$el.find('.search-count')).toContainHtml('2');
- expect(this.listView.$el.find('li').length).toEqual(2);
- });
-
- it('shows a link to load more results', function () {
- this.collection.totalCount = 123;
- this.collection.hasNextPage = function () { return true; };
- this.listView.render();
- expect(this.listView.$el.find('a.search-load-next')[0]).toExist();
-
- this.collection.totalCount = 123;
- this.collection.hasNextPage = function () { return false; };
- this.listView.render();
- expect(this.listView.$el.find('a.search-load-next')[0]).not.toExist();
- });
-
- it('triggers an event for next page', function () {
- var onNext = jasmine.createSpy('onNext');
- this.listView.on('next', onNext);
- this.collection.totalCount = 123;
- this.collection.hasNextPage = function () { return true; };
- this.listView.render();
- this.listView.$el.find('a.search-load-next').click();
- expect(onNext).toHaveBeenCalled();
- });
-
- it('shows a spinner when loading more results', function () {
- this.collection.totalCount = 123;
- this.collection.hasNextPage = function () { return true; };
- this.listView.render();
- this.listView.loadNext();
-
- // Do we really need to check if a loading indicator exists? - CR
-
- // jasmine.Clock.useMock(1000);
- // expect(this.listView.$el.find('a.search-load-next .icon')[0]).toBeVisible();
- this.listView.renderNext();
- // jasmine.Clock.useMock(1000);
- // expect(this.listView.$el.find('a.search-load-next .icon')[0]).toBeHidden();
- });
-
- });
-
-
describe('SearchRouter', function () {
beforeEach(function () {
this.router = new SearchRouter();
+ this.onSearch = jasmine.createSpy('onSearch');
+ this.router.on('search', this.onSearch);
});
it ('has a search route', function () {
expect(this.router.routes['search/:query']).toEqual('search');
});
+ it ('triggers a search event', function () {
+ var query = 'mercury';
+ this.router.search(query);
+ expect(this.onSearch).toHaveBeenCalledWith(query);
+ });
+
+ });
+
+
+ describe('SearchItemView', function () {
+
+ function beforeEachHelper(SearchItemView) {
+ TemplateHelpers.installTemplates([
+ 'templates/search/course_search_item',
+ 'templates/search/dashboard_search_item'
+ ]);
+
+ this.model = new SearchResult({
+ location: ['section', 'subsection', 'unit'],
+ content_type: 'Video',
+ course_name: 'Course Name',
+ excerpt: 'A short excerpt.',
+ url: 'path/to/content'
+ });
+
+ this.seqModel = new SearchResult({
+ location: ['section', 'subsection'],
+ content_type: 'Sequence',
+ course_name: 'Course Name',
+ excerpt: 'A short excerpt.',
+ url: 'path/to/content'
+ });
+
+ this.item = new SearchItemView({ model: this.model });
+ this.item.render();
+ this.seqItem = new SearchItemView({ model: this.seqModel });
+ this.seqItem.render();
+ }
+
+ function rendersItem() {
+ expect(this.item.$el).toHaveAttr('role', 'region');
+ expect(this.item.$el).toHaveAttr('aria-label', 'search result');
+ expect(this.item.$el).toContain('a[href="' + this.model.get('url') + '"]');
+ expect(this.item.$el.find('.result-type')).toContainHtml(this.model.get('content_type'));
+ expect(this.item.$el.find('.result-excerpt')).toContainHtml(this.model.get('excerpt'));
+ expect(this.item.$el.find('.result-location')).toContainHtml('section ▸ subsection ▸ unit');
+ }
+
+ function rendersSequentialItem() {
+ expect(this.seqItem.$el).toHaveAttr('role', 'region');
+ expect(this.seqItem.$el).toHaveAttr('aria-label', 'search result');
+ expect(this.seqItem.$el).toContain('a[href="' + this.seqModel.get('url') + '"]');
+ expect(this.seqItem.$el.find('.result-type')).toBeEmpty();
+ expect(this.seqItem.$el.find('.result-excerpt')).toBeEmpty();
+ expect(this.seqItem.$el.find('.result-location')).toContainHtml('section ▸ subsection');
+ }
+
+ function logsSearchItemViewEvent() {
+ this.model.collection = new SearchCollection([this.model], { course_id: 'edx101' });
+ this.item.render();
+ // Mock the redirect call
+ spyOn(this.item, 'redirect').andCallFake( function() {} );
+ spyOn(Logger, 'log').andReturn($.Deferred().resolve());
+ this.item.$el.find('a').trigger('click');
+ expect(this.item.redirect).toHaveBeenCalled();
+ this.item.$el.trigger('click');
+ expect(this.item.redirect).toHaveBeenCalled();
+ }
+
+ describe('CourseSearchItemView', function () {
+ beforeEach(function () {
+ beforeEachHelper.call(this, CourseSearchItemView);
+ });
+ it('renders items correctly', rendersItem);
+ it('renders Sequence items correctly', rendersSequentialItem);
+ it('logs view event', logsSearchItemViewEvent);
+ });
+
+ describe('DashSearchItemView', function () {
+ beforeEach(function () {
+ beforeEachHelper.call(this, DashSearchItemView);
+ });
+ it('renders items correctly', rendersItem);
+ it('renders Sequence items correctly', rendersSequentialItem);
+ it('displays course name in breadcrumbs', function () {
+ expect(this.seqItem.$el.find('.result-course-name')).toContainHtml(this.model.get('course_name'));
+ });
+ it('logs view event', logsSearchItemViewEvent);
+ });
+
+ });
+
+
+ describe('SearchForm', function () {
+
+ function trimsInputString() {
+ var term = ' search string ';
+ $('.search-field').val(term);
+ $('form').trigger('submit');
+ expect(this.onSearch).toHaveBeenCalledWith($.trim(term));
+ }
+
+ function doesSearch() {
+ var term = ' search string ';
+ $('.search-field').val(term);
+ this.form.doSearch(term);
+ expect(this.onSearch).toHaveBeenCalledWith($.trim(term));
+ expect($('.search-field').val()).toEqual(term);
+ expect($('.search-field')).toHaveClass('is-active');
+ expect($('.search-button')).toBeHidden();
+ expect($('.cancel-button')).toBeVisible();
+ }
+
+ function triggersSearchEvent() {
+ var term = 'search string';
+ $('.search-field').val(term);
+ $('form').trigger('submit');
+ expect(this.onSearch).toHaveBeenCalledWith(term);
+ expect($('.search-field')).toHaveClass('is-active');
+ expect($('.search-button')).toBeHidden();
+ expect($('.cancel-button')).toBeVisible();
+ }
+
+ function clearsSearchOnCancel() {
+ $('.search-field').val('search string');
+ $('.search-button').trigger('click');
+ $('.cancel-button').trigger('click');
+ expect($('.search-field')).not.toHaveClass('is-active');
+ expect($('.search-button')).toBeVisible();
+ expect($('.cancel-button')).toBeHidden();
+ expect($('.search-field')).toHaveValue('');
+ }
+
+ function clearsSearchOnEmpty() {
+ $('.search-field').val('');
+ $('form').trigger('submit');
+ expect(this.onClear).toHaveBeenCalled();
+ expect($('.search-field')).not.toHaveClass('is-active');
+ expect($('.cancel-button')).toBeHidden();
+ expect($('.search-button')).toBeVisible();
+ }
+
+ describe('CourseSearchForm', function () {
+ beforeEach(function () {
+ loadFixtures('js/fixtures/search/course_search_form.html');
+ this.form = new CourseSearchForm();
+ this.onClear = jasmine.createSpy('onClear');
+ this.onSearch = jasmine.createSpy('onSearch');
+ this.form.on('clear', this.onClear);
+ this.form.on('search', this.onSearch);
+ });
+ it('trims input string', trimsInputString);
+ it('handles calls to doSearch', doesSearch);
+ it('triggers a search event and changes to active state', triggersSearchEvent);
+ it('clears search when clicking on cancel button', clearsSearchOnCancel);
+ it('clears search when search box is empty', clearsSearchOnEmpty);
+ });
+
+ describe('DashSearchForm', function () {
+ beforeEach(function () {
+ loadFixtures('js/fixtures/search/dashboard_search_form.html');
+ this.form = new DashSearchForm();
+ this.onClear = jasmine.createSpy('onClear');
+ this.onSearch = jasmine.createSpy('onSearch');
+ this.form.on('clear', this.onClear);
+ this.form.on('search', this.onSearch);
+ });
+ it('trims input string', trimsInputString);
+ it('handles calls to doSearch', doesSearch);
+ it('triggers a search event and changes to active state', triggersSearchEvent);
+ it('clears search when clicking on cancel button', clearsSearchOnCancel);
+ it('clears search when search box is empty', clearsSearchOnEmpty);
+ });
+
+ });
+
+
+ describe('SearchResultsView', function () {
+
+ function showsLoadingMessage () {
+ this.resultsView.showLoadingMessage();
+ expect(this.resultsView.$contentElement).toBeHidden();
+ expect(this.resultsView.$el).toBeVisible();
+ expect(this.resultsView.$el).not.toBeEmpty();
+ }
+
+ function showsErrorMessage () {
+ this.resultsView.showErrorMessage();
+ expect(this.resultsView.$contentElement).toBeHidden();
+ expect(this.resultsView.$el).toBeVisible();
+ expect(this.resultsView.$el).not.toBeEmpty();
+ }
+
+ function returnsToContent () {
+ this.resultsView.clear();
+ expect(this.resultsView.$contentElement).toBeVisible();
+ expect(this.resultsView.$el).toBeHidden();
+ expect(this.resultsView.$el).toBeEmpty();
+ }
+
+ function showsNoResultsMessage() {
+ this.collection.reset();
+ this.resultsView.render();
+ expect(this.resultsView.$el).toContainHtml('no results');
+ expect(this.resultsView.$el.find('ol')).not.toExist();
+ }
+
+ function rendersSearchResults () {
+ var searchResults = [{
+ location: ['section', 'subsection', 'unit'],
+ url: '/some/url/to/content',
+ content_type: 'text',
+ course_name: '',
+ excerpt: 'this is a short excerpt'
+ }];
+ this.collection.set(searchResults);
+ this.collection.latestModelsCount = 1;
+ this.collection.totalCount = 1;
+
+ this.resultsView.render();
+ expect(this.resultsView.$el.find('ol')[0]).toExist();
+ expect(this.resultsView.$el.find('li').length).toEqual(1);
+ expect(this.resultsView.$el).toContainHtml('Search Results');
+ expect(this.resultsView.$el).toContainHtml('this is a short excerpt');
+
+ this.collection.set(searchResults);
+ this.collection.totalCount = 2;
+ this.resultsView.renderNext();
+ expect(this.resultsView.$el.find('.search-count')).toContainHtml('2');
+ expect(this.resultsView.$el.find('li').length).toEqual(2);
+ }
+
+ function showsMoreResultsLink () {
+ this.collection.totalCount = 123;
+ this.collection.hasNextPage = function () { return true; };
+ this.resultsView.render();
+ expect(this.resultsView.$el.find('a.search-load-next')[0]).toExist();
+
+ this.collection.totalCount = 123;
+ this.collection.hasNextPage = function () { return false; };
+ this.resultsView.render();
+ expect(this.resultsView.$el.find('a.search-load-next')[0]).not.toExist();
+ }
+
+ function triggersNextPageEvent () {
+ var onNext = jasmine.createSpy('onNext');
+ this.resultsView.on('next', onNext);
+ this.collection.totalCount = 123;
+ this.collection.hasNextPage = function () { return true; };
+ this.resultsView.render();
+ this.resultsView.$el.find('a.search-load-next').click();
+ expect(onNext).toHaveBeenCalled();
+ }
+
+ function showsLoadMoreSpinner () {
+ this.collection.totalCount = 123;
+ this.collection.hasNextPage = function () { return true; };
+ this.resultsView.render();
+ expect(this.resultsView.$el.find('a.search-load-next .icon')).toBeHidden();
+ this.resultsView.loadNext();
+ // toBeVisible does not work with inline
+ expect(this.resultsView.$el.find('a.search-load-next .icon')).toHaveCss({ 'display': 'inline' });
+ this.resultsView.renderNext();
+ expect(this.resultsView.$el.find('a.search-load-next .icon')).toBeHidden();
+ }
+
+ function beforeEachHelper(SearchResultsView) {
+ appendSetFixtures(
+ '' +
+ '' +
+ '' +
+ ''
+ );
+
+ TemplateHelpers.installTemplates([
+ 'templates/search/course_search_item',
+ 'templates/search/dashboard_search_item',
+ 'templates/search/course_search_results',
+ 'templates/search/dashboard_search_results',
+ 'templates/search/search_list',
+ 'templates/search/search_loading',
+ 'templates/search/search_error'
+ ]);
+
+ var MockCollection = Backbone.Collection.extend({
+ hasNextPage: function () {},
+ latestModelsCount: 0,
+ pageSize: 20,
+ latestModels: function () {
+ return SearchCollection.prototype.latestModels.apply(this, arguments);
+ }
+ });
+ this.collection = new MockCollection();
+ this.resultsView = new SearchResultsView({ collection: this.collection });
+ }
+
+ describe('CourseSearchResultsView', function () {
+ beforeEach(function() {
+ beforeEachHelper.call(this, CourseSearchResultsView);
+ });
+ it('shows loading message', showsLoadingMessage);
+ it('shows error message', showsErrorMessage);
+ it('returns to content', returnsToContent);
+ it('shows a message when there are no results', showsNoResultsMessage);
+ it('renders search results', rendersSearchResults);
+ it('shows a link to load more results', showsMoreResultsLink);
+ it('triggers an event for next page', triggersNextPageEvent);
+ it('shows a spinner when loading more results', showsLoadMoreSpinner);
+ });
+
+ describe('DashSearchResultsView', function () {
+ beforeEach(function() {
+ beforeEachHelper.call(this, DashSearchResultsView);
+ });
+ it('shows loading message', showsLoadingMessage);
+ it('shows error message', showsErrorMessage);
+ it('returns to content', returnsToContent);
+ it('shows a message when there are no results', showsNoResultsMessage);
+ it('renders search results', rendersSearchResults);
+ it('shows a link to load more results', showsMoreResultsLink);
+ it('triggers an event for next page', triggersNextPageEvent);
+ it('shows a spinner when loading more results', showsLoadMoreSpinner);
+ it('returns back to courses', function () {
+ var onReset = jasmine.createSpy('onReset');
+ this.resultsView.on('reset', onReset);
+ this.resultsView.render();
+ expect(this.resultsView.$el.find('a.search-back-to-courses')).toExist();
+ this.resultsView.$el.find('.search-back-to-courses').click();
+ expect(onReset).toHaveBeenCalled();
+ expect(this.resultsView.$contentElement).toBeVisible();
+ expect(this.resultsView.$el).toBeHidden();
+ });
+ });
+
});
describe('SearchApp', function () {
- beforeEach(function () {
- loadFixtures('js/fixtures/search_form.html');
- appendSetFixtures(
- '' +
- ''
- );
- TemplateHelpers.installTemplates([
- 'templates/courseware_search/search_item',
- 'templates/courseware_search/search_item_seq',
- 'templates/courseware_search/search_list',
- 'templates/courseware_search/search_loading',
- 'templates/courseware_search/search_error'
- ]);
+ function showsLoadingMessage () {
+ $('.search-field').val('search string');
+ $('.search-button').trigger('click');
+ expect(this.$contentElement).toBeHidden();
+ expect(this.$searchResults).toBeVisible();
+ expect(this.$searchResults).not.toBeEmpty();
+ }
- this.server = Sinon.fakeServer.create();
+ function performsSearch () {
+ $('.search-field').val('search string');
+ $('.search-button').trigger('click');
this.server.respondWith([200, {}, JSON.stringify({
total: 1337,
access_denied_count: 12,
@@ -454,82 +568,203 @@ define([
location: ['section', 'subsection', 'unit'],
url: '/some/url/to/content',
content_type: 'text',
- excerpt: 'this is a short excerpt'
+ excerpt: 'this is a short excerpt',
+ course_name: ''
}
}]
})]);
-
- Backbone.history.stop();
- this.app = new SearchApp('a/b/c');
-
- // start history after the application has finished creating
- // all of its routers
- Backbone.history.start();
- });
-
- afterEach(function () {
- this.server.restore();
- });
-
- it ('shows loading message on search', function () {
- $('.search-field').val('search string');
- $('.search-button').trigger('click');
- expect($('#course-content')).toBeHidden();
- expect($('#courseware-search-results')).toBeVisible();
- expect($('#courseware-search-results')).not.toBeEmpty();
- });
-
- it ('performs search', function () {
- $('.search-field').val('search string');
- $('.search-button').trigger('click');
this.server.respond();
expect($('.search-info')).toExist();
- expect($('.search-results')).toBeVisible();
- });
+ expect($('.search-result-list')).toBeVisible();
+ expect(this.$searchResults.find('li').length).toEqual(1);
+ }
- it ('updates navigation history on search', function () {
+ function showsErrorMessage () {
+ $('.search-field').val('search string');
+ $('.search-button').trigger('click');
+ this.server.respondWith([500, {}]);
+ this.server.respond();
+ expect(this.$searchResults).toEqual($('#search_error-tpl'));
+ }
+
+ function updatesNavigationHistory () {
$('.search-field').val('edx');
$('.search-button').trigger('click');
- expect(Backbone.history.fragment).toEqual('search/edx');
- });
+ expect(Backbone.history.navigate.calls[0].args).toContain('search/edx');
+ $('.cancel-button').trigger('click');
+ expect(Backbone.history.navigate.calls[1].args).toContain('');
+ }
- it ('aborts sent search request', function () {
+ function cancelsSearchRequest () {
// send search request to server
$('.search-field').val('search string');
$('.search-button').trigger('click');
// cancel search
$('.cancel-button').trigger('click');
+ this.server.respondWith([200, {}, JSON.stringify({
+ total: 1337,
+ access_denied_count: 12,
+ results: [{
+ data: {
+ location: ['section', 'subsection', 'unit'],
+ url: '/some/url/to/content',
+ content_type: 'text',
+ excerpt: 'this is a short excerpt',
+ course_name: ''
+ }
+ }]
+ })]);
this.server.respond();
// there should be no results
- expect($('#course-content')).toBeVisible();
- expect($('#courseware-search-results')).toBeHidden();
- });
+ expect(this.$contentElement).toBeVisible();
+ expect(this.$searchResults).toBeHidden();
+ }
- it ('clears results', function () {
+ function clearsResults () {
$('.cancel-button').trigger('click');
- expect($('#course-content')).toBeVisible();
- expect($('#courseware-search-results')).toBeHidden();
- });
+ expect(this.$contentElement).toBeVisible();
+ expect(this.$searchResults).toBeHidden();
+ }
- it ('updates navigation history on clear', function () {
- $('.cancel-button').trigger('click');
- expect(Backbone.history.fragment).toEqual('');
- });
-
- it ('loads next page', function () {
+ function loadsNextPage () {
$('.search-field').val('query');
$('.search-button').trigger('click');
+ this.server.respondWith([200, {}, JSON.stringify({
+ total: 1337,
+ access_denied_count: 12,
+ results: [{
+ data: {
+ location: ['section', 'subsection', 'unit'],
+ url: '/some/url/to/content',
+ content_type: 'text',
+ excerpt: 'this is a short excerpt',
+ course_name: ''
+ }
+ }]
+ })]);
this.server.respond();
+ expect(this.$searchResults.find('li').length).toEqual(1);
expect($('.search-load-next')).toBeVisible();
$('.search-load-next').trigger('click');
var body = this.server.requests[1].requestBody;
expect(body).toContain('search_string=query');
expect(body).toContain('page_index=1');
- });
+ this.server.respond();
+ expect(this.$searchResults.find('li').length).toEqual(2);
+ }
- it ('navigates to search', function () {
+ function navigatesToSearch () {
Backbone.history.loadUrl('search/query');
expect(this.server.requests[0].requestBody).toContain('search_string=query');
+ }
+
+ function loadTemplates () {
+ TemplateHelpers.installTemplates([
+ 'templates/search/course_search_item',
+ 'templates/search/dashboard_search_item',
+ 'templates/search/search_loading',
+ 'templates/search/search_error',
+ 'templates/search/course_search_results',
+ 'templates/search/dashboard_search_results'
+ ]);
+ }
+
+ describe('CourseSearchApp', function () {
+
+ beforeEach(function () {
+ loadFixtures('js/fixtures/search/course_search_form.html');
+ appendSetFixtures(
+ '' +
+ ''
+ );
+ loadTemplates.call(this);
+
+ this.server = Sinon.fakeServer.create();
+ var courseId = 'a/b/c';
+ this.app = new CourseSearchApp(
+ courseId,
+ SearchRouter,
+ CourseSearchForm,
+ SearchCollection,
+ CourseSearchResultsView
+ );
+ spyOn(Backbone.history, 'navigate');
+ this.$contentElement = $('#course-content');
+ this.$searchResults = $('#courseware-search-results');
+ });
+
+ afterEach(function () {
+ this.server.restore();
+ });
+
+ it('shows loading message on search', showsLoadingMessage);
+ it('performs search', performsSearch);
+ it('updates navigation history', updatesNavigationHistory);
+ it('cancels search request', cancelsSearchRequest);
+ it('clears results', clearsResults);
+ it('loads next page', loadsNextPage);
+ it('navigates to search', navigatesToSearch);
+
+ });
+
+ describe('DashSearchApp', function () {
+
+ beforeEach(function () {
+ loadFixtures('js/fixtures/search/dashboard_search_form.html');
+ appendSetFixtures(
+ '' +
+ ''
+ );
+ loadTemplates.call(this);
+
+ this.server = Sinon.fakeServer.create();
+ this.app = new DashSearchApp(
+ SearchRouter,
+ DashSearchForm,
+ SearchCollection,
+ DashSearchResultsView
+ );
+
+ spyOn(Backbone.history, 'navigate');
+ this.$contentElement = $('#my-courses');
+ this.$searchResults = $('#dashboard-search-results');
+ });
+
+ afterEach(function () {
+ this.server.restore();
+ });
+
+ it('shows loading message on search', showsLoadingMessage);
+ it('performs search', performsSearch);
+ it('updates navigation history', updatesNavigationHistory);
+ it('cancels search request', cancelsSearchRequest);
+ it('clears results', clearsResults);
+ it('loads next page', loadsNextPage);
+ it('navigates to search', navigatesToSearch);
+ it('returns to course list', function () {
+ $('.search-field').val('search string');
+ $('.search-button').trigger('click');
+ this.server.respondWith([200, {}, JSON.stringify({
+ total: 1337,
+ access_denied_count: 12,
+ results: [{
+ data: {
+ location: ['section', 'subsection', 'unit'],
+ url: '/some/url/to/content',
+ content_type: 'text',
+ excerpt: 'this is a short excerpt',
+ course_name: ''
+ }
+ }]
+ })]);
+ this.server.respond();
+ expect($('.search-back-to-courses')).toExist();
+ $('.search-back-to-courses').trigger('click');
+ expect(this.$contentElement).toBeVisible();
+ expect(this.$searchResults).toBeHidden();
+ expect(this.$searchResults).toBeEmpty();
+ });
+
});
});
diff --git a/lms/static/js_test.yml b/lms/static/js_test.yml
index 53badbe193..3ac6aa5f21 100644
--- a/lms/static/js_test.yml
+++ b/lms/static/js_test.yml
@@ -89,7 +89,8 @@ fixture_paths:
- templates/verify_student
- templates/file-upload.underscore
- js/fixtures/edxnotes
- - templates/courseware_search
+ - js/fixtures/search
+ - templates/search
requirejs:
paths:
diff --git a/lms/static/sass/application-extend1.scss.mako b/lms/static/sass/application-extend1.scss.mako
index beb628e653..c954ca1cf8 100644
--- a/lms/static/sass/application-extend1.scss.mako
+++ b/lms/static/sass/application-extend1.scss.mako
@@ -57,6 +57,11 @@
@import 'multicourse/edge';
@import 'multicourse/survey-page';
+## Import styles for search
+% if env["FEATURES"].get("ENABLE_DASHBOARD_SEARCH", False):
+ @import 'search/_search';
+% endif
+
@import 'developer'; // used for any developer-created scss that needs further polish/refactoring
@import 'shame'; // used for any bad-form/orphaned scss
## NOTE: needed here for cascade and dependency purposes, but not a great permanent solution
diff --git a/lms/static/sass/course.scss.mako b/lms/static/sass/course.scss.mako
index b3b8ebb2bb..c749f82f62 100644
--- a/lms/static/sass/course.scss.mako
+++ b/lms/static/sass/course.scss.mako
@@ -41,9 +41,9 @@
@import 'course/courseware/sidebar';
@import 'course/courseware/amplifier';
-## Import styles for courseware search
+## Import styles for search
% if env["FEATURES"].get("ENABLE_COURSEWARE_SEARCH"):
- @import 'course/courseware/courseware_search';
+ @import 'search/_search';
% endif
// course - modules
diff --git a/lms/static/sass/course/courseware/_courseware_search.scss b/lms/static/sass/course/courseware/_courseware_search.scss
deleted file mode 100644
index d91d416865..0000000000
--- a/lms/static/sass/course/courseware/_courseware_search.scss
+++ /dev/null
@@ -1,110 +0,0 @@
-.course-index .courseware-search-bar {
-
- @include box-sizing(border-box);
- position: relative;
- padding: 5px;
- box-shadow: 0 1px 0 #fff inset, 0 -1px 0 rgba(0, 0, 0, .1) inset;
- font-family: $sans-serif;
-
- .search-field {
- @include box-sizing(border-box);
- top: 5px;
- width: 100%;
- @include border-radius(4px);
- background: $white-t1;
- &.is-active {
- background: $white;
- }
- }
-
- .search-button, .cancel-button {
- @include box-sizing(border-box);
- color: #888;
- font-size: 14px;
- font-weight: normal;
- display: block;
- position: absolute;
- right: 12px;
- top: 5px;
- height: 35px;
- line-height: 35px;
- padding: 0;
- border: none;
- box-shadow: none;
- background: transparent;
- }
-
- .cancel-button {
- display: none;
- }
-
-}
-
-
-.courseware-search-results {
-
- display: none;
- padding: 40px;
-
- .search-info {
- padding-bottom: lh(.75);
- margin-bottom: lh(.50);
- border-bottom: 1px solid $gray-l2;
- .search-count {
- float: right;
- color: $gray;
- }
- }
-
- .search-results {
- padding: 0;
- }
-
- .search-results-item {
- position: relative;
- border-bottom: 1px solid $gray-l4;
- list-style-type: none;
- margin-bottom: lh(.75);
- padding-bottom: lh(.75);
- padding-right: 140px;
-
- .sri-excerpt {
- color: $gray;
- margin-bottom: lh(1);
- }
- .sri-type {
- position: absolute;
- right: 0;
- top: 0;
- color: $gray;
- }
- .sri-link {
- position: absolute;
- right: 0;
- line-height: 1.6em;
- bottom: lh(.75);
- text-transform: uppercase;
- }
- .search-results-ellipsis {
- @extend %t-copy-sub2;
- color: $gray;
-
- &:after {
- content: '\2026';
- }
- }
- }
-
- .search-load-next {
- display: block;
- text-transform: uppercase;
- color: $base-font-color;
- border: 2px solid $link-color;
- @include border-radius(3px);
- padding: 1rem;
- .icon-spin {
- display: none;
- }
- }
-
-}
diff --git a/lms/static/sass/search/_search.scss b/lms/static/sass/search/_search.scss
new file mode 100644
index 0000000000..dd757ae95f
--- /dev/null
+++ b/lms/static/sass/search/_search.scss
@@ -0,0 +1,177 @@
+.search-bar {
+
+ @include box-sizing(border-box);
+ position: relative;
+ padding: ($baseline/4);
+
+ .search-field-wrapper {
+ position: relative;
+ }
+
+ .search-field {
+ @extend %t-weight2;
+ @include box-sizing(border-box);
+ top: 5px;
+ width: 100%;
+ border-radius: 4px;
+ background: $white-t1;
+ &.is-active {
+ background: $white;
+ }
+ }
+
+ .search-button, .cancel-button,
+ .search-button:hover, .cancel-button:hover {
+ @extend %t-icon6;
+ @extend %t-regular;
+ @include box-sizing(border-box);
+ @include right(12px);
+ display: block;
+ position: absolute;
+ top: 0;
+ border: none;
+ background: transparent;
+ padding: 0;
+ height: 35px;
+ color: $gray-l1;
+ box-shadow: none;
+ line-height: 35px;
+ text-shadow: none;
+ text-transform: none;
+ }
+
+ .cancel-button {
+ display: none;
+ }
+
+}
+
+.search-results {
+
+ display: none;
+
+ .search-info {
+ border-bottom: 4px solid $border-color-l4;
+ padding-bottom: $baseline;
+ .search-count {
+ @include float(right);
+ color: $gray-l1;
+ }
+ }
+
+ .search-result-list {
+ margin: 0;
+ padding: 0;
+ }
+
+ .search-results-item {
+ @include padding-right(140px);
+ position: relative;
+ border-bottom: 1px solid $gray-l4;
+ padding: $baseline ($baseline/2);
+ list-style-type: none;
+ cursor: pointer;
+
+ &:hover {
+ background: $gray-l6;
+ }
+
+ .result-excerpt {
+ margin-bottom: $baseline;
+ }
+ .result-type {
+ @include right($baseline/2);
+ position: absolute;
+ bottom: $baseline;
+ font-size: 14px;
+ color: $gray-l1;
+ }
+ .result-course-name {
+ @include margin-right(1em);
+ font-size: 14px;
+ color: $gray-l1;
+ }
+ .result-location {
+ font-size: 14px;
+ color: $gray-l1;
+ }
+ .result-link {
+ @include right($baseline/2);
+ position: absolute;
+ top: $baseline;
+ line-height: 1.6em;
+ text-transform: uppercase;
+ }
+ .search-results-ellipsis {
+ @extend %t-copy-sub2;
+ color: $gray;
+
+ &:after {
+ content: '\2026';
+ }
+ }
+ }
+
+ .search-load-next {
+ display: block;
+ border: 2px solid $link-color;
+ padding: 1rem;
+ border-radius: 3px;
+ color: $base-font-color;
+ text-transform: uppercase;
+ }
+
+}
+
+
+.courseware-search-bar {
+ box-shadow: 0 1px 0 $white inset, 0 -1px 0 $shadow-l1 inset;
+}
+
+
+.dashboard-search-bar {
+ @include float(right);
+ @include margin-left(flex-gutter());
+ margin-bottom: $baseline;
+ padding: 0;
+ width: flex-grid(3);
+ label {
+ @extend %t-regular;
+ font-family: $sans-serif;
+ color: $gray;
+ font-size: 13px;
+ font-style: normal;
+ text-transform: uppercase;
+ }
+ .search-field {
+ background: $white;
+ box-shadow: 0 1px 0 0 $white, inset 0 0 3px 0 $shadow-l2;
+ font-family: $sans-serif;
+ font-style: normal;
+ }
+}
+
+.dashboard-search-results {
+ @include float(left);
+ margin: 0;
+ padding: 0;
+ width: flex-grid(9);
+ min-height: 300px;
+ .search-info {
+ padding-bottom: lh(1.75);
+ a {
+ display: block;
+ margin-bottom: lh(.5);
+ font-size: 13px;
+ }
+ h2 {
+ @extend %t-title5;
+ @include float(left);
+ @include clear(left);
+ }
+ }
+}
+
+.courseware-search-results {
+ padding: ($baseline*2);
+}
diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html
index 1c081a959a..db1936dfc1 100644
--- a/lms/templates/courseware/courseware.html
+++ b/lms/templates/courseware/courseware.html
@@ -24,9 +24,9 @@ ${page_title_breadcrumbs(course_name())}
% endfor
-% for template_name in ["search_item", "search_item_seq", "search_list", "search_loading", "search_error"]:
+% for template_name in ["course_search_item", "course_search_results", "search_loading", "search_error"]:
% endfor
@@ -149,16 +149,18 @@ ${fragment.foot_html()}
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
-
+
% endif
@@ -198,7 +200,7 @@ ${fragment.foot_html()}
${fragment.body_html()}
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
-
diff --git a/lms/templates/courseware_search/search_item.underscore b/lms/templates/courseware_search/search_item.underscore
deleted file mode 100644
index 77efc34d2c..0000000000
--- a/lms/templates/courseware_search/search_item.underscore
+++ /dev/null
@@ -1,4 +0,0 @@
-<%= excerpt %>
-<%- content_type %>
-<%- location.join(' ▸ ') %>
-<%= gettext("View") %>
diff --git a/lms/templates/courseware_search/search_item_seq.underscore b/lms/templates/courseware_search/search_item_seq.underscore
deleted file mode 100644
index 5261d89206..0000000000
--- a/lms/templates/courseware_search/search_item_seq.underscore
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-<%- location.join(' ▸ ') %>
-<%= gettext("View") %>
diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html
index b33502fbfe..8a311a5655 100644
--- a/lms/templates/dashboard.html
+++ b/lms/templates/dashboard.html
@@ -27,6 +27,12 @@
<%static:include path="dashboard/${template_name}.underscore" />
% endfor
+
+% for template_name in ["dashboard_search_item", "dashboard_search_results", "search_loading", "search_error"]:
+
+% endfor
%block>
<%block name="js_extra">
@@ -69,8 +75,8 @@
-
-
+
+
% if len(course_enrollment_pairs) > 0:
<% share_settings = settings.FEATURES.get('DASHBOARD_SHARE_SETTINGS', {}) %>
@@ -126,7 +132,32 @@
% endif
-