diff --git a/lib/main/models/__init__.py b/lib/main/models/__init__.py index f3180b8a79..e22afb9aa9 100644 --- a/lib/main/models/__init__.py +++ b/lib/main/models/__init__.py @@ -255,6 +255,21 @@ class Group(CommonModelNameNotUnique): def get_absolute_url(self): return reverse('main:groups_detail', args=(self.pk,)) + @property + def all_hosts(self): + qs = self.hosts.distinct() + for group in self.children.exclude(pk=self.pk): + qs = qs | group.all_hosts + return qs + + @property + def job_host_summaries(self): + return JobHostSummary.objects.filter(host__in=self.all_hosts) + + @property + def job_events(self): + return JobEvent.objects.filter(host__in=self.all_hosts) + # FIXME: audit nullables # FIXME: audit cascades diff --git a/lib/main/serializers.py b/lib/main/serializers.py index 5ffdfeec4d..7fc72de83d 100644 --- a/lib/main/serializers.py +++ b/lib/main/serializers.py @@ -230,6 +230,8 @@ class GroupSerializer(BaseSerializer): children = reverse('main:groups_children_list', args=(obj.pk,)), all_hosts = reverse('main:groups_all_hosts_list', args=(obj.pk,)), inventory = reverse('main:inventory_detail', args=(obj.inventory.pk,)), + job_events = reverse('main:group_job_event_list', args=(obj.pk,)), + job_host_summaries = reverse('main:group_job_host_summary_list', args=(obj.pk,)), )) return res @@ -443,8 +445,8 @@ class JobEventSerializer(BaseSerializer): class Meta: model = JobEvent - fields = ('id', 'url', 'job', 'event', 'event_data', 'failed', 'host', - 'related', 'summary_fields') + fields = ('id', 'url', 'created', 'job', 'event', 'event_data', + 'failed', 'host', 'related', 'summary_fields') def get_related(self, obj): res = super(JobEventSerializer, self).get_related(obj) diff --git a/lib/main/tests/jobs.py b/lib/main/tests/jobs.py index 211ab3f8cb..95cfc6a7d9 100644 --- a/lib/main/tests/jobs.py +++ b/lib/main/tests/jobs.py @@ -908,6 +908,16 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.TransactionTestCase): self.check_pagination_and_size(response, qs.count()) self.check_list_ids(response, qs) + # Test job event list for groups. + for group in self.inv_ops_east.groups.all(): + url = reverse('main:group_job_event_list', args=(group.pk,)) + with self.current_user(self.user_sue): + response = self.get(url) + qs = group.job_events.all() + self.assertTrue(qs.count()) + self.check_pagination_and_size(response, qs.count()) + self.check_list_ids(response, qs) + # Test global job event list. url = reverse('main:job_event_list') with self.current_user(self.user_sue): @@ -946,3 +956,13 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.TransactionTestCase): self.assertTrue(qs.count()) self.check_pagination_and_size(response, qs.count()) self.check_list_ids(response, qs) + + # Test job host summaries for groups. + for group in self.inv_ops_east.groups.all(): + url = reverse('main:group_job_host_summary_list', args=(group.pk,)) + with self.current_user(self.user_sue): + response = self.get(url) + qs = group.job_host_summaries.all() + self.assertTrue(qs.count()) + self.check_pagination_and_size(response, qs.count()) + self.check_list_ids(response, qs) diff --git a/lib/main/urls.py b/lib/main/urls.py index a62c291a9a..90c3564cc0 100644 --- a/lib/main/urls.py +++ b/lib/main/urls.py @@ -78,6 +78,8 @@ groups_urls = patterns('lib.main.views', url(r'^(?P[0-9]+)/hosts/$', 'groups_hosts_list'), url(r'^(?P[0-9]+)/all_hosts/$', 'groups_all_hosts_list'), url(r'^(?P[0-9]+)/variable_data/$', 'groups_variable_detail'), + url(r'^(?P[0-9]+)/job_events/$', 'group_job_event_list'), + url(r'^(?P[0-9]+)/job_host_summaries/$', 'group_job_host_summary_list'), ) variable_data_urls = patterns('lib.main.views', diff --git a/lib/main/views.py b/lib/main/views.py index 5c18945dd7..8a169e4bc8 100644 --- a/lib/main/views.py +++ b/lib/main/views.py @@ -799,26 +799,13 @@ class GroupsAllHostsList(BaseSubList): relationship = 'hosts' filter_fields = ('name',) - def _child_hosts(self, parent): - # TODO: should probably be a method on the model - result = parent.hosts.distinct() - if parent.children.count() == 0: - return result - else: - for child in parent.children.all(): - if child == parent: - # shouldn't happen, but be prepared in case DB is weird - continue - result = result | self._child_hosts(child) - return result - def get_queryset(self): parent = Group.objects.get(pk=self.kwargs['pk']) # FIXME: verify read permissions on this object are still required at a higher level - base = self._child_hosts(parent) + base = parent.all_hosts if self.request.user.is_superuser: return base.all() @@ -1012,37 +999,33 @@ class JobCancel(generics.RetrieveAPIView): else: return Response(status=405) -class HostJobHostSummaryList(generics.ListAPIView): +class BaseJobHostSummaryList(generics.ListAPIView): model = JobHostSummary serializer_class = JobHostSummarySerializer permission_classes = (CustomRbac,) + parent_model = None # Subclasses must define this attribute. + relationship = 'job_host_summaries' + + def get_name(self): + return 'Job Host Summary List' + + def get_queryset(self): + # FIXME: Verify read permission on the parent object and job. + parent_obj = get_object_or_404(self.parent_model, pk=self.kwargs['pk']) + return getattr(parent_obj, self.relationship) + +class HostJobHostSummaryList(BaseJobHostSummaryList): + parent_model = Host - relationship = 'job_host_summaries' - def get_name(self): - return 'Job Host Summary List' +class GroupJobHostSummaryList(BaseJobHostSummaryList): - def get_queryset(self): - # FIXME: Verify read permission on the host and job. - host = get_object_or_404(Host, pk=self.kwargs['pk']) - return host.job_host_summaries + parent_model = Group -class JobJobHostSummaryList(generics.ListAPIView): +class JobJobHostSummaryList(BaseJobHostSummaryList): - model = JobHostSummary - serializer_class = JobHostSummarySerializer - permission_classes = (CustomRbac,) parent_model = Job - relationship = 'job_host_summaries' - - def get_name(self): - return 'Job Host Summary List' - - def get_queryset(self): - # FIXME: Verify read permission on the host and job. - job = get_object_or_404(Job, pk=self.kwargs['pk']) - return job.job_host_summaries # FIXME: Subclasses of XJobHostSummaryList for failed/successful/etc. @@ -1067,36 +1050,30 @@ class JobEventDetail(generics.RetrieveAPIView): serializer_class = JobEventSerializer permission_classes = (CustomRbac,) -class JobJobEventList(BaseSubList): +class BaseJobEventList(generics.ListAPIView): model = JobEvent serializer_class = JobEventSerializer permission_classes = (CustomRbac,) - parent_model = Job + parent_model = None # Subclasses must define this attribute. relationship = 'job_events' - postable = False - severable = False def get_queryset(self): - job = get_object_or_404(Job, pk=self.kwargs['pk']) - # FIXME: Verify read permission on the job. - return job.job_events + # FIXME: Verify read permission on the parent object and job. + parent_obj = get_object_or_404(self.parent_model, pk=self.kwargs['pk']) + return getattr(parent_obj, self.relationship) -class HostJobEventList(BaseSubList): +class HostJobEventList(BaseJobEventList): - model = JobEvent - serializer_class = JobEventSerializer - permission_classes = (CustomRbac,) parent_model = Host - relationship = 'job_events' - postable = False - severable = False - def get_queryset(self): - host = get_object_or_404(Host, pk=self.kwargs['pk']) - # FIXME: Verify read permission on the host. - return host.job_events +class GroupJobEventList(BaseJobEventList): + parent_model = Group + +class JobJobEventList(BaseJobEventList): + + parent_model = Job # Create view functions for all of the class-based views to simplify inclusion # in URL patterns and reverse URL lookups, converting CamelCase names to