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