Merge pull request #7522 from edx/feature/dashboard-search
Feature/dashboard search
This commit is contained in:
40
common/test/acceptance/pages/lms/dashboard_search.py
Normal file
40
common/test/acceptance/pages/lms/dashboard_search.py
Normal file
@@ -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()
|
||||
@@ -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):
|
||||
|
||||
193
common/test/acceptance/tests/lms/test_lms_dashboard_search.py
Normal file
193
common/test/acceptance/tests/lms/test_lms_dashboard_search.py
Normal file
@@ -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
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
22
lms/lib/courseware_search/lms_filter_generator.py
Normal file
22
lms/lib/courseware_search/lms_filter_generator.py
Normal file
@@ -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
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
0
lms/lib/courseware_search/test/__init__.py
Normal file
0
lms/lib/courseware_search/test/__init__.py
Normal file
72
lms/lib/courseware_search/test/test_lms_filter_generator.py
Normal file
72
lms/lib/courseware_search/test/test_lms_filter_generator.py
Normal file
@@ -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']))
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
12
lms/static/js/fixtures/search/course_search_form.html
Normal file
12
lms/static/js/fixtures/search/course_search_form.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<div id="courseware-search-bar" class="search-bar" role="search" aria-label="Course">
|
||||
<form>
|
||||
<label for="course-search-input" class="sr">Course Search</label>
|
||||
<input id="course-search-input" type="text" class="search-field"/>
|
||||
<button type="submit" class="search-button">
|
||||
search <i class="icon fa fa-search" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="cancel-button" aria-label="Clear search">
|
||||
<i class="icon fa fa-remove" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
14
lms/static/js/fixtures/search/dashboard_search_form.html
Normal file
14
lms/static/js/fixtures/search/dashboard_search_form.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<div id="dashboard-search-bar" class="search-bar" role="search" aria-label="Dashboard">
|
||||
<form>
|
||||
<label for="dashboard-search-input">Search Your Courses</label>
|
||||
<div>
|
||||
<input id="dashboard-search-input" type="text" class="search-field"/>
|
||||
<button type="submit" class="search-button" aria-label="Search">
|
||||
<i class="icon fa fa-search" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="cancel-button" aria-label="Clear search">
|
||||
<i class="icon fa fa-remove" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1,7 +0,0 @@
|
||||
<div id="courseware-search-bar" class="search-container">
|
||||
<form role="search-form">
|
||||
<input type="text" class="search-field"/>
|
||||
<button type="submit" class="search-button" aria-label="Search">search <i class="icon-search"></i></button>
|
||||
<button type="button" class="cancel-button" aria-label="Cancel"><i class="icon-remove"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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');
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
});
|
||||
22
lms/static/js/search/course/main.js
Normal file
22
lms/static/js/search/course/main.js
Normal file
@@ -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();
|
||||
|
||||
});
|
||||
50
lms/static/js/search/course/search_app.js
Normal file
50
lms/static/js/search/course/search_app.js
Normal file
@@ -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);
|
||||
14
lms/static/js/search/course/views/search_form.js
Normal file
14
lms/static/js/search/course/views/search_form.js
Normal file
@@ -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);
|
||||
14
lms/static/js/search/course/views/search_item_view.js
Normal file
14
lms/static/js/search/course/views/search_item_view.js
Normal file
@@ -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);
|
||||
26
lms/static/js/search/course/views/search_results_view.js
Normal file
26
lms/static/js/search/course/views/search_results_view.js
Normal file
@@ -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);
|
||||
19
lms/static/js/search/dashboard/main.js
Normal file
19
lms/static/js/search/dashboard/main.js
Normal file
@@ -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();
|
||||
|
||||
});
|
||||
54
lms/static/js/search/dashboard/search_app.js
Normal file
54
lms/static/js/search/dashboard/search_app.js
Normal file
@@ -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);
|
||||
14
lms/static/js/search/dashboard/views/search_form.js
Normal file
14
lms/static/js/search/dashboard/views/search_form.js
Normal file
@@ -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);
|
||||
14
lms/static/js/search/dashboard/views/search_item_view.js
Normal file
14
lms/static/js/search/dashboard/views/search_item_view.js
Normal file
@@ -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);
|
||||
33
lms/static/js/search/dashboard/views/search_results_view.js
Normal file
33
lms/static/js/search/dashboard/views/search_results_view.js
Normal file
@@ -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);
|
||||
@@ -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();
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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(
|
||||
'<section id="courseware-search-results" data-course-name="Test Course"></section>' +
|
||||
'<section id="course-content"></section>'
|
||||
);
|
||||
|
||||
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(
|
||||
'<section id="courseware-search-results"></section>' +
|
||||
'<section id="course-content"></section>' +
|
||||
'<section id="dashboard-search-results"></section>' +
|
||||
'<section id="my-courses"></section>'
|
||||
);
|
||||
|
||||
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(
|
||||
'<section id="courseware-search-results" data-course-name="Test Course"></section>' +
|
||||
'<section id="course-content"></section>'
|
||||
);
|
||||
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(
|
||||
'<section id="courseware-search-results"></section>' +
|
||||
'<section id="course-content"></section>'
|
||||
);
|
||||
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(
|
||||
'<section id="dashboard-search-results"></section>' +
|
||||
'<section id="my-courses"></section>'
|
||||
);
|
||||
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();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
177
lms/static/sass/search/_search.scss
Normal file
177
lms/static/sass/search/_search.scss
Normal file
@@ -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);
|
||||
}
|
||||
@@ -24,9 +24,9 @@ ${page_title_breadcrumbs(course_name())}
|
||||
</script>
|
||||
% 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"]:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="courseware_search/${template_name}.underscore" />
|
||||
<%static:include path="search/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
|
||||
@@ -149,16 +149,18 @@ ${fragment.foot_html()}
|
||||
</header>
|
||||
|
||||
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
|
||||
<div id="courseware-search-bar" class="courseware-search-bar" role="search" aria-label="Course">
|
||||
<div id="courseware-search-bar" class="search-bar courseware-search-bar" role="search" aria-label="Course">
|
||||
<form>
|
||||
<label for="course-search-input" class="sr">${_('Course Search')}</label>
|
||||
<input id="course-search-input" type="text" class="search-field"/>
|
||||
<button type="submit" class="search-button">
|
||||
${_('search')} <i class="icon fa fa-search" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="cancel-button" aria-label="${_('Clear search')}">
|
||||
<i class="icon fa fa-remove" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="search-field-wrapper">
|
||||
<input id="course-search-input" type="text" class="search-field"/>
|
||||
<button type="submit" class="search-button">
|
||||
${_('search')} <i class="icon fa fa-search" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="cancel-button" aria-label="${_('Clear search')}">
|
||||
<i class="icon fa fa-remove" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
% endif
|
||||
@@ -198,7 +200,7 @@ ${fragment.foot_html()}
|
||||
${fragment.body_html()}
|
||||
</section>
|
||||
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
|
||||
<section class="courseware-search-results" id="courseware-search-results" data-course-id="${course.id}" data-course-name="${course.display_name_with_default}">
|
||||
<section id="courseware-search-results" class="search-results courseware-search-results" data-course-id="${course.id}">
|
||||
</section>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<div class='sri-excerpt'><%= excerpt %></div>
|
||||
<span class='sri-type'><%- content_type %></span>
|
||||
<span class='sri-location'><%- location.join(' ▸ ') %></span>
|
||||
<a class="sri-link" href="<%- url %>"><%= gettext("View") %> <i class="icon fa fa-arrow-right"></i></a>
|
||||
@@ -1,4 +0,0 @@
|
||||
<div class='sri-excerpt'></div>
|
||||
<span class='sri-type'></span>
|
||||
<span class='sri-location'><%- location.join(' ▸ ') %></span>
|
||||
<a class="sri-link" href="<%- url %>"><%= gettext("View") %> <i class="icon fa fa-arrow-right"></i></a>
|
||||
@@ -27,6 +27,12 @@
|
||||
<%static:include path="dashboard/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
|
||||
% for template_name in ["dashboard_search_item", "dashboard_search_results", "search_loading", "search_error"]:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="search/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
</%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
@@ -69,8 +75,8 @@
|
||||
<header class="wrapper-header-courses">
|
||||
<h2 class="header-courses">${_("Current Courses")}</h2>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
|
||||
% if len(course_enrollment_pairs) > 0:
|
||||
<ul class="listing-courses">
|
||||
<% share_settings = settings.FEATURES.get('DASHBOARD_SHARE_SETTINGS', {}) %>
|
||||
@@ -126,7 +132,32 @@
|
||||
</div>
|
||||
% endif
|
||||
</section>
|
||||
<section class="profile-sidebar" role="region" aria-label="User info">
|
||||
|
||||
% if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'):
|
||||
<div id="dashboard-search-bar" class="search-bar dashboard-search-bar" role="search" aria-label="Dashboard">
|
||||
<form>
|
||||
<label for="dashboard-search-input">${_('Search Your Courses')}</label>
|
||||
<div class="search-field-wrapper">
|
||||
<input id="dashboard-search-input" type="text" class="search-field"/>
|
||||
<button type="submit" class="search-button" aria-label="${_('Search')}">
|
||||
<i class="icon fa fa-search" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="cancel-button" aria-label="${_('Clear search')}">
|
||||
<i class="icon fa fa-remove" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
% if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'):
|
||||
<section id="dashboard-search-results" class="search-results dashboard-search-results"></section>
|
||||
% endif
|
||||
|
||||
<section class="profile-sidebar" id="profile-sidebar" role="region" aria-label="User info">
|
||||
<header class="profile">
|
||||
<h2 class="username-header"><span class="sr">${_("Username")}: </span><span class="username-label">${ user.username }</span></h2>
|
||||
</header>
|
||||
<section class="user-info">
|
||||
<ul>
|
||||
<li class="heads-up">
|
||||
|
||||
4
lms/templates/search/course_search_item.underscore
Normal file
4
lms/templates/search/course_search_item.underscore
Normal file
@@ -0,0 +1,4 @@
|
||||
<div class="result-excerpt"><%= excerpt %></div>
|
||||
<a class="result-link" href="<%- url %>"><%= gettext("View") %> <i class="icon fa fa-arrow-right"></i></a>
|
||||
<span class="result-location"><%- location.join(' ▸ ') %></span>
|
||||
<span class="result-type"><%- content_type %></span>
|
||||
@@ -5,10 +5,10 @@
|
||||
|
||||
<% if (totalCount > 0 ) { %>
|
||||
|
||||
<ol class='search-results'></ol>
|
||||
<ol class='search-result-list'></ol>
|
||||
|
||||
<% if (hasMoreResults) { %>
|
||||
<a class="search-load-next" href="javascript:void(0);">
|
||||
<a class="search-load-next" href="#">
|
||||
<%= interpolate(
|
||||
ngettext("Load next %(num_items)s result", "Load next %(num_items)s results", pageSize),
|
||||
{ num_items: pageSize },
|
||||
5
lms/templates/search/dashboard_search_item.underscore
Normal file
5
lms/templates/search/dashboard_search_item.underscore
Normal file
@@ -0,0 +1,5 @@
|
||||
<div class="result-excerpt"><%= excerpt %></div>
|
||||
<a class="result-link" href="<%- url %>"><%= gettext("View") %> <i class="icon fa fa-arrow-right"></i></a>
|
||||
<span class="result-course-name"><%- course_name %>:</span>
|
||||
<span class="result-location"><%- location.join(' ▸ ') %></span>
|
||||
<span class="result-type"><%- content_type %></span>
|
||||
26
lms/templates/search/dashboard_search_results.underscore
Normal file
26
lms/templates/search/dashboard_search_results.underscore
Normal file
@@ -0,0 +1,26 @@
|
||||
<header class="search-info">
|
||||
<a class="search-back-to-courses" href="#"><%= gettext("Back to Dashboard") %></a>
|
||||
<h2><%= gettext("Search Results") %></h2>
|
||||
<div class="search-count"><%= totalCountMsg %></div>
|
||||
</header>
|
||||
|
||||
<% if (totalCount > 0 ) { %>
|
||||
|
||||
<ol class='search-result-list'></ol>
|
||||
|
||||
<% if (hasMoreResults) { %>
|
||||
<a class="search-load-next" href="#">
|
||||
<%= interpolate(
|
||||
ngettext("Load next %(num_items)s result", "Load next %(num_items)s results", pageSize),
|
||||
{ num_items: pageSize },
|
||||
true
|
||||
) %>
|
||||
<i class="icon fa fa-spinner fa-spin"></i>
|
||||
</a>
|
||||
<% } %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<p><%= gettext("Sorry, no results were found.") %></p>
|
||||
|
||||
<% } %>
|
||||
Reference in New Issue
Block a user