Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions cms/djangoapps/contentstore/views/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import load_services_for_studio
from common.djangoapps.edxmako.shortcuts import render_to_response
from common.djangoapps.student.auth import has_course_author_access
from common.djangoapps.xblock_django.api import authorable_xblocks, disabled_xblocks
from common.djangoapps.xblock_django.api import authorable_xblocks, default_advanced_xblocks, disabled_xblocks
from common.djangoapps.xblock_django.models import XBlockStudioConfigurationFlag
from openedx.core.djangoapps.content_tagging.api import get_object_tags
from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration
Expand Down Expand Up @@ -73,6 +73,9 @@
"edit-title-button", "edit-upstream-alert",
]

# Advanced modules which are offered in every course, in addition to the ones each course lists in its own
# Advanced Module List. Operators can add to this list without forking the platform by marking XBlocks as
# advanced by default in XBlockStudioConfiguration (see `default_advanced_xblocks`).
DEFAULT_ADVANCED_MODULES = [
'google-calendar',
'google-document',
Expand Down Expand Up @@ -444,8 +447,10 @@ def create_support_legend_dict():
# Check if there are any advanced modules specified in the course policy.
# These modules should be specified as a list of strings, where the strings
# are the names of the modules in ADVANCED_COMPONENT_TYPES that should be
# enabled for the course.
course_advanced_keys = list(dict.fromkeys(courselike.advanced_modules + DEFAULT_ADVANCED_MODULES))
# enabled for the course. They are combined with the modules which are enabled for every course: the
# platform-wide DEFAULT_ADVANCED_MODULES, plus any which this operator has marked as advanced by default.
default_advanced_keys = DEFAULT_ADVANCED_MODULES + [block.name for block in default_advanced_xblocks()]
course_advanced_keys = list(dict.fromkeys(courselike.advanced_modules + default_advanced_keys))
advanced_component_templates = {
"type": "advanced",
"templates": [],
Expand Down
63 changes: 63 additions & 0 deletions cms/djangoapps/contentstore/views/tests/test_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -2945,6 +2945,69 @@ def test_advanced_components(self):
self.templates = get_component_templates(self.course)
self.assertTrue((not any(item.get("category") == "done" for item in self.get_templates_of_type("advanced")))) # noqa: PT009, UP034 # pylint: disable=line-too-long

def test_advanced_by_default_components(self):
"""
Test that an xblock which an operator has marked as advanced by default is offered to a course
which does not list it in its own Advanced Module List.
"""
advanced_categories = [
template.get("category") for template in self.get_templates_of_type("advanced")
]
self.assertNotIn("done", advanced_categories) # noqa: PT009

XBlockStudioConfiguration.objects.create(
name="done", enabled=True, support_level="fs", advanced_by_default=True
)
self.templates = get_component_templates(self.course)
advanced_templates = self.get_templates_of_type("advanced")
self.assertEqual(len(advanced_templates), len(DEFAULT_ADVANCED_MODULES) + 1) # noqa: PT009
done_template = self.get_template(advanced_templates, "Completion")
self.assertEqual(done_template.get("category"), "done") # noqa: PT009
self.assertIsNone(done_template.get("boilerplate_name", None)) # noqa: PT009

# Verify that the component is not added twice if the course lists it as well.
self.course.advanced_modules.append("done")
self.templates = get_component_templates(self.course)
self.assertEqual( # noqa: PT009
len(self.get_templates_of_type("advanced")), len(DEFAULT_ADVANCED_MODULES) + 1
)

# Now fully disable done through XBlockConfiguration.
XBlockConfiguration.objects.create(name="done", enabled=False)
self.templates = get_component_templates(self.course)
self.assertTrue((not any(item.get("category") == "done" for item in self.get_templates_of_type("advanced")))) # noqa: PT009, UP034 # pylint: disable=line-too-long

def test_advanced_by_default_components_support_levels(self):
"""
Test that an xblock which is advanced by default still honors Studio support levels, so that
opting an unsupported xblock into every course requires course author opt-in as usual.
"""
XBlockStudioConfiguration.objects.create(
name="done", enabled=True, support_level="us", advanced_by_default=True
)
XBlockStudioConfigurationFlag.objects.create(enabled=True)

self.templates = get_component_templates(self.course)
self.assertTrue((not any(item.get("category") == "done" for item in self.get_templates_of_type("advanced")))) # noqa: PT009, UP034 # pylint: disable=line-too-long

self.course.allow_unsupported_xblocks = True
self.templates = get_component_templates(self.course)
done_template = self.get_template(self.get_templates_of_type("advanced"), "Completion")
self.assertEqual(done_template.get("category"), "done") # noqa: PT009
self.assertEqual(done_template.get("support_level"), "us") # noqa: PT009

def test_advanced_by_default_components_not_offered_to_libraries(self):
"""
Test that xblocks which are advanced by default are not offered to libraries, which do not
support advanced components at all.
"""
XBlockStudioConfiguration.objects.create(
name="done", enabled=True, support_level="fs", advanced_by_default=True
)
library = LibraryFactory.create()
self.templates = get_component_templates(library, library=True)
self.assertIsNone(self.get_templates_of_type("advanced")) # noqa: PT009

def test_deprecated_no_advance_component_button(self):
"""
Test that there will be no `Advanced` button on unit page if xblocks have disabled
Expand Down
9 changes: 9 additions & 0 deletions common/djangoapps/xblock_django/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ class XBlockStudioConfigurationAdmin(KeyedConfigurationModelAdmin):
),
'fields': ('support_level',)
}),
('Enable in All Courses', {
'description': _(
"XBlocks that are advanced by default are offered in the Advanced component list of every course, "
"so that course teams do not have to add them to each course's Advanced Module List. The XBlock "
"must also be enabled above and in XBlockConfiguration, and, if XBlockStudioConfigurationFlag is "
"enabled, have a support level which allows course authors to create it."
),
'fields': ('advanced_by_default',)
}),
)


Expand Down
16 changes: 16 additions & 0 deletions common/djangoapps/xblock_django/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from openedx.core.lib.cache_utils import CacheInvalidationManager

cacher = CacheInvalidationManager(model=XBlockConfiguration)
studio_config_cacher = CacheInvalidationManager(model=XBlockStudioConfiguration)


@cacher
Expand All @@ -27,6 +28,21 @@ def disabled_xblocks():
return XBlockConfiguration.objects.current_set().filter(enabled=False)


@studio_config_cacher
def default_advanced_xblocks():
"""
Return the QuerySet of XBlock types which operators have marked as advanced by default, meaning that
Studio offers them in the Advanced component list of every course, without course teams having to add
them to the course's Advanced Module List.

Note that this method is independent of `XBlockStudioConfigurationFlag`: the flag governs whether support
levels are enforced, not whether an operator has opted an XBlock into every course. It does not take into
account fully disabled xblocks (as returned by `disabled_xblocks`) or deprecated xblocks (as returned by
`deprecated_xblocks`); callers are expected to filter those out, as `get_component_templates` does.
"""
return XBlockStudioConfiguration.objects.current_set().filter(enabled=True, advanced_by_default=True)


def authorable_xblocks(allow_unsupported=False, name=None):
"""
This method returns the QuerySet of XBlocks that can be created in Studio (by default, only fully supported
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('xblock_django', '0004_delete_xblock_disable_config'),
]

operations = [
migrations.AddField(
model_name='xblockstudioconfiguration',
name='advanced_by_default',
field=models.BooleanField(default=False, help_text="Offer this XBlock in the Advanced component list of every course, without course teams having to add it to the course's Advanced Module List. Has no effect on XBlocks that are already offered as basic components."),
),
]
12 changes: 10 additions & 2 deletions common/djangoapps/xblock_django/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,19 @@ class XBlockStudioConfiguration(ConfigurationModel):
name = models.CharField(max_length=255, null=False, db_index=True)
template = models.CharField(max_length=255, blank=True, default='')
support_level = models.CharField(max_length=2, choices=SUPPORT_CHOICES, default=UNSUPPORTED)
advanced_by_default = models.BooleanField(
default=False,
help_text=_(
"Offer this XBlock in the Advanced component list of every course, without course teams having to "
"add it to the course's Advanced Module List. Has no effect on XBlocks that are already offered as "
"basic components."
)
)

class Meta:
app_label = "xblock_django"

def __str__(self):
return ( # noqa: UP032
"XBlockStudioConfiguration(name={}, template={}, enabled={}, support_level={})"
).format(self.name, self.template, self.enabled, self.support_level)
"XBlockStudioConfiguration(name={}, template={}, enabled={}, support_level={}, advanced_by_default={})"
).format(self.name, self.template, self.enabled, self.support_level, self.advanced_by_default)
51 changes: 50 additions & 1 deletion common/djangoapps/xblock_django/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
"""
Tests related to XBlock support API.
"""
from common.djangoapps.xblock_django.api import authorable_xblocks, deprecated_xblocks, disabled_xblocks
from common.djangoapps.xblock_django.api import (
authorable_xblocks,
default_advanced_xblocks,
deprecated_xblocks,
disabled_xblocks,
)
from common.djangoapps.xblock_django.models import ( # pylint: disable=line-too-long
XBlockConfiguration,
XBlockStudioConfiguration,
Expand Down Expand Up @@ -65,6 +70,50 @@ def test_disabled_blocks(self):
disabled_xblock_names = [block.name for block in disabled_xblocks()]
self.assertCountEqual(["survey", "poll"], disabled_xblock_names) # noqa: PT009

def test_default_advanced_blocks(self):
""" Tests the default_advanced_xblocks method """

# None of the xblocks configured in setUp are advanced by default.
assert [] == [block.name for block in default_advanced_xblocks()]

XBlockStudioConfiguration(
name="done", template="", enabled=True, support_level=XBlockStudioConfiguration.FULL_SUPPORT,
advanced_by_default=True
).save()
# Note that support level is not taken into account: it is enforced by Studio (and only when
# XBlockStudioConfigurationFlag is enabled), not by this method.
XBlockStudioConfiguration(
name="split_module", template="", enabled=True, support_level=XBlockStudioConfiguration.UNSUPPORTED,
advanced_by_default=True
).save()
default_advanced_xblock_names = [block.name for block in default_advanced_xblocks()]
self.assertCountEqual(["done", "split_module"], default_advanced_xblock_names) # noqa: PT009

# An xblock which is disabled in XBlockStudioConfiguration cannot be created in Studio at all,
# so it is not advanced by default either.
XBlockStudioConfiguration(
name="done", template="", enabled=False, support_level=XBlockStudioConfiguration.FULL_SUPPORT,
advanced_by_default=True
).save()
default_advanced_xblock_names = [block.name for block in default_advanced_xblocks()]
self.assertCountEqual(["split_module"], default_advanced_xblock_names) # noqa: PT009

def test_default_advanced_blocks_ignores_studio_configuration_flag(self):
"""
Tests that default_advanced_xblocks is independent of XBlockStudioConfigurationFlag, which governs
whether support levels are enforced rather than whether an operator has opted an xblock into all courses.
"""
XBlockStudioConfiguration(
name="done", template="", enabled=True, support_level=XBlockStudioConfiguration.FULL_SUPPORT,
advanced_by_default=True
).save()

assert not XBlockStudioConfigurationFlag.is_enabled()
assert ["done"] == [block.name for block in default_advanced_xblocks()]

XBlockStudioConfigurationFlag(enabled=True).save()
assert ["done"] == [block.name for block in default_advanced_xblocks()]

def test_authorable_blocks_empty_model(self):
"""
Tests authorable_xblocks returns an empty list if XBlockStudioConfiguration table is empty, regardless
Expand Down
Loading