From afc0f16e582cafedecee59d5fffc3e11854e4c18 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Thu, 2 Feb 2017 11:44:42 -0500 Subject: [PATCH] add a field to OPTIONS that tells if the setting came from a .py file additionally, update help_text with a helpful message about this See: #4876 --- awx/api/metadata.py | 5 +++ awx/conf/registry.py | 8 +++++ awx/conf/settings.py | 1 + awx/conf/tests/unit/test_registry.py | 14 ++++---- awx/conf/tests/unit/test_settings.py | 53 +++++++++++++++++++++------- 5 files changed, 61 insertions(+), 20 deletions(-) diff --git a/awx/api/metadata.py b/awx/api/metadata.py index 8a3e75bdfb..6d0d49632f 100644 --- a/awx/api/metadata.py +++ b/awx/api/metadata.py @@ -32,6 +32,7 @@ class Metadata(metadata.SimpleMetadata): 'min_length', 'max_length', 'min_value', 'max_value', 'category', 'category_slug', + 'defined_in_file' ] for attr in text_attrs: @@ -156,6 +157,10 @@ class Metadata(metadata.SimpleMetadata): # For PUT/POST methods, remove read-only fields. if method in ('PUT', 'POST'): + # This value should always be False for PUT/POST, so don't + # show it (file-based read-only settings can't be updated) + meta.pop('defined_in_file', False) + if meta.pop('read_only', False): actions[method].pop(field) diff --git a/awx/conf/registry.py b/awx/conf/registry.py index dd4fdb7e6a..9ca05ccda7 100644 --- a/awx/conf/registry.py +++ b/awx/conf/registry.py @@ -116,6 +116,7 @@ class SettingsRegistry(object): placeholder = field_kwargs.pop('placeholder', empty) feature_required = field_kwargs.pop('feature_required', empty) encrypted = bool(field_kwargs.pop('encrypted', False)) + defined_in_file = bool(field_kwargs.pop('defined_in_file', False)) if getattr(field_kwargs.get('child', None), 'source', None) is not None: field_kwargs['child'].source = None field_instance = field_class(**field_kwargs) @@ -126,6 +127,13 @@ class SettingsRegistry(object): field_instance.placeholder = placeholder if feature_required is not empty: field_instance.feature_required = feature_required + field_instance.defined_in_file = defined_in_file + if field_instance.defined_in_file: + field_instance.help_text = ( + str(_('This value has been set manually in a settings file.')) + + '\n\n' + + str(field_instance.help_text) + ) field_instance.encrypted = encrypted original_field_instance = field_instance if field_class != original_field_class: diff --git a/awx/conf/settings.py b/awx/conf/settings.py index d07eccb91e..5dd6697def 100644 --- a/awx/conf/settings.py +++ b/awx/conf/settings.py @@ -205,6 +205,7 @@ class SettingsWrapper(UserSettingsHolder): if file_default != init_default and file_default is not None: logger.warning('Setting %s has been marked read-only!', key) self.registry._registry[key]['read_only'] = True + self.registry._registry[key]['defined_in_file'] = True self.__dict__['_awx_conf_init_readonly'] = True # If local preload timer has expired, check to see if another process # has already preloaded the cache and skip preloading if so. diff --git a/awx/conf/tests/unit/test_registry.py b/awx/conf/tests/unit/test_registry.py index 95db1c3c73..e8fc5a477b 100644 --- a/awx/conf/tests/unit/test_registry.py +++ b/awx/conf/tests/unit/test_registry.py @@ -25,11 +25,11 @@ def reg(request): settings = LazySettings() registry = SettingsRegistry(settings) - # @pytest.mark.readonly can be used to mark specific setting values as - # "read-only". This is analogous to manually specifying a setting on the - # filesystem (e.g., in a local_settings.py in development, or in - # /etc/tower/conf.d/.py) - defaults = request.node.get_marker('readonly') + # @pytest.mark.defined_in_file can be used to mark specific setting values + # as "defined in a settings file". This is analogous to manually + # specifying a setting on the filesystem (e.g., in a local_settings.py in + # development, or in /etc/tower/conf.d/.py) + defaults = request.node.get_marker('defined_in_file') if defaults: settings.configure(**defaults.kwargs) settings._wrapped = SettingsWrapper(settings._wrapped, @@ -280,7 +280,7 @@ def test_field_with_custom_mixin(reg): assert field.is_great() is True -@pytest.mark.readonly(AWX_SOME_SETTING='DEFAULT') +@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT') def test_default_value_from_settings(reg): reg.register( 'AWX_SOME_SETTING', @@ -293,7 +293,7 @@ def test_default_value_from_settings(reg): assert field.default == 'DEFAULT' -@pytest.mark.readonly(AWX_SOME_SETTING='DEFAULT') +@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT') def test_default_value_from_settings_with_custom_representation(reg): class LowercaseCharField(fields.CharField): diff --git a/awx/conf/tests/unit/test_settings.py b/awx/conf/tests/unit/test_settings.py index f33d27bdbc..c42e527137 100644 --- a/awx/conf/tests/unit/test_settings.py +++ b/awx/conf/tests/unit/test_settings.py @@ -40,12 +40,12 @@ def settings(request): settings = LazySettings() registry = SettingsRegistry(settings) - # @pytest.mark.readonly can be used to mark specific setting values as - # "read-only". This is analogous to manually specifying a setting on the - # filesystem (e.g., in a local_settings.py in development, or in - # /etc/tower/conf.d/.py) - readonly_marker = request.node.get_marker('readonly') - defaults = readonly_marker.kwargs if readonly_marker else {} + # @pytest.mark.defined_in_file can be used to mark specific setting values + # as "defined in a settings file". This is analogous to manually + # specifying a setting on the filesystem (e.g., in a local_settings.py in + # development, or in /etc/tower/conf.d/.py) + in_file_marker = request.node.get_marker('defined_in_file') + defaults = in_file_marker.kwargs if in_file_marker else {} defaults['DEFAULTS_SNAPSHOT'] = {} settings.configure(**defaults) settings._wrapped = SettingsWrapper(settings._wrapped, @@ -54,14 +54,14 @@ def settings(request): return settings -@pytest.mark.readonly(DEBUG=True) +@pytest.mark.defined_in_file(DEBUG=True) def test_unregistered_setting(settings): "native Django settings are not stored in DB, and aren't cached" assert settings.DEBUG is True assert settings.cache.get('DEBUG') is None -@pytest.mark.readonly(AWX_SOME_SETTING='DEFAULT') +@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT') def test_read_only_setting(settings): settings.registry.register( 'AWX_SOME_SETTING', @@ -75,7 +75,7 @@ def test_read_only_setting(settings): assert settings == ['AWX_SOME_SETTING'] -@pytest.mark.readonly(AWX_SOME_SETTING='DEFAULT') +@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT') def test_read_only_setting_with_empty_default(settings): settings.registry.register( 'AWX_SOME_SETTING', @@ -90,7 +90,7 @@ def test_read_only_setting_with_empty_default(settings): assert settings == ['AWX_SOME_SETTING'] -@pytest.mark.readonly(AWX_SOME_SETTING='DEFAULT') +@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT') def test_read_only_defaults_are_cached(settings): "read-only settings are stored in the cache" settings.registry.register( @@ -103,7 +103,7 @@ def test_read_only_defaults_are_cached(settings): assert settings.cache.get('AWX_SOME_SETTING') == 'DEFAULT' -@pytest.mark.readonly(AWX_SOME_SETTING='DEFAULT') +@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT') def test_cache_respects_timeout(settings): "only preload the cache every SETTING_CACHE_TIMEOUT settings" settings.registry.register( @@ -137,6 +137,33 @@ def test_default_setting(settings, mocker): assert settings.cache.get('AWX_SOME_SETTING') == 'DEFAULT' +@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT') +def test_setting_is_from_setting_file(settings, mocker): + settings.registry.register( + 'AWX_SOME_SETTING', + field_class=fields.CharField, + category=_('System'), + category_slug='system' + ) + assert settings.AWX_SOME_SETTING == 'DEFAULT' + assert settings.registry.get_setting_field('AWX_SOME_SETTING').defined_in_file is True + + +def test_setting_is_not_from_setting_file(settings, mocker): + settings.registry.register( + 'AWX_SOME_SETTING', + field_class=fields.CharField, + category=_('System'), + category_slug='system', + default='DEFAULT' + ) + + settings_to_cache = mocker.Mock(**{'order_by.return_value': []}) + with mocker.patch('awx.conf.models.Setting.objects.filter', return_value=settings_to_cache): + assert settings.AWX_SOME_SETTING == 'DEFAULT' + assert settings.registry.get_setting_field('AWX_SOME_SETTING').defined_in_file is False + + def test_empty_setting(settings, mocker): "settings with no default and no defined value are not valid" settings.registry.register( @@ -180,7 +207,7 @@ def test_setting_from_db(settings, mocker): assert settings.cache.get('AWX_SOME_SETTING') == 'FROM_DB' -@pytest.mark.readonly(AWX_SOME_SETTING='DEFAULT') +@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT') def test_read_only_setting_assignment(settings): "read-only settings cannot be overwritten" settings.registry.register( @@ -255,7 +282,7 @@ def test_db_setting_deletion(settings, mocker): assert existing_setting.delete.call_count == 1 -@pytest.mark.readonly(AWX_SOME_SETTING='DEFAULT') +@pytest.mark.defined_in_file(AWX_SOME_SETTING='DEFAULT') def test_read_only_setting_deletion(settings): "read-only settings cannot be deleted" settings.registry.register(