Limit course outline to a depth of two
Previously, we'd show three layers deep (section, subsection, and unit). But now we cut out unit and stop at subsection. AA-17
This commit is contained in:
@@ -308,6 +308,35 @@
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
|
||||
// Course outline accordion / link
|
||||
.outline-button {
|
||||
display: block;
|
||||
padding: 10px 0 10px 2px;
|
||||
border: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
margin: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
|
||||
.fa {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.prerequisites-icon {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.complete-checkmark {
|
||||
border: 1px solid $green;
|
||||
margin-right: $baseline / 2;
|
||||
border-radius: 100px;
|
||||
color: white;
|
||||
background-color: $green;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
@include media-breakpoint-up(md) {
|
||||
margin: 0;
|
||||
@@ -346,7 +375,6 @@
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
font-weight: $font-regular;
|
||||
margin-left: $baseline - 2;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@@ -354,7 +382,6 @@
|
||||
.details {
|
||||
font-size: $body-font-size;
|
||||
color: theme-color("secondary");
|
||||
margin-left: 34px;
|
||||
}
|
||||
|
||||
.prerequisite {
|
||||
@@ -362,72 +389,12 @@
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
}
|
||||
|
||||
.vertical {
|
||||
@include margin-left(16px);
|
||||
|
||||
list-style-type: none;
|
||||
|
||||
a.outline-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: ($baseline / 2) 0 ($baseline / 2) 0;
|
||||
margin: 0 0 0 ($baseline);
|
||||
border-top: 1px solid $border-color;
|
||||
}
|
||||
|
||||
a.outline-item:hover {
|
||||
text-decoration: none;
|
||||
|
||||
.vertical-details {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.complete-checkmark {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.vertical-details {
|
||||
float:left;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: palette(primary, x-back);
|
||||
border-radius: $btn-border-radius;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.vertical-actions {
|
||||
.resume-right {
|
||||
position: relative;
|
||||
top: calc(50% - (#{$baseline} / 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Course outline accordion
|
||||
button.accordion-trigger, button.prerequisite-button {
|
||||
padding: 10px 0 10px 2px;
|
||||
border: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
margin: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
|
||||
.fa {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
#expand-collapse-outline-all-button {
|
||||
float: right;
|
||||
background-color: $white;
|
||||
@@ -444,15 +411,6 @@ button.accordion-trigger, button.prerequisite-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fa-check.fa.complete-checkmark {
|
||||
border: 1px solid $green;
|
||||
margin-right: $baseline / 2;
|
||||
border-radius: 100px;
|
||||
color: white;
|
||||
background-color: $green;
|
||||
float: right;
|
||||
}
|
||||
|
||||
// date summary
|
||||
.date-summary-container {
|
||||
.date-summary {
|
||||
|
||||
@@ -35,7 +35,7 @@ self_paced = context.get('self_paced', False)
|
||||
scored = 'scored' if section.get('scored', False) else ''
|
||||
%>
|
||||
<li class="outline-item section ${scored}">
|
||||
<button class="section-name accordion-trigger"
|
||||
<button class="section-name accordion-trigger outline-button"
|
||||
aria-expanded="${ 'true' if section_is_auto_opened else 'false' }"
|
||||
aria-controls="${ section['id'] }_contents"
|
||||
id="${ section['id'] }">
|
||||
@@ -52,23 +52,30 @@ self_paced = context.get('self_paced', False)
|
||||
% 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
|
||||
needs_prereqs = not gated_content[subsection['id']]['completed_prereqs'] if gated_subsection else False
|
||||
scored = 'scored' if subsection.get('scored', False) else ''
|
||||
graded = 'graded' if subsection.get('graded') else ''
|
||||
%>
|
||||
<li class="subsection accordion ${ 'current' if subsection.get('resume_block') else '' } ${graded} ${scored}">
|
||||
% 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>
|
||||
<h4 class="subsection-title">
|
||||
${ subsection['display_name'] }
|
||||
</h4>
|
||||
<li class="subsection accordion ${ 'current' if subsection.get('resume_block') else '' } ${graded} ${scored}">
|
||||
<a
|
||||
% if enable_links:
|
||||
href="${ subsection['lms_web_url'] }"
|
||||
% else:
|
||||
aria-disabled="true"
|
||||
% endif
|
||||
class="subsection-text outline-button"
|
||||
id="${ subsection['id'] }"
|
||||
>
|
||||
<h4 class="subsection-title">
|
||||
${ subsection['display_name'] }
|
||||
</h4>
|
||||
% if subsection.get('complete'):
|
||||
<span class="complete-checkmark fa fa-check" aria-hidden="true"></span>
|
||||
<span class="sr">${_("Completed")}</span>
|
||||
% endif
|
||||
% if needs_prereqs:
|
||||
<div class="details prerequisite">
|
||||
<span class="prerequisites-icon icon fa fa-lock" aria-hidden="true"></span>
|
||||
${ _("Prerequisite: ") }
|
||||
<%
|
||||
prerequisite_id = gated_content[subsection['id']]['prerequisite']
|
||||
@@ -76,21 +83,7 @@ self_paced = context.get('self_paced', False)
|
||||
%>
|
||||
${ 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>
|
||||
<h4 class="subsection-title">
|
||||
${ subsection['display_name'] }
|
||||
</h4>
|
||||
% if subsection.get('complete'):
|
||||
<span class="complete-checkmark fa fa-check" aria-hidden="true"></span>
|
||||
<span class="sr">${_("Completed")}</span>
|
||||
% endif
|
||||
% endif
|
||||
% endif
|
||||
<div class="details">
|
||||
|
||||
## There are behavior differences between rendering of subsections which have
|
||||
@@ -150,47 +143,8 @@ self_paced = context.get('self_paced', False)
|
||||
</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"
|
||||
aria-labelledby="${ subsection['id'] }"
|
||||
>
|
||||
% for vertical in subsection.get('children', []):
|
||||
<%
|
||||
vertical_is_access_denied = vertical.get('authorization_denial_reason')
|
||||
if vertical_is_access_denied:
|
||||
continue
|
||||
scored = 'scored' if vertical.get('scored', False) else ''
|
||||
access_restricted = list(vertical.get('all_denial_reasons', []))
|
||||
%>
|
||||
<li class="vertical outline-item focusable ${scored}" data-access-denied="${json.dumps(access_restricted)}">
|
||||
<a class="outline-item focusable"
|
||||
% if enable_links:
|
||||
href="${ vertical['lms_web_url'] }"
|
||||
% else:
|
||||
aria-disabled="true"
|
||||
% endif
|
||||
id="${ vertical['id'] }">
|
||||
<div class="vertical-details">
|
||||
<div class="vertical-title">
|
||||
${ vertical['display_name'] }
|
||||
</div>
|
||||
</div>
|
||||
% if vertical.get('complete'):
|
||||
<span class="complete-checkmark fa fa-check" aria-hidden="true"></span>
|
||||
<span class="sr">${_("Completed")}</span>
|
||||
|
||||
% endif
|
||||
</a>
|
||||
</li>
|
||||
% endfor
|
||||
</ol>
|
||||
% endif
|
||||
</li>
|
||||
</a>
|
||||
</li>
|
||||
% endfor
|
||||
</ol>
|
||||
</li>
|
||||
|
||||
@@ -131,8 +131,6 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
|
||||
self.assertContains(response, sequential.due.strftime(u'%Y-%m-%d %H:%M:%S'))
|
||||
self.assertContains(response, sequential.format)
|
||||
self.assertTrue(sequential.children)
|
||||
for vertical in sequential.children:
|
||||
self.assertContains(response, vertical.display_name)
|
||||
|
||||
|
||||
class TestCourseOutlinePageWithPrerequisites(SharedModuleStoreTestCase, MilestonesTestCaseMixin):
|
||||
@@ -318,14 +316,22 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT
|
||||
course = CourseFactory.create()
|
||||
with cls.store.bulk_operations(course.id):
|
||||
chapter = ItemFactory.create(category='chapter', parent_location=course.location)
|
||||
chapter2 = ItemFactory.create(category='chapter', parent_location=course.location)
|
||||
sequential = ItemFactory.create(category='sequential', parent_location=chapter.location)
|
||||
sequential2 = ItemFactory.create(category='sequential', parent_location=chapter.location)
|
||||
sequential3 = ItemFactory.create(category='sequential', parent_location=chapter2.location)
|
||||
sequential4 = ItemFactory.create(category='sequential', parent_location=chapter2.location)
|
||||
vertical = ItemFactory.create(category='vertical', parent_location=sequential.location)
|
||||
vertical2 = ItemFactory.create(category='vertical', parent_location=sequential2.location)
|
||||
course.children = [chapter]
|
||||
vertical3 = ItemFactory.create(category='vertical', parent_location=sequential3.location)
|
||||
vertical4 = ItemFactory.create(category='vertical', parent_location=sequential4.location)
|
||||
course.children = [chapter, chapter2]
|
||||
chapter.children = [sequential, sequential2]
|
||||
chapter2.children = [sequential3, sequential4]
|
||||
sequential.children = [vertical]
|
||||
sequential2.children = [vertical2]
|
||||
sequential3.children = [vertical3]
|
||||
sequential4.children = [vertical4]
|
||||
if hasattr(cls, 'user'):
|
||||
CourseEnrollment.enroll(cls.user, course.id)
|
||||
return course
|
||||
@@ -407,8 +413,8 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT
|
||||
response = self.client.get(course_home_url(course))
|
||||
content = pq(response.content)
|
||||
|
||||
# vertical and its parent should be checked
|
||||
self.assertEqual(len(content('.fa-check')), 2)
|
||||
# Subsection should be checked
|
||||
self.assertEqual(len(content('.fa-check')), 1)
|
||||
|
||||
def test_start_course(self):
|
||||
"""
|
||||
@@ -434,7 +440,6 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT
|
||||
|
||||
# Course tree
|
||||
course = self.course
|
||||
course_key = CourseKey.from_string(str(course.id))
|
||||
vertical1 = course.children[0].children[0].children[0]
|
||||
vertical2 = course.children[0].children[1].children[0]
|
||||
|
||||
@@ -534,9 +539,9 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT
|
||||
)
|
||||
def test_course_outline_auto_open(self):
|
||||
"""
|
||||
Tests that the course outline auto-opens to the first unit
|
||||
Tests that the course outline auto-opens to the first subsection
|
||||
in a course if a user has no completion data, and to the
|
||||
last-accessed unit if a user does have completion data.
|
||||
last-accessed subsection if a user does have completion data.
|
||||
"""
|
||||
def get_sequential_button(url, is_hidden):
|
||||
is_hidden_string = "is-hidden" if is_hidden else ""
|
||||
@@ -547,15 +552,14 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT
|
||||
">"
|
||||
# Course tree
|
||||
course = self.course
|
||||
chapter = course.children[0]
|
||||
sequential1 = chapter.children[0]
|
||||
sequential2 = chapter.children[1]
|
||||
chapter1 = course.children[0]
|
||||
chapter2 = course.children[1]
|
||||
|
||||
response_content = self.client.get(course_home_url(course)).content
|
||||
stripped_response = text_type(re.sub(b"\\s+", b"", 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.assertIn(get_sequential_button(text_type(chapter1.location), False), stripped_response)
|
||||
self.assertIn(get_sequential_button(text_type(chapter2.location), True), stripped_response)
|
||||
|
||||
content = pq(response_content)
|
||||
button = content('#expand-collapse-outline-all-button')
|
||||
|
||||
@@ -4,22 +4,12 @@ Common utilities for the course experience, including course outline.
|
||||
|
||||
|
||||
from completion.models import BlockCompletion
|
||||
from django.utils.translation import ugettext as _
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from six.moves import range
|
||||
from web_fragments.fragment import Fragment
|
||||
|
||||
from lms.djangoapps.course_api.blocks.api import get_blocks
|
||||
from lms.djangoapps.course_blocks.utils import get_student_module_as_dict
|
||||
from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
from openedx.core.lib.cache_utils import request_cached
|
||||
from openedx.features.discounts.applicability import (
|
||||
can_receive_discount,
|
||||
get_discount_expiration_date,
|
||||
discount_percentage
|
||||
)
|
||||
from openedx.features.discounts.utils import format_strikeout_price
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
set -e
|
||||
|
||||
export LOWER_PYLINT_THRESHOLD=1000
|
||||
export UPPER_PYLINT_THRESHOLD=2100
|
||||
export UPPER_PYLINT_THRESHOLD=2005
|
||||
export ESLINT_THRESHOLD=5530
|
||||
export STYLELINT_THRESHOLD=880
|
||||
|
||||
Reference in New Issue
Block a user