refactor!: update CourseWaffleFlag (#30351)

BREAKING: get rid of the LegacyWaffle-based CourseWaffleFlag.
Both CourseWaffleFlag and FutureCourseWaffleFlag now use the modern
WaffleFlag as parent class. FutureCourseWaffleFlag left to support ORA
transition to modern waffle.

Switch to the ORA version which supporting new Waffles.
This commit is contained in:
Eugene Dyudyunov
2022-05-10 22:08:59 +03:00
committed by GitHub
parent 695d23489e
commit 655e4a344f
22 changed files with 143 additions and 157 deletions

View File

@@ -6,7 +6,7 @@ waffle switches for the contentstore app.
from edx_toggles.toggles import WaffleFlag, WaffleSwitch
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
# Namespace
WAFFLE_NAMESPACE = 'studio'

View File

@@ -50,7 +50,7 @@ from openedx.core.djangoapps.video_pipeline.config.waffle import (
DEPRECATE_YOUTUBE,
ENABLE_DEVSTACK_VIDEO_UPLOADS,
)
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
from openedx.core.lib.api.view_utils import view_auth_classes
from xmodule.video_module.transcripts_utils import Transcript # lint-amnesty, pylint: disable=wrong-import-order

View File

@@ -1,7 +1,7 @@
"""
Togglable settings for Course Grading behavior
"""
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
WAFFLE_NAMESPACE = 'grades'

View File

@@ -2,7 +2,7 @@
Toggles for course home experience.
"""
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
WAFFLE_FLAG_NAMESPACE = 'course_home'

View File

@@ -4,7 +4,7 @@ Toggles for courseware in-course experience.
from edx_toggles.toggles import SettingToggle, WaffleSwitch
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
# Namespace for courseware waffle flags.
WAFFLE_FLAG_NAMESPACE = 'courseware'

View File

@@ -1,7 +1,7 @@
"""
Discussions feature toggles
"""
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
WAFFLE_FLAG_NAMESPACE = "discussions"

View File

@@ -12,7 +12,7 @@ from edx_django_utils.cache import RequestCache
from common.djangoapps.track import segment
from lms.djangoapps.experiments.stable_bucketing import stable_bucketing_hash_group
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
log = logging.getLogger(__name__)

View File

@@ -18,7 +18,7 @@ from lms.djangoapps.experiments.factories import ExperimentKeyValueFactory
from lms.djangoapps.experiments.flags import ExperimentWaffleFlag
from lms.djangoapps.experiments.testutils import override_experiment_waffle_flag
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils.models import WaffleFlagCourseOverrideModel
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order

View File

@@ -6,7 +6,7 @@ waffle switches for the Grades app.
from edx_toggles.toggles import WaffleSwitch
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
# Namespace
WAFFLE_NAMESPACE = 'grades'

View File

@@ -5,7 +5,7 @@ waffle switches for the instructor_task app.
from edx_toggles.toggles import WaffleSwitch
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
WAFFLE_NAMESPACE = 'instructor_task'

View File

@@ -57,7 +57,7 @@ from lms.djangoapps.ora_staff_grader.utils import require_params
from openedx.core.djangoapps.content.course_overviews.api import (
get_course_overview_or_none,
)
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
log = logging.getLogger(__name__)

View File

@@ -3,7 +3,7 @@ Togglable settings for Teams behavior
"""
from edx_toggles.toggles import SettingDictToggle
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
# Course Waffle inherited from edx/edx-ora2
WAFFLE_NAMESPACE = "openresponseassessment"

View File

@@ -1,7 +1,7 @@
"""
Toggles for course apps.
"""
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
#: Namespace for use by course apps for creating availability toggles
COURSE_APPS_WAFFLE_NAMESPACE = 'course_apps'

View File

@@ -3,7 +3,7 @@ This module contains various configuration settings via
waffle switches for the live app.
"""
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
WAFFLE_NAMESPACE = 'course_live'

View File

@@ -3,7 +3,7 @@ This module contains various configuration settings via
waffle switches for the discussions app.
"""
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
WAFFLE_NAMESPACE = 'discussions'

View File

@@ -5,7 +5,7 @@ for the Video Pipeline app.
from edx_toggles.toggles import WaffleFlag
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
# Videos Namespace
WAFFLE_NAMESPACE = 'videos'

View File

@@ -1,116 +1,18 @@
"""
Temporary module to switch from the LegacyWaffle* classes.
"""
import logging
from edx_django_utils.monitoring import set_custom_attribute
from edx_toggles.toggles import WaffleFlag
from opaque_keys.edx.keys import CourseKey
log = logging.getLogger(__name__)
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
class FutureCourseWaffleFlag(WaffleFlag):
class FutureCourseWaffleFlag(CourseWaffleFlag):
"""
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.
Temporary class to support ORA transition to the modern CourseWaffleFlag.
"""
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()
def __init__(self, name, module_name, log_prefix=""):
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,29 +4,115 @@ we keep here some extra classes for usage within edx-platform. These classes cov
"""
import logging
from edx_django_utils.monitoring import set_custom_attribute
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag
from edx_toggles.toggles import WaffleFlag
from opaque_keys.edx.keys import CourseKey
log = logging.getLogger(__name__)
class CourseWaffleFlag(FutureCourseWaffleFlag):
class CourseWaffleFlag(WaffleFlag):
"""
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
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.
# 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}]"
)
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):
"""
Check whether the course flag was overriden.
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

@@ -12,8 +12,8 @@ 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 import CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag
from openedx.core.djangoapps.waffle_utils.models import WaffleFlagCourseOverrideModel, WaffleFlagOrgOverrideModel
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
@@ -83,20 +83,18 @@ class TestCourseWaffleFlag(CacheIsolationTestCase):
(False, WaffleFlagCourseOverrideModel.ALL_CHOICES.unset, False),
)
@ddt.unpack
def test_legacy_course_waffle_flag(self, waffle_enabled, course_override, result):
def test_future_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.
Tests various combinations of a __future__ flag being set in waffle and overridden for a course.
"""
test_legacy_course_flag = LegacyCourseWaffleFlag(
self.NAMESPACE_NAME,
self.FLAG_NAME,
__name__,
test_future_course_flag = FutureCourseWaffleFlag(
self.NAMESPACED_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
assert test_future_course_flag.is_enabled(self.TEST_COURSE_KEY) == result
assert test_future_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(
@@ -108,11 +106,11 @@ class TestCourseWaffleFlag(CacheIsolationTestCase):
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
assert test_future_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
assert test_future_course_flag.is_enabled(self.TEST_COURSE_2_KEY) is False
@ddt.data(
(False, WaffleFlagOrgOverrideModel.ALL_CHOICES.unset, False),

View File

@@ -5,7 +5,7 @@ Unified course experience settings and helper methods.
from django.urls import reverse
from django.utils.translation import gettext as _
from edx_toggles.toggles import WaffleFlag
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
# Namespace for course experience waffle flags.

View File

@@ -2,7 +2,7 @@
Feature toggles used for effort estimation.
"""
from openedx.core.djangoapps.waffle_utils.__future__ import FutureCourseWaffleFlag as CourseWaffleFlag
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
WAFFLE_FLAG_NAMESPACE = 'effort_estimation'

View File

@@ -121,7 +121,7 @@ oauthlib # OAuth specification support for authentica
openedx-calc # Library supporting mathematical calculations for Open edX
openedx-events # Open edX Events from Hooks Extension Framework (OEP-50)
openedx-filters # Open edX Filters from Hooks Extension Framework (OEP-50)
ora2
ora2>=4.3.0
piexif # Exif image metadata manipulation, used in the profile_images app
Pillow # Image manipulation library; used for course assets, profile images, invoice PDFs, etc.
py2neo # Driver for converting Python modulestore structures to Neo4j's schema (for Coursegraph).