From 216b99264a50011cb313f5b391abeae61870acee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Behmo?= <regis@behmo.com> Date: Thu, 5 Nov 2020 09:53:47 +0100 Subject: [PATCH] Upgrade waffle classes to the new edx-toggles API Waffle classes no longer have namespaces. All features are moved to the WaffleFlag/WaffleSwitch classes. Here we use the edx_toggles.toggles.__future__ API, which is available in 1.2.0. This means that we don't have to upgrade edx-toggles. We should remove the __future__ imports as soon as we upgrade to 2.0.0. --- lms/djangoapps/experiments/flags.py | 35 ++++++++++++++++--- .../experiments/tests/test_flags.py | 9 +++++ openedx/core/djangoapps/schedules/config.py | 8 ++++- .../core/djangoapps/waffle_utils/__init__.py | 9 +++-- openedx/core/djangoapps/waffle_utils/views.py | 6 ++-- .../tests/views/test_course_home.py | 6 ++-- .../tests/views/test_course_outline.py | 1 + .../course_experience/views/course_outline.py | 4 +-- 8 files changed, 58 insertions(+), 20 deletions(-) diff --git a/lms/djangoapps/experiments/flags.py b/lms/djangoapps/experiments/flags.py index 645be723626..4d9fd09c67c 100644 --- a/lms/djangoapps/experiments/flags.py +++ b/lms/djangoapps/experiments/flags.py @@ -4,7 +4,6 @@ Feature flag support for experiments import datetime import logging -from contextlib import contextmanager import dateutil import pytz @@ -62,6 +61,7 @@ class ExperimentWaffleFlag(CourseWaffleFlag): def test_my_experiment(self): ... """ + def __init__( self, waffle_namespace, @@ -81,6 +81,33 @@ class ExperimentWaffleFlag(CourseWaffleFlag): ] self.use_course_aware_bucketing = use_course_aware_bucketing + @property + def _app_label(self): + """ + By convention, the app label associated to an experiment waffle flag is the dotted prefix of the flag name. For + example: if the flag name is "grades.my.experiment.waffle.flag", then the `_app_label` will be "grades". + This app label replaces what was formerly known as the waffle flag namespace. + """ + return self._split_name[0] + + @property + def _experiment_name(self): + """ + By convention, the app label associated to an experiment waffle flag is the first dotted suffix of the flag + name. For example: if the flag name name is "grades.my.experiment.waffle.flag", then the `_experiment_name` + will be "my.experiment.waffle.flag". + """ + return self._split_name[1] + + @property + def _split_name(self): + """ + Return the flag name prefix (before the first dot) and suffix. This raises a ValueError if the flag does not + contain a dot ".". + """ + prefix, suffix = self.name.split(".", maxsplit=1) + return prefix, suffix + def _cache_bucket(self, key, value): request_cache = RequestCache('experiments') request_cache.set(key, value) @@ -178,7 +205,7 @@ class ExperimentWaffleFlag(CourseWaffleFlag): # If we are using course-aware bucketing, then also append that course key # to `bucketing_group_name`, such that users can be hashed into different # buckets for different course-runs. - experiment_name = bucketing_group_name = self.namespaced_flag_name + experiment_name = bucketing_group_name = self.name if course_key: experiment_name += ".{}".format(course_key) if course_key and self.use_course_aware_bucketing: @@ -225,8 +252,8 @@ class ExperimentWaffleFlag(CourseWaffleFlag): event_name='edx.bi.experiment.user.bucketed', properties={ 'site': request.site.domain, - 'app_label': self.waffle_namespace.name, - 'experiment': self.flag_name, + 'app_label': self._app_label, + 'experiment': self._experiment_name, 'course_id': str(course_key) if course_key else None, 'bucket': bucket, 'is_staff': user.is_staff, diff --git a/lms/djangoapps/experiments/tests/test_flags.py b/lms/djangoapps/experiments/tests/test_flags.py index 0540a65c8a7..425fd788e23 100644 --- a/lms/djangoapps/experiments/tests/test_flags.py +++ b/lms/djangoapps/experiments/tests/test_flags.py @@ -156,6 +156,15 @@ class ExperimentWaffleFlagTests(SharedModuleStoreTestCase): self.assertEqual(self.flag.get_bucket(), expected_bucket) self.assertEqual(self.flag.is_experiment_on(), active) + 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) + class ExperimentWaffleFlagCourseAwarenessTest(SharedModuleStoreTestCase): """ diff --git a/openedx/core/djangoapps/schedules/config.py b/openedx/core/djangoapps/schedules/config.py index 30571996ea0..82c31888100 100644 --- a/openedx/core/djangoapps/schedules/config.py +++ b/openedx/core/djangoapps/schedules/config.py @@ -3,7 +3,13 @@ Contains configuration for schedules app """ -from edx_toggles.toggles import LegacyWaffleFlag, LegacyWaffleFlagNamespace, LegacyWaffleSwitch, LegacyWaffleSwitchNamespace +from edx_toggles.toggles import ( + LegacyWaffleFlag, + LegacyWaffleFlagNamespace, + LegacyWaffleSwitch, + LegacyWaffleSwitchNamespace +) + from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag WAFFLE_FLAG_NAMESPACE = LegacyWaffleFlagNamespace(name='schedules') diff --git a/openedx/core/djangoapps/waffle_utils/__init__.py b/openedx/core/djangoapps/waffle_utils/__init__.py index 2f03aea2701..09150e6ceea 100644 --- a/openedx/core/djangoapps/waffle_utils/__init__.py +++ b/openedx/core/djangoapps/waffle_utils/__init__.py @@ -131,7 +131,7 @@ class WaffleFlag(LegacyWaffleFlag): yield -class CourseWaffleFlag(BaseWaffleFlag): +class CourseWaffleFlag(LegacyWaffleFlag): """ 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. @@ -165,13 +165,13 @@ class CourseWaffleFlag(BaseWaffleFlag): from .models import WaffleFlagCourseOverrideModel cache_key = "{}.{}".format(self.namespaced_flag_name, str(course_key)) - course_override = self._cached_flags.get(cache_key) + course_override = self.cached_flags().get(cache_key) if course_override is None: course_override = WaffleFlagCourseOverrideModel.override_value( self.namespaced_flag_name, course_key ) - self._cached_flags[cache_key] = course_override + self.cached_flags()[cache_key] = course_override if course_override == WaffleFlagCourseOverrideModel.ALL_CHOICES.on: return True @@ -196,7 +196,6 @@ class CourseWaffleFlag(BaseWaffleFlag): ) is_enabled_for_course = self._get_course_override_value(course_key) if is_enabled_for_course is not None: - # pylint: disable=protected-access - self.waffle_namespace._monitor_value(self.flag_name, is_enabled_for_course) + self.set_monitor_value(is_enabled_for_course) return is_enabled_for_course return super().is_enabled() diff --git a/openedx/core/djangoapps/waffle_utils/views.py b/openedx/core/djangoapps/waffle_utils/views.py index 8c409c2c5cb..0608cf4e33a 100644 --- a/openedx/core/djangoapps/waffle_utils/views.py +++ b/openedx/core/djangoapps/waffle_utils/views.py @@ -49,8 +49,7 @@ class ToggleStateView(views.APIView): """ waffle_switch_instances = WaffleSwitch.get_instances() for switch_instance in waffle_switch_instances: - switch_name = switch_instance.namespaced_switch_name - switch = self._get_or_create_toggle_response(switches_dict, switch_name) + switch = self._get_or_create_toggle_response(switches_dict, switch_instance.name) self._add_toggle_instance_details(switch, switch_instance) def _add_waffle_switch_state(self, switches_dict): @@ -98,8 +97,7 @@ class ToggleStateView(views.APIView): """ waffle_flag_instances = WaffleFlag.get_instances() for flag_instance in waffle_flag_instances: - flag_name = flag_instance.namespaced_flag_name - flag = self._get_or_create_toggle_response(flags_dict, flag_name) + flag = self._get_or_create_toggle_response(flags_dict, flag_instance.name) self._add_toggle_instance_details(flag, flag_instance) def _add_toggle_instance_details(self, toggle, toggle_instance): diff --git a/openedx/features/course_experience/tests/views/test_course_home.py b/openedx/features/course_experience/tests/views/test_course_home.py index 9c1bf73fb87..5bf97e406b4 100644 --- a/openedx/features/course_experience/tests/views/test_course_home.py +++ b/openedx/features/course_experience/tests/views/test_course_home.py @@ -951,9 +951,9 @@ class CourseHomeFragmentViewTests(ModuleStoreTestCase): self.user = UserFactory() self.client.login(username=self.user.username, password=TEST_PASSWORD) - name = SHOW_UPGRADE_MSG_ON_COURSE_HOME.waffle_namespace._namespaced_name( - SHOW_UPGRADE_MSG_ON_COURSE_HOME.flag_name) - self.flag, __ = Flag.objects.update_or_create(name=name, defaults={'everyone': True}) + self.flag, __ = Flag.objects.update_or_create( + name=SHOW_UPGRADE_MSG_ON_COURSE_HOME.name, defaults={'everyone': True} + ) def assert_upgrade_message_not_displayed(self): response = self.client.get(self.url) diff --git a/openedx/features/course_experience/tests/views/test_course_outline.py b/openedx/features/course_experience/tests/views/test_course_outline.py index 8ac1fa5da19..28064fa4843 100644 --- a/openedx/features/course_experience/tests/views/test_course_outline.py +++ b/openedx/features/course_experience/tests/views/test_course_outline.py @@ -720,6 +720,7 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT switch_name = ENABLE_COMPLETION_TRACKING_SWITCH.name switch, _ = Switch.objects.get_or_create(name=switch_name) + # pylint: disable=protected-access self.assertEqual(switch.created, view._completion_data_collection_start()) switch.delete() diff --git a/openedx/features/course_experience/views/course_outline.py b/openedx/features/course_experience/views/course_outline.py index 80f0566bf4f..8fa266117f5 100644 --- a/openedx/features/course_experience/views/course_outline.py +++ b/openedx/features/course_experience/views/course_outline.py @@ -166,10 +166,8 @@ class CourseOutlineFragmentView(EdxFragmentView): """ Returns the date that the ENABLE_COMPLETION_TRACKING waffle switch was enabled. """ - # pylint: disable=protected-access - switch_name = completion_waffle.ENABLE_COMPLETION_TRACKING_SWITCH.name try: - return Switch.objects.get(name=switch_name).created + return Switch.objects.get(name=ENABLE_COMPLETION_TRACKING_SWITCH.name).created except Switch.DoesNotExist: return DEFAULT_COMPLETION_TRACKING_START -- GitLab