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,
......
......@@ -27,7 +27,10 @@ from opaque_keys.edx.keys import UsageKey
from student.auth import has_course_author_access
from django.utils.translation import ugettext as _
from xblock_django.models import XBlockDisableConfig
from xblock_django.api import disabled_xblocks, authorable_xblocks
from xblock_django.models import XBlockStudioConfigurationFlag
__all__ = [
'container_handler',
......@@ -47,17 +50,41 @@ CONTAINER_TEMPLATES = [
"basic-modal", "modal-button", "edit-xblock-modal",
"editor-mode-button", "upload-dialog",
"add-xblock-component", "add-xblock-component-button", "add-xblock-component-menu",
"add-xblock-component-menu-problem", "xblock-string-field-editor", "publish-xblock", "publish-history",
"add-xblock-component-support-legend", "add-xblock-component-support-level", "add-xblock-component-menu-problem",
"xblock-string-field-editor", "publish-xblock", "publish-history",
"unit-outline", "container-message", "license-selector",
]
def _advanced_component_types():
def _advanced_component_types(show_unsupported):
"""
Return advanced component types which can be created.
Args:
show_unsupported: if True, unsupported XBlocks may be included in the return value
Returns:
A dict of authorable XBlock types and their support levels (see XBlockStudioConfiguration). For example:
{
"done": "us", # unsupported
"discussion: "fs" # fully supported
}
Note that the support level will be "True" for all XBlocks if XBlockStudioConfigurationFlag
is not enabled.
"""
disabled_create_block_types = XBlockDisableConfig.disabled_create_block_types()
return [c_type for c_type in ADVANCED_COMPONENT_TYPES if c_type not in disabled_create_block_types]
enabled_block_types = _filter_disabled_blocks(ADVANCED_COMPONENT_TYPES)
if XBlockStudioConfigurationFlag.is_enabled():
authorable_blocks = authorable_xblocks(allow_unsupported=show_unsupported)
filtered_blocks = {}
for block in authorable_blocks:
if block.name in enabled_block_types:
filtered_blocks[block.name] = block.support_level
return filtered_blocks
else:
all_blocks = {}
for block_name in enabled_block_types:
all_blocks[block_name] = True
return all_blocks
def _load_mixed_class(category):
......@@ -152,13 +179,14 @@ def get_component_templates(courselike, library=False):
"""
Returns the applicable component templates that can be used by the specified course or library.
"""
def create_template_dict(name, cat, boilerplate_name=None, tab="common", hinted=False):
def create_template_dict(name, category, support_level, boilerplate_name=None, tab="common", hinted=False):
"""
Creates a component template dict.
Parameters
display_name: the user-visible name of the component
category: the type of component (problem, html, etc.)
support_level: the support level of this component
boilerplate_name: name of boilerplate for filling in default values. May be None.
hinted: True if hinted problem else False
tab: common(default)/advanced, which tab it goes in
......@@ -166,10 +194,50 @@ def get_component_templates(courselike, library=False):
"""
return {
"display_name": name,
"category": cat,
"category": category,
"boilerplate_name": boilerplate_name,
"hinted": hinted,
"tab": tab
"tab": tab,
"support_level": support_level
}
def component_support_level(editable_types, name, template=None):
"""
Returns the support level for the given xblock name/template combination.
Args:
editable_types: a QuerySet of xblocks with their support levels
name: the name of the xblock
template: optional template for the xblock
Returns:
If XBlockStudioConfigurationFlag is enabled, returns the support level
(see XBlockStudioConfiguration) or False if this xblock name/template combination
has no Studio support at all. If XBlockStudioConfigurationFlag is disabled,
simply returns True.
"""
# If the Studio support feature is disabled, return True for all.
if not XBlockStudioConfigurationFlag.is_enabled():
return True
if template is None:
template = ""
extension_index = template.rfind(".yaml")
if extension_index >= 0:
template = template[0:extension_index]
for block in editable_types:
if block.name == name and block.template == template:
return block.support_level
return False
def create_support_legend_dict():
"""
Returns a dict of settings information for the display of the support level legend.
"""
return {
"show_legend": XBlockStudioConfigurationFlag.is_enabled(),
"allow_unsupported_xblocks": allow_unsupported,
"documentation_label": _("{platform_name} Support Levels:").format(platform_name=settings.PLATFORM_NAME)
}
component_display_names = {
......@@ -189,57 +257,92 @@ def get_component_templates(courselike, library=False):
if library:
component_types = [component for component in component_types if component != 'discussion']
component_types = _filter_disabled_blocks(component_types)
# Content Libraries currently don't allow opting in to unsupported xblocks/problem types.
allow_unsupported = getattr(courselike, "allow_unsupported_xblocks", False)
for category in component_types:
authorable_variations = authorable_xblocks(allow_unsupported=allow_unsupported, name=category)
support_level_without_template = component_support_level(authorable_variations, category)
templates_for_category = []
component_class = _load_mixed_class(category)
# add the default template with localized display name
# TODO: Once mixins are defined per-application, rather than per-runtime,
# this should use a cms mixed-in class. (cpennington)
display_name = xblock_type_display_name(category, _('Blank')) # this is the Blank Advanced problem
templates_for_category.append(create_template_dict(display_name, category, None, 'advanced'))
categories.add(category)
if support_level_without_template:
# add the default template with localized display name
# TODO: Once mixins are defined per-application, rather than per-runtime,
# this should use a cms mixed-in class. (cpennington)
display_name = xblock_type_display_name(category, _('Blank')) # this is the Blank Advanced problem
templates_for_category.append(
create_template_dict(display_name, category, support_level_without_template, None, 'advanced')
)
categories.add(category)
# add boilerplates
if hasattr(component_class, 'templates'):
for template in component_class.templates():
filter_templates = getattr(component_class, 'filter_templates', None)
if not filter_templates or filter_templates(template, courselike):
# Tab can be 'common' 'advanced'
# Default setting is common/advanced depending on the presence of markdown
tab = 'common'
if template['metadata'].get('markdown') is None:
tab = 'advanced'
hinted = template.get('hinted', False)
templates_for_category.append(
create_template_dict(
_(template['metadata'].get('display_name')), # pylint: disable=translation-of-non-string
category,
template.get('template_id'),
tab,
hinted,
)
template_id = template.get('template_id')
support_level_with_template = component_support_level(
authorable_variations, category, template_id
)
if support_level_with_template:
# Tab can be 'common' 'advanced'
# Default setting is common/advanced depending on the presence of markdown
tab = 'common'
if template['metadata'].get('markdown') is None:
tab = 'advanced'
hinted = template.get('hinted', False)
templates_for_category.append(
create_template_dict(
_(template['metadata'].get('display_name')), # pylint: disable=translation-of-non-string
category,
support_level_with_template,
template_id,
tab,
hinted,
)
)
# Add any advanced problem types
# Add any advanced problem types. Note that these are different xblocks being stored as Advanced Problems.
if category == 'problem':
for advanced_problem_type in ADVANCED_PROBLEM_TYPES:
disabled_block_names = [block.name for block in disabled_xblocks()]
advanced_problem_types = [advanced_problem_type for advanced_problem_type in ADVANCED_PROBLEM_TYPES
if advanced_problem_type['component'] not in disabled_block_names]
for advanced_problem_type in advanced_problem_types:
component = advanced_problem_type['component']
boilerplate_name = advanced_problem_type['boilerplate_name']
try:
component_display_name = xblock_type_display_name(component)
except PluginMissingError:
log.warning('Unable to load xblock type %s to read display_name', component, exc_info=True)
else:
templates_for_category.append(
create_template_dict(component_display_name, component, boilerplate_name, 'advanced')
)
categories.add(component)
authorable_advanced_component_variations = authorable_xblocks(
allow_unsupported=allow_unsupported, name=component
)
advanced_component_support_level = component_support_level(
authorable_advanced_component_variations, component, boilerplate_name
)
if advanced_component_support_level:
try:
component_display_name = xblock_type_display_name(component)
except PluginMissingError:
log.warning('Unable to load xblock type %s to read display_name', component, exc_info=True)
else:
templates_for_category.append(
create_template_dict(
component_display_name,
component,
advanced_component_support_level,
boilerplate_name,
'advanced'
)
)
categories.add(component)
component_templates.append({
"type": category,
"templates": templates_for_category,
"display_name": component_display_names[category]
"display_name": component_display_names[category],
"support_legend": create_support_legend_dict()
})
# Libraries do not support advanced components at this time.
......@@ -251,19 +354,25 @@ def get_component_templates(courselike, library=False):
# are the names of the modules in ADVANCED_COMPONENT_TYPES that should be
# enabled for the course.
course_advanced_keys = courselike.advanced_modules
advanced_component_templates = {"type": "advanced", "templates": [], "display_name": _("Advanced")}
advanced_component_types = _advanced_component_types()
advanced_component_templates = {
"type": "advanced",
"templates": [],
"display_name": _("Advanced"),
"support_legend": create_support_legend_dict()
}
advanced_component_types = _advanced_component_types(allow_unsupported)
# Set component types according to course policy file
if isinstance(course_advanced_keys, list):
for category in course_advanced_keys:
if category in advanced_component_types and category not in categories:
if category in advanced_component_types.keys() and category not in categories:
# boilerplates not supported for advanced components
try:
component_display_name = xblock_type_display_name(category, default_display_name=category)
advanced_component_templates['templates'].append(
create_template_dict(
component_display_name,
category
category,
advanced_component_types[category]
)
)
categories.add(category)
......@@ -288,6 +397,14 @@ def get_component_templates(courselike, library=False):
return component_templates
def _filter_disabled_blocks(all_blocks):
"""
Filter out disabled xblocks from the provided list of xblock names.
"""
disabled_block_names = [block.name for block in disabled_xblocks()]
return [block_name for block_name in all_blocks if block_name not in disabled_block_names]
@login_required
def _get_item_in_course(request, usage_key):
"""
......
......@@ -24,7 +24,7 @@ from contentstore.views.item import (
)
from contentstore.tests.utils import CourseTestCase
from student.tests.factories import UserFactory
from xblock_django.models import XBlockDisableConfig
from xblock_django.models import XBlockConfiguration, XBlockStudioConfiguration, XBlockStudioConfigurationFlag
from xmodule.capa_module import CapaDescriptor
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
......@@ -1457,12 +1457,19 @@ class TestComponentTemplates(CourseTestCase):
def setUp(self):
super(TestComponentTemplates, self).setUp()
self.templates = get_component_templates(self.course)
# Advanced Module support levels.
XBlockStudioConfiguration.objects.create(name='poll', enabled=True, support_level="fs")
XBlockStudioConfiguration.objects.create(name='survey', enabled=True, support_level="ps")
XBlockStudioConfiguration.objects.create(name='annotatable', enabled=True, support_level="us")
# Basic component support levels.
XBlockStudioConfiguration.objects.create(name='html', enabled=True, support_level="fs")
XBlockStudioConfiguration.objects.create(name='discussion', enabled=True, support_level="ps")
XBlockStudioConfiguration.objects.create(name='problem', enabled=True, support_level="us")
XBlockStudioConfiguration.objects.create(name='video', enabled=True, support_level="us")
# XBlock masquerading as a problem
XBlockStudioConfiguration.objects.create(name='openassessment', enabled=True, support_level="us")
# Initialize the deprecated modules settings with empty list
XBlockDisableConfig.objects.create(
disabled_create_blocks='', enabled=True
)
self.templates = get_component_templates(self.course)
def get_templates_of_type(self, template_type):
"""
......@@ -1481,12 +1488,40 @@ class TestComponentTemplates(CourseTestCase):
"""
Test the handling of the basic component templates.
"""
self.assertIsNotNone(self.get_templates_of_type('discussion'))
self.assertIsNotNone(self.get_templates_of_type('html'))
self.assertIsNotNone(self.get_templates_of_type('problem'))
self.assertIsNotNone(self.get_templates_of_type('video'))
self._verify_basic_component("discussion", "Discussion")
self._verify_basic_component("video", "Video")
self.assertGreater(self.get_templates_of_type('html'), 0)
self.assertGreater(self.get_templates_of_type('problem'), 0)
self.assertIsNone(self.get_templates_of_type('advanced'))
# Now fully disable video through XBlockConfiguration
XBlockConfiguration.objects.create(name='video', enabled=False)
self.templates = get_component_templates(self.course)
self.assertIsNone(self.get_templates_of_type('video'))
def test_basic_components_support_levels(self):
"""
Test that support levels can be set on basic component templates.
"""
XBlockStudioConfigurationFlag.objects.create(enabled=True)
self.templates = get_component_templates(self.course)
self._verify_basic_component("discussion", "Discussion", "ps")
self.assertEqual([], self.get_templates_of_type("video"))
self.assertEqual([], self.get_templates_of_type("problem"))
self.course.allow_unsupported_xblocks = True
self.templates = get_component_templates(self.course)
self._verify_basic_component("video", "Video", "us")
problem_templates = self.get_templates_of_type('problem')
problem_no_boilerplate = self.get_template(problem_templates, u'Blank Advanced Problem')
self.assertIsNotNone(problem_no_boilerplate)
self.assertEqual('us', problem_no_boilerplate['support_level'])
# Now fully disable video through XBlockConfiguration
XBlockConfiguration.objects.create(name='video', enabled=False)
self.templates = get_component_templates(self.course)
self.assertIsNone(self.get_templates_of_type('video'))
def test_advanced_components(self):
"""
Test the handling of advanced component templates.
......@@ -1510,6 +1545,11 @@ class TestComponentTemplates(CourseTestCase):
self.assertNotEqual(only_template.get('category'), 'video')
self.assertNotEqual(only_template.get('category'), 'openassessment')
# Now fully disable word_cloud through XBlockConfiguration
XBlockConfiguration.objects.create(name='word_cloud', enabled=False)
self.templates = get_component_templates(self.course)
self.assertIsNone(self.get_templates_of_type('advanced'))
def test_advanced_problems(self):
"""
Test the handling of advanced problem templates.
......@@ -1520,44 +1560,101 @@ class TestComponentTemplates(CourseTestCase):
self.assertEqual(circuit_template.get('category'), 'problem')
self.assertEqual(circuit_template.get('boilerplate_name'), 'circuitschematic.yaml')
@patch('django.conf.settings.DEPRECATED_ADVANCED_COMPONENT_TYPES', [])
def test_deprecated_no_advance_component_button(self):
"""
Test that there will be no `Advanced` button on unit page if units are
deprecated provided that they are the only modules in `Advanced Module List`
Test that there will be no `Advanced` button on unit page if xblocks have disabled
Studio support given that they are the only modules in `Advanced Module List`
"""
XBlockDisableConfig.objects.create(disabled_create_blocks='poll survey', enabled=True)
# Update poll and survey to have "enabled=False".
XBlockStudioConfiguration.objects.create(name='poll', enabled=False, support_level="fs")
XBlockStudioConfiguration.objects.create(name='survey', enabled=False, support_level="fs")
XBlockStudioConfigurationFlag.objects.create(enabled=True)
self.course.advanced_modules.extend(['poll', 'survey'])
templates = get_component_templates(self.course)
button_names = [template['display_name'] for template in templates]
self.assertNotIn('Advanced', button_names)
@patch('django.conf.settings.DEPRECATED_ADVANCED_COMPONENT_TYPES', [])
def test_cannot_create_deprecated_problems(self):
"""
Test that we can't create problems if they are deprecated
Test that xblocks that have Studio support disabled do not show on the "new component" menu.
"""
XBlockDisableConfig.objects.create(disabled_create_blocks='poll survey', enabled=True)
# Update poll to have "enabled=False".
XBlockStudioConfiguration.objects.create(name='poll', enabled=False, support_level="fs")
XBlockStudioConfigurationFlag.objects.create(enabled=True)
self.course.advanced_modules.extend(['annotatable', 'poll', 'survey'])
templates = get_component_templates(self.course)
button_names = [template['display_name'] for template in templates]
self.assertIn('Advanced', button_names)
self.assertEqual(len(templates[0]['templates']), 1)
template_display_names = [template['display_name'] for template in templates[0]['templates']]
self.assertEqual(template_display_names, ['Annotation'])
# Annotatable doesn't show up because it is unsupported (in test setUp).
self._verify_advanced_xblocks(['Survey'], ['ps'])
# Now enable unsupported components.
self.course.allow_unsupported_xblocks = True
self._verify_advanced_xblocks(['Annotation', 'Survey'], ['us', 'ps'])
@patch('django.conf.settings.DEPRECATED_ADVANCED_COMPONENT_TYPES', ['poll'])
def test_create_non_deprecated_problems(self):
# Now disable Annotatable completely through XBlockConfiguration
XBlockConfiguration.objects.create(name='annotatable', enabled=False)
self._verify_advanced_xblocks(['Survey'], ['ps'])
def test_create_support_level_flag_off(self):
"""
Test that we can create problems if they are not deprecated
Test that we can create any advanced xblock (that isn't completely disabled through
XBlockConfiguration) if XBlockStudioConfigurationFlag is False.
"""
XBlockStudioConfigurationFlag.objects.create(enabled=False)
self.course.advanced_modules.extend(['annotatable', 'survey'])
self._verify_advanced_xblocks(['Annotation', 'Survey'], [True, True])
def test_xblock_masquerading_as_problem(self):
"""
Test the integration of xblocks masquerading as problems.
"""
def get_openassessment():
""" Helper method to return the openassessment template from problem list """
self.templates = get_component_templates(self.course)
problem_templates = self.get_templates_of_type('problem')
return self.get_template(problem_templates, u'Peer Assessment')
def verify_openassessment_present(support_level):
""" Helper method to verify that openassessment template is present """
openassessment = get_openassessment()
self.assertIsNotNone(openassessment)
self.assertEqual(openassessment.get('category'), 'openassessment')
self.assertEqual(openassessment.get('support_level'), support_level)
verify_openassessment_present(True)
# Now enable XBlockStudioConfigurationFlag. The openassessment block is marked
# unsupported, so will no longer show up.
XBlockStudioConfigurationFlag.objects.create(enabled=True)
self.assertIsNone(get_openassessment())
# Now allow unsupported components.
self.course.allow_unsupported_xblocks = True
verify_openassessment_present('us')
# Now disable openassessment completely through XBlockConfiguration
XBlockConfiguration.objects.create(name='openassessment', enabled=False)
self.assertIsNone(get_openassessment())
def _verify_advanced_xblocks(self, expected_xblocks, expected_support_levels):
"""
Verify the names of the advanced xblocks showing in the "new component" menu.
"""
self.course.advanced_modules.extend(['annotatable', 'poll', 'survey'])
templates = get_component_templates(self.course)
button_names = [template['display_name'] for template in templates]
self.assertIn('Advanced', button_names)
self.assertEqual(len(templates[0]['templates']), 2)
self.assertEqual(len(templates[0]['templates']), len(expected_xblocks))
template_display_names = [template['display_name'] for template in templates[0]['templates']]
self.assertEqual(template_display_names, ['Annotation', 'Survey'])
self.assertEqual(template_display_names, expected_xblocks)
template_support_levels = [template['support_level'] for template in templates[0]['templates']]
self.assertEqual(template_support_levels, expected_support_levels)
def _verify_basic_component(self, component_type, display_name, support_level=True):
"""
Verify the display name and support level of basic components (that have no boilerplates).
"""
templates = self.get_templates_of_type(component_type)
self.assertEqual(1, len(templates))
self.assertEqual(display_name, templates[0]['display_name'])
self.assertEqual(support_level, templates[0]['support_level'])
@ddt.ddt
......
......@@ -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 () {
......
define(["jquery", "underscore", "underscore.string", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers",
"common/js/spec_helpers/template_helpers", "js/spec_helpers/edit_helpers",
"js/views/pages/container", "js/views/pages/paged_container", "js/models/xblock_info", "jquery.simulate"],
function ($, _, str, AjaxHelpers, TemplateHelpers, EditHelpers, ContainerPage, PagedContainerPage, XBlockInfo) {
"js/views/pages/container", "js/views/pages/paged_container", "js/models/xblock_info",
"js/collections/component_template", "jquery.simulate"],
function ($, _, str, AjaxHelpers, TemplateHelpers, EditHelpers, ContainerPage, PagedContainerPage,
XBlockInfo, ComponentTemplates) {
'use strict';
function parameterized_suite(label, globalPageOptions) {
......@@ -55,18 +57,19 @@ define(["jquery", "underscore", "underscore.string", "edx-ui-toolkit/js/utils/sp
);
};
getContainerPage = function (options) {
getContainerPage = function (options, componentTemplates) {
var default_options = {
model: model,
templates: EditHelpers.mockComponentTemplates,
templates: componentTemplates === undefined ?
EditHelpers.mockComponentTemplates : componentTemplates,
el: $('#content')
};
return new PageClass(_.extend(options || {}, globalPageOptions, default_options));
};
renderContainerPage = function (test, html, options) {
renderContainerPage = function (test, html, options, componentTemplates) {
requests = AjaxHelpers.requests(test);
containerPage = getContainerPage(options);
containerPage = getContainerPage(options, componentTemplates);
containerPage.render();
respondWithHtml(html);
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
......@@ -652,6 +655,138 @@ define(["jquery", "underscore", "underscore.string", "edx-ui-toolkit/js/utils/sp
"parent_locator": "locator-group-A"
});
});
it('does not show the support legend if show_legend is false', function () {
// By default, show_legend is false in the mock component Templates.
renderContainerPage(this, mockContainerXBlockHtml);
showTemplatePicker();
expect(containerPage.$('.support-documentation').length).toBe(0);
});
it('does show the support legend if show_legend is true', function () {
var templates = new ComponentTemplates([
{
"templates": [
{
"category": "html",
"boilerplate_name": null,
"display_name": "Text"
}, {
"category": "html",
"boilerplate_name": "announcement.yaml",
"display_name": "Announcement"
}, {
"category": "html",
"boilerplate_name": "raw.yaml",
"display_name": "Raw HTML"
}],
"type": "html",
"support_legend": {
"show_legend": true,
"documentation_label": "Documentation Label:",
"allow_unsupported_xblocks": false
}
}],
{
parse: true
}), supportDocumentation;
renderContainerPage(this, mockContainerXBlockHtml, {}, templates);
showTemplatePicker();
supportDocumentation = containerPage.$('.support-documentation');
// On this page, groups are being shown, each of which has a new component menu.
expect(supportDocumentation.length).toBeGreaterThan(0);
// check that the documentation label is displayed
expect($(supportDocumentation[0]).find('.support-documentation-link').text().trim())
.toBe('Documentation Label:');
// show_unsupported_xblocks is false, so only 2 support levels should be shown
expect($(supportDocumentation[0]).find('.support-documentation-level').length).toBe(2);
});
it('does show unsupported level if enabled', function () {
var templates = new ComponentTemplates([
{
"templates": [
{
"category": "html",
"boilerplate_name": null,
"display_name": "Text"
}, {
"category": "html",
"boilerplate_name": "announcement.yaml",
"display_name": "Announcement"
}, {
"category": "html",
"boilerplate_name": "raw.yaml",
"display_name": "Raw HTML"
}],
"type": "html",
"support_legend": {
"show_legend": true,
"documentation_label": "Documentation Label:",
"allow_unsupported_xblocks": true
}
}],
{
parse: true
}), supportDocumentation;
renderContainerPage(this, mockContainerXBlockHtml, {}, templates);
showTemplatePicker();
supportDocumentation = containerPage.$('.support-documentation');
// show_unsupported_xblocks is true, so 3 support levels should be shown
expect($(supportDocumentation[0]).find('.support-documentation-level').length).toBe(3);
// verify only one has the unsupported item
expect($(supportDocumentation[0]).find('.fa-circle-o').length).toBe(1);
});
it('does render support level indicators if present in JSON', function () {
var templates = new ComponentTemplates([
{
"templates": [
{
"category": "html",
"boilerplate_name": null,
"display_name": "Text",
"support_level": "fs"
}, {
"category": "html",
"boilerplate_name": "announcement.yaml",
"display_name": "Announcement",
"support_level": "ps"
}, {
"category": "html",
"boilerplate_name": "raw.yaml",
"display_name": "Raw HTML",
"support_level": "us"
}],
"type": "html",
"support_legend": {
"show_legend": true,
"documentation_label": "Documentation Label:",
"allow_unsupported_xblocks": true
}
}],
{
parse: true
}), supportLevelIndicators, getScreenReaderText;
renderContainerPage(this, mockContainerXBlockHtml, {}, templates);
showTemplatePicker();
supportLevelIndicators = $(containerPage.$('.new-component-template')[0])
.find('.support-level');
expect(supportLevelIndicators.length).toBe(3);
getScreenReaderText = function(index){
return $($(supportLevelIndicators[index]).siblings()[0]).text().trim();
};
// Verify one level of each type was rendered.
expect(getScreenReaderText(0)).toBe('Fully Supported');
expect(getScreenReaderText(1)).toBe('Provisionally Supported');
expect(getScreenReaderText(2)).toBe('Not Supported');
});
});
});
});
......
......@@ -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