Commit a830073b by Eric Fischer

Add waffle flag for NewAssetsPage

parent cc150813
...@@ -2,10 +2,30 @@ ...@@ -2,10 +2,30 @@
Admin site bindings for contentstore Admin site bindings for contentstore
""" """
from config_models.admin import ConfigurationModelAdmin from config_models.admin import ConfigurationModelAdmin, KeyedConfigurationModelAdmin
from django.contrib import admin from django.contrib import admin
from contentstore.config.forms import CourseNewAssetsPageAdminForm
from contentstore.config.models import NewAssetsPageFlag, CourseNewAssetsPageFlag
from contentstore.models import PushNotificationConfig, VideoUploadConfig from contentstore.models import PushNotificationConfig, VideoUploadConfig
class CourseNewAssetsPageAdmin(KeyedConfigurationModelAdmin):
"""
Admin for enabling new asset page on a course-by-course basis.
Allows searching by course id.
"""
form = CourseNewAssetsPageAdminForm
search_fields = ['course_id']
fieldsets = (
(None, {
'fields': ('course_id', 'enabled'),
'description': 'Enter a valid course id. If it is invalid, an error message will display.'
}),
)
admin.site.register(NewAssetsPageFlag, ConfigurationModelAdmin)
admin.site.register(CourseNewAssetsPageFlag, CourseNewAssetsPageAdmin)
admin.site.register(VideoUploadConfig, ConfigurationModelAdmin) admin.site.register(VideoUploadConfig, ConfigurationModelAdmin)
admin.site.register(PushNotificationConfig, ConfigurationModelAdmin) admin.site.register(PushNotificationConfig, ConfigurationModelAdmin)
"""
Defines a form for providing validation.
"""
import logging
from django import forms
from contentstore.config.models import CourseNewAssetsPageFlag
from opaque_keys import InvalidKeyError
from xmodule.modulestore.django import modulestore
from opaque_keys.edx.locator import CourseLocator
log = logging.getLogger(__name__)
class CourseNewAssetsPageAdminForm(forms.ModelForm):
"""Input form for new asset page enablment, allowing us to verify user input."""
class Meta(object):
model = CourseNewAssetsPageFlag
fields = '__all__'
def clean_course_id(self):
"""Validate the course id"""
cleaned_id = self.cleaned_data["course_id"]
try:
course_key = CourseLocator.from_string(cleaned_id)
except InvalidKeyError:
msg = u'Course id invalid. Entered course id was: "{0}."'.format(cleaned_id)
raise forms.ValidationError(msg)
if not modulestore().has_course(course_key):
msg = u'Course not found. Entered course id was: "{0}". '.format(course_key.to_deprecated_string())
raise forms.ValidationError(msg)
return course_key
"""
Models for configuration of the feature flags
controlling the new assets page.
"""
from config_models.models import ConfigurationModel
from django.db.models import BooleanField
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField
class NewAssetsPageFlag(ConfigurationModel):
"""
Enables the in-development new assets page from studio-frontend.
Defaults to False platform-wide, but can be overriden via a course-specific
flag. The idea is that we can use this to do a gradual rollout, and remove
the flag entirely once generally released to everyone.
"""
# this field overrides course-specific settings to enable the feature for all courses
enabled_for_all_courses = BooleanField(default=False)
@classmethod
def feature_enabled(cls, course_id=None):
"""
Looks at the currently active configuration model to determine whether
the new assets page feature is available.
There are 2 booleans to be concerned with - enabled_for_all_courses,
and the implicit is_enabled(). They interact in the following ways:
- is_enabled: False, enabled_for_all_courses: True or False
- no one can use the feature.
- is_enabled: True, enabled_for_all_courses: False
- check for a CourseNewAssetsPageFlag, use that value (default False)
- if no course_id provided, return False
- is_enabled: True, enabled_for_all_courses: True
- everyone can use the feature
"""
if not NewAssetsPageFlag.is_enabled():
return False
elif not NewAssetsPageFlag.current().enabled_for_all_courses:
if course_id:
effective = CourseNewAssetsPageFlag.objects.filter(course_id=course_id).order_by('-change_date').first()
return effective.enabled if effective is not None else False
else:
return False
else:
return True
class Meta(object):
app_label = "contentstore"
def __unicode__(self):
current_model = NewAssetsPageFlag.current()
return u"NewAssetsPageFlag: enabled {}".format(
current_model.is_enabled()
)
class CourseNewAssetsPageFlag(ConfigurationModel):
"""
Enables new assets page for a specific
course. Only has an effect if the general
flag above is set to True.
"""
KEY_FIELDS = ('course_id',)
class Meta(object):
app_label = "contentstore"
# The course that these features are attached to.
course_id = CourseKeyField(max_length=255, db_index=True)
def __unicode__(self):
not_en = "Not "
if self.enabled:
not_en = ""
# pylint: disable=no-member
return u"Course '{}': Persistent Grades {}Enabled".format(self.course_id.to_deprecated_string(), not_en)
"""
Tests for the models that control the
persistent grading feature.
"""
import itertools
import ddt
from django.conf import settings
from django.test import TestCase
from mock import patch
from opaque_keys.edx.locator import CourseLocator
from contentstore.config.models import NewAssetsPageFlag
from contentstore.config.tests.utils import new_assets_page_feature_flags
@ddt.ddt
class NewAssetsPageFlagTests(TestCase):
"""
Tests the behavior of the feature flags for the new assets page.
These are set via Django admin settings.
"""
def setUp(self):
super(NewAssetsPageFlagTests, self).setUp()
self.course_id_1 = CourseLocator(org="edx", course="course", run="run")
self.course_id_2 = CourseLocator(org="edx", course="course2", run="run")
@ddt.data(*itertools.product(
(True, False),
(True, False),
(True, False),
))
@ddt.unpack
def test_new_assets_page_feature_flags(self, global_flag, enabled_for_all_courses, enabled_for_course_1):
with new_assets_page_feature_flags(
global_flag=global_flag,
enabled_for_all_courses=enabled_for_all_courses,
course_id=self.course_id_1,
enabled_for_course=enabled_for_course_1
):
self.assertEqual(NewAssetsPageFlag.feature_enabled(), global_flag and enabled_for_all_courses)
self.assertEqual(
NewAssetsPageFlag.feature_enabled(self.course_id_1),
global_flag and (enabled_for_all_courses or enabled_for_course_1)
)
self.assertEqual(
NewAssetsPageFlag.feature_enabled(self.course_id_2),
global_flag and enabled_for_all_courses
)
def test_enable_disable_course_flag(self):
"""
Ensures that the flag, once enabled for a course, can also be disabled.
"""
with new_assets_page_feature_flags(
global_flag=True,
enabled_for_all_courses=False,
course_id=self.course_id_1,
enabled_for_course=True
):
self.assertTrue(NewAssetsPageFlag.feature_enabled(self.course_id_1))
with new_assets_page_feature_flags(
global_flag=True,
enabled_for_all_courses=False,
course_id=self.course_id_1,
enabled_for_course=False
):
self.assertFalse(NewAssetsPageFlag.feature_enabled(self.course_id_1))
def test_enable_disable_globally(self):
"""
Ensures that the flag, once enabled globally, can also be disabled.
"""
with new_assets_page_feature_flags(
global_flag=True,
enabled_for_all_courses=True,
):
self.assertTrue(NewAssetsPageFlag.feature_enabled())
self.assertTrue(NewAssetsPageFlag.feature_enabled(self.course_id_1))
with new_assets_page_feature_flags(
global_flag=True,
enabled_for_all_courses=False,
course_id=self.course_id_1,
enabled_for_course=True
):
self.assertFalse(NewAssetsPageFlag.feature_enabled())
self.assertTrue(NewAssetsPageFlag.feature_enabled(self.course_id_1))
with new_assets_page_feature_flags(
global_flag=False,
):
self.assertFalse(NewAssetsPageFlag.feature_enabled())
self.assertFalse(NewAssetsPageFlag.feature_enabled(self.course_id_1))
"""
Provides helper functions for tests that want
to configure flags related to persistent grading.
"""
from contextlib import contextmanager
from contentstore.config.models import NewAssetsPageFlag, CourseNewAssetsPageFlag
from request_cache.middleware import RequestCache
@contextmanager
def new_assets_page_feature_flags(
global_flag,
enabled_for_all_courses=False,
course_id=None,
enabled_for_course=False
):
"""
Most test cases will use a single call to this manager,
as they need to set the global setting and the course-specific
setting for a single course.
"""
RequestCache.clear_request_cache()
NewAssetsPageFlag.objects.create(enabled=global_flag, enabled_for_all_courses=enabled_for_all_courses)
if course_id:
CourseNewAssetsPageFlag.objects.create(course_id=course_id, enabled=enabled_for_course)
yield
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import django.db.models.deletion
import openedx.core.djangoapps.xmodule_django.models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contentstore', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='CourseNewAssetsPageFlag',
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')),
('course_id', openedx.core.djangoapps.xmodule_django.models.CourseKeyField(max_length=255, db_index=True)),
('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='NewAssetsPageFlag',
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')),
('enabled_for_all_courses', models.BooleanField(default=False)),
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
],
),
]
...@@ -18,6 +18,7 @@ from xmodule.exceptions import NotFoundError ...@@ -18,6 +18,7 @@ from xmodule.exceptions import NotFoundError
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from contentstore.config.models import NewAssetsPageFlag
from contentstore.utils import reverse_course_url from contentstore.utils import reverse_course_url
from contentstore.views.exception import AssetNotFoundException, AssetSizeTooLargeException from contentstore.views.exception import AssetNotFoundException, AssetSizeTooLargeException
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
...@@ -95,6 +96,7 @@ def _asset_index(course_key): ...@@ -95,6 +96,7 @@ def _asset_index(course_key):
course_module = modulestore().get_course(course_key) course_module = modulestore().get_course(course_key)
return render_to_response('asset_index.html', { return render_to_response('asset_index.html', {
'waffle_flag_enabled': NewAssetsPageFlag.feature_enabled(course_key),
'context_course': course_module, 'context_course': course_module,
'max_file_size_in_mbs': settings.MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB, 'max_file_size_in_mbs': settings.MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB,
'chunk_size_in_mbs': settings.UPLOAD_CHUNK_SIZE_IN_MB, 'chunk_size_in_mbs': settings.UPLOAD_CHUNK_SIZE_IN_MB,
......
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