From 4a113898d9d41fe333e94187daa249de764aa831 Mon Sep 17 00:00:00 2001 From: Paulo Viadanna Date: Thu, 5 Jul 2018 17:45:17 -0300 Subject: [PATCH 1/3] Add course_visibility setting --- .../models/settings/course_metadata.py | 14 +++++-- common/lib/xmodule/xmodule/course_module.py | 19 +++++++++ lms/djangoapps/courseware/tests/test_views.py | 27 ++++++------ lms/djangoapps/courseware/views/index.py | 11 +++-- .../sass/features/_course-experience.scss | 1 + .../features/course_experience/__init__.py | 4 ++ .../course-outline-fragment.html | 10 ++++- .../tests/views/test_course_home.py | 42 ++++++++++++------- openedx/features/course_experience/utils.py | 16 +++---- .../course_experience/views/course_home.py | 36 ++++++++++------ .../course_experience/views/course_outline.py | 18 ++++++-- 11 files changed, 140 insertions(+), 58 deletions(-) diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index ff3fb4661606..40ee1cc5b0b2 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -9,6 +9,8 @@ from xblock_django.models import XBlockStudioConfigurationFlag from xmodule.modulestore.django import modulestore +from openedx.features.course_experience import COURSE_ENABLE_ANONYMOUS_ACCESS_FLAG + class CourseMetadata(object): ''' @@ -63,7 +65,7 @@ class CourseMetadata(object): ] @classmethod - def filtered_list(cls): + def filtered_list(cls, course_key=None): """ Filter fields based on feature flag, i.e. enabled, disabled. """ @@ -117,6 +119,10 @@ def filtered_list(cls): if not XBlockStudioConfigurationFlag.is_enabled(): filtered_list.append('allow_unsupported_xblocks') + # Do not show "Course Visibility For Unauthenticated Students" in Studio Advanced Settings + # if the enable_anonymous_access flag is not enabled + if not COURSE_ENABLE_ANONYMOUS_ACCESS_FLAG.is_enabled(course_key=course_key): + filtered_list.append('course_visibility') return filtered_list @classmethod @@ -128,7 +134,7 @@ def fetch(cls, descriptor): result = {} metadata = cls.fetch_all(descriptor) for key, value in metadata.iteritems(): - if key in cls.filtered_list(): + if key in cls.filtered_list(descriptor.id): continue result[key] = value return result @@ -163,7 +169,7 @@ def update_from_json(cls, descriptor, jsondict, user, filter_tabs=True): Ensures none of the fields are in the blacklist. """ - filtered_list = cls.filtered_list() + filtered_list = cls.filtered_list(descriptor.id) # Don't filter on the tab attribute if filter_tabs is False. if not filter_tabs: filtered_list.remove("tabs") @@ -199,7 +205,7 @@ def validate_and_update_from_json(cls, descriptor, jsondict, user, filter_tabs=T errors: list of error objects result: the updated course metadata or None if error """ - filtered_list = cls.filtered_list() + filtered_list = cls.filtered_list(descriptor.id) if not filter_tabs: filtered_list.remove("tabs") diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 574058e5f118..34402f542253 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -44,6 +44,10 @@ DEFAULT_MOBILE_AVAILABLE = getattr(settings, 'DEFAULT_MOBILE_AVAILABLE', False) +COURSE_VISIBILITY_PRIVATE = 'private' +COURSE_VISIBILITY_PREVIEW = 'preview' +COURSE_VISIBILITY_PUBLIC = 'public' + class StringOrDate(Date): def from_json(self, value): @@ -814,6 +818,21 @@ class CourseFields(object): scope=Scope.settings ) + course_visibility = String( + display_name=_("Course Visibility For Unauthenticated Students"), + help=_( + "Defines the access permissions for unauthenticated users. This can be set to one of three values: " + "'private' (default visibility, only allowed for enrolled students), 'preview' (allow access to course " + "outline) and 'public' (allow full-access to course material)." + ), + default=COURSE_VISIBILITY_PRIVATE, + scope=Scope.settings, + values=[ + {"display_name": _("private"), "value": COURSE_VISIBILITY_PRIVATE}, + {"display_name": _("preview"), "value": COURSE_VISIBILITY_PREVIEW}, + {"display_name": _("public"), "value": COURSE_VISIBILITY_PUBLIC}] + ) + """ instructor_info dict structure: { diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index be6d9d92ff35..ee2f874de405 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -58,13 +58,13 @@ from openedx.core.djangoapps.crawlers.models import CrawlersConfig from openedx.core.djangoapps.credit.api import set_credit_requirements from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider -from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag from openedx.core.djangolib.testing.utils import get_mock_request from openedx.core.lib.gating import api as gating_api from openedx.core.lib.tests import attr from openedx.core.lib.url_utils import quote_slashes -from openedx.features.course_experience import COURSE_OUTLINE_PAGE_FLAG, UNIFIED_COURSE_TAB_FLAG +from openedx.features.course_experience import COURSE_OUTLINE_PAGE_FLAG, UNIFIED_COURSE_TAB_FLAG, \ + COURSE_ENABLE_ANONYMOUS_ACCESS_FLAG from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired from student.models import CourseEnrollment from student.tests.factories import TEST_PASSWORD, AdminFactory, CourseEnrollmentFactory, UserFactory @@ -2265,7 +2265,6 @@ class TestIndexView(ModuleStoreTestCase): """ Tests of the courseware.views.index view. """ - SEO_WAFFLE_FLAG = CourseWaffleFlag(WaffleFlagNamespace(name='seo'), 'enable_anonymous_courseware_access') @XBlock.register_temp_plugin(ViewCheckerBlock, 'view_checker') @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @@ -2335,8 +2334,17 @@ def test_activate_block_id(self): ) self.assertIn("Activate Block ID: test_block_id", response.content) - def test_anonymous_access(self): - course = CourseFactory() + @ddt.data( + [False, 'private', 302], + [False, 'preview', 302], + [False, 'public', 302], + [True, 'private', 302], + [True, 'preview', 302], + [True, 'public', 200], + ) + @ddt.unpack + def test_anonymous_access(self, waffle_override, course_visibility, expected_status): + course = CourseFactory(course_visibility=course_visibility) with self.store.bulk_operations(course.id): chapter = ItemFactory(parent=course, category='chapter') section = ItemFactory(parent=chapter, category='sequential') @@ -2350,15 +2358,10 @@ def test_anonymous_access(self): 'section': section.url_name, } ) - response = self.client.get(url, follow=False) - assert response.status_code == 302 - waffle_flag = CourseWaffleFlag(WaffleFlagNamespace(name='seo'), 'enable_anonymous_courseware_access') - with override_waffle_flag(waffle_flag, active=True): + with override_waffle_flag(COURSE_ENABLE_ANONYMOUS_ACCESS_FLAG, active=waffle_override): response = self.client.get(url, follow=False) - assert response.status_code == 200 - self.assertIn('data-save-position="false"', response.content) - self.assertIn('data-show-completion="false"', response.content) + assert response.status_code == expected_status user = UserFactory() CourseEnrollmentFactory(user=user, course_id=course.id) diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py index 55f263d81f0f..ac5fc61dcdd8 100644 --- a/lms/djangoapps/courseware/views/index.py +++ b/lms/djangoapps/courseware/views/index.py @@ -32,9 +32,10 @@ from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY from openedx.core.djangoapps.user_api.preferences.api import get_user_preference from openedx.core.djangoapps.util.user_messages import PageLevelMessages -from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace, WaffleFlagNamespace, CourseWaffleFlag +from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace from openedx.core.djangolib.markup import HTML, Text -from openedx.features.course_experience import COURSE_OUTLINE_PAGE_FLAG, default_course_url_name +from openedx.features.course_experience import COURSE_OUTLINE_PAGE_FLAG, default_course_url_name, \ + COURSE_ENABLE_ANONYMOUS_ACCESS_FLAG from openedx.features.course_experience.views.course_sock import CourseSockFragmentView from openedx.features.enterprise_support.api import data_sharing_consent_required from shoppingcart.models import CourseRegistrationCode @@ -69,8 +70,7 @@ class CoursewareIndex(View): @cached_property def enable_anonymous_courseware_access(self): - waffle_flag = CourseWaffleFlag(WaffleFlagNamespace(name='seo'), 'enable_anonymous_courseware_access') - return waffle_flag.is_enabled(self.course_key) + return COURSE_ENABLE_ANONYMOUS_ACCESS_FLAG.is_enabled(self.course_key) @method_decorator(ensure_csrf_cookie) @method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True)) @@ -118,6 +118,9 @@ def get(self, request, course_id, chapter=None, section=None, position=None): depth=CONTENT_DEPTH, check_if_enrolled=not self.enable_anonymous_courseware_access, ) + if not (request.user.is_authenticated or self.course.course_visibility == 'public'): + return redirect_to_login(request.get_full_path()) + self.is_staff = has_access(request.user, 'staff', self.course) self._setup_masquerade_for_effective_user() return self.render(request) diff --git a/lms/static/sass/features/_course-experience.scss b/lms/static/sass/features/_course-experience.scss index f5e86db529ac..22b13be41339 100644 --- a/lms/static/sass/features/_course-experience.scss +++ b/lms/static/sass/features/_course-experience.scss @@ -368,6 +368,7 @@ list-style-type: none; + span.outline-item, a.outline-item { display: flex; justify-content: space-between; diff --git a/openedx/features/course_experience/__init__.py b/openedx/features/course_experience/__init__.py index 7741d2bc1221..785a54de39a8 100644 --- a/openedx/features/course_experience/__init__.py +++ b/openedx/features/course_experience/__init__.py @@ -43,6 +43,10 @@ # Waffle flag to enable the use of Bootstrap for course experience pages USE_BOOTSTRAP_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'use_bootstrap', flag_undefined_default=True) +# Waffle flag to enable anonymous access to a course +SEO_WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='seo') +COURSE_ENABLE_ANONYMOUS_ACCESS_FLAG = CourseWaffleFlag(SEO_WAFFLE_FLAG_NAMESPACE, 'enable_anonymous_courseware_access') + def course_home_page_title(course): # pylint: disable=unused-argument """ diff --git a/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html b/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html index 5f89fd2e9ce0..ebb2ce57b6ee 100644 --- a/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +++ b/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html @@ -51,7 +51,7 @@

${ section['display_name'] }

completed_prereqs = gated_content[subsection['id']]['completed_prereqs'] if gated_subsection else False subsection_is_auto_opened = subsection.get('resume_block') is True %> -
  • +
  • % if gated_subsection and not completed_prereqs: