Repository: ansible/tower-cli
Pull request #259 (2017-03-07 03:47:44)

Add ability to view and add survey specs
This is done for JTs here, I need to figure out a way to do them for workflows too. Also did some work making extra_vars processing a little less terrible.

This might be a little early in development to make a PR, but I want to put it out there so people can see the direction I'm going in.

Connect #216

Also thinking might be better to call the GET command "survey"

coveralls: 
[![Coverage Status](https://coveralls.io/builds/10464549/badge)](https://coveralls.io/builds/10464549)

Coverage decreased (-0.6%) to 93.937% when pulling **c6316bc2efdb11fffb4629a574fb1635a848cc8b on AlanCoding:survey_spec** into **c5d246a1bfa5dcc56f716c7d5b0cac8acb4ad7cd on ansible:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/10491095/badge)](https://coveralls.io/builds/10491095)

Coverage decreased (-0.6%) to 93.937% when pulling **b2db7f62296e5f67495a184ae15b47eab1cc5b83 on AlanCoding:survey_spec** into **c5d246a1bfa5dcc56f716c7d5b0cac8acb4ad7cd on ansible:master**.


Patch:
diff --git a/tower_cli/models/__init__.py b/tower_cli/models/__init__.py
index 4ee9f62..ff64af4 100644
--- a/tower_cli/models/__init__.py
+++ b/tower_cli/models/__init__.py
@@ -18,6 +18,6 @@
 from __future__ import absolute_import, unicode_literals
 
 from tower_cli.models.base import Resource, MonitorableResource, ExeResource, \
-                                  BaseResource
+                                  BaseResource, SurveyResource
 from tower_cli.models.fields import Field
 from tower_cli.utils.types import File
diff --git a/tower_cli/models/base.py b/tower_cli/models/base.py
index 37918cc..d4707fa 100644
--- a/tower_cli/models/base.py
+++ b/tower_cli/models/base.py
@@ -36,12 +36,10 @@ from tower_cli import resources
 from tower_cli.api import client
 from tower_cli.conf import settings
 from tower_cli.models.fields import Field
-from tower_cli.utils import exceptions as exc
+from tower_cli.utils import exceptions as exc, parser, debug, secho
 from tower_cli.utils.command import Command
-from tower_cli.utils import debug, secho
 from tower_cli.utils.data_structures import OrderedDict
 from tower_cli.utils.decorators import command
-from tower_cli.utils.parser import ordered_dump
 
 
 class ResourceMeta(type):
@@ -337,8 +335,8 @@ class BaseResource(six.with_metaclass(ResourceMeta)):
                 """Convert the payload into a YAML string with proper
                 indentation and return it.
                 """
-                return ordered_dump(payload, Dumper=yaml.SafeDumper,
-                                    default_flow_style=False)
+                return parser.ordered_dump(payload, Dumper=yaml.SafeDumper,
+                                           default_flow_style=False)
 
             def _format_human(self, payload):
                 """Convert the payload into an ASCII table suitable for
@@ -1208,3 +1206,45 @@ class Resource(ResourceMethods):
         """
         return self.write(pk, create_on_missing=create_on_missing,
                           force_on_exists=True, **kwargs)
+
+
+class SurveyResource(Resource):
+    """Contains utilities and commands common to "job template" models,
+    which take extra_vars and have a survey_spec."""
+    abstract = True
+
+    def _survey_endpoint(self, pk):
+        return '{0}{1}/survey_spec/'.format(self.endpoint, pk)
+
+    def write(self, pk=None, **kwargs):
+        survey_input = kwargs.pop('survey_spec', None)
+        if kwargs.get('extra_vars', None):
+            kwargs['extra_vars'] = parser.process_extra_vars(
+                kwargs['extra_vars'])
+        ret = super(SurveyResource, self).write(pk=pk, **kwargs)
+        if survey_input is not None and ret.get('id', None):
+            if not isinstance(survey_input, dict):
+                survey_input = json.loads(survey_input.strip(' '))
+            if survey_input == {}:
+                debug.log('Deleting the survey_spec.', header='details')
+                r = client.delete(self._survey_endpoint(ret['id']))
+            else:
+                debug.log('Saving the survey_spec.', header='details')
+                r = client.post(self._survey_endpoint(ret['id']),
+                                data=survey_input)
+            if r.status_code == 200:
+                ret['changed'] = True
+            if survey_input and not ret['survey_enabled']:
+                debug.log('For survey to take effect, set survey_enabled'
+                          ' field to True.', header='warning')
+        return ret
+
+    @resources.command
+    def survey(self, pk=None, **kwargs):
+        """Get the survey_spec for the job template.
+        To write a survey, use the modify command with the --survey-spec
+        parameter."""
+        job_template = self.get(pk=pk, **kwargs)
+        if settings.format == 'human':
+            settings.format = 'json'
+        return client.get(self._survey_endpoint(job_template['id'])).json()
diff --git a/tower_cli/resources/job_template.py b/tower_cli/resources/job_template.py
index 3e1abb5..1da228b 100644
--- a/tower_cli/resources/job_template.py
+++ b/tower_cli/resources/job_template.py
@@ -19,10 +19,9 @@ import click
 
 from tower_cli import models, resources
 from tower_cli.utils import types
-from tower_cli.utils import parser
 
 
-class Resource(models.Resource):
+class Resource(models.SurveyResource):
     cli_help = 'Manage job templates.'
     endpoint = '/job_templates/'
 
@@ -61,7 +60,10 @@ class Resource(models.Resource):
     )
     job_tags = models.Field(required=False, display=False)
     skip_tags = models.Field(required=False, display=False)
-    extra_vars = models.Field(required=False, display=False)
+    extra_vars = models.Field(
+        type=types.Variables(), required=False, display=False, multiple=True,
+        help_text='Extra variables used by Ansible in YAML or key=value '
+                  'format. Use @ to get YAML from a file.')
     host_config_key = models.Field(
         required=False, display=False,
         help_text='Allow Provisioning Callbacks using this host config key')
@@ -89,22 +91,18 @@ class Resource(models.Resource):
     become_enabled = models.Field(type=bool, required=False, display=False)
     timeout = models.Field(type=int, required=False, display=False,
                            help_text='The timeout field (in seconds).')
+    survey_enabled = models.Field(
+        type=bool, required=False, display=False,
+        help_text='Prompt user for job type on launch.')
+    survey_spec = models.Field(
+        type=types.Variables(), required=False, display=False,
+        help_text='On write commands, perform extra POST to the '
+                  'survey_spec endpoint.')
 
     @resources.command
-    @click.option('--extra-vars', required=False, multiple=True,
-                  help='Extra variables used by Ansible in YAML or key=value '
-                       'format. Use @ to get YAML from a file.')
     def create(self, fail_on_found=False, force_on_exists=False,
                extra_vars=None, **kwargs):
-        """Create a job template.
-        You may include multiple --extra-vars flags in order to combine
-        different sources of extra variables. Start this
-        with @ in order to indicate a filename."""
-        if extra_vars:
-            # combine sources of extra variables, if given
-            kwargs['extra_vars'] = parser.process_extra_vars(
-                extra_vars, force_json=False
-            )
+        """Create a job template."""
         # Provide a default value for job_type, but only in creation of JT
         if not kwargs.get('job_type', False):
             kwargs['job_type'] = 'run'
@@ -113,25 +111,6 @@ class Resource(models.Resource):
             **kwargs
         )
 
-    @resources.command
-    @click.option('--extra-vars', required=False, multiple=True,
-                  help='Extra variables used by Ansible in YAML or key=value '
-                       'format. Use @ to get YAML from a file.')
-    def modify(self, pk=None, create_on_missing=False,
-               extra_vars=None, **kwargs):
-        """Modify a job template.
-        You may include multiple --extra-vars flags in order to combine
-        different sources of extra variables. Start this
-        with @ in order to indicate a filename."""
-        if extra_vars:
-            # combine sources of extra variables, if given
-            kwargs['extra_vars'] = parser.process_extra_vars(
-                extra_vars, force_json=False
-            )
-        return super(Resource, self).modify(
-            pk=pk, create_on_missing=create_on_missing, **kwargs
-        )
-
     @resources.command(use_fields_as_options=False)
     @click.option('--job-template', type=types.Related('job_template'))
     @click.option('--label', type=types.Related('label'))
diff --git a/tower_cli/resources/workflow.py b/tower_cli/resources/workflow.py
index d51da8e..b40e62d 100644
--- a/tower_cli/resources/workflow.py
+++ b/tower_cli/resources/workflow.py
@@ -25,18 +25,26 @@ from tower_cli.resources.node import NODE_STANDARD_FIELDS, JOB_TYPES
 import click
 
 
-class Resource(models.Resource):
+class Resource(models.SurveyResource):
     cli_help = 'Manage workflow job templates.'
     endpoint = '/workflow_job_templates/'
 
     name = models.Field(unique=True)
     description = models.Field(required=False, display=False)
     extra_vars = models.Field(
-        type=types.Variables(), required=False, display=False,
-        help_text='Extra playbook variables to pass to jobs run inside this '
-                  'workflow, use "@" to get from file.')
+        type=types.Variables(), required=False, display=False, multiple=True,
+        help_text='Extra variables used by Ansible in YAML or key=value '
+                  'format. Use @ to get YAML from a file. Use the option '
+                  'multiple times to add multiple extra variables')
     organization = models.Field(type=types.Related('organization'),
                                 required=False)
+    survey_enabled = models.Field(
+        type=bool, required=False, display=False,
+        help_text='Prompt user for job type on launch.')
+    survey_spec = models.Field(
+        type=types.Variables(), required=False, display=False,
+        help_text='On write commands, perform extra POST to the '
+                  'survey_spec endpoint.')
 
     @staticmethod
     def _workflow_node_structure(node_results):


Test patch:
diff --git a/tests/test_models_base.py b/tests/test_models_base.py
index c9fe613..00dd6b1 100644
--- a/tests/test_models_base.py
+++ b/tests/test_models_base.py
@@ -889,3 +889,40 @@ class MonitorableResourcesTests(unittest.TestCase):
         """
         with self.assertRaises(NotImplementedError):
             models.MonitorableResource().status(None)
+
+
+class SurveyResourceTests(unittest.TestCase):
+    """Test methods specific to survey models."""
+    def setUp(self):
+        self.res = models.SurveyResource()
+        self.res.endpoint = '/job_templates/'
+
+    def test_survey_no_op(self):
+        with mock.patch.object(models.base.ResourceMethods, 'write') as w:
+            self.res.modify(name='foobar')
+            w.assert_called_once_with(
+                create_on_missing=False, force_on_exists=True,
+                name='foobar', pk=None)
+
+    def test_survey_create(self):
+        with mock.patch.object(models.base.ResourceMethods, 'write') as w:
+            w.return_value = {'id': 42, 'survey_enabled': True}
+            survey_data = {'foobar': 'foo'}
+            with client.test_mode as t:
+                t.register_json(
+                    '/job_templates/42/survey_spec/', {},
+                    method='POST'
+                )
+                self.res.modify(survey_spec=survey_data, verbose=True)
+                self.assertEqual(t.requests[0].body, json.dumps(survey_data))
+
+    def test_survey_delete(self):
+        with mock.patch.object(models.base.ResourceMethods, 'write') as w:
+            w.return_value = {'id': 42, 'survey_enabled': True}
+            with client.test_mode as t:
+                t.register_json(
+                    '/job_templates/42/survey_spec/', {},
+                    method='DELETE'
+                )
+                self.res.modify(survey_spec={}, verbose=True)
+                self.assertEqual(t.requests[0].method, 'DELETE')