Repository: ansible/awx

Issue:
Discrepancy in POST response for jobs vs project & inventory updates
##### ISSUE TYPE
 - Bug Report
 - Feature Idea

##### COMPONENT NAME
 - API

##### SUMMARY
When you launch a job, the API returns the full serialization of the new job, _as if_ you performed a GET to the job detail view.

When you update a project, it returns a minimal response like `{"project_update": 42}`.

Arguably, the project update endpoint (and others) should follow suit and return the full serialization of the newly-created object.

##### STEPS TO REPRODUCE

POST to `/api/v2/job_templates/N/launch/`

POST to `/api/v2/projects/N/update/`

##### EXPECTED RESULTS

consistency

##### ACTUAL RESULTS

somewhat similar


Discussion:
This should actually be a pretty easy task as it's just sending that object through the serializer and then returning that. Here's how we do that with the job template launch: https://github.com/ansible/awx/blob/devel/awx/api/views.py#L2822

Resolving patch:
diff --git a/awx/api/views.py b/awx/api/views.py
--- a/awx/api/views.py
+++ b/awx/api/views.py
@@ -1299,8 +1299,11 @@ def post(self, request, *args, **kwargs):
             if not project_update:
                 return Response({}, status=status.HTTP_400_BAD_REQUEST)
             else:
+                data = OrderedDict()
+                data['project_update'] = project_update.id
+                data.update(ProjectUpdateSerializer(project_update, context=self.get_serializer_context()).to_representation(project_update))
                 headers = {'Location': project_update.get_absolute_url(request=request)}
-                return Response({'project_update': project_update.id},
+                return Response(data,
                                 headers=headers,
                                 status=status.HTTP_202_ACCEPTED)
         else:
@@ -2512,10 +2515,14 @@ def post(self, request, *args, **kwargs):
         successes = 0
         failures = 0
         for inventory_source in inventory.inventory_sources.exclude(source=''):
-            details = {'inventory_source': inventory_source.pk, 'status': None}
+            details = OrderedDict()
+            details['inventory_source'] = inventory_source.pk
+            details['status'] = None
             if inventory_source.can_update:
+                update = inventory_source.update()
+                details.update(InventoryUpdateSerializer(update, context=self.get_serializer_context()).to_representation(update))
                 details['status'] = 'started'
-                details['inventory_update'] = inventory_source.update().id
+                details['inventory_update'] = update.id
                 successes += 1
             else:
                 if not details.get('status'):
@@ -2656,8 +2663,10 @@ def post(self, request, *args, **kwargs):
                 return Response({}, status=status.HTTP_400_BAD_REQUEST)
             else:
                 headers = {'Location': update.get_absolute_url(request=request)}
-                return Response(dict(inventory_update=update.id),
-                                status=status.HTTP_202_ACCEPTED, headers=headers)
+                data = OrderedDict()
+                data['inventory_update'] = update.id
+                data.update(InventoryUpdateSerializer(update, context=self.get_serializer_context()).to_representation(update))
+                return Response(data, status=status.HTTP_202_ACCEPTED, headers=headers)
         else:
             return self.http_method_not_allowed(request, *args, **kwargs)
 
@@ -2733,7 +2742,7 @@ class JobTemplateDetail(RetrieveUpdateDestroyAPIView):
     always_allow_superuser = False
 
 
-class JobTemplateLaunch(RetrieveAPIView, GenericAPIView):
+class JobTemplateLaunch(RetrieveAPIView):
 
     model = JobTemplate
     metadata_class = JobTypeMetadata
@@ -2818,9 +2827,9 @@ def post(self, request, *args, **kwargs):
             return Response(data, status=status.HTTP_400_BAD_REQUEST)
         else:
             data = OrderedDict()
+            data['job'] = new_job.id
             data['ignored_fields'] = ignored_fields
             data.update(JobSerializer(new_job, context=self.get_serializer_context()).to_representation(new_job))
-            data['job'] = new_job.id
             return Response(data, status=status.HTTP_201_CREATED)
 
 
@@ -3442,9 +3451,9 @@ def post(self, request, *args, **kwargs):
         new_job.signal_start()
 
         data = OrderedDict()
+        data['workflow_job'] = new_job.id
         data['ignored_fields'] = ignored_fields
         data.update(WorkflowJobSerializer(new_job, context=self.get_serializer_context()).to_representation(new_job))
-        data['workflow_job'] = new_job.id
         return Response(data, status=status.HTTP_201_CREATED)
 
 
@@ -3676,7 +3685,9 @@ def post(self, request, *args, **kwargs):
 
         new_job = obj.create_unified_job(extra_vars=request.data.get('extra_vars', {}))
         new_job.signal_start()
-        data = dict(system_job=new_job.id)
+        data = OrderedDict()
+        data['system_job'] = new_job.id
+        data.update(SystemJobSerializer(new_job, context=self.get_serializer_context()).to_representation(new_job))
         return Response(data, status=status.HTTP_201_CREATED)
 
 
@@ -3852,7 +3863,7 @@ def post(self, request, *args, **kwargs):
             return self.http_method_not_allowed(request, *args, **kwargs)
 
 
-class JobRelaunch(RetrieveAPIView, GenericAPIView):
+class JobRelaunch(RetrieveAPIView):
 
     model = Job
     serializer_class = JobRelaunchSerializer
@@ -4481,8 +4492,11 @@ def post(self, request, *args, **kwargs):
             return Response({}, status=status.HTTP_400_BAD_REQUEST)
         else:
             send_notifications.delay([notification.id])
+            data = OrderedDict()
+            data['notification'] = notification.id
+            data.update(NotificationSerializer(notification, context=self.get_serializer_context()).to_representation(notification))
             headers = {'Location': notification.get_absolute_url(request=request)}
-            return Response({"notification": notification.id},
+            return Response(data,
                             headers=headers,
                             status=status.HTTP_202_ACCEPTED)
 


Test patch:
diff --git a/awx/main/tests/unit/api/test_views.py b/awx/main/tests/unit/api/test_views.py
--- a/awx/main/tests/unit/api/test_views.py
+++ b/awx/main/tests/unit/api/test_views.py
@@ -126,9 +126,14 @@ def exclude(self, **kwargs):
         mock_request.user.can_access.return_value = can_access
 
         with mocker.patch.object(InventoryInventorySourcesUpdate, 'get_object', return_value=obj):
-            view = InventoryInventorySourcesUpdate()
-            response = view.post(mock_request)
-            assert response.data == expected
+            with mocker.patch.object(InventoryInventorySourcesUpdate, 'get_serializer_context', return_value=None):
+                with mocker.patch('awx.api.views.InventoryUpdateSerializer') as serializer_class:
+                    serializer = serializer_class.return_value
+                    serializer.to_representation.return_value = {}
+
+                    view = InventoryInventorySourcesUpdate()
+                    response = view.post(mock_request)
+                    assert response.data == expected
 
 
 class TestHostInsights():