diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 5e732e64ca..2d684a7a84 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -6,7 +6,7 @@ import ddt import json import copy import mock -from mock import patch +from mock import Mock, patch import unittest from django.conf import settings @@ -19,7 +19,7 @@ from models.settings.course_metadata import CourseMetadata from models.settings.encoder import CourseSettingsEncoder from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.models.course_details import CourseDetails -from student.roles import CourseInstructorRole +from student.roles import CourseInstructorRole, CourseStaffRole from student.tests.factories import UserFactory from xmodule.fields import Date from xmodule.modulestore import ModuleStoreEnum @@ -28,7 +28,7 @@ from xmodule.modulestore.tests.factories import CourseFactory from xmodule.tabs import InvalidTabsException from milestones.tests.utils import MilestonesTestCaseMixin -from .utils import CourseTestCase +from .utils import CourseTestCase, AjaxEnabledTestClient def get_url(course_id, handler_name='settings_handler'): @@ -949,6 +949,23 @@ class CourseMetadataEditingTest(CourseTestCase): tab_list.append(self.notes_tab) self.assertEqual(tab_list, course.tabs) + @patch.dict(settings.FEATURES, {'ENABLE_EDXNOTES': True}) + @patch('xmodule.util.django.get_current_request') + def test_post_settings_with_staff_not_enrolled(self, mock_request): + """ + Tests that we can post advance settings when course staff is not enrolled. + """ + mock_request.return_value = Mock(META={'HTTP_HOST': 'localhost'}) + user = UserFactory.create(is_staff=True) + CourseStaffRole(self.course.id).add_users(user) + + client = AjaxEnabledTestClient() + client.login(username=user.username, password=user.password) + response = self.client.ajax_post(self.course_setting_url, { + 'advanced_modules': {"value": [""]} + }) + self.assertEqual(response.status_code, 200) + class CourseGraderUpdatesTest(CourseTestCase): """ diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py index 3bc8621bf1..38890c4e4c 100644 --- a/cms/djangoapps/contentstore/tests/test_utils.py +++ b/cms/djangoapps/contentstore/tests/test_utils.py @@ -79,7 +79,7 @@ class LMSLinksTestCase(TestCase): link = utils.get_lms_link_for_item(location, True) self.assertEquals( link, - "//preview/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us" + "//preview.localhost/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us" ) # now test with the course' location diff --git a/cms/envs/bok_choy.env.json b/cms/envs/bok_choy.env.json index ce0d95e8d3..44cd55ef73 100644 --- a/cms/envs/bok_choy.env.json +++ b/cms/envs/bok_choy.env.json @@ -73,7 +73,7 @@ "ENABLE_S3_GRADE_DOWNLOADS": true, "ENTRANCE_EXAMS": true, "MILESTONES_APP": true, - "PREVIEW_LMS_BASE": "localhost:8003", + "PREVIEW_LMS_BASE": "preview.localhost:8003", "ALLOW_ALL_ADVANCED_COMPONENTS": true, "ENABLE_CONTENT_LIBRARIES": true, "ENABLE_SPECIAL_EXAMS": true, diff --git a/cms/envs/test.py b/cms/envs/test.py index ce63bc4d90..554f82cb68 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -136,7 +136,8 @@ if os.environ.get('DISABLE_MIGRATIONS'): MIGRATION_MODULES = NoOpMigrationModules() LMS_BASE = "localhost:8000" -FEATURES['PREVIEW_LMS_BASE'] = "preview" +FEATURES['PREVIEW_LMS_BASE'] = "preview.localhost" + CACHES = { # This is the cache used for most things. Askbot will not work without a diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 178568dd8e..98306e7a60 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -61,7 +61,10 @@ from courseware.access_response import ( MobileAvailabilityError, VisibilityError, ) -from courseware.access_utils import adjust_start_date, check_start_date, debug, ACCESS_GRANTED, ACCESS_DENIED +from courseware.access_utils import ( + adjust_start_date, check_start_date, debug, ACCESS_GRANTED, ACCESS_DENIED, + in_preview_mode +) from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException from lms.djangoapps.ccx.models import CustomCourseForEdX @@ -135,6 +138,10 @@ def has_access(user, action, obj, course_key=None): if isinstance(course_key, CCXLocator): course_key = course_key.to_course_locator() + if in_preview_mode(): + if not bool(has_staff_access_to_preview_mode(user=user, obj=obj, course_key=course_key)): + return ACCESS_DENIED + # delegate the work to type-specific functions. # (start with more specific types, then get more general) if isinstance(obj, CourseDescriptor): @@ -172,6 +179,52 @@ def has_access(user, action, obj, course_key=None): # ================ Implementation helpers ================================ + +def has_staff_access_to_preview_mode(user, obj, course_key=None): + """ + Returns whether user has staff access to specified modules or not. + + Arguments: + + user: a Django user object. + + obj: The object to check access for. + + course_key: A course_key specifying which course this access is for. + + Returns an AccessResponse object. + """ + if course_key is None: + if isinstance(obj, CourseDescriptor) or isinstance(obj, CourseOverview): + course_key = obj.id + + elif isinstance(obj, ErrorDescriptor): + course_key = obj.location.course_key + + elif isinstance(obj, XModule): + course_key = obj.descriptor.course_key + + elif isinstance(obj, XBlock): + course_key = obj.location.course_key + + elif isinstance(obj, CCXLocator): + course_key = obj.to_course_locator() + + elif isinstance(obj, CourseKey): + course_key = obj + + elif isinstance(obj, UsageKey): + course_key = obj.course_key + + if course_key is None: + if GlobalStaff().has_user(user): + return ACCESS_GRANTED + else: + return ACCESS_DENIED + + return _has_access_to_course(user, 'staff', course_key=course_key) + + def _can_access_descriptor_with_start_date(user, descriptor, course_key): # pylint: disable=invalid-name """ Checks if a user has access to a descriptor based on its start date. diff --git a/lms/djangoapps/courseware/access_utils.py b/lms/djangoapps/courseware/access_utils.py index e0e96b5711..fd07a269f3 100644 --- a/lms/djangoapps/courseware/access_utils.py +++ b/lms/djangoapps/courseware/access_utils.py @@ -78,4 +78,5 @@ def in_preview_mode(): Returns whether the user is in preview mode or not. """ hostname = get_current_request_hostname() - return bool(hostname and settings.PREVIEW_DOMAIN in hostname.split('.')) + preview_lms_base = settings.FEATURES.get('PREVIEW_LMS_BASE', None) + return bool(preview_lms_base and hostname and hostname.split(':')[0] == preview_lms_base.split(':')[0]) diff --git a/lms/djangoapps/courseware/tests/test_access.py b/lms/djangoapps/courseware/tests/test_access.py index add8c738ee..de7da98124 100644 --- a/lms/djangoapps/courseware/tests/test_access.py +++ b/lms/djangoapps/courseware/tests/test_access.py @@ -17,6 +17,7 @@ from mock import Mock, patch from nose.plugins.attrib import attr from opaque_keys.edx.locations import SlashSeparatedCourseKey +from ccx.tests.factories import CcxFactory import courseware.access as access import courseware.access_response as access_response from courseware.masquerade import CourseMasquerade @@ -39,17 +40,21 @@ from student.tests.factories import ( CourseEnrollmentAllowedFactory, CourseEnrollmentFactory, ) + from xmodule.course_module import ( CATALOG_VISIBILITY_CATALOG_AND_ABOUT, CATALOG_VISIBILITY_ABOUT, CATALOG_VISIBILITY_NONE, ) -from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.error_module import ErrorDescriptor +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.django_utils import ( ModuleStoreTestCase, SharedModuleStoreTestCase, TEST_DATA_SPLIT_MODULESTORE ) +from xmodule.modulestore.xml import CourseLocationManager +from xmodule.tests import get_test_system from util.milestones_helpers import ( set_prerequisite_courses, @@ -157,23 +162,23 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes """ TOMORROW = datetime.datetime.now(pytz.utc) + datetime.timedelta(days=1) YESTERDAY = datetime.datetime.now(pytz.utc) - datetime.timedelta(days=1) + MODULESTORE = TEST_DATA_SPLIT_MODULESTORE def setUp(self): super(AccessTestCase, self).setUp() - course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') - self.course = course_key.make_usage_key('course', course_key.run) + self.course = CourseFactory.create(org='edX', course='toy', run='test_run') self.anonymous_user = AnonymousUserFactory() - self.beta_user = BetaTesterFactory(course_key=self.course.course_key) + self.beta_user = BetaTesterFactory(course_key=self.course.id) self.student = UserFactory() self.global_staff = UserFactory(is_staff=True) - self.course_staff = StaffFactory(course_key=self.course.course_key) - self.course_instructor = InstructorFactory(course_key=self.course.course_key) + self.course_staff = StaffFactory(course_key=self.course.id) + self.course_instructor = InstructorFactory(course_key=self.course.id) self.staff = GlobalStaffFactory() def verify_access(self, mock_unit, student_should_have_access, expected_error_type=None): """ Verify the expected result from _has_access_descriptor """ response = access._has_access_descriptor(self.anonymous_user, 'load', - mock_unit, course_key=self.course.course_key) + mock_unit, course_key=self.course.id) self.assertEqual(student_should_have_access, bool(response)) if expected_error_type is not None: @@ -181,55 +186,144 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes self.assertIsNotNone(response.to_json()['error_code']) self.assertTrue( - access._has_access_descriptor(self.course_staff, 'load', mock_unit, course_key=self.course.course_key) + access._has_access_descriptor(self.course_staff, 'load', mock_unit, course_key=self.course.id) ) + def test_has_staff_access_to_preview_mode(self): + """ + Tests users have right access to content in preview mode. + """ + course_key = self.course.id + usage_key = self.course.scope_ids.usage_id + chapter = ItemFactory.create(category="chapter", parent_location=self.course.location) + overview = CourseOverview.get_from_id(course_key) + test_system = get_test_system() + + ccx = CcxFactory(course_id=course_key) + ccx_locator = CCXLocator.from_course_locator(course_key, ccx.id) + + error_descriptor = ErrorDescriptor.from_xml( + u"ABC \N{SNOWMAN}", + test_system, + CourseLocationManager(course_key), + "error msg" + ) + # Enroll student to the course + CourseEnrollmentFactory(user=self.student, course_id=self.course.id) + + modules = [ + self.course, + overview, + chapter, + ccx_locator, + error_descriptor, + course_key, + usage_key, + ] + # Course key is not None + self.assertTrue( + bool(access.has_staff_access_to_preview_mode(self.global_staff, obj=self.course, course_key=course_key)) + ) + + for user in [self.global_staff, self.course_staff, self.course_instructor]: + for obj in modules: + self.assertTrue(bool(access.has_staff_access_to_preview_mode(user, obj=obj))) + self.assertFalse(bool(access.has_staff_access_to_preview_mode(self.student, obj=obj))) + + def test_student_has_access(self): + """ + Tests course student have right access to content w/o preview. + """ + course_key = self.course.id + chapter = ItemFactory.create(category="chapter", parent_location=self.course.location) + overview = CourseOverview.get_from_id(course_key) + + # Enroll student to the course + CourseEnrollmentFactory(user=self.student, course_id=self.course.id) + + modules = [ + self.course, + overview, + chapter, + ] + with patch('courseware.access.in_preview_mode') as mock_preview: + mock_preview.return_value = False + for obj in modules: + self.assertTrue(bool(access.has_access(self.student, 'load', obj, course_key=self.course.id))) + + with patch('courseware.access.in_preview_mode') as mock_preview: + mock_preview.return_value = True + for obj in modules: + self.assertFalse(bool(access.has_access(self.student, 'load', obj, course_key=self.course.id))) + + def test_string_has_staff_access_to_preview_mode(self): + """ + Tests different users has right access to string content in preview mode. + """ + self.assertTrue(bool(access.has_staff_access_to_preview_mode(self.global_staff, obj='global'))) + self.assertFalse(bool(access.has_staff_access_to_preview_mode(self.course_staff, obj='global'))) + self.assertFalse(bool(access.has_staff_access_to_preview_mode(self.course_instructor, obj='global'))) + self.assertFalse(bool(access.has_staff_access_to_preview_mode(self.student, obj='global'))) + + @patch('courseware.access.in_preview_mode', Mock(return_value=True)) + def test_has_access_with_preview_mode(self): + """ + Tests particular user's can access content via has_access in preview mode. + """ + self.assertTrue(bool(access.has_access(self.global_staff, 'staff', self.course, course_key=self.course.id))) + self.assertTrue(bool(access.has_access(self.course_staff, 'staff', self.course, course_key=self.course.id))) + self.assertTrue(bool(access.has_access( + self.course_instructor, 'staff', self.course, course_key=self.course.id + ))) + self.assertFalse(bool(access.has_access(self.student, 'staff', self.course, course_key=self.course.id))) + self.assertFalse(bool(access.has_access(self.student, 'load', self.course, course_key=self.course.id))) + def test_has_access_to_course(self): self.assertFalse(access._has_access_to_course( - None, 'staff', self.course.course_key + None, 'staff', self.course.id )) self.assertFalse(access._has_access_to_course( - self.anonymous_user, 'staff', self.course.course_key + self.anonymous_user, 'staff', self.course.id )) self.assertFalse(access._has_access_to_course( - self.anonymous_user, 'instructor', self.course.course_key + self.anonymous_user, 'instructor', self.course.id )) self.assertTrue(access._has_access_to_course( - self.global_staff, 'staff', self.course.course_key + self.global_staff, 'staff', self.course.id )) self.assertTrue(access._has_access_to_course( - self.global_staff, 'instructor', self.course.course_key + self.global_staff, 'instructor', self.course.id )) # A user has staff access if they are in the staff group self.assertTrue(access._has_access_to_course( - self.course_staff, 'staff', self.course.course_key + self.course_staff, 'staff', self.course.id )) self.assertFalse(access._has_access_to_course( - self.course_staff, 'instructor', self.course.course_key + self.course_staff, 'instructor', self.course.id )) # A user has staff and instructor access if they are in the instructor group self.assertTrue(access._has_access_to_course( - self.course_instructor, 'staff', self.course.course_key + self.course_instructor, 'staff', self.course.id )) self.assertTrue(access._has_access_to_course( - self.course_instructor, 'instructor', self.course.course_key + self.course_instructor, 'instructor', self.course.id )) # A user does not have staff or instructor access if they are # not in either the staff or the the instructor group self.assertFalse(access._has_access_to_course( - self.student, 'staff', self.course.course_key + self.student, 'staff', self.course.id )) self.assertFalse(access._has_access_to_course( - self.student, 'instructor', self.course.course_key + self.student, 'instructor', self.course.id )) self.assertFalse(access._has_access_to_course( - self.student, 'not_staff_or_instructor', self.course.course_key + self.student, 'not_staff_or_instructor', self.course.id )) def test__has_access_string(self): @@ -256,12 +350,12 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes (self.course_instructor, expected_instructor) ): self.assertEquals( - bool(access._has_access_error_desc(user, action, descriptor, self.course.course_key)), + bool(access._has_access_error_desc(user, action, descriptor, self.course.id)), expected_response ) with self.assertRaises(ValueError): - access._has_access_error_desc(self.course_instructor, 'not_load_or_staff', descriptor, self.course.course_key) + access._has_access_error_desc(self.course_instructor, 'not_load_or_staff', descriptor, self.course.id) def test__has_access_descriptor(self): # TODO: override DISABLE_START_DATES and test the start date branch of the method @@ -304,7 +398,7 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes mock_unit.visible_to_staff_only = False self.assertTrue(bool(access._has_access_descriptor( - self.beta_user, 'load', mock_unit, course_key=self.course.course_key))) + self.beta_user, 'load', mock_unit, course_key=self.course.id))) @ddt.data(None, YESTERDAY, TOMORROW) @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False}) diff --git a/lms/envs/aws.py b/lms/envs/aws.py index fac0fc2bed..7e01dd36f0 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -405,6 +405,13 @@ if 'DJFS' in AUTH_TOKENS and AUTH_TOKENS['DJFS'] is not None: ############### Module Store Items ########## HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS = ENV_TOKENS.get('HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS', {}) +# PREVIEW DOMAIN must be present in HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS for the preview to show draft changes +if 'PREVIEW_LMS_BASE' in FEATURES and FEATURES['PREVIEW_LMS_BASE'] != '': + PREVIEW_DOMAIN = FEATURES['PREVIEW_LMS_BASE'].split(':')[0] + # update dictionary with preview domain regex + HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS.update({ + PREVIEW_DOMAIN: 'draft-preferred' + }) ############### Mixed Related(Secure/Not-Secure) Items ########## LMS_SEGMENT_KEY = AUTH_TOKENS.get('SEGMENT_KEY') diff --git a/lms/envs/bok_choy.env.json b/lms/envs/bok_choy.env.json index e00a3dcaef..a745c4c615 100644 --- a/lms/envs/bok_choy.env.json +++ b/lms/envs/bok_choy.env.json @@ -81,7 +81,7 @@ "ENABLE_S3_GRADE_DOWNLOADS": true, "ENABLE_THIRD_PARTY_AUTH": true, "ENABLE_COMBINED_LOGIN_REGISTRATION": true, - "PREVIEW_LMS_BASE": "localhost:8003", + "PREVIEW_LMS_BASE": "preview.localhost:8003", "ALLOW_AUTOMATED_SIGNUPS": true, "AUTOMATIC_AUTH_FOR_TESTING": true, "MODE_CREATION_FOR_TESTING": true, diff --git a/lms/envs/common.py b/lms/envs/common.py index 4437ccdf2d..091db6934c 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2667,9 +2667,6 @@ PROFILE_IMAGE_SECRET_KEY = 'placeholder secret key' PROFILE_IMAGE_MAX_BYTES = 1024 * 1024 PROFILE_IMAGE_MIN_BYTES = 100 -# This is to check the domain in case of preview. -PREVIEW_DOMAIN = 'preview' - # Sets the maximum number of courses listed on the homepage # If set to None, all courses will be listed on the homepage HOMEPAGE_COURSE_MAX = None diff --git a/lms/envs/test.py b/lms/envs/test.py index cee2425db6..505d32958f 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -394,6 +394,14 @@ YOUTUBE_PORT = 8031 LTI_PORT = 8765 VIDEO_SOURCE_PORT = 8777 +FEATURES['PREVIEW_LMS_BASE'] = "preview.localhost" +############### Module Store Items ########## +PREVIEW_DOMAIN = FEATURES['PREVIEW_LMS_BASE'].split(':')[0] +HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS = { + PREVIEW_DOMAIN: 'draft-preferred' +} + + ################### Make tests faster #http://slacy.com/blog/2012/04/make-your-tests-faster-in-django-1-4/