Commit 6f9b810f by Nimisha Asthagiri

Storage-backed versioned Block Structures: Configuration

parent 2f3b0b4c
......@@ -85,17 +85,10 @@ def request_cached(f):
"""
Wrapper function to decorate with.
"""
# Build our cache key based on the module the function belongs to, the functions name, and a stringified
# list of arguments and a query string-style stringified list of keyword arguments.
converted_args = map(str, args)
converted_kwargs = map(str, reduce(list.__add__, map(list, sorted(kwargs.iteritems())), []))
cache_keys = [f.__module__, f.func_name] + converted_args + converted_kwargs
cache_key = '.'.join(cache_keys)
# Check to see if we have a result in cache. If not, invoke our wrapped
# function. Cache and return the result to the caller.
rcache = RequestCache.get_request_cache()
cache_key = func_call_cache_key(f, *args, **kwargs)
if cache_key in rcache.data:
return rcache.data.get(cache_key)
......@@ -105,4 +98,17 @@ def request_cached(f):
return result
wrapper.request_cached_contained_func = f
return wrapper
def func_call_cache_key(func, *args, **kwargs):
"""
Returns a cache key based on the function's module
the function's name, and a stringified list of arguments
and a query string-style stringified list of keyword arguments.
"""
converted_args = map(str, args)
converted_kwargs = map(str, reduce(list.__add__, map(list, sorted(kwargs.iteritems())), []))
cache_keys = [func.__module__, func.func_name] + converted_args + converted_kwargs
return '.'.join(cache_keys)
"""
Django Admin for Block Structures.
"""
from django.contrib import admin
from config_models.admin import ConfigurationModelAdmin
from .config.models import BlockStructureConfiguration
class BlockStructureAdmin(ConfigurationModelAdmin):
"""
Configuration Admin for BlockStructureConfiguration.
"""
def get_displayable_field_names(self):
"""
Excludes unused 'enabled field from super's list.
"""
displayable_field_names = super(BlockStructureAdmin, self).get_displayable_field_names()
displayable_field_names.remove('enabled')
return displayable_field_names
admin.site.register(BlockStructureConfiguration, BlockStructureAdmin)
"""
This module contains various configuration settings via
waffle switches for the Block Structure framework.
"""
from waffle import switch_is_active
INVALIDATE_CACHE_ON_PUBLISH = u'invalidate_cache_on_publish'
STORAGE_BACKING_FOR_CACHE = u'storage_backing_for_cache'
RAISE_ERROR_WHEN_NOT_FOUND = u'raise_error_when_not_found'
def is_enabled(setting_name):
"""
Returns whether the given setting is enabled.
"""
return switch_is_active(
waffle_switch_name(setting_name)
)
def waffle_switch_name(setting_name):
"""
Returns the name of the waffle switch for the
given name of the setting.
"""
return u'block_structure.{}'.format(setting_name)
"""
This module contains various configuration settings via
waffle switches for the Block Structure framework.
"""
from openedx.core.djangolib.waffle_utils import is_switch_enabled
from request_cache.middleware import request_cached
from .models import BlockStructureConfiguration
INVALIDATE_CACHE_ON_PUBLISH = u'invalidate_cache_on_publish'
STORAGE_BACKING_FOR_CACHE = u'storage_backing_for_cache'
RAISE_ERROR_WHEN_NOT_FOUND = u'raise_error_when_not_found'
PRUNE_OLD_VERSIONS = u'prune_old_versions'
def is_enabled(setting_name):
"""
Returns whether the given block_structure setting
is enabled.
"""
bs_waffle_name = _bs_waffle_switch_name(setting_name)
return is_switch_enabled(bs_waffle_name)
@request_cached
def num_versions_to_keep():
"""
Returns and caches the current setting for num_versions_to_keep.
"""
return BlockStructureConfiguration.current().num_versions_to_keep
@request_cached
def cache_timeout_in_seconds():
"""
Returns and caches the current setting for cache_timeout_in_seconds.
"""
return BlockStructureConfiguration.current().cache_timeout_in_seconds
def _bs_waffle_switch_name(setting_name):
"""
Returns the name of the waffle switch for the
given block structure setting.
"""
return u'block_structure.{}'.format(setting_name)
"""
Models for configuration of Block Structures.
"""
from django.db.models import IntegerField
from config_models.models import ConfigurationModel
class BlockStructureConfiguration(ConfigurationModel):
"""
Configuration model for Block Structures.
"""
DEFAULT_PRUNE_KEEP_COUNT = 5
DEFAULT_CACHE_TIMEOUT_IN_SECONDS = 60 * 60 * 24 # 24 hours
class Meta(object):
app_label = 'block_structure'
db_table = 'block_structure_config'
num_versions_to_keep = IntegerField(blank=True, null=True, default=DEFAULT_PRUNE_KEEP_COUNT)
cache_timeout_in_seconds = IntegerField(blank=True, null=True, default=DEFAULT_CACHE_TIMEOUT_IN_SECONDS)
def __unicode__(self):
return u"BlockStructureConfiguration: num_versions_to_keep: {}, cache_timeout_in_seconds: {}".format(
self.num_versions_to_keep,
self.cache_timeout_in_seconds,
)
# -*- 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),
]
operations = [
migrations.CreateModel(
name='BlockStructureConfiguration',
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')),
('num_versions_to_keep', models.IntegerField(default=5, null=True, blank=True)),
('cache_timeout_in_seconds', models.IntegerField(default=86400, null=True, blank=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')),
],
options={
'db_table': 'block_structure_config',
},
),
]
......@@ -3,7 +3,10 @@ Helpers for Course Blocks tests.
"""
from openedx.core.lib.block_structure.cache import BlockStructureCache
from openedx.core.djangolib.testing.waffle_utils import override_switch
from ..api import get_cache
from ..config import _bs_waffle_switch_name
def is_course_in_block_structure_cache(course_key, store):
......@@ -12,3 +15,15 @@ def is_course_in_block_structure_cache(course_key, store):
"""
course_usage_key = store.make_course_usage_key(course_key)
return BlockStructureCache(get_cache()).get(course_usage_key) is not None
class override_config_setting(override_switch): # pylint:disable=invalid-name
"""
Subclasses override_switch to use the block structure
name-spaced switch names.
"""
def __init__(self, name, active):
super(override_config_setting, self).__init__(
_bs_waffle_switch_name(name),
active
)
......@@ -3,7 +3,6 @@ Unit tests for the Course Blocks signals
"""
import ddt
from mock import patch
from waffle.testutils import override_switch
from opaque_keys.edx.locator import LibraryLocator, CourseLocator
from xmodule.modulestore.exceptions import ItemNotFoundError
......@@ -11,9 +10,9 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from ..api import get_block_structure_manager
from ..config import INVALIDATE_CACHE_ON_PUBLISH
from ..signals import _listen_for_course_publish
from ..config import INVALIDATE_CACHE_ON_PUBLISH, waffle_switch_name
from .helpers import is_course_in_block_structure_cache
from .helpers import is_course_in_block_structure_cache, override_config_setting
@ddt.ddt
......@@ -55,7 +54,7 @@ class CourseBlocksSignalTest(ModuleStoreTestCase):
def test_cache_invalidation(self, invalidate_cache_enabled, mock_bs_manager_clear):
test_display_name = "Jedi 101"
with override_switch(waffle_switch_name(INVALIDATE_CACHE_ON_PUBLISH), active=invalidate_cache_enabled):
with override_config_setting(INVALIDATE_CACHE_ON_PUBLISH, active=invalidate_cache_enabled):
self.course.display_name = test_display_name
self.store.update_item(self.course, self.user.id)
......
......@@ -5,9 +5,9 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.http.request import HttpRequest
from django.test import TestCase
from waffle.models import Switch
from ..utils import get_mock_request, toggle_switch
from ..utils import get_mock_request
USER_MODEL = get_user_model()
......@@ -28,27 +28,3 @@ class TestGetMockRequest(TestCase):
def test_mock_request_without_user(self):
request = get_mock_request()
self.assertIsInstance(request.user, AnonymousUser)
class TestToggleSwitch(TestCase):
"""
Verify that the toggle_switch utility can be used to turn Waffle Switches
on and off.
"""
def test_toggle_switch(self):
"""Verify that a new switch can be turned on and off."""
name = 'foo'
switch = toggle_switch(name)
# Verify that the switch was saved.
self.assertEqual(switch, Switch.objects.get())
# Verify that the switch has the right name and is active.
self.assertEqual(switch.name, name)
self.assertTrue(switch.active)
switch = toggle_switch(name)
# Verify that the switch has been turned off.
self.assertFalse(switch.active)
......@@ -190,25 +190,3 @@ def skip_unless_lms(func):
Only run the decorated test in the LMS test suite
"""
return skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in LMS')(func)
def toggle_switch(name, active=True):
"""
Activate or deactivate a Waffle switch. The switch is created if it does not exist.
Arguments:
name (str): Name of the switch to be toggled.
Keyword Arguments:
active (bool): Whether a newly created switch should be on or off.
Returns:
Switch
"""
switch, created = Switch.objects.get_or_create(name=name, defaults={'active': active})
if not created:
switch.active = not switch.active
switch.save()
return switch
"""
Test utilities when using waffle.
"""
from waffle.testutils import override_switch as waffle_override_switch
from request_cache.middleware import RequestCache, func_call_cache_key
from ..waffle_utils import is_switch_enabled
class override_switch(waffle_override_switch): # pylint:disable=invalid-name
"""
Subclasses waffle's override_switch in order clear the cache
used on the is_switch_enabled function.
"""
def _clear_cache(self):
"""
Clears the requestcached values on the is_switch_enabled function.
"""
cache_key = func_call_cache_key(is_switch_enabled.request_cached_contained_func, self.name)
RequestCache.get_request_cache().data.pop(cache_key, None)
def __enter__(self):
self._clear_cache()
super(override_switch, self).__enter__()
def __exit__(self, *args, **kwargs):
self._clear_cache()
super(override_switch, self).__exit__(*args, **kwargs)
"""
Utilities for waffle usage.
"""
from request_cache.middleware import request_cached
from waffle import switch_is_active
@request_cached
def is_switch_enabled(waffle_name):
"""
Returns and caches whether the given waffle switch is enabled.
See testing.waffle_utils.override_config_setting for a
helper to override and clear the cache during tests.
"""
return switch_is_active(waffle_name)
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