Commit 81e5be22 by Christina Roberts Committed by GitHub

Merge pull request #12699 from edx/christina/config-models

New XBlock Configuration Models
parents 249467c9 39cf70ec
......@@ -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)
"""
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
# -*- 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)
"""
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)
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