Merge pull request #14756 from edx/andya/new-bookmarks-page
Andya/new bookmarks page
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/* JavaScript for Vertical Student View. */
|
||||
window.VerticalStudentView = function(runtime, element) {
|
||||
'use strict';
|
||||
RequireJS.require(['js/bookmarks/views/bookmark_button'], function(BookmarkButton) {
|
||||
RequireJS.require(['course_bookmarks/js/views/bookmark_button'], function(BookmarkButton) {
|
||||
var $element = $(element);
|
||||
var $bookmarkButtonElement = $element.find('.bookmark-button');
|
||||
|
||||
@@ -10,7 +10,7 @@ window.VerticalStudentView = function(runtime, element) {
|
||||
bookmarkId: $bookmarkButtonElement.data('bookmarkId'),
|
||||
usageId: $element.data('usageId'),
|
||||
bookmarked: $element.parent('#seq_content').data('bookmarked'),
|
||||
apiUrl: $('.courseware-bookmarks-button').data('bookmarksApiUrl')
|
||||
apiUrl: $bookmarkButtonElement.data('bookmarksApiUrl')
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -268,7 +268,17 @@
|
||||
this.updatePageTitle();
|
||||
sequenceLinks = this.content_container.find('a.seqnav');
|
||||
sequenceLinks.click(this.goto);
|
||||
this.path.text(this.el.find('.nav-item.active').data('path'));
|
||||
|
||||
edx.HtmlUtils.setHtml(
|
||||
this.path,
|
||||
edx.HtmlUtils.template($('#sequence-breadcrumbs-tpl').text())({
|
||||
courseId: this.el.parent().data('course-id'),
|
||||
blockId: this.id,
|
||||
pathText: this.el.find('.nav-item.active').data('path'),
|
||||
unifiedCourseView: this.path.data('unified-course-view')
|
||||
})
|
||||
);
|
||||
|
||||
this.sr_container.focus();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<% if (unifiedCourseView) { %>
|
||||
<a href="<%- '/courses/' + courseId + '/course/#' + blockId %>">
|
||||
<span class="fa fa-arrow-circle-left" aria-hidden="true" aria-describedby="outline-description"></span>
|
||||
<span class="sr-only" id="outline-description"><%- gettext('Return to course outline') %></span>
|
||||
<b><%- gettext('Outline') %></b>
|
||||
</a>
|
||||
<span> > </span>
|
||||
<% } %>
|
||||
<span class="position"><%- pathText %></span>
|
||||
@@ -227,9 +227,9 @@ class CourseFixture(XBlockContainerFixture):
|
||||
self._configure_course()
|
||||
|
||||
@property
|
||||
def course_outline(self):
|
||||
def studio_course_outline_as_json(self):
|
||||
"""
|
||||
Retrieves course outline in JSON format.
|
||||
Retrieves Studio course outline in JSON format.
|
||||
"""
|
||||
url = STUDIO_BASE_URL + '/course/' + self._course_key + "?format=json"
|
||||
response = self.session.get(url, headers=self.headers)
|
||||
|
||||
@@ -10,29 +10,23 @@ class BookmarksPage(CoursePage, PaginatedUIMixin):
|
||||
"""
|
||||
Courseware Bookmarks Page.
|
||||
"""
|
||||
url = None
|
||||
url_path = "courseware/"
|
||||
url_path = "bookmarks"
|
||||
BOOKMARKS_BUTTON_SELECTOR = '.bookmarks-list-button'
|
||||
BOOKMARKS_ELEMENT_SELECTOR = '#my-bookmarks'
|
||||
BOOKMARKED_ITEMS_SELECTOR = '.bookmarks-results-list .bookmarks-results-list-item'
|
||||
BOOKMARKED_BREADCRUMBS = BOOKMARKED_ITEMS_SELECTOR + ' .list-item-breadcrumbtrail'
|
||||
|
||||
def is_browser_on_page(self):
|
||||
""" Verify if we are on correct page """
|
||||
return self.q(css=self.BOOKMARKS_BUTTON_SELECTOR).visible
|
||||
return self.q(css=self.BOOKMARKS_ELEMENT_SELECTOR).present
|
||||
|
||||
def bookmarks_button_visible(self):
|
||||
""" Check if bookmarks button is visible """
|
||||
return self.q(css=self.BOOKMARKS_BUTTON_SELECTOR).visible
|
||||
|
||||
def click_bookmarks_button(self, wait_for_results=True):
|
||||
""" Click on Bookmarks button """
|
||||
self.q(css=self.BOOKMARKS_BUTTON_SELECTOR).first.click()
|
||||
if wait_for_results:
|
||||
EmptyPromise(self.results_present, "Bookmarks results present").fulfill()
|
||||
|
||||
def results_present(self):
|
||||
""" Check if bookmarks results are present """
|
||||
return self.q(css='#my-bookmarks').present
|
||||
return self.q(css=self.BOOKMARKS_ELEMENT_SELECTOR).present
|
||||
|
||||
def results_header_text(self):
|
||||
""" Returns the bookmarks results header text """
|
||||
|
||||
155
common/test/acceptance/pages/lms/course_home.py
Normal file
155
common/test/acceptance/pages/lms/course_home.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""
|
||||
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)
|
||||
)
|
||||
@@ -1,212 +0,0 @@
|
||||
"""
|
||||
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 @@
|
||||
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
|
||||
import re
|
||||
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):
|
||||
"""
|
||||
@@ -17,8 +21,12 @@ class CoursewarePage(CoursePage):
|
||||
section_selector = '.chapter'
|
||||
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):
|
||||
return self.q(css='body.courseware').present
|
||||
return self.q(css='.course-content').present
|
||||
|
||||
@property
|
||||
def chapter_count_in_navigation(self):
|
||||
@@ -27,6 +35,7 @@ class CoursewarePage(CoursePage):
|
||||
"""
|
||||
return len(self.q(css='nav.course-navigation a.chapter'))
|
||||
|
||||
# TODO: TNL-6546: Remove and find callers.
|
||||
@property
|
||||
def num_sections(self):
|
||||
"""
|
||||
@@ -34,6 +43,7 @@ class CoursewarePage(CoursePage):
|
||||
"""
|
||||
return len(self.q(css=self.section_selector))
|
||||
|
||||
# TODO: TNL-6546: Remove and find callers.
|
||||
@property
|
||||
def num_subsections(self):
|
||||
"""
|
||||
@@ -274,7 +284,7 @@ class CoursewarePage(CoursePage):
|
||||
@property
|
||||
def breadcrumb(self):
|
||||
""" 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):
|
||||
""" Check if unit title is visible """
|
||||
@@ -301,6 +311,13 @@ class CoursewarePage(CoursePage):
|
||||
self.q(css='.bookmark-button').first.click()
|
||||
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):
|
||||
"""
|
||||
@@ -319,3 +336,255 @@ class CoursewareSequentialTabPage(CoursePage):
|
||||
return the body of the sequential currently selected
|
||||
"""
|
||||
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
|
||||
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.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.course_nav import CourseNavPage
|
||||
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.common.logout import LogoutPage
|
||||
from common.test.acceptance.pages.common import BASE_URL
|
||||
|
||||
@@ -25,6 +25,40 @@ class BookmarksTestMixin(EventsTestMixin, UniqueCourseTest):
|
||||
USERNAME = "STUDENT"
|
||||
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):
|
||||
"""
|
||||
Create course fixture
|
||||
@@ -59,50 +93,6 @@ class BookmarksTestMixin(EventsTestMixin, UniqueCourseTest):
|
||||
actual_events = self.wait_for_events(event_filter={'event_type': event_type}, number_of_matches=1)
|
||||
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):
|
||||
"""
|
||||
Bookmark a unit
|
||||
@@ -124,7 +114,7 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
)
|
||||
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
|
||||
|
||||
@@ -135,6 +125,19 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
for index in range(num_units):
|
||||
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):
|
||||
"""
|
||||
Creates breadcrumbs for the first `num_units`
|
||||
@@ -166,10 +169,10 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
).visit()
|
||||
|
||||
# Visit course outline page in studio.
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.wait_for_page()
|
||||
self.studio_course_outline_page.visit()
|
||||
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.
|
||||
LogoutPage(self.browser).visit()
|
||||
@@ -187,7 +190,7 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
self.courseware_page.click_bookmark_unit_button()
|
||||
self.assertEqual(self.courseware_page.bookmark_icon_visible, bookmark_icon_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)
|
||||
|
||||
def _verify_pagination_info(
|
||||
@@ -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_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):
|
||||
"""
|
||||
Verifies the breadcrumb trail.
|
||||
@@ -232,11 +227,11 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
"""
|
||||
Update and publish the block/unit display name.
|
||||
"""
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.wait_for_page()
|
||||
self.studio_course_outline_page.visit()
|
||||
self.studio_course_outline_page.wait_for_page()
|
||||
|
||||
self.course_outline_page.expand_all_subsections()
|
||||
section = self.course_outline_page.section_at(0)
|
||||
self.studio_course_outline_page.expand_all_subsections()
|
||||
section = self.studio_course_outline_page.section_at(0)
|
||||
container_page = section.subsection_at(0).unit_at(0).go_to()
|
||||
|
||||
self.course_fixture._update_xblock(container_page.locator, { # pylint: disable=protected-access
|
||||
@@ -265,34 +260,41 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
Then I click again on the bookmark button
|
||||
And I should see a unit un-bookmarked
|
||||
"""
|
||||
self._test_setup()
|
||||
self.setup_test()
|
||||
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.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)
|
||||
|
||||
# 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):
|
||||
"""
|
||||
Scenario: An empty bookmarks list is shown if there are no bookmarked units.
|
||||
|
||||
Given that I am a registered user
|
||||
And I visit my courseware page
|
||||
And I can see the Bookmarks button
|
||||
When I click on Bookmarks button
|
||||
And I visit my bookmarks page
|
||||
Then I should see an empty bookmarks list
|
||||
And empty bookmarks list content is correct
|
||||
"""
|
||||
self._test_setup()
|
||||
self.assertTrue(self.bookmarks_page.bookmarks_button_visible())
|
||||
self.bookmarks_page.click_bookmarks_button()
|
||||
self.assertEqual(self.bookmarks_page.results_header_text(), 'My Bookmarks')
|
||||
self.assertEqual(self.bookmarks_page.empty_header_text(), 'You have not bookmarked any courseware pages yet.')
|
||||
|
||||
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.setup_test()
|
||||
self.bookmarks_page.visit()
|
||||
empty_list_text = (
|
||||
'Use bookmarks to help you easily return to courseware pages. '
|
||||
'To bookmark a page, click "Bookmark this page" under the page title.')
|
||||
self.assertEqual(self.bookmarks_page.empty_list_text(), empty_list_text)
|
||||
|
||||
def test_bookmarks_list(self):
|
||||
@@ -300,18 +302,16 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
Scenario: A bookmarks list is shown if there are bookmarked units.
|
||||
|
||||
Given that I am a registered user
|
||||
And I visit my courseware page
|
||||
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
|
||||
And breadcrumb trail is correct for a bookmark
|
||||
When I click on bookmarked link
|
||||
Then I can navigate to correct bookmarked unit
|
||||
"""
|
||||
self._test_setup()
|
||||
self._bookmark_units(2)
|
||||
|
||||
self._navigate_to_bookmarks_list()
|
||||
self.setup_test()
|
||||
self.bookmark_units(2)
|
||||
self.bookmarks_page.visit()
|
||||
self._verify_breadcrumbs(num_units=2)
|
||||
|
||||
self._verify_pagination_info(
|
||||
@@ -328,11 +328,10 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
xblock_usage_ids = [xblock.locator for xblock in xblocks]
|
||||
# Verify link navigation
|
||||
for index in range(2):
|
||||
self.bookmarks_page.visit()
|
||||
self.bookmarks_page.click_bookmarked_block(index)
|
||||
self.courseware_page.wait_for_page()
|
||||
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):
|
||||
"""
|
||||
@@ -344,16 +343,14 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
Then I visit unit page in studio
|
||||
Then I change unit display_name
|
||||
And I publish the changes
|
||||
Then I visit my courseware page
|
||||
And I visit bookmarks list page
|
||||
Then I visit my bookmarks page
|
||||
When I see the bookmark
|
||||
Then I can see the breadcrumb trail
|
||||
with updated display_name.
|
||||
Then I can see the breadcrumb trail has the updated display_name.
|
||||
"""
|
||||
self._test_setup(num_chapters=1)
|
||||
self._bookmark_units(num_units=1)
|
||||
self.setup_test(num_chapters=1)
|
||||
self.bookmark_units(num_units=1)
|
||||
|
||||
self._navigate_to_bookmarks_list()
|
||||
self.bookmarks_page.visit()
|
||||
self._verify_breadcrumbs(num_units=1)
|
||||
|
||||
LogoutPage(self.browser).visit()
|
||||
@@ -370,9 +367,8 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
|
||||
LogoutPage(self.browser).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)
|
||||
|
||||
def test_unreachable_bookmark(self):
|
||||
@@ -380,19 +376,18 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
Scenario: We should get a HTTP 404 for an unreachable bookmark.
|
||||
|
||||
Given that I am a registered user
|
||||
And I visit my courseware page
|
||||
And I have bookmarked 2 units
|
||||
Then I delete a bookmarked unit
|
||||
Then I click on Bookmarks button
|
||||
And I should see a bookmarked list
|
||||
When I click on deleted bookmark
|
||||
And I delete a bookmarked unit
|
||||
And I visit my bookmarks page
|
||||
Then I should see a bookmarked list
|
||||
When I click on the deleted bookmark
|
||||
Then I should navigated to 404 page
|
||||
"""
|
||||
self._test_setup(num_chapters=1)
|
||||
self._bookmark_units(1)
|
||||
self.setup_test(num_chapters=1)
|
||||
self.bookmark_units(1)
|
||||
self._delete_section(0)
|
||||
|
||||
self._navigate_to_bookmarks_list()
|
||||
self.bookmarks_page.visit()
|
||||
|
||||
self._verify_pagination_info(
|
||||
bookmark_count_on_current_page=1,
|
||||
@@ -411,15 +406,14 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
Scenario: We can't get bookmarks more than default page size.
|
||||
|
||||
Given that I am a registered user
|
||||
And I visit my courseware page
|
||||
And I have bookmarked all the 11 units available
|
||||
Then I click on Bookmarks button
|
||||
And I should see a bookmarked list
|
||||
And bookmark list contains 10 bookmarked items
|
||||
And I visit my bookmarks page
|
||||
Then I should see a bookmarked list
|
||||
And the bookmark list should contain 10 bookmarked items
|
||||
"""
|
||||
self._test_setup(11)
|
||||
self._bookmark_units(11)
|
||||
self._navigate_to_bookmarks_list()
|
||||
self.setup_test(11)
|
||||
self.bookmark_units(11)
|
||||
self.bookmarks_page.visit()
|
||||
|
||||
self._verify_pagination_info(
|
||||
bookmark_count_on_current_page=10,
|
||||
@@ -434,17 +428,15 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
"""
|
||||
Scenario: Bookmarks list pagination is working as expected for single page
|
||||
Given that I am a registered user
|
||||
And I visit my courseware page
|
||||
And I have bookmarked all the 2 units available
|
||||
Then I click on Bookmarks button
|
||||
And I should see a bookmarked list with 2 bookmarked items
|
||||
And I visit my bookmarks page
|
||||
Then I should see a bookmarked list with 2 bookmarked items
|
||||
And I should see paging header and footer with correct data
|
||||
And previous and next buttons are disabled
|
||||
"""
|
||||
self._test_setup(num_chapters=2)
|
||||
self._bookmark_units(num_units=2)
|
||||
|
||||
self.bookmarks_page.click_bookmarks_button()
|
||||
self.setup_test(num_chapters=2)
|
||||
self.bookmark_units(num_units=2)
|
||||
self.bookmarks_page.visit()
|
||||
self.assertTrue(self.bookmarks_page.results_present())
|
||||
self._verify_pagination_info(
|
||||
bookmark_count_on_current_page=2,
|
||||
@@ -460,11 +452,10 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
Scenario: Next button is working as expected for bookmarks list pagination
|
||||
|
||||
Given that I am a registered user
|
||||
And I visit my courseware page
|
||||
And I have bookmarked all the 12 units available
|
||||
And I visit my bookmarks page
|
||||
|
||||
Then I click on Bookmarks button
|
||||
And I should see a bookmarked list of 10 items
|
||||
Then I should see a bookmarked list of 10 items
|
||||
And I should see paging header and footer with correct info
|
||||
|
||||
Then I click on next page button in footer
|
||||
@@ -472,10 +463,10 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
And I should see a bookmarked list with 2 items
|
||||
And I should see paging header and footer with correct info
|
||||
"""
|
||||
self._test_setup(num_chapters=12)
|
||||
self._bookmark_units(num_units=12)
|
||||
self.setup_test(num_chapters=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._verify_pagination_info(
|
||||
@@ -502,9 +493,8 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
Scenario: Previous button is working as expected for bookmarks list pagination
|
||||
|
||||
Given that I am a registered user
|
||||
And I visit my courseware page
|
||||
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
|
||||
And I should be navigated to second page
|
||||
@@ -515,10 +505,10 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
And I should be navigated to first page
|
||||
And I should see paging header and footer with correct info
|
||||
"""
|
||||
self._test_setup(num_chapters=12)
|
||||
self._bookmark_units(num_units=12)
|
||||
self.setup_test(num_chapters=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.bookmarks_page.press_next_page_button()
|
||||
@@ -546,19 +536,17 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
Scenario: Bookmarks list pagination works as expected for valid page number
|
||||
|
||||
Given that I am a registered user
|
||||
And I visit my courseware page
|
||||
And I have bookmarked all the 12 units available
|
||||
|
||||
Then I click on Bookmarks button
|
||||
And I should see a bookmarked list
|
||||
And I visit my bookmarks page
|
||||
Then I should see a bookmarked list
|
||||
And I should see total page value is 2
|
||||
Then I enter 2 in the page number input
|
||||
And I should be navigated to page 2
|
||||
"""
|
||||
self._test_setup(num_chapters=11)
|
||||
self._bookmark_units(num_units=11)
|
||||
self.setup_test(num_chapters=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.assertEqual(self.bookmarks_page.get_total_pages, 2)
|
||||
|
||||
@@ -577,18 +565,17 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
Scenario: Bookmarks list pagination works as expected for invalid page number
|
||||
|
||||
Given that I am a registered user
|
||||
And I visit my courseware page
|
||||
And I have bookmarked all the 11 units available
|
||||
Then I click on Bookmarks button
|
||||
And I should see a bookmarked list
|
||||
And I visit my bookmarks page
|
||||
Then I should see a bookmarked list
|
||||
And I should see total page value is 2
|
||||
Then I enter 3 in the page number input
|
||||
And I should stay at page 1
|
||||
"""
|
||||
self._test_setup(num_chapters=11)
|
||||
self._bookmark_units(num_units=11)
|
||||
self.setup_test(num_chapters=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.assertEqual(self.bookmarks_page.get_total_pages, 2)
|
||||
|
||||
@@ -612,7 +599,7 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
When I click on bookmarked unit
|
||||
Then `edx.course.bookmark.accessed` event is emitted
|
||||
"""
|
||||
self._test_setup(num_chapters=1)
|
||||
self.setup_test(num_chapters=1)
|
||||
self.reset_event_tracking()
|
||||
|
||||
# create expected event data
|
||||
@@ -626,8 +613,8 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
}
|
||||
}
|
||||
]
|
||||
self._bookmark_units(num_units=1)
|
||||
self.bookmarks_page.click_bookmarks_button()
|
||||
self.bookmark_units(num_units=1)
|
||||
self.bookmarks_page.visit()
|
||||
|
||||
self._verify_pagination_info(
|
||||
bookmark_count_on_current_page=1,
|
||||
@@ -640,3 +627,18 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
|
||||
self.bookmarks_page.click_bookmarked_block(0)
|
||||
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
|
||||
from common.test.acceptance.fixtures.certificates import CertificateConfigFixture
|
||||
from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage
|
||||
from common.test.acceptance.pages.lms.certificate_page import CertificatePage
|
||||
from common.test.acceptance.pages.lms.course_home import CourseHomePage
|
||||
from common.test.acceptance.pages.lms.course_info import CourseInfoPage
|
||||
from common.test.acceptance.pages.lms.tab_nav import TabNavPage
|
||||
from common.test.acceptance.pages.lms.course_nav import CourseNavPage
|
||||
from common.test.acceptance.pages.lms.courseware import CoursewarePage
|
||||
from common.test.acceptance.pages.lms.progress import ProgressPage
|
||||
from common.test.acceptance.pages.lms.tab_nav import TabNavPage
|
||||
|
||||
|
||||
@attr(shard=5)
|
||||
@@ -154,7 +155,8 @@ class CertificateProgressPageTest(UniqueCourseTest):
|
||||
|
||||
self.course_info_page = CourseInfoPage(self.browser, self.course_id)
|
||||
self.progress_page = ProgressPage(self.browser, self.course_id)
|
||||
self.course_nav = CourseNavPage(self.browser)
|
||||
self.courseware_page = CoursewarePage(self.browser, self.course_id)
|
||||
self.course_home_page = CourseHomePage(self.browser, self.course_id)
|
||||
self.tab_nav = TabNavPage(self.browser)
|
||||
|
||||
def log_in_as_unique_user(self):
|
||||
@@ -205,38 +207,42 @@ class CertificateProgressPageTest(UniqueCourseTest):
|
||||
|
||||
Problems were added in the setUp
|
||||
"""
|
||||
self.course_info_page.visit()
|
||||
self.tab_nav.go_to_tab('Course')
|
||||
# self.course_info_page.visit()
|
||||
# self.tab_nav.go_to_tab('Course')
|
||||
#
|
||||
# # TODO: TNL-6546: Remove extra visit call.
|
||||
self.course_home_page.visit()
|
||||
|
||||
# Navigate to Test Subsection in Test Section Section
|
||||
self.course_nav.go_to_section('Test Section', 'Test Subsection')
|
||||
self.course_home_page.outline.go_to_section('Test Section', 'Test Subsection')
|
||||
|
||||
# Navigate to Test Problem 1
|
||||
self.course_nav.go_to_vertical('Test Problem 1')
|
||||
self.courseware_page.nav.go_to_vertical('Test Problem 1')
|
||||
|
||||
# Select correct value for from select menu
|
||||
self.course_nav.q(css='select option[value="{}"]'.format('blue')).first.click()
|
||||
self.courseware_page.q(css='select option[value="{}"]'.format('blue')).first.click()
|
||||
|
||||
# Select correct radio button for the answer
|
||||
self.course_nav.q(css='fieldset div.field:nth-child(4) input').nth(0).click()
|
||||
self.courseware_page.q(css='fieldset div.field:nth-child(4) input').nth(0).click()
|
||||
|
||||
# Select correct radio buttons for the answer
|
||||
self.course_nav.q(css='fieldset div.field:nth-child(2) input').nth(1).click()
|
||||
self.course_nav.q(css='fieldset div.field:nth-child(4) input').nth(1).click()
|
||||
self.courseware_page.q(css='fieldset div.field:nth-child(2) input').nth(1).click()
|
||||
self.courseware_page.q(css='fieldset div.field:nth-child(4) input').nth(1).click()
|
||||
|
||||
# Submit the answer
|
||||
self.course_nav.q(css='button.submit').click()
|
||||
self.course_nav.wait_for_ajax()
|
||||
self.courseware_page.q(css='button.submit').click()
|
||||
self.courseware_page.wait_for_ajax()
|
||||
|
||||
# Navigate to the 'Test Subsection 2' of 'Test Section 2'
|
||||
self.course_nav.go_to_section('Test Section 2', 'Test Subsection 2')
|
||||
self.course_home_page.visit()
|
||||
self.course_home_page.outline.go_to_section('Test Section 2', 'Test Subsection 2')
|
||||
|
||||
# Navigate to Test Problem 2
|
||||
self.course_nav.go_to_vertical('Test Problem 2')
|
||||
self.courseware_page.nav.go_to_vertical('Test Problem 2')
|
||||
|
||||
# Fill in the answer of the problem
|
||||
self.course_nav.q(css='input[id^=input_][id$=_2_1]').fill('A*x^2 + sqrt(y)')
|
||||
self.courseware_page.q(css='input[id^=input_][id$=_2_1]').fill('A*x^2 + sqrt(y)')
|
||||
|
||||
# Submit the answer
|
||||
self.course_nav.q(css='button.submit').click()
|
||||
self.course_nav.wait_for_ajax()
|
||||
self.courseware_page.q(css='button.submit').click()
|
||||
self.courseware_page.wait_for_ajax()
|
||||
|
||||
@@ -8,7 +8,7 @@ import textwrap
|
||||
from nose.plugins.attrib import attr
|
||||
from common.test.acceptance.tests.helpers import UniqueCourseTest, TestWithSearchIndexMixin
|
||||
from common.test.acceptance.pages.studio.auto_auth import AutoAuthPage
|
||||
from common.test.acceptance.pages.studio.overview import CourseOutlinePage
|
||||
from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
|
||||
from common.test.acceptance.pages.studio.library import StudioLibraryContentEditor, StudioLibraryContainerXBlockWrapper
|
||||
from common.test.acceptance.pages.lms.courseware import CoursewarePage
|
||||
from common.test.acceptance.pages.lms.library import LibraryContentXBlockWrapper
|
||||
@@ -44,7 +44,7 @@ class LibraryContentTestBase(UniqueCourseTest):
|
||||
|
||||
self.courseware_page = CoursewarePage(self.browser, self.course_id)
|
||||
|
||||
self.course_outline = CourseOutlinePage(
|
||||
self.studio_course_outline = StudioCourseOutlinePage(
|
||||
self.browser,
|
||||
self.course_info['org'],
|
||||
self.course_info['number'],
|
||||
@@ -116,9 +116,9 @@ class LibraryContentTestBase(UniqueCourseTest):
|
||||
if change_login:
|
||||
LogoutPage(self.browser).visit()
|
||||
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
|
||||
self.course_outline.visit()
|
||||
self.studio_course_outline.visit()
|
||||
|
||||
subsection = self.course_outline.section(SECTION_NAME).subsection(SUBSECTION_NAME)
|
||||
subsection = self.studio_course_outline.section(SECTION_NAME).subsection(SUBSECTION_NAME)
|
||||
return subsection.expand_subsection().unit(UNIT_NAME).go_to()
|
||||
|
||||
def _goto_library_block_page(self, block_id=None):
|
||||
|
||||
@@ -21,26 +21,27 @@ from common.test.acceptance.tests.helpers import (
|
||||
select_option_by_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.account_settings import AccountSettingsPage
|
||||
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.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.tab_nav import TabNavPage
|
||||
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.dashboard import DashboardPage
|
||||
from common.test.acceptance.pages.lms.problem import ProblemPage
|
||||
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.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.pages.lms.courseware import CoursewarePage
|
||||
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.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.studio.settings import SettingsPage
|
||||
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc
|
||||
|
||||
|
||||
@@ -634,7 +635,6 @@ class CourseWikiTest(UniqueCourseTest):
|
||||
children_page.a11y_audit.check_for_accessibility_errors()
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
class HighLevelTabTest(UniqueCourseTest):
|
||||
"""
|
||||
Tests that verify each of the high-level tabs available within a course.
|
||||
@@ -651,7 +651,8 @@ class HighLevelTabTest(UniqueCourseTest):
|
||||
|
||||
self.course_info_page = CourseInfoPage(self.browser, self.course_id)
|
||||
self.progress_page = ProgressPage(self.browser, self.course_id)
|
||||
self.course_nav = CourseNavPage(self.browser)
|
||||
self.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.video = VideoPage(self.browser)
|
||||
|
||||
@@ -678,13 +679,16 @@ class HighLevelTabTest(UniqueCourseTest):
|
||||
),
|
||||
XBlockFixtureDesc('chapter', 'Test Section 2').add_children(
|
||||
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()
|
||||
|
||||
# Auto-auth register for the course
|
||||
AutoAuthPage(self.browser, course_id=self.course_id).visit()
|
||||
|
||||
@attr(shard=1)
|
||||
def test_course_info(self):
|
||||
"""
|
||||
Navigate to the course info page.
|
||||
@@ -702,6 +706,7 @@ class HighLevelTabTest(UniqueCourseTest):
|
||||
self.assertEqual(len(handout_links), 1)
|
||||
self.assertIn('demoPDF.pdf', handout_links[0])
|
||||
|
||||
@attr(shard=1)
|
||||
def test_progress(self):
|
||||
"""
|
||||
Navigate to the progress page.
|
||||
@@ -719,6 +724,7 @@ class HighLevelTabTest(UniqueCourseTest):
|
||||
actual_scores = self.progress_page.scores(CHAPTER, SECTION)
|
||||
self.assertEqual(actual_scores, EXPECTED_SCORES)
|
||||
|
||||
@attr(shard=1)
|
||||
def test_static_tab(self):
|
||||
"""
|
||||
Navigate to a static tab (course content)
|
||||
@@ -728,6 +734,7 @@ class HighLevelTabTest(UniqueCourseTest):
|
||||
self.tab_nav.go_to_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):
|
||||
"""
|
||||
Navigate to a static tab (course content)
|
||||
@@ -740,6 +747,7 @@ class HighLevelTabTest(UniqueCourseTest):
|
||||
# Verify that Mathjax has rendered
|
||||
self.tab_nav.mathjax_has_rendered()
|
||||
|
||||
@attr(shard=1)
|
||||
def test_wiki_tab_first_time(self):
|
||||
"""
|
||||
Navigate to the course wiki tab. When the wiki is accessed for
|
||||
@@ -760,6 +768,8 @@ class HighLevelTabTest(UniqueCourseTest):
|
||||
)
|
||||
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):
|
||||
"""
|
||||
Navigate to a particular unit in the course.
|
||||
@@ -774,26 +784,86 @@ class HighLevelTabTest(UniqueCourseTest):
|
||||
'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():
|
||||
self.assertIn(section, actual_sections)
|
||||
self.assertEqual(actual_sections[section], EXPECTED_SECTIONS[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
|
||||
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.course_nav.go_to_section('Test Section 2', 'Test Subsection 3')
|
||||
self.assertTrue(self.course_nav.is_on_section('Test Section 2', 'Test Subsection 3'))
|
||||
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))
|
||||
for expected in EXPECTED_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.
|
||||
self.course_home_page.outline.go_to_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)
|
||||
@@ -878,7 +948,6 @@ class VisibleToStaffOnlyTest(UniqueCourseTest):
|
||||
).install()
|
||||
|
||||
self.courseware_page = CoursewarePage(self.browser, self.course_id)
|
||||
self.course_nav = CourseNavPage(self.browser)
|
||||
|
||||
def test_visible_to_staff(self):
|
||||
"""
|
||||
@@ -891,16 +960,16 @@ class VisibleToStaffOnlyTest(UniqueCourseTest):
|
||||
course_id=self.course_id, staff=True).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.assertEqual([u'Locked Unit', u'Unlocked Unit'], self.course_nav.sequence_items)
|
||||
self.courseware_page.nav.go_to_section("Test Section", "Subsection With Locked Unit")
|
||||
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.assertEqual([u'Test Unit'], self.course_nav.sequence_items)
|
||||
self.courseware_page.nav.go_to_section("Test Section", "Unlocked Subsection")
|
||||
self.assertEqual([u'Test Unit'], self.courseware_page.nav.sequence_items)
|
||||
|
||||
self.course_nav.go_to_section("Test Section", "Locked Subsection")
|
||||
self.assertEqual([u'Test Unit'], self.course_nav.sequence_items)
|
||||
self.courseware_page.nav.go_to_section("Test Section", "Locked Subsection")
|
||||
self.assertEqual([u'Test Unit'], self.courseware_page.nav.sequence_items)
|
||||
|
||||
def test_visible_to_student(self):
|
||||
"""
|
||||
@@ -913,13 +982,13 @@ class VisibleToStaffOnlyTest(UniqueCourseTest):
|
||||
course_id=self.course_id, staff=False).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.assertEqual([u'Unlocked Unit'], self.course_nav.sequence_items)
|
||||
self.courseware_page.nav.go_to_section("Test Section", "Subsection With Locked Unit")
|
||||
self.assertEqual([u'Unlocked Unit'], self.courseware_page.nav.sequence_items)
|
||||
|
||||
self.course_nav.go_to_section("Test Section", "Unlocked Subsection")
|
||||
self.assertEqual([u'Test Unit'], self.course_nav.sequence_items)
|
||||
self.courseware_page.nav.go_to_section("Test Section", "Unlocked Subsection")
|
||||
self.assertEqual([u'Test Unit'], self.courseware_page.nav.sequence_items)
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
@@ -1065,7 +1134,7 @@ class ProblemExecutionTest(UniqueCourseTest):
|
||||
super(ProblemExecutionTest, self).setUp()
|
||||
|
||||
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)
|
||||
|
||||
# Install a course with sections and problems.
|
||||
@@ -1112,7 +1181,7 @@ class ProblemExecutionTest(UniqueCourseTest):
|
||||
# Navigate to the problem page
|
||||
self.course_info_page.visit()
|
||||
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)
|
||||
self.assertEqual(problem_page.problem_name.upper(), 'PYTHON PROBLEM')
|
||||
@@ -1391,6 +1460,6 @@ class CourseInfoA11yTest(UniqueCourseTest):
|
||||
self.course_info_page = CourseInfoPage(self.browser, self.course_id)
|
||||
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.a11y_audit.check_for_accessibility_errors()
|
||||
|
||||
@@ -7,7 +7,7 @@ import uuid
|
||||
|
||||
from common.test.acceptance.tests.helpers import remove_file
|
||||
from common.test.acceptance.pages.common.logout import LogoutPage
|
||||
from common.test.acceptance.pages.studio.overview import CourseOutlinePage
|
||||
from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
|
||||
from common.test.acceptance.pages.lms.courseware_search import CoursewareSearchPage
|
||||
from common.test.acceptance.pages.lms.staff_view import StaffPage
|
||||
from common.test.acceptance.fixtures.course import XBlockFixtureDesc
|
||||
@@ -45,7 +45,7 @@ class CoursewareSearchCohortTest(ContainerBase):
|
||||
super(CoursewareSearchCohortTest, self).setUp(is_staff=is_staff)
|
||||
self.staff_user = self.user
|
||||
|
||||
self.course_outline = CourseOutlinePage(
|
||||
self.studio_course_outline = StudioCourseOutlinePage(
|
||||
self.browser,
|
||||
self.course_info['org'],
|
||||
self.course_info['number'],
|
||||
@@ -101,9 +101,9 @@ class CoursewareSearchCohortTest(ContainerBase):
|
||||
Reindex course content on studio course page
|
||||
"""
|
||||
self._auto_auth(self.staff_user["username"], self.staff_user["email"], True)
|
||||
self.course_outline.visit()
|
||||
self.course_outline.start_reindex()
|
||||
self.course_outline.wait_for_ajax()
|
||||
self.studio_course_outline.visit()
|
||||
self.studio_course_outline.start_reindex()
|
||||
self.studio_course_outline.wait_for_ajax()
|
||||
|
||||
def _goto_staff_page(self):
|
||||
"""
|
||||
|
||||
@@ -5,6 +5,7 @@ End-to-end tests for the LMS.
|
||||
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from unittest import skip
|
||||
|
||||
import ddt
|
||||
from flaky import flaky
|
||||
@@ -13,7 +14,7 @@ from nose.plugins.attrib import attr
|
||||
from ..helpers import UniqueCourseTest, EventsTestMixin, auto_auth, create_multiple_choice_problem
|
||||
from ...fixtures.course import CourseFixture, XBlockFixtureDesc
|
||||
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.create_mode import ModeCreationPage
|
||||
from ...pages.lms.dashboard import DashboardPage
|
||||
@@ -23,7 +24,7 @@ from ...pages.lms.progress import ProgressPage
|
||||
from ...pages.lms.staff_view import StaffPage
|
||||
from ...pages.lms.track_selection import TrackSelectionPage
|
||||
from ...pages.studio.auto_auth import AutoAuthPage
|
||||
from ...pages.studio.overview import CourseOutlinePage
|
||||
from ...pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
|
||||
|
||||
|
||||
@attr(shard=9)
|
||||
@@ -38,9 +39,9 @@ class CoursewareTest(UniqueCourseTest):
|
||||
super(CoursewareTest, self).setUp()
|
||||
|
||||
self.courseware_page = CoursewarePage(self.browser, self.course_id)
|
||||
self.course_nav = CourseNavPage(self.browser)
|
||||
self.course_home_page = CourseHomePage(self.browser, self.course_id)
|
||||
|
||||
self.course_outline = CourseOutlinePage(
|
||||
self.studio_course_outline = StudioCourseOutlinePage(
|
||||
self.browser,
|
||||
self.course_info['org'],
|
||||
self.course_info['number'],
|
||||
@@ -94,10 +95,10 @@ class CoursewareTest(UniqueCourseTest):
|
||||
auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id)
|
||||
|
||||
# Visit course outline page in studio.
|
||||
self.course_outline.visit()
|
||||
self.studio_course_outline.visit()
|
||||
|
||||
# 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.
|
||||
LogoutPage(self.browser).visit()
|
||||
@@ -116,11 +117,10 @@ class CoursewareTest(UniqueCourseTest):
|
||||
And I visit my courseware page
|
||||
Then I should see correct course tree breadcrumb
|
||||
"""
|
||||
self.courseware_page.visit()
|
||||
|
||||
xblocks = self.course_fix.get_nested_xblocks(category="problem")
|
||||
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
|
||||
expected_breadcrumb = self._create_breadcrumb(index) # pylint: disable=no-member
|
||||
self.assertEqual(courseware_page_breadcrumb, expected_breadcrumb)
|
||||
@@ -140,7 +140,7 @@ class ProctoredExamTest(UniqueCourseTest):
|
||||
|
||||
self.courseware_page = CoursewarePage(self.browser, self.course_id)
|
||||
|
||||
self.course_outline = CourseOutlinePage(
|
||||
self.studio_course_outline = StudioCourseOutlinePage(
|
||||
self.browser,
|
||||
self.course_info['org'],
|
||||
self.course_info['number'],
|
||||
@@ -234,10 +234,10 @@ class ProctoredExamTest(UniqueCourseTest):
|
||||
"""
|
||||
LogoutPage(self.browser).visit()
|
||||
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.assertTrue(self.course_outline.proctoring_items_are_displayed())
|
||||
self.studio_course_outline.open_subsection_settings_dialog()
|
||||
self.assertTrue(self.studio_course_outline.proctoring_items_are_displayed())
|
||||
|
||||
def test_proctored_exam_flow(self):
|
||||
"""
|
||||
@@ -251,11 +251,11 @@ class ProctoredExamTest(UniqueCourseTest):
|
||||
"""
|
||||
LogoutPage(self.browser).visit()
|
||||
auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id)
|
||||
self.course_outline.visit()
|
||||
self.course_outline.open_subsection_settings_dialog()
|
||||
self.studio_course_outline.visit()
|
||||
self.studio_course_outline.open_subsection_settings_dialog()
|
||||
|
||||
self.course_outline.select_advanced_tab()
|
||||
self.course_outline.make_exam_proctored()
|
||||
self.studio_course_outline.select_advanced_tab()
|
||||
self.studio_course_outline.make_exam_proctored()
|
||||
|
||||
LogoutPage(self.browser).visit()
|
||||
self._login_as_a_verified_user()
|
||||
@@ -272,11 +272,11 @@ class ProctoredExamTest(UniqueCourseTest):
|
||||
"""
|
||||
LogoutPage(self.browser).visit()
|
||||
auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id)
|
||||
self.course_outline.visit()
|
||||
self.course_outline.open_subsection_settings_dialog()
|
||||
self.studio_course_outline.visit()
|
||||
self.studio_course_outline.open_subsection_settings_dialog()
|
||||
|
||||
self.course_outline.select_advanced_tab()
|
||||
self.course_outline.make_exam_timed(hide_after_due=hide_after_due)
|
||||
self.studio_course_outline.select_advanced_tab()
|
||||
self.studio_course_outline.make_exam_timed(hide_after_due=hide_after_due)
|
||||
|
||||
LogoutPage(self.browser).visit()
|
||||
self._login_as_a_verified_user()
|
||||
@@ -312,9 +312,9 @@ class ProctoredExamTest(UniqueCourseTest):
|
||||
|
||||
LogoutPage(self.browser).visit()
|
||||
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")
|
||||
self.course_outline.change_problem_due_date(last_week)
|
||||
self.studio_course_outline.change_problem_due_date(last_week)
|
||||
|
||||
LogoutPage(self.browser).visit()
|
||||
auto_auth(self.browser, self.USERNAME, self.EMAIL, False, self.course_id)
|
||||
@@ -355,26 +355,26 @@ class ProctoredExamTest(UniqueCourseTest):
|
||||
"""
|
||||
LogoutPage(self.browser).visit()
|
||||
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.course_outline.select_advanced_tab()
|
||||
self.studio_course_outline.open_subsection_settings_dialog()
|
||||
self.studio_course_outline.select_advanced_tab()
|
||||
|
||||
self.course_outline.select_none_exam()
|
||||
self.assertFalse(self.course_outline.time_allotted_field_visible())
|
||||
self.assertFalse(self.course_outline.exam_review_rules_field_visible())
|
||||
self.studio_course_outline.select_none_exam()
|
||||
self.assertFalse(self.studio_course_outline.time_allotted_field_visible())
|
||||
self.assertFalse(self.studio_course_outline.exam_review_rules_field_visible())
|
||||
|
||||
self.course_outline.select_timed_exam()
|
||||
self.assertTrue(self.course_outline.time_allotted_field_visible())
|
||||
self.assertFalse(self.course_outline.exam_review_rules_field_visible())
|
||||
self.studio_course_outline.select_timed_exam()
|
||||
self.assertTrue(self.studio_course_outline.time_allotted_field_visible())
|
||||
self.assertFalse(self.studio_course_outline.exam_review_rules_field_visible())
|
||||
|
||||
self.course_outline.select_proctored_exam()
|
||||
self.assertTrue(self.course_outline.time_allotted_field_visible())
|
||||
self.assertTrue(self.course_outline.exam_review_rules_field_visible())
|
||||
self.studio_course_outline.select_proctored_exam()
|
||||
self.assertTrue(self.studio_course_outline.time_allotted_field_visible())
|
||||
self.assertTrue(self.studio_course_outline.exam_review_rules_field_visible())
|
||||
|
||||
self.course_outline.select_practice_exam()
|
||||
self.assertTrue(self.course_outline.time_allotted_field_visible())
|
||||
self.assertFalse(self.course_outline.exam_review_rules_field_visible())
|
||||
self.studio_course_outline.select_practice_exam()
|
||||
self.assertTrue(self.studio_course_outline.time_allotted_field_visible())
|
||||
self.assertFalse(self.studio_course_outline.exam_review_rules_field_visible())
|
||||
|
||||
|
||||
@attr(shard=9)
|
||||
@@ -389,8 +389,9 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin):
|
||||
super(CoursewareMultipleVerticalsTest, self).setUp()
|
||||
|
||||
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.course_info['org'],
|
||||
self.course_info['number'],
|
||||
@@ -433,10 +434,11 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin):
|
||||
# Auto-auth register for the course.
|
||||
AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL,
|
||||
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):
|
||||
self.courseware_page.visit()
|
||||
|
||||
# start in first section
|
||||
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):
|
||||
sequence_ui_events
|
||||
)
|
||||
|
||||
# TODO: TNL-6546: Delete this whole test if these events are going away(?)
|
||||
def test_outline_selected_events(self):
|
||||
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')
|
||||
|
||||
# test UI events emitted by navigating via the course outline
|
||||
filter_selected_events = lambda event: event.get('name', '') == 'edx.ui.lms.outline.selected'
|
||||
@@ -588,8 +593,10 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin):
|
||||
When I navigate via the left-hand nav
|
||||
Then a link clicked event is logged
|
||||
"""
|
||||
self.course_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.visit()
|
||||
|
||||
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'
|
||||
link_clicked_events = self.wait_for_events(event_filter=filter_link_clicked, timeout=2)
|
||||
@@ -601,15 +608,17 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin):
|
||||
"""
|
||||
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.is_next_button_enabled, next_enabled)
|
||||
self.assertEquals(self.courseware_page.is_previous_button_enabled, prev_enabled)
|
||||
|
||||
def test_tab_position(self):
|
||||
# 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')
|
||||
subsection_url = self.course_nav.active_subsection_url
|
||||
self.course_home_page.visit()
|
||||
|
||||
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('/')
|
||||
self.assertEqual(len(url_part_list), 9)
|
||||
|
||||
@@ -657,7 +666,8 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin):
|
||||
"""
|
||||
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
|
||||
self.courseware_page.a11y_audit.config.set_scope(
|
||||
include=['div.sequence-nav'])
|
||||
@@ -840,7 +850,7 @@ class SubsectionHiddenAfterDueDateTest(UniqueCourseTest):
|
||||
self.courseware_page = CoursewarePage(self.browser, self.course_id)
|
||||
self.logout_page = LogoutPage(self.browser)
|
||||
|
||||
self.course_outline = CourseOutlinePage(
|
||||
self.studio_course_outline = StudioCourseOutlinePage(
|
||||
self.browser,
|
||||
self.course_info['org'],
|
||||
self.course_info['number'],
|
||||
@@ -876,11 +886,11 @@ class SubsectionHiddenAfterDueDateTest(UniqueCourseTest):
|
||||
"""
|
||||
self.logout_page.visit()
|
||||
auto_auth(self.browser, "STAFF_TESTER", "staff101@example.com", True, self.course_id)
|
||||
self.course_outline.visit()
|
||||
self.course_outline.open_subsection_settings_dialog()
|
||||
self.studio_course_outline.visit()
|
||||
self.studio_course_outline.open_subsection_settings_dialog()
|
||||
|
||||
self.course_outline.select_advanced_tab('hide_after_due_date')
|
||||
self.course_outline.make_subsection_hidden_after_due_date()
|
||||
self.studio_course_outline.select_advanced_tab('hide_after_due_date')
|
||||
self.studio_course_outline.make_subsection_hidden_after_due_date()
|
||||
|
||||
self.logout_page.visit()
|
||||
auto_auth(self.browser, self.USERNAME, self.EMAIL, False, self.course_id)
|
||||
@@ -916,9 +926,9 @@ class SubsectionHiddenAfterDueDateTest(UniqueCourseTest):
|
||||
|
||||
self.logout_page.visit()
|
||||
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")
|
||||
self.course_outline.change_problem_due_date(last_week)
|
||||
self.studio_course_outline.change_problem_due_date(last_week)
|
||||
|
||||
self.logout_page.visit()
|
||||
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
|
||||
from common.test.acceptance.pages.common.utils import click_css
|
||||
from common.test.acceptance.pages.studio.utils import add_html_component, type_in_codemirror
|
||||
from common.test.acceptance.pages.studio.auto_auth import AutoAuthPage
|
||||
from common.test.acceptance.pages.studio.overview import CourseOutlinePage
|
||||
from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
|
||||
from common.test.acceptance.pages.studio.container import ContainerPage
|
||||
from common.test.acceptance.pages.lms.courseware_search import CoursewareSearchPage
|
||||
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
|
||||
@@ -54,7 +54,7 @@ class CoursewareSearchTest(UniqueCourseTest):
|
||||
super(CoursewareSearchTest, self).setUp()
|
||||
self.courseware_search_page = CoursewareSearchPage(self.browser, self.course_id)
|
||||
|
||||
self.course_outline = CourseOutlinePage(
|
||||
self.studio_course_outline = StudioCourseOutlinePage(
|
||||
self.browser,
|
||||
self.course_info['org'],
|
||||
self.course_info['number'],
|
||||
@@ -91,8 +91,8 @@ class CoursewareSearchTest(UniqueCourseTest):
|
||||
Publish content on studio course page under specified section
|
||||
"""
|
||||
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
|
||||
self.course_outline.visit()
|
||||
subsection = self.course_outline.section_at(section_index).subsection_at(0)
|
||||
self.studio_course_outline.visit()
|
||||
subsection = self.studio_course_outline.section_at(section_index).subsection_at(0)
|
||||
subsection.expand_subsection()
|
||||
unit = subsection.unit_at(0)
|
||||
unit.publish()
|
||||
@@ -102,8 +102,8 @@ class CoursewareSearchTest(UniqueCourseTest):
|
||||
Edit chapter name on studio course page under specified section
|
||||
"""
|
||||
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
|
||||
self.course_outline.visit()
|
||||
section = self.course_outline.section_at(section_index)
|
||||
self.studio_course_outline.visit()
|
||||
section = self.studio_course_outline.section_at(section_index)
|
||||
section.change_name(self.EDITED_CHAPTER_NAME)
|
||||
|
||||
def _studio_add_content(self, section_index):
|
||||
@@ -113,8 +113,8 @@ class CoursewareSearchTest(UniqueCourseTest):
|
||||
|
||||
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
|
||||
# create a unit in course outline
|
||||
self.course_outline.visit()
|
||||
subsection = self.course_outline.section_at(section_index).subsection_at(0)
|
||||
self.studio_course_outline.visit()
|
||||
subsection = self.studio_course_outline.section_at(section_index).subsection_at(0)
|
||||
subsection.expand_subsection()
|
||||
subsection.add_unit()
|
||||
|
||||
@@ -134,9 +134,9 @@ class CoursewareSearchTest(UniqueCourseTest):
|
||||
"""
|
||||
|
||||
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
|
||||
self.course_outline.visit()
|
||||
self.course_outline.start_reindex()
|
||||
self.course_outline.wait_for_ajax()
|
||||
self.studio_course_outline.visit()
|
||||
self.studio_course_outline.start_reindex()
|
||||
self.studio_course_outline.wait_for_ajax()
|
||||
|
||||
def _search_for_content(self, search_term):
|
||||
"""
|
||||
|
||||
@@ -9,7 +9,7 @@ from common.test.acceptance.pages.common.logout import LogoutPage
|
||||
from common.test.acceptance.pages.common.utils import click_css
|
||||
from common.test.acceptance.pages.studio.utils import add_html_component, type_in_codemirror
|
||||
from common.test.acceptance.pages.studio.auto_auth import AutoAuthPage
|
||||
from common.test.acceptance.pages.studio.overview import CourseOutlinePage
|
||||
from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
|
||||
from common.test.acceptance.pages.studio.container import ContainerPage
|
||||
from common.test.acceptance.pages.lms.dashboard_search import DashboardSearchPage
|
||||
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
|
||||
@@ -60,10 +60,10 @@ class DashboardSearchTest(AcceptanceTest):
|
||||
}
|
||||
|
||||
# generate course fixtures and outline pages
|
||||
self.course_outlines = {}
|
||||
self.studio_course_outlines = {}
|
||||
self.course_fixtures = {}
|
||||
for key, course_info in self.courses.iteritems():
|
||||
course_outline = CourseOutlinePage(
|
||||
studio_course_outline = StudioCourseOutlinePage(
|
||||
self.browser,
|
||||
course_info['org'],
|
||||
course_info['number'],
|
||||
@@ -89,7 +89,7 @@ class DashboardSearchTest(AcceptanceTest):
|
||||
)
|
||||
).install()
|
||||
|
||||
self.course_outlines[key] = course_outline
|
||||
self.studio_course_outlines[key] = studio_course_outline
|
||||
self.course_fixtures[key] = course_fix
|
||||
|
||||
def tearDown(self):
|
||||
@@ -106,13 +106,13 @@ class DashboardSearchTest(AcceptanceTest):
|
||||
LogoutPage(self.browser).visit()
|
||||
AutoAuthPage(self.browser, username=username, email=email, staff=staff).visit()
|
||||
|
||||
def _studio_add_content(self, course_outline, html_content):
|
||||
def _studio_add_content(self, studio_course_outline, html_content):
|
||||
"""
|
||||
Add content to first section on studio course page.
|
||||
"""
|
||||
# create a unit in course outline
|
||||
course_outline.visit()
|
||||
subsection = course_outline.section_at(0).subsection_at(0)
|
||||
studio_course_outline.visit()
|
||||
subsection = studio_course_outline.section_at(0).subsection_at(0)
|
||||
subsection.expand_subsection()
|
||||
subsection.add_unit()
|
||||
|
||||
@@ -126,12 +126,12 @@ class DashboardSearchTest(AcceptanceTest):
|
||||
type_in_codemirror(unit_page, 0, html_content)
|
||||
click_css(unit_page, '.action-save', 0)
|
||||
|
||||
def _studio_publish_content(self, course_outline):
|
||||
def _studio_publish_content(self, studio_course_outline):
|
||||
"""
|
||||
Publish content in first section on studio course page.
|
||||
"""
|
||||
course_outline.visit()
|
||||
subsection = course_outline.section_at(0).subsection_at(0)
|
||||
studio_course_outline.visit()
|
||||
subsection = studio_course_outline.section_at(0).subsection_at(0)
|
||||
subsection.expand_subsection()
|
||||
unit = subsection.unit_at(0)
|
||||
unit.publish()
|
||||
@@ -167,9 +167,9 @@ class DashboardSearchTest(AcceptanceTest):
|
||||
|
||||
# Create content in studio without publishing.
|
||||
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
|
||||
self._studio_add_content(self.course_outlines['A'], html_content)
|
||||
self._studio_add_content(self.course_outlines['B'], html_content)
|
||||
self._studio_add_content(self.course_outlines['C'], html_content)
|
||||
self._studio_add_content(self.studio_course_outlines['A'], html_content)
|
||||
self._studio_add_content(self.studio_course_outlines['B'], html_content)
|
||||
self._studio_add_content(self.studio_course_outlines['C'], html_content)
|
||||
|
||||
# Do a search, there should be no results shown.
|
||||
self._auto_auth(self.USERNAME, self.EMAIL, False)
|
||||
@@ -179,9 +179,9 @@ class DashboardSearchTest(AcceptanceTest):
|
||||
|
||||
# Publish in studio to trigger indexing.
|
||||
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
|
||||
self._studio_publish_content(self.course_outlines['A'])
|
||||
self._studio_publish_content(self.course_outlines['B'])
|
||||
self._studio_publish_content(self.course_outlines['C'])
|
||||
self._studio_publish_content(self.studio_course_outlines['A'])
|
||||
self._studio_publish_content(self.studio_course_outlines['B'])
|
||||
self._studio_publish_content(self.studio_course_outlines['C'])
|
||||
|
||||
# Do the search again, this time we expect results from courses A & B, but not C
|
||||
self._auto_auth(self.USERNAME, self.EMAIL, False)
|
||||
|
||||
@@ -9,7 +9,7 @@ from nose.plugins.attrib import attr
|
||||
from common.test.acceptance.tests.helpers import UniqueCourseTest, EventsTestMixin
|
||||
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
|
||||
from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage
|
||||
from common.test.acceptance.pages.lms.course_nav import CourseNavPage
|
||||
from common.test.acceptance.pages.lms.course_home import CourseHomePage
|
||||
from common.test.acceptance.pages.lms.courseware import CoursewarePage
|
||||
from common.test.acceptance.pages.lms.edxnotes import EdxNotesUnitPage, EdxNotesPage, EdxNotesPageNoContent
|
||||
from common.test.acceptance.fixtures.edxnotes import EdxNotesFixture, Note, Range
|
||||
@@ -26,7 +26,7 @@ class EdxNotesTestMixin(UniqueCourseTest):
|
||||
"""
|
||||
super(EdxNotesTestMixin, self).setUp()
|
||||
self.courseware_page = CoursewarePage(self.browser, self.course_id)
|
||||
self.course_nav = CourseNavPage(self.browser)
|
||||
self.course_home_page = CourseHomePage(self.browser, self.course_id)
|
||||
self.note_unit_page = EdxNotesUnitPage(self.browser, self.course_id)
|
||||
self.notes_page = EdxNotesPage(self.browser, self.course_id)
|
||||
|
||||
@@ -1504,7 +1504,8 @@ class EdxNotesToggleNotesTest(EdxNotesTestMixin):
|
||||
self.assertEqual(len(self.note_unit_page.notes), 0)
|
||||
self.courseware_page.go_to_sequential_position(2)
|
||||
self.assertEqual(len(self.note_unit_page.notes), 0)
|
||||
self.course_nav.go_to_section(u"Test Section 1", u"Test Subsection 2")
|
||||
self.course_home_page.visit()
|
||||
self.course_home_page.outline.go_to_section(u"Test Section 1", u"Test Subsection 2")
|
||||
self.assertEqual(len(self.note_unit_page.notes), 0)
|
||||
|
||||
def test_can_reenable_all_notes(self):
|
||||
@@ -1530,5 +1531,6 @@ class EdxNotesToggleNotesTest(EdxNotesTestMixin):
|
||||
self.assertGreater(len(self.note_unit_page.notes), 0)
|
||||
self.courseware_page.go_to_sequential_position(2)
|
||||
self.assertGreater(len(self.note_unit_page.notes), 0)
|
||||
self.course_nav.go_to_section(u"Test Section 1", u"Test Subsection 2")
|
||||
self.course_home_page.visit()
|
||||
self.course_home_page.outline.go_to_section(u"Test Section 1", u"Test Subsection 2")
|
||||
self.assertGreater(len(self.note_unit_page.notes), 0)
|
||||
|
||||
@@ -43,7 +43,7 @@ class EntranceExamTest(UniqueCourseTest):
|
||||
).install()
|
||||
|
||||
entrance_exam_subsection = None
|
||||
outline = course_fixture.course_outline
|
||||
outline = course_fixture.studio_course_outline_as_json
|
||||
for child in outline['child_info']['children']:
|
||||
if child.get('display_name') == "Entrance Exam":
|
||||
entrance_exam_subsection = child['child_info']['children'][0]
|
||||
|
||||
@@ -6,7 +6,7 @@ from textwrap import dedent
|
||||
|
||||
from common.test.acceptance.tests.helpers import UniqueCourseTest
|
||||
from common.test.acceptance.pages.studio.auto_auth import AutoAuthPage
|
||||
from common.test.acceptance.pages.studio.overview import CourseOutlinePage
|
||||
from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
|
||||
from common.test.acceptance.pages.lms.courseware import CoursewarePage
|
||||
from common.test.acceptance.pages.lms.problem import ProblemPage
|
||||
from common.test.acceptance.pages.lms.staff_view import StaffPage
|
||||
@@ -29,7 +29,7 @@ class GatingTest(UniqueCourseTest):
|
||||
|
||||
self.logout_page = LogoutPage(self.browser)
|
||||
self.courseware_page = CoursewarePage(self.browser, self.course_id)
|
||||
self.course_outline = CourseOutlinePage(
|
||||
self.studio_course_outline = StudioCourseOutlinePage(
|
||||
self.browser,
|
||||
self.course_info['org'],
|
||||
self.course_info['number'],
|
||||
@@ -89,10 +89,10 @@ class GatingTest(UniqueCourseTest):
|
||||
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
|
||||
|
||||
# Make the first subsection a prerequisite
|
||||
self.course_outline.visit()
|
||||
self.course_outline.open_subsection_settings_dialog(0)
|
||||
self.course_outline.select_advanced_tab(desired_item='gated_content')
|
||||
self.course_outline.make_gating_prerequisite()
|
||||
self.studio_course_outline.visit()
|
||||
self.studio_course_outline.open_subsection_settings_dialog(0)
|
||||
self.studio_course_outline.select_advanced_tab(desired_item='gated_content')
|
||||
self.studio_course_outline.make_gating_prerequisite()
|
||||
|
||||
def _setup_gated_subsection(self):
|
||||
"""
|
||||
@@ -102,10 +102,10 @@ class GatingTest(UniqueCourseTest):
|
||||
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
|
||||
|
||||
# Gate the second subsection based on the score achieved in the first subsection
|
||||
self.course_outline.visit()
|
||||
self.course_outline.open_subsection_settings_dialog(1)
|
||||
self.course_outline.select_advanced_tab(desired_item='gated_content')
|
||||
self.course_outline.add_prerequisite_to_subsection("80")
|
||||
self.studio_course_outline.visit()
|
||||
self.studio_course_outline.open_subsection_settings_dialog(1)
|
||||
self.studio_course_outline.select_advanced_tab(desired_item='gated_content')
|
||||
self.studio_course_outline.add_prerequisite_to_subsection("80")
|
||||
|
||||
def _fulfill_prerequisite(self):
|
||||
"""
|
||||
@@ -127,23 +127,23 @@ class GatingTest(UniqueCourseTest):
|
||||
self._setup_prereq()
|
||||
|
||||
# Assert settings are displayed correctly for a prerequisite subsection
|
||||
self.course_outline.visit()
|
||||
self.course_outline.open_subsection_settings_dialog(0)
|
||||
self.course_outline.select_advanced_tab(desired_item='gated_content')
|
||||
self.assertTrue(self.course_outline.gating_prerequisite_checkbox_is_visible())
|
||||
self.assertTrue(self.course_outline.gating_prerequisite_checkbox_is_checked())
|
||||
self.assertFalse(self.course_outline.gating_prerequisites_dropdown_is_visible())
|
||||
self.assertFalse(self.course_outline.gating_prerequisite_min_score_is_visible())
|
||||
self.studio_course_outline.visit()
|
||||
self.studio_course_outline.open_subsection_settings_dialog(0)
|
||||
self.studio_course_outline.select_advanced_tab(desired_item='gated_content')
|
||||
self.assertTrue(self.studio_course_outline.gating_prerequisite_checkbox_is_visible())
|
||||
self.assertTrue(self.studio_course_outline.gating_prerequisite_checkbox_is_checked())
|
||||
self.assertFalse(self.studio_course_outline.gating_prerequisites_dropdown_is_visible())
|
||||
self.assertFalse(self.studio_course_outline.gating_prerequisite_min_score_is_visible())
|
||||
|
||||
self._setup_gated_subsection()
|
||||
|
||||
# Assert settings are displayed correctly for a gated subsection
|
||||
self.course_outline.visit()
|
||||
self.course_outline.open_subsection_settings_dialog(1)
|
||||
self.course_outline.select_advanced_tab(desired_item='gated_content')
|
||||
self.assertTrue(self.course_outline.gating_prerequisite_checkbox_is_visible())
|
||||
self.assertTrue(self.course_outline.gating_prerequisites_dropdown_is_visible())
|
||||
self.assertTrue(self.course_outline.gating_prerequisite_min_score_is_visible())
|
||||
self.studio_course_outline.visit()
|
||||
self.studio_course_outline.open_subsection_settings_dialog(1)
|
||||
self.studio_course_outline.select_advanced_tab(desired_item='gated_content')
|
||||
self.assertTrue(self.studio_course_outline.gating_prerequisite_checkbox_is_visible())
|
||||
self.assertTrue(self.studio_course_outline.gating_prerequisites_dropdown_is_visible())
|
||||
self.assertTrue(self.studio_course_outline.gating_prerequisite_min_score_is_visible())
|
||||
|
||||
def test_gated_subsection_in_lms_for_student(self):
|
||||
"""
|
||||
|
||||
@@ -12,7 +12,7 @@ from flaky import flaky
|
||||
from common.test.acceptance.tests.helpers import UniqueCourseTest, get_modal_alert, EventsTestMixin
|
||||
from common.test.acceptance.pages.common.logout import LogoutPage
|
||||
from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage
|
||||
from common.test.acceptance.pages.studio.overview import CourseOutlinePage
|
||||
from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
|
||||
from common.test.acceptance.pages.lms.create_mode import ModeCreationPage
|
||||
from common.test.acceptance.pages.lms.courseware import CoursewarePage
|
||||
from common.test.acceptance.pages.lms.instructor_dashboard import InstructorDashboardPage, EntranceExamAdmin
|
||||
@@ -227,7 +227,7 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
|
||||
|
||||
self.courseware_page = CoursewarePage(self.browser, self.course_id)
|
||||
|
||||
self.course_outline = CourseOutlinePage(
|
||||
self.studio_course_outline = StudioCourseOutlinePage(
|
||||
self.browser,
|
||||
self.course_info['org'],
|
||||
self.course_info['number'],
|
||||
@@ -301,15 +301,15 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
|
||||
# Visit the course outline page in studio
|
||||
LogoutPage(self.browser).visit()
|
||||
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
|
||||
self.course_outline.visit()
|
||||
self.studio_course_outline.visit()
|
||||
|
||||
# open the exam settings to make it a proctored exam.
|
||||
self.course_outline.open_subsection_settings_dialog()
|
||||
self.studio_course_outline.open_subsection_settings_dialog()
|
||||
|
||||
# select advanced settings tab
|
||||
self.course_outline.select_advanced_tab()
|
||||
self.studio_course_outline.select_advanced_tab()
|
||||
|
||||
self.course_outline.make_exam_proctored()
|
||||
self.studio_course_outline.make_exam_proctored()
|
||||
|
||||
# login as a verified student and visit the courseware.
|
||||
LogoutPage(self.browser).visit()
|
||||
@@ -327,15 +327,15 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
|
||||
# Visit the course outline page in studio
|
||||
LogoutPage(self.browser).visit()
|
||||
self._auto_auth("STAFF_TESTER", "staff101@example.com", True)
|
||||
self.course_outline.visit()
|
||||
self.studio_course_outline.visit()
|
||||
|
||||
# open the exam settings to make it a proctored exam.
|
||||
self.course_outline.open_subsection_settings_dialog()
|
||||
self.studio_course_outline.open_subsection_settings_dialog()
|
||||
|
||||
# select advanced settings tab
|
||||
self.course_outline.select_advanced_tab()
|
||||
self.studio_course_outline.select_advanced_tab()
|
||||
|
||||
self.course_outline.make_exam_timed()
|
||||
self.studio_course_outline.make_exam_timed()
|
||||
|
||||
# login as a verified student and visit the courseware.
|
||||
LogoutPage(self.browser).visit()
|
||||
|
||||
@@ -6,9 +6,8 @@ import json
|
||||
|
||||
from common.test.acceptance.tests.helpers import remove_file
|
||||
from common.test.acceptance.pages.common.logout import LogoutPage
|
||||
from common.test.acceptance.pages.studio.overview import CourseOutlinePage
|
||||
from common.test.acceptance.pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
|
||||
from common.test.acceptance.pages.lms.courseware_search import CoursewareSearchPage
|
||||
from common.test.acceptance.pages.lms.course_nav import CourseNavPage
|
||||
from common.test.acceptance.fixtures.course import XBlockFixtureDesc
|
||||
from common.test.acceptance.tests.helpers import create_user_partition_json
|
||||
|
||||
@@ -44,8 +43,7 @@ class SplitTestCoursewareSearchTest(ContainerBase):
|
||||
self.staff_user = self.user
|
||||
|
||||
self.courseware_search_page = CoursewareSearchPage(self.browser, self.course_id)
|
||||
self.course_navigation_page = CourseNavPage(self.browser)
|
||||
self.course_outline = CourseOutlinePage(
|
||||
self.studio_course_outline = StudioCourseOutlinePage(
|
||||
self.browser,
|
||||
self.course_info['org'],
|
||||
self.course_info['number'],
|
||||
@@ -68,9 +66,9 @@ class SplitTestCoursewareSearchTest(ContainerBase):
|
||||
Reindex course content on studio course page
|
||||
"""
|
||||
self._auto_auth(self.staff_user["username"], self.staff_user["email"], True)
|
||||
self.course_outline.visit()
|
||||
self.course_outline.start_reindex()
|
||||
self.course_outline.wait_for_ajax()
|
||||
self.studio_course_outline.visit()
|
||||
self.studio_course_outline.start_reindex()
|
||||
self.studio_course_outline.wait_for_ajax()
|
||||
|
||||
def _create_group_configuration(self):
|
||||
"""
|
||||
|
||||
@@ -20,7 +20,7 @@ from ...pages.lms.problem import ProblemPage
|
||||
from ...pages.lms.progress import ProgressPage
|
||||
from ...pages.studio.component_editor import ComponentEditorView
|
||||
from ...pages.studio.utils import type_in_codemirror
|
||||
from ...pages.studio.overview import CourseOutlinePage
|
||||
from ...pages.studio.overview import CourseOutlinePage as StudioCourseOutlinePage
|
||||
|
||||
|
||||
class ProgressPageBaseTest(UniqueCourseTest):
|
||||
@@ -43,7 +43,7 @@ class ProgressPageBaseTest(UniqueCourseTest):
|
||||
self.progress_page = ProgressPage(self.browser, self.course_id)
|
||||
self.logout_page = LogoutPage(self.browser)
|
||||
|
||||
self.course_outline = CourseOutlinePage(
|
||||
self.studio_course_outline = StudioCourseOutlinePage(
|
||||
self.browser,
|
||||
self.course_info['org'],
|
||||
self.course_info['number'],
|
||||
@@ -140,11 +140,11 @@ class PersistentGradesTest(ProgressPageBaseTest):
|
||||
Adds a unit to the subsection, which
|
||||
should not affect a persisted subsection grade.
|
||||
"""
|
||||
self.course_outline.visit()
|
||||
subsection = self.course_outline.section(self.SECTION_NAME).subsection(self.SUBSECTION_NAME)
|
||||
self.studio_course_outline.visit()
|
||||
subsection = self.studio_course_outline.section(self.SECTION_NAME).subsection(self.SUBSECTION_NAME)
|
||||
subsection.expand_subsection()
|
||||
subsection.add_unit()
|
||||
self.course_outline.wait_for_ajax()
|
||||
self.studio_course_outline.wait_for_ajax()
|
||||
subsection.publish()
|
||||
|
||||
def _set_staff_lock_on_subsection(self, locked):
|
||||
@@ -152,8 +152,8 @@ class PersistentGradesTest(ProgressPageBaseTest):
|
||||
Sets staff lock for a subsection, which should hide the
|
||||
subsection score from students on the progress page.
|
||||
"""
|
||||
self.course_outline.visit()
|
||||
subsection = self.course_outline.section_at(0).subsection_at(0)
|
||||
self.studio_course_outline.visit()
|
||||
subsection = self.studio_course_outline.section_at(0).subsection_at(0)
|
||||
subsection.set_staff_lock(locked)
|
||||
self.assertEqual(subsection.has_staff_lock_warning, locked)
|
||||
|
||||
@@ -163,9 +163,9 @@ class PersistentGradesTest(ProgressPageBaseTest):
|
||||
along with its container unit, so any changes can
|
||||
be published.
|
||||
"""
|
||||
self.course_outline.visit()
|
||||
self.course_outline.section_at(0).subsection_at(0).expand_subsection()
|
||||
unit = self.course_outline.section_at(0).subsection_at(0).unit(self.UNIT_NAME).go_to()
|
||||
self.studio_course_outline.visit()
|
||||
self.studio_course_outline.section_at(0).subsection_at(0).expand_subsection()
|
||||
unit = self.studio_course_outline.section_at(0).subsection_at(0).unit(self.UNIT_NAME).go_to()
|
||||
component = unit.xblocks[1]
|
||||
return unit, component
|
||||
|
||||
@@ -289,8 +289,8 @@ class SubsectionGradingPolicyTest(ProgressPageBaseTest):
|
||||
If a section index is not provided, 0 is assumed.
|
||||
"""
|
||||
with self._logged_in_session(staff=True):
|
||||
self.course_outline.visit()
|
||||
modal = self.course_outline.section_at(section).subsection_at(0).edit()
|
||||
self.studio_course_outline.visit()
|
||||
modal = self.studio_course_outline.section_at(section).subsection_at(0).edit()
|
||||
modal.policy = policy
|
||||
modal.save()
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ from nose.plugins.attrib import attr
|
||||
from common.test.acceptance.pages.studio.settings_advanced import AdvancedSettingsPage
|
||||
from common.test.acceptance.pages.studio.overview import CourseOutlinePage, ContainerPage, ExpandCollapseLinkState
|
||||
from common.test.acceptance.pages.studio.utils import add_discussion, drag, verify_ordering
|
||||
from common.test.acceptance.pages.lms.course_home import CourseHomePage
|
||||
from common.test.acceptance.pages.lms.courseware import CoursewarePage
|
||||
from common.test.acceptance.pages.lms.course_nav import CourseNavPage
|
||||
from common.test.acceptance.pages.lms.staff_view import StaffPage
|
||||
from common.test.acceptance.fixtures.config import ConfigModelFixture
|
||||
from common.test.acceptance.fixtures.course import XBlockFixtureDesc
|
||||
@@ -1490,7 +1490,7 @@ class PublishSectionTest(CourseOutlineTest):
|
||||
The first subsection has 2 units, and the second subsection has one unit.
|
||||
"""
|
||||
self.courseware = CoursewarePage(self.browser, self.course_id)
|
||||
self.course_nav = CourseNavPage(self.browser)
|
||||
self.course_home_page = CourseHomePage(self.browser, self.course_id)
|
||||
course_fixture.add_children(
|
||||
XBlockFixtureDesc('chapter', SECTION_NAME).add_children(
|
||||
XBlockFixtureDesc('sequential', SUBSECTION_NAME).add_children(
|
||||
@@ -1578,7 +1578,8 @@ class PublishSectionTest(CourseOutlineTest):
|
||||
self.assertEqual(1, self.courseware.num_xblock_components)
|
||||
self.courseware.go_to_sequential_position(2)
|
||||
self.assertEqual(1, self.courseware.num_xblock_components)
|
||||
self.course_nav.go_to_section(SECTION_NAME, 'Test Subsection 2')
|
||||
self.course_home_page.visit()
|
||||
self.course_home_page.outline.go_to_section(SECTION_NAME, 'Test Subsection 2')
|
||||
self.assertEqual(1, self.courseware.num_xblock_components)
|
||||
|
||||
def _add_unpublished_content(self):
|
||||
|
||||
@@ -15,7 +15,6 @@ from common.test.acceptance.tests.helpers import UniqueCourseTest, is_youtube_av
|
||||
from common.test.acceptance.pages.lms.video.video import VideoPage
|
||||
from common.test.acceptance.pages.lms.tab_nav import TabNavPage
|
||||
from common.test.acceptance.pages.lms.courseware import CoursewarePage
|
||||
from common.test.acceptance.pages.lms.course_nav import CourseNavPage
|
||||
from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage
|
||||
from common.test.acceptance.pages.lms.course_info import CourseInfoPage
|
||||
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
|
||||
@@ -53,7 +52,6 @@ class VideoBaseTest(UniqueCourseTest):
|
||||
|
||||
self.video = VideoPage(self.browser)
|
||||
self.tab_nav = TabNavPage(self.browser)
|
||||
self.course_nav = CourseNavPage(self.browser)
|
||||
self.courseware = CoursewarePage(self.browser, self.course_id)
|
||||
self.course_info_page = CourseInfoPage(self.browser, self.course_id)
|
||||
self.auth_page = AutoAuthPage(self.browser, course_id=self.course_id)
|
||||
@@ -531,7 +529,7 @@ class YouTubeVideoTest(VideoBaseTest):
|
||||
self.assertTrue(self.video.downloaded_transcript_contains_text(file_type, search_text))
|
||||
|
||||
# open vertical containing video "C"
|
||||
self.course_nav.go_to_vertical('Test Vertical-2')
|
||||
self.courseware.nav.go_to_vertical('Test Vertical-2')
|
||||
|
||||
# menu "download_transcript" doesn't exist
|
||||
self.assertFalse(self.video.is_menu_present('download_transcript'))
|
||||
@@ -678,17 +676,17 @@ class YouTubeVideoTest(VideoBaseTest):
|
||||
self.navigate_to_video()
|
||||
|
||||
# select the "2.0" speed on video "A"
|
||||
self.course_nav.go_to_vertical('Test Vertical-0')
|
||||
self.courseware.nav.go_to_vertical('Test Vertical-0')
|
||||
self.video.wait_for_video_player_render()
|
||||
self.video.speed = '2.0'
|
||||
|
||||
# select the "0.50" speed on video "B"
|
||||
self.course_nav.go_to_vertical('Test Vertical-1')
|
||||
self.courseware.nav.go_to_vertical('Test Vertical-1')
|
||||
self.video.wait_for_video_player_render()
|
||||
self.video.speed = '0.50'
|
||||
|
||||
# open video "C"
|
||||
self.course_nav.go_to_vertical('Test Vertical-2')
|
||||
self.courseware.nav.go_to_vertical('Test Vertical-2')
|
||||
self.video.wait_for_video_player_render()
|
||||
|
||||
# Since the playback speed was set to .5 in "B", this video will also be impacted
|
||||
@@ -697,7 +695,7 @@ class YouTubeVideoTest(VideoBaseTest):
|
||||
self.video.verify_speed_changed('0.75x')
|
||||
|
||||
# go to the vertical containing video "A"
|
||||
self.course_nav.go_to_vertical('Test Vertical-0')
|
||||
self.courseware.nav.go_to_vertical('Test Vertical-0')
|
||||
|
||||
# Video "A" should still play at speed 2.0 because it was explicitly set to that.
|
||||
self.assertEqual(self.video.speed, '2.0x')
|
||||
@@ -706,7 +704,7 @@ class YouTubeVideoTest(VideoBaseTest):
|
||||
self.video.reload_page()
|
||||
|
||||
# go to the vertical containing video "A"
|
||||
self.course_nav.go_to_vertical('Test Vertical-0')
|
||||
self.courseware.nav.go_to_vertical('Test Vertical-0')
|
||||
|
||||
# check if video "A" should start playing at speed "2.0"
|
||||
self.assertEqual(self.video.speed, '2.0x')
|
||||
@@ -715,13 +713,13 @@ class YouTubeVideoTest(VideoBaseTest):
|
||||
self.video.speed = '1.0'
|
||||
|
||||
# go to the vertical containing "B"
|
||||
self.course_nav.go_to_vertical('Test Vertical-1')
|
||||
self.courseware.nav.go_to_vertical('Test Vertical-1')
|
||||
|
||||
# Video "B" should still play at speed .5 because it was explicitly set to that.
|
||||
self.assertEqual(self.video.speed, '0.50x')
|
||||
|
||||
# go to the vertical containing video "C"
|
||||
self.course_nav.go_to_vertical('Test Vertical-2')
|
||||
self.courseware.nav.go_to_vertical('Test Vertical-2')
|
||||
|
||||
# The change of speed for Video "A" should impact Video "C" because it still has
|
||||
# not been explicitly set to a speed.
|
||||
|
||||
@@ -231,18 +231,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
|
||||
# # of sql queries to default,
|
||||
# # of mongo queries,
|
||||
# )
|
||||
('no_overrides', 1, True, False): (21, 6),
|
||||
('no_overrides', 2, True, False): (21, 6),
|
||||
('no_overrides', 3, True, False): (21, 6),
|
||||
('ccx', 1, True, False): (21, 6),
|
||||
('ccx', 2, True, False): (21, 6),
|
||||
('ccx', 3, True, False): (21, 6),
|
||||
('no_overrides', 1, False, False): (21, 6),
|
||||
('no_overrides', 2, False, False): (21, 6),
|
||||
('no_overrides', 3, False, False): (21, 6),
|
||||
('ccx', 1, False, False): (21, 6),
|
||||
('ccx', 2, False, False): (21, 6),
|
||||
('ccx', 3, False, False): (21, 6),
|
||||
('no_overrides', 1, True, False): (22, 6),
|
||||
('no_overrides', 2, True, False): (22, 6),
|
||||
('no_overrides', 3, True, False): (22, 6),
|
||||
('ccx', 1, True, False): (22, 6),
|
||||
('ccx', 2, True, False): (22, 6),
|
||||
('ccx', 3, True, False): (22, 6),
|
||||
('no_overrides', 1, False, False): (22, 6),
|
||||
('no_overrides', 2, False, False): (22, 6),
|
||||
('no_overrides', 3, False, False): (22, 6),
|
||||
('ccx', 1, False, False): (22, 6),
|
||||
('ccx', 2, False, False): (22, 6),
|
||||
('ccx', 3, False, False): (22, 6),
|
||||
}
|
||||
|
||||
|
||||
@@ -254,19 +254,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
|
||||
__test__ = True
|
||||
|
||||
TEST_DATA = {
|
||||
('no_overrides', 1, True, False): (21, 3),
|
||||
('no_overrides', 2, True, False): (21, 3),
|
||||
('no_overrides', 3, True, False): (21, 3),
|
||||
('ccx', 1, True, False): (21, 3),
|
||||
('ccx', 2, True, False): (21, 3),
|
||||
('ccx', 3, True, False): (21, 3),
|
||||
('ccx', 1, True, True): (22, 3),
|
||||
('ccx', 2, True, True): (22, 3),
|
||||
('ccx', 3, True, True): (22, 3),
|
||||
('no_overrides', 1, False, False): (21, 3),
|
||||
('no_overrides', 2, False, False): (21, 3),
|
||||
('no_overrides', 3, False, False): (21, 3),
|
||||
('ccx', 1, False, False): (21, 3),
|
||||
('ccx', 2, False, False): (21, 3),
|
||||
('ccx', 3, False, False): (21, 3),
|
||||
('no_overrides', 1, True, False): (22, 3),
|
||||
('no_overrides', 2, True, False): (22, 3),
|
||||
('no_overrides', 3, True, False): (22, 3),
|
||||
('ccx', 1, True, False): (22, 3),
|
||||
('ccx', 2, True, False): (22, 3),
|
||||
('ccx', 3, True, False): (22, 3),
|
||||
('ccx', 1, True, True): (23, 3),
|
||||
('ccx', 2, True, True): (23, 3),
|
||||
('ccx', 3, True, True): (23, 3),
|
||||
('no_overrides', 1, False, False): (22, 3),
|
||||
('no_overrides', 2, False, False): (22, 3),
|
||||
('no_overrides', 3, False, False): (22, 3),
|
||||
('ccx', 1, False, False): (22, 3),
|
||||
('ccx', 2, False, False): (22, 3),
|
||||
('ccx', 3, False, False): (22, 3),
|
||||
}
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
This module is essentially a broker to xmodule/tabs.py -- it was originally introduced to
|
||||
perform some LMS-specific tab display gymnastics for the Entrance Exams feature
|
||||
"""
|
||||
import waffle
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _, ugettext_noop
|
||||
|
||||
from courseware.access import has_access
|
||||
from courseware.entrance_exams import user_must_complete_entrance_exam
|
||||
from openedx.core.lib.course_tabs import CourseTabPluginManager
|
||||
from request_cache.middleware import RequestCache
|
||||
from student.models import CourseEnrollment
|
||||
from xmodule.tabs import CourseTab, CourseTabList, key_checker
|
||||
from xmodule.tabs import CourseTab, CourseTabList, key_checker, link_reverse_func
|
||||
|
||||
|
||||
class EnrolledTab(CourseTab):
|
||||
@@ -34,6 +37,25 @@ class CoursewareTab(EnrolledTab):
|
||||
is_movable = False
|
||||
is_default = False
|
||||
|
||||
@staticmethod
|
||||
def main_course_url_name(request):
|
||||
"""
|
||||
Returns the main course URL for the current user.
|
||||
"""
|
||||
if waffle.flag_is_active(request, 'unified_course_view'):
|
||||
return 'edx.course_experience.course_home'
|
||||
else:
|
||||
return 'courseware'
|
||||
|
||||
@property
|
||||
def link_func(self):
|
||||
"""
|
||||
Returns a function that computes the URL for this tab.
|
||||
"""
|
||||
request = RequestCache.get_current_request()
|
||||
url_name = self.main_course_url_name(request)
|
||||
return link_reverse_func(url_name)
|
||||
|
||||
|
||||
class CourseInfoTab(CourseTab):
|
||||
"""
|
||||
|
||||
@@ -1419,17 +1419,17 @@ class ProgressPageTests(ModuleStoreTestCase):
|
||||
"""Test that query counts remain the same for self-paced and instructor-paced courses."""
|
||||
SelfPacedConfiguration(enabled=self_paced_enabled).save()
|
||||
self.setup_course(self_paced=self_paced)
|
||||
with self.assertNumQueries(38), check_mongo_calls(4):
|
||||
with self.assertNumQueries(39), check_mongo_calls(4):
|
||||
self._get_progress_page()
|
||||
|
||||
def test_progress_queries(self):
|
||||
self.setup_course()
|
||||
with self.assertNumQueries(38), check_mongo_calls(4):
|
||||
with self.assertNumQueries(39), check_mongo_calls(4):
|
||||
self._get_progress_page()
|
||||
|
||||
# subsequent accesses to the progress page require fewer queries.
|
||||
for _ in range(2):
|
||||
with self.assertNumQueries(24), check_mongo_calls(4):
|
||||
with self.assertNumQueries(25), check_mongo_calls(4):
|
||||
self._get_progress_page()
|
||||
|
||||
@patch(
|
||||
|
||||
@@ -21,12 +21,14 @@ from edxmako.shortcuts import render_to_response, render_to_string
|
||||
import logging
|
||||
import newrelic.agent
|
||||
import urllib
|
||||
import waffle
|
||||
|
||||
from xblock.fragment import Fragment
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
|
||||
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
|
||||
from openedx.core.djangoapps.crawlers.models import CrawlersConfig
|
||||
from request_cache.middleware import RequestCache
|
||||
from shoppingcart.models import CourseRegistrationCode
|
||||
from student.models import CourseEnrollment
|
||||
from student.views import is_course_blocked
|
||||
@@ -36,6 +38,7 @@ from util.views import ensure_valid_course_key
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.x_module import STUDENT_VIEW
|
||||
from survey.utils import must_answer_survey
|
||||
from web_fragments.fragment import Fragment
|
||||
|
||||
from ..access import has_access, _adjust_start_date_for_beta_testers
|
||||
from ..access_utils import in_preview_mode
|
||||
@@ -396,9 +399,9 @@ class CoursewareIndex(View):
|
||||
Returns and creates the rendering context for the courseware.
|
||||
Also returns the table of contents for the courseware.
|
||||
"""
|
||||
request = RequestCache.get_current_request()
|
||||
courseware_context = {
|
||||
'csrf': csrf(self.request)['csrf_token'],
|
||||
'COURSE_TITLE': self.course.display_name_with_default_escaped,
|
||||
'course': self.course,
|
||||
'init': '',
|
||||
'fragment': Fragment(),
|
||||
@@ -411,7 +414,8 @@ class CoursewareIndex(View):
|
||||
'language_preference': self._get_language_preference(),
|
||||
'disable_optimizely': True,
|
||||
'section_title': None,
|
||||
'sequence_title': None
|
||||
'sequence_title': None,
|
||||
'disable_accordion': waffle.flag_is_active(request, 'unified_course_view')
|
||||
}
|
||||
table_of_contents = toc_for_course(
|
||||
self.effective_user,
|
||||
@@ -455,7 +459,7 @@ class CoursewareIndex(View):
|
||||
courseware_context['default_tab'] = self.section.default_tab
|
||||
|
||||
# section data
|
||||
courseware_context['section_title'] = self.section.display_name_with_default_escaped
|
||||
courseware_context['section_title'] = self.section.display_name_with_default
|
||||
section_context = self._create_section_context(
|
||||
table_of_contents['previous_of_active_section'],
|
||||
table_of_contents['next_of_active_section'],
|
||||
|
||||
@@ -223,6 +223,18 @@ ECOMMERCE_API_URL = 'http://localhost:8043/api/v2/'
|
||||
LMS_ROOT_URL = "http://localhost:8000"
|
||||
DOC_LINK_BASE_URL = 'http://edx.readthedocs.io/projects/edx-guide-for-students'
|
||||
|
||||
# TODO: TNL-6546: Remove this waffle and flag code.
|
||||
from django.db.utils import ProgrammingError
|
||||
from waffle.models import Flag
|
||||
try:
|
||||
flag, created = Flag.objects.get_or_create(name='unified_course_view')
|
||||
flag.everyone = True
|
||||
flag.save
|
||||
WAFFLE_OVERRIDE = True
|
||||
except ProgrammingError:
|
||||
# during initial reset_db, the table for the flag doesn't yet exist.
|
||||
pass
|
||||
|
||||
#####################################################################
|
||||
# Lastly, see if the developer has any local overrides.
|
||||
try:
|
||||
|
||||
@@ -1724,7 +1724,7 @@ REQUIRE_ENVIRONMENT = "node"
|
||||
# but you don't want to include those dependencies in the JS bundle for the page,
|
||||
# then you need to add the js urls in this list.
|
||||
REQUIRE_JS_PATH_OVERRIDES = {
|
||||
'js/bookmarks/views/bookmark_button': 'js/bookmarks/views/bookmark_button.js',
|
||||
'course_bookmarks/js/views/bookmark_button': 'course_bookmarks/js/views/bookmark_button.js',
|
||||
'js/views/message_banner': 'js/views/message_banner.js',
|
||||
'moment': 'common/js/vendor/moment-with-locales.js',
|
||||
'moment-timezone': 'common/js/vendor/moment-timezone-with-data.js',
|
||||
@@ -2173,6 +2173,10 @@ INSTALLED_APPS = (
|
||||
|
||||
# Unusual migrations
|
||||
'database_fixups',
|
||||
|
||||
# Features
|
||||
'openedx.features.course_bookmarks',
|
||||
'openedx.features.course_experience',
|
||||
)
|
||||
|
||||
######################### CSRF #########################################
|
||||
|
||||
1
lms/static/course_bookmarks
Symbolic link
1
lms/static/course_bookmarks
Symbolic link
@@ -0,0 +1 @@
|
||||
../../openedx/features/course_bookmarks/static/course_bookmarks
|
||||
1
lms/static/course_experience
Symbolic link
1
lms/static/course_experience
Symbolic link
@@ -0,0 +1 @@
|
||||
../../openedx/features/course_experience/static/course_experience
|
||||
@@ -1,46 +0,0 @@
|
||||
(function(define, undefined) {
|
||||
'use strict';
|
||||
define(['gettext', 'jquery', 'underscore', 'backbone', 'js/bookmarks/views/bookmarks_list',
|
||||
'js/bookmarks/collections/bookmarks', 'js/views/message_banner'],
|
||||
function(gettext, $, _, Backbone, BookmarksListView, BookmarksCollection, MessageBannerView) {
|
||||
return Backbone.View.extend({
|
||||
|
||||
el: '.courseware-bookmarks-button',
|
||||
|
||||
loadingMessageElement: '#loading-message',
|
||||
errorMessageElement: '#error-message',
|
||||
|
||||
events: {
|
||||
'click .bookmarks-list-button': 'toggleBookmarksListView'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
var bookmarksCollection = new BookmarksCollection([],
|
||||
{
|
||||
course_id: $('.courseware-results').data('courseId'),
|
||||
url: $('.courseware-bookmarks-button').data('bookmarksApiUrl')
|
||||
}
|
||||
);
|
||||
this.bookmarksListView = new BookmarksListView(
|
||||
{
|
||||
collection: bookmarksCollection,
|
||||
loadingMessageView: new MessageBannerView({el: $(this.loadingMessageElement)}),
|
||||
errorMessageView: new MessageBannerView({el: $(this.errorMessageElement)})
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
toggleBookmarksListView: function() {
|
||||
if (this.bookmarksListView.areBookmarksVisible()) {
|
||||
this.bookmarksListView.hideBookmarks();
|
||||
this.$('.bookmarks-list-button').attr('aria-pressed', 'false');
|
||||
this.$('.bookmarks-list-button').removeClass('is-active').addClass('is-inactive');
|
||||
} else {
|
||||
this.bookmarksListView.showBookmarks();
|
||||
this.$('.bookmarks-list-button').attr('aria-pressed', 'true');
|
||||
this.$('.bookmarks-list-button').removeClass('is-inactive').addClass('is-active');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -3,10 +3,9 @@
|
||||
|
||||
define([
|
||||
'jquery',
|
||||
'logger',
|
||||
'js/bookmarks/views/bookmarks_list_button'
|
||||
'logger'
|
||||
],
|
||||
function($, Logger, BookmarksListButton) {
|
||||
function($, Logger) {
|
||||
return function() {
|
||||
// This function performs all actions common to all courseware.
|
||||
// 1. adding an event to all link clicks.
|
||||
@@ -18,9 +17,6 @@
|
||||
target_url: event.currentTarget.href
|
||||
});
|
||||
});
|
||||
|
||||
// 2. instantiating this button attaches events to all buttons in the courseware.
|
||||
new BookmarksListButton(); // eslint-disable-line no-new
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<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>
|
||||
@@ -1,311 +0,0 @@
|
||||
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 = {
|
||||
// Otherwise Istanbul which is used for coverage tracking will cause tests to not run.
|
||||
sourceFiles: [
|
||||
{pattern: 'coffee/src/**/!(*spec).js'},
|
||||
{pattern: 'course_bookmarks/**/!(*spec).js'},
|
||||
{pattern: 'course_experience/js/**/!(*spec).js'},
|
||||
{pattern: 'discussion/js/**/!(*spec).js'},
|
||||
{pattern: 'js/**/!(*spec|djangojs).js'},
|
||||
{pattern: 'lms/js/**/!(*spec).js'},
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
* done.
|
||||
*/
|
||||
modules: getModulesList([
|
||||
'course_bookmarks/js/course_bookmarks_factory',
|
||||
'course_experience/js/course_outline_factory',
|
||||
'discussion/js/discussion_board_factory',
|
||||
'discussion/js/discussion_profile_page_factory',
|
||||
'js/api_admin/catalog_preview_factory',
|
||||
|
||||
@@ -92,12 +92,6 @@
|
||||
'js/student_profile/views/learner_profile_factory': 'js/student_profile/views/learner_profile_factory',
|
||||
'js/student_profile/views/learner_profile_view': 'js/student_profile/views/learner_profile_view',
|
||||
'js/ccx/schedule': 'js/ccx/schedule',
|
||||
|
||||
'js/bookmarks/collections/bookmarks': 'js/bookmarks/collections/bookmarks',
|
||||
'js/bookmarks/models/bookmark': 'js/bookmarks/models/bookmark',
|
||||
'js/bookmarks/views/bookmarks_list_button': 'js/bookmarks/views/bookmarks_list_button',
|
||||
'js/bookmarks/views/bookmarks_list': 'js/bookmarks/views/bookmarks_list',
|
||||
'js/bookmarks/views/bookmark_button': 'js/bookmarks/views/bookmark_button',
|
||||
'js/views/message_banner': 'js/views/message_banner',
|
||||
|
||||
// edxnotes
|
||||
@@ -679,14 +673,16 @@
|
||||
});
|
||||
|
||||
testFiles = [
|
||||
'course_bookmarks/js/spec/bookmark_button_view_spec.js',
|
||||
'course_bookmarks/js/spec/bookmarks_list_view_spec.js',
|
||||
'course_bookmarks/js/spec/course_bookmarks_factory_spec.js',
|
||||
'course_experience/js/spec/course_outline_factory_spec.js',
|
||||
'discussion/js/spec/discussion_board_factory_spec.js',
|
||||
'discussion/js/spec/discussion_profile_page_factory_spec.js',
|
||||
'discussion/js/spec/discussion_board_view_spec.js',
|
||||
'discussion/js/spec/views/discussion_user_profile_view_spec.js',
|
||||
'lms/js/spec/preview/preview_factory_spec.js',
|
||||
'js/spec/api_admin/catalog_preview_spec.js',
|
||||
'js/spec/courseware/bookmark_button_view_spec.js',
|
||||
'js/spec/courseware/bookmarks_list_view_spec.js',
|
||||
'js/spec/ccx/schedule_spec.js',
|
||||
'js/spec/commerce/receipt_view_spec.js',
|
||||
'js/spec/components/card/card_spec.js',
|
||||
|
||||
@@ -62,10 +62,12 @@
|
||||
@import 'views/support';
|
||||
@import 'views/oauth2';
|
||||
@import "views/financial-assistance";
|
||||
@import 'views/bookmarks';
|
||||
@import 'course/auto-cert';
|
||||
@import 'views/api-access';
|
||||
|
||||
// features
|
||||
@import 'features/bookmarks-v1';
|
||||
|
||||
// search
|
||||
@import 'search/search';
|
||||
|
||||
|
||||
@@ -19,4 +19,10 @@
|
||||
@import 'shared-v2/modal';
|
||||
@import 'shared-v2/help-tab';
|
||||
|
||||
// Elements
|
||||
@import 'notifications';
|
||||
@import 'elements-v2/pagination';
|
||||
|
||||
// Features
|
||||
@import 'features/bookmarks';
|
||||
@import 'features/course-outline';
|
||||
|
||||
134
lms/static/sass/elements-v2/_pagination.scss
Normal file
134
lms/static/sass/elements-v2/_pagination.scss
Normal file
@@ -0,0 +1,134 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
64
lms/static/sass/features/_bookmarks-v1.scss
Normal file
64
lms/static/sass/features/_bookmarks-v1.scss
Normal file
@@ -0,0 +1,64 @@
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
87
lms/static/sass/features/_bookmarks.scss
Normal file
87
lms/static/sass/features/_bookmarks.scss
Normal file
@@ -0,0 +1,87 @@
|
||||
$bookmark-icon: "\f097"; // .fa-bookmark-o
|
||||
$bookmarked-icon: "\f02e"; // .fa-bookmark
|
||||
|
||||
// Rules for Bookmarks Results Header
|
||||
.bookmarks-results-header {
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
margin-bottom: ($baseline/2);
|
||||
}
|
||||
|
||||
// Rules for Bookmarks Results
|
||||
.bookmarks-results-list {
|
||||
padding-top: ($baseline/2);
|
||||
|
||||
.bookmarks-results-list-item {
|
||||
@include padding(0, $baseline, ($baseline/4), $baseline);
|
||||
display: block;
|
||||
border: 1px solid $lms-border-color;
|
||||
margin-bottom: $baseline;
|
||||
|
||||
&:hover {
|
||||
border-color: palette(primary, base);
|
||||
|
||||
.list-item-breadcrumbtrail {
|
||||
color: palette(primary, base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.results-list-item-view {
|
||||
@include float(right);
|
||||
margin-top: $baseline;
|
||||
}
|
||||
|
||||
.list-item-date {
|
||||
margin-top: ($baseline/4);
|
||||
color: $lms-gray;
|
||||
font-size: font-size(small);
|
||||
}
|
||||
|
||||
.bookmarks-results-list-item:before {
|
||||
content: $bookmarked-icon;
|
||||
position: relative;
|
||||
top: -7px;
|
||||
font-family: FontAwesome;
|
||||
color: palette(primary, base);
|
||||
}
|
||||
|
||||
.list-item-content {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-item-left-section {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.list-item-right-section {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
||||
.fa-arrow-right {
|
||||
|
||||
@include rtl {
|
||||
@include transform(rotate(180deg));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rules for empty bookmarks list
|
||||
.bookmarks-empty {
|
||||
margin-top: $baseline;
|
||||
border: 1px solid $lms-border-color;
|
||||
padding: $baseline;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.bookmarks-empty-header {
|
||||
@extend %t-title5;
|
||||
margin-bottom: ($baseline/2);
|
||||
}
|
||||
|
||||
.bookmarks-empty-detail {
|
||||
@extend %t-copy-sub1;
|
||||
}
|
||||
50
lms/static/sass/features/_course-outline.scss
Normal file
50
lms/static/sass/features/_course-outline.scss
Normal file
@@ -0,0 +1,50 @@
|
||||
.course-outline {
|
||||
color: $lms-gray;
|
||||
|
||||
.block-tree {
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
|
||||
.section {
|
||||
margin: 0 (-1 * $baseline);
|
||||
width: calc(100% + (2 * $baseline));
|
||||
padding: 0 ($baseline * 2);
|
||||
|
||||
&:not(:first-child) {
|
||||
border-top: 1px solid $lms-border-color;
|
||||
|
||||
.section-name {
|
||||
margin-top: $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.section-name {
|
||||
@include margin(0, 0, ($baseline / 2), ($baseline / 2));
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.outline-item {
|
||||
@include padding-left(0);
|
||||
}
|
||||
|
||||
ol.outline-item {
|
||||
margin: 0 0 ($baseline / 2) 0;
|
||||
|
||||
.subsection {
|
||||
list-style-type: none;
|
||||
|
||||
a.outline-item {
|
||||
display: block;
|
||||
padding: ($baseline / 2);
|
||||
|
||||
&:hover {
|
||||
background-color: palette(primary, x-back);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,16 +46,6 @@
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.form-actions > * {
|
||||
@include margin-left($baseline/2);
|
||||
vertical-align: middle;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.form-actions > button {
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.form-actions > *:first-child {
|
||||
@include margin-left(0);
|
||||
}
|
||||
|
||||
@@ -1,165 +0,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;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: lighten($action-primary-bg,10%);
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Rules for Bookmarks Results Header
|
||||
.bookmarks-results-header {
|
||||
@extend %t-title4;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
margin-bottom: ($baseline/2);
|
||||
}
|
||||
|
||||
// Rules for Bookmarks Results
|
||||
.bookmarks-results-list {
|
||||
padding-top: ($baseline/2);
|
||||
|
||||
.bookmarks-results-list-item {
|
||||
@include padding(0, $baseline, ($baseline/4), $baseline);
|
||||
display: block;
|
||||
border: 1px solid $gray-l4;
|
||||
margin-bottom: $baseline;
|
||||
|
||||
&:hover {
|
||||
border-color: $m-blue;
|
||||
|
||||
.list-item-breadcrumbtrail {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
@extend %t-icon6;
|
||||
}
|
||||
}
|
||||
|
||||
.results-list-item-view {
|
||||
@include float(right);
|
||||
margin-top: $baseline;
|
||||
}
|
||||
|
||||
.list-item-date {
|
||||
@extend %t-copy-sub2;
|
||||
margin-top: ($baseline/4);
|
||||
color: $gray;
|
||||
}
|
||||
|
||||
.bookmarks-results-list-item:before {
|
||||
content: $bookmarked-icon;
|
||||
position: relative;
|
||||
top: -7px;
|
||||
font-family: FontAwesome;
|
||||
color: $m-blue;
|
||||
}
|
||||
|
||||
.list-item-content {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-item-left-section {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.list-item-right-section {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
||||
.fa-arrow-right {
|
||||
|
||||
@include rtl {
|
||||
@include transform(rotate(180deg));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Rules for empty bookmarks list
|
||||
.bookmarks-empty {
|
||||
margin-top: $baseline;
|
||||
border: 1px solid $gray-l4;
|
||||
padding: $baseline;
|
||||
background-color: $gray-l6;
|
||||
}
|
||||
|
||||
.bookmarks-empty-header {
|
||||
@extend %t-title5;
|
||||
margin-bottom: ($baseline/2);
|
||||
}
|
||||
|
||||
.bookmarks-empty-detail {
|
||||
@extend %t-copy-sub1;
|
||||
}
|
||||
|
||||
|
||||
// Rules for bookmark icon shown on each sequence nav item
|
||||
.course-content {
|
||||
|
||||
.bookmark-icon.bookmarked {
|
||||
@include right($baseline / 4);
|
||||
top: -3px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
|
||||
// Rules for bookmark button's different styles
|
||||
.bookmark-button-wrapper {
|
||||
margin-bottom: ($baseline * 1.5);
|
||||
}
|
||||
|
||||
.bookmark-button {
|
||||
|
||||
&:before {
|
||||
content: $bookmark-icon;
|
||||
font-family: FontAwesome;
|
||||
}
|
||||
|
||||
&.bookmarked {
|
||||
&:before {
|
||||
content: $bookmarked-icon;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
<%page expression_filter="h" args="bookmark_id, is_bookmarked" />
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<%!
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
%>
|
||||
|
||||
<div class="bookmark-button-wrapper">
|
||||
<button class="btn btn-link bookmark-button ${"bookmarked" if is_bookmarked else ""}"
|
||||
aria-pressed="${"true" if is_bookmarked else "false"}"
|
||||
data-bookmark-id="${bookmark_id}">
|
||||
data-bookmark-id="${bookmark_id}"
|
||||
data-bookmarks-api-url="${reverse('bookmarks')}">
|
||||
<span class="bookmark-text">${_("Bookmarked") if is_bookmarked else _("Bookmark this page")}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
<div id="my-bookmarks" class="sr-is-focusable" tabindex="-1"></div>
|
||||
<h2 class="bookmarks-results-header"><%= gettext("My Bookmarks") %></h2>
|
||||
|
||||
<% if (bookmarksCollection.length) { %>
|
||||
|
||||
<div class="paging-header"></div>
|
||||
|
||||
<div class='bookmarks-results-list'>
|
||||
<% bookmarksCollection.each(function(bookmark, index) { %>
|
||||
<a class="bookmarks-results-list-item" href="<%= bookmark.blockUrl() %>" aria-labelledby="bookmark-link-<%= index %>" data-bookmark-id="<%= bookmark.get('id') %>" data-component-type="<%= bookmark.get('block_type') %>" data-usage-id="<%= bookmark.get('usage_id') %>" aria-describedby="bookmark-type-<%= index %> bookmark-date-<%= index %>">
|
||||
<div class="list-item-content">
|
||||
<div class="list-item-left-section">
|
||||
<h3 id="bookmark-link-<%= index %>" class="list-item-breadcrumbtrail"> <%= _.map(_.pluck(bookmark.get('path'), 'display_name'), _.escape).concat([_.escape(bookmark.get('display_name'))]).join(' <span class="icon fa fa-caret-right" aria-hidden="true"></span><span class="sr">-</span> ') %> </h3>
|
||||
<p id="bookmark-date-<%= index %>" class="list-item-date"> <%= gettext("Bookmarked on") %> <%= humanFriendlyDate(bookmark.get('created')) %> </p>
|
||||
</div>
|
||||
|
||||
<p id="bookmark-type-<%= index %>" class="list-item-right-section">
|
||||
<span aria-hidden="true"><%= gettext("View") %></span>
|
||||
<span class="icon fa fa-arrow-right" aria-hidden="true"></span>
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
<% }); %>
|
||||
</div>
|
||||
|
||||
<div class="paging-footer"></div>
|
||||
|
||||
<% } else {%>
|
||||
|
||||
<div class="bookmarks-empty">
|
||||
<div class="bookmarks-empty-header">
|
||||
<span class="icon fa fa-bookmark-o bookmarks-empty-header-icon" aria-hidden="true"></span>
|
||||
<%= gettext("You have not bookmarked any courseware pages yet.") %>
|
||||
<br>
|
||||
</div>
|
||||
<div class="bookmarks-empty-detail">
|
||||
<span class="bookmarks-empty-detail-title">
|
||||
<%= gettext("Use bookmarks to help you easily return to courseware pages. To bookmark a page, select Bookmark in the upper right corner of that page. To see a list of all your bookmarks, select Bookmarks in the upper left corner of any courseware page.") %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% } %>
|
||||
@@ -3,12 +3,15 @@
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
<%def name="online_help_token()"><% return "courseware" %></%def>
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
import waffle
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from edxnotes.helpers import is_feature_enabled as is_edxnotes_enabled
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
from openedx.core.djangolib.js_utils import js_escaped_string
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
%>
|
||||
<%
|
||||
include_special_exams = settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and (course.enable_proctored_exams or course.enable_timed_exams)
|
||||
@@ -27,7 +30,7 @@ from openedx.core.djangolib.js_utils import js_escaped_string
|
||||
|
||||
<%block name="header_extras">
|
||||
|
||||
% for template_name in ["image-modal"]:
|
||||
% for template_name in ["image-modal", "sequence-breadcrumbs"]:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="common/templates/${template_name}.underscore" />
|
||||
</script>
|
||||
@@ -115,10 +118,10 @@ ${HTML(fragment.foot_html())}
|
||||
|
||||
<div class="wrapper-course-modes">
|
||||
|
||||
<div class="courseware-bookmarks-button" data-bookmarks-api-url="${bookmarks_api_url}">
|
||||
<button type="button" class="bookmarks-list-button is-inactive" aria-pressed="false">
|
||||
<div class="courseware-bookmarks-button">
|
||||
<a class="bookmarks-list-button" href="${reverse('openedx.course_bookmarks.home', args=[course.id])}">
|
||||
${_('Bookmarks')}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
|
||||
@@ -152,7 +155,10 @@ ${HTML(fragment.foot_html())}
|
||||
% endif
|
||||
<section class="course-content" id="course-content">
|
||||
<main id="main" tabindex="-1" aria-label="Content">
|
||||
<div class="path"></div>
|
||||
<div
|
||||
class="path"
|
||||
data-unified-course-view="${'true' if waffle.flag_is_active(request, 'unified_course_view') else 'false'}"
|
||||
></div>
|
||||
% if getattr(course, 'entrance_exam_enabled') and \
|
||||
getattr(course, 'entrance_exam_minimum_score_pct') and \
|
||||
entrance_exam_current_score is not UNDEFINED:
|
||||
|
||||
17
lms/urls.py
17
lms/urls.py
@@ -601,10 +601,27 @@ urlpatterns += (
|
||||
name='edxnotes_endpoints',
|
||||
),
|
||||
|
||||
# Branding API
|
||||
url(
|
||||
r'^api/branding/v1/',
|
||||
include('branding.api_urls')
|
||||
),
|
||||
|
||||
# Course experience
|
||||
url(
|
||||
r'^courses/{}/course/'.format(
|
||||
settings.COURSE_ID_PATTERN,
|
||||
),
|
||||
include('openedx.features.course_experience.urls'),
|
||||
),
|
||||
|
||||
# Course bookmarks
|
||||
url(
|
||||
r'^courses/{}/bookmarks/'.format(
|
||||
settings.COURSE_ID_PATTERN,
|
||||
),
|
||||
include('openedx.features.course_bookmarks.urls'),
|
||||
),
|
||||
)
|
||||
|
||||
if settings.FEATURES["ENABLE_TEAMS"]:
|
||||
|
||||
@@ -45,21 +45,18 @@ class EdxFragmentView(FragmentView):
|
||||
else:
|
||||
return settings.PIPELINE_JS[group]['source_filenames']
|
||||
|
||||
@abstractmethod
|
||||
def vendor_js_dependencies(self):
|
||||
"""
|
||||
Returns list of the vendor JS files that this view depends on.
|
||||
"""
|
||||
return []
|
||||
|
||||
@abstractmethod
|
||||
def js_dependencies(self):
|
||||
"""
|
||||
Returns list of the JavaScript files that this view depends on.
|
||||
"""
|
||||
return []
|
||||
|
||||
@abstractmethod
|
||||
def css_dependencies(self):
|
||||
"""
|
||||
Returns list of the CSS files that this view depends on.
|
||||
|
||||
6
openedx/features/README.rst
Normal file
6
openedx/features/README.rst
Normal file
@@ -0,0 +1,6 @@
|
||||
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.
|
||||
0
openedx/features/__init__.py
Normal file
0
openedx/features/__init__.py
Normal file
0
openedx/features/course_bookmarks/__init__.py
Normal file
0
openedx/features/course_bookmarks/__init__.py
Normal file
@@ -1,12 +1,11 @@
|
||||
|
||||
<div class="message-banner" aria-live="polite"></div>
|
||||
|
||||
<div class="xblock xblock-student_view xblock-student_view-vertical xblock-initialized">
|
||||
<div class="bookmark-button-wrapper">
|
||||
<button class="btn bookmark-button"
|
||||
aria-pressed="false"
|
||||
data-bookmark-id="bilbo,usage_1">
|
||||
<span class="bookmark-text">Bookmark this page</span>
|
||||
<button class="btn bookmark-button"
|
||||
aria-pressed="false"
|
||||
data-bookmark-id="bilbo,usage_1">
|
||||
<span class="bookmark-text">Bookmark this page</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
<div class="course-view container" id="course-container">
|
||||
<header class="page-header has-secondary">
|
||||
<div class="page-header-main">
|
||||
<nav aria-label="My Bookmarks" class="sr-is-focusable" tabindex="-1">
|
||||
<div class="has-breadcrumbs"><div class="breadcrumbs">
|
||||
<span class="nav-item">
|
||||
<a href="/courses/course-v1:test-course/course/">Course</a>
|
||||
</span>
|
||||
<span class="icon fa fa-angle-right" aria-hidden="true"></span>
|
||||
<span class="nav-item">My Bookmarks</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<div class="page-content">
|
||||
<div class="course-bookmarks courseware-results-wrapper" id="main">
|
||||
<div id="loading-message" aria-live="polite" aria-relevant="all"></div>
|
||||
<div id="error-message" aria-live="polite"></div>
|
||||
<div class="courseware-results search-results" data-course-id="course-v1:test-course" data-lang-code="en"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3,7 +3,7 @@
|
||||
define([
|
||||
'backbone',
|
||||
'edx-ui-toolkit/js/pagination/paging-collection',
|
||||
'js/bookmarks/models/bookmark'
|
||||
'course_bookmarks/js/models/bookmark'
|
||||
], function(Backbone, PagingCollection, BookmarkModel) {
|
||||
return PagingCollection.extend({
|
||||
model: BookmarkModel,
|
||||
@@ -24,5 +24,5 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
})(define || RequireJS.define);
|
||||
}(define || RequireJS.define));
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
(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 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
})(define || RequireJS.define);
|
||||
}(define || RequireJS.define));
|
||||
@@ -1,23 +1,24 @@
|
||||
define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
|
||||
'common/js/spec_helpers/template_helpers', 'js/bookmarks/views/bookmark_button'
|
||||
],
|
||||
define([
|
||||
'backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
|
||||
'common/js/spec_helpers/template_helpers', 'course_bookmarks/js/views/bookmark_button'
|
||||
],
|
||||
function(Backbone, $, _, AjaxHelpers, TemplateHelpers, BookmarkButtonView) {
|
||||
'use strict';
|
||||
|
||||
describe('bookmarks.button', function() {
|
||||
var timerCallback;
|
||||
describe('BookmarkButtonView', function() {
|
||||
var createBookmarkButtonView, verifyBookmarkButtonState;
|
||||
|
||||
var API_URL = 'bookmarks/api/v1/bookmarks/';
|
||||
|
||||
beforeEach(function() {
|
||||
loadFixtures('js/fixtures/bookmarks/bookmark_button.html');
|
||||
loadFixtures('course_bookmarks/fixtures/bookmark_button.html');
|
||||
TemplateHelpers.installTemplates(
|
||||
[
|
||||
'templates/fields/message_banner'
|
||||
]
|
||||
);
|
||||
|
||||
timerCallback = jasmine.createSpy('timerCallback');
|
||||
jasmine.createSpy('timerCallback');
|
||||
jasmine.clock().install();
|
||||
});
|
||||
|
||||
@@ -25,7 +26,7 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helper
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
|
||||
var createBookmarkButtonView = function(isBookmarked) {
|
||||
createBookmarkButtonView = function(isBookmarked) {
|
||||
return new BookmarkButtonView({
|
||||
el: '.bookmark-button',
|
||||
bookmarked: isBookmarked,
|
||||
@@ -35,7 +36,7 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helper
|
||||
});
|
||||
};
|
||||
|
||||
var verifyBookmarkButtonState = function(view, bookmarked) {
|
||||
verifyBookmarkButtonState = function(view, bookmarked) {
|
||||
if (bookmarked) {
|
||||
expect(view.$el).toHaveAttr('aria-pressed', 'true');
|
||||
expect(view.$el).toHaveClass('bookmarked');
|
||||
@@ -46,7 +47,7 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helper
|
||||
expect(view.$el.data('bookmarkId')).toBe('bilbo,usage_1');
|
||||
};
|
||||
|
||||
it('rendered correctly ', function() {
|
||||
it('rendered correctly', function() {
|
||||
var view = createBookmarkButtonView(false);
|
||||
verifyBookmarkButtonState(view, false);
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
(function(define, undefined) {
|
||||
(function(define) {
|
||||
'use strict';
|
||||
define(['gettext', 'jquery', 'underscore', 'backbone', 'js/views/message_banner'],
|
||||
function(gettext, $, _, Backbone, MessageBannerView) {
|
||||
@@ -9,7 +9,7 @@
|
||||
bookmarkedText: gettext('Bookmarked'),
|
||||
|
||||
events: {
|
||||
'click': 'toggleBookmark'
|
||||
click: 'toggleBookmark'
|
||||
},
|
||||
|
||||
showBannerInterval: 5000, // time in ms
|
||||
@@ -46,14 +46,14 @@
|
||||
view.setBookmarkState(true);
|
||||
},
|
||||
error: function(jqXHR) {
|
||||
var response, userMessage;
|
||||
try {
|
||||
var response = jqXHR.responseText ? JSON.parse(jqXHR.responseText) : '';
|
||||
var userMessage = response ? response.user_message : '';
|
||||
response = jqXHR.responseText ? JSON.parse(jqXHR.responseText) : '';
|
||||
userMessage = response ? response.user_message : '';
|
||||
view.showError(userMessage);
|
||||
} catch (err) {
|
||||
view.showError();
|
||||
}
|
||||
catch (err) {
|
||||
view.showError();
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
view.$el.prop('disabled', false);
|
||||
@@ -1,11 +1,12 @@
|
||||
(function(define, undefined) {
|
||||
(function(define) {
|
||||
'use strict';
|
||||
define(['gettext', 'jquery', 'underscore', 'backbone', 'logger', 'moment', 'edx-ui-toolkit/js/utils/html-utils',
|
||||
'common/js/components/views/paging_header', 'common/js/components/views/paging_footer',
|
||||
'text!templates/bookmarks/bookmarks-list.underscore'
|
||||
],
|
||||
define([
|
||||
'gettext', 'jquery', 'underscore', 'backbone', 'logger', 'moment', 'edx-ui-toolkit/js/utils/html-utils',
|
||||
'common/js/components/views/paging_header', 'common/js/components/views/paging_footer',
|
||||
'text!course_bookmarks/templates/bookmarks-list.underscore'
|
||||
],
|
||||
function(gettext, $, _, Backbone, Logger, _moment, HtmlUtils,
|
||||
PagingHeaderView, PagingFooterView, BookmarksListTemplate) {
|
||||
PagingHeaderView, PagingFooterView, bookmarksListTemplate) {
|
||||
var moment = _moment || window.moment;
|
||||
|
||||
return Backbone.View.extend({
|
||||
@@ -15,7 +16,7 @@
|
||||
coursewareResultsWrapperEl: '.courseware-results-wrapper',
|
||||
|
||||
errorIcon: '<span class="fa fa-fw fa-exclamation-triangle message-error" aria-hidden="true"></span>',
|
||||
loadingIcon: '<span class="fa fa-fw fa-spinner fa-pulse message-in-progress" aria-hidden="true"></span>',
|
||||
loadingIcon: '<span class="fa fa-fw fa-spinner fa-pulse message-in-progress" aria-hidden="true"></span>', // eslint-disable-line max-len
|
||||
|
||||
errorMessage: gettext('An error has occurred. Please try again.'),
|
||||
loadingMessage: gettext('Loading'),
|
||||
@@ -27,7 +28,7 @@
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
this.template = HtmlUtils.template(BookmarksListTemplate);
|
||||
this.template = HtmlUtils.template(bookmarksListTemplate);
|
||||
this.loadingMessageView = options.loadingMessageView;
|
||||
this.errorMessageView = options.errorMessageView;
|
||||
this.langCode = $(this.el).data('langCode');
|
||||
@@ -65,47 +66,39 @@
|
||||
},
|
||||
|
||||
visitBookmark: function(event) {
|
||||
var bookmarkedComponent = $(event.currentTarget);
|
||||
var bookmark_id = bookmarkedComponent.data('bookmarkId');
|
||||
var component_usage_id = bookmarkedComponent.data('usageId');
|
||||
var component_type = bookmarkedComponent.data('componentType');
|
||||
var $bookmarkedComponent = $(event.currentTarget),
|
||||
bookmarkId = $bookmarkedComponent.data('bookmarkId'),
|
||||
componentUsageId = $bookmarkedComponent.data('usageId'),
|
||||
componentType = $bookmarkedComponent.data('componentType');
|
||||
Logger.log(
|
||||
'edx.bookmark.accessed',
|
||||
'edx.bookmark.accessed',
|
||||
{
|
||||
bookmark_id: bookmark_id,
|
||||
component_type: component_type,
|
||||
component_usage_id: component_usage_id
|
||||
bookmark_id: bookmarkId,
|
||||
component_type: componentType,
|
||||
component_usage_id: componentUsageId
|
||||
}
|
||||
).always(function() {
|
||||
window.location.href = event.currentTarget.pathname;
|
||||
});
|
||||
).always(function() {
|
||||
window.location.href = event.currentTarget.pathname;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert ISO 8601 formatted date into human friendly format. e.g, `2014-05-23T14:00:00Z` to `May 23, 2014`
|
||||
* @param {String} isoDate - ISO 8601 formatted date string.
|
||||
*/
|
||||
/**
|
||||
* Convert ISO 8601 formatted date into human friendly format.
|
||||
*
|
||||
* e.g, `2014-05-23T14:00:00Z` to `May 23, 2014`
|
||||
*
|
||||
* @param {String} isoDate - ISO 8601 formatted date string.
|
||||
*/
|
||||
humanFriendlyDate: function(isoDate) {
|
||||
moment.locale(this.langCode);
|
||||
return moment(isoDate).format('LL');
|
||||
},
|
||||
|
||||
areBookmarksVisible: function() {
|
||||
return this.$('#my-bookmarks').is(':visible');
|
||||
},
|
||||
|
||||
hideBookmarks: function() {
|
||||
this.$el.hide();
|
||||
$(this.coursewareResultsWrapperEl).hide();
|
||||
$(this.coursewareContentEl).css('display', 'table-cell');
|
||||
},
|
||||
|
||||
showBookmarksContainer: function() {
|
||||
$(this.coursewareContentEl).hide();
|
||||
// Empty el if it's not empty to get the clean state.
|
||||
// Empty el if it's not empty to get the clean state.
|
||||
this.$el.html('');
|
||||
this.$el.show();
|
||||
$(this.coursewareResultsWrapperEl).css('display', 'table-cell');
|
||||
},
|
||||
|
||||
showLoadingMessage: function() {
|
||||
@@ -0,0 +1,54 @@
|
||||
<div id="my-bookmarks" class="sr-is-focusable" tabindex="-1"></div>
|
||||
|
||||
<% if (bookmarksCollection.length) { %>
|
||||
|
||||
<div class="paging-header"></div>
|
||||
|
||||
<div class='bookmarks-results-list'>
|
||||
<% bookmarksCollection.each(function(bookmark, index) { %>
|
||||
<a class="bookmarks-results-list-item"
|
||||
href="<%- bookmark.blockUrl() %>"
|
||||
aria-labelledby="bookmark-link-<%- index %>"
|
||||
data-bookmark-id="<%- bookmark.get('id') %>"
|
||||
data-component-type="<%- bookmark.get('block_type') %>"
|
||||
data-usage-id="<%- bookmark.get('usage_id') %>"
|
||||
aria-describedby="bookmark-type-<%- index %> bookmark-date-<%- index %>">
|
||||
<div class="list-item-content">
|
||||
<div class="list-item-left-section">
|
||||
<h3 id="bookmark-link-<%- index %>" class="list-item-breadcrumbtrail">
|
||||
<%=
|
||||
HtmlUtils.HTML(_.map(_.pluck(bookmark.get('path'), 'display_name'), _.escape)
|
||||
.concat([_.escape(bookmark.get('display_name'))])
|
||||
.join(' <span class="icon fa fa-caret-right" aria-hidden="true"></span><span class="sr">-</span> '))
|
||||
%>
|
||||
</h3>
|
||||
<p id="bookmark-date-<%- index %>" class="list-item-date"> <%- gettext("Bookmarked on") %> <%- humanFriendlyDate(bookmark.get('created')) %> </p>
|
||||
</div>
|
||||
|
||||
<p id="bookmark-type-<%- index %>" class="list-item-right-section">
|
||||
<span aria-hidden="true"><%- gettext("View") %></span>
|
||||
<span class="icon fa fa-arrow-right" aria-hidden="true"></span>
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
<% }); %>
|
||||
</div>
|
||||
|
||||
<div class="paging-footer"></div>
|
||||
|
||||
<% } else {%>
|
||||
|
||||
<div class="bookmarks-empty">
|
||||
<h3 class="hd-4 bookmarks-empty-header">
|
||||
<span class="icon fa fa-bookmark-o bookmarks-empty-header-icon" aria-hidden="true"></span>
|
||||
<%- gettext("You have not bookmarked any courseware pages yet") %>
|
||||
<br>
|
||||
</h3>
|
||||
<div class="bookmarks-empty-detail">
|
||||
<span class="bookmarks-empty-detail-title">
|
||||
<%- gettext('Use bookmarks to help you easily return to courseware pages. To bookmark a page, click "Bookmark this page" under the page title.') %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% } %>
|
||||
@@ -0,0 +1,11 @@
|
||||
## 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>
|
||||
@@ -0,0 +1,59 @@
|
||||
## 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>
|
||||
@@ -0,0 +1,15 @@
|
||||
## 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);
|
||||
20
openedx/features/course_bookmarks/urls.py
Normal file
20
openedx/features/course_bookmarks/urls.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
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',
|
||||
),
|
||||
]
|
||||
0
openedx/features/course_bookmarks/views/__init__.py
Normal file
0
openedx/features/course_bookmarks/views/__init__.py
Normal file
82
openedx/features/course_bookmarks/views/course_bookmarks.py
Normal file
82
openedx/features/course_bookmarks/views/course_bookmarks.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
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
|
||||
0
openedx/features/course_experience/__init__.py
Normal file
0
openedx/features/course_experience/__init__.py
Normal file
@@ -0,0 +1,124 @@
|
||||
<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>
|
||||
@@ -0,0 +1,29 @@
|
||||
(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);
|
||||
@@ -0,0 +1,86 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,69 @@
|
||||
## 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>
|
||||
@@ -0,0 +1,44 @@
|
||||
## 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>
|
||||
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
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)
|
||||
21
openedx/features/course_experience/urls.py
Normal file
21
openedx/features/course_experience/urls.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
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',
|
||||
),
|
||||
]
|
||||
50
openedx/features/course_experience/views/course_home.py
Normal file
50
openedx/features/course_experience/views/course_home.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
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)
|
||||
61
openedx/features/course_experience/views/course_outline.py
Normal file
61
openedx/features/course_experience/views/course_outline.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Views to show a course outline.
|
||||
"""
|
||||
|
||||
from django.core.context_processors import csrf
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from courseware.courses import get_course_with_access
|
||||
from lms.djangoapps.course_api.blocks.api import get_blocks
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from web_fragments.fragment import Fragment
|
||||
from web_fragments.views import FragmentView
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
class CourseOutlineFragmentView(FragmentView):
|
||||
"""
|
||||
Course outline fragment to be shown in the unified course view.
|
||||
"""
|
||||
|
||||
def populate_children(self, block, all_blocks):
|
||||
"""
|
||||
For a passed block, replace each id in its children array with the full representation of that child,
|
||||
which will be looked up by id in the passed all_blocks dict.
|
||||
Recursively do the same replacement for children of those children.
|
||||
"""
|
||||
children = block.get('children') or []
|
||||
|
||||
for i in range(len(children)):
|
||||
child_id = block['children'][i]
|
||||
child_detail = self.populate_children(all_blocks[child_id], all_blocks)
|
||||
block['children'][i] = child_detail
|
||||
|
||||
return block
|
||||
|
||||
def render_to_fragment(self, request, course_id=None, **kwargs):
|
||||
"""
|
||||
Renders the course outline as a fragment.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
|
||||
course_usage_key = modulestore().make_course_usage_key(course_key)
|
||||
all_blocks = get_blocks(
|
||||
request,
|
||||
course_usage_key,
|
||||
user=request.user,
|
||||
nav_depth=3,
|
||||
requested_fields=['children', 'display_name', 'type'],
|
||||
block_types_filter=['course', 'chapter', 'sequential']
|
||||
)
|
||||
|
||||
course_block_tree = all_blocks['blocks'][all_blocks['root']] # Get the root of the block tree
|
||||
|
||||
context = {
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'course': course,
|
||||
# Recurse through the block tree, fleshing out each child object
|
||||
'blocks': self.populate_children(course_block_tree, all_blocks['blocks'])
|
||||
}
|
||||
html = render_to_string('course_experience/course-outline-fragment.html', context)
|
||||
return Fragment(html)
|
||||
@@ -5,13 +5,20 @@ from paver.easy import sh, task, cmdopts, needs, BuildFailure
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from string import join
|
||||
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
|
||||
from .utils.envs import Env
|
||||
from .utils.timer import timed
|
||||
|
||||
ALL_SYSTEMS = 'lms,cms,common,openedx,pavelib'
|
||||
ALL_SYSTEMS = [
|
||||
'cms',
|
||||
'common',
|
||||
'lms',
|
||||
'openedx',
|
||||
'pavelib',
|
||||
]
|
||||
|
||||
|
||||
def top_python_dirs(dirname):
|
||||
@@ -45,7 +52,7 @@ def find_fixme(options):
|
||||
Run pylint on system code, only looking for fixme items.
|
||||
"""
|
||||
num_fixme = 0
|
||||
systems = getattr(options, 'system', ALL_SYSTEMS).split(',')
|
||||
systems = getattr(options, 'system', '').split(',') or ALL_SYSTEMS
|
||||
|
||||
for system in systems:
|
||||
# Directory to put the pylint report in.
|
||||
@@ -93,7 +100,7 @@ def run_pylint(options):
|
||||
num_violations = 0
|
||||
violations_limit = int(getattr(options, 'limit', -1))
|
||||
errors = getattr(options, 'errors', False)
|
||||
systems = getattr(options, 'system', ALL_SYSTEMS).split(',')
|
||||
systems = getattr(options, 'system', '').split(',') or ALL_SYSTEMS
|
||||
|
||||
# Make sure the metrics subdirectory exists
|
||||
Env.METRICS_DIR.makedirs_p()
|
||||
@@ -234,7 +241,7 @@ def run_complexity():
|
||||
Uses radon to examine cyclomatic complexity.
|
||||
For additional details on radon, see http://radon.readthedocs.org/
|
||||
"""
|
||||
system_string = 'cms/ lms/ common/ openedx/'
|
||||
system_string = join(ALL_SYSTEMS, '/ ') + '/'
|
||||
complexity_report_dir = (Env.REPORT_DIR / "complexity")
|
||||
complexity_report = complexity_report_dir / "python_complexity.log"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user