Commit dae137fe by Diana Huang

Convert all tabs to the new plugin framework.

parent 94e1c423
......@@ -225,17 +225,6 @@ class TestCourseListing(ModuleStoreTestCase):
self._create_course_with_access_groups(course_location, self.user)
store.delete_course(course_location, self.user.id)
course_location = self.store.make_course_key('testOrg', 'erroredCourse', 'RunBabyRun')
course = self._create_course_with_access_groups(course_location, self.user)
course_db_record = store._find_one(course.location)
course_db_record.setdefault('metadata', {}).get('tabs', []).append({"type": "wiko", "name": "Wiki"})
store.collection.update(
{'_id': course.location.to_deprecated_son()},
{'$set': {
'metadata.tabs': course_db_record['metadata']['tabs'],
}},
)
courses_list, __ = _accessible_courses_list_from_groups(self.request)
self.assertEqual(len(courses_list), 1, courses_list)
......
......@@ -14,7 +14,7 @@ from django.conf import settings
from models.settings.course_details import (CourseDetails, CourseSettingsEncoder)
from models.settings.course_grading import CourseGradingModel
from contentstore.utils import EXTRA_TAB_PANELS, reverse_course_url, reverse_usage_url
from contentstore.utils import reverse_course_url, reverse_usage_url
from xmodule.modulestore.tests.factories import CourseFactory
from models.settings.course_metadata import CourseMetadata
......@@ -662,7 +662,7 @@ class CourseMetadataEditingTest(CourseTestCase):
If feature flag is off, then giturl must be filtered.
"""
# pylint: disable=unused-variable
is_valid, errors, test_model = CourseMetadata.validate_from_json(
is_valid, errors, test_model = CourseMetadata.validate_and_update_from_json(
self.course,
{
"giturl": {"value": "http://example.com"},
......@@ -677,7 +677,7 @@ class CourseMetadataEditingTest(CourseTestCase):
If feature flag is on, then giturl must not be filtered.
"""
# pylint: disable=unused-variable
is_valid, errors, test_model = CourseMetadata.validate_from_json(
is_valid, errors, test_model = CourseMetadata.validate_and_update_from_json(
self.course,
{
"giturl": {"value": "http://example.com"},
......@@ -736,7 +736,7 @@ class CourseMetadataEditingTest(CourseTestCase):
If feature flag is off, then edxnotes must be filtered.
"""
# pylint: disable=unused-variable
is_valid, errors, test_model = CourseMetadata.validate_from_json(
is_valid, errors, test_model = CourseMetadata.validate_and_update_from_json(
self.course,
{
"edxnotes": {"value": "true"},
......@@ -751,7 +751,7 @@ class CourseMetadataEditingTest(CourseTestCase):
If feature flag is on, then edxnotes must not be filtered.
"""
# pylint: disable=unused-variable
is_valid, errors, test_model = CourseMetadata.validate_from_json(
is_valid, errors, test_model = CourseMetadata.validate_and_update_from_json(
self.course,
{
"edxnotes": {"value": "true"},
......@@ -789,7 +789,7 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertNotIn('edxnotes', test_model)
def test_validate_from_json_correct_inputs(self):
is_valid, errors, test_model = CourseMetadata.validate_from_json(
is_valid, errors, test_model = CourseMetadata.validate_and_update_from_json(
self.course,
{
"advertised_start": {"value": "start A"},
......@@ -808,7 +808,7 @@ class CourseMetadataEditingTest(CourseTestCase):
def test_validate_from_json_wrong_inputs(self):
# input incorrectly formatted data
is_valid, errors, test_model = CourseMetadata.validate_from_json(
is_valid, errors, test_model = CourseMetadata.validate_and_update_from_json(
self.course,
{
"advertised_start": {"value": 1, "display_name": "Course Advertised Start Date", },
......@@ -819,7 +819,7 @@ class CourseMetadataEditingTest(CourseTestCase):
user=self.user
)
# Check valid results from validate_from_json
# Check valid results from validate_and_update_from_json
self.assertFalse(is_valid)
self.assertEqual(len(errors), 3)
self.assertFalse(test_model)
......@@ -928,19 +928,50 @@ class CourseMetadataEditingTest(CourseTestCase):
"""
Test that adding and removing specific advanced components adds and removes tabs.
"""
self.assertNotIn(EXTRA_TAB_PANELS.get("open_ended"), self.course.tabs)
self.assertNotIn(EXTRA_TAB_PANELS.get("notes"), self.course.tabs)
open_ended_tab = {"type": "open_ended", "name": "Open Ended Panel"}
peer_grading_tab = {"type": "peer_grading", "name": "Peer grading"}
notes_tab = {"type": "notes", "name": "My Notes"}
# First ensure that none of the tabs are visible
self.assertNotIn(open_ended_tab, self.course.tabs)
self.assertNotIn(peer_grading_tab, self.course.tabs)
self.assertNotIn(notes_tab, self.course.tabs)
# Now add the "combinedopenended" component and verify that the tab has been added
self.client.ajax_post(self.course_setting_url, {
ADVANCED_COMPONENT_POLICY_KEY: {"value": ["combinedopenended"]}
})
course = modulestore().get_course(self.course.id)
self.assertIn(EXTRA_TAB_PANELS.get("open_ended"), course.tabs)
self.assertNotIn(EXTRA_TAB_PANELS.get("notes"), course.tabs)
self.assertIn(open_ended_tab, course.tabs)
self.assertIn(peer_grading_tab, course.tabs)
self.assertNotIn(notes_tab, course.tabs)
# Now enable student notes and verify that the "My Notes" tab has also been added
self.client.ajax_post(self.course_setting_url, {
ADVANCED_COMPONENT_POLICY_KEY: {"value": ["combinedopenended", "notes"]}
})
course = modulestore().get_course(self.course.id)
self.assertIn(open_ended_tab, course.tabs)
self.assertIn(peer_grading_tab, course.tabs)
self.assertIn(notes_tab, course.tabs)
# Now remove the "combinedopenended" component and verify that the tab is gone
self.client.ajax_post(self.course_setting_url, {
ADVANCED_COMPONENT_POLICY_KEY: {"value": ["notes"]}
})
course = modulestore().get_course(self.course.id)
self.assertNotIn(open_ended_tab, course.tabs)
self.assertNotIn(peer_grading_tab, course.tabs)
self.assertIn(notes_tab, course.tabs)
# Finally disable student notes and verify that the "My Notes" tab is gone
self.client.ajax_post(self.course_setting_url, {
ADVANCED_COMPONENT_POLICY_KEY: {"value": []}
ADVANCED_COMPONENT_POLICY_KEY: {"value": [""]}
})
course = modulestore().get_course(self.course.id)
self.assertNotIn(EXTRA_TAB_PANELS.get("open_ended"), course.tabs)
self.assertNotIn(open_ended_tab, course.tabs)
self.assertNotIn(peer_grading_tab, course.tabs)
self.assertNotIn(notes_tab, course.tabs)
class CourseGraderUpdatesTest(CourseTestCase):
......
......@@ -26,11 +26,6 @@ from student import auth
log = logging.getLogger(__name__)
# In order to instantiate an open ended tab automatically, need to have this data
OPEN_ENDED_PANEL = {"name": _("Open Ended Panel"), "type": "open_ended"}
NOTES_PANEL = {"name": _("My Notes"), "type": "notes"}
EXTRA_TAB_PANELS = {p['type']: p for p in [OPEN_ENDED_PANEL, NOTES_PANEL]}
def add_instructor(course_key, requesting_user, new_instructor):
"""
......
......@@ -16,20 +16,19 @@ from django.core.urlresolvers import reverse
from django.http import HttpResponseBadRequest, HttpResponseNotFound, HttpResponse, Http404
from util.json_request import JsonResponse, JsonResponseBadRequest
from util.date_utils import get_default_time_display
from util.db import generate_int_id, MYSQL_MAX_INT
from edxmako.shortcuts import render_to_response
from xmodule.course_module import DEFAULT_START_DATE
from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent
from xmodule.tabs import PDFTextbookTabs, CourseTab, CourseTabManager
from xmodule.tabs import CourseTab
from openedx.core.djangoapps.course_views.course_views import CourseViewTypeManager
from xmodule.modulestore import EdxJSONEncoder
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError
from opaque_keys import InvalidKeyError
from opaque_keys.edx.locations import Location
from opaque_keys.edx.keys import CourseKey
from openedx.core.lib.plugins.api import CourseViewType
from django_future.csrf import ensure_csrf_cookie
from contentstore.course_info_model import get_course_updates, update_course_updates, delete_course_update
......@@ -46,10 +45,8 @@ from contentstore.utils import (
get_lms_link_for_item,
reverse_course_url,
reverse_library_url,
reverse_usage_url,
reverse_url,
remove_all_instructors,
EXTRA_TAB_PANELS,
)
from models.settings.course_details import CourseDetails, CourseSettingsEncoder
from models.settings.course_grading import CourseGradingModel
......@@ -58,9 +55,6 @@ from util.json_request import expect_json
from util.string_utils import _has_non_ascii_characters
from student.auth import has_studio_write_access, has_studio_read_access
from .component import (
OPEN_ENDED_COMPONENT_TYPES,
NOTE_COMPONENT_TYPES,
ADVANCED_COMPONENT_POLICY_KEY,
SPLIT_TEST_COMPONENT_TYPE,
ADVANCED_COMPONENT_TYPES,
)
......@@ -84,7 +78,6 @@ from course_action_state.models import CourseRerunState, CourseRerunUIStateManag
from course_action_state.managers import CourseActionStateItemNotFoundError
from microsite_configuration import microsite
from xmodule.course_module import CourseFields
from xmodule.split_test_module import get_split_user_partitions
from student.auth import has_course_author_access
from util.milestones_helpers import (
......@@ -994,88 +987,38 @@ def grading_handler(request, course_key_string, grader_index=None):
return JsonResponse()
def is_advanced_component_present(request, advanced_components):
"""
Return True when one of `advanced_components` is present in the request.
raises TypeError
when request.ADVANCED_COMPONENT_POLICY_KEY is malformed (not iterable)
"""
if ADVANCED_COMPONENT_POLICY_KEY not in request.json:
return False
new_advanced_component_list = request.json[ADVANCED_COMPONENT_POLICY_KEY]['value']
for ac_type in advanced_components:
if ac_type in new_advanced_component_list and ac_type in ADVANCED_COMPONENT_TYPES:
return True
def is_field_value_true(request, field_list):
"""
Return True when one of field values is set to True by request
"""
return any([request.json.get(field, {}).get('value') for field in field_list])
def _refresh_course_tabs(request, course_module):
"""
Automatically adds/removes tabs if changes to the course require them.
"""
tab_component_map = {
# 'tab_type': (check_function, list_of_checked_components_or_values),
# open ended tab by combinedopendended or peergrading module
'open_ended': (is_advanced_component_present, OPEN_ENDED_COMPONENT_TYPES),
# notes tab
'notes': (is_advanced_component_present, NOTE_COMPONENT_TYPES),
}
def update_tab(tabs, tab_type, tab_enabled):
"""
Adds or removes a course tab based upon whether it is enabled.
"""
tab_panel = _get_tab_panel_for_type(tab_type)
if tab_enabled:
tab_panel = {
"type": tab_type.name,
"name": tab_type.title,
}
has_tab = tab_panel in tabs
if tab_enabled and not has_tab:
tabs.append(CourseTab.from_json(tab_panel))
elif tab_panel in tabs:
elif not tab_enabled and has_tab:
tabs.remove(tab_panel)
course_tabs = copy.copy(course_module.tabs)
for tab_type in tab_component_map.keys():
check, component_types = tab_component_map[tab_type]
try:
tab_enabled = check(request, component_types)
except TypeError:
# user has failed to put iterable value into advanced component list.
# return immediately and let validation handle.
return
update_tab(course_tabs, tab_type, tab_enabled)
# Additionally update any persistent tabs provided by course views
for tab_type in CourseTabManager.get_tab_types().values():
if issubclass(tab_type, CourseViewType) and tab_type.is_persistent:
tab_enabled = tab_type.is_enabled(course_module, settings, user=request.user)
# Additionally update any tabs that are provided by non-dynamic course views
for tab_type in CourseViewTypeManager.get_course_view_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)
# Save the tabs into the course if they have been changed
if not course_tabs == course_module.tabs:
if course_tabs != course_module.tabs:
course_module.tabs = course_tabs
def _get_tab_panel_for_type(tab_type):
"""
Returns a tab panel representation for the specified tab type.
"""
tab_panel = EXTRA_TAB_PANELS.get(tab_type)
if tab_panel:
return tab_panel
return {
"name": tab_type.title,
"type": tab_type.name
}
@login_required
@ensure_csrf_cookie
@require_http_methods(("GET", "POST", "PUT"))
......@@ -1107,7 +1050,7 @@ def advanced_settings_handler(request, course_key_string):
try:
# validate data formats and update the course module.
# Note: don't update mongo yet, but wait until after any tabs are changed
is_valid, errors, updated_data = CourseMetadata.validate_from_json(
is_valid, errors, updated_data = CourseMetadata.validate_and_update_from_json(
course_module,
request.json,
user=request.user,
......@@ -1238,8 +1181,8 @@ def textbooks_list_handler(request, course_key_string):
textbook["id"] = tid
tids.add(tid)
if not any(tab['type'] == PDFTextbookTabs.type for tab in course.tabs):
course.tabs.append(PDFTextbookTabs())
if not any(tab['type'] == 'pdf_textbooks' for tab in course.tabs):
course.tabs.append(CourseTab.load('pdf_textbooks'))
course.pdf_textbooks = textbooks
store.update_item(course, request.user.id)
return JsonResponse(course.pdf_textbooks)
......@@ -1255,8 +1198,8 @@ def textbooks_list_handler(request, course_key_string):
existing = course.pdf_textbooks
existing.append(textbook)
course.pdf_textbooks = existing
if not any(tab['type'] == PDFTextbookTabs.type for tab in course.tabs):
course.tabs.append(PDFTextbookTabs())
if not any(tab['type'] == 'pdf_textbooks' for tab in course.tabs):
course.tabs.append(CourseTab.load('pdf_textbooks'))
store.update_item(course, request.user.id)
resp = JsonResponse(textbook, status=201)
resp["Location"] = reverse_course_url(
......
......@@ -12,12 +12,12 @@ 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.tabs import StaticTab
from xmodule.x_module import DEPRECATION_VSCOMPAT_EVENT
from contentstore.utils import reverse_course_url, reverse_library_url, reverse_usage_url
......
......@@ -10,11 +10,13 @@ from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django_future.csrf import ensure_csrf_cookie
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, StaticTab, CourseTab, InvalidTabsException
from xmodule.tabs import CourseTabList, CourseTab, InvalidTabsException
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
......@@ -61,7 +63,7 @@ def tabs_handler(request, course_key_string):
# present in the same order they are displayed in LMS
tabs_to_render = []
for tab in CourseTabList.iterate_displayable(course_item, settings, inline_collections=False):
for tab in CourseTabList.iterate_displayable(course_item, inline_collections=False):
if isinstance(tab, StaticTab):
# static tab needs its locator information to render itself as an xmodule
static_tab_loc = course_key.make_usage_key('static_tab', tab.url_slug)
......
......@@ -8,7 +8,7 @@ from contentstore.utils import reverse_course_url
from xmodule.x_module import STUDENT_VIEW
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.tabs import CourseTabList, WikiTab
from xmodule.tabs import CourseTabList
from xmodule.modulestore.django import modulestore
......@@ -52,7 +52,7 @@ class TabsPageTests(CourseTestCase):
self.client.ajax_post(
self.url,
data=json.dumps({
'tab_id_locator': {'tab_id': WikiTab.type},
'tab_id_locator': {'tab_id': 'courseware'},
'unsupported_request': None,
}),
)
......@@ -158,10 +158,9 @@ class TabsPageTests(CourseTestCase):
self.assertEqual(new_tab.is_hidden, new_is_hidden_setting)
def test_toggle_tab_visibility(self):
"""Test toggling of tab visiblity"""
self.check_toggle_tab_visiblity(WikiTab.type, True)
self.check_toggle_tab_visiblity(WikiTab.type, False)
"""Test toggling of tab visibility"""
self.check_toggle_tab_visiblity('wiki', True)
self.check_toggle_tab_visiblity('wiki', False)
def test_toggle_invalid_tab_visibility(self):
"""Test toggling visibility of an invalid tab"""
......
......@@ -150,11 +150,12 @@ class CourseMetadata(object):
return cls.update_from_dict(key_values, descriptor, user)
@classmethod
def validate_from_json(cls, descriptor, jsondict, user, filter_tabs=True):
def validate_and_update_from_json(cls, descriptor, jsondict, user, filter_tabs=True):
"""
Validate the values in the json dict (validated by xblock fields from_json method)
If all fields validate, go ahead and update those values on the object and return it.
If all fields validate, go ahead and update those values on the object and return it without
persisting it to the DB.
If not, return the error objects list.
Returns:
......
......@@ -4,7 +4,7 @@
<%!
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
from xmodule.tabs import StaticTab
from openedx.core.djangoapps.course_views.course_views import StaticTab
from django.template.defaultfilters import escapejs
%>
<%block name="title">${_("Pages")}</%block>
......
......@@ -112,20 +112,6 @@ class TestCourseListing(ModuleStoreTestCase):
self._create_course_with_access_groups(course_location, default_store=ModuleStoreEnum.Type.mongo)
mongo_store.delete_course(course_location, ModuleStoreEnum.UserID.test)
course_location = mongo_store.make_course_key('testOrg', 'erroredCourse', 'RunBabyRun')
course = self._create_course_with_access_groups(course_location, default_store=ModuleStoreEnum.Type.mongo)
course_db_record = mongo_store._find_one(course.location)
course_db_record.setdefault('metadata', {}).get('tabs', []).append({
"type": "wiko",
"name": "Wiki",
})
mongo_store.collection.update(
{'_id': course.location.to_deprecated_son()},
{'$set': {
'metadata.tabs': course_db_record['metadata']['tabs'],
}},
)
courses_list = list(get_course_enrollment_pairs(self.student, None, []))
self.assertEqual(len(courses_list), 1, courses_list)
self.assertEqual(courses_list[0][0].id, good_location)
......
......@@ -22,7 +22,6 @@ from xmodule.modulestore.django import modulestore, clear_existing_modulestores
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
from xmodule.modulestore.tests.sample_courses import default_block_info_tree, TOY_BLOCK_INFO_TREE
from xmodule.modulestore.tests.factories import XMODULE_FACTORY_LOCK
from xmodule.tabs import CoursewareTab, CourseInfoTab, StaticTab, DiscussionTab, ProgressTab, WikiTab
class StoreConstructors(object):
......@@ -379,15 +378,6 @@ class ModuleStoreTestCase(TestCase):
"wiki_slug": "toy",
"display_name": "Toy Course",
"graded": True,
"tabs": [
CoursewareTab(),
CourseInfoTab(),
StaticTab(name="Syllabus", url_slug="syllabus"),
StaticTab(name="Resources", url_slug="resources"),
DiscussionTab(),
WikiTab(),
ProgressTab(),
],
"discussion_topics": {"General": {"id": "i4x-edX-toy-course-2012_Fall"}},
"graceperiod": datetime.timedelta(days=2, seconds=21599),
"start": datetime.datetime(2015, 07, 17, 12, tzinfo=pytz.utc),
......
......@@ -13,8 +13,8 @@ import dogstats_wrapper as dog_stats_api
from opaque_keys.edx.locations import Location
from opaque_keys.edx.keys import UsageKey
from xblock.core import XBlock
from xmodule.tabs import StaticTab
from xmodule.modulestore import prefer_xmodules, ModuleStoreEnum
from xmodule.tabs import CourseTab
from xmodule.x_module import DEPRECATION_VSCOMPAT_EVENT
......@@ -277,10 +277,7 @@ class ItemFactory(XModuleFactory):
course = store.get_course(location.course_key)
course.tabs.append(
StaticTab(
name=display_name,
url_slug=location.name,
)
CourseTab.load('static_tab', name='Static Tab', url_slug=location.name)
)
store.update_item(course, user_id)
......
......@@ -373,25 +373,6 @@ class TestMongoModuleStore(TestMongoModuleStoreBase):
'{0} is a template course'.format(course)
)
def test_static_tab_names(self):
def get_tab_name(index):
"""
Helper function for pulling out the name of a given static tab.
Assumes the information is desired for courses[4] ('toy' course).
"""
course = self.draft_store.get_course(SlashSeparatedCourseKey('edX', 'toy', '2012_Fall'))
return course.tabs[index]['name']
# There was a bug where model.save was not getting called after the static tab name
# was set set for tabs that have a URL slug. 'Syllabus' and 'Resources' fall into that
# category, but for completeness, I'm also testing 'Course Info' and 'Discussion' (no url slug).
assert_equals('Course Info', get_tab_name(1))
assert_equals('Syllabus', get_tab_name(2))
assert_equals('Resources', get_tab_name(3))
assert_equals('Discussion', get_tab_name(4))
def test_contentstore_attrs(self):
"""
Test getting, setting, and defaulting the locked attr and arbitrary attrs.
......
"""
Test split modulestore w/o using any django stuff.
"""
from mock import patch
import datetime
from importlib import import_module
from path import path
......@@ -36,6 +37,14 @@ 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):
'''
......@@ -596,7 +605,8 @@ class SplitModuleCourseTests(SplitModuleTest):
Course CRUD operation tests
'''
def test_get_courses(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_get_courses(self, _from_json):
courses = modulestore().get_courses(branch=BRANCH_NAME_DRAFT)
# should have gotten 3 draft courses
self.assertEqual(len(courses), 3, "Wrong number of courses")
......@@ -635,7 +645,8 @@ class SplitModuleCourseTests(SplitModuleTest):
courses = modulestore().get_courses(branch=BRANCH_NAME_DRAFT)
self.assertEqual(len(courses), 3)
def test_branch_requests(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_branch_requests(self, _from_json):
# query w/ branch qualifier (both draft and published)
def _verify_published_course(courses_published):
""" Helper function for verifying published course. """
......@@ -665,7 +676,8 @@ class SplitModuleCourseTests(SplitModuleTest):
locator_key_fields=['org', 'course', 'run']
)
def test_get_course(self):
@patch('xmodule.tabs.CourseTab.from_json', side_effect=mock_tab_from_json)
def test_get_course(self, _from_json):
'''
Test the various calling forms for get_course
'''
......
......@@ -5,7 +5,7 @@ well-formed and not-well-formed XML.
import os.path
import unittest
from glob import glob
from mock import patch
from mock import patch, Mock
from xmodule.modulestore.xml import XMLModuleStore
from xmodule.modulestore import ModuleStoreEnum
......@@ -35,6 +35,7 @@ class TestXMLModuleStore(unittest.TestCase):
store = XMLModuleStore(DATA_DIR, source_dirs=[])
self.assertEqual(store.get_modulestore_type(), ModuleStoreEnum.Type.xml)
@patch('xmodule.tabs.CourseTabList.initialize_default', Mock())
def test_unicode_chars_in_xml_content(self):
# edX/full/6.002_Spring_2012 has non-ASCII chars, and during
# uniquification of names, would raise a UnicodeError. It no longer does.
......
......@@ -2,9 +2,10 @@
Registers the CCX feature for the edX platform.
"""
from django.conf import settings
from django.utils.translation import ugettext as _
from openedx.core.lib.plugins.api import CourseViewType
from openedx.core.djangoapps.course_views.course_views import CourseViewType
from student.roles import CourseCcxCoachRole
......@@ -16,10 +17,10 @@ class CcxCourseViewType(CourseViewType):
name = "ccx_coach"
title = _("CCX Coach")
view_name = "ccx_coach_dashboard"
is_persistent = False
is_dynamic = True # The CCX view is dynamically added to the set of tabs when it is enabled
@classmethod
def is_enabled(cls, course, settings, user=None):
def is_enabled(cls, course, user=None):
"""
Returns true if CCX has been enabled and the specified user is a coach
"""
......
......@@ -14,6 +14,7 @@ from capa.tests.response_xml_factory import StringResponseXMLFactory
from courseware.field_overrides import OverrideFieldData # pylint: disable=import-error
from courseware.tests.factories import StudentModuleFactory # pylint: disable=import-error
from courseware.tests.helpers import LoginEnrollmentTestCase # pylint: disable=import-error
from courseware.tabs import get_course_tab_list
from django.core.urlresolvers import reverse
from django.test.utils import override_settings
from django.test import RequestFactory
......@@ -773,21 +774,24 @@ class TestSwitchActiveCCX(ModuleStoreTestCase, LoginEnrollmentTestCase):
@ddt.ddt
class CCXCoachTabTestCase(unittest.TestCase):
class CCXCoachTabTestCase(ModuleStoreTestCase):
"""
Test case for CCX coach tab.
"""
def setUp(self):
super(CCXCoachTabTestCase, self).setUp()
self.course = MagicMock()
self.course.id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
self.settings = MagicMock()
self.settings.FEATURES = {}
self.course = CourseFactory.create()
self.user = UserFactory.create()
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id)
role = CourseCcxCoachRole(self.course.id)
role.add_users(self.user)
def check_ccx_tab(self):
"""Helper function for verifying the ccx tab."""
tab = tabs.CcxCoachTab({'type': tabs.CcxCoachTab.type, 'name': 'CCX Coach'})
return tab
request = RequestFactory().request()
request.user = self.user
all_tabs = get_course_tab_list(request, self.course)
return any(tab.type == 'ccx_coach' for tab in all_tabs)
@ddt.data(
(True, True, True),
......@@ -796,22 +800,17 @@ class CCXCoachTabTestCase(unittest.TestCase):
(False, False, False),
(True, None, False)
)
@patch('ccx.overrides.get_current_request', ccx_dummy_request)
@ddt.unpack
def test_coach_tab_for_ccx_advance_settings(self, ccx_feature_flag, enable_ccx, expected_result):
"""
Test ccx coach tab state (visible or hidden) depending on the value of enable_ccx flag, ccx feature flag.
"""
tab = self.check_ccx_tab()
self.settings.FEATURES = {'CUSTOM_COURSES_EDX': ccx_feature_flag}
self.course.enable_ccx = enable_ccx
self.assertEquals(
expected_result,
tab.can_display(
self.course, self.settings, is_user_authenticated=True, is_user_staff=False, is_user_enrolled=True
with self.settings(FEATURES={'CUSTOM_COURSES_EDX': ccx_feature_flag}):
self.course.enable_ccx = enable_ccx
self.assertEquals(
expected_result,
self.check_ccx_tab()
)
)
def flatten(seq):
......
"""
These callables are used by django-wiki to check various permissions
a user has on an article.
"""
from django.conf import settings
from django.utils.translation import ugettext as _
from courseware.tabs import EnrolledCourseViewType
class WikiCourseViewType(EnrolledCourseViewType):
"""
Defines the Wiki view type that is shown as a course tab.
"""
name = "wiki"
title = _('Wiki')
view_name = "course_wiki"
is_hideable = True
@classmethod
def is_enabled(cls, course, user=None):
"""
Returns true if the wiki is enabled and the specified user is enrolled or has staff access.
"""
if not settings.WIKI_ENABLED:
return False
if course.allow_public_wiki_access:
return True
return super(WikiCourseViewType, cls).is_enabled(course, user=user)
"""
Tests for wiki views.
"""
from django.conf import settings
from django.test.client import RequestFactory
from courseware.tabs import get_course_tab_list
from student.tests.factories import AdminFactory, UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
class WikiTabTestCase(ModuleStoreTestCase):
"""Test cases for Wiki Tab."""
def setUp(self):
super(WikiTabTestCase, self).setUp()
self.course = CourseFactory.create()
self.instructor = AdminFactory.create()
self.user = UserFactory()
def get_wiki_tab(self, user, course):
"""Returns true if the "Wiki" tab is shown."""
request = RequestFactory().request()
request.user = user
all_tabs = get_course_tab_list(request, course)
wiki_tabs = [tab for tab in all_tabs if tab.name == 'Wiki']
return wiki_tabs[0] if len(wiki_tabs) == 1 else None
def test_wiki_enabled_and_public(self):
"""
Test wiki tab when Enabled setting is True and the wiki is open to
the public.
"""
settings.WIKI_ENABLED = True
self.course.allow_public_wiki_access = True
self.assertIsNotNone(self.get_wiki_tab(self.user, self.course))
def test_wiki_enabled_and_not_public(self):
"""
Test wiki when it is enabled but not open to the public
"""
settings.WIKI_ENABLED = True
self.course.allow_public_wiki_access = False
self.assertIsNone(self.get_wiki_tab(self.user, self.course))
self.assertIsNotNone(self.get_wiki_tab(self.instructor, self.course))
def test_wiki_enabled_false(self):
"""Test wiki tab when Enabled setting is False"""
settings.WIKI_ENABLED = False
self.assertIsNone(self.get_wiki_tab(self.user, self.course))
self.assertIsNone(self.get_wiki_tab(self.instructor, self.course))
def test_wiki_visibility(self):
"""Test toggling of visibility of wiki tab"""
settings.WIKI_ENABLED = True
self.course.allow_public_wiki_access = True
wiki_tab = self.get_wiki_tab(self.user, self.course)
self.assertIsNotNone(wiki_tab)
self.assertTrue(wiki_tab.is_hideable)
wiki_tab.is_hidden = True
self.assertTrue(wiki_tab['is_hidden'])
wiki_tab['is_hidden'] = False
self.assertFalse(wiki_tab.is_hidden)
......@@ -51,13 +51,14 @@ from courseware.models import StudentModule, StudentModuleHistory
from course_modes.models import CourseMode
from open_ended_grading import open_ended_notifications
from open_ended_grading.views import StaffGradingTab, PeerGradingTab, OpenEndedGradingTab
from student.models import UserTestGroup, CourseEnrollment
from student.views import is_course_blocked
from util.cache import cache, cache_if_anonymous
from xblock.fragment import Fragment
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
from xmodule.tabs import CourseTabList, StaffGradingTab, PeerGradingTab, OpenEndedGradingTab
from xmodule.tabs import CourseTabList
from xmodule.x_module import STUDENT_VIEW
import shoppingcart
from shoppingcart.models import CourseRegistrationCode
......@@ -741,8 +742,6 @@ def static_tab(request, course_id, tab_slug):
'tab_contents': contents,
})
# TODO arjun: remove when custom tabs in place, see courseware/syllabus.py
@ensure_csrf_cookie
@ensure_valid_course_key
......@@ -1136,13 +1135,13 @@ def notification_image_for_tab(course_tab, user, course):
"""
tab_notification_handlers = {
StaffGradingTab.type: open_ended_notifications.staff_grading_notifications,
PeerGradingTab.type: open_ended_notifications.peer_grading_notifications,
OpenEndedGradingTab.type: open_ended_notifications.combined_notifications
StaffGradingTab.name: open_ended_notifications.staff_grading_notifications,
PeerGradingTab.name: open_ended_notifications.peer_grading_notifications,
OpenEndedGradingTab.name: open_ended_notifications.combined_notifications
}
if course_tab.type in tab_notification_handlers:
notifications = tab_notification_handlers[course_tab.type](course, user)
if course_tab.name in tab_notification_handlers:
notifications = tab_notification_handlers[course_tab.name](course, user)
if notifications and notifications['pending_grading']:
return notifications['img_path']
......
......@@ -24,7 +24,6 @@ from django_comment_client.utils import get_accessible_discussion_modules
from lms.lib.comment_client.thread import Thread
from lms.lib.comment_client.utils import CommentClientRequestError
from openedx.core.djangoapps.course_groups.cohorts import get_cohort_id
from xmodule.tabs import DiscussionTab
def _get_course_or_404(course_key, user):
......@@ -34,7 +33,7 @@ def _get_course_or_404(course_key, user):
disabled for the course.
"""
course = get_course_with_access(user, 'load_forum', course_key)
if not any([isinstance(tab, DiscussionTab) for tab in course.tabs]):
if not any([tab.type == 'discussion' for tab in course.tabs]):
raise Http404
return course
......
......@@ -44,7 +44,6 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.partitions.partitions import Group, UserPartition
from xmodule.tabs import DiscussionTab
def _remove_discussion_tab(course, user_id):
......@@ -53,7 +52,7 @@ def _remove_discussion_tab(course, user_id):
user_id is passed to the modulestore as the editor of the module.
"""
course.tabs = [tab for tab in course.tabs if not isinstance(tab, DiscussionTab)]
course.tabs = [tab for tab in course.tabs if not tab.type == 'discussion']
modulestore().update_item(course, user_id)
......
......@@ -8,10 +8,12 @@ import logging
import xml.sax.saxutils as saxutils
from django.contrib.auth.decorators import login_required
from django.conf import settings
from django.core.context_processors import csrf
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from django.http import Http404, HttpResponseBadRequest
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_GET
import newrelic.agent
......@@ -23,8 +25,10 @@ from openedx.core.djangoapps.course_groups.cohorts import (
get_course_cohorts,
is_commentable_cohorted
)
from courseware.tabs import EnrolledCourseViewType
from courseware.access import has_access
from xmodule.modulestore.django import modulestore
from ccx.overrides import get_current_ccx
from django_comment_client.permissions import cached_has_permission
from django_comment_client.utils import (
......@@ -45,6 +49,27 @@ PAGES_NEARBY_DELTA = 2
log = logging.getLogger("edx.discussions")
class DiscussionCourseViewType(EnrolledCourseViewType):
"""
A tab for the cs_comments_service forums.
"""
name = '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):
return False
if settings.FEATURES.get('CUSTOM_COURSES_EDX', False):
if get_current_ccx():
return False
return settings.FEATURES.get('ENABLE_DISCUSSION_SERVICE')
def _attr_safe_json(obj):
"""
return a JSON string for obj which is safe to embed as the value of an attribute in a DOM node
......
......@@ -6,11 +6,8 @@ from nose.plugins.attrib import attr
from pytz import UTC
from django.utils.timezone import UTC as django_utc
from django_comment_client.tests.factories import RoleFactory
from django_comment_client.tests.unicode import UnicodeTestMixin
import django_comment_client.utils as utils
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test import TestCase, RequestFactory
from edxmako import add_lookup
from django_comment_client.tests.factories import RoleFactory
......@@ -18,8 +15,9 @@ from django_comment_client.tests.unicode import UnicodeTestMixin
import django_comment_client.utils as utils
from courseware.tests.factories import InstructorFactory
from courseware.tabs import get_course_tab_list
from openedx.core.djangoapps.course_groups.cohorts import set_course_cohort_settings
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory
from openedx.core.djangoapps.util.testing import ContentGroupTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
......@@ -951,3 +949,37 @@ class RenderMustacheTests(TestCase):
"""
add_lookup('main', '', package=__name__)
self.assertEqual(utils.render_mustache('test.mustache', {}), 'Testing 1 2 3.\n')
class DiscussionTabTestCase(ModuleStoreTestCase):
""" Test visibility of the discussion tab. """
def setUp(self):
super(DiscussionTabTestCase, self).setUp()
self.course = CourseFactory.create()
self.enrolled_user = UserFactory.create()
self.staff_user = AdminFactory.create()
CourseEnrollmentFactory.create(user=self.enrolled_user, course_id=self.course.id)
self.unenrolled_user = UserFactory.create()
def discussion_tab_present(self, user):
""" Returns true if the user has access to the discussion tab. """
request = RequestFactory().request()
request.user = user
all_tabs = get_course_tab_list(request, self.course)
return any(tab.type == 'discussion' for tab in all_tabs)
def test_tab_access(self):
with self.settings(FEATURES={'ENABLE_DISCUSSION_SERVICE': True}):
self.assertTrue(self.discussion_tab_present(self.staff_user))
self.assertTrue(self.discussion_tab_present(self.enrolled_user))
self.assertFalse(self.discussion_tab_present(self.unenrolled_user))
@mock.patch('ccx.overrides.get_current_ccx')
def test_tab_settings(self, mock_get_ccx):
mock_get_ccx.return_value = True
with self.settings(FEATURES={'ENABLE_DISCUSSION_SERVICE': False}):
self.assertFalse(self.discussion_tab_present(self.enrolled_user))
with self.settings(FEATURES={'CUSTOM_COURSES_EDX': True}):
self.assertFalse(self.discussion_tab_present(self.enrolled_user))
......@@ -4,10 +4,10 @@ Registers the "edX Notes" feature for the edX platform.
from django.utils.translation import ugettext as _
from openedx.core.lib.plugins.api import CourseViewType
from courseware.tabs import EnrolledCourseViewType
class EdxNotesCourseViewType(CourseViewType):
class EdxNotesCourseViewType(EnrolledCourseViewType):
"""
The representation of the edX Notes course view type.
"""
......@@ -15,13 +15,9 @@ class EdxNotesCourseViewType(CourseViewType):
name = "edxnotes"
title = _("Notes")
view_name = "edxnotes"
is_persistent = True
# The course field that indicates that this feature is enabled
feature_flag_field_name = "edxnotes"
@classmethod
def is_enabled(cls, course, settings, user=None): # pylint: disable=unused-argument
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument
"""Returns true if the edX Notes feature is enabled in the course.
Args:
......@@ -29,4 +25,6 @@ class EdxNotesCourseViewType(CourseViewType):
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):
return False
return course.edxnotes
......@@ -25,14 +25,14 @@ from xmodule.tabs import CourseTab
from courseware.model_data import FieldDataCache
from courseware.module_render import get_module_for_descriptor
from courseware.tabs import get_course_tab_list
from student.tests.factories import UserFactory
from student.tests.factories import UserFactory, CourseEnrollmentFactory
def enable_edxnotes_for_the_course(course, user_id):
"""
Enable EdxNotes for the course.
"""
course.tabs.append(CourseTab.from_json({"type": "edxnotes", "name": "Notes"}))
course.tabs.append(CourseTab.load("edxnotes"))
modulestore().update_item(course, user_id)
......@@ -798,6 +798,7 @@ class EdxNotesViewsTest(ModuleStoreTestCase):
super(EdxNotesViewsTest, self).setUp()
self.course = CourseFactory.create(edxnotes=True)
self.user = UserFactory.create(username="Bob", email="bob@example.com", password="edx")
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id)
self.client.login(username=self.user.username, password="edx")
self.notes_page_url = reverse("edxnotes", args=[unicode(self.course.id)])
self.search_url = reverse("search_notes", args=[unicode(self.course.id)])
......@@ -820,10 +821,16 @@ class EdxNotesViewsTest(ModuleStoreTestCase):
request = RequestFactory().request()
request.user = user
tabs = get_course_tab_list(request, course)
return len([tab for tab in tabs if tab.name == 'Notes']) == 1
return len([tab for tab in tabs if tab.type == 'edxnotes']) == 1
self.assertFalse(has_notes_tab(self.user, self.course))
enable_edxnotes_for_the_course(self.course, self.user.id)
# disable course.edxnotes
self.course.edxnotes = False
self.assertFalse(has_notes_tab(self.user, self.course))
# reenable course.edxnotes
self.course.edxnotes = True
self.assertTrue(has_notes_tab(self.user, self.course))
# pylint: disable=unused-argument
......
......@@ -38,7 +38,7 @@ 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.lib.plugins.api import CourseViewType
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
......@@ -55,10 +55,10 @@ class InstructorDashboardViewType(CourseViewType):
name = "instructor"
title = _('Instructor')
view_name = "instructor_dashboard"
is_persistent = False
is_dynamic = True # The "Instructor" tab is instead dynamically added when it is enabled
@classmethod
def is_enabled(cls, course, settings, user=None): # pylint: disable=unused-argument,redefined-outer-name
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument,redefined-outer-name
"""
Returns true if the specified user has staff access.
"""
......
......@@ -5,27 +5,30 @@ Unit tests for the notes app.
from mock import patch, Mock
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from django.test import TestCase
from django.test import TestCase, RequestFactory
from django.test.client import Client
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
import collections
import json
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from courseware.tabs import get_course_tab_list, CourseTab
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from notes import utils, api, models
class UtilsTest(TestCase):
class UtilsTest(ModuleStoreTestCase):
""" Tests for the notes utils. """
def setUp(self):
'''
Setup a dummy course-like object with a tabs field that can be
accessed via attribute lookup.
'''
super(UtilsTest, self).setUp()
self.course = collections.namedtuple('DummyCourse', ['tabs'])
self.course.tabs = []
self.course = CourseFactory.create()
def test_notes_not_enabled(self):
'''
......@@ -39,11 +42,54 @@ class UtilsTest(TestCase):
Tests that notes are enabled when the course tab configuration contains
a tab with type "notes."
'''
self.course.tabs = [{'type': 'foo'},
{'name': 'My Notes', 'type': 'notes'},
{'type': 'bar'}]
with self.settings(FEATURES={'ENABLE_STUDENT_NOTES': True}):
self.course.advanced_modules = ["notes"]
self.assertTrue(utils.notes_enabled_for_course(self.course))
self.assertTrue(utils.notes_enabled_for_course(self.course))
class CourseTabTest(ModuleStoreTestCase):
"""
Test that the course tab shows up the way we expect.
"""
def setUp(self):
'''
Setup a dummy course-like object with a tabs field that can be
accessed via attribute lookup.
'''
super(CourseTabTest, self).setUp()
self.course = CourseFactory.create()
self.user = UserFactory()
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id)
def enable_notes(self):
"""Enable notes and add the tab to the course."""
self.course.tabs.append(CourseTab.load("notes"))
self.course.advanced_modules = ["notes"]
def has_notes_tab(self, course, user):
""" Returns true if the current course and user have a notes tab, false otherwise. """
request = RequestFactory().request()
request.user = user
all_tabs = get_course_tab_list(request, course)
return any([tab.name == u'My Notes' for tab in all_tabs])
def test_course_tab_not_visible(self):
# module not enabled in the course
self.assertFalse(self.has_notes_tab(self.course, self.user))
with self.settings(FEATURES={'ENABLE_STUDENT_NOTES': False}):
# setting not enabled and the module is not enabled
self.assertFalse(self.has_notes_tab(self.course, self.user))
# module is enabled and the setting is not enabled
self.course.advanced_modules = ["notes"]
self.assertFalse(self.has_notes_tab(self.course, self.user))
def test_course_tab_visible(self):
self.enable_notes()
self.assertTrue(self.has_notes_tab(self.course, self.user))
self.course.advanced_modules = []
self.assertFalse(self.has_notes_tab(self.course, self.user))
class ApiTest(TestCase):
......
......@@ -11,7 +11,7 @@ def notes_enabled_for_course(course):
2) present in the course tab configuration.
'''
tab_found = next((True for t in course.tabs if t['type'] == 'notes'), False)
tab_found = "notes" in course.advanced_modules
feature_enabled = settings.FEATURES.get('ENABLE_STUDENT_NOTES')
return feature_enabled and tab_found
"""
Views to support the edX Notes feature.
"""
from django.conf import settings
from django.contrib.auth.decorators import login_required
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 notes.models import Note
from notes.utils import notes_enabled_for_course
from xmodule.annotator_token import retrieve_token
from django.utils.translation import ugettext as _
@login_required
......@@ -30,3 +38,18 @@ def notes(request, course_id):
}
return render_to_response('notes.html', context)
class NotesCourseViewType(EnrolledCourseViewType):
"""
A tab for the course notes.
"""
name = 'notes'
title = _("My Notes")
view_name = "notes"
@classmethod
def is_enabled(cls, course, user=None):
if not super(NotesCourseViewType, cls).is_enabled(course, user):
return False
return settings.FEATURES.get('ENABLE_STUDENT_NOTES') and "notes" in course.advanced_modules
......@@ -26,8 +26,7 @@ from student.models import unique_id_for_user
from xmodule import peer_grading_module
from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_TOY_MODULESTORE
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_TOY_MODULESTORE, ModuleStoreTestCase
from xmodule.modulestore.xml_importer import import_course_from_xml
from xmodule.open_ended_grading_classes import peer_grading_service, controller_query_service
from xmodule.tests import test_util_open_ended
......
......@@ -4,7 +4,11 @@ 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 xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
import json
......@@ -62,6 +66,55 @@ ALERT_DICT = {
}
class StaffGradingTab(CourseViewType):
"""
A tab for staff grading.
"""
name = 'staff_grading'
title = _("Staff grading")
view_name = "staff_grading"
@classmethod
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument
if user and not has_access(user, 'staff', course, course.id):
return False
return "combinedopenended" in course.advanced_modules
class PeerGradingTab(EnrolledCourseViewType):
"""
A tab for peer grading.
"""
name = 'peer_grading'
# Translators: "Peer grading" appears on a tab that allows
# students to view open-ended problems that require grading
title = _("Peer grading")
view_name = "peer_grading"
@classmethod
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument
if not super(PeerGradingTab, cls).is_enabled(course, user=user):
return False
return "combinedopenended" in course.advanced_modules
class OpenEndedGradingTab(EnrolledCourseViewType):
"""
A tab for open ended grading.
"""
name = '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")
view_name = "open_ended_notifications"
@classmethod
def is_enabled(cls, course, user=None): # pylint: disable=unused-argument
if not super(OpenEndedGradingTab, cls).is_enabled(course, user=user):
return False
return "combinedopenended" in course.advanced_modules
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def staff_grading(request, course_id):
"""
......
......@@ -37,7 +37,7 @@
<%
discussion_tab = CourseTabList.get_discussion(course) if course else None
discussion_link = discussion_tab.link_func(course, reverse) if (discussion_tab and discussion_tab.is_enabled(course, settings, user=user)) else None
discussion_link = discussion_tab.link_func(course, reverse) if (discussion_tab and discussion_tab.is_enabled(course, user=user)) else None
%>
% if discussion_link:
......
......@@ -9,13 +9,12 @@ from django.contrib.auth.models import User
from django.db import IntegrityError
from django.http import Http404
from django.test import TestCase
from django.test.utils import override_settings
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from xmodule.modulestore.django import modulestore, clear_existing_modulestores
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_TOY_MODULESTORE, mixed_store_config, ModuleStoreTestCase
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_TOY_MODULESTORE, ModuleStoreTestCase
from ..models import CourseUserGroup, CourseCohort, CourseUserGroupPartitionGroup
from .. import cohorts
......
"""
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')
......@@ -4,7 +4,8 @@ Tests for the plugin API
from django.test import TestCase
from ..api import CourseViewTypeManager, PluginError
from openedx.core.lib.api.plugins import PluginError
from openedx.core.djangoapps.course_views.course_views import CourseViewTypeManager
class TestPluginApi(TestCase):
......
""" Tests of specific tabs. """
from mock import patch, Mock
from unittest import TestCase
import xmodule.tabs as xmodule_tabs
from openedx.core.djangoapps.course_views.course_views import CourseViewTypeManager
class CourseViewTypeManagerTestCase(TestCase):
"""Test cases for CourseViewTypeManager class"""
@patch('openedx.core.djangoapps.course_views.course_views.CourseViewTypeManager.get_available_plugins')
def test_get_course_view_types(self, get_available_plugins):
"""
Verify that get_course_view_types sorts appropriately
"""
def create_mock_plugin(name, priority):
""" Create a mock plugin with the specified name and priority. """
mock_plugin = Mock()
mock_plugin.name = name
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),
}
get_available_plugins.return_value = mock_plugins
self.assertEqual(
[plugin.name for plugin in CourseViewTypeManager.get_course_view_types()],
["First", "Second", "Third", "Duplicate", "Duplicate", "Last"]
)
class KeyCheckerTestCase(TestCase):
"""Test cases for KeyChecker class"""
def setUp(self):
super(KeyCheckerTestCase, self).setUp()
self.valid_keys = ['a', 'b']
self.invalid_keys = ['a', 'v', 'g']
self.dict_value = {'a': 1, 'b': 2, 'c': 3}
def test_key_checker(self):
self.assertTrue(xmodule_tabs.key_checker(self.valid_keys)(self.dict_value, raise_error=False))
self.assertFalse(xmodule_tabs.key_checker(self.invalid_keys)(self.dict_value, raise_error=False))
with self.assertRaises(xmodule_tabs.InvalidTabsException):
xmodule_tabs.key_checker(self.invalid_keys)(self.dict_value)
class NeedNameTestCase(TestCase):
"""Test cases for NeedName validator"""
def setUp(self):
super(NeedNameTestCase, self).setUp()
self.valid_dict1 = {'a': 1, 'name': 2}
self.valid_dict2 = {'name': 1}
self.valid_dict3 = {'a': 1, 'name': 2, 'b': 3}
self.invalid_dict = {'a': 1, 'b': 2}
def test_need_name(self):
self.assertTrue(xmodule_tabs.need_name(self.valid_dict1))
self.assertTrue(xmodule_tabs.need_name(self.valid_dict2))
self.assertTrue(xmodule_tabs.need_name(self.valid_dict3))
with self.assertRaises(xmodule_tabs.InvalidTabsException):
xmodule_tabs.need_name(self.invalid_dict)
......@@ -4,9 +4,6 @@ Adds support for first class features that can be added to the edX platform.
from stevedore.extension import ExtensionManager
# Stevedore extension point namespaces
COURSE_VIEW_TYPE_NAMESPACE = 'openedx.course_view_type'
class PluginError(Exception):
"""
......@@ -46,44 +43,3 @@ class PluginManager(object):
namespace=cls.NAMESPACE # pylint: disable=no-member
))
return plugins[name]
class CourseViewType(object):
"""
Base class of all course view type plugins.
"""
name = None
title = None
view_name = None
is_persistent = False
# The course field that indicates that this feature is enabled
feature_flag_field_name = None
@classmethod
def is_enabled(cls, course, settings, 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
settings (dict): a dict of configuration settings
user (User): the user interacting with the course
"""
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
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
......@@ -20,8 +20,26 @@ setup(
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",
"textbooks = lms.djangoapps.courseware.tabs:TextbookCourseViews",
"wiki = lms.djangoapps.course_wiki.tab:WikiCourseViewType",
# ORA 1 tabs (deprecated)
"peer_grading = lms.djangoapps.open_ended_grading.views:PeerGradingTab",
"staff_grading = lms.djangoapps.open_ended_grading.views:StaffGradingTab",
"open_ended = lms.djangoapps.open_ended_grading.views:OpenEndedGradingTab",
],
"openedx.user_partition_scheme": [
"random = openedx.core.djangoapps.user_api.partition_schemes:RandomUserPartitionScheme",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment