Commit 94e1c423 by Andy Armstrong Committed by Diana Huang

Add extensible course view types for edX platform

parent 9008548c
......@@ -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_and_update_from_json(
is_valid, errors, test_model = CourseMetadata.validate_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_and_update_from_json(
is_valid, errors, test_model = CourseMetadata.validate_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_and_update_from_json(
is_valid, errors, test_model = CourseMetadata.validate_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_and_update_from_json(
is_valid, errors, test_model = CourseMetadata.validate_from_json(
self.course,
{
"edxnotes": {"value": "true"},
......@@ -788,8 +788,8 @@ class CourseMetadataEditingTest(CourseTestCase):
)
self.assertNotIn('edxnotes', test_model)
def test_validate_and_update_from_json_correct_inputs(self):
is_valid, errors, test_model = CourseMetadata.validate_and_update_from_json(
def test_validate_from_json_correct_inputs(self):
is_valid, errors, test_model = CourseMetadata.validate_from_json(
self.course,
{
"advertised_start": {"value": "start A"},
......@@ -802,18 +802,13 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertTrue(len(errors) == 0)
self.update_check(test_model)
# fresh fetch to ensure persistence
fresh = modulestore().get_course(self.course.id)
test_model = CourseMetadata.fetch(fresh)
self.update_check(test_model)
# Tab gets tested in test_advanced_settings_munge_tabs
self.assertIn('advanced_modules', test_model, 'Missing advanced_modules')
self.assertEqual(test_model['advanced_modules']['value'], ['combinedopenended'], 'advanced_module is not updated')
def test_validate_and_update_from_json_wrong_inputs(self):
def test_validate_from_json_wrong_inputs(self):
# input incorrectly formatted data
is_valid, errors, test_model = CourseMetadata.validate_and_update_from_json(
is_valid, errors, test_model = CourseMetadata.validate_from_json(
self.course,
{
"advertised_start": {"value": 1, "display_name": "Course Advertised Start Date", },
......@@ -824,7 +819,7 @@ class CourseMetadataEditingTest(CourseTestCase):
user=self.user
)
# Check valid results from validate_and_update_from_json
# Check valid results from validate_from_json
self.assertFalse(is_valid)
self.assertEqual(len(errors), 3)
self.assertFalse(test_model)
......@@ -947,23 +942,6 @@ class CourseMetadataEditingTest(CourseTestCase):
course = modulestore().get_course(self.course.id)
self.assertNotIn(EXTRA_TAB_PANELS.get("open_ended"), course.tabs)
@patch.dict(settings.FEATURES, {'ENABLE_EDXNOTES': True})
def test_course_settings_munge_tabs(self):
"""
Test that adding and removing specific course settings adds and removes tabs.
"""
self.assertNotIn(EXTRA_TAB_PANELS.get("edxnotes"), self.course.tabs)
self.client.ajax_post(self.course_setting_url, {
"edxnotes": {"value": True}
})
course = modulestore().get_course(self.course.id)
self.assertIn(EXTRA_TAB_PANELS.get("edxnotes"), course.tabs)
self.client.ajax_post(self.course_setting_url, {
"edxnotes": {"value": False}
})
course = modulestore().get_course(self.course.id)
self.assertNotIn(EXTRA_TAB_PANELS.get("edxnotes"), course.tabs)
class CourseGraderUpdatesTest(CourseTestCase):
"""
......
......@@ -108,61 +108,6 @@ class ExtraPanelTabTestCase(TestCase):
course.tabs = tabs
return course
def test_add_extra_panel_tab(self):
""" Tests if a tab can be added to a course tab list. """
for tab_type in utils.EXTRA_TAB_PANELS.keys():
tab = utils.EXTRA_TAB_PANELS.get(tab_type)
# test adding with changed = True
for tab_setup in ['', 'x', 'x,y,z']:
course = self.get_course_with_tabs(tab_setup)
expected_tabs = copy.copy(course.tabs)
expected_tabs.append(tab)
changed, actual_tabs = utils.add_extra_panel_tab(tab_type, course)
self.assertTrue(changed)
self.assertEqual(actual_tabs, expected_tabs)
# test adding with changed = False
tab_test_setup = [
[tab],
[tab, self.get_tab_type_dicts('x,y,z')],
[self.get_tab_type_dicts('x,y'), tab, self.get_tab_type_dicts('z')],
[self.get_tab_type_dicts('x,y,z'), tab]]
for tab_setup in tab_test_setup:
course = self.get_course_with_tabs(tab_setup)
expected_tabs = copy.copy(course.tabs)
changed, actual_tabs = utils.add_extra_panel_tab(tab_type, course)
self.assertFalse(changed)
self.assertEqual(actual_tabs, expected_tabs)
def test_remove_extra_panel_tab(self):
""" Tests if a tab can be removed from a course tab list. """
for tab_type in utils.EXTRA_TAB_PANELS.keys():
tab = utils.EXTRA_TAB_PANELS.get(tab_type)
# test removing with changed = True
tab_test_setup = [
[tab],
[tab, self.get_tab_type_dicts('x,y,z')],
[self.get_tab_type_dicts('x,y'), tab, self.get_tab_type_dicts('z')],
[self.get_tab_type_dicts('x,y,z'), tab]]
for tab_setup in tab_test_setup:
course = self.get_course_with_tabs(tab_setup)
expected_tabs = [t for t in course.tabs if t != utils.EXTRA_TAB_PANELS.get(tab_type)]
changed, actual_tabs = utils.remove_extra_panel_tab(tab_type, course)
self.assertTrue(changed)
self.assertEqual(actual_tabs, expected_tabs)
# test removing with changed = False
for tab_setup in ['', 'x', 'x,y,z']:
course = self.get_course_with_tabs(tab_setup)
expected_tabs = copy.copy(course.tabs)
changed, actual_tabs = utils.remove_extra_panel_tab(tab_type, course)
self.assertFalse(changed)
self.assertEqual(actual_tabs, expected_tabs)
class CourseImageTestCase(ModuleStoreTestCase):
"""Tests for course image URLs."""
......
......@@ -95,7 +95,7 @@ class CourseTestCase(ModuleStoreTestCase):
client = AjaxEnabledTestClient()
if authenticate:
client.login(username=nonstaff.username, password=password)
nonstaff.is_authenticated = True
nonstaff.is_authenticated = lambda: authenticate
return client, nonstaff
def populate_course(self, branching=2):
......
......@@ -3,7 +3,6 @@ Common utility functions useful throughout the contentstore
"""
# pylint: disable=no-member
import copy
import logging
import re
from datetime import datetime
......@@ -30,8 +29,7 @@ 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"}
EDXNOTES_PANEL = {"name": _("Notes"), "type": "edxnotes"}
EXTRA_TAB_PANELS = dict([(p['type'], p) for p in [OPEN_ENDED_PANEL, NOTES_PANEL, EDXNOTES_PANEL]])
EXTRA_TAB_PANELS = {p['type']: p for p in [OPEN_ENDED_PANEL, NOTES_PANEL]}
def add_instructor(course_key, requesting_user, new_instructor):
......@@ -287,46 +285,6 @@ def ancestor_has_staff_lock(xblock, parent_xblock=None):
return parent_xblock.visible_to_staff_only
def add_extra_panel_tab(tab_type, course):
"""
Used to add the panel tab to a course if it does not exist.
@param tab_type: A string representing the tab type.
@param course: A course object from the modulestore.
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
"""
# Copy course tabs
course_tabs = copy.copy(course.tabs)
changed = False
# Check to see if open ended panel is defined in the course
tab_panel = EXTRA_TAB_PANELS.get(tab_type)
if tab_panel not in course_tabs:
# Add panel to the tabs if it is not defined
course_tabs.append(tab_panel)
changed = True
return changed, course_tabs
def remove_extra_panel_tab(tab_type, course):
"""
Used to remove the panel tab from a course if it exists.
@param tab_type: A string representing the tab type.
@param course: A course object from the modulestore.
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
"""
# Copy course tabs
course_tabs = copy.copy(course.tabs)
changed = False
# Check to see if open ended panel is defined in the course
tab_panel = EXTRA_TAB_PANELS.get(tab_type)
if tab_panel in course_tabs:
# Add panel to the tabs if it is not defined
course_tabs = [ct for ct in course_tabs if ct != tab_panel]
changed = True
return changed, course_tabs
def reverse_url(handler_name, key_name=None, key_value=None, kwargs=None):
"""
Creates the URL for the given handler.
......
"""
Views related to operations on course objects
"""
import copy
from django.shortcuts import redirect
import json
import random
......@@ -22,12 +23,13 @@ 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
from xmodule.tabs import PDFTextbookTabs, CourseTab, CourseTabManager
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
......@@ -42,13 +44,12 @@ from contentstore.utils import (
add_instructor,
initialize_permissions,
get_lms_link_for_item,
add_extra_panel_tab,
remove_extra_panel_tab,
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
......@@ -993,37 +994,6 @@ def grading_handler(request, course_key_string, grader_index=None):
return JsonResponse()
# pylint: disable=invalid-name
def _add_tab(request, tab_type, course_module):
"""
Adds tab to the course.
"""
# Add tab to the course if needed
changed, new_tabs = add_extra_panel_tab(tab_type, course_module)
# If a tab has been added to the course, then send the
# metadata along to CourseMetadata.update_from_json
if changed:
course_module.tabs = new_tabs
request.json.update({'tabs': {'value': new_tabs}})
# Indicate that tabs should not be filtered out of
# the metadata
return True
return False
# pylint: disable=invalid-name
def _remove_tab(request, tab_type, course_module):
"""
Removes the tab from the course.
"""
changed, new_tabs = remove_extra_panel_tab(tab_type, course_module)
if changed:
course_module.tabs = new_tabs
request.json.update({'tabs': {'value': new_tabs}})
return True
return False
def is_advanced_component_present(request, advanced_components):
"""
Return True when one of `advanced_components` is present in the request.
......@@ -1047,13 +1017,9 @@ def is_field_value_true(request, field_list):
return any([request.json.get(field, {}).get('value') for field in field_list])
# pylint: disable=invalid-name
def _modify_tabs_to_components(request, course_module):
def _refresh_course_tabs(request, course_module):
"""
Automatically adds/removes tabs if user indicated that they want
respective modules enabled in the course
Return True when tab configuration has been modified.
Automatically adds/removes tabs if changes to the course require them.
"""
tab_component_map = {
# 'tab_type': (check_function, list_of_checked_components_or_values),
......@@ -1062,11 +1028,20 @@ def _modify_tabs_to_components(request, course_module):
'open_ended': (is_advanced_component_present, OPEN_ENDED_COMPONENT_TYPES),
# notes tab
'notes': (is_advanced_component_present, NOTE_COMPONENT_TYPES),
# student notes tab
'edxnotes': (is_field_value_true, ['edxnotes'])
}
tabs_changed = False
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:
tabs.append(CourseTab.from_json(tab_panel))
elif tab_panel in tabs:
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:
......@@ -1075,19 +1050,30 @@ def _modify_tabs_to_components(request, course_module):
# 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)
if tab_enabled:
# check passed, some of this component_types are present, adding tab
if _add_tab(request, tab_type, course_module):
# tab indeed was added, the change needs to propagate
tabs_changed = True
else:
# the tab should not be present (anymore)
if _remove_tab(request, tab_type, course_module):
# tab indeed was removed, the change needs to propagate
tabs_changed = True
# 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)
update_tab(course_tabs, tab_type, tab_enabled)
return tabs_changed
# Save the tabs into the course if they have been changed
if not 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
......@@ -1119,18 +1105,21 @@ def advanced_settings_handler(request, course_key_string):
return JsonResponse(CourseMetadata.fetch(course_module))
else:
try:
# do not process tabs unless they were modified according to course metadata
filter_tabs = not _modify_tabs_to_components(request, course_module)
# validate data formats and update
is_valid, errors, updated_data = CourseMetadata.validate_and_update_from_json(
# 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(
course_module,
request.json,
filter_tabs=filter_tabs,
user=request.user,
)
if is_valid:
# update the course tabs if required by any setting changes
_refresh_course_tabs(request, course_module)
# now update mongo
modulestore().update_item(course_module, request.user.id)
return JsonResponse(updated_data)
else:
return JsonResponseBadRequest(errors)
......
......@@ -61,10 +61,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_cms(
course_item,
settings,
):
for tab in CourseTabList.iterate_displayable(course_item, settings, 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)
......
......@@ -116,7 +116,7 @@ class ImportTestCase(CourseTestCase):
Check that course is imported successfully in existing course and users have their access roles
"""
# Create a non_staff user and add it to course staff only
__, nonstaff_user = self.create_non_staff_authed_user_client(authenticate=False)
__, nonstaff_user = self.create_non_staff_authed_user_client()
auth.add_users(self.user, CourseStaffRole(self.course.id), nonstaff_user)
course = self.store.get_course(self.course.id)
......
......@@ -150,11 +150,11 @@ class CourseMetadata(object):
return cls.update_from_dict(key_values, descriptor, user)
@classmethod
def validate_and_update_from_json(cls, descriptor, jsondict, user, filter_tabs=True):
def validate_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 in the database.
If all fields validate, go ahead and update those values on the object and return it.
If not, return the error objects list.
Returns:
......@@ -183,19 +183,19 @@ class CourseMetadata(object):
# If did validate, go ahead and update the metadata
if did_validate:
updated_data = cls.update_from_dict(key_values, descriptor, user)
updated_data = cls.update_from_dict(key_values, descriptor, user, save=False)
return did_validate, errors, updated_data
@classmethod
def update_from_dict(cls, key_values, descriptor, user):
def update_from_dict(cls, key_values, descriptor, user, save=True):
"""
Update metadata descriptor in modulestore from key_values.
Update metadata descriptor from key_values. Saves to modulestore if save is true.
"""
for key, value in key_values.iteritems():
setattr(descriptor, key, value)
if len(key_values):
if save and len(key_values):
modulestore().update_item(descriptor, user.id)
return cls.fetch(descriptor)
......@@ -1171,6 +1171,8 @@ class CourseEnrollment(models.Model):
`course_id` is our usual course_id string (e.g. "edX/Test101/2013_Fall)
"""
if not user.is_authenticated():
return False
try:
record = CourseEnrollment.objects.get(user=user, course_id=course_key)
return record.is_active
......
......@@ -64,15 +64,6 @@ def get_current_ccx():
return _CCX_CONTEXT.ccx
def get_current_request():
"""
Return the active request, so that we can get context information in places
where it is limited, like in the tabs.
"""
request = _CCX_CONTEXT.request
return request
def get_override_for_ccx(ccx, block, name, default=None):
"""
Gets the value of the overridden field for the `ccx`. `block` and `name`
......
"""
Registers the CCX feature for the edX platform.
"""
from django.utils.translation import ugettext as _
from openedx.core.lib.plugins.api import CourseViewType
from student.roles import CourseCcxCoachRole
class CcxCourseViewType(CourseViewType):
"""
The representation of the CCX course view type.
"""
name = "ccx_coach"
title = _("CCX Coach")
view_name = "ccx_coach_dashboard"
is_persistent = False
@classmethod
def is_enabled(cls, course, settings, user=None):
"""
Returns true if CCX has been enabled and the specified user is a coach
"""
if not user:
return True
if not settings.FEATURES.get('CUSTOM_COURSES_EDX', False) or not course.enable_ccx:
return False
role = CourseCcxCoachRole(course.id)
return role.has_user(user)
......@@ -3,44 +3,51 @@ This module is essentially a broker to xmodule/tabs.py -- it was originally intr
perform some LMS-specific tab display gymnastics for the Entrance Exams feature
"""
from django.conf import settings
from django.test.client import RequestFactory
from django.utils.translation import ugettext as _
from courseware.access import has_access
from courseware.entrance_exams import user_must_complete_entrance_exam
from student.models import CourseEnrollment, EntranceExamConfiguration
from xmodule.tabs import CourseTabList
from xmodule.tabs import CourseTabList, CourseViewTab, CourseTabManager
from util import milestones_helpers
def get_course_tab_list(course, user):
def get_course_tab_list(request, course):
"""
Retrieves the course tab list from xmodule.tabs and manipulates the set as necessary
"""
user_is_enrolled = user.is_authenticated() and CourseEnrollment.is_enrolled(user, course.id)
xmodule_tab_list = CourseTabList.iterate_displayable(
course,
settings,
user.is_authenticated(),
has_access(user, 'staff', course, course.id),
user_is_enrolled
)
user = request.user
xmodule_tab_list = CourseTabList.iterate_displayable(course, settings, user=user)
# Now that we've loaded the tabs for this course, perform the Entrance Exam work
# If the user has to take an entrance exam, we'll need to hide away all of the tabs
# except for the Courseware and Instructor tabs (latter is only viewed if applicable)
# We don't have access to the true request object in this context, but we can use a mock
request = RequestFactory().request()
request.user = user
course_tab_list = []
for tab in xmodule_tab_list:
if user_must_complete_entrance_exam(request, user, course):
# Hide all of the tabs except for 'Courseware' and 'Instructor'
# Hide all of the tabs except for 'Courseware'
# Rename 'Courseware' tab to 'Entrance Exam'
if tab.type not in ['courseware', 'instructor']:
if tab.type is not 'courseware':
continue
if tab.type == 'courseware':
tab.name = _("Entrance Exam")
tab.name = _("Entrance Exam")
course_tab_list.append(tab)
# Add in any dynamic tabs, i.e. those that are not persisted
course_tab_list += _get_dynamic_tabs(course, user)
return course_tab_list
def _get_dynamic_tabs(course, user):
"""
Returns the dynamic tab types for the current user.
Note: dynamic tabs are those that are not persisted in the course, but are
instead added dynamically based upon the user's role.
"""
dynamic_tabs = list()
for tab_type in CourseTabManager.get_tab_types().values():
if not getattr(tab_type, "is_persistent", True):
tab = CourseViewTab(tab_type)
if tab.is_enabled(course, settings, user=user):
dynamic_tabs.append(tab)
dynamic_tabs.sort(key=lambda dynamic_tab: dynamic_tab.name)
return dynamic_tabs
......@@ -54,13 +54,15 @@ class StaticTabDateTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
self.assertIn("OOGIE BLOOGIE", resp.content)
def test_invalid_course_key(self):
request = get_request_for_user(UserFactory.create())
self.setup_user()
request = get_request_for_user(self.user)
with self.assertRaises(Http404):
static_tab(request, course_id='edX/toy', tab_slug='new_tab')
def test_get_static_tab_contents(self):
self.setup_user()
course = get_course_by_id(self.toy_course_key)
request = get_request_for_user(UserFactory.create())
request = get_request_for_user(self.user)
tab = tabs.CourseTabList.get_tab_by_slug(course.tabs, 'resources')
# Test render works okay
......@@ -162,6 +164,7 @@ class EntranceExamsTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
'description': 'Testing Courseware Tabs'
}
self.user.is_staff = False
request = get_request_for_user(self.user)
self.course.entrance_exam_enabled = True
self.course.entrance_exam_id = unicode(entrance_exam.location)
milestone = milestones_helpers.add_milestone(milestone)
......@@ -176,7 +179,7 @@ class EntranceExamsTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
self.relationship_types['FULFILLS'],
milestone
)
course_tab_list = get_course_tab_list(self.course, self.user)
course_tab_list = get_course_tab_list(request, self.course)
self.assertEqual(len(course_tab_list), 1)
self.assertEqual(course_tab_list[0]['tab_id'], 'courseware')
self.assertEqual(course_tab_list[0]['name'], 'Entrance Exam')
......@@ -201,7 +204,8 @@ class EntranceExamsTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
# log in again as student
self.client.logout()
self.login(self.email, self.password)
course_tab_list = get_course_tab_list(self.course, self.user)
request = get_request_for_user(self.user)
course_tab_list = get_course_tab_list(request, self.course)
self.assertEqual(len(course_tab_list), 5)
def test_course_tabs_list_for_staff_members(self):
......@@ -213,8 +217,8 @@ class EntranceExamsTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
self.client.logout()
staff_user = StaffFactory(course_key=self.course.id)
self.client.login(username=staff_user.username, password='test')
course_tab_list = get_course_tab_list(self.course, staff_user)
request = get_request_for_user(staff_user)
course_tab_list = get_course_tab_list(request, self.course)
self.assertEqual(len(course_tab_list), 5)
......@@ -256,8 +260,8 @@ class TextBookTabsTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
Test that all textbooks tab links generating correctly.
"""
type_to_reverse_name = {'textbook': 'book', 'pdftextbook': 'pdf_book', 'htmltextbook': 'html_book'}
course_tab_list = get_course_tab_list(self.course, self.user)
request = get_request_for_user(self.user)
course_tab_list = get_course_tab_list(request, self.course)
num_of_textbooks_found = 0
for tab in course_tab_list:
# Verify links of all textbook type tabs.
......
"""
Registers the "edX Notes" feature for the edX platform.
"""
from django.utils.translation import ugettext as _
from openedx.core.lib.plugins.api import CourseViewType
class EdxNotesCourseViewType(CourseViewType):
"""
The representation of the edX Notes course view type.
"""
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
"""Returns true if the edX Notes feature 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
"""
return course.edxnotes
......@@ -6,6 +6,7 @@ import jwt
from mock import patch, MagicMock
from unittest import skipUnless
from datetime import datetime
from edxmako.shortcuts import render_to_string
from edxnotes import helpers
from edxnotes.decorators import edxnotes
......@@ -13,15 +14,17 @@ from edxnotes.exceptions import EdxNotesParseError, EdxNotesServiceUnavailable
from django.conf import settings
from django.core.urlresolvers import reverse
from django.core.exceptions import ImproperlyConfigured
from django.test.client import RequestFactory
from oauth2_provider.tests.factories import ClientFactory
from provider.oauth2.models import Client
from xmodule.tabs import EdxNotesTab
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
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
......@@ -29,7 +32,7 @@ def enable_edxnotes_for_the_course(course, user_id):
"""
Enable EdxNotes for the course.
"""
course.tabs.append(EdxNotesTab())
course.tabs.append(CourseTab.from_json({"type": "edxnotes", "name": "Notes"}))
modulestore().update_item(course, user_id)
......@@ -808,6 +811,21 @@ class EdxNotesViewsTest(ModuleStoreTestCase):
field_data_cache = FieldDataCache([self.course], self.course.id, self.user)
return get_module_for_descriptor(self.user, MagicMock(), self.course, field_data_cache, self.course.id)
def test_edxnotes_tab(self):
"""
Tests that edxnotes tab is shown only when the feature is enabled.
"""
def has_notes_tab(user, course):
"""Returns true if the "Notes" tab is shown."""
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
self.assertFalse(has_notes_tab(self.user, self.course))
enable_edxnotes_for_the_course(self.course, self.user.id)
self.assertTrue(has_notes_tab(self.user, self.course))
# pylint: disable=unused-argument
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": True})
@patch("edxnotes.views.get_notes", return_value=[])
......
......@@ -6,7 +6,11 @@ from mock import patch
from django.conf import settings
from django.core.urlresolvers import reverse
from django.test.client import RequestFactory
from django.test.utils import override_settings
from courseware.tabs import get_course_tab_list
from courseware.tests.factories import UserFactory
from courseware.tests.helpers import LoginEnrollmentTestCase
from student.tests.factories import AdminFactory, UserFactory
......@@ -56,6 +60,21 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase):
return 'Demographic data is now available in <a href="http://example.com/courses/{}" ' \
'target="_blank">Example</a>.'.format(unicode(self.course.id))
def test_instructor_tab(self):
"""
Verify that the instructor tab appears for staff only.
"""
def has_instructor_tab(user, course):
"""Returns true if the "Instructor" tab is shown."""
request = RequestFactory().request()
request.user = user
tabs = get_course_tab_list(request, course)
return len([tab for tab in tabs if tab.name == 'Instructor']) == 1
self.assertTrue(has_instructor_tab(self.instructor, self.course))
student = UserFactory.create()
self.assertFalse(has_instructor_tab(student, self.course))
def test_default_currency_in_the_html_response(self):
"""
Test that checks the default currency_symbol ($) in the response
......
......@@ -38,15 +38,33 @@ 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 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
from opaque_keys.edx.locations import SlashSeparatedCourseKey
log = logging.getLogger(__name__)
class InstructorDashboardViewType(CourseViewType):
"""
Defines the Instructor Dashboard view type that is shown as a course tab.
"""
name = "instructor"
title = _('Instructor')
view_name = "instructor_dashboard"
is_persistent = False
@classmethod
def is_enabled(cls, course, settings, user=None): # pylint: disable=unused-argument,redefined-outer-name
"""
Returns true if the specified user has staff access.
"""
return user and has_access(user, 'staff', course, course.id)
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def instructor_dashboard_2(request, course_id):
......
......@@ -54,7 +54,7 @@ def url_class(is_active):
<nav class="${active_page} wrapper-course-material" aria-label="${_('Course Material')}">
<div class="course-material">
<ol class="course-tabs">
% for tab in get_course_tab_list(course, user):
% for tab in get_course_tab_list(request, course):
<%
tab_is_active = (tab.tab_id == active_page) or (tab.tab_id == default_tab)
tab_image = notification_image_for_tab(tab, user, course)
......
......@@ -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.can_display(course, settings, True, True, True)) else None
discussion_link = discussion_tab.link_func(course, reverse) if (discussion_tab and discussion_tab.is_enabled(course, settings, user=user)) else None
%>
% if discussion_link:
......
"""
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):
"""
Base Exception for when an error was found regarding features.
"""
pass
class PluginManager(object):
"""
Base class that manages plugins to the edX platform.
"""
@classmethod
def get_available_plugins(cls):
"""
Returns a dict of all the plugins that have been made available through the platform.
"""
# Note: we're creating the extension manager lazily to ensure that the Python path
# has been correctly set up. Trying to create this statically will fail, unfortunately.
if not hasattr(cls, "_plugins"):
plugins = {}
extension_manager = ExtensionManager(namespace=cls.NAMESPACE) # pylint: disable=no-member
for plugin_name in extension_manager.names():
plugins[plugin_name] = extension_manager[plugin_name].plugin
cls._plugins = plugins
return cls._plugins
@classmethod
def get_plugin(cls, name):
"""
Returns the plugin with the given name.
"""
plugins = cls.get_available_plugins()
if name not in plugins:
raise PluginError("No such plugin {name} for entry point {namespace}".format(
name=name,
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
"""
Tests for the plugin API
"""
from django.test import TestCase
from ..api import CourseViewTypeManager, PluginError
class TestPluginApi(TestCase):
"""
Unit tests for the plugin API
"""
def test_get_plugin(self):
"""
Verify that get_plugin works as expected.
"""
course_view_type = CourseViewTypeManager.get_plugin("instructor")
self.assertEqual(course_view_type.title, "Instructor")
with self.assertRaises(PluginError):
CourseViewTypeManager.get_plugin("no_such_type")
......@@ -6,21 +6,26 @@ from setuptools import setup
setup(
name="Open edX",
version="0.2",
install_requires=['distribute'],
version="0.3",
install_requires=["distribute"],
requires=[],
# NOTE: These are not the names we should be installing. This tree should
# be reorganized to be a more conventional Python tree.
packages=[
"openedx.core.djangoapps.user_api",
"openedx.core.djangoapps.course_groups",
"openedx.core.djangoapps.user_api",
"lms",
"cms",
],
entry_points={
'openedx.user_partition_scheme': [
'random = openedx.core.djangoapps.user_api.partition_schemes:RandomUserPartitionScheme',
'cohort = openedx.core.djangoapps.course_groups.partition_scheme:CohortPartitionScheme',
"openedx.course_view_type": [
"ccx = lms.djangoapps.ccx.plugins:CcxCourseViewType",
"edxnotes = lms.djangoapps.edxnotes.plugins:EdxNotesCourseViewType",
"instructor = lms.djangoapps.instructor.views.instructor_dashboard:InstructorDashboardViewType",
],
"openedx.user_partition_scheme": [
"random = openedx.core.djangoapps.user_api.partition_schemes:RandomUserPartitionScheme",
"cohort = openedx.core.djangoapps.course_groups.partition_scheme:CohortPartitionScheme",
],
}
)
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