Merge pull request #21961 from edx/dcs/jumpnav
Improve navigation on Studio unit page
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
// layout with breadcrumb navigation
|
||||
&.has-navigation {
|
||||
.nav-actions {
|
||||
bottom: -($baseline*1.5);
|
||||
bottom: $baseline;
|
||||
}
|
||||
|
||||
.navigation-item {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"> ›</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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user