diff --git a/awx/api/fields.py b/awx/api/fields.py index 9f9845373c..f1acaa7e72 100644 --- a/awx/api/fields.py +++ b/awx/api/fields.py @@ -7,7 +7,7 @@ from django.utils.encoding import force_text # Django REST Framework from rest_framework import serializers -__all__ = ['BooleanNullField', 'CharNullField', 'ChoiceNullField', 'EncryptedPasswordField'] +__all__ = ['BooleanNullField', 'CharNullField', 'ChoiceNullField', 'EncryptedPasswordField', 'VerbatimField'] class NullFieldMixin(object): @@ -76,5 +76,14 @@ class EncryptedPasswordField(CharNullField): return '$encrypted$' return value - - \ No newline at end of file + +class VerbatimField(serializers.Field): + ''' + Custom field that passes the value through without changes. + ''' + + def to_internal_value(self, data): + return data + + def to_representation(self, value): + return value diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 81c7ef5c8e..572e690ce6 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -44,7 +44,7 @@ from awx.main.redact import REPLACE_STR from awx.main.conf import tower_settings from awx.api.license import feature_enabled -from awx.api.fields import BooleanNullField, CharNullField, ChoiceNullField, EncryptedPasswordField +from awx.api.fields import BooleanNullField, CharNullField, ChoiceNullField, EncryptedPasswordField, VerbatimField from awx.fact.models import * # noqa @@ -2216,6 +2216,8 @@ class ActivityStreamSerializer(BaseSerializer): class TowerSettingsSerializer(BaseSerializer): + value = VerbatimField() + class Meta: model = TowerSettings fields = ('key', 'description', 'category', 'value', 'value_type', 'user') @@ -2249,14 +2251,20 @@ class TowerSettingsSerializer(BaseSerializer): manifest = settings.TOWER_SETTINGS_MANIFEST if attrs['key'] not in manifest: raise serializers.ValidationError(dict(key=["Key {0} is not a valid settings key".format(attrs['key'])])) - # TODO: Type checking/coercion, contextual validation - return super(TowerSettingsSerializer, self).validate(attrs) - def _create(self, validated_data): - current_val = TowerSettings.objects.filter(key=validated_data['key']) - if current_val.exists(): - return self.update(current_val[0], validated_data) - return super(TowerSettingsSerializer, self).create(validated_data) + if attrs['value_type'] == 'json': + attrs['value'] = json.dumps(attrs['value']) + elif attrs['value_type'] == 'list': + try: + attrs['value'] = ','.join(map(force_text, attrs['value'])) + except TypeError: + attrs['value'] = force_text(attrs['value']) + elif attrs['value_type'] == 'bool': + attrs['value'] = force_text(bool(attrs['value'])) + else: + attrs['value'] = force_text(attrs['value']) + + return super(TowerSettingsSerializer, self).validate(attrs) class AuthTokenSerializer(serializers.Serializer): diff --git a/awx/main/models/configuration.py b/awx/main/models/configuration.py index 3bd43f5649..208ccbd487 100644 --- a/awx/main/models/configuration.py +++ b/awx/main/models/configuration.py @@ -6,7 +6,7 @@ import json # Django from django.db import models -from django.utils.encoding import smart_text +from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ # Tower @@ -34,7 +34,9 @@ class TowerSettings(CreatedModifiedModel): ) description = models.TextField() category = models.CharField(max_length=128) - value = models.TextField() + value = models.TextField( + blank=True, + ) value_type = models.CharField( max_length=12, choices=SETTINGS_TYPE_CHOICES @@ -54,9 +56,12 @@ class TowerSettings(CreatedModifiedModel): elif self.value_type == 'password': converted_type = self.value elif self.value_type == 'list': - converted_type = [x.strip() for x in self.value.split(',')] + if self.value: + converted_type = [x.strip() for x in self.value.split(',')] + else: + converted_type = [] elif self.value_type == 'bool': - converted_type = smart_text(self.value).lower() in ('true', 'yes', '1') + converted_type = force_text(self.value).lower() in ('true', 'yes', '1') elif self.value_type == 'string': converted_type = self.value else: @@ -69,8 +74,11 @@ class TowerSettings(CreatedModifiedModel): if self.value_type == 'json': self.value = json.dumps(value) elif self.value_type == 'list': - self.value = ','.join(value) + try: + self.value = ','.join(map(force_text, value)) + except TypeError: + self.value = force_text(value) elif self.value_type == 'bool': - self.value = smart_text(bool(value)) + self.value = force_text(bool(value)) else: - self.value = smart_text(value) + self.value = force_text(value) diff --git a/awx/main/tests/old/settings.py b/awx/main/tests/old/settings.py index 3f08bd1a7a..f0d7cf63ac 100644 --- a/awx/main/tests/old/settings.py +++ b/awx/main/tests/old/settings.py @@ -35,6 +35,13 @@ TEST_TOWER_SETTINGS_MANIFEST = { "default": ["A", "Simple", "List"], "type": "list", "category": "test" + }, + "TEST_SETTING_JSON": { + "name": "A JSON Field", + "description": "A JSON Field", + "default": {"key": "value", "otherkey": ["list", "of", "things"]}, + "type": "json", + "category": "test" } } @@ -46,7 +53,7 @@ class SettingsTest(BaseTest): self.setup_instances() self.setup_users() - def get_settings(self, expected_count=4): + def get_settings(self, expected_count=5): result = self.get(reverse('api:settings_list'), expect=200) self.assertEqual(result['count'], expected_count) return result['results'] @@ -74,21 +81,29 @@ class SettingsTest(BaseTest): with self.current_user(self.super_django_user): self.get_settings(expected_count=len(TEST_TOWER_SETTINGS_MANIFEST)) - def test_set_and_reset_settings(self): + def set_and_reset_setting(self, key, values, expected_values=()): settings_reset = reverse('api:settings_reset') + setting = self.get_individual_setting(key) + self.assertEqual(setting['value'], TEST_TOWER_SETTINGS_MANIFEST[key]['default']) + for n, value in enumerate(values): + self.set_setting(key, value) + setting = self.get_individual_setting(key) + if len(expected_values) > n: + self.assertEqual(setting['value'], expected_values[n]) + else: + self.assertEqual(setting['value'], value) + self.post(settings_reset, data={"key": key}, expect=204) + setting = self.get_individual_setting(key) + self.assertEqual(setting['value'], TEST_TOWER_SETTINGS_MANIFEST[key]['default']) + + def test_set_and_reset_settings(self): with self.current_user(self.super_django_user): - # Set and reset a single setting - setting_int = self.get_individual_setting('TEST_SETTING_INT') - self.assertEqual(setting_int['value'], TEST_TOWER_SETTINGS_MANIFEST['TEST_SETTING_INT']['default']) - self.set_setting('TEST_SETTING_INT', 2) - setting_int = self.get_individual_setting('TEST_SETTING_INT') - self.assertEqual(setting_int['value'], 2) - self.set_setting('TEST_SETTING_INT', 3) - setting_int = self.get_individual_setting('TEST_SETTING_INT') - self.assertEqual(setting_int['value'], 3) - self.post(settings_reset, data={"key": 'TEST_SETTING_INT'}, expect=204) - setting_int = self.get_individual_setting('TEST_SETTING_INT') - self.assertEqual(setting_int['value'], TEST_TOWER_SETTINGS_MANIFEST['TEST_SETTING_INT']['default']) + self.set_and_reset_setting('TEST_SETTING_INT', (2, 0)) + self.set_and_reset_setting('TEST_SETTING_STRING', ('blah', '', u'\u2620')) + self.set_and_reset_setting('TEST_SETTING_BOOL', (True, False)) + # List values are always saved as strings. + self.set_and_reset_setting('TEST_SETTING_LIST', ([4, 5, 6], [], [2]), (['4', '5', '6'], [], ['2'])) + self.set_and_reset_setting('TEST_SETTING_JSON', ({"k": "v"}, {}, [], [7, 8], 'str')) def test_clear_all_settings(self): settings_list = reverse('api:settings_list') @@ -97,6 +112,7 @@ class SettingsTest(BaseTest): self.set_setting('TEST_SETTING_STRING', "foo") self.set_setting('TEST_SETTING_BOOL', False) self.set_setting('TEST_SETTING_LIST', [1,2,3]) + self.set_setting('TEST_SETTING_JSON', '{"key": "new value"}') all_settings = self.get_settings() for setting_entry in all_settings: self.assertNotEqual(setting_entry['value'],