Merge pull request #21961 from edx/dcs/jumpnav

Improve navigation on Studio unit page
This commit is contained in:
Dave St.Germain
2019-11-08 10:32:43 -05:00
committed by GitHub
14 changed files with 520 additions and 73 deletions

View File

@@ -518,3 +518,49 @@ def is_self_paced(course):
Returns True if course is self-paced, False otherwise.
"""
return course and course.self_paced
def get_sibling_urls(subsection):
"""
Given a subsection, returns the urls for the next and previous units.
(the first unit of the next subsection or section, and
the last unit of the previous subsection/section)
"""
section = subsection.get_parent()
prev_url = next_url = ''
prev_loc = next_loc = None
last_block = None
siblings = list(section.get_children())
for i, block in enumerate(siblings):
if block.location == subsection.location:
if last_block:
try:
prev_loc = last_block.get_children()[0].location
except IndexError:
pass
try:
next_loc = siblings[i + 1].get_children()[0].location
except IndexError:
pass
break
last_block = block
if not prev_loc:
sections = section.get_parent().get_children()
try:
prev_section = sections[sections.index(section) - 1]
prev_loc = prev_section.get_children()[-1].get_children()[-1].location
except IndexError:
pass
if not next_loc:
sections = section.get_parent().get_children()
try:
next_section = sections[sections.index(section) + 1]
next_loc = next_section.get_children()[0].get_children()[0].location
except IndexError:
pass
if prev_loc:
prev_url = reverse_usage_url('container_handler', prev_loc)
if next_loc:
next_url = reverse_usage_url('container_handler', next_loc)
return prev_url, next_url

View File

@@ -1,8 +1,11 @@
"""
Studio component views
"""
from __future__ import absolute_import
import logging
import six
import six
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
@@ -11,17 +14,18 @@ from django.utils.translation import ugettext as _
from django.views.decorators.http import require_GET
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import UsageKey
from six.moves.urllib.parse import quote_plus # pylint: disable=import-error
from xblock.core import XBlock
from xblock.django.request import django_to_webob_request, webob_to_django_response
from xblock.exceptions import NoSuchHandlerError
from xblock.plugin import PluginMissingError
from xblock.runtime import Mixologist
from contentstore.utils import get_lms_link_for_item, reverse_course_url
from contentstore.utils import get_lms_link_for_item, get_sibling_urls, reverse_course_url
from contentstore.views.helpers import get_parent_xblock, is_unit, xblock_type_display_name
from contentstore.views.item import StudioEditModuleRuntime, add_container_page_publishing_info, create_xblock_info
from edxmako.shortcuts import render_to_response
from openedx.core.lib.xblock_utils import is_xblock_aside, get_aside_from_xblock
from openedx.core.lib.xblock_utils import get_aside_from_xblock, is_xblock_aside
from student.auth import has_course_author_access
from xblock_django.api import authorable_xblocks, disabled_xblocks
from xblock_django.models import XBlockStudioConfigurationFlag
@@ -123,11 +127,16 @@ def container_handler(request, usage_key_string):
is_unit_page = is_unit(xblock)
unit = xblock if is_unit_page else None
while parent and parent.category != 'course':
is_first = True
while parent:
if unit is None and is_unit(parent):
unit = parent
ancestor_xblocks.append(parent)
elif parent.category != 'sequential':
current_block = {'block': parent, 'children': parent.get_children(), 'is_last': is_first}
is_first = False
ancestor_xblocks.append(current_block)
parent = get_parent_xblock(parent)
ancestor_xblocks.reverse()
assert unit is not None, "Could not determine unit page"
@@ -137,6 +146,13 @@ def container_handler(request, usage_key_string):
section = get_parent_xblock(subsection)
assert section is not None, "Could not determine ancestor section from unit " + six.text_type(unit.location)
# for the sequence navigator
prev_url, next_url = get_sibling_urls(subsection)
# these are quoted here because they'll end up in a query string on the page,
# and quoting with mako will trigger the xss linter...
prev_url = quote_plus(prev_url) if prev_url else None
next_url = quote_plus(next_url) if next_url else None
# Fetch the XBlock info for use by the container page. Note that it includes information
# about the block's ancestors and siblings for use by the Unit Outline.
xblock_info = create_xblock_info(xblock, include_ancestor_info=is_unit_page)
@@ -162,6 +178,9 @@ def container_handler(request, usage_key_string):
'is_unit_page': is_unit_page,
'subsection': subsection,
'section': section,
'position': index,
'prev_url': prev_url,
'next_url': next_url,
'new_unit_category': 'vertical',
'outline_url': '{url}?format=concise'.format(url=reverse_course_url('course_handler', course.id)),
'ancestor_xblocks': ancestor_xblocks,
@@ -392,7 +411,7 @@ def get_component_templates(courselike, library=False):
u"Improper format for course advanced keys! %s",
course_advanced_keys
)
if len(advanced_component_templates['templates']) > 0:
if advanced_component_templates['templates']:
component_templates.insert(0, advanced_component_templates)
return component_templates

View File

@@ -72,7 +72,7 @@ from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundErr
from xmodule.modulestore.inheritance import own_metadata
from xmodule.services import ConfigurationService, SettingsService
from xmodule.tabs import CourseTabList
from xmodule.x_module import DEPRECATION_VSCOMPAT_EVENT, PREVIEW_VIEWS, STUDENT_VIEW, STUDIO_VIEW
from xmodule.x_module import AUTHOR_VIEW, PREVIEW_VIEWS, STUDENT_VIEW, STUDIO_VIEW
from edx_proctoring.api import get_exam_configuration_dashboard_url, does_backend_support_onboarding
from cms.djangoapps.contentstore.config.waffle import SHOW_REVIEW_RULES_FLAG
@@ -273,6 +273,7 @@ class StudioEditModuleRuntime(object):
(i.e. whenever we're not using PreviewModuleSystem.) This is required to make information
about the current user (especially permissions) available via services as needed.
"""
def __init__(self, user):
self._user = user
@@ -381,16 +382,18 @@ def xblock_view_handler(request, usage_key_string, view_name):
force_render = request.GET.get('force_render', None)
# Set up the context to be passed to each XBlock's render method.
context = {
'is_pages_view': is_pages_view, # This setting disables the recursive wrapping of xblocks
context = request.GET.dict()
context.update({
# This setting disables the recursive wrapping of xblocks
'is_pages_view': is_pages_view or view_name == AUTHOR_VIEW,
'is_unit_page': is_unit(xblock),
'can_edit': can_edit,
'root_xblock': xblock if (view_name == 'container_preview') else None,
'reorderable_items': reorderable_items,
'paging': paging,
'force_render': force_render,
}
'item_url': '/container/{usage_key}',
})
fragment = get_preview_fragment(request, xblock, context)
# Note that the container view recursively adds headers into the preview fragment,
@@ -998,7 +1001,11 @@ def _get_xblock(usage_key, user):
except ItemNotFoundError:
if usage_key.block_type in CREATE_IF_NOT_FOUND:
# Create a new one for certain categories only. Used for course info handouts.
return store.create_item(user.id, usage_key.course_key, usage_key.block_type, block_id=usage_key.block_id)
return store.create_item(
user.id,
usage_key.course_key,
usage_key.block_type,
block_id=usage_key.block_id)
else:
raise
except InvalidLocationError:
@@ -1052,7 +1059,7 @@ def _get_gating_info(course, xblock):
if not hasattr(course, 'gating_prerequisites'):
# Cache gating prerequisites on course module so that we are not
# hitting the database for every xblock in the course
setattr(course, 'gating_prerequisites', gating_api.get_prerequisites(course.id))
course.gating_prerequisites = gating_api.get_prerequisites(course.id)
info["is_prereq"] = gating_api.is_prerequisite(course.id, xblock.location)
info["prereqs"] = [
p for p in course.gating_prerequisites if text_type(xblock.location) not in p['namespace']
@@ -1163,7 +1170,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
'has_children': xblock.has_children
}
if is_concise:
if child_info and len(child_info.get('children', [])) > 0:
if child_info and child_info.get('children', []):
xblock_info['child_info'] = child_info
# Groups are labelled with their internal ids, rather than with the group name. Replace id with display name.
group_display_name = get_split_group_display_name(xblock, course)
@@ -1334,7 +1341,8 @@ class VisibilityState(object):
unscheduled - the block and all of its descendants have no release date (excluding staff only items)
Note: it is valid for items to be published with no release date in which case they are still unscheduled.
needs_attention - the block or its descendants are not fully live, ready or unscheduled (excluding staff only items)
needs_attention - the block or its descendants are not fully live, ready or unscheduled
(excluding staff only items)
For example: one subsection has draft content, or there's both unreleased and released content in one section.
staff_only - all of the block's content is to be shown to staff only

View File

@@ -63,13 +63,10 @@ class ContainerPageTestCase(StudioPageTestCase, LibraryTestCase):
u'data-locator="{0}" data-course-key="{0.course_key}">'.format(self.child_container.location)
),
expected_breadcrumbs=(
u'<a href="/course/{course}{section_parameters}" class="{classes}">\\s*Week 1\\s*</a>\\s*'
u'<a href="/course/{course}{subsection_parameters}" class="{classes}">\\s*Lesson 1\\s*</a>\\s*'
u'<a href="/container/{unit}" class="{classes}">\\s*Unit\\s*</a>'
u'<li class="nav-item">\\s*<a href="/course/{course}{section_parameters}">Week 1<\\/a>.*'
u'<a href="/course/{course}{subsection_parameters}">Lesson 1</a>'
).format(
course=re.escape(six.text_type(self.course.id)),
unit=re.escape(six.text_type(self.vertical.location)),
classes='navigation-item navigation-link navigation-parent',
section_parameters=re.escape(u'?show={}'.format(http.urlquote(self.chapter.location))),
subsection_parameters=re.escape(u'?show={}'.format(http.urlquote(self.sequential.location))),
),
@@ -91,15 +88,10 @@ class ContainerPageTestCase(StudioPageTestCase, LibraryTestCase):
u'data-locator="{0}" data-course-key="{0.course_key}">'.format(draft_container.location)
),
expected_breadcrumbs=(
u'<a href="/course/{course}{section_parameters}" class="{classes}">\\s*Week 1\\s*</a>\\s*'
u'<a href="/course/{course}{subsection_parameters}" class="{classes}">\\s*Lesson 1\\s*</a>\\s*'
u'<a href="/container/{unit}" class="{classes}">\\s*Unit\\s*</a>\\s*'
u'<a href="/container/{split_test}" class="{classes}">\\s*Split Test\\s*</a>'
u'<a href="/course/{course}{section_parameters}">Week 1</a>.*'
u'<a href="/course/{course}{subsection_parameters}">Lesson 1</a>.*'
).format(
course=re.escape(six.text_type(self.course.id)),
unit=re.escape(six.text_type(self.vertical.location)),
split_test=re.escape(six.text_type(self.child_container.location)),
classes=u'navigation-item navigation-link navigation-parent',
section_parameters=re.escape(u'?show={}'.format(http.urlquote(self.chapter.location))),
subsection_parameters=re.escape(u'?show={}'.format(http.urlquote(self.sequential.location))),
),
@@ -120,7 +112,7 @@ class ContainerPageTestCase(StudioPageTestCase, LibraryTestCase):
"""
html = self.get_page_html(xblock)
self.assertIn(expected_section_tag, html)
self.assertRegexpMatches(html, expected_breadcrumbs)
self.assertRegexpMatches(html, re.compile(expected_breadcrumbs, re.DOTALL))
def test_public_container_preview_html(self):
"""

View File

@@ -98,7 +98,7 @@
// layout with breadcrumb navigation
&.has-navigation {
.nav-actions {
bottom: -($baseline*1.5);
bottom: $baseline;
}
.navigation-item {

View File

@@ -53,7 +53,7 @@ nav {
.ui-toggle-dd {
@include transition(all $tmg-f2 ease-in-out 0s);
margin: 0;
margin-left: $baseline/4;
display: inline-block;
vertical-align: middle;
}
@@ -206,3 +206,295 @@ nav {
}
}
}
.jump-nav {
.nav-item {
display: inline-block;
margin-bottom: 20px;
.title {
&:hover,
&:active {
color: theme-color("primary");
}
}
.spacer {
margin-right: 20px;
margin-left: 20px;
font-size: 24px;
}
.wrapper-nav-sub {
top: 35px;
z-index: 100;
min-width: 250px;
}
}
}
// ====================
$seq-nav-border-color: $border-color !default;
$seq-nav-hover-color: rgb(245, 245, 245) !default;
$seq-nav-link-color: $link-color !default;
$seq-nav-icon-color: rgb(10, 10, 10) !default;
$seq-nav-icon-color-muted: rgb(90, 90, 90) !default;
$seq-nav-tooltip-color: rgb(51, 51, 51) !default;
$seq-nav-height: 40px;
#sequence-nav {
clear: both;
}
.sequence-nav {
@extend .topbar;
background-color: #fff;
margin: 0 auto $baseline;
position: relative;
border-bottom: none;
z-index: 0;
height: $seq-nav-height;
display: flex;
justify-content: center;
@media print {
display: none;
}
.sequence-list-wrapper {
@extend %ui-depth2;
position: relative;
height: 100%;
flex-grow: 1;
@include media-breakpoint-down(md) {
white-space: nowrap;
overflow-x: scroll;
}
}
ol {
display: flex;
li {
box-sizing: border-box;
min-width: 40px;
flex-grow: 1;
border-color: $seq-nav-border-color;
border-width: 1px;
border-top-style: solid;
&:not(:last-child) {
@include border-right-style(solid);
}
button {
@extend %ui-fake-link;
@extend %ui-clear-button;
width: 100%;
height: ($seq-nav-height - 1);
position: relative;
margin: 0;
padding: 0;
display: block;
text-align: center;
border-color: $seq-nav-border-color;
border-width: 1px;
border-bottom-style: solid;
box-sizing: border-box;
overflow: visible; // for tooltip - IE11 uses 'hidden' by default if width/height is specified
.icon {
display: inline-block;
line-height: 100%; // This matches the height of the <a> its within (the parent) to get vertical centering.
font-size: 110%;
color: $seq-nav-icon-color-muted;
}
.fa-bookmark {
color: $seq-nav-link-color;
}
//video
&.seq_video {
.icon::before {
content: "\f008"; // .fa-film
}
}
//other
&.seq_other {
.icon::before {
content: "\f02d"; // .fa-book
}
}
//vertical
&.seq_vertical {
.icon::before {
content: "\f00b"; // .fa-tasks
}
}
//problems
&.seq_problem {
.icon::before {
content: "\f044"; // .fa-pencil-square-o
}
}
.sequence-tooltip {
@include text-align(left);
@extend %ui-depth2;
margin-top: 12px;
background: $seq-nav-tooltip-color;
color: $white;
font-family: $font-family-sans-serif;
line-height: lh();
right: 0; // Should not be RTLed, tooltips do not move in RTL
padding: 6px;
position: absolute;
top: 48px;
text-shadow: 0 -1px 0 $black;
white-space: pre;
pointer-events: none;
&:empty {
background: none;
&::after {
display: none;
}
}
&::after {
@include transform(rotate(45deg));
@include right(18px);
background: $seq-nav-tooltip-color;
content: " ";
display: block;
height: 10px;
right: 18px; // Not RTLed, positions tooltips relative to seq nav item
position: absolute;
top: -5px;
width: 10px;
}
}
}
}
}
body.touch-based-device & ol li button:hover p {
display: none;
}
}
.sequence-nav-button {
@extend %ui-depth3;
display: block;
top: 0;
min-width: 40px;
max-width: 40px;
height: 100%;
text-shadow: none; // overrides default button text-shadow
background: none; // overrides default button gradient
background-color: theme-color("inverse");
border-color: $seq-nav-border-color;
box-shadow: none;
font-size: inherit;
font-weight: normal;
padding: 0;
white-space: nowrap;
overflow-x: hidden;
@include media-breakpoint-up(md) {
min-width: 120px;
max-width: 200px;
text-overflow: ellipsis;
span:not(:last-child) {
@include padding-right($baseline / 2);
}
}
.sequence-nav-button-label {
display: none;
@include media-breakpoint-up(md) {
display: inline;
}
}
&.button-previous {
order: -999;
@include media-breakpoint-up(md) {
@include left(0);
@include border-top-left-radius(3px);
@include border-top-right-radius(0);
@include border-bottom-right-radius(0);
@include border-bottom-left-radius(3px);
}
}
&.button-next {
order: 999;
@include media-breakpoint-up(md) {
@include right(0);
@include border-top-left-radius(0);
@include border-top-right-radius(3px);
@include border-bottom-right-radius(3px);
@include border-bottom-left-radius(0);
}
}
&.disabled {
cursor: normal;
}
}
.seq_contents {
display: none;
}
#seq_content {
&:focus,
&:active {
outline: none;
}
}
// hover and active states
.sequence-nav-button,
.sequence-nav button {
&.focused,
&:hover,
&:active,
&.active {
padding-top: 2px;
background-color: theme-color("primary");
.icon {
color: theme-color("inverse");
}
@include media-breakpoint-up(sm) {
border-bottom: 3px solid $seq-nav-link-color;
background-color: theme-color("inverse");
.icon {
color: $seq-nav-icon-color;
}
}
}
}

View File

@@ -51,6 +51,28 @@ from openedx.core.djangolib.markup import HTML, Text
outlineURL: "${outline_url | n, js_escaped_string}"
}
);
require(["js/models/xblock_info", "js/views/xblock", "js/views/utils/xblock_utils", "common/js/components/utils/view_utils"], function (XBlockInfo, XBlockView, XBlockUtils, ViewUtils) {
var model = new XBlockInfo({
id: '${subsection.location|n, decode.utf8}'
});
var xblockView = new XBlockView({
model: model,
el: $('#sequence-nav'),
view: 'author_view?position=${position|n, decode.utf8}&next_url=${next_url|n, decode.utf8}&prev_url=${prev_url|n, decode.utf8}'
});
xblockView.xblockReady = function() {
$('.seq_new_button').click(function(evt) {
evt.preventDefault();
XBlockUtils.addXBlock($(evt.target)).done(function(locator) {
ViewUtils.redirect('/container/' + locator + '?action=new');
return false;
});
return false;
});
};
xblockView.render();
});
</%static:webpack>
</%block>
@@ -63,20 +85,34 @@ from openedx.core.djangolib.markup import HTML, Text
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-navigation has-subtitle">
<div class="page-header">
<small class="navigation navigation-parents subtitle">
% for ancestor in ancestor_xblocks:
<%
ancestor_url = xblock_studio_url(ancestor)
%>
% if ancestor_url:
<a href="${ancestor_url}" class="navigation-item navigation-link navigation-parent">${ancestor.display_name_with_default}</a>
% else:
<span class="navigation-item navigation-parent">${ancestor.display_name_with_default}</span>
% endif
<div class="jump-nav">
<nav class="nav-dd title ui-left">
<ol>
% for block in ancestor_xblocks:
<li class="nav-item">
<span class="title label">${block['block'].display_name_with_default}
<span class="icon fa fa-caret-down ui-toggle-dd" aria-hidden="true"></span>
</span>
% if not block['is_last']:
<span class="spacer"> &rsaquo;</span>
% endif
<div class="wrapper wrapper-nav-sub">
<div class="nav-sub">
<ul>
% for child in block['children']:
<li class="nav-item">
<a href="${xblock_studio_url(child)}">${child.display_name_with_default}</a>
</li>
% endfor
</ul>
</div>
</div>
</li>
% endfor
</small>
<div class="wrapper-xblock-field incontext-editor is-editable"
</ol>
</nav>
<div class="wrapper-xblock-field incontext-editor is-editable"
data-field="display_name" data-field-display-name="${_("Display Name")}">
<h1 class="page-header-title xblock-field-value incontext-editor-value"><span class="title-value">${xblock.display_name_with_default}</span></h1>
</div>
@@ -108,6 +144,7 @@ from openedx.core.djangolib.markup import HTML, Text
% endif
</ul>
</nav>
<div id="sequence-nav"></div>
</header>
</div>
@@ -158,12 +195,7 @@ from openedx.core.djangolib.markup import HTML, Text
<span class="tip"><span class="sr">Tip: </span>${Text(_('To create a link to this unit from an HTML component in this course, enter "/jump_to_id/<location ID>" as the URL value.'))}</span>
</p>
</div>
<div class="wrapper-unit-tree-location bar-mod-content">
<h5 class="title">${_("Location in Course Outline")}</h5>
<div class="wrapper-unit-overview outline outline-simple">
</div>
</div>
</div>
</div>
% endif
</aside>
</section>

View File

@@ -252,7 +252,7 @@
// update the data-attributes with latest contents only for updated problems.
this.content_container
.html(currentTab.text())
.html(currentTab.text()) // xss-lint: disable=javascript-jquery-html
.attr('aria-labelledby', currentTab.attr('aria-labelledby'))
.data('bookmarked', bookmarked);
@@ -294,6 +294,9 @@
// Links from courseware <a class='seqnav' href='n'>...</a>, was .target_tab
if ($(event.currentTarget).hasClass('seqnav')) {
newPosition = $(event.currentTarget).attr('href');
} else if ($(event.currentTarget).data('href') !== undefined) {
location.href = $(event.currentTarget).data('href');
return true;
// Tab links generated by backend template
} else {
newPosition = $(event.currentTarget).data('element');
@@ -326,6 +329,7 @@
this.render(newPosition);
} else {
alertTemplate = gettext('Sequence error! Cannot navigate to %(tab_name)s in the current SequenceModule. Please contact the course staff.'); // eslint-disable-line max-len
// xss-lint: disable=javascript-interpolate
alertText = interpolate(alertTemplate, {
tab_name: newPosition
}, true);

View File

@@ -27,7 +27,7 @@ from .exceptions import NotFoundError
from .fields import Date
from .mako_module import MakoModuleDescriptor
from .progress import Progress
from .x_module import PUBLIC_VIEW, STUDENT_VIEW, XModule
from .x_module import AUTHOR_VIEW, PUBLIC_VIEW, STUDENT_VIEW, XModule
from .xml_module import XmlDescriptor
log = logging.getLogger(__name__)
@@ -286,6 +286,13 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
prereq_met, prereq_meta_info = self._compute_is_prereq_met(True)
return self._student_or_public_view(context or {}, prereq_met, prereq_meta_info, None, PUBLIC_VIEW)
def author_view(self, context):
context = context or {}
context['exclude_units'] = True
if 'position' in context:
context['position'] = int(context['position'])
return self._student_or_public_view(context, True, {}, view=AUTHOR_VIEW)
def _special_exam_student_view(self):
"""
Checks whether this sequential is a special exam. If so, returns
@@ -367,7 +374,8 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
'banner_text': banner_text,
'save_position': view != PUBLIC_VIEW,
'show_completion': view != PUBLIC_VIEW,
'gated_content': self._get_gated_content_info(prereq_met, prereq_meta_info)
'gated_content': self._get_gated_content_info(prereq_met, prereq_meta_info),
'exclude_units': context.get('exclude_units', False)
}
fragment.add_content(self.system.render_template("seq_module.html", params))
@@ -464,9 +472,13 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
display_items. Returns a list of dict objects with information about
the given display_items.
"""
render_items = not context.get('exclude_units', False)
is_user_authenticated = self.is_user_authenticated(context)
bookmarks_service = self.runtime.service(self, 'bookmarks')
completion_service = self.runtime.service(self, 'completion')
if render_items:
bookmarks_service = self.runtime.service(self, 'bookmarks')
completion_service = self.runtime.service(self, 'completion')
else:
bookmarks_service = completion_service = None
context['username'] = self.runtime.service(self, 'user').get_current_user().opt_attrs.get(
'edx-platform.username')
display_names = [
@@ -489,18 +501,21 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
show_bookmark_button = False
is_bookmarked = False
if is_user_authenticated:
if is_user_authenticated and render_items:
show_bookmark_button = True
is_bookmarked = bookmarks_service.is_bookmarked(usage_key=usage_id)
context['show_bookmark_button'] = show_bookmark_button
context['bookmarked'] = is_bookmarked
rendered_item = item.render(view, context)
fragment.add_fragment_resources(rendered_item)
if render_items:
rendered_item = item.render(view, context)
fragment.add_fragment_resources(rendered_item)
content = rendered_item.content
else:
content = ''
iteminfo = {
'content': rendered_item.content,
'content': content,
'page_title': getattr(item, 'tooltip_title', ''),
'type': item_type,
'id': text_type(usage_id),
@@ -508,8 +523,11 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
'path': " > ".join(display_names + [item.display_name_with_default]),
'graded': item.graded
}
if is_user_authenticated:
if not render_items:
# The item url format can be defined in the template context like so:
# context['item_url'] = '/my/item/path/{usage_key}/whatever'
iteminfo['href'] = context.get('item_url', '').format(usage_key=usage_id)
if is_user_authenticated and render_items:
if item.location.block_type == 'vertical':
if completion_service:
iteminfo['complete'] = completion_service.vertical_is_complete(item)
@@ -679,6 +697,7 @@ class SequenceDescriptor(SequenceFields, ProctoringFields, MakoModuleDescriptor,
mako_template = 'widgets/sequence-edit.html'
module_class = SequenceModule
resources_dir = None
has_author_view = True
show_in_read_only_mode = True

View File

@@ -9,7 +9,7 @@ from bok_choy.promise import EmptyPromise, Promise
from common.test.acceptance.pages.common.utils import click_css, confirm_prompt
from common.test.acceptance.pages.studio import BASE_URL
from common.test.acceptance.pages.studio.utils import HelpMixin, type_in_codemirror
from common.test.acceptance.pages.studio.utils import HelpMixin, set_input_value_and_save, type_in_codemirror
from common.test.acceptance.tests.helpers import click_and_wait_for_window
@@ -18,8 +18,8 @@ class ContainerPage(PageObject, HelpMixin):
Container page in Studio
"""
NAME_SELECTOR = '.page-header-title'
NAME_INPUT_SELECTOR = '.page-header .xblock-field-input'
NAME_FIELD_WRAPPER_SELECTOR = '.page-header .wrapper-xblock-field'
NAME_INPUT_SELECTOR = '.wrapper-xblock-field .xblock-field-input'
NAME_FIELD_WRAPPER_SELECTOR = '.wrapper-xblock-field'
ADD_MISSING_GROUPS_SELECTOR = '.notification-action-button[data-notification-action="add-missing-groups"]'
def __init__(self, browser, locator):
@@ -410,6 +410,13 @@ class ContainerPage(PageObject, HelpMixin):
)
return self.q(css=css).html
def set_name(self, name):
"""
Set the name of the unit.
"""
set_input_value_and_save(self, self.NAME_INPUT_SELECTOR, name)
self.wait_for_ajax()
class XBlockWrapper(PageObject):
"""

View File

@@ -33,7 +33,7 @@ class CourseOutlineItem(object):
NAME_SELECTOR = '.item-title'
NAME_INPUT_SELECTOR = '.xblock-field-input'
NAME_FIELD_WRAPPER_SELECTOR = '.xblock-title .wrapper-xblock-field'
STATUS_MESSAGE_SELECTOR = '> div[class$="status"] .status-message'
STATUS_MESSAGE_SELECTOR = '> div[class$="-status"] .status-messages'
CONFIGURATION_BUTTON_SELECTOR = '.action-item .configure-button'
def __repr__(self):
@@ -84,7 +84,8 @@ class CourseOutlineItem(object):
"""
Returns the status message of this item.
"""
return self.q(css=self._bounded_selector(self.STATUS_MESSAGE_SELECTOR)).text[0] # pylint: disable=no-member
selector = self._bounded_selector(self.STATUS_MESSAGE_SELECTOR)
return self.q(css=selector).text[0] # pylint: disable=no-member
@property
def has_staff_lock_warning(self):

View File

@@ -88,14 +88,10 @@ class GradingPage(SettingsPage):
Drag and drop grade range.
"""
self.wait_for_element_visibility(self.grade_ranges, "Grades ranges are visible")
# We have used jquery here to adjust the width of slider to
# desired range because drag and drop has behaved very inconsistently.
# This does not updates the text of range on the slider.
# So as a work around, we have used drag_and_drop without any offset
self.browser.execute_script('$(".ui-resizable").css("width","10")')
action = ActionChains(self.browser)
moveable_css = self.q(css='.ui-resizable-e').results[0]
action.drag_and_drop_by_offset(moveable_css, 0, 0).perform()
action.drag_and_drop_by_offset(moveable_css, -280, 0)
action.perform()
@property
def get_assignment_names(self):

View File

@@ -340,6 +340,7 @@ class WarningMessagesTest(CourseOutlineTest):
subsection.add_unit()
unit = ContainerPage(self.browser, None)
unit.wait_for_page()
unit.set_name(name)
if unit.is_staff_locked != unit_state.is_locked:
unit.toggle_staff_lock()

View File

@@ -7,6 +7,8 @@
data-save-position="${'true' if save_position else 'false'}"
data-show-completion="${'true' if show_completion else 'false'}"
>
% if not exclude_units:
% if banner_text:
<div class="pattern-library-shim alert alert-information subsection-header" tabindex="-1">
<span class="pattern-library-shim icon alert-icon fa fa-info-circle" aria-hidden="true"></span>
@@ -18,7 +20,7 @@
</div>
</div>
% endif
% endif
<div class="sequence-nav">
<button class="sequence-nav-button button-previous">
<span class="icon fa fa-chevron-prev" aria-hidden="true"></span>
@@ -53,6 +55,9 @@
data-page-title="${item['page_title']}"
data-path="${item['path']}"
data-graded="${item['graded']}"
% if item.get('href'):
data-href="${item['href']}"
% endif
id="tab_${idx}">
<span class="icon fa seq_${item['type']}" aria-hidden="true"></span>
% if 'complete' in item:
@@ -71,9 +76,29 @@
</li>
% endfor
% endif
% if exclude_units:
<li role="presentation">
<button class="seq_new_button inactive xnav-item tab"
role="tab"
tabindex="-1"
aria-selected="false"
aria-expanded="false"
aria-controls="seq_content"
data-parent="${item_id}"
data-category="vertical"
data-default-name="${_('Unit')}"
>
<span
class="fa fa-plus"
aria-hidden="true"
></span> New Unit
</button>
</li>
% endif
</ol>
</nav>
</div>
% if not exclude_units:
% if gated_content['gated']:
<%include file="_gated_content.html" args="prereq_url=gated_content['prereq_url'], prereq_section_name=gated_content['prereq_section_name'], gated_section_name=gated_content['gated_section_name']"/>
% else:
@@ -89,7 +114,11 @@
% endfor
<div id="seq_content" role="tabpanel"></div>
% endif
% else:
<div id="seq_content" role="tabpanel"></div>
% endif
% if not exclude_units:
<nav class="sequence-bottom" aria-label="${_('Section')}">
<button class="sequence-nav-button button-previous">
<span class="icon fa fa-chevron-prev" aria-hidden="true"></span>
@@ -103,3 +132,4 @@
</button>
</nav>
</div>
% endif