diff --git a/awx/api/serializers.py b/awx/api/serializers.py index ef6f08ebd0..f993a49c56 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2983,13 +2983,16 @@ class JobTemplateMixin(object): ''' def _recent_jobs(self, obj): - job_mgr = obj.unifiedjob_unified_jobs.non_polymorphic().exclude(job__job_slice_count__gt=1).only( - 'id', 'status', 'finished', 'polymorphic_ctype_id') - type_mapping = {} - for model, ct in ContentType.objects.get_for_models(*UnifiedJob.__subclasses__()).iteritems(): - type_mapping[ct.pk] = model._meta.verbose_name - return [{'id': x.id, 'status': x.status, 'finished': x.finished, 'type': type_mapping[x.polymorphic_ctype_id]} - for x in job_mgr.order_by('-created')[:10]] + # Exclude "joblets", jobs that ran as part of a sliced workflow job + uj_qs = obj.unifiedjob_unified_jobs.exclude(job__job_slice_count__gt=1).order_by('-created') + # Would like to apply an .only, but does not play well with non_polymorphic + # .only('id', 'status', 'finished', 'polymorphic_ctype_id') + optimized_qs = uj_qs.non_polymorphic() + return [{ + 'id': x.id, 'status': x.status, 'finished': x.finished, + # Make type consistent with API top-level key, for instance workflow_job + 'type': x.get_real_instance_class()._meta.verbose_name.replace(' ', '_') + } for x in optimized_qs[:10]] def get_summary_fields(self, obj): d = super(JobTemplateMixin, self).get_summary_fields(obj) diff --git a/awx/main/access.py b/awx/main/access.py index 30e704d195..ffee1a04f0 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1276,6 +1276,7 @@ class JobTemplateAccess(BaseAccess): 'instance_groups', 'credentials__credential_type', Prefetch('labels', queryset=Label.objects.all().order_by('name')), + Prefetch('last_job', queryset=UnifiedJob.objects.non_polymorphic()), ) def filtered_queryset(self): diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 3f041f8b33..16c82d65c2 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -697,9 +697,9 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique ) def get_absolute_url(self, request=None): - real_instance = self.get_real_instance() - if real_instance != self: - return real_instance.get_absolute_url(request=request) + RealClass = self.get_real_instance_class() + if RealClass != UnifiedJob: + return RealClass.get_absolute_url(RealClass(pk=self.pk), request=request) else: return '' diff --git a/awx/main/tests/functional/api/test_job.py b/awx/main/tests/functional/api/test_job.py index d635a35e0f..074a1bfcea 100644 --- a/awx/main/tests/functional/api/test_job.py +++ b/awx/main/tests/functional/api/test_job.py @@ -4,9 +4,11 @@ import mock from dateutil.parser import parse from dateutil.relativedelta import relativedelta from crum import impersonate +import datetime # Django rest framework from rest_framework.exceptions import PermissionDenied +from django.utils import timezone # AWX from awx.api.versioning import reverse @@ -122,6 +124,31 @@ def test_job_relaunch_on_failed_hosts(post, inventory, project, machine_credenti assert r.data.get('limit') == hosts +@pytest.mark.django_db +def test_summary_fields_recent_jobs(job_template, admin_user, get): + jobs = [] + for i in range(13): + jobs.append(Job.objects.create( + job_template=job_template, + status='failed', + created=timezone.make_aware(datetime.datetime(2017, 3, 21, 9, i)), + finished=timezone.make_aware(datetime.datetime(2017, 3, 21, 10, i)) + )) + r = get( + url = job_template.get_absolute_url(), + user = admin_user, + exepect = 200 + ) + recent_jobs = r.data['summary_fields']['recent_jobs'] + assert len(recent_jobs) == 10 + assert recent_jobs == [{ + 'id': job.id, + 'status': 'failed', + 'finished': job.finished, + 'type': 'job' + } for job in jobs[-10:][::-1]] + + @pytest.mark.django_db def test_slice_jt_recent_jobs(slice_job_factory, admin_user, get): workflow_job = slice_job_factory(3, spawn=True) @@ -132,10 +159,9 @@ def test_slice_jt_recent_jobs(slice_job_factory, admin_user, get): expect=200 ) job_ids = [entry['id'] for entry in r.data['summary_fields']['recent_jobs']] - assert workflow_job.pk not in job_ids - for node in workflow_job.workflow_nodes.all(): - job = node.job - assert job.pk in job_ids + # decision is that workflow job should be shown in the related jobs + # joblets of the workflow job should NOT be shown + assert job_ids == [workflow_job.pk] @pytest.mark.django_db diff --git a/awx/main/tests/unit/api/serializers/test_job_template_serializers.py b/awx/main/tests/unit/api/serializers/test_job_template_serializers.py index 8b02c5ef8b..dee880f416 100644 --- a/awx/main/tests/unit/api/serializers/test_job_template_serializers.py +++ b/awx/main/tests/unit/api/serializers/test_job_template_serializers.py @@ -69,35 +69,17 @@ class TestJobTemplateSerializerGetRelated(): class TestJobTemplateSerializerGetSummaryFields(): - def test__recent_jobs(self, mocker, job_template, jobs): - - job_template.unifiedjob_unified_jobs = mocker.MagicMock(**{ - 'non_polymorphic.return_value': mocker.MagicMock(**{ - 'only.return_value': mocker.MagicMock(**{ - 'order_by.return_value': jobs - }) - }) - }) - - serializer = JobTemplateSerializer() - recent_jobs = serializer._recent_jobs(job_template) - - job_template.unifiedjob_unified_jobs.non_polymorphic.assert_called_once_with() - job_template.unifiedjob_unified_jobs.non_polymorphic().only().order_by.assert_called_once_with('-created') - - job_template.jobs.all.assert_called_once_with() - job_template.jobs.all.order_by.assert_called_once_with('-created') - assert len(recent_jobs) == 10 - for x in jobs[:10]: - assert recent_jobs == [{'id': x.id, 'status': x.status, 'finished': x.finished} for x in jobs[:10]] - def test_survey_spec_exists(self, test_get_summary_fields, mocker, job_template): job_template.survey_spec = {'name': 'blah', 'description': 'blah blah'} - test_get_summary_fields(JobTemplateSerializer, job_template, 'survey') + with mocker.patch.object(JobTemplateSerializer, '_recent_jobs') as mock_rj: + mock_rj.return_value = [] + test_get_summary_fields(JobTemplateSerializer, job_template, 'survey') - def test_survey_spec_absent(self, get_summary_fields_mock_and_run, job_template): + def test_survey_spec_absent(self, get_summary_fields_mock_and_run, mocker, job_template): job_template.survey_spec = None - summary = get_summary_fields_mock_and_run(JobTemplateSerializer, job_template) + with mocker.patch.object(JobTemplateSerializer, '_recent_jobs') as mock_rj: + mock_rj.return_value = [] + summary = get_summary_fields_mock_and_run(JobTemplateSerializer, job_template) assert 'survey' not in summary def test_copy_edit_standard(self, mocker, job_template_factory): diff --git a/awx/ui/client/src/smart-status/smart-status.controller.js b/awx/ui/client/src/smart-status/smart-status.controller.js index 5e88e97c02..5eb451cd71 100644 --- a/awx/ui/client/src/smart-status/smart-status.controller.js +++ b/awx/ui/client/src/smart-status/smart-status.controller.js @@ -26,7 +26,7 @@ export default ['$scope', '$filter', 'i18n', const finished = $filter('longDate')(job.finished) || job.status+""; // We now get the job type of recent jobs associated with a JT - if (job.type === 'workflow job') { + if (job.type === 'workflow_job') { detailsBaseUrl = '/#/workflows/'; } else if (job.type === 'job') { detailsBaseUrl = '/#/jobs/playbook/';