Commit c0007a11 by Andy Armstrong Committed by Diana Huang

Add new course search results page

LEARNER-1112
parent 0096c80a
...@@ -46,6 +46,14 @@ class CourseHomePage(CoursePage): ...@@ -46,6 +46,14 @@ class CourseHomePage(CoursePage):
courseware_page = CoursewarePage(self.browser, self.course_id) courseware_page = CoursewarePage(self.browser, self.course_id)
courseware_page.wait_for_page() courseware_page.wait_for_page()
def search_for_term(self, search_term):
"""
Search within a class for a particular term.
"""
self.q(css='.search-form > .search-input').fill(search_term)
self.q(css='.search-form > .search-button').click()
return CourseSearchResultsPage(self.browser, self.course_id)
class CourseOutlinePage(PageObject): class CourseOutlinePage(PageObject):
""" """
...@@ -225,3 +233,22 @@ class CourseOutlinePage(PageObject): ...@@ -225,3 +233,22 @@ class CourseOutlinePage(PageObject):
promise_check_func=lambda: courseware_page.nav.is_on_section(section_title, subsection_title), promise_check_func=lambda: courseware_page.nav.is_on_section(section_title, subsection_title),
description="Waiting for course page with section '{0}' and subsection '{1}'".format(section_title, subsection_title) description="Waiting for course page with section '{0}' and subsection '{1}'".format(section_title, subsection_title)
) )
class CourseSearchResultsPage(CoursePage):
"""
Course search page
"""
# url = "courses/{course_id}/search/?query={query_string}"
def is_browser_on_page(self):
return self.q(css='.page-content > .search-results').present
def __init__(self, browser, course_id):
super(CourseSearchResultsPage, self).__init__(browser, course_id)
self.course_id = course_id
@property
def search_results(self):
return self.q(css='.search-results-item')
...@@ -18,7 +18,7 @@ class DashboardSearchPage(PageObject): ...@@ -18,7 +18,7 @@ class DashboardSearchPage(PageObject):
@property @property
def search_results(self): def search_results(self):
""" search results list showing """ """ search results list showing """
return self.q(css='#dashboard-search-results') return self.q(css='.search-results')
def is_browser_on_page(self): def is_browser_on_page(self):
""" did we find the search bar in the UI """ """ did we find the search bar in the UI """
......
...@@ -285,7 +285,7 @@ class DiscussionNavigationTest(BaseDiscussionTestCase): ...@@ -285,7 +285,7 @@ class DiscussionNavigationTest(BaseDiscussionTestCase):
def test_breadcrumbs_clear_search(self): def test_breadcrumbs_clear_search(self):
self.thread_page.q(css=".search-input").fill("search text") self.thread_page.q(css=".search-input").fill("search text")
self.thread_page.q(css=".search-btn").click() self.thread_page.q(css=".search-button").click()
# Verify that clicking the first breadcrumb clears your search # Verify that clicking the first breadcrumb clears your search
self.thread_page.q(css=".breadcrumbs .nav-item")[0].click() self.thread_page.q(css=".breadcrumbs .nav-item")[0].click()
......
...@@ -10,7 +10,7 @@ from nose.plugins.attrib import attr ...@@ -10,7 +10,7 @@ from nose.plugins.attrib import attr
from common.test.acceptance.fixtures.course import XBlockFixtureDesc from common.test.acceptance.fixtures.course import XBlockFixtureDesc
from common.test.acceptance.pages.common.auto_auth import AutoAuthPage from common.test.acceptance.pages.common.auto_auth import AutoAuthPage
from common.test.acceptance.pages.common.logout import LogoutPage from common.test.acceptance.pages.common.logout import LogoutPage
from common.test.acceptance.pages.lms.courseware_search import CoursewareSearchPage from common.test.acceptance.pages.lms.course_home import CourseHomePage
from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage
from common.test.acceptance.pages.lms.staff_view import StaffCoursewarePage from common.test.acceptance.pages.lms.staff_view import StaffCoursewarePage
from common.test.acceptance.pages.studio.component_editor import ComponentVisibilityEditorView from common.test.acceptance.pages.studio.component_editor import ComponentVisibilityEditorView
...@@ -73,7 +73,7 @@ class CoursewareSearchCohortTest(ContainerBase, CohortTestMixin): ...@@ -73,7 +73,7 @@ class CoursewareSearchCohortTest(ContainerBase, CohortTestMixin):
email=self.cohort_default_student_email, no_login=True email=self.cohort_default_student_email, no_login=True
).visit() ).visit()
self.courseware_search_page = CoursewareSearchPage(self.browser, self.course_id) self.course_home_page = CourseHomePage(self.browser, self.course_id)
# Enable Cohorting and assign cohorts and content groups # Enable Cohorting and assign cohorts and content groups
self._auto_auth(self.staff_user["username"], self.staff_user["email"], True) self._auto_auth(self.staff_user["username"], self.staff_user["email"], True)
...@@ -105,11 +105,21 @@ class CoursewareSearchCohortTest(ContainerBase, CohortTestMixin): ...@@ -105,11 +105,21 @@ class CoursewareSearchCohortTest(ContainerBase, CohortTestMixin):
""" """
Open staff page with assertion Open staff page with assertion
""" """
self.courseware_search_page.visit() self.course_home_page.visit()
self.course_home_page.resume_course_from_header()
staff_page = StaffCoursewarePage(self.browser, self.course_id) staff_page = StaffCoursewarePage(self.browser, self.course_id)
self.assertEqual(staff_page.staff_view_mode, 'Staff') self.assertEqual(staff_page.staff_view_mode, 'Staff')
return staff_page return staff_page
def _search_for_term(self, term):
"""
Search for term in course and return results.
"""
self.course_home_page.visit()
course_search_results_page = self.course_home_page.search_for_term(term)
results = course_search_results_page.search_results.html
return results[0] if len(results) > 0 else []
def populate_course_fixture(self, course_fixture): def populate_course_fixture(self, course_fixture):
""" """
Populate the children of the test course fixture. Populate the children of the test course fixture.
...@@ -195,48 +205,21 @@ class CoursewareSearchCohortTest(ContainerBase, CohortTestMixin): ...@@ -195,48 +205,21 @@ class CoursewareSearchCohortTest(ContainerBase, CohortTestMixin):
add_cohort_with_student("Cohort B", self.content_group_b, self.cohort_b_student_username) add_cohort_with_student("Cohort B", self.content_group_b, self.cohort_b_student_username)
cohort_management_page.wait_for_ajax() cohort_management_page.wait_for_ajax()
def test_page_existence(self):
"""
Make sure that the page is accessible.
"""
self._auto_auth(self.cohort_default_student_username, self.cohort_default_student_email, False)
self.courseware_search_page.visit()
def test_cohorted_search_user_a_a_content(self): def test_cohorted_search_user_a_a_content(self):
""" """
Test user can search content restricted to his cohort. Test user can search content restricted to his cohort.
""" """
self._auto_auth(self.cohort_a_student_username, self.cohort_a_student_email, False) self._auto_auth(self.cohort_a_student_username, self.cohort_a_student_email, False)
self.courseware_search_page.visit() search_results = self._search_for_term(self.group_a_html)
self.courseware_search_page.search_for_term(self.group_a_html) assert self.group_a_html in search_results
assert self.group_a_html in self.courseware_search_page.search_results.html[0]
def test_cohorted_search_user_b_a_content(self): def test_cohorted_search_user_b_a_content(self):
""" """
Test user can not search content restricted to his cohort. Test user can not search content restricted to his cohort.
""" """
self._auto_auth(self.cohort_b_student_username, self.cohort_b_student_email, False) self._auto_auth(self.cohort_b_student_username, self.cohort_b_student_email, False)
self.courseware_search_page.visit() search_results = self._search_for_term(self.group_a_html)
self.courseware_search_page.search_for_term(self.group_a_html) assert self.group_a_html not in search_results
assert self.group_a_html not in self.courseware_search_page.search_results.html[0]
def test_cohorted_search_user_default_ab_content(self):
"""
Test user not enrolled in any cohorts can't see any of restricted content.
"""
self._auto_auth(self.cohort_default_student_username, self.cohort_default_student_email, False)
self.courseware_search_page.visit()
self.courseware_search_page.search_for_term(self.group_a_and_b_html)
assert self.group_a_and_b_html not in self.courseware_search_page.search_results.html[0]
def test_cohorted_search_user_default_all_content(self):
"""
Test user can search public content if cohorts used on course.
"""
self._auto_auth(self.cohort_default_student_username, self.cohort_default_student_email, False)
self.courseware_search_page.visit()
self.courseware_search_page.search_for_term(self.visible_to_all_html)
assert self.visible_to_all_html in self.courseware_search_page.search_results.html[0]
def test_cohorted_search_user_staff_all_content(self): def test_cohorted_search_user_staff_all_content(self):
""" """
...@@ -244,17 +227,14 @@ class CoursewareSearchCohortTest(ContainerBase, CohortTestMixin): ...@@ -244,17 +227,14 @@ class CoursewareSearchCohortTest(ContainerBase, CohortTestMixin):
""" """
self._auto_auth(self.staff_user["username"], self.staff_user["email"], False) self._auto_auth(self.staff_user["username"], self.staff_user["email"], False)
self._goto_staff_page().set_staff_view_mode('Staff') self._goto_staff_page().set_staff_view_mode('Staff')
self.courseware_search_page.search_for_term(self.visible_to_all_html) search_results = self._search_for_term(self.visible_to_all_html)
assert self.visible_to_all_html in self.courseware_search_page.search_results.html[0] assert self.visible_to_all_html in search_results
self.courseware_search_page.clear_search() search_results = self._search_for_term(self.group_a_and_b_html)
self.courseware_search_page.search_for_term(self.group_a_and_b_html) assert self.group_a_and_b_html in search_results
assert self.group_a_and_b_html in self.courseware_search_page.search_results.html[0] search_results = self._search_for_term(self.group_a_html)
self.courseware_search_page.clear_search() assert self.group_a_html in search_results
self.courseware_search_page.search_for_term(self.group_a_html) search_results = self._search_for_term(self.group_b_html)
assert self.group_a_html in self.courseware_search_page.search_results.html[0] assert self.group_b_html in search_results
self.courseware_search_page.clear_search()
self.courseware_search_page.search_for_term(self.group_b_html)
assert self.group_b_html in self.courseware_search_page.search_results.html[0]
def test_cohorted_search_user_staff_masquerade_student_content(self): def test_cohorted_search_user_staff_masquerade_student_content(self):
""" """
...@@ -262,17 +242,14 @@ class CoursewareSearchCohortTest(ContainerBase, CohortTestMixin): ...@@ -262,17 +242,14 @@ class CoursewareSearchCohortTest(ContainerBase, CohortTestMixin):
""" """
self._auto_auth(self.staff_user["username"], self.staff_user["email"], False) self._auto_auth(self.staff_user["username"], self.staff_user["email"], False)
self._goto_staff_page().set_staff_view_mode('Learner') self._goto_staff_page().set_staff_view_mode('Learner')
self.courseware_search_page.search_for_term(self.visible_to_all_html) search_results = self._search_for_term(self.visible_to_all_html)
assert self.visible_to_all_html in self.courseware_search_page.search_results.html[0] assert self.visible_to_all_html in search_results
self.courseware_search_page.clear_search() search_results = self._search_for_term(self.group_a_and_b_html)
self.courseware_search_page.search_for_term(self.group_a_and_b_html) assert self.group_a_and_b_html not in search_results
assert self.group_a_and_b_html not in self.courseware_search_page.search_results.html[0] search_results = self._search_for_term(self.group_a_html)
self.courseware_search_page.clear_search() assert self.group_a_html not in search_results
self.courseware_search_page.search_for_term(self.group_a_html) search_results = self._search_for_term(self.group_b_html)
assert self.group_a_html not in self.courseware_search_page.search_results.html[0] assert self.group_b_html not in search_results
self.courseware_search_page.clear_search()
self.courseware_search_page.search_for_term(self.group_b_html)
assert self.group_b_html not in self.courseware_search_page.search_results.html[0]
def test_cohorted_search_user_staff_masquerade_cohort_content(self): def test_cohorted_search_user_staff_masquerade_cohort_content(self):
""" """
...@@ -280,14 +257,11 @@ class CoursewareSearchCohortTest(ContainerBase, CohortTestMixin): ...@@ -280,14 +257,11 @@ class CoursewareSearchCohortTest(ContainerBase, CohortTestMixin):
""" """
self._auto_auth(self.staff_user["username"], self.staff_user["email"], False) self._auto_auth(self.staff_user["username"], self.staff_user["email"], False)
self._goto_staff_page().set_staff_view_mode('Learner in ' + self.content_group_a) self._goto_staff_page().set_staff_view_mode('Learner in ' + self.content_group_a)
self.courseware_search_page.search_for_term(self.visible_to_all_html) search_results = self._search_for_term(self.visible_to_all_html)
assert self.visible_to_all_html in self.courseware_search_page.search_results.html[0] assert self.visible_to_all_html in search_results
self.courseware_search_page.clear_search() search_results = self._search_for_term(self.group_a_and_b_html)
self.courseware_search_page.search_for_term(self.group_a_and_b_html) assert self.group_a_and_b_html in search_results
assert self.group_a_and_b_html in self.courseware_search_page.search_results.html[0] search_results = self._search_for_term(self.group_a_html)
self.courseware_search_page.clear_search() assert self.group_a_html in search_results
self.courseware_search_page.search_for_term(self.group_a_html) search_results = self._search_for_term(self.group_b_html)
assert self.group_a_html in self.courseware_search_page.search_results.html[0] assert self.group_b_html not in search_results
self.courseware_search_page.clear_search()
self.courseware_search_page.search_for_term(self.group_b_html)
assert self.group_b_html not in self.courseware_search_page.search_results.html[0]
...@@ -7,7 +7,7 @@ from nose.plugins.attrib import attr ...@@ -7,7 +7,7 @@ from nose.plugins.attrib import attr
from ...fixtures.course import CourseFixture, XBlockFixtureDesc from ...fixtures.course import CourseFixture, XBlockFixtureDesc
from ...pages.lms.bookmarks import BookmarksPage from ...pages.lms.bookmarks import BookmarksPage
from ...pages.lms.course_home import CourseHomePage from ...pages.lms.course_home import CourseHomePage, CourseSearchResultsPage
from ...pages.lms.courseware import CoursewarePage from ...pages.lms.courseware import CoursewarePage
from ..helpers import UniqueCourseTest, auto_auth, load_data_str from ..helpers import UniqueCourseTest, auto_auth, load_data_str
...@@ -134,3 +134,12 @@ class CourseHomeA11yTest(CourseHomeBaseTest): ...@@ -134,3 +134,12 @@ class CourseHomeA11yTest(CourseHomeBaseTest):
course_home_page = CourseHomePage(self.browser, self.course_id) course_home_page = CourseHomePage(self.browser, self.course_id)
course_home_page.visit() course_home_page.visit()
course_home_page.a11y_audit.check_for_accessibility_errors() course_home_page.a11y_audit.check_for_accessibility_errors()
def test_course_search_a11y(self):
"""
Test the accessibility of the search results page.
"""
course_home_page = CourseHomePage(self.browser, self.course_id)
course_home_page.visit()
course_search_results_page = course_home_page.search_for_term("Test Search")
course_search_results_page.a11y_audit.check_for_accessibility_errors()
...@@ -10,6 +10,7 @@ from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureD ...@@ -10,6 +10,7 @@ from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureD
from common.test.acceptance.pages.common.auto_auth import AutoAuthPage from common.test.acceptance.pages.common.auto_auth import AutoAuthPage
from common.test.acceptance.pages.common.logout import LogoutPage from common.test.acceptance.pages.common.logout import LogoutPage
from common.test.acceptance.pages.common.utils import click_css from common.test.acceptance.pages.common.utils import click_css
from common.test.acceptance.pages.lms.course_home import CourseHomePage
from common.test.acceptance.pages.lms.courseware_search import CoursewareSearchPage from common.test.acceptance.pages.lms.courseware_search import CoursewareSearchPage
from common.test.acceptance.pages.studio.container import ContainerPage from common.test.acceptance.pages.studio.container import ContainerPage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
...@@ -52,7 +53,8 @@ class CoursewareSearchTest(UniqueCourseTest): ...@@ -52,7 +53,8 @@ class CoursewareSearchTest(UniqueCourseTest):
self.addCleanup(remove_file, self.TEST_INDEX_FILENAME) self.addCleanup(remove_file, self.TEST_INDEX_FILENAME)
super(CoursewareSearchTest, self).setUp() super(CoursewareSearchTest, self).setUp()
self.courseware_search_page = CoursewareSearchPage(self.browser, self.course_id)
self.course_home_page = CourseHomePage(self.browser, self.course_id)
self.studio_course_outline = StudioCourseOutlinePage( self.studio_course_outline = StudioCourseOutlinePage(
self.browser, self.browser,
...@@ -149,17 +151,31 @@ class CoursewareSearchTest(UniqueCourseTest): ...@@ -149,17 +151,31 @@ class CoursewareSearchTest(UniqueCourseTest):
(bool) True if search term is found in resulting content; False if not found (bool) True if search term is found in resulting content; False if not found
""" """
self._auto_auth(self.USERNAME, self.EMAIL, False) self._auto_auth(self.USERNAME, self.EMAIL, False)
self.course_home_page.visit()
course_search_results_page = self.course_home_page.search_for_term(search_term)
if len(course_search_results_page.search_results.html) > 0:
search_string = course_search_results_page.search_results.html[0]
else:
search_string = ""
return search_term in search_string
# TODO: TNL-6546: Remove usages of sidebar search
def _search_for_content_in_sidebar(self, search_term, perform_auto_auth=True):
"""
Login and search for specific content in the legacy sidebar search
Arguments:
search_term - term to be searched for
perform_auto_auth - if False, skip auto_auth call.
Returns:
(bool) True if search term is found in resulting content; False if not found
"""
if perform_auto_auth:
self._auto_auth(self.USERNAME, self.EMAIL, False)
self.courseware_search_page = CoursewareSearchPage(self.browser, self.course_id)
self.courseware_search_page.visit() self.courseware_search_page.visit()
self.courseware_search_page.search_for_term(search_term) self.courseware_search_page.search_for_term(search_term)
return search_term in self.courseware_search_page.search_results.html[0] return search_term in self.courseware_search_page.search_results.html[0]
def test_page_existence(self):
"""
Make sure that the page is accessible.
"""
self._auto_auth(self.USERNAME, self.EMAIL, False)
self.courseware_search_page.visit()
def test_search(self): def test_search(self):
""" """
Make sure that you can search for something. Make sure that you can search for something.
...@@ -171,12 +187,18 @@ class CoursewareSearchTest(UniqueCourseTest): ...@@ -171,12 +187,18 @@ class CoursewareSearchTest(UniqueCourseTest):
# Do a search, there should be no results shown. # Do a search, there should be no results shown.
self.assertFalse(self._search_for_content(self.SEARCH_STRING)) self.assertFalse(self._search_for_content(self.SEARCH_STRING))
# Do a search in the legacy sidebar, there should be no results shown.
self.assertFalse(self._search_for_content_in_sidebar(self.SEARCH_STRING, False))
# Publish in studio to trigger indexing. # Publish in studio to trigger indexing.
self._studio_publish_content(0) self._studio_publish_content(0)
# Do the search again, this time we expect results. # Do the search again, this time we expect results.
self.assertTrue(self._search_for_content(self.SEARCH_STRING)) self.assertTrue(self._search_for_content(self.SEARCH_STRING))
# Do the search again in the legacy sidebar, this time we expect results.
self.assertTrue(self._search_for_content_in_sidebar(self.SEARCH_STRING, False))
@flaky # TNL-5771 @flaky # TNL-5771
def test_reindex(self): def test_reindex(self):
""" """
......
...@@ -9,7 +9,7 @@ from nose.plugins.attrib import attr ...@@ -9,7 +9,7 @@ from nose.plugins.attrib import attr
from common.test.acceptance.fixtures.course import XBlockFixtureDesc from common.test.acceptance.fixtures.course import XBlockFixtureDesc
from common.test.acceptance.pages.common.auto_auth import AutoAuthPage from common.test.acceptance.pages.common.auto_auth import AutoAuthPage
from common.test.acceptance.pages.common.logout import LogoutPage from common.test.acceptance.pages.common.logout import LogoutPage
from common.test.acceptance.pages.lms.courseware_search import CoursewareSearchPage from common.test.acceptance.pages.lms.course_home import CourseHomePage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
from common.test.acceptance.tests.helpers import create_user_partition_json, remove_file from common.test.acceptance.tests.helpers import create_user_partition_json, remove_file
from common.test.acceptance.tests.studio.base_studio_test import ContainerBase from common.test.acceptance.tests.studio.base_studio_test import ContainerBase
...@@ -38,7 +38,7 @@ class SplitTestCoursewareSearchTest(ContainerBase): ...@@ -38,7 +38,7 @@ class SplitTestCoursewareSearchTest(ContainerBase):
super(SplitTestCoursewareSearchTest, self).setUp(is_staff=is_staff) super(SplitTestCoursewareSearchTest, self).setUp(is_staff=is_staff)
self.staff_user = self.user self.staff_user = self.user
self.courseware_search_page = CoursewareSearchPage(self.browser, self.course_id) self.course_home_page = CourseHomePage(self.browser, self.course_id)
self.studio_course_outline = StudioCourseOutlinePage( self.studio_course_outline = StudioCourseOutlinePage(
self.browser, self.browser,
self.course_info['org'], self.course_info['org'],
...@@ -112,19 +112,12 @@ class SplitTestCoursewareSearchTest(ContainerBase): ...@@ -112,19 +112,12 @@ class SplitTestCoursewareSearchTest(ContainerBase):
) )
) )
def test_page_existence(self):
"""
Make sure that the page is accessible.
"""
self._auto_auth(self.USERNAME, self.EMAIL, False)
self.courseware_search_page.visit()
def test_search_for_experiment_content_user_assigned_to_one_group(self): def test_search_for_experiment_content_user_assigned_to_one_group(self):
""" """
Test user can search for experiment content restricted to his group Test user can search for experiment content restricted to his group
when assigned to just one experiment group when assigned to just one experiment group
""" """
self._auto_auth(self.USERNAME, self.EMAIL, False) self._auto_auth(self.USERNAME, self.EMAIL, False)
self.courseware_search_page.visit() self.course_home_page.visit()
self.courseware_search_page.search_for_term("VISIBLE TO") course_search_results_page = self.course_home_page.search_for_term("VISIBLE TO")
assert "1 result" in self.courseware_search_page.search_results.html[0] assert "result-excerpt" in course_search_results_page.search_results.html[0]
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
discussionBoardView.render(); discussionBoardView.render();
threadListView = discussionBoardView.discussionThreadListView; threadListView = discussionBoardView.discussionThreadListView;
spyOn(threadListView, 'performSearch'); spyOn(threadListView, 'performSearch');
discussionBoardView.$el.find('.search-btn').click(); discussionBoardView.$el.find('.search-button').click();
expect(threadListView.performSearch).toHaveBeenCalled(); expect(threadListView.performSearch).toHaveBeenCalled();
}); });
}); });
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
'keydown .forum-nav-browse-filter-input': 'keyboardBinding', 'keydown .forum-nav-browse-filter-input': 'keyboardBinding',
'click .forum-nav-browse-menu-wrapper': 'ignoreClick', 'click .forum-nav-browse-menu-wrapper': 'ignoreClick',
'keydown .search-input': 'performSearch', 'keydown .search-input': 'performSearch',
'click .search-btn': 'performSearch', 'click .search-button': 'performSearch',
'topic:selected': 'clearSearch' 'topic:selected': 'clearSearch'
}, },
......
...@@ -6,4 +6,4 @@ ...@@ -6,4 +6,4 @@
id="search" id="search"
placeholder="<%- gettext("Search all posts") %>" placeholder="<%- gettext("Search all posts") %>"
/> />
<button class="btn btn-small search-btn" type="button"><%- gettext("Search") %></button> <button class="btn btn-small search-button" type="button"><%- gettext("Search") %></button>
...@@ -676,7 +676,7 @@ ...@@ -676,7 +676,7 @@
'course_bookmarks/js/spec/bookmark_button_view_spec.js', 'course_bookmarks/js/spec/bookmark_button_view_spec.js',
'course_bookmarks/js/spec/bookmarks_list_view_spec.js', 'course_bookmarks/js/spec/bookmarks_list_view_spec.js',
'course_bookmarks/js/spec/course_bookmarks_factory_spec.js', 'course_bookmarks/js/spec/course_bookmarks_factory_spec.js',
'course_search/js/spec/search_spec.js', 'course_search/js/spec/course_search_spec.js',
'discussion/js/spec/discussion_board_factory_spec.js', 'discussion/js/spec/discussion_board_factory_spec.js',
'discussion/js/spec/discussion_profile_page_factory_spec.js', 'discussion/js/spec/discussion_profile_page_factory_spec.js',
'discussion/js/spec/discussion_board_view_spec.js', 'discussion/js/spec/discussion_board_view_spec.js',
......
...@@ -26,3 +26,4 @@ ...@@ -26,3 +26,4 @@
// Features // Features
@import 'features/bookmarks'; @import 'features/bookmarks';
@import 'features/course-experience'; @import 'features/course-experience';
@import 'features/course-search';
// Styles for course search results
.search-results {
.search-result-list {
list-style: none;
margin: 0;
padding: 0;
clear: both;
}
.search-results-title {
display: inline-block;
color: black;
font-size: 1.5rem;
line-height: 1.5;
}
.search-count {
@include float(right);
color: $lms-gray;
}
.search-results-item {
@include padding-right(140px);
position: relative;
border-top: 1px solid $border-color;
padding: $baseline ($baseline/2);
list-style-type: none;
cursor: pointer;
&:last-child {
border-bottom: 1px solid $border-color;
}
&:hover {
background: $lms-background-color;
}
}
.result-excerpt {
display: inline-block;
}
.result-location {
display: block;
color: $lms-gray;
font-size: 14px;
padding-top: ($baseline/2);
}
.result-link {
@include right($baseline/2);
position: absolute;
top: $baseline;
line-height: 1.6em;
}
.result-type {
@include right($baseline/2);
position: absolute;
color: $lms-gray;
font-size: 14px;
bottom: $baseline;
}
.search-load-next {
display: block;
margin-top: $baseline;
}
}
...@@ -62,12 +62,8 @@ ...@@ -62,12 +62,8 @@
vertical-align: middle; vertical-align: middle;
} }
.search-field { .search-input {
transition: all $tmg-f2 ease-in-out; width: 12rem;
border: 1px solid $lms-border-color;
border-radius: 3px;
padding: $baseline/4 $baseline*1.5;
font-family: inherit;
} }
.action-search { .action-search {
......
...@@ -40,6 +40,7 @@ ...@@ -40,6 +40,7 @@
vertical-align: text-bottom; vertical-align: text-bottom;
.form-actions { .form-actions {
@include margin-left($baseline/2);
display: inline-block; display: inline-block;
} }
......
...@@ -75,7 +75,10 @@ from openedx.features.course_experience import course_home_page_title, UNIFIED_C ...@@ -75,7 +75,10 @@ from openedx.features.course_experience import course_home_page_title, UNIFIED_C
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'): % if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
<%static:require_module module_name="course_search/js/course_search_factory" class_name="CourseSearchFactory"> <%static:require_module module_name="course_search/js/course_search_factory" class_name="CourseSearchFactory">
var courseId = $('.courseware-results').data('courseId'); var courseId = $('.courseware-results').data('courseId');
CourseSearchFactory(courseId); CourseSearchFactory({
courseId: courseId,
searchHeader: $('.search-bar')
});
</%static:require_module> </%static:require_module>
% endif % endif
...@@ -119,7 +122,7 @@ ${HTML(fragment.foot_html())} ...@@ -119,7 +122,7 @@ ${HTML(fragment.foot_html())}
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'): % if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
<div id="courseware-search-bar" class="search-bar 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> <form class="search-form">
<label for="course-search-input" class="sr">${_('Course Search')}</label> <label for="course-search-input" class="sr">${_('Course Search')}</label>
<div class="search-field-wrapper"> <div class="search-field-wrapper">
<input id="course-search-input" type="text" class="search-field"/> <input id="course-search-input" type="text" class="search-field"/>
......
...@@ -43,7 +43,7 @@ from openedx.core.djangolib.markup import HTML, Text ...@@ -43,7 +43,7 @@ from openedx.core.djangolib.markup import HTML, Text
}); });
</script> </script>
% if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'): % if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'):
<%static:require_module module_name="course_search/js/dashboard/dashboard_search_factory" class_name="DashboardSearchFactory"> <%static:require_module module_name="course_search/js/dashboard_search_factory" class_name="DashboardSearchFactory">
DashboardSearchFactory(); DashboardSearchFactory();
</%static:require_module> </%static:require_module>
% endif % endif
...@@ -159,7 +159,7 @@ from openedx.core.djangolib.markup import HTML, Text ...@@ -159,7 +159,7 @@ from openedx.core.djangolib.markup import HTML, Text
% if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'): % if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'):
<div id="dashboard-search-bar" class="search-bar dashboard-search-bar" role="search" aria-label="Dashboard"> <div id="dashboard-search-bar" class="search-bar dashboard-search-bar" role="search" aria-label="Dashboard">
<form> <form class="search-form">
<label for="dashboard-search-input">${_('Search Your Courses')}</label> <label for="dashboard-search-input">${_('Search Your Courses')}</label>
<div class="search-field-wrapper"> <div class="search-field-wrapper">
<input id="dashboard-search-input" type="text" class="search-field"/> <input id="dashboard-search-input" type="text" class="search-field"/>
......
...@@ -56,7 +56,10 @@ ...@@ -56,7 +56,10 @@
id="search" id="search"
placeholder="Search all the things" placeholder="Search all the things"
/> />
<button class="btn btn-small search-btn" type="button">Search</button> <button type="button" class="action action-clear" aria-label="Clear search">
<span class="icon fa fa-times-circle" aria-hidden="true"></span>
</button>
<button class="btn btn-small search-button" type="button">Search</button>
</form> </form>
</div> </div>
</div> </div>
......
...@@ -636,6 +636,14 @@ urlpatterns += ( ...@@ -636,6 +636,14 @@ urlpatterns += (
), ),
include('openedx.features.course_bookmarks.urls'), include('openedx.features.course_bookmarks.urls'),
), ),
# Course search
url(
r'^courses/{}/search/'.format(
settings.COURSE_ID_PATTERN,
),
include('openedx.features.course_search.urls'),
),
) )
if settings.FEATURES["ENABLE_TEAMS"]: if settings.FEATURES["ENABLE_TEAMS"]:
......
...@@ -28,16 +28,16 @@ from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG ...@@ -28,16 +28,16 @@ from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG
<div class="page-header-secondary"> <div class="page-header-secondary">
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'): % if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
<div class="page-header-search"> <div class="page-header-search">
<form class="search-form" role="search"> <form class="search-form" role="search" action="${reverse('openedx.course_search.course_search_results', args=[course_key])}">
<label class="field-label sr-only" for="search" id="search-hint">${_('Search the course')}</label> <label class="field-label sr-only" for="search" id="search-hint">${_('Search the course')}</label>
<input <input
class="field-input input-text search-input" class="field-input input-text search-input"
type="search" type="search"
name="search" name="query"
id="search" id="search"
placeholder="${_('Search the course')}" placeholder="${_('Search the course')}"
/> />
<button class="btn btn-small search-btn" type="button">${_('Search')}</button> <button class="btn btn-small search-button" type="submit">${_('Search')}</button>
</form> </form>
</div> </div>
% endif % endif
......
...@@ -113,7 +113,6 @@ class CourseHomeFragmentView(EdxFragmentView): ...@@ -113,7 +113,6 @@ class CourseHomeFragmentView(EdxFragmentView):
'csrf': csrf(request)['csrf_token'], 'csrf': csrf(request)['csrf_token'],
'course': course, 'course': course,
'course_key': course_key, 'course_key': course_key,
'course': course,
'outline_fragment': outline_fragment, 'outline_fragment': outline_fragment,
'handouts_html': handouts_html, 'handouts_html': handouts_html,
'has_visited_course': has_visited_course, 'has_visited_course': has_visited_course,
......
<div id="courseware-search-bar" class="search-bar" role="search" aria-label="Course"> <div class="course-view container" id="course-container">
<form> <div id="courseware-search-bar" class="search-bar" role="search" aria-label="Course">
<label for="course-search-input" class="sr">Course Search</label> <form class="search-form">
<input id="course-search-input" type="text" class="search-field"/> <label for="course-search-input" class="sr">Course Search</label>
<button type="submit" class="search-button">Search</button> <input id="course-search-input" type="text" class="search-field"/>
<button type="button" class="cancel-button" title="Clear search"> <button type="submit" class="search-button">Search</button>
<span class="icon fa fa-remove" aria-hidden="true"></span> <button type="button" class="cancel-button" title="Clear search">
</button> <span class="icon fa fa-remove" aria-hidden="true"></span>
</form> </button>
</form>
</div>
<div class="search-results courseware-results">
<section id="course-content"></section>
</div> </div>
<div class="course-view container" id="course-container">
<header class="page-header has-secondary">
<div class="page-header-main">
<nav aria-label="My Bookmarks" class="sr-is-focusable" tabindex="-1">
<div class="has-breadcrumbs">
<div class="breadcrumbs">
<span class="nav-item">
<a href="/course">Course</a>
</span>
<span class="icon fa fa-angle-right" aria-hidden="true"></span>
<span class="nav-item">Search Results</span>
</div>
</div>
</nav>
</div>
<div class="page-header-secondary">
<div class="page-header-search">
<form class="search-form" role="search" action="/search">
<label class="field-label sr-only" for="search" id="search-hint">Search the course</label>
<input
class="field-input input-text search-field"
type="search"
name="query"
id="search"
value="query"
placeholder="Search the course"
/>
<button class="btn btn-small search-button" type="submit">Search</button>
</form>
</div>
</div>
</header>
<div class="page-content">
<main class="search-results">
</main>
</div>
</div>
<div id="dashboard-search-bar" class="search-bar" role="search" aria-label="Dashboard"> <div class="course-view container" id="course-container">
<form> <div id="dashboard-search-bar" class="search-bar" role="search" aria-label="Dashboard">
<label for="dashboard-search-input">Search Your Courses</label> <form class="search-form">
<div> <label for="dashboard-search-input">Search Your Courses</label>
<input id="dashboard-search-input" type="text" class="search-field"/> <div>
<button type="submit" class="search-button" aria-label="Search"> <input id="dashboard-search-input" type="text" class="search-field"/>
<span class="icon fa fa-search" aria-hidden="true"></span> <button type="submit" class="search-button" aria-label="Search">
</button> <span class="icon fa fa-search" aria-hidden="true"></span>
<button type="button" class="cancel-button" aria-label="Clear search"> </button>
<span class="icon fa fa-remove" aria-hidden="true"></span> <button type="button" class="cancel-button" aria-label="Clear search">
</button> <span class="icon fa fa-remove" aria-hidden="true"></span>
</div> </button>
</form> </div>
</form>
</div>
<section id="dashboard-search-results" class="search-results dashboard-search-results"></section>
<section id="my-courses" tabindex="-1"></section>
</div> </div>
...@@ -6,9 +6,15 @@ ...@@ -6,9 +6,15 @@
'course_search/js/collections/search_collection', 'course_search/js/views/course_search_results_view' 'course_search/js/collections/search_collection', 'course_search/js/views/course_search_results_view'
], ],
function(_, Backbone, SearchRouter, CourseSearchForm, SearchCollection, CourseSearchResultsView) { function(_, Backbone, SearchRouter, CourseSearchForm, SearchCollection, CourseSearchResultsView) {
return function(courseId) { return function(options) {
var courseId = options.courseId;
var requestedQuery = options.query;
var supportsActive = options.supportsActive;
var router = new SearchRouter(); var router = new SearchRouter();
var form = new CourseSearchForm(); var form = new CourseSearchForm({
el: options.searchHeader,
supportsActive: supportsActive
});
var collection = new SearchCollection([], {courseId: courseId}); var collection = new SearchCollection([], {courseId: courseId});
var results = new CourseSearchResultsView({collection: collection}); var results = new CourseSearchResultsView({collection: collection});
var dispatcher = _.clone(Backbone.Events); var dispatcher = _.clone(Backbone.Events);
...@@ -44,6 +50,11 @@ ...@@ -44,6 +50,11 @@
dispatcher.listenTo(collection, 'error', function() { dispatcher.listenTo(collection, 'error', function() {
results.showErrorMessage(); results.showErrorMessage();
}); });
// Perform a search if an initial query has been provided.
if (requestedQuery) {
router.trigger('search', requestedQuery);
}
}; };
}); });
}(define || RequireJS.define)); }(define || RequireJS.define));
...@@ -5,14 +5,14 @@ ...@@ -5,14 +5,14 @@
'underscore', 'backbone', 'course_search/js/search_router', 'course_search/js/views/search_form', 'underscore', 'backbone', 'course_search/js/search_router', 'course_search/js/views/search_form',
'course_search/js/collections/search_collection', 'course_search/js/views/dashboard_search_results_view' 'course_search/js/collections/search_collection', 'course_search/js/views/dashboard_search_results_view'
], ],
function(_, Backbone, SearchRouter, SearchForm, SearchCollection, SearchListView) { function(_, Backbone, SearchRouter, SearchForm, SearchCollection, DashboardSearchResultsView) {
return function() { return function() {
var router = new SearchRouter(); var router = new SearchRouter();
var form = new SearchForm({ var form = new SearchForm({
el: $('#dashboard-search-bar') el: $('#dashboard-search-bar')
}); });
var collection = new SearchCollection([]); var collection = new SearchCollection([]);
var results = new SearchListView({collection: collection}); var results = new DashboardSearchResultsView({collection: collection});
var dispatcher = _.clone(Backbone.Events); var dispatcher = _.clone(Backbone.Events);
dispatcher.listenTo(router, 'search', function(query) { dispatcher.listenTo(router, 'search', function(query) {
......
...@@ -35,7 +35,7 @@ define([ ...@@ -35,7 +35,7 @@ define([
) { ) {
'use strict'; 'use strict';
describe('Search', function() { describe('Course Search', function() {
beforeEach(function() { beforeEach(function() {
PageHelpers.preventBackboneChangingUrl(); PageHelpers.preventBackboneChangingUrl();
}); });
...@@ -330,9 +330,9 @@ define([ ...@@ -330,9 +330,9 @@ define([
describe('SearchForm', function() { describe('SearchForm', function() {
beforeEach(function() { beforeEach(function() {
loadFixtures('course_search/fixtures/course_search_form.html'); loadFixtures('course_search/fixtures/course_content_page.html');
this.form = new SearchForm({ this.form = new SearchForm({
el: '#courseware-search-bar' el: '.search-bar'
}); });
this.onClear = jasmine.createSpy('onClear'); this.onClear = jasmine.createSpy('onClear');
this.onSearch = jasmine.createSpy('onSearch'); this.onSearch = jasmine.createSpy('onSearch');
...@@ -450,25 +450,19 @@ define([ ...@@ -450,25 +450,19 @@ define([
} }
}); });
appendSetFixtures(
'<div class="courseware-results"></div>' +
'<section id="course-content"></section>' +
'<section id="dashboard-search-results"></section>' +
'<section id="my-courses" tabindex="-1"></section>'
);
this.collection = new MockCollection(); this.collection = new MockCollection();
this.resultsView = new SearchResultsView({collection: this.collection}); this.resultsView = new SearchResultsView({collection: this.collection});
} }
describe('CourseSearchResultsView', function() { describe('CourseSearchResultsView', function() {
beforeEach(function() { beforeEach(function() {
loadFixtures('course_search/fixtures/course_content_page.html');
beforeEachHelper.call(this, CourseSearchResultsView); beforeEachHelper.call(this, CourseSearchResultsView);
this.contentElementDisplayValue = 'table-cell'; this.contentElementDisplayValue = 'table-cell';
}); });
it('shows loading message', showsLoadingMessage); it('shows loading message', showsLoadingMessage);
it('shows error message', showsErrorMessage); it('shows error message', showsErrorMessage);
it('returns to content', returnsToContent); xit('returns to content', returnsToContent);
it('shows a message when there are no results', showsNoResultsMessage); it('shows a message when there are no results', showsNoResultsMessage);
it('renders search results', rendersSearchResults); it('renders search results', rendersSearchResults);
it('shows a link to load more results', showsMoreResultsLink); it('shows a link to load more results', showsMoreResultsLink);
...@@ -478,6 +472,7 @@ define([ ...@@ -478,6 +472,7 @@ define([
describe('DashSearchResultsView', function() { describe('DashSearchResultsView', function() {
beforeEach(function() { beforeEach(function() {
loadFixtures('course_search/fixtures/dashboard_search_page.html');
beforeEachHelper.call(this, DashSearchResultsView); beforeEachHelper.call(this, DashSearchResultsView);
this.contentElementDisplayValue = 'block'; this.contentElementDisplayValue = 'block';
}); });
...@@ -507,7 +502,9 @@ define([ ...@@ -507,7 +502,9 @@ define([
function showsLoadingMessage() { function showsLoadingMessage() {
$('.search-field').val('search string'); $('.search-field').val('search string');
$('.search-button').trigger('click'); $('.search-button').trigger('click');
expect(this.$contentElement).toBeHidden(); if (this.$contentElement) {
expect(this.$contentElement).toBeHidden();
}
expect(this.$searchResults).toBeVisible(); expect(this.$searchResults).toBeVisible();
expect(this.$searchResults).not.toBeEmpty(); expect(this.$searchResults).not.toBeEmpty();
} }
...@@ -608,16 +605,15 @@ define([ ...@@ -608,16 +605,15 @@ define([
describe('CourseSearchApp', function() { describe('CourseSearchApp', function() {
beforeEach(function() { beforeEach(function() {
var courseId = 'a/b/c'; var courseId = 'a/b/c';
loadFixtures('course_search/fixtures/course_search_form.html'); loadFixtures('course_search/fixtures/course_content_page.html');
appendSetFixtures( CourseSearchFactory({
'<div class="courseware-results"></div>' + courseId: courseId,
'<section id="course-content"></section>' searchHeader: $('.search-bar')
); });
CourseSearchFactory(courseId);
spyOn(Backbone.history, 'navigate'); spyOn(Backbone.history, 'navigate');
this.$contentElement = $('#course-content'); this.$contentElement = $('#course-content');
this.contentElementDisplayValue = 'table-cell'; this.contentElementDisplayValue = 'table-cell';
this.$searchResults = $('.courseware-results'); this.$searchResults = $('.search-results');
}); });
afterEach(function() { afterEach(function() {
...@@ -628,25 +624,21 @@ define([ ...@@ -628,25 +624,21 @@ define([
it('performs search', performsSearch); it('performs search', performsSearch);
it('shows an error message', showsErrorMessage); it('shows an error message', showsErrorMessage);
it('updates navigation history', updatesNavigationHistory); it('updates navigation history', updatesNavigationHistory);
it('cancels search request', cancelsSearchRequest); xit('cancels search request', cancelsSearchRequest);
it('clears results', clearsResults); xit('clears results', clearsResults);
it('loads next page', loadsNextPage); it('loads next page', loadsNextPage);
it('navigates to search', navigatesToSearch); it('navigates to search', navigatesToSearch);
}); });
describe('DashSearchApp', function() { describe('DashboardSearchApp', function() {
beforeEach(function() { beforeEach(function() {
loadFixtures('course_search/fixtures/dashboard_search_form.html'); loadFixtures('course_search/fixtures/dashboard_search_page.html');
appendSetFixtures(
'<section id="dashboard-search-results"></section>' +
'<section id="my-courses" tabindex="-1"></section>'
);
DashboardSearchFactory(); DashboardSearchFactory();
spyOn(Backbone.history, 'navigate'); spyOn(Backbone.history, 'navigate');
this.$contentElement = $('#my-courses'); this.$contentElement = $('#my-courses');
this.contentElementDisplayValue = 'block'; this.contentElementDisplayValue = 'block';
this.$searchResults = $('#dashboard-search-results'); this.$searchResults = $('.search-results');
}); });
afterEach(function() { afterEach(function() {
...@@ -685,6 +677,30 @@ define([ ...@@ -685,6 +677,30 @@ define([
expect(this.$searchResults).toBeEmpty(); expect(this.$searchResults).toBeEmpty();
}); });
}); });
describe('Course Search Results Page', function() {
beforeEach(function() {
var courseId = 'a/b/c';
loadFixtures('course_search/fixtures/course_search_results_page.html');
CourseSearchFactory({
courseId: courseId,
searchHeader: $('.page-header-search')
});
spyOn(Backbone.history, 'navigate');
this.$contentElement = null; // The search results page does not show over a content element
this.contentElementDisplayValue = 'table-cell';
this.$searchResults = $('.search-results');
});
afterEach(function() {
Backbone.history.stop();
});
it('shows loading message on search', showsLoadingMessage);
it('performs search', performsSearch);
it('shows an error message', showsErrorMessage);
it('loads next page', loadsNextPage);
});
}); });
}); });
}); });
...@@ -11,8 +11,7 @@ ...@@ -11,8 +11,7 @@
courseSearchItemTemplate courseSearchItemTemplate
) { ) {
return SearchResultsView.extend({ return SearchResultsView.extend({
el: '.search-results',
el: '.courseware-results',
contentElement: '#course-content', contentElement: '#course-content',
coursewareResultsWrapperElement: '.courseware-results-wrapper', coursewareResultsWrapperElement: '.courseware-results-wrapper',
resultsTemplate: courseSearchResultsTemplate, resultsTemplate: courseSearchResultsTemplate,
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
dashboardSearchItemTemplate dashboardSearchItemTemplate
) { ) {
return SearchResultsView.extend({ return SearchResultsView.extend({
el: '#dashboard-search-results', el: '.search-results',
contentElement: '#my-courses, #profile-sidebar', contentElement: '#my-courses, #profile-sidebar',
resultsTemplate: dashboardSearchResultsTemplate, resultsTemplate: dashboardSearchResultsTemplate,
itemTemplate: dashboardSearchItemTemplate, itemTemplate: dashboardSearchItemTemplate,
......
...@@ -6,14 +6,15 @@ ...@@ -6,14 +6,15 @@
el: '', el: '',
events: { events: {
'submit form': 'submitForm', 'submit .search-form': 'submitForm',
'click .cancel-button': 'clearSearch' 'click .cancel-button': 'clearSearch'
}, },
initialize: function() { initialize: function(options) {
this.$searchField = this.$el.find('.search-field'); this.$searchField = this.$el.find('.search-field');
this.$searchButton = this.$el.find('.search-button'); this.$searchButton = this.$el.find('.search-button');
this.$cancelButton = this.$el.find('.cancel-button'); this.$cancelButton = this.$el.find('.cancel-button');
this.supportsActive = options.supportsActive === undefined ? true : options.supportsActive;
}, },
submitForm: function(event) { submitForm: function(event) {
...@@ -22,16 +23,16 @@ ...@@ -22,16 +23,16 @@
}, },
doSearch: function(term) { doSearch: function(term) {
var trimmed; var trimmedTerm;
if (term) { if (term) {
trimmed = term.trim(); trimmedTerm = term.trim();
this.$searchField.val(trimmed); this.$searchField.val(trimmedTerm);
} else { } else {
trimmed = this.$searchField.val().trim(); trimmedTerm = this.$searchField.val().trim();
} }
if (trimmed) { if (trimmedTerm) {
this.setActiveStyle(); this.setActiveStyle();
this.trigger('search', trimmed); this.trigger('search', trimmedTerm);
} else { } else {
this.clearSearch(); this.clearSearch();
} }
...@@ -48,17 +49,20 @@ ...@@ -48,17 +49,20 @@
}, },
setActiveStyle: function() { setActiveStyle: function() {
this.$searchField.addClass('is-active'); if (this.supportsActive) {
this.$searchButton.hide(); this.$searchField.addClass('is-active');
this.$cancelButton.show(); this.$searchButton.hide();
this.$cancelButton.show();
}
}, },
setInitialStyle: function() { setInitialStyle: function() {
this.$searchField.removeClass('is-active'); if (this.supportsActive) {
this.$searchButton.show(); this.$searchField.removeClass('is-active');
this.$cancelButton.hide(); this.$searchButton.show();
this.$cancelButton.hide();
}
} }
}); });
}); });
}(define || RequireJS.define)); }(define || RequireJS.define));
...@@ -35,6 +35,8 @@ ...@@ -35,6 +35,8 @@
data.excerpt = ''; data.excerpt = '';
data.content_type = ''; data.content_type = '';
} }
data.excerptHtml = HtmlUtils.HTML(data.excerpt);
delete data.excerpt;
HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.template)(data)); HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.template)(data));
return this; return this;
}, },
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
spinner: '.search-load-next .icon', spinner: '.search-load-next .icon',
initialize: function() { initialize: function() {
this.$contentElement = $(this.contentElement); this.$contentElement = this.contentElement ? $(this.contentElement) : $([]);
}, },
render: function() { render: function() {
...@@ -59,6 +59,7 @@ ...@@ -59,6 +59,7 @@
}); });
return item.render().el; return item.render().el;
}, this); }, this);
// safe-lint: disable=javascript-jquery-append
this.$el.find('ol').append(items); this.$el.find('ol').append(items);
}, },
...@@ -80,8 +81,14 @@ ...@@ -80,8 +81,14 @@
}, },
showLoadingMessage: function() { showLoadingMessage: function() {
this.doCleanup(); // Empty any previous loading/error message
$('#loading-message').html('');
$('#error-message').html('');
// Show the loading message
HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.loadingTemplate)()); HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.loadingTemplate)());
// Show the results
this.showResults(); this.showResults();
}, },
...@@ -90,15 +97,6 @@ ...@@ -90,15 +97,6 @@
this.showResults(); this.showResults();
}, },
doCleanup: function() {
// Empty any loading/error message and empty the el
// Bookmarks share the same container element, So we are doing
// this to ensure that elements are in clean/initial state
$('#loading-message').html('');
$('#error-message').html('');
this.$el.html('');
},
loadNext: function(event) { loadNext: function(event) {
if (event) { if (event) {
event.preventDefault(); event.preventDefault();
......
<div class="result-excerpt"><%= excerpt %></div> <div class="result-excerpt"><%= HtmlUtils.ensureHtml(excerptHtml) %></div>
<a class="result-link" href="<%- url %>"><%= gettext("View") %> <span class="icon fa fa-arrow-right" aria-hidden="true"></span></a> <a class="result-link" href="<%- url %>"><%- gettext("View") %> <span class="icon fa fa-arrow-right" aria-hidden="true"></span></a>
<span class="result-location"><%- location.join(' ▸ ') %></span> <span class="result-location"><%- location.join(' ▸ ') %></span>
<span class="result-type"><%- content_type %></span> <span class="result-type"><%- content_type %></span>
<div class="search-info"> <div class="search-info">
<%= gettext("Search Results") %> <h2 class="search-results-title"><%- gettext("Search Results") %></h2>
<div class="search-count"><%= totalCountMsg %></div> <div class="search-count"><%- totalCountMsg %></div>
</div> </div>
<% if (totalCount > 0 ) { %> <% if (totalCount > 0 ) { %>
...@@ -9,17 +9,20 @@ ...@@ -9,17 +9,20 @@
<% if (hasMoreResults) { %> <% if (hasMoreResults) { %>
<a class="search-load-next" href="#"> <a class="search-load-next" href="#">
<%= interpolate( <%-
ngettext("Load next %(num_items)s result", "Load next %(num_items)s results", pageSize), StringUtils.interpolate(
{ num_items: pageSize }, ngettext("Load next {num_items} result", "Load next {num_items} results", pageSize),
true {
) %> num_items: pageSize
}
)
%>
<span class="icon fa fa-spinner fa-spin" aria-hidden="true"></span> <span class="icon fa fa-spinner fa-spin" aria-hidden="true"></span>
</a> </a>
<% } %> <% } %>
<% } else { %> <% } else { %>
<p><%= gettext("Sorry, no results were found.") %></p> <p><%- gettext("Sorry, no results were found.") %></p>
<% } %> <% } %>
<div class="result-excerpt"><%= excerpt %></div> <div class="result-excerpt"><%= HtmlUtils.ensureHtml(excerptHtml) %></div>
<a class="result-link" href="<%- url %>"><%= gettext("View") %> <span class="icon fa fa-arrow-right" aria-hidden="true"></span></a> <a class="result-link" href="<%- url %>"><%- gettext("View") %> <span class="icon fa fa-arrow-right" aria-hidden="true"></span></a>
<span class="result-course-name"><%- course_name %>:</span> <span class="result-course-name"><%- course_name %>:</span>
<span class="result-location"><%- location.join(' ▸ ') %></span> <span class="result-location"><%- location.join(' ▸ ') %></span>
<span class="result-type"><%- content_type %></span> <span class="result-type"><%- content_type %></span>
<header class="search-info"> <header class="search-info">
<a class="search-back-to-courses" href="#"><%= gettext("Back to Dashboard") %></a> <a class="search-back-to-courses" href="#"><%- gettext("Back to Dashboard") %></a>
<h2><%= gettext("Search Results") %></h2> <h2><%- gettext("Search Results") %></h2>
<div class="search-count"><%= totalCountMsg %></div> <div class="search-count"><%- totalCountMsg %></div>
</header> </header>
<% if (totalCount > 0 ) { %> <% if (totalCount > 0 ) { %>
...@@ -10,17 +10,20 @@ ...@@ -10,17 +10,20 @@
<% if (hasMoreResults) { %> <% if (hasMoreResults) { %>
<a class="search-load-next" href="#"> <a class="search-load-next" href="#">
<%= interpolate( <%-
ngettext("Load next %(num_items)s result", "Load next %(num_items)s results", pageSize), StringUtils.interpolate(
{ num_items: pageSize }, ngettext("Load next {num_items} result", "Load next {num_items} results", pageSize),
true {
) %> num_items: pageSize
}
)
%>
<span class="icon fa fa-spinner fa-spin" aria-hidden="true"></span> <span class="icon fa fa-spinner fa-spin" aria-hidden="true"></span>
</a> </a>
<% } %> <% } %>
<% } else { %> <% } else { %>
<p><%= gettext("Sorry, no results were found.") %></p> <p><%- gettext("Sorry, no results were found.") %></p>
<% } %> <% } %>
<span class="icon fa fa-spinner fa-spin" aria-hidden="true"></span> <%= gettext("Loading") %> <span class="icon fa fa-spinner fa-spin" aria-hidden="true"></span> <%- gettext("Loading") %>
## mako
<%page expression_filter="h"/>
<%namespace name='static' file='../static_content.html'/>
<%!
import json
import waffle
from django.conf import settings
from django.utils.translation import ugettext as _
from django.template.defaultfilters import escapejs
from django.core.urlresolvers import reverse
from django_comment_client.permissions import has_permission
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
from openedx.core.djangolib.markup import HTML
from openedx.features.course_experience import course_home_page_title
%>
<%block name="content">
<div class="course-view container" id="course-container">
<header class="page-header has-secondary">
<div class="page-header-main">
<nav aria-label="${_('My Bookmarks')}" class="sr-is-focusable" tabindex="-1">
<div class="has-breadcrumbs">
<div class="breadcrumbs">
<span class="nav-item">
<a href="${course_url}">${course_home_page_title(course)}</a>
</span>
<span class="icon fa fa-angle-right" aria-hidden="true"></span>
<span class="nav-item">${_('Search Results')}</span>
</div>
</div>
</nav>
</div>
<div class="page-header-secondary">
<div class="page-header-search">
<form class="search-form" role="search">
<label class="field-label sr-only" for="search" id="search-hint">${_('Search the course')}</label>
<input
class="field-input input-text search-field"
type="search"
name="query"
id="search"
value="${query}"
placeholder="${_('Search the course')}"
/>
<button class="btn btn-small search-button" type="submit">${_('Search')}</button>
</form>
</div>
</div>
</header>
<div class="page-content">
<main role="main" class="search-results" id="main" tabindex="-1">
</main>
</div>
</div>
</%block>
<%block name="js_extra">
<%static:require_module module_name="course_search/js/course_search_factory" class_name="CourseSearchFactory">
var courseId = '${course_key | n, js_escaped_string}';
CourseSearchFactory({
courseId: courseId,
searchHeader: $('.page-header-search'),
supportsActive: false,
query: '${query | n, js_escaped_string}'
});
</%static:require_module>
</%block>
"""
Defines URLs for course search.
"""
from django.conf.urls import url
from views.course_search import CourseSearchView, CourseSearchFragmentView
urlpatterns = [
url(
r'^$',
CourseSearchView.as_view(),
name='openedx.course_search.course_search_results',
),
url(
r'^home_fragment$',
CourseSearchFragmentView.as_view(),
name='openedx.course_search.course_search_results_fragment_view',
),
]
"""
Views for the course search page.
"""
from django.contrib.auth.decorators import login_required
from django.core.context_processors import csrf
from django.core.urlresolvers import reverse
from django.template.loader import render_to_string
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
from django.views.decorators.csrf import ensure_csrf_cookie
from courseware.courses import get_course_overview_with_access
from lms.djangoapps.courseware.views.views import CourseTabView
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
from openedx.features.course_experience import default_course_url_name
from util.views import ensure_valid_course_key
from web_fragments.fragment import Fragment
class CourseSearchView(CourseTabView):
"""
The home page for a course.
"""
@method_decorator(login_required)
@method_decorator(ensure_csrf_cookie)
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True))
@method_decorator(ensure_valid_course_key)
def get(self, request, course_id, **kwargs):
"""
Displays the home page for the specified course.
"""
return super(CourseSearchView, self).get(request, course_id, 'courseware', **kwargs)
def render_to_fragment(self, request, course=None, tab=None, **kwargs):
course_id = unicode(course.id)
home_fragment_view = CourseSearchFragmentView()
return home_fragment_view.render_to_fragment(request, course_id=course_id, **kwargs)
class CourseSearchFragmentView(EdxFragmentView):
"""
A fragment to render the home page for a course.
"""
def render_to_fragment(self, request, course_id=None, **kwargs):
"""
Renders the course's home page as a fragment.
"""
course_key = CourseKey.from_string(course_id)
course = get_course_overview_with_access(request.user, 'load', course_key, check_if_enrolled=True)
course_url_name = default_course_url_name(request)
course_url = reverse(course_url_name, kwargs={'course_id': unicode(course.id)})
# Render the course home fragment
context = {
'csrf': csrf(request)['csrf_token'],
'course': course,
'course_key': course_key,
'course_url': course_url,
'query': request.GET.get('query', ''),
'disable_courseware_js': True,
'uses_pattern_library': True,
}
html = render_to_string('course_search/course-search-fragment.html', context)
return Fragment(html)
...@@ -158,7 +158,7 @@ from openedx.core.djangoapps.theming import helpers as theming_helpers ...@@ -158,7 +158,7 @@ from openedx.core.djangoapps.theming import helpers as theming_helpers
% if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'): % if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'):
<div id="dashboard-search-bar" class="search-bar dashboard-search-bar" role="search" aria-label="Dashboard"> <div id="dashboard-search-bar" class="search-bar dashboard-search-bar" role="search" aria-label="Dashboard">
<form> <form class="search-form">
<label for="dashboard-search-input">${_('Search Your Courses')}</label> <label for="dashboard-search-input">${_('Search Your Courses')}</label>
<div class="search-field-wrapper"> <div class="search-field-wrapper">
<input id="dashboard-search-input" type="text" class="search-field"/> <input id="dashboard-search-input" type="text" class="search-field"/>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment