diff --git a/openedx/core/djangoapps/waffle_utils/tests/test_views.py b/openedx/core/djangoapps/waffle_utils/tests/test_views.py index 26410eade3..49cd95f31b 100644 --- a/openedx/core/djangoapps/waffle_utils/tests/test_views.py +++ b/openedx/core/djangoapps/waffle_utils/tests/test_views.py @@ -1,7 +1,10 @@ """ Tests for waffle utils views. """ +from django.conf import settings from django.test import TestCase +from django.test.utils import override_settings +from edx_toggles.toggles import SettingDictToggle, SettingToggle from edx_toggles.toggles.testutils import override_waffle_flag from rest_framework.test import APIRequestFactory from waffle.testutils import override_switch @@ -11,8 +14,8 @@ from student.tests.factories import UserFactory from .. import WaffleFlag, WaffleFlagNamespace from ..views import ToggleStateView -TEST_WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace('test') -TEST_WAFFLE_FLAG = WaffleFlag(TEST_WAFFLE_FLAG_NAMESPACE, 'flag', __name__) +TEST_WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace("test") +TEST_WAFFLE_FLAG = WaffleFlag(TEST_WAFFLE_FLAG_NAMESPACE, "flag", __name__) # TODO: Missing coverage for: @@ -21,7 +24,7 @@ TEST_WAFFLE_FLAG = WaffleFlag(TEST_WAFFLE_FLAG_NAMESPACE, 'flag', __name__) class ToggleStateViewTests(TestCase): def test_success_for_staff(self): - response = self._get_toggle_state_response(is_staff=True) + response = self._get_toggle_state_response() self.assertEqual(response.status_code, 200) self.assertTrue(response.data) @@ -31,19 +34,105 @@ class ToggleStateViewTests(TestCase): @override_waffle_flag(TEST_WAFFLE_FLAG, True) def test_response_with_waffle_flag(self): - response = self._get_toggle_state_response(is_staff=True) + response = self._get_toggle_state_response() self.assertIn('waffle_flags', response.data) self.assertTrue(response.data['waffle_flags']) - # This is no longer the first flag - #self.assertEqual(response.data['waffle_flags'][0]['name'], 'test.flag') + waffle_names = [waffle["name"] for waffle in response.data['waffle_flags']] + self.assertIn('test.flag', waffle_names) @override_switch('test.switch', True) def test_response_with_waffle_switch(self): - response = self._get_toggle_state_response(is_staff=True) + response = self._get_toggle_state_response() self.assertIn('waffle_switches', response.data) self.assertTrue(response.data['waffle_switches']) - # This is no longer the first switch - #self.assertEqual(response.data['waffle_switches'][0]['name'], 'test.switch') + waffle_names = [waffle["name"] for waffle in response.data['waffle_switches']] + self.assertIn('test.switch', waffle_names) + + def test_response_with_setting_toggle(self): + _toggle = SettingToggle("MYSETTING", default=False, module_name="module1") + with override_settings(MYSETTING=True): + response = self._get_toggle_state_response() + + self.assertIn( + { + "name": "MYSETTING", + "is_active": True, + "module": "module1", + "class": "SettingToggle", + }, + response.data["django_settings"], + ) + + def test_response_with_existing_setting_dict_toggle(self): + response = self._get_toggle_state_response() + self.assertIn( + { + "name": "FEATURES['MILESTONES_APP']", + "is_active": True, + "module": "util.milestones_helpers", + "class": "SettingDictToggle", + }, + response.data["django_settings"], + ) + + def test_response_with_new_setting_dict_toggle(self): + _toggle = SettingDictToggle( + "CUSTOM_FEATURES", "MYSETTING", default=False, module_name="module1" + ) + with override_settings(CUSTOM_FEATURES={"MYSETTING": True}): + response = self._get_toggle_state_response() + + setting_dict = {toggle["name"]: toggle for toggle in response.data["django_settings"]} + + self.assertEqual( + { + "name": "CUSTOM_FEATURES['MYSETTING']", + "is_active": True, + "module": "module1", + "class": "SettingDictToggle", + }, + setting_dict["CUSTOM_FEATURES['MYSETTING']"], + ) + + def test_setting_overridden_by_setting_toggle(self): + _toggle2 = SettingToggle( + "MYSETTING2", module_name="module1" + ) + _toggle3 = SettingDictToggle( + "MYDICT", "MYSETTING3", module_name="module1" + ) + with override_settings(MYSETTING1=True, MYSETTING2=False, MYDICT={"MYSETTING3": False}): + # Need to pre-load settings, otherwise they are not picked up by the view + self.assertTrue(settings.MYSETTING1) + response = self._get_toggle_state_response() + + setting_dict = {toggle["name"]: toggle for toggle in response.data["django_settings"]} + + # Check that Django settings for which a SettingToggle exists have both the correct is_active and class values + self.assertTrue(setting_dict["MYSETTING1"]["is_active"]) + self.assertNotIn("class", setting_dict["MYSETTING1"]) + self.assertFalse(setting_dict["MYSETTING2"]["is_active"]) + self.assertEqual("SettingToggle", setting_dict["MYSETTING2"]["class"]) + self.assertFalse(setting_dict["MYDICT['MYSETTING3']"]["is_active"]) + self.assertEqual("SettingDictToggle", setting_dict["MYDICT['MYSETTING3']"]["class"]) + + def test_no_duplicate_setting_toggle(self): + _toggle1 = SettingToggle( + "MYSETTING1", module_name="module1" + ) + _toggle2 = SettingDictToggle( + "MYDICT", "MYSETTING2", module_name="module1" + ) + with override_settings(MYSETTING1=True, MYDICT={"MYSETTING2": False}): + response = self._get_toggle_state_response() + + # Check there are no duplicate setting/toggle + response_toggles_1 = [toggle for toggle in response.data["django_settings"] if toggle["name"] == "MYSETTING1"] + response_toggles_2 = [ + toggle for toggle in response.data["django_settings"] if toggle["name"] == "MYDICT['MYSETTING2']" + ] + self.assertEqual(1, len(response_toggles_1)) + self.assertEqual(1, len(response_toggles_2)) def test_code_owners_without_module_information(self): # Create a waffle flag without any associated module_name diff --git a/openedx/core/djangoapps/waffle_utils/views.py b/openedx/core/djangoapps/waffle_utils/views.py index 2ce629a62e..8c409c2c5c 100644 --- a/openedx/core/djangoapps/waffle_utils/views.py +++ b/openedx/core/djangoapps/waffle_utils/views.py @@ -7,6 +7,7 @@ from django.conf import settings from edx_django_utils.monitoring import get_code_owner_from_module from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication from edx_rest_framework_extensions.permissions import IsStaff +from edx_toggles.toggles import SettingDictToggle, SettingToggle from rest_framework import permissions, views from rest_framework.authentication import SessionAuthentication from rest_framework.response import Response @@ -214,20 +215,52 @@ class ToggleStateView(views.APIView): def _get_settings_state(self): """ - Returns a dictionary of settings values. Will only return values that are set to true or false. + Return a list of setting-based toggles: Django settings, SettingToggle and SettingDictToggle instances. + SettingToggle and SettingDictToggle override the settings with identical names (if any). """ + settings_dict = {} + self._add_settings(settings_dict) + self._add_setting_toggles(settings_dict) + self._add_setting_dict_toggles(settings_dict) + return sorted(settings_dict.values(), key=(lambda toggle: toggle['name'])) - bool_settings = list() + def _add_settings(self, settings_dict): + """ + Fill the `settings_dict`: will only include values that are set to true or false. + """ for setting_name, setting_value in vars(settings).items(): if isinstance(setting_value, dict): for dict_name, dict_value in setting_value.items(): if isinstance(dict_value, bool): - bool_settings.append( - { - 'name': "{setting_name}['{dict_name}']".format(setting_name=setting_name, dict_name=dict_name), - 'is_active': dict_value, - } - ) + name = setting_dict_name(setting_name, dict_name) + toggle_response = self._get_or_create_toggle_response(settings_dict, name) + toggle_response['is_active'] = dict_value elif isinstance(setting_value, bool): - bool_settings.append({'name': setting_name, 'is_active': setting_value}) - return bool_settings + toggle_response = self._get_or_create_toggle_response(settings_dict, setting_name) + toggle_response['is_active'] = setting_value + + def _add_setting_toggles(self, settings_dict): + """ + Fill the `settings_dict` with values from the list of SettingToggle instances. + """ + for toggle in SettingToggle.get_instances(): + toggle_response = self._get_or_create_toggle_response(settings_dict, toggle.name) + toggle_response["is_active"] = toggle.is_enabled() + self._add_toggle_instance_details(toggle_response, toggle) + + def _add_setting_dict_toggles(self, settings_dict): + """ + Fill the `settings_dict` with values from the list of SettingDictToggle instances. + """ + for toggle in SettingDictToggle.get_instances(): + name = setting_dict_name(toggle.name, toggle.key) + toggle_response = self._get_or_create_toggle_response(settings_dict, name) + toggle_response["is_active"] = toggle.is_enabled() + self._add_toggle_instance_details(toggle_response, toggle) + + +def setting_dict_name(dict_name, key): + """ + Return the name associated to a `dict_name[key]` setting. + """ + return "{dict_name}['{key}']".format(dict_name=dict_name, key=key)