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. */ /* JavaScript for Vertical Student View. */
window.VerticalStudentView = function(runtime, element) { window.VerticalStudentView = function(runtime, element) {
'use strict'; '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 $element = $(element);
var $bookmarkButtonElement = $element.find('.bookmark-button'); var $bookmarkButtonElement = $element.find('.bookmark-button');
...@@ -10,7 +10,7 @@ window.VerticalStudentView = function(runtime, element) { ...@@ -10,7 +10,7 @@ window.VerticalStudentView = function(runtime, element) {
bookmarkId: $bookmarkButtonElement.data('bookmarkId'), bookmarkId: $bookmarkButtonElement.data('bookmarkId'),
usageId: $element.data('usageId'), usageId: $element.data('usageId'),
bookmarked: $element.parent('#seq_content').data('bookmarked'), bookmarked: $element.parent('#seq_content').data('bookmarked'),
apiUrl: $('.courseware-bookmarks-button').data('bookmarksApiUrl') apiUrl: $bookmarkButtonElement.data('bookmarksApiUrl')
}); });
}); });
}; };
...@@ -268,7 +268,17 @@ ...@@ -268,7 +268,17 @@
this.updatePageTitle(); this.updatePageTitle();
sequenceLinks = this.content_container.find('a.seqnav'); sequenceLinks = this.content_container.find('a.seqnav');
sequenceLinks.click(this.goto); 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(); 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): ...@@ -227,9 +227,9 @@ class CourseFixture(XBlockContainerFixture):
self._configure_course() self._configure_course()
@property @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" url = STUDIO_BASE_URL + '/course/' + self._course_key + "?format=json"
response = self.session.get(url, headers=self.headers) response = self.session.get(url, headers=self.headers)
......
...@@ -10,29 +10,23 @@ class BookmarksPage(CoursePage, PaginatedUIMixin): ...@@ -10,29 +10,23 @@ class BookmarksPage(CoursePage, PaginatedUIMixin):
""" """
Courseware Bookmarks Page. Courseware Bookmarks Page.
""" """
url = None url_path = "bookmarks"
url_path = "courseware/"
BOOKMARKS_BUTTON_SELECTOR = '.bookmarks-list-button' BOOKMARKS_BUTTON_SELECTOR = '.bookmarks-list-button'
BOOKMARKS_ELEMENT_SELECTOR = '#my-bookmarks'
BOOKMARKED_ITEMS_SELECTOR = '.bookmarks-results-list .bookmarks-results-list-item' BOOKMARKED_ITEMS_SELECTOR = '.bookmarks-results-list .bookmarks-results-list-item'
BOOKMARKED_BREADCRUMBS = BOOKMARKED_ITEMS_SELECTOR + ' .list-item-breadcrumbtrail' BOOKMARKED_BREADCRUMBS = BOOKMARKED_ITEMS_SELECTOR + ' .list-item-breadcrumbtrail'
def is_browser_on_page(self): def is_browser_on_page(self):
""" Verify if we are on correct page """ """ 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): def bookmarks_button_visible(self):
""" Check if bookmarks button is visible """ """ Check if bookmarks button is visible """
return self.q(css=self.BOOKMARKS_BUTTON_SELECTOR).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): def results_present(self):
""" Check if bookmarks results are present """ """ 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): def results_header_text(self):
""" Returns the bookmarks results header text """ """ 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]
...@@ -2,10 +2,14 @@ ...@@ -2,10 +2,14 @@
Courseware page. Courseware page.
""" """
from common.test.acceptance.pages.lms.course_page import CoursePage from bok_choy.page_object import PageObject, unguarded
from bok_choy.promise import EmptyPromise from bok_choy.promise import EmptyPromise
import re
from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.action_chains import ActionChains
from common.test.acceptance.pages.lms.bookmarks import BookmarksPage
from common.test.acceptance.pages.lms.course_page import CoursePage
class CoursewarePage(CoursePage): class CoursewarePage(CoursePage):
""" """
...@@ -17,8 +21,12 @@ class CoursewarePage(CoursePage): ...@@ -17,8 +21,12 @@ class CoursewarePage(CoursePage):
section_selector = '.chapter' section_selector = '.chapter'
subsection_selector = '.chapter-content-container a' subsection_selector = '.chapter-content-container a'
def __init__(self, browser, course_id):
super(CoursewarePage, self).__init__(browser, course_id)
self.nav = CourseNavPage(browser, self)
def is_browser_on_page(self): def is_browser_on_page(self):
return self.q(css='body.courseware').present return self.q(css='.course-content').present
@property @property
def chapter_count_in_navigation(self): def chapter_count_in_navigation(self):
...@@ -27,6 +35,7 @@ class CoursewarePage(CoursePage): ...@@ -27,6 +35,7 @@ class CoursewarePage(CoursePage):
""" """
return len(self.q(css='nav.course-navigation a.chapter')) return len(self.q(css='nav.course-navigation a.chapter'))
# TODO: TNL-6546: Remove and find callers.
@property @property
def num_sections(self): def num_sections(self):
""" """
...@@ -34,6 +43,7 @@ class CoursewarePage(CoursePage): ...@@ -34,6 +43,7 @@ class CoursewarePage(CoursePage):
""" """
return len(self.q(css=self.section_selector)) return len(self.q(css=self.section_selector))
# TODO: TNL-6546: Remove and find callers.
@property @property
def num_subsections(self): def num_subsections(self):
""" """
...@@ -274,7 +284,7 @@ class CoursewarePage(CoursePage): ...@@ -274,7 +284,7 @@ class CoursewarePage(CoursePage):
@property @property
def breadcrumb(self): def breadcrumb(self):
""" Return the course tree breadcrumb shown above the sequential bar """ """ Return the course tree breadcrumb shown above the sequential bar """
return [part.strip() for part in self.q(css='.path').text[0].split('>')] return [part.strip() for part in self.q(css='.path .position').text[0].split('>')]
def unit_title_visible(self): def unit_title_visible(self):
""" Check if unit title is visible """ """ Check if unit title is visible """
...@@ -301,6 +311,13 @@ class CoursewarePage(CoursePage): ...@@ -301,6 +311,13 @@ class CoursewarePage(CoursePage):
self.q(css='.bookmark-button').first.click() self.q(css='.bookmark-button').first.click()
EmptyPromise(lambda: self.bookmark_button_state != previous_state, "Bookmark button toggled").fulfill() EmptyPromise(lambda: self.bookmark_button_state != previous_state, "Bookmark button toggled").fulfill()
# TODO: TNL-6546: Remove this helper function
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 CoursewareSequentialTabPage(CoursePage): class CoursewareSequentialTabPage(CoursePage):
""" """
...@@ -319,3 +336,255 @@ class CoursewareSequentialTabPage(CoursePage): ...@@ -319,3 +336,255 @@ class CoursewareSequentialTabPage(CoursePage):
return the body of the sequential currently selected return the body of the sequential currently selected
""" """
return self.q(css='#seq_content .xblock').text[0] return self.q(css='#seq_content .xblock').text[0]
class CourseNavPage(PageObject):
"""
Handles navigation on the courseware pages, including sequence navigation and
breadcrumbs.
"""
url = None
def __init__(self, browser, parent_page):
super(CourseNavPage, self).__init__(browser)
self.parent_page = parent_page
# TODO: TNL-6546: Remove the following
self.unified_course_view = False
def is_browser_on_page(self):
return self.parent_page.is_browser_on_page
# TODO: TNL-6546: Remove method, outline no longer on courseware 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
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
# TODO: TNL-6546: Remove method, outline no longer on courseware page
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()
# TODO: TNL-6546: Remove method, outline no longer on courseware page
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
# TODO: TNL-6546: Remove method, outline no longer on courseware page
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
# TODO: TNL-6546: Remove method, outline no longer on courseware page
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
)
def go_to_outline(self):
"""
Navigates using breadcrumb to the course outline on the course home page.
Returns CourseHomePage page object.
"""
# To avoid circular dependency, importing inside the function
from common.test.acceptance.pages.lms.course_home import CourseHomePage
course_home_page = CourseHomePage(self.browser, self.parent_page.course_id)
self.q(css='.path a').click()
course_home_page.wait_for_page()
return course_home_page
@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.
"""
# TODO: TNL-6546: Remove if/else; always use unified_course_view version (if)
if self.unified_course_view:
# breadcrumb location of form: "SECTION_TITLE > SUBSECTION_TITLE > SEQUENTIAL_TITLE"
bread_crumb_current = self.q(css='.position').text
if len(bread_crumb_current) != 1:
self.warning("Could not find the current bread crumb with section and subsection.")
return False
return bread_crumb_current[0].strip().startswith(section_title + ' > ' + subsection_title + ' > ')
else:
# 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()
# TODO: TNL-6546: Remove from here and move to course_home.py:CourseOutlinePage
@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]
# TODO: TNL-6546: Remove all references to self.unified_course_view
# TODO: TNL-6546: Remove the following function
def visit_unified_course_view(self):
# use unified_course_view version of the nav
self.unified_course_view = True
# reload the same page with the unified course view
self.browser.get(self.browser.current_url + "&unified_course_view=1")
self.wait_for_page()
...@@ -8,9 +8,9 @@ import requests ...@@ -8,9 +8,9 @@ import requests
from common.test.acceptance.pages.studio.auto_auth import AutoAuthPage as StudioAutoAuthPage from common.test.acceptance.pages.studio.auto_auth import AutoAuthPage as StudioAutoAuthPage
from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage as LmsAutoAuthPage from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage as LmsAutoAuthPage
from common.test.acceptance.pages.lms.bookmarks import BookmarksPage from common.test.acceptance.pages.lms.bookmarks import BookmarksPage
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.courseware import CoursewarePage
from common.test.acceptance.pages.lms.course_nav import CourseNavPage from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage
from common.test.acceptance.pages.common.logout import LogoutPage from common.test.acceptance.pages.common.logout import LogoutPage
from common.test.acceptance.pages.common import BASE_URL from common.test.acceptance.pages.common import BASE_URL
...@@ -25,6 +25,40 @@ class BookmarksTestMixin(EventsTestMixin, UniqueCourseTest): ...@@ -25,6 +25,40 @@ class BookmarksTestMixin(EventsTestMixin, UniqueCourseTest):
USERNAME = "STUDENT" USERNAME = "STUDENT"
EMAIL = "student@example.com" EMAIL = "student@example.com"
def setUp(self):
super(BookmarksTestMixin, self).setUp()
self.studio_course_outline_page = StudioCourseOutlinePage(
self.browser,
self.course_info['org'],
self.course_info['number'],
self.course_info['run']
)
self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.course_home_page = CourseHomePage(self.browser, self.course_id)
self.bookmarks_page = BookmarksPage(self.browser, self.course_id)
# Get session to be used for bookmarking units
self.session = requests.Session()
params = {'username': self.USERNAME, 'email': self.EMAIL, 'course_id': self.course_id}
response = self.session.get(BASE_URL + "/auto_auth", params=params)
self.assertTrue(response.ok, "Failed to get session")
def setup_test(self, num_chapters=2):
"""
Setup test settings.
Arguments:
num_chapters: number of chapters to create in course
"""
self.create_course_fixture(num_chapters)
# Auto-auth register for the course.
LmsAutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, course_id=self.course_id).visit()
self.courseware_page.visit()
def create_course_fixture(self, num_chapters): def create_course_fixture(self, num_chapters):
""" """
Create course fixture Create course fixture
...@@ -59,50 +93,6 @@ class BookmarksTestMixin(EventsTestMixin, UniqueCourseTest): ...@@ -59,50 +93,6 @@ class BookmarksTestMixin(EventsTestMixin, UniqueCourseTest):
actual_events = self.wait_for_events(event_filter={'event_type': event_type}, number_of_matches=1) actual_events = self.wait_for_events(event_filter={'event_type': event_type}, number_of_matches=1)
self.assert_events_match(event_data, actual_events) self.assert_events_match(event_data, actual_events)
@attr(shard=8)
class BookmarksTest(BookmarksTestMixin):
"""
Tests to verify bookmarks functionality.
"""
def setUp(self):
"""
Initialize test setup.
"""
super(BookmarksTest, self).setUp()
self.course_outline_page = CourseOutlinePage(
self.browser,
self.course_info['org'],
self.course_info['number'],
self.course_info['run']
)
self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.bookmarks_page = BookmarksPage(self.browser, self.course_id)
self.course_nav = CourseNavPage(self.browser)
# Get session to be used for bookmarking units
self.session = requests.Session()
params = {'username': self.USERNAME, 'email': self.EMAIL, 'course_id': self.course_id}
response = self.session.get(BASE_URL + "/auto_auth", params=params)
self.assertTrue(response.ok, "Failed to get session")
def _test_setup(self, num_chapters=2):
"""
Setup test settings.
Arguments:
num_chapters: number of chapters to create in course
"""
self.create_course_fixture(num_chapters)
# Auto-auth register for the course.
LmsAutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, course_id=self.course_id).visit()
self.courseware_page.visit()
def _bookmark_unit(self, location): def _bookmark_unit(self, location):
""" """
Bookmark a unit Bookmark a unit
...@@ -124,7 +114,7 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -124,7 +114,7 @@ class BookmarksTest(BookmarksTestMixin):
) )
self.assertTrue(response.ok, "Failed to bookmark unit") self.assertTrue(response.ok, "Failed to bookmark unit")
def _bookmark_units(self, num_units): def bookmark_units(self, num_units):
""" """
Bookmark first `num_units` units Bookmark first `num_units` units
...@@ -135,6 +125,19 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -135,6 +125,19 @@ class BookmarksTest(BookmarksTestMixin):
for index in range(num_units): for index in range(num_units):
self._bookmark_unit(xblocks[index].locator) self._bookmark_unit(xblocks[index].locator)
@attr(shard=8)
class BookmarksTest(BookmarksTestMixin):
"""
Tests to verify bookmarks functionality.
"""
def setUp(self):
"""
Initialize test setup.
"""
super(BookmarksTest, self).setUp()
def _breadcrumb(self, num_units, modified_name=None): def _breadcrumb(self, num_units, modified_name=None):
""" """
Creates breadcrumbs for the first `num_units` Creates breadcrumbs for the first `num_units`
...@@ -166,10 +169,10 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -166,10 +169,10 @@ class BookmarksTest(BookmarksTestMixin):
).visit() ).visit()
# Visit course outline page in studio. # Visit course outline page in studio.
self.course_outline_page.visit() self.studio_course_outline_page.visit()
self.course_outline_page.wait_for_page() self.studio_course_outline_page.wait_for_page()
self.course_outline_page.section_at(index).delete() self.studio_course_outline_page.section_at(index).delete()
# Logout and login as a student. # Logout and login as a student.
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
...@@ -187,7 +190,7 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -187,7 +190,7 @@ class BookmarksTest(BookmarksTestMixin):
self.courseware_page.click_bookmark_unit_button() self.courseware_page.click_bookmark_unit_button()
self.assertEqual(self.courseware_page.bookmark_icon_visible, bookmark_icon_state) self.assertEqual(self.courseware_page.bookmark_icon_visible, bookmark_icon_state)
self.assertEqual(self.courseware_page.bookmark_button_state, bookmark_button_state) self.assertEqual(self.courseware_page.bookmark_button_state, bookmark_button_state)
self.bookmarks_page.click_bookmarks_button() self.bookmarks_page.visit()
self.assertEqual(self.bookmarks_page.count(), bookmarked_count) self.assertEqual(self.bookmarks_page.count(), bookmarked_count)
def _verify_pagination_info( def _verify_pagination_info(
...@@ -209,14 +212,6 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -209,14 +212,6 @@ class BookmarksTest(BookmarksTestMixin):
self.assertEqual(self.bookmarks_page.get_current_page_number(), current_page_number) self.assertEqual(self.bookmarks_page.get_current_page_number(), current_page_number)
self.assertEqual(self.bookmarks_page.get_total_pages, total_pages) self.assertEqual(self.bookmarks_page.get_total_pages, total_pages)
def _navigate_to_bookmarks_list(self):
"""
Navigates and verifies the bookmarks list page.
"""
self.bookmarks_page.click_bookmarks_button()
self.assertTrue(self.bookmarks_page.results_present())
self.assertEqual(self.bookmarks_page.results_header_text(), 'My Bookmarks')
def _verify_breadcrumbs(self, num_units, modified_name=None): def _verify_breadcrumbs(self, num_units, modified_name=None):
""" """
Verifies the breadcrumb trail. Verifies the breadcrumb trail.
...@@ -232,11 +227,11 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -232,11 +227,11 @@ class BookmarksTest(BookmarksTestMixin):
""" """
Update and publish the block/unit display name. Update and publish the block/unit display name.
""" """
self.course_outline_page.visit() self.studio_course_outline_page.visit()
self.course_outline_page.wait_for_page() self.studio_course_outline_page.wait_for_page()
self.course_outline_page.expand_all_subsections() self.studio_course_outline_page.expand_all_subsections()
section = self.course_outline_page.section_at(0) section = self.studio_course_outline_page.section_at(0)
container_page = section.subsection_at(0).unit_at(0).go_to() container_page = section.subsection_at(0).unit_at(0).go_to()
self.course_fixture._update_xblock(container_page.locator, { # pylint: disable=protected-access self.course_fixture._update_xblock(container_page.locator, { # pylint: disable=protected-access
...@@ -265,34 +260,41 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -265,34 +260,41 @@ class BookmarksTest(BookmarksTestMixin):
Then I click again on the bookmark button Then I click again on the bookmark button
And I should see a unit un-bookmarked And I should see a unit un-bookmarked
""" """
self._test_setup() self.setup_test()
for index in range(2): for index in range(2):
self.course_nav.go_to_section('TestSection{}'.format(index), 'TestSubsection{}'.format(index)) self.course_home_page.visit()
self.course_home_page.outline.go_to_section('TestSection{}'.format(index), 'TestSubsection{}'.format(index))
self._toggle_bookmark_and_verify(True, 'bookmarked', 1) self._toggle_bookmark_and_verify(True, 'bookmarked', 1)
self.bookmarks_page.click_bookmarks_button(False) self.course_home_page.visit()
self.course_home_page.outline.go_to_section('TestSection{}'.format(index), 'TestSubsection{}'.format(index))
self._toggle_bookmark_and_verify(False, '', 0) self._toggle_bookmark_and_verify(False, '', 0)
# TODO: TNL-6546: Remove this test
def test_courseware_bookmarks_button(self):
"""
Scenario: (Temporarily) test that the courseware's "Bookmarks" button works.
"""
self.setup_test()
self.bookmark_units(2)
self.courseware_page.visit()
self.courseware_page.click_bookmarks_button()
self.assertTrue(self.bookmarks_page.is_browser_on_page())
def test_empty_bookmarks_list(self): def test_empty_bookmarks_list(self):
""" """
Scenario: An empty bookmarks list is shown if there are no bookmarked units. Scenario: An empty bookmarks list is shown if there are no bookmarked units.
Given that I am a registered user Given that I am a registered user
And I visit my courseware page And I visit my bookmarks page
And I can see the Bookmarks button
When I click on Bookmarks button
Then I should see an empty bookmarks list Then I should see an empty bookmarks list
And empty bookmarks list content is correct And empty bookmarks list content is correct
""" """
self._test_setup() self.setup_test()
self.assertTrue(self.bookmarks_page.bookmarks_button_visible()) self.bookmarks_page.visit()
self.bookmarks_page.click_bookmarks_button() empty_list_text = (
self.assertEqual(self.bookmarks_page.results_header_text(), 'My Bookmarks') 'Use bookmarks to help you easily return to courseware pages. '
self.assertEqual(self.bookmarks_page.empty_header_text(), 'You have not bookmarked any courseware pages yet.') 'To bookmark a page, click "Bookmark this page" under the page title.')
empty_list_text = ("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.")
self.assertEqual(self.bookmarks_page.empty_list_text(), empty_list_text) self.assertEqual(self.bookmarks_page.empty_list_text(), empty_list_text)
def test_bookmarks_list(self): def test_bookmarks_list(self):
...@@ -300,18 +302,16 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -300,18 +302,16 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: A bookmarks list is shown if there are bookmarked units. Scenario: A bookmarks list is shown if there are bookmarked units.
Given that I am a registered user Given that I am a registered user
And I visit my courseware page
And I have bookmarked 2 units And I have bookmarked 2 units
When I click on Bookmarks button And I visit my bookmarks page
Then I should see a bookmarked list with 2 bookmark links Then I should see a bookmarked list with 2 bookmark links
And breadcrumb trail is correct for a bookmark And breadcrumb trail is correct for a bookmark
When I click on bookmarked link When I click on bookmarked link
Then I can navigate to correct bookmarked unit Then I can navigate to correct bookmarked unit
""" """
self._test_setup() self.setup_test()
self._bookmark_units(2) self.bookmark_units(2)
self.bookmarks_page.visit()
self._navigate_to_bookmarks_list()
self._verify_breadcrumbs(num_units=2) self._verify_breadcrumbs(num_units=2)
self._verify_pagination_info( self._verify_pagination_info(
...@@ -328,11 +328,10 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -328,11 +328,10 @@ class BookmarksTest(BookmarksTestMixin):
xblock_usage_ids = [xblock.locator for xblock in xblocks] xblock_usage_ids = [xblock.locator for xblock in xblocks]
# Verify link navigation # Verify link navigation
for index in range(2): for index in range(2):
self.bookmarks_page.visit()
self.bookmarks_page.click_bookmarked_block(index) self.bookmarks_page.click_bookmarked_block(index)
self.courseware_page.wait_for_page() self.courseware_page.wait_for_page()
self.assertIn(self.courseware_page.active_usage_id(), xblock_usage_ids) self.assertIn(self.courseware_page.active_usage_id(), xblock_usage_ids)
self.courseware_page.visit().wait_for_page()
self.bookmarks_page.click_bookmarks_button()
def test_bookmark_shows_updated_breadcrumb_after_publish(self): def test_bookmark_shows_updated_breadcrumb_after_publish(self):
""" """
...@@ -344,16 +343,14 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -344,16 +343,14 @@ class BookmarksTest(BookmarksTestMixin):
Then I visit unit page in studio Then I visit unit page in studio
Then I change unit display_name Then I change unit display_name
And I publish the changes And I publish the changes
Then I visit my courseware page Then I visit my bookmarks page
And I visit bookmarks list page
When I see the bookmark When I see the bookmark
Then I can see the breadcrumb trail Then I can see the breadcrumb trail has the updated display_name.
with updated display_name.
""" """
self._test_setup(num_chapters=1) self.setup_test(num_chapters=1)
self._bookmark_units(num_units=1) self.bookmark_units(num_units=1)
self._navigate_to_bookmarks_list() self.bookmarks_page.visit()
self._verify_breadcrumbs(num_units=1) self._verify_breadcrumbs(num_units=1)
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
...@@ -370,9 +367,8 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -370,9 +367,8 @@ class BookmarksTest(BookmarksTestMixin):
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
LmsAutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, course_id=self.course_id).visit() LmsAutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, course_id=self.course_id).visit()
self.courseware_page.visit()
self._navigate_to_bookmarks_list() self.bookmarks_page.visit()
self._verify_breadcrumbs(num_units=1, modified_name=modified_name) self._verify_breadcrumbs(num_units=1, modified_name=modified_name)
def test_unreachable_bookmark(self): def test_unreachable_bookmark(self):
...@@ -380,19 +376,18 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -380,19 +376,18 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: We should get a HTTP 404 for an unreachable bookmark. Scenario: We should get a HTTP 404 for an unreachable bookmark.
Given that I am a registered user Given that I am a registered user
And I visit my courseware page
And I have bookmarked 2 units And I have bookmarked 2 units
Then I delete a bookmarked unit And I delete a bookmarked unit
Then I click on Bookmarks button And I visit my bookmarks page
And I should see a bookmarked list Then I should see a bookmarked list
When I click on deleted bookmark When I click on the deleted bookmark
Then I should navigated to 404 page Then I should navigated to 404 page
""" """
self._test_setup(num_chapters=1) self.setup_test(num_chapters=1)
self._bookmark_units(1) self.bookmark_units(1)
self._delete_section(0) self._delete_section(0)
self._navigate_to_bookmarks_list() self.bookmarks_page.visit()
self._verify_pagination_info( self._verify_pagination_info(
bookmark_count_on_current_page=1, bookmark_count_on_current_page=1,
...@@ -411,15 +406,14 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -411,15 +406,14 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: We can't get bookmarks more than default page size. Scenario: We can't get bookmarks more than default page size.
Given that I am a registered user Given that I am a registered user
And I visit my courseware page
And I have bookmarked all the 11 units available And I have bookmarked all the 11 units available
Then I click on Bookmarks button And I visit my bookmarks page
And I should see a bookmarked list Then I should see a bookmarked list
And bookmark list contains 10 bookmarked items And the bookmark list should contain 10 bookmarked items
""" """
self._test_setup(11) self.setup_test(11)
self._bookmark_units(11) self.bookmark_units(11)
self._navigate_to_bookmarks_list() self.bookmarks_page.visit()
self._verify_pagination_info( self._verify_pagination_info(
bookmark_count_on_current_page=10, bookmark_count_on_current_page=10,
...@@ -434,17 +428,15 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -434,17 +428,15 @@ class BookmarksTest(BookmarksTestMixin):
""" """
Scenario: Bookmarks list pagination is working as expected for single page Scenario: Bookmarks list pagination is working as expected for single page
Given that I am a registered user Given that I am a registered user
And I visit my courseware page
And I have bookmarked all the 2 units available And I have bookmarked all the 2 units available
Then I click on Bookmarks button And I visit my bookmarks page
And I should see a bookmarked list with 2 bookmarked items Then I should see a bookmarked list with 2 bookmarked items
And I should see paging header and footer with correct data And I should see paging header and footer with correct data
And previous and next buttons are disabled And previous and next buttons are disabled
""" """
self._test_setup(num_chapters=2) self.setup_test(num_chapters=2)
self._bookmark_units(num_units=2) self.bookmark_units(num_units=2)
self.bookmarks_page.visit()
self.bookmarks_page.click_bookmarks_button()
self.assertTrue(self.bookmarks_page.results_present()) self.assertTrue(self.bookmarks_page.results_present())
self._verify_pagination_info( self._verify_pagination_info(
bookmark_count_on_current_page=2, bookmark_count_on_current_page=2,
...@@ -460,11 +452,10 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -460,11 +452,10 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: Next button is working as expected for bookmarks list pagination Scenario: Next button is working as expected for bookmarks list pagination
Given that I am a registered user Given that I am a registered user
And I visit my courseware page
And I have bookmarked all the 12 units available And I have bookmarked all the 12 units available
And I visit my bookmarks page
Then I click on Bookmarks button Then I should see a bookmarked list of 10 items
And I should see a bookmarked list of 10 items
And I should see paging header and footer with correct info And I should see paging header and footer with correct info
Then I click on next page button in footer Then I click on next page button in footer
...@@ -472,10 +463,10 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -472,10 +463,10 @@ class BookmarksTest(BookmarksTestMixin):
And I should see a bookmarked list with 2 items And I should see a bookmarked list with 2 items
And I should see paging header and footer with correct info And I should see paging header and footer with correct info
""" """
self._test_setup(num_chapters=12) self.setup_test(num_chapters=12)
self._bookmark_units(num_units=12) self.bookmark_units(num_units=12)
self.bookmarks_page.click_bookmarks_button() self.bookmarks_page.visit()
self.assertTrue(self.bookmarks_page.results_present()) self.assertTrue(self.bookmarks_page.results_present())
self._verify_pagination_info( self._verify_pagination_info(
...@@ -502,9 +493,8 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -502,9 +493,8 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: Previous button is working as expected for bookmarks list pagination Scenario: Previous button is working as expected for bookmarks list pagination
Given that I am a registered user Given that I am a registered user
And I visit my courseware page
And I have bookmarked all the 12 units available And I have bookmarked all the 12 units available
And I click on Bookmarks button And I visit my bookmarks page
Then I click on next page button in footer Then I click on next page button in footer
And I should be navigated to second page And I should be navigated to second page
...@@ -515,10 +505,10 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -515,10 +505,10 @@ class BookmarksTest(BookmarksTestMixin):
And I should be navigated to first page And I should be navigated to first page
And I should see paging header and footer with correct info And I should see paging header and footer with correct info
""" """
self._test_setup(num_chapters=12) self.setup_test(num_chapters=12)
self._bookmark_units(num_units=12) self.bookmark_units(num_units=12)
self.bookmarks_page.click_bookmarks_button() self.bookmarks_page.visit()
self.assertTrue(self.bookmarks_page.results_present()) self.assertTrue(self.bookmarks_page.results_present())
self.bookmarks_page.press_next_page_button() self.bookmarks_page.press_next_page_button()
...@@ -546,19 +536,17 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -546,19 +536,17 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: Bookmarks list pagination works as expected for valid page number Scenario: Bookmarks list pagination works as expected for valid page number
Given that I am a registered user Given that I am a registered user
And I visit my courseware page
And I have bookmarked all the 12 units available And I have bookmarked all the 12 units available
And I visit my bookmarks page
Then I click on Bookmarks button Then I should see a bookmarked list
And I should see a bookmarked list
And I should see total page value is 2 And I should see total page value is 2
Then I enter 2 in the page number input Then I enter 2 in the page number input
And I should be navigated to page 2 And I should be navigated to page 2
""" """
self._test_setup(num_chapters=11) self.setup_test(num_chapters=11)
self._bookmark_units(num_units=11) self.bookmark_units(num_units=11)
self.bookmarks_page.click_bookmarks_button() self.bookmarks_page.visit()
self.assertTrue(self.bookmarks_page.results_present()) self.assertTrue(self.bookmarks_page.results_present())
self.assertEqual(self.bookmarks_page.get_total_pages, 2) self.assertEqual(self.bookmarks_page.get_total_pages, 2)
...@@ -577,18 +565,17 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -577,18 +565,17 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: Bookmarks list pagination works as expected for invalid page number Scenario: Bookmarks list pagination works as expected for invalid page number
Given that I am a registered user Given that I am a registered user
And I visit my courseware page
And I have bookmarked all the 11 units available And I have bookmarked all the 11 units available
Then I click on Bookmarks button And I visit my bookmarks page
And I should see a bookmarked list Then I should see a bookmarked list
And I should see total page value is 2 And I should see total page value is 2
Then I enter 3 in the page number input Then I enter 3 in the page number input
And I should stay at page 1 And I should stay at page 1
""" """
self._test_setup(num_chapters=11) self.setup_test(num_chapters=11)
self._bookmark_units(num_units=11) self.bookmark_units(num_units=11)
self.bookmarks_page.click_bookmarks_button() self.bookmarks_page.visit()
self.assertTrue(self.bookmarks_page.results_present()) self.assertTrue(self.bookmarks_page.results_present())
self.assertEqual(self.bookmarks_page.get_total_pages, 2) self.assertEqual(self.bookmarks_page.get_total_pages, 2)
...@@ -612,7 +599,7 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -612,7 +599,7 @@ class BookmarksTest(BookmarksTestMixin):
When I click on bookmarked unit When I click on bookmarked unit
Then `edx.course.bookmark.accessed` event is emitted Then `edx.course.bookmark.accessed` event is emitted
""" """
self._test_setup(num_chapters=1) self.setup_test(num_chapters=1)
self.reset_event_tracking() self.reset_event_tracking()
# create expected event data # create expected event data
...@@ -626,8 +613,8 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -626,8 +613,8 @@ class BookmarksTest(BookmarksTestMixin):
} }
} }
] ]
self._bookmark_units(num_units=1) self.bookmark_units(num_units=1)
self.bookmarks_page.click_bookmarks_button() self.bookmarks_page.visit()
self._verify_pagination_info( self._verify_pagination_info(
bookmark_count_on_current_page=1, bookmark_count_on_current_page=1,
...@@ -640,3 +627,18 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -640,3 +627,18 @@ class BookmarksTest(BookmarksTestMixin):
self.bookmarks_page.click_bookmarked_block(0) self.bookmarks_page.click_bookmarked_block(0)
self.verify_event_data('edx.bookmark.accessed', event_data) self.verify_event_data('edx.bookmark.accessed', event_data)
@attr('a11y')
class BookmarksA11yTests(BookmarksTestMixin):
"""
Tests for checking the a11y of the bookmarks page.
"""
def test_view_a11y(self):
"""
Verify the basic accessibility of the bookmarks page while paginated.
"""
self.setup_test(num_chapters=11)
self.bookmark_units(num_units=11)
self.bookmarks_page.visit()
self.bookmarks_page.a11y_audit.check_for_accessibility_errors()
...@@ -7,10 +7,11 @@ from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureD ...@@ -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.fixtures.certificates import CertificateConfigFixture
from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage 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.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.course_info import CourseInfoPage
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.progress import ProgressPage from common.test.acceptance.pages.lms.progress import ProgressPage
from common.test.acceptance.pages.lms.tab_nav import TabNavPage
@attr(shard=5) @attr(shard=5)
...@@ -154,7 +155,8 @@ class CertificateProgressPageTest(UniqueCourseTest): ...@@ -154,7 +155,8 @@ class CertificateProgressPageTest(UniqueCourseTest):
self.course_info_page = CourseInfoPage(self.browser, self.course_id) self.course_info_page = CourseInfoPage(self.browser, self.course_id)
self.progress_page = ProgressPage(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) self.tab_nav = TabNavPage(self.browser)
def log_in_as_unique_user(self): def log_in_as_unique_user(self):
...@@ -205,38 +207,42 @@ class CertificateProgressPageTest(UniqueCourseTest): ...@@ -205,38 +207,42 @@ class CertificateProgressPageTest(UniqueCourseTest):
Problems were added in the setUp Problems were added in the setUp
""" """
self.course_info_page.visit() # self.course_info_page.visit()
self.tab_nav.go_to_tab('Course') # 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 # 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 # 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 # 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 # 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 # Select correct radio buttons for the answer
self.course_nav.q(css='fieldset div.field:nth-child(2) input').nth(1).click() self.courseware_page.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(4) input').nth(1).click()
# Submit the answer # Submit the answer
self.course_nav.q(css='button.submit').click() self.courseware_page.q(css='button.submit').click()
self.course_nav.wait_for_ajax() self.courseware_page.wait_for_ajax()
# Navigate to the 'Test Subsection 2' of 'Test Section 2' # 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 # 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 # 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 # Submit the answer
self.course_nav.q(css='button.submit').click() self.courseware_page.q(css='button.submit').click()
self.course_nav.wait_for_ajax() self.courseware_page.wait_for_ajax()
...@@ -8,7 +8,7 @@ import textwrap ...@@ -8,7 +8,7 @@ import textwrap
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from common.test.acceptance.tests.helpers import UniqueCourseTest, TestWithSearchIndexMixin 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.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.studio.library import StudioLibraryContentEditor, StudioLibraryContainerXBlockWrapper
from common.test.acceptance.pages.lms.courseware import CoursewarePage from common.test.acceptance.pages.lms.courseware import CoursewarePage
from common.test.acceptance.pages.lms.library import LibraryContentXBlockWrapper from common.test.acceptance.pages.lms.library import LibraryContentXBlockWrapper
...@@ -44,7 +44,7 @@ class LibraryContentTestBase(UniqueCourseTest): ...@@ -44,7 +44,7 @@ class LibraryContentTestBase(UniqueCourseTest):
self.courseware_page = CoursewarePage(self.browser, self.course_id) self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.course_outline = CourseOutlinePage( self.studio_course_outline = StudioCourseOutlinePage(
self.browser, self.browser,
self.course_info['org'], self.course_info['org'],
self.course_info['number'], self.course_info['number'],
...@@ -116,9 +116,9 @@ class LibraryContentTestBase(UniqueCourseTest): ...@@ -116,9 +116,9 @@ class LibraryContentTestBase(UniqueCourseTest):
if change_login: if change_login:
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True) 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() return subsection.expand_subsection().unit(UNIT_NAME).go_to()
def _goto_library_block_page(self, block_id=None): def _goto_library_block_page(self, block_id=None):
......
...@@ -21,26 +21,27 @@ from common.test.acceptance.tests.helpers import ( ...@@ -21,26 +21,27 @@ from common.test.acceptance.tests.helpers import (
select_option_by_text, select_option_by_text,
get_selected_option_text get_selected_option_text
) )
from common.test.acceptance.pages.common.logout import LogoutPage
from common.test.acceptance.pages.lms import BASE_URL from common.test.acceptance.pages.lms import BASE_URL
from common.test.acceptance.pages.lms.account_settings import AccountSettingsPage from common.test.acceptance.pages.lms.account_settings import AccountSettingsPage
from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage
from common.test.acceptance.pages.lms.bookmarks import BookmarksPage
from common.test.acceptance.pages.lms.create_mode import ModeCreationPage from common.test.acceptance.pages.lms.create_mode import ModeCreationPage
from common.test.acceptance.pages.common.logout import LogoutPage 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.course_info import CourseInfoPage
from common.test.acceptance.pages.lms.tab_nav import TabNavPage from common.test.acceptance.pages.lms.course_wiki import (
from common.test.acceptance.pages.lms.course_nav import CourseNavPage CourseWikiPage, CourseWikiEditPage, CourseWikiHistoryPage, CourseWikiChildrenPage
from common.test.acceptance.pages.lms.progress import ProgressPage )
from common.test.acceptance.pages.lms.courseware import CoursewarePage
from common.test.acceptance.pages.lms.dashboard import DashboardPage from common.test.acceptance.pages.lms.dashboard import DashboardPage
from common.test.acceptance.pages.lms.login_and_register import CombinedLoginAndRegisterPage, ResetPasswordPage
from common.test.acceptance.pages.lms.pay_and_verify import PaymentAndVerificationFlow, FakePaymentPage
from common.test.acceptance.pages.lms.progress import ProgressPage
from common.test.acceptance.pages.lms.problem import ProblemPage from common.test.acceptance.pages.lms.problem import ProblemPage
from common.test.acceptance.pages.lms.tab_nav import TabNavPage
from common.test.acceptance.pages.lms.track_selection import TrackSelectionPage
from common.test.acceptance.pages.lms.video.video import VideoPage from common.test.acceptance.pages.lms.video.video import VideoPage
from common.test.acceptance.pages.lms.courseware import CoursewarePage
from common.test.acceptance.pages.studio.settings import SettingsPage from common.test.acceptance.pages.studio.settings import SettingsPage
from common.test.acceptance.pages.lms.login_and_register import CombinedLoginAndRegisterPage, ResetPasswordPage
from common.test.acceptance.pages.lms.track_selection import TrackSelectionPage
from common.test.acceptance.pages.lms.pay_and_verify import PaymentAndVerificationFlow, FakePaymentPage
from common.test.acceptance.pages.lms.course_wiki import (
CourseWikiPage, CourseWikiEditPage, CourseWikiHistoryPage, CourseWikiChildrenPage
)
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc
...@@ -634,7 +635,6 @@ class CourseWikiTest(UniqueCourseTest): ...@@ -634,7 +635,6 @@ class CourseWikiTest(UniqueCourseTest):
children_page.a11y_audit.check_for_accessibility_errors() children_page.a11y_audit.check_for_accessibility_errors()
@attr(shard=1)
class HighLevelTabTest(UniqueCourseTest): class HighLevelTabTest(UniqueCourseTest):
""" """
Tests that verify each of the high-level tabs available within a course. Tests that verify each of the high-level tabs available within a course.
...@@ -651,7 +651,8 @@ class HighLevelTabTest(UniqueCourseTest): ...@@ -651,7 +651,8 @@ class HighLevelTabTest(UniqueCourseTest):
self.course_info_page = CourseInfoPage(self.browser, self.course_id) self.course_info_page = CourseInfoPage(self.browser, self.course_id)
self.progress_page = ProgressPage(self.browser, self.course_id) self.progress_page = ProgressPage(self.browser, self.course_id)
self.course_nav = CourseNavPage(self.browser) self.course_home_page = CourseHomePage(self.browser, self.course_id)
self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.tab_nav = TabNavPage(self.browser) self.tab_nav = TabNavPage(self.browser)
self.video = VideoPage(self.browser) self.video = VideoPage(self.browser)
...@@ -678,13 +679,16 @@ class HighLevelTabTest(UniqueCourseTest): ...@@ -678,13 +679,16 @@ class HighLevelTabTest(UniqueCourseTest):
), ),
XBlockFixtureDesc('chapter', 'Test Section 2').add_children( XBlockFixtureDesc('chapter', 'Test Section 2').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection 2'), XBlockFixtureDesc('sequential', 'Test Subsection 2'),
XBlockFixtureDesc('sequential', 'Test Subsection 3'), XBlockFixtureDesc('sequential', 'Test Subsection 3').add_children(
XBlockFixtureDesc('problem', 'Test Problem A', data=load_data_str('multiple_choice.xml'))
),
) )
).install() ).install()
# Auto-auth register for the course # Auto-auth register for the course
AutoAuthPage(self.browser, course_id=self.course_id).visit() AutoAuthPage(self.browser, course_id=self.course_id).visit()
@attr(shard=1)
def test_course_info(self): def test_course_info(self):
""" """
Navigate to the course info page. Navigate to the course info page.
...@@ -702,6 +706,7 @@ class HighLevelTabTest(UniqueCourseTest): ...@@ -702,6 +706,7 @@ class HighLevelTabTest(UniqueCourseTest):
self.assertEqual(len(handout_links), 1) self.assertEqual(len(handout_links), 1)
self.assertIn('demoPDF.pdf', handout_links[0]) self.assertIn('demoPDF.pdf', handout_links[0])
@attr(shard=1)
def test_progress(self): def test_progress(self):
""" """
Navigate to the progress page. Navigate to the progress page.
...@@ -719,6 +724,7 @@ class HighLevelTabTest(UniqueCourseTest): ...@@ -719,6 +724,7 @@ class HighLevelTabTest(UniqueCourseTest):
actual_scores = self.progress_page.scores(CHAPTER, SECTION) actual_scores = self.progress_page.scores(CHAPTER, SECTION)
self.assertEqual(actual_scores, EXPECTED_SCORES) self.assertEqual(actual_scores, EXPECTED_SCORES)
@attr(shard=1)
def test_static_tab(self): def test_static_tab(self):
""" """
Navigate to a static tab (course content) Navigate to a static tab (course content)
...@@ -728,6 +734,7 @@ class HighLevelTabTest(UniqueCourseTest): ...@@ -728,6 +734,7 @@ class HighLevelTabTest(UniqueCourseTest):
self.tab_nav.go_to_tab('Test Static Tab') self.tab_nav.go_to_tab('Test Static Tab')
self.assertTrue(self.tab_nav.is_on_tab('Test Static Tab')) self.assertTrue(self.tab_nav.is_on_tab('Test Static Tab'))
@attr(shard=1)
def test_static_tab_with_mathjax(self): def test_static_tab_with_mathjax(self):
""" """
Navigate to a static tab (course content) Navigate to a static tab (course content)
...@@ -740,6 +747,7 @@ class HighLevelTabTest(UniqueCourseTest): ...@@ -740,6 +747,7 @@ class HighLevelTabTest(UniqueCourseTest):
# Verify that Mathjax has rendered # Verify that Mathjax has rendered
self.tab_nav.mathjax_has_rendered() self.tab_nav.mathjax_has_rendered()
@attr(shard=1)
def test_wiki_tab_first_time(self): def test_wiki_tab_first_time(self):
""" """
Navigate to the course wiki tab. When the wiki is accessed for Navigate to the course wiki tab. When the wiki is accessed for
...@@ -760,6 +768,8 @@ class HighLevelTabTest(UniqueCourseTest): ...@@ -760,6 +768,8 @@ class HighLevelTabTest(UniqueCourseTest):
) )
self.assertEqual(expected_article_name, course_wiki.article_name) self.assertEqual(expected_article_name, course_wiki.article_name)
# TODO: TNL-6546: This whole function will be able to go away, replaced by test_course_home below.
@attr(shard=1)
def test_courseware_nav(self): def test_courseware_nav(self):
""" """
Navigate to a particular unit in the course. Navigate to a particular unit in the course.
...@@ -774,26 +784,86 @@ class HighLevelTabTest(UniqueCourseTest): ...@@ -774,26 +784,86 @@ class HighLevelTabTest(UniqueCourseTest):
'Test Section 2': ['Test Subsection 2', 'Test Subsection 3'] 'Test Section 2': ['Test Subsection 2', 'Test Subsection 3']
} }
actual_sections = self.course_nav.sections actual_sections = self.courseware_page.nav.sections
for section, subsections in EXPECTED_SECTIONS.iteritems(): for section, subsections in EXPECTED_SECTIONS.iteritems():
self.assertIn(section, actual_sections) self.assertIn(section, actual_sections)
self.assertEqual(actual_sections[section], EXPECTED_SECTIONS[section]) self.assertEqual(actual_sections[section], EXPECTED_SECTIONS[section])
# Navigate to a particular section # Navigate to a particular section
self.course_nav.go_to_section('Test Section', 'Test Subsection') self.courseware_page.nav.go_to_section('Test Section', 'Test Subsection')
# Check the sequence items # Check the sequence items
EXPECTED_ITEMS = ['Test Problem 1', 'Test Problem 2', 'Test HTML'] EXPECTED_ITEMS = ['Test Problem 1', 'Test Problem 2', 'Test HTML']
actual_items = self.course_nav.sequence_items actual_items = self.courseware_page.nav.sequence_items
self.assertEqual(len(actual_items), len(EXPECTED_ITEMS))
for expected in EXPECTED_ITEMS:
self.assertIn(expected, actual_items)
# Navigate to a particular section other than the default landing section.
self.courseware_page.nav.go_to_section('Test Section 2', 'Test Subsection 3')
self.assertTrue(self.courseware_page.nav.is_on_section('Test Section 2', 'Test Subsection 3'))
@attr(shard=1)
def test_course_home(self):
"""
Navigate to the course home page using the tab.
Includes smoke test of course outline, courseware page, and breadcrumbs.
"""
# TODO: TNL-6546: Use tab navigation and remove course_home_page.visit().
#self.course_info_page.visit()
#self.tab_nav.go_to_tab('Course')
self.course_home_page.visit()
# TODO: TNL-6546: Remove unified_course_view.
self.course_home_page.unified_course_view = True
self.courseware_page.nav.unified_course_view = True
# Check that the tab lands on the course home page.
self.assertTrue(self.course_home_page.is_browser_on_page())
# Check that the course navigation appears correctly
EXPECTED_SECTIONS = {
'Test Section': ['Test Subsection'],
'Test Section 2': ['Test Subsection 2', 'Test Subsection 3']
}
actual_sections = self.course_home_page.outline.sections
for section, subsections in EXPECTED_SECTIONS.iteritems():
self.assertIn(section, actual_sections)
self.assertEqual(actual_sections[section], EXPECTED_SECTIONS[section])
# Navigate to a particular section
self.course_home_page.outline.go_to_section('Test Section', 'Test Subsection')
# Check the sequence items on the courseware page
EXPECTED_ITEMS = ['Test Problem 1', 'Test Problem 2', 'Test HTML']
actual_items = self.courseware_page.nav.sequence_items
self.assertEqual(len(actual_items), len(EXPECTED_ITEMS)) self.assertEqual(len(actual_items), len(EXPECTED_ITEMS))
for expected in EXPECTED_ITEMS: for expected in EXPECTED_ITEMS:
self.assertIn(expected, actual_items) self.assertIn(expected, actual_items)
# Use outline breadcrumb to get back to course home page.
self.courseware_page.nav.go_to_outline()
# Navigate to a particular section other than the default landing section. # Navigate to a particular section other than the default landing section.
self.course_nav.go_to_section('Test Section 2', 'Test Subsection 3') self.course_home_page.outline.go_to_section('Test Section 2', 'Test Subsection 3')
self.assertTrue(self.course_nav.is_on_section('Test Section 2', 'Test Subsection 3')) self.assertTrue(self.courseware_page.nav.is_on_section('Test Section 2', 'Test Subsection 3'))
# Verify that we can navigate to the bookmarks page
self.course_home_page.visit()
self.course_home_page.click_bookmarks_button()
bookmarks_page = BookmarksPage(self.browser, self.course_id)
self.assertTrue(bookmarks_page.is_browser_on_page())
@attr('a11y')
def test_course_home_a11y(self):
self.course_home_page.visit()
self.course_home_page.a11y_audit.check_for_accessibility_errors()
@attr(shard=1) @attr(shard=1)
...@@ -878,7 +948,6 @@ class VisibleToStaffOnlyTest(UniqueCourseTest): ...@@ -878,7 +948,6 @@ class VisibleToStaffOnlyTest(UniqueCourseTest):
).install() ).install()
self.courseware_page = CoursewarePage(self.browser, self.course_id) self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.course_nav = CourseNavPage(self.browser)
def test_visible_to_staff(self): def test_visible_to_staff(self):
""" """
...@@ -891,16 +960,16 @@ class VisibleToStaffOnlyTest(UniqueCourseTest): ...@@ -891,16 +960,16 @@ class VisibleToStaffOnlyTest(UniqueCourseTest):
course_id=self.course_id, staff=True).visit() course_id=self.course_id, staff=True).visit()
self.courseware_page.visit() self.courseware_page.visit()
self.assertEqual(3, len(self.course_nav.sections['Test Section'])) self.assertEqual(3, len(self.courseware_page.nav.sections['Test Section']))
self.course_nav.go_to_section("Test Section", "Subsection With Locked Unit") self.courseware_page.nav.go_to_section("Test Section", "Subsection With Locked Unit")
self.assertEqual([u'Locked Unit', u'Unlocked Unit'], self.course_nav.sequence_items) self.assertEqual([u'Locked Unit', u'Unlocked Unit'], self.courseware_page.nav.sequence_items)
self.course_nav.go_to_section("Test Section", "Unlocked Subsection") self.courseware_page.nav.go_to_section("Test Section", "Unlocked Subsection")
self.assertEqual([u'Test Unit'], self.course_nav.sequence_items) self.assertEqual([u'Test Unit'], self.courseware_page.nav.sequence_items)
self.course_nav.go_to_section("Test Section", "Locked Subsection") self.courseware_page.nav.go_to_section("Test Section", "Locked Subsection")
self.assertEqual([u'Test Unit'], self.course_nav.sequence_items) self.assertEqual([u'Test Unit'], self.courseware_page.nav.sequence_items)
def test_visible_to_student(self): def test_visible_to_student(self):
""" """
...@@ -913,13 +982,13 @@ class VisibleToStaffOnlyTest(UniqueCourseTest): ...@@ -913,13 +982,13 @@ class VisibleToStaffOnlyTest(UniqueCourseTest):
course_id=self.course_id, staff=False).visit() course_id=self.course_id, staff=False).visit()
self.courseware_page.visit() self.courseware_page.visit()
self.assertEqual(2, len(self.course_nav.sections['Test Section'])) self.assertEqual(2, len(self.courseware_page.nav.sections['Test Section']))
self.course_nav.go_to_section("Test Section", "Subsection With Locked Unit") self.courseware_page.nav.go_to_section("Test Section", "Subsection With Locked Unit")
self.assertEqual([u'Unlocked Unit'], self.course_nav.sequence_items) self.assertEqual([u'Unlocked Unit'], self.courseware_page.nav.sequence_items)
self.course_nav.go_to_section("Test Section", "Unlocked Subsection") self.courseware_page.nav.go_to_section("Test Section", "Unlocked Subsection")
self.assertEqual([u'Test Unit'], self.course_nav.sequence_items) self.assertEqual([u'Test Unit'], self.courseware_page.nav.sequence_items)
@attr(shard=1) @attr(shard=1)
...@@ -1065,7 +1134,7 @@ class ProblemExecutionTest(UniqueCourseTest): ...@@ -1065,7 +1134,7 @@ class ProblemExecutionTest(UniqueCourseTest):
super(ProblemExecutionTest, self).setUp() super(ProblemExecutionTest, self).setUp()
self.course_info_page = CourseInfoPage(self.browser, self.course_id) self.course_info_page = CourseInfoPage(self.browser, self.course_id)
self.course_nav = CourseNavPage(self.browser) self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.tab_nav = TabNavPage(self.browser) self.tab_nav = TabNavPage(self.browser)
# Install a course with sections and problems. # Install a course with sections and problems.
...@@ -1112,7 +1181,7 @@ class ProblemExecutionTest(UniqueCourseTest): ...@@ -1112,7 +1181,7 @@ class ProblemExecutionTest(UniqueCourseTest):
# Navigate to the problem page # Navigate to the problem page
self.course_info_page.visit() self.course_info_page.visit()
self.tab_nav.go_to_tab('Course') self.tab_nav.go_to_tab('Course')
self.course_nav.go_to_section('Test Section', 'Test Subsection') self.courseware_page.nav.go_to_section('Test Section', 'Test Subsection')
problem_page = ProblemPage(self.browser) problem_page = ProblemPage(self.browser)
self.assertEqual(problem_page.problem_name.upper(), 'PYTHON PROBLEM') self.assertEqual(problem_page.problem_name.upper(), 'PYTHON PROBLEM')
...@@ -1391,6 +1460,6 @@ class CourseInfoA11yTest(UniqueCourseTest): ...@@ -1391,6 +1460,6 @@ class CourseInfoA11yTest(UniqueCourseTest):
self.course_info_page = CourseInfoPage(self.browser, self.course_id) self.course_info_page = CourseInfoPage(self.browser, self.course_id)
AutoAuthPage(self.browser, course_id=self.course_id).visit() AutoAuthPage(self.browser, course_id=self.course_id).visit()
def test_course_home_a11y(self): def test_course_info_a11y(self):
self.course_info_page.visit() self.course_info_page.visit()
self.course_info_page.a11y_audit.check_for_accessibility_errors() self.course_info_page.a11y_audit.check_for_accessibility_errors()
...@@ -7,7 +7,7 @@ import uuid ...@@ -7,7 +7,7 @@ import uuid
from common.test.acceptance.tests.helpers import remove_file from common.test.acceptance.tests.helpers import remove_file
from common.test.acceptance.pages.common.logout import LogoutPage 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.courseware_search import CoursewareSearchPage
from common.test.acceptance.pages.lms.staff_view import StaffPage from common.test.acceptance.pages.lms.staff_view import StaffPage
from common.test.acceptance.fixtures.course import XBlockFixtureDesc from common.test.acceptance.fixtures.course import XBlockFixtureDesc
...@@ -45,7 +45,7 @@ class CoursewareSearchCohortTest(ContainerBase): ...@@ -45,7 +45,7 @@ class CoursewareSearchCohortTest(ContainerBase):
super(CoursewareSearchCohortTest, self).setUp(is_staff=is_staff) super(CoursewareSearchCohortTest, self).setUp(is_staff=is_staff)
self.staff_user = self.user self.staff_user = self.user
self.course_outline = CourseOutlinePage( self.studio_course_outline = StudioCourseOutlinePage(
self.browser, self.browser,
self.course_info['org'], self.course_info['org'],
self.course_info['number'], self.course_info['number'],
...@@ -101,9 +101,9 @@ class CoursewareSearchCohortTest(ContainerBase): ...@@ -101,9 +101,9 @@ class CoursewareSearchCohortTest(ContainerBase):
Reindex course content on studio course page Reindex course content on studio course page
""" """
self._auto_auth(self.staff_user["username"], self.staff_user["email"], True) self._auto_auth(self.staff_user["username"], self.staff_user["email"], True)
self.course_outline.visit() self.studio_course_outline.visit()
self.course_outline.start_reindex() self.studio_course_outline.start_reindex()
self.course_outline.wait_for_ajax() self.studio_course_outline.wait_for_ajax()
def _goto_staff_page(self): def _goto_staff_page(self):
""" """
......
...@@ -5,6 +5,7 @@ End-to-end tests for the LMS. ...@@ -5,6 +5,7 @@ End-to-end tests for the LMS.
import json import json
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest import skip
import ddt import ddt
from flaky import flaky from flaky import flaky
...@@ -13,7 +14,7 @@ from nose.plugins.attrib import attr ...@@ -13,7 +14,7 @@ from nose.plugins.attrib import attr
from ..helpers import UniqueCourseTest, EventsTestMixin, auto_auth, create_multiple_choice_problem from ..helpers import UniqueCourseTest, EventsTestMixin, auto_auth, create_multiple_choice_problem
from ...fixtures.course import CourseFixture, XBlockFixtureDesc from ...fixtures.course import CourseFixture, XBlockFixtureDesc
from ...pages.common.logout import LogoutPage from ...pages.common.logout import LogoutPage
from ...pages.lms.course_nav import CourseNavPage from ...pages.lms.course_home import CourseHomePage
from ...pages.lms.courseware import CoursewarePage, CoursewareSequentialTabPage from ...pages.lms.courseware import CoursewarePage, CoursewareSequentialTabPage
from ...pages.lms.create_mode import ModeCreationPage from ...pages.lms.create_mode import ModeCreationPage
from ...pages.lms.dashboard import DashboardPage from ...pages.lms.dashboard import DashboardPage
...@@ -23,7 +24,7 @@ from ...pages.lms.progress import ProgressPage ...@@ -23,7 +24,7 @@ from ...pages.lms.progress import ProgressPage
from ...pages.lms.staff_view import StaffPage from ...pages.lms.staff_view import StaffPage
from ...pages.lms.track_selection import TrackSelectionPage from ...pages.lms.track_selection import TrackSelectionPage
from ...pages.studio.auto_auth import AutoAuthPage from ...pages.studio.auto_auth import AutoAuthPage
from ...pages.studio.overview import CourseOutlinePage from ...pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
@attr(shard=9) @attr(shard=9)
...@@ -38,9 +39,9 @@ class CoursewareTest(UniqueCourseTest): ...@@ -38,9 +39,9 @@ class CoursewareTest(UniqueCourseTest):
super(CoursewareTest, self).setUp() super(CoursewareTest, self).setUp()
self.courseware_page = CoursewarePage(self.browser, self.course_id) 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.course_outline = CourseOutlinePage( self.studio_course_outline = StudioCourseOutlinePage(
self.browser, self.browser,
self.course_info['org'], self.course_info['org'],
self.course_info['number'], self.course_info['number'],
...@@ -94,10 +95,10 @@ class CoursewareTest(UniqueCourseTest): ...@@ -94,10 +95,10 @@ class CoursewareTest(UniqueCourseTest):
auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id) auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id)
# Visit course outline page in studio. # Visit course outline page in studio.
self.course_outline.visit() self.studio_course_outline.visit()
# Set release date for subsection in future. # Set release date for subsection in future.
self.course_outline.change_problem_release_date() self.studio_course_outline.change_problem_release_date()
# Logout and login as a student. # Logout and login as a student.
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
...@@ -116,11 +117,10 @@ class CoursewareTest(UniqueCourseTest): ...@@ -116,11 +117,10 @@ class CoursewareTest(UniqueCourseTest):
And I visit my courseware page And I visit my courseware page
Then I should see correct course tree breadcrumb Then I should see correct course tree breadcrumb
""" """
self.courseware_page.visit()
xblocks = self.course_fix.get_nested_xblocks(category="problem") xblocks = self.course_fix.get_nested_xblocks(category="problem")
for index in range(1, len(xblocks) + 1): for index in range(1, len(xblocks) + 1):
self.course_nav.go_to_section('Test Section {}'.format(index), 'Test Subsection {}'.format(index)) self.course_home_page.visit()
self.course_home_page.outline.go_to_section('Test Section {}'.format(index), 'Test Subsection {}'.format(index))
courseware_page_breadcrumb = self.courseware_page.breadcrumb courseware_page_breadcrumb = self.courseware_page.breadcrumb
expected_breadcrumb = self._create_breadcrumb(index) # pylint: disable=no-member expected_breadcrumb = self._create_breadcrumb(index) # pylint: disable=no-member
self.assertEqual(courseware_page_breadcrumb, expected_breadcrumb) self.assertEqual(courseware_page_breadcrumb, expected_breadcrumb)
...@@ -140,7 +140,7 @@ class ProctoredExamTest(UniqueCourseTest): ...@@ -140,7 +140,7 @@ class ProctoredExamTest(UniqueCourseTest):
self.courseware_page = CoursewarePage(self.browser, self.course_id) self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.course_outline = CourseOutlinePage( self.studio_course_outline = StudioCourseOutlinePage(
self.browser, self.browser,
self.course_info['org'], self.course_info['org'],
self.course_info['number'], self.course_info['number'],
...@@ -234,10 +234,10 @@ class ProctoredExamTest(UniqueCourseTest): ...@@ -234,10 +234,10 @@ class ProctoredExamTest(UniqueCourseTest):
""" """
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id) auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id)
self.course_outline.visit() self.studio_course_outline.visit()
self.course_outline.open_subsection_settings_dialog() self.studio_course_outline.open_subsection_settings_dialog()
self.assertTrue(self.course_outline.proctoring_items_are_displayed()) self.assertTrue(self.studio_course_outline.proctoring_items_are_displayed())
def test_proctored_exam_flow(self): def test_proctored_exam_flow(self):
""" """
...@@ -251,11 +251,11 @@ class ProctoredExamTest(UniqueCourseTest): ...@@ -251,11 +251,11 @@ class ProctoredExamTest(UniqueCourseTest):
""" """
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id) auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id)
self.course_outline.visit() self.studio_course_outline.visit()
self.course_outline.open_subsection_settings_dialog() self.studio_course_outline.open_subsection_settings_dialog()
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()
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
self._login_as_a_verified_user() self._login_as_a_verified_user()
...@@ -272,11 +272,11 @@ class ProctoredExamTest(UniqueCourseTest): ...@@ -272,11 +272,11 @@ class ProctoredExamTest(UniqueCourseTest):
""" """
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id) auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id)
self.course_outline.visit() self.studio_course_outline.visit()
self.course_outline.open_subsection_settings_dialog() self.studio_course_outline.open_subsection_settings_dialog()
self.course_outline.select_advanced_tab() self.studio_course_outline.select_advanced_tab()
self.course_outline.make_exam_timed(hide_after_due=hide_after_due) self.studio_course_outline.make_exam_timed(hide_after_due=hide_after_due)
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
self._login_as_a_verified_user() self._login_as_a_verified_user()
...@@ -312,9 +312,9 @@ class ProctoredExamTest(UniqueCourseTest): ...@@ -312,9 +312,9 @@ class ProctoredExamTest(UniqueCourseTest):
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id) auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id)
self.course_outline.visit() self.studio_course_outline.visit()
last_week = (datetime.today() - timedelta(days=7)).strftime("%m/%d/%Y") last_week = (datetime.today() - timedelta(days=7)).strftime("%m/%d/%Y")
self.course_outline.change_problem_due_date(last_week) self.studio_course_outline.change_problem_due_date(last_week)
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
auto_auth(self.browser, self.USERNAME, self.EMAIL, False, self.course_id) auto_auth(self.browser, self.USERNAME, self.EMAIL, False, self.course_id)
...@@ -355,26 +355,26 @@ class ProctoredExamTest(UniqueCourseTest): ...@@ -355,26 +355,26 @@ class ProctoredExamTest(UniqueCourseTest):
""" """
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id) auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id)
self.course_outline.visit() self.studio_course_outline.visit()
self.course_outline.open_subsection_settings_dialog() self.studio_course_outline.open_subsection_settings_dialog()
self.course_outline.select_advanced_tab() self.studio_course_outline.select_advanced_tab()
self.course_outline.select_none_exam() self.studio_course_outline.select_none_exam()
self.assertFalse(self.course_outline.time_allotted_field_visible()) self.assertFalse(self.studio_course_outline.time_allotted_field_visible())
self.assertFalse(self.course_outline.exam_review_rules_field_visible()) self.assertFalse(self.studio_course_outline.exam_review_rules_field_visible())
self.course_outline.select_timed_exam() self.studio_course_outline.select_timed_exam()
self.assertTrue(self.course_outline.time_allotted_field_visible()) self.assertTrue(self.studio_course_outline.time_allotted_field_visible())
self.assertFalse(self.course_outline.exam_review_rules_field_visible()) self.assertFalse(self.studio_course_outline.exam_review_rules_field_visible())
self.course_outline.select_proctored_exam() self.studio_course_outline.select_proctored_exam()
self.assertTrue(self.course_outline.time_allotted_field_visible()) self.assertTrue(self.studio_course_outline.time_allotted_field_visible())
self.assertTrue(self.course_outline.exam_review_rules_field_visible()) self.assertTrue(self.studio_course_outline.exam_review_rules_field_visible())
self.course_outline.select_practice_exam() self.studio_course_outline.select_practice_exam()
self.assertTrue(self.course_outline.time_allotted_field_visible()) self.assertTrue(self.studio_course_outline.time_allotted_field_visible())
self.assertFalse(self.course_outline.exam_review_rules_field_visible()) self.assertFalse(self.studio_course_outline.exam_review_rules_field_visible())
@attr(shard=9) @attr(shard=9)
...@@ -389,8 +389,9 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin): ...@@ -389,8 +389,9 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin):
super(CoursewareMultipleVerticalsTest, self).setUp() super(CoursewareMultipleVerticalsTest, self).setUp()
self.courseware_page = CoursewarePage(self.browser, self.course_id) self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.course_home_page = CourseHomePage(self.browser, self.course_id)
self.course_outline = CourseOutlinePage( self.studio_course_outline = StudioCourseOutlinePage(
self.browser, self.browser,
self.course_info['org'], self.course_info['org'],
self.course_info['number'], self.course_info['number'],
...@@ -433,10 +434,11 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin): ...@@ -433,10 +434,11 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin):
# Auto-auth register for the course. # Auto-auth register for the course.
AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL,
course_id=self.course_id, staff=False).visit() course_id=self.course_id, staff=False).visit()
self.courseware_page.visit()
self.course_nav = CourseNavPage(self.browser)
@skip('Disable temporarily to get course bookmarks out')
def test_navigation_buttons(self): def test_navigation_buttons(self):
self.courseware_page.visit()
# start in first section # start in first section
self.assert_navigation_state('Test Section 1', 'Test Subsection 1,1', 0, next_enabled=True, prev_enabled=False) self.assert_navigation_state('Test Section 1', 'Test Subsection 1,1', 0, next_enabled=True, prev_enabled=False)
...@@ -549,10 +551,13 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin): ...@@ -549,10 +551,13 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin):
sequence_ui_events sequence_ui_events
) )
# TODO: TNL-6546: Delete this whole test if these events are going away(?)
def test_outline_selected_events(self): def test_outline_selected_events(self):
self.course_nav.go_to_section('Test Section 1', 'Test Subsection 1,2') self.courseware_page.visit()
self.courseware_page.nav.go_to_section('Test Section 1', 'Test Subsection 1,2')
self.course_nav.go_to_section('Test Section 2', 'Test Subsection 2,1') self.courseware_page.nav.go_to_section('Test Section 2', 'Test Subsection 2,1')
# test UI events emitted by navigating via the course outline # test UI events emitted by navigating via the course outline
filter_selected_events = lambda event: event.get('name', '') == 'edx.ui.lms.outline.selected' filter_selected_events = lambda event: event.get('name', '') == 'edx.ui.lms.outline.selected'
...@@ -588,8 +593,10 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin): ...@@ -588,8 +593,10 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin):
When I navigate via the left-hand nav When I navigate via the left-hand nav
Then a link clicked event is logged Then a link clicked event is logged
""" """
self.course_nav.go_to_section('Test Section 1', 'Test Subsection 1,2') self.courseware_page.visit()
self.course_nav.go_to_section('Test Section 2', 'Test Subsection 2,1')
self.courseware_page.nav.go_to_section('Test Section 1', 'Test Subsection 1,2')
self.courseware_page.nav.go_to_section('Test Section 2', 'Test Subsection 2,1')
filter_link_clicked = lambda event: event.get('name', '') == 'edx.ui.lms.link_clicked' filter_link_clicked = lambda event: event.get('name', '') == 'edx.ui.lms.link_clicked'
link_clicked_events = self.wait_for_events(event_filter=filter_link_clicked, timeout=2) link_clicked_events = self.wait_for_events(event_filter=filter_link_clicked, timeout=2)
...@@ -601,15 +608,17 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin): ...@@ -601,15 +608,17 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin):
""" """
Verifies that the navigation state is as expected. Verifies that the navigation state is as expected.
""" """
self.assertTrue(self.course_nav.is_on_section(section_title, subsection_title)) self.assertTrue(self.courseware_page.nav.is_on_section(section_title, subsection_title))
self.assertEquals(self.courseware_page.sequential_position, subsection_position) self.assertEquals(self.courseware_page.sequential_position, subsection_position)
self.assertEquals(self.courseware_page.is_next_button_enabled, next_enabled) self.assertEquals(self.courseware_page.is_next_button_enabled, next_enabled)
self.assertEquals(self.courseware_page.is_previous_button_enabled, prev_enabled) self.assertEquals(self.courseware_page.is_previous_button_enabled, prev_enabled)
def test_tab_position(self): def test_tab_position(self):
# test that using the position in the url direct to correct tab in courseware # test that using the position in the url direct to correct tab in courseware
self.course_nav.go_to_section('Test Section 1', 'Test Subsection 1,1') self.course_home_page.visit()
subsection_url = self.course_nav.active_subsection_url
self.course_home_page.outline.go_to_section('Test Section 1', 'Test Subsection 1,1')
subsection_url = self.courseware_page.nav.active_subsection_url
url_part_list = subsection_url.split('/') url_part_list = subsection_url.split('/')
self.assertEqual(len(url_part_list), 9) self.assertEqual(len(url_part_list), 9)
...@@ -657,7 +666,8 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin): ...@@ -657,7 +666,8 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin):
""" """
Run accessibility audit for the problem type. Run accessibility audit for the problem type.
""" """
self.course_nav.go_to_section('Test Section 1', 'Test Subsection 1,1') self.course_home_page.visit()
self.course_home_page.outline.go_to_section('Test Section 1', 'Test Subsection 1,1')
# Set the scope to the sequence navigation # Set the scope to the sequence navigation
self.courseware_page.a11y_audit.config.set_scope( self.courseware_page.a11y_audit.config.set_scope(
include=['div.sequence-nav']) include=['div.sequence-nav'])
...@@ -840,7 +850,7 @@ class SubsectionHiddenAfterDueDateTest(UniqueCourseTest): ...@@ -840,7 +850,7 @@ class SubsectionHiddenAfterDueDateTest(UniqueCourseTest):
self.courseware_page = CoursewarePage(self.browser, self.course_id) self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.logout_page = LogoutPage(self.browser) self.logout_page = LogoutPage(self.browser)
self.course_outline = CourseOutlinePage( self.studio_course_outline = StudioCourseOutlinePage(
self.browser, self.browser,
self.course_info['org'], self.course_info['org'],
self.course_info['number'], self.course_info['number'],
...@@ -876,11 +886,11 @@ class SubsectionHiddenAfterDueDateTest(UniqueCourseTest): ...@@ -876,11 +886,11 @@ class SubsectionHiddenAfterDueDateTest(UniqueCourseTest):
""" """
self.logout_page.visit() self.logout_page.visit()
auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id) auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id)
self.course_outline.visit() self.studio_course_outline.visit()
self.course_outline.open_subsection_settings_dialog() self.studio_course_outline.open_subsection_settings_dialog()
self.course_outline.select_advanced_tab('hide_after_due_date') self.studio_course_outline.select_advanced_tab('hide_after_due_date')
self.course_outline.make_subsection_hidden_after_due_date() self.studio_course_outline.make_subsection_hidden_after_due_date()
self.logout_page.visit() self.logout_page.visit()
auto_auth(self.browser, self.USERNAME, self.EMAIL, False, self.course_id) auto_auth(self.browser, self.USERNAME, self.EMAIL, False, self.course_id)
...@@ -916,9 +926,9 @@ class SubsectionHiddenAfterDueDateTest(UniqueCourseTest): ...@@ -916,9 +926,9 @@ class SubsectionHiddenAfterDueDateTest(UniqueCourseTest):
self.logout_page.visit() self.logout_page.visit()
auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id) auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id)
self.course_outline.visit() self.studio_course_outline.visit()
last_week = (datetime.today() - timedelta(days=7)).strftime("%m/%d/%Y") last_week = (datetime.today() - timedelta(days=7)).strftime("%m/%d/%Y")
self.course_outline.change_problem_due_date(last_week) self.studio_course_outline.change_problem_due_date(last_week)
self.logout_page.visit() self.logout_page.visit()
auto_auth(self.browser, self.USERNAME, self.EMAIL, False, self.course_id) auto_auth(self.browser, self.USERNAME, self.EMAIL, False, self.course_id)
......
...@@ -11,7 +11,7 @@ from common.test.acceptance.pages.common.logout import LogoutPage ...@@ -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.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.utils import add_html_component, type_in_codemirror
from common.test.acceptance.pages.studio.auto_auth import AutoAuthPage 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.studio.container import ContainerPage
from common.test.acceptance.pages.lms.courseware_search import CoursewareSearchPage from common.test.acceptance.pages.lms.courseware_search import CoursewareSearchPage
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
...@@ -54,7 +54,7 @@ class CoursewareSearchTest(UniqueCourseTest): ...@@ -54,7 +54,7 @@ class CoursewareSearchTest(UniqueCourseTest):
super(CoursewareSearchTest, self).setUp() super(CoursewareSearchTest, self).setUp()
self.courseware_search_page = CoursewareSearchPage(self.browser, self.course_id) self.courseware_search_page = CoursewareSearchPage(self.browser, self.course_id)
self.course_outline = CourseOutlinePage( self.studio_course_outline = StudioCourseOutlinePage(
self.browser, self.browser,
self.course_info['org'], self.course_info['org'],
self.course_info['number'], self.course_info['number'],
...@@ -91,8 +91,8 @@ class CoursewareSearchTest(UniqueCourseTest): ...@@ -91,8 +91,8 @@ class CoursewareSearchTest(UniqueCourseTest):
Publish content on studio course page under specified section Publish content on studio course page under specified section
""" """
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True) self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
self.course_outline.visit() self.studio_course_outline.visit()
subsection = self.course_outline.section_at(section_index).subsection_at(0) subsection = self.studio_course_outline.section_at(section_index).subsection_at(0)
subsection.expand_subsection() subsection.expand_subsection()
unit = subsection.unit_at(0) unit = subsection.unit_at(0)
unit.publish() unit.publish()
...@@ -102,8 +102,8 @@ class CoursewareSearchTest(UniqueCourseTest): ...@@ -102,8 +102,8 @@ class CoursewareSearchTest(UniqueCourseTest):
Edit chapter name on studio course page under specified section Edit chapter name on studio course page under specified section
""" """
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True) self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
self.course_outline.visit() self.studio_course_outline.visit()
section = self.course_outline.section_at(section_index) section = self.studio_course_outline.section_at(section_index)
section.change_name(self.EDITED_CHAPTER_NAME) section.change_name(self.EDITED_CHAPTER_NAME)
def _studio_add_content(self, section_index): def _studio_add_content(self, section_index):
...@@ -113,8 +113,8 @@ class CoursewareSearchTest(UniqueCourseTest): ...@@ -113,8 +113,8 @@ class CoursewareSearchTest(UniqueCourseTest):
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True) self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
# create a unit in course outline # create a unit in course outline
self.course_outline.visit() self.studio_course_outline.visit()
subsection = self.course_outline.section_at(section_index).subsection_at(0) subsection = self.studio_course_outline.section_at(section_index).subsection_at(0)
subsection.expand_subsection() subsection.expand_subsection()
subsection.add_unit() subsection.add_unit()
...@@ -134,9 +134,9 @@ class CoursewareSearchTest(UniqueCourseTest): ...@@ -134,9 +134,9 @@ class CoursewareSearchTest(UniqueCourseTest):
""" """
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True) self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
self.course_outline.visit() self.studio_course_outline.visit()
self.course_outline.start_reindex() self.studio_course_outline.start_reindex()
self.course_outline.wait_for_ajax() self.studio_course_outline.wait_for_ajax()
def _search_for_content(self, search_term): def _search_for_content(self, search_term):
""" """
......
...@@ -9,7 +9,7 @@ from common.test.acceptance.pages.common.logout import LogoutPage ...@@ -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.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.utils import add_html_component, type_in_codemirror
from common.test.acceptance.pages.studio.auto_auth import AutoAuthPage 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.studio.container import ContainerPage
from common.test.acceptance.pages.lms.dashboard_search import DashboardSearchPage from common.test.acceptance.pages.lms.dashboard_search import DashboardSearchPage
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
...@@ -60,10 +60,10 @@ class DashboardSearchTest(AcceptanceTest): ...@@ -60,10 +60,10 @@ class DashboardSearchTest(AcceptanceTest):
} }
# generate course fixtures and outline pages # generate course fixtures and outline pages
self.course_outlines = {} self.studio_course_outlines = {}
self.course_fixtures = {} self.course_fixtures = {}
for key, course_info in self.courses.iteritems(): for key, course_info in self.courses.iteritems():
course_outline = CourseOutlinePage( studio_course_outline = StudioCourseOutlinePage(
self.browser, self.browser,
course_info['org'], course_info['org'],
course_info['number'], course_info['number'],
...@@ -89,7 +89,7 @@ class DashboardSearchTest(AcceptanceTest): ...@@ -89,7 +89,7 @@ class DashboardSearchTest(AcceptanceTest):
) )
).install() ).install()
self.course_outlines[key] = course_outline self.studio_course_outlines[key] = studio_course_outline
self.course_fixtures[key] = course_fix self.course_fixtures[key] = course_fix
def tearDown(self): def tearDown(self):
...@@ -106,13 +106,13 @@ class DashboardSearchTest(AcceptanceTest): ...@@ -106,13 +106,13 @@ class DashboardSearchTest(AcceptanceTest):
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
AutoAuthPage(self.browser, username=username, email=email, staff=staff).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. Add content to first section on studio course page.
""" """
# create a unit in course outline # create a unit in course outline
course_outline.visit() studio_course_outline.visit()
subsection = course_outline.section_at(0).subsection_at(0) subsection = studio_course_outline.section_at(0).subsection_at(0)
subsection.expand_subsection() subsection.expand_subsection()
subsection.add_unit() subsection.add_unit()
...@@ -126,12 +126,12 @@ class DashboardSearchTest(AcceptanceTest): ...@@ -126,12 +126,12 @@ class DashboardSearchTest(AcceptanceTest):
type_in_codemirror(unit_page, 0, html_content) type_in_codemirror(unit_page, 0, html_content)
click_css(unit_page, '.action-save', 0) 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. Publish content in first section on studio course page.
""" """
course_outline.visit() studio_course_outline.visit()
subsection = course_outline.section_at(0).subsection_at(0) subsection = studio_course_outline.section_at(0).subsection_at(0)
subsection.expand_subsection() subsection.expand_subsection()
unit = subsection.unit_at(0) unit = subsection.unit_at(0)
unit.publish() unit.publish()
...@@ -167,9 +167,9 @@ class DashboardSearchTest(AcceptanceTest): ...@@ -167,9 +167,9 @@ class DashboardSearchTest(AcceptanceTest):
# Create content in studio without publishing. # Create content in studio without publishing.
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True) 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.studio_course_outlines['A'], html_content)
self._studio_add_content(self.course_outlines['B'], html_content) self._studio_add_content(self.studio_course_outlines['B'], html_content)
self._studio_add_content(self.course_outlines['C'], html_content) self._studio_add_content(self.studio_course_outlines['C'], html_content)
# Do a search, there should be no results shown. # Do a search, there should be no results shown.
self._auto_auth(self.USERNAME, self.EMAIL, False) self._auto_auth(self.USERNAME, self.EMAIL, False)
...@@ -179,9 +179,9 @@ class DashboardSearchTest(AcceptanceTest): ...@@ -179,9 +179,9 @@ class DashboardSearchTest(AcceptanceTest):
# Publish in studio to trigger indexing. # Publish in studio to trigger indexing.
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True) self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
self._studio_publish_content(self.course_outlines['A']) self._studio_publish_content(self.studio_course_outlines['A'])
self._studio_publish_content(self.course_outlines['B']) self._studio_publish_content(self.studio_course_outlines['B'])
self._studio_publish_content(self.course_outlines['C']) 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 # Do the search again, this time we expect results from courses A & B, but not C
self._auto_auth(self.USERNAME, self.EMAIL, False) self._auto_auth(self.USERNAME, self.EMAIL, False)
......
...@@ -9,7 +9,7 @@ from nose.plugins.attrib import attr ...@@ -9,7 +9,7 @@ from nose.plugins.attrib import attr
from common.test.acceptance.tests.helpers import UniqueCourseTest, EventsTestMixin from common.test.acceptance.tests.helpers import UniqueCourseTest, EventsTestMixin
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc 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.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.courseware import CoursewarePage
from common.test.acceptance.pages.lms.edxnotes import EdxNotesUnitPage, EdxNotesPage, EdxNotesPageNoContent from common.test.acceptance.pages.lms.edxnotes import EdxNotesUnitPage, EdxNotesPage, EdxNotesPageNoContent
from common.test.acceptance.fixtures.edxnotes import EdxNotesFixture, Note, Range from common.test.acceptance.fixtures.edxnotes import EdxNotesFixture, Note, Range
...@@ -26,7 +26,7 @@ class EdxNotesTestMixin(UniqueCourseTest): ...@@ -26,7 +26,7 @@ class EdxNotesTestMixin(UniqueCourseTest):
""" """
super(EdxNotesTestMixin, self).setUp() super(EdxNotesTestMixin, self).setUp()
self.courseware_page = CoursewarePage(self.browser, self.course_id) 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.note_unit_page = EdxNotesUnitPage(self.browser, self.course_id)
self.notes_page = EdxNotesPage(self.browser, self.course_id) self.notes_page = EdxNotesPage(self.browser, self.course_id)
...@@ -1504,7 +1504,8 @@ class EdxNotesToggleNotesTest(EdxNotesTestMixin): ...@@ -1504,7 +1504,8 @@ class EdxNotesToggleNotesTest(EdxNotesTestMixin):
self.assertEqual(len(self.note_unit_page.notes), 0) self.assertEqual(len(self.note_unit_page.notes), 0)
self.courseware_page.go_to_sequential_position(2) self.courseware_page.go_to_sequential_position(2)
self.assertEqual(len(self.note_unit_page.notes), 0) 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) self.assertEqual(len(self.note_unit_page.notes), 0)
def test_can_reenable_all_notes(self): def test_can_reenable_all_notes(self):
...@@ -1530,5 +1531,6 @@ class EdxNotesToggleNotesTest(EdxNotesTestMixin): ...@@ -1530,5 +1531,6 @@ class EdxNotesToggleNotesTest(EdxNotesTestMixin):
self.assertGreater(len(self.note_unit_page.notes), 0) self.assertGreater(len(self.note_unit_page.notes), 0)
self.courseware_page.go_to_sequential_position(2) self.courseware_page.go_to_sequential_position(2)
self.assertGreater(len(self.note_unit_page.notes), 0) 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) self.assertGreater(len(self.note_unit_page.notes), 0)
...@@ -43,7 +43,7 @@ class EntranceExamTest(UniqueCourseTest): ...@@ -43,7 +43,7 @@ class EntranceExamTest(UniqueCourseTest):
).install() ).install()
entrance_exam_subsection = None entrance_exam_subsection = None
outline = course_fixture.course_outline outline = course_fixture.studio_course_outline_as_json
for child in outline['child_info']['children']: for child in outline['child_info']['children']:
if child.get('display_name') == "Entrance Exam": if child.get('display_name') == "Entrance Exam":
entrance_exam_subsection = child['child_info']['children'][0] entrance_exam_subsection = child['child_info']['children'][0]
......
...@@ -6,7 +6,7 @@ from textwrap import dedent ...@@ -6,7 +6,7 @@ from textwrap import dedent
from common.test.acceptance.tests.helpers import UniqueCourseTest from common.test.acceptance.tests.helpers import UniqueCourseTest
from common.test.acceptance.pages.studio.auto_auth import AutoAuthPage 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.courseware import CoursewarePage
from common.test.acceptance.pages.lms.problem import ProblemPage from common.test.acceptance.pages.lms.problem import ProblemPage
from common.test.acceptance.pages.lms.staff_view import StaffPage from common.test.acceptance.pages.lms.staff_view import StaffPage
...@@ -29,7 +29,7 @@ class GatingTest(UniqueCourseTest): ...@@ -29,7 +29,7 @@ class GatingTest(UniqueCourseTest):
self.logout_page = LogoutPage(self.browser) self.logout_page = LogoutPage(self.browser)
self.courseware_page = CoursewarePage(self.browser, self.course_id) self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.course_outline = CourseOutlinePage( self.studio_course_outline = StudioCourseOutlinePage(
self.browser, self.browser,
self.course_info['org'], self.course_info['org'],
self.course_info['number'], self.course_info['number'],
...@@ -89,10 +89,10 @@ class GatingTest(UniqueCourseTest): ...@@ -89,10 +89,10 @@ class GatingTest(UniqueCourseTest):
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True) self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
# Make the first subsection a prerequisite # Make the first subsection a prerequisite
self.course_outline.visit() self.studio_course_outline.visit()
self.course_outline.open_subsection_settings_dialog(0) self.studio_course_outline.open_subsection_settings_dialog(0)
self.course_outline.select_advanced_tab(desired_item='gated_content') self.studio_course_outline.select_advanced_tab(desired_item='gated_content')
self.course_outline.make_gating_prerequisite() self.studio_course_outline.make_gating_prerequisite()
def _setup_gated_subsection(self): def _setup_gated_subsection(self):
""" """
...@@ -102,10 +102,10 @@ class GatingTest(UniqueCourseTest): ...@@ -102,10 +102,10 @@ class GatingTest(UniqueCourseTest):
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True) self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
# Gate the second subsection based on the score achieved in the first subsection # Gate the second subsection based on the score achieved in the first subsection
self.course_outline.visit() self.studio_course_outline.visit()
self.course_outline.open_subsection_settings_dialog(1) self.studio_course_outline.open_subsection_settings_dialog(1)
self.course_outline.select_advanced_tab(desired_item='gated_content') self.studio_course_outline.select_advanced_tab(desired_item='gated_content')
self.course_outline.add_prerequisite_to_subsection("80") self.studio_course_outline.add_prerequisite_to_subsection("80")
def _fulfill_prerequisite(self): def _fulfill_prerequisite(self):
""" """
...@@ -127,23 +127,23 @@ class GatingTest(UniqueCourseTest): ...@@ -127,23 +127,23 @@ class GatingTest(UniqueCourseTest):
self._setup_prereq() self._setup_prereq()
# Assert settings are displayed correctly for a prerequisite subsection # Assert settings are displayed correctly for a prerequisite subsection
self.course_outline.visit() self.studio_course_outline.visit()
self.course_outline.open_subsection_settings_dialog(0) self.studio_course_outline.open_subsection_settings_dialog(0)
self.course_outline.select_advanced_tab(desired_item='gated_content') self.studio_course_outline.select_advanced_tab(desired_item='gated_content')
self.assertTrue(self.course_outline.gating_prerequisite_checkbox_is_visible()) self.assertTrue(self.studio_course_outline.gating_prerequisite_checkbox_is_visible())
self.assertTrue(self.course_outline.gating_prerequisite_checkbox_is_checked()) self.assertTrue(self.studio_course_outline.gating_prerequisite_checkbox_is_checked())
self.assertFalse(self.course_outline.gating_prerequisites_dropdown_is_visible()) self.assertFalse(self.studio_course_outline.gating_prerequisites_dropdown_is_visible())
self.assertFalse(self.course_outline.gating_prerequisite_min_score_is_visible()) self.assertFalse(self.studio_course_outline.gating_prerequisite_min_score_is_visible())
self._setup_gated_subsection() self._setup_gated_subsection()
# Assert settings are displayed correctly for a gated subsection # Assert settings are displayed correctly for a gated subsection
self.course_outline.visit() self.studio_course_outline.visit()
self.course_outline.open_subsection_settings_dialog(1) self.studio_course_outline.open_subsection_settings_dialog(1)
self.course_outline.select_advanced_tab(desired_item='gated_content') self.studio_course_outline.select_advanced_tab(desired_item='gated_content')
self.assertTrue(self.course_outline.gating_prerequisite_checkbox_is_visible()) self.assertTrue(self.studio_course_outline.gating_prerequisite_checkbox_is_visible())
self.assertTrue(self.course_outline.gating_prerequisites_dropdown_is_visible()) self.assertTrue(self.studio_course_outline.gating_prerequisites_dropdown_is_visible())
self.assertTrue(self.course_outline.gating_prerequisite_min_score_is_visible()) self.assertTrue(self.studio_course_outline.gating_prerequisite_min_score_is_visible())
def test_gated_subsection_in_lms_for_student(self): def test_gated_subsection_in_lms_for_student(self):
""" """
......
...@@ -12,7 +12,7 @@ from flaky import flaky ...@@ -12,7 +12,7 @@ from flaky import flaky
from common.test.acceptance.tests.helpers import UniqueCourseTest, get_modal_alert, EventsTestMixin 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.common.logout import LogoutPage
from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage 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.create_mode import ModeCreationPage
from common.test.acceptance.pages.lms.courseware import CoursewarePage from common.test.acceptance.pages.lms.courseware import CoursewarePage
from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage, EntranceExamAdmin from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage, EntranceExamAdmin
...@@ -227,7 +227,7 @@ class ProctoredExamsTest(BaseInstructorDashboardTest): ...@@ -227,7 +227,7 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
self.courseware_page = CoursewarePage(self.browser, self.course_id) self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.course_outline = CourseOutlinePage( self.studio_course_outline = StudioCourseOutlinePage(
self.browser, self.browser,
self.course_info['org'], self.course_info['org'],
self.course_info['number'], self.course_info['number'],
...@@ -301,15 +301,15 @@ class ProctoredExamsTest(BaseInstructorDashboardTest): ...@@ -301,15 +301,15 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
# Visit the course outline page in studio # Visit the course outline page in studio
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
self._auto_auth("STAFF_TESTER", "staff101@example.com", True) 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. # 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 # 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. # login as a verified student and visit the courseware.
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
...@@ -327,15 +327,15 @@ class ProctoredExamsTest(BaseInstructorDashboardTest): ...@@ -327,15 +327,15 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
# Visit the course outline page in studio # Visit the course outline page in studio
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
self._auto_auth("STAFF_TESTER", "staff101@example.com", True) 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. # 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 # 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. # login as a verified student and visit the courseware.
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
......
...@@ -6,9 +6,8 @@ import json ...@@ -6,9 +6,8 @@ import json
from common.test.acceptance.tests.helpers import remove_file from common.test.acceptance.tests.helpers import remove_file
from common.test.acceptance.pages.common.logout import LogoutPage 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.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.fixtures.course import XBlockFixtureDesc
from common.test.acceptance.tests.helpers import create_user_partition_json from common.test.acceptance.tests.helpers import create_user_partition_json
...@@ -44,8 +43,7 @@ class SplitTestCoursewareSearchTest(ContainerBase): ...@@ -44,8 +43,7 @@ class SplitTestCoursewareSearchTest(ContainerBase):
self.staff_user = self.user self.staff_user = self.user
self.courseware_search_page = CoursewareSearchPage(self.browser, self.course_id) self.courseware_search_page = CoursewareSearchPage(self.browser, self.course_id)
self.course_navigation_page = CourseNavPage(self.browser) self.studio_course_outline = StudioCourseOutlinePage(
self.course_outline = CourseOutlinePage(
self.browser, self.browser,
self.course_info['org'], self.course_info['org'],
self.course_info['number'], self.course_info['number'],
...@@ -68,9 +66,9 @@ class SplitTestCoursewareSearchTest(ContainerBase): ...@@ -68,9 +66,9 @@ class SplitTestCoursewareSearchTest(ContainerBase):
Reindex course content on studio course page Reindex course content on studio course page
""" """
self._auto_auth(self.staff_user["username"], self.staff_user["email"], True) self._auto_auth(self.staff_user["username"], self.staff_user["email"], True)
self.course_outline.visit() self.studio_course_outline.visit()
self.course_outline.start_reindex() self.studio_course_outline.start_reindex()
self.course_outline.wait_for_ajax() self.studio_course_outline.wait_for_ajax()
def _create_group_configuration(self): def _create_group_configuration(self):
""" """
......
...@@ -20,7 +20,7 @@ from ...pages.lms.problem import ProblemPage ...@@ -20,7 +20,7 @@ from ...pages.lms.problem import ProblemPage
from ...pages.lms.progress import ProgressPage from ...pages.lms.progress import ProgressPage
from ...pages.studio.component_editor import ComponentEditorView from ...pages.studio.component_editor import ComponentEditorView
from ...pages.studio.utils import type_in_codemirror 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): class ProgressPageBaseTest(UniqueCourseTest):
...@@ -43,7 +43,7 @@ class ProgressPageBaseTest(UniqueCourseTest): ...@@ -43,7 +43,7 @@ class ProgressPageBaseTest(UniqueCourseTest):
self.progress_page = ProgressPage(self.browser, self.course_id) self.progress_page = ProgressPage(self.browser, self.course_id)
self.logout_page = LogoutPage(self.browser) self.logout_page = LogoutPage(self.browser)
self.course_outline = CourseOutlinePage( self.studio_course_outline = StudioCourseOutlinePage(
self.browser, self.browser,
self.course_info['org'], self.course_info['org'],
self.course_info['number'], self.course_info['number'],
...@@ -140,11 +140,11 @@ class PersistentGradesTest(ProgressPageBaseTest): ...@@ -140,11 +140,11 @@ class PersistentGradesTest(ProgressPageBaseTest):
Adds a unit to the subsection, which Adds a unit to the subsection, which
should not affect a persisted subsection grade. should not affect a persisted subsection grade.
""" """
self.course_outline.visit() self.studio_course_outline.visit()
subsection = self.course_outline.section(self.SECTION_NAME).subsection(self.SUBSECTION_NAME) subsection = self.studio_course_outline.section(self.SECTION_NAME).subsection(self.SUBSECTION_NAME)
subsection.expand_subsection() subsection.expand_subsection()
subsection.add_unit() subsection.add_unit()
self.course_outline.wait_for_ajax() self.studio_course_outline.wait_for_ajax()
subsection.publish() subsection.publish()
def _set_staff_lock_on_subsection(self, locked): def _set_staff_lock_on_subsection(self, locked):
...@@ -152,8 +152,8 @@ class PersistentGradesTest(ProgressPageBaseTest): ...@@ -152,8 +152,8 @@ class PersistentGradesTest(ProgressPageBaseTest):
Sets staff lock for a subsection, which should hide the Sets staff lock for a subsection, which should hide the
subsection score from students on the progress page. subsection score from students on the progress page.
""" """
self.course_outline.visit() self.studio_course_outline.visit()
subsection = self.course_outline.section_at(0).subsection_at(0) subsection = self.studio_course_outline.section_at(0).subsection_at(0)
subsection.set_staff_lock(locked) subsection.set_staff_lock(locked)
self.assertEqual(subsection.has_staff_lock_warning, locked) self.assertEqual(subsection.has_staff_lock_warning, locked)
...@@ -163,9 +163,9 @@ class PersistentGradesTest(ProgressPageBaseTest): ...@@ -163,9 +163,9 @@ class PersistentGradesTest(ProgressPageBaseTest):
along with its container unit, so any changes can along with its container unit, so any changes can
be published. be published.
""" """
self.course_outline.visit() self.studio_course_outline.visit()
self.course_outline.section_at(0).subsection_at(0).expand_subsection() self.studio_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() unit = self.studio_course_outline.section_at(0).subsection_at(0).unit(self.UNIT_NAME).go_to()
component = unit.xblocks[1] component = unit.xblocks[1]
return unit, component return unit, component
...@@ -289,8 +289,8 @@ class SubsectionGradingPolicyTest(ProgressPageBaseTest): ...@@ -289,8 +289,8 @@ class SubsectionGradingPolicyTest(ProgressPageBaseTest):
If a section index is not provided, 0 is assumed. If a section index is not provided, 0 is assumed.
""" """
with self._logged_in_session(staff=True): with self._logged_in_session(staff=True):
self.course_outline.visit() self.studio_course_outline.visit()
modal = self.course_outline.section_at(section).subsection_at(0).edit() modal = self.studio_course_outline.section_at(section).subsection_at(0).edit()
modal.policy = policy modal.policy = policy
modal.save() modal.save()
......
...@@ -12,8 +12,8 @@ from nose.plugins.attrib import attr ...@@ -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.settings_advanced import AdvancedSettingsPage
from common.test.acceptance.pages.studio.overview import CourseOutlinePage, ContainerPage, ExpandCollapseLinkState 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.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.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.pages.lms.staff_view import StaffPage
from common.test.acceptance.fixtures.config import ConfigModelFixture from common.test.acceptance.fixtures.config import ConfigModelFixture
from common.test.acceptance.fixtures.course import XBlockFixtureDesc from common.test.acceptance.fixtures.course import XBlockFixtureDesc
...@@ -1490,7 +1490,7 @@ class PublishSectionTest(CourseOutlineTest): ...@@ -1490,7 +1490,7 @@ class PublishSectionTest(CourseOutlineTest):
The first subsection has 2 units, and the second subsection has one unit. The first subsection has 2 units, and the second subsection has one unit.
""" """
self.courseware = CoursewarePage(self.browser, self.course_id) 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( course_fixture.add_children(
XBlockFixtureDesc('chapter', SECTION_NAME).add_children( XBlockFixtureDesc('chapter', SECTION_NAME).add_children(
XBlockFixtureDesc('sequential', SUBSECTION_NAME).add_children( XBlockFixtureDesc('sequential', SUBSECTION_NAME).add_children(
...@@ -1578,7 +1578,8 @@ class PublishSectionTest(CourseOutlineTest): ...@@ -1578,7 +1578,8 @@ class PublishSectionTest(CourseOutlineTest):
self.assertEqual(1, self.courseware.num_xblock_components) self.assertEqual(1, self.courseware.num_xblock_components)
self.courseware.go_to_sequential_position(2) self.courseware.go_to_sequential_position(2)
self.assertEqual(1, self.courseware.num_xblock_components) 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) self.assertEqual(1, self.courseware.num_xblock_components)
def _add_unpublished_content(self): def _add_unpublished_content(self):
......
...@@ -15,7 +15,6 @@ from common.test.acceptance.tests.helpers import UniqueCourseTest, is_youtube_av ...@@ -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.video.video import VideoPage
from common.test.acceptance.pages.lms.tab_nav import TabNavPage 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.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.auto_auth import AutoAuthPage
from common.test.acceptance.pages.lms.course_info import CourseInfoPage from common.test.acceptance.pages.lms.course_info import CourseInfoPage
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
...@@ -53,7 +52,6 @@ class VideoBaseTest(UniqueCourseTest): ...@@ -53,7 +52,6 @@ class VideoBaseTest(UniqueCourseTest):
self.video = VideoPage(self.browser) self.video = VideoPage(self.browser)
self.tab_nav = TabNavPage(self.browser) self.tab_nav = TabNavPage(self.browser)
self.course_nav = CourseNavPage(self.browser)
self.courseware = CoursewarePage(self.browser, self.course_id) self.courseware = CoursewarePage(self.browser, self.course_id)
self.course_info_page = CourseInfoPage(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) self.auth_page = AutoAuthPage(self.browser, course_id=self.course_id)
...@@ -531,7 +529,7 @@ class YouTubeVideoTest(VideoBaseTest): ...@@ -531,7 +529,7 @@ class YouTubeVideoTest(VideoBaseTest):
self.assertTrue(self.video.downloaded_transcript_contains_text(file_type, search_text)) self.assertTrue(self.video.downloaded_transcript_contains_text(file_type, search_text))
# open vertical containing video "C" # 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 # menu "download_transcript" doesn't exist
self.assertFalse(self.video.is_menu_present('download_transcript')) self.assertFalse(self.video.is_menu_present('download_transcript'))
...@@ -678,17 +676,17 @@ class YouTubeVideoTest(VideoBaseTest): ...@@ -678,17 +676,17 @@ class YouTubeVideoTest(VideoBaseTest):
self.navigate_to_video() self.navigate_to_video()
# select the "2.0" speed on video "A" # 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.wait_for_video_player_render()
self.video.speed = '2.0' self.video.speed = '2.0'
# select the "0.50" speed on video "B" # 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.wait_for_video_player_render()
self.video.speed = '0.50' self.video.speed = '0.50'
# open video "C" # 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() self.video.wait_for_video_player_render()
# Since the playback speed was set to .5 in "B", this video will also be impacted # Since the playback speed was set to .5 in "B", this video will also be impacted
...@@ -697,7 +695,7 @@ class YouTubeVideoTest(VideoBaseTest): ...@@ -697,7 +695,7 @@ class YouTubeVideoTest(VideoBaseTest):
self.video.verify_speed_changed('0.75x') self.video.verify_speed_changed('0.75x')
# go to the vertical containing video "A" # 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. # Video "A" should still play at speed 2.0 because it was explicitly set to that.
self.assertEqual(self.video.speed, '2.0x') self.assertEqual(self.video.speed, '2.0x')
...@@ -706,7 +704,7 @@ class YouTubeVideoTest(VideoBaseTest): ...@@ -706,7 +704,7 @@ class YouTubeVideoTest(VideoBaseTest):
self.video.reload_page() self.video.reload_page()
# go to the vertical containing video "A" # 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" # check if video "A" should start playing at speed "2.0"
self.assertEqual(self.video.speed, '2.0x') self.assertEqual(self.video.speed, '2.0x')
...@@ -715,13 +713,13 @@ class YouTubeVideoTest(VideoBaseTest): ...@@ -715,13 +713,13 @@ class YouTubeVideoTest(VideoBaseTest):
self.video.speed = '1.0' self.video.speed = '1.0'
# go to the vertical containing "B" # 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. # Video "B" should still play at speed .5 because it was explicitly set to that.
self.assertEqual(self.video.speed, '0.50x') self.assertEqual(self.video.speed, '0.50x')
# go to the vertical containing video "C" # 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 # The change of speed for Video "A" should impact Video "C" because it still has
# not been explicitly set to a speed. # not been explicitly set to a speed.
......
...@@ -231,18 +231,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): ...@@ -231,18 +231,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
# # of sql queries to default, # # of sql queries to default,
# # of mongo queries, # # of mongo queries,
# ) # )
('no_overrides', 1, True, False): (21, 6), ('no_overrides', 1, True, False): (22, 6),
('no_overrides', 2, True, False): (21, 6), ('no_overrides', 2, True, False): (22, 6),
('no_overrides', 3, True, False): (21, 6), ('no_overrides', 3, True, False): (22, 6),
('ccx', 1, True, False): (21, 6), ('ccx', 1, True, False): (22, 6),
('ccx', 2, True, False): (21, 6), ('ccx', 2, True, False): (22, 6),
('ccx', 3, True, False): (21, 6), ('ccx', 3, True, False): (22, 6),
('no_overrides', 1, False, False): (21, 6), ('no_overrides', 1, False, False): (22, 6),
('no_overrides', 2, False, False): (21, 6), ('no_overrides', 2, False, False): (22, 6),
('no_overrides', 3, False, False): (21, 6), ('no_overrides', 3, False, False): (22, 6),
('ccx', 1, False, False): (21, 6), ('ccx', 1, False, False): (22, 6),
('ccx', 2, False, False): (21, 6), ('ccx', 2, False, False): (22, 6),
('ccx', 3, False, False): (21, 6), ('ccx', 3, False, False): (22, 6),
} }
...@@ -254,19 +254,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase): ...@@ -254,19 +254,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
__test__ = True __test__ = True
TEST_DATA = { TEST_DATA = {
('no_overrides', 1, True, False): (21, 3), ('no_overrides', 1, True, False): (22, 3),
('no_overrides', 2, True, False): (21, 3), ('no_overrides', 2, True, False): (22, 3),
('no_overrides', 3, True, False): (21, 3), ('no_overrides', 3, True, False): (22, 3),
('ccx', 1, True, False): (21, 3), ('ccx', 1, True, False): (22, 3),
('ccx', 2, True, False): (21, 3), ('ccx', 2, True, False): (22, 3),
('ccx', 3, True, False): (21, 3), ('ccx', 3, True, False): (22, 3),
('ccx', 1, True, True): (22, 3), ('ccx', 1, True, True): (23, 3),
('ccx', 2, True, True): (22, 3), ('ccx', 2, True, True): (23, 3),
('ccx', 3, True, True): (22, 3), ('ccx', 3, True, True): (23, 3),
('no_overrides', 1, False, False): (21, 3), ('no_overrides', 1, False, False): (22, 3),
('no_overrides', 2, False, False): (21, 3), ('no_overrides', 2, False, False): (22, 3),
('no_overrides', 3, False, False): (21, 3), ('no_overrides', 3, False, False): (22, 3),
('ccx', 1, False, False): (21, 3), ('ccx', 1, False, False): (22, 3),
('ccx', 2, False, False): (21, 3), ('ccx', 2, False, False): (22, 3),
('ccx', 3, False, False): (21, 3), ('ccx', 3, False, False): (22, 3),
} }
...@@ -2,14 +2,17 @@ ...@@ -2,14 +2,17 @@
This module is essentially a broker to xmodule/tabs.py -- it was originally introduced to 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 perform some LMS-specific tab display gymnastics for the Entrance Exams feature
""" """
import waffle
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext as _, ugettext_noop from django.utils.translation import ugettext as _, ugettext_noop
from courseware.access import has_access from courseware.access import has_access
from courseware.entrance_exams import user_must_complete_entrance_exam from courseware.entrance_exams import user_must_complete_entrance_exam
from openedx.core.lib.course_tabs import CourseTabPluginManager from openedx.core.lib.course_tabs import CourseTabPluginManager
from request_cache.middleware import RequestCache
from student.models import CourseEnrollment 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): class EnrolledTab(CourseTab):
...@@ -34,6 +37,25 @@ class CoursewareTab(EnrolledTab): ...@@ -34,6 +37,25 @@ class CoursewareTab(EnrolledTab):
is_movable = False is_movable = False
is_default = 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): class CourseInfoTab(CourseTab):
""" """
......
...@@ -1419,17 +1419,17 @@ class ProgressPageTests(ModuleStoreTestCase): ...@@ -1419,17 +1419,17 @@ class ProgressPageTests(ModuleStoreTestCase):
"""Test that query counts remain the same for self-paced and instructor-paced courses.""" """Test that query counts remain the same for self-paced and instructor-paced courses."""
SelfPacedConfiguration(enabled=self_paced_enabled).save() SelfPacedConfiguration(enabled=self_paced_enabled).save()
self.setup_course(self_paced=self_paced) 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() self._get_progress_page()
def test_progress_queries(self): def test_progress_queries(self):
self.setup_course() self.setup_course()
with self.assertNumQueries(38), check_mongo_calls(4): with self.assertNumQueries(39), check_mongo_calls(4):
self._get_progress_page() self._get_progress_page()
# subsequent accesses to the progress page require fewer queries. # subsequent accesses to the progress page require fewer queries.
for _ in range(2): for _ in range(2):
with self.assertNumQueries(24), check_mongo_calls(4): with self.assertNumQueries(25), check_mongo_calls(4):
self._get_progress_page() self._get_progress_page()
@patch( @patch(
......
...@@ -21,12 +21,14 @@ from edxmako.shortcuts import render_to_response, render_to_string ...@@ -21,12 +21,14 @@ from edxmako.shortcuts import render_to_response, render_to_string
import logging import logging
import newrelic.agent import newrelic.agent
import urllib import urllib
import waffle
from xblock.fragment import Fragment from xblock.fragment import Fragment
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY 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.user_api.preferences.api import get_user_preference
from openedx.core.djangoapps.crawlers.models import CrawlersConfig from openedx.core.djangoapps.crawlers.models import CrawlersConfig
from request_cache.middleware import RequestCache
from shoppingcart.models import CourseRegistrationCode from shoppingcart.models import CourseRegistrationCode
from student.models import CourseEnrollment from student.models import CourseEnrollment
from student.views import is_course_blocked from student.views import is_course_blocked
...@@ -36,6 +38,7 @@ from util.views import ensure_valid_course_key ...@@ -36,6 +38,7 @@ from util.views import ensure_valid_course_key
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.x_module import STUDENT_VIEW from xmodule.x_module import STUDENT_VIEW
from survey.utils import must_answer_survey 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 import has_access, _adjust_start_date_for_beta_testers
from ..access_utils import in_preview_mode from ..access_utils import in_preview_mode
...@@ -396,9 +399,9 @@ class CoursewareIndex(View): ...@@ -396,9 +399,9 @@ class CoursewareIndex(View):
Returns and creates the rendering context for the courseware. Returns and creates the rendering context for the courseware.
Also returns the table of contents for the courseware. Also returns the table of contents for the courseware.
""" """
request = RequestCache.get_current_request()
courseware_context = { courseware_context = {
'csrf': csrf(self.request)['csrf_token'], 'csrf': csrf(self.request)['csrf_token'],
'COURSE_TITLE': self.course.display_name_with_default_escaped,
'course': self.course, 'course': self.course,
'init': '', 'init': '',
'fragment': Fragment(), 'fragment': Fragment(),
...@@ -411,7 +414,8 @@ class CoursewareIndex(View): ...@@ -411,7 +414,8 @@ class CoursewareIndex(View):
'language_preference': self._get_language_preference(), 'language_preference': self._get_language_preference(),
'disable_optimizely': True, 'disable_optimizely': True,
'section_title': None, '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( table_of_contents = toc_for_course(
self.effective_user, self.effective_user,
...@@ -455,7 +459,7 @@ class CoursewareIndex(View): ...@@ -455,7 +459,7 @@ class CoursewareIndex(View):
courseware_context['default_tab'] = self.section.default_tab courseware_context['default_tab'] = self.section.default_tab
# section data # 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( section_context = self._create_section_context(
table_of_contents['previous_of_active_section'], table_of_contents['previous_of_active_section'],
table_of_contents['next_of_active_section'], table_of_contents['next_of_active_section'],
......
...@@ -223,6 +223,18 @@ ECOMMERCE_API_URL = 'http://localhost:8043/api/v2/' ...@@ -223,6 +223,18 @@ ECOMMERCE_API_URL = 'http://localhost:8043/api/v2/'
LMS_ROOT_URL = "http://localhost:8000" LMS_ROOT_URL = "http://localhost:8000"
DOC_LINK_BASE_URL = 'http://edx.readthedocs.io/projects/edx-guide-for-students' 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. # Lastly, see if the developer has any local overrides.
try: try:
......
...@@ -1724,7 +1724,7 @@ REQUIRE_ENVIRONMENT = "node" ...@@ -1724,7 +1724,7 @@ REQUIRE_ENVIRONMENT = "node"
# but you don't want to include those dependencies in the JS bundle for the page, # 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. # then you need to add the js urls in this list.
REQUIRE_JS_PATH_OVERRIDES = { 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', 'js/views/message_banner': 'js/views/message_banner.js',
'moment': 'common/js/vendor/moment-with-locales.js', 'moment': 'common/js/vendor/moment-with-locales.js',
'moment-timezone': 'common/js/vendor/moment-timezone-with-data.js', 'moment-timezone': 'common/js/vendor/moment-timezone-with-data.js',
...@@ -2173,6 +2173,10 @@ INSTALLED_APPS = ( ...@@ -2173,6 +2173,10 @@ INSTALLED_APPS = (
# Unusual migrations # Unusual migrations
'database_fixups', 'database_fixups',
# Features
'openedx.features.course_bookmarks',
'openedx.features.course_experience',
) )
######################### CSRF ######################################### ######################### 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 @@ ...@@ -3,10 +3,9 @@
define([ define([
'jquery', 'jquery',
'logger', 'logger'
'js/bookmarks/views/bookmarks_list_button'
], ],
function($, Logger, BookmarksListButton) { function($, Logger) {
return function() { return function() {
// This function performs all actions common to all courseware. // This function performs all actions common to all courseware.
// 1. adding an event to all link clicks. // 1. adding an event to all link clicks.
...@@ -18,9 +17,6 @@ ...@@ -18,9 +17,6 @@
target_url: event.currentTarget.href 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>
define(['backbone',
'jquery',
'underscore',
'logger',
'URI',
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'common/js/spec_helpers/template_helpers',
'js/bookmarks/views/bookmarks_list_button',
'js/bookmarks/views/bookmarks_list',
'js/bookmarks/collections/bookmarks'],
function(Backbone, $, _, Logger, URI, AjaxHelpers, TemplateHelpers, BookmarksListButtonView, BookmarksListView,
BookmarksCollection) {
'use strict';
describe('lms.courseware.bookmarks', function() {
var bookmarksButtonView;
beforeEach(function() {
loadFixtures('js/fixtures/bookmarks/bookmarks.html');
TemplateHelpers.installTemplates(
[
'templates/fields/message_banner',
'templates/bookmarks/bookmarks-list'
]
);
spyOn(Logger, 'log').and.returnValue($.Deferred().resolve());
jasmine.addMatchers({
toHaveBeenCalledWithUrl: function() {
return {
compare: function(actual, expectedUrl) {
return {
pass: expectedUrl === actual.calls.mostRecent().args[0].currentTarget.pathname
};
}
};
}
});
bookmarksButtonView = new BookmarksListButtonView();
});
var verifyRequestParams = function(requests, params) {
var urlParams = (new URI(requests[requests.length - 1].url)).query(true);
_.each(params, function(value, key) {
expect(urlParams[key]).toBe(value);
});
};
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: []
};
for (var i = 0; i < options.numBookmarksToCreate; i++) {
var 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_DISAPLAY_NAME', usage_id: 'SECTION_USAGE_ID'},
{display_name: 'SUBSECTION_DISAPLAY_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;
expect(bookmarks.length, results.length);
for (var bookmark_index = 0; bookmark_index < results.length; bookmark_index++) {
courseId = results[bookmark_index].course_id;
usageId = results[bookmark_index].usage_id;
expect(bookmarks[bookmark_index]).toHaveAttr('href', createBookmarkUrl(courseId, usageId));
expect($(bookmarks[bookmark_index]).data('bookmarkId')).toBe(bookmark_index);
expect($(bookmarks[bookmark_index]).data('componentType')).toBe('vertical');
expect($(bookmarks[bookmark_index]).data('usageId')).toBe(usageId);
expect($(bookmarks[bookmark_index]).find('.list-item-breadcrumbtrail').html().trim()).
toBe(breadcrumbTrail(results[bookmark_index].path, results[bookmark_index].display_name));
expect($(bookmarks[bookmark_index]).find('.list-item-date').text().trim()).
toBe('Bookmarked on ' + view.humanFriendlyDate(results[bookmark_index].created));
}
};
var verifyPaginationInfo = function(requests, expectedData, currentPage, headerMessage) {
AjaxHelpers.respondWithJson(requests, expectedData);
verifyBookmarkedData(bookmarksButtonView.bookmarksListView, expectedData);
expect(bookmarksButtonView.bookmarksListView.$('.paging-footer span.current-page').text().trim()).
toBe(currentPage);
expect(bookmarksButtonView.bookmarksListView.$('.paging-header span').text().trim()).
toBe(headerMessage);
};
it('has correct behavior for bookmarks button', function() {
var requests = AjaxHelpers.requests(this);
spyOn(bookmarksButtonView, 'toggleBookmarksListView').and.callThrough();
bookmarksButtonView.delegateEvents();
expect(bookmarksButtonView.$('.bookmarks-list-button')).toHaveAttr('aria-pressed', 'false');
expect(bookmarksButtonView.$('.bookmarks-list-button')).toHaveClass('is-inactive');
bookmarksButtonView.$('.bookmarks-list-button').click();
expect(bookmarksButtonView.toggleBookmarksListView).toHaveBeenCalled();
expect(bookmarksButtonView.$('.bookmarks-list-button')).toHaveAttr('aria-pressed', 'true');
expect(bookmarksButtonView.$('.bookmarks-list-button')).toHaveClass('is-active');
AjaxHelpers.respondWithJson(requests, createBookmarksData({numBookmarksToCreate: 1}));
bookmarksButtonView.$('.bookmarks-list-button').click();
expect(bookmarksButtonView.$('.bookmarks-list-button')).toHaveAttr('aria-pressed', 'false');
expect(bookmarksButtonView.$('.bookmarks-list-button')).toHaveClass('is-inactive');
});
it('can correctly render an empty bookmarks list', function() {
var requests = AjaxHelpers.requests(this);
var expectedData = createBookmarksData({numBookmarksToCreate: 0});
bookmarksButtonView.$('.bookmarks-list-button').click();
AjaxHelpers.respondWithJson(requests, expectedData);
expect(bookmarksButtonView.bookmarksListView.$('.bookmarks-empty-header').text().trim()).
toBe('You have not bookmarked any courseware pages yet.');
var emptyListText = '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.';
expect(bookmarksButtonView.bookmarksListView.$('.bookmarks-empty-detail-title').text().trim()).
toBe(emptyListText);
expect(bookmarksButtonView.bookmarksListView.$('.paging-header').length).toBe(0);
expect(bookmarksButtonView.bookmarksListView.$('.paging-footer').length).toBe(0);
});
it('has rendered bookmarked list correctly', function() {
var requests = AjaxHelpers.requests(this);
var expectedData = createBookmarksData({numBookmarksToCreate: 3});
bookmarksButtonView.$('.bookmarks-list-button').click();
verifyRequestParams(
requests,
{course_id: 'a/b/c', fields: 'display_name,path', page: '1', page_size: '10'}
);
AjaxHelpers.respondWithJson(requests, expectedData);
expect(bookmarksButtonView.bookmarksListView.$('.bookmarks-results-header').text().trim()).
toBe('My Bookmarks');
verifyBookmarkedData(bookmarksButtonView.bookmarksListView, expectedData);
expect(bookmarksButtonView.bookmarksListView.$('.paging-header').length).toBe(1);
expect(bookmarksButtonView.bookmarksListView.$('.paging-footer').length).toBe(1);
});
it('calls bookmarks list render on page_changed event', function() {
var renderSpy = spyOn(BookmarksListView.prototype, 'render');
var listView = new BookmarksListView({
collection: new BookmarksCollection([], {
course_id: 'abc',
url: '/test-bookmarks/url/'
})
});
listView.collection.trigger('page_changed');
expect(renderSpy).toHaveBeenCalled();
});
it('can go to a page number', function() {
var requests = AjaxHelpers.requests(this);
var expectedData = createBookmarksData(
{
numBookmarksToCreate: 10,
count: 12,
num_pages: 2,
current_page: 1,
start: 0
}
);
bookmarksButtonView.$('.bookmarks-list-button').click();
AjaxHelpers.respondWithJson(requests, expectedData);
verifyBookmarkedData(bookmarksButtonView.bookmarksListView, expectedData);
bookmarksButtonView.bookmarksListView.$('input#page-number-input').val('2');
bookmarksButtonView.bookmarksListView.$('input#page-number-input').trigger('change');
expectedData = createBookmarksData(
{
numBookmarksToCreate: 2,
count: 12,
num_pages: 2,
current_page: 2,
start: 10
}
);
AjaxHelpers.respondWithJson(requests, expectedData);
verifyBookmarkedData(bookmarksButtonView.bookmarksListView, expectedData);
expect(bookmarksButtonView.bookmarksListView.$('.paging-footer span.current-page').text().trim()).
toBe('2');
expect(bookmarksButtonView.bookmarksListView.$('.paging-header span').text().trim()).
toBe('Showing 11-12 out of 12 total');
});
it('can navigate forward and backward', function() {
var requests = AjaxHelpers.requests(this);
var expectedData = createBookmarksData(
{
numBookmarksToCreate: 10,
count: 15,
num_pages: 2,
current_page: 1,
start: 0
}
);
bookmarksButtonView.$('.bookmarks-list-button').click();
verifyPaginationInfo(requests, expectedData, '1', 'Showing 1-10 out of 15 total');
verifyRequestParams(
requests,
{course_id: 'a/b/c', fields: 'display_name,path', page: '1', page_size: '10'}
);
bookmarksButtonView.bookmarksListView.$('.paging-footer .next-page-link').click();
expectedData = createBookmarksData(
{
numBookmarksToCreate: 5,
count: 15,
num_pages: 2,
current_page: 2,
start: 10
}
);
verifyPaginationInfo(requests, expectedData, '2', 'Showing 11-15 out of 15 total');
verifyRequestParams(
requests,
{course_id: 'a/b/c', fields: 'display_name,path', page: '2', page_size: '10'}
);
expectedData = createBookmarksData(
{
numBookmarksToCreate: 10,
count: 15,
num_pages: 2,
current_page: 1,
start: 0
}
);
bookmarksButtonView.bookmarksListView.$('.paging-footer .previous-page-link').click();
verifyPaginationInfo(requests, expectedData, '1', 'Showing 1-10 out of 15 total');
verifyRequestParams(
requests,
{course_id: 'a/b/c', fields: 'display_name,path', page: '1', page_size: '10'}
);
});
it('can navigate to correct url', function() {
var requests = AjaxHelpers.requests(this);
spyOn(bookmarksButtonView.bookmarksListView, 'visitBookmark');
bookmarksButtonView.$('.bookmarks-list-button').click();
AjaxHelpers.respondWithJson(requests, createBookmarksData({numBookmarksToCreate: 1}));
bookmarksButtonView.bookmarksListView.$('.bookmarks-results-list-item').click();
var url = bookmarksButtonView.bookmarksListView.$('.bookmarks-results-list-item').attr('href');
expect(bookmarksButtonView.bookmarksListView.visitBookmark).toHaveBeenCalledWithUrl(url);
});
it('shows an error message for HTTP 500', function() {
var requests = AjaxHelpers.requests(this);
bookmarksButtonView.$('.bookmarks-list-button').click();
AjaxHelpers.respondWithError(requests);
expect(bookmarksButtonView.bookmarksListView.$('.bookmarks-results-header').text().trim()).not
.toBe('My Bookmarks');
expect($('#error-message').text().trim()).toBe(bookmarksButtonView.bookmarksListView.errorMessage);
});
});
});
...@@ -27,6 +27,8 @@ var options = { ...@@ -27,6 +27,8 @@ var options = {
// Otherwise Istanbul which is used for coverage tracking will cause tests to not run. // Otherwise Istanbul which is used for coverage tracking will cause tests to not run.
sourceFiles: [ sourceFiles: [
{pattern: 'coffee/src/**/!(*spec).js'}, {pattern: 'coffee/src/**/!(*spec).js'},
{pattern: 'course_bookmarks/**/!(*spec).js'},
{pattern: 'course_experience/js/**/!(*spec).js'},
{pattern: 'discussion/js/**/!(*spec).js'}, {pattern: 'discussion/js/**/!(*spec).js'},
{pattern: 'js/**/!(*spec|djangojs).js'}, {pattern: 'js/**/!(*spec|djangojs).js'},
{pattern: 'lms/js/**/!(*spec).js'}, {pattern: 'lms/js/**/!(*spec).js'},
......
...@@ -18,6 +18,8 @@ ...@@ -18,6 +18,8 @@
* done. * done.
*/ */
modules: getModulesList([ modules: getModulesList([
'course_bookmarks/js/course_bookmarks_factory',
'course_experience/js/course_outline_factory',
'discussion/js/discussion_board_factory', 'discussion/js/discussion_board_factory',
'discussion/js/discussion_profile_page_factory', 'discussion/js/discussion_profile_page_factory',
'js/api_admin/catalog_preview_factory', 'js/api_admin/catalog_preview_factory',
......
...@@ -92,12 +92,6 @@ ...@@ -92,12 +92,6 @@
'js/student_profile/views/learner_profile_factory': 'js/student_profile/views/learner_profile_factory', '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/student_profile/views/learner_profile_view': 'js/student_profile/views/learner_profile_view',
'js/ccx/schedule': 'js/ccx/schedule', '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', 'js/views/message_banner': 'js/views/message_banner',
// edxnotes // edxnotes
...@@ -679,14 +673,16 @@ ...@@ -679,14 +673,16 @@
}); });
testFiles = [ 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_board_factory_spec.js',
'discussion/js/spec/discussion_profile_page_factory_spec.js', 'discussion/js/spec/discussion_profile_page_factory_spec.js',
'discussion/js/spec/discussion_board_view_spec.js', 'discussion/js/spec/discussion_board_view_spec.js',
'discussion/js/spec/views/discussion_user_profile_view_spec.js', 'discussion/js/spec/views/discussion_user_profile_view_spec.js',
'lms/js/spec/preview/preview_factory_spec.js', 'lms/js/spec/preview/preview_factory_spec.js',
'js/spec/api_admin/catalog_preview_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/ccx/schedule_spec.js',
'js/spec/commerce/receipt_view_spec.js', 'js/spec/commerce/receipt_view_spec.js',
'js/spec/components/card/card_spec.js', 'js/spec/components/card/card_spec.js',
......
...@@ -62,10 +62,12 @@ ...@@ -62,10 +62,12 @@
@import 'views/support'; @import 'views/support';
@import 'views/oauth2'; @import 'views/oauth2';
@import "views/financial-assistance"; @import "views/financial-assistance";
@import 'views/bookmarks';
@import 'course/auto-cert'; @import 'course/auto-cert';
@import 'views/api-access'; @import 'views/api-access';
// features
@import 'features/bookmarks-v1';
// search // search
@import 'search/search'; @import 'search/search';
......
...@@ -19,4 +19,10 @@ ...@@ -19,4 +19,10 @@
@import 'shared-v2/modal'; @import 'shared-v2/modal';
@import 'shared-v2/help-tab'; @import 'shared-v2/help-tab';
// Elements
@import 'notifications'; @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 $bookmark-icon: "\f097"; // .fa-bookmark-o
$bookmarked-icon: "\f02e"; // .fa-bookmark $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;
}
&.is-active {
background-color: lighten($action-primary-bg,10%);
color: $white;
}
}
}
// Rules for Bookmarks Results Header // Rules for Bookmarks Results Header
.bookmarks-results-header { .bookmarks-results-header {
@extend %t-title4;
letter-spacing: 0; letter-spacing: 0;
text-transform: none; text-transform: none;
margin-bottom: ($baseline/2); margin-bottom: ($baseline/2);
...@@ -55,20 +15,16 @@ $bookmarked-icon: "\f02e"; // .fa-bookmark ...@@ -55,20 +15,16 @@ $bookmarked-icon: "\f02e"; // .fa-bookmark
.bookmarks-results-list-item { .bookmarks-results-list-item {
@include padding(0, $baseline, ($baseline/4), $baseline); @include padding(0, $baseline, ($baseline/4), $baseline);
display: block; display: block;
border: 1px solid $gray-l4; border: 1px solid $lms-border-color;
margin-bottom: $baseline; margin-bottom: $baseline;
&:hover { &:hover {
border-color: $m-blue; border-color: palette(primary, base);
.list-item-breadcrumbtrail { .list-item-breadcrumbtrail {
color: $blue; color: palette(primary, base);
} }
} }
.icon {
@extend %t-icon6;
}
} }
.results-list-item-view { .results-list-item-view {
...@@ -77,9 +33,9 @@ $bookmarked-icon: "\f02e"; // .fa-bookmark ...@@ -77,9 +33,9 @@ $bookmarked-icon: "\f02e"; // .fa-bookmark
} }
.list-item-date { .list-item-date {
@extend %t-copy-sub2;
margin-top: ($baseline/4); margin-top: ($baseline/4);
color: $gray; color: $lms-gray;
font-size: font-size(small);
} }
.bookmarks-results-list-item:before { .bookmarks-results-list-item:before {
...@@ -87,7 +43,7 @@ $bookmarked-icon: "\f02e"; // .fa-bookmark ...@@ -87,7 +43,7 @@ $bookmarked-icon: "\f02e"; // .fa-bookmark
position: relative; position: relative;
top: -7px; top: -7px;
font-family: FontAwesome; font-family: FontAwesome;
color: $m-blue; color: palette(primary, base);
} }
.list-item-content { .list-item-content {
...@@ -113,13 +69,12 @@ $bookmarked-icon: "\f02e"; // .fa-bookmark ...@@ -113,13 +69,12 @@ $bookmarked-icon: "\f02e"; // .fa-bookmark
} }
} }
// Rules for empty bookmarks list // Rules for empty bookmarks list
.bookmarks-empty { .bookmarks-empty {
margin-top: $baseline; margin-top: $baseline;
border: 1px solid $gray-l4; border: 1px solid $lms-border-color;
padding: $baseline; padding: $baseline;
background-color: $gray-l6; background-color: $white;
} }
.bookmarks-empty-header { .bookmarks-empty-header {
...@@ -130,36 +85,3 @@ $bookmarked-icon: "\f02e"; // .fa-bookmark ...@@ -130,36 +85,3 @@ $bookmarked-icon: "\f02e"; // .fa-bookmark
.bookmarks-empty-detail { .bookmarks-empty-detail {
@extend %t-copy-sub1; @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;
}
}
}
}
.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 @@ ...@@ -46,16 +46,6 @@
display: inline-block; display: inline-block;
} }
.form-actions > * {
@include margin-left($baseline/2);
vertical-align: middle;
height: 34px;
}
.form-actions > button {
height: 34px;
}
.form-actions > *:first-child { .form-actions > *:first-child {
@include margin-left(0); @include margin-left(0);
} }
......
<%page expression_filter="h" args="bookmark_id, is_bookmarked" /> <%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"> <div class="bookmark-button-wrapper">
<button class="btn btn-link bookmark-button ${"bookmarked" if is_bookmarked else ""}" <button class="btn btn-link bookmark-button ${"bookmarked" if is_bookmarked else ""}"
aria-pressed="${"true" if is_bookmarked else "false"}" 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> <span class="bookmark-text">${_("Bookmarked") if is_bookmarked else _("Bookmark this page")}</span>
</button> </button>
</div> </div>
...@@ -3,12 +3,15 @@ ...@@ -3,12 +3,15 @@
<%namespace name='static' file='/static_content.html'/> <%namespace name='static' file='/static_content.html'/>
<%def name="online_help_token()"><% return "courseware" %></%def> <%def name="online_help_token()"><% return "courseware" %></%def>
<%! <%!
from django.utils.translation import ugettext as _ import waffle
from django.conf import settings 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 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.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) 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 ...@@ -27,7 +30,7 @@ from openedx.core.djangolib.js_utils import js_escaped_string
<%block name="header_extras"> <%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"> <script type="text/template" id="${template_name}-tpl">
<%static:include path="common/templates/${template_name}.underscore" /> <%static:include path="common/templates/${template_name}.underscore" />
</script> </script>
...@@ -115,10 +118,10 @@ ${HTML(fragment.foot_html())} ...@@ -115,10 +118,10 @@ ${HTML(fragment.foot_html())}
<div class="wrapper-course-modes"> <div class="wrapper-course-modes">
<div class="courseware-bookmarks-button" data-bookmarks-api-url="${bookmarks_api_url}"> <div class="courseware-bookmarks-button">
<button type="button" class="bookmarks-list-button is-inactive" aria-pressed="false"> <a class="bookmarks-list-button" href="${reverse('openedx.course_bookmarks.home', args=[course.id])}">
${_('Bookmarks')} ${_('Bookmarks')}
</button> </a>
</div> </div>
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'): % if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
...@@ -152,7 +155,10 @@ ${HTML(fragment.foot_html())} ...@@ -152,7 +155,10 @@ ${HTML(fragment.foot_html())}
% endif % endif
<section class="course-content" id="course-content"> <section class="course-content" id="course-content">
<main id="main" tabindex="-1" aria-label="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 \ % if getattr(course, 'entrance_exam_enabled') and \
getattr(course, 'entrance_exam_minimum_score_pct') and \ getattr(course, 'entrance_exam_minimum_score_pct') and \
entrance_exam_current_score is not UNDEFINED: entrance_exam_current_score is not UNDEFINED:
......
...@@ -601,10 +601,27 @@ urlpatterns += ( ...@@ -601,10 +601,27 @@ urlpatterns += (
name='edxnotes_endpoints', name='edxnotes_endpoints',
), ),
# Branding API
url( url(
r'^api/branding/v1/', r'^api/branding/v1/',
include('branding.api_urls') 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"]: if settings.FEATURES["ENABLE_TEAMS"]:
......
...@@ -45,21 +45,18 @@ class EdxFragmentView(FragmentView): ...@@ -45,21 +45,18 @@ class EdxFragmentView(FragmentView):
else: else:
return settings.PIPELINE_JS[group]['source_filenames'] return settings.PIPELINE_JS[group]['source_filenames']
@abstractmethod
def vendor_js_dependencies(self): def vendor_js_dependencies(self):
""" """
Returns list of the vendor JS files that this view depends on. Returns list of the vendor JS files that this view depends on.
""" """
return [] return []
@abstractmethod
def js_dependencies(self): def js_dependencies(self):
""" """
Returns list of the JavaScript files that this view depends on. Returns list of the JavaScript files that this view depends on.
""" """
return [] return []
@abstractmethod
def css_dependencies(self): def css_dependencies(self):
""" """
Returns list of the CSS files that this view depends on. 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="message-banner" aria-live="polite"></div>
<div class="xblock xblock-student_view xblock-student_view-vertical xblock-initialized"> <div class="xblock xblock-student_view xblock-student_view-vertical xblock-initialized">
......
<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 @@ ...@@ -3,7 +3,7 @@
define([ define([
'backbone', 'backbone',
'edx-ui-toolkit/js/pagination/paging-collection', 'edx-ui-toolkit/js/pagination/paging-collection',
'js/bookmarks/models/bookmark' 'course_bookmarks/js/models/bookmark'
], function(Backbone, PagingCollection, BookmarkModel) { ], function(Backbone, PagingCollection, BookmarkModel) {
return PagingCollection.extend({ return PagingCollection.extend({
model: BookmarkModel, model: BookmarkModel,
...@@ -24,5 +24,5 @@ ...@@ -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);
...@@ -16,4 +16,4 @@ ...@@ -16,4 +16,4 @@
} }
}); });
}); });
})(define || RequireJS.define); }(define || RequireJS.define));
define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', define([
'common/js/spec_helpers/template_helpers', 'js/bookmarks/views/bookmark_button' '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) { function(Backbone, $, _, AjaxHelpers, TemplateHelpers, BookmarkButtonView) {
'use strict'; 'use strict';
describe('bookmarks.button', function() { describe('BookmarkButtonView', function() {
var timerCallback; var createBookmarkButtonView, verifyBookmarkButtonState;
var API_URL = 'bookmarks/api/v1/bookmarks/'; var API_URL = 'bookmarks/api/v1/bookmarks/';
beforeEach(function() { beforeEach(function() {
loadFixtures('js/fixtures/bookmarks/bookmark_button.html'); loadFixtures('course_bookmarks/fixtures/bookmark_button.html');
TemplateHelpers.installTemplates( TemplateHelpers.installTemplates(
[ [
'templates/fields/message_banner' 'templates/fields/message_banner'
] ]
); );
timerCallback = jasmine.createSpy('timerCallback'); jasmine.createSpy('timerCallback');
jasmine.clock().install(); jasmine.clock().install();
}); });
...@@ -25,7 +26,7 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helper ...@@ -25,7 +26,7 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helper
jasmine.clock().uninstall(); jasmine.clock().uninstall();
}); });
var createBookmarkButtonView = function(isBookmarked) { createBookmarkButtonView = function(isBookmarked) {
return new BookmarkButtonView({ return new BookmarkButtonView({
el: '.bookmark-button', el: '.bookmark-button',
bookmarked: isBookmarked, bookmarked: isBookmarked,
...@@ -35,7 +36,7 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helper ...@@ -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) { if (bookmarked) {
expect(view.$el).toHaveAttr('aria-pressed', 'true'); expect(view.$el).toHaveAttr('aria-pressed', 'true');
expect(view.$el).toHaveClass('bookmarked'); expect(view.$el).toHaveClass('bookmarked');
...@@ -46,7 +47,7 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helper ...@@ -46,7 +47,7 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helper
expect(view.$el.data('bookmarkId')).toBe('bilbo,usage_1'); expect(view.$el.data('bookmarkId')).toBe('bilbo,usage_1');
}; };
it('rendered correctly ', function() { it('rendered correctly', function() {
var view = createBookmarkButtonView(false); var view = createBookmarkButtonView(false);
verifyBookmarkButtonState(view, false); verifyBookmarkButtonState(view, false);
......
define([
'backbone',
'jquery',
'underscore',
'logger',
'URI',
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'common/js/spec_helpers/template_helpers',
'js/views/message_banner',
'course_bookmarks/js/spec_helpers/bookmark_helpers',
'course_bookmarks/js/views/bookmarks_list',
'course_bookmarks/js/collections/bookmarks'
],
function(Backbone, $, _, Logger, URI, AjaxHelpers, TemplateHelpers, MessageBannerView,
BookmarkHelpers, BookmarksListView, BookmarksCollection) {
'use strict';
describe('BookmarksListView', function() {
var createBookmarksView, verifyRequestParams;
beforeEach(function() {
loadFixtures('course_bookmarks/fixtures/bookmarks.html');
TemplateHelpers.installTemplates([
'templates/fields/message_banner'
]);
spyOn(Logger, 'log').and.returnValue($.Deferred().resolve());
jasmine.addMatchers({
toHaveBeenCalledWithUrl: function() {
return {
compare: function(actual, expectedUrl) {
return {
pass: expectedUrl === actual.calls.mostRecent().args[0].currentTarget.pathname
};
}
};
}
});
});
createBookmarksView = function() {
var bookmarksCollection = new BookmarksCollection(
[],
{
course_id: BookmarkHelpers.TEST_COURSE_ID,
url: BookmarkHelpers.TEST_API_URL
}
);
var bookmarksView = new BookmarksListView({
$el: $('.course-bookmarks'),
collection: bookmarksCollection,
loadingMessageView: new MessageBannerView({el: $('#loading-message')}),
errorMessageView: new MessageBannerView({el: $('#error-message')})
});
return bookmarksView;
};
verifyRequestParams = function(requests, params) {
var urlParams = (new URI(requests[requests.length - 1].url)).query(true);
_.each(params, function(value, key) {
expect(urlParams[key]).toBe(value);
});
};
it('can correctly render an empty bookmarks list', function() {
var requests = AjaxHelpers.requests(this);
var bookmarksView = createBookmarksView();
var expectedData = BookmarkHelpers.createBookmarksData({numBookmarksToCreate: 0});
bookmarksView.showBookmarks();
AjaxHelpers.respondWithJson(requests, expectedData);
expect(bookmarksView.$('.bookmarks-empty-header').text().trim()).toBe(
'You have not bookmarked any courseware pages yet'
);
expect(bookmarksView.$('.bookmarks-empty-detail-title').text().trim()).toBe(
'Use bookmarks to help you easily return to courseware pages. ' +
'To bookmark a page, click "Bookmark this page" under the page title.'
);
expect(bookmarksView.$('.paging-header').length).toBe(0);
expect(bookmarksView.$('.paging-footer').length).toBe(0);
});
it('has rendered bookmarked list correctly', function() {
var requests = AjaxHelpers.requests(this);
var bookmarksView = createBookmarksView();
var expectedData = BookmarkHelpers.createBookmarksData({numBookmarksToCreate: 3});
bookmarksView.showBookmarks();
verifyRequestParams(
requests,
{
course_id: BookmarkHelpers.TEST_COURSE_ID,
fields: 'display_name,path',
page: '1',
page_size: '10'
}
);
AjaxHelpers.respondWithJson(requests, expectedData);
BookmarkHelpers.verifyBookmarkedData(bookmarksView, expectedData);
expect(bookmarksView.$('.paging-header').length).toBe(1);
expect(bookmarksView.$('.paging-footer').length).toBe(1);
});
it('calls bookmarks list render on page_changed event', function() {
var renderSpy = spyOn(BookmarksListView.prototype, 'render');
var listView = new BookmarksListView({
collection: new BookmarksCollection([], {
course_id: 'abc',
url: '/test-bookmarks/url/'
})
});
listView.collection.trigger('page_changed');
expect(renderSpy).toHaveBeenCalled();
});
it('can go to a page number', function() {
var requests = AjaxHelpers.requests(this);
var expectedData = BookmarkHelpers.createBookmarksData(
{
numBookmarksToCreate: 10,
count: 12,
num_pages: 2,
current_page: 1,
start: 0
}
);
var bookmarksView = createBookmarksView();
bookmarksView.showBookmarks();
AjaxHelpers.respondWithJson(requests, expectedData);
BookmarkHelpers.verifyBookmarkedData(bookmarksView, expectedData);
bookmarksView.$('input#page-number-input').val('2');
bookmarksView.$('input#page-number-input').trigger('change');
expectedData = BookmarkHelpers.createBookmarksData(
{
numBookmarksToCreate: 2,
count: 12,
num_pages: 2,
current_page: 2,
start: 10
}
);
AjaxHelpers.respondWithJson(requests, expectedData);
BookmarkHelpers.verifyBookmarkedData(bookmarksView, expectedData);
expect(bookmarksView.$('.paging-footer span.current-page').text().trim()).toBe('2');
expect(bookmarksView.$('.paging-header span').text().trim()).toBe('Showing 11-12 out of 12 total');
});
it('can navigate forward and backward', function() {
var requests = AjaxHelpers.requests(this);
var bookmarksView = createBookmarksView();
var expectedData = BookmarkHelpers.createBookmarksData(
{
numBookmarksToCreate: 10,
count: 15,
num_pages: 2,
current_page: 1,
start: 0
}
);
bookmarksView.showBookmarks();
BookmarkHelpers.verifyPaginationInfo(
requests,
bookmarksView,
expectedData,
'1',
'Showing 1-10 out of 15 total'
);
verifyRequestParams(
requests,
{
course_id: BookmarkHelpers.TEST_COURSE_ID,
fields: 'display_name,path',
page: '1',
page_size: '10'
}
);
bookmarksView.$('.paging-footer .next-page-link').click();
expectedData = BookmarkHelpers.createBookmarksData(
{
numBookmarksToCreate: 5,
count: 15,
num_pages: 2,
current_page: 2,
start: 10
}
);
BookmarkHelpers.verifyPaginationInfo(
requests,
bookmarksView,
expectedData,
'2',
'Showing 11-15 out of 15 total'
);
verifyRequestParams(
requests,
{
course_id: BookmarkHelpers.TEST_COURSE_ID,
fields: 'display_name,path',
page: '2',
page_size: '10'
}
);
expectedData = BookmarkHelpers.createBookmarksData(
{
numBookmarksToCreate: 10,
count: 15,
num_pages: 2,
current_page: 1,
start: 0
}
);
bookmarksView.$('.paging-footer .previous-page-link').click();
BookmarkHelpers.verifyPaginationInfo(
requests,
bookmarksView,
expectedData,
'1',
'Showing 1-10 out of 15 total'
);
verifyRequestParams(
requests,
{
course_id: BookmarkHelpers.TEST_COURSE_ID,
fields: 'display_name,path',
page: '1',
page_size: '10'
}
);
});
it('can navigate to correct url', function() {
var requests = AjaxHelpers.requests(this);
var bookmarksView = createBookmarksView();
var url;
spyOn(bookmarksView, 'visitBookmark');
bookmarksView.showBookmarks();
AjaxHelpers.respondWithJson(requests, BookmarkHelpers.createBookmarksData({numBookmarksToCreate: 1}));
bookmarksView.$('.bookmarks-results-list-item').click();
url = bookmarksView.$('.bookmarks-results-list-item').attr('href');
expect(bookmarksView.visitBookmark).toHaveBeenCalledWithUrl(url);
});
it('shows an error message for HTTP 500', function() {
var requests = AjaxHelpers.requests(this);
var bookmarksView = createBookmarksView();
bookmarksView.showBookmarks();
AjaxHelpers.respondWithError(requests);
expect($('#error-message').text().trim()).toBe(bookmarksView.errorMessage);
});
});
});
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'; 'use strict';
define(['gettext', 'jquery', 'underscore', 'backbone', 'js/views/message_banner'], define(['gettext', 'jquery', 'underscore', 'backbone', 'js/views/message_banner'],
function(gettext, $, _, Backbone, MessageBannerView) { function(gettext, $, _, Backbone, MessageBannerView) {
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
bookmarkedText: gettext('Bookmarked'), bookmarkedText: gettext('Bookmarked'),
events: { events: {
'click': 'toggleBookmark' click: 'toggleBookmark'
}, },
showBannerInterval: 5000, // time in ms showBannerInterval: 5000, // time in ms
...@@ -46,12 +46,12 @@ ...@@ -46,12 +46,12 @@
view.setBookmarkState(true); view.setBookmarkState(true);
}, },
error: function(jqXHR) { error: function(jqXHR) {
var response, userMessage;
try { try {
var response = jqXHR.responseText ? JSON.parse(jqXHR.responseText) : ''; response = jqXHR.responseText ? JSON.parse(jqXHR.responseText) : '';
var userMessage = response ? response.user_message : ''; userMessage = response ? response.user_message : '';
view.showError(userMessage); view.showError(userMessage);
} } catch (err) {
catch (err) {
view.showError(); view.showError();
} }
}, },
......
(function(define, undefined) { (function(define) {
'use strict'; 'use strict';
define(['gettext', 'jquery', 'underscore', 'backbone', 'logger', 'moment', 'edx-ui-toolkit/js/utils/html-utils', 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', 'common/js/components/views/paging_header', 'common/js/components/views/paging_footer',
'text!templates/bookmarks/bookmarks-list.underscore' 'text!course_bookmarks/templates/bookmarks-list.underscore'
], ],
function(gettext, $, _, Backbone, Logger, _moment, HtmlUtils, function(gettext, $, _, Backbone, Logger, _moment, HtmlUtils,
PagingHeaderView, PagingFooterView, BookmarksListTemplate) { PagingHeaderView, PagingFooterView, bookmarksListTemplate) {
var moment = _moment || window.moment; var moment = _moment || window.moment;
return Backbone.View.extend({ return Backbone.View.extend({
...@@ -15,7 +16,7 @@ ...@@ -15,7 +16,7 @@
coursewareResultsWrapperEl: '.courseware-results-wrapper', coursewareResultsWrapperEl: '.courseware-results-wrapper',
errorIcon: '<span class="fa fa-fw fa-exclamation-triangle message-error" aria-hidden="true"></span>', 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.'), errorMessage: gettext('An error has occurred. Please try again.'),
loadingMessage: gettext('Loading'), loadingMessage: gettext('Loading'),
...@@ -27,7 +28,7 @@ ...@@ -27,7 +28,7 @@
}, },
initialize: function(options) { initialize: function(options) {
this.template = HtmlUtils.template(BookmarksListTemplate); this.template = HtmlUtils.template(bookmarksListTemplate);
this.loadingMessageView = options.loadingMessageView; this.loadingMessageView = options.loadingMessageView;
this.errorMessageView = options.errorMessageView; this.errorMessageView = options.errorMessageView;
this.langCode = $(this.el).data('langCode'); this.langCode = $(this.el).data('langCode');
...@@ -65,16 +66,16 @@ ...@@ -65,16 +66,16 @@
}, },
visitBookmark: function(event) { visitBookmark: function(event) {
var bookmarkedComponent = $(event.currentTarget); var $bookmarkedComponent = $(event.currentTarget),
var bookmark_id = bookmarkedComponent.data('bookmarkId'); bookmarkId = $bookmarkedComponent.data('bookmarkId'),
var component_usage_id = bookmarkedComponent.data('usageId'); componentUsageId = $bookmarkedComponent.data('usageId'),
var component_type = bookmarkedComponent.data('componentType'); componentType = $bookmarkedComponent.data('componentType');
Logger.log( Logger.log(
'edx.bookmark.accessed', 'edx.bookmark.accessed',
{ {
bookmark_id: bookmark_id, bookmark_id: bookmarkId,
component_type: component_type, component_type: componentType,
component_usage_id: component_usage_id component_usage_id: componentUsageId
} }
).always(function() { ).always(function() {
window.location.href = event.currentTarget.pathname; window.location.href = event.currentTarget.pathname;
...@@ -82,7 +83,10 @@ ...@@ -82,7 +83,10 @@
}, },
/** /**
* Convert ISO 8601 formatted date into human friendly format. e.g, `2014-05-23T14:00:00Z` to `May 23, 2014` * 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. * @param {String} isoDate - ISO 8601 formatted date string.
*/ */
humanFriendlyDate: function(isoDate) { humanFriendlyDate: function(isoDate) {
...@@ -90,22 +94,11 @@ ...@@ -90,22 +94,11 @@
return moment(isoDate).format('LL'); 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() { showBookmarksContainer: function() {
$(this.coursewareContentEl).hide(); $(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.html('');
this.$el.show(); this.$el.show();
$(this.coursewareResultsWrapperEl).css('display', 'table-cell');
}, },
showLoadingMessage: function() { showLoadingMessage: function() {
......
<div id="my-bookmarks" class="sr-is-focusable" tabindex="-1"></div> <div id="my-bookmarks" class="sr-is-focusable" tabindex="-1"></div>
<h2 class="bookmarks-results-header"><%= gettext("My Bookmarks") %></h2>
<% if (bookmarksCollection.length) { %> <% if (bookmarksCollection.length) { %>
...@@ -7,15 +6,27 @@ ...@@ -7,15 +6,27 @@
<div class='bookmarks-results-list'> <div class='bookmarks-results-list'>
<% bookmarksCollection.each(function(bookmark, index) { %> <% 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-content">
<div class="list-item-left-section"> <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> <h3 id="bookmark-link-<%- index %>" class="list-item-breadcrumbtrail">
<p id="bookmark-date-<%= index %>" class="list-item-date"> <%= gettext("Bookmarked on") %> <%= humanFriendlyDate(bookmark.get('created')) %> </p> <%=
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> </div>
<p id="bookmark-type-<%= index %>" class="list-item-right-section"> <p id="bookmark-type-<%- index %>" class="list-item-right-section">
<span aria-hidden="true"><%= gettext("View") %></span> <span aria-hidden="true"><%- gettext("View") %></span>
<span class="icon fa fa-arrow-right" aria-hidden="true"></span> <span class="icon fa fa-arrow-right" aria-hidden="true"></span>
</p> </p>
</div> </div>
...@@ -28,14 +39,14 @@ ...@@ -28,14 +39,14 @@
<% } else {%> <% } else {%>
<div class="bookmarks-empty"> <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> <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> <br>
</div> </h3>
<div class="bookmarks-empty-detail"> <div class="bookmarks-empty-detail">
<span class="bookmarks-empty-detail-title"> <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> </span>
</div> </div>
</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 ...@@ -5,13 +5,20 @@ from paver.easy import sh, task, cmdopts, needs, BuildFailure
import json import json
import os import os
import re import re
from string import join
from openedx.core.djangolib.markup import HTML from openedx.core.djangolib.markup import HTML
from .utils.envs import Env from .utils.envs import Env
from .utils.timer import timed from .utils.timer import timed
ALL_SYSTEMS = 'lms,cms,common,openedx,pavelib' ALL_SYSTEMS = [
'cms',
'common',
'lms',
'openedx',
'pavelib',
]
def top_python_dirs(dirname): def top_python_dirs(dirname):
...@@ -45,7 +52,7 @@ def find_fixme(options): ...@@ -45,7 +52,7 @@ def find_fixme(options):
Run pylint on system code, only looking for fixme items. Run pylint on system code, only looking for fixme items.
""" """
num_fixme = 0 num_fixme = 0
systems = getattr(options, 'system', ALL_SYSTEMS).split(',') systems = getattr(options, 'system', '').split(',') or ALL_SYSTEMS
for system in systems: for system in systems:
# Directory to put the pylint report in. # Directory to put the pylint report in.
...@@ -93,7 +100,7 @@ def run_pylint(options): ...@@ -93,7 +100,7 @@ def run_pylint(options):
num_violations = 0 num_violations = 0
violations_limit = int(getattr(options, 'limit', -1)) violations_limit = int(getattr(options, 'limit', -1))
errors = getattr(options, 'errors', False) 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 # Make sure the metrics subdirectory exists
Env.METRICS_DIR.makedirs_p() Env.METRICS_DIR.makedirs_p()
...@@ -234,7 +241,7 @@ def run_complexity(): ...@@ -234,7 +241,7 @@ def run_complexity():
Uses radon to examine cyclomatic complexity. Uses radon to examine cyclomatic complexity.
For additional details on radon, see http://radon.readthedocs.org/ 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_dir = (Env.REPORT_DIR / "complexity")
complexity_report = complexity_report_dir / "python_complexity.log" 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