From 2bbd7da9334eeeeb2184bb0e59bafdbd3f207b22 Mon Sep 17 00:00:00 2001 From: cahrens <christina@edx.org> Date: Thu, 2 Jun 2016 11:49:50 -0400 Subject: [PATCH] Add new xblock config models. TNL-4666 --- common/djangoapps/xblock_django/admin.py | 12 ++++++++++-- common/djangoapps/xblock_django/migrations/0003_xblock_config_models.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ common/djangoapps/xblock_django/models.py | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ common/djangoapps/xblock_django/tests/test_models.py | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 334 insertions(+), 3 deletions(-) create mode 100644 common/djangoapps/xblock_django/migrations/0003_xblock_config_models.py diff --git a/common/djangoapps/xblock_django/admin.py b/common/djangoapps/xblock_django/admin.py index 8114460..49a9129 100644 --- a/common/djangoapps/xblock_django/admin.py +++ b/common/djangoapps/xblock_django/admin.py @@ -1,9 +1,17 @@ """ -Django admin dashboard configuration. +Django admin XBlock support configuration. """ from django.contrib import admin from config_models.admin import ConfigurationModelAdmin -from xblock_django.models import XBlockDisableConfig +from xblock_django.models import XBlockDisableConfig, XBlockConfig, XBlockConfigFlag +from simple_history.admin import SimpleHistoryAdmin + + +class XBlockConfigAdmin(SimpleHistoryAdmin): + """Admin for XBlock Configuration""" + list_display = ('name', 'template', 'support_level', 'deprecated', 'changed_by', 'change_date') admin.site.register(XBlockDisableConfig, ConfigurationModelAdmin) +admin.site.register(XBlockConfigFlag, ConfigurationModelAdmin) +admin.site.register(XBlockConfig, XBlockConfigAdmin) diff --git a/common/djangoapps/xblock_django/migrations/0003_xblock_config_models.py b/common/djangoapps/xblock_django/migrations/0003_xblock_config_models.py new file mode 100644 index 0000000..f9f70a7 --- /dev/null +++ b/common/djangoapps/xblock_django/migrations/0003_xblock_config_models.py @@ -0,0 +1,63 @@ +# -*- 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='HistoricalXBlockConfig', + fields=[ + ('id', models.IntegerField(verbose_name='ID', db_index=True, auto_created=True, blank=True)), + ('change_date', models.DateTimeField(verbose_name='change date', editable=False, blank=True)), + ('name', models.CharField(max_length=255)), + ('template', models.CharField(default=b'', max_length=255, blank=True)), + ('support_level', models.CharField(default=b'ud', max_length=2, choices=[(b'fs', 'Fully Supported'), (b'ps', 'Provisionally Supported'), (b'ua', 'Unsupported (Opt-in allowed)'), (b'ud', 'Unsupported (Opt-in disallowed)'), (b'da', 'Disabled')])), + ('deprecated', models.BooleanField(default=False, help_text="Only XBlocks listed in a course's Advanced Module List can be flagged as deprecated. Note that deprecation is by XBlock name, and is not specific to template.", verbose_name='show deprecation messaging in Studio')), + ('history_id', models.AutoField(serialize=False, primary_key=True)), + ('history_date', models.DateTimeField()), + ('history_type', models.CharField(max_length=1, choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')])), + ('changed_by', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.DO_NOTHING, db_constraint=False, blank=True, to=settings.AUTH_USER_MODEL, null=True)), + ('history_user', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, null=True)), + ], + options={ + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + 'verbose_name': 'historical x block config', + }, + ), + migrations.CreateModel( + name='XBlockConfig', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('change_date', models.DateTimeField(auto_now=True, verbose_name='change date')), + ('name', models.CharField(max_length=255)), + ('template', models.CharField(default=b'', max_length=255, blank=True)), + ('support_level', models.CharField(default=b'ud', max_length=2, choices=[(b'fs', 'Fully Supported'), (b'ps', 'Provisionally Supported'), (b'ua', 'Unsupported (Opt-in allowed)'), (b'ud', 'Unsupported (Opt-in disallowed)'), (b'da', 'Disabled')])), + ('deprecated', models.BooleanField(default=False, help_text="Only XBlocks listed in a course's Advanced Module List can be flagged as deprecated. Note that deprecation is by XBlock name, and is not specific to template.", 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')), + ], + ), + migrations.CreateModel( + name='XBlockConfigFlag', + 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')), + ], + ), + migrations.AlterUniqueTogether( + name='xblockconfig', + unique_together=set([('name', 'template')]), + ), + ] diff --git a/common/djangoapps/xblock_django/models.py b/common/djangoapps/xblock_django/models.py index 35cfb52..8b77533 100644 --- a/common/djangoapps/xblock_django/models.py +++ b/common/djangoapps/xblock_django/models.py @@ -8,6 +8,10 @@ from django.conf import settings from django.db.models import TextField from config_models.models import ConfigurationModel +from django.db import models +from django.contrib.auth.models import User + +from simple_history.models import HistoricalRecords class XBlockDisableConfig(ConfigurationModel): @@ -72,3 +76,125 @@ class XBlockDisableConfig(ConfigurationModel): disabled_xblocks=config.disabled_blocks, disabled_create_block_types=config.disabled_create_block_types ) + + +class XBlockConfigFlag(ConfigurationModel): + """ + Enables site-wide configuration for xblock support state. + """ + + class Meta(object): + app_label = "xblock_django" + + def __unicode__(self): + return "[XBlockConfigFlag] enabled={}".format(self.enabled) + + +class XBlockConfig(models.Model): + """ + Configuration for a specific xblock. Currently used for support state. + """ + FULL_SUPPORT = 'fs' + PROVISIONAL_SUPPORT = 'ps' + UNSUPPORTED_OPT_IN = 'ua' + UNSUPPORTED_NO_OPT_IN = 'ud' + DISABLED = 'da' + + SUPPORT_CHOICES = ( + (FULL_SUPPORT, _('Fully Supported')), + # May lack robustness, course staff should test. + (PROVISIONAL_SUPPORT, _('Provisionally Supported')), + # Unsupported, may not meet edX standards. + (UNSUPPORTED_OPT_IN, _('Unsupported (Opt-in allowed)')), + # Used when deprecating components, never allowed to create in Studio. + (UNSUPPORTED_NO_OPT_IN, _('Unsupported (Opt-in disallowed)')), + # Will not render in the LMS + (DISABLED, _('Disabled')), + ) + + # for archiving + history = HistoricalRecords() + change_date = models.DateTimeField(auto_now=True, verbose_name=_("change date")) + changed_by = models.ForeignKey( + User, + editable=False, + null=True, + on_delete=models.PROTECT, + # Translators: this label indicates the name of the user who made this change: + verbose_name=_("changed by"), + ) + + name = models.CharField(max_length=255, null=False) + template = models.CharField(max_length=255, blank=True, default='') + support_level = models.CharField(max_length=2, choices=SUPPORT_CHOICES, default=UNSUPPORTED_NO_OPT_IN) + deprecated = models.BooleanField( + default=False, + verbose_name=_('show deprecation messaging in Studio'), + help_text=_( + "Only XBlocks listed in a course's Advanced Module List can be flagged as deprecated. " + "Note that deprecation is by XBlock name, and is not specific to template.") + ) + + class Meta(object): + app_label = "xblock_django" + unique_together = ("name", "template") + + @property + def _history_user(self): + """ Show in history the user who made the last change. """ + return self.changed_by + + @_history_user.setter + def _history_user(self, value): + """ Show in history the user who made the last change. """ + self.changed_by = value + + @property + def _history_date(self): + """ Show in history the date of the last change. """ + return self.change_date + + @_history_date.setter + def _history_date(self, value): + """ Show in history the date of the last change. """ + self.change_date = value + + @classmethod + def deprecated_xblocks(cls): + """ Return the QuerySet of deprecated XBlock types. """ + return cls.objects.filter(deprecated=True) + + @classmethod + def disabled_xblocks(cls): + """ Return the QuerySet of XBlocks that are disabled. """ + return cls.objects.filter(support_level=cls.DISABLED) + + @classmethod + def authorable_xblocks(cls, allow_unsupported=False, name=None): + """ + Return the QuerySet of XBlocks that can be created in Studio (by default, only fully supported and + provisionally supported). Note that this method looks only at `support_level` and does not take into + account `deprecated`. + Arguments: + allow_unsupported (bool): If `True`, unsupported XBlocks that are flagged as allowing opt-in + 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: Authorable XBlocks, taking into account `support_level` and `name` (if specified). + """ + blocks = cls.objects.exclude(support_level=cls.DISABLED).exclude(support_level=cls.UNSUPPORTED_NO_OPT_IN) + if not allow_unsupported: + blocks = blocks.exclude(support_level=cls.UNSUPPORTED_OPT_IN) + + if name: + blocks = blocks.filter(name=name) + + return blocks + + def __unicode__(self): + return ( + "[XBlockConfig] '{}': template='{}', support level='{}', deprecated={}" + ).format(self.name, self.template, self.support_level, self.deprecated) diff --git a/common/djangoapps/xblock_django/tests/test_models.py b/common/djangoapps/xblock_django/tests/test_models.py index 72b73f6..75b23aa 100644 --- a/common/djangoapps/xblock_django/tests/test_models.py +++ b/common/djangoapps/xblock_django/tests/test_models.py @@ -5,7 +5,7 @@ import ddt from mock import patch from django.test import TestCase -from xblock_django.models import XBlockDisableConfig +from xblock_django.models import XBlockDisableConfig, XBlockConfig @ddt.ddt @@ -56,3 +56,137 @@ class XBlockDisableConfigTestCase(TestCase): ) self.assertEqual(XBlockDisableConfig.disabled_create_block_types(), ['annotatable', 'poll', 'survey']) + + +class XBlockConfigTestCase(TestCase): + """ + Tests for XBlockConfig. + """ + + def tearDown(self): + super(XBlockConfigTestCase, self).tearDown() + XBlockConfig.objects.all().delete() + + def test_deprecated_blocks(self): + """ Tests the deprecated_xblocks method """ + + XBlockConfig.objects.create( + name="poll", + support_level=XBlockConfig.UNSUPPORTED_NO_OPT_IN, + deprecated=True + ) + + XBlockConfig.objects.create( + name="survey", + support_level=XBlockConfig.DISABLED, + deprecated=True + ) + + XBlockConfig.objects.create( + name="done", + support_level=XBlockConfig.FULL_SUPPORT + ) + + deprecated_xblock_names = [block.name for block in XBlockConfig.deprecated_xblocks()] + self.assertEqual(["poll", "survey"], deprecated_xblock_names) + + def test_disabled_blocks(self): + """ Tests the disabled_xblocks method """ + + XBlockConfig.objects.create( + name="poll", + support_level=XBlockConfig.UNSUPPORTED_NO_OPT_IN, + deprecated=True + ) + + XBlockConfig.objects.create( + name="survey", + support_level=XBlockConfig.DISABLED, + deprecated=True + ) + + XBlockConfig.objects.create( + name="annotatable", + support_level=XBlockConfig.DISABLED, + deprecated=False + ) + + XBlockConfig.objects.create( + name="done", + support_level=XBlockConfig.FULL_SUPPORT + ) + + disabled_xblock_names = [block.name for block in XBlockConfig.disabled_xblocks()] + self.assertEqual(["survey", "annotatable"], disabled_xblock_names) + + def test_authorable_blocks(self): + """ Tests the authorable_xblocks method """ + + XBlockConfig.objects.create( + name="problem", + support_level=XBlockConfig.FULL_SUPPORT + ) + + XBlockConfig.objects.create( + name="problem", + support_level=XBlockConfig.FULL_SUPPORT, + template="multiple_choice" + ) + + XBlockConfig.objects.create( + name="problem", + support_level=XBlockConfig.UNSUPPORTED_OPT_IN, + template="circuit_simulator" + ) + + XBlockConfig.objects.create( + name="html", + support_level=XBlockConfig.PROVISIONAL_SUPPORT, + template="zoom" + ) + + XBlockConfig.objects.create( + name="split_module", + support_level=XBlockConfig.UNSUPPORTED_OPT_IN, + deprecated=True + ) + + XBlockConfig.objects.create( + name="poll", + support_level=XBlockConfig.UNSUPPORTED_NO_OPT_IN, + deprecated=True + ) + + XBlockConfig.objects.create( + name="survey", + support_level=XBlockConfig.DISABLED, + ) + + authorable_xblock_names = [block.name for block in XBlockConfig.authorable_xblocks()] + self.assertEqual(["problem", "problem", "html"], authorable_xblock_names) + + authorable_xblock_names = [block.name for block in XBlockConfig.authorable_xblocks(allow_unsupported=True)] + self.assertEqual(["problem", "problem", "problem", "html", "split_module"], authorable_xblock_names) + + authorable_xblocks = XBlockConfig.authorable_xblocks(name="problem", allow_unsupported=True) + self.assertEqual(3, len(authorable_xblocks)) + + self.assertEqual("problem", authorable_xblocks[0].name) + self.assertEqual("", authorable_xblocks[0].template) + self.assertEqual(XBlockConfig.FULL_SUPPORT, authorable_xblocks[0].support_level) + + self.assertEqual("problem", authorable_xblocks[1].name) + self.assertEqual("circuit_simulator", authorable_xblocks[1].template) + self.assertEqual(XBlockConfig.UNSUPPORTED_OPT_IN, authorable_xblocks[1].support_level) + + self.assertEqual("problem", authorable_xblocks[2].name) + self.assertEqual("multiple_choice", authorable_xblocks[2].template) + self.assertEqual(XBlockConfig.FULL_SUPPORT, authorable_xblocks[2].support_level) + + authorable_xblocks = XBlockConfig.authorable_xblocks(name="html") + self.assertEqual(1, len(authorable_xblocks)) + self.assertEqual("html", authorable_xblocks[0].name) + self.assertEqual("zoom", authorable_xblocks[0].template) + + authorable_xblocks = XBlockConfig.authorable_xblocks(name="video") + self.assertEqual(0, len(authorable_xblocks)) -- libgit2 0.26.0