Commit 3cffded2 by Andy Armstrong

Improve course breadcrumbs

LEARNER-877
parent 0d9146a2
...@@ -136,15 +136,20 @@ ...@@ -136,15 +136,20 @@
Sequence.prototype.updatePageTitle = function() { Sequence.prototype.updatePageTitle = function() {
// update the page title to include the current section // update the page title to include the current section
var currentSectionTitle, var currentUnitTitle,
newPageTitle,
positionLink = this.link_for(this.position); positionLink = this.link_for(this.position);
if (positionLink && positionLink.data('page-title')) { 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) { if (newPageTitle !== document.title) {
document.title = currentSectionTitle; document.title = newPageTitle;
} }
// Update the title section of the breadcrumb
$('.nav-item-sequence').text(currentUnitTitle);
} }
}; };
...@@ -269,16 +274,6 @@ ...@@ -269,16 +274,6 @@
sequenceLinks = this.content_container.find('a.seqnav'); sequenceLinks = this.content_container.find('a.seqnav');
sequenceLinks.click(this.goto); 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(); 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): ...@@ -291,11 +291,6 @@ class CoursewarePage(CoursePage):
attribute_value = lambda el: el.get_attribute('data-id') 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] 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): def unit_title_visible(self):
""" Check if unit title is visible """ """ Check if unit title is visible """
return self.q(css='.unit-title').visible return self.q(css='.unit-title').visible
...@@ -365,6 +360,30 @@ class CourseNavPage(PageObject): ...@@ -365,6 +360,30 @@ class CourseNavPage(PageObject):
def is_browser_on_page(self): def is_browser_on_page(self):
return self.parent_page.is_browser_on_page 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 # TODO: TNL-6546: Remove method, outline no longer on courseware page
@property @property
def sections(self): def sections(self):
...@@ -531,7 +550,7 @@ class CourseNavPage(PageObject): ...@@ -531,7 +550,7 @@ class CourseNavPage(PageObject):
from common.test.acceptance.pages.lms.course_home import CourseHomePage from common.test.acceptance.pages.lms.course_home import CourseHomePage
course_home_page = CourseHomePage(self.browser, self.parent_page.course_id) 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() course_home_page.wait_for_page()
return course_home_page return course_home_page
...@@ -540,38 +559,8 @@ class CourseNavPage(PageObject): ...@@ -540,38 +559,8 @@ class CourseNavPage(PageObject):
""" """
Return a boolean indicating whether the user is on the section and subsection Return a boolean indicating whether the user is on the section and subsection
with the specified titles. with the specified titles.
""" """
# TODO: TNL-6546: Remove if/else; always use unified_course_view version (if) return self.breadcrumb_section_title == section_title and self.breadcrumb_subsection_title == subsection_title
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 # Regular expression to remove HTML span tags from a string
REMOVE_SPAN_TAG_RE = re.compile(r'</span>(.+)<span') REMOVE_SPAN_TAG_RE = re.compile(r'</span>(.+)<span')
......
...@@ -149,7 +149,10 @@ class ProblemPage(PageObject): ...@@ -149,7 +149,10 @@ class ProblemPage(PageObject):
""" """
Click the Show Answer button. 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() self.wait_for_ajax()
def is_hint_notification_visible(self): def is_hint_notification_visible(self):
......
...@@ -77,10 +77,6 @@ class CoursewareTest(UniqueCourseTest): ...@@ -77,10 +77,6 @@ class CoursewareTest(UniqueCourseTest):
self.problem_page = ProblemPage(self.browser) # pylint: disable=attribute-defined-outside-init self.problem_page = ProblemPage(self.browser) # pylint: disable=attribute-defined-outside-init
self.assertEqual(self.problem_page.problem_name, 'Test Problem 1') 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): def test_courseware(self):
""" """
Test courseware if recent visited subsection become unpublished. Test courseware if recent visited subsection become unpublished.
...@@ -118,11 +114,15 @@ class CoursewareTest(UniqueCourseTest): ...@@ -118,11 +114,15 @@ class CoursewareTest(UniqueCourseTest):
""" """
xblocks = self.course_fix.get_nested_xblocks(category="problem") xblocks = self.course_fix.get_nested_xblocks(category="problem")
for index in range(1, len(xblocks) + 1): for index in range(1, len(xblocks) + 1):
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.visit()
self.course_home_page.outline.go_to_section('Test Section {}'.format(index), 'Test Subsection {}'.format(index)) self.course_home_page.outline.go_to_section(test_section_title, test_subsection_title)
courseware_page_breadcrumb = self.courseware_page.breadcrumb course_nav = self.courseware_page.nav
expected_breadcrumb = self._create_breadcrumb(index) # pylint: disable=no-member self.assertEqual(course_nav.breadcrumb_section_title, test_section_title)
self.assertEqual(courseware_page_breadcrumb, expected_breadcrumb) self.assertEqual(course_nav.breadcrumb_subsection_title, test_subsection_title)
self.assertEqual(course_nav.breadcrumb_unit_title, test_unit_title)
@attr(shard=9) @attr(shard=9)
......
...@@ -210,8 +210,8 @@ class IndexQueryTestCase(ModuleStoreTestCase): ...@@ -210,8 +210,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
NUM_PROBLEMS = 20 NUM_PROBLEMS = 20
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.mongo, 10, 143), (ModuleStoreEnum.Type.mongo, 10, 144),
(ModuleStoreEnum.Type.split, 4, 143), (ModuleStoreEnum.Type.split, 4, 144),
) )
@ddt.unpack @ddt.unpack
def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count): 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 ...@@ -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.crawlers.models import CrawlersConfig
from openedx.core.djangoapps.monitoring_utils import set_custom_metrics_for_course_key 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.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 request_cache.middleware import RequestCache
from shoppingcart.models import CourseRegistrationCode from shoppingcart.models import CourseRegistrationCode
from student.views import is_course_blocked from student.views import is_course_blocked
...@@ -324,9 +324,14 @@ class CoursewareIndex(View): ...@@ -324,9 +324,14 @@ class CoursewareIndex(View):
Also returns the table of contents for the courseware. Also returns the table of contents for the courseware.
""" """
request = RequestCache.get_current_request() 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 = { courseware_context = {
'csrf': csrf(self.request)['csrf_token'], 'csrf': csrf(self.request)['csrf_token'],
'course': self.course, 'course': self.course,
'course_url': course_url,
'chapter': self.chapter,
'section': self.section,
'init': '', 'init': '',
'fragment': Fragment(), 'fragment': Fragment(),
'staff_access': self.is_staff, 'staff_access': self.is_staff,
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
// Pattern Library shims // Pattern Library shims
@import 'edx-pattern-library-shims/base/variables'; @import 'edx-pattern-library-shims/base/variables';
@import 'edx-pattern-library-shims/breadcrumbs';
@import 'edx-pattern-library-shims/buttons'; @import 'edx-pattern-library-shims/buttons';
// base - elements // base - elements
......
...@@ -107,10 +107,21 @@ html.video-fullscreen { ...@@ -107,10 +107,21 @@ html.video-fullscreen {
display: none; 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, .course-content,
.courseware-results-wrapper { .courseware-results-wrapper {
@extend .content; @extend .content;
padding: ($baseline*2) 3%; // percent allows for smaller padding on mobile
line-height: 1.6; line-height: 1.6;
.xblock { .xblock {
......
...@@ -894,13 +894,18 @@ ...@@ -894,13 +894,18 @@
} }
.doc-link { .doc-link {
@include float(right); @include float(right);
@include margin(($baseline*0.75), ($baseline*0.75), ($baseline*0.75), ($baseline*0.75)); @include margin(($baseline*0.75), ($baseline*0.75), ($baseline*0.75), ($baseline*0.75));
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
color: $base-font-color;
&:visited {
color: $base-font-color; color: $base-font-color;
}
}
&:visited { .page-header {
color: $base-font-color; padding: $baseline;
} border-bottom: 1px solid $border-color-2;
} }
...@@ -12,7 +12,7 @@ from django.utils.translation import ugettext as _ ...@@ -12,7 +12,7 @@ from django.utils.translation import ugettext as _
from edxnotes.helpers import is_feature_enabled as is_edxnotes_enabled from edxnotes.helpers import is_feature_enabled as is_edxnotes_enabled
from openedx.core.djangolib.js_utils import js_escaped_string from openedx.core.djangolib.js_utils import js_escaped_string
from openedx.core.djangolib.markup import HTML 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) 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 ...@@ -31,7 +31,7 @@ from openedx.features.course_experience import UNIFIED_COURSE_VIEW_FLAG
<%block name="header_extras"> <%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"> <script type="text/template" id="${template_name}-tpl">
<%static:include path="common/templates/${template_name}.underscore" /> <%static:include path="common/templates/${template_name}.underscore" />
</script> </script>
...@@ -155,11 +155,37 @@ ${HTML(fragment.foot_html())} ...@@ -155,11 +155,37 @@ ${HTML(fragment.foot_html())}
</div> </div>
% endif % endif
<section class="course-content" id="course-content"> <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"> <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 \ % if getattr(course, 'entrance_exam_enabled') and \
getattr(course, 'entrance_exam_minimum_score_pct') and \ getattr(course, 'entrance_exam_minimum_score_pct') and \
entrance_exam_current_score is not UNDEFINED: entrance_exam_current_score is not UNDEFINED:
......
<%= 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 ...@@ -18,6 +18,7 @@ from django.template.defaultfilters import escapejs
from django_comment_client.permissions import has_permission 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.js_utils import dump_js_escaped_json, js_escaped_string
from openedx.core.djangolib.markup import HTML from openedx.core.djangolib.markup import HTML
from openedx.features.course_experience import course_home_page_title
%> %>
<%block name="bodyclass">course</%block> <%block name="bodyclass">course</%block>
...@@ -43,7 +44,7 @@ ${HTML(bookmarks_fragment.foot_html())} ...@@ -43,7 +44,7 @@ ${HTML(bookmarks_fragment.foot_html())}
<div class="has-breadcrumbs"> <div class="has-breadcrumbs">
<div class="breadcrumbs"> <div class="breadcrumbs">
<span class="nav-item"> <span class="nav-item">
<a href="${course_url}">Course</a> <a href="${course_url}">${course_home_page_title(course)}</a>
</span> </span>
<span class="icon fa fa-angle-right" aria-hidden="true"></span> <span class="icon fa fa-angle-right" aria-hidden="true"></span>
<span class="nav-item">${_('My Bookmarks')}</span> <span class="nav-item">${_('My Bookmarks')}</span>
......
...@@ -3,6 +3,8 @@ Unified course experience settings and helper methods. ...@@ -3,6 +3,8 @@ Unified course experience settings and helper methods.
""" """
import waffle import waffle
from django.utils.translation import ugettext as _
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace
from request_cache.middleware import RequestCache from request_cache.middleware import RequestCache
...@@ -18,6 +20,13 @@ WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='course_experience') ...@@ -18,6 +20,13 @@ WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='course_experience')
UNIFIED_COURSE_TAB_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'unified_course_tab') 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): def default_course_url_name(request=None):
""" """
Returns the default course URL name for the current user. Returns the default course URL name for the current user.
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML from openedx.core.djangolib.markup import HTML
from openedx.features.course_experience import course_home_page_title
%> %>
<%block name="content"> <%block name="content">
...@@ -18,7 +19,7 @@ from openedx.core.djangolib.markup import HTML ...@@ -18,7 +19,7 @@ from openedx.core.djangolib.markup import HTML
<div class="has-breadcrumbs"> <div class="has-breadcrumbs">
<div class="breadcrumbs"> <div class="breadcrumbs">
<span class="nav-item"> <span class="nav-item">
<a href="${course_url}">Course</a> <a href="${course_url}">${course_home_page_title(course)}</a>
</span> </span>
<span class="icon fa fa-angle-right" aria-hidden="true"></span> <span class="icon fa fa-angle-right" aria-hidden="true"></span>
<span class="nav-item">${_('Course Updates')}</span> <span class="nav-item">${_('Course Updates')}</span>
......
...@@ -72,7 +72,7 @@ site_status_msg = get_site_status_msg(course_id) ...@@ -72,7 +72,7 @@ site_status_msg = get_site_status_msg(course_id)
% if user.is_authenticated(): % if user.is_authenticated():
% if not course or disable_courseware_header: % if not course or disable_courseware_header:
% if not nav_hidden or show_program_listing: % 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"> <ul class="left list-inline nav-global authenticated">
% if not nav_hidden: % if not nav_hidden:
<%block name="navigation_global_links_authenticated"> <%block name="navigation_global_links_authenticated">
...@@ -121,7 +121,7 @@ site_status_msg = get_site_status_msg(course_id) ...@@ -121,7 +121,7 @@ site_status_msg = get_site_status_msg(course_id)
% endif % endif
% else: % 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="right nav-courseware list-inline">
<div class="item nav-courseware-01"> <div class="item nav-courseware-01">
% if not settings.FEATURES['DISABLE_LOGIN_BUTTON']: % 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