From c225d86e8359c195fa616001f91782b407f48b17 Mon Sep 17 00:00:00 2001 From: "E. Kolpakov" Date: Fri, 27 Feb 2015 15:47:26 +0300 Subject: [PATCH] SettingsService for accessing server-wide settings from XBlock --- cms/djangoapps/contentstore/views/preview.py | 2 + cms/envs/aws.py | 2 + common/lib/xmodule/xmodule/services.py | 63 +++++++++++++++ .../xmodule/xmodule/tests/test_services.py | 78 +++++++++++++++++++ lms/djangoapps/lms_xblock/runtime.py | 3 +- lms/envs/aws.py | 2 + 6 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 common/lib/xmodule/xmodule/services.py create mode 100644 common/lib/xmodule/xmodule/tests/test_services.py diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 66e4d9de18..8ef95db43a 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -15,6 +15,7 @@ from xmodule.contentstore.django import contentstore from xmodule.error_module import ErrorDescriptor from xmodule.exceptions import NotFoundError, ProcessingError from xmodule.library_tools import LibraryToolsService +from xmodule.services import SettingsService from xmodule.modulestore.django import modulestore, ModuleI18nService from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.locator import LibraryUsageLocator @@ -191,6 +192,7 @@ def _preview_module_system(request, descriptor, field_data): "i18n": ModuleI18nService(), "field-data": field_data, "library_tools": LibraryToolsService(modulestore()), + "settings": SettingsService(), "user": DjangoXBlockUserService(request.user), }, ) diff --git a/cms/envs/aws.py b/cms/envs/aws.py index 863809fc16..dc46ada0ad 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -329,3 +329,5 @@ VIDEO_CDN_URL = ENV_TOKENS.get('VIDEO_CDN_URL', {}) if FEATURES['ENABLE_COURSEWARE_INDEX']: # Use ElasticSearch for the search engine SEARCH_ENGINE = "search.elastic.ElasticSearchEngine" + +XBLOCK_SETTINGS = ENV_TOKENS.get('XBLOCK_SETTINGS', {}) diff --git a/common/lib/xmodule/xmodule/services.py b/common/lib/xmodule/xmodule/services.py new file mode 100644 index 0000000000..fc8a08851a --- /dev/null +++ b/common/lib/xmodule/xmodule/services.py @@ -0,0 +1,63 @@ +""" +Module contains various XModule/XBlock services +""" +from django.conf import settings + + +class SettingsService(object): + """ + Allows server-wide configuration of XBlocks on a per-type basis + + XBlock settings are read from XBLOCK_SETTINGS settings key. Each XBlock is allowed access + to single settings bucket. Bucket is determined by this service using the following rules: + + * Value of SettingsService.xblock_settings_bucket_selector is examined. If XBlock have attribute/property + with the name of that value this attribute/property is read to get the bucket key (e.g. if XBlock have + `block_settings_key = 'my_block_settings'`, bucket key would be 'my_block_settings'). + * Otherwise, XBlock class name is used + + Service is content-agnostic: it just returns whatever happen to be in the settings bucket (technically, it returns + the bucket itself). + + If `default` argument is specified it is returned if: + * There are no XBLOCK_SETTINGS setting + * XBLOCK_SETTINGS is empty + * XBLOCK_SETTINGS does not contain settings bucket + + If `default` is not specified or None, empty dictionary is used for default. + + Example: + + "XBLOCK_SETTINGS": { + "my_block": { + "setting1": 1, + "setting2": [] + }, + "my_other_block": [1, 2, 3], + "MyThirdBlock": "QWERTY" + } + + class MyBlock: block_settings_key='my_block' + class MyOtherBlock: block_settings_key='my_other_block' + class MyThirdBlock: pass + class MissingBlock: pass + + service = SettingsService() + service.get_settings_bucket(MyBlock()) # { "setting1": 1, "setting2": [] } + service.get_settings_bucket(MyOtherBlock()) # [1, 2, 3] + service.get_settings_bucket(MyThirdBlock()) # "QWERTY" + service.get_settings_bucket(MissingBlock()) # {} + service.get_settings_bucket(MissingBlock(), "default") # "default" + service.get_settings_bucket(MissingBlock(), None) # {} + """ + xblock_settings_bucket_selector = 'block_settings_key' + + def get_settings_bucket(self, block, default=None): + """ Gets xblock settings dictionary from settings. """ + if not block: + raise ValueError("Expected XBlock instance, got {0} of type {1}".format(block, type(block))) + + actual_default = default if default is not None else {} + xblock_settings_bucket = getattr(block, self.xblock_settings_bucket_selector, block.unmixed_class.__name__) + xblock_settings = settings.XBLOCK_SETTINGS if hasattr(settings, "XBLOCK_SETTINGS") else {} + return xblock_settings.get(xblock_settings_bucket, actual_default) diff --git a/common/lib/xmodule/xmodule/tests/test_services.py b/common/lib/xmodule/xmodule/tests/test_services.py new file mode 100644 index 0000000000..59c49fbb03 --- /dev/null +++ b/common/lib/xmodule/xmodule/tests/test_services.py @@ -0,0 +1,78 @@ +""" +Tests for SettingsService +""" + +import ddt +import mock +from unittest import TestCase + +from django.conf import settings +from django.test.utils import override_settings + +from xblock.runtime import Mixologist +from xmodule.services import SettingsService + + +class _DummyBlock(object): + """ Dummy Xblock class """ + pass + + +@ddt.ddt +class TestSettingsService(TestCase): + """ Test SettingsService """ + + xblock_setting_key1 = 'dummy_block' + xblock_setting_key2 = 'other_dummy_block' + + def setUp(self): + """ Setting up tests """ + super(TestSettingsService, self).setUp() + self.settings_service = SettingsService() + self.xblock_mock = mock.Mock() + self.xblock_mock.block_settings_key = self.xblock_setting_key1 + self.xblock_mock.unmixed_class = mock.Mock() + self.xblock_mock.unmixed_class.__name__ = self.xblock_setting_key2 + + def test_get_given_none_throws_value_error(self): + """ Test that given None throws value error """ + with self.assertRaises(ValueError): + self.settings_service.get_settings_bucket(None) + + def test_get_return_default_if_xblock_settings_is_missing(self): + """ Test that returns default (or None if default not set) if XBLOCK_SETTINGS is not set """ + self.assertFalse(hasattr(settings, 'XBLOCK_SETTINGS')) # precondition check + self.assertEqual(self.settings_service.get_settings_bucket(self.xblock_mock, 'zzz'), 'zzz') + + def test_get_return_empty_dictionary_if_xblock_settings_and_default_is_missing(self): + """ Test that returns default (or None if default not set) if XBLOCK_SETTINGS is not set """ + self.assertFalse(hasattr(settings, 'XBLOCK_SETTINGS')) # precondition check + self.assertEqual(self.settings_service.get_settings_bucket(self.xblock_mock), {}) + + @override_settings(XBLOCK_SETTINGS={xblock_setting_key2: {'b': 1}}) + def test_get_returns_none_or_default_if_bucket_not_found(self): + """ Test if settings service returns default if setting not found """ + self.assertEqual(getattr(settings, 'XBLOCK_SETTINGS'), {self.xblock_setting_key2: {'b': 1}}) + self.assertEqual(self.settings_service.get_settings_bucket(self.xblock_mock), {}) + self.assertEqual(self.settings_service.get_settings_bucket(self.xblock_mock, 123), 123) + + @override_settings(XBLOCK_SETTINGS={xblock_setting_key1: 42}) + def test_get_returns_correct_value(self): + """ Test if settings service returns correct bucket """ + self.assertEqual(getattr(settings, 'XBLOCK_SETTINGS'), {self.xblock_setting_key1: 42}) + self.assertEqual(self.settings_service.get_settings_bucket(self.xblock_mock), 42) + + @override_settings(XBLOCK_SETTINGS={xblock_setting_key2: "I'm a setting"}) + def test_get_respects_block_settings_key(self): + """ Test if settings service respects block_settings_key value """ + self.assertEqual(getattr(settings, 'XBLOCK_SETTINGS'), {self.xblock_setting_key2: "I'm a setting"}) + self.xblock_mock.block_settings_key = self.xblock_setting_key2 + self.assertEqual(self.settings_service.get_settings_bucket(self.xblock_mock), "I'm a setting") + + @override_settings(XBLOCK_SETTINGS={_DummyBlock.__name__: [1, 2, 3]}) + def test_get_uses_class_name_if_block_settings_key_is_not_set(self): + """ Test if settings service uses class name if block_settings_key attribute does not exist """ + mixologist = Mixologist([]) + block = mixologist.mix(_DummyBlock) + self.assertEqual(getattr(settings, 'XBLOCK_SETTINGS'), {"_DummyBlock": [1, 2, 3]}) + self.assertEqual(self.settings_service.get_settings_bucket(block), [1, 2, 3]) diff --git a/lms/djangoapps/lms_xblock/runtime.py b/lms/djangoapps/lms_xblock/runtime.py index af0c32f14c..72ee689dbd 100644 --- a/lms/djangoapps/lms_xblock/runtime.py +++ b/lms/djangoapps/lms_xblock/runtime.py @@ -5,12 +5,12 @@ Module implementing `xblock.runtime.Runtime` functionality for the LMS import re import xblock.reference.plugins -from django.conf import settings from django.core.urlresolvers import reverse from django.conf import settings from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig from openedx.core.djangoapps.user_api.api import course_tag as user_course_tag_api from xmodule.modulestore.django import modulestore +from xmodule.services import SettingsService from xmodule.library_tools import LibraryToolsService from xmodule.x_module import ModuleSystem from xmodule.partitions.partitions_service import PartitionService @@ -204,6 +204,7 @@ class LmsModuleSystem(LmsHandlerUrls, ModuleSystem): # pylint: disable=abstract ) services['library_tools'] = LibraryToolsService(modulestore()) services['fs'] = xblock.reference.plugins.FSService() + services['settings'] = SettingsService() self.request_token = kwargs.pop('request_token', None) super(LmsModuleSystem, self).__init__(**kwargs) diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 0b1a766f80..e98e3e25c0 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -522,3 +522,5 @@ if FEATURES.get('ENABLE_COURSEWARE_SEARCH'): FACEBOOK_API_VERSION = AUTH_TOKENS.get("FACEBOOK_API_VERSION") FACEBOOK_APP_SECRET = AUTH_TOKENS.get("FACEBOOK_APP_SECRET") FACEBOOK_APP_ID = AUTH_TOKENS.get("FACEBOOK_APP_ID") + +XBLOCK_SETTINGS = ENV_TOKENS.get('XBLOCK_SETTINGS', {})