Commit 3cffded2 by Andy Armstrong

Improve course breadcrumbs

LEARNER-877
parent 0d9146a2
......@@ -136,15 +136,20 @@
Sequence.prototype.updatePageTitle = function() {
// update the page title to include the current section
var currentSectionTitle,
var currentUnitTitle,
newPageTitle,
positionLink = this.link_for(this.position);
if (positionLink && positionLink.data('page-title')) {
currentSectionTitle = positionLink.data('page-title') + ' | ' + this.base_page_title;
currentUnitTitle = positionLink.data('page-title');
newPageTitle = currentUnitTitle + ' | ' + this.base_page_title;
if (currentSectionTitle !== document.title) {
document.title = currentSectionTitle;
if (newPageTitle !== document.title) {
document.title = newPageTitle;
}
// Update the title section of the breadcrumb
$('.nav-item-sequence').text(currentUnitTitle);
}
};
......@@ -269,16 +274,6 @@
sequenceLinks = this.content_container.find('a.seqnav');
sequenceLinks.click(this.goto);
edx.HtmlUtils.setHtml(
this.path,
edx.HtmlUtils.template($('#sequence-breadcrumbs-tpl').text())({
courseId: this.el.parent().data('course-id'),
blockId: this.id,
pathText: this.el.find('.nav-item.active').data('path'),
unifiedCourseView: this.path.data('unified-course-view')
})
);
this.sr_container.focus();
}
};
......
<% if (unifiedCourseView) { %>
<a href="<%- '/courses/' + courseId + '/course/#' + blockId %>">
<span class="fa fa-arrow-circle-prev icon" 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>
// ------------------------------
// Breadcrumb styles
//
// Mirrors styles from the Pattern Library
.breadcrumbs {
font-size: font-size(small);
line-height: line-height(small);
.nav-item {
@include margin-left($baseline/4);
display: inline-block;
a, a:visited {
color: $uxpl-blue-base;
}
a:hover {
color: $uxpl-blue-hover-active;
}
}
.fa-angle-right {
@include margin-left($baseline/4);
display: inline-block;
color: $base-font-color;
@include rtl {
@include transform(rotateY(180deg));
}
}
}
......@@ -291,11 +291,6 @@ class CoursewarePage(CoursePage):
attribute_value = lambda el: el.get_attribute('data-id')
return self.q(css='#sequence-list .nav-item').filter(get_active).map(attribute_value).results[0]
@property
def breadcrumb(self):
""" Return the course tree breadcrumb shown above the sequential bar """
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 """
return self.q(css='.unit-title').visible
......@@ -365,6 +360,30 @@ class CourseNavPage(PageObject):
def is_browser_on_page(self):
return self.parent_page.is_browser_on_page
@property
def breadcrumb_section_title(self):
"""
Returns the section's title from the breadcrumb, or None if one is not found.
"""
label = self.q(css='.breadcrumbs .nav-item-chapter').text
return label[0].strip() if label else None
@property
def breadcrumb_subsection_title(self):
"""
Returns the subsection's title from the breadcrumb, or None if one is not found
"""
label = self.q(css='.breadcrumbs .nav-item-section').text
return label[0].strip() if label else None
@property
def breadcrumb_unit_title(self):
"""
Returns the unit's title from the breadcrumb, or None if one is not found
"""
label = self.q(css='.breadcrumbs .nav-item-sequence').text
return label[0].strip() if label else None
# TODO: TNL-6546: Remove method, outline no longer on courseware page
@property
def sections(self):
......@@ -531,7 +550,7 @@ class CourseNavPage(PageObject):
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()
self.q(css='.nav-item-course').click()
course_home_page.wait_for_page()
return course_home_page
......@@ -540,38 +559,8 @@ class CourseNavPage(PageObject):
"""
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
)
return self.breadcrumb_section_title == section_title and self.breadcrumb_subsection_title == subsection_title
# Regular expression to remove HTML span tags from a string
REMOVE_SPAN_TAG_RE = re.compile(r'</span>(.+)<span')
......
......@@ -149,7 +149,10 @@ class ProblemPage(PageObject):
"""
Click the Show Answer button.
"""
self.q(css='.problem .show').click()
css = '.problem .show'
# First make sure that the button visible and can be clicked on.
self.scroll_to_element(css)
self.q(css=css).click()
self.wait_for_ajax()
def is_hint_notification_visible(self):
......
......@@ -77,10 +77,6 @@ class CoursewareTest(UniqueCourseTest):
self.problem_page = ProblemPage(self.browser) # pylint: disable=attribute-defined-outside-init
self.assertEqual(self.problem_page.problem_name, 'Test Problem 1')
def _create_breadcrumb(self, index):
""" Create breadcrumb """
return ['Test Section {}'.format(index), 'Test Subsection {}'.format(index), 'Test Problem {}'.format(index)]
def test_courseware(self):
"""
Test courseware if recent visited subsection become unpublished.
......@@ -118,11 +114,15 @@ class CoursewareTest(UniqueCourseTest):
"""
xblocks = self.course_fix.get_nested_xblocks(category="problem")
for index in range(1, len(xblocks) + 1):
test_section_title = 'Test Section {}'.format(index)
test_subsection_title = 'Test Subsection {}'.format(index)
test_unit_title = 'Test Problem {}'.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)
self.course_home_page.outline.go_to_section(test_section_title, test_subsection_title)
course_nav = self.courseware_page.nav
self.assertEqual(course_nav.breadcrumb_section_title, test_section_title)
self.assertEqual(course_nav.breadcrumb_subsection_title, test_subsection_title)
self.assertEqual(course_nav.breadcrumb_unit_title, test_unit_title)
@attr(shard=9)
......
......@@ -210,8 +210,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
NUM_PROBLEMS = 20
@ddt.data(
(ModuleStoreEnum.Type.mongo, 10, 143),
(ModuleStoreEnum.Type.split, 4, 143),
(ModuleStoreEnum.Type.mongo, 10, 144),
(ModuleStoreEnum.Type.split, 4, 144),
)
@ddt.unpack
def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count):
......
......@@ -33,7 +33,7 @@ from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
from openedx.core.djangoapps.crawlers.models import CrawlersConfig
from openedx.core.djangoapps.monitoring_utils import set_custom_metrics_for_course_key
from openedx.features.enterprise_support.api import data_sharing_consent_required
from openedx.features.course_experience import UNIFIED_COURSE_VIEW_FLAG
from openedx.features.course_experience import UNIFIED_COURSE_VIEW_FLAG, default_course_url_name
from request_cache.middleware import RequestCache
from shoppingcart.models import CourseRegistrationCode
from student.views import is_course_blocked
......@@ -324,9 +324,14 @@ class CoursewareIndex(View):
Also returns the table of contents for the courseware.
"""
request = RequestCache.get_current_request()
course_url_name = default_course_url_name(request)
course_url = reverse(course_url_name, kwargs={'course_id': unicode(self.course.id)})
courseware_context = {
'csrf': csrf(self.request)['csrf_token'],
'course': self.course,
'course_url': course_url,
'chapter': self.chapter,
'section': self.section,
'init': '',
'fragment': Fragment(),
'staff_access': self.is_staff,
......
......@@ -14,6 +14,7 @@
// Pattern Library shims
@import 'edx-pattern-library-shims/base/variables';
@import 'edx-pattern-library-shims/breadcrumbs';
@import 'edx-pattern-library-shims/buttons';
// base - elements
......
......@@ -107,10 +107,21 @@ html.video-fullscreen {
display: none;
}
main {
padding: $baseline;
}
.course-content {
padding: 0;
}
.courseware-results-wrapper {
padding: ($baseline*2) 3%; // percent allows for smaller padding on mobile
}
.course-content,
.courseware-results-wrapper {
@extend .content;
padding: ($baseline*2) 3%; // percent allows for smaller padding on mobile
line-height: 1.6;
.xblock {
......
......@@ -894,13 +894,18 @@
}
.doc-link {
@include float(right);
@include margin(($baseline*0.75), ($baseline*0.75), ($baseline*0.75), ($baseline*0.75));
font-size: 14px;
font-weight: bold;
@include float(right);
@include margin(($baseline*0.75), ($baseline*0.75), ($baseline*0.75), ($baseline*0.75));
font-size: 14px;
font-weight: bold;
color: $base-font-color;
&:visited {
color: $base-font-color;
}
}
&:visited {
color: $base-font-color;
}
.page-header {
padding: $baseline;
border-bottom: 1px solid $border-color-2;
}
......@@ -12,7 +12,7 @@ from django.utils.translation import ugettext as _
from edxnotes.helpers import is_feature_enabled as is_edxnotes_enabled
from openedx.core.djangolib.js_utils import js_escaped_string
from openedx.core.djangolib.markup import HTML
from openedx.features.course_experience import UNIFIED_COURSE_VIEW_FLAG
from openedx.features.course_experience import course_home_page_title, UNIFIED_COURSE_VIEW_FLAG
%>
<%
include_special_exams = settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and (course.enable_proctored_exams or course.enable_timed_exams)
......@@ -31,7 +31,7 @@ from openedx.features.course_experience import UNIFIED_COURSE_VIEW_FLAG
<%block name="header_extras">
% for template_name in ["image-modal", "sequence-breadcrumbs"]:
% for template_name in ["image-modal"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="common/templates/${template_name}.underscore" />
</script>
......@@ -155,11 +155,37 @@ ${HTML(fragment.foot_html())}
</div>
% endif
<section class="course-content" id="course-content">
<header class="page-header has-secondary">
<div class="page-header-main">
<nav aria-label="${_('Course')}" class="sr-is-focusable" tabindex="-1">
<div class="has-breadcrumbs">
<div class="breadcrumbs">
% if waffle.flag_is_active(request, UNIFIED_COURSE_VIEW_FLAG):
<span class="nav-item nav-item-course">
<a href="${course_url}">${course_home_page_title(course)}</a>
</span>
<span class="icon fa fa-angle-right" aria-hidden="true"></span>
% endif
% if chapter:
<span class="nav-item nav-item-chapter">
<a href="${course_url}#${unicode(chapter.location)}">${chapter.display_name_with_default}</a>
</span>
<span class="icon fa fa-angle-right" aria-hidden="true"></span>
% endif
% if section:
<span class="nav-item nav-item-section">
<a href="${course_url}#${unicode(section.location)}">${section.display_name_with_default}</a>
</span>
<span class="icon fa fa-angle-right" aria-hidden="true"></span>
% endif
<span class="nav-item nav-item-sequence">${sequence_title}</span>
</div>
</div>
</nav>
</div>
</header>
<main id="main" tabindex="-1" aria-label="Content">
<div
class="path"
data-unified-course-view="${'true' if waffle.flag_is_active(request, UNIFIED_COURSE_VIEW_FLAG) 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:
......
<%= gettext("There was an error, try searching again.") %>
<%- gettext("There was an error, try searching again.") %>
......@@ -18,6 +18,7 @@ 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
from openedx.features.course_experience import course_home_page_title
%>
<%block name="bodyclass">course</%block>
......@@ -43,7 +44,7 @@ ${HTML(bookmarks_fragment.foot_html())}
<div class="has-breadcrumbs">
<div class="breadcrumbs">
<span class="nav-item">
<a href="${course_url}">Course</a>
<a href="${course_url}">${course_home_page_title(course)}</a>
</span>
<span class="icon fa fa-angle-right" aria-hidden="true"></span>
<span class="nav-item">${_('My Bookmarks')}</span>
......
......@@ -3,6 +3,8 @@ Unified course experience settings and helper methods.
"""
import waffle
from django.utils.translation import ugettext as _
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace
from request_cache.middleware import RequestCache
......@@ -18,6 +20,13 @@ WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='course_experience')
UNIFIED_COURSE_TAB_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'unified_course_tab')
def course_home_page_title(course): # pylint: disable=unused-argument
"""
Returns the title for the course home page.
"""
return _('Course')
def default_course_url_name(request=None):
"""
Returns the default course URL name for the current user.
......
......@@ -7,6 +7,7 @@
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML
from openedx.features.course_experience import course_home_page_title
%>
<%block name="content">
......@@ -18,7 +19,7 @@ from openedx.core.djangolib.markup import HTML
<div class="has-breadcrumbs">
<div class="breadcrumbs">
<span class="nav-item">
<a href="${course_url}">Course</a>
<a href="${course_url}">${course_home_page_title(course)}</a>
</span>
<span class="icon fa fa-angle-right" aria-hidden="true"></span>
<span class="nav-item">${_('Course Updates')}</span>
......
......@@ -72,7 +72,7 @@ site_status_msg = get_site_status_msg(course_id)
% if user.is_authenticated():
% if not course or disable_courseware_header:
% if not nav_hidden or show_program_listing:
<nav aria-label="Main" class="nav-main">
<nav aria-label="${_('Main')}" class="nav-main">
<ul class="left list-inline nav-global authenticated">
% if not nav_hidden:
<%block name="navigation_global_links_authenticated">
......@@ -121,7 +121,7 @@ site_status_msg = get_site_status_msg(course_id)
% endif
% else:
<nav aria-label="Account" class="nav-account-management">
<nav aria-label="${_('Account')}" class="nav-account-management">
<div class="right nav-courseware list-inline">
<div class="item nav-courseware-01">
% if not settings.FEATURES['DISABLE_LOGIN_BUTTON']:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment