Skip to content

Commit

Permalink
Merge pull request #304 from edx/nimisha/view-decorator
Browse files Browse the repository at this point in the history
Add "supports" functionality tagging on xBlock views.
  • Loading branch information
nasthagiri committed Aug 5, 2015
2 parents a0931ed + 211e57f commit d1ff8cf
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 13 deletions.
5 changes: 3 additions & 2 deletions xblock/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
RuntimeServicesMixin,
HandlersMixin,
XmlSerializationMixin,
IndexInfoMixin
IndexInfoMixin,
ViewsMixin,
)
from xblock.plugin import Plugin
from xblock.validation import Validation
Expand Down Expand Up @@ -80,7 +81,7 @@ def open_local_resource(cls, uri):

# -- Base Block
class XBlock(XmlSerializationMixin, HierarchyMixin, ScopedStorageMixin, RuntimeServicesMixin, HandlersMixin,
IndexInfoMixin, SharedBlockBase):
IndexInfoMixin, ViewsMixin, SharedBlockBase):
"""Base class for XBlocks.
Derive from this class to create a new kind of XBlock. There are no
Expand Down
62 changes: 56 additions & 6 deletions xblock/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,18 +121,20 @@ def __init__(self, runtime, **kwargs):
super(RuntimeServicesMixin, self).__init__(**kwargs)

@classmethod
def needs(cls, service_name):
"""A class decorator to indicate that an XBlock class needs a particular service."""
def needs(cls, *service_names):
"""A class decorator to indicate that an XBlock class needs particular services."""
def _decorator(cls_): # pylint: disable=missing-docstring
cls_._services_requested[service_name] = "need" # pylint: disable=protected-access
for service_name in service_names:
cls_._services_requested[service_name] = "need" # pylint: disable=protected-access
return cls_
return _decorator

@classmethod
def wants(cls, service_name):
"""A class decorator to indicate that an XBlock class wants a particular service."""
def wants(cls, *service_names):
"""A class decorator to indicate that an XBlock class wants particular services."""
def _decorator(cls_): # pylint: disable=missing-docstring
cls_._services_requested[service_name] = "want" # pylint: disable=protected-access
for service_name in service_names:
cls_._services_requested[service_name] = "want" # pylint: disable=protected-access
return cls_
return _decorator

Expand Down Expand Up @@ -538,3 +540,51 @@ def index_dictionary(self):
default implementation is an empty dict
"""
return {}


class ViewsMixin(object):
"""
This mixin provides decorators that can be used on xBlock view methods.
"""
@classmethod
def supports(cls, *functionalities):
"""
A view decorator to indicate that an xBlock view has support for the
given functionalities.
Arguments:
functionalities: String identifiers for the functionalities of the view.
For example: "multi_device".
"""
def _decorator(view):
"""
Internal decorator that updates the given view's list of supported
functionalities.
"""
# pylint: disable=protected-access
if not hasattr(view, "_supports"):
view._supports = set()
for functionality in functionalities:
view._supports.add(functionality)
return view
return _decorator

def has_support(self, view, functionality):
"""
Returns whether the given view has support for the given functionality.
An XBlock view declares support for a functionality with the
@XBlock.supports decorator. The decorator stores information on the view.
Note: We implement this as an instance method to allow xBlocks to
override it, if necessary.
Arguments:
view (object): The view of the xBlock.
functionality (string): A functionality of the view.
For example: "multi_device".
Returns:
True or False
"""
return hasattr(view, "_supports") and functionality in view._supports # pylint: disable=protected-access
78 changes: 77 additions & 1 deletion xblock/test/test_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from unittest import TestCase

from xblock.fields import List, Scope, Integer
from xblock.mixins import ScopedStorageMixin, HierarchyMixin, IndexInfoMixin
from xblock.mixins import ScopedStorageMixin, HierarchyMixin, IndexInfoMixin, ViewsMixin


class AttrAssertionMixin(TestCase):
Expand Down Expand Up @@ -133,3 +133,79 @@ def test_index_info(self):
with_index_info = self.IndexInfoMixinTester().index_dictionary()
self.assertFalse(with_index_info)
self.assertTrue(isinstance(with_index_info, dict))


class TestViewsMixin(TestCase):
"""
Tests for ViewsMixin
"""
def test_supports_view_decorator(self):
"""
Tests the @supports decorator for xBlock view methods
"""
class SupportsDecoratorTester(ViewsMixin):
"""
Test class for @supports decorator
"""
@ViewsMixin.supports("a_functionality")
def functionality_supported_view(self):
"""
A view that supports a functionality
"""
pass # pragma: no cover

@ViewsMixin.supports("functionality1", "functionality2")
def multi_featured_view(self):
"""
A view that supports multiple functionalities
"""
pass # pragma: no cover

def an_unsupported_view(self):
"""
A view that does not support any functionality
"""
pass # pragma: no cover

test_xblock = SupportsDecoratorTester()

for view_name, functionality, expected_result in (
("functionality_supported_view", "a_functionality", True),
("functionality_supported_view", "bogus_functionality", False),
("functionality_supported_view", None, False),

("an_unsupported_view", "a_functionality", False),

("multi_featured_view", "functionality1", True),
("multi_featured_view", "functionality2", True),
("multi_featured_view", "bogus_functionality", False),
):
self.assertEquals(
test_xblock.has_support(getattr(test_xblock, view_name), functionality),
expected_result
)

def test_has_support_override(self):
"""
Tests overriding has_support
"""
class HasSupportOverrideTester(ViewsMixin):
"""
Test class for overriding has_support
"""
def has_support(self, view, functionality):
"""
Overrides implementation of has_support
"""
return functionality == "a_functionality"

test_xblock = HasSupportOverrideTester()

for view_name, functionality, expected_result in (
("functionality_supported_view", "a_functionality", True),
("functionality_supported_view", "bogus_functionality", False),
):
self.assertEquals(
test_xblock.has_support(getattr(test_xblock, view_name, None), functionality),
expected_result
)
6 changes: 2 additions & 4 deletions xblock/test/test_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,10 +524,8 @@ def test_multiply_mixed(self):
assert_equals(4, len(post_mixed.__bases__))


@XBlock.needs("i18n")
@XBlock.wants("secret_service")
@XBlock.needs("no_such_service")
@XBlock.wants("another_not_service")
@XBlock.needs("i18n", "no_such_service")
@XBlock.wants("secret_service", "another_not_service")
class XBlockWithServices(XBlock):
"""
Test XBlock class with service declarations.
Expand Down

0 comments on commit d1ff8cf

Please sign in to comment.