Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
7461a2fd
Commit
7461a2fd
authored
Jun 04, 2015
by
Diana Huang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Refactor and merge CourseViewType and CourseTab.
TNL-2321
parent
8d1651cc
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
28 changed files
with
442 additions
and
514 deletions
+442
-514
cms/djangoapps/contentstore/views/course.py
+3
-3
cms/djangoapps/contentstore/views/helpers.py
+1
-1
cms/djangoapps/contentstore/views/tabs.py
+1
-2
cms/templates/edit-tabs.html
+1
-1
common/lib/xmodule/xmodule/modulestore/tests/test_cross_modulestore_import_export.py
+7
-1
common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py
+4
-2
common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
+42
-34
common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py
+27
-21
common/lib/xmodule/xmodule/modulestore/tests/utils.py
+8
-0
common/lib/xmodule/xmodule/tabs.py
+125
-35
lms/djangoapps/ccx/plugins.py
+4
-4
lms/djangoapps/course_wiki/tab.py
+4
-4
lms/djangoapps/courseware/tabs.py
+74
-111
lms/djangoapps/courseware/tests/test_tabs.py
+27
-26
lms/djangoapps/courseware/views.py
+3
-3
lms/djangoapps/django_comment_client/forum/views.py
+4
-4
lms/djangoapps/edxnotes/plugins.py
+5
-5
lms/djangoapps/instructor/views/instructor_dashboard.py
+3
-3
lms/djangoapps/notes/views.py
+4
-4
lms/djangoapps/open_ended_grading/views.py
+7
-8
lms/djangoapps/teams/plugins.py
+4
-4
openedx/core/djangoapps/course_views/__init__.py
+0
-0
openedx/core/djangoapps/course_views/course_views.py
+0
-201
openedx/core/djangoapps/course_views/tests/__init__.py
+0
-0
openedx/core/lib/course_tabs.py
+47
-0
openedx/core/lib/tests/test_course_tab_api.py
+4
-4
openedx/core/lib/tests/test_course_tabs.py
+14
-14
setup.py
+19
-19
No files found.
cms/djangoapps/contentstore/views/course.py
View file @
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
CourseViewType
Manager
from
openedx.core.
lib.course_tabs
import
CourseTabPlugin
Manager
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
.
nam
e
,
"type"
:
tab_type
.
typ
e
,
"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
Course
ViewTypeManager
.
get_course_view
_types
():
for
tab_type
in
Course
TabPluginManager
.
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
)
...
...
cms/djangoapps/contentstore/views/helpers.py
View file @
7461a2fd
...
...
@@ -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
...
...
cms/djangoapps/contentstore/views/tabs.py
View file @
7461a2fd
...
...
@@ -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
...
...
cms/templates/edit-tabs.html
View file @
7461a2fd
...
...
@@ -4,7 +4,7 @@
<
%!
from
django
.
utils
.
translation
import
ugettext
as
_
from
django
.
core
.
urlresolvers
import
reverse
from
openedx
.
core
.
djangoapps
.
course_views
.
course_view
s
import
StaticTab
from
xmodule
.
tab
s
import
StaticTab
from
django
.
template
.
defaultfilters
import
escapejs
%
>
<
%
block
name=
"title"
>
${_("Pages")}
</
%
block>
...
...
common/lib/xmodule/xmodule/modulestore/tests/test_cross_modulestore_import_export.py
View file @
7461a2fd
...
...
@@ -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)
...
...
common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py
View file @
7461a2fd
...
...
@@ -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
,
...
...
common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
View file @
7461a2fd
...
...
@@ -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
...
...
common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py
View file @
7461a2fd
...
...
@@ -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.
"""
...
...
common/lib/xmodule/xmodule/modulestore/tests/utils.py
View file @
7461a2fd
...
...
@@ -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
...
...
common/lib/xmodule/xmodule/tabs.py
View file @
7461a2fd
...
...
@@ -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)
"""
r
eturn
True
r
aise
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
CourseViewType
Manager
from
openedx.core.
lib.course_tabs
import
CourseTabPlugin
Manager
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
=
Course
ViewType
Manager
.
get_plugin
(
tab_type_name
)
tab_type
=
Course
TabPlugin
Manager
.
get_plugin
(
tab_type_name
)
except
PluginError
:
log
.
exception
(
"Unknown tab type
%
r Known types:
%
r."
,
tab_type_name
,
Course
ViewTypeManager
.
get_course_view
_types
()
Course
TabPluginManager
.
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
CourseViewType
Manager
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
.
nam
e
,
1
)
from
openedx.core.
lib.course_tabs
import
CourseTabPlugin
Manager
for
tab_type
in
CourseTabPluginManager
.
get_tab
_types
():
if
not
tab
_type
.
allow_multiple
:
cls
.
_validate_num_tabs_of_type
(
tabs
,
tab_type
.
typ
e
,
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.
...
...
lms/djangoapps/ccx/plugins.py
View file @
7461a2fd
...
...
@@ -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
CcxCourse
ViewType
(
CourseViewType
):
class
CcxCourse
Tab
(
CourseTab
):
"""
The representation of the CCX course
view type.
The representation of the CCX course
tab
"""
nam
e
=
"ccx_coach"
typ
e
=
"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
...
...
lms/djangoapps/course_wiki/tab.py
View file @
7461a2fd
...
...
@@ -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
Enrolled
CourseViewType
from
courseware.tabs
import
Enrolled
Tab
class
Wiki
CourseViewType
(
EnrolledCourseViewType
):
class
Wiki
Tab
(
EnrolledTab
):
"""
Defines the Wiki view type that is shown as a course tab.
"""
nam
e
=
"wiki"
typ
e
=
"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
(
Wiki
CourseViewType
,
cls
)
.
is_enabled
(
course
,
user
=
user
)
return
super
(
Wiki
Tab
,
cls
)
.
is_enabled
(
course
,
user
=
user
)
lms/djangoapps/courseware/tabs.py
View file @
7461a2fd
This diff is collapsed.
Click to expand it.
lms/djangoapps/courseware/tests/test_tabs.py
View file @
7461a2fd
...
...
@@ -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
,
Courseware
ViewType
,
CourseInfoViewType
,
ProgressCourseViewType
,
StaticCourseViewType
,
ExternalDiscussionCourseViewType
,
ExternalLinkCourseViewType
get_course_tab_list
,
Courseware
Tab
,
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'
:
Courseware
ViewType
.
nam
e
}],
[{
'type'
:
Courseware
Tab
.
typ
e
}],
# missing course_info
[{
'type'
:
Courseware
ViewType
.
nam
e
},
{
'type'
:
'discussion'
,
'name'
:
'fake_name'
}],
[{
'type'
:
Courseware
Tab
.
typ
e
},
{
'type'
:
'discussion'
,
'name'
:
'fake_name'
}],
# incorrect order
[{
'type'
:
CourseInfo
ViewType
.
name
,
'name'
:
'fake_name'
},
{
'type'
:
CoursewareViewType
.
nam
e
}],
[{
'type'
:
CourseInfo
Tab
.
type
,
'name'
:
'fake_name'
},
{
'type'
:
CoursewareTab
.
typ
e
}],
]
# tab types that should appear only once
unique_tab_types
=
[
Courseware
ViewType
.
nam
e
,
CourseInfo
ViewType
.
nam
e
,
Courseware
Tab
.
typ
e
,
CourseInfo
Tab
.
typ
e
,
'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'
:
Courseware
ViewType
.
nam
e
},
{
'type'
:
CourseInfo
ViewType
.
nam
e
,
'name'
:
'fake_name'
},
{
'type'
:
Courseware
Tab
.
typ
e
},
{
'type'
:
CourseInfo
Tab
.
typ
e
,
'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'
:
Courseware
ViewType
.
nam
e
},
{
'type'
:
CourseInfo
ViewType
.
nam
e
,
'name'
:
'fake_name'
},
{
'type'
:
Courseware
Tab
.
typ
e
},
{
'type'
:
CourseInfo
Tab
.
typ
e
,
'name'
:
'fake_name'
},
{
'type'
:
'discussion'
,
'name'
:
'fake_name'
},
{
'type'
:
ExternalLinkCourse
ViewType
.
nam
e
,
'name'
:
'fake_name'
,
'link'
:
'fake_link'
},
{
'type'
:
ExternalLinkCourse
Tab
.
typ
e
,
'name'
:
'fake_name'
,
'link'
:
'fake_link'
},
{
'type'
:
'textbooks'
},
{
'type'
:
'pdf_textbooks'
},
{
'type'
:
'html_textbooks'
},
{
'type'
:
Progress
CourseViewType
.
nam
e
,
'name'
:
'fake_name'
},
{
'type'
:
StaticCourseViewType
.
nam
e
,
'name'
:
'fake_name'
,
'url_slug'
:
'schlug'
},
{
'type'
:
Progress
Tab
.
typ
e
,
'name'
:
'fake_name'
},
{
'type'
:
xmodule_tabs
.
StaticTab
.
typ
e
,
'name'
:
'fake_name'
,
'url_slug'
:
'schlug'
},
{
'type'
:
'syllabus'
},
],
# with external discussion
[
{
'type'
:
Courseware
ViewType
.
nam
e
},
{
'type'
:
CourseInfo
ViewType
.
nam
e
,
'name'
:
'fake_name'
},
{
'type'
:
ExternalDiscussionCourse
ViewType
.
nam
e
,
'name'
:
'fake_name'
,
'link'
:
'fake_link'
}
{
'type'
:
Courseware
Tab
.
typ
e
},
{
'type'
:
CourseInfo
Tab
.
typ
e
,
'name'
:
'fake_name'
},
{
'type'
:
ExternalDiscussionCourse
Tab
.
typ
e
,
'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'
:
Courseware
ViewType
.
nam
e
},
{
'type'
:
CourseInfo
ViewType
.
nam
e
,
'name'
:
'fake_name'
},
{
'type'
:
Courseware
Tab
.
typ
e
},
{
'type'
:
CourseInfo
Tab
.
typ
e
,
'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
=
Progress
CourseViewType
,
dict_tab
=
{
'type'
:
Progress
CourseViewType
.
nam
e
,
'name'
:
'same'
},
tab_class
=
Progress
Tab
,
dict_tab
=
{
'type'
:
Progress
Tab
.
typ
e
,
'name'
:
'same'
},
expected_link
=
self
.
reverse
(
'progress'
,
args
=
[
self
.
course
.
id
.
to_deprecated_string
()]),
expected_tab_id
=
Progress
CourseViewType
.
nam
e
,
expected_tab_id
=
Progress
Tab
.
typ
e
,
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
.
nam
e
,
'name'
:
'same'
,
'url_slug'
:
url_slug
},
tab_class
=
xmodule_tabs
.
StaticTab
,
dict_tab
=
{
'type'
:
xmodule_tabs
.
StaticTab
.
typ
e
,
'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
,
...
...
lms/djangoapps/courseware/views.py
View file @
7461a2fd
...
...
@@ -1135,9 +1135,9 @@ def notification_image_for_tab(course_tab, user, course):
"""
tab_notification_handlers
=
{
StaffGradingTab
.
nam
e
:
open_ended_notifications
.
staff_grading_notifications
,
PeerGradingTab
.
nam
e
:
open_ended_notifications
.
peer_grading_notifications
,
OpenEndedGradingTab
.
nam
e
:
open_ended_notifications
.
combined_notifications
StaffGradingTab
.
typ
e
:
open_ended_notifications
.
staff_grading_notifications
,
PeerGradingTab
.
typ
e
:
open_ended_notifications
.
peer_grading_notifications
,
OpenEndedGradingTab
.
typ
e
:
open_ended_notifications
.
combined_notifications
}
if
course_tab
.
name
in
tab_notification_handlers
:
...
...
lms/djangoapps/django_comment_client/forum/views.py
View file @
7461a2fd
...
...
@@ -25,7 +25,7 @@ from openedx.core.djangoapps.course_groups.cohorts import (
get_course_cohorts
,
is_commentable_cohorted
)
from
courseware.tabs
import
Enrolled
CourseViewType
from
courseware.tabs
import
Enrolled
Tab
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
Discussion
CourseViewType
(
EnrolledCourseViewType
):
class
Discussion
Tab
(
EnrolledTab
):
"""
A tab for the cs_comments_service forums.
"""
nam
e
=
'discussion'
typ
e
=
'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
(
Discussion
CourseViewType
,
cls
)
.
is_enabled
(
course
,
user
):
if
not
super
(
Discussion
Tab
,
cls
)
.
is_enabled
(
course
,
user
):
return
False
if
settings
.
FEATURES
.
get
(
'CUSTOM_COURSES_EDX'
,
False
):
...
...
lms/djangoapps/edxnotes/plugins.py
View file @
7461a2fd
...
...
@@ -4,15 +4,15 @@ Registers the "edX Notes" feature for the edX platform.
from
django.utils.translation
import
ugettext
as
_
from
courseware.tabs
import
Enrolled
CourseViewType
from
courseware.tabs
import
Enrolled
Tab
class
EdxNotes
CourseViewType
(
EnrolledCourseViewType
):
class
EdxNotes
Tab
(
EnrolledTab
):
"""
The representation of the edX Notes course
view
type.
The representation of the edX Notes course
tab
type.
"""
nam
e
=
"edxnotes"
typ
e
=
"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
(
EdxNotes
CourseViewType
,
cls
)
.
is_enabled
(
course
,
user
=
user
):
if
not
super
(
EdxNotes
Tab
,
cls
)
.
is_enabled
(
course
,
user
=
user
):
return
False
return
course
.
edxnotes
lms/djangoapps/instructor/views/instructor_dashboard.py
View file @
7461a2fd
...
...
@@ -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
InstructorDashboard
ViewType
(
CourseViewType
):
class
InstructorDashboard
Tab
(
CourseTab
):
"""
Defines the Instructor Dashboard view type that is shown as a course tab.
"""
nam
e
=
"instructor"
typ
e
=
"instructor"
title
=
_
(
'Instructor'
)
view_name
=
"instructor_dashboard"
is_dynamic
=
True
# The "Instructor" tab is instead dynamically added when it is enabled
...
...
lms/djangoapps/notes/views.py
View file @
7461a2fd
...
...
@@ -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
Enrolled
CourseViewType
from
courseware.tabs
import
Enrolled
Tab
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
Notes
CourseViewType
(
EnrolledCourseViewType
):
class
Notes
Tab
(
EnrolledTab
):
"""
A tab for the course notes.
"""
nam
e
=
'notes'
typ
e
=
'notes'
title
=
_
(
"My Notes"
)
view_name
=
"notes"
@classmethod
def
is_enabled
(
cls
,
course
,
user
=
None
):
if
not
super
(
Notes
CourseViewType
,
cls
)
.
is_enabled
(
course
,
user
):
if
not
super
(
Notes
Tab
,
cls
)
.
is_enabled
(
course
,
user
):
return
False
return
settings
.
FEATURES
.
get
(
'ENABLE_STUDENT_NOTES'
)
and
"notes"
in
course
.
advanced_modules
lms/djangoapps/open_ended_grading/views.py
View file @
7461a2fd
...
...
@@ -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
Enrolled
CourseViewType
from
courseware.tabs
import
Enrolled
Tab
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.
"""
nam
e
=
'staff_grading'
typ
e
=
'staff_grading'
title
=
_
(
"Staff grading"
)
view_name
=
"staff_grading"
...
...
@@ -81,11 +80,11 @@ class StaffGradingTab(CourseViewType):
return
"combinedopenended"
in
course
.
advanced_modules
class
PeerGradingTab
(
Enrolled
CourseViewType
):
class
PeerGradingTab
(
Enrolled
Tab
):
"""
A tab for peer grading.
"""
nam
e
=
'peer_grading'
typ
e
=
'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
(
Enrolled
CourseViewType
):
class
OpenEndedGradingTab
(
Enrolled
Tab
):
"""
A tab for open ended grading.
"""
nam
e
=
'open_ended'
typ
e
=
'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"
)
...
...
lms/djangoapps/teams/plugins.py
View file @
7461a2fd
...
...
@@ -3,16 +3,16 @@ Definition of the course team feature.
"""
from
django.utils.translation
import
ugettext
as
_
from
courseware.tabs
import
Enrolled
CourseViewType
from
courseware.tabs
import
Enrolled
Tab
from
.views
import
is_feature_enabled
class
Teams
CourseViewType
(
EnrolledCourseViewType
):
class
Teams
Tab
(
EnrolledTab
):
"""
The representation of the course teams view type.
"""
nam
e
=
"teams"
typ
e
=
"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
(
Teams
CourseViewType
,
cls
)
.
is_enabled
(
course
,
user
=
user
):
if
not
super
(
Teams
Tab
,
cls
)
.
is_enabled
(
course
,
user
=
user
):
return
False
return
is_feature_enabled
(
course
)
openedx/core/djangoapps/course_views/__init__.py
deleted
100644 → 0
View file @
8d1651cc
openedx/core/djangoapps/course_views/course_views.py
deleted
100644 → 0
View file @
8d1651cc
"""
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'
)
openedx/core/djangoapps/course_views/tests/__init__.py
deleted
100644 → 0
View file @
8d1651cc
openedx/core/lib/course_tabs.py
0 → 100644
View file @
7461a2fd
"""
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
openedx/core/
djangoapps/course_views/tests/test
_api.py
→
openedx/core/
lib/tests/test_course_tab
_api.py
View file @
7461a2fd
...
...
@@ -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
CourseViewType
Manager
from
openedx.core.
lib.course_tabs
import
CourseTabPlugin
Manager
class
TestPluginApi
(
TestCase
):
...
...
@@ -17,8 +17,8 @@ class TestPluginApi(TestCase):
"""
Verify that get_plugin works as expected.
"""
course_view_type
=
CourseViewType
Manager
.
get_plugin
(
"instructor"
)
self
.
assertEqual
(
course_view
_type
.
title
,
"Instructor"
)
tab_type
=
CourseTabPlugin
Manager
.
get_plugin
(
"instructor"
)
self
.
assertEqual
(
tab
_type
.
title
,
"Instructor"
)
with
self
.
assertRaises
(
PluginError
):
Course
ViewType
Manager
.
get_plugin
(
"no_such_type"
)
Course
TabPlugin
Manager
.
get_plugin
(
"no_such_type"
)
openedx/core/
djangoapps/course_views/tests/test_course_view
s.py
→
openedx/core/
lib/tests/test_course_tab
s.py
View file @
7461a2fd
...
...
@@ -5,34 +5,34 @@ from unittest import TestCase
import
xmodule.tabs
as
xmodule_tabs
from
openedx.core.
djangoapps.course_views.course_views
import
CourseViewType
Manager
from
openedx.core.
lib.course_tabs
import
CourseTabPlugin
Manager
class
Course
ViewType
ManagerTestCase
(
TestCase
):
"""Test cases for Course
ViewType
Manager class"""
class
Course
TabPlugin
ManagerTestCase
(
TestCase
):
"""Test cases for Course
TabPlugin
Manager class"""
@patch
(
'openedx.core.
djangoapps.course_views.course_views.CourseViewType
Manager.get_available_plugins'
)
def
test_get_
course_view
_types
(
self
,
get_available_plugins
):
@patch
(
'openedx.core.
lib.course_tabs.CourseTabPlugin
Manager.get_available_plugins'
)
def
test_get_
tab
_types
(
self
,
get_available_plugins
):
"""
Verify that get_course_view_types sorts appropriately
"""
def
create_mock_plugin
(
nam
e
,
priority
):
def
create_mock_plugin
(
tab_typ
e
,
priority
):
""" Create a mock plugin with the specified name and priority. """
mock_plugin
=
Mock
()
mock_plugin
.
name
=
nam
e
mock_plugin
.
type
=
tab_typ
e
mock_plugin
.
priority
=
priority
return
mock_plugin
mock_plugins
=
{
"Last"
:
create_mock_plugin
(
nam
e
=
"Last"
,
priority
=
None
),
"Duplicate1"
:
create_mock_plugin
(
nam
e
=
"Duplicate"
,
priority
=
None
),
"Duplicate2"
:
create_mock_plugin
(
nam
e
=
"Duplicate"
,
priority
=
None
),
"First"
:
create_mock_plugin
(
nam
e
=
"First"
,
priority
=
1
),
"Second"
:
create_mock_plugin
(
nam
e
=
"Second"
,
priority
=
1
),
"Third"
:
create_mock_plugin
(
nam
e
=
"Third"
,
priority
=
3
),
"Last"
:
create_mock_plugin
(
tab_typ
e
=
"Last"
,
priority
=
None
),
"Duplicate1"
:
create_mock_plugin
(
tab_typ
e
=
"Duplicate"
,
priority
=
None
),
"Duplicate2"
:
create_mock_plugin
(
tab_typ
e
=
"Duplicate"
,
priority
=
None
),
"First"
:
create_mock_plugin
(
tab_typ
e
=
"First"
,
priority
=
1
),
"Second"
:
create_mock_plugin
(
tab_typ
e
=
"Second"
,
priority
=
1
),
"Third"
:
create_mock_plugin
(
tab_typ
e
=
"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"
]
)
...
...
setup.py
View file @
7461a2fd
...
...
@@ -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:CcxCourse
ViewType
"
,
"courseware = lms.djangoapps.courseware.tabs:Courseware
ViewType
"
,
"course_info = lms.djangoapps.courseware.tabs:CourseInfo
ViewType
"
,
"discussion = lms.djangoapps.django_comment_client.forum.views:Discussion
CourseViewType
"
,
"edxnotes = lms.djangoapps.edxnotes.plugins:EdxNotes
CourseViewType
"
,
"external_discussion = lms.djangoapps.courseware.tabs:ExternalDiscussionCourse
ViewType
"
,
"external_link = lms.djangoapps.courseware.tabs:ExternalLinkCourse
ViewType
"
,
"html_textbooks = lms.djangoapps.courseware.tabs:HtmlTextbook
CourseView
s"
,
"instructor = lms.djangoapps.instructor.views.instructor_dashboard:InstructorDashboard
ViewType
"
,
"notes = lms.djangoapps.notes.views:Notes
CourseViewType
"
,
"pdf_textbooks = lms.djangoapps.courseware.tabs:PDFTextbook
CourseView
s"
,
"progress = lms.djangoapps.courseware.tabs:Progress
CourseViewType
"
,
"static_tab =
lms.djangoapps.courseware.tabs:StaticCourseViewType
"
,
"syllabus = lms.djangoapps.courseware.tabs:Syllabus
CourseViewType
"
,
"teams = lms.djangoapps.teams.plugins:Teams
CourseViewType
"
,
"textbooks = lms.djangoapps.courseware.tabs:Textbook
CourseView
s"
,
"wiki = lms.djangoapps.course_wiki.tab:Wiki
CourseViewType
"
,
"openedx.course_
tab
"
:
[
"ccx = lms.djangoapps.ccx.plugins:CcxCourse
Tab
"
,
"courseware = lms.djangoapps.courseware.tabs:Courseware
Tab
"
,
"course_info = lms.djangoapps.courseware.tabs:CourseInfo
Tab
"
,
"discussion = lms.djangoapps.django_comment_client.forum.views:Discussion
Tab
"
,
"edxnotes = lms.djangoapps.edxnotes.plugins:EdxNotes
Tab
"
,
"external_discussion = lms.djangoapps.courseware.tabs:ExternalDiscussionCourse
Tab
"
,
"external_link = lms.djangoapps.courseware.tabs:ExternalLinkCourse
Tab
"
,
"html_textbooks = lms.djangoapps.courseware.tabs:HtmlTextbook
Tab
s"
,
"instructor = lms.djangoapps.instructor.views.instructor_dashboard:InstructorDashboard
Tab
"
,
"notes = lms.djangoapps.notes.views:Notes
Tab
"
,
"pdf_textbooks = lms.djangoapps.courseware.tabs:PDFTextbook
Tab
s"
,
"progress = lms.djangoapps.courseware.tabs:Progress
Tab
"
,
"static_tab =
xmodule.tabs:StaticTab
"
,
"syllabus = lms.djangoapps.courseware.tabs:Syllabus
Tab
"
,
"teams = lms.djangoapps.teams.plugins:Teams
Tab
"
,
"textbooks = lms.djangoapps.courseware.tabs:Textbook
Tab
s"
,
"wiki = lms.djangoapps.course_wiki.tab:Wiki
Tab
"
,
# ORA 1 tabs (deprecated)
"peer_grading = lms.djangoapps.open_ended_grading.views:PeerGradingTab"
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment