diff --git a/cms/lib/studio_tabs.py b/cms/lib/studio_tabs.py new file mode 100644 index 0000000000..d91f6fa9d8 --- /dev/null +++ b/cms/lib/studio_tabs.py @@ -0,0 +1,64 @@ +"""Studio tab plugin manager and API.""" +import abc + +from openedx.core.lib.api.plugins import PluginManager + + +class StudioTabPluginManager(PluginManager): + """Manager for all available Studio tabs. + + Examples of Studio tabs include Courses, Libraries, and Programs. All Studio + tabs should implement `StudioTab`. + """ + NAMESPACE = 'openedx.studio_tab' + + @classmethod + def get_enabled_tabs(cls): + """Returns a list of enabled Studio tabs.""" + tabs = cls.get_available_plugins() + enabled_tabs = [tab for tab in tabs.viewvalues() if tab.is_enabled()] + + return enabled_tabs + + +class StudioTab(object): + """Abstract class used to represent Studio tabs. + + Examples of Studio tabs include Courses, Libraries, and Programs. + """ + __metaclass__ = abc.ABCMeta + + @abc.abstractproperty + def tab_text(self): + """Text to display in a tab used to navigate to a list of instances of this tab. + + Should be internationalized using `ugettext_noop()` since the user won't be available in this context. + """ + pass + + @abc.abstractproperty + def button_text(self): + """Text to display in a button used to create a new instance of this tab. + + Should be internationalized using `ugettext_noop()` since the user won't be available in this context. + """ + pass + + @abc.abstractproperty + def view_name(self): + """Name of the view used to render this tab. + + Used within templates in conjuction with Django's `reverse()` to generate a URL for this tab. + """ + pass + + @abc.abstractmethod + def is_enabled(cls, user=None): # pylint: disable=no-self-argument,unused-argument + """Indicates whether this tab should be enabled. + + This is a class method; override with @classmethod. + + Keyword Arguments: + user (User): The user signed in to Studio. + """ + pass diff --git a/cms/lib/tests/__init__.py b/cms/lib/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cms/lib/tests/test_studio_tabs.py b/cms/lib/tests/test_studio_tabs.py new file mode 100644 index 0000000000..35140c5464 --- /dev/null +++ b/cms/lib/tests/test_studio_tabs.py @@ -0,0 +1,35 @@ +"""Tests for the Studio tab plugin API.""" +from django.test import TestCase +import mock + +from cms.lib.studio_tabs import StudioTabPluginManager +from openedx.core.lib.api.plugins import PluginError + + +class TestStudioTabPluginApi(TestCase): + """Unit tests for the Studio tab plugin API.""" + + @mock.patch('cms.lib.studio_tabs.StudioTabPluginManager.get_available_plugins') + def test_get_enabled_tabs(self, get_available_plugins): + """Verify that only enabled tabs are retrieved.""" + enabled_tab = self._mock_tab(is_enabled=True) + mock_tabs = { + 'disabled_tab': self._mock_tab(), + 'enabled_tab': enabled_tab, + } + + get_available_plugins.return_value = mock_tabs + + self.assertEqual(StudioTabPluginManager.get_enabled_tabs(), [enabled_tab]) + + def test_get_invalid_plugin(self): + """Verify that get_plugin fails when an invalid plugin is requested.""" + with self.assertRaises(PluginError): + StudioTabPluginManager.get_plugin('invalid_tab') + + def _mock_tab(self, is_enabled=False): + """Generate a mock tab.""" + tab = mock.Mock() + tab.is_enabled = mock.Mock(return_value=is_enabled) + + return tab