Commit db22bc40 by Eugeny Kolpakov

Merge pull request #17 from edx/theming

Mixins to simplify accessing SettingsService and theming
parents a0e77eeb 77e39518
...@@ -9,6 +9,7 @@ install: ...@@ -9,6 +9,7 @@ install:
- "pip install -r $VIRTUAL_ENV/src/xblock-sdk/requirements.txt" - "pip install -r $VIRTUAL_ENV/src/xblock-sdk/requirements.txt"
- "pip install -r $VIRTUAL_ENV/src/xblock-sdk/test-requirements.txt" - "pip install -r $VIRTUAL_ENV/src/xblock-sdk/test-requirements.txt"
- "pip install -r requirements.txt" - "pip install -r requirements.txt"
- "pip install -r test_requirements.txt"
script: script:
- pep8 xblockutils --max-line-length=120 - pep8 xblockutils --max-line-length=120
- pylint xblockutils - pylint xblockutils
......
...@@ -195,3 +195,15 @@ orders of magnitude faster. ...@@ -195,3 +195,15 @@ orders of magnitude faster.
.. |Screenshot 1| image:: https://cloud.githubusercontent.com/assets/945577/6341782/7d237966-bb83-11e4-9344-faa647056999.png .. |Screenshot 1| image:: https://cloud.githubusercontent.com/assets/945577/6341782/7d237966-bb83-11e4-9344-faa647056999.png
.. |Screenshot 2| image:: https://cloud.githubusercontent.com/assets/945577/6341803/d0195ec4-bb83-11e4-82f6-8052c9f70690.png .. |Screenshot 2| image:: https://cloud.githubusercontent.com/assets/945577/6341803/d0195ec4-bb83-11e4-82f6-8052c9f70690.png
XBlockWithSettingsMixin
-----------------------
This mixin provides access to instance-wide XBlock-specific configuration settings.
See [wiki page](https://github.com/edx/xblock-utils/wiki/Settings-and-theme-support#accessing-xblock-specific-settings) for details
ThemableXBlockMixin
-------------------
This mixin provides XBlock theming capabilities built on top of XBlock-specific settings.
See [wiki page](https://github.com/edx/xblock-utils/wiki/Settings-and-theme-support#theming-support) for details
\ No newline at end of file
ddt
mock
\ No newline at end of file
import unittest
import ddt
import itertools
from mock import Mock, MagicMock, patch
from xblock.core import XBlock
from xblockutils.settings import XBlockWithSettingsMixin, ThemableXBlockMixin
@XBlock.wants('settings')
class DummyXBlockWithSettings(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
block_settings_key = 'dummy_settings_bucket'
default_theme_config = {
'package': 'xblock_utils',
'locations': ['qwe.css']
}
@XBlock.wants('settings')
class OtherXBlockWithSettings(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
block_settings_key = 'other_settings_bucket'
theme_key = 'other_xblock_theme'
default_theme_config = {
'package': 'xblock_utils',
'locations': ['qwe.css']
}
@ddt.ddt
class TestXBlockWithSettingsMixin(unittest.TestCase):
def setUp(self):
self.settings_service = Mock()
self.runtime = Mock()
self.runtime.service = Mock(return_value=self.settings_service)
@ddt.data(None, 1, "2", [3, 4], {5: '6'})
def test_no_settings_service_return_default(self, default_value):
xblock = DummyXBlockWithSettings(self.runtime, scope_ids=Mock())
self.runtime.service.return_value = None
self.assertEqual(xblock.get_xblock_settings(default=default_value), default_value)
@ddt.data(*itertools.product(
(DummyXBlockWithSettings, OtherXBlockWithSettings),
(None, 1, "2", [3, 4], {5: '6'}),
(None, 'default1')
))
@ddt.unpack
def test_invokes_get_settings_bucket_and_returns_result(self, block, settings_service_return_value, default):
xblock = block(self.runtime, scope_ids=Mock())
self.settings_service.get_settings_bucket = Mock(return_value=settings_service_return_value)
self.assertEqual(xblock.get_xblock_settings(default=default), settings_service_return_value)
self.settings_service.get_settings_bucket.assert_called_with(xblock, default=default)
@ddt.ddt
class TextThemableXBlockMixin(unittest.TestCase):
def setUp(self):
self.service_mock = Mock()
self.runtime_mock = Mock()
self.runtime_mock.service = Mock(return_value=self.service_mock)
@ddt.data(DummyXBlockWithSettings, OtherXBlockWithSettings)
def test_theme_uses_default_theme_if_settings_service_is_not_available(self, xblock_class):
xblock = xblock_class(self.runtime_mock, scope_ids=Mock())
self.runtime_mock.service = Mock(return_value=None)
self.assertEqual(xblock.get_theme(), xblock_class.default_theme_config)
@ddt.data(DummyXBlockWithSettings, OtherXBlockWithSettings)
def test_theme_uses_default_theme_if_no_theme_is_set(self, xblock_class):
xblock = xblock_class(self.runtime_mock, scope_ids=Mock())
self.service_mock.get_settings_bucket = Mock(return_value=None)
self.assertEqual(xblock.get_theme(), xblock_class.default_theme_config)
self.service_mock.get_settings_bucket.assert_called_once_with(xblock, default={})
@ddt.data(*itertools.product(
(DummyXBlockWithSettings, OtherXBlockWithSettings),
(123, object())
))
@ddt.unpack
def test_theme_raises_if_theme_object_is_not_iterable(self, xblock_class, theme_config):
xblock = xblock_class(self.runtime_mock, scope_ids=Mock())
self.service_mock.get_settings_bucket = Mock(return_value=theme_config)
with self.assertRaises(TypeError):
xblock.get_theme()
self.service_mock.get_settings_bucket.assert_called_once_with(xblock, default={})
@ddt.data(*itertools.product(
(DummyXBlockWithSettings, OtherXBlockWithSettings),
({}, {'mass': 123}, {'spin': {}}, {'parity': "1"})
))
@ddt.unpack
def test_theme_uses_default_theme_if_no_mentoring_theme_is_set_up(self, xblock_class, theme_config):
xblock = xblock_class(self.runtime_mock, scope_ids=Mock())
self.service_mock.get_settings_bucket = Mock(return_value=theme_config)
self.assertEqual(xblock.get_theme(), xblock_class.default_theme_config)
self.service_mock.get_settings_bucket.assert_called_once_with(xblock, default={})
@ddt.data(*itertools.product(
(DummyXBlockWithSettings, OtherXBlockWithSettings),
(
123,
[1, 2, 3],
{'package': 'qwerty', 'locations': ['something_else.css']}
),
))
@ddt.unpack
def test_theme_correctly_returns_configured_theme(self, xblock_class, theme_config):
xblock = xblock_class(self.runtime_mock, scope_ids=Mock())
self.service_mock.get_settings_bucket = Mock(return_value={xblock_class.theme_key: theme_config})
self.assertEqual(xblock.get_theme(), theme_config)
@ddt.data(DummyXBlockWithSettings, OtherXBlockWithSettings)
def test_theme_files_are_loaded_from_correct_package(self, xblock_class):
xblock = xblock_class(self.runtime_mock, scope_ids=Mock())
fragment = MagicMock()
package_name = 'some_package'
theme_config = {xblock_class.theme_key: {'package': package_name, 'locations': ['lms.css']}}
self.service_mock.get_settings_bucket = Mock(return_value=theme_config)
with patch("xblockutils.settings.ResourceLoader") as patched_resource_loader:
xblock.include_theme_files(fragment)
patched_resource_loader.assert_called_with(package_name)
@ddt.data(
('dummy_block', ['']),
('dummy_block', ['public/themes/lms.css']),
('other_block', ['public/themes/lms.css', 'public/themes/lms.part2.css']),
('dummy_app.dummy_block', ['typography.css', 'icons.css']),
)
@ddt.unpack
def test_theme_files_are_added_to_fragment(self, package_name, locations):
xblock = DummyXBlockWithSettings(self.runtime_mock, scope_ids=Mock())
fragment = MagicMock()
theme_config = {DummyXBlockWithSettings.theme_key: {'package': package_name, 'locations': locations}}
self.service_mock.get_settings_bucket = Mock(return_value=theme_config)
with patch("xblockutils.settings.ResourceLoader.load_unicode") as patched_load_unicode:
xblock.include_theme_files(fragment)
for location in locations:
patched_load_unicode.assert_any_call(location)
self.assertEqual(patched_load_unicode.call_count, len(locations))
@ddt.data(None, {}, {'locations': ['red.css']})
def test_invalid_default_theme_config(self, theme_config):
xblock = DummyXBlockWithSettings(self.runtime_mock, scope_ids=Mock())
xblock.default_theme_config = theme_config
self.service_mock.get_settings_bucket = Mock(return_value={})
fragment = MagicMock()
with patch("xblockutils.settings.ResourceLoader.load_unicode") as patched_load_unicode:
xblock.include_theme_files(fragment)
patched_load_unicode.assert_not_called()
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 OpenCraft
# License: AGPLv3
"""
This module contains a mixins that allows third party XBlocks to access Settings Service in edX LMS.
"""
from xblockutils.resources import ResourceLoader
class XBlockWithSettingsMixin(object):
"""
This XBlock Mixin provides access to XBlock settings service
Descendant Xblock must add @XBlock.wants('settings') declaration
Configuration:
block_settings_key: string - XBlock settings is essentially a dictionary-like object (key-value storage).
Each XBlock must provide a key to look its settings up in this storage.
Settings Service uses `block_settings_key` attribute to get the XBlock settings key
"""
block_settings_key = None
def get_xblock_settings(self, default=None):
"""
Gets XBlock-specific settigns for current XBlock
Returns default if settings service is not available.
Parameters:
default - default value to be used in two cases:
* No settings service is available
* As a `default` parameter to `SettingsService.get_settings_bucket`
"""
settings_service = self.runtime.service(self, "settings")
if settings_service:
return settings_service.get_settings_bucket(self, default=default)
else:
return default
class ThemableXBlockMixin(object):
"""
This XBlock Mixin provides configurable theme support via Settings Service.
This mixin implies XBlockWithSettingsMixin is already mixed in into Descendant XBlock
Parameters:
default_theme_config: dict - default theme configuration in case no theme configuration is obtained from
Settings Service
theme_key: string - XBlock settings key to look theme up
block_settings_key: string - (implicit)
Examples:
Looks up red.css and small.css in `my_xblock` package:
default_theme_config = {
'package': 'my_xblock',
'locations': ['red.css', 'small.css']
}
Looks up public/themes/red.css in my_other_xblock.assets
default_theme_config = {
'package': 'my_other_xblock.assets',
'locations': ['public/themes/red.css']
}
"""
default_theme_config = None
theme_key = "theme"
def get_theme(self):
"""
Gets theme settings from settings service. Falls back to default (LMS) theme
if settings service is not available, xblock theme settings are not set or does
contain mentoring theme settings.
"""
xblock_settings = self.get_xblock_settings(default={})
if xblock_settings and self.theme_key in xblock_settings:
return xblock_settings[self.theme_key]
return self.default_theme_config
def include_theme_files(self, fragment):
"""
Gets theme configuration and renders theme css into fragment
"""
theme = self.get_theme()
if not theme or 'package' not in theme:
return
theme_package, theme_files = theme.get('package', None), theme.get('locations', [])
resource_loader = ResourceLoader(theme_package)
for theme_file in theme_files:
fragment.add_css(resource_loader.load_unicode(theme_file))
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