refactor!: switch from LegacyWaffle* to modern waffles (#30330)

This is a first stage for removing the LegacyWaffle* classes.

LegacyWaffleFlag usage replaced with WaffleFlag;
LegacyWaffleSwitche usage replaced with WaffleSwitch;
New CourseWaffleFlag added to the temporary module __future__ as FutureCourseWaffleFlag;
Updated all the imports to use CourseWaffleFlag from the __future__ module;

BREAKING CHANGE: A number of toggle related constants (e.g. ENABLE_ACCESSIBILITY_POLICY_PAGE)
changed types. They were strings, and are now toggle instances (e.g. WaffleSwitch). Although the entire
refactor should be self-contained in edx-platform, if any plugins or dependencies were directly
using these constants, they will break. If this is the case, try to find a better publicized way of
exposing those toggles.
This commit is contained in:
Eugene Dyudyunov
2022-05-05 19:03:10 +03:00
committed by GitHub
parent f1bd793cc5
commit 8bd43207ca
60 changed files with 471 additions and 644 deletions

View File

@@ -1,12 +1,10 @@
"""
Toggles for course apps.
"""
from edx_toggles.toggles import LegacyWaffleSwitchNamespace
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
#: Namespace for use by course apps for creating availability toggles
COURSE_APPS_WAFFLE_NAMESPACE = LegacyWaffleSwitchNamespace("course_apps")
COURSE_APPS_WAFFLE_NAMESPACE = 'course_apps'
# .. toggle_name: course_apps.proctoring_settings_modal_view
# .. toggle_use_cases: temporary
@@ -18,7 +16,7 @@ COURSE_APPS_WAFFLE_NAMESPACE = LegacyWaffleSwitchNamespace("course_apps")
# .. toggle_creation_date: 2021-08-17
# .. toggle_target_removal_date: None
PROCTORING_SETTINGS_MODAL_VIEW = CourseWaffleFlag(
COURSE_APPS_WAFFLE_NAMESPACE, 'proctoring_settings_modal_view', module_name=__name__,
f'{COURSE_APPS_WAFFLE_NAMESPACE}.proctoring_settings_modal_view', __name__
)

View File

@@ -3,11 +3,9 @@ This module contains various configuration settings via
waffle switches for the live app.
"""
from edx_toggles.toggles import LegacyWaffleFlagNamespace
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
WAFFLE_NAMESPACE = LegacyWaffleFlagNamespace(name='course_live')
WAFFLE_NAMESPACE = 'course_live'
# .. toggle_name: course_live.enable_course_live
# .. toggle_implementation: CourseWaffleFlag
@@ -18,8 +16,4 @@ WAFFLE_NAMESPACE = LegacyWaffleFlagNamespace(name='course_live')
# .. toggle_target_removal_date: 2022-06-02
# .. toggle_warnings: When the flag is ON, the course live app will be visible in the course authoring mfe
# .. toggle_tickets: TNL-9603
ENABLE_COURSE_LIVE = CourseWaffleFlag(
waffle_namespace=WAFFLE_NAMESPACE,
flag_name='enable_course_live',
module_name=__name__,
)
ENABLE_COURSE_LIVE = CourseWaffleFlag(f'{WAFFLE_NAMESPACE}.enable_course_live', __name__)

View File

@@ -3,12 +3,10 @@ This module contains various configuration settings via
waffle switches for the discussions app.
"""
from edx_toggles.toggles import LegacyWaffleFlagNamespace
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
WAFFLE_NAMESPACE = LegacyWaffleFlagNamespace(name='discussions')
WAFFLE_NAMESPACE = 'discussions'
# .. toggle_name: discussions.override_discussion_legacy_settings
# .. toggle_implementation: CourseWaffleFlag
@@ -20,9 +18,7 @@ WAFFLE_NAMESPACE = LegacyWaffleFlagNamespace(name='discussions')
# .. toggle_warnings: When the flag is ON, the discussion settings will be available on legacy experience.
# .. toggle_tickets: TNL-8389
OVERRIDE_DISCUSSION_LEGACY_SETTINGS_FLAG = CourseWaffleFlag(
waffle_namespace=WAFFLE_NAMESPACE,
flag_name='override_discussion_legacy_settings',
module_name=__name__,
f'{WAFFLE_NAMESPACE}.override_discussion_legacy_settings', __name__
)
@@ -36,7 +32,5 @@ OVERRIDE_DISCUSSION_LEGACY_SETTINGS_FLAG = CourseWaffleFlag(
# .. toggle_warnings: When the flag is ON, the new experience for Pages and Resources will be enabled.
# .. toggle_tickets: TNL-7791
ENABLE_PAGES_AND_RESOURCES_MICROFRONTEND = CourseWaffleFlag(
waffle_namespace=WAFFLE_NAMESPACE,
flag_name='pages_and_resources_mfe',
module_name=__name__,
f'{WAFFLE_NAMESPACE}.pages_and_resources_mfe', __name__
)

View File

@@ -8,15 +8,14 @@ if and only if the service is deployed in the Open edX installation.
To ensure maximum separation of concerns, and a minimum of interdependencies,
this package should be kept small, thin, and stateless.
"""
from edx_toggles.toggles import WaffleSwitch
default_app_config = 'openedx.core.djangoapps.programs.apps.ProgramsConfig'
from edx_toggles.toggles import LegacyWaffleSwitch, LegacyWaffleSwitchNamespace # lint-amnesty, pylint: disable=wrong-import-position
PROGRAMS_WAFFLE_SWITCH_NAMESPACE = LegacyWaffleSwitchNamespace(name='programs')
PROGRAMS_WAFFLE_SWITCH_NAMESPACE = 'programs'
# This is meant to be enabled until https://openedx.atlassian.net/browse/LEARNER-5573 needs to be resolved
ALWAYS_CALCULATE_PROGRAM_PRICE_AS_ANONYMOUS_USER = LegacyWaffleSwitch( # lint-amnesty, pylint: disable=toggle-missing-annotation
PROGRAMS_WAFFLE_SWITCH_NAMESPACE,
'always_calculate_program_price_as_anonymous_user',
ALWAYS_CALCULATE_PROGRAM_PRICE_AS_ANONYMOUS_USER = WaffleSwitch( # lint-amnesty, pylint: disable=toggle-missing-annotation
f'{PROGRAMS_WAFFLE_SWITCH_NAMESPACE}.always_calculate_program_price_as_anonymous_user',
__name__
)

View File

@@ -3,13 +3,12 @@ Contains configuration for schedules app
"""
from crum import get_current_request
from edx_toggles.toggles import LegacyWaffleSwitch, LegacyWaffleFlagNamespace, LegacyWaffleSwitchNamespace, WaffleFlag
from edx_toggles.toggles import WaffleFlag, WaffleSwitch
from lms.djangoapps.experiments.flags import ExperimentWaffleFlag
from lms.djangoapps.experiments.models import ExperimentData
WAFFLE_FLAG_NAMESPACE = LegacyWaffleFlagNamespace(name='schedules')
WAFFLE_SWITCH_NAMESPACE = LegacyWaffleSwitchNamespace(name='schedules')
WAFFLE_NAMESPACE = 'schedules'
# .. toggle_name: schedules.enable_debugging
# .. toggle_implementation: WaffleFlag
@@ -17,12 +16,10 @@ WAFFLE_SWITCH_NAMESPACE = LegacyWaffleSwitchNamespace(name='schedules')
# .. toggle_description: Enable debug level of logging for schedules messages.
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2017-09-17
DEBUG_MESSAGE_WAFFLE_FLAG = WaffleFlag('schedules.enable_debugging', __name__)
DEBUG_MESSAGE_WAFFLE_FLAG = WaffleFlag(f'{WAFFLE_NAMESPACE}.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__
COURSE_UPDATE_SHOW_UNSUBSCRIBE_WAFFLE_SWITCH = WaffleSwitch( # lint-amnesty, pylint: disable=toggle-missing-annotation
f'{WAFFLE_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,
@@ -31,9 +28,11 @@ COURSE_UPDATE_SHOW_UNSUBSCRIBE_WAFFLE_SWITCH = LegacyWaffleSwitch( # lint-amnes
# 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)
_EXTERNAL_COURSE_UPDATES_FLAG = ExperimentWaffleFlag( # lint-amnesty, pylint: disable=toggle-missing-annotation
f'{WAFFLE_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):

View File

@@ -2,7 +2,8 @@
Toggles for accounts related code.
"""
from edx_toggles.toggles import LegacyWaffleFlag
from edx_toggles.toggles import WaffleFlag
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
# .. toggle_name: order_history.redirect_to_microfrontend
@@ -15,7 +16,7 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_
# .. toggle_warnings: Also set settings.ORDER_HISTORY_MICROFRONTEND_URL and site's
# ENABLE_ORDER_HISTORY_MICROFRONTEND.
# .. toggle_tickets: DEPR-17
REDIRECT_TO_ORDER_HISTORY_MICROFRONTEND = LegacyWaffleFlag('order_history', 'redirect_to_microfrontend', __name__)
REDIRECT_TO_ORDER_HISTORY_MICROFRONTEND = WaffleFlag('order_history.redirect_to_microfrontend', __name__)
def should_redirect_to_order_history_microfrontend():
@@ -35,7 +36,7 @@ def should_redirect_to_order_history_microfrontend():
# .. toggle_target_removal_date: 2021-12-31
# .. toggle_warnings: Also set settings.ACCOUNT_MICROFRONTEND_URL.
# .. toggle_tickets: DEPR-17
REDIRECT_TO_ACCOUNT_MICROFRONTEND = LegacyWaffleFlag('account', 'redirect_to_microfrontend', __name__)
REDIRECT_TO_ACCOUNT_MICROFRONTEND = WaffleFlag('account.redirect_to_microfrontend', __name__)
def should_redirect_to_account_microfrontend():

View File

@@ -3,10 +3,9 @@ Waffle flags and switches for user authn.
"""
from edx_toggles.toggles import LegacyWaffleSwitch, LegacyWaffleSwitchNamespace
from edx_toggles.toggles import WaffleSwitch
_WAFFLE_NAMESPACE = 'user_authn'
_WAFFLE_SWITCH_NAMESPACE = LegacyWaffleSwitchNamespace(name=_WAFFLE_NAMESPACE, log_prefix='UserAuthN: ')
# .. toggle_name: user_authn.enable_login_using_thirdparty_auth_only
# .. toggle_implementation: WaffleSwitch
@@ -18,10 +17,8 @@ _WAFFLE_SWITCH_NAMESPACE = LegacyWaffleSwitchNamespace(name=_WAFFLE_NAMESPACE, l
# .. toggle_target_removal_date: 2020-01-31
# .. toggle_warnings: Requires THIRD_PARTY_AUTH_ONLY_DOMAIN to also be set.
# .. toggle_tickets: ENT-2461
ENABLE_LOGIN_USING_THIRDPARTY_AUTH_ONLY = LegacyWaffleSwitch(
_WAFFLE_SWITCH_NAMESPACE,
'enable_login_using_thirdparty_auth_only',
__name__
ENABLE_LOGIN_USING_THIRDPARTY_AUTH_ONLY = WaffleSwitch(
f'{_WAFFLE_NAMESPACE}.enable_login_using_thirdparty_auth_only', __name__
)
# .. toggle_name: user_authn.enable_pwned_password_api
@@ -32,8 +29,6 @@ ENABLE_LOGIN_USING_THIRDPARTY_AUTH_ONLY = LegacyWaffleSwitch(
# .. toggle_creation_date: 2021-09-22
# .. toggle_target_removal_date: 2021-12-31
# .. toggle_tickets: VAN-664
ENABLE_PWNED_PASSWORD_API = LegacyWaffleSwitch(
_WAFFLE_SWITCH_NAMESPACE,
'enable_pwned_password_api',
__name__
ENABLE_PWNED_PASSWORD_API = WaffleSwitch(
f'{_WAFFLE_NAMESPACE}.enable_pwned_password_api', __name__
)

View File

@@ -22,7 +22,7 @@ from django.utils.translation import gettext as _
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
from django.views.decorators.debug import sensitive_post_parameters
from edx_django_utils.monitoring import set_custom_attribute
from edx_toggles.toggles import LegacyWaffleFlag, LegacyWaffleFlagNamespace
from edx_toggles.toggles import WaffleFlag
from openedx_events.learning.data import UserData, UserPersonalData
from openedx_events.learning.signals import STUDENT_REGISTRATION_COMPLETED
from openedx_filters.learning.filters import StudentRegistrationRequested
@@ -115,11 +115,7 @@ REGISTER_USER = Signal()
# .. toggle_creation_date: 2020-04-30
# .. toggle_target_removal_date: 2020-06-01
# .. toggle_warnings: This temporary feature toggle does not have a target removal date.
REGISTRATION_FAILURE_LOGGING_FLAG = LegacyWaffleFlag(
waffle_namespace=LegacyWaffleFlagNamespace(name='registration'),
flag_name='enable_failure_logging',
module_name=__name__,
)
REGISTRATION_FAILURE_LOGGING_FLAG = WaffleFlag('registration.enable_failure_logging', __name__)
REAL_IP_KEY = 'openedx.core.djangoapps.util.ratelimit.real_ip'

View File

@@ -3,11 +3,13 @@ This module contains configuration settings via waffle flags
for the Video Pipeline app.
"""
from edx_toggles.toggles import LegacyWaffleFlag, LegacyWaffleFlagNamespace
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
from edx_toggles.toggles import WaffleFlag
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
# Videos Namespace
WAFFLE_NAMESPACE = 'videos'
LOG_PREFIX = 'Videos: '
# .. toggle_name: videos.deprecate_youtube
# .. toggle_implementation: CourseWaffleFlag
@@ -17,8 +19,8 @@ WAFFLE_NAMESPACE = 'videos'
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2018-08-03
# .. toggle_tickets: https://github.com/edx/edx-platform/pull/18765
# TODO: Replace with CourseWaffleFlag() from waffle_flags().
DEPRECATE_YOUTUBE = 'deprecate_youtube'
DEPRECATE_YOUTUBE = CourseWaffleFlag(f'{WAFFLE_NAMESPACE}.deprecate_youtube', __name__, LOG_PREFIX)
# .. toggle_name: videos.enable_devstack_video_uploads
# .. toggle_implementation: WaffleFlag
# .. toggle_default: False
@@ -30,34 +32,8 @@ DEPRECATE_YOUTUBE = 'deprecate_youtube'
# .. toggle_warnings: Enabling this feature requires that the ROLE_ARN, MFA_SERIAL_NUMBER, MFA_TOKEN settings are
# properly defined.
# .. toggle_tickets: https://github.com/edx/edx-platform/pull/23375
# TODO: Replace with WaffleFlag() from waffle_flags().
ENABLE_DEVSTACK_VIDEO_UPLOADS = 'enable_devstack_video_uploads'
# TODO: Replace with CourseWaffleFlag() from waffle_flags().
ENABLE_VEM_PIPELINE = 'enable_vem_pipeline'
ENABLE_DEVSTACK_VIDEO_UPLOADS = WaffleFlag(f'{WAFFLE_NAMESPACE}.enable_devstack_video_uploads', __name__, LOG_PREFIX)
def waffle_flags():
"""
Returns the namespaced, cached, audited Waffle flags dictionary for Videos.
IMPORTANT: Do NOT copy this dict pattern and do NOT add new flags to this dict.
Instead, replace the string constants above with the actual flag instances.
"""
namespace = LegacyWaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix='Videos: ')
return {
DEPRECATE_YOUTUBE: CourseWaffleFlag(
waffle_namespace=namespace,
flag_name=DEPRECATE_YOUTUBE,
module_name=__name__,
),
ENABLE_DEVSTACK_VIDEO_UPLOADS: LegacyWaffleFlag(
waffle_namespace=namespace,
flag_name=ENABLE_DEVSTACK_VIDEO_UPLOADS,
module_name=__name__,
),
ENABLE_VEM_PIPELINE: CourseWaffleFlag( # lint-amnesty, pylint: disable=toggle-missing-annotation
waffle_namespace=namespace,
flag_name=ENABLE_VEM_PIPELINE,
module_name=__name__,
)
}
ENABLE_VEM_PIPELINE = CourseWaffleFlag( # lint-amnesty, pylint: disable=toggle-missing-annotation
f'{WAFFLE_NAMESPACE}.enable_vem_pipeline', __name__, LOG_PREFIX
)

View File

@@ -0,0 +1,116 @@
"""
Temporary module to switch from the LegacyWaffle* classes.
"""
import logging
from edx_toggles.toggles import WaffleFlag
from opaque_keys.edx.keys import CourseKey
log = logging.getLogger(__name__)
class FutureCourseWaffleFlag(WaffleFlag):
"""
Represents a single waffle flag that can be forced on/off for a course. This class should be used instead of
WaffleFlag when in the context of a course. This class will also respect any org-level overrides, though
course-level overrides will take precedence.
Uses a cached waffle namespace.
Usage:
SOME_COURSE_FLAG = CourseWaffleFlag('my_namespace.some_course_feature', __name__, log_prefix='')
And then we can check this flag in code with::
SOME_COURSE_FLAG.is_enabled(course_key)
To configure a course-level override, go to Django Admin "waffle_utils" -> "Waffle flag course overrides".
Waffle flag: Set this to the flag name (e.g. my_namespace.some_course_feature).
Course id: Set this to the course id (e.g. course-v1:edx+100+Demo)
Override choice: (Force on/Force off). "Force on" will enable the waffle flag for all users in a course,
overriding any behavior configured on the waffle flag itself. "Force off" will disable the waffle flag
for all users in a course, overriding any behavior configured on the waffle flag itself. Requires
"Enabled" (see below) to apply.
Enabled: Must be marked as "enabled" in order for the override to be applied. These settings can't be
deleted, so instead, you need to add another disabled override entry to disable the override.
To configure an org-level override, go to Django Admin "waffle_utils" -> "Waffle flag org overrides".
Waffle flag: Set this to the flag name (e.g. my_namespace.some_course_feature).
Org name: Set this to the organization name (e.g. edx)
Override choice: (Force on/Force off). "Force on" will enable the waffle flag for all users in an org's courses,
overriding any behavior configured on the waffle flag itself. "Force off" will disable the waffle flag
for all users in a org's courses, overriding any behavior configured on the waffle flag itself. Requires
"Enabled" (see below) to apply.
Enabled: Must be marked as "enabled" in order for the override to be applied. These settings can't be
deleted, so instead, you need to add another disabled override entry to disable the override.
"""
def _get_course_override_value(self, course_key):
"""
Returns True/False if the flag was forced on or off for the provided course. Returns None if the flag was not
overridden.
Note: Has side effect of caching the override value.
Arguments:
course_key (CourseKey): The course to check for override before checking waffle.
"""
# Import is placed here to avoid model import at project startup.
from .models import WaffleFlagCourseOverrideModel, WaffleFlagOrgOverrideModel
course_cache_key = f"{self.name}.cwaffle.{str(course_key)}"
course_override = self.cached_flags().get(course_cache_key)
if course_override is None:
course_override = WaffleFlagCourseOverrideModel.override_value(
self.name, course_key
)
self.cached_flags()[course_cache_key] = course_override
if course_override == WaffleFlagCourseOverrideModel.ALL_CHOICES.on:
return True
if course_override == WaffleFlagCourseOverrideModel.ALL_CHOICES.off:
return False
# Since no course-specific override was found, fall back to checking at the org-level.
if course_key:
org = course_key.org
org_cache_key = f"{self.name}.owaffle.{org}"
org_override = self.cached_flags().get(org_cache_key)
if org_override is None:
org_override = WaffleFlagOrgOverrideModel.override_value(
self.name, org
)
self.cached_flags()[org_cache_key] = org_override
if org_override == WaffleFlagOrgOverrideModel.ALL_CHOICES.on:
return True
if org_override == WaffleFlagOrgOverrideModel.ALL_CHOICES.off:
return False
return None
def is_enabled(self, course_key=None): # pylint: disable=arguments-differ
"""
Returns whether or not the flag is enabled within the context of a given course.
Arguments:
course_key (Optional[CourseKey]): The course to check for override before
checking waffle. If omitted, check whether the flag is enabled
outside the context of any course.
"""
if course_key:
assert isinstance(
course_key, CourseKey
), "Provided course_key '{}' is not instance of CourseKey.".format(
course_key
)
is_enabled_for_course = self._get_course_override_value(course_key)
if is_enabled_for_course is not None:
return is_enabled_for_course
return super().is_enabled()

View File

@@ -3,123 +3,30 @@ Extra utilities for waffle: most classes are defined in edx_toggles.toggles (htt
we keep here some extra classes for usage within edx-platform. These classes cover course override use cases.
"""
import logging
import warnings
from contextlib import contextmanager
from edx_django_utils.monitoring import set_custom_attribute
from edx_toggles.toggles import (
LegacyWaffleFlag,
LegacyWaffleFlagNamespace,
LegacyWaffleSwitch,
LegacyWaffleSwitchNamespace,
)
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag
log = logging.getLogger(__name__)
class CourseWaffleFlag(LegacyWaffleFlag):
class CourseWaffleFlag(FutureCourseWaffleFlag):
"""
Represents a single waffle flag that can be forced on/off for a course. This class should be used instead of
WaffleFlag when in the context of a course. This class will also respect any org-level overrides, though
course-level overrides will take precedence.
Uses a cached waffle namespace.
Usage:
SOME_COURSE_FLAG = CourseWaffleFlag('my_namespace', 'some_course_feature', __name__)
And then we can check this flag in code with::
SOME_COURSE_FLAG.is_enabled(course_key)
To configure a course-level override, go to Django Admin "waffle_utils" -> "Waffle flag course overrides".
Waffle flag: Set this to the flag name (e.g. my_namespace.some_course_feature).
Course id: Set this to the course id (e.g. course-v1:edx+100+Demo)
Override choice: (Force on/Force off). "Force on" will enable the waffle flag for all users in a course,
overriding any behavior configured on the waffle flag itself. "Force off" will disable the waffle flag
for all users in a course, overriding any behavior configured on the waffle flag itself. Requires
"Enabled" (see below) to apply.
Enabled: Must be marked as "enabled" in order for the override to be applied. These settings can't be
deleted, so instead, you need to add another disabled override entry to disable the override.
To configure an org-level override, go to Django Admin "waffle_utils" -> "Waffle flag org overrides".
Waffle flag: Set this to the flag name (e.g. my_namespace.some_course_feature).
Org name: Set this to the organization name (e.g. edx)
Override choice: (Force on/Force off). "Force on" will enable the waffle flag for all users in an org's courses,
overriding any behavior configured on the waffle flag itself. "Force off" will disable the waffle flag
for all users in a org's courses, overriding any behavior configured on the waffle flag itself. Requires
"Enabled" (see below) to apply.
Enabled: Must be marked as "enabled" in order for the override to be applied. These settings can't be
deleted, so instead, you need to add another disabled override entry to disable the override.
Represents a single waffle flag that can be forced on/off for a course.
Deprecated: use the FutureCourseWaffleFlag instead.
"""
def __init__(self, waffle_namespace, flag_name, module_name=None):
log_prefix = ""
if not isinstance(waffle_namespace, str):
log_prefix = waffle_namespace.log_prefix or log_prefix
waffle_namespace = waffle_namespace.name
def _get_course_override_value(self, course_key):
"""
Returns True/False if the flag was forced on or off for the provided course. Returns None if the flag was not
overridden.
Note: Has side effect of caching the override value.
Arguments:
course_key (CourseKey): The course to check for override before checking waffle.
"""
# Import is placed here to avoid model import at project startup.
from .models import WaffleFlagCourseOverrideModel, WaffleFlagOrgOverrideModel
course_cache_key = f"{self.name}.cwaffle.{str(course_key)}"
course_override = self.cached_flags().get(course_cache_key)
if course_override is None:
course_override = WaffleFlagCourseOverrideModel.override_value(
self.name, course_key
)
self.cached_flags()[course_cache_key] = course_override
if course_override == WaffleFlagCourseOverrideModel.ALL_CHOICES.on:
return True
if course_override == WaffleFlagCourseOverrideModel.ALL_CHOICES.off:
return False
# Since no course-specific override was found, fall back to checking at the org-level.
if course_key:
org = course_key.org
org_cache_key = f"{self.name}.owaffle.{org}"
org_override = self.cached_flags().get(org_cache_key)
if org_override is None:
org_override = WaffleFlagOrgOverrideModel.override_value(
self.name, org
)
self.cached_flags()[org_cache_key] = org_override
if org_override == WaffleFlagOrgOverrideModel.ALL_CHOICES.on:
return True
if org_override == WaffleFlagOrgOverrideModel.ALL_CHOICES.off:
return False
return None
def is_enabled(self, course_key=None): # pylint: disable=arguments-differ
"""
Returns whether or not the flag is enabled within the context of a given course.
Arguments:
course_key (Optional[CourseKey]): The course to check for override before
checking waffle. If omitted, check whether the flag is enabled
outside the context of any course.
"""
if course_key:
assert isinstance(
course_key, CourseKey
), "Provided course_key '{}' is not instance of CourseKey.".format(
course_key
)
is_enabled_for_course = self._get_course_override_value(course_key)
if is_enabled_for_course is not None:
return is_enabled_for_course
return super().is_enabled()
# Non-namespaced flag_name attribute preserved for backward compatibility
self._flag_name = flag_name
name = f"{waffle_namespace}.{flag_name}"
super().__init__(name, module_name=module_name, log_prefix=log_prefix)
set_custom_attribute(
"deprecated_legacy_waffle_class",
f"{self.__class__.__module__}.{self.__class__.__name__}[{self.name}]"
)

View File

@@ -4,6 +4,7 @@ Tests for waffle utils features.
# pylint: disable=toggle-missing-annotation
from unittest.mock import patch
import crum
import ddt
from django.test.client import RequestFactory
@@ -11,11 +12,11 @@ from edx_django_utils.cache import RequestCache
from opaque_keys.edx.keys import CourseKey
from waffle.testutils import override_flag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag as LegacyCourseWaffleFlag
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils.models import WaffleFlagCourseOverrideModel, WaffleFlagOrgOverrideModel
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
from .. import CourseWaffleFlag
from ..models import WaffleFlagCourseOverrideModel, WaffleFlagOrgOverrideModel
@ddt.ddt
class TestCourseWaffleFlag(CacheIsolationTestCase):
@@ -33,7 +34,7 @@ class TestCourseWaffleFlag(CacheIsolationTestCase):
TEST_COURSE_KEY = CourseKey.from_string(f"{TEST_ORG}/DemoX/Demo_Course")
TEST_COURSE_2_KEY = CourseKey.from_string(f"{TEST_ORG}/DemoX/Demo_Course_2")
TEST_COURSE_3_KEY = CourseKey.from_string("CollegeX/DemoX/Demo_Course")
TEST_COURSE_FLAG = CourseWaffleFlag(NAMESPACE_NAME, FLAG_NAME, __name__)
TEST_COURSE_FLAG = CourseWaffleFlag(NAMESPACED_FLAG_NAME, __name__)
def setUp(self):
super().setUp()
@@ -75,6 +76,44 @@ class TestCourseWaffleFlag(CacheIsolationTestCase):
# course which should get the default value of False.
assert self.TEST_COURSE_FLAG.is_enabled(self.TEST_COURSE_2_KEY) is False
@ddt.data(
(False, WaffleFlagCourseOverrideModel.ALL_CHOICES.on, True),
(True, WaffleFlagCourseOverrideModel.ALL_CHOICES.off, False),
(True, WaffleFlagCourseOverrideModel.ALL_CHOICES.unset, True),
(False, WaffleFlagCourseOverrideModel.ALL_CHOICES.unset, False),
)
@ddt.unpack
def test_legacy_course_waffle_flag(self, waffle_enabled, course_override, result):
"""
Tests various combinations of a legacy flag being set in waffle and overridden for a course.
"""
test_legacy_course_flag = LegacyCourseWaffleFlag(
self.NAMESPACE_NAME,
self.FLAG_NAME,
__name__,
)
with patch.object(WaffleFlagCourseOverrideModel, 'override_value', return_value=course_override):
with override_flag(self.NAMESPACED_FLAG_NAME, active=waffle_enabled):
# check twice to test that the result is properly cached
assert test_legacy_course_flag.is_enabled(self.TEST_COURSE_KEY) == result
assert test_legacy_course_flag.is_enabled(self.TEST_COURSE_KEY) == result
# result is cached, so override check should happen only once
# pylint: disable=no-member
WaffleFlagCourseOverrideModel.override_value.assert_called_once_with(
self.NAMESPACED_FLAG_NAME,
self.TEST_COURSE_KEY
)
# check flag for a second course
if course_override == WaffleFlagCourseOverrideModel.ALL_CHOICES.unset:
# When course override wasn't set for the first course, the second course will get the same
# cached value from waffle.
assert test_legacy_course_flag.is_enabled(self.TEST_COURSE_2_KEY) == waffle_enabled
else:
# When course override was set for the first course, it should not apply to the second
# course which should get the default value of False.
assert test_legacy_course_flag.is_enabled(self.TEST_COURSE_2_KEY) is False
@ddt.data(
(False, WaffleFlagOrgOverrideModel.ALL_CHOICES.unset, False),
(True, WaffleFlagOrgOverrideModel.ALL_CHOICES.unset, True),
@@ -196,11 +235,7 @@ class TestCourseWaffleFlag(CacheIsolationTestCase):
"""
Test flag with undefined waffle flag.
"""
test_course_flag = CourseWaffleFlag(
self.NAMESPACE_NAME,
self.FLAG_NAME,
__name__,
)
test_course_flag = CourseWaffleFlag(self.NAMESPACED_FLAG_NAME, __name__)
with patch.object(
WaffleFlagCourseOverrideModel,
@@ -222,11 +257,7 @@ class TestCourseWaffleFlag(CacheIsolationTestCase):
Test the flag behavior when outside a request context and waffle data undefined.
"""
crum.set_current_request(None)
test_course_flag = CourseWaffleFlag(
self.NAMESPACE_NAME,
self.FLAG_NAME,
__name__,
)
test_course_flag = CourseWaffleFlag(self.NAMESPACED_FLAG_NAME, __name__)
assert test_course_flag.is_enabled(self.TEST_COURSE_KEY) is False
def test_without_request_and_everyone_active_waffle(self):
@@ -234,10 +265,7 @@ class TestCourseWaffleFlag(CacheIsolationTestCase):
Test the flag behavior when outside a request context and waffle active for everyone.
"""
crum.set_current_request(None)
test_course_flag = CourseWaffleFlag(
self.NAMESPACE_NAME,
self.FLAG_NAME,
__name__,
)
test_course_flag = CourseWaffleFlag(self.NAMESPACED_FLAG_NAME, __name__)
with override_flag(self.NAMESPACED_FLAG_NAME, active=True):
assert test_course_flag.is_enabled(self.TEST_COURSE_KEY) is True

View File

@@ -4,28 +4,28 @@ Unified course experience settings and helper methods.
from django.urls import reverse
from django.utils.translation import gettext as _
from edx_toggles.toggles import LegacyWaffleFlag, LegacyWaffleFlagNamespace
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
from edx_toggles.toggles import WaffleFlag
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
# Namespace for course experience waffle flags.
WAFFLE_FLAG_NAMESPACE = LegacyWaffleFlagNamespace(name='course_experience')
WAFFLE_FLAG_NAMESPACE = 'course_experience'
# Waffle flag to disable the separate course outline page and full width content.
DISABLE_COURSE_OUTLINE_PAGE_FLAG = CourseWaffleFlag( # lint-amnesty, pylint: disable=toggle-missing-annotation
WAFFLE_FLAG_NAMESPACE, 'disable_course_outline_page', __name__
f'{WAFFLE_FLAG_NAMESPACE}.disable_course_outline_page', __name__
)
# Waffle flag to enable a single unified "Course" tab.
DISABLE_UNIFIED_COURSE_TAB_FLAG = CourseWaffleFlag( # lint-amnesty, pylint: disable=toggle-missing-annotation
WAFFLE_FLAG_NAMESPACE, 'disable_unified_course_tab', __name__
f'{WAFFLE_FLAG_NAMESPACE}.disable_unified_course_tab', __name__
)
# Waffle flag to enable the sock on the footer of the home and courseware pages.
DISPLAY_COURSE_SOCK_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'display_course_sock', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
DISPLAY_COURSE_SOCK_FLAG = CourseWaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.display_course_sock', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
# Waffle flag to let learners access a course before its start date.
COURSE_PRE_START_ACCESS_FLAG = LegacyWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'pre_start_access', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
COURSE_PRE_START_ACCESS_FLAG = WaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.pre_start_access', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
# .. toggle_name: course_experience.enable_course_goals
# .. toggle_implementation: CourseWaffleFlag
@@ -35,14 +35,12 @@ COURSE_PRE_START_ACCESS_FLAG = LegacyWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'pre_star
# .. toggle_creation_date: 2017-09-11
# .. toggle_target_removal_date: None
# .. toggle_warnings: This temporary feature toggle does not have a target removal date.
ENABLE_COURSE_GOALS = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'enable_course_goals', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
ENABLE_COURSE_GOALS = CourseWaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.enable_course_goals', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
# Waffle flag to enable anonymous access to a course
SEO_WAFFLE_FLAG_NAMESPACE = LegacyWaffleFlagNamespace(name='seo')
SEO_WAFFLE_FLAG_NAMESPACE = 'seo'
COURSE_ENABLE_UNENROLLED_ACCESS_FLAG = CourseWaffleFlag( # lint-amnesty, pylint: disable=toggle-missing-annotation
SEO_WAFFLE_FLAG_NAMESPACE,
'enable_anonymous_courseware_access',
__name__,
f'{SEO_WAFFLE_FLAG_NAMESPACE}.enable_anonymous_courseware_access', __name__
)
# .. toggle_name: course_experience.relative_dates
@@ -55,7 +53,7 @@ COURSE_ENABLE_UNENROLLED_ACCESS_FLAG = CourseWaffleFlag( # lint-amnesty, pylint
# .. toggle_warnings: To set a relative due date for self-paced courses, the weeks_to_complete field for a course run
# needs to be set. Currently it can be set through the publisher app.
# .. toggle_tickets: https://openedx.atlassian.net/browse/AA-27
RELATIVE_DATES_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'relative_dates', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
RELATIVE_DATES_FLAG = CourseWaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.relative_dates', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
# .. toggle_name: course_experience.calendar_sync
# .. toggle_implementation: CourseWaffleFlag
@@ -69,7 +67,7 @@ RELATIVE_DATES_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'relative_dates',
# .. toggle_creation_date: 2021-01-26
# .. toggle_target_removal_date: 2021-04-26
# .. toggle_tickets: https://openedx.atlassian.net/browse/AA-36
CALENDAR_SYNC_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'calendar_sync', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
CALENDAR_SYNC_FLAG = CourseWaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.calendar_sync', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
def course_home_page_title(_course):

View File

@@ -3,7 +3,7 @@ Miscellaneous waffle switches that both LMS and Studio need to access
"""
from edx_toggles.toggles import LegacyWaffleSwitchNamespace
from edx_toggles.toggles import WaffleSwitch
# Namespace
WAFFLE_NAMESPACE = 'course_experience'
@@ -15,18 +15,4 @@ WAFFLE_NAMESPACE = 'course_experience'
# .. toggle_description: Used to determine whether to show custom HTML in the sidebar on the internal course about page.
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2018-01-26
# TODO: Replace with WaffleSwitch(). See waffle() docstring.
ENABLE_COURSE_ABOUT_SIDEBAR_HTML = 'enable_about_sidebar_html'
def waffle():
"""
Deprecated: Returns the namespaced, cached, audited shared Waffle Switch class.
IMPORTANT: Do NOT copy this pattern and do NOT use this to reference new switches.
Instead, replace the string constant above with the actual switch instance.
For example::
ENABLE_COURSE_ABOUT_SIDEBAR_HTML = WaffleSwitch(f'{WAFFLE_NAMESPACE}.enable_about_sidebar_html')
"""
return LegacyWaffleSwitchNamespace(name=WAFFLE_NAMESPACE, log_prefix='Course Experience: ')
ENABLE_COURSE_ABOUT_SIDEBAR_HTML = WaffleSwitch(f'{WAFFLE_NAMESPACE}.enable_about_sidebar_html', __name__)

View File

@@ -15,7 +15,7 @@ import pytz
from crum import get_current_request, impersonate
from django.utils import timezone
from django.utils.dateparse import parse_datetime
from edx_toggles.toggles import LegacyWaffleFlag, LegacyWaffleFlagNamespace
from edx_toggles.toggles import WaffleFlag
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.entitlements.models import CourseEntitlement
@@ -37,11 +37,7 @@ from common.djangoapps.track import segment
# .. toggle_target_removal_date: None
# .. toggle_tickets: REVEM-282
# .. toggle_warnings: This temporary feature toggle does not have a target removal date.
DISCOUNT_APPLICABILITY_FLAG = LegacyWaffleFlag(
waffle_namespace=LegacyWaffleFlagNamespace(name='discounts'),
flag_name='enable_discounting',
module_name=__name__,
)
DISCOUNT_APPLICABILITY_FLAG = WaffleFlag('discounts.enable_discounting', __name__)
DISCOUNT_APPLICABILITY_HOLDBACK = 'first_purchase_discount_holdback'
REV1008_EXPERIMENT_ID = 16

View File

@@ -2,12 +2,10 @@
Feature toggles used for effort estimation.
"""
from edx_toggles.toggles import LegacyWaffleFlagNamespace
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
WAFFLE_FLAG_NAMESPACE = LegacyWaffleFlagNamespace(name='effort_estimation')
WAFFLE_FLAG_NAMESPACE = 'effort_estimation'
# .. toggle_name: effort_estimation.disabled
# .. toggle_implementation: CourseWaffleFlag
@@ -16,4 +14,4 @@ WAFFLE_FLAG_NAMESPACE = LegacyWaffleFlagNamespace(name='effort_estimation')
# estimates), you can turn them off case by case here.
# .. toggle_use_cases: opt_out
# .. toggle_creation_date: 2021-07-27
EFFORT_ESTIMATION_DISABLED_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'disabled', __name__)
EFFORT_ESTIMATION_DISABLED_FLAG = CourseWaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.disabled', __name__)

View File

@@ -13,7 +13,7 @@ from django.core.cache import cache
from django.urls import NoReverseMatch, reverse
from django.utils.translation import gettext as _
from edx_django_utils.cache import TieredCache, get_cache_key
from edx_toggles.toggles import LegacyWaffleFlag
from edx_toggles.toggles import WaffleFlag
from enterprise.api.v1.serializers import EnterpriseCustomerBrandingConfigurationSerializer
from enterprise.models import EnterpriseCustomer, EnterpriseCustomerUser
from social_django.models import UserSocialAuth
@@ -25,7 +25,7 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_
from openedx.core.djangoapps.user_authn.cookies import standard_cookie_settings
from openedx.core.djangolib.markup import HTML, Text
ENTERPRISE_HEADER_LINKS = LegacyWaffleFlag('enterprise', 'enterprise_header_links', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
ENTERPRISE_HEADER_LINKS = WaffleFlag('enterprise.enterprise_header_links', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation
def get_data_consent_share_cache_key(user_id, course_id, enterprise_customer_uuid=None):

View File

@@ -3,11 +3,11 @@ Toggles for Learner Profile page.
"""
from edx_toggles.toggles import LegacyWaffleFlag, LegacyWaffleFlagNamespace
from edx_toggles.toggles import WaffleFlag
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
# Namespace for learner profile waffle flags.
WAFFLE_FLAG_NAMESPACE = LegacyWaffleFlagNamespace(name='learner_profile')
WAFFLE_FLAG_NAMESPACE = 'learner_profile'
# Waffle flag to redirect to another learner profile experience.
# .. toggle_name: learner_profile.redirect_to_microfrontend
@@ -19,7 +19,7 @@ WAFFLE_FLAG_NAMESPACE = LegacyWaffleFlagNamespace(name='learner_profile')
# .. toggle_target_removal_date: 2020-12-31
# .. toggle_warnings: Also set settings.PROFILE_MICROFRONTEND_URL and site's ENABLE_PROFILE_MICROFRONTEND.
# .. toggle_tickets: DEPR-17
REDIRECT_TO_PROFILE_MICROFRONTEND = LegacyWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'redirect_to_microfrontend', __name__)
REDIRECT_TO_PROFILE_MICROFRONTEND = WaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.redirect_to_microfrontend', __name__)
def should_redirect_to_profile_microfrontend():