diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 83b47e339c..da85f102dd 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -42,6 +42,7 @@ from lms.djangoapps.courseware.masquerade import get_masquerade_role, is_masquer from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException from lms.djangoapps.ccx.models import CustomCourseForEdX from lms.djangoapps.mobile_api.models import IgnoreMobileAvailableFlagConfig +from lms.djangoapps.courseware.toggles import is_courses_default_invite_only_enabled from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.features.course_duration_limits.access import check_course_expired from common.djangoapps.student import auth @@ -271,7 +272,8 @@ def _can_enroll_courselike(user, courselike): if _has_staff_access_to_descriptor(user, courselike, course_key): return ACCESS_GRANTED - if courselike.invitation_only: + # Access denied when default value of COURSES_INVITE_ONLY set to True + if is_courses_default_invite_only_enabled() or courselike.invitation_only: debug("Deny: invitation only") return ACCESS_DENIED diff --git a/lms/djangoapps/courseware/tests/test_access.py b/lms/djangoapps/courseware/tests/test_access.py index ac2773f6ef..c82723323f 100644 --- a/lms/djangoapps/courseware/tests/test_access.py +++ b/lms/djangoapps/courseware/tests/test_access.py @@ -14,6 +14,7 @@ from ccx_keys.locator import CCXLocator from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.test import TestCase from django.test.client import RequestFactory +from django.test.utils import override_settings from django.urls import reverse from milestones.tests.utils import MilestonesTestCaseMixin from opaque_keys.edx.locator import CourseLocator @@ -500,6 +501,48 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes ) assert not access._has_access_course(user, 'enroll', course) + @override_settings(COURSES_INVITE_ONLY=False) + def test__course_default_invite_only_flag_false(self): + """ + Ensure that COURSES_INVITE_ONLY does not take precedence, + if it is not set over the course invitation_only settings. + """ + + user = UserFactory.create() + + # User cannot enroll in the course if it is just invitation only. + course = self._mock_course_with_invitation(invitation=True) + self.assertFalse(access._has_access_course(user, 'enroll', course)) + + # User can enroll in the course if it is not just invitation only. + course = self._mock_course_with_invitation(invitation=False) + self.assertTrue(access._has_access_course(user, 'enroll', course)) + + @override_settings(COURSES_INVITE_ONLY=True) + def test__course_default_invite_only_flag_true(self): + """ + Ensure that COURSES_INVITE_ONLY takes precedence over the course invitation_only settings. + """ + + user = UserFactory.create() + + # User cannot enroll in the course if it is just invitation only and COURSES_INVITE_ONLY is also set. + course = self._mock_course_with_invitation(invitation=True) + self.assertFalse(access._has_access_course(user, 'enroll', course)) + + # User cannot enroll in the course if COURSES_INVITE_ONLY is set despite of the course invitation_only value. + course = self._mock_course_with_invitation(invitation=False) + self.assertFalse(access._has_access_course(user, 'enroll', course)) + + def _mock_course_with_invitation(self, invitation): + yesterday = datetime.datetime.now(pytz.utc) - datetime.timedelta(days=1) + tomorrow = datetime.datetime.now(pytz.utc) + datetime.timedelta(days=1) + return Mock( + enrollment_start=yesterday, enrollment_end=tomorrow, + id=CourseLocator('edX', 'test', '2012_Fall'), enrollment_domain='', + invitation_only=invitation + ) + def test__user_passed_as_none(self): """Ensure has_access handles a user being passed as null""" access.has_access(None, 'staff', 'global', None) diff --git a/lms/djangoapps/courseware/toggles.py b/lms/djangoapps/courseware/toggles.py index 715d178804..ee3430da79 100644 --- a/lms/djangoapps/courseware/toggles.py +++ b/lms/djangoapps/courseware/toggles.py @@ -2,7 +2,7 @@ Toggles for courseware in-course experience. """ -from edx_toggles.toggles import LegacyWaffleFlagNamespace +from edx_toggles.toggles import LegacyWaffleFlagNamespace, SettingToggle from opaque_keys.edx.keys import CourseKey from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag @@ -190,3 +190,18 @@ def streak_celebration_is_active(course_key): courseware_mfe_progress_milestones_are_active(course_key) and COURSEWARE_MICROFRONTEND_PROGRESS_MILESTONES_STREAK_CELEBRATION.is_enabled(course_key) ) + + +# .. toggle_name: COURSES_INVITE_ONLY +# .. toggle_implementation: SettingToggle +# .. toggle_type: feature_flag +# .. toggle_default: False +# .. toggle_description: Setting this sets the default value of INVITE_ONLY across all courses in a given deployment +# .. toggle_category: admin +# .. toggle_use_cases: open_edx +# .. toggle_creation_date: 2019-05-16 +# .. toggle_expiration_date: None +# .. toggle_tickets: https://github.com/mitodl/edx-platform/issues/123 +# .. toggle_status: unsupported +def is_courses_default_invite_only_enabled(): + return SettingToggle("COURSES_INVITE_ONLY", default=False).is_enabled() diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index f15213d831..a62713f218 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -86,6 +86,8 @@ from lms.djangoapps.courseware.permissions import ( # lint-amnesty, pylint: dis VIEW_COURSEWARE, VIEW_XQA_INTERFACE ) + +from lms.djangoapps.courseware.toggles import is_courses_default_invite_only_enabled from lms.djangoapps.courseware.user_state_client import DjangoXBlockUserStateClient from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context from lms.djangoapps.grades.api import CourseGradeFactory @@ -947,7 +949,7 @@ def course_about(request, course_id): # Used to provide context to message to student if enrollment not allowed can_enroll = bool(request.user.has_perm(ENROLL_IN_COURSE, course)) - invitation_only = course.invitation_only + invitation_only = is_courses_default_invite_only_enabled() or course.invitation_only is_course_full = CourseEnrollment.objects.is_course_full(course) # Register button should be disabled if one of the following is true: diff --git a/lms/templates/courseware/course_about.html b/lms/templates/courseware/course_about.html index a65258c527..47c7f29755 100644 --- a/lms/templates/courseware/course_about.html +++ b/lms/templates/courseware/course_about.html @@ -5,6 +5,7 @@ from django.utils.translation import ugettext as _ from django.utils.translation import pgettext from django.urls import reverse from lms.djangoapps.courseware.courses import get_course_about_section +from lms.djangoapps.courseware.toggles import is_courses_default_invite_only_enabled from django.conf import settings from six import text_type from common.djangoapps.edxmako.shortcuts import marketing_link @@ -90,7 +91,7 @@ from six import string_types ${_("Course is full")} - % elif invitation_only and not can_enroll: + % elif (is_courses_default_invite_only_enabled() or invitation_only) and not can_enroll: ${_("Enrollment in this course is by invitation only")} ## Shib courses need the enrollment button to be displayed even when can_enroll is False, ## because AnonymousUsers cause can_enroll for shib courses to be False, but we need them to be able to click