Commit d4cc7b8f by cahrens

Support level support for Studio xblock creation.

TNL-4670
parent 71bebec5
...@@ -691,7 +691,6 @@ class MiscCourseTests(ContentStoreTestCase): ...@@ -691,7 +691,6 @@ class MiscCourseTests(ContentStoreTestCase):
# Test that malicious code does not appear in html # Test that malicious code does not appear in html
self.assertNotIn(malicious_code, resp.content) self.assertNotIn(malicious_code, resp.content)
@patch('django.conf.settings.DEPRECATED_ADVANCED_COMPONENT_TYPES', [])
def test_advanced_components_in_edit_unit(self): 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 # This could be made better, but for now let's just assert that we see the advanced modules mentioned in the page
# response HTML # response HTML
......
...@@ -21,6 +21,7 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration ...@@ -21,6 +21,7 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.djangoapps.models.course_details import CourseDetails from openedx.core.djangoapps.models.course_details import CourseDetails
from student.roles import CourseInstructorRole, CourseStaffRole from student.roles import CourseInstructorRole, CourseStaffRole
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xblock_django.models import XBlockStudioConfigurationFlag
from xmodule.fields import Date from xmodule.fields import Date
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -784,6 +785,15 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -784,6 +785,15 @@ class CourseMetadataEditingTest(CourseTestCase):
) )
self.assertNotIn('edxnotes', test_model) 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): def test_validate_from_json_correct_inputs(self):
is_valid, errors, test_model = CourseMetadata.validate_and_update_from_json( is_valid, errors, test_model = CourseMetadata.validate_and_update_from_json(
self.course, self.course,
......
...@@ -27,7 +27,10 @@ from opaque_keys.edx.keys import UsageKey ...@@ -27,7 +27,10 @@ from opaque_keys.edx.keys import UsageKey
from student.auth import has_course_author_access from student.auth import has_course_author_access
from django.utils.translation import ugettext as _ 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__ = [ __all__ = [
'container_handler', 'container_handler',
...@@ -47,17 +50,41 @@ CONTAINER_TEMPLATES = [ ...@@ -47,17 +50,41 @@ CONTAINER_TEMPLATES = [
"basic-modal", "modal-button", "edit-xblock-modal", "basic-modal", "modal-button", "edit-xblock-modal",
"editor-mode-button", "upload-dialog", "editor-mode-button", "upload-dialog",
"add-xblock-component", "add-xblock-component-button", "add-xblock-component-menu", "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", "unit-outline", "container-message", "license-selector",
] ]
def _advanced_component_types(): def _advanced_component_types(show_unsupported):
""" """
Return advanced component types which can be created. 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() enabled_block_types = _filter_disabled_blocks(ADVANCED_COMPONENT_TYPES)
return [c_type for c_type in ADVANCED_COMPONENT_TYPES if c_type not in disabled_create_block_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): def _load_mixed_class(category):
...@@ -152,13 +179,14 @@ def get_component_templates(courselike, library=False): ...@@ -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. 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. Creates a component template dict.
Parameters Parameters
display_name: the user-visible name of the component display_name: the user-visible name of the component
category: the type of component (problem, html, etc.) 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. boilerplate_name: name of boilerplate for filling in default values. May be None.
hinted: True if hinted problem else False hinted: True if hinted problem else False
tab: common(default)/advanced, which tab it goes in tab: common(default)/advanced, which tab it goes in
...@@ -166,10 +194,50 @@ def get_component_templates(courselike, library=False): ...@@ -166,10 +194,50 @@ def get_component_templates(courselike, library=False):
""" """
return { return {
"display_name": name, "display_name": name,
"category": cat, "category": category,
"boilerplate_name": boilerplate_name, "boilerplate_name": boilerplate_name,
"hinted": hinted, "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 = { component_display_names = {
...@@ -189,14 +257,25 @@ def get_component_templates(courselike, library=False): ...@@ -189,14 +257,25 @@ def get_component_templates(courselike, library=False):
if library: if library:
component_types = [component for component in component_types if component != 'discussion'] 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: 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 = [] templates_for_category = []
component_class = _load_mixed_class(category) component_class = _load_mixed_class(category)
if support_level_without_template:
# add the default template with localized display name # add the default template with localized display name
# TODO: Once mixins are defined per-application, rather than per-runtime, # TODO: Once mixins are defined per-application, rather than per-runtime,
# this should use a cms mixed-in class. (cpennington) # this should use a cms mixed-in class. (cpennington)
display_name = xblock_type_display_name(category, _('Blank')) # this is the Blank Advanced problem 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')) templates_for_category.append(
create_template_dict(display_name, category, support_level_without_template, None, 'advanced')
)
categories.add(category) categories.add(category)
# add boilerplates # add boilerplates
...@@ -204,6 +283,11 @@ def get_component_templates(courselike, library=False): ...@@ -204,6 +283,11 @@ def get_component_templates(courselike, library=False):
for template in component_class.templates(): for template in component_class.templates():
filter_templates = getattr(component_class, 'filter_templates', None) filter_templates = getattr(component_class, 'filter_templates', None)
if not filter_templates or filter_templates(template, courselike): if not filter_templates or filter_templates(template, courselike):
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' # Tab can be 'common' 'advanced'
# Default setting is common/advanced depending on the presence of markdown # Default setting is common/advanced depending on the presence of markdown
tab = 'common' tab = 'common'
...@@ -215,31 +299,50 @@ def get_component_templates(courselike, library=False): ...@@ -215,31 +299,50 @@ def get_component_templates(courselike, library=False):
create_template_dict( create_template_dict(
_(template['metadata'].get('display_name')), # pylint: disable=translation-of-non-string _(template['metadata'].get('display_name')), # pylint: disable=translation-of-non-string
category, category,
template.get('template_id'), support_level_with_template,
template_id,
tab, tab,
hinted, 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': 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'] component = advanced_problem_type['component']
boilerplate_name = advanced_problem_type['boilerplate_name'] boilerplate_name = advanced_problem_type['boilerplate_name']
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: try:
component_display_name = xblock_type_display_name(component) component_display_name = xblock_type_display_name(component)
except PluginMissingError: except PluginMissingError:
log.warning('Unable to load xblock type %s to read display_name', component, exc_info=True) log.warning('Unable to load xblock type %s to read display_name', component, exc_info=True)
else: else:
templates_for_category.append( templates_for_category.append(
create_template_dict(component_display_name, component, boilerplate_name, 'advanced') create_template_dict(
component_display_name,
component,
advanced_component_support_level,
boilerplate_name,
'advanced'
)
) )
categories.add(component) categories.add(component)
component_templates.append({ component_templates.append({
"type": category, "type": category,
"templates": templates_for_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. # Libraries do not support advanced components at this time.
...@@ -251,19 +354,25 @@ def get_component_templates(courselike, library=False): ...@@ -251,19 +354,25 @@ def get_component_templates(courselike, library=False):
# are the names of the modules in ADVANCED_COMPONENT_TYPES that should be # are the names of the modules in ADVANCED_COMPONENT_TYPES that should be
# enabled for the course. # enabled for the course.
course_advanced_keys = courselike.advanced_modules course_advanced_keys = courselike.advanced_modules
advanced_component_templates = {"type": "advanced", "templates": [], "display_name": _("Advanced")} advanced_component_templates = {
advanced_component_types = _advanced_component_types() "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 # Set component types according to course policy file
if isinstance(course_advanced_keys, list): if isinstance(course_advanced_keys, list):
for category in course_advanced_keys: 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 # boilerplates not supported for advanced components
try: try:
component_display_name = xblock_type_display_name(category, default_display_name=category) component_display_name = xblock_type_display_name(category, default_display_name=category)
advanced_component_templates['templates'].append( advanced_component_templates['templates'].append(
create_template_dict( create_template_dict(
component_display_name, component_display_name,
category category,
advanced_component_types[category]
) )
) )
categories.add(category) categories.add(category)
...@@ -288,6 +397,14 @@ def get_component_templates(courselike, library=False): ...@@ -288,6 +397,14 @@ def get_component_templates(courselike, library=False):
return component_templates 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 @login_required
def _get_item_in_course(request, usage_key): def _get_item_in_course(request, usage_key):
""" """
......
...@@ -24,7 +24,7 @@ from contentstore.views.item import ( ...@@ -24,7 +24,7 @@ from contentstore.views.item import (
) )
from contentstore.tests.utils import CourseTestCase from contentstore.tests.utils import CourseTestCase
from student.tests.factories import UserFactory 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.capa_module import CapaDescriptor
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -1457,12 +1457,19 @@ class TestComponentTemplates(CourseTestCase): ...@@ -1457,12 +1457,19 @@ class TestComponentTemplates(CourseTestCase):
def setUp(self): def setUp(self):
super(TestComponentTemplates, self).setUp() 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 self.templates = get_component_templates(self.course)
XBlockDisableConfig.objects.create(
disabled_create_blocks='', enabled=True
)
def get_templates_of_type(self, template_type): def get_templates_of_type(self, template_type):
""" """
...@@ -1481,12 +1488,40 @@ class TestComponentTemplates(CourseTestCase): ...@@ -1481,12 +1488,40 @@ class TestComponentTemplates(CourseTestCase):
""" """
Test the handling of the basic component templates. Test the handling of the basic component templates.
""" """
self.assertIsNotNone(self.get_templates_of_type('discussion')) self._verify_basic_component("discussion", "Discussion")
self.assertIsNotNone(self.get_templates_of_type('html')) self._verify_basic_component("video", "Video")
self.assertIsNotNone(self.get_templates_of_type('problem')) self.assertGreater(self.get_templates_of_type('html'), 0)
self.assertIsNotNone(self.get_templates_of_type('video')) self.assertGreater(self.get_templates_of_type('problem'), 0)
self.assertIsNone(self.get_templates_of_type('advanced')) 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): def test_advanced_components(self):
""" """
Test the handling of advanced component templates. Test the handling of advanced component templates.
...@@ -1510,6 +1545,11 @@ class TestComponentTemplates(CourseTestCase): ...@@ -1510,6 +1545,11 @@ class TestComponentTemplates(CourseTestCase):
self.assertNotEqual(only_template.get('category'), 'video') self.assertNotEqual(only_template.get('category'), 'video')
self.assertNotEqual(only_template.get('category'), 'openassessment') 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): def test_advanced_problems(self):
""" """
Test the handling of advanced problem templates. Test the handling of advanced problem templates.
...@@ -1520,44 +1560,101 @@ class TestComponentTemplates(CourseTestCase): ...@@ -1520,44 +1560,101 @@ class TestComponentTemplates(CourseTestCase):
self.assertEqual(circuit_template.get('category'), 'problem') self.assertEqual(circuit_template.get('category'), 'problem')
self.assertEqual(circuit_template.get('boilerplate_name'), 'circuitschematic.yaml') 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): def test_deprecated_no_advance_component_button(self):
""" """
Test that there will be no `Advanced` button on unit page if units are Test that there will be no `Advanced` button on unit page if xblocks have disabled
deprecated provided that they are the only modules in `Advanced Module List` 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']) self.course.advanced_modules.extend(['poll', 'survey'])
templates = get_component_templates(self.course) templates = get_component_templates(self.course)
button_names = [template['display_name'] for template in templates] button_names = [template['display_name'] for template in templates]
self.assertNotIn('Advanced', button_names) self.assertNotIn('Advanced', button_names)
@patch('django.conf.settings.DEPRECATED_ADVANCED_COMPONENT_TYPES', [])
def test_cannot_create_deprecated_problems(self): 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']) self.course.advanced_modules.extend(['annotatable', 'poll', 'survey'])
templates = get_component_templates(self.course) # Annotatable doesn't show up because it is unsupported (in test setUp).
button_names = [template['display_name'] for template in templates] self._verify_advanced_xblocks(['Survey'], ['ps'])
self.assertIn('Advanced', button_names)
self.assertEqual(len(templates[0]['templates']), 1) # Now enable unsupported components.
template_display_names = [template['display_name'] for template in templates[0]['templates']] self.course.allow_unsupported_xblocks = True
self.assertEqual(template_display_names, ['Annotation']) self._verify_advanced_xblocks(['Annotation', 'Survey'], ['us', 'ps'])
# Now disable Annotatable completely through XBlockConfiguration
XBlockConfiguration.objects.create(name='annotatable', enabled=False)
self._verify_advanced_xblocks(['Survey'], ['ps'])
@patch('django.conf.settings.DEPRECATED_ADVANCED_COMPONENT_TYPES', ['poll']) def test_create_support_level_flag_off(self):
def test_create_non_deprecated_problems(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) templates = get_component_templates(self.course)
button_names = [template['display_name'] for template in templates] button_names = [template['display_name'] for template in templates]
self.assertIn('Advanced', button_names) 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']] 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 @ddt.ddt
......
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
Django module for Course Metadata class -- manages advanced settings and related parameters Django module for Course Metadata class -- manages advanced settings and related parameters
""" """
from xblock.fields import Scope from xblock.fields import Scope
from xblock_django.models import XBlockStudioConfigurationFlag
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.conf import settings from django.conf import settings
...@@ -93,6 +95,11 @@ class CourseMetadata(object): ...@@ -93,6 +95,11 @@ class CourseMetadata(object):
filtered_list.append('enable_ccx') filtered_list.append('enable_ccx')
filtered_list.append('ccx_connector') 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 return filtered_list
@classmethod @classmethod
......
...@@ -398,9 +398,6 @@ ADVANCED_SECURITY_CONFIG = ENV_TOKENS.get('ADVANCED_SECURITY_CONFIG', {}) ...@@ -398,9 +398,6 @@ ADVANCED_SECURITY_CONFIG = ENV_TOKENS.get('ADVANCED_SECURITY_CONFIG', {})
################ ADVANCED COMPONENT/PROBLEM TYPES ############### ################ ADVANCED COMPONENT/PROBLEM TYPES ###############
ADVANCED_PROBLEM_TYPES = ENV_TOKENS.get('ADVANCED_PROBLEM_TYPES', ADVANCED_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 ############### ################ VIDEO UPLOAD PIPELINE ###############
......
...@@ -1132,19 +1132,6 @@ XBLOCK_SETTINGS = { ...@@ -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 ################################ ################################ Settings for Credit Course Requirements ################################
# Initial delay used for retrying tasks. # Initial delay used for retrying tasks.
# Additional retries use longer delays. # Additional retries use longer delays.
......
...@@ -10,7 +10,8 @@ define(["backbone"], function (Backbone) { ...@@ -10,7 +10,8 @@ define(["backbone"], function (Backbone) {
// category (may or may not match "type") // category (may or may not match "type")
// boilerplate_name (may be null) // boilerplate_name (may be null)
// is_common (only used for problems) // is_common (only used for problems)
templates: [] templates: [],
support_legend: {}
}, },
parse: function (response) { parse: function (response) {
// Returns true only for templates that both have no boilerplate and are of // Returns true only for templates that both have no boilerplate and are of
...@@ -24,6 +25,7 @@ define(["backbone"], function (Backbone) { ...@@ -24,6 +25,7 @@ define(["backbone"], function (Backbone) {
this.type = response.type; this.type = response.type;
this.templates = response.templates; this.templates = response.templates;
this.display_name = response.display_name; this.display_name = response.display_name;
this.support_legend = response.support_legend;
// Sort the templates. // Sort the templates.
this.templates.sort(function (a, b) { this.templates.sort(function (a, b) {
......
...@@ -49,7 +49,8 @@ define(["js/models/component_template"], ...@@ -49,7 +49,8 @@ define(["js/models/component_template"],
"boilerplate_name": "alternate_word_cloud.yaml", "boilerplate_name": "alternate_word_cloud.yaml",
"display_name": "Word Cloud" "display_name": "Word Cloud"
}], }],
"type": "problem" "type": "problem",
"support_legend": {"show_legend": false}
}; };
it('orders templates correctly', function () { it('orders templates correctly', function () {
......
define(["jquery", "underscore", "underscore.string", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", 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", "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"], "js/views/pages/container", "js/views/pages/paged_container", "js/models/xblock_info",
function ($, _, str, AjaxHelpers, TemplateHelpers, EditHelpers, ContainerPage, PagedContainerPage, XBlockInfo) { "js/collections/component_template", "jquery.simulate"],
function ($, _, str, AjaxHelpers, TemplateHelpers, EditHelpers, ContainerPage, PagedContainerPage,
XBlockInfo, ComponentTemplates) {
'use strict'; 'use strict';
function parameterized_suite(label, globalPageOptions) { function parameterized_suite(label, globalPageOptions) {
...@@ -55,18 +57,19 @@ define(["jquery", "underscore", "underscore.string", "edx-ui-toolkit/js/utils/sp ...@@ -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 = { var default_options = {
model: model, model: model,
templates: EditHelpers.mockComponentTemplates, templates: componentTemplates === undefined ?
EditHelpers.mockComponentTemplates : componentTemplates,
el: $('#content') el: $('#content')
}; };
return new PageClass(_.extend(options || {}, globalPageOptions, default_options)); return new PageClass(_.extend(options || {}, globalPageOptions, default_options));
}; };
renderContainerPage = function (test, html, options) { renderContainerPage = function (test, html, options, componentTemplates) {
requests = AjaxHelpers.requests(test); requests = AjaxHelpers.requests(test);
containerPage = getContainerPage(options); containerPage = getContainerPage(options, componentTemplates);
containerPage.render(); containerPage.render();
respondWithHtml(html); respondWithHtml(html);
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
...@@ -652,6 +655,138 @@ define(["jquery", "underscore", "underscore.string", "edx-ui-toolkit/js/utils/sp ...@@ -652,6 +655,138 @@ define(["jquery", "underscore", "underscore.string", "edx-ui-toolkit/js/utils/sp
"parent_locator": "locator-group-A" "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 ...@@ -41,12 +41,13 @@ define(["jquery", "underscore", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
mockComponentTemplates = new ComponentTemplates([ mockComponentTemplates = new ComponentTemplates([
{ {
templates: [ "templates": [
{ {
category: 'discussion', "category": "discussion",
display_name: 'Discussion' "display_name": "Discussion"
}], }],
type: 'discussion' "type": "discussion",
"support_legend": {"show_legend": false}
}, { }, {
"templates": [ "templates": [
{ {
...@@ -62,7 +63,8 @@ define(["jquery", "underscore", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe ...@@ -62,7 +63,8 @@ define(["jquery", "underscore", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe
"boilerplate_name": "raw.yaml", "boilerplate_name": "raw.yaml",
"display_name": "Raw HTML" "display_name": "Raw HTML"
}], }],
"type": "html" "type": "html",
"support_legend": {"show_legend": false}
}], }],
{ {
parse: true parse: true
...@@ -76,6 +78,8 @@ define(["jquery", "underscore", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpe ...@@ -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-button');
TemplateHelpers.installTemplate('add-xblock-component-menu'); TemplateHelpers.installTemplate('add-xblock-component-menu');
TemplateHelpers.installTemplate('add-xblock-component-menu-problem'); 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 // Add templates needed by the edit XBlock modal
TemplateHelpers.installTemplate('edit-xblock-modal'); TemplateHelpers.installTemplate('edit-xblock-modal');
......
define(["jquery", "js/views/baseview"], define(["jquery", "js/views/baseview", 'edx-ui-toolkit/js/utils/html-utils'],
function ($, BaseView) { function ($, BaseView, HtmlUtils) {
return BaseView.extend({ return BaseView.extend({
className: function () { className: function () {
...@@ -9,8 +9,19 @@ define(["jquery", "js/views/baseview"], ...@@ -9,8 +9,19 @@ define(["jquery", "js/views/baseview"],
BaseView.prototype.initialize.call(this); BaseView.prototype.initialize.call(this);
var template_name = this.model.type === "problem" ? "add-xblock-component-menu-problem" : var template_name = this.model.type === "problem" ? "add-xblock-component-menu-problem" :
"add-xblock-component-menu"; "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.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" // Make the tabs on problems into "real tabs"
this.$('.tab-group').tabs(); this.$('.tab-group').tabs();
} }
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
// +Base - Utilities // +Base - Utilities
// ==================== // ====================
@import 'variables'; @import 'partials/variables';
@import 'mixins'; @import 'mixins';
@import 'mixins-inherited'; @import 'mixins-inherited';
......
...@@ -168,18 +168,47 @@ ...@@ -168,18 +168,47 @@
// specific menu types // specific menu types
&.new-component-problem { &.new-component-problem {
padding-bottom: ($baseline/2);
.problem-type-tabs { .problem-type-tabs {
display: inline-block; 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 // individual menus
// -------------------- // --------------------
.new-component-template { .new-component-template {
@include clearfix(); @include clearfix();
margin-bottom: 0;
li { li {
border: none; border: none;
...@@ -201,11 +230,16 @@ ...@@ -201,11 +230,16 @@
background: $white; background: $white;
color: $gray-d3; color: $gray-d3;
text-align: left; text-align: left;
font-family: $f-sans-serif;
&:hover { &:hover {
@include transition(background-color $tmg-f2 linear 0s); @include transition(background-color $tmg-f2 linear 0s);
background: tint($green,30%); background: tint($green,30%);
color: $white; color: $white;
.icon {
color: $white;
}
} }
} }
} }
......
...@@ -39,6 +39,16 @@ $f-monospace: 'Bitstream Vera Sans Mono', Consolas, Courier, monospace; ...@@ -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 $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 // +Colors - Primary
// ==================== // ====================
$black: rgb(0,0,0); $black: rgb(0,0,0);
...@@ -87,12 +97,6 @@ $blue-t1: rgba($blue, 0.25); ...@@ -87,12 +97,6 @@ $blue-t1: rgba($blue, 0.25);
$blue-t2: rgba($blue, 0.50); $blue-t2: rgba($blue, 0.50);
$blue-t3: rgba($blue, 0.75); $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: rgb(183, 37, 103); // #b72567;
$pink-l1: tint($pink,20%); $pink-l1: tint($pink,20%);
$pink-l2: tint($pink,40%); $pink-l2: tint($pink,40%);
......
...@@ -7,10 +7,10 @@ ...@@ -7,10 +7,10 @@
%>"> %>">
<ul class="problem-type-tabs nav-tabs" tabindex='-1'> <ul class="problem-type-tabs nav-tabs" tabindex='-1'>
<li class="current"> <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>
<li> <li>
<a class="link-tab" href="#tab2"><%= gettext("Advanced") %></a> <a class="link-tab" href="#tab2"><%- gettext("Advanced") %></a>
</li> </li>
</ul> </ul>
<div class="tab current" id="tab1"> <div class="tab current" id="tab1">
...@@ -19,15 +19,17 @@ ...@@ -19,15 +19,17 @@
<% if (templates[i].tab == "common") { %> <% if (templates[i].tab == "common") { %>
<% if (!templates[i].boilerplate_name) { %> <% if (!templates[i].boilerplate_name) { %>
<li class="editor-md empty"> <li class="editor-md empty">
<button type="button" class="button-component" data-category="<%= templates[i].category %>"> <button type="button" class="button-component" data-category="<%- templates[i].category %>">
<span class="name"><%= templates[i].display_name %></span> <%= HtmlUtils.HTML(support_indicator_template({support_level: templates[i].support_level})) %>
<span class="name"><%- templates[i].display_name %></span>
</button> </button>
</li> </li>
<% } else { %> <% } else { %>
<li class="editor-md"> <li class="editor-md">
<button type="button" class="button-component" data-category="<%= templates[i].category %>" <button type="button" class="button-component" data-category="<%- templates[i].category %>"
data-boilerplate="<%= templates[i].boilerplate_name %>"> data-boilerplate="<%- templates[i].boilerplate_name %>">
<span class="name"><%= templates[i].display_name %></span> <%= HtmlUtils.HTML(support_indicator_template({support_level: templates[i].support_level})) %>
<span class="name"><%- templates[i].display_name %></span>
</button> </button>
</li> </li>
<% } %> <% } %>
...@@ -40,14 +42,16 @@ ...@@ -40,14 +42,16 @@
<% for (var i = 0; i < templates.length; i++) { %> <% for (var i = 0; i < templates.length; i++) { %>
<% if (templates[i].tab == "advanced") { %> <% if (templates[i].tab == "advanced") { %>
<li class="editor-manual"> <li class="editor-manual">
<button type="button" class="button-component" data-category="<%= templates[i].category %>" <button type="button" class="button-component" data-category="<%- templates[i].category %>"
data-boilerplate="<%= templates[i].boilerplate_name %>"> data-boilerplate="<%- templates[i].boilerplate_name %>">
<span class="name"><%= templates[i].display_name %></span> <%= HtmlUtils.HTML(support_indicator_template({support_level: templates[i].support_level})) %>
<span class="name"><%- templates[i].display_name %></span>
</button> </button>
</li> </li>
<% } %> <% } %>
<% } %> <% } %>
</ul> </ul>
</div> </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> </div>
...@@ -10,20 +10,23 @@ ...@@ -10,20 +10,23 @@
<% for (var i = 0; i < templates.length; i++) { %> <% for (var i = 0; i < templates.length; i++) { %>
<% if (!templates[i].boilerplate_name) { %> <% if (!templates[i].boilerplate_name) { %>
<li class="editor-md empty"> <li class="editor-md empty">
<button type="button" class="button-component" data-category="<%= templates[i].category %>"> <button type="button" class="button-component" data-category="<%- templates[i].category %>">
<span class="name"><%= templates[i].display_name %></span> <%= HtmlUtils.HTML(support_indicator_template({support_level: templates[i].support_level})) %>
<span class="name"><%- templates[i].display_name %></span>
</button> </button>
</li> </li>
<% } else { %> <% } else { %>
<li class="editor-md"> <li class="editor-md">
<button type="button" class="button-component" data-category="<%= templates[i].category %>" <button type="button" class="button-component" data-category="<%- templates[i].category %>"
data-boilerplate="<%= templates[i].boilerplate_name %>"> data-boilerplate="<%- templates[i].boilerplate_name %>">
<span class="name"><%= templates[i].display_name %></span> <%= HtmlUtils.HTML(support_indicator_template({support_level: templates[i].support_level})) %>
<span class="name"><%- templates[i].display_name %></span>
</button> </button>
</li> </li>
<% } %> <% } %>
<% } %> <% } %>
</ul> </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> </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(): ...@@ -22,11 +22,11 @@ def disabled_xblocks():
def authorable_xblocks(allow_unsupported=False, name=None): def authorable_xblocks(allow_unsupported=False, name=None):
""" """
If Studio XBlock support state is enabled (via `XBlockStudioConfigurationFlag`), this method returns This method returns the QuerySet of XBlocks that can be created in Studio (by default, only fully supported
the QuerySet of XBlocks that can be created in Studio (by default, only fully supported and provisionally and provisionally supported XBlocks), as stored in `XBlockStudioConfiguration`.
supported). If `XBlockStudioConfigurationFlag` is not enabled, this method returns None. Note that this method does NOT check the value `XBlockStudioConfigurationFlag`, nor does it take into account
Note that this method does not take into account fully disabled xblocks (as returned fully disabled xblocks (as returned by `disabled_xblocks`) or deprecated xblocks
by `disabled_xblocks`) or deprecated xblocks (as returned by `deprecated_xblocks`). (as returned by `deprecated_xblocks`).
Arguments: Arguments:
allow_unsupported (bool): If `True`, enabled but unsupported XBlocks will also be returned. 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): ...@@ -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 name (str): If provided, filters the returned XBlocks to those with the provided name. This is
useful for XBlocks with lots of template types. useful for XBlocks with lots of template types.
Returns: Returns:
QuerySet: If `XBlockStudioConfigurationFlag` is enabled, returns authorable XBlocks, QuerySet: Returns authorable XBlocks, taking into account `support_level`, `enabled` and `name`
taking into account `support_level`, `enabled` and `name` (if specified). (if specified) as specified by `XBlockStudioConfiguration`. Does not take into account whether or not
If `XBlockStudioConfigurationFlag` is disabled, returns None. `XBlockStudioConfigurationFlag` is enabled.
""" """
if not XBlockStudioConfigurationFlag.is_enabled():
return None
blocks = XBlockStudioConfiguration.objects.current_set().filter(enabled=True) blocks = XBlockStudioConfiguration.objects.current_set().filter(enabled=True)
if not allow_unsupported: if not allow_unsupported:
blocks = blocks.exclude(support_level=XBlockStudioConfiguration.UNSUPPORTED) blocks = blocks.exclude(support_level=XBlockStudioConfiguration.UNSUPPORTED)
......
...@@ -41,27 +41,10 @@ class XBlockDisableConfig(ConfigurationModel): ...@@ -41,27 +41,10 @@ class XBlockDisableConfig(ConfigurationModel):
return block_type in config.disabled_blocks.split() 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): def __unicode__(self):
config = XBlockDisableConfig.current() config = XBlockDisableConfig.current()
return u"Disabled xblocks = {disabled_xblocks}\nDeprecated xblocks = {disabled_create_block_types}".format( return u"Disabled xblocks = {disabled_xblocks}".format(
disabled_xblocks=config.disabled_blocks, disabled_xblocks=config.disabled_blocks
disabled_create_block_types=config.disabled_create_block_types
) )
......
...@@ -61,28 +61,21 @@ class XBlockSupportTestCase(CacheIsolationTestCase): ...@@ -61,28 +61,21 @@ class XBlockSupportTestCase(CacheIsolationTestCase):
disabled_xblock_names = [block.name for block in disabled_xblocks()] disabled_xblock_names = [block.name for block in disabled_xblocks()]
self.assertItemsEqual(["survey", "poll"], disabled_xblock_names) 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): def test_authorable_blocks_empty_model(self):
""" """
Tests authorable_xblocks returns an empty list if the configuration flag is enabled but Tests authorable_xblocks returns an empty list if XBlockStudioConfiguration table is empty, regardless
the XBlockStudioConfiguration table is empty. of whether or not XBlockStudioConfigurationFlag is enabled.
""" """
XBlockStudioConfigurationFlag(enabled=True).save()
XBlockStudioConfiguration.objects.all().delete() 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))) self.assertEqual(0, len(authorable_xblocks(allow_unsupported=True)))
def test_authorable_blocks(self): 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()] authorable_xblock_names = [block.name for block in authorable_xblocks()]
self.assertItemsEqual(["done", "problem", "problem", "html"], authorable_xblock_names) self.assertItemsEqual(["done", "problem", "problem", "html"], authorable_xblock_names)
...@@ -99,7 +92,7 @@ class XBlockSupportTestCase(CacheIsolationTestCase): ...@@ -99,7 +92,7 @@ class XBlockSupportTestCase(CacheIsolationTestCase):
def test_authorable_blocks_by_name(self): 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): def verify_xblock_fields(name, template, support_level, block):
""" """
...@@ -109,8 +102,6 @@ class XBlockSupportTestCase(CacheIsolationTestCase): ...@@ -109,8 +102,6 @@ class XBlockSupportTestCase(CacheIsolationTestCase):
self.assertEqual(template, block.template) self.assertEqual(template, block.template)
self.assertEqual(support_level, block.support_level) self.assertEqual(support_level, block.support_level)
XBlockStudioConfigurationFlag(enabled=True).save()
# There are no xblocks with name video. # There are no xblocks with name video.
authorable_blocks = authorable_xblocks(name="video") authorable_blocks = authorable_xblocks(name="video")
self.assertEqual(0, len(authorable_blocks)) 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): ...@@ -408,7 +408,7 @@ class CourseFields(object):
) )
advanced_modules = List( advanced_modules = List(
display_name=_("Advanced Module 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 scope=Scope.settings
) )
has_children = True has_children = True
...@@ -830,6 +830,15 @@ class CourseFields(object): ...@@ -830,6 +830,15 @@ class CourseFields(object):
}, },
scope=Scope.settings 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 class CourseModule(CourseFields, SequenceModule): # pylint: disable=abstract-method
......
...@@ -2905,10 +2905,6 @@ APP_UPGRADE_CACHE_TIMEOUT = 3600 ...@@ -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. # if you want to avoid an overlap in ids while searching for history across the two tables.
STUDENTMODULEHISTORYEXTENDED_OFFSET = 10000 STUDENTMODULEHISTORYEXTENDED_OFFSET = 10000
# Deprecated xblock types
DEPRECATED_ADVANCED_COMPONENT_TYPES = []
# Cutoff date for granting audit certificates # Cutoff date for granting audit certificates
AUDIT_CERT_CUTOFF_DATE = None 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