Commit dbdb433f by Diana Huang Committed by GitHub

Merge pull request #14756 from edx/andya/new-bookmarks-page

Andya/new bookmarks page
parents 6a85cd1a 38d73e31
/* JavaScript for Vertical Student View. */
window.VerticalStudentView = function(runtime, element) {
'use strict';
RequireJS.require(['js/bookmarks/views/bookmark_button'], function(BookmarkButton) {
RequireJS.require(['course_bookmarks/js/views/bookmark_button'], function(BookmarkButton) {
var $element = $(element);
var $bookmarkButtonElement = $element.find('.bookmark-button');
......@@ -10,7 +10,7 @@ window.VerticalStudentView = function(runtime, element) {
bookmarkId: $bookmarkButtonElement.data('bookmarkId'),
usageId: $element.data('usageId'),
bookmarked: $element.parent('#seq_content').data('bookmarked'),
apiUrl: $('.courseware-bookmarks-button').data('bookmarksApiUrl')
apiUrl: $bookmarkButtonElement.data('bookmarksApiUrl')
});
});
};
......@@ -268,7 +268,17 @@
this.updatePageTitle();
sequenceLinks = this.content_container.find('a.seqnav');
sequenceLinks.click(this.goto);
this.path.text(this.el.find('.nav-item.active').data('path'));
edx.HtmlUtils.setHtml(
this.path,
edx.HtmlUtils.template($('#sequence-breadcrumbs-tpl').text())({
courseId: this.el.parent().data('course-id'),
blockId: this.id,
pathText: this.el.find('.nav-item.active').data('path'),
unifiedCourseView: this.path.data('unified-course-view')
})
);
this.sr_container.focus();
}
};
......
<% if (unifiedCourseView) { %>
<a href="<%- '/courses/' + courseId + '/course/#' + blockId %>">
<span class="fa fa-arrow-circle-left" aria-hidden="true" aria-describedby="outline-description"></span>
<span class="sr-only" id="outline-description"><%- gettext('Return to course outline') %></span>
<b><%- gettext('Outline') %></b>
</a>
<span> > </span>
<% } %>
<span class="position"><%- pathText %></span>
......@@ -227,9 +227,9 @@ class CourseFixture(XBlockContainerFixture):
self._configure_course()
@property
def course_outline(self):
def studio_course_outline_as_json(self):
"""
Retrieves course outline in JSON format.
Retrieves Studio course outline in JSON format.
"""
url = STUDIO_BASE_URL + '/course/' + self._course_key + "?format=json"
response = self.session.get(url, headers=self.headers)
......
......@@ -10,29 +10,23 @@ class BookmarksPage(CoursePage, PaginatedUIMixin):
"""
Courseware Bookmarks Page.
"""
url = None
url_path = "courseware/"
url_path = "bookmarks"
BOOKMARKS_BUTTON_SELECTOR = '.bookmarks-list-button'
BOOKMARKS_ELEMENT_SELECTOR = '#my-bookmarks'
BOOKMARKED_ITEMS_SELECTOR = '.bookmarks-results-list .bookmarks-results-list-item'
BOOKMARKED_BREADCRUMBS = BOOKMARKED_ITEMS_SELECTOR + ' .list-item-breadcrumbtrail'
def is_browser_on_page(self):
""" Verify if we are on correct page """
return self.q(css=self.BOOKMARKS_BUTTON_SELECTOR).visible
return self.q(css=self.BOOKMARKS_ELEMENT_SELECTOR).present
def bookmarks_button_visible(self):
""" Check if bookmarks button is visible """
return self.q(css=self.BOOKMARKS_BUTTON_SELECTOR).visible
def click_bookmarks_button(self, wait_for_results=True):
""" Click on Bookmarks button """
self.q(css=self.BOOKMARKS_BUTTON_SELECTOR).first.click()
if wait_for_results:
EmptyPromise(self.results_present, "Bookmarks results present").fulfill()
def results_present(self):
""" Check if bookmarks results are present """
return self.q(css='#my-bookmarks').present
return self.q(css=self.BOOKMARKS_ELEMENT_SELECTOR).present
def results_header_text(self):
""" Returns the bookmarks results header text """
......
"""
LMS Course Home page object
"""
from bok_choy.page_object import PageObject
from common.test.acceptance.pages.lms.bookmarks import BookmarksPage
from common.test.acceptance.pages.lms.course_page import CoursePage
from common.test.acceptance.pages.lms.courseware import CoursewarePage
class CourseHomePage(CoursePage):
"""
Course home page, including course outline.
"""
url_path = "course/"
def is_browser_on_page(self):
return self.q(css='.course-outline').present
def __init__(self, browser, course_id):
super(CourseHomePage, self).__init__(browser, course_id)
self.course_id = course_id
self.outline = CourseOutlinePage(browser, self)
# TODO: TNL-6546: Remove the following
self.unified_course_view = False
def click_bookmarks_button(self):
""" Click on Bookmarks button """
self.q(css='.bookmarks-list-button').first.click()
bookmarks_page = BookmarksPage(self.browser, self.course_id)
bookmarks_page.visit()
class CourseOutlinePage(PageObject):
"""
Course outline fragment of page.
"""
url = None
def __init__(self, browser, parent_page):
super(CourseOutlinePage, self).__init__(browser)
self.parent_page = parent_page
self.courseware_page = CoursewarePage(self.browser, self.parent_page.course_id)
def is_browser_on_page(self):
return self.parent_page.is_browser_on_page
@property
def sections(self):
"""
Return a dictionary representation of sections and subsections.
Example:
{
'Introduction': ['Course Overview'],
'Week 1': ['Lesson 1', 'Lesson 2', 'Homework']
'Final Exam': ['Final Exam']
}
You can use these titles in `go_to_section` to navigate to the section.
"""
# Dict to store the result
outline_dict = dict()
section_titles = self._section_titles()
# Get the section titles for each chapter
for sec_index, sec_title in enumerate(section_titles):
if len(section_titles) < 1:
self.warning("Could not find subsections for '{0}'".format(sec_title))
else:
# Add one to convert list index (starts at 0) to CSS index (starts at 1)
outline_dict[sec_title] = self._subsection_titles(sec_index + 1)
return outline_dict
def go_to_section(self, section_title, subsection_title):
"""
Go to the section in the courseware.
Every section must have at least one subsection, so specify
both the section and subsection title.
Example:
go_to_section("Week 1", "Lesson 1")
"""
# Get the section by index
try:
section_index = self._section_titles().index(section_title)
except ValueError:
self.warning("Could not find section '{0}'".format(section_title))
return
# Get the subsection by index
try:
subsection_index = self._subsection_titles(section_index + 1).index(subsection_title)
except ValueError:
msg = "Could not find subsection '{0}' in section '{1}'".format(subsection_title, section_title)
self.warning(msg)
return
# Convert list indices (start at zero) to CSS indices (start at 1)
subsection_css = (
".outline-item.section:nth-of-type({0}) .subsection:nth-of-type({1}) .outline-item"
).format(section_index + 1, subsection_index + 1)
# Click the subsection and ensure that the page finishes reloading
self.q(css=subsection_css).first.click()
self.courseware_page.wait_for_page()
# TODO: TNL-6546: Remove this if/visit_unified_course_view
if self.parent_page.unified_course_view:
self.courseware_page.nav.visit_unified_course_view()
self._wait_for_course_section(section_title, subsection_title)
def _section_titles(self):
"""
Return a list of all section titles on the page.
"""
section_css = '.section-name span'
return self.q(css=section_css).map(lambda el: el.text.strip()).results
def _subsection_titles(self, section_index):
"""
Return a list of all subsection titles on the page
for the section at index `section_index` (starts at 1).
"""
# Retrieve the subsection title for the section
# Add one to the list index to get the CSS index, which starts at one
subsection_css = (
# TODO: TNL-6387: Will need to switch to this selector for subsections
# ".outline-item.section:nth-of-type({0}) .subsection span:nth-of-type(1)"
".outline-item.section:nth-of-type({0}) .subsection a"
).format(section_index)
return self.q(
css=subsection_css
).map(
lambda el: el.get_attribute('innerHTML').strip()
).results
def _wait_for_course_section(self, section_title, subsection_title):
"""
Ensures the user navigates to the course content page with the correct section and subsection.
"""
self.wait_for(
promise_check_func=lambda: self.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)
)
"""
Course navigation page object
"""
import re
from bok_choy.page_object import PageObject, unguarded
from bok_choy.promise import EmptyPromise
class CourseNavPage(PageObject):
"""
Navigate sections and sequences in the courseware.
"""
url = None
def is_browser_on_page(self):
return self.q(css='div.course-index').present
@property
def sections(self):
"""
Return a dictionary representation of sections and subsections.
Example:
{
'Introduction': ['Course Overview'],
'Week 1': ['Lesson 1', 'Lesson 2', 'Homework']
'Final Exam': ['Final Exam']
}
You can use these titles in `go_to_section` to navigate to the section.
"""
# Dict to store the result
nav_dict = dict()
section_titles = self._section_titles()
# Get the section titles for each chapter
for sec_index, sec_title in enumerate(section_titles):
if len(section_titles) < 1:
self.warning("Could not find subsections for '{0}'".format(sec_title))
else:
# Add one to convert list index (starts at 0) to CSS index (starts at 1)
nav_dict[sec_title] = self._subsection_titles(sec_index + 1)
return nav_dict
@property
def sequence_items(self):
"""
Return a list of sequence items on the page.
Sequence items are one level below subsections in the course nav.
Example return value:
['Chemical Bonds Video', 'Practice Problems', 'Homework']
"""
seq_css = 'ol#sequence-list>li>.nav-item>.sequence-tooltip'
return self.q(css=seq_css).map(self._clean_seq_titles).results
def go_to_section(self, section_title, subsection_title):
"""
Go to the section in the courseware.
Every section must have at least one subsection, so specify
both the section and subsection title.
Example:
go_to_section("Week 1", "Lesson 1")
"""
# For test stability, disable JQuery animations (opening / closing menus)
self.browser.execute_script("jQuery.fx.off = true;")
# Get the section by index
try:
sec_index = self._section_titles().index(section_title)
except ValueError:
self.warning("Could not find section '{0}'".format(section_title))
return
# Click the section to ensure it's open (no harm in clicking twice if it's already open)
# Add one to convert from list index to CSS index
section_css = '.course-navigation .chapter:nth-of-type({0})'.format(sec_index + 1)
self.q(css=section_css).first.click()
# Get the subsection by index
try:
subsec_index = self._subsection_titles(sec_index + 1).index(subsection_title)
except ValueError:
msg = "Could not find subsection '{0}' in section '{1}'".format(subsection_title, section_title)
self.warning(msg)
return
# Convert list indices (start at zero) to CSS indices (start at 1)
subsection_css = (
".course-navigation .chapter-content-container:nth-of-type({0}) "
".menu-item:nth-of-type({1})"
).format(sec_index + 1, subsec_index + 1)
# Click the subsection and ensure that the page finishes reloading
self.q(css=subsection_css).first.click()
self._on_section_promise(section_title, subsection_title).fulfill()
def go_to_vertical(self, vertical_title):
"""
Within a section/subsection, navigate to the vertical with `vertical_title`.
"""
# Get the index of the item in the sequence
all_items = self.sequence_items
try:
seq_index = all_items.index(vertical_title)
except ValueError:
msg = "Could not find sequential '{0}'. Available sequentials: [{1}]".format(
vertical_title, ", ".join(all_items)
)
self.warning(msg)
else:
# Click on the sequence item at the correct index
# Convert the list index (starts at 0) to a CSS index (starts at 1)
seq_css = "ol#sequence-list>li:nth-of-type({0})>.nav-item".format(seq_index + 1)
self.q(css=seq_css).first.click()
# Click triggers an ajax event
self.wait_for_ajax()
def _section_titles(self):
"""
Return a list of all section titles on the page.
"""
chapter_css = '.course-navigation .chapter .group-heading'
return self.q(css=chapter_css).map(lambda el: el.text.strip()).results
def _subsection_titles(self, section_index):
"""
Return a list of all subsection titles on the page
for the section at index `section_index` (starts at 1).
"""
# Retrieve the subsection title for the section
# Add one to the list index to get the CSS index, which starts at one
subsection_css = (
".course-navigation .chapter-content-container:nth-of-type({0}) "
".menu-item a p:nth-of-type(1)"
).format(section_index)
# If the element is visible, we can get its text directly
# Otherwise, we need to get the HTML
# It *would* make sense to always get the HTML, but unfortunately
# the open tab has some child <span> tags that we don't want.
return self.q(
css=subsection_css
).map(
lambda el: el.text.strip().split('\n')[0] if el.is_displayed() else el.get_attribute('innerHTML').strip()
).results
def _on_section_promise(self, section_title, subsection_title):
"""
Return a `Promise` that is fulfilled when the user is on
the correct section and subsection.
"""
desc = "currently at section '{0}' and subsection '{1}'".format(section_title, subsection_title)
return EmptyPromise(
lambda: self.is_on_section(section_title, subsection_title), desc
)
@unguarded
def is_on_section(self, section_title, subsection_title):
"""
Return a boolean indicating whether the user is on the section and subsection
with the specified titles.
This assumes that the currently expanded section is the one we're on
That's true right after we click the section/subsection, but not true in general
(the user could go to a section, then expand another tab).
"""
current_section_list = self.q(css='.course-navigation .chapter.is-open .group-heading').text
current_subsection_list = self.q(css='.course-navigation .chapter-content-container .menu-item.active a p').text
if len(current_section_list) == 0:
self.warning("Could not find the current section")
return False
elif len(current_subsection_list) == 0:
self.warning("Could not find current subsection")
return False
else:
return (
current_section_list[0].strip() == section_title and
current_subsection_list[0].strip().split('\n')[0] == subsection_title
)
# Regular expression to remove HTML span tags from a string
REMOVE_SPAN_TAG_RE = re.compile(r'</span>(.+)<span')
def _clean_seq_titles(self, element):
"""
Clean HTML of sequence titles, stripping out span tags and returning the first line.
"""
return self.REMOVE_SPAN_TAG_RE.search(element.get_attribute('innerHTML')).groups()[0].strip()
@property
def active_subsection_url(self):
"""
return the url of the active subsection in the left nav
"""
return self.q(css='.chapter-content-container .menu-item.active a').attrs('href')[0]
......@@ -7,10 +7,11 @@ from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureD
from common.test.acceptance.fixtures.certificates import CertificateConfigFixture
from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage
from common.test.acceptance.pages.lms.certificate_page import CertificatePage
from common.test.acceptance.pages.lms.course_home import CourseHomePage
from common.test.acceptance.pages.lms.course_info import CourseInfoPage
from common.test.acceptance.pages.lms.tab_nav import TabNavPage
from common.test.acceptance.pages.lms.course_nav import CourseNavPage
from common.test.acceptance.pages.lms.courseware import CoursewarePage
from common.test.acceptance.pages.lms.progress import ProgressPage
from common.test.acceptance.pages.lms.tab_nav import TabNavPage
@attr(shard=5)
......@@ -154,7 +155,8 @@ class CertificateProgressPageTest(UniqueCourseTest):
self.course_info_page = CourseInfoPage(self.browser, self.course_id)
self.progress_page = ProgressPage(self.browser, self.course_id)
self.course_nav = CourseNavPage(self.browser)
self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.course_home_page = CourseHomePage(self.browser, self.course_id)
self.tab_nav = TabNavPage(self.browser)
def log_in_as_unique_user(self):
......@@ -205,38 +207,42 @@ class CertificateProgressPageTest(UniqueCourseTest):
Problems were added in the setUp
"""
self.course_info_page.visit()
self.tab_nav.go_to_tab('Course')
# self.course_info_page.visit()
# self.tab_nav.go_to_tab('Course')
#
# # TODO: TNL-6546: Remove extra visit call.
self.course_home_page.visit()
# Navigate to Test Subsection in Test Section Section
self.course_nav.go_to_section('Test Section', 'Test Subsection')
self.course_home_page.outline.go_to_section('Test Section', 'Test Subsection')
# Navigate to Test Problem 1
self.course_nav.go_to_vertical('Test Problem 1')
self.courseware_page.nav.go_to_vertical('Test Problem 1')
# Select correct value for from select menu
self.course_nav.q(css='select option[value="{}"]'.format('blue')).first.click()
self.courseware_page.q(css='select option[value="{}"]'.format('blue')).first.click()
# Select correct radio button for the answer
self.course_nav.q(css='fieldset div.field:nth-child(4) input').nth(0).click()
self.courseware_page.q(css='fieldset div.field:nth-child(4) input').nth(0).click()
# Select correct radio buttons for the answer
self.course_nav.q(css='fieldset div.field:nth-child(2) input').nth(1).click()
self.course_nav.q(css='fieldset div.field:nth-child(4) input').nth(1).click()
self.courseware_page.q(css='fieldset div.field:nth-child(2) input').nth(1).click()
self.courseware_page.q(css='fieldset div.field:nth-child(4) input').nth(1).click()
# Submit the answer
self.course_nav.q(css='button.submit').click()
self.course_nav.wait_for_ajax()
self.courseware_page.q(css='button.submit').click()
self.courseware_page.wait_for_ajax()
# Navigate to the 'Test Subsection 2' of 'Test Section 2'
self.course_nav.go_to_section('Test Section 2', 'Test Subsection 2')
self.course_home_page.visit()
self.course_home_page.outline.go_to_section('Test Section 2', 'Test Subsection 2')
# Navigate to Test Problem 2
self.course_nav.go_to_vertical('Test Problem 2')
self.courseware_page.nav.go_to_vertical('Test Problem 2')
# Fill in the answer of the problem
self.course_nav.q(css='input[id^=input_][id$=_2_1]').fill('A*x^2 + sqrt(y)')
self.courseware_page.q(css='input[id^=input_][id$=_2_1]').fill('A*x^2 + sqrt(y)')
# Submit the answer
self.course_nav.q(css='button.submit').click()
self.course_nav.wait_for_ajax()
self.courseware_page.q(css='button.submit').click()
self.courseware_page.wait_for_ajax()
......@@ -8,7 +8,7 @@ import textwrap
from nose.plugins.attrib import attr
from common.test.acceptance.tests.helpers import UniqueCourseTest, TestWithSearchIndexMixin
from common.test.acceptance.pages.studio.auto_auth import AutoAuthPage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
from common.test.acceptance.pages.studio.library import StudioLibraryContentEditor, StudioLibraryContainerXBlockWrapper
from common.test.acceptance.pages.lms.courseware import CoursewarePage
from common.test.acceptance.pages.lms.library import LibraryContentXBlockWrapper
......@@ -44,7 +44,7 @@ class LibraryContentTestBase(UniqueCourseTest):
self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.course_outline = CourseOutlinePage(
self.studio_course_outline = StudioCourseOutlinePage(
self.browser,
self.course_info['org'],
self.course_info['number'],
......@@ -116,9 +116,9 @@ class LibraryContentTestBase(UniqueCourseTest):
if change_login:
LogoutPage(self.browser).visit()
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
self.course_outline.visit()
self.studio_course_outline.visit()
subsection = self.course_outline.section(SECTION_NAME).subsection(SUBSECTION_NAME)
subsection = self.studio_course_outline.section(SECTION_NAME).subsection(SUBSECTION_NAME)
return subsection.expand_subsection().unit(UNIT_NAME).go_to()
def _goto_library_block_page(self, block_id=None):
......
......@@ -7,7 +7,7 @@ import uuid
from common.test.acceptance.tests.helpers import remove_file
from common.test.acceptance.pages.common.logout import LogoutPage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
from common.test.acceptance.pages.lms.courseware_search import CoursewareSearchPage
from common.test.acceptance.pages.lms.staff_view import StaffPage
from common.test.acceptance.fixtures.course import XBlockFixtureDesc
......@@ -45,7 +45,7 @@ class CoursewareSearchCohortTest(ContainerBase):
super(CoursewareSearchCohortTest, self).setUp(is_staff=is_staff)
self.staff_user = self.user
self.course_outline = CourseOutlinePage(
self.studio_course_outline = StudioCourseOutlinePage(
self.browser,
self.course_info['org'],
self.course_info['number'],
......@@ -101,9 +101,9 @@ class CoursewareSearchCohortTest(ContainerBase):
Reindex course content on studio course page
"""
self._auto_auth(self.staff_user["username"], self.staff_user["email"], True)
self.course_outline.visit()
self.course_outline.start_reindex()
self.course_outline.wait_for_ajax()
self.studio_course_outline.visit()
self.studio_course_outline.start_reindex()
self.studio_course_outline.wait_for_ajax()
def _goto_staff_page(self):
"""
......
......@@ -11,7 +11,7 @@ from common.test.acceptance.pages.common.logout import LogoutPage
from common.test.acceptance.pages.common.utils import click_css
from common.test.acceptance.pages.studio.utils import add_html_component, type_in_codemirror
from common.test.acceptance.pages.studio.auto_auth import AutoAuthPage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
from common.test.acceptance.pages.studio.container import ContainerPage
from common.test.acceptance.pages.lms.courseware_search import CoursewareSearchPage
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
......@@ -54,7 +54,7 @@ class CoursewareSearchTest(UniqueCourseTest):
super(CoursewareSearchTest, self).setUp()
self.courseware_search_page = CoursewareSearchPage(self.browser, self.course_id)
self.course_outline = CourseOutlinePage(
self.studio_course_outline = StudioCourseOutlinePage(
self.browser,
self.course_info['org'],
self.course_info['number'],
......@@ -91,8 +91,8 @@ class CoursewareSearchTest(UniqueCourseTest):
Publish content on studio course page under specified section
"""
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
self.course_outline.visit()
subsection = self.course_outline.section_at(section_index).subsection_at(0)
self.studio_course_outline.visit()
subsection = self.studio_course_outline.section_at(section_index).subsection_at(0)
subsection.expand_subsection()
unit = subsection.unit_at(0)
unit.publish()
......@@ -102,8 +102,8 @@ class CoursewareSearchTest(UniqueCourseTest):
Edit chapter name on studio course page under specified section
"""
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
self.course_outline.visit()
section = self.course_outline.section_at(section_index)
self.studio_course_outline.visit()
section = self.studio_course_outline.section_at(section_index)
section.change_name(self.EDITED_CHAPTER_NAME)
def _studio_add_content(self, section_index):
......@@ -113,8 +113,8 @@ class CoursewareSearchTest(UniqueCourseTest):
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
# create a unit in course outline
self.course_outline.visit()
subsection = self.course_outline.section_at(section_index).subsection_at(0)
self.studio_course_outline.visit()
subsection = self.studio_course_outline.section_at(section_index).subsection_at(0)
subsection.expand_subsection()
subsection.add_unit()
......@@ -134,9 +134,9 @@ class CoursewareSearchTest(UniqueCourseTest):
"""
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
self.course_outline.visit()
self.course_outline.start_reindex()
self.course_outline.wait_for_ajax()
self.studio_course_outline.visit()
self.studio_course_outline.start_reindex()
self.studio_course_outline.wait_for_ajax()
def _search_for_content(self, search_term):
"""
......
......@@ -9,7 +9,7 @@ from common.test.acceptance.pages.common.logout import LogoutPage
from common.test.acceptance.pages.common.utils import click_css
from common.test.acceptance.pages.studio.utils import add_html_component, type_in_codemirror
from common.test.acceptance.pages.studio.auto_auth import AutoAuthPage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
from common.test.acceptance.pages.studio.container import ContainerPage
from common.test.acceptance.pages.lms.dashboard_search import DashboardSearchPage
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
......@@ -60,10 +60,10 @@ class DashboardSearchTest(AcceptanceTest):
}
# generate course fixtures and outline pages
self.course_outlines = {}
self.studio_course_outlines = {}
self.course_fixtures = {}
for key, course_info in self.courses.iteritems():
course_outline = CourseOutlinePage(
studio_course_outline = StudioCourseOutlinePage(
self.browser,
course_info['org'],
course_info['number'],
......@@ -89,7 +89,7 @@ class DashboardSearchTest(AcceptanceTest):
)
).install()
self.course_outlines[key] = course_outline
self.studio_course_outlines[key] = studio_course_outline
self.course_fixtures[key] = course_fix
def tearDown(self):
......@@ -106,13 +106,13 @@ class DashboardSearchTest(AcceptanceTest):
LogoutPage(self.browser).visit()
AutoAuthPage(self.browser, username=username, email=email, staff=staff).visit()
def _studio_add_content(self, course_outline, html_content):
def _studio_add_content(self, studio_course_outline, html_content):
"""
Add content to first section on studio course page.
"""
# create a unit in course outline
course_outline.visit()
subsection = course_outline.section_at(0).subsection_at(0)
studio_course_outline.visit()
subsection = studio_course_outline.section_at(0).subsection_at(0)
subsection.expand_subsection()
subsection.add_unit()
......@@ -126,12 +126,12 @@ class DashboardSearchTest(AcceptanceTest):
type_in_codemirror(unit_page, 0, html_content)
click_css(unit_page, '.action-save', 0)
def _studio_publish_content(self, course_outline):
def _studio_publish_content(self, studio_course_outline):
"""
Publish content in first section on studio course page.
"""
course_outline.visit()
subsection = course_outline.section_at(0).subsection_at(0)
studio_course_outline.visit()
subsection = studio_course_outline.section_at(0).subsection_at(0)
subsection.expand_subsection()
unit = subsection.unit_at(0)
unit.publish()
......@@ -167,9 +167,9 @@ class DashboardSearchTest(AcceptanceTest):
# Create content in studio without publishing.
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
self._studio_add_content(self.course_outlines['A'], html_content)
self._studio_add_content(self.course_outlines['B'], html_content)
self._studio_add_content(self.course_outlines['C'], html_content)
self._studio_add_content(self.studio_course_outlines['A'], html_content)
self._studio_add_content(self.studio_course_outlines['B'], html_content)
self._studio_add_content(self.studio_course_outlines['C'], html_content)
# Do a search, there should be no results shown.
self._auto_auth(self.USERNAME, self.EMAIL, False)
......@@ -179,9 +179,9 @@ class DashboardSearchTest(AcceptanceTest):
# Publish in studio to trigger indexing.
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
self._studio_publish_content(self.course_outlines['A'])
self._studio_publish_content(self.course_outlines['B'])
self._studio_publish_content(self.course_outlines['C'])
self._studio_publish_content(self.studio_course_outlines['A'])
self._studio_publish_content(self.studio_course_outlines['B'])
self._studio_publish_content(self.studio_course_outlines['C'])
# Do the search again, this time we expect results from courses A & B, but not C
self._auto_auth(self.USERNAME, self.EMAIL, False)
......
......@@ -9,7 +9,7 @@ from nose.plugins.attrib import attr
from common.test.acceptance.tests.helpers import UniqueCourseTest, EventsTestMixin
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage
from common.test.acceptance.pages.lms.course_nav import CourseNavPage
from common.test.acceptance.pages.lms.course_home import CourseHomePage
from common.test.acceptance.pages.lms.courseware import CoursewarePage
from common.test.acceptance.pages.lms.edxnotes import EdxNotesUnitPage, EdxNotesPage, EdxNotesPageNoContent
from common.test.acceptance.fixtures.edxnotes import EdxNotesFixture, Note, Range
......@@ -26,7 +26,7 @@ class EdxNotesTestMixin(UniqueCourseTest):
"""
super(EdxNotesTestMixin, self).setUp()
self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.course_nav = CourseNavPage(self.browser)
self.course_home_page = CourseHomePage(self.browser, self.course_id)
self.note_unit_page = EdxNotesUnitPage(self.browser, self.course_id)
self.notes_page = EdxNotesPage(self.browser, self.course_id)
......@@ -1504,7 +1504,8 @@ class EdxNotesToggleNotesTest(EdxNotesTestMixin):
self.assertEqual(len(self.note_unit_page.notes), 0)
self.courseware_page.go_to_sequential_position(2)
self.assertEqual(len(self.note_unit_page.notes), 0)
self.course_nav.go_to_section(u"Test Section 1", u"Test Subsection 2")
self.course_home_page.visit()
self.course_home_page.outline.go_to_section(u"Test Section 1", u"Test Subsection 2")
self.assertEqual(len(self.note_unit_page.notes), 0)
def test_can_reenable_all_notes(self):
......@@ -1530,5 +1531,6 @@ class EdxNotesToggleNotesTest(EdxNotesTestMixin):
self.assertGreater(len(self.note_unit_page.notes), 0)
self.courseware_page.go_to_sequential_position(2)
self.assertGreater(len(self.note_unit_page.notes), 0)
self.course_nav.go_to_section(u"Test Section 1", u"Test Subsection 2")
self.course_home_page.visit()
self.course_home_page.outline.go_to_section(u"Test Section 1", u"Test Subsection 2")
self.assertGreater(len(self.note_unit_page.notes), 0)
......@@ -43,7 +43,7 @@ class EntranceExamTest(UniqueCourseTest):
).install()
entrance_exam_subsection = None
outline = course_fixture.course_outline
outline = course_fixture.studio_course_outline_as_json
for child in outline['child_info']['children']:
if child.get('display_name') == "Entrance Exam":
entrance_exam_subsection = child['child_info']['children'][0]
......
......@@ -6,7 +6,7 @@ from textwrap import dedent
from common.test.acceptance.tests.helpers import UniqueCourseTest
from common.test.acceptance.pages.studio.auto_auth import AutoAuthPage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
from common.test.acceptance.pages.lms.courseware import CoursewarePage
from common.test.acceptance.pages.lms.problem import ProblemPage
from common.test.acceptance.pages.lms.staff_view import StaffPage
......@@ -29,7 +29,7 @@ class GatingTest(UniqueCourseTest):
self.logout_page = LogoutPage(self.browser)
self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.course_outline = CourseOutlinePage(
self.studio_course_outline = StudioCourseOutlinePage(
self.browser,
self.course_info['org'],
self.course_info['number'],
......@@ -89,10 +89,10 @@ class GatingTest(UniqueCourseTest):
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
# Make the first subsection a prerequisite
self.course_outline.visit()
self.course_outline.open_subsection_settings_dialog(0)
self.course_outline.select_advanced_tab(desired_item='gated_content')
self.course_outline.make_gating_prerequisite()
self.studio_course_outline.visit()
self.studio_course_outline.open_subsection_settings_dialog(0)
self.studio_course_outline.select_advanced_tab(desired_item='gated_content')
self.studio_course_outline.make_gating_prerequisite()
def _setup_gated_subsection(self):
"""
......@@ -102,10 +102,10 @@ class GatingTest(UniqueCourseTest):
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
# Gate the second subsection based on the score achieved in the first subsection
self.course_outline.visit()
self.course_outline.open_subsection_settings_dialog(1)
self.course_outline.select_advanced_tab(desired_item='gated_content')
self.course_outline.add_prerequisite_to_subsection("80")
self.studio_course_outline.visit()
self.studio_course_outline.open_subsection_settings_dialog(1)
self.studio_course_outline.select_advanced_tab(desired_item='gated_content')
self.studio_course_outline.add_prerequisite_to_subsection("80")
def _fulfill_prerequisite(self):
"""
......@@ -127,23 +127,23 @@ class GatingTest(UniqueCourseTest):
self._setup_prereq()
# Assert settings are displayed correctly for a prerequisite subsection
self.course_outline.visit()
self.course_outline.open_subsection_settings_dialog(0)
self.course_outline.select_advanced_tab(desired_item='gated_content')
self.assertTrue(self.course_outline.gating_prerequisite_checkbox_is_visible())
self.assertTrue(self.course_outline.gating_prerequisite_checkbox_is_checked())
self.assertFalse(self.course_outline.gating_prerequisites_dropdown_is_visible())
self.assertFalse(self.course_outline.gating_prerequisite_min_score_is_visible())
self.studio_course_outline.visit()
self.studio_course_outline.open_subsection_settings_dialog(0)
self.studio_course_outline.select_advanced_tab(desired_item='gated_content')
self.assertTrue(self.studio_course_outline.gating_prerequisite_checkbox_is_visible())
self.assertTrue(self.studio_course_outline.gating_prerequisite_checkbox_is_checked())
self.assertFalse(self.studio_course_outline.gating_prerequisites_dropdown_is_visible())
self.assertFalse(self.studio_course_outline.gating_prerequisite_min_score_is_visible())
self._setup_gated_subsection()
# Assert settings are displayed correctly for a gated subsection
self.course_outline.visit()
self.course_outline.open_subsection_settings_dialog(1)
self.course_outline.select_advanced_tab(desired_item='gated_content')
self.assertTrue(self.course_outline.gating_prerequisite_checkbox_is_visible())
self.assertTrue(self.course_outline.gating_prerequisites_dropdown_is_visible())
self.assertTrue(self.course_outline.gating_prerequisite_min_score_is_visible())
self.studio_course_outline.visit()
self.studio_course_outline.open_subsection_settings_dialog(1)
self.studio_course_outline.select_advanced_tab(desired_item='gated_content')
self.assertTrue(self.studio_course_outline.gating_prerequisite_checkbox_is_visible())
self.assertTrue(self.studio_course_outline.gating_prerequisites_dropdown_is_visible())
self.assertTrue(self.studio_course_outline.gating_prerequisite_min_score_is_visible())
def test_gated_subsection_in_lms_for_student(self):
"""
......
......@@ -12,7 +12,7 @@ from flaky import flaky
from common.test.acceptance.tests.helpers import UniqueCourseTest, get_modal_alert, EventsTestMixin
from common.test.acceptance.pages.common.logout import LogoutPage
from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
from common.test.acceptance.pages.lms.create_mode import ModeCreationPage
from common.test.acceptance.pages.lms.courseware import CoursewarePage
from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage, EntranceExamAdmin
......@@ -227,7 +227,7 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.course_outline = CourseOutlinePage(
self.studio_course_outline = StudioCourseOutlinePage(
self.browser,
self.course_info['org'],
self.course_info['number'],
......@@ -301,15 +301,15 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
# Visit the course outline page in studio
LogoutPage(self.browser).visit()
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
self.course_outline.visit()
self.studio_course_outline.visit()
# open the exam settings to make it a proctored exam.
self.course_outline.open_subsection_settings_dialog()
self.studio_course_outline.open_subsection_settings_dialog()
# select advanced settings tab
self.course_outline.select_advanced_tab()
self.studio_course_outline.select_advanced_tab()
self.course_outline.make_exam_proctored()
self.studio_course_outline.make_exam_proctored()
# login as a verified student and visit the courseware.
LogoutPage(self.browser).visit()
......@@ -327,15 +327,15 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
# Visit the course outline page in studio
LogoutPage(self.browser).visit()
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
self.course_outline.visit()
self.studio_course_outline.visit()
# open the exam settings to make it a proctored exam.
self.course_outline.open_subsection_settings_dialog()
self.studio_course_outline.open_subsection_settings_dialog()
# select advanced settings tab
self.course_outline.select_advanced_tab()
self.studio_course_outline.select_advanced_tab()
self.course_outline.make_exam_timed()
self.studio_course_outline.make_exam_timed()
# login as a verified student and visit the courseware.
LogoutPage(self.browser).visit()
......
......@@ -6,9 +6,8 @@ import json
from common.test.acceptance.tests.helpers import remove_file
from common.test.acceptance.pages.common.logout import LogoutPage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
from common.test.acceptance.pages.lms.courseware_search import CoursewareSearchPage
from common.test.acceptance.pages.lms.course_nav import CourseNavPage
from common.test.acceptance.fixtures.course import XBlockFixtureDesc
from common.test.acceptance.tests.helpers import create_user_partition_json
......@@ -44,8 +43,7 @@ class SplitTestCoursewareSearchTest(ContainerBase):
self.staff_user = self.user
self.courseware_search_page = CoursewareSearchPage(self.browser, self.course_id)
self.course_navigation_page = CourseNavPage(self.browser)
self.course_outline = CourseOutlinePage(
self.studio_course_outline = StudioCourseOutlinePage(
self.browser,
self.course_info['org'],
self.course_info['number'],
......@@ -68,9 +66,9 @@ class SplitTestCoursewareSearchTest(ContainerBase):
Reindex course content on studio course page
"""
self._auto_auth(self.staff_user["username"], self.staff_user["email"], True)
self.course_outline.visit()
self.course_outline.start_reindex()
self.course_outline.wait_for_ajax()
self.studio_course_outline.visit()
self.studio_course_outline.start_reindex()
self.studio_course_outline.wait_for_ajax()
def _create_group_configuration(self):
"""
......
......@@ -20,7 +20,7 @@ from ...pages.lms.problem import ProblemPage
from ...pages.lms.progress import ProgressPage
from ...pages.studio.component_editor import ComponentEditorView
from ...pages.studio.utils import type_in_codemirror
from ...pages.studio.overview import CourseOutlinePage
from ...pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
class ProgressPageBaseTest(UniqueCourseTest):
......@@ -43,7 +43,7 @@ class ProgressPageBaseTest(UniqueCourseTest):
self.progress_page = ProgressPage(self.browser, self.course_id)
self.logout_page = LogoutPage(self.browser)
self.course_outline = CourseOutlinePage(
self.studio_course_outline = StudioCourseOutlinePage(
self.browser,
self.course_info['org'],
self.course_info['number'],
......@@ -140,11 +140,11 @@ class PersistentGradesTest(ProgressPageBaseTest):
Adds a unit to the subsection, which
should not affect a persisted subsection grade.
"""
self.course_outline.visit()
subsection = self.course_outline.section(self.SECTION_NAME).subsection(self.SUBSECTION_NAME)
self.studio_course_outline.visit()
subsection = self.studio_course_outline.section(self.SECTION_NAME).subsection(self.SUBSECTION_NAME)
subsection.expand_subsection()
subsection.add_unit()
self.course_outline.wait_for_ajax()
self.studio_course_outline.wait_for_ajax()
subsection.publish()
def _set_staff_lock_on_subsection(self, locked):
......@@ -152,8 +152,8 @@ class PersistentGradesTest(ProgressPageBaseTest):
Sets staff lock for a subsection, which should hide the
subsection score from students on the progress page.
"""
self.course_outline.visit()
subsection = self.course_outline.section_at(0).subsection_at(0)
self.studio_course_outline.visit()
subsection = self.studio_course_outline.section_at(0).subsection_at(0)
subsection.set_staff_lock(locked)
self.assertEqual(subsection.has_staff_lock_warning, locked)
......@@ -163,9 +163,9 @@ class PersistentGradesTest(ProgressPageBaseTest):
along with its container unit, so any changes can
be published.
"""
self.course_outline.visit()
self.course_outline.section_at(0).subsection_at(0).expand_subsection()
unit = self.course_outline.section_at(0).subsection_at(0).unit(self.UNIT_NAME).go_to()
self.studio_course_outline.visit()
self.studio_course_outline.section_at(0).subsection_at(0).expand_subsection()
unit = self.studio_course_outline.section_at(0).subsection_at(0).unit(self.UNIT_NAME).go_to()
component = unit.xblocks[1]
return unit, component
......@@ -289,8 +289,8 @@ class SubsectionGradingPolicyTest(ProgressPageBaseTest):
If a section index is not provided, 0 is assumed.
"""
with self._logged_in_session(staff=True):
self.course_outline.visit()
modal = self.course_outline.section_at(section).subsection_at(0).edit()
self.studio_course_outline.visit()
modal = self.studio_course_outline.section_at(section).subsection_at(0).edit()
modal.policy = policy
modal.save()
......
......@@ -12,8 +12,8 @@ from nose.plugins.attrib import attr
from common.test.acceptance.pages.studio.settings_advanced import AdvancedSettingsPage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage, ContainerPage, ExpandCollapseLinkState
from common.test.acceptance.pages.studio.utils import add_discussion, drag, verify_ordering
from common.test.acceptance.pages.lms.course_home import CourseHomePage
from common.test.acceptance.pages.lms.courseware import CoursewarePage
from common.test.acceptance.pages.lms.course_nav import CourseNavPage
from common.test.acceptance.pages.lms.staff_view import StaffPage
from common.test.acceptance.fixtures.config import ConfigModelFixture
from common.test.acceptance.fixtures.course import XBlockFixtureDesc
......@@ -1490,7 +1490,7 @@ class PublishSectionTest(CourseOutlineTest):
The first subsection has 2 units, and the second subsection has one unit.
"""
self.courseware = CoursewarePage(self.browser, self.course_id)
self.course_nav = CourseNavPage(self.browser)
self.course_home_page = CourseHomePage(self.browser, self.course_id)
course_fixture.add_children(
XBlockFixtureDesc('chapter', SECTION_NAME).add_children(
XBlockFixtureDesc('sequential', SUBSECTION_NAME).add_children(
......@@ -1578,7 +1578,8 @@ class PublishSectionTest(CourseOutlineTest):
self.assertEqual(1, self.courseware.num_xblock_components)
self.courseware.go_to_sequential_position(2)
self.assertEqual(1, self.courseware.num_xblock_components)
self.course_nav.go_to_section(SECTION_NAME, 'Test Subsection 2')
self.course_home_page.visit()
self.course_home_page.outline.go_to_section(SECTION_NAME, 'Test Subsection 2')
self.assertEqual(1, self.courseware.num_xblock_components)
def _add_unpublished_content(self):
......
......@@ -15,7 +15,6 @@ from common.test.acceptance.tests.helpers import UniqueCourseTest, is_youtube_av
from common.test.acceptance.pages.lms.video.video import VideoPage
from common.test.acceptance.pages.lms.tab_nav import TabNavPage
from common.test.acceptance.pages.lms.courseware import CoursewarePage
from common.test.acceptance.pages.lms.course_nav import CourseNavPage
from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage
from common.test.acceptance.pages.lms.course_info import CourseInfoPage
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
......@@ -53,7 +52,6 @@ class VideoBaseTest(UniqueCourseTest):
self.video = VideoPage(self.browser)
self.tab_nav = TabNavPage(self.browser)
self.course_nav = CourseNavPage(self.browser)
self.courseware = CoursewarePage(self.browser, self.course_id)
self.course_info_page = CourseInfoPage(self.browser, self.course_id)
self.auth_page = AutoAuthPage(self.browser, course_id=self.course_id)
......@@ -531,7 +529,7 @@ class YouTubeVideoTest(VideoBaseTest):
self.assertTrue(self.video.downloaded_transcript_contains_text(file_type, search_text))
# open vertical containing video "C"
self.course_nav.go_to_vertical('Test Vertical-2')
self.courseware.nav.go_to_vertical('Test Vertical-2')
# menu "download_transcript" doesn't exist
self.assertFalse(self.video.is_menu_present('download_transcript'))
......@@ -678,17 +676,17 @@ class YouTubeVideoTest(VideoBaseTest):
self.navigate_to_video()
# select the "2.0" speed on video "A"
self.course_nav.go_to_vertical('Test Vertical-0')
self.courseware.nav.go_to_vertical('Test Vertical-0')
self.video.wait_for_video_player_render()
self.video.speed = '2.0'
# select the "0.50" speed on video "B"
self.course_nav.go_to_vertical('Test Vertical-1')
self.courseware.nav.go_to_vertical('Test Vertical-1')
self.video.wait_for_video_player_render()
self.video.speed = '0.50'
# open video "C"
self.course_nav.go_to_vertical('Test Vertical-2')
self.courseware.nav.go_to_vertical('Test Vertical-2')
self.video.wait_for_video_player_render()
# Since the playback speed was set to .5 in "B", this video will also be impacted
......@@ -697,7 +695,7 @@ class YouTubeVideoTest(VideoBaseTest):
self.video.verify_speed_changed('0.75x')
# go to the vertical containing video "A"
self.course_nav.go_to_vertical('Test Vertical-0')
self.courseware.nav.go_to_vertical('Test Vertical-0')
# Video "A" should still play at speed 2.0 because it was explicitly set to that.
self.assertEqual(self.video.speed, '2.0x')
......@@ -706,7 +704,7 @@ class YouTubeVideoTest(VideoBaseTest):
self.video.reload_page()
# go to the vertical containing video "A"
self.course_nav.go_to_vertical('Test Vertical-0')
self.courseware.nav.go_to_vertical('Test Vertical-0')
# check if video "A" should start playing at speed "2.0"
self.assertEqual(self.video.speed, '2.0x')
......@@ -715,13 +713,13 @@ class YouTubeVideoTest(VideoBaseTest):
self.video.speed = '1.0'
# go to the vertical containing "B"
self.course_nav.go_to_vertical('Test Vertical-1')
self.courseware.nav.go_to_vertical('Test Vertical-1')
# Video "B" should still play at speed .5 because it was explicitly set to that.
self.assertEqual(self.video.speed, '0.50x')
# go to the vertical containing video "C"
self.course_nav.go_to_vertical('Test Vertical-2')
self.courseware.nav.go_to_vertical('Test Vertical-2')
# The change of speed for Video "A" should impact Video "C" because it still has
# not been explicitly set to a speed.
......
......@@ -231,18 +231,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
# # of sql queries to default,
# # of mongo queries,
# )
('no_overrides', 1, True, False): (21, 6),
('no_overrides', 2, True, False): (21, 6),
('no_overrides', 3, True, False): (21, 6),
('ccx', 1, True, False): (21, 6),
('ccx', 2, True, False): (21, 6),
('ccx', 3, True, False): (21, 6),
('no_overrides', 1, False, False): (21, 6),
('no_overrides', 2, False, False): (21, 6),
('no_overrides', 3, False, False): (21, 6),
('ccx', 1, False, False): (21, 6),
('ccx', 2, False, False): (21, 6),
('ccx', 3, False, False): (21, 6),
('no_overrides', 1, True, False): (22, 6),
('no_overrides', 2, True, False): (22, 6),
('no_overrides', 3, True, False): (22, 6),
('ccx', 1, True, False): (22, 6),
('ccx', 2, True, False): (22, 6),
('ccx', 3, True, False): (22, 6),
('no_overrides', 1, False, False): (22, 6),
('no_overrides', 2, False, False): (22, 6),
('no_overrides', 3, False, False): (22, 6),
('ccx', 1, False, False): (22, 6),
('ccx', 2, False, False): (22, 6),
('ccx', 3, False, False): (22, 6),
}
......@@ -254,19 +254,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
__test__ = True
TEST_DATA = {
('no_overrides', 1, True, False): (21, 3),
('no_overrides', 2, True, False): (21, 3),
('no_overrides', 3, True, False): (21, 3),
('ccx', 1, True, False): (21, 3),
('ccx', 2, True, False): (21, 3),
('ccx', 3, True, False): (21, 3),
('ccx', 1, True, True): (22, 3),
('ccx', 2, True, True): (22, 3),
('ccx', 3, True, True): (22, 3),
('no_overrides', 1, False, False): (21, 3),
('no_overrides', 2, False, False): (21, 3),
('no_overrides', 3, False, False): (21, 3),
('ccx', 1, False, False): (21, 3),
('ccx', 2, False, False): (21, 3),
('ccx', 3, False, False): (21, 3),
('no_overrides', 1, True, False): (22, 3),
('no_overrides', 2, True, False): (22, 3),
('no_overrides', 3, True, False): (22, 3),
('ccx', 1, True, False): (22, 3),
('ccx', 2, True, False): (22, 3),
('ccx', 3, True, False): (22, 3),
('ccx', 1, True, True): (23, 3),
('ccx', 2, True, True): (23, 3),
('ccx', 3, True, True): (23, 3),
('no_overrides', 1, False, False): (22, 3),
('no_overrides', 2, False, False): (22, 3),
('no_overrides', 3, False, False): (22, 3),
('ccx', 1, False, False): (22, 3),
('ccx', 2, False, False): (22, 3),
('ccx', 3, False, False): (22, 3),
}
......@@ -2,14 +2,17 @@
This module is essentially a broker to xmodule/tabs.py -- it was originally introduced to
perform some LMS-specific tab display gymnastics for the Entrance Exams feature
"""
import waffle
from django.conf import settings
from django.utils.translation import ugettext as _, ugettext_noop
from courseware.access import has_access
from courseware.entrance_exams import user_must_complete_entrance_exam
from openedx.core.lib.course_tabs import CourseTabPluginManager
from request_cache.middleware import RequestCache
from student.models import CourseEnrollment
from xmodule.tabs import CourseTab, CourseTabList, key_checker
from xmodule.tabs import CourseTab, CourseTabList, key_checker, link_reverse_func
class EnrolledTab(CourseTab):
......@@ -34,6 +37,25 @@ class CoursewareTab(EnrolledTab):
is_movable = False
is_default = False
@staticmethod
def main_course_url_name(request):
"""
Returns the main course URL for the current user.
"""
if waffle.flag_is_active(request, 'unified_course_view'):
return 'edx.course_experience.course_home'
else:
return 'courseware'
@property
def link_func(self):
"""
Returns a function that computes the URL for this tab.
"""
request = RequestCache.get_current_request()
url_name = self.main_course_url_name(request)
return link_reverse_func(url_name)
class CourseInfoTab(CourseTab):
"""
......
......@@ -1419,17 +1419,17 @@ class ProgressPageTests(ModuleStoreTestCase):
"""Test that query counts remain the same for self-paced and instructor-paced courses."""
SelfPacedConfiguration(enabled=self_paced_enabled).save()
self.setup_course(self_paced=self_paced)
with self.assertNumQueries(38), check_mongo_calls(4):
with self.assertNumQueries(39), check_mongo_calls(4):
self._get_progress_page()
def test_progress_queries(self):
self.setup_course()
with self.assertNumQueries(38), check_mongo_calls(4):
with self.assertNumQueries(39), check_mongo_calls(4):
self._get_progress_page()
# subsequent accesses to the progress page require fewer queries.
for _ in range(2):
with self.assertNumQueries(24), check_mongo_calls(4):
with self.assertNumQueries(25), check_mongo_calls(4):
self._get_progress_page()
@patch(
......
......@@ -21,12 +21,14 @@ from edxmako.shortcuts import render_to_response, render_to_string
import logging
import newrelic.agent
import urllib
import waffle
from xblock.fragment import Fragment
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
from openedx.core.djangoapps.crawlers.models import CrawlersConfig
from request_cache.middleware import RequestCache
from shoppingcart.models import CourseRegistrationCode
from student.models import CourseEnrollment
from student.views import is_course_blocked
......@@ -36,6 +38,7 @@ from util.views import ensure_valid_course_key
from xmodule.modulestore.django import modulestore
from xmodule.x_module import STUDENT_VIEW
from survey.utils import must_answer_survey
from web_fragments.fragment import Fragment
from ..access import has_access, _adjust_start_date_for_beta_testers
from ..access_utils import in_preview_mode
......@@ -396,9 +399,9 @@ class CoursewareIndex(View):
Returns and creates the rendering context for the courseware.
Also returns the table of contents for the courseware.
"""
request = RequestCache.get_current_request()
courseware_context = {
'csrf': csrf(self.request)['csrf_token'],
'COURSE_TITLE': self.course.display_name_with_default_escaped,
'course': self.course,
'init': '',
'fragment': Fragment(),
......@@ -411,7 +414,8 @@ class CoursewareIndex(View):
'language_preference': self._get_language_preference(),
'disable_optimizely': True,
'section_title': None,
'sequence_title': None
'sequence_title': None,
'disable_accordion': waffle.flag_is_active(request, 'unified_course_view')
}
table_of_contents = toc_for_course(
self.effective_user,
......@@ -455,7 +459,7 @@ class CoursewareIndex(View):
courseware_context['default_tab'] = self.section.default_tab
# section data
courseware_context['section_title'] = self.section.display_name_with_default_escaped
courseware_context['section_title'] = self.section.display_name_with_default
section_context = self._create_section_context(
table_of_contents['previous_of_active_section'],
table_of_contents['next_of_active_section'],
......
......@@ -223,6 +223,18 @@ ECOMMERCE_API_URL = 'http://localhost:8043/api/v2/'
LMS_ROOT_URL = "http://localhost:8000"
DOC_LINK_BASE_URL = 'http://edx.readthedocs.io/projects/edx-guide-for-students'
# TODO: TNL-6546: Remove this waffle and flag code.
from django.db.utils import ProgrammingError
from waffle.models import Flag
try:
flag, created = Flag.objects.get_or_create(name='unified_course_view')
flag.everyone = True
flag.save
WAFFLE_OVERRIDE = True
except ProgrammingError:
# during initial reset_db, the table for the flag doesn't yet exist.
pass
#####################################################################
# Lastly, see if the developer has any local overrides.
try:
......
......@@ -1724,7 +1724,7 @@ REQUIRE_ENVIRONMENT = "node"
# but you don't want to include those dependencies in the JS bundle for the page,
# then you need to add the js urls in this list.
REQUIRE_JS_PATH_OVERRIDES = {
'js/bookmarks/views/bookmark_button': 'js/bookmarks/views/bookmark_button.js',
'course_bookmarks/js/views/bookmark_button': 'course_bookmarks/js/views/bookmark_button.js',
'js/views/message_banner': 'js/views/message_banner.js',
'moment': 'common/js/vendor/moment-with-locales.js',
'moment-timezone': 'common/js/vendor/moment-timezone-with-data.js',
......@@ -2173,6 +2173,10 @@ INSTALLED_APPS = (
# Unusual migrations
'database_fixups',
# Features
'openedx.features.course_bookmarks',
'openedx.features.course_experience',
)
######################### CSRF #########################################
......
../../openedx/features/course_bookmarks/static/course_bookmarks
\ No newline at end of file
../../openedx/features/course_experience/static/course_experience
\ No newline at end of file
(function(define, undefined) {
'use strict';
define(['gettext', 'jquery', 'underscore', 'backbone', 'js/bookmarks/views/bookmarks_list',
'js/bookmarks/collections/bookmarks', 'js/views/message_banner'],
function(gettext, $, _, Backbone, BookmarksListView, BookmarksCollection, MessageBannerView) {
return Backbone.View.extend({
el: '.courseware-bookmarks-button',
loadingMessageElement: '#loading-message',
errorMessageElement: '#error-message',
events: {
'click .bookmarks-list-button': 'toggleBookmarksListView'
},
initialize: function() {
var bookmarksCollection = new BookmarksCollection([],
{
course_id: $('.courseware-results').data('courseId'),
url: $('.courseware-bookmarks-button').data('bookmarksApiUrl')
}
);
this.bookmarksListView = new BookmarksListView(
{
collection: bookmarksCollection,
loadingMessageView: new MessageBannerView({el: $(this.loadingMessageElement)}),
errorMessageView: new MessageBannerView({el: $(this.errorMessageElement)})
}
);
},
toggleBookmarksListView: function() {
if (this.bookmarksListView.areBookmarksVisible()) {
this.bookmarksListView.hideBookmarks();
this.$('.bookmarks-list-button').attr('aria-pressed', 'false');
this.$('.bookmarks-list-button').removeClass('is-active').addClass('is-inactive');
} else {
this.bookmarksListView.showBookmarks();
this.$('.bookmarks-list-button').attr('aria-pressed', 'true');
this.$('.bookmarks-list-button').removeClass('is-inactive').addClass('is-active');
}
}
});
});
}).call(this, define || RequireJS.define);
......@@ -3,10 +3,9 @@
define([
'jquery',
'logger',
'js/bookmarks/views/bookmarks_list_button'
'logger'
],
function($, Logger, BookmarksListButton) {
function($, Logger) {
return function() {
// This function performs all actions common to all courseware.
// 1. adding an event to all link clicks.
......@@ -18,9 +17,6 @@
target_url: event.currentTarget.href
});
});
// 2. instantiating this button attaches events to all buttons in the courseware.
new BookmarksListButton(); // eslint-disable-line no-new
};
}
);
......
<div class="courseware-bookmarks-button" data-bookmarks-api-url="/api/bookmarks/v1/bookmarks/">
<button type="button" class="bookmarks-list-button is-inactive" aria-pressed="false">
Bookmarks
</button>
</div>
<section class="courseware-results-wrapper">
<div id="loading-message" aria-live="assertive" aria-relevant="all"></div>
<div id="error-message" aria-live="polite"></div>
<div class="courseware-results" data-course-id="a/b/c" data-lang-code="en"></div>
</section>
......@@ -27,6 +27,8 @@ var options = {
// Otherwise Istanbul which is used for coverage tracking will cause tests to not run.
sourceFiles: [
{pattern: 'coffee/src/**/!(*spec).js'},
{pattern: 'course_bookmarks/**/!(*spec).js'},
{pattern: 'course_experience/js/**/!(*spec).js'},
{pattern: 'discussion/js/**/!(*spec).js'},
{pattern: 'js/**/!(*spec|djangojs).js'},
{pattern: 'lms/js/**/!(*spec).js'},
......
......@@ -18,6 +18,8 @@
* done.
*/
modules: getModulesList([
'course_bookmarks/js/course_bookmarks_factory',
'course_experience/js/course_outline_factory',
'discussion/js/discussion_board_factory',
'discussion/js/discussion_profile_page_factory',
'js/api_admin/catalog_preview_factory',
......
......@@ -92,12 +92,6 @@
'js/student_profile/views/learner_profile_factory': 'js/student_profile/views/learner_profile_factory',
'js/student_profile/views/learner_profile_view': 'js/student_profile/views/learner_profile_view',
'js/ccx/schedule': 'js/ccx/schedule',
'js/bookmarks/collections/bookmarks': 'js/bookmarks/collections/bookmarks',
'js/bookmarks/models/bookmark': 'js/bookmarks/models/bookmark',
'js/bookmarks/views/bookmarks_list_button': 'js/bookmarks/views/bookmarks_list_button',
'js/bookmarks/views/bookmarks_list': 'js/bookmarks/views/bookmarks_list',
'js/bookmarks/views/bookmark_button': 'js/bookmarks/views/bookmark_button',
'js/views/message_banner': 'js/views/message_banner',
// edxnotes
......@@ -679,14 +673,16 @@
});
testFiles = [
'course_bookmarks/js/spec/bookmark_button_view_spec.js',
'course_bookmarks/js/spec/bookmarks_list_view_spec.js',
'course_bookmarks/js/spec/course_bookmarks_factory_spec.js',
'course_experience/js/spec/course_outline_factory_spec.js',
'discussion/js/spec/discussion_board_factory_spec.js',
'discussion/js/spec/discussion_profile_page_factory_spec.js',
'discussion/js/spec/discussion_board_view_spec.js',
'discussion/js/spec/views/discussion_user_profile_view_spec.js',
'lms/js/spec/preview/preview_factory_spec.js',
'js/spec/api_admin/catalog_preview_spec.js',
'js/spec/courseware/bookmark_button_view_spec.js',
'js/spec/courseware/bookmarks_list_view_spec.js',
'js/spec/ccx/schedule_spec.js',
'js/spec/commerce/receipt_view_spec.js',
'js/spec/components/card/card_spec.js',
......
......@@ -62,10 +62,12 @@
@import 'views/support';
@import 'views/oauth2';
@import "views/financial-assistance";
@import 'views/bookmarks';
@import 'course/auto-cert';
@import 'views/api-access';
// features
@import 'features/bookmarks-v1';
// search
@import 'search/search';
......
......@@ -19,4 +19,10 @@
@import 'shared-v2/modal';
@import 'shared-v2/help-tab';
// Elements
@import 'notifications';
@import 'elements-v2/pagination';
// Features
@import 'features/bookmarks';
@import 'features/course-outline';
// Copied from elements/_pagination.scss
.pagination {
@include clearfix();
display: inline-block;
width: flex-grid(3, 12);
&.pagination-compact {
@include text-align(right);
}
&.pagination-full {
display: block;
width: flex-grid(4, 12);
margin: $baseline auto;
}
.nav-item {
position: relative;
display: inline-block;
vertical-align: middle;
}
.nav-link {
@include transition(all $tmg-f2 ease-in-out 0s);
display: block;
border: 0;
background-image: none;
background-color: transparent;
padding: ($baseline/2) ($baseline*0.75);
&.previous {
margin-right: ($baseline/2);
}
&.next {
margin-left: ($baseline/2);
}
&:hover {
background-color: $lms-active-color;
background-image: none;
border-radius: 3px;
color: $white;
}
&.is-disabled {
background-color: transparent;
color: $lms-gray;
pointer-events: none;
}
}
.nav-label {
@extend .sr-only;
}
.pagination-form,
.current-page,
.page-divider,
.total-pages {
display: inline-block;
}
.current-page,
.page-number-input,
.total-pages {
width: ($baseline*2.5);
vertical-align: middle;
margin: 0 ($baseline*0.75);
padding: ($baseline/4);
text-align: center;
color: $lms-gray;
}
.current-page {
position: absolute;
@include left(-($baseline/4));
}
.page-divider {
vertical-align: middle;
color: $lms-gray;
}
.pagination-form {
position: relative;
z-index: 100;
.page-number-label,
.submit-pagination-form {
@extend .sr-only;
}
.page-number-input {
@include transition(all $tmg-f2 ease-in-out 0s);
border: 1px solid transparent;
border-bottom: 1px dotted $lms-gray;
border-radius: 0;
box-shadow: none;
background: none;
&:hover {
background-color: $white;
opacity: 0.6;
}
&:focus {
// borrowing the base input focus styles to match overall app
@include linear-gradient($yellow-l4, tint($yellow-l4, 90%));
opacity: 1.0;
box-shadow: 0 0 3px $black inset;
background-color: $white;
border: 1px solid transparent;
border-radius: 3px;
}
}
}
}
// styles for search/pagination metadata and sorting
.listing-tools {
color: $lms-gray;
label { // override
color: inherit;
font-size: inherit;
cursor: auto;
}
.listing-sort-select {
border: 0;
}
}
$bookmark-icon: "\f097"; // .fa-bookmark-o
$bookmarked-icon: "\f02e"; // .fa-bookmark
// Rules for placing bookmarks and search button side by side
.wrapper-course-modes {
border-bottom: 1px solid $gray-l3;
padding: ($baseline/4);
> div {
@include box-sizing(border-box);
display: inline-block;
}
}
// Rules for Bookmarks Button
.courseware-bookmarks-button {
width: flex-grid(5);
vertical-align: top;
.bookmarks-list-button {
@extend %ui-clear-button;
// set styles
@extend %btn-pl-default-base;
@include font-size(13);
width: 100%;
padding: ($baseline/4) ($baseline/2);
&:before {
content: $bookmarked-icon;
font-family: FontAwesome;
}
}
}
// Rules for bookmark icon shown on each sequence nav item
.course-content {
.bookmark-icon.bookmarked {
@include right($baseline / 4);
top: -3px;
position: absolute;
}
// Rules for bookmark button's different styles
.bookmark-button-wrapper {
margin-bottom: ($baseline * 1.5);
}
.bookmark-button {
&:before {
content: $bookmark-icon;
font-family: FontAwesome;
}
&.bookmarked {
&:before {
content: $bookmarked-icon;
}
}
}
}
$bookmark-icon: "\f097"; // .fa-bookmark-o
$bookmarked-icon: "\f02e"; // .fa-bookmark
// Rules for placing bookmarks and search button side by side
.wrapper-course-modes {
border-bottom: 1px solid $gray-l3;
padding: ($baseline/4);
> div {
@include box-sizing(border-box);
display: inline-block;
}
// Rules for Bookmarks Results Header
.bookmarks-results-header {
letter-spacing: 0;
text-transform: none;
margin-bottom: ($baseline/2);
}
// Rules for Bookmarks Results
.bookmarks-results-list {
padding-top: ($baseline/2);
// Rules for Bookmarks Button
.courseware-bookmarks-button {
width: flex-grid(5);
vertical-align: top;
.bookmarks-list-button {
@extend %ui-clear-button;
.bookmarks-results-list-item {
@include padding(0, $baseline, ($baseline/4), $baseline);
display: block;
border: 1px solid $lms-border-color;
margin-bottom: $baseline;
// set styles
@extend %btn-pl-default-base;
@include font-size(13);
width: 100%;
padding: ($baseline/4) ($baseline/2);
&:hover {
border-color: palette(primary, base);
&:before {
content: $bookmarked-icon;
font-family: FontAwesome;
.list-item-breadcrumbtrail {
color: palette(primary, base);
}
}
}
&.is-active {
background-color: lighten($action-primary-bg,10%);
color: $white;
.results-list-item-view {
@include float(right);
margin-top: $baseline;
}
}
}
// Rules for Bookmarks Results Header
.bookmarks-results-header {
@extend %t-title4;
letter-spacing: 0;
text-transform: none;
margin-bottom: ($baseline/2);
}
// Rules for Bookmarks Results
.bookmarks-results-list {
padding-top: ($baseline/2);
.bookmarks-results-list-item {
@include padding(0, $baseline, ($baseline/4), $baseline);
display: block;
border: 1px solid $gray-l4;
margin-bottom: $baseline;
.list-item-date {
margin-top: ($baseline/4);
color: $lms-gray;
font-size: font-size(small);
}
&:hover {
border-color: $m-blue;
.bookmarks-results-list-item:before {
content: $bookmarked-icon;
position: relative;
top: -7px;
font-family: FontAwesome;
color: palette(primary, base);
}
.list-item-breadcrumbtrail {
color: $blue;
}
.list-item-content {
overflow: hidden;
}
.icon {
@extend %t-icon6;
.list-item-left-section {
display: inline-block;
vertical-align: middle;
width: 90%;
}
}
.results-list-item-view {
@include float(right);
margin-top: $baseline;
}
.list-item-date {
@extend %t-copy-sub2;
margin-top: ($baseline/4);
color: $gray;
}
.bookmarks-results-list-item:before {
content: $bookmarked-icon;
position: relative;
top: -7px;
font-family: FontAwesome;
color: $m-blue;
}
.list-item-content {
overflow: hidden;
}
.list-item-left-section {
display: inline-block;
vertical-align: middle;
width: 90%;
}
.list-item-right-section {
display: inline-block;
vertical-align: middle;
.fa-arrow-right {
@include rtl {
@include transform(rotate(180deg));
}
.list-item-right-section {
display: inline-block;
vertical-align: middle;
.fa-arrow-right {
@include rtl {
@include transform(rotate(180deg));
}
}
}
}
}
// Rules for empty bookmarks list
.bookmarks-empty {
margin-top: $baseline;
border: 1px solid $gray-l4;
padding: $baseline;
background-color: $gray-l6;
margin-top: $baseline;
border: 1px solid $lms-border-color;
padding: $baseline;
background-color: $white;
}
.bookmarks-empty-header {
@extend %t-title5;
margin-bottom: ($baseline/2);
@extend %t-title5;
margin-bottom: ($baseline/2);
}
.bookmarks-empty-detail {
@extend %t-copy-sub1;
}
// Rules for bookmark icon shown on each sequence nav item
.course-content {
.bookmark-icon.bookmarked {
@include right($baseline / 4);
top: -3px;
position: absolute;
}
// Rules for bookmark button's different styles
.bookmark-button-wrapper {
margin-bottom: ($baseline * 1.5);
}
.bookmark-button {
&:before {
content: $bookmark-icon;
font-family: FontAwesome;
}
&.bookmarked {
&:before {
content: $bookmarked-icon;
}
}
}
@extend %t-copy-sub1;
}
.course-outline {
color: $lms-gray;
.block-tree {
margin: 0;
list-style-type: none;
.section {
margin: 0 (-1 * $baseline);
width: calc(100% + (2 * $baseline));
padding: 0 ($baseline * 2);
&:not(:first-child) {
border-top: 1px solid $lms-border-color;
.section-name {
margin-top: $baseline;
}
}
.section-name {
@include margin(0, 0, ($baseline / 2), ($baseline / 2));
padding: 0;
font-weight: bold;
}
.outline-item {
@include padding-left(0);
}
ol.outline-item {
margin: 0 0 ($baseline / 2) 0;
.subsection {
list-style-type: none;
a.outline-item {
display: block;
padding: ($baseline / 2);
&:hover {
background-color: palette(primary, x-back);
text-decoration: none;
}
}
}
}
}
}
}
......@@ -46,16 +46,6 @@
display: inline-block;
}
.form-actions > * {
@include margin-left($baseline/2);
vertical-align: middle;
height: 34px;
}
.form-actions > button {
height: 34px;
}
.form-actions > *:first-child {
@include margin-left(0);
}
......
<%page expression_filter="h" args="bookmark_id, is_bookmarked" />
<%! from django.utils.translation import ugettext as _ %>
<%!
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
%>
<div class="bookmark-button-wrapper">
<button class="btn btn-link bookmark-button ${"bookmarked" if is_bookmarked else ""}"
aria-pressed="${"true" if is_bookmarked else "false"}"
data-bookmark-id="${bookmark_id}">
data-bookmark-id="${bookmark_id}"
data-bookmarks-api-url="${reverse('bookmarks')}">
<span class="bookmark-text">${_("Bookmarked") if is_bookmarked else _("Bookmark this page")}</span>
</button>
</div>
......@@ -3,12 +3,15 @@
<%namespace name='static' file='/static_content.html'/>
<%def name="online_help_token()"><% return "courseware" %></%def>
<%!
from django.utils.translation import ugettext as _
import waffle
from django.conf import settings
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from edxnotes.helpers import is_feature_enabled as is_edxnotes_enabled
from openedx.core.djangolib.markup import HTML
from openedx.core.djangolib.js_utils import js_escaped_string
from openedx.core.djangolib.markup import HTML
%>
<%
include_special_exams = settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and (course.enable_proctored_exams or course.enable_timed_exams)
......@@ -27,7 +30,7 @@ from openedx.core.djangolib.js_utils import js_escaped_string
<%block name="header_extras">
% for template_name in ["image-modal"]:
% for template_name in ["image-modal", "sequence-breadcrumbs"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="common/templates/${template_name}.underscore" />
</script>
......@@ -115,10 +118,10 @@ ${HTML(fragment.foot_html())}
<div class="wrapper-course-modes">
<div class="courseware-bookmarks-button" data-bookmarks-api-url="${bookmarks_api_url}">
<button type="button" class="bookmarks-list-button is-inactive" aria-pressed="false">
<div class="courseware-bookmarks-button">
<a class="bookmarks-list-button" href="${reverse('openedx.course_bookmarks.home', args=[course.id])}">
${_('Bookmarks')}
</button>
</a>
</div>
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
......@@ -152,7 +155,10 @@ ${HTML(fragment.foot_html())}
% endif
<section class="course-content" id="course-content">
<main id="main" tabindex="-1" aria-label="Content">
<div class="path"></div>
<div
class="path"
data-unified-course-view="${'true' if waffle.flag_is_active(request, 'unified_course_view') else 'false'}"
></div>
% if getattr(course, 'entrance_exam_enabled') and \
getattr(course, 'entrance_exam_minimum_score_pct') and \
entrance_exam_current_score is not UNDEFINED:
......
......@@ -601,10 +601,27 @@ urlpatterns += (
name='edxnotes_endpoints',
),
# Branding API
url(
r'^api/branding/v1/',
include('branding.api_urls')
),
# Course experience
url(
r'^courses/{}/course/'.format(
settings.COURSE_ID_PATTERN,
),
include('openedx.features.course_experience.urls'),
),
# Course bookmarks
url(
r'^courses/{}/bookmarks/'.format(
settings.COURSE_ID_PATTERN,
),
include('openedx.features.course_bookmarks.urls'),
),
)
if settings.FEATURES["ENABLE_TEAMS"]:
......
......@@ -45,21 +45,18 @@ class EdxFragmentView(FragmentView):
else:
return settings.PIPELINE_JS[group]['source_filenames']
@abstractmethod
def vendor_js_dependencies(self):
"""
Returns list of the vendor JS files that this view depends on.
"""
return []
@abstractmethod
def js_dependencies(self):
"""
Returns list of the JavaScript files that this view depends on.
"""
return []
@abstractmethod
def css_dependencies(self):
"""
Returns list of the CSS files that this view depends on.
......
Open EdX Features
-----------------
This is the root package for Open edX features that extend the edX platform.
The intention is that these features would ideally live in an external
repository, but for now they live in edx-platform but are cleanly modularized.
<div class="message-banner" aria-live="polite"></div>
<div class="xblock xblock-student_view xblock-student_view-vertical xblock-initialized">
<div class="bookmark-button-wrapper">
<button class="btn bookmark-button"
aria-pressed="false"
data-bookmark-id="bilbo,usage_1">
<span class="bookmark-text">Bookmark this page</span>
<button class="btn bookmark-button"
aria-pressed="false"
data-bookmark-id="bilbo,usage_1">
<span class="bookmark-text">Bookmark this page</span>
</button>
</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="/courses/course-v1:test-course/course/">Course</a>
</span>
<span class="icon fa fa-angle-right" aria-hidden="true"></span>
<span class="nav-item">My Bookmarks</span>
</div>
</div>
</nav>
</div>
</header>
<div class="page-content">
<div class="course-bookmarks courseware-results-wrapper" id="main">
<div id="loading-message" aria-live="polite" aria-relevant="all"></div>
<div id="error-message" aria-live="polite"></div>
<div class="courseware-results search-results" data-course-id="course-v1:test-course" data-lang-code="en"></div>
</div>
</div>
</div>
......@@ -3,7 +3,7 @@
define([
'backbone',
'edx-ui-toolkit/js/pagination/paging-collection',
'js/bookmarks/models/bookmark'
'course_bookmarks/js/models/bookmark'
], function(Backbone, PagingCollection, BookmarkModel) {
return PagingCollection.extend({
model: BookmarkModel,
......@@ -24,5 +24,5 @@
}
});
});
})(define || RequireJS.define);
}(define || RequireJS.define));
(function(define) {
'use strict';
define(
[
'jquery',
'js/views/message_banner',
'course_bookmarks/js/collections/bookmarks',
'course_bookmarks/js/views/bookmarks_list'
],
function($, MessageBannerView, BookmarksCollection, BookmarksListView) {
return function(options) {
var courseId = options.courseId,
bookmarksApiUrl = options.bookmarksApiUrl,
bookmarksCollection = new BookmarksCollection([],
{
course_id: courseId,
url: bookmarksApiUrl
}
);
var bookmarksView = new BookmarksListView(
{
$el: options.$el,
collection: bookmarksCollection,
loadingMessageView: new MessageBannerView({el: $('#loading-message')}),
errorMessageView: new MessageBannerView({el: $('#error-message')})
}
);
bookmarksView.render();
bookmarksView.showBookmarks();
return bookmarksView;
};
}
);
}).call(this, define || RequireJS.define);
define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'common/js/spec_helpers/template_helpers', 'js/bookmarks/views/bookmark_button'
],
define([
'backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'common/js/spec_helpers/template_helpers', 'course_bookmarks/js/views/bookmark_button'
],
function(Backbone, $, _, AjaxHelpers, TemplateHelpers, BookmarkButtonView) {
'use strict';
describe('bookmarks.button', function() {
var timerCallback;
describe('BookmarkButtonView', function() {
var createBookmarkButtonView, verifyBookmarkButtonState;
var API_URL = 'bookmarks/api/v1/bookmarks/';
beforeEach(function() {
loadFixtures('js/fixtures/bookmarks/bookmark_button.html');
loadFixtures('course_bookmarks/fixtures/bookmark_button.html');
TemplateHelpers.installTemplates(
[
'templates/fields/message_banner'
]
);
timerCallback = jasmine.createSpy('timerCallback');
jasmine.createSpy('timerCallback');
jasmine.clock().install();
});
......@@ -25,7 +26,7 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helper
jasmine.clock().uninstall();
});
var createBookmarkButtonView = function(isBookmarked) {
createBookmarkButtonView = function(isBookmarked) {
return new BookmarkButtonView({
el: '.bookmark-button',
bookmarked: isBookmarked,
......@@ -35,7 +36,7 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helper
});
};
var verifyBookmarkButtonState = function(view, bookmarked) {
verifyBookmarkButtonState = function(view, bookmarked) {
if (bookmarked) {
expect(view.$el).toHaveAttr('aria-pressed', 'true');
expect(view.$el).toHaveClass('bookmarked');
......@@ -46,7 +47,7 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helper
expect(view.$el.data('bookmarkId')).toBe('bilbo,usage_1');
};
it('rendered correctly ', function() {
it('rendered correctly', function() {
var view = createBookmarkButtonView(false);
verifyBookmarkButtonState(view, false);
......
define([
'jquery',
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'course_bookmarks/js/spec_helpers/bookmark_helpers',
'course_bookmarks/js/course_bookmarks_factory'
],
function($, AjaxHelpers, BookmarkHelpers, CourseBookmarksFactory) {
'use strict';
describe('CourseBookmarksFactory', function() {
beforeEach(function() {
loadFixtures('course_bookmarks/fixtures/bookmarks.html');
});
it('can render the initial bookmarks', function() {
var requests = AjaxHelpers.requests(this),
expectedData = BookmarkHelpers.createBookmarksData(
{
numBookmarksToCreate: 10,
count: 15,
num_pages: 2,
current_page: 1,
start: 0
}
),
bookmarksView;
bookmarksView = CourseBookmarksFactory({
$el: $('.course-bookmarks'),
courseId: BookmarkHelpers.TEST_COURSE_ID,
bookmarksApiUrl: BookmarkHelpers.TEST_API_URL
});
BookmarkHelpers.verifyPaginationInfo(
requests, bookmarksView, expectedData, '1', 'Showing 1-10 out of 15 total'
);
});
});
});
define(
[
'underscore',
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'
],
function(_, AjaxHelpers) {
'use strict';
var TEST_COURSE_ID = 'course-v1:test-course';
var createBookmarksData = function(options) {
var data = {
count: options.count || 0,
num_pages: options.num_pages || 1,
current_page: options.current_page || 1,
start: options.start || 0,
results: []
},
i, bookmarkInfo;
for (i = 0; i < options.numBookmarksToCreate; i++) {
bookmarkInfo = {
id: i,
display_name: 'UNIT_DISPLAY_NAME_' + i,
created: new Date().toISOString(),
course_id: 'COURSE_ID',
usage_id: 'UNIT_USAGE_ID_' + i,
block_type: 'vertical',
path: [
{display_name: 'SECTION_DISPLAY_NAME', usage_id: 'SECTION_USAGE_ID'},
{display_name: 'SUBSECTION_DISPLAY_NAME', usage_id: 'SUBSECTION_USAGE_ID'}
]
};
data.results.push(bookmarkInfo);
}
return data;
};
var createBookmarkUrl = function(courseId, usageId) {
return '/courses/' + courseId + '/jump_to/' + usageId;
};
var breadcrumbTrail = function(path, unitDisplayName) {
return _.pluck(path, 'display_name').
concat([unitDisplayName]).
join(' <span class="icon fa fa-caret-right" aria-hidden="true"></span><span class="sr">-</span> ');
};
var verifyBookmarkedData = function(view, expectedData) {
var courseId, usageId;
var bookmarks = view.$('.bookmarks-results-list-item');
var results = expectedData.results;
var i, $bookmark;
expect(bookmarks.length, results.length);
for (i = 0; i < results.length; i++) {
$bookmark = $(bookmarks[i]);
courseId = results[i].course_id;
usageId = results[i].usage_id;
expect(bookmarks[i]).toHaveAttr('href', createBookmarkUrl(courseId, usageId));
expect($bookmark.data('bookmarkId')).toBe(i);
expect($bookmark.data('componentType')).toBe('vertical');
expect($bookmark.data('usageId')).toBe(usageId);
expect($bookmark.find('.list-item-breadcrumbtrail').html().trim())
.toBe(breadcrumbTrail(results[i].path, results[i].display_name));
expect($bookmark.find('.list-item-date').text().trim())
.toBe('Bookmarked on ' + view.humanFriendlyDate(results[i].created));
}
};
var verifyPaginationInfo = function(requests, view, expectedData, currentPage, headerMessage) {
AjaxHelpers.respondWithJson(requests, expectedData);
verifyBookmarkedData(view, expectedData);
expect(view.$('.paging-footer span.current-page').text().trim()).toBe(currentPage);
expect(view.$('.paging-header span').text().trim()).toBe(headerMessage);
};
return {
TEST_COURSE_ID: TEST_COURSE_ID,
TEST_API_URL: '/bookmarks/api',
createBookmarksData: createBookmarksData,
createBookmarkUrl: createBookmarkUrl,
verifyBookmarkedData: verifyBookmarkedData,
verifyPaginationInfo: verifyPaginationInfo
};
});
(function(define, undefined) {
(function(define) {
'use strict';
define(['gettext', 'jquery', 'underscore', 'backbone', 'js/views/message_banner'],
function(gettext, $, _, Backbone, MessageBannerView) {
......@@ -9,7 +9,7 @@
bookmarkedText: gettext('Bookmarked'),
events: {
'click': 'toggleBookmark'
click: 'toggleBookmark'
},
showBannerInterval: 5000, // time in ms
......@@ -46,14 +46,14 @@
view.setBookmarkState(true);
},
error: function(jqXHR) {
var response, userMessage;
try {
var response = jqXHR.responseText ? JSON.parse(jqXHR.responseText) : '';
var userMessage = response ? response.user_message : '';
response = jqXHR.responseText ? JSON.parse(jqXHR.responseText) : '';
userMessage = response ? response.user_message : '';
view.showError(userMessage);
} catch (err) {
view.showError();
}
catch (err) {
view.showError();
}
},
complete: function() {
view.$el.prop('disabled', false);
......
(function(define, undefined) {
(function(define) {
'use strict';
define(['gettext', 'jquery', 'underscore', 'backbone', 'logger', 'moment', 'edx-ui-toolkit/js/utils/html-utils',
'common/js/components/views/paging_header', 'common/js/components/views/paging_footer',
'text!templates/bookmarks/bookmarks-list.underscore'
],
define([
'gettext', 'jquery', 'underscore', 'backbone', 'logger', 'moment', 'edx-ui-toolkit/js/utils/html-utils',
'common/js/components/views/paging_header', 'common/js/components/views/paging_footer',
'text!course_bookmarks/templates/bookmarks-list.underscore'
],
function(gettext, $, _, Backbone, Logger, _moment, HtmlUtils,
PagingHeaderView, PagingFooterView, BookmarksListTemplate) {
PagingHeaderView, PagingFooterView, bookmarksListTemplate) {
var moment = _moment || window.moment;
return Backbone.View.extend({
......@@ -15,7 +16,7 @@
coursewareResultsWrapperEl: '.courseware-results-wrapper',
errorIcon: '<span class="fa fa-fw fa-exclamation-triangle message-error" aria-hidden="true"></span>',
loadingIcon: '<span class="fa fa-fw fa-spinner fa-pulse message-in-progress" aria-hidden="true"></span>',
loadingIcon: '<span class="fa fa-fw fa-spinner fa-pulse message-in-progress" aria-hidden="true"></span>', // eslint-disable-line max-len
errorMessage: gettext('An error has occurred. Please try again.'),
loadingMessage: gettext('Loading'),
......@@ -27,7 +28,7 @@
},
initialize: function(options) {
this.template = HtmlUtils.template(BookmarksListTemplate);
this.template = HtmlUtils.template(bookmarksListTemplate);
this.loadingMessageView = options.loadingMessageView;
this.errorMessageView = options.errorMessageView;
this.langCode = $(this.el).data('langCode');
......@@ -65,47 +66,39 @@
},
visitBookmark: function(event) {
var bookmarkedComponent = $(event.currentTarget);
var bookmark_id = bookmarkedComponent.data('bookmarkId');
var component_usage_id = bookmarkedComponent.data('usageId');
var component_type = bookmarkedComponent.data('componentType');
var $bookmarkedComponent = $(event.currentTarget),
bookmarkId = $bookmarkedComponent.data('bookmarkId'),
componentUsageId = $bookmarkedComponent.data('usageId'),
componentType = $bookmarkedComponent.data('componentType');
Logger.log(
'edx.bookmark.accessed',
'edx.bookmark.accessed',
{
bookmark_id: bookmark_id,
component_type: component_type,
component_usage_id: component_usage_id
bookmark_id: bookmarkId,
component_type: componentType,
component_usage_id: componentUsageId
}
).always(function() {
window.location.href = event.currentTarget.pathname;
});
).always(function() {
window.location.href = event.currentTarget.pathname;
});
},
/**
* Convert ISO 8601 formatted date into human friendly format. e.g, `2014-05-23T14:00:00Z` to `May 23, 2014`
* @param {String} isoDate - ISO 8601 formatted date string.
*/
/**
* Convert ISO 8601 formatted date into human friendly format.
*
* e.g, `2014-05-23T14:00:00Z` to `May 23, 2014`
*
* @param {String} isoDate - ISO 8601 formatted date string.
*/
humanFriendlyDate: function(isoDate) {
moment.locale(this.langCode);
return moment(isoDate).format('LL');
},
areBookmarksVisible: function() {
return this.$('#my-bookmarks').is(':visible');
},
hideBookmarks: function() {
this.$el.hide();
$(this.coursewareResultsWrapperEl).hide();
$(this.coursewareContentEl).css('display', 'table-cell');
},
showBookmarksContainer: function() {
$(this.coursewareContentEl).hide();
// Empty el if it's not empty to get the clean state.
// Empty el if it's not empty to get the clean state.
this.$el.html('');
this.$el.show();
$(this.coursewareResultsWrapperEl).css('display', 'table-cell');
},
showLoadingMessage: function() {
......
<div id="my-bookmarks" class="sr-is-focusable" tabindex="-1"></div>
<h2 class="bookmarks-results-header"><%= gettext("My Bookmarks") %></h2>
<% if (bookmarksCollection.length) { %>
......@@ -7,15 +6,27 @@
<div class='bookmarks-results-list'>
<% bookmarksCollection.each(function(bookmark, index) { %>
<a class="bookmarks-results-list-item" href="<%= bookmark.blockUrl() %>" aria-labelledby="bookmark-link-<%= index %>" data-bookmark-id="<%= bookmark.get('id') %>" data-component-type="<%= bookmark.get('block_type') %>" data-usage-id="<%= bookmark.get('usage_id') %>" aria-describedby="bookmark-type-<%= index %> bookmark-date-<%= index %>">
<a class="bookmarks-results-list-item"
href="<%- bookmark.blockUrl() %>"
aria-labelledby="bookmark-link-<%- index %>"
data-bookmark-id="<%- bookmark.get('id') %>"
data-component-type="<%- bookmark.get('block_type') %>"
data-usage-id="<%- bookmark.get('usage_id') %>"
aria-describedby="bookmark-type-<%- index %> bookmark-date-<%- index %>">
<div class="list-item-content">
<div class="list-item-left-section">
<h3 id="bookmark-link-<%= index %>" class="list-item-breadcrumbtrail"> <%= _.map(_.pluck(bookmark.get('path'), 'display_name'), _.escape).concat([_.escape(bookmark.get('display_name'))]).join(' <span class="icon fa fa-caret-right" aria-hidden="true"></span><span class="sr">-</span> ') %> </h3>
<p id="bookmark-date-<%= index %>" class="list-item-date"> <%= gettext("Bookmarked on") %> <%= humanFriendlyDate(bookmark.get('created')) %> </p>
<h3 id="bookmark-link-<%- index %>" class="list-item-breadcrumbtrail">
<%=
HtmlUtils.HTML(_.map(_.pluck(bookmark.get('path'), 'display_name'), _.escape)
.concat([_.escape(bookmark.get('display_name'))])
.join(' <span class="icon fa fa-caret-right" aria-hidden="true"></span><span class="sr">-</span> '))
%>
</h3>
<p id="bookmark-date-<%- index %>" class="list-item-date"> <%- gettext("Bookmarked on") %> <%- humanFriendlyDate(bookmark.get('created')) %> </p>
</div>
<p id="bookmark-type-<%= index %>" class="list-item-right-section">
<span aria-hidden="true"><%= gettext("View") %></span>
<p id="bookmark-type-<%- index %>" class="list-item-right-section">
<span aria-hidden="true"><%- gettext("View") %></span>
<span class="icon fa fa-arrow-right" aria-hidden="true"></span>
</p>
</div>
......@@ -28,14 +39,14 @@
<% } else {%>
<div class="bookmarks-empty">
<div class="bookmarks-empty-header">
<h3 class="hd-4 bookmarks-empty-header">
<span class="icon fa fa-bookmark-o bookmarks-empty-header-icon" aria-hidden="true"></span>
<%= gettext("You have not bookmarked any courseware pages yet.") %>
<%- gettext("You have not bookmarked any courseware pages yet") %>
<br>
</div>
</h3>
<div class="bookmarks-empty-detail">
<span class="bookmarks-empty-detail-title">
<%= gettext("Use bookmarks to help you easily return to courseware pages. To bookmark a page, select Bookmark in the upper right corner of that page. To see a list of all your bookmarks, select Bookmarks in the upper left corner of any courseware page.") %>
<%- gettext('Use bookmarks to help you easily return to courseware pages. To bookmark a page, click "Bookmark this page" under the page title.') %>
</span>
</div>
</div>
......
## mako
<%page expression_filter="h"/>
<%namespace name='static' file='../static_content.html'/>
<div class="course-bookmarks courseware-results-wrapper" id="main" tabindex="-1">
<div id="loading-message" aria-live="polite" aria-relevant="all"></div>
<div id="error-message" aria-live="polite"></div>
<div class="courseware-results search-results" data-course-id="${course.id}" data-lang-code="${language_preference}"></div>
</div>
## mako
<%! main_css = "style-main-v2" %>
<%page expression_filter="h"/>
<%inherit file="../main.html" />
<%namespace name='static' file='../static_content.html'/>
<%def name="online_help_token()"><% return "courseware" %></%def>
<%def name="course_name()">
<% return _("{course_number} Courseware").format(course_number=course.display_number_with_default) %>
</%def>
<%!
import json
from django.utils.translation import ugettext as _
from django.template.defaultfilters import escapejs
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
%>
<%block name="bodyclass">course</%block>
<%block name="pagetitle">${course_name()}</%block>
<%include file="../courseware/course_navigation.html" args="active_page='courseware'" />
<%block name="head_extra">
${HTML(bookmarks_fragment.head_html())}
</%block>
<%block name="footer_extra">
${HTML(bookmarks_fragment.foot_html())}
</%block>
<%block name="content">
<div class="course-view container" id="course-container">
<header class="page-header has-secondary">
## Breadcrumb navigation
<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</a>
</span>
<span class="icon fa fa-angle-right" aria-hidden="true"></span>
<span class="nav-item">${_('My Bookmarks')}</span>
</div>
</div>
</nav>
</div>
</header>
<div class="page-content">
${HTML(bookmarks_fragment.body_html())}
</div>
</div>
</%block>
## mako
<%!
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
%>
(function (require, define) {
require(['course_bookmarks/js/course_bookmarks_factory'], function (CourseBookmarksFactory) {
CourseBookmarksFactory({
$el: $(".course-bookmarks"),
courseId: '${unicode(course.id) | n, js_escaped_string}',
bookmarksApiUrl: '${bookmarks_api_url | n, js_escaped_string}',
});
});
}).call(this, require || RequireJS.require, define || RequireJS.define);
"""
Defines URLs for the course experience.
"""
from django.conf.urls import url
from views.course_bookmarks import CourseBookmarksView, CourseBookmarksFragmentView
urlpatterns = [
url(
r'^$',
CourseBookmarksView.as_view(),
name='openedx.course_bookmarks.home',
),
url(
r'^bookmarks_fragment$',
CourseBookmarksFragmentView.as_view(),
name='openedx.course_bookmarks.course_bookmarks_fragment_view',
),
]
"""
Views to show a course's bookmarks.
"""
from django.contrib.auth.decorators import login_required
from django.core.context_processors import csrf
from django.core.urlresolvers import reverse
from django.shortcuts import render_to_response
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 django.views.generic import View
from courseware.courses import get_course_with_access
from lms.djangoapps.courseware.tabs import CoursewareTab
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
from util.views import ensure_valid_course_key
from web_fragments.fragment import Fragment
from xmodule.modulestore.django import modulestore
class CourseBookmarksView(View):
"""
View showing the user's bookmarks 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):
"""
Displays the user's bookmarks for the specified course.
Arguments:
request: HTTP request
course_id (unicode): course id
"""
course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
course_url_name = CoursewareTab.main_course_url_name(request)
course_url = reverse(course_url_name, kwargs={'course_id': unicode(course.id)})
# Render the bookmarks list as a fragment
bookmarks_fragment = CourseBookmarksFragmentView().render_to_fragment(request, course_id=course_id)
# Render the course bookmarks page
context = {
'csrf': csrf(request)['csrf_token'],
'course': course,
'course_url': course_url,
'bookmarks_fragment': bookmarks_fragment,
'disable_courseware_js': True,
'uses_pattern_library': True,
}
return render_to_response('course_bookmarks/course-bookmarks.html', context)
class CourseBookmarksFragmentView(EdxFragmentView):
"""
Fragment view that shows a user's bookmarks for a course.
"""
def render_to_fragment(self, request, course_id=None, **kwargs):
"""
Renders the user's course bookmarks as a fragment.
"""
course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
context = {
'csrf': csrf(request)['csrf_token'],
'course': course,
'bookmarks_api_url': reverse('bookmarks'),
'language_preference': 'en', # TODO:
}
html = render_to_string('course_bookmarks/course-bookmarks-fragment.html', context)
inline_js = render_to_string('course_bookmarks/course_bookmarks_js.template', context)
fragment = Fragment(html)
self.add_fragment_resource_urls(fragment)
fragment.add_javascript(inline_js)
return fragment
<section class="course-outline" id="main">
<ol class="block-tree" role="tree">
<li aria-expanded="true" class="outline-item focusable" id="block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b"
role="treeitem" tabindex="0">
<div class="section-name">
<span class="icon fa fa-chevron-down" aria-hidden="true"></span>
<span>Introduction</span>
</div>
<ol class="outline-item focusable" role="group" tabindex="0">
<li role="treeitem" tabindex="-1" aria-expanded="true">
<a class="outline-item focusable" href="/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction"
id="block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction">
Demo Course Overview
</a>
</li>
</ol>
</li>
<li aria-expanded="true" class="outline-item focusable" id="block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations"
role="treeitem" tabindex="0">
<div class="section-name">
<span class="icon fa fa-chevron-down" aria-hidden="true"></span>
<span>Example Week 1: Getting Started</span>
</div>
<ol class="outline-item focusable" role="group" tabindex="0">
<li role="treeitem" tabindex="-1" aria-expanded="true">
<a class="outline-item focusable" href="/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5"
id="block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5">
Lesson 1 - Getting Started
</a>
</li>
<li role="treeitem" tabindex="-1" aria-expanded="true">
<a class="outline-item focusable" href="/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions"
id="block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions">
Homework - Question Styles
</a>
</li>
</ol>
</li>
<li aria-expanded="true" class="outline-item focusable" id="block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions"
role="treeitem" tabindex="0">
<div class="section-name">
<span class="icon fa fa-chevron-down" aria-hidden="true"></span>
<span>Example Week 2: Get Interactive</span>
</div>
<ol class="outline-item focusable" role="group" tabindex="0">
<li role="treeitem" tabindex="-1" aria-expanded="true">
<a class="outline-item focusable" href="/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@simulations"
id="block-v1:edX+DemoX+Demo_Course+type@sequential+block@simulations">
Lesson 2 - Let's Get Interactive!
</a>
</li>
<li role="treeitem" tabindex="-1" aria-expanded="true">
<a class="outline-item focusable" href="/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@graded_simulations"
id="block-v1:edX+DemoX+Demo_Course+type@sequential+block@graded_simulations">
Homework - Labs and Demos
</a>
</li>
<li role="treeitem" tabindex="-1" aria-expanded="true">
<a class="outline-item focusable" href="/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@175e76c4951144a29d46211361266e0e"
id="block-v1:edX+DemoX+Demo_Course+type@sequential+block@175e76c4951144a29d46211361266e0e">
Homework - Essays
</a>
</li>
</ol>
</li>
<li aria-expanded="true" class="outline-item focusable" id="block-v1:edX+DemoX+Demo_Course+type@chapter+block@social_integration"
role="treeitem" tabindex="0">
<div class="section-name">
<span class="icon fa fa-chevron-down" aria-hidden="true"></span>
<span>Example Week 3: Be Social</span>
</div>
<ol class="outline-item focusable" role="group" tabindex="0">
<li role="treeitem" tabindex="-1" aria-expanded="true">
<a class="outline-item focusable" href="/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@48ecb924d7fe4b66a230137626bfa93e"
id="block-v1:edX+DemoX+Demo_Course+type@sequential+block@48ecb924d7fe4b66a230137626bfa93e">
Lesson 3 - Be Social
</a>
</li>
<li role="treeitem" tabindex="-1" aria-expanded="true">
<a class="outline-item focusable" href="/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@dbe8fc027bcb4fe9afb744d2e8415855"
id="block-v1:edX+DemoX+Demo_Course+type@sequential+block@dbe8fc027bcb4fe9afb744d2e8415855">
Homework - Find Your Study Buddy
</a>
</li>
<li role="treeitem" tabindex="-1" aria-expanded="true">
<a class="outline-item focusable" href="/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@6ab9c442501d472c8ed200e367b4edfa"
id="block-v1:edX+DemoX+Demo_Course+type@sequential+block@6ab9c442501d472c8ed200e367b4edfa">
More Ways to Connect
</a>
</li>
</ol>
</li>
<li aria-expanded="true" class="outline-item focusable" id="block-v1:edX+DemoX+Demo_Course+type@chapter+block@1414ffd5143b4b508f739b563ab468b7"
role="treeitem" tabindex="0">
<div class="section-name">
<span class="icon fa fa-chevron-down" aria-hidden="true"></span>
<span>About Exams and Certificates</span>
</div>
<ol class="outline-item focusable" role="group" tabindex="0">
<li role="treeitem" tabindex="-1" aria-expanded="true">
<a class="outline-item focusable" href="/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@workflow"
id="block-v1:edX+DemoX+Demo_Course+type@sequential+block@workflow">
edX Exams
</a>
</li>
</ol>
</li>
<li aria-expanded="true" class="outline-item focusable" id="block-v1:edX+DemoX+Demo_Course+type@chapter+block@9fca584977d04885bc911ea76a9ef29e"
role="treeitem" tabindex="0">
<div class="section-name">
<span class="icon fa fa-chevron-down" aria-hidden="true"></span>
<span>holding section</span>
</div>
<ol class="outline-item focusable" role="group" tabindex="0">
<li role="treeitem" tabindex="-1" aria-expanded="true">
<a class="outline-item focusable" href="/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@07bc32474380492cb34f76e5f9d9a135"
id="block-v1:edX+DemoX+Demo_Course+type@sequential+block@07bc32474380492cb34f76e5f9d9a135">
New Subsection
</a>
</li>
</ol>
</li>
</ol>
</section>
(function(define) {
'use strict';
define([
'jquery',
'edx-ui-toolkit/js/utils/constants'
],
function($, constants) {
return function(root) {
// In the future this factory could instantiate a Backbone view or React component that handles events
$(root).keydown(function(event) {
var $focusable = $('.outline-item.focusable'),
currentFocusIndex = $.inArray(event.target, $focusable);
switch (event.keyCode) { // eslint-disable-line default-case
case constants.keyCodes.down:
event.preventDefault();
$focusable.eq(Math.min(currentFocusIndex + 1, $focusable.length - 1)).focus();
break;
case constants.keyCodes.up:
event.preventDefault();
$focusable.eq(Math.max(currentFocusIndex - 1, 0)).focus();
break;
}
});
};
}
);
}).call(this, define || RequireJS.define);
define([
'jquery',
'edx-ui-toolkit/js/utils/constants',
'course_experience/js/course_outline_factory'
],
function($, constants, CourseOutlineFactory) {
'use strict';
describe('Course outline factory', function() {
describe('keyboard listener', function() {
var triggerKeyListener = function(current, destination, keyCode) {
current.focus();
spyOn(destination, 'focus');
$('.block-tree').trigger($.Event('keydown', {
keyCode: keyCode,
target: current
}));
};
beforeEach(function() {
loadFixtures('course_experience/fixtures/course-outline-fragment.html');
CourseOutlineFactory('.block-tree');
});
describe('when the down arrow is pressed', function() {
it('moves focus from a subsection to the next subsection in the outline', function() {
var current = $('a.focusable:contains("Homework - Labs and Demos")')[0],
destination = $('a.focusable:contains("Homework - Essays")')[0];
triggerKeyListener(current, destination, constants.keyCodes.down);
expect(destination.focus).toHaveBeenCalled();
});
it('moves focus to the section list if at a section boundary', function() {
var current = $('li.focusable:contains("Example Week 3: Be Social")')[0],
destination = $('ol.focusable:contains("Lesson 3 - Be Social")')[0];
triggerKeyListener(current, destination, constants.keyCodes.down);
expect(destination.focus).toHaveBeenCalled();
});
it('moves focus to the next section if on the last subsection', function() {
var current = $('a.focusable:contains("Homework - Essays")')[0],
destination = $('li.focusable:contains("Example Week 3: Be Social")')[0];
triggerKeyListener(current, destination, constants.keyCodes.down);
expect(destination.focus).toHaveBeenCalled();
});
});
describe('when the up arrow is pressed', function() {
it('moves focus from a subsection to the previous subsection in the outline', function() {
var current = $('a.focusable:contains("Homework - Essays")')[0],
destination = $('a.focusable:contains("Homework - Labs and Demos")')[0];
triggerKeyListener(current, destination, constants.keyCodes.up);
expect(destination.focus).toHaveBeenCalled();
});
it('moves focus to the section group if at the first subsection', function() {
var current = $('a.focusable:contains("Lesson 3 - Be Social")')[0],
destination = $('ol.focusable:contains("Lesson 3 - Be Social")')[0];
triggerKeyListener(current, destination, constants.keyCodes.up);
expect(destination.focus).toHaveBeenCalled();
});
it('moves focus last subsection of the previous section if at a section boundary', function() {
var current = $('li.focusable:contains("Example Week 3: Be Social")')[0],
destination = $('a.focusable:contains("Homework - Essays")')[0];
triggerKeyListener(current, destination, constants.keyCodes.up);
expect(destination.focus).toHaveBeenCalled();
});
});
});
});
}
);
## mako
<%! main_css = "style-main-v2" %>
<%page expression_filter="h"/>
<%inherit file="../main.html" />
<%namespace name='static' file='../static_content.html'/>
<%def name="online_help_token()"><% return "courseware" %></%def>
<%def name="course_name()">
<% return _("{course_number} Courseware").format(course_number=course.display_number_with_default) %>
</%def>
<%!
import json
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
%>
<%block name="bodyclass">course</%block>
<%block name="pagetitle">${course_name()}</%block>
<%include file="../courseware/course_navigation.html" args="active_page='courseware'" />
<%block name="headextra">
${HTML(outline_fragment.head_html())}
</%block>
<%block name="js_extra">
${HTML(outline_fragment.foot_html())}
</%block>
<%block name="content">
<div class="course-view container" id="course-container">
<header class="page-header has-secondary">
<div class="page-header-secondary">
<div class="form-actions">
<a class="btn action-resume-course" href="${reverse('courseware', kwargs={'course_id': unicode(course.id.to_deprecated_string())})}">
${_("Resume Course")}
</a>
<a class="btn action-show-bookmarks" href="${reverse('openedx.course_bookmarks.home', args=[course.id])}">
${_("Bookmarks")}
</a>
</div>
<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-input"
type="search"
name="search"
id="search"
placeholder="${_('Search the course')}'"
/>
<button class="btn btn-small search-btn" type="button">${_('Search')}</button>
</form>
</div>
</div>
</header>
<div class="page-content">
${HTML(outline_fragment.body_html())}
</div>
</div>
</%block>
## mako
<%page expression_filter="h"/>
<%namespace name='static' file='../static_content.html'/>
<%!
from django.utils.translation import ugettext as _
%>
<%static:require_module_async module_name="course_experience/js/course_outline_factory" class_name="CourseOutlineFactory">
CourseOutlineFactory('.block-tree');
</%static:require_module_async>
<div class="course-outline" id="main" tabindex="-1">
<ol class="block-tree" role="tree">
% for section in blocks.get('children') or []:
<li
aria-expanded="true"
class="outline-item focusable section"
id="${ section['id'] }"
role="treeitem"
tabindex="0"
>
<div class="section-name">
<span>${ section['display_name'] }</span>
</div>
<ol class="outline-item focusable" role="group" tabindex="0">
% for subsection in section.get('children') or []:
<li class="subsection" role="treeitem" tabindex="-1" aria-expanded="true">
<a
class="outline-item focusable"
href="${ subsection['lms_web_url'] }"
id="${ subsection['id'] }"
>
${ subsection['display_name'] }
</a>
</li>
% endfor
</ol>
</li>
% endfor
</ol>
</div>
"""
Tests for the Course Outline view and supporting views.
"""
from django.core.urlresolvers import reverse
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
class TestCourseOutlinePage(SharedModuleStoreTestCase):
"""
Test the new course outline view.
"""
@classmethod
def setUpClass(cls):
"""Set up the simplest course possible."""
# setUpClassAndTestData() already calls setUpClass on SharedModuleStoreTestCase
# pylint: disable=super-method-not-called
with super(TestCourseOutlinePage, cls).setUpClassAndTestData():
cls.courses = []
course = CourseFactory.create()
with cls.store.bulk_operations(course.id):
chapter = ItemFactory.create(category='chapter', parent_location=course.location)
section = ItemFactory.create(category='sequential', parent_location=chapter.location)
ItemFactory.create(category='vertical', parent_location=section.location)
cls.courses.append(course)
course = CourseFactory.create()
with cls.store.bulk_operations(course.id):
chapter = ItemFactory.create(category='chapter', parent_location=course.location)
section = ItemFactory.create(category='sequential', parent_location=chapter.location)
section2 = ItemFactory.create(category='sequential', parent_location=chapter.location)
ItemFactory.create(category='vertical', parent_location=section.location)
ItemFactory.create(category='vertical', parent_location=section2.location)
@classmethod
def setUpTestData(cls):
"""Set up and enroll our fake user in the course."""
cls.password = 'test'
cls.user = UserFactory(password=cls.password)
for course in cls.courses:
CourseEnrollment.enroll(cls.user, course.id)
def setUp(self):
"""
Set up for the tests.
"""
super(TestCourseOutlinePage, self).setUp()
self.client.login(username=self.user.username, password=self.password)
def test_render(self):
for course in self.courses:
url = reverse(
'edx.course_experience.course_home',
kwargs={
'course_id': unicode(course.id),
}
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
response_content = response.content.decode("utf-8")
for chapter in course.children:
self.assertIn(chapter.display_name, response_content)
for section in chapter.children:
self.assertIn(section.display_name, response_content)
for vertical in section.children:
self.assertNotIn(vertical.display_name, response_content)
"""
Defines URLs for the course experience.
"""
from django.conf.urls import url
from views.course_home import CourseHomeView
from views.course_outline import CourseOutlineFragmentView
urlpatterns = [
url(
r'^$',
CourseHomeView.as_view(),
name='edx.course_experience.course_home',
),
url(
r'^outline_fragment$',
CourseOutlineFragmentView.as_view(),
name='edx.course_experience.course_outline_fragment_view',
),
]
"""
Views for the course home page.
"""
from django.contrib.auth.decorators import login_required
from django.core.context_processors import csrf
from django.shortcuts import render_to_response
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 django.views.generic import View
from courseware.courses import get_course_with_access
from opaque_keys.edx.keys import CourseKey
from util.views import ensure_valid_course_key
from course_outline import CourseOutlineFragmentView
class CourseHomeView(View):
"""
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):
"""
Displays the home page for the specified course.
Arguments:
request: HTTP request
course_id (unicode): course id
"""
course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
# Render the outline as a fragment
outline_fragment = CourseOutlineFragmentView().render_to_fragment(request, course_id=course_id)
# Render the entire unified course view
context = {
'csrf': csrf(request)['csrf_token'],
'course': course,
'outline_fragment': outline_fragment,
'disable_courseware_js': True,
'uses_pattern_library': True,
}
return render_to_response('course_experience/course-home.html', context)
"""
Views to show a course outline.
"""
from django.core.context_processors import csrf
from django.template.loader import render_to_string
from courseware.courses import get_course_with_access
from lms.djangoapps.course_api.blocks.api import get_blocks
from opaque_keys.edx.keys import CourseKey
from web_fragments.fragment import Fragment
from web_fragments.views import FragmentView
from xmodule.modulestore.django import modulestore
class CourseOutlineFragmentView(FragmentView):
"""
Course outline fragment to be shown in the unified course view.
"""
def populate_children(self, block, all_blocks):
"""
For a passed block, replace each id in its children array with the full representation of that child,
which will be looked up by id in the passed all_blocks dict.
Recursively do the same replacement for children of those children.
"""
children = block.get('children') or []
for i in range(len(children)):
child_id = block['children'][i]
child_detail = self.populate_children(all_blocks[child_id], all_blocks)
block['children'][i] = child_detail
return block
def render_to_fragment(self, request, course_id=None, **kwargs):
"""
Renders the course outline as a fragment.
"""
course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
course_usage_key = modulestore().make_course_usage_key(course_key)
all_blocks = get_blocks(
request,
course_usage_key,
user=request.user,
nav_depth=3,
requested_fields=['children', 'display_name', 'type'],
block_types_filter=['course', 'chapter', 'sequential']
)
course_block_tree = all_blocks['blocks'][all_blocks['root']] # Get the root of the block tree
context = {
'csrf': csrf(request)['csrf_token'],
'course': course,
# Recurse through the block tree, fleshing out each child object
'blocks': self.populate_children(course_block_tree, all_blocks['blocks'])
}
html = render_to_string('course_experience/course-outline-fragment.html', context)
return Fragment(html)
......@@ -5,13 +5,20 @@ from paver.easy import sh, task, cmdopts, needs, BuildFailure
import json
import os
import re
from string import join
from openedx.core.djangolib.markup import HTML
from .utils.envs import Env
from .utils.timer import timed
ALL_SYSTEMS = 'lms,cms,common,openedx,pavelib'
ALL_SYSTEMS = [
'cms',
'common',
'lms',
'openedx',
'pavelib',
]
def top_python_dirs(dirname):
......@@ -45,7 +52,7 @@ def find_fixme(options):
Run pylint on system code, only looking for fixme items.
"""
num_fixme = 0
systems = getattr(options, 'system', ALL_SYSTEMS).split(',')
systems = getattr(options, 'system', '').split(',') or ALL_SYSTEMS
for system in systems:
# Directory to put the pylint report in.
......@@ -93,7 +100,7 @@ def run_pylint(options):
num_violations = 0
violations_limit = int(getattr(options, 'limit', -1))
errors = getattr(options, 'errors', False)
systems = getattr(options, 'system', ALL_SYSTEMS).split(',')
systems = getattr(options, 'system', '').split(',') or ALL_SYSTEMS
# Make sure the metrics subdirectory exists
Env.METRICS_DIR.makedirs_p()
......@@ -234,7 +241,7 @@ def run_complexity():
Uses radon to examine cyclomatic complexity.
For additional details on radon, see http://radon.readthedocs.org/
"""
system_string = 'cms/ lms/ common/ openedx/'
system_string = join(ALL_SYSTEMS, '/ ') + '/'
complexity_report_dir = (Env.REPORT_DIR / "complexity")
complexity_report = complexity_report_dir / "python_complexity.log"
......
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