Add new xblock config models.
TNL-4666
This commit is contained in:
@@ -3,7 +3,63 @@ Django admin dashboard configuration.
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
from config_models.admin import ConfigurationModelAdmin
|
||||
from xblock_django.models import XBlockDisableConfig
|
||||
from config_models.admin import ConfigurationModelAdmin, KeyedConfigurationModelAdmin
|
||||
from xblock_django.models import (
|
||||
XBlockDisableConfig, XBlockConfiguration, XBlockStudioConfiguration, XBlockStudioConfigurationFlag
|
||||
)
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
admin.site.register(XBlockDisableConfig, ConfigurationModelAdmin)
|
||||
|
||||
|
||||
class XBlockConfigurationAdmin(KeyedConfigurationModelAdmin):
|
||||
"""
|
||||
Admin for XBlockConfiguration.
|
||||
"""
|
||||
fieldsets = (
|
||||
('XBlock Name', {
|
||||
'fields': ('name',)
|
||||
}),
|
||||
('Enable/Disable XBlock', {
|
||||
'description': _('To disable the XBlock and prevent rendering in the LMS, leave "Enabled" deselected; '
|
||||
'for clarity, update XBlockStudioConfiguration support state accordingly.'),
|
||||
'fields': ('enabled',)
|
||||
}),
|
||||
('Deprecate XBlock', {
|
||||
'description': _("Only XBlocks listed in a course's Advanced Module List can be flagged as deprecated. "
|
||||
"Remember to update XBlockStudioConfiguration support state accordingly, as deprecated "
|
||||
"does not impact whether or not new XBlock instances can be created in Studio."),
|
||||
'fields': ('deprecated',)
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
class XBlockStudioConfigurationAdmin(KeyedConfigurationModelAdmin):
|
||||
"""
|
||||
Admin for XBlockStudioConfiguration.
|
||||
"""
|
||||
fieldsets = (
|
||||
('', {
|
||||
'fields': ('name', 'template')
|
||||
}),
|
||||
('Enable Studio Authoring', {
|
||||
'description': _(
|
||||
'XBlock/template combinations that are disabled cannot be edited in Studio, regardless of support '
|
||||
'level. Remember to also check if all instances of the XBlock are disabled in XBlockConfiguration.'
|
||||
),
|
||||
'fields': ('enabled',)
|
||||
}),
|
||||
('Support Level', {
|
||||
'description': _(
|
||||
"Enabled XBlock/template combinations with full or provisional support can always be created "
|
||||
"in Studio. Unsupported XBlock/template combinations require course author opt-in."
|
||||
),
|
||||
'fields': ('support_level',)
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
admin.site.register(XBlockConfiguration, XBlockConfigurationAdmin)
|
||||
admin.site.register(XBlockStudioConfiguration, XBlockStudioConfigurationAdmin)
|
||||
admin.site.register(XBlockStudioConfigurationFlag, ConfigurationModelAdmin)
|
||||
|
||||
53
common/djangoapps/xblock_django/api.py
Normal file
53
common/djangoapps/xblock_django/api.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
API methods related to xblock state.
|
||||
"""
|
||||
from xblock_django.models import XBlockConfiguration, XBlockStudioConfiguration, XBlockStudioConfigurationFlag
|
||||
|
||||
|
||||
def deprecated_xblocks():
|
||||
"""
|
||||
Return the QuerySet of deprecated XBlock types. Note that this method is independent of
|
||||
`XBlockStudioConfigurationFlag` and `XBlockStudioConfiguration`.
|
||||
"""
|
||||
return XBlockConfiguration.objects.current_set().filter(deprecated=True)
|
||||
|
||||
|
||||
def disabled_xblocks():
|
||||
"""
|
||||
Return the QuerySet of disabled XBlock types (which should not render in the LMS).
|
||||
Note that this method is independent of `XBlockStudioConfigurationFlag` and `XBlockStudioConfiguration`.
|
||||
"""
|
||||
return XBlockConfiguration.objects.current_set().filter(enabled=False)
|
||||
|
||||
|
||||
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`).
|
||||
|
||||
Arguments:
|
||||
allow_unsupported (bool): If `True`, enabled but unsupported XBlocks will also be returned.
|
||||
Note that unsupported XBlocks 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. Default value is `False`.
|
||||
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.
|
||||
"""
|
||||
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)
|
||||
|
||||
if name:
|
||||
blocks = blocks.filter(name=name)
|
||||
|
||||
return blocks
|
||||
@@ -0,0 +1,53 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('xblock_django', '0002_auto_20160204_0809'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='XBlockConfiguration',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
|
||||
('name', models.CharField(max_length=255, db_index=True)),
|
||||
('deprecated', models.BooleanField(default=False, verbose_name='show deprecation messaging in Studio')),
|
||||
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-change_date',),
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='XBlockStudioConfiguration',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
|
||||
('name', models.CharField(max_length=255, db_index=True)),
|
||||
('template', models.CharField(default=b'', max_length=255, blank=True)),
|
||||
('support_level', models.CharField(default=b'us', max_length=2, choices=[(b'fs', 'Fully Supported'), (b'ps', 'Provisionally Supported'), (b'us', 'Unsupported')])),
|
||||
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='XBlockStudioConfigurationFlag',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
|
||||
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -6,6 +6,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
|
||||
from django.db.models import TextField
|
||||
from django.db import models
|
||||
|
||||
from config_models.models import ConfigurationModel
|
||||
|
||||
@@ -72,3 +73,70 @@ class XBlockDisableConfig(ConfigurationModel):
|
||||
disabled_xblocks=config.disabled_blocks,
|
||||
disabled_create_block_types=config.disabled_create_block_types
|
||||
)
|
||||
|
||||
|
||||
class XBlockConfiguration(ConfigurationModel):
|
||||
"""
|
||||
XBlock configuration used by both LMS and Studio, and not specific to a particular template.
|
||||
"""
|
||||
|
||||
KEY_FIELDS = ('name',) # xblock name is unique
|
||||
|
||||
class Meta(ConfigurationModel.Meta):
|
||||
app_label = 'xblock_django'
|
||||
|
||||
# boolean field 'enabled' inherited from parent ConfigurationModel
|
||||
name = models.CharField(max_length=255, null=False, db_index=True)
|
||||
deprecated = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('show deprecation messaging in Studio')
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
return (
|
||||
"XBlockConfiguration(name={}, enabled={}, deprecated={})"
|
||||
).format(self.name, self.enabled, self.deprecated)
|
||||
|
||||
|
||||
class XBlockStudioConfigurationFlag(ConfigurationModel):
|
||||
"""
|
||||
Enables site-wide Studio configuration for XBlocks.
|
||||
"""
|
||||
|
||||
class Meta(object):
|
||||
app_label = "xblock_django"
|
||||
|
||||
# boolean field 'enabled' inherited from parent ConfigurationModel
|
||||
|
||||
def __unicode__(self):
|
||||
return "XBlockStudioConfigurationFlag(enabled={})".format(self.enabled)
|
||||
|
||||
|
||||
class XBlockStudioConfiguration(ConfigurationModel):
|
||||
"""
|
||||
Studio editing configuration for a specific XBlock/template combination.
|
||||
"""
|
||||
KEY_FIELDS = ('name', 'template') # xblock name/template combination is unique
|
||||
|
||||
FULL_SUPPORT = 'fs'
|
||||
PROVISIONAL_SUPPORT = 'ps'
|
||||
UNSUPPORTED = 'us'
|
||||
|
||||
SUPPORT_CHOICES = (
|
||||
(FULL_SUPPORT, _('Fully Supported')),
|
||||
(PROVISIONAL_SUPPORT, _('Provisionally Supported')),
|
||||
(UNSUPPORTED, _('Unsupported'))
|
||||
)
|
||||
|
||||
# boolean field 'enabled' inherited from parent 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)
|
||||
|
||||
class Meta(object):
|
||||
app_label = "xblock_django"
|
||||
|
||||
def __unicode__(self):
|
||||
return (
|
||||
"XBlockStudioConfiguration(name={}, template={}, enabled={}, support_level={})"
|
||||
).format(self.name, self.template, self.enabled, self.support_level)
|
||||
|
||||
138
common/djangoapps/xblock_django/tests/test_api.py
Normal file
138
common/djangoapps/xblock_django/tests/test_api.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
Tests related to XBlock support API.
|
||||
"""
|
||||
from xblock_django.models import XBlockConfiguration, XBlockStudioConfiguration, XBlockStudioConfigurationFlag
|
||||
from xblock_django.api import deprecated_xblocks, disabled_xblocks, authorable_xblocks
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
|
||||
|
||||
|
||||
class XBlockSupportTestCase(CacheIsolationTestCase):
|
||||
"""
|
||||
Tests for XBlock Support methods.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(XBlockSupportTestCase, self).setUp()
|
||||
|
||||
# Set up XBlockConfigurations for disabled and deprecated states
|
||||
block_config = [
|
||||
("poll", True, True),
|
||||
("survey", False, True),
|
||||
("done", True, False),
|
||||
]
|
||||
|
||||
for name, enabled, deprecated in block_config:
|
||||
XBlockConfiguration(name=name, enabled=enabled, deprecated=deprecated).save()
|
||||
|
||||
# Set up XBlockStudioConfigurations for studio support level
|
||||
studio_block_config = [
|
||||
("poll", "", False, XBlockStudioConfiguration.FULL_SUPPORT), # FULL_SUPPORT negated by enabled=False
|
||||
("survey", "", True, XBlockStudioConfiguration.UNSUPPORTED),
|
||||
("done", "", True, XBlockStudioConfiguration.FULL_SUPPORT),
|
||||
("problem", "", True, XBlockStudioConfiguration.FULL_SUPPORT),
|
||||
("problem", "multiple_choice", True, XBlockStudioConfiguration.FULL_SUPPORT),
|
||||
("problem", "circuit_schematic_builder", True, XBlockStudioConfiguration.UNSUPPORTED),
|
||||
("problem", "ora1", False, XBlockStudioConfiguration.FULL_SUPPORT),
|
||||
("html", "zoom", True, XBlockStudioConfiguration.PROVISIONAL_SUPPORT),
|
||||
("split_module", "", True, XBlockStudioConfiguration.UNSUPPORTED),
|
||||
]
|
||||
|
||||
for name, template, enabled, support_level in studio_block_config:
|
||||
XBlockStudioConfiguration(name=name, template=template, enabled=enabled, support_level=support_level).save()
|
||||
|
||||
def test_deprecated_blocks(self):
|
||||
""" Tests the deprecated_xblocks method """
|
||||
|
||||
deprecated_xblock_names = [block.name for block in deprecated_xblocks()]
|
||||
self.assertItemsEqual(["poll", "survey"], deprecated_xblock_names)
|
||||
|
||||
XBlockConfiguration(name="poll", enabled=True, deprecated=False).save()
|
||||
|
||||
deprecated_xblock_names = [block.name for block in deprecated_xblocks()]
|
||||
self.assertItemsEqual(["survey"], deprecated_xblock_names)
|
||||
|
||||
def test_disabled_blocks(self):
|
||||
""" Tests the disabled_xblocks method """
|
||||
|
||||
disabled_xblock_names = [block.name for block in disabled_xblocks()]
|
||||
self.assertItemsEqual(["survey"], disabled_xblock_names)
|
||||
|
||||
XBlockConfiguration(name="poll", enabled=False, deprecated=True).save()
|
||||
|
||||
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.
|
||||
"""
|
||||
XBlockStudioConfigurationFlag(enabled=True).save()
|
||||
XBlockStudioConfiguration.objects.all().delete()
|
||||
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.
|
||||
"""
|
||||
XBlockStudioConfigurationFlag(enabled=True).save()
|
||||
|
||||
authorable_xblock_names = [block.name for block in authorable_xblocks()]
|
||||
self.assertItemsEqual(["done", "problem", "problem", "html"], authorable_xblock_names)
|
||||
|
||||
# Note that "survey" is disabled in XBlockConfiguration, but it is still returned by
|
||||
# authorable_xblocks because it is marked as enabled and unsupported in XBlockStudioConfiguration.
|
||||
# Since XBlockConfiguration is a blacklist and relates to xblock type, while XBlockStudioConfiguration
|
||||
# is a whitelist and uses a combination of xblock type and template (and in addition has a global feature flag),
|
||||
# it is expected that Studio code will need to filter by both disabled_xblocks and authorable_xblocks.
|
||||
authorable_xblock_names = [block.name for block in authorable_xblocks(allow_unsupported=True)]
|
||||
self.assertItemsEqual(
|
||||
["survey", "done", "problem", "problem", "problem", "html", "split_module"],
|
||||
authorable_xblock_names
|
||||
)
|
||||
|
||||
def test_authorable_blocks_by_name(self):
|
||||
"""
|
||||
Tests authorable_xblocks when configuration flag is enabled and name is specified.
|
||||
"""
|
||||
def verify_xblock_fields(name, template, support_level, block):
|
||||
"""
|
||||
Verifies the returned xblock state.
|
||||
"""
|
||||
self.assertEqual(name, block.name)
|
||||
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))
|
||||
|
||||
# There is only a single html xblock.
|
||||
authorable_blocks = authorable_xblocks(name="html")
|
||||
self.assertEqual(1, len(authorable_blocks))
|
||||
verify_xblock_fields("html", "zoom", XBlockStudioConfiguration.PROVISIONAL_SUPPORT, authorable_blocks[0])
|
||||
|
||||
authorable_blocks = authorable_xblocks(name="problem", allow_unsupported=True)
|
||||
self.assertEqual(3, len(authorable_blocks))
|
||||
no_template = None
|
||||
circuit = None
|
||||
multiple_choice = None
|
||||
for block in authorable_blocks:
|
||||
if block.template == '':
|
||||
no_template = block
|
||||
elif block.template == 'circuit_schematic_builder':
|
||||
circuit = block
|
||||
elif block.template == 'multiple_choice':
|
||||
multiple_choice = block
|
||||
|
||||
verify_xblock_fields("problem", "", XBlockStudioConfiguration.FULL_SUPPORT, no_template)
|
||||
verify_xblock_fields("problem", "circuit_schematic_builder", XBlockStudioConfiguration.UNSUPPORTED, circuit)
|
||||
verify_xblock_fields("problem", "multiple_choice", XBlockStudioConfiguration.FULL_SUPPORT, multiple_choice)
|
||||
Reference in New Issue
Block a user