diff --git a/awx/api/serializers.py b/awx/api/serializers.py index b373e492d7..0254edcb3c 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2270,14 +2270,27 @@ class WorkflowJobTemplateNodeSerializer(WorkflowNodeBaseSerializer): def to_internal_value(self, data): internal_value = super(WorkflowNodeBaseSerializer, self).to_internal_value(data) - char_prompts = self.extract_char_prompts(data) + view = self.context.get('view', None) + request_method = None + if view and view.request: + request_method = view.request.method + if request_method in ['PATCH']: + obj = view.get_object() + char_prompts = copy.copy(obj.char_prompts) + char_prompts.update(self.extract_char_prompts(data)) + else: + char_prompts = self.extract_char_prompts(data) + for fd in copy.copy(char_prompts): + if char_prompts[fd] is None: + char_prompts.pop(fd) internal_value['char_prompts'] = char_prompts return internal_value def extract_char_prompts(self, data): char_prompts = {} for fd in ['job_type', 'job_tags', 'skip_tags', 'limit']: - if data.get(fd, None): + # Accept null values, if given + if fd in data: char_prompts[fd] = data[fd] return char_prompts diff --git a/awx/main/migrations/0037_v310_workflow_rbac_prompts.py b/awx/main/migrations/0037_v310_workflow_rbac_prompts.py index d126ac9c1d..c134ed36fc 100644 --- a/awx/main/migrations/0037_v310_workflow_rbac_prompts.py +++ b/awx/main/migrations/0037_v310_workflow_rbac_prompts.py @@ -10,7 +10,7 @@ import awx.main.fields class Migration(migrations.Migration): dependencies = [ - ('main', '0035_v310_jobevent_uuid'), + ('main', '0036_v310_remove_tower_settings'), ] operations = [ diff --git a/awx/main/tests/unit/api/serializers/test_workflow_serializers.py b/awx/main/tests/unit/api/serializers/test_workflow_serializers.py index 371b02c7b8..3d86952f8d 100644 --- a/awx/main/tests/unit/api/serializers/test_workflow_serializers.py +++ b/awx/main/tests/unit/api/serializers/test_workflow_serializers.py @@ -104,6 +104,42 @@ class TestWorkflowJobTemplateNodeSerializerGetRelated(): assert 'workflow_job_template' not in related +class FakeView: + def __init__(self, obj): + self.obj = obj + + def get_object(self): + return self.obj + +class FakeRequest: + pass + +class TestWorkflowJobTemplateNodeSerializerCharPrompts(): + + @pytest.fixture + def WFJT_serializer(self): + serializer = WorkflowJobTemplateNodeSerializer() + node = WorkflowJobTemplateNode(pk=1) + node.char_prompts = {'limit': 'webservers'} + view = FakeView(node) + view.request = FakeRequest() + view.request.method = "PATCH" + serializer.context = {'view': view} + return serializer + + def test_change_single_field(self, WFJT_serializer): + "Test that a single prompt field can be changed without affecting other fields" + internal_value = WFJT_serializer.to_internal_value({'job_type': 'check'}) + assert internal_value['char_prompts']['job_type'] == 'check' + assert internal_value['char_prompts']['limit'] == 'webservers' + + def test_null_single_field(self, WFJT_serializer): + "Test that a single prompt field can be removed without affecting other fields" + internal_value = WFJT_serializer.to_internal_value({'job_type': None}) + assert 'job_type' not in internal_value['char_prompts'] + assert internal_value['char_prompts']['limit'] == 'webservers' + + @mock.patch('awx.api.serializers.WorkflowNodeBaseSerializer.get_related', lambda x,y: {}) class TestWorkflowJobNodeSerializerGetRelated(): @pytest.fixture