Merge pull request #17902 from edx/rm-new-course-outline-waffle
EDUCATOR 2283 - Removes the new course outline waffle flag.
This commit is contained in:
@@ -5,6 +5,8 @@ LMS Course Home page object
|
||||
from collections import OrderedDict
|
||||
|
||||
from bok_choy.page_object import PageObject
|
||||
from bok_choy.promise import BrokenPromise
|
||||
from six import text_type
|
||||
|
||||
from .bookmarks import BookmarksPage
|
||||
from .course_page import CoursePage
|
||||
@@ -80,15 +82,11 @@ class CourseOutlinePage(PageObject):
|
||||
|
||||
url = None
|
||||
|
||||
SECTION_SELECTOR = '.outline-item.section:nth-of-type({0})'
|
||||
SECTION_TITLES_SELECTOR = '.section-name h3'
|
||||
SUBSECTION_SELECTOR = SECTION_SELECTOR + ' .subsection:nth-of-type({1}) .outline-item'
|
||||
SUBSECTION_TITLES_SELECTOR = SECTION_SELECTOR + ' .subsection .subsection-title .subsection-title-name'
|
||||
OUTLINE_RESUME_COURSE_SELECTOR = '.outline-item .resume-right'
|
||||
|
||||
def __init__(self, browser, parent_page):
|
||||
super(CourseOutlinePage, self).__init__(browser)
|
||||
self.parent_page = parent_page
|
||||
self._section_selector = '.outline-item.section'
|
||||
self._subsection_selector = '.subsection.accordion'
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return self.parent_page.is_browser_on_page
|
||||
@@ -108,28 +106,14 @@ class CourseOutlinePage(PageObject):
|
||||
|
||||
You can use these titles in `go_to_section` to navigate to the section.
|
||||
"""
|
||||
# Dict to store the result
|
||||
outline_dict = OrderedDict()
|
||||
|
||||
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:
|
||||
raise ValueError("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
|
||||
return self._get_outline_structure_as_dictionary()
|
||||
|
||||
@property
|
||||
def num_sections(self):
|
||||
"""
|
||||
Return the number of sections
|
||||
"""
|
||||
return len(self.q(css=self.SECTION_TITLES_SELECTOR))
|
||||
return len(self._get_sections_as_selenium_webelements())
|
||||
|
||||
@property
|
||||
def num_subsections(self, section_title=None):
|
||||
@@ -145,14 +129,19 @@ class CourseOutlinePage(PageObject):
|
||||
if not section_index:
|
||||
return
|
||||
else:
|
||||
section_index = 1
|
||||
section_index = 0
|
||||
|
||||
return len(self.q(css=self.SUBSECTION_TITLES_SELECTOR.format(section_index)))
|
||||
sections = self._get_sections_as_selenium_webelements()
|
||||
subsections = self._get_subsections(sections[section_index])
|
||||
return len(subsections)
|
||||
|
||||
@property
|
||||
def num_units(self):
|
||||
"""
|
||||
Return the number of units in the first subsection
|
||||
Return the number of units in the first subsection.
|
||||
|
||||
This method returns the number of units in the horizontal navigation
|
||||
bar; not the course outline.
|
||||
"""
|
||||
return len(self.q(css='.sequence-list-wrapper ol li'))
|
||||
|
||||
@@ -165,22 +154,23 @@ class CourseOutlinePage(PageObject):
|
||||
Example:
|
||||
go_to_section("Week 1", "Lesson 1")
|
||||
"""
|
||||
section_index = self._section_title_to_index(section_title)
|
||||
if section_index is None:
|
||||
raise ValueError("Could not find section '{0}'".format(section_title))
|
||||
subsection_webelements = self._get_subsections_as_selenium_webelements()
|
||||
subsection_titles = [self._get_outline_element_title(sub_webel)
|
||||
for sub_webel in subsection_webelements]
|
||||
|
||||
try:
|
||||
subsection_index = self._subsection_titles(section_index + 1).index(subsection_title)
|
||||
subsection_index = subsection_titles.index(text_type(subsection_title))
|
||||
except ValueError:
|
||||
raise ValueError("Could not find subsection '{0}' in section '{1}'".format(
|
||||
subsection_title, section_title
|
||||
))
|
||||
|
||||
# Convert list indices (start at zero) to CSS indices (start at 1)
|
||||
subsection_css = self.SUBSECTION_SELECTOR.format(section_index + 1, subsection_index + 1)
|
||||
target_subsection = subsection_webelements[subsection_index]
|
||||
units = self._get_units(target_subsection)
|
||||
|
||||
# Click the subsection and ensure that the page finishes reloading
|
||||
self.q(css=subsection_css).first.click()
|
||||
# Click the subsection's first problem and ensure that the page finishes
|
||||
# reloading
|
||||
units[0].click()
|
||||
|
||||
self._wait_for_course_section(section_title, subsection_title)
|
||||
|
||||
@@ -200,7 +190,7 @@ class CourseOutlinePage(PageObject):
|
||||
except IndexError:
|
||||
raise ValueError("Section index '{0}' is out of range.".format(section_index))
|
||||
try:
|
||||
subsection_title = self._subsection_titles(section_index + 1)[subsection_index]
|
||||
subsection_title = self._subsection_titles(section_index)[subsection_index]
|
||||
except IndexError:
|
||||
raise ValueError("Subsection index '{0}' in section index '{1}' is out of range.".format(
|
||||
subsection_index, section_index
|
||||
@@ -223,7 +213,7 @@ class CourseOutlinePage(PageObject):
|
||||
"""
|
||||
Navigate to courseware using Resume Course button in the header.
|
||||
"""
|
||||
self.q(css=self.OUTLINE_RESUME_COURSE_SELECTOR).first.click()
|
||||
self.q(css='.btn.btn-primary.action-resume-course').results[0].click()
|
||||
courseware_page = CoursewarePage(self.browser, self.parent_page.course_id)
|
||||
courseware_page.wait_for_page()
|
||||
|
||||
@@ -231,21 +221,26 @@ class CourseOutlinePage(PageObject):
|
||||
"""
|
||||
Return a list of all section titles on the page.
|
||||
"""
|
||||
return self.q(css=self.SECTION_TITLES_SELECTOR).map(lambda el: el.text.strip()).results
|
||||
outline_sections = self._get_sections_as_selenium_webelements()
|
||||
section_titles = [self._get_outline_element_title(section) for section in outline_sections]
|
||||
return section_titles
|
||||
|
||||
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).
|
||||
for the section at index `section_index` (starts at 0).
|
||||
"""
|
||||
subsection_css = self.SUBSECTION_TITLES_SELECTOR.format(section_index)
|
||||
return self.q(css=subsection_css).map(
|
||||
lambda el: el.get_attribute('innerHTML').strip()
|
||||
).results
|
||||
outline_sections = self._get_sections_as_selenium_webelements()
|
||||
target_section = outline_sections[section_index]
|
||||
target_subsections = self._get_subsections(target_section)
|
||||
subsection_titles = [self._get_outline_element_title(subsection)
|
||||
for subsection in target_subsections]
|
||||
return subsection_titles
|
||||
|
||||
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.
|
||||
Ensures the user navigates to the course content page with the correct section and
|
||||
subsection.
|
||||
"""
|
||||
courseware_page = CoursewarePage(self.browser, self.parent_page.course_id)
|
||||
courseware_page.wait_for_page()
|
||||
@@ -255,10 +250,78 @@ class CourseOutlinePage(PageObject):
|
||||
courseware_page.nav.visit_course_outline_page()
|
||||
|
||||
self.wait_for(
|
||||
promise_check_func=lambda: courseware_page.nav.is_on_section(section_title, subsection_title),
|
||||
description="Waiting for course page with section '{0}' and subsection '{1}'".format(section_title, subsection_title)
|
||||
promise_check_func=lambda: courseware_page.nav.is_on_section(
|
||||
section_title, subsection_title),
|
||||
description="Waiting for course page with section '{0}' and subsection '{1}'".format(
|
||||
section_title, subsection_title)
|
||||
)
|
||||
|
||||
def _get_outline_structure_as_dictionary(self):
|
||||
'''
|
||||
Implements self.sections().
|
||||
'''
|
||||
outline_dict = OrderedDict()
|
||||
|
||||
try:
|
||||
outline_sections = self._get_sections_as_selenium_webelements()
|
||||
except BrokenPromise:
|
||||
outline_sections = []
|
||||
|
||||
for section in outline_sections:
|
||||
subsections = self._get_subsections(section)
|
||||
section_title = self._get_outline_element_title(section)
|
||||
subsection_titles = [self._get_outline_element_title(subsection)
|
||||
for subsection in subsections]
|
||||
outline_dict[section_title] = subsection_titles
|
||||
|
||||
return outline_dict
|
||||
|
||||
@staticmethod
|
||||
def _is_html_element_aria_expanded(html_element):
|
||||
return html_element.get_attribute('aria-expanded') == u'true'
|
||||
|
||||
@staticmethod
|
||||
def _get_outline_element_title(outline_element):
|
||||
return outline_element.text.split('\n')[0]
|
||||
|
||||
def _get_subsections(self, section):
|
||||
self._expand_all_outline_folds()
|
||||
return section.find_elements_by_css_selector(self._subsection_selector)
|
||||
|
||||
def _get_units(self, subsection):
|
||||
self._expand_all_outline_folds()
|
||||
return subsection.find_elements_by_tag_name('a')
|
||||
|
||||
def _get_sections_as_selenium_webelements(self):
|
||||
self._expand_all_outline_folds()
|
||||
return self.q(css=self._section_selector).results
|
||||
|
||||
def _get_subsections_as_selenium_webelements(self):
|
||||
self._expand_all_outline_folds()
|
||||
return self.q(css=self._subsection_selector).results
|
||||
|
||||
def _expand_all_outline_folds(self):
|
||||
'''
|
||||
Expands all parts of the collapsible outline.
|
||||
'''
|
||||
section_button_selector = '.section-name.accordion-trigger'
|
||||
subsection_button_selector = '.subsection-text.accordion-trigger'
|
||||
self._expand_outline_fold(section_button_selector)
|
||||
self._expand_outline_fold(subsection_button_selector)
|
||||
|
||||
def _expand_outline_fold(self, fold_selector):
|
||||
'''
|
||||
Ensures an outline fold is loaded, then clicks it open.
|
||||
'''
|
||||
folds_as_elements = self.q(css=fold_selector)
|
||||
self.wait_for_element_visibility(
|
||||
fold_selector, "'{}' is visible".format(fold_selector)
|
||||
)
|
||||
|
||||
for fold_element in folds_as_elements:
|
||||
if not self._is_html_element_aria_expanded(fold_element):
|
||||
fold_element.click()
|
||||
|
||||
|
||||
class CourseSearchResultsPage(CoursePage):
|
||||
"""
|
||||
|
||||
@@ -91,8 +91,8 @@ class CourseHomeTest(CourseHomeBaseTest):
|
||||
|
||||
# Check that the course navigation appears correctly
|
||||
EXPECTED_SECTIONS = {
|
||||
'Test Section': ['Test Subsection'],
|
||||
'Test Section 2': ['Test Subsection 2', 'Test Subsection 3']
|
||||
u'Test Section': [u'Test Subsection'],
|
||||
u'Test Section 2': [u'Test Subsection 2', u'Test Subsection 3']
|
||||
}
|
||||
|
||||
actual_sections = self.course_home_page.outline.sections
|
||||
@@ -101,7 +101,7 @@ class CourseHomeTest(CourseHomeBaseTest):
|
||||
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')
|
||||
self.course_home_page.outline.go_to_section(u'Test Section', u'Test Subsection')
|
||||
|
||||
# Check the sequence items on the courseware page
|
||||
EXPECTED_ITEMS = ['Test Problem 1', 'Test Problem 2', 'Test HTML']
|
||||
|
||||
@@ -639,24 +639,6 @@ class CoursewareMultipleVerticalsTest(CoursewareMultipleVerticalsTestBase):
|
||||
self.assertIn('html 2 dummy body', html2_page.get_selected_tab_content())
|
||||
|
||||
|
||||
@attr('a11y')
|
||||
class CoursewareMultipleVerticalsA11YTest(CoursewareMultipleVerticalsTestBase):
|
||||
"""
|
||||
Test a11y for courseware with multiple verticals
|
||||
"""
|
||||
|
||||
def test_courseware_a11y(self):
|
||||
"""
|
||||
Run accessibility audit for the problem type.
|
||||
"""
|
||||
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'])
|
||||
self.courseware_page.a11y_audit.check_for_accessibility_errors()
|
||||
|
||||
|
||||
@attr(shard=9)
|
||||
class ProblemStateOnNavigationTest(UniqueCourseTest):
|
||||
"""
|
||||
|
||||
@@ -182,17 +182,10 @@ class GatingTest(UniqueCourseTest):
|
||||
Then I can see a gated subsection
|
||||
The gated subsection should have a lock icon
|
||||
and be in the format: "<Subsection Title> (Prerequisite Required)"
|
||||
When I fufill the gating prerequisite
|
||||
Then I can see the gated subsection (without a banner)
|
||||
"""
|
||||
self._setup_prereq()
|
||||
self._setup_gated_subsection()
|
||||
|
||||
# Fulfill prerequisites for specific student
|
||||
self._auto_auth(self.STUDENT_USERNAME, self.STUDENT_EMAIL, False)
|
||||
self.courseware_page.visit()
|
||||
self._fulfill_prerequisite()
|
||||
|
||||
self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True)
|
||||
|
||||
self.course_home_page.visit()
|
||||
@@ -216,11 +209,3 @@ class GatingTest(UniqueCourseTest):
|
||||
self.courseware_page.wait_for_page()
|
||||
# banner displayed informing section is a prereq
|
||||
self.assertTrue(self.courseware_page.has_banner())
|
||||
|
||||
self.course_home_page.visit()
|
||||
self.course_home_page.preview.set_staff_view_mode_specific_student(self.STUDENT_USERNAME)
|
||||
self.course_home_page.wait_for_page()
|
||||
self.assertEqual(self.course_home_page.outline.num_subsections, 2)
|
||||
self.course_home_page.outline.go_to_section('Test Section 1', 'Test Subsection 2')
|
||||
self.courseware_page.wait_for_page()
|
||||
self.assertFalse(self.courseware_page.has_banner())
|
||||
|
||||
@@ -302,7 +302,7 @@
|
||||
}
|
||||
|
||||
// Course outline for visual progress waffle switch
|
||||
.course-outline-visualprogress {
|
||||
.course-outline {
|
||||
.block-tree {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -451,111 +451,6 @@ button.accordion-trigger, button.prerequisite-button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
// Course outline
|
||||
.course-outline {
|
||||
.block-tree {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
|
||||
.section {
|
||||
margin: 0 (-1 * $baseline);
|
||||
width: calc(100% + (2 * $baseline));
|
||||
padding: 0 ($baseline);
|
||||
|
||||
&:not(:first-child) {
|
||||
.section-name {
|
||||
margin-top: $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.section-name {
|
||||
@include margin(0, 0, ($baseline / 2), ($baseline / 2));
|
||||
|
||||
padding: 0;
|
||||
|
||||
.section-title {
|
||||
font-weight: $font-bold;
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.outline-item {
|
||||
@include padding-left(0);
|
||||
}
|
||||
|
||||
ol.outline-item {
|
||||
padding-bottom: $baseline;
|
||||
border-bottom: 1px solid $border-color;
|
||||
margin: 0 0 ($baseline / 2) 0;
|
||||
|
||||
.subsection {
|
||||
@include margin-left(10px);
|
||||
|
||||
list-style-type: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
margin-bottom: $baseline/4;
|
||||
|
||||
a.outline-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: ($baseline / 2);
|
||||
|
||||
.subsection-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: palette(primary, x-back);
|
||||
border-radius: $btn-border-radius;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.subsection-text {
|
||||
.details {
|
||||
font-size: $body-font-size;
|
||||
color: theme-color("secondary");
|
||||
}
|
||||
}
|
||||
|
||||
.subsection-actions {
|
||||
.resume-right {
|
||||
position: relative;
|
||||
top: calc(50% - (#{$baseline} / 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.current {
|
||||
border: 1px solid theme-color("primary");
|
||||
border-radius: $btn-border-radius;
|
||||
|
||||
.resume-right {
|
||||
@include float(right);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 1px solid theme-color("primary");
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// date summary
|
||||
.date-summary-container {
|
||||
.date-summary {
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
<section class="course-outline" id="main">
|
||||
<button class="btn btn-primary"
|
||||
id="expand-collapse-outline-all-button"
|
||||
aria-expanded="false"
|
||||
aria-controls="course-outline-block-tree"
|
||||
>
|
||||
<span class="expand-collapse-outline-all-extra-padding" id="expand-collapse-outline-all-span">${_("Expand All")}</span>
|
||||
</button>
|
||||
<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">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { keys } from 'edx-ui-toolkit/js/utils/constants';
|
||||
|
||||
// @TODO: Figure out how to make webpack handle default exports when libraryTarget: 'window'
|
||||
export class CourseOutline { // eslint-disable-line import/prefer-default-export
|
||||
constructor(newCourseOutlineEnabled) {
|
||||
constructor() {
|
||||
const focusable = [...document.querySelectorAll('.outline-item.focusable')];
|
||||
|
||||
focusable.forEach(el => el.addEventListener('keydown', (event) => {
|
||||
@@ -54,49 +54,46 @@ export class CourseOutline { // eslint-disable-line import/prefer-default-expor
|
||||
sectionToggleButton.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
// TODO: EDUCATOR-2283 Remove check for waffle flag after it is turned on.
|
||||
if (newCourseOutlineEnabled) {
|
||||
[...document.querySelectorAll(('.accordion'))]
|
||||
.forEach((accordion) => {
|
||||
const sections = Array.prototype.slice.call(accordion.querySelectorAll('.accordion-trigger'));
|
||||
[...document.querySelectorAll(('.accordion'))]
|
||||
.forEach((accordion) => {
|
||||
const sections = Array.prototype.slice.call(accordion.querySelectorAll('.accordion-trigger'));
|
||||
|
||||
sections.forEach(section => section.addEventListener('click', (event) => {
|
||||
const sectionToggleButton = event.currentTarget;
|
||||
if (sectionToggleButton.classList.contains('accordion-trigger')) {
|
||||
const isExpanded = sectionToggleButton.getAttribute('aria-expanded') === 'true';
|
||||
if (!isExpanded) {
|
||||
expandSection(sectionToggleButton);
|
||||
} else if (isExpanded) {
|
||||
collapseSection(sectionToggleButton);
|
||||
}
|
||||
event.stopImmediatePropagation();
|
||||
sections.forEach(section => section.addEventListener('click', (event) => {
|
||||
const sectionToggleButton = event.currentTarget;
|
||||
if (sectionToggleButton.classList.contains('accordion-trigger')) {
|
||||
const isExpanded = sectionToggleButton.getAttribute('aria-expanded') === 'true';
|
||||
if (!isExpanded) {
|
||||
expandSection(sectionToggleButton);
|
||||
} else if (isExpanded) {
|
||||
collapseSection(sectionToggleButton);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
const toggleAllButton = document.querySelector('#expand-collapse-outline-all-button');
|
||||
const toggleAllSpan = document.querySelector('#expand-collapse-outline-all-span');
|
||||
const extraPaddingClass = 'expand-collapse-outline-all-extra-padding';
|
||||
toggleAllButton.addEventListener('click', (event) => {
|
||||
const toggleAllExpanded = toggleAllButton.getAttribute('aria-expanded') === 'true';
|
||||
let sectionAction;
|
||||
if (toggleAllExpanded) {
|
||||
toggleAllButton.setAttribute('aria-expanded', 'false');
|
||||
sectionAction = collapseSection;
|
||||
toggleAllSpan.classList.add(extraPaddingClass);
|
||||
toggleAllSpan.innerText = 'Expand All';
|
||||
} else {
|
||||
toggleAllButton.setAttribute('aria-expanded', 'true');
|
||||
sectionAction = expandSection;
|
||||
toggleAllSpan.classList.remove(extraPaddingClass);
|
||||
toggleAllSpan.innerText = 'Collapse All';
|
||||
}
|
||||
const sections = Array.prototype.slice.call(document.querySelectorAll('.accordion-trigger'));
|
||||
sections.forEach((sectionToggleButton) => {
|
||||
sectionAction(sectionToggleButton);
|
||||
});
|
||||
event.stopImmediatePropagation();
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
const toggleAllButton = document.querySelector('#expand-collapse-outline-all-button');
|
||||
const toggleAllSpan = document.querySelector('#expand-collapse-outline-all-span');
|
||||
const extraPaddingClass = 'expand-collapse-outline-all-extra-padding';
|
||||
toggleAllButton.addEventListener('click', (event) => {
|
||||
const toggleAllExpanded = toggleAllButton.getAttribute('aria-expanded') === 'true';
|
||||
let sectionAction;
|
||||
if (toggleAllExpanded) {
|
||||
toggleAllButton.setAttribute('aria-expanded', 'false');
|
||||
sectionAction = collapseSection;
|
||||
toggleAllSpan.classList.add(extraPaddingClass);
|
||||
toggleAllSpan.innerText = 'Expand All';
|
||||
} else {
|
||||
toggleAllButton.setAttribute('aria-expanded', 'true');
|
||||
sectionAction = expandSection;
|
||||
toggleAllSpan.classList.remove(extraPaddingClass);
|
||||
toggleAllSpan.innerText = 'Collapse All';
|
||||
}
|
||||
const sections = Array.prototype.slice.call(document.querySelectorAll('.accordion-trigger'));
|
||||
sections.forEach((sectionToggleButton) => {
|
||||
sectionAction(sectionToggleButton);
|
||||
});
|
||||
event.stopImmediatePropagation();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
## mako
|
||||
|
||||
<%page expression_filter="h"/>
|
||||
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<%!
|
||||
from datetime import date
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
%>
|
||||
|
||||
<main role="main" class="course-outline-visualprogress" id="main" tabindex="-1">
|
||||
% if blocks.get('children'):
|
||||
<button class="btn btn-primary"
|
||||
id="expand-collapse-outline-all-button"
|
||||
aria-expanded="false"
|
||||
aria-controls="course-outline-block-tree"
|
||||
>
|
||||
<span class="expand-collapse-outline-all-extra-padding" id="expand-collapse-outline-all-span">${_("Expand All")}</span>
|
||||
</button>
|
||||
<ol class="block-tree accordion"
|
||||
id="course-outline-block-tree"
|
||||
role="presentation"
|
||||
aria-labeledby="expand-collapse-outline-all-button">
|
||||
% for section in blocks.get('children'):
|
||||
<% section_is_auto_opened = section.get('resume_block') is True %>
|
||||
<li
|
||||
class="outline-item section"
|
||||
role="heading"
|
||||
>
|
||||
<button class="section-name accordion-trigger"
|
||||
aria-expanded="${ 'true' if section_is_auto_opened else 'false' }"
|
||||
aria-controls="${ section['id'] }_contents"
|
||||
id="${ section['id'] }">
|
||||
<span class="fa fa-chevron-right ${ 'fa-rotate-90' if section_is_auto_opened else '' }" aria-hidden="true"></span>
|
||||
<h3 class="section-title">${ section['display_name'] }</h3>
|
||||
% if section.get('complete'):
|
||||
<span class="complete-checkmark fa fa-check"></span>
|
||||
% endif
|
||||
</button>
|
||||
<ol class="outline-item accordion-panel ${ '' if section_is_auto_opened else 'is-hidden' }"
|
||||
id="${ section['id'] }_contents"
|
||||
role="region"
|
||||
aria-labelledby="${ section['id'] }"
|
||||
>
|
||||
% for subsection in section.get('children', []):
|
||||
<%
|
||||
gated_subsection = subsection['id'] in gated_content
|
||||
completed_prereqs = gated_content[subsection['id']]['completed_prereqs'] if gated_subsection else False
|
||||
subsection_is_auto_opened = subsection.get('resume_block') is True
|
||||
%>
|
||||
<li class="subsection accordion ${ 'current' if subsection['resume_block'] else '' }" role="heading">
|
||||
% if gated_subsection and not completed_prereqs:
|
||||
<a href="${ subsection['lms_web_url'] }">
|
||||
<button class="subsection-text prerequisite-button"
|
||||
id="${ subsection['id'] }"
|
||||
>
|
||||
<span class="menu-icon icon fa fa-lock"
|
||||
aria-hidden="true">
|
||||
</span>
|
||||
<span class="subsection-title">
|
||||
${ subsection['display_name'] }
|
||||
</span>
|
||||
<div class="details prerequisite">
|
||||
${ _("Prerequisite: ") }
|
||||
<%
|
||||
prerequisite_id = gated_content[subsection['id']]['prerequisite']
|
||||
prerequisite_name = xblock_display_names.get(prerequisite_id)
|
||||
%>
|
||||
${ prerequisite_name }
|
||||
</div>
|
||||
% else:
|
||||
<button class="subsection-text accordion-trigger"
|
||||
id="${ subsection['id'] }"
|
||||
aria-expanded="${ 'true' if subsection_is_auto_opened else 'false' }"
|
||||
aria-controls="${ subsection['id'] }_contents"
|
||||
>
|
||||
<span class="fa fa-chevron-right ${ 'fa-rotate-90' if subsection_is_auto_opened else '' }"
|
||||
aria-hidden="true"></span>
|
||||
<span class="subsection-title">
|
||||
${ subsection['display_name'] }
|
||||
</span>
|
||||
% if subsection.get('complete'):
|
||||
<span class="complete-checkmark fa fa-check"></span>
|
||||
% endif
|
||||
% endif
|
||||
<div class="details">
|
||||
|
||||
## There are behavior differences between rendering of subsections which have
|
||||
## exams (timed, graded, etc) and those that do not.
|
||||
##
|
||||
## Exam subsections expose exam status message field as well as a status icon
|
||||
<%
|
||||
if subsection.get('due') is None:
|
||||
# examples: Homework, Lab, etc.
|
||||
data_string = subsection.get('format')
|
||||
else:
|
||||
if 'special_exam_info' in subsection:
|
||||
data_string = _('due {date}')
|
||||
else:
|
||||
data_string = _("{subsection_format} due {{date}}").format(subsection_format=subsection.get('format'))
|
||||
%>
|
||||
% if subsection.get('format') or 'special_exam_info' in subsection:
|
||||
<span class="subtitle">
|
||||
% if 'special_exam' in subsection:
|
||||
## Display the exam status icon and status message
|
||||
<span
|
||||
class="menu-icon icon fa ${subsection['special_exam_info'].get('suggested_icon', 'fa-pencil-square-o')} ${subsection['special_exam_info'].get('status', 'eligible')}"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="subtitle-name">
|
||||
${subsection['special_exam_info'].get('short_description', '')}
|
||||
</span>
|
||||
|
||||
## completed exam statuses should not show the due date
|
||||
## since the exam has already been submitted by the user
|
||||
% if not subsection['special_exam_info'].get('in_completed_state', False):
|
||||
<span
|
||||
class="localized-datetime subtitle-name"
|
||||
data-datetime="${subsection.get('due')}"
|
||||
data-string="${data_string}"
|
||||
data-format= "${due_date_display_format}"
|
||||
data-timezone="${user_timezone}"
|
||||
data-language="${user_language}"
|
||||
></span>
|
||||
% endif
|
||||
% else:
|
||||
## non-graded section, we just show the exam format and the due date
|
||||
## this is the standard case in edx-platform
|
||||
<span
|
||||
class="localized-datetime subtitle-name"
|
||||
data-datetime="${subsection.get('due')}"
|
||||
data-format= "${due_date_display_format}"
|
||||
data-string="${data_string}"
|
||||
data-timezone="${user_timezone}"
|
||||
data-language="${user_language}"
|
||||
></span>
|
||||
|
||||
% if subsection.get('graded'):
|
||||
<span class="sr"> ${_("This content is graded")}</span>
|
||||
% endif
|
||||
% endif
|
||||
</span>
|
||||
% endif
|
||||
</div> <!-- /details -->
|
||||
</button> <!-- /subsection-text -->
|
||||
% if gated_subsection and not completed_prereqs:
|
||||
</a>
|
||||
% endif
|
||||
% if not gated_subsection or (gated_subsection and completed_prereqs):
|
||||
<ol class="outline-item accordion-panel ${ '' if subsection_is_auto_opened else 'is-hidden' }"
|
||||
id="${ subsection['id'] }_contents"
|
||||
role="region"
|
||||
aria-labelledby="${ subsection['id'] }"
|
||||
>
|
||||
% for vertical in subsection.get('children', []):
|
||||
<li class="vertical outline-item focusable">
|
||||
<a
|
||||
class="outline-item focusable"
|
||||
href="${ vertical['lms_web_url'] }"
|
||||
id="${ vertical['id'] }"
|
||||
>
|
||||
<div class="vertical-details">
|
||||
<span class="vertical-title">
|
||||
${ vertical['display_name'] }
|
||||
</span>
|
||||
</div>
|
||||
% if vertical.get('complete'):
|
||||
<span class="complete-checkmark fa fa-check"></span>
|
||||
% endif
|
||||
</a>
|
||||
</li>
|
||||
% endfor
|
||||
</ol>
|
||||
% endif
|
||||
</li>
|
||||
% endfor
|
||||
</ol>
|
||||
</li>
|
||||
% endfor
|
||||
</ol>
|
||||
% endif
|
||||
</main>
|
||||
|
||||
<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory">
|
||||
DateUtilFactory.transform('.localized-datetime');
|
||||
</%static:require_module_async>
|
||||
|
||||
<%static:webpack entry="CourseOutline">
|
||||
new CourseOutline('.block-tree', true);
|
||||
</%static:webpack>
|
||||
@@ -1,153 +0,0 @@
|
||||
## mako
|
||||
|
||||
<%page expression_filter="h"/>
|
||||
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<%!
|
||||
from datetime import date
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
%>
|
||||
|
||||
<main class="course-outline" id="main" aria-label="Content" tabindex="-1">
|
||||
% if blocks.get('children'):
|
||||
<ol class="block-tree">
|
||||
% for section in blocks.get('children'):
|
||||
<li
|
||||
class="outline-item focusable section"
|
||||
id="${ section['id'] }"
|
||||
>
|
||||
<div class="section-name">
|
||||
<h3 class="section-title">${ section['display_name'] }</h3>
|
||||
</div>
|
||||
<ol class="outline-item focusable">
|
||||
% for subsection in section.get('children', []):
|
||||
<li
|
||||
class="subsection ${ 'current' if subsection['resume_block'] else '' }"
|
||||
>
|
||||
<a
|
||||
class="outline-item focusable"
|
||||
href="${ subsection['lms_web_url'] }"
|
||||
id="${ subsection['id'] }"
|
||||
>
|
||||
<div class="subsection-text">
|
||||
## Subsection title
|
||||
<span class="subsection-title">
|
||||
% if subsection['id'] in gated_content:
|
||||
% if gated_content[subsection['id']]['completed_prereqs']:
|
||||
<span class="menu-icon icon fa fa-unlock"
|
||||
aria-hidden="true">
|
||||
</span>
|
||||
<span class="subsection-title-name">
|
||||
${ subsection['display_name'] }
|
||||
</span>
|
||||
<span class="sr"> ${_("Unlocked")}</span>
|
||||
% else:
|
||||
<span class="menu-icon icon fa fa-lock"
|
||||
aria-hidden="true">
|
||||
</span>
|
||||
<span class="subsection-title-name">
|
||||
${ subsection['display_name'] }
|
||||
</span>
|
||||
<span class="details">
|
||||
${ _("(Prerequisite required)") }
|
||||
</span>
|
||||
% endif
|
||||
% else:
|
||||
<span class="subsection-title-name">
|
||||
${ subsection['display_name'] }
|
||||
</span>
|
||||
% endif
|
||||
<div class="details">
|
||||
|
||||
## There are behavior differences between rendering of subsections which have
|
||||
## exams (timed, graded, etc) and those that do not.
|
||||
##
|
||||
## Exam subsections expose exam status message field as well as a status icon
|
||||
<%
|
||||
if subsection.get('due') is None:
|
||||
# examples: Homework, Lab, etc.
|
||||
data_string = subsection.get('format')
|
||||
else:
|
||||
if 'special_exam_info' in subsection:
|
||||
data_string = _('due {date}')
|
||||
else:
|
||||
data_string = _("{subsection_format} due {{date}}").format(subsection_format=subsection.get('format'))
|
||||
%>
|
||||
% if subsection.get('format') or 'special_exam_info' in subsection:
|
||||
<span class="subtitle">
|
||||
% if 'special_exam' in subsection:
|
||||
## Display the exam status icon and status message
|
||||
<span
|
||||
class="menu-icon icon fa ${subsection['special_exam_info'].get('suggested_icon', 'fa-pencil-square-o')} ${subsection['special_exam_info'].get('status', 'eligible')}"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="subtitle-name">
|
||||
${subsection['special_exam_info'].get('short_description', '')}
|
||||
</span>
|
||||
|
||||
## completed exam statuses should not show the due date
|
||||
## since the exam has already been submitted by the user
|
||||
% if not subsection['special_exam_info'].get('in_completed_state', False):
|
||||
<span
|
||||
class="localized-datetime subtitle-name"
|
||||
data-datetime="${subsection.get('due')}"
|
||||
data-format= "${due_date_display_format}"
|
||||
data-string="${data_string}"
|
||||
data-timezone="${user_timezone}"
|
||||
data-language="${user_language}"
|
||||
></span>
|
||||
% endif
|
||||
% else:
|
||||
## non-graded section, we just show the exam format and the due date
|
||||
## this is the standard case in edx-platform
|
||||
<span
|
||||
class="localized-datetime subtitle-name"
|
||||
data-datetime="${subsection.get('due')}"
|
||||
data-format= "${due_date_display_format}"
|
||||
data-string="${data_string}"
|
||||
data-timezone="${user_timezone}"
|
||||
data-language="${user_language}"
|
||||
></span>
|
||||
|
||||
% if 'graded' in subsection and subsection['graded']:
|
||||
<span
|
||||
class="menu-icon icon fa fa-pencil-square-o"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="sr"> ${_("This content is graded")}</span>
|
||||
% endif
|
||||
% endif
|
||||
</span>
|
||||
% endif
|
||||
</div> <!-- /details -->
|
||||
</div> <!-- /subsection-text -->
|
||||
<div class="subsection-actions">
|
||||
## Resume button (if last visited section)
|
||||
% if subsection['resume_block']:
|
||||
<span class="resume-right">
|
||||
<b>${ _("Resume Course") }</b>
|
||||
<span class="icon fa fa-arrow-circle-right" aria-hidden="true"></span>
|
||||
</span>
|
||||
%endif
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
% endfor
|
||||
</ol>
|
||||
</li>
|
||||
% endfor
|
||||
</ol>
|
||||
% endif
|
||||
</main>
|
||||
|
||||
<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory">
|
||||
DateUtilFactory.transform('.localized-datetime');
|
||||
</%static:require_module_async>
|
||||
|
||||
<%static:webpack entry="CourseOutline">
|
||||
new CourseOutline('.block-tree', false);
|
||||
</%static:webpack>
|
||||
@@ -0,0 +1,189 @@
|
||||
## mako
|
||||
|
||||
<%page expression_filter="h"/>
|
||||
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<%!
|
||||
from datetime import date
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
%>
|
||||
|
||||
<%
|
||||
course_sections = blocks.get('children')
|
||||
%>
|
||||
<main role="main" class="course-outline" id="main" tabindex="-1">
|
||||
% if course_sections is not None:
|
||||
<button class="btn btn-primary"
|
||||
id="expand-collapse-outline-all-button"
|
||||
aria-expanded="false"
|
||||
aria-controls="course-outline-block-tree"
|
||||
>
|
||||
<span class="expand-collapse-outline-all-extra-padding" id="expand-collapse-outline-all-span">${_("Expand All")}</span>
|
||||
</button>
|
||||
<ol class="block-tree accordion"
|
||||
id="course-outline-block-tree"
|
||||
role="presentation"
|
||||
aria-labelledby="expand-collapse-outline-all-button">
|
||||
% for section in course_sections:
|
||||
<%
|
||||
section_is_auto_opened = section.get('resume_block') is True
|
||||
%>
|
||||
<li class="outline-item section" role="heading">
|
||||
<button class="section-name accordion-trigger"
|
||||
aria-expanded="${ 'true' if section_is_auto_opened else 'false' }"
|
||||
aria-controls="${ section['id'] }_contents"
|
||||
id="${ section['id'] }">
|
||||
<span class="fa fa-chevron-right ${ 'fa-rotate-90' if section_is_auto_opened else '' }" aria-hidden="true"></span>
|
||||
<h3 class="section-title">${ section['display_name'] }</h3>
|
||||
% if show_visual_progress and section.get('complete'):
|
||||
<span class="complete-checkmark fa fa-check"></span>
|
||||
% endif
|
||||
</button>
|
||||
<ol class="outline-item accordion-panel ${ '' if section_is_auto_opened else 'is-hidden' }"
|
||||
id="${ section['id'] }_contents"
|
||||
role="region"
|
||||
aria-labelledby="${ section['id'] }">
|
||||
% for subsection in section.get('children', []):
|
||||
<%
|
||||
gated_subsection = subsection['id'] in gated_content
|
||||
completed_prereqs = gated_content[subsection['id']]['completed_prereqs'] if gated_subsection else False
|
||||
subsection_is_auto_opened = subsection.get('resume_block') is True
|
||||
%>
|
||||
<li class="subsection accordion ${ 'current' if subsection['resume_block'] else '' }" role="heading">
|
||||
% if gated_subsection and not completed_prereqs:
|
||||
<a href="${ subsection['lms_web_url'] }">
|
||||
<button class="subsection-text prerequisite-button"
|
||||
id="${ subsection['id'] }">
|
||||
<span class="menu-icon icon fa fa-lock"
|
||||
aria-hidden="true">
|
||||
</span>
|
||||
<span class="subsection-title">
|
||||
${ subsection['display_name'] }
|
||||
</span>
|
||||
<div class="details prerequisite">
|
||||
${ _("Prerequisite: ") }
|
||||
<%
|
||||
prerequisite_id = gated_content[subsection['id']]['prerequisite']
|
||||
prerequisite_name = xblock_display_names.get(prerequisite_id)
|
||||
%>
|
||||
${ prerequisite_name }
|
||||
</div>
|
||||
% else:
|
||||
<button class="subsection-text accordion-trigger"
|
||||
id="${ subsection['id'] }"
|
||||
aria-expanded="${ 'true' if subsection_is_auto_opened else 'false' }"
|
||||
aria-controls="${ subsection['id'] }_contents">
|
||||
<span class="fa fa-chevron-right ${ 'fa-rotate-90' if subsection_is_auto_opened else '' }"
|
||||
aria-hidden="true"></span>
|
||||
<span class="subsection-title">
|
||||
${ subsection['display_name'] }
|
||||
</span>
|
||||
% if show_visual_progress and subsection.get('complete'):
|
||||
<span class="complete-checkmark fa fa-check"></span>
|
||||
% endif
|
||||
% endif
|
||||
<div class="details">
|
||||
|
||||
## There are behavior differences between rendering of subsections which have
|
||||
## exams (timed, graded, etc) and those that do not.
|
||||
##
|
||||
## Exam subsections expose exam status message field as well as a status icon
|
||||
<%
|
||||
if subsection.get('due') is None:
|
||||
# examples: Homework, Lab, etc.
|
||||
data_string = subsection.get('format')
|
||||
else:
|
||||
if 'special_exam_info' in subsection:
|
||||
data_string = _('due {date}')
|
||||
else:
|
||||
data_string = _("{subsection_format} due {{date}}").format(subsection_format=subsection.get('format'))
|
||||
%>
|
||||
% if subsection.get('format') or 'special_exam_info' in subsection:
|
||||
<span class="subtitle">
|
||||
% if 'special_exam' in subsection:
|
||||
## Display the exam status icon and status message
|
||||
<span
|
||||
class="menu-icon icon fa ${subsection['special_exam_info'].get('suggested_icon', 'fa-pencil-square-o')} ${subsection['special_exam_info'].get('status', 'eligible')}"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="subtitle-name">
|
||||
${subsection['special_exam_info'].get('short_description', '')}
|
||||
</span>
|
||||
|
||||
## completed exam statuses should not show the due date
|
||||
## since the exam has already been submitted by the user
|
||||
% if not subsection['special_exam_info'].get('in_completed_state', False):
|
||||
<span
|
||||
class="localized-datetime subtitle-name"
|
||||
data-datetime="${subsection.get('due')}"
|
||||
data-string="${data_string}"
|
||||
data-timezone="${user_timezone}"
|
||||
data-language="${user_language}"
|
||||
></span>
|
||||
% endif
|
||||
% else:
|
||||
## non-graded section, we just show the exam format and the due date
|
||||
## this is the standard case in edx-platform
|
||||
<span
|
||||
class="localized-datetime subtitle-name"
|
||||
data-datetime="${subsection.get('due')}"
|
||||
data-string="${data_string}"
|
||||
data-timezone="${user_timezone}"
|
||||
data-language="${user_language}"
|
||||
></span>
|
||||
|
||||
% if subsection.get('graded'):
|
||||
<span class="sr"> ${_("This content is graded")}</span>
|
||||
% endif
|
||||
% endif
|
||||
</span>
|
||||
% endif
|
||||
</div> <!-- /details -->
|
||||
</button> <!-- /subsection-text -->
|
||||
% if gated_subsection and not completed_prereqs:
|
||||
</a>
|
||||
% endif
|
||||
% if not gated_subsection or (gated_subsection and completed_prereqs):
|
||||
<ol class="outline-item accordion-panel ${ '' if subsection_is_auto_opened else 'is-hidden' }"
|
||||
id="${ subsection['id'] }_contents"
|
||||
role="region"
|
||||
aria-labelledby="${ subsection['id'] }"
|
||||
>
|
||||
% for vertical in subsection.get('children', []):
|
||||
<li class="vertical outline-item focusable">
|
||||
<a class="outline-item focusable"
|
||||
href="${ vertical['lms_web_url'] }"
|
||||
id="${ vertical['id'] }">
|
||||
<div class="vertical-details">
|
||||
<span class="vertical-title">
|
||||
${ vertical['display_name'] }
|
||||
</span>
|
||||
</div>
|
||||
% if show_visual_progress and vertical.get('complete'):
|
||||
<span class="complete-checkmark fa fa-check"></span>
|
||||
% endif
|
||||
</a>
|
||||
</li>
|
||||
% endfor
|
||||
</ol>
|
||||
% endif
|
||||
</li>
|
||||
% endfor
|
||||
</ol>
|
||||
</li>
|
||||
% endfor
|
||||
</ol>
|
||||
% endif
|
||||
</main>
|
||||
|
||||
<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory">
|
||||
DateUtilFactory.transform('.localized-datetime');
|
||||
</%static:require_module_async>
|
||||
|
||||
<%static:webpack entry="CourseOutline">
|
||||
new CourseOutline();
|
||||
</%static:webpack>
|
||||
@@ -131,7 +131,7 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
|
||||
self.assertIn(sequential.format, response_content)
|
||||
self.assertTrue(sequential.children)
|
||||
for vertical in sequential.children:
|
||||
self.assertNotIn(vertical.display_name, response_content)
|
||||
self.assertIn(vertical.display_name, response_content)
|
||||
|
||||
|
||||
class TestCourseOutlinePageWithPrerequisites(SharedModuleStoreTestCase, MilestonesTestCaseMixin):
|
||||
@@ -230,21 +230,21 @@ class TestCourseOutlinePageWithPrerequisites(SharedModuleStoreTestCase, Mileston
|
||||
lock_icon = response_content('.fa-lock')
|
||||
self.assertTrue(lock_icon, "lock icon is not present, but should be")
|
||||
|
||||
subsection = lock_icon.parents('.subsection-title')
|
||||
subsection = lock_icon.parents('.subsection-text')
|
||||
|
||||
# check that subsection-title-name is the display name
|
||||
gated_subsection_title = self.course_blocks['gated_content'].display_name
|
||||
self.assertIn(gated_subsection_title, subsection.children('.subsection-title-name').html())
|
||||
self.assertIn(gated_subsection_title, subsection.children('.subsection-title').html())
|
||||
|
||||
# check that it says prerequisite required
|
||||
self.assertIn(self.PREREQ_REQUIRED, subsection.children('.details').html())
|
||||
self.assertIn("Prerequisite:", subsection.children('.details').html())
|
||||
|
||||
# check that there is not a screen reader message
|
||||
self.assertFalse(subsection.children('.sr'))
|
||||
|
||||
def test_content_unlocked(self):
|
||||
"""
|
||||
Test that a sequential/subsection with unmet prereqs correctly indicated that its content is locked
|
||||
Test that a sequential/subsection with met prereqs correctly indicated that its content is unlocked
|
||||
"""
|
||||
course = self.course
|
||||
self.setup_gated_section(self.course_blocks['gated_content'], self.course_blocks['prerequisite'])
|
||||
@@ -263,24 +263,23 @@ class TestCourseOutlinePageWithPrerequisites(SharedModuleStoreTestCase, Mileston
|
||||
|
||||
response_content = pq(response.content)
|
||||
|
||||
# check unlock icon is present
|
||||
# check unlock icon is not present
|
||||
unlock_icon = response_content('.fa-unlock')
|
||||
self.assertTrue(unlock_icon, "unlock icon is not present, but should be")
|
||||
self.assertFalse(unlock_icon, "unlock icon is present, yet shouldn't be.")
|
||||
|
||||
subsection = unlock_icon.parents('.subsection-title')
|
||||
gated_subsection_title = self.course_blocks['gated_content'].display_name
|
||||
every_subsection_on_outline = response_content('.subsection-title')
|
||||
|
||||
subsection_has_gated_text = False
|
||||
says_prerequisite_required = False
|
||||
|
||||
for subsection_contents in every_subsection_on_outline.contents():
|
||||
subsection_has_gated_text = gated_subsection_title in subsection_contents
|
||||
says_prerequisite_required = "Prerequisite:" in subsection_contents
|
||||
|
||||
# check that subsection-title-name is the display name of gated content section
|
||||
gated_subsection_title = self.course_blocks['gated_content'].display_name
|
||||
self.assertIn(gated_subsection_title, subsection.children('.subsection-title-name').html())
|
||||
|
||||
# check that it doesn't say prerequisite required
|
||||
self.assertNotIn(self.PREREQ_REQUIRED, subsection.children('.subsection-title-name').html())
|
||||
|
||||
# check that there is a screen reader message
|
||||
self.assertTrue(subsection.children('.sr'))
|
||||
|
||||
# check that the screen reader message is correct
|
||||
self.assertIn(self.UNLOCKED, subsection.children('.sr').html())
|
||||
self.assertTrue(subsection_has_gated_text)
|
||||
self.assertFalse(says_prerequisite_required)
|
||||
|
||||
|
||||
class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleTestMixin):
|
||||
@@ -415,7 +414,7 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT
|
||||
|
||||
self.complete_sequential(self.course, vertical1)
|
||||
# Test for 'resume' link
|
||||
response = self.visit_course_home(course, resume_count=2)
|
||||
response = self.visit_course_home(course, resume_count=1)
|
||||
|
||||
# Test for 'resume' link URL - should be vertical 1
|
||||
content = pq(response.content)
|
||||
@@ -423,7 +422,7 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT
|
||||
|
||||
self.complete_sequential(self.course, vertical2)
|
||||
# Test for 'resume' link
|
||||
response = self.visit_course_home(course, resume_count=2)
|
||||
response = self.visit_course_home(course, resume_count=1)
|
||||
|
||||
# Test for 'resume' link URL - should be vertical 2
|
||||
content = pq(response.content)
|
||||
@@ -434,7 +433,7 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT
|
||||
self.visit_sequential(course, course.children[0], course.children[0].children[0])
|
||||
|
||||
# Test for 'resume' link URL - should be vertical 2 (last completed block, NOT last visited)
|
||||
response = self.visit_course_home(course, resume_count=2)
|
||||
response = self.visit_course_home(course, resume_count=1)
|
||||
content = pq(response.content)
|
||||
self.assertTrue(content('.action-resume-course').attr('href').endswith('/vertical/' + vertical2.url_name))
|
||||
|
||||
@@ -459,7 +458,7 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT
|
||||
self.store.delete_item(sequential.location, self.user.id)
|
||||
|
||||
# check resume course buttons
|
||||
response = self.visit_course_home(course, resume_count=2)
|
||||
response = self.visit_course_home(course, resume_count=1)
|
||||
|
||||
content = pq(response.content)
|
||||
self.assertTrue(content('.action-resume-course').attr('href').endswith('/sequential/' + sequential2.url_name))
|
||||
@@ -522,22 +521,21 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT
|
||||
"aria-labelledby=\"" + url + "\"" \
|
||||
">"
|
||||
|
||||
with patch('openedx.features.course_experience.waffle.new_course_outline_enabled', Mock(return_value=True)):
|
||||
# Course tree
|
||||
course = self.course
|
||||
chapter = course.children[0]
|
||||
sequential1 = chapter.children[0]
|
||||
sequential2 = chapter.children[1]
|
||||
# Course tree
|
||||
course = self.course
|
||||
chapter = course.children[0]
|
||||
sequential1 = chapter.children[0]
|
||||
sequential2 = chapter.children[1]
|
||||
|
||||
response_content = self.client.get(course_home_url(course)).content
|
||||
stripped_response = text_type(re.sub("\\s+", "", response_content), "utf-8")
|
||||
response_content = self.client.get(course_home_url(course)).content
|
||||
stripped_response = text_type(re.sub("\\s+", "", response_content), "utf-8")
|
||||
|
||||
self.assertTrue(get_sequential_button(text_type(sequential1.location), False) in stripped_response)
|
||||
self.assertTrue(get_sequential_button(text_type(sequential2.location), True) in stripped_response)
|
||||
self.assertTrue(get_sequential_button(text_type(sequential1.location), False) in stripped_response)
|
||||
self.assertTrue(get_sequential_button(text_type(sequential2.location), True) in stripped_response)
|
||||
|
||||
content = pq(response_content)
|
||||
button = content('#expand-collapse-outline-all-button')
|
||||
self.assertEqual('Expand All', button.children()[0].text)
|
||||
content = pq(response_content)
|
||||
button = content('#expand-collapse-outline-all-button')
|
||||
self.assertEqual('Expand All', button.children()[0].text)
|
||||
|
||||
def test_user_enrolled_after_completion_collection(self):
|
||||
"""
|
||||
|
||||
@@ -15,7 +15,6 @@ from web_fragments.fragment import Fragment
|
||||
|
||||
from courseware.courses import get_course_overview_with_access
|
||||
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
|
||||
from openedx.features.course_experience import waffle as course_experience_waffle
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
from util.milestones_helpers import get_course_content_milestones
|
||||
@@ -50,29 +49,18 @@ class CourseOutlineFragmentView(EdxFragmentView):
|
||||
'blocks': course_block_tree
|
||||
}
|
||||
|
||||
# TODO: EDUCATOR-2283 Remove this check when the waffle flag is turned on in production
|
||||
if course_experience_waffle.new_course_outline_enabled(course_key=course_key):
|
||||
resume_block = get_resume_block(course_block_tree)
|
||||
if not resume_block:
|
||||
self.mark_first_unit_to_resume(course_block_tree)
|
||||
resume_block = get_resume_block(course_block_tree)
|
||||
if not resume_block:
|
||||
self.mark_first_unit_to_resume(course_block_tree)
|
||||
|
||||
xblock_display_names = self.create_xblock_id_and_name_dict(course_block_tree)
|
||||
gated_content = self.get_content_milestones(request, course_key)
|
||||
xblock_display_names = self.create_xblock_id_and_name_dict(course_block_tree)
|
||||
gated_content = self.get_content_milestones(request, course_key)
|
||||
|
||||
context['gated_content'] = gated_content
|
||||
context['xblock_display_names'] = xblock_display_names
|
||||
context['gated_content'] = gated_content
|
||||
context['xblock_display_names'] = xblock_display_names
|
||||
|
||||
# TODO: EDUCATOR-2283 Rename this file to course-outline-fragment.html
|
||||
html = render_to_string('course_experience/course-outline-fragment-new.html', context)
|
||||
return Fragment(html)
|
||||
else:
|
||||
content_milestones = self.get_content_milestones_old(request, course_key)
|
||||
|
||||
context['gated_content'] = content_milestones
|
||||
|
||||
# TODO: EDUCATOR-2283 Remove this file
|
||||
html = render_to_string('course_experience/course-outline-fragment-old.html', context)
|
||||
return Fragment(html)
|
||||
html = render_to_string('course_experience/course-outline-fragment.html', context)
|
||||
return Fragment(html)
|
||||
|
||||
def create_xblock_id_and_name_dict(self, course_block_tree, xblock_display_names=None):
|
||||
"""
|
||||
@@ -114,28 +102,6 @@ class CourseOutlineFragmentView(EdxFragmentView):
|
||||
|
||||
return gated_content
|
||||
|
||||
# TODO: EDUCATOR-2283 Remove this function when the visual progress waffle flag is turned on in production
|
||||
def get_content_milestones_old(self, request, course_key):
|
||||
"""
|
||||
Returns dict of subsections with prerequisites and whether the prerequisite has been completed or not
|
||||
"""
|
||||
|
||||
all_course_prereqs = get_course_content_milestones(course_key)
|
||||
|
||||
content_ids_of_unfulfilled_prereqs = {
|
||||
milestone['content_id']
|
||||
for milestone in get_course_content_milestones(course_key, user_id=request.user.id)
|
||||
}
|
||||
|
||||
course_content_milestones = {
|
||||
milestone['content_id']: {
|
||||
'completed_prereqs': milestone['content_id'] not in content_ids_of_unfulfilled_prereqs
|
||||
}
|
||||
for milestone in all_course_prereqs
|
||||
}
|
||||
|
||||
return course_content_milestones
|
||||
|
||||
def user_enrolled_after_completion_collection(self, user, course_key):
|
||||
"""
|
||||
Checks that the user has enrolled in the course after 01/24/2018, the date that
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
"""
|
||||
This module contains various configuration settings via
|
||||
waffle switches for the course experience app.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
|
||||
from openedx.core.djangoapps.theming.helpers import get_current_site
|
||||
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace, WaffleSwitchNamespace
|
||||
|
||||
# Namespace
|
||||
WAFFLE_NAMESPACE = 'course_experience'
|
||||
|
||||
# Switches
|
||||
# Full name course_experience.enable_new_course_outline
|
||||
# Enables the UI changes to the course outline for all courses
|
||||
ENABLE_NEW_COURSE_OUTLINE = 'enable_new_course_outline'
|
||||
|
||||
# Full name course_experience.enable_new_course_outline_for_course
|
||||
# Enables the UI changes to the course outline for a course
|
||||
ENABLE_NEW_COURSE_OUTLINE_FOR_COURSE = 'enable_new_course_outline_for_course'
|
||||
|
||||
# Full name course_experience.enable_new_course_outline_for_site
|
||||
# Enables the UI changes to the course outline for a site configuration
|
||||
ENABLE_NEW_COURSE_OUTLINE_FOR_SITE = 'enable_new_course_outline_for_site'
|
||||
|
||||
|
||||
def waffle_switch():
|
||||
"""
|
||||
Returns the namespaced, cached, audited Waffle class for course experience.
|
||||
"""
|
||||
return WaffleSwitchNamespace(name=WAFFLE_NAMESPACE, log_prefix='course_experience: ')
|
||||
|
||||
|
||||
def waffle_flag():
|
||||
"""
|
||||
Returns the namespaced, cached, audited Waffle flags dictionary for course experience.
|
||||
"""
|
||||
namespace = WaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'course_experience: ')
|
||||
# By default, disable the new course outline. Can be enabled on a course-by-course basis.
|
||||
# And overridden site-globally by ENABLE_SITE_NEW_COURSE_OUTLINE
|
||||
return CourseWaffleFlag(
|
||||
namespace,
|
||||
ENABLE_NEW_COURSE_OUTLINE_FOR_COURSE,
|
||||
flag_undefined_default=False
|
||||
)
|
||||
|
||||
|
||||
def new_course_outline_enabled(course_key):
|
||||
"""
|
||||
Returns whether the new course outline is enabled.
|
||||
"""
|
||||
try:
|
||||
current_site = get_current_site()
|
||||
if not current_site.configuration.get_value(ENABLE_NEW_COURSE_OUTLINE_FOR_SITE, False):
|
||||
return
|
||||
except SiteConfiguration.DoesNotExist:
|
||||
return
|
||||
|
||||
if not waffle_switch().is_enabled(ENABLE_NEW_COURSE_OUTLINE):
|
||||
return waffle_flag().is_enabled(course_key)
|
||||
|
||||
return True
|
||||
Reference in New Issue
Block a user