Commit d4cc7b8f by cahrens

Support level support for Studio xblock creation.

TNL-4670
parent 71bebec5
......@@ -691,7 +691,6 @@ class MiscCourseTests(ContentStoreTestCase):
# Test that malicious code does not appear in html
self.assertNotIn(malicious_code, resp.content)
@patch('django.conf.settings.DEPRECATED_ADVANCED_COMPONENT_TYPES', [])
def test_advanced_components_in_edit_unit(self):
# This could be made better, but for now let's just assert that we see the advanced modules mentioned in the page
# response HTML
......
......@@ -21,6 +21,7 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.djangoapps.models.course_details import CourseDetails
from student.roles import CourseInstructorRole, CourseStaffRole
from student.tests.factories import UserFactory
from xblock_django.models import XBlockStudioConfigurationFlag
from xmodule.fields import Date
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
......@@ -784,6 +785,15 @@ class CourseMetadataEditingTest(CourseTestCase):
)
self.assertNotIn('edxnotes', test_model)
def test_allow_unsupported_xblocks(self):
"""
allow_unsupported_xblocks is only shown in Advanced Settings if
XBlockStudioConfigurationFlag is enabled.
"""
self.assertNotIn('allow_unsupported_xblocks', CourseMetadata.fetch(self.fullcourse))
XBlockStudioConfigurationFlag(enabled=True).save()
self.assertIn('allow_unsupported_xblocks', CourseMetadata.fetch(self.fullcourse))
def test_validate_from_json_correct_inputs(self):
is_valid, errors, test_model = CourseMetadata.validate_and_update_from_json(
self.course,
......
......@@ -2,7 +2,9 @@
Django module for Course Metadata class -- manages advanced settings and related parameters
"""
from xblock.fields import Scope
from xblock_django.models import XBlockStudioConfigurationFlag
from xmodule.modulestore.django import modulestore
from django.utils.translation import ugettext as _
from django.conf import settings
......@@ -93,6 +95,11 @@ class CourseMetadata(object):
filtered_list.append('enable_ccx')
filtered_list.append('ccx_connector')
# If the XBlockStudioConfiguration table is not being used, there is no need to
# display the "Allow Unsupported XBlocks" setting.
if not XBlockStudioConfigurationFlag.is_enabled():
filtered_list.append('allow_unsupported_xblocks')
return filtered_list
@classmethod
......
......@@ -398,9 +398,6 @@ ADVANCED_SECURITY_CONFIG = ENV_TOKENS.get('ADVANCED_SECURITY_CONFIG', {})
################ ADVANCED COMPONENT/PROBLEM TYPES ###############
ADVANCED_PROBLEM_TYPES = ENV_TOKENS.get('ADVANCED_PROBLEM_TYPES', ADVANCED_PROBLEM_TYPES)
DEPRECATED_ADVANCED_COMPONENT_TYPES = ENV_TOKENS.get(
'DEPRECATED_ADVANCED_COMPONENT_TYPES', DEPRECATED_ADVANCED_COMPONENT_TYPES
)
################ VIDEO UPLOAD PIPELINE ###############
......
......@@ -1132,19 +1132,6 @@ XBLOCK_SETTINGS = {
}
}
################################ XBlock Deprecation ################################
# The following settings are used for deprecating XBlocks.
# Adding components in this list will disable the creation of new problems for
# those advanced components in Studio. Existing problems will work fine
# and one can edit them in Studio.
# DEPRECATED. Please use /admin/xblock_django/xblockdisableconfig instead.
DEPRECATED_ADVANCED_COMPONENT_TYPES = []
# XBlocks can be disabled from rendering in LMS Courseware by adding them to
# /admin/xblock_django/xblockdisableconfig/.
################################ Settings for Credit Course Requirements ################################
# Initial delay used for retrying tasks.
# Additional retries use longer delays.
......
......@@ -10,7 +10,8 @@ define(["backbone"], function (Backbone) {
// category (may or may not match "type")
// boilerplate_name (may be null)
// is_common (only used for problems)
templates: []
templates: [],
support_legend: {}
},
parse: function (response) {
// Returns true only for templates that both have no boilerplate and are of
......@@ -24,6 +25,7 @@ define(["backbone"], function (Backbone) {
this.type = response.type;
this.templates = response.templates;
this.display_name = response.display_name;
this.support_legend = response.support_legend;
// Sort the templates.
this.templates.sort(function (a, b) {
......
......@@ -49,7 +49,8 @@ define(["js/models/component_template"],
"boilerplate_name": "alternate_word_cloud.yaml",
"display_name": "Word Cloud"
}],
"type": "problem"
"type": "problem",
"support_legend": {"show_legend": false}
};
it('orders templates correctly', function () {
......
......@@ -41,12 +41,13 @@ define(["jquery", "underscore", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
mockComponentTemplates = new ComponentTemplates([
{
templates: [
"templates": [
{
category: 'discussion',
display_name: 'Discussion'
"category": "discussion",
"display_name": "Discussion"
}],
type: 'discussion'
"type": "discussion",
"support_legend": {"show_legend": false}
}, {
"templates": [
{
......@@ -62,7 +63,8 @@ define(["jquery", "underscore", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
"boilerplate_name": "raw.yaml",
"display_name": "Raw HTML"
}],
"type": "html"
"type": "html",
"support_legend": {"show_legend": false}
}],
{
parse: true
......@@ -76,6 +78,8 @@ define(["jquery", "underscore", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
TemplateHelpers.installTemplate('add-xblock-component-button');
TemplateHelpers.installTemplate('add-xblock-component-menu');
TemplateHelpers.installTemplate('add-xblock-component-menu-problem');
TemplateHelpers.installTemplate('add-xblock-component-support-legend');
TemplateHelpers.installTemplate('add-xblock-component-support-level');
// Add templates needed by the edit XBlock modal
TemplateHelpers.installTemplate('edit-xblock-modal');
......
define(["jquery", "js/views/baseview"],
function ($, BaseView) {
define(["jquery", "js/views/baseview", 'edx-ui-toolkit/js/utils/html-utils'],
function ($, BaseView, HtmlUtils) {
return BaseView.extend({
className: function () {
......@@ -9,8 +9,19 @@ define(["jquery", "js/views/baseview"],
BaseView.prototype.initialize.call(this);
var template_name = this.model.type === "problem" ? "add-xblock-component-menu-problem" :
"add-xblock-component-menu";
var support_indicator_template = this.loadTemplate("add-xblock-component-support-level");
var support_legend_template = this.loadTemplate("add-xblock-component-support-legend");
this.template = this.loadTemplate(template_name);
this.$el.html(this.template({type: this.model.type, templates: this.model.templates}));
HtmlUtils.setHtml(
this.$el,
HtmlUtils.HTML(this.template({
type: this.model.type, templates: this.model.templates,
support_legend: this.model.support_legend,
support_indicator_template: support_indicator_template,
support_legend_template: support_legend_template,
HtmlUtils: HtmlUtils
}))
);
// Make the tabs on problems into "real tabs"
this.$('.tab-group').tabs();
}
......
......@@ -10,7 +10,7 @@
// +Base - Utilities
// ====================
@import 'variables';
@import 'partials/variables';
@import 'mixins';
@import 'mixins-inherited';
......
......@@ -168,18 +168,47 @@
// specific menu types
&.new-component-problem {
padding-bottom: ($baseline/2);
.problem-type-tabs {
display: inline-block;
}
}
.support-documentation {
float: right;
@include margin($baseline, 0, ($baseline/2), ($baseline/2));
@include font-size(14);
.support-documentation-level {
padding-right: ($baseline/2);
}
.support-documentation-link {
// Override JQuery ui-widget-content link color (black) with our usual link color and hover action.
color: $uxpl-blue-base;
text-decoration: none;
padding-right: ($baseline/2);
&:hover {
color: $uxpl-blue-hover-active;
text-decoration: underline;
}
}
}
.support-level {
padding-right: ($baseline/2);
}
.icon {
color: $uxpl-primary-accent;
}
}
// individual menus
// --------------------
.new-component-template {
@include clearfix();
margin-bottom: 0;
li {
border: none;
......@@ -190,7 +219,7 @@
}
}
.button-component {
.button-component {
@include clearfix();
@include transition(none);
@extend %t-demi-strong;
......@@ -201,11 +230,16 @@
background: $white;
color: $gray-d3;
text-align: left;
font-family: $f-sans-serif;
&:hover {
@include transition(background-color $tmg-f2 linear 0s);
background: tint($green,30%);
color: $white;
.icon {
color: $white;
}
}
}
}
......
......@@ -39,6 +39,16 @@ $f-monospace: 'Bitstream Vera Sans Mono', Consolas, Courier, monospace;
// ====================
$transparent: rgba(0,0,0,0); // used when color value is needed for UI width/transitions but element is transparent
// +Colors - UXPL new pattern library colors
// ====================
$uxpl-blue-base: rgba(0, 116, 180, 1); // wcag2a compliant
$uxpl-blue-hover-active: lighten($uxpl-blue-base, 7%); // wcag2a compliant
$uxpl-green-base: rgba(0, 129, 0, 1); // wcag2a compliant
$uxpl-green-hover-active: lighten($uxpl-green-base, 7%); // wcag2a compliant
$uxpl-primary-accent: rgb(14, 166, 236);
// +Colors - Primary
// ====================
$black: rgb(0,0,0);
......@@ -87,12 +97,6 @@ $blue-t1: rgba($blue, 0.25);
$blue-t2: rgba($blue, 0.50);
$blue-t3: rgba($blue, 0.75);
$uxpl-blue-base: rgba(0, 116, 180, 1); // wcag2a compliant
$uxpl-blue-hover-active: lighten($uxpl-blue-base, 7%); // wcag2a compliant
$uxpl-green-base: rgba(0, 129, 0, 1); // wcag2a compliant
$uxpl-green-hover-active: lighten($uxpl-green-base, 7%); // wcag2a compliant
$pink: rgb(183, 37, 103); // #b72567;
$pink-l1: tint($pink,20%);
$pink-l2: tint($pink,40%);
......
......@@ -7,10 +7,10 @@
%>">
<ul class="problem-type-tabs nav-tabs" tabindex='-1'>
<li class="current">
<a class="link-tab" href="#tab1"><%= gettext("Common Problem Types") %></a>
<a class="link-tab" href="#tab1"><%- gettext("Common Problem Types") %></a>
</li>
<li>
<a class="link-tab" href="#tab2"><%= gettext("Advanced") %></a>
<a class="link-tab" href="#tab2"><%- gettext("Advanced") %></a>
</li>
</ul>
<div class="tab current" id="tab1">
......@@ -19,15 +19,17 @@
<% if (templates[i].tab == "common") { %>
<% if (!templates[i].boilerplate_name) { %>
<li class="editor-md empty">
<button type="button" class="button-component" data-category="<%= templates[i].category %>">
<span class="name"><%= templates[i].display_name %></span>
<button type="button" class="button-component" data-category="<%- templates[i].category %>">
<%= HtmlUtils.HTML(support_indicator_template({support_level: templates[i].support_level})) %>
<span class="name"><%- templates[i].display_name %></span>
</button>
</li>
<% } else { %>
<li class="editor-md">
<button type="button" class="button-component" data-category="<%= templates[i].category %>"
data-boilerplate="<%= templates[i].boilerplate_name %>">
<span class="name"><%= templates[i].display_name %></span>
<button type="button" class="button-component" data-category="<%- templates[i].category %>"
data-boilerplate="<%- templates[i].boilerplate_name %>">
<%= HtmlUtils.HTML(support_indicator_template({support_level: templates[i].support_level})) %>
<span class="name"><%- templates[i].display_name %></span>
</button>
</li>
<% } %>
......@@ -40,14 +42,16 @@
<% for (var i = 0; i < templates.length; i++) { %>
<% if (templates[i].tab == "advanced") { %>
<li class="editor-manual">
<button type="button" class="button-component" data-category="<%= templates[i].category %>"
data-boilerplate="<%= templates[i].boilerplate_name %>">
<span class="name"><%= templates[i].display_name %></span>
<button type="button" class="button-component" data-category="<%- templates[i].category %>"
data-boilerplate="<%- templates[i].boilerplate_name %>">
<%= HtmlUtils.HTML(support_indicator_template({support_level: templates[i].support_level})) %>
<span class="name"><%- templates[i].display_name %></span>
</button>
</li>
<% } %>
<% } %>
</ul>
</div>
<button class="cancel-button" data-type="<%= type %>"><%= gettext("Cancel") %></button>
<button class="cancel-button" data-type="<%- type %>"><%- gettext("Cancel") %></button>
<%= HtmlUtils.HTML(support_legend_template({support_legend: support_legend})) %>
</div>
......@@ -10,20 +10,23 @@
<% for (var i = 0; i < templates.length; i++) { %>
<% if (!templates[i].boilerplate_name) { %>
<li class="editor-md empty">
<button type="button" class="button-component" data-category="<%= templates[i].category %>">
<span class="name"><%= templates[i].display_name %></span>
<button type="button" class="button-component" data-category="<%- templates[i].category %>">
<%= HtmlUtils.HTML(support_indicator_template({support_level: templates[i].support_level})) %>
<span class="name"><%- templates[i].display_name %></span>
</button>
</li>
<% } else { %>
<li class="editor-md">
<button type="button" class="button-component" data-category="<%= templates[i].category %>"
data-boilerplate="<%= templates[i].boilerplate_name %>">
<span class="name"><%= templates[i].display_name %></span>
<button type="button" class="button-component" data-category="<%- templates[i].category %>"
data-boilerplate="<%- templates[i].boilerplate_name %>">
<%= HtmlUtils.HTML(support_indicator_template({support_level: templates[i].support_level})) %>
<span class="name"><%- templates[i].display_name %></span>
</button>
</li>
<% } %>
<% } %>
</ul>
<button class="cancel-button" data-type="<%= type %>"><%= gettext("Cancel") %></button>
<button class="cancel-button" data-type="<%- type %>"><%- gettext("Cancel") %></button>
<%= HtmlUtils.HTML(support_legend_template({support_legend: support_legend})) %>
</div>
<% } %>
<% if (support_legend.show_legend) { %>
<span class="support-documentation">
<a class="support-documentation-link"
href="http://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/create_exercises_and_tools.html#levels-of-support-for-tools" target="_blank">
<%- support_legend.documentation_label %>
</a>
<span class="support-documentation-level">
<span class="icon fa fa-circle" aria-hidden="true"></span>
<span><%- gettext('Supported') %></span>
</span>
<span class="support-documentation-level">
<span class="icon fa fa-adjust" aria-hidden="true"></span>
<span><%- gettext('Provisional') %></span>
</span>
<% if (support_legend.allow_unsupported_xblocks) { %>
<span class="support-documentation-level">
<span class="icon fa fa-circle-o" aria-hidden="true"></span>
<span><%- gettext('Not Supported') %></span>
</span>
<% } %>
</span>
<% } %>
<% if (support_level === "fs"){ %>
<span class="icon support-level fa fa-circle" aria-hidden="true"></span>
<span class="sr"><%- gettext('Fully Supported') %></span>
<% } else if (support_level === "ps"){ %>
<span class="icon support-level fa fa-adjust" aria-hidden="true"></span>
<span class="sr"><%- gettext('Provisionally Supported') %></span>
<% } else if (support_level === "us"){ %>
<span class="icon support-level fa fa-circle-o" aria-hidden="true"></span>
<span class="sr"><%- gettext('Not Supported') %></span>
<% } %>
......@@ -22,11 +22,11 @@ def disabled_xblocks():
def authorable_xblocks(allow_unsupported=False, name=None):
"""
If Studio XBlock support state is enabled (via `XBlockStudioConfigurationFlag`), this method returns
the QuerySet of XBlocks that can be created in Studio (by default, only fully supported and provisionally
supported). If `XBlockStudioConfigurationFlag` is not enabled, this method returns None.
Note that this method does not take into account fully disabled xblocks (as returned
by `disabled_xblocks`) or deprecated xblocks (as returned by `deprecated_xblocks`).
This method returns the QuerySet of XBlocks that can be created in Studio (by default, only fully supported
and provisionally supported XBlocks), as stored in `XBlockStudioConfiguration`.
Note that this method does NOT check the value `XBlockStudioConfigurationFlag`, nor does it take into account
fully disabled xblocks (as returned by `disabled_xblocks`) or deprecated xblocks
(as returned by `deprecated_xblocks`).
Arguments:
allow_unsupported (bool): If `True`, enabled but unsupported XBlocks will also be returned.
......@@ -36,13 +36,10 @@ def authorable_xblocks(allow_unsupported=False, name=None):
name (str): If provided, filters the returned XBlocks to those with the provided name. This is
useful for XBlocks with lots of template types.
Returns:
QuerySet: If `XBlockStudioConfigurationFlag` is enabled, returns authorable XBlocks,
taking into account `support_level`, `enabled` and `name` (if specified).
If `XBlockStudioConfigurationFlag` is disabled, returns None.
QuerySet: Returns authorable XBlocks, taking into account `support_level`, `enabled` and `name`
(if specified) as specified by `XBlockStudioConfiguration`. Does not take into account whether or not
`XBlockStudioConfigurationFlag` is enabled.
"""
if not XBlockStudioConfigurationFlag.is_enabled():
return None
blocks = XBlockStudioConfiguration.objects.current_set().filter(enabled=True)
if not allow_unsupported:
blocks = blocks.exclude(support_level=XBlockStudioConfiguration.UNSUPPORTED)
......
......@@ -41,27 +41,10 @@ class XBlockDisableConfig(ConfigurationModel):
return block_type in config.disabled_blocks.split()
@classmethod
def disabled_create_block_types(cls):
""" Return list of deprecated XBlock types. Merges types in settings file and field. """
config = cls.current()
xblock_types = config.disabled_create_blocks.split() if config.enabled else []
# Merge settings list with one in the admin config;
if hasattr(settings, 'DEPRECATED_ADVANCED_COMPONENT_TYPES'):
xblock_types.extend(
xblock_type for xblock_type in settings.DEPRECATED_ADVANCED_COMPONENT_TYPES
if xblock_type not in xblock_types
)
return xblock_types
def __unicode__(self):
config = XBlockDisableConfig.current()
return u"Disabled xblocks = {disabled_xblocks}\nDeprecated xblocks = {disabled_create_block_types}".format(
disabled_xblocks=config.disabled_blocks,
disabled_create_block_types=config.disabled_create_block_types
return u"Disabled xblocks = {disabled_xblocks}".format(
disabled_xblocks=config.disabled_blocks
)
......
......@@ -61,28 +61,21 @@ class XBlockSupportTestCase(CacheIsolationTestCase):
disabled_xblock_names = [block.name for block in disabled_xblocks()]
self.assertItemsEqual(["survey", "poll"], disabled_xblock_names)
def test_authorable_blocks_flag_disabled(self):
"""
Tests authorable_xblocks returns None if the configuration flag is not enabled.
"""
self.assertFalse(XBlockStudioConfigurationFlag.is_enabled())
self.assertIsNone(authorable_xblocks())
def test_authorable_blocks_empty_model(self):
"""
Tests authorable_xblocks returns an empty list if the configuration flag is enabled but
the XBlockStudioConfiguration table is empty.
Tests authorable_xblocks returns an empty list if XBlockStudioConfiguration table is empty, regardless
of whether or not XBlockStudioConfigurationFlag is enabled.
"""
XBlockStudioConfigurationFlag(enabled=True).save()
XBlockStudioConfiguration.objects.all().delete()
self.assertFalse(XBlockStudioConfigurationFlag.is_enabled())
self.assertEqual(0, len(authorable_xblocks(allow_unsupported=True)))
XBlockStudioConfigurationFlag(enabled=True).save()
self.assertEqual(0, len(authorable_xblocks(allow_unsupported=True)))
def test_authorable_blocks(self):
"""
Tests authorable_xblocks when configuration flag is enabled and name is not specified.
Tests authorable_xblocks when name is not specified.
"""
XBlockStudioConfigurationFlag(enabled=True).save()
authorable_xblock_names = [block.name for block in authorable_xblocks()]
self.assertItemsEqual(["done", "problem", "problem", "html"], authorable_xblock_names)
......@@ -99,7 +92,7 @@ class XBlockSupportTestCase(CacheIsolationTestCase):
def test_authorable_blocks_by_name(self):
"""
Tests authorable_xblocks when configuration flag is enabled and name is specified.
Tests authorable_xblocks when name is specified.
"""
def verify_xblock_fields(name, template, support_level, block):
"""
......@@ -109,8 +102,6 @@ class XBlockSupportTestCase(CacheIsolationTestCase):
self.assertEqual(template, block.template)
self.assertEqual(support_level, block.support_level)
XBlockStudioConfigurationFlag(enabled=True).save()
# There are no xblocks with name video.
authorable_blocks = authorable_xblocks(name="video")
self.assertEqual(0, len(authorable_blocks))
......
"""
Tests for deprecated xblocks in XBlockDisableConfig.
"""
import ddt
from mock import patch
from django.test import TestCase
from xblock_django.models import XBlockDisableConfig
@ddt.ddt
class XBlockDisableConfigTestCase(TestCase):
"""
Tests for the DjangoXBlockUserService.
"""
def setUp(self):
super(XBlockDisableConfigTestCase, self).setUp()
# Initialize the deprecated modules settings with empty list
XBlockDisableConfig.objects.create(
disabled_blocks='', enabled=True
)
@ddt.data(
('poll', ['poll']),
('poll survey annotatable textannotation', ['poll', 'survey', 'annotatable', 'textannotation']),
('', [])
)
@ddt.unpack
def test_deprecated_blocks_splitting(self, xblocks, expected_result):
"""
Tests that it correctly splits the xblocks defined in field.
"""
XBlockDisableConfig.objects.create(
disabled_create_blocks=xblocks, enabled=True
)
self.assertEqual(
XBlockDisableConfig.disabled_create_block_types(), expected_result
)
@patch('django.conf.settings.DEPRECATED_ADVANCED_COMPONENT_TYPES', ['poll', 'survey'])
def test_deprecated_blocks_file(self):
"""
Tests that deprecated modules contain entries from settings file DEPRECATED_ADVANCED_COMPONENT_TYPES
"""
self.assertEqual(XBlockDisableConfig.disabled_create_block_types(), ['poll', 'survey'])
@patch('django.conf.settings.DEPRECATED_ADVANCED_COMPONENT_TYPES', ['poll', 'survey'])
def test_deprecated_blocks_file_and_config(self):
"""
Tests that deprecated types defined in both settings and config model are read.
"""
XBlockDisableConfig.objects.create(
disabled_create_blocks='annotatable', enabled=True
)
self.assertEqual(XBlockDisableConfig.disabled_create_block_types(), ['annotatable', 'poll', 'survey'])
......@@ -408,7 +408,7 @@ class CourseFields(object):
)
advanced_modules = List(
display_name=_("Advanced Module List"),
help=_("Enter the names of the advanced components to use in your course."),
help=_("Enter the names of the advanced modules to use in your course."),
scope=Scope.settings
)
has_children = True
......@@ -830,6 +830,15 @@ class CourseFields(object):
},
scope=Scope.settings
)
allow_unsupported_xblocks = Boolean(
display_name=_("Add Unsupported Problems and Tools"),
help=_(
"Enter true or false. If true, you can add unsupported problems and tools to your course in Studio. "
"Unsupported problems and tools are not recommended for use in courses due to non-compliance with one or "
"more of the base requirements, such as testing, accessibility, internationalization, and documentation."
),
scope=Scope.settings, default=False
)
class CourseModule(CourseFields, SequenceModule): # pylint: disable=abstract-method
......
......@@ -2905,10 +2905,6 @@ APP_UPGRADE_CACHE_TIMEOUT = 3600
# if you want to avoid an overlap in ids while searching for history across the two tables.
STUDENTMODULEHISTORYEXTENDED_OFFSET = 10000
# Deprecated xblock types
DEPRECATED_ADVANCED_COMPONENT_TYPES = []
# Cutoff date for granting audit certificates
AUDIT_CERT_CUTOFF_DATE = None
......
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