diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index ad253e1506..5859205317 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -50,7 +50,9 @@ class CourseMetadata(object): 'is_time_limited', 'is_practice_exam', 'exam_review_rules', - 'self_paced' + 'self_paced', + 'chrome', + 'default_tab', ] @classmethod diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 35607174d3..93e1481417 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -752,6 +752,17 @@ class CourseFields(object): scope=Scope.settings ) + bypass_home = Boolean( + display_name=_("Bypass Course Home"), + help=_( + "Bypass the course home tab when students arrive from the dashboard, " + "sending them directly to course content." + ), + default=False, + scope=Scope.settings, + deprecated=True + ) + enable_subsection_gating = Boolean( display_name=_("Enable Subsection Prerequisites"), help=_( diff --git a/common/lib/xmodule/xmodule/tests/test_course_module.py b/common/lib/xmodule/xmodule/tests/test_course_module.py index ffd63fed5b..1ccb50c0fa 100644 --- a/common/lib/xmodule/xmodule/tests/test_course_module.py +++ b/common/lib/xmodule/xmodule/tests/test_course_module.py @@ -365,6 +365,16 @@ class SelfPacedTestCase(unittest.TestCase): self.assertFalse(self.course.self_paced) +class BypassHomeTestCase(unittest.TestCase): + """Tests for setting which allows course home to be bypassed.""" + def setUp(self): + super(BypassHomeTestCase, self).setUp() + self.course = get_dummy_course('2012-12-02T12:00') + + def test_default(self): + self.assertFalse(self.course.bypass_home) + + class CourseDescriptorTestCase(unittest.TestCase): """ Tests for a select few functions from CourseDescriptor. diff --git a/common/test/acceptance/pages/studio/settings_advanced.py b/common/test/acceptance/pages/studio/settings_advanced.py index 10b3939c08..e209207189 100644 --- a/common/test/acceptance/pages/studio/settings_advanced.py +++ b/common/test/acceptance/pages/studio/settings_advanced.py @@ -184,9 +184,7 @@ class AdvancedSettingsPage(CoursePage): 'display_coursenumber', 'display_organization', 'catalog_visibility', - 'chrome', 'days_early_for_beta', - 'default_tab', 'disable_progress_graph', 'discussion_blackouts', 'discussion_sort_alpha', diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py index b5347d8501..4d9c1b9dc7 100644 --- a/lms/djangoapps/courseware/date_summary.py +++ b/lms/djangoapps/courseware/date_summary.py @@ -182,7 +182,11 @@ class CourseEndDate(DateSummary): @property def description(self): if datetime.now(pytz.UTC) <= self.date: - return _('To earn a certificate, you must complete all requirements before this date.') + mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) + if is_active and CourseMode.is_eligible_for_certificate(mode): + return _('To earn a certificate, you must complete all requirements before this date.') + else: + return _('After this date, course content will be archived.') return _('This course is archived, which means you can review course content but it is no longer active.') @property diff --git a/lms/djangoapps/courseware/tests/test_date_summary.py b/lms/djangoapps/courseware/tests/test_date_summary.py index 1a6f4bb60e..8682749c70 100644 --- a/lms/djangoapps/courseware/tests/test_date_summary.py +++ b/lms/djangoapps/courseware/tests/test_date_summary.py @@ -173,7 +173,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ## CourseEndDate - def test_course_end_date_during_course(self): + def test_course_end_date_for_certificate_eligible_mode(self): self.setup_course_and_user(days_till_start=-1) block = CourseEndDate(self.course, self.user) self.assertEqual( @@ -181,6 +181,14 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): 'To earn a certificate, you must complete all requirements before this date.' ) + def test_course_end_date_for_non_certificate_eligible_mode(self): + self.setup_course_and_user(days_till_start=-1, enrollment_mode=CourseMode.AUDIT) + block = CourseEndDate(self.course, self.user) + self.assertEqual( + block.description, + 'After this date, course content will be archived.' + ) + def test_course_end_date_after_course(self): self.setup_course_and_user(days_till_start=-2, days_till_end=-1) block = CourseEndDate(self.course, self.user) diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index cae1b67c95..f8221cc247 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -652,6 +652,44 @@ class ViewsTestCase(ModuleStoreTestCase): response = self.client.get(url) self.assertRedirects(response, reverse('signin_user') + '?next=' + url) + def test_bypass_course_info(self): + course_id = unicode(self.course_key) + request = self.request_factory.get( + reverse('info', args=[course_id]) + ) + + # Middleware is not supported by the request factory. Simulate a + # logged-in user by setting request.user manually. + request.user = self.user + mako_middleware_process_request(request) + + self.assertFalse(self.course.bypass_home) + + self.assertIsNone(request.META.get('HTTP_REFERER')) # pylint: disable=no-member + response = views.course_info(request, course_id) + self.assertEqual(response.status_code, 200) + + request.META['HTTP_REFERER'] = reverse('dashboard') # pylint: disable=no-member + response = views.course_info(request, course_id) + self.assertEqual(response.status_code, 200) + + self.course.bypass_home = True + self.store.update_item(self.course, self.user.id) # pylint: disable=no-member + self.assertTrue(self.course.bypass_home) + + response = views.course_info(request, course_id) + + # assertRedirects would be great here, but it forces redirections to be absolute URLs. + self.assertEqual(response.status_code, 302) + self.assertEqual( + response.url, + reverse('courseware', args=[course_id]) + ) + + request.META['HTTP_REFERER'] = 'foo' # pylint: disable=no-member + response = views.course_info(request, course_id) + self.assertEqual(response.status_code, 200) + @attr('shard_1') # setting TIME_ZONE_DISPLAYED_FOR_DEADLINES explicitly diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index ee6df9b384..6eb261f7a3 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -694,6 +694,10 @@ def course_info(request, course_id): if request.user.is_authenticated() and survey.utils.must_answer_survey(course, user): return redirect(reverse('course_survey', args=[unicode(course.id)])) + is_from_dashboard = reverse('dashboard') in request.META.get('HTTP_REFERER', []) + if course.bypass_home and is_from_dashboard: + return redirect(reverse('courseware', args=[course_id])) + studio_url = get_studio_url(course, 'course_info') # link to where the student should go to enroll in the course: diff --git a/lms/djangoapps/django_comment_client/tests/test_utils.py b/lms/djangoapps/django_comment_client/tests/test_utils.py index 024e459576..677144e8da 100644 --- a/lms/djangoapps/django_comment_client/tests/test_utils.py +++ b/lms/djangoapps/django_comment_client/tests/test_utils.py @@ -666,6 +666,7 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase): self.create_discussion("Chapter 2 / Section 1 / Subsection 2", "Discussion", start=later) self.create_discussion("Chapter 3 / Section 1", "Discussion", start=later) + self.assertFalse(self.course.self_paced) self.assert_category_map_equals( { "entries": {}, @@ -696,7 +697,102 @@ class CategoryMapTestCase(CategoryMapTestMixin, ModuleStoreTestCase): "children": ["Chapter 1", "Chapter 2"] } ) - self.maxDiff = None + + def test_self_paced_start_date_filter(self): + self.course.self_paced = True + self.course.save() + + now = datetime.datetime.now() + later = datetime.datetime.max + self.create_discussion("Chapter 1", "Discussion 1", start=now) + self.create_discussion("Chapter 1", "Discussion 2", start=later) + self.create_discussion("Chapter 2", "Discussion", start=now) + self.create_discussion("Chapter 2 / Section 1 / Subsection 1", "Discussion", start=later) + self.create_discussion("Chapter 2 / Section 1 / Subsection 2", "Discussion", start=later) + self.create_discussion("Chapter 3 / Section 1", "Discussion", start=later) + + self.assertTrue(self.course.self_paced) + self.assert_category_map_equals( + { + "entries": {}, + "subcategories": { + "Chapter 1": { + "entries": { + "Discussion 1": { + "id": "discussion1", + "sort_key": None, + "is_cohorted": False, + }, + "Discussion 2": { + "id": "discussion2", + "sort_key": None, + "is_cohorted": False, + } + }, + "subcategories": {}, + "children": ["Discussion 1", "Discussion 2"] + }, + "Chapter 2": { + "entries": { + "Discussion": { + "id": "discussion3", + "sort_key": None, + "is_cohorted": False, + } + }, + "subcategories": { + "Section 1": { + "entries": {}, + "subcategories": { + "Subsection 1": { + "entries": { + "Discussion": { + "id": "discussion4", + "sort_key": None, + "is_cohorted": False, + } + }, + "subcategories": {}, + "children": ["Discussion"] + }, + "Subsection 2": { + "entries": { + "Discussion": { + "id": "discussion5", + "sort_key": None, + "is_cohorted": False, + } + }, + "subcategories": {}, + "children": ["Discussion"] + } + }, + "children": ["Subsection 1", "Subsection 2"] + } + }, + "children": ["Discussion", "Section 1"] + }, + "Chapter 3": { + "entries": {}, + "subcategories": { + "Section 1": { + "entries": { + "Discussion": { + "id": "discussion6", + "sort_key": None, + "is_cohorted": False, + } + }, + "subcategories": {}, + "children": ["Discussion"] + } + }, + "children": ["Section 1"] + } + }, + "children": ["Chapter 1", "Chapter 2", "Chapter 3"] + } + ) def test_sort_inline_explicit(self): self.create_discussion("Chapter", "Discussion 1", sort_key="D") diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index d079b5be41..21730c0bcb 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -198,7 +198,7 @@ def get_discussion_id_map(course, user): return dict(map(get_discussion_id_map_entry, get_accessible_discussion_modules(course, user))) -def _filter_unstarted_categories(category_map): +def _filter_unstarted_categories(category_map, course): """ Returns a subset of categories from the provided map which have not yet met the start date Includes information about category children, subcategories (different), and entries @@ -221,7 +221,7 @@ def _filter_unstarted_categories(category_map): for child in unfiltered_map["children"]: if child in unfiltered_map["entries"]: - if unfiltered_map["entries"][child]["start_date"] <= now: + if course.self_paced or unfiltered_map["entries"][child]["start_date"] <= now: filtered_map["children"].append(child) filtered_map["entries"][child] = {} for key in unfiltered_map["entries"][child]: @@ -230,7 +230,7 @@ def _filter_unstarted_categories(category_map): else: log.debug(u"Filtering out:%s with start_date: %s", child, unfiltered_map["entries"][child]["start_date"]) else: - if unfiltered_map["subcategories"][child]["start_date"] < now: + if course.self_paced or unfiltered_map["subcategories"][child]["start_date"] < now: filtered_map["children"].append(child) filtered_map["subcategories"][child] = {} unfiltered_queue.append(unfiltered_map["subcategories"][child]) @@ -382,7 +382,7 @@ def get_discussion_category_map(course, user, cohorted_if_in_list=False, exclude _sort_map_entries(category_map, course.discussion_sort_alpha) - return _filter_unstarted_categories(category_map) if exclude_unstarted else category_map + return _filter_unstarted_categories(category_map, course) if exclude_unstarted else category_map def discussion_category_id_access(course, user, discussion_id): diff --git a/lms/envs/aws.py b/lms/envs/aws.py index f3fa7a5d3d..8995a09385 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -190,7 +190,6 @@ if ENV_TOKENS.get('SESSION_COOKIE_NAME', None): SESSION_COOKIE_NAME = str(ENV_TOKENS.get('SESSION_COOKIE_NAME')) BOOK_URL = ENV_TOKENS['BOOK_URL'] -MEDIA_URL = ENV_TOKENS['MEDIA_URL'] LOG_DIR = ENV_TOKENS['LOG_DIR'] CACHES = ENV_TOKENS['CACHES'] @@ -619,6 +618,7 @@ if FEATURES.get('ENABLE_OAUTH2_PROVIDER'): OAUTH_EXPIRE_DELTA_PUBLIC = datetime.timedelta( days=ENV_TOKENS.get('OAUTH_EXPIRE_PUBLIC_CLIENT_DAYS', OAUTH_EXPIRE_PUBLIC_CLIENT_DAYS) ) + OAUTH_ID_TOKEN_EXPIRATION = ENV_TOKENS.get('OAUTH_ID_TOKEN_EXPIRATION', OAUTH_ID_TOKEN_EXPIRATION) ##### ADVANCED_SECURITY_CONFIG ##### diff --git a/lms/envs/common.py b/lms/envs/common.py index 9b1c85f206..4876b7dbf2 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2551,9 +2551,8 @@ COURSE_ABOUT_VISIBILITY_PERMISSION = 'see_exists' # Enrollment API Cache Timeout ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT = 60 -# for Student Notes we would like to avoid too frequent token refreshes (default is 30 seconds) -if FEATURES['ENABLE_EDXNOTES']: - OAUTH_ID_TOKEN_EXPIRATION = 60 * 60 + +OAUTH_ID_TOKEN_EXPIRATION = 60 * 60 # These tabs are currently disabled NOTES_DISABLED_TABS = ['course_structure', 'tags']