Commit cddd387b by Diana Huang

Merge pull request #8367 from edx/diana/merge-course-view-and-tab

Refactor and merge CourseViewType and CourseTab.
parents 6809d5e7 7461a2fd
......@@ -23,7 +23,7 @@ from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent
from xmodule.tabs import CourseTab
from openedx.core.djangoapps.course_views.course_views import CourseViewTypeManager
from openedx.core.lib.course_tabs import CourseTabPluginManager
from xmodule.modulestore import EdxJSONEncoder
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError
from opaque_keys import InvalidKeyError
......@@ -998,7 +998,7 @@ def _refresh_course_tabs(request, course_module):
Adds or removes a course tab based upon whether it is enabled.
"""
tab_panel = {
"type": tab_type.name,
"type": tab_type.type,
"name": tab_type.title,
}
has_tab = tab_panel in tabs
......@@ -1010,7 +1010,7 @@ def _refresh_course_tabs(request, course_module):
course_tabs = copy.copy(course_module.tabs)
# Additionally update any tabs that are provided by non-dynamic course views
for tab_type in CourseViewTypeManager.get_course_view_types():
for tab_type in CourseTabPluginManager.get_tab_types():
if not tab_type.is_dynamic and tab_type.is_default:
tab_enabled = tab_type.is_enabled(course_module, user=request.user)
update_tab(course_tabs, tab_type, tab_enabled)
......
......@@ -12,13 +12,13 @@ from django.http import HttpResponse
from django.shortcuts import redirect
from django.utils.translation import ugettext as _
from openedx.core.djangoapps.course_views.course_views import StaticTab
from edxmako.shortcuts import render_to_string, render_to_response
from opaque_keys.edx.keys import UsageKey
from xblock.core import XBlock
import dogstats_wrapper as dog_stats_api
from xmodule.modulestore.django import modulestore
from xmodule.x_module import DEPRECATION_VSCOMPAT_EVENT
from xmodule.tabs import StaticTab
from contentstore.utils import reverse_course_url, reverse_library_url, reverse_usage_url
from models.settings.course_grading import CourseGradingModel
......
......@@ -14,9 +14,8 @@ from django.views.decorators.http import require_http_methods
from edxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import ModuleStoreEnum
from xmodule.tabs import CourseTabList, CourseTab, InvalidTabsException
from xmodule.tabs import CourseTabList, CourseTab, InvalidTabsException, StaticTab
from opaque_keys.edx.keys import CourseKey, UsageKey
from openedx.core.djangoapps.course_views.course_views import StaticTab
from ..utils import get_lms_link_for_item
......
......@@ -4,7 +4,7 @@
<%!
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
from openedx.core.djangoapps.course_views.course_views import StaticTab
from xmodule.tabs import StaticTab
from django.template.defaultfilters import escapejs
%>
<%block name="title">${_("Pages")}</%block>
......
......@@ -21,6 +21,7 @@ from tempfile import mkdtemp
import ddt
from nose.plugins.attrib import attr
from mock import patch
from xmodule.tests import CourseComparisonTest
from xmodule.modulestore.mongo.base import ModuleStoreEnum
......@@ -31,6 +32,7 @@ from xmodule.modulestore.xml_importer import import_course_from_xml
from xmodule.modulestore.xml_exporter import export_course_to_xml
from xmodule.modulestore.split_mongo.split_draft import DraftVersioningModuleStore
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
from xmodule.modulestore.tests.utils import mock_tab_from_json
from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.partitions.tests.test_partitions import PartitionTestCase
from xmodule.x_module import XModuleMixin
......@@ -365,6 +367,7 @@ class CrossStoreXMLRoundtrip(CourseComparisonTest, PartitionTestCase):
self.export_dir = mkdtemp()
self.addCleanup(rmtree, self.export_dir, ignore_errors=True)
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
@ddt.data(*itertools.product(
MODULESTORE_SETUPS,
MODULESTORE_SETUPS,
......@@ -373,7 +376,10 @@ class CrossStoreXMLRoundtrip(CourseComparisonTest, PartitionTestCase):
COURSE_DATA_NAMES,
))
@ddt.unpack
def test_round_trip(self, source_builder, dest_builder, source_content_builder, dest_content_builder, course_data_name):
def test_round_trip(
self, source_builder, dest_builder, source_content_builder,
dest_content_builder, course_data_name, _mock_tab_from_json
):
# Construct the contentstore for storing the first import
with source_content_builder.build() as source_content:
# Construct the modulestore for storing the first import (using the previously created contentstore)
......
......@@ -11,6 +11,7 @@ import mimetypes
from unittest import skip
from uuid import uuid4
from contextlib import contextmanager
from mock import patch
# Mixed modulestore depends on django, so we'll manually configure some django settings
# before importing the module
......@@ -47,7 +48,7 @@ from xmodule.modulestore.mixed import MixedModuleStore
from xmodule.modulestore.search import path_to_location, navigation_index
from xmodule.modulestore.tests.factories import check_mongo_calls, check_exact_number_of_calls, \
mongo_uses_error_check
from xmodule.modulestore.tests.utils import create_modulestore_instance, LocationMixin
from xmodule.modulestore.tests.utils import create_modulestore_instance, LocationMixin, mock_tab_from_json
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
from xmodule.tests import DATA_DIR, CourseComparisonTest
......@@ -2057,8 +2058,9 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
self.store.clone_course(course_key, dest_course_id, self.user_id)
self.assertEqual(receiver.call_count, 1)
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_publish_signal_import_firing(self, default):
def test_course_publish_signal_import_firing(self, default, _from_json):
with MongoContentstoreBuilder().build() as contentstore:
self.store = MixedModuleStore(
contentstore=contentstore,
......
......@@ -17,6 +17,7 @@ from uuid import uuid4
from datetime import datetime
from pytz import UTC
import unittest
from mock import patch
from xblock.core import XBlock
from xblock.fields import Scope, Reference, ReferenceList, ReferenceValueDict
......@@ -41,7 +42,7 @@ from git.test.lib.asserts import assert_not_none
from xmodule.x_module import XModuleMixin
from xmodule.modulestore.mongo.base import as_draft
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
from xmodule.modulestore.tests.utils import LocationMixin
from xmodule.modulestore.tests.utils import LocationMixin, mock_tab_from_json
from xmodule.modulestore.edit_info import EditInfoMixin
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import InheritanceMixin
......@@ -129,36 +130,38 @@ class TestMongoModuleStoreBase(unittest.TestCase):
xblock_mixins=(EditInfoMixin, InheritanceMixin, LocationMixin, XModuleMixin)
)
import_course_from_xml(
draft_store,
999,
DATA_DIR,
cls.courses,
static_content_store=content_store
)
# also test a course with no importing of static content
import_course_from_xml(
draft_store,
999,
DATA_DIR,
['test_import_course'],
static_content_store=content_store,
do_import_static=False,
verbose=True
)
with patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json):
import_course_from_xml(
draft_store,
999,
DATA_DIR,
cls.courses,
static_content_store=content_store
)
# also import a course under a different course_id (especially ORG)
import_course_from_xml(
draft_store,
999,
DATA_DIR,
['test_import_course'],
static_content_store=content_store,
do_import_static=False,
verbose=True,
target_id=SlashSeparatedCourseKey('guestx', 'foo', 'bar')
)
# also test a course with no importing of static content
import_course_from_xml(
draft_store,
999,
DATA_DIR,
['test_import_course'],
static_content_store=content_store,
do_import_static=False,
verbose=True
)
# also import a course under a different course_id (especially ORG)
import_course_from_xml(
draft_store,
999,
DATA_DIR,
['test_import_course'],
static_content_store=content_store,
do_import_static=False,
verbose=True,
target_id=SlashSeparatedCourseKey('guestx', 'foo', 'bar')
)
return content_store, draft_store
......@@ -203,7 +206,8 @@ class TestMongoModuleStore(TestMongoModuleStoreBase):
)
assert_equals(store.get_modulestore_type(''), ModuleStoreEnum.Type.mongo)
def test_get_courses(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_get_courses(self, _from_json):
'''Make sure the course objects loaded properly'''
courses = self.draft_store.get_courses()
......@@ -241,7 +245,8 @@ class TestMongoModuleStore(TestMongoModuleStoreBase):
assert_false(self.draft_store.has_course(mix_cased))
assert_true(self.draft_store.has_course(mix_cased, ignore_case=True))
def test_get_org_courses(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_get_org_courses(self, _from_json):
"""
Make sure that we can query for a filtered list of courses for a given ORG
"""
......@@ -437,7 +442,8 @@ class TestMongoModuleStore(TestMongoModuleStoreBase):
{'displayname': 'hello'}
)
def test_get_courses_for_wiki(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_get_courses_for_wiki(self, _from_json):
"""
Test the get_courses_for_wiki method
"""
......@@ -552,7 +558,8 @@ class TestMongoModuleStore(TestMongoModuleStoreBase):
check_xblock_fields()
check_mongo_fields()
def test_export_course_image(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_export_course_image(self, _from_json):
"""
Test to make sure that we have a course image in the contentstore,
then export it to ensure it gets copied to both file locations.
......@@ -571,7 +578,8 @@ class TestMongoModuleStore(TestMongoModuleStoreBase):
finally:
shutil.rmtree(root_dir)
def test_export_course_image_nondefault(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_export_course_image_nondefault(self, _from_json):
"""
Make sure that if a non-default image path is specified that we
don't export it to the static default location
......
......@@ -30,6 +30,7 @@ from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore
from xmodule.modulestore.tests.test_modulestore import check_has_course_method
from xmodule.modulestore.split_mongo import BlockKey
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
from xmodule.modulestore.tests.utils import mock_tab_from_json
from xmodule.modulestore.edit_info import EditInfoMixin
......@@ -37,14 +38,6 @@ BRANCH_NAME_DRAFT = ModuleStoreEnum.BranchName.draft
BRANCH_NAME_PUBLISHED = ModuleStoreEnum.BranchName.published
def mock_tab_from_json(tab_dict):
"""
Mocks out the CourseTab.from_json to just return the tab_dict itself so that we don't have to deal
with plugin errors.
"""
return tab_dict
@attr('mongo')
class SplitModuleTest(unittest.TestCase):
'''
......@@ -567,7 +560,8 @@ class SplitModuleTest(unittest.TestCase):
class TestHasChildrenAtDepth(SplitModuleTest):
"""Test the has_children_at_depth method of XModuleMixin. """
def test_has_children_at_depth(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_has_children_at_depth(self, _from_json):
course_locator = CourseLocator(
org='testx', course='GreekHero', run="run", branch=BRANCH_NAME_DRAFT
)
......@@ -628,7 +622,8 @@ class SplitModuleCourseTests(SplitModuleTest):
self.assertEqual(course.edited_by, "testassist@edx.org")
self.assertDictEqual(course.grade_cutoffs, {"Pass": 0.45})
def test_get_org_courses(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_get_org_courses(self, _from_json):
courses = modulestore().get_courses(branch=BRANCH_NAME_DRAFT, org='guestx')
# should have gotten 1 draft courses
......@@ -730,7 +725,8 @@ class SplitModuleCourseTests(SplitModuleTest):
with self.assertRaises(ItemNotFoundError):
modulestore().get_course(CourseLocator(org='testx', course='GreekHero', run="run", branch=BRANCH_NAME_PUBLISHED))
def test_cache(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_cache(self, _from_json):
"""
Test that the mechanics of caching work.
"""
......@@ -742,7 +738,8 @@ class SplitModuleCourseTests(SplitModuleTest):
self.assertIn(BlockKey('chapter', 'chapter1'), block_map)
self.assertIn(BlockKey('problem', 'problem3_2'), block_map)
def test_course_successors(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_course_successors(self, _from_json):
"""
get_course_successors(course_locator, version_history_depth=1)
"""
......@@ -779,7 +776,8 @@ class SplitModuleItemTests(SplitModuleTest):
Item read tests including inheritance
'''
def test_has_item(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_has_item(self, _from_json):
'''
has_item(BlockUsageLocator)
'''
......@@ -843,7 +841,8 @@ class SplitModuleItemTests(SplitModuleTest):
)
self.assertFalse(modulestore().has_item(locator))
def test_get_item(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_get_item(self, _from_json):
'''
get_item(blocklocator)
'''
......@@ -1001,7 +1000,8 @@ class SplitModuleItemTests(SplitModuleTest):
parent = modulestore().get_parent_location(locator)
self.assertIsNone(parent)
def test_get_children(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_get_children(self, _from_json):
"""
Test the existing get_children method on xdescriptors
"""
......@@ -1354,7 +1354,8 @@ class TestItemCrud(SplitModuleTest):
other_updated = modulestore().update_item(other_block, self.user_id)
self.assertIn(moved_child.version_agnostic(), version_agnostic(other_updated.children))
def test_update_definition(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_update_definition(self, _from_json):
"""
test updating an item's definition: ensure it gets versioned as well as the course getting versioned
"""
......@@ -1625,7 +1626,8 @@ class TestCourseCreation(SplitModuleTest):
fields['grading_policy']['GRADE_CUTOFFS']
)
def test_update_course_index(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_update_course_index(self, _from_json):
"""
Test the versions pointers. NOTE: you can change the org, course, or other things, but
it's not clear how you'd find them again or associate them w/ existing student history since
......@@ -1680,7 +1682,8 @@ class TestCourseCreation(SplitModuleTest):
dupe_course_key.org, dupe_course_key.course, dupe_course_key.run, user, BRANCH_NAME_DRAFT
)
def test_bulk_ops_get_courses(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_bulk_ops_get_courses(self, _from_json):
"""
Test get_courses when some are created, updated, and deleted w/in a bulk operation
"""
......@@ -1719,7 +1722,8 @@ class TestInheritance(SplitModuleTest):
"""
Test the metadata inheritance mechanism.
"""
def test_inheritance(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_inheritance(self, _from_json):
"""
The actual test
"""
......@@ -1799,7 +1803,8 @@ class TestPublish(SplitModuleTest):
def tearDown(self):
SplitModuleTest.tearDown(self)
def test_publish_safe(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_publish_safe(self, _from_json):
"""
Test the standard patterns: publish to new branch, revise and publish
"""
......@@ -1868,7 +1873,8 @@ class TestPublish(SplitModuleTest):
with self.assertRaises(ItemNotFoundError):
modulestore().copy(self.user_id, source_course, destination_course, [problem1], [])
def test_move_delete(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_move_delete(self, _from_json):
"""
Test publishing moves and deletes.
"""
......
......@@ -54,6 +54,14 @@ def create_modulestore_instance(
)
def mock_tab_from_json(tab_dict):
"""
Mocks out the CourseTab.from_json to just return the tab_dict itself so that we don't have to deal
with plugin errors.
"""
return tab_dict
class LocationMixin(XBlockMixin):
"""
Adds a `location` property to an :class:`XBlock` so it is more compatible
......
......@@ -28,54 +28,59 @@ class CourseTab(object):
# subclass, shared by all instances of the subclass.
type = ''
# The title of the tab, which should be internationalized
title = None
# Class property that specifies whether the tab can be hidden for a particular course
is_hideable = False
# Class property that specifies whether the tab is hidden for a particular course
is_hidden = False
# The relative priority of this view that affects the ordering (lower numbers shown first)
priority = None
# Class property that specifies whether the tab can be moved within a course's list of tabs
is_movable = True
# Class property that specifies whether the tab is a collection of other tabs
is_collection = False
def __init__(self, name, tab_id, link_func):
"""
Initializes class members with values passed in by subclasses.
# True if this tab is dynamically added to the list of tabs
is_dynamic = False
Args:
name: The name of the tab
# True if this tab is a default for the course (when enabled)
is_default = True
# True if this tab can be included more than once for a course.
allow_multiple = False
tab_id: Intended to be a unique id for this tab, although it is currently not enforced
within this module. It is used by the UI to determine which page is active.
# If there is a single view associated with this tab, this is the name of it
view_name = None
link_func: A function that computes the link for the tab,
given the course and a reverse-url function as input parameters
def __init__(self, tab_dict):
"""
Initializes class members with values passed in by subclasses.
self.name = name
Args:
tab_dict (dict) - a dictionary of parameters used to build the tab.
"""
self.tab_id = tab_id
self.name = tab_dict.get('name', self.title)
self.tab_id = tab_dict.get('tab_id', getattr(self, 'tab_id', self.type))
self.link_func = tab_dict.get('link_func', link_reverse_func(self.view_name))
self.link_func = link_func
self.is_hidden = tab_dict.get('is_hidden', False)
def is_enabled(self, course, user=None): # pylint: disable=unused-argument
"""
Determines whether the tab is enabled for the given course and a particular user.
This method is to be overridden by subclasses when applicable. The base class
implementation always returns True.
@classmethod
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument
"""Returns true if this course tab is enabled in the course.
Args:
course: An xModule CourseDescriptor
user: An optional user for whom the tab will be displayed. If none,
then the code should assume a staff user or an author.
Returns:
A boolean value to indicate whether this instance of the tab is enabled.
course (CourseDescriptor): the course using the feature
user (User): an optional user interacting with the course (defaults to None)
"""
return True
raise NotImplementedError()
def get(self, key, default=None):
"""
......@@ -98,6 +103,8 @@ class CourseTab(object):
return self.type
elif key == 'tab_id':
return self.tab_id
elif key == 'is_hidden':
return self.is_hidden
else:
raise KeyError('Key {0} not present in tab {1}'.format(key, self.to_json()))
......@@ -112,6 +119,8 @@ class CourseTab(object):
self.name = value
elif key == 'tab_id':
self.tab_id = value
elif key == 'is_hidden':
self.is_hidden = value
else:
raise KeyError('Key {0} cannot be set in tab {1}'.format(key, self.to_json()))
......@@ -129,8 +138,10 @@ class CourseTab(object):
# allow tabs without names; if a name is required, its presence was checked in the validator.
name_is_eq = (other.get('name') is None or self.name == other['name'])
is_hidden_eq = self.is_hidden == other.get('is_hidden', False)
# only compare the persisted/serialized members: 'type' and 'name'
return self.type == other.get('type') and name_is_eq
return self.type == other.get('type') and name_is_eq and is_hidden_eq
def __ne__(self, other):
"""
......@@ -170,7 +181,10 @@ class CourseTab(object):
Returns:
a dictionary with keys for the properties of the CourseTab object.
"""
return {'type': self.type, 'name': self.name}
to_json_val = {'type': self.type, 'name': self.name}
if self.is_hidden:
to_json_val.update({'is_hidden': True})
return to_json_val
@staticmethod
def from_json(tab_dict):
......@@ -191,22 +205,88 @@ class CourseTab(object):
InvalidTabsException if the given tab doesn't have the right keys.
"""
# TODO: don't import openedx capabilities from common
from openedx.core.djangoapps.course_views.course_views import CourseViewTypeManager
from openedx.core.lib.course_tabs import CourseTabPluginManager
tab_type_name = tab_dict.get('type')
if tab_type_name is None:
log.error('No type included in tab_dict: %r', tab_dict)
return None
try:
tab_type = CourseViewTypeManager.get_plugin(tab_type_name)
tab_type = CourseTabPluginManager.get_plugin(tab_type_name)
except PluginError:
log.exception(
"Unknown tab type %r Known types: %r.",
tab_type_name,
CourseViewTypeManager.get_course_view_types()
CourseTabPluginManager.get_tab_types()
)
return None
tab_type.validate(tab_dict)
return tab_type.create_tab(tab_dict=tab_dict)
return tab_type(tab_dict=tab_dict)
class StaticTab(CourseTab):
"""
A custom tab.
"""
type = 'static_tab'
is_default = False # A static tab is never added to a course by default
allow_multiple = True
def __init__(self, tab_dict=None, name=None, url_slug=None):
def link_func(course, reverse_func):
""" Returns a url for a given course and reverse function. """
return reverse_func(self.type, args=[course.id.to_deprecated_string(), self.url_slug])
self.url_slug = tab_dict.get('url_slug') if tab_dict else url_slug
if tab_dict is None:
tab_dict = dict()
if name is not None:
tab_dict['name'] = name
tab_dict['link_func'] = link_func
tab_dict['tab_id'] = 'static_tab_{0}'.format(self.url_slug)
super(StaticTab, self).__init__(tab_dict)
@classmethod
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument
"""
Static tabs are viewable to everyone, even anonymous users.
"""
return True
@classmethod
def validate(cls, tab_dict, raise_error=True):
"""
Ensures that the specified tab_dict is valid.
"""
return (super(StaticTab, cls).validate(tab_dict, raise_error)
and key_checker(['name', 'url_slug'])(tab_dict, raise_error))
def __getitem__(self, key):
if key == 'url_slug':
return self.url_slug
else:
return super(StaticTab, self).__getitem__(key)
def __setitem__(self, key, value):
if key == 'url_slug':
self.url_slug = value
else:
super(StaticTab, self).__setitem__(key, value)
def to_json(self):
""" Return a dictionary representation of this tab. """
to_json_val = super(StaticTab, self).to_json()
to_json_val.update({'url_slug': self.url_slug})
return to_json_val
def __eq__(self, other):
if not super(StaticTab, self).__eq__(other):
return False
return self.url_slug == other.get('url_slug')
class CourseTabList(List):
......@@ -338,10 +418,10 @@ class CourseTabList(List):
# the following tabs should appear only once
# TODO: don't import openedx capabilities from common
from openedx.core.djangoapps.course_views.course_views import CourseViewTypeManager
for course_view_type in CourseViewTypeManager.get_course_view_types():
if not course_view_type.allow_multiple:
cls._validate_num_tabs_of_type(tabs, course_view_type.name, 1)
from openedx.core.lib.course_tabs import CourseTabPluginManager
for tab_type in CourseTabPluginManager.get_tab_types():
if not tab_type.allow_multiple:
cls._validate_num_tabs_of_type(tabs, tab_type.type, 1)
@staticmethod
def _validate_num_tabs_of_type(tabs, tab_type, max_num):
......@@ -411,6 +491,16 @@ def key_checker(expected_keys):
return check
def link_reverse_func(reverse_name):
"""
Returns a function that takes in a course and reverse_url_func,
and calls the reverse_url_func with the given reverse_name and course's ID.
This is used to generate the url for a CourseTab without having access to Django's reverse function.
"""
return lambda course, reverse_url_func: reverse_url_func(reverse_name, args=[course.id.to_deprecated_string()])
def need_name(dictionary, raise_error=True):
"""
Returns whether the 'name' key exists in the given dictionary.
......
......@@ -5,16 +5,16 @@ Registers the CCX feature for the edX platform.
from django.conf import settings
from django.utils.translation import ugettext as _
from openedx.core.djangoapps.course_views.course_views import CourseViewType
from xmodule.tabs import CourseTab
from student.roles import CourseCcxCoachRole
class CcxCourseViewType(CourseViewType):
class CcxCourseTab(CourseTab):
"""
The representation of the CCX course view type.
The representation of the CCX course tab
"""
name = "ccx_coach"
type = "ccx_coach"
title = _("CCX Coach")
view_name = "ccx_coach_dashboard"
is_dynamic = True # The CCX view is dynamically added to the set of tabs when it is enabled
......
......@@ -6,15 +6,15 @@ a user has on an article.
from django.conf import settings
from django.utils.translation import ugettext as _
from courseware.tabs import EnrolledCourseViewType
from courseware.tabs import EnrolledTab
class WikiCourseViewType(EnrolledCourseViewType):
class WikiTab(EnrolledTab):
"""
Defines the Wiki view type that is shown as a course tab.
"""
name = "wiki"
type = "wiki"
title = _('Wiki')
view_name = "course_wiki"
is_hideable = True
......@@ -28,4 +28,4 @@ class WikiCourseViewType(EnrolledCourseViewType):
return False
if course.allow_public_wiki_access:
return True
return super(WikiCourseViewType, cls).is_enabled(course, user=user)
return super(WikiTab, cls).is_enabled(course, user=user)
......@@ -11,8 +11,8 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
from courseware.courses import get_course_by_id
from courseware.tabs import (
get_course_tab_list, CoursewareViewType, CourseInfoViewType, ProgressCourseViewType,
StaticCourseViewType, ExternalDiscussionCourseViewType, ExternalLinkCourseViewType
get_course_tab_list, CoursewareTab, CourseInfoTab, ProgressTab,
ExternalDiscussionCourseTab, ExternalLinkCourseTab
)
from courseware.tests.helpers import get_request_for_user, LoginEnrollmentTestCase
from courseware.tests.factories import InstructorFactory, StaffFactory
......@@ -85,7 +85,7 @@ class TabTestCase(ModuleStoreTestCase):
Can be 'None' if the given tab class does not have any keys to validate.
"""
# create tab
tab = tab_class.create_tab(tab_dict=dict_tab)
tab = tab_class(tab_dict=dict_tab)
# name is as expected
self.assertEqual(tab.name, expected_name)
......@@ -475,17 +475,17 @@ class TabListTestCase(TabTestCase):
# invalid tabs
self.invalid_tabs = [
# less than 2 tabs
[{'type': CoursewareViewType.name}],
[{'type': CoursewareTab.type}],
# missing course_info
[{'type': CoursewareViewType.name}, {'type': 'discussion', 'name': 'fake_name'}],
[{'type': CoursewareTab.type}, {'type': 'discussion', 'name': 'fake_name'}],
# incorrect order
[{'type': CourseInfoViewType.name, 'name': 'fake_name'}, {'type': CoursewareViewType.name}],
[{'type': CourseInfoTab.type, 'name': 'fake_name'}, {'type': CoursewareTab.type}],
]
# tab types that should appear only once
unique_tab_types = [
CoursewareViewType.name,
CourseInfoViewType.name,
CoursewareTab.type,
CourseInfoTab.type,
'textbooks',
'pdf_textbooks',
'html_textbooks',
......@@ -493,8 +493,8 @@ class TabListTestCase(TabTestCase):
for unique_tab_type in unique_tab_types:
self.invalid_tabs.append([
{'type': CoursewareViewType.name},
{'type': CourseInfoViewType.name, 'name': 'fake_name'},
{'type': CoursewareTab.type},
{'type': CourseInfoTab.type, 'name': 'fake_name'},
# add the unique tab multiple times
{'type': unique_tab_type},
{'type': unique_tab_type},
......@@ -502,26 +502,27 @@ class TabListTestCase(TabTestCase):
# valid tabs
self.valid_tabs = [
# empty list
# any empty list is valid because a default list of tabs will be
# generated to replace the empty list.
[],
# all valid tabs
[
{'type': CoursewareViewType.name},
{'type': CourseInfoViewType.name, 'name': 'fake_name'},
{'type': CoursewareTab.type},
{'type': CourseInfoTab.type, 'name': 'fake_name'},
{'type': 'discussion', 'name': 'fake_name'},
{'type': ExternalLinkCourseViewType.name, 'name': 'fake_name', 'link': 'fake_link'},
{'type': ExternalLinkCourseTab.type, 'name': 'fake_name', 'link': 'fake_link'},
{'type': 'textbooks'},
{'type': 'pdf_textbooks'},
{'type': 'html_textbooks'},
{'type': ProgressCourseViewType.name, 'name': 'fake_name'},
{'type': StaticCourseViewType.name, 'name': 'fake_name', 'url_slug': 'schlug'},
{'type': ProgressTab.type, 'name': 'fake_name'},
{'type': xmodule_tabs.StaticTab.type, 'name': 'fake_name', 'url_slug': 'schlug'},
{'type': 'syllabus'},
],
# with external discussion
[
{'type': CoursewareViewType.name},
{'type': CourseInfoViewType.name, 'name': 'fake_name'},
{'type': ExternalDiscussionCourseViewType.name, 'name': 'fake_name', 'link': 'fake_link'}
{'type': CoursewareTab.type},
{'type': CourseInfoTab.type, 'name': 'fake_name'},
{'type': ExternalDiscussionCourseTab.type, 'name': 'fake_name', 'link': 'fake_link'}
],
]
......@@ -550,8 +551,8 @@ class ValidateTabsTestCase(TabListTestCase):
tab_list = xmodule_tabs.CourseTabList()
self.assertEquals(
len(tab_list.from_json([
{'type': CoursewareViewType.name},
{'type': CourseInfoViewType.name, 'name': 'fake_name'},
{'type': CoursewareTab.type},
{'type': CourseInfoTab.type, 'name': 'fake_name'},
{'type': 'no_such_type'}
])),
2
......@@ -660,10 +661,10 @@ class ProgressTestCase(TabTestCase):
def check_progress_tab(self):
"""Helper function for verifying the progress tab."""
return self.check_tab(
tab_class=ProgressCourseViewType,
dict_tab={'type': ProgressCourseViewType.name, 'name': 'same'},
tab_class=ProgressTab,
dict_tab={'type': ProgressTab.type, 'name': 'same'},
expected_link=self.reverse('progress', args=[self.course.id.to_deprecated_string()]),
expected_tab_id=ProgressCourseViewType.name,
expected_tab_id=ProgressTab.type,
invalid_dict_tab=None,
)
......@@ -692,8 +693,8 @@ class StaticTabTestCase(TabTestCase):
url_slug = 'schmug'
tab = self.check_tab(
tab_class=StaticCourseViewType,
dict_tab={'type': StaticCourseViewType.name, 'name': 'same', 'url_slug': url_slug},
tab_class=xmodule_tabs.StaticTab,
dict_tab={'type': xmodule_tabs.StaticTab.type, 'name': 'same', 'url_slug': url_slug},
expected_link=self.reverse('static_tab', args=[self.course.id.to_deprecated_string(), url_slug]),
expected_tab_id='static_tab_schmug',
invalid_dict_tab=self.fake_dict_tab,
......
......@@ -1150,9 +1150,9 @@ def notification_image_for_tab(course_tab, user, course):
"""
tab_notification_handlers = {
StaffGradingTab.name: open_ended_notifications.staff_grading_notifications,
PeerGradingTab.name: open_ended_notifications.peer_grading_notifications,
OpenEndedGradingTab.name: open_ended_notifications.combined_notifications
StaffGradingTab.type: open_ended_notifications.staff_grading_notifications,
PeerGradingTab.type: open_ended_notifications.peer_grading_notifications,
OpenEndedGradingTab.type: open_ended_notifications.combined_notifications
}
if course_tab.name in tab_notification_handlers:
......
......@@ -25,7 +25,7 @@ from openedx.core.djangoapps.course_groups.cohorts import (
get_course_cohorts,
is_commentable_cohorted
)
from courseware.tabs import EnrolledCourseViewType
from courseware.tabs import EnrolledTab
from courseware.access import has_access
from xmodule.modulestore.django import modulestore
from ccx.overrides import get_current_ccx
......@@ -49,19 +49,19 @@ PAGES_NEARBY_DELTA = 2
log = logging.getLogger("edx.discussions")
class DiscussionCourseViewType(EnrolledCourseViewType):
class DiscussionTab(EnrolledTab):
"""
A tab for the cs_comments_service forums.
"""
name = 'discussion'
type = 'discussion'
title = _('Discussion')
priority = None
view_name = 'django_comment_client.forum.views.forum_form_discussion'
@classmethod
def is_enabled(cls, course, user=None):
if not super(DiscussionCourseViewType, cls).is_enabled(course, user):
if not super(DiscussionTab, cls).is_enabled(course, user):
return False
if settings.FEATURES.get('CUSTOM_COURSES_EDX', False):
......
......@@ -4,15 +4,15 @@ Registers the "edX Notes" feature for the edX platform.
from django.utils.translation import ugettext as _
from courseware.tabs import EnrolledCourseViewType
from courseware.tabs import EnrolledTab
class EdxNotesCourseViewType(EnrolledCourseViewType):
class EdxNotesTab(EnrolledTab):
"""
The representation of the edX Notes course view type.
The representation of the edX Notes course tab type.
"""
name = "edxnotes"
type = "edxnotes"
title = _("Notes")
view_name = "edxnotes"
......@@ -25,6 +25,6 @@ class EdxNotesCourseViewType(EnrolledCourseViewType):
settings (dict): a dict of configuration settings
user (User): the user interacting with the course
"""
if not super(EdxNotesCourseViewType, cls).is_enabled(course, user=user):
if not super(EdxNotesTab, cls).is_enabled(course, user=user):
return False
return course.edxnotes
......@@ -26,6 +26,7 @@ from lms.djangoapps.lms_xblock.runtime import quote_slashes
from openedx.core.lib.xblock_utils import wrap_xblock
from xmodule.html_module import HtmlDescriptor
from xmodule.modulestore.django import modulestore
from xmodule.tabs import CourseTab
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from courseware.access import has_access
......@@ -38,7 +39,6 @@ from course_modes.models import CourseMode, CourseModesArchive
from student.roles import CourseFinanceAdminRole, CourseSalesAdminRole
from certificates.models import CertificateGenerationConfiguration
from certificates import api as certs_api
from openedx.core.djangoapps.course_views.course_views import CourseViewType
from class_dashboard.dashboard_data import get_section_display_name, get_array_section_has_problem
from .tools import get_units_with_due_date, title_or_url, bulk_email_is_enabled_for_course
......@@ -47,12 +47,12 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
log = logging.getLogger(__name__)
class InstructorDashboardViewType(CourseViewType):
class InstructorDashboardTab(CourseTab):
"""
Defines the Instructor Dashboard view type that is shown as a course tab.
"""
name = "instructor"
type = "instructor"
title = _('Instructor')
view_name = "instructor_dashboard"
is_dynamic = True # The "Instructor" tab is instead dynamically added when it is enabled
......
......@@ -9,7 +9,7 @@ from django.http import Http404
from edxmako.shortcuts import render_to_response
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from courseware.courses import get_course_with_access
from courseware.tabs import EnrolledCourseViewType
from courseware.tabs import EnrolledTab
from notes.models import Note
from notes.utils import notes_enabled_for_course
from xmodule.annotator_token import retrieve_token
......@@ -40,16 +40,16 @@ def notes(request, course_id):
return render_to_response('notes.html', context)
class NotesCourseViewType(EnrolledCourseViewType):
class NotesTab(EnrolledTab):
"""
A tab for the course notes.
"""
name = 'notes'
type = 'notes'
title = _("My Notes")
view_name = "notes"
@classmethod
def is_enabled(cls, course, user=None):
if not super(NotesCourseViewType, cls).is_enabled(course, user):
if not super(NotesTab, cls).is_enabled(course, user):
return False
return settings.FEATURES.get('ENABLE_STUDENT_NOTES') and "notes" in course.advanced_modules
......@@ -4,11 +4,10 @@ from django.views.decorators.cache import cache_control
from edxmako.shortcuts import render_to_response
from django.core.urlresolvers import reverse
from openedx.core.djangoapps.course_views.course_views import CourseViewType
from courseware.courses import get_course_with_access
from courseware.access import has_access
from courseware.tabs import EnrolledCourseViewType
from courseware.tabs import EnrolledTab
from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
import json
......@@ -66,11 +65,11 @@ ALERT_DICT = {
}
class StaffGradingTab(CourseViewType):
class StaffGradingTab(EnrolledTab):
"""
A tab for staff grading.
"""
name = 'staff_grading'
type = 'staff_grading'
title = _("Staff grading")
view_name = "staff_grading"
......@@ -81,11 +80,11 @@ class StaffGradingTab(CourseViewType):
return "combinedopenended" in course.advanced_modules
class PeerGradingTab(EnrolledCourseViewType):
class PeerGradingTab(EnrolledTab):
"""
A tab for peer grading.
"""
name = 'peer_grading'
type = 'peer_grading'
# Translators: "Peer grading" appears on a tab that allows
# students to view open-ended problems that require grading
title = _("Peer grading")
......@@ -98,11 +97,11 @@ class PeerGradingTab(EnrolledCourseViewType):
return "combinedopenended" in course.advanced_modules
class OpenEndedGradingTab(EnrolledCourseViewType):
class OpenEndedGradingTab(EnrolledTab):
"""
A tab for open ended grading.
"""
name = 'open_ended'
type = 'open_ended'
# Translators: "Open Ended Panel" appears on a tab that, when clicked, opens up a panel that
# displays information about open-ended problems that a user has submitted or needs to grade
title = _("Open Ended Panel")
......
......@@ -3,16 +3,16 @@ Definition of the course team feature.
"""
from django.utils.translation import ugettext as _
from courseware.tabs import EnrolledCourseViewType
from courseware.tabs import EnrolledTab
from .views import is_feature_enabled
class TeamsCourseViewType(EnrolledCourseViewType):
class TeamsTab(EnrolledTab):
"""
The representation of the course teams view type.
"""
name = "teams"
type = "teams"
title = _("Teams")
view_name = "teams_dashboard"
......@@ -24,7 +24,7 @@ class TeamsCourseViewType(EnrolledCourseViewType):
course (CourseDescriptor): the course using the feature
user (User): the user interacting with the course
"""
if not super(TeamsCourseViewType, cls).is_enabled(course, user=user):
if not super(TeamsTab, cls).is_enabled(course, user=user):
return False
return is_feature_enabled(course)
"""
Tabs for courseware.
"""
from openedx.core.lib.api.plugins import PluginManager
from xmodule.tabs import CourseTab
_ = lambda text: text
# Stevedore extension point namespaces
COURSE_VIEW_TYPE_NAMESPACE = 'openedx.course_view_type'
def link_reverse_func(reverse_name):
"""
Returns a function that takes in a course and reverse_url_func,
and calls the reverse_url_func with the given reverse_name and course' ID.
"""
return lambda course, reverse_url_func: reverse_url_func(reverse_name, args=[course.id.to_deprecated_string()])
class CourseViewType(object):
"""
Base class of all course view type plugins.
These are responsible for defining tabs that can be displayed in the courseware. In order to create
and register a new CourseViewType. Create a class (either in edx-platform or in a pip installable library)
that inherits from CourseViewType and create a new entry in setup.py.
For example:
entry_points={
"openedx.course_view_type": [
"new_view = my_feature.NewCourseViewType",
],
}
"""
name = None # The name of the view type, which is used for persistence and view type lookup
title = None # The title of the view, which should be internationalized
priority = None # The relative priority of this view that affects the ordering (lower numbers shown first)
view_name = None # The name of the Django view to show this view
tab_id = None # The id to be used to show a tab for this view
is_movable = True # True if this course view can be moved
is_dynamic = False # True if this course view is dynamically added to the list of tabs
is_default = True # True if this course view is a default for the course (when enabled)
is_hideable = False # True if this course view's visibility can be toggled by the author
allow_multiple = False # True if this tab can be included more than once for a course.
@classmethod
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument
"""Returns true if this course view is enabled in the course.
Args:
course (CourseDescriptor): the course using the feature
user (User): an optional user interacting with the course (defaults to None)
"""
raise NotImplementedError()
@classmethod
def validate(cls, tab_dict, raise_error=True): # pylint: disable=unused-argument
"""
Validates the given dict-type `tab_dict` object to ensure it contains the expected keys.
This method should be overridden by subclasses that require certain keys to be persisted in the tab.
"""
return True
@classmethod
def create_tab(cls, tab_dict):
"""
Returns the tab that will be shown to represent an instance of a view.
"""
return CourseViewTab(cls, tab_dict=tab_dict)
class CourseViewTypeManager(PluginManager):
"""
Manager for all of the course view types that have been made available.
All course view types should implement `CourseViewType`.
"""
NAMESPACE = COURSE_VIEW_TYPE_NAMESPACE
@classmethod
def get_course_view_types(cls):
"""
Returns the list of available course view types in their canonical order.
"""
def compare_course_view_types(first_type, second_type):
"""Compares two course view types, for use in sorting."""
first_priority = first_type.priority
second_priority = second_type.priority
if not first_priority == second_priority:
if not first_priority:
return 1
elif not second_priority:
return -1
else:
return first_priority - second_priority
first_name = first_type.name
second_name = second_type.name
if first_name < second_name:
return -1
elif first_name == second_name:
return 0
else:
return 1
course_view_types = cls.get_available_plugins().values()
course_view_types.sort(cmp=compare_course_view_types)
return course_view_types
class CourseViewTab(CourseTab):
"""
A tab that renders a course view.
"""
def __init__(self, course_view_type, tab_dict=None):
super(CourseViewTab, self).__init__(
name=tab_dict.get('name', course_view_type.title) if tab_dict else course_view_type.title,
tab_id=course_view_type.tab_id if course_view_type.tab_id else course_view_type.name,
link_func=link_reverse_func(course_view_type.view_name),
)
self.type = course_view_type.name
self.course_view_type = course_view_type
self.is_hideable = course_view_type.is_hideable
self.is_hidden = tab_dict.get('is_hidden', False) if tab_dict else False
self.is_collection = course_view_type.is_collection if hasattr(course_view_type, 'is_collection') else False
self.is_movable = course_view_type.is_movable
def is_enabled(self, course, user=None):
""" Returns True if the tab has been enabled for this course and this user, False otherwise. """
if not super(CourseViewTab, self).is_enabled(course, user=user):
return False
return self.course_view_type.is_enabled(course, user=user)
def __getitem__(self, key):
if key == 'is_hidden':
return self.is_hidden
else:
return super(CourseViewTab, self).__getitem__(key)
def __setitem__(self, key, value):
if key == 'is_hidden':
self.is_hidden = value
else:
super(CourseViewTab, self).__setitem__(key, value)
def to_json(self):
""" Return a dictionary representation of this tab. """
to_json_val = super(CourseViewTab, self).to_json()
if self.is_hidden:
to_json_val.update({'is_hidden': True})
return to_json_val
def items(self, course):
""" If this tab is a collection, this will fetch the items in the collection. """
for item in self.course_view_type.items(course):
yield item
class StaticTab(CourseTab):
"""
A custom tab.
"""
type = 'static_tab'
def __init__(self, tab_dict=None, name=None, url_slug=None):
def link_func(course, reverse_func):
""" Returns a url for a given course and reverse function. """
return reverse_func(self.type, args=[course.id.to_deprecated_string(), self.url_slug])
self.url_slug = tab_dict['url_slug'] if tab_dict else url_slug
super(StaticTab, self).__init__(
name=tab_dict['name'] if tab_dict else name,
tab_id='static_tab_{0}'.format(self.url_slug),
link_func=link_func,
)
def __getitem__(self, key):
if key == 'url_slug':
return self.url_slug
else:
return super(StaticTab, self).__getitem__(key)
def __setitem__(self, key, value):
if key == 'url_slug':
self.url_slug = value
else:
super(StaticTab, self).__setitem__(key, value)
def to_json(self):
""" Return a dictionary representation of this tab. """
to_json_val = super(StaticTab, self).to_json()
to_json_val.update({'url_slug': self.url_slug})
return to_json_val
def __eq__(self, other):
if not super(StaticTab, self).__eq__(other):
return False
return self.url_slug == other.get('url_slug')
"""
Tabs for courseware.
"""
from openedx.core.lib.api.plugins import PluginManager
_ = lambda text: text
# Stevedore extension point namespaces
COURSE_TAB_NAMESPACE = 'openedx.course_tab'
class CourseTabPluginManager(PluginManager):
"""
Manager for all of the course tabs that have been made available.
All course tabs should implement `CourseTab`.
"""
NAMESPACE = COURSE_TAB_NAMESPACE
@classmethod
def get_tab_types(cls):
"""
Returns the list of available course tabs in their canonical order.
"""
def compare_tabs(first_type, second_type):
"""Compares two course tabs, for use in sorting."""
first_priority = first_type.priority
second_priority = second_type.priority
if first_priority != second_priority:
if first_priority is None:
return 1
elif second_priority is None:
return -1
else:
return first_priority - second_priority
first_type = first_type.type
second_type = second_type.type
if first_type < second_type:
return -1
elif first_type == second_type:
return 0
else:
return 1
tab_types = cls.get_available_plugins().values()
tab_types.sort(cmp=compare_tabs)
return tab_types
......@@ -5,7 +5,7 @@ Tests for the plugin API
from django.test import TestCase
from openedx.core.lib.api.plugins import PluginError
from openedx.core.djangoapps.course_views.course_views import CourseViewTypeManager
from openedx.core.lib.course_tabs import CourseTabPluginManager
class TestPluginApi(TestCase):
......@@ -17,8 +17,8 @@ class TestPluginApi(TestCase):
"""
Verify that get_plugin works as expected.
"""
course_view_type = CourseViewTypeManager.get_plugin("instructor")
self.assertEqual(course_view_type.title, "Instructor")
tab_type = CourseTabPluginManager.get_plugin("instructor")
self.assertEqual(tab_type.title, "Instructor")
with self.assertRaises(PluginError):
CourseViewTypeManager.get_plugin("no_such_type")
CourseTabPluginManager.get_plugin("no_such_type")
......@@ -5,34 +5,34 @@ from unittest import TestCase
import xmodule.tabs as xmodule_tabs
from openedx.core.djangoapps.course_views.course_views import CourseViewTypeManager
from openedx.core.lib.course_tabs import CourseTabPluginManager
class CourseViewTypeManagerTestCase(TestCase):
"""Test cases for CourseViewTypeManager class"""
class CourseTabPluginManagerTestCase(TestCase):
"""Test cases for CourseTabPluginManager class"""
@patch('openedx.core.djangoapps.course_views.course_views.CourseViewTypeManager.get_available_plugins')
def test_get_course_view_types(self, get_available_plugins):
@patch('openedx.core.lib.course_tabs.CourseTabPluginManager.get_available_plugins')
def test_get_tab_types(self, get_available_plugins):
"""
Verify that get_course_view_types sorts appropriately
"""
def create_mock_plugin(name, priority):
def create_mock_plugin(tab_type, priority):
""" Create a mock plugin with the specified name and priority. """
mock_plugin = Mock()
mock_plugin.name = name
mock_plugin.type = tab_type
mock_plugin.priority = priority
return mock_plugin
mock_plugins = {
"Last": create_mock_plugin(name="Last", priority=None),
"Duplicate1": create_mock_plugin(name="Duplicate", priority=None),
"Duplicate2": create_mock_plugin(name="Duplicate", priority=None),
"First": create_mock_plugin(name="First", priority=1),
"Second": create_mock_plugin(name="Second", priority=1),
"Third": create_mock_plugin(name="Third", priority=3),
"Last": create_mock_plugin(tab_type="Last", priority=None),
"Duplicate1": create_mock_plugin(tab_type="Duplicate", priority=None),
"Duplicate2": create_mock_plugin(tab_type="Duplicate", priority=None),
"First": create_mock_plugin(tab_type="First", priority=1),
"Second": create_mock_plugin(tab_type="Second", priority=1),
"Third": create_mock_plugin(tab_type="Third", priority=3),
}
get_available_plugins.return_value = mock_plugins
self.assertEqual(
[plugin.name for plugin in CourseViewTypeManager.get_course_view_types()],
[plugin.type for plugin in CourseTabPluginManager.get_tab_types()],
["First", "Second", "Third", "Duplicate", "Duplicate", "Last"]
)
......
......@@ -6,7 +6,7 @@ from setuptools import setup
setup(
name="Open edX",
version="0.3",
version="0.4",
install_requires=["distribute"],
requires=[],
# NOTE: These are not the names we should be installing. This tree should
......@@ -18,24 +18,24 @@ setup(
"cms",
],
entry_points={
"openedx.course_view_type": [
"ccx = lms.djangoapps.ccx.plugins:CcxCourseViewType",
"courseware = lms.djangoapps.courseware.tabs:CoursewareViewType",
"course_info = lms.djangoapps.courseware.tabs:CourseInfoViewType",
"discussion = lms.djangoapps.django_comment_client.forum.views:DiscussionCourseViewType",
"edxnotes = lms.djangoapps.edxnotes.plugins:EdxNotesCourseViewType",
"external_discussion = lms.djangoapps.courseware.tabs:ExternalDiscussionCourseViewType",
"external_link = lms.djangoapps.courseware.tabs:ExternalLinkCourseViewType",
"html_textbooks = lms.djangoapps.courseware.tabs:HtmlTextbookCourseViews",
"instructor = lms.djangoapps.instructor.views.instructor_dashboard:InstructorDashboardViewType",
"notes = lms.djangoapps.notes.views:NotesCourseViewType",
"pdf_textbooks = lms.djangoapps.courseware.tabs:PDFTextbookCourseViews",
"progress = lms.djangoapps.courseware.tabs:ProgressCourseViewType",
"static_tab = lms.djangoapps.courseware.tabs:StaticCourseViewType",
"syllabus = lms.djangoapps.courseware.tabs:SyllabusCourseViewType",
"teams = lms.djangoapps.teams.plugins:TeamsCourseViewType",
"textbooks = lms.djangoapps.courseware.tabs:TextbookCourseViews",
"wiki = lms.djangoapps.course_wiki.tab:WikiCourseViewType",
"openedx.course_tab": [
"ccx = lms.djangoapps.ccx.plugins:CcxCourseTab",
"courseware = lms.djangoapps.courseware.tabs:CoursewareTab",
"course_info = lms.djangoapps.courseware.tabs:CourseInfoTab",
"discussion = lms.djangoapps.django_comment_client.forum.views:DiscussionTab",
"edxnotes = lms.djangoapps.edxnotes.plugins:EdxNotesTab",
"external_discussion = lms.djangoapps.courseware.tabs:ExternalDiscussionCourseTab",
"external_link = lms.djangoapps.courseware.tabs:ExternalLinkCourseTab",
"html_textbooks = lms.djangoapps.courseware.tabs:HtmlTextbookTabs",
"instructor = lms.djangoapps.instructor.views.instructor_dashboard:InstructorDashboardTab",
"notes = lms.djangoapps.notes.views:NotesTab",
"pdf_textbooks = lms.djangoapps.courseware.tabs:PDFTextbookTabs",
"progress = lms.djangoapps.courseware.tabs:ProgressTab",
"static_tab = xmodule.tabs:StaticTab",
"syllabus = lms.djangoapps.courseware.tabs:SyllabusTab",
"teams = lms.djangoapps.teams.plugins:TeamsTab",
"textbooks = lms.djangoapps.courseware.tabs:TextbookTabs",
"wiki = lms.djangoapps.course_wiki.tab:WikiTab",
# ORA 1 tabs (deprecated)
"peer_grading = lms.djangoapps.open_ended_grading.views:PeerGradingTab",
......
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