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)
Beispiel #2
0
    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
Beispiel #3
0
    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)
Beispiel #4
0
    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
Beispiel #7
0
# 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')
Beispiel #8
0
# .. 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):
    """
Beispiel #9
0
"""
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)
Beispiel #10
0
# .. 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.
Beispiel #11
0
# 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(
Beispiel #12
0
"""
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))
Beispiel #13
0
    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')
Beispiel #14
0
"""
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)
Beispiel #15
0
# .. 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.