Merge pull request #8367 from edx/diana/merge-course-view-and-tab
Refactor and merge CourseViewType and CourseTab.
This commit is contained in:
@@ -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):
|
||||
# True if this tab is dynamically added to the list of tabs
|
||||
is_dynamic = False
|
||||
|
||||
# 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
|
||||
|
||||
# If there is a single view associated with this tab, this is the name of it
|
||||
view_name = None
|
||||
|
||||
def __init__(self, tab_dict):
|
||||
"""
|
||||
Initializes class members with values passed in by subclasses.
|
||||
|
||||
Args:
|
||||
name: The name of the tab
|
||||
|
||||
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.
|
||||
|
||||
link_func: A function that computes the link for the tab,
|
||||
given the course and a reverse-url function as input parameters
|
||||
tab_dict (dict) - a dictionary of parameters used to build the tab.
|
||||
"""
|
||||
|
||||
self.name = name
|
||||
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.tab_id = tab_id
|
||||
self.is_hidden = tab_dict.get('is_hidden', False)
|
||||
|
||||
self.link_func = link_func
|
||||
|
||||
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)
|
||||
|
||||
@@ -7,12 +7,12 @@ from django.utils.translation import ugettext as _
|
||||
|
||||
from courseware.access import has_access
|
||||
from courseware.entrance_exams import user_must_complete_entrance_exam
|
||||
from openedx.core.djangoapps.course_views.course_views import CourseViewTypeManager, CourseViewType, StaticTab
|
||||
from openedx.core.lib.course_tabs import CourseTabPluginManager
|
||||
from student.models import CourseEnrollment
|
||||
from xmodule.tabs import CourseTab, CourseTabList, key_checker
|
||||
|
||||
|
||||
class EnrolledCourseViewType(CourseViewType):
|
||||
class EnrolledTab(CourseTab):
|
||||
"""
|
||||
A base class for any view types that require a user to be enrolled.
|
||||
"""
|
||||
@@ -23,22 +23,22 @@ class EnrolledCourseViewType(CourseViewType):
|
||||
return CourseEnrollment.is_enrolled(user, course.id) or has_access(user, 'staff', course, course.id)
|
||||
|
||||
|
||||
class CoursewareViewType(EnrolledCourseViewType):
|
||||
class CoursewareTab(EnrolledTab):
|
||||
"""
|
||||
The main courseware view.
|
||||
"""
|
||||
name = 'courseware'
|
||||
type = 'courseware'
|
||||
title = _('Courseware')
|
||||
priority = 10
|
||||
view_name = 'courseware'
|
||||
is_movable = False
|
||||
|
||||
|
||||
class CourseInfoViewType(CourseViewType):
|
||||
class CourseInfoTab(CourseTab):
|
||||
"""
|
||||
The course info view.
|
||||
"""
|
||||
name = 'course_info'
|
||||
type = 'course_info'
|
||||
title = _('Course Info')
|
||||
priority = 20
|
||||
view_name = 'info'
|
||||
@@ -50,27 +50,28 @@ class CourseInfoViewType(CourseViewType):
|
||||
return True
|
||||
|
||||
|
||||
class SyllabusCourseViewType(EnrolledCourseViewType):
|
||||
class SyllabusTab(EnrolledTab):
|
||||
"""
|
||||
A tab for the course syllabus.
|
||||
"""
|
||||
name = 'syllabus'
|
||||
type = 'syllabus'
|
||||
title = _('Syllabus')
|
||||
priority = 30
|
||||
view_name = 'syllabus'
|
||||
allow_multiple = True
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument
|
||||
if not super(SyllabusCourseViewType, cls).is_enabled(course, user=user):
|
||||
if not super(SyllabusTab, cls).is_enabled(course, user=user):
|
||||
return False
|
||||
return getattr(course, 'syllabus_present', False)
|
||||
|
||||
|
||||
class ProgressCourseViewType(EnrolledCourseViewType):
|
||||
class ProgressTab(EnrolledTab):
|
||||
"""
|
||||
The course progress view.
|
||||
"""
|
||||
name = 'progress'
|
||||
type = 'progress'
|
||||
title = _('Progress')
|
||||
priority = 40
|
||||
view_name = 'progress'
|
||||
@@ -78,12 +79,12 @@ class ProgressCourseViewType(EnrolledCourseViewType):
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument
|
||||
if not super(ProgressCourseViewType, cls).is_enabled(course, user=user):
|
||||
if not super(ProgressTab, cls).is_enabled(course, user=user):
|
||||
return False
|
||||
return not course.hide_progress_tab
|
||||
|
||||
|
||||
class TextbookCourseViewsBase(CourseViewType):
|
||||
class TextbookTabsBase(CourseTab):
|
||||
"""
|
||||
Abstract class for textbook collection tabs classes.
|
||||
"""
|
||||
@@ -104,17 +105,17 @@ class TextbookCourseViewsBase(CourseViewType):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class TextbookCourseViews(TextbookCourseViewsBase):
|
||||
class TextbookTabs(TextbookTabsBase):
|
||||
"""
|
||||
A tab representing the collection of all textbook tabs.
|
||||
"""
|
||||
name = 'textbooks'
|
||||
type = 'textbooks'
|
||||
priority = None
|
||||
view_name = 'book'
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument
|
||||
parent_is_enabled = super(TextbookCourseViews, cls).is_enabled(course, user)
|
||||
parent_is_enabled = super(TextbookTabs, cls).is_enabled(course, user)
|
||||
return settings.FEATURES.get('ENABLE_TEXTBOOK') and parent_is_enabled
|
||||
|
||||
@classmethod
|
||||
@@ -128,11 +129,11 @@ class TextbookCourseViews(TextbookCourseViewsBase):
|
||||
)
|
||||
|
||||
|
||||
class PDFTextbookCourseViews(TextbookCourseViewsBase):
|
||||
class PDFTextbookTabs(TextbookTabsBase):
|
||||
"""
|
||||
A tab representing the collection of all PDF textbook tabs.
|
||||
"""
|
||||
name = 'pdf_textbooks'
|
||||
type = 'pdf_textbooks'
|
||||
priority = None
|
||||
view_name = 'pdf_book'
|
||||
|
||||
@@ -147,11 +148,11 @@ class PDFTextbookCourseViews(TextbookCourseViewsBase):
|
||||
)
|
||||
|
||||
|
||||
class HtmlTextbookCourseViews(TextbookCourseViewsBase):
|
||||
class HtmlTextbookTabs(TextbookTabsBase):
|
||||
"""
|
||||
A tab representing the collection of all Html textbook tabs.
|
||||
"""
|
||||
name = 'html_textbooks'
|
||||
type = 'html_textbooks'
|
||||
priority = None
|
||||
view_name = 'html_book'
|
||||
|
||||
@@ -166,89 +167,6 @@ class HtmlTextbookCourseViews(TextbookCourseViewsBase):
|
||||
)
|
||||
|
||||
|
||||
class StaticCourseViewType(CourseViewType):
|
||||
"""
|
||||
The view type that shows a static tab.
|
||||
"""
|
||||
name = 'static_tab'
|
||||
is_default = False # A static tab is never added to a course by default
|
||||
allow_multiple = True
|
||||
|
||||
@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(StaticCourseViewType, cls).validate(tab_dict, raise_error)
|
||||
and key_checker(['name', 'url_slug'])(tab_dict, raise_error))
|
||||
|
||||
@classmethod
|
||||
def create_tab(cls, tab_dict):
|
||||
"""
|
||||
Returns the tab that will be shown to represent an instance of a view.
|
||||
"""
|
||||
return StaticTab(tab_dict)
|
||||
|
||||
|
||||
class ExternalDiscussionCourseViewType(EnrolledCourseViewType):
|
||||
"""
|
||||
A course view links to an external discussion service.
|
||||
"""
|
||||
|
||||
name = 'external_discussion'
|
||||
# Translators: 'Discussion' refers to the tab in the courseware that leads to the discussion forums
|
||||
title = _('Discussion')
|
||||
priority = None
|
||||
|
||||
@classmethod
|
||||
def create_tab(cls, tab_dict):
|
||||
"""
|
||||
Returns the tab that will be shown to represent an instance of a view.
|
||||
"""
|
||||
return LinkTab(tab_dict, cls.title)
|
||||
|
||||
@classmethod
|
||||
def validate(cls, tab_dict, raise_error=True):
|
||||
""" Validate that the tab_dict for this course view has the necessary information to render. """
|
||||
return (super(ExternalDiscussionCourseViewType, cls).validate(tab_dict, raise_error) and
|
||||
key_checker(['link'])(tab_dict, raise_error))
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument
|
||||
if not super(ExternalDiscussionCourseViewType, cls).is_enabled(course, user=user):
|
||||
return False
|
||||
return course.discussion_link
|
||||
|
||||
|
||||
class ExternalLinkCourseViewType(EnrolledCourseViewType):
|
||||
"""
|
||||
A course view containing an external link.
|
||||
"""
|
||||
name = 'external_link'
|
||||
priority = None
|
||||
is_default = False # An external link tab is not added to a course by default
|
||||
|
||||
@classmethod
|
||||
def create_tab(cls, tab_dict):
|
||||
"""
|
||||
Returns the tab that will be shown to represent an instance of a view.
|
||||
"""
|
||||
return LinkTab(tab_dict)
|
||||
|
||||
@classmethod
|
||||
def validate(cls, tab_dict, raise_error=True):
|
||||
""" Validate that the tab_dict for this course view has the necessary information to render. """
|
||||
return (super(ExternalLinkCourseViewType, cls).validate(tab_dict, raise_error) and
|
||||
key_checker(['link', 'name'])(tab_dict, raise_error))
|
||||
|
||||
|
||||
class LinkTab(CourseTab):
|
||||
"""
|
||||
Abstract class for tabs that contain external links.
|
||||
@@ -264,11 +182,9 @@ class LinkTab(CourseTab):
|
||||
|
||||
self.type = tab_dict['type']
|
||||
|
||||
super(LinkTab, self).__init__(
|
||||
name=tab_dict['name'] if tab_dict else name,
|
||||
tab_id=None,
|
||||
link_func=link_value_func,
|
||||
)
|
||||
tab_dict['link_func'] = link_value_func
|
||||
|
||||
super(LinkTab, self).__init__(tab_dict)
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key == 'link':
|
||||
@@ -292,6 +208,49 @@ class LinkTab(CourseTab):
|
||||
return False
|
||||
return self.link_value == other.get('link')
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument
|
||||
return True
|
||||
|
||||
|
||||
class ExternalDiscussionCourseTab(LinkTab):
|
||||
"""
|
||||
A course tab that links to an external discussion service.
|
||||
"""
|
||||
|
||||
type = 'external_discussion'
|
||||
# Translators: 'Discussion' refers to the tab in the courseware that leads to the discussion forums
|
||||
title = _('Discussion')
|
||||
priority = None
|
||||
|
||||
@classmethod
|
||||
def validate(cls, tab_dict, raise_error=True):
|
||||
""" Validate that the tab_dict for this course tab has the necessary information to render. """
|
||||
return (super(ExternalDiscussionCourseTab, cls).validate(tab_dict, raise_error) and
|
||||
key_checker(['link'])(tab_dict, raise_error))
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument
|
||||
if not super(ExternalDiscussionCourseTab, cls).is_enabled(course, user=user):
|
||||
return False
|
||||
return course.discussion_link
|
||||
|
||||
|
||||
class ExternalLinkCourseTab(LinkTab):
|
||||
"""
|
||||
A course tab containing an external link.
|
||||
"""
|
||||
type = 'external_link'
|
||||
priority = None
|
||||
is_default = False # An external link tab is not added to a course by default
|
||||
allow_multiple = True
|
||||
|
||||
@classmethod
|
||||
def validate(cls, tab_dict, raise_error=True):
|
||||
""" Validate that the tab_dict for this course tab has the necessary information to render. """
|
||||
return (super(ExternalLinkCourseTab, cls).validate(tab_dict, raise_error) and
|
||||
key_checker(['link', 'name'])(tab_dict, raise_error))
|
||||
|
||||
|
||||
class SingleTextbookTab(CourseTab):
|
||||
"""
|
||||
@@ -308,7 +267,11 @@ class SingleTextbookTab(CourseTab):
|
||||
""" Constructs a link for textbooks from a view name, a course, and an index. """
|
||||
return reverse_func(view_name, args=[unicode(course.id), index])
|
||||
|
||||
super(SingleTextbookTab, self).__init__(name, tab_id, link_func)
|
||||
tab_dict = dict()
|
||||
tab_dict['name'] = name
|
||||
tab_dict['tab_id'] = tab_id
|
||||
tab_dict['link_func'] = link_func
|
||||
super(SingleTextbookTab, self).__init__(tab_dict)
|
||||
|
||||
def to_json(self):
|
||||
raise NotImplementedError('SingleTextbookTab should not be serialized.')
|
||||
@@ -347,9 +310,9 @@ def _get_dynamic_tabs(course, user):
|
||||
instead added dynamically based upon the user's role.
|
||||
"""
|
||||
dynamic_tabs = list()
|
||||
for tab_type in CourseViewTypeManager.get_course_view_types():
|
||||
for tab_type in CourseTabPluginManager.get_tab_types():
|
||||
if getattr(tab_type, "is_dynamic", False):
|
||||
tab = tab_type.create_tab(dict())
|
||||
tab = tab_type(dict())
|
||||
if tab.is_enabled(course, user=user):
|
||||
dynamic_tabs.append(tab)
|
||||
dynamic_tabs.sort(key=lambda dynamic_tab: dynamic_tab.name)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
"""
|
||||
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')
|
||||
47
openedx/core/lib/course_tabs.py
Normal file
47
openedx/core/lib/course_tabs.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
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"]
|
||||
)
|
||||
|
||||
38
setup.py
38
setup.py
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user