Add new course search results page
LEARNER-1112
This commit is contained in:
committed by
Diana Huang
parent
0096c80a13
commit
c0007a11f3
@@ -46,6 +46,14 @@ class CourseHomePage(CoursePage):
|
||||
courseware_page = CoursewarePage(self.browser, self.course_id)
|
||||
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):
|
||||
"""
|
||||
@@ -225,3 +233,22 @@ class CourseOutlinePage(PageObject):
|
||||
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)
|
||||
)
|
||||
|
||||
|
||||
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):
|
||||
@property
|
||||
def search_results(self):
|
||||
""" search results list showing """
|
||||
return self.q(css='#dashboard-search-results')
|
||||
return self.q(css='.search-results')
|
||||
|
||||
def is_browser_on_page(self):
|
||||
""" did we find the search bar in the UI """
|
||||
|
||||
@@ -285,7 +285,7 @@ class DiscussionNavigationTest(BaseDiscussionTestCase):
|
||||
|
||||
def test_breadcrumbs_clear_search(self):
|
||||
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
|
||||
self.thread_page.q(css=".breadcrumbs .nav-item")[0].click()
|
||||
|
||||
@@ -10,7 +10,7 @@ from nose.plugins.attrib import attr
|
||||
from common.test.acceptance.fixtures.course import XBlockFixtureDesc
|
||||
from common.test.acceptance.pages.common.auto_auth import AutoAuthPage
|
||||
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.staff_view import StaffCoursewarePage
|
||||
from common.test.acceptance.pages.studio.component_editor import ComponentVisibilityEditorView
|
||||
@@ -73,7 +73,7 @@ class CoursewareSearchCohortTest(ContainerBase, CohortTestMixin):
|
||||
email=self.cohort_default_student_email, no_login=True
|
||||
).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
|
||||
self._auto_auth(self.staff_user["username"], self.staff_user["email"], True)
|
||||
@@ -105,11 +105,21 @@ class CoursewareSearchCohortTest(ContainerBase, CohortTestMixin):
|
||||
"""
|
||||
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)
|
||||
self.assertEqual(staff_page.staff_view_mode, 'Staff')
|
||||
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):
|
||||
"""
|
||||
Populate the children of the test course fixture.
|
||||
@@ -195,48 +205,21 @@ class CoursewareSearchCohortTest(ContainerBase, CohortTestMixin):
|
||||
add_cohort_with_student("Cohort B", self.content_group_b, self.cohort_b_student_username)
|
||||
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):
|
||||
"""
|
||||
Test user can search content restricted to his cohort.
|
||||
"""
|
||||
self._auto_auth(self.cohort_a_student_username, self.cohort_a_student_email, False)
|
||||
self.courseware_search_page.visit()
|
||||
self.courseware_search_page.search_for_term(self.group_a_html)
|
||||
assert self.group_a_html in self.courseware_search_page.search_results.html[0]
|
||||
search_results = self._search_for_term(self.group_a_html)
|
||||
assert self.group_a_html in search_results
|
||||
|
||||
def test_cohorted_search_user_b_a_content(self):
|
||||
"""
|
||||
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.courseware_search_page.visit()
|
||||
self.courseware_search_page.search_for_term(self.group_a_html)
|
||||
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]
|
||||
search_results = self._search_for_term(self.group_a_html)
|
||||
assert self.group_a_html not in search_results
|
||||
|
||||
def test_cohorted_search_user_staff_all_content(self):
|
||||
"""
|
||||
@@ -244,17 +227,14 @@ class CoursewareSearchCohortTest(ContainerBase, CohortTestMixin):
|
||||
"""
|
||||
self._auto_auth(self.staff_user["username"], self.staff_user["email"], False)
|
||||
self._goto_staff_page().set_staff_view_mode('Staff')
|
||||
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]
|
||||
self.courseware_search_page.clear_search()
|
||||
self.courseware_search_page.search_for_term(self.group_a_and_b_html)
|
||||
assert self.group_a_and_b_html in self.courseware_search_page.search_results.html[0]
|
||||
self.courseware_search_page.clear_search()
|
||||
self.courseware_search_page.search_for_term(self.group_a_html)
|
||||
assert self.group_a_html in self.courseware_search_page.search_results.html[0]
|
||||
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]
|
||||
search_results = self._search_for_term(self.visible_to_all_html)
|
||||
assert self.visible_to_all_html in search_results
|
||||
search_results = self._search_for_term(self.group_a_and_b_html)
|
||||
assert self.group_a_and_b_html in search_results
|
||||
search_results = self._search_for_term(self.group_a_html)
|
||||
assert self.group_a_html in search_results
|
||||
search_results = self._search_for_term(self.group_b_html)
|
||||
assert self.group_b_html in search_results
|
||||
|
||||
def test_cohorted_search_user_staff_masquerade_student_content(self):
|
||||
"""
|
||||
@@ -262,17 +242,14 @@ class CoursewareSearchCohortTest(ContainerBase, CohortTestMixin):
|
||||
"""
|
||||
self._auto_auth(self.staff_user["username"], self.staff_user["email"], False)
|
||||
self._goto_staff_page().set_staff_view_mode('Learner')
|
||||
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]
|
||||
self.courseware_search_page.clear_search()
|
||||
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]
|
||||
self.courseware_search_page.clear_search()
|
||||
self.courseware_search_page.search_for_term(self.group_a_html)
|
||||
assert self.group_a_html not in self.courseware_search_page.search_results.html[0]
|
||||
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]
|
||||
search_results = self._search_for_term(self.visible_to_all_html)
|
||||
assert self.visible_to_all_html in search_results
|
||||
search_results = self._search_for_term(self.group_a_and_b_html)
|
||||
assert self.group_a_and_b_html not in search_results
|
||||
search_results = self._search_for_term(self.group_a_html)
|
||||
assert self.group_a_html not in search_results
|
||||
search_results = self._search_for_term(self.group_b_html)
|
||||
assert self.group_b_html not in search_results
|
||||
|
||||
def test_cohorted_search_user_staff_masquerade_cohort_content(self):
|
||||
"""
|
||||
@@ -280,14 +257,11 @@ class CoursewareSearchCohortTest(ContainerBase, CohortTestMixin):
|
||||
"""
|
||||
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.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]
|
||||
self.courseware_search_page.clear_search()
|
||||
self.courseware_search_page.search_for_term(self.group_a_and_b_html)
|
||||
assert self.group_a_and_b_html in self.courseware_search_page.search_results.html[0]
|
||||
self.courseware_search_page.clear_search()
|
||||
self.courseware_search_page.search_for_term(self.group_a_html)
|
||||
assert self.group_a_html in self.courseware_search_page.search_results.html[0]
|
||||
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]
|
||||
search_results = self._search_for_term(self.visible_to_all_html)
|
||||
assert self.visible_to_all_html in search_results
|
||||
search_results = self._search_for_term(self.group_a_and_b_html)
|
||||
assert self.group_a_and_b_html in search_results
|
||||
search_results = self._search_for_term(self.group_a_html)
|
||||
assert self.group_a_html in search_results
|
||||
search_results = self._search_for_term(self.group_b_html)
|
||||
assert self.group_b_html not in search_results
|
||||
|
||||
@@ -7,7 +7,7 @@ from nose.plugins.attrib import attr
|
||||
|
||||
from ...fixtures.course import CourseFixture, XBlockFixtureDesc
|
||||
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 ..helpers import UniqueCourseTest, auto_auth, load_data_str
|
||||
|
||||
@@ -134,3 +134,12 @@ class CourseHomeA11yTest(CourseHomeBaseTest):
|
||||
course_home_page = CourseHomePage(self.browser, self.course_id)
|
||||
course_home_page.visit()
|
||||
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
|
||||
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.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.studio.container import ContainerPage
|
||||
from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
|
||||
@@ -52,7 +53,8 @@ class CoursewareSearchTest(UniqueCourseTest):
|
||||
self.addCleanup(remove_file, self.TEST_INDEX_FILENAME)
|
||||
|
||||
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.browser,
|
||||
@@ -149,17 +151,31 @@ class CoursewareSearchTest(UniqueCourseTest):
|
||||
(bool) True if search term is found in resulting content; False if not found
|
||||
"""
|
||||
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.search_for_term(search_term)
|
||||
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):
|
||||
"""
|
||||
Make sure that you can search for something.
|
||||
@@ -171,12 +187,18 @@ class CoursewareSearchTest(UniqueCourseTest):
|
||||
# Do a search, there should be no results shown.
|
||||
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.
|
||||
self._studio_publish_content(0)
|
||||
|
||||
# Do the search again, this time we expect results.
|
||||
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
|
||||
def test_reindex(self):
|
||||
"""
|
||||
|
||||
@@ -9,7 +9,7 @@ from nose.plugins.attrib import attr
|
||||
from common.test.acceptance.fixtures.course import XBlockFixtureDesc
|
||||
from common.test.acceptance.pages.common.auto_auth import AutoAuthPage
|
||||
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.tests.helpers import create_user_partition_json, remove_file
|
||||
from common.test.acceptance.tests.studio.base_studio_test import ContainerBase
|
||||
@@ -38,7 +38,7 @@ class SplitTestCoursewareSearchTest(ContainerBase):
|
||||
super(SplitTestCoursewareSearchTest, self).setUp(is_staff=is_staff)
|
||||
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.browser,
|
||||
self.course_info['org'],
|
||||
@@ -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):
|
||||
"""
|
||||
Test user can search for experiment content restricted to his group
|
||||
when assigned to just one experiment group
|
||||
"""
|
||||
self._auto_auth(self.USERNAME, self.EMAIL, False)
|
||||
self.courseware_search_page.visit()
|
||||
self.courseware_search_page.search_for_term("VISIBLE TO")
|
||||
assert "1 result" in self.courseware_search_page.search_results.html[0]
|
||||
self.course_home_page.visit()
|
||||
course_search_results_page = self.course_home_page.search_for_term("VISIBLE TO")
|
||||
assert "result-excerpt" in course_search_results_page.search_results.html[0]
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
discussionBoardView.render();
|
||||
threadListView = discussionBoardView.discussionThreadListView;
|
||||
spyOn(threadListView, 'performSearch');
|
||||
discussionBoardView.$el.find('.search-btn').click();
|
||||
discussionBoardView.$el.find('.search-button').click();
|
||||
expect(threadListView.performSearch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
'keydown .forum-nav-browse-filter-input': 'keyboardBinding',
|
||||
'click .forum-nav-browse-menu-wrapper': 'ignoreClick',
|
||||
'keydown .search-input': 'performSearch',
|
||||
'click .search-btn': 'performSearch',
|
||||
'click .search-button': 'performSearch',
|
||||
'topic:selected': 'clearSearch'
|
||||
},
|
||||
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
id="search"
|
||||
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 @@
|
||||
'course_bookmarks/js/spec/bookmark_button_view_spec.js',
|
||||
'course_bookmarks/js/spec/bookmarks_list_view_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_profile_page_factory_spec.js',
|
||||
'discussion/js/spec/discussion_board_view_spec.js',
|
||||
|
||||
@@ -26,3 +26,4 @@
|
||||
// Features
|
||||
@import 'features/bookmarks';
|
||||
@import 'features/course-experience';
|
||||
@import 'features/course-search';
|
||||
|
||||
70
lms/static/sass/features/_course-search.scss
Normal file
70
lms/static/sass/features/_course-search.scss
Normal file
@@ -0,0 +1,70 @@
|
||||
// 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 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.search-field {
|
||||
transition: all $tmg-f2 ease-in-out;
|
||||
border: 1px solid $lms-border-color;
|
||||
border-radius: 3px;
|
||||
padding: $baseline/4 $baseline*1.5;
|
||||
font-family: inherit;
|
||||
.search-input {
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.action-search {
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
vertical-align: text-bottom;
|
||||
|
||||
.form-actions {
|
||||
@include margin-left($baseline/2);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,10 @@ from openedx.features.course_experience import course_home_page_title, UNIFIED_C
|
||||
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
|
||||
<%static:require_module module_name="course_search/js/course_search_factory" class_name="CourseSearchFactory">
|
||||
var courseId = $('.courseware-results').data('courseId');
|
||||
CourseSearchFactory(courseId);
|
||||
CourseSearchFactory({
|
||||
courseId: courseId,
|
||||
searchHeader: $('.search-bar')
|
||||
});
|
||||
</%static:require_module>
|
||||
% endif
|
||||
|
||||
@@ -119,7 +122,7 @@ ${HTML(fragment.foot_html())}
|
||||
|
||||
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
|
||||
<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>
|
||||
<div class="search-field-wrapper">
|
||||
<input id="course-search-input" type="text" class="search-field"/>
|
||||
|
||||
@@ -43,7 +43,7 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
});
|
||||
</script>
|
||||
% 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();
|
||||
</%static:require_module>
|
||||
% endif
|
||||
@@ -159,7 +159,7 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
|
||||
% if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'):
|
||||
<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>
|
||||
<div class="search-field-wrapper">
|
||||
<input id="dashboard-search-input" type="text" class="search-field"/>
|
||||
|
||||
@@ -56,7 +56,10 @@
|
||||
id="search"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -636,6 +636,14 @@ urlpatterns += (
|
||||
),
|
||||
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"]:
|
||||
|
||||
@@ -28,16 +28,16 @@ from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG
|
||||
<div class="page-header-secondary">
|
||||
% if settings.FEATURES.get('ENABLE_COURSEWARE_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>
|
||||
<input
|
||||
class="field-input input-text search-input"
|
||||
type="search"
|
||||
name="search"
|
||||
name="query"
|
||||
id="search"
|
||||
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>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
@@ -113,7 +113,6 @@ class CourseHomeFragmentView(EdxFragmentView):
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'course': course,
|
||||
'course_key': course_key,
|
||||
'course': course,
|
||||
'outline_fragment': outline_fragment,
|
||||
'handouts_html': handouts_html,
|
||||
'has_visited_course': has_visited_course,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="course-view container" id="course-container">
|
||||
<div id="courseware-search-bar" class="search-bar" role="search" aria-label="Course">
|
||||
<form class="search-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</button>
|
||||
<button type="button" class="cancel-button" title="Clear search">
|
||||
<span class="icon fa fa-remove" aria-hidden="true"></span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="search-results courseware-results">
|
||||
<section id="course-content"></section>
|
||||
</div>
|
||||
@@ -1,10 +0,0 @@
|
||||
<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</button>
|
||||
<button type="button" class="cancel-button" title="Clear search">
|
||||
<span class="icon fa fa-remove" aria-hidden="true"></span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,37 @@
|
||||
<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>
|
||||
@@ -1,14 +0,0 @@
|
||||
<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">
|
||||
<span class="icon fa fa-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button type="button" class="cancel-button" aria-label="Clear search">
|
||||
<span class="icon fa fa-remove" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
<div class="course-view container" id="course-container">
|
||||
<div id="dashboard-search-bar" class="search-bar" role="search" aria-label="Dashboard">
|
||||
<form class="search-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">
|
||||
<span class="icon fa fa-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button type="button" class="cancel-button" aria-label="Clear search">
|
||||
<span class="icon fa fa-remove" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<section id="dashboard-search-results" class="search-results dashboard-search-results"></section>
|
||||
<section id="my-courses" tabindex="-1"></section>
|
||||
</div>
|
||||
@@ -6,9 +6,15 @@
|
||||
'course_search/js/collections/search_collection', 'course_search/js/views/course_search_results_view'
|
||||
],
|
||||
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 form = new CourseSearchForm();
|
||||
var form = new CourseSearchForm({
|
||||
el: options.searchHeader,
|
||||
supportsActive: supportsActive
|
||||
});
|
||||
var collection = new SearchCollection([], {courseId: courseId});
|
||||
var results = new CourseSearchResultsView({collection: collection});
|
||||
var dispatcher = _.clone(Backbone.Events);
|
||||
@@ -44,6 +50,11 @@
|
||||
dispatcher.listenTo(collection, 'error', function() {
|
||||
results.showErrorMessage();
|
||||
});
|
||||
|
||||
// Perform a search if an initial query has been provided.
|
||||
if (requestedQuery) {
|
||||
router.trigger('search', requestedQuery);
|
||||
}
|
||||
};
|
||||
});
|
||||
}(define || RequireJS.define));
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
'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'
|
||||
],
|
||||
function(_, Backbone, SearchRouter, SearchForm, SearchCollection, SearchListView) {
|
||||
function(_, Backbone, SearchRouter, SearchForm, SearchCollection, DashboardSearchResultsView) {
|
||||
return function() {
|
||||
var router = new SearchRouter();
|
||||
var form = new SearchForm({
|
||||
el: $('#dashboard-search-bar')
|
||||
});
|
||||
var collection = new SearchCollection([]);
|
||||
var results = new SearchListView({collection: collection});
|
||||
var results = new DashboardSearchResultsView({collection: collection});
|
||||
var dispatcher = _.clone(Backbone.Events);
|
||||
|
||||
dispatcher.listenTo(router, 'search', function(query) {
|
||||
|
||||
@@ -35,7 +35,7 @@ define([
|
||||
) {
|
||||
'use strict';
|
||||
|
||||
describe('Search', function() {
|
||||
describe('Course Search', function() {
|
||||
beforeEach(function() {
|
||||
PageHelpers.preventBackboneChangingUrl();
|
||||
});
|
||||
@@ -330,9 +330,9 @@ define([
|
||||
|
||||
describe('SearchForm', function() {
|
||||
beforeEach(function() {
|
||||
loadFixtures('course_search/fixtures/course_search_form.html');
|
||||
loadFixtures('course_search/fixtures/course_content_page.html');
|
||||
this.form = new SearchForm({
|
||||
el: '#courseware-search-bar'
|
||||
el: '.search-bar'
|
||||
});
|
||||
this.onClear = jasmine.createSpy('onClear');
|
||||
this.onSearch = jasmine.createSpy('onSearch');
|
||||
@@ -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.resultsView = new SearchResultsView({collection: this.collection});
|
||||
}
|
||||
|
||||
describe('CourseSearchResultsView', function() {
|
||||
beforeEach(function() {
|
||||
loadFixtures('course_search/fixtures/course_content_page.html');
|
||||
beforeEachHelper.call(this, CourseSearchResultsView);
|
||||
this.contentElementDisplayValue = 'table-cell';
|
||||
});
|
||||
it('shows loading message', showsLoadingMessage);
|
||||
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('renders search results', rendersSearchResults);
|
||||
it('shows a link to load more results', showsMoreResultsLink);
|
||||
@@ -478,6 +472,7 @@ define([
|
||||
|
||||
describe('DashSearchResultsView', function() {
|
||||
beforeEach(function() {
|
||||
loadFixtures('course_search/fixtures/dashboard_search_page.html');
|
||||
beforeEachHelper.call(this, DashSearchResultsView);
|
||||
this.contentElementDisplayValue = 'block';
|
||||
});
|
||||
@@ -507,7 +502,9 @@ define([
|
||||
function showsLoadingMessage() {
|
||||
$('.search-field').val('search string');
|
||||
$('.search-button').trigger('click');
|
||||
expect(this.$contentElement).toBeHidden();
|
||||
if (this.$contentElement) {
|
||||
expect(this.$contentElement).toBeHidden();
|
||||
}
|
||||
expect(this.$searchResults).toBeVisible();
|
||||
expect(this.$searchResults).not.toBeEmpty();
|
||||
}
|
||||
@@ -608,16 +605,15 @@ define([
|
||||
describe('CourseSearchApp', function() {
|
||||
beforeEach(function() {
|
||||
var courseId = 'a/b/c';
|
||||
loadFixtures('course_search/fixtures/course_search_form.html');
|
||||
appendSetFixtures(
|
||||
'<div class="courseware-results"></div>' +
|
||||
'<section id="course-content"></section>'
|
||||
);
|
||||
CourseSearchFactory(courseId);
|
||||
loadFixtures('course_search/fixtures/course_content_page.html');
|
||||
CourseSearchFactory({
|
||||
courseId: courseId,
|
||||
searchHeader: $('.search-bar')
|
||||
});
|
||||
spyOn(Backbone.history, 'navigate');
|
||||
this.$contentElement = $('#course-content');
|
||||
this.contentElementDisplayValue = 'table-cell';
|
||||
this.$searchResults = $('.courseware-results');
|
||||
this.$searchResults = $('.search-results');
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
@@ -628,25 +624,21 @@ define([
|
||||
it('performs search', performsSearch);
|
||||
it('shows an error message', showsErrorMessage);
|
||||
it('updates navigation history', updatesNavigationHistory);
|
||||
it('cancels search request', cancelsSearchRequest);
|
||||
it('clears results', clearsResults);
|
||||
xit('cancels search request', cancelsSearchRequest);
|
||||
xit('clears results', clearsResults);
|
||||
it('loads next page', loadsNextPage);
|
||||
it('navigates to search', navigatesToSearch);
|
||||
});
|
||||
|
||||
describe('DashSearchApp', function() {
|
||||
describe('DashboardSearchApp', function() {
|
||||
beforeEach(function() {
|
||||
loadFixtures('course_search/fixtures/dashboard_search_form.html');
|
||||
appendSetFixtures(
|
||||
'<section id="dashboard-search-results"></section>' +
|
||||
'<section id="my-courses" tabindex="-1"></section>'
|
||||
);
|
||||
loadFixtures('course_search/fixtures/dashboard_search_page.html');
|
||||
DashboardSearchFactory();
|
||||
|
||||
spyOn(Backbone.history, 'navigate');
|
||||
this.$contentElement = $('#my-courses');
|
||||
this.contentElementDisplayValue = 'block';
|
||||
this.$searchResults = $('#dashboard-search-results');
|
||||
this.$searchResults = $('.search-results');
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
@@ -685,6 +677,30 @@ define([
|
||||
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 @@
|
||||
courseSearchItemTemplate
|
||||
) {
|
||||
return SearchResultsView.extend({
|
||||
|
||||
el: '.courseware-results',
|
||||
el: '.search-results',
|
||||
contentElement: '#course-content',
|
||||
coursewareResultsWrapperElement: '.courseware-results-wrapper',
|
||||
resultsTemplate: courseSearchResultsTemplate,
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
dashboardSearchItemTemplate
|
||||
) {
|
||||
return SearchResultsView.extend({
|
||||
el: '#dashboard-search-results',
|
||||
el: '.search-results',
|
||||
contentElement: '#my-courses, #profile-sidebar',
|
||||
resultsTemplate: dashboardSearchResultsTemplate,
|
||||
itemTemplate: dashboardSearchItemTemplate,
|
||||
|
||||
@@ -6,14 +6,15 @@
|
||||
|
||||
el: '',
|
||||
events: {
|
||||
'submit form': 'submitForm',
|
||||
'submit .search-form': 'submitForm',
|
||||
'click .cancel-button': 'clearSearch'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
initialize: function(options) {
|
||||
this.$searchField = this.$el.find('.search-field');
|
||||
this.$searchButton = this.$el.find('.search-button');
|
||||
this.$cancelButton = this.$el.find('.cancel-button');
|
||||
this.supportsActive = options.supportsActive === undefined ? true : options.supportsActive;
|
||||
},
|
||||
|
||||
submitForm: function(event) {
|
||||
@@ -22,16 +23,16 @@
|
||||
},
|
||||
|
||||
doSearch: function(term) {
|
||||
var trimmed;
|
||||
var trimmedTerm;
|
||||
if (term) {
|
||||
trimmed = term.trim();
|
||||
this.$searchField.val(trimmed);
|
||||
trimmedTerm = term.trim();
|
||||
this.$searchField.val(trimmedTerm);
|
||||
} else {
|
||||
trimmed = this.$searchField.val().trim();
|
||||
trimmedTerm = this.$searchField.val().trim();
|
||||
}
|
||||
if (trimmed) {
|
||||
if (trimmedTerm) {
|
||||
this.setActiveStyle();
|
||||
this.trigger('search', trimmed);
|
||||
this.trigger('search', trimmedTerm);
|
||||
} else {
|
||||
this.clearSearch();
|
||||
}
|
||||
@@ -48,17 +49,20 @@
|
||||
},
|
||||
|
||||
setActiveStyle: function() {
|
||||
this.$searchField.addClass('is-active');
|
||||
this.$searchButton.hide();
|
||||
this.$cancelButton.show();
|
||||
if (this.supportsActive) {
|
||||
this.$searchField.addClass('is-active');
|
||||
this.$searchButton.hide();
|
||||
this.$cancelButton.show();
|
||||
}
|
||||
},
|
||||
|
||||
setInitialStyle: function() {
|
||||
this.$searchField.removeClass('is-active');
|
||||
this.$searchButton.show();
|
||||
this.$cancelButton.hide();
|
||||
if (this.supportsActive) {
|
||||
this.$searchField.removeClass('is-active');
|
||||
this.$searchButton.show();
|
||||
this.$cancelButton.hide();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
}(define || RequireJS.define));
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
data.excerpt = '';
|
||||
data.content_type = '';
|
||||
}
|
||||
data.excerptHtml = HtmlUtils.HTML(data.excerpt);
|
||||
delete data.excerpt;
|
||||
HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.template)(data));
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
spinner: '.search-load-next .icon',
|
||||
|
||||
initialize: function() {
|
||||
this.$contentElement = $(this.contentElement);
|
||||
this.$contentElement = this.contentElement ? $(this.contentElement) : $([]);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
@@ -59,6 +59,7 @@
|
||||
});
|
||||
return item.render().el;
|
||||
}, this);
|
||||
// safe-lint: disable=javascript-jquery-append
|
||||
this.$el.find('ol').append(items);
|
||||
},
|
||||
|
||||
@@ -80,8 +81,14 @@
|
||||
},
|
||||
|
||||
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)());
|
||||
|
||||
// Show the results
|
||||
this.showResults();
|
||||
},
|
||||
|
||||
@@ -90,15 +97,6 @@
|
||||
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) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="result-excerpt"><%= excerpt %></div>
|
||||
<a class="result-link" href="<%- url %>"><%= gettext("View") %> <span class="icon fa fa-arrow-right" aria-hidden="true"></span></a>
|
||||
<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>
|
||||
<span class="result-location"><%- location.join(' ▸ ') %></span>
|
||||
<span class="result-type"><%- content_type %></span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="search-info">
|
||||
<%= gettext("Search Results") %>
|
||||
<div class="search-count"><%= totalCountMsg %></div>
|
||||
<h2 class="search-results-title"><%- gettext("Search Results") %></h2>
|
||||
<div class="search-count"><%- totalCountMsg %></div>
|
||||
</div>
|
||||
|
||||
<% if (totalCount > 0 ) { %>
|
||||
@@ -9,17 +9,20 @@
|
||||
|
||||
<% 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
|
||||
) %>
|
||||
<%-
|
||||
StringUtils.interpolate(
|
||||
ngettext("Load next {num_items} result", "Load next {num_items} results", pageSize),
|
||||
{
|
||||
num_items: pageSize
|
||||
}
|
||||
)
|
||||
%>
|
||||
<span class="icon fa fa-spinner fa-spin" aria-hidden="true"></span>
|
||||
</a>
|
||||
<% } %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<p><%= gettext("Sorry, no results were found.") %></p>
|
||||
<p><%- gettext("Sorry, no results were found.") %></p>
|
||||
|
||||
<% } %>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="result-excerpt"><%= excerpt %></div>
|
||||
<a class="result-link" href="<%- url %>"><%= gettext("View") %> <span class="icon fa fa-arrow-right" aria-hidden="true"></span></a>
|
||||
<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>
|
||||
<span class="result-course-name"><%- course_name %>:</span>
|
||||
<span class="result-location"><%- location.join(' ▸ ') %></span>
|
||||
<span class="result-type"><%- content_type %></span>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<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>
|
||||
<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 ) { %>
|
||||
@@ -10,17 +10,20 @@
|
||||
|
||||
<% 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
|
||||
) %>
|
||||
<%-
|
||||
StringUtils.interpolate(
|
||||
ngettext("Load next {num_items} result", "Load next {num_items} results", pageSize),
|
||||
{
|
||||
num_items: pageSize
|
||||
}
|
||||
)
|
||||
%>
|
||||
<span class="icon fa fa-spinner fa-spin" aria-hidden="true"></span>
|
||||
</a>
|
||||
<% } %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<p><%= gettext("Sorry, no results were found.") %></p>
|
||||
<p><%- gettext("Sorry, no results were found.") %></p>
|
||||
|
||||
<% } %>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
<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") %>
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
## 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>
|
||||
20
openedx/features/course_search/urls.py
Normal file
20
openedx/features/course_search/urls.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
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',
|
||||
),
|
||||
]
|
||||
66
openedx/features/course_search/views/course_search.py
Normal file
66
openedx/features/course_search/views/course_search.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
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
|
||||
|
||||
% if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'):
|
||||
<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>
|
||||
<div class="search-field-wrapper">
|
||||
<input id="dashboard-search-input" type="text" class="search-field"/>
|
||||
|
||||
Reference in New Issue
Block a user