def setUp(self): super().setUp() self.user = UserFactory() self.request = RequestFactory().request() self.request.session = {} self.request.site = SiteFactory() self.request.user = self.user self.addCleanup(set_current_request, None) set_current_request(self.request) self.flag = ExperimentWaffleFlag('experiments', 'test', __name__, num_buckets=2, experiment_id=0) self.key = CourseKey.from_string('a/b/c') bucket_patch = patch( 'lms.djangoapps.experiments.flags.stable_bucketing_hash_group', return_value=1) self.addCleanup(bucket_patch.stop) bucket_patch.start() self.addCleanup(RequestCache.clear_all_namespaces)
def test_app_label_experiment_name(self): # pylint: disable=protected-access assert 'experiments' == self.flag._app_label assert 'test' == self.flag._experiment_name flag = ExperimentWaffleFlag("namespace", "flag.name", __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation assert 'namespace' == flag._app_label assert 'flag.name' == flag._experiment_name
def test_app_label_experiment_name(self): # pylint: disable=protected-access self.assertEqual("experiments", self.flag._app_label) self.assertEqual("test", self.flag._experiment_name) flag = ExperimentWaffleFlag("namespace", "flag.name", __name__) self.assertEqual("namespace", flag._app_label) self.assertEqual("flag.name", flag._experiment_name)
def test_app_label_experiment_name(self): # pylint: disable=protected-access assert 'experiments' == self.flag._app_label assert 'test' == self.flag._experiment_name flag = ExperimentWaffleFlag("namespace", "flag.name", __name__) assert 'namespace' == flag._app_label assert 'flag.name' == flag._experiment_name
class ExperimentWaffleFlagTests(SharedModuleStoreTestCase): """ Tests for the ExperimentWaffleFlag class """ def setUp(self): super().setUp() self.user = UserFactory() self.request = RequestFactory().request() self.request.session = {} self.request.site = SiteFactory() self.request.user = self.user self.addCleanup(set_current_request, None) set_current_request(self.request) self.flag = ExperimentWaffleFlag('experiments', 'test', __name__, num_buckets=2, experiment_id=0) self.key = CourseKey.from_string('a/b/c') bucket_patch = patch( 'lms.djangoapps.experiments.flags.stable_bucketing_hash_group', return_value=1) self.addCleanup(bucket_patch.stop) bucket_patch.start() self.addCleanup(RequestCache.clear_all_namespaces) def get_bucket(self, track=False, active=True): # Does not use override_experiment_waffle_flag, since that shortcuts get_bucket and we want to test internals with override_waffle_flag(self.flag, active): with override_waffle_flag(self.flag.bucket_flags[1], True): return self.flag.get_bucket(course_key=self.key, track=track) def test_basic_happy_path(self): self.assertEqual(self.get_bucket(), 1) def test_no_request(self): set_current_request(None) self.assertEqual(self.get_bucket(), 0) def test_not_enabled(self): self.assertEqual(self.get_bucket(active=False), 0) @ddt.data( ( '2012-01-06', None, 1 ), # no enrollment, but start is in past (we allow normal bucketing in this case) ( '9999-01-06', None, 0 ), # no enrollment, but start is in future (we give bucket 0 in that case) ('2012-01-06', '2012-01-05', 0), # enrolled before experiment start ('2012-01-06', '2012-01-07', 1), # enrolled after experiment start (None, '2012-01-07', 1), # no experiment date ('not-a-date', '2012-01-07', 0), # bad experiment date ) @ddt.unpack def test_enrollment_start(self, experiment_start, enrollment_created, expected_bucket): if enrollment_created: enrollment = CourseEnrollmentFactory(user=self.user, course_id='a/b/c') enrollment.created = parser.parse(enrollment_created).replace( tzinfo=pytz.UTC) enrollment.save() if experiment_start: ExperimentKeyValueFactory(experiment_id=0, key='enrollment_start', value=experiment_start) self.assertEqual(self.get_bucket(), expected_bucket) @ddt.data( ( '2012-01-06', None, 0 ), # no enrollment, but end is in past (we give bucket 0 in that case) ( '9999-01-06', None, 1 ), # no enrollment, but end is in future (we allow normal bucketing in this case) ('2012-01-06', '2012-01-05', 1), # enrolled before experiment end ('2012-01-06', '2012-01-07', 0), # enrolled after experiment end (None, '2012-01-07', 1), # no experiment date ('not-a-date', '2012-01-07', 0), # bad experiment date ) @ddt.unpack def test_enrollment_end(self, experiment_end, enrollment_created, expected_bucket): if enrollment_created: enrollment = CourseEnrollmentFactory(user=self.user, course_id='a/b/c') enrollment.created = parser.parse(enrollment_created).replace( tzinfo=pytz.UTC) enrollment.save() if experiment_end: ExperimentKeyValueFactory(experiment_id=0, key='enrollment_end', value=experiment_end) self.assertEqual(self.get_bucket(), expected_bucket) @ddt.data( (True, 0), (False, 1), ) @ddt.unpack def test_forcing_bucket(self, active, expected_bucket): bucket_flag = CourseWaffleFlag('experiments', 'test.0', __name__) with override_waffle_flag(bucket_flag, active=active): self.assertEqual(self.get_bucket(), expected_bucket) def test_tracking(self): # Run twice, with same request with patch('lms.djangoapps.experiments.flags.segment') as segment_mock: self.assertEqual(self.get_bucket(track=True), 1) RequestCache.clear_all_namespaces( ) # we want to force get_bucket to check session, not early exit self.assertEqual(self.get_bucket(track=True), 1) # Now test that we only sent the signal once, and with the correct properties self.assertEqual(segment_mock.track.call_count, 1) self.assertEqual(segment_mock.track.call_args, ((), { 'user_id': self.user.id, 'event_name': 'edx.bi.experiment.user.bucketed', 'properties': { 'site': self.request.site.domain, 'app_label': 'experiments', 'experiment': 'test', 'bucket': 1, 'course_id': 'a/b/c', 'is_staff': self.user.is_staff, 'nonInteraction': 1, }, })) def test_caching(self): self.assertEqual(self.get_bucket(active=True), 1) self.assertEqual(self.get_bucket(active=False), 1) # still returns 1! def test_is_enabled(self): with patch( 'lms.djangoapps.experiments.flags.ExperimentWaffleFlag.get_bucket', return_value=1): self.assertEqual(self.flag.is_enabled(self.key), True) self.assertEqual(self.flag.is_enabled(), True) with patch( 'lms.djangoapps.experiments.flags.ExperimentWaffleFlag.get_bucket', return_value=0): self.assertEqual(self.flag.is_enabled(self.key), False) self.assertEqual(self.flag.is_enabled(), False) @ddt.data( (True, 1, 1), (True, 0, 0), (False, 1, 0), # bucket is always 0 if the experiment is off (False, 0, 0), ) @ddt.unpack # Test the override method def test_override_method(self, active, bucket_override, expected_bucket): with override_experiment_waffle_flag(self.flag, active=active, bucket=bucket_override): self.assertEqual(self.flag.get_bucket(), expected_bucket) self.assertEqual(self.flag.is_experiment_on(), active)
class ExperimentWaffleFlagCourseAwarenessTest(SharedModuleStoreTestCase): """ Tests for how course context awareness/unawareness interacts with the ExperimentWaffleFlag class. """ course_aware_flag = ExperimentWaffleFlag( 'exp', 'aware', __name__, num_buckets=20, use_course_aware_bucketing=True, ) course_aware_subflag = CourseWaffleFlag('exp', 'aware.1', __name__) course_unaware_flag = ExperimentWaffleFlag( 'exp', 'unaware', __name__, num_buckets=20, use_course_aware_bucketing=False, ) course_unaware_subflag = CourseWaffleFlag('exp', 'unaware.1', __name__) course_key_1 = CourseKey.from_string("x/y/1") course_key_2 = CourseKey.from_string("x/y/22") course_key_3 = CourseKey.from_string("x/y/333") @classmethod def setUpTestData(cls): super().setUpTestData() # Force all users into Bucket 1 for course at `course_key_1`. WaffleFlagCourseOverrideModel.objects.create( waffle_flag="exp.aware.1", course_id=cls.course_key_1, enabled=True) WaffleFlagCourseOverrideModel.objects.create( waffle_flag="exp.unaware.1", course_id=cls.course_key_1, enabled=True) cls.user = UserFactory() def setUp(self): super().setUp() self.request = RequestFactory().request() self.request.session = {} self.request.site = SiteFactory() self.request.user = self.user self.addCleanup(set_current_request, None) set_current_request(self.request) self.addCleanup(RequestCache.clear_all_namespaces) # Enable all experiment waffle flags. experiment_waffle_flag_patcher = patch.object(ExperimentWaffleFlag, 'is_experiment_on', return_value=True) experiment_waffle_flag_patcher.start() self.addCleanup(experiment_waffle_flag_patcher.stop) # Use our custom fake `stable_bucketing_hash_group` implementation. stable_bucket_patcher = patch( 'lms.djangoapps.experiments.flags.stable_bucketing_hash_group', self._mock_stable_bucket) stable_bucket_patcher.start() self.addCleanup(stable_bucket_patcher.stop) @staticmethod def _mock_stable_bucket(group_name, *_args, **_kwargs): """ A fake version of `stable_bucketing_hash_group` that just returns the length of `group_name`. """ return len(group_name) def test_course_aware_bucketing(self): """ Test behavior of an experiment flag configured wtih course-aware bucket hashing. """ # Expect queries for Course 1 to be forced into Bucket 1 # due to `course_aware_subflag`. assert self.course_aware_flag.get_bucket(self.course_key_1) == 1 # Because we are using course-aware bucket hashing, different # courses may default to different buckets. # In the case of Courses 2 and 3 here, we expect two different buckets. assert self.course_aware_flag.get_bucket(self.course_key_2) == 16 assert self.course_aware_flag.get_bucket(self.course_key_3) == 17 # We can still query a course-aware flag outside of course context, # which has its own default bucket. assert self.course_aware_flag.get_bucket() == 9 def test_course_unaware_bucketing(self): """ Test behavior of an experiment flag configured wtih course-unaware bucket hashing. """ # Expect queries for Course 1 to be forced into Bucket 1 # due to `course_unaware_subflag`. # This should happen in spite of the fact that *default* bucketing # is unaware of courses. assert self.course_unaware_flag.get_bucket(self.course_key_1) == 1 # Expect queries for Course 2, queries for Course 3, and queries outside # the context of the course to all be hashed into the same default bucket. assert self.course_unaware_flag.get_bucket(self.course_key_2) == 11 assert self.course_unaware_flag.get_bucket(self.course_key_3) == 11 assert self.course_unaware_flag.get_bucket() == 11
# Namespace for courseware waffle flags. WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='courseware') # Waffle flag to redirect to another learner profile experience. # .. toggle_name: courseware.courseware_mfe # .. toggle_implementation: ExperimentWaffleFlag # .. toggle_default: False # .. toggle_description: Supports staged rollout to students for a new micro-frontend-based implementation of the courseware page. # .. toggle_category: micro-frontend # .. toggle_use_cases: incremental_release, open_edx # .. toggle_creation_date: 2020-01-29 # .. toggle_expiration_date: 2020-12-31 # .. toggle_warnings: Also set settings.LEARNING_MICROFRONTEND_URL and ENABLE_COURSEWARE_MICROFRONTEND. # .. toggle_tickets: TNL-7000 # .. toggle_status: supported REDIRECT_TO_COURSEWARE_MICROFRONTEND = ExperimentWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'courseware_mfe') # Waffle flag to display a link for the new learner experience to course teams without redirecting students. # # .. toggle_name: courseware.microfrontend_course_team_preview # .. toggle_implementation: CourseWaffleFlag # .. toggle_default: False # .. toggle_description: Supports staged rollout to course teams of a new micro-frontend-based implementation of the courseware page. # .. toggle_category: micro-frontend # .. toggle_use_cases: incremental_release, open_edx # .. toggle_creation_date: 2020-03-09 # .. toggle_expiration_date: 2020-12-31 # .. toggle_warnings: Also set settings.LEARNING_MICROFRONTEND_URL and ENABLE_COURSEWARE_MICROFRONTEND. # .. toggle_tickets: TNL-6982 # .. toggle_status: supported COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'microfrontend_course_team_preview')
# .. toggle_use_cases: opt_out # .. toggle_creation_date: 2017-09-11 # .. toggle_expiration_date: ??? # .. toggle_warnings: This is meant to be configured using waffle_utils course override only. Either do not create the actual waffle flag, or be sure to unset the flag even for Superusers. # .. toggle_tickets: N/A # .. toggle_status: supported LATEST_UPDATE_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'latest_update') # Waffle flag to enable anonymous access to a course SEO_WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='seo') COURSE_ENABLE_UNENROLLED_ACCESS_FLAG = CourseWaffleFlag( SEO_WAFFLE_FLAG_NAMESPACE, 'enable_anonymous_courseware_access') # Waffle flag to enable relative dates for course content RELATIVE_DATES_FLAG = ExperimentWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'relative_dates', experiment_id=17) # Waffle flag to enable user calendar syncing CALENDAR_SYNC_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'calendar_sync') def course_home_page_title(course): # pylint: disable=unused-argument """ Returns the title for the course home page. """ return _('Course') def default_course_url_name(course_id): """
""" Feature/experiment toggles used for effort estimation. """ from edx_toggles.toggles import LegacyWaffleFlagNamespace from lms.djangoapps.experiments.flags import ExperimentWaffleFlag WAFFLE_FLAG_NAMESPACE = LegacyWaffleFlagNamespace(name='effort_estimation') # Temporary flag while we test which location works best: # - Bucket 0: off # - Bucket 1: section (chapter) estimations # - Bucket 2: subsection (sequential) estimations EFFORT_ESTIMATION_LOCATION_FLAG = ExperimentWaffleFlag( WAFFLE_FLAG_NAMESPACE, 'location', __name__, num_buckets=3, # lint-amnesty, pylint: disable=toggle-missing-annotation use_course_aware_bucketing=False)
# .. toggle_use_cases: open_edx # .. toggle_creation_date: 2017-09-17 DEBUG_MESSAGE_WAFFLE_FLAG = WaffleFlag('schedules.enable_debugging', __name__) COURSE_UPDATE_SHOW_UNSUBSCRIBE_WAFFLE_SWITCH = LegacyWaffleSwitch( # lint-amnesty, pylint: disable=toggle-missing-annotation WAFFLE_SWITCH_NAMESPACE, 'course_update_show_unsubscribe', __name__) # This experiment waffle is supporting an A/B test we are running on sending course updates from an external service, # rather than through platform and ACE. See ticket AA-661 for more information. # Don't use this flag directly, instead use the `set_up_external_updates_for_enrollment` and `query_external_updates` # methods below. We save this flag decision at enrollment time and don't change it even if the flag changes. So you # can't just directly look at flag result. _EXTERNAL_COURSE_UPDATES_EXPERIMENT_ID = 18 _EXTERNAL_COURSE_UPDATES_FLAG = ExperimentWaffleFlag( WAFFLE_FLAG_NAMESPACE, 'external_updates', __name__, # lint-amnesty, pylint: disable=toggle-missing-annotation experiment_id=_EXTERNAL_COURSE_UPDATES_EXPERIMENT_ID, use_course_aware_bucketing=False) def set_up_external_updates_for_enrollment(user, course_key): """ Returns and stores whether a user should be getting the "external course updates" experience. See the description of this experiment with the waffle flag definition above. But basically, if a user is getting external course updates for a course, edx-platform just stops sending any updates, trustingn that the user is receiving them elsewhere. This is basically just a wrapper around our experiment waffle flag, but only buckets users that directly enrolled (rather than users enrolled by staff), for technical "waffle-flags-can-only-get-the-user-from-the-request" reasons.
# Namespace for courseware waffle flags. WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='courseware') # Waffle flag to redirect to another learner profile experience. # .. toggle_name: courseware.courseware_mfe # .. toggle_implementation: ExperimentWaffleFlag # .. toggle_default: False # .. toggle_description: Supports staged rollout to students for a new micro-frontend-based implementation of the courseware page. # .. toggle_category: micro-frontend # .. toggle_use_cases: incremental_release, open_edx # .. toggle_creation_date: 2020-01-29 # .. toggle_expiration_date: 2020-12-31 # .. toggle_warnings: Also set settings.LEARNING_MICROFRONTEND_URL and ENABLE_COURSEWARE_MICROFRONTEND. # .. toggle_tickets: TNL-7000 # .. toggle_status: supported REDIRECT_TO_COURSEWARE_MICROFRONTEND = ExperimentWaffleFlag( WAFFLE_FLAG_NAMESPACE, 'courseware_mfe', use_course_aware_bucketing=False) # Waffle flag to display a link for the new learner experience to course teams without redirecting students. # # .. toggle_name: courseware.microfrontend_course_team_preview # .. toggle_implementation: CourseWaffleFlag # .. toggle_default: False # .. toggle_description: Supports staged rollout to course teams of a new micro-frontend-based implementation of the courseware page. # .. toggle_category: micro-frontend # .. toggle_use_cases: incremental_release, open_edx # .. toggle_creation_date: 2020-03-09 # .. toggle_expiration_date: 2020-12-31 # .. toggle_warnings: Also set settings.LEARNING_MICROFRONTEND_URL and ENABLE_COURSEWARE_MICROFRONTEND. # .. toggle_tickets: TNL-6982 # .. toggle_status: supported COURSEWARE_MICROFRONTEND_COURSE_TEAM_PREVIEW = CourseWaffleFlag(
""" Toggles for course home experience. """ from lms.djangoapps.experiments.flags import ExperimentWaffleFlag from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='course_home') COURSE_HOME_MICROFRONTEND = ExperimentWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'course_home_mfe') COURSE_HOME_MICROFRONTEND_DATES_TAB = CourseWaffleFlag( WAFFLE_FLAG_NAMESPACE, 'course_home_mfe_dates_tab') def course_home_mfe_dates_tab_is_active(course_key): return (COURSE_HOME_MICROFRONTEND.is_enabled(course_key) and COURSE_HOME_MICROFRONTEND_DATES_TAB.is_enabled(course_key))
module_name=__name__, ) # TODO END: Clean up as part of REV-1205 (End) # .. toggle_name: streak_celebration.AA-759 # .. toggle_implementation: ExperimentWaffleFlag # .. toggle_default: False # .. toggle_description: This experiment flag enables an engagement discount incentive message. # .. toggle_warnings: This flag depends on the streak celebration feature being enabled # .. toggle_use_cases: temporary # .. toggle_creation_date: 2021-05-05 # .. toggle_target_removal_date: 2021-07-05 # .. toggle_tickets: https://openedx.atlassian.net/browse/AA-759 STREAK_DISCOUNT_EXPERIMENT_FLAG = ExperimentWaffleFlag( LegacyWaffleFlagNamespace(name='streak_celebration'), 'discount_experiment_AA759', __name__, use_course_aware_bucketing=False) def check_and_get_upgrade_link_and_date(user, enrollment=None, course=None): """ For an authenticated user, return a link to allow them to upgrade in the specified course. Returns the upgrade link and upgrade deadline for a user in a given course given that the user is within the window to upgrade defined by our dynamic pacing feature; otherwise, returns None for both the link and date. """ if enrollment is None and course is None: logger.warning('Must specify either an enrollment or a course')
""" Feature/experiment toggles used for effort estimation. """ from edx_toggles.toggles import LegacyWaffleFlagNamespace from lms.djangoapps.experiments.flags import ExperimentWaffleFlag WAFFLE_FLAG_NAMESPACE = LegacyWaffleFlagNamespace(name='effort_estimation') # Temporary flag while we test which location works best: # - Bucket 0: off # - Bucket 1: section (chapter) estimations # - Bucket 2: subsection (sequential) estimations EFFORT_ESTIMATION_LOCATION_FLAG = ExperimentWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'location', __name__, num_buckets=3, use_course_aware_bucketing=False)
# .. toggle_use_cases: open_edx # .. toggle_creation_date: 2017-09-17 DEBUG_MESSAGE_WAFFLE_FLAG = WaffleFlag('schedules.enable_debugging', __name__) COURSE_UPDATE_SHOW_UNSUBSCRIBE_WAFFLE_SWITCH = LegacyWaffleSwitch( WAFFLE_SWITCH_NAMESPACE, 'course_update_show_unsubscribe', __name__) # This experiment waffle is supporting an A/B test we are running on sending course updates from an external service, # rather than through platform and ACE. See ticket AA-661 for more information. # Don't use this flag directly, instead use the `set_up_external_updates_for_enrollment` and `query_external_updates` # methods below. We save this flag decision at enrollment time and don't change it even if the flag changes. So you # can't just directly look at flag result. _EXTERNAL_COURSE_UPDATES_EXPERIMENT_ID = 18 _EXTERNAL_COURSE_UPDATES_FLAG = ExperimentWaffleFlag( WAFFLE_FLAG_NAMESPACE, 'external_updates', __name__, experiment_id=_EXTERNAL_COURSE_UPDATES_EXPERIMENT_ID, use_course_aware_bucketing=False) def set_up_external_updates_for_enrollment(user, course_key): """ Returns and stores whether a user should be getting the "external course updates" experience. See the description of this experiment with the waffle flag definition above. But basically, if a user is getting external course updates for a course, edx-platform just stops sending any updates, trustingn that the user is receiving them elsewhere. This is basically just a wrapper around our experiment waffle flag, but only buckets users that directly enrolled (rather than users enrolled by staff), for technical "waffle-flags-can-only-get-the-user-from-the-request" reasons.