feat: add Waffle flag to control start date access for masquerading users

This introduces the `ENFORCE_MASQUERADE_START_DATES` flag.
When the flag is disabled (default), masquerading users bypass start dates.
When the flag is enabled, masquerading users see the same start date
restrictions as regular students.
This commit is contained in:
Agrendalath
2025-10-08 19:57:36 +02:00
committed by Piotr Surowiec
parent 6deb4f8d05
commit 890b8268c9
3 changed files with 72 additions and 2 deletions

View File

@@ -23,7 +23,11 @@ from lms.djangoapps.courseware.access_response import (
StartDateError,
)
from lms.djangoapps.courseware.masquerade import get_course_masquerade, is_masquerading_as_student
from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, COURSE_PRE_START_ACCESS_FLAG
from openedx.features.course_experience import (
COURSE_ENABLE_UNENROLLED_ACCESS_FLAG,
COURSE_PRE_START_ACCESS_FLAG,
ENFORCE_MASQUERADE_START_DATES,
)
from xmodule.course_block import COURSE_VISIBILITY_PUBLIC # lint-amnesty, pylint: disable=wrong-import-order
DEBUG_ACCESS = False
@@ -137,7 +141,10 @@ def check_start_date(user, days_early_for_beta, start, course_key, display_error
if start_dates_disabled and not masquerading_as_student:
return ACCESS_GRANTED
else:
if start is None or get_course_masquerade(user, course_key):
if start is None:
return ACCESS_GRANTED
if not ENFORCE_MASQUERADE_START_DATES.is_enabled(course_key) and get_course_masquerade(user, course_key):
return ACCESS_GRANTED
if now is None:

View File

@@ -17,6 +17,7 @@ from django.test import TestCase
from django.test.client import RequestFactory
from django.test.utils import override_settings
from django.urls import reverse
from edx_toggles.toggles.testutils import override_waffle_flag
from milestones.tests.utils import MilestonesTestCaseMixin
from opaque_keys.edx.locator import CourseLocator
@@ -30,6 +31,7 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOvervi
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
from openedx.features.course_experience import ENFORCE_MASQUERADE_START_DATES
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.roles import CourseCcxCoachRole, CourseStaffRole
from common.djangoapps.student.tests.factories import (
@@ -465,6 +467,50 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes
self.verify_access(mock_unit, expected_access, expected_error_type)
@ddt.data(
# Flag inactive (default)
(False, True, None), # Masquerading, no start date
(False, True, YESTERDAY), # Masquerading, past start date
(False, False, TOMORROW), # Not masquerading, future start date
(False, True, TOMORROW), # Masquerading, future start date
# Flag active
(True, True, None), # Masquerading, no start date
(True, True, YESTERDAY), # Masquerading, past start date
(True, False, TOMORROW), # Not masquerading, future start date
(True, True, TOMORROW, False), # Masquerading, future start date - no access
)
@ddt.unpack
@patch.dict("django.conf.settings.FEATURES", {"DISABLE_START_DATES": False})
def test_enforce_masquerade_start_dates_flag(self, flag_active, is_masquerading, start, expected_access=True):
"""
Test that the ENFORCE_MASQUERADE_START_DATES flag controls whether masquerading bypasses start date
restrictions.
When the flag is disabled (default), masquerading users bypass start dates.
When the flag is enabled, masquerading users see the same start date restrictions as regular students.
"""
mock_unit = Mock(
location=self.course.location,
user_partitions=[],
_class_tags={},
start=self.DATES[start],
visible_to_staff_only=False,
merged_group_access={},
)
if is_masquerading:
self.course_staff.masquerade_settings = {self.course.id: CourseMasquerade(self.course.id, role="student")}
with override_waffle_flag(ENFORCE_MASQUERADE_START_DATES, active=flag_active):
response = access._has_access_to_block(self.course_staff, "load", mock_unit, course_key=self.course.id)
if expected_access:
assert response == access.ACCESS_GRANTED
else:
assert isinstance(response, access_response.StartDateError)
assert response.to_json()["error_code"] is not None
assert str(self.DATES[start]) in response.developer_message
def test__has_access_course_can_enroll(self):
yesterday = datetime.datetime.now(pytz.utc) - datetime.timedelta(days=1)
tomorrow = datetime.datetime.now(pytz.utc) + datetime.timedelta(days=1)

View File

@@ -83,6 +83,23 @@ RELATIVE_DATES_DISABLE_RESET_FLAG = CourseWaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.r
# .. toggle_tickets: https://openedx.atlassian.net/browse/AA-36
CALENDAR_SYNC_FLAG = CourseWaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.calendar_sync', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
# .. toggle_name: course_experience.enforce_masquerade_start_dates
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: When enabled, staff masquerading as students will see the same start date
# restrictions as actual students. This provides a more accurate preview experience by enforcing
# section and subsection start dates even when viewing the course as a masqueraded user.
# When disabled (default), masquerading continues to bypass start date restrictions as before.
# .. toggle_use_cases: opt_in
# .. toggle_creation_date: 2025-10-08
# .. toggle_warning: Enabling this flag means staff members masquerading as students will not be able to access course
# content before its start date, which may impact course testing workflows.
# Also, when you masquerade as a student in a course that starts in the future, you will lock yourself out of the
# course in the current Django session. To revert this, you need to log out and log back in.
ENFORCE_MASQUERADE_START_DATES = CourseWaffleFlag(
f'{WAFFLE_FLAG_NAMESPACE}.enforce_masquerade_start_dates', __name__
)
def course_home_page_title(_course):
"""