Commit 2eab85eb by rabiaiftikhar

Merge branch 'master' into ri/EDUCATOR-394-disable-self-generation-certificates

parents 22d3b028 a42f9306
...@@ -282,6 +282,7 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False): ...@@ -282,6 +282,7 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
selected_groups_label = get_visibility_partition_info(xblock)['selected_groups_label'] selected_groups_label = get_visibility_partition_info(xblock)['selected_groups_label']
if selected_groups_label: if selected_groups_label:
selected_groups_label = _('Access restricted to: {list_of_groups}').format(list_of_groups=selected_groups_label) selected_groups_label = _('Access restricted to: {list_of_groups}').format(list_of_groups=selected_groups_label)
course = modulestore().get_course(xblock.location.course_key)
template_context = { template_context = {
'xblock_context': context, 'xblock_context': context,
'xblock': xblock, 'xblock': xblock,
...@@ -293,8 +294,10 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False): ...@@ -293,8 +294,10 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
'can_edit_visibility': context.get('can_edit_visibility', True), 'can_edit_visibility': context.get('can_edit_visibility', True),
'selected_groups_label': selected_groups_label, 'selected_groups_label': selected_groups_label,
'can_add': context.get('can_add', True), 'can_add': context.get('can_add', True),
'can_move': context.get('can_move', True) 'can_move': context.get('can_move', True),
'language': getattr(course, 'language', None)
} }
html = render_to_string('studio_xblock_wrapper.html', template_context) html = render_to_string('studio_xblock_wrapper.html', template_context)
frag = wrap_fragment(frag, html) frag = wrap_fragment(frag, html)
return frag return frag
......
# below are the sub-paths to the documentation for the various pages # below are the sub-paths to the documentation for the various pages
# NOTE: If any of these page settings change, then their corresponding test should be updated # NOTE: If any of these page settings change, their corresponding test must be updated
# in edx-platform/common/test/acceptance/tests/studio/test_studio.help.py # in edx-platform/common/test/acceptance/tests/studio/test_studio.help.py
[pages] [pages]
default = course_author:index.html default = course_author:index.html
...@@ -12,12 +12,12 @@ updates = course_author:course_assets/handouts_updates.html ...@@ -12,12 +12,12 @@ updates = course_author:course_assets/handouts_updates.html
pages = course_author:course_assets/pages.html pages = course_author:course_assets/pages.html
files = course_author:course_assets/course_files.html files = course_author:course_assets/course_files.html
textbooks = course_author:course_assets/textbooks.html textbooks = course_author:course_assets/textbooks.html
schedule = course_author:set_up_course/setting_up_student_view.html schedule = course_author:set_up_course/studio_add_course_information/index.html
grading = course_author:grading/index.html grading = course_author:grading/index.html
team_course = course_author:set_up_course/course_staffing.html#add-course-team-members team_course = course_author:set_up_course/studio_add_course_information/studio_course_staffing.html
team_library = course_author:course_components/libraries.html#give-other-users-access-to-your-library team_library = course_author:course_components/libraries.html#give-other-users-access-to-your-library
advanced = course_author:index.html advanced = course_author:index.html
checklist = course_author:set_up_course/creating_new_course.html checklist = course_author:set_up_course/index.html
import_library = course_author:course_components/libraries.html#import-a-library import_library = course_author:course_components/libraries.html#import-a-library
import_course = course_author:releasing_course/export_import_course.html#import-a-course import_course = course_author:releasing_course/export_import_course.html#import-a-course
export_library = course_author:course_components/libraries.html#export-a-library export_library = course_author:course_components/libraries.html#export-a-library
...@@ -27,11 +27,11 @@ login = course_author:getting_started/index.html ...@@ -27,11 +27,11 @@ login = course_author:getting_started/index.html
register = course_author:getting_started/index.html register = course_author:getting_started/index.html
content_libraries = course_author:course_components/libraries.html content_libraries = course_author:course_components/libraries.html
content_groups = course_author:course_features/cohorts/cohorted_courseware.html content_groups = course_author:course_features/cohorts/cohorted_courseware.html
enrollment_tracks = course_author:course_features/cohorts/cohorted_courseware.html enrollment_tracks = course_author:course_features/diff_content/enroll_track_courseware.html
group_configurations = course_author:course_features/content_experiments/content_experiments_configure.html#set-up-group-configurations-in-edx-studio group_configurations = course_author:course_features/content_experiments/content_experiments_configure.html#set-up-group-configurations-in-edx-studio
container = course_author:developing_course/course_components.html#components-that-contain-other-components container = course_author:developing_course/course_components.html#components-that-contain-other-components
video = course_author:video/video_uploads.html video = course_author:video/video_uploads.html
certificates = course_author:set_up_course/creating_course_certificates.html certificates = course_author:set_up_course/studio_add_course_information/studio_creating_certificates.html
# below are the language directory names for the different locales # below are the language directory names for the different locales
[locales] [locales]
......
...@@ -272,11 +272,11 @@ ...@@ -272,11 +272,11 @@
.wrapper-header { .wrapper-header {
.wrapper-l { .wrapper-l {
width: flex-grid(8,12); width: flex-grid(9,12);
} }
.wrapper-r { .wrapper-r {
width: flex-grid(4,12); width: flex-grid(3,12);
} }
.branding { .branding {
......
...@@ -62,7 +62,11 @@ from openedx.core.djangolib.js_utils import ( ...@@ -62,7 +62,11 @@ from openedx.core.djangolib.js_utils import (
<div class="main-wrapper"> <div class="main-wrapper">
<div class="inner-wrapper"> <div class="inner-wrapper">
<div class="course-info-wrapper"> <div class="course-info-wrapper"
% if getattr(context_course, 'language'):
lang="${context_course.language}"
% endif
>
<div class="main-column window"> <div class="main-column window">
<article class="course-updates" id="course-update-view"> <article class="course-updates" id="course-update-view">
<ol class="update-list" id="course-update-list"></ol> <ol class="update-list" id="course-update-list"></ol>
......
...@@ -175,8 +175,11 @@ from openedx.core.djangolib.markup import HTML, Text ...@@ -175,8 +175,11 @@ from openedx.core.djangolib.markup import HTML, Text
</div> </div>
% endif % endif
</div> </div>
<div class="wrapper-dnd"
<div class="wrapper-dnd"> % if getattr(context_course, 'language'):
lang="${context_course.language}"
% endif
>
<% <%
course_locator = context_course.location course_locator = context_course.location
%> %>
......
...@@ -158,7 +158,11 @@ messages = xblock.validate().to_json() ...@@ -158,7 +158,11 @@ messages = xblock.validate().to_json()
% if show_preview: % if show_preview:
% if is_root or not xblock_url: % if is_root or not xblock_url:
<article class="xblock-render"> % if not is_root and language:
<article class="xblock-render" lang="${language}">
% else:
<article class="xblock-render">
% endif
${content | n, decode.utf8} ${content | n, decode.utf8}
</article> </article>
% else: % else:
......
...@@ -87,9 +87,8 @@ class PartitionService(object): ...@@ -87,9 +87,8 @@ class PartitionService(object):
with a given course. with a given course.
""" """
def __init__(self, course_id, track_function=None, cache=None): def __init__(self, course_id, cache=None):
self._course_id = course_id self._course_id = course_id
self._track_function = track_function
self._cache = cache self._cache = cache
def get_course(self): def get_course(self):
...@@ -165,7 +164,7 @@ class PartitionService(object): ...@@ -165,7 +164,7 @@ class PartitionService(object):
the partition's scheme. the partition's scheme.
""" """
return user_partition.scheme.get_group_for_user( return user_partition.scheme.get_group_for_user(
self._course_id, user, user_partition, assign=assign, track_function=self._track_function self._course_id, user, user_partition, assign=assign,
) )
......
...@@ -96,7 +96,7 @@ class MockUserPartitionScheme(object): ...@@ -96,7 +96,7 @@ class MockUserPartitionScheme(object):
self.name = name self.name = name
self.current_group = current_group self.current_group = current_group
def get_group_for_user(self, course_id, user, user_partition, assign=True, track_function=None): # pylint: disable=unused-argument def get_group_for_user(self, course_id, user, user_partition, assign=True): # pylint: disable=unused-argument
""" """
Returns the current group if set, else the first group from the specified user partition. Returns the current group if set, else the first group from the specified user partition.
""" """
...@@ -446,7 +446,6 @@ class PartitionServiceBaseClass(PartitionTestCase): ...@@ -446,7 +446,6 @@ class PartitionServiceBaseClass(PartitionTestCase):
return MockPartitionService( return MockPartitionService(
self.course, self.course,
course_id=self.course.id, course_id=self.course.id,
track_function=Mock(),
cache=cache cache=cache
) )
......
...@@ -96,7 +96,6 @@ class SplitTestModuleTest(XModuleXmlImportTest, PartitionTestCase): ...@@ -96,7 +96,6 @@ class SplitTestModuleTest(XModuleXmlImportTest, PartitionTestCase):
partitions_service = MockPartitionService( partitions_service = MockPartitionService(
self.course, self.course,
course_id=self.course.id, course_id=self.course.id,
track_function=Mock(name='track_function'),
) )
self.module_system._services['partitions'] = partitions_service # pylint: disable=protected-access self.module_system._services['partitions'] = partitions_service # pylint: disable=protected-access
......
...@@ -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]
...@@ -808,7 +808,7 @@ class SettingsHelpTest(StudioCourseTest): ...@@ -808,7 +808,7 @@ class SettingsHelpTest(StudioCourseTest):
Then Help link should open. Then Help link should open.
And help url should be correct And help url should be correct
""" """
expected_url = _get_expected_documentation_url('/set_up_course/setting_up_student_view.html') expected_url = _get_expected_documentation_url('/set_up_course/studio_add_course_information/index.html')
# Assert that help link is correct. # Assert that help link is correct.
assert_nav_help_link( assert_nav_help_link(
...@@ -880,7 +880,7 @@ class CourseTeamSettingsHelpTest(StudioCourseTest): ...@@ -880,7 +880,7 @@ class CourseTeamSettingsHelpTest(StudioCourseTest):
Then Help link should open. Then Help link should open.
And help url should be correct And help url should be correct
""" """
expected_url = _get_expected_documentation_url('/set_up_course/course_staffing.html#add-course-team-members') expected_url = _get_expected_documentation_url('/set_up_course/studio_add_course_information/studio_course_staffing.html')
# Assert that help link is correct. # Assert that help link is correct.
assert_nav_help_link( assert_nav_help_link(
...@@ -1009,7 +1009,7 @@ class CertificatePageHelpTest(StudioCourseTest): ...@@ -1009,7 +1009,7 @@ class CertificatePageHelpTest(StudioCourseTest):
Then Help link should open. Then Help link should open.
And help url should be correct And help url should be correct
""" """
expected_url = _get_expected_documentation_url('/set_up_course/creating_course_certificates.html') expected_url = _get_expected_documentation_url('/set_up_course/studio_add_course_information/studio_creating_certificates.html')
# Assert that help link is correct. # Assert that help link is correct.
assert_nav_help_link( assert_nav_help_link(
...@@ -1027,7 +1027,7 @@ class CertificatePageHelpTest(StudioCourseTest): ...@@ -1027,7 +1027,7 @@ class CertificatePageHelpTest(StudioCourseTest):
Then Help link should open. Then Help link should open.
And help url should be correct And help url should be correct
""" """
expected_url = _get_expected_documentation_url('/set_up_course/creating_course_certificates.html') expected_url = _get_expected_documentation_url('/set_up_course/studio_add_course_information/studio_creating_certificates.html')
# Assert that help link is correct. # Assert that help link is correct.
assert_side_bar_help_link( assert_side_bar_help_link(
......
...@@ -30,7 +30,7 @@ class MemoryUserPartitionScheme(object): ...@@ -30,7 +30,7 @@ class MemoryUserPartitionScheme(object):
""" """
self.current_group.setdefault(user.id, {})[user_partition.id] = group self.current_group.setdefault(user.id, {})[user_partition.id] = group
def get_group_for_user(self, course_id, user, user_partition, track_function=None): # pylint: disable=unused-argument def get_group_for_user(self, course_id, user, user_partition): # pylint: disable=unused-argument
""" """
Fetch the group to which this user is linked in this partition, or None. Fetch the group to which this user is linked in this partition, or None.
""" """
......
...@@ -348,8 +348,10 @@ class CoursewareIndex(View): ...@@ -348,8 +348,10 @@ class CoursewareIndex(View):
'section_title': None, 'section_title': None,
'sequence_title': None, 'sequence_title': None,
'disable_accordion': waffle.flag_is_active(request, UNIFIED_COURSE_VIEW_FLAG), 'disable_accordion': waffle.flag_is_active(request, UNIFIED_COURSE_VIEW_FLAG),
# TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts
'upgrade_link': check_and_get_upgrade_link(request, self.effective_user, self.course.id), 'upgrade_link': check_and_get_upgrade_link(request, self.effective_user, self.course.id),
'upgrade_price': get_cosmetic_verified_display_price(self.course), 'upgrade_price': get_cosmetic_verified_display_price(self.course),
# ENDTODO
} }
table_of_contents = toc_for_course( table_of_contents = toc_for_course(
self.effective_user, self.effective_user,
......
...@@ -336,8 +336,10 @@ def course_info(request, course_id): ...@@ -336,8 +336,10 @@ def course_info(request, course_id):
'show_enroll_banner': show_enroll_banner, 'show_enroll_banner': show_enroll_banner,
'dates_fragment': dates_fragment, 'dates_fragment': dates_fragment,
'url_to_enroll': url_to_enroll, 'url_to_enroll': url_to_enroll,
# TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts
'upgrade_link': check_and_get_upgrade_link(request, user, course.id), 'upgrade_link': check_and_get_upgrade_link(request, user, course.id),
'upgrade_price': get_cosmetic_verified_display_price(course), 'upgrade_price': get_cosmetic_verified_display_price(course),
# ENDTODO
} }
# Get the URL of the user's last position in order to display the 'where you were last' message # Get the URL of the user's last position in order to display the 'where you were last' message
...@@ -360,6 +362,7 @@ def course_info(request, course_id): ...@@ -360,6 +362,7 @@ def course_info(request, course_id):
UPGRADE_COOKIE_NAME = 'show_upgrade_notification' UPGRADE_COOKIE_NAME = 'show_upgrade_notification'
# TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts
def check_and_get_upgrade_link(request, user, course_id): def check_and_get_upgrade_link(request, user, course_id):
upgrade_link = None upgrade_link = None
...@@ -370,6 +373,7 @@ def check_and_get_upgrade_link(request, user, course_id): ...@@ -370,6 +373,7 @@ def check_and_get_upgrade_link(request, user, course_id):
request.need_to_set_upgrade_cookie = True request.need_to_set_upgrade_cookie = True
return upgrade_link return upgrade_link
# ENDTODO
class StaticCourseTabView(EdxFragmentView): class StaticCourseTabView(EdxFragmentView):
...@@ -494,8 +498,10 @@ class CourseTabView(EdxFragmentView): ...@@ -494,8 +498,10 @@ class CourseTabView(EdxFragmentView):
'supports_preview_menu': supports_preview_menu, 'supports_preview_menu': supports_preview_menu,
'uses_pattern_library': True, 'uses_pattern_library': True,
'disable_courseware_js': True, 'disable_courseware_js': True,
# TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts
'upgrade_link': check_and_get_upgrade_link(request, request.user, course.id), 'upgrade_link': check_and_get_upgrade_link(request, request.user, course.id),
'upgrade_price': get_cosmetic_verified_display_price(course), 'upgrade_price': get_cosmetic_verified_display_price(course),
# ENDTODO
} }
def render_to_fragment(self, request, course=None, page_context=None, **kwargs): def render_to_fragment(self, request, course=None, page_context=None, **kwargs):
...@@ -901,8 +907,10 @@ def _progress(request, course_key, student_id): ...@@ -901,8 +907,10 @@ def _progress(request, course_key, student_id):
'passed': is_course_passed(course, grade_summary), 'passed': is_course_passed(course, grade_summary),
'credit_course_requirements': _credit_course_requirements(course_key, student), 'credit_course_requirements': _credit_course_requirements(course_key, student),
'certificate_data': _get_cert_data(student, course, course_key, is_active, enrollment_mode), 'certificate_data': _get_cert_data(student, course, course_key, is_active, enrollment_mode),
# TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts
'upgrade_link': check_and_get_upgrade_link(request, student, course.id), 'upgrade_link': check_and_get_upgrade_link(request, student, course.id),
'upgrade_price': get_cosmetic_verified_display_price(course), 'upgrade_price': get_cosmetic_verified_display_price(course),
# ENDTODO
} }
with outer_atomic(): with outer_atomic():
......
...@@ -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>
...@@ -41,7 +41,11 @@ from openedx.core.djangolib.markup import HTML ...@@ -41,7 +41,11 @@ from openedx.core.djangolib.markup import HTML
<div class="forum-search"></div> <div class="forum-search"></div>
</div> </div>
</header> </header>
<div class="page-content"> <div class="page-content"
% if getattr(course, 'language'):
lang="${course.language}"
% endif
>
<div class="discussion-body layout layout-1t2t"> <div class="discussion-body layout layout-1t2t">
<aside class="forum-nav layout-col layout-col-a" role="complementary" aria-label="${_("Discussion thread list")}"> <aside class="forum-nav layout-col layout-col-a" role="complementary" aria-label="${_("Discussion thread list")}">
<%include file="_filter_dropdown.html" /> <%include file="_filter_dropdown.html" />
......
...@@ -443,8 +443,10 @@ def _create_discussion_board_context(request, course_key, discussion_id=None, th ...@@ -443,8 +443,10 @@ def _create_discussion_board_context(request, course_key, discussion_id=None, th
'category_map': course_settings["category_map"], 'category_map': course_settings["category_map"],
'course_settings': course_settings, 'course_settings': course_settings,
'is_commentable_divided': is_commentable_divided(course_key, discussion_id, course_discussion_settings), 'is_commentable_divided': is_commentable_divided(course_key, discussion_id, course_discussion_settings),
# TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts
'upgrade_link': check_and_get_upgrade_link(request, user, course.id), 'upgrade_link': check_and_get_upgrade_link(request, user, course.id),
'upgrade_price': get_cosmetic_verified_display_price(course), 'upgrade_price': get_cosmetic_verified_display_price(course),
# ENDTODO
}) })
return context return context
......
...@@ -600,7 +600,7 @@ class TestGradeReportConditionalContent(TestReportMixin, TestConditionalContent, ...@@ -600,7 +600,7 @@ class TestGradeReportConditionalContent(TestReportMixin, TestConditionalContent,
group_config_hdr_tpl = 'Experiment Group ({})' group_config_hdr_tpl = 'Experiment Group ({})'
return { return {
group_config_hdr_tpl.format(self.partition.name): self.partition.scheme.get_group_for_user( group_config_hdr_tpl.format(self.partition.name): self.partition.scheme.get_group_for_user(
self.course.id, user, self.partition, track_function=None self.course.id, user, self.partition
).name ).name
} }
......
...@@ -139,7 +139,6 @@ class LmsModuleSystem(ModuleSystem): # pylint: disable=abstract-method ...@@ -139,7 +139,6 @@ class LmsModuleSystem(ModuleSystem): # pylint: disable=abstract-method
services['library_tools'] = LibraryToolsService(modulestore()) services['library_tools'] = LibraryToolsService(modulestore())
services['partitions'] = PartitionService( services['partitions'] = PartitionService(
course_id=kwargs.get('course_id'), course_id=kwargs.get('course_id'),
track_function=kwargs.get('track_function', None),
cache=request_cache_dict cache=request_cache_dict
) )
store = modulestore() store = modulestore()
......
...@@ -2230,6 +2230,7 @@ INSTALLED_APPS = ( ...@@ -2230,6 +2230,7 @@ INSTALLED_APPS = (
# Features # Features
'openedx.features.course_bookmarks', 'openedx.features.course_bookmarks',
'openedx.features.course_experience', 'openedx.features.course_experience',
'openedx.features.course_search',
'openedx.features.enterprise_support', 'openedx.features.enterprise_support',
) )
......
../../openedx/features/course_search/static/course_search
\ No newline at end of file
...@@ -7,11 +7,15 @@ ...@@ -7,11 +7,15 @@
function OpenResponseAssessmentBlock($section) { function OpenResponseAssessmentBlock($section) {
this.$section = $section; this.$section = $section;
this.$section.data('wrapper', this); this.$section.data('wrapper', this);
this.initialized = false;
} }
OpenResponseAssessmentBlock.prototype.onClickTitle = function() { OpenResponseAssessmentBlock.prototype.onClickTitle = function() {
var block = this.$section.find('.open-response-assessment'); var block = this.$section.find('.open-response-assessment');
XBlock.initializeBlock($(block).find('.xblock')[0]); if (!this.initialized) {
this.initialized = true;
XBlock.initializeBlock($(block).find('.xblock')[0]);
}
}; };
return OpenResponseAssessmentBlock; return OpenResponseAssessmentBlock;
......
(function(define) {
define([
'js/search/base/views/search_form'
], function(SearchForm) {
'use strict';
return SearchForm.extend({
el: '#courseware-search-bar'
});
});
})(define || RequireJS.define);
(function(define) {
define([
'js/search/base/views/search_item_view'
], function(SearchItemView) {
'use strict';
return SearchItemView.extend({
templateId: '#course_search_item-tpl'
});
});
})(define || RequireJS.define);
(function(define) {
define([
'js/search/base/views/search_form'
], function(SearchForm) {
'use strict';
return SearchForm.extend({
el: '#dashboard-search-bar'
});
});
})(define || RequireJS.define);
(function(define) {
define([
'js/search/base/views/search_item_view'
], function(SearchItemView) {
'use strict';
return SearchItemView.extend({
templateId: '#dashboard_search_item-tpl'
});
});
})(define || RequireJS.define);
...@@ -28,6 +28,7 @@ var options = { ...@@ -28,6 +28,7 @@ var options = {
sourceFiles: [ sourceFiles: [
{pattern: 'coffee/src/**/!(*spec).js'}, {pattern: 'coffee/src/**/!(*spec).js'},
{pattern: 'course_bookmarks/**/!(*spec).js'}, {pattern: 'course_bookmarks/**/!(*spec).js'},
{pattern: 'course_search/**/!(*spec).js'},
{pattern: 'discussion/js/**/!(*spec).js'}, {pattern: 'discussion/js/**/!(*spec).js'},
{pattern: 'js/**/!(*spec|djangojs).js'}, {pattern: 'js/**/!(*spec|djangojs).js'},
{pattern: 'lms/js/**/!(*spec).js'}, {pattern: 'lms/js/**/!(*spec).js'},
......
...@@ -19,6 +19,8 @@ ...@@ -19,6 +19,8 @@
*/ */
modules: getModulesList([ modules: getModulesList([
'course_bookmarks/js/course_bookmarks_factory', 'course_bookmarks/js/course_bookmarks_factory',
'course_search/js/course_search_factory',
'course_search/js/dashboard_search_factory',
'discussion/js/discussion_board_factory', 'discussion/js/discussion_board_factory',
'discussion/js/discussion_profile_page_factory', 'discussion/js/discussion_profile_page_factory',
'js/api_admin/catalog_preview_factory', 'js/api_admin/catalog_preview_factory',
...@@ -32,8 +34,6 @@ ...@@ -32,8 +34,6 @@
'js/header_factory', 'js/header_factory',
'js/learner_dashboard/program_details_factory', 'js/learner_dashboard/program_details_factory',
'js/learner_dashboard/program_list_factory', 'js/learner_dashboard/program_list_factory',
'js/search/course/course_search_factory',
'js/search/dashboard/dashboard_search_factory',
'js/student_account/logistration_factory', 'js/student_account/logistration_factory',
'js/student_account/views/account_settings_factory', 'js/student_account/views/account_settings_factory',
'js/student_account/views/finish_auth_factory', 'js/student_account/views/finish_auth_factory',
......
...@@ -676,6 +676,7 @@ ...@@ -676,6 +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/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',
...@@ -749,7 +750,6 @@ ...@@ -749,7 +750,6 @@
'js/spec/markdown_editor_spec.js', 'js/spec/markdown_editor_spec.js',
'js/spec/dateutil_factory_spec.js', 'js/spec/dateutil_factory_spec.js',
'js/spec/navigation_spec.js', 'js/spec/navigation_spec.js',
'js/spec/search/search_spec.js',
'js/spec/shoppingcart/shoppingcart_spec.js', 'js/spec/shoppingcart/shoppingcart_spec.js',
'js/spec/staff_debug_actions_spec.js', 'js/spec/staff_debug_actions_spec.js',
'js/spec/student_account/access_spec.js', 'js/spec/student_account/access_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;
} }
......
...@@ -37,14 +37,6 @@ from openedx.features.course_experience import course_home_page_title, UNIFIED_C ...@@ -37,14 +37,6 @@ from openedx.features.course_experience import course_home_page_title, UNIFIED_C
</script> </script>
% endfor % endfor
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
% for template_name in ["course_search_item", "course_search_results", "search_loading", "search_error"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="search/${template_name}.underscore" />
</script>
% endfor
% endif
% if include_special_exams is not UNDEFINED and include_special_exams: % if include_special_exams is not UNDEFINED and include_special_exams:
% for template_name in ["proctored-exam-status"]: % for template_name in ["proctored-exam-status"]:
<script type="text/template" id="${template_name}-tpl"> <script type="text/template" id="${template_name}-tpl">
...@@ -81,9 +73,12 @@ from openedx.features.course_experience import course_home_page_title, UNIFIED_C ...@@ -81,9 +73,12 @@ from openedx.features.course_experience import course_home_page_title, UNIFIED_C
<%include file="/mathjax_include.html" args="disable_fast_preview=True"/> <%include file="/mathjax_include.html" args="disable_fast_preview=True"/>
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'): % if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
<%static:require_module module_name="js/search/course/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
...@@ -111,7 +106,11 @@ ${HTML(fragment.foot_html())} ...@@ -111,7 +106,11 @@ ${HTML(fragment.foot_html())}
<%include file="/courseware/course_navigation.html" args="active_page='courseware'" /> <%include file="/courseware/course_navigation.html" args="active_page='courseware'" />
% endif % endif
<div class="container"> <div class="container"
% if getattr(course, 'language'):
lang="${course.language}"
% endif
>
<div class="course-wrapper" role="presentation"> <div class="course-wrapper" role="presentation">
% if disable_accordion is UNDEFINED or not disable_accordion: % if disable_accordion is UNDEFINED or not disable_accordion:
...@@ -127,7 +126,7 @@ ${HTML(fragment.foot_html())} ...@@ -127,7 +126,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"/>
......
...@@ -58,7 +58,11 @@ from openedx.core.djangolib.markup import HTML, Text ...@@ -58,7 +58,11 @@ from openedx.core.djangolib.markup import HTML, Text
<%block name="bodyclass">view-in-course view-course-info ${course.css_class or ''}</%block> <%block name="bodyclass">view-in-course view-course-info ${course.css_class or ''}</%block>
<main id="main" aria-label="Content" tabindex="-1"> <main id="main" aria-label="Content" tabindex="-1">
<div class="container"> <div class="container"
% if getattr(course, 'language'):
lang="${course.language}"
% endif
>
<div class="home"> <div class="home">
<div class="page-header-main"> <div class="page-header-main">
<h2 class="hd hd-2 page-title">${_("Welcome to {org}'s {course_name}!").format(org=course.display_org_with_default, course_name=course.display_number_with_default)} <h2 class="hd hd-2 page-title">${_("Welcome to {org}'s {course_name}!").format(org=course.display_org_with_default, course_name=course.display_number_with_default)}
......
...@@ -42,7 +42,11 @@ from django.utils.http import urlquote_plus ...@@ -42,7 +42,11 @@ from django.utils.http import urlquote_plus
<main id="main" aria-label="Content" tabindex="-1"> <main id="main" aria-label="Content" tabindex="-1">
<div class="container"> <div class="container">
<div class="profile-wrapper"> <div class="profile-wrapper">
<section class="course-info" id="course-info-progress"> <section class="course-info" id="course-info-progress"
% if getattr(course, 'language'):
lang="${course.language}"
% endif
>
% if staff_access and studio_url is not None: % if staff_access and studio_url is not None:
<div class="wrap-instructor-info"> <div class="wrap-instructor-info">
<a class="instructor-info-action studio-view" href="${studio_url}">${_("View Grading in studio")}</a> <a class="instructor-info-action studio-view" href="${studio_url}">${_("View Grading in studio")}</a>
......
...@@ -25,6 +25,11 @@ ${HTML(fragment.foot_html())} ...@@ -25,6 +25,11 @@ ${HTML(fragment.foot_html())}
<%include file="/courseware/course_navigation.html" args="active_page=active_page" /> <%include file="/courseware/course_navigation.html" args="active_page=active_page" />
<main id="main" aria-label="Content" tabindex="-1"> <main id="main" aria-label="Content" tabindex="-1">
<section class="container"
% if getattr(course, 'language'):
lang=${course.language}
% endif
>
<section class="container"> <section class="container">
<div class="static_tab_wrapper"> <div class="static_tab_wrapper">
${HTML(fragment.body_html())} ${HTML(fragment.body_html())}
......
## TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts
<%page expression_filter="h"/> <%page expression_filter="h"/>
% if upgrade_link: % if upgrade_link:
...@@ -7,3 +8,4 @@ ...@@ -7,3 +8,4 @@
data-price="${upgrade_price}"> data-price="${upgrade_price}">
</script> </script>
% endif % endif
## ENDTODO
...@@ -28,12 +28,6 @@ from openedx.core.djangolib.markup import HTML, Text ...@@ -28,12 +28,6 @@ from openedx.core.djangolib.markup import HTML, Text
<%static:include path="dashboard/${template_name}.underscore" /> <%static:include path="dashboard/${template_name}.underscore" />
</script> </script>
% endfor % endfor
% for template_name in ["dashboard_search_item", "dashboard_search_results", "search_loading", "search_error"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="search/${template_name}.underscore" />
</script>
% endfor
</%block> </%block>
<%block name="js_extra"> <%block name="js_extra">
...@@ -49,7 +43,7 @@ from openedx.core.djangolib.markup import HTML, Text ...@@ -49,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="js/search/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
...@@ -165,7 +159,7 @@ from openedx.core.djangolib.markup import HTML, Text ...@@ -165,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"/>
......
...@@ -54,7 +54,11 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_ ...@@ -54,7 +54,11 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
% else: % else:
<% mode_class = '' %> <% mode_class = '' %>
% endif % endif
<div class="course-container"> <div class="course-container"
% if getattr(course_overview, 'language'):
lang="${course_overview.language}"
% endif
>
<article class="course${mode_class}"> <article class="course${mode_class}">
<% course_target = reverse(course_home_url_name(course_overview.id), args=[unicode(course_overview.id)]) %> <% course_target = reverse(course_home_url_name(course_overview.id), args=[unicode(course_overview.id)]) %>
<section class="details" aria-labelledby="details-heading-${course_overview.number}"> <section class="details" aria-labelledby="details-heading-${course_overview.number}">
......
...@@ -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>
......
...@@ -59,7 +59,11 @@ ...@@ -59,7 +59,11 @@
<div class="container"> <div class="container">
<div class="wiki-wrapper"> <div class="wiki-wrapper">
<main id="main" aria-label="Content" tabindex="-1"> <main id="main" aria-label="Content" tabindex="-1">
<section class="wiki {{ selected_tab }}" id="wiki-content"> <section class="wiki {{ selected_tab }}" id="wiki-content"
{% if request.course.language %}
lang="{{ request.course.language }}"
{% endif %}
>
{% block wiki_body %} {% block wiki_body %}
{% block wiki_breadcrumbs %}{% endblock %} {% block wiki_breadcrumbs %}{% endblock %}
......
...@@ -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"]:
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course_overviews', '0012_courseoverview_eligible_for_financial_aid'),
]
operations = [
migrations.AddField(
model_name='courseoverview',
name='language',
field=models.TextField(null=True),
),
]
...@@ -101,6 +101,8 @@ class CourseOverview(TimeStampedModel): ...@@ -101,6 +101,8 @@ class CourseOverview(TimeStampedModel):
marketing_url = TextField(null=True) marketing_url = TextField(null=True)
eligible_for_financial_aid = BooleanField(default=True) eligible_for_financial_aid = BooleanField(default=True)
language = TextField(null=True)
@classmethod @classmethod
def _create_or_update(cls, course): def _create_or_update(cls, course):
""" """
...@@ -190,6 +192,8 @@ class CourseOverview(TimeStampedModel): ...@@ -190,6 +192,8 @@ class CourseOverview(TimeStampedModel):
course_overview.course_video_url = CourseDetails.fetch_video_url(course.id) course_overview.course_video_url = CourseDetails.fetch_video_url(course.id)
course_overview.self_paced = course.self_paced course_overview.self_paced = course.self_paced
course_overview.language = course.language
return course_overview return course_overview
@classmethod @classmethod
......
...@@ -25,7 +25,7 @@ class CohortPartitionScheme(object): ...@@ -25,7 +25,7 @@ class CohortPartitionScheme(object):
# pylint: disable=unused-argument # pylint: disable=unused-argument
@classmethod @classmethod
def get_group_for_user(cls, course_key, user, user_partition, track_function=None, use_cached=True): def get_group_for_user(cls, course_key, user, user_partition, use_cached=True):
""" """
Returns the Group from the specified user partition to which the user Returns the Group from the specified user partition to which the user
is assigned, via their cohort membership and any mappings from cohorts is assigned, via their cohort membership and any mappings from cohorts
......
...@@ -5,6 +5,8 @@ import logging ...@@ -5,6 +5,8 @@ import logging
import random import random
import course_tag.api as course_tag_api import course_tag.api as course_tag_api
from eventtracking import tracker
from xmodule.partitions.partitions import UserPartitionError, NoSuchUserPartitionGroupError from xmodule.partitions.partitions import UserPartitionError, NoSuchUserPartitionGroupError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -17,7 +19,7 @@ class NotImplementedPartitionScheme(object): ...@@ -17,7 +19,7 @@ class NotImplementedPartitionScheme(object):
""" """
@classmethod @classmethod
def get_group_for_user(cls, course_key, user, user_partition, assign=True, track_function=None): # pylint: disable=unused-argument def get_group_for_user(cls, course_key, user, user_partition, assign=True): # pylint: disable=unused-argument
""" """
Returning None is equivalent to saying "This user is not in any groups Returning None is equivalent to saying "This user is not in any groups
using this partition scheme", be sure the scheme you're removing is using this partition scheme", be sure the scheme you're removing is
...@@ -31,7 +33,7 @@ class ReturnGroup1PartitionScheme(object): ...@@ -31,7 +33,7 @@ class ReturnGroup1PartitionScheme(object):
This scheme is needed to allow verification partitions to be killed, see EDUCATOR-199 This scheme is needed to allow verification partitions to be killed, see EDUCATOR-199
""" """
@classmethod @classmethod
def get_group_for_user(cls, course_key, user, user_partition, assign=True, track_function=None): # pylint: disable=unused-argument def get_group_for_user(cls, course_key, user, user_partition, assign=True): # pylint: disable=unused-argument
""" """
The previous "allow" definition for verification was defined as 1, so return that. The previous "allow" definition for verification was defined as 1, so return that.
Details at https://github.com/edx/edx-platform/pull/14913/files#diff-feff1466ec4d1b8c38894310d8342a80 Details at https://github.com/edx/edx-platform/pull/14913/files#diff-feff1466ec4d1b8c38894310d8342a80
...@@ -46,7 +48,7 @@ class RandomUserPartitionScheme(object): ...@@ -46,7 +48,7 @@ class RandomUserPartitionScheme(object):
RANDOM = random.Random() RANDOM = random.Random()
@classmethod @classmethod
def get_group_for_user(cls, course_key, user, user_partition, assign=True, track_function=None): def get_group_for_user(cls, course_key, user, user_partition, assign=True):
""" """
Returns the group from the specified user position to which the user is assigned. Returns the group from the specified user position to which the user is assigned.
If the user has not yet been assigned, a group will be randomly chosen for them if assign flag is True. If the user has not yet been assigned, a group will be randomly chosen for them if assign flag is True.
...@@ -83,20 +85,24 @@ class RandomUserPartitionScheme(object): ...@@ -83,20 +85,24 @@ class RandomUserPartitionScheme(object):
# persist the value as a course tag # persist the value as a course tag
course_tag_api.set_course_tag(user, course_key, partition_key, group.id) course_tag_api.set_course_tag(user, course_key, partition_key, group.id)
if track_function: # emit event for analytics
# emit event for analytics # FYI - context is always user ID that is logged in, NOT the user id that is
# FYI - context is always user ID that is logged in, NOT the user id that is # being operated on. If instructor can move user explicitly, then we should
# being operated on. If instructor can move user explicitly, then we should # put in event_info the user id that is being operated on.
# put in event_info the user id that is being operated on. event_name = 'xmodule.partitions.assigned_user_to_partition'
event_info = { event_info = {
'group_id': group.id, 'group_id': group.id,
'group_name': group.name, 'group_name': group.name,
'partition_id': user_partition.id, 'partition_id': user_partition.id,
'partition_name': user_partition.name 'partition_name': user_partition.name
} }
# pylint: disable=fixme # pylint: disable=fixme
# TODO: Use the XBlock publish api instead # TODO: Use the XBlock publish api instead
track_function('xmodule.partitions.assigned_user_to_partition', event_info) with tracker.get_tracker().context(event_name, {}):
tracker.emit(
event_name,
event_info,
)
return group return group
......
""" """
Defines URLs for the course experience. Defines URLs for course bookmarks.
""" """
from django.conf.urls import url from django.conf.urls import url
......
...@@ -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,
......
...@@ -32,8 +32,10 @@ class CourseOutlineFragmentView(EdxFragmentView): ...@@ -32,8 +32,10 @@ class CourseOutlineFragmentView(EdxFragmentView):
'csrf': csrf(request)['csrf_token'], 'csrf': csrf(request)['csrf_token'],
'course': course_overview, 'course': course_overview,
'blocks': course_block_tree, 'blocks': course_block_tree,
# TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts
'upgrade_link': check_and_get_upgrade_link(request, request.user, course_key), 'upgrade_link': check_and_get_upgrade_link(request, request.user, course_key),
'upgrade_price': get_cosmetic_verified_display_price(course_overview), 'upgrade_price': get_cosmetic_verified_display_price(course_overview),
# ENDTODO
} }
html = render_to_string('course_experience/course-outline-fragment.html', context) html = render_to_string('course_experience/course-outline-fragment.html', context)
return Fragment(html) return Fragment(html)
<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>
(function(define) { (function(define) {
'use strict';
define([ define([
'underscore',
'backbone', 'backbone',
'js/search/base/models/search_result' 'course_search/js/models/search_result'
], function(Backbone, SearchResult) { ], function(_, Backbone, SearchResult) {
'use strict';
return Backbone.Collection.extend({ return Backbone.Collection.extend({
model: SearchResult, model: SearchResult,
...@@ -26,7 +27,9 @@ ...@@ -26,7 +27,9 @@
}, },
performSearch: function(searchTerm) { performSearch: function(searchTerm) {
this.fetchXhr && this.fetchXhr.abort(); if (this.fetchXhr) {
this.fetchXhr.abort();
}
this.searchTerm = searchTerm || ''; this.searchTerm = searchTerm || '';
this.resetState(); this.resetState();
this.fetchXhr = this.fetch({ this.fetchXhr = this.fetch({
...@@ -36,17 +39,19 @@ ...@@ -36,17 +39,19 @@
page_index: 0 page_index: 0
}, },
type: 'POST', type: 'POST',
success: function(self, xhr) { success: function(self) {
self.trigger('search'); self.trigger('search');
}, },
error: function(self, xhr) { error: function(self) {
self.trigger('error'); self.trigger('error');
} }
}); });
}, },
loadNextPage: function() { loadNextPage: function() {
this.fetchXhr && this.fetchXhr.abort(); if (this.fetchXhr) {
this.fetchXhr.abort();
}
this.fetchXhr = this.fetch({ this.fetchXhr = this.fetch({
data: { data: {
search_string: this.searchTerm, search_string: this.searchTerm,
...@@ -54,11 +59,11 @@ ...@@ -54,11 +59,11 @@
page_index: this.page + 1 page_index: this.page + 1
}, },
type: 'POST', type: 'POST',
success: function(self, xhr) { success: function(self) {
self.page += 1; self.page += 1; // eslint-disable-line no-param-reassign
self.trigger('next'); self.trigger('next');
}, },
error: function(self, xhr) { error: function(self) {
self.trigger('error'); self.trigger('error');
}, },
add: true, add: true,
...@@ -68,7 +73,9 @@ ...@@ -68,7 +73,9 @@
}, },
cancelSearch: function() { cancelSearch: function() {
this.fetchXhr && this.fetchXhr.abort(); if (this.fetchXhr) {
this.fetchXhr.abort();
}
this.resetState(); this.resetState();
}, },
...@@ -101,4 +108,4 @@ ...@@ -101,4 +108,4 @@
}); });
}); });
})(define || RequireJS.define); }(define || RequireJS.define));
(function(define) { (function(define) {
'use strict'; 'use strict';
define(['backbone', 'js/search/base/routers/search_router', 'js/search/course/views/search_form', define([
'js/search/base/collections/search_collection', 'js/search/course/views/search_results_view'], 'underscore', 'backbone', 'course_search/js/search_router', 'course_search/js/views/search_form',
function(Backbone, SearchRouter, CourseSearchForm, SearchCollection, SearchResultsView) { 'course_search/js/collections/search_collection', 'course_search/js/views/course_search_results_view'
return function(courseId) { ],
function(_, Backbone, SearchRouter, CourseSearchForm, SearchCollection, CourseSearchResultsView) {
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 SearchResultsView({collection: collection}); var results = new CourseSearchResultsView({collection: collection});
var dispatcher = _.clone(Backbone.Events); var dispatcher = _.clone(Backbone.Events);
dispatcher.listenTo(router, 'search', function(query) { dispatcher.listenTo(router, 'search', function(query) {
...@@ -42,6 +50,11 @@ ...@@ -42,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));
(function(define) { (function(define) {
'use strict'; 'use strict';
define(['backbone', 'js/search/base/routers/search_router', 'js/search/dashboard/views/search_form', define([
'js/search/base/collections/search_collection', 'js/search/dashboard/views/search_results_view'], 'underscore', 'backbone', 'course_search/js/search_router', 'course_search/js/views/search_form',
function(Backbone, SearchRouter, SearchForm, SearchCollection, SearchListView) { 'course_search/js/collections/search_collection', 'course_search/js/views/dashboard_search_results_view'
],
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')
});
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) {
...@@ -48,4 +52,4 @@ ...@@ -48,4 +52,4 @@
}); });
}; };
}); });
})(define || RequireJS.define); }(define || RequireJS.define));
(function(define) { (function(define) {
define(['backbone'], function(Backbone) { 'use strict';
'use strict';
define(['backbone'], function(Backbone) {
return Backbone.Model.extend({ return Backbone.Model.extend({
defaults: { defaults: {
location: [], location: [],
...@@ -11,4 +11,4 @@ ...@@ -11,4 +11,4 @@
} }
}); });
}); });
})(define || RequireJS.define); }(define || RequireJS.define));
(function(define) { (function(define) {
define(['backbone'], function(Backbone) { 'use strict';
'use strict';
define(['backbone'], function(Backbone) {
return Backbone.Router.extend({ return Backbone.Router.extend({
routes: { routes: {
'search/:query': 'search' 'search/:query': 'search'
...@@ -11,4 +11,4 @@ ...@@ -11,4 +11,4 @@
} }
}); });
}); });
})(define || RequireJS.define); }(define || RequireJS.define));
(function(define) { (function(define) {
define([ 'use strict';
'js/search/base/views/search_results_view',
'js/search/course/views/search_item_view'
], function(SearchResultsView, CourseSearchItemView) {
'use strict';
define([
'course_search/js/views/search_results_view',
'text!course_search/templates/course_search_results.underscore',
'text!course_search/templates/course_search_item.underscore'
], function(
SearchResultsView,
courseSearchResultsTemplate,
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',
resultsTemplateId: '#course_search_results-tpl', resultsTemplate: courseSearchResultsTemplate,
loadingTemplateId: '#search_loading-tpl', itemTemplate: courseSearchItemTemplate,
errorTemplateId: '#search_error-tpl',
events: { events: {
'click .search-load-next': 'loadNext' 'click .search-load-next': 'loadNext'
}, },
SearchItemView: CourseSearchItemView,
clear: function() { clear: function() {
SearchResultsView.prototype.clear.call(this); SearchResultsView.prototype.clear.call(this);
...@@ -31,4 +33,4 @@ ...@@ -31,4 +33,4 @@
}); });
}); });
})(define || RequireJS.define); }(define || RequireJS.define));
(function(define) { (function(define) {
define([ 'use strict';
'js/search/base/views/search_results_view',
'js/search/dashboard/views/search_item_view'
], function(SearchResultsView, DashSearchItemView) {
'use strict';
define([
'course_search/js/views/search_results_view',
'text!course_search/templates/dashboard_search_results.underscore',
'text!course_search/templates/dashboard_search_item.underscore'
], function(
SearchResultsView,
dashboardSearchResultsTemplate,
dashboardSearchItemTemplate
) {
return SearchResultsView.extend({ return SearchResultsView.extend({
el: '.search-results',
el: '#dashboard-search-results',
contentElement: '#my-courses, #profile-sidebar', contentElement: '#my-courses, #profile-sidebar',
resultsTemplateId: '#dashboard_search_results-tpl', resultsTemplate: dashboardSearchResultsTemplate,
loadingTemplateId: '#search_loading-tpl', itemTemplate: dashboardSearchItemTemplate,
errorTemplateId: '#search_error-tpl',
events: { events: {
'click .search-load-next': 'loadNext', 'click .search-load-next': 'loadNext',
'click .search-back-to-courses': 'backToCourses' 'click .search-back-to-courses': 'backToCourses'
}, },
SearchItemView: DashSearchItemView,
backToCourses: function() { backToCourses: function() {
this.clear(); this.clear();
...@@ -26,4 +28,4 @@ ...@@ -26,4 +28,4 @@
}); });
}); });
})(define || RequireJS.define); }(define || RequireJS.define));
(function(define) { (function(define) {
define(['jquery', 'backbone'], function($, Backbone) { 'use strict';
'use strict';
define(['jquery', 'backbone'], function($, Backbone) {
return Backbone.View.extend({ return Backbone.View.extend({
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,19 +23,17 @@ ...@@ -22,19 +23,17 @@
}, },
doSearch: function(term) { doSearch: function(term) {
var trimmedTerm;
if (term) { if (term) {
this.$searchField.val(term); trimmedTerm = term.trim();
this.$searchField.val(trimmedTerm);
} else {
trimmedTerm = this.$searchField.val().trim();
} }
else { if (trimmedTerm) {
term = this.$searchField.val();
}
var trimmed = $.trim(term);
if (trimmed) {
this.setActiveStyle(); this.setActiveStyle();
this.trigger('search', trimmed); this.trigger('search', trimmedTerm);
} } else {
else {
this.clearSearch(); this.clearSearch();
} }
}, },
...@@ -50,17 +49,20 @@ ...@@ -50,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));
(function(define) { (function(define) {
'use strict';
define([ define([
'jquery', 'jquery',
'underscore', 'underscore',
'backbone', 'backbone',
'gettext', 'gettext',
'logger' 'logger',
], function($, _, Backbone, gettext, Logger) { 'edx-ui-toolkit/js/utils/html-utils'
'use strict'; ], function($, _, Backbone, gettext, Logger, HtmlUtils) {
return Backbone.View.extend({ return Backbone.View.extend({
tagName: 'li', tagName: 'li',
templateId: '',
className: 'search-results-item', className: 'search-results-item',
attributes: { attributes: {
'role': 'region', role: 'region',
'aria-label': 'search result' 'aria-label': 'search result'
}, },
events: { events: {
'click': 'logSearchItem' click: 'logSearchItem'
}, },
initialize: function() { initialize: function(options) {
this.tpl = _.template($(this.templateId).html()); this.template = options.template;
}, },
render: function() { render: function() {
var data = _.clone(this.model.attributes); var data = _.clone(this.model.attributes);
// Drop the preview text and result type if the search term is found
// in the title/location in the course hierarchy // Drop the preview text and result type if the search term is found
// in the title/location in the course hierarchy
if (this.model.get('content_type') === 'Sequence') { if (this.model.get('content_type') === 'Sequence') {
data.excerpt = ''; data.excerpt = '';
data.content_type = ''; data.content_type = '';
} }
this.$el.html(this.tpl(data)); data.excerptHtml = HtmlUtils.HTML(data.excerpt);
delete data.excerpt;
HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.template)(data));
return this; return this;
}, },
...@@ -47,7 +50,6 @@ ...@@ -47,7 +50,6 @@
}, },
logSearchItem: function(event) { logSearchItem: function(event) {
event.preventDefault();
var self = this; var self = this;
var target = this.model.id; var target = this.model.id;
var link = this.model.get('url'); var link = this.model.get('url');
...@@ -56,10 +58,13 @@ ...@@ -56,10 +58,13 @@
var pageSize = collection.pageSize; var pageSize = collection.pageSize;
var searchTerm = collection.searchTerm; var searchTerm = collection.searchTerm;
var index = collection.indexOf(this.model); var index = collection.indexOf(this.model);
event.preventDefault();
Logger.log('edx.course.search.result_selected', { Logger.log('edx.course.search.result_selected', {
'search_term': searchTerm, search_term: searchTerm,
'result_position': (page * pageSize + index), result_position: (page * pageSize) + index,
'result_link': target result_link: target
}).always(function() { }).always(function() {
self.redirect(link); self.redirect(link);
}); });
...@@ -67,4 +72,4 @@ ...@@ -67,4 +72,4 @@
}); });
}); });
})(define || RequireJS.define); }(define || RequireJS.define));
(function(define) { (function(define) {
'use strict';
define([ define([
'jquery', 'jquery',
'underscore', 'underscore',
'backbone', 'backbone',
'gettext' 'edx-ui-toolkit/js/utils/html-utils',
], function($, _, Backbone, gettext) { 'edx-ui-toolkit/js/utils/string-utils',
'use strict'; 'course_search/js/views/search_item_view',
'text!course_search/templates/search_loading.underscore',
'text!course_search/templates/search_error.underscore'
], function($, _, Backbone, HtmlUtils, StringUtils, SearchItemView, searchLoadingTemplate, searchErrorTemplate) {
return Backbone.View.extend({ return Backbone.View.extend({
// these should be defined by subclasses // these should be defined by subclasses
el: '', el: '',
contentElement: '', contentElement: '',
resultsTemplateId: '', resultsTemplate: null,
loadingTemplateId: '', itemTemplate: null,
errorTemplateId: '', loadingTemplate: searchLoadingTemplate,
errorTemplate: searchErrorTemplate,
events: {}, events: {},
spinner: '.search-load-next .icon', spinner: '.search-load-next .icon',
SearchItemView: function() {},
initialize: function() { initialize: function() {
this.$contentElement = $(this.contentElement); this.$contentElement = this.contentElement ? $(this.contentElement) : $([]);
this.resultsTemplate = _.template($(this.resultsTemplateId).html());
this.loadingTemplate = _.template($(this.loadingTemplateId).html());
this.errorTemplate = _.template($(this.errorTemplateId).html());
}, },
render: function() { render: function() {
this.$el.html(this.resultsTemplate({ HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.resultsTemplate)({
totalCount: this.collection.totalCount, totalCount: this.collection.totalCount,
totalCountMsg: this.totalCountMsg(), totalCountMsg: this.totalCountMsg(),
pageSize: this.collection.pageSize, pageSize: this.collection.pageSize,
...@@ -40,10 +41,10 @@ ...@@ -40,10 +41,10 @@
}, },
renderNext: function() { renderNext: function() {
// total count may have changed // total count may have changed
this.$el.find('.search-count').text(this.totalCountMsg()); this.$el.find('.search-count').text(this.totalCountMsg());
this.renderItems(); this.renderItems();
if (! this.collection.hasNextPage()) { if (!this.collection.hasNextPage()) {
this.$el.find('.search-load-next').remove(); this.$el.find('.search-load-next').remove();
} }
this.$el.find(this.spinner).hide(); this.$el.find(this.spinner).hide();
...@@ -52,15 +53,21 @@ ...@@ -52,15 +53,21 @@
renderItems: function() { renderItems: function() {
var latest = this.collection.latestModels(); var latest = this.collection.latestModels();
var items = latest.map(function(result) { var items = latest.map(function(result) {
var item = new this.SearchItemView({model: result}); var item = new SearchItemView({
model: result,
template: this.itemTemplate
});
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);
}, },
totalCountMsg: function() { totalCountMsg: function() {
var fmt = ngettext('%s result', '%s results', this.collection.totalCount); var fmt = ngettext('{total_results} result', '{total_results} results', this.collection.totalCount);
return interpolate(fmt, [this.collection.totalCount]); return StringUtils.interpolate(fmt, {
total_results: this.collection.totalCount
});
}, },
clear: function() { clear: function() {
...@@ -74,27 +81,26 @@ ...@@ -74,27 +81,26 @@
}, },
showLoadingMessage: function() { showLoadingMessage: function() {
this.doCleanup(); // Empty any previous loading/error message
this.$el.html(this.loadingTemplate()); $('#loading-message').html('');
$('#error-message').html('');
// Show the loading message
HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.loadingTemplate)());
// Show the results
this.showResults(); this.showResults();
}, },
showErrorMessage: function() { showErrorMessage: function() {
this.$el.html(this.errorTemplate()); HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.errorTemplate)());
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) {
event && event.preventDefault(); if (event) {
event.preventDefault();
}
this.$el.find(this.spinner).show(); this.$el.find(this.spinner).show();
this.trigger('next'); this.trigger('next');
return false; return false;
...@@ -102,4 +108,4 @@ ...@@ -102,4 +108,4 @@
}); });
}); });
})(define || RequireJS.define); }(define || RequireJS.define));
<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)
...@@ -57,7 +57,7 @@ edx-oauth2-provider==1.2.0 ...@@ -57,7 +57,7 @@ edx-oauth2-provider==1.2.0
edx-opaque-keys==0.4.0 edx-opaque-keys==0.4.0
edx-organizations==0.4.4 edx-organizations==0.4.4
edx-rest-api-client==1.7.1 edx-rest-api-client==1.7.1
edx-search==0.1.2 edx-search==1.0.1
facebook-sdk==0.4.0 facebook-sdk==0.4.0
feedparser==5.1.3 feedparser==5.1.3
firebase-token-generator==1.3.2 firebase-token-generator==1.3.2
......
...@@ -75,7 +75,7 @@ git+https://github.com/edx/lettuce.git@0.2.20.002#egg=lettuce==0.2.20.002 ...@@ -75,7 +75,7 @@ git+https://github.com/edx/lettuce.git@0.2.20.002#egg=lettuce==0.2.20.002
-e git+https://github.com/edx/event-tracking.git@0.2.1#egg=event-tracking==0.2.1 -e git+https://github.com/edx/event-tracking.git@0.2.1#egg=event-tracking==0.2.1
-e git+https://github.com/edx/django-splash.git@v0.2#egg=django-splash==0.2 -e git+https://github.com/edx/django-splash.git@v0.2#egg=django-splash==0.2
-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock -e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
git+https://github.com/edx/edx-ora2.git@1.4.1#egg=ora2==1.4.1 git+https://github.com/edx/edx-ora2.git@1.4.2#egg=ora2==1.4.2
-e git+https://github.com/edx/edx-submissions.git@2.0.0#egg=edx-submissions==2.0.0 -e git+https://github.com/edx/edx-submissions.git@2.0.0#egg=edx-submissions==2.0.0
git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3 git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3
git+https://github.com/edx/edx-val.git@0.0.13#egg=edxval==0.0.13 git+https://github.com/edx/edx-val.git@0.0.13#egg=edxval==0.0.13
......
...@@ -29,12 +29,6 @@ from openedx.core.djangoapps.theming import helpers as theming_helpers ...@@ -29,12 +29,6 @@ from openedx.core.djangoapps.theming import helpers as theming_helpers
<%static:include path="dashboard/${template_name}.underscore" /> <%static:include path="dashboard/${template_name}.underscore" />
</script> </script>
% endfor % endfor
% for template_name in ["dashboard_search_item", "dashboard_search_results", "search_loading", "search_error"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="search/${template_name}.underscore" />
</script>
% endfor
</%block> </%block>
<%block name="js_extra"> <%block name="js_extra">
...@@ -50,7 +44,7 @@ from openedx.core.djangoapps.theming import helpers as theming_helpers ...@@ -50,7 +44,7 @@ from openedx.core.djangoapps.theming import helpers as theming_helpers
}); });
</script> </script>
% if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'): % if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'):
<%static:require_module module_name="js/search/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
...@@ -164,7 +158,7 @@ from openedx.core.djangoapps.theming import helpers as theming_helpers ...@@ -164,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"/>
......
...@@ -94,23 +94,20 @@ ...@@ -94,23 +94,20 @@
</div> </div>
</div> </div>
</footer> </footer>
% if include_dependencies: % if include_dependencies:
<%static:js group='base_vendor'/> <%static:js group='base_vendor'/>
<%static:css group='style-vendor'/> <%static:css group='style-vendor'/>
<%include file="widgets/segment-io.html" /> <%include file="widgets/segment-io.html" />
<%include file="widgets/segment-io-footer.html" /> <%include file="widgets/segment-io-footer.html" />
% endif % endif
% if bidi == 'rtl': % if bidi == 'rtl':
<%static:css group='style-lms-footer-edx-rtl'/> <%static:css group='style-lms-footer-edx-rtl'/>
% else: % else:
<%static:css group='style-lms-footer-edx'/> <%static:css group='style-lms-footer-edx'/>
% endif % endif
% if footer_css_urls:
% for url in footer_css_urls:
<link rel="stylesheet" type="text/css" href="${url}"></link>
% endfor
% endif
% if footer_js_url: % if footer_js_url:
<script type="text/javascript" src="${footer_js_url}"></script> <script type="text/javascript" src="${footer_js_url}"></script>
% endif % endif
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