diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index e78c45c2ab..0eb74623da 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -83,6 +83,10 @@ class CourseMetadata(object): if not settings.FEATURES.get('ENABLE_VIDEO_BUMPER'): filtered_list.append('video_bumper') + # Do not show enable_ccx if feature is not enabled. + if not settings.FEATURES.get('CUSTOM_COURSES_EDX'): + filtered_list.append('enable_ccx') + return filtered_list @classmethod diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index d626f085d2..388b74d98f 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -386,6 +386,21 @@ class CourseFields(object): ), scope=Scope.settings ) + enable_ccx = Boolean( + # Translators: Custom Courses for edX (CCX) is an edX feature for re-using course content. CCX Coach is + # a role created by a course Instructor to enable a person (the "Coach") to manage the custom course for + # his students. + display_name=_("Enable CCX"), + # Translators: Custom Courses for edX (CCX) is an edX feature for re-using course content. CCX Coach is + # a role created by a course Instructor to enable a person (the "Coach") to manage the custom course for + # his students. + help=_( + "Allow course instructors to assign CCX Coach roles, and allow coaches to manage Custom Courses on edX." + " When false, Custom Courses cannot be created, but existing Custom Courses will be preserved." + ), + default=False, + scope=Scope.settings + ) allow_anonymous = Boolean( display_name=_("Allow Anonymous Discussion Posts"), help=_("Enter true or false. If true, students can create discussion posts that are anonymous to all users."), diff --git a/common/lib/xmodule/xmodule/tabs.py b/common/lib/xmodule/xmodule/tabs.py index a28432918a..1dd6b72eba 100644 --- a/common/lib/xmodule/xmodule/tabs.py +++ b/common/lib/xmodule/xmodule/tabs.py @@ -758,7 +758,7 @@ class CcxCoachTab(CourseTab): the user is one. """ user_is_coach = False - if settings.FEATURES.get('CUSTOM_COURSES_EDX', False): + if settings.FEATURES.get('CUSTOM_COURSES_EDX', False) and course.enable_ccx: from opaque_keys.edx.locations import SlashSeparatedCourseKey from student.roles import CourseCcxCoachRole # pylint: disable=import-error from ccx.overrides import get_current_request # pylint: disable=import-error diff --git a/lms/djangoapps/ccx/tests/test_views.py b/lms/djangoapps/ccx/tests/test_views.py index bf61c0f84c..28679a8a04 100644 --- a/lms/djangoapps/ccx/tests/test_views.py +++ b/lms/djangoapps/ccx/tests/test_views.py @@ -5,7 +5,9 @@ import datetime import json import re import pytz -from mock import patch +import ddt +import unittest +from mock import patch, MagicMock from nose.plugins.attrib import attr from capa.tests.response_xml_factory import StringResponseXMLFactory @@ -14,6 +16,7 @@ from courseware.tests.factories import StudentModuleFactory # pylint: disable=i from courseware.tests.helpers import LoginEnrollmentTestCase # pylint: disable=import-error from django.core.urlresolvers import reverse from django.test.utils import override_settings +from django.test import RequestFactory from edxmako.shortcuts import render_to_response # pylint: disable=import-error from student.roles import CourseCcxCoachRole # pylint: disable=import-error from student.tests.factories import ( # pylint: disable=import-error @@ -22,12 +25,14 @@ from student.tests.factories import ( # pylint: disable=import-error UserFactory, ) +from opaque_keys.edx.locations import SlashSeparatedCourseKey from xmodule.x_module import XModuleMixin from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import ( CourseFactory, ItemFactory, ) +import xmodule.tabs as tabs from ..models import ( CustomCourseForEdX, CcxMembership, @@ -56,6 +61,17 @@ def intercept_renderer(path, context): return response +def ccx_dummy_request(): + """ + Returns dummy request object for CCX coach tab test + """ + factory = RequestFactory() + request = factory.get('ccx_coach_dashboard') + request.user = MagicMock() + + return request + + @attr('shard_1') class TestCoachDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): """ @@ -756,6 +772,48 @@ class TestSwitchActiveCCX(ModuleStoreTestCase, LoginEnrollmentTestCase): self.verify_active_ccx(self.client) +@ddt.ddt +class CCXCoachTabTestCase(unittest.TestCase): + """ + 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 = {} + + def check_ccx_tab(self): + """Helper function for verifying the ccx tab.""" + tab = tabs.CcxCoachTab({'type': tabs.CcxCoachTab.type, 'name': 'CCX Coach'}) + return tab + + @ddt.data( + (True, True, True), + (True, False, False), + (False, True, False), + (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 + ) + ) + + def flatten(seq): """ For [[1, 2], [3, 4]] returns [1, 2, 3, 4]. Does not recurse. diff --git a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py index 4182a9576c..e53e41535b 100644 --- a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py +++ b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py @@ -1,6 +1,7 @@ """ Unit tests for instructor_dashboard.py. """ +import ddt from mock import patch from django.conf import settings @@ -16,6 +17,7 @@ from course_modes.models import CourseMode from student.roles import CourseFinanceAdminRole +@ddt.ddt class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Tests for the instructor dashboard (not legacy). @@ -179,3 +181,29 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): total_amount = single_purchase_total + bulk_purchase_total response = self.client.get(self.url) self.assertIn('{currency}{amount}'.format(currency='$', amount=total_amount), response.content) + + @ddt.data( + (True, True, True), + (True, False, False), + (True, None, False), + (False, True, False), + (False, False, False), + (False, None, False), + ) + @ddt.unpack + def test_ccx_coaches_option_on_admin_list_management_instructor( + self, ccx_feature_flag, enable_ccx, expected_result + ): + """ + Test whether the "CCX Coaches" option is visible or hidden depending on the value of course.enable_ccx. + """ + with patch.dict(settings.FEATURES, {'CUSTOM_COURSES_EDX': ccx_feature_flag}): + self.course.enable_ccx = enable_ccx + self.store.update_item(self.course, self.instructor.id) + + response = self.client.get(self.url) + + self.assertEquals( + expected_result, + 'CCX Coaches are able to create their own Custom Courses based on this course' in response.content + ) diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 103b137652..ab93882d3e 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -324,10 +324,12 @@ def _section_course_info(course, access): def _section_membership(course, access): """ Provide data for the corresponding dashboard section """ course_key = course.id + ccx_enabled = settings.FEATURES.get('CUSTOM_COURSES_EDX', False) and course.enable_ccx section_data = { 'section_key': 'membership', 'section_display_name': _('Membership'), 'access': access, + 'ccx_is_enabled': ccx_enabled, 'enroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': unicode(course_key)}), 'unenroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': unicode(course_key)}), 'upload_student_csv_button_url': reverse('register_and_enroll_students', kwargs={'course_id': unicode(course_key)}), diff --git a/lms/templates/instructor/instructor_dashboard_2/membership.html b/lms/templates/instructor/instructor_dashboard_2/membership.html index 13e9c6388c..dfd983426a 100644 --- a/lms/templates/instructor/instructor_dashboard_2/membership.html +++ b/lms/templates/instructor/instructor_dashboard_2/membership.html @@ -243,8 +243,8 @@ data-add-button-label="${_("Add Community TA")}" > %endif - - %if section_data['access']['instructor'] and settings.FEATURES.get('CUSTOM_COURSES_EDX', False): + + %if section_data['access']['instructor'] and section_data['ccx_is_enabled']: