TNL-6929:

Improves navigation within Studio for Learning Sequences, speeding up authors who want to see how a learner progresses through content without needing to jump over to the LMS.

This adds a dropdown section navigator to the breadcrumbs on the unit page and copies the sequence navigator from LMS to the studio unit page.
This commit is contained in:
Dave St.Germain
2019-09-17 14:51:31 -04:00
parent 08877db858
commit 3e73ba3879
14 changed files with 520 additions and 73 deletions

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()