diff --git a/awx/api/views.py b/awx/api/views.py index 52bad9c564..2d1044fb8f 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -612,6 +612,7 @@ class InstanceList(ListAPIView): view_name = _("Instances") model = Instance serializer_class = InstanceSerializer + search_fields = ('hostname',) class InstanceDetail(RetrieveUpdateAPIView): @@ -698,6 +699,7 @@ class InstanceGroupInstanceList(InstanceGroupMembershipMixin, SubListAttachDetac serializer_class = InstanceSerializer parent_model = InstanceGroup relationship = "instances" + search_fields = ('hostname',) class ScheduleList(ListAPIView): @@ -1074,6 +1076,7 @@ class OrganizationActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIV serializer_class = ActivityStreamSerializer parent_model = Organization relationship = 'activitystream_set' + search_fields = ('changes',) class OrganizationNotificationTemplatesList(SubListCreateAttachDetachAPIView): @@ -1128,6 +1131,7 @@ class OrganizationObjectRolesList(SubListAPIView): model = Role serializer_class = RoleSerializer parent_model = Organization + search_fields = ('role_field', 'content_type__model',) def get_queryset(self): po = self.get_parent_object() @@ -1167,6 +1171,7 @@ class TeamRolesList(SubListAttachDetachAPIView): metadata_class = RoleMetadata parent_model = Team relationship='member_role.children' + search_fields = ('role_field', 'content_type__model',) def get_queryset(self): team = get_object_or_404(Team, pk=self.kwargs['pk']) @@ -1204,6 +1209,7 @@ class TeamObjectRolesList(SubListAPIView): model = Role serializer_class = RoleSerializer parent_model = Team + search_fields = ('role_field', 'content_type__model',) def get_queryset(self): po = self.get_parent_object() @@ -1235,6 +1241,7 @@ class TeamActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): serializer_class = ActivityStreamSerializer parent_model = Team relationship = 'activitystream_set' + search_fields = ('changes',) def get_queryset(self): parent = self.get_parent_object() @@ -1330,6 +1337,7 @@ class ProjectActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): serializer_class = ActivityStreamSerializer parent_model = Project relationship = 'activitystream_set' + search_fields = ('changes',) def get_queryset(self): parent = self.get_parent_object() @@ -1417,6 +1425,7 @@ class ProjectUpdateEventsList(SubListAPIView): parent_model = ProjectUpdate relationship = 'project_update_events' view_name = _('Project Update Events List') + search_fields = ('stdout',) def finalize_response(self, request, response, *args, **kwargs): response['X-UI-Max-Events'] = settings.MAX_UI_JOB_EVENTS @@ -1430,6 +1439,7 @@ class SystemJobEventsList(SubListAPIView): parent_model = SystemJob relationship = 'system_job_events' view_name = _('System Job Events List') + search_fields = ('stdout',) def finalize_response(self, request, response, *args, **kwargs): response['X-UI-Max-Events'] = settings.MAX_UI_JOB_EVENTS @@ -1443,6 +1453,7 @@ class InventoryUpdateEventsList(SubListAPIView): parent_model = InventoryUpdate relationship = 'inventory_update_events' view_name = _('Inventory Update Events List') + search_fields = ('stdout',) def finalize_response(self, request, response, *args, **kwargs): response['X-UI-Max-Events'] = settings.MAX_UI_JOB_EVENTS @@ -1470,6 +1481,7 @@ class ProjectUpdateNotificationsList(SubListAPIView): serializer_class = NotificationSerializer parent_model = ProjectUpdate relationship = 'notifications' + search_fields = ('subject', 'notification_type', 'body',) class ProjectUpdateScmInventoryUpdates(SubListCreateAPIView): @@ -1493,6 +1505,7 @@ class ProjectObjectRolesList(SubListAPIView): model = Role serializer_class = RoleSerializer parent_model = Project + search_fields = ('role_field', 'content_type__model',) def get_queryset(self): po = self.get_parent_object() @@ -1575,6 +1588,7 @@ class OAuth2ApplicationActivityStreamList(ActivityStreamEnforcementMixin, SubLis parent_model = OAuth2Application relationship = 'activitystream_set' swagger_topic = 'Authentication' + search_fields = ('changes',) class OAuth2TokenList(ListCreateAPIView): @@ -1659,6 +1673,7 @@ class OAuth2TokenActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIVi parent_model = OAuth2AccessToken relationship = 'activitystream_set' swagger_topic = 'Authentication' + search_fields = ('changes',) class UserTeamsList(ListAPIView): @@ -1682,6 +1697,7 @@ class UserRolesList(SubListAttachDetachAPIView): parent_model = User relationship='roles' permission_classes = (IsAuthenticated,) + search_fields = ('role_field', 'content_type__model',) def get_queryset(self): u = get_object_or_404(User, pk=self.kwargs['pk']) @@ -1768,6 +1784,7 @@ class UserActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): serializer_class = ActivityStreamSerializer parent_model = User relationship = 'activitystream_set' + search_fields = ('changes',) def get_queryset(self): parent = self.get_parent_object() @@ -1854,6 +1871,7 @@ class CredentialTypeActivityStreamList(ActivityStreamEnforcementMixin, SubListAP serializer_class = ActivityStreamSerializer parent_model = CredentialType relationship = 'activitystream_set' + search_fields = ('changes',) # remove in 3.3 @@ -1967,6 +1985,7 @@ class CredentialActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIVie serializer_class = ActivityStreamSerializer parent_model = Credential relationship = 'activitystream_set' + search_fields = ('changes',) class CredentialAccessList(ResourceAccessList): @@ -1980,6 +1999,7 @@ class CredentialObjectRolesList(SubListAPIView): model = Role serializer_class = RoleSerializer parent_model = Credential + search_fields = ('role_field', 'content_type__model',) def get_queryset(self): po = self.get_parent_object() @@ -2020,6 +2040,7 @@ class InventoryScriptObjectRolesList(SubListAPIView): model = Role serializer_class = RoleSerializer parent_model = CustomInventoryScript + search_fields = ('role_field', 'content_type__model',) def get_queryset(self): po = self.get_parent_object() @@ -2107,6 +2128,7 @@ class InventoryActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView serializer_class = ActivityStreamSerializer parent_model = Inventory relationship = 'activitystream_set' + search_fields = ('changes',) def get_queryset(self): parent = self.get_parent_object() @@ -2134,6 +2156,7 @@ class InventoryObjectRolesList(SubListAPIView): model = Role serializer_class = RoleSerializer parent_model = Inventory + search_fields = ('role_field', 'content_type__model',) def get_queryset(self): po = self.get_parent_object() @@ -2277,6 +2300,7 @@ class HostActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): serializer_class = ActivityStreamSerializer parent_model = Host relationship = 'activitystream_set' + search_fields = ('changes',) def get_queryset(self): parent = self.get_parent_object() @@ -2290,6 +2314,7 @@ class HostFactVersionsList(SystemTrackingEnforcementMixin, ParentMixin, ListAPIV model = Fact serializer_class = FactVersionSerializer parent_model = Host + search_fields = ('facts',) def get_queryset(self): from_spec = self.request.query_params.get('from', None) @@ -2523,6 +2548,7 @@ class GroupActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): serializer_class = ActivityStreamSerializer parent_model = Group relationship = 'activitystream_set' + search_fields = ('changes',) def get_queryset(self): parent = self.get_parent_object() @@ -2754,6 +2780,7 @@ class InventorySourceActivityStreamList(ActivityStreamEnforcementMixin, SubListA serializer_class = ActivityStreamSerializer parent_model = InventorySource relationship = 'activitystream_set' + search_fields = ('changes',) class InventorySourceNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView): @@ -2893,6 +2920,7 @@ class InventoryUpdateNotificationsList(SubListAPIView): serializer_class = NotificationSerializer parent_model = InventoryUpdate relationship = 'notifications' + search_fields = ('subject', 'notification_type', 'body',) class JobTemplateList(ListCreateAPIView): @@ -3231,6 +3259,7 @@ class JobTemplateActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIVi serializer_class = ActivityStreamSerializer parent_model = JobTemplate relationship = 'activitystream_set' + search_fields = ('changes',) class JobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView): @@ -3514,6 +3543,7 @@ class JobTemplateObjectRolesList(SubListAPIView): model = Role serializer_class = RoleSerializer parent_model = JobTemplate + search_fields = ('role_field', 'content_type__model',) def get_queryset(self): po = self.get_parent_object() @@ -3531,6 +3561,7 @@ class WorkflowJobNodeList(WorkflowsEnforcementMixin, ListAPIView): model = WorkflowJobNode serializer_class = WorkflowJobNodeListSerializer + search_fields = ('unified_job_template__name', 'unified_job_template__description',) class WorkflowJobNodeDetail(WorkflowsEnforcementMixin, RetrieveAPIView): @@ -3551,6 +3582,7 @@ class WorkflowJobTemplateNodeList(WorkflowsEnforcementMixin, ListCreateAPIView): model = WorkflowJobTemplateNode serializer_class = WorkflowJobTemplateNodeSerializer + search_fields = ('unified_job_template__name', 'unified_job_template__description',) class WorkflowJobTemplateNodeDetail(WorkflowsEnforcementMixin, RetrieveUpdateDestroyAPIView): @@ -3572,6 +3604,7 @@ class WorkflowJobTemplateNodeChildrenBaseList(WorkflowsEnforcementMixin, Enforce parent_model = WorkflowJobTemplateNode relationship = '' enforce_parent_relationship = 'workflow_job_template' + search_fields = ('unified_job_template__name', 'unified_job_template__description',) ''' Limit the set of WorkflowJobTemplateNodes to the related nodes of specified by @@ -3641,6 +3674,7 @@ class WorkflowJobNodeChildrenBaseList(WorkflowsEnforcementMixin, SubListAPIView) serializer_class = WorkflowJobNodeListSerializer parent_model = WorkflowJobNode relationship = '' + search_fields = ('unified_job_template__name', 'unified_job_template__description',) # #Limit the set of WorkflowJobeNodes to the related nodes of specified by @@ -3796,6 +3830,7 @@ class WorkflowJobTemplateWorkflowNodesList(WorkflowsEnforcementMixin, SubListCre parent_model = WorkflowJobTemplate relationship = 'workflow_job_template_nodes' parent_key = 'workflow_job_template' + search_fields = ('unified_job_template__name', 'unified_job_template__description',) def get_queryset(self): return super(WorkflowJobTemplateWorkflowNodesList, self).get_queryset().order_by('id') @@ -3856,6 +3891,7 @@ class WorkflowJobTemplateObjectRolesList(WorkflowsEnforcementMixin, SubListAPIVi model = Role serializer_class = RoleSerializer parent_model = WorkflowJobTemplate + search_fields = ('role_field', 'content_type__model',) def get_queryset(self): po = self.get_parent_object() @@ -3869,6 +3905,7 @@ class WorkflowJobTemplateActivityStreamList(WorkflowsEnforcementMixin, ActivityS serializer_class = ActivityStreamSerializer parent_model = WorkflowJobTemplate relationship = 'activitystream_set' + search_fields = ('changes',) def get_queryset(self): parent = self.get_parent_object() @@ -3898,6 +3935,7 @@ class WorkflowJobWorkflowNodesList(WorkflowsEnforcementMixin, SubListAPIView): parent_model = WorkflowJob relationship = 'workflow_job_nodes' parent_key = 'workflow_job' + search_fields = ('unified_job_template__name', 'unified_job_template__description',) def get_queryset(self): return super(WorkflowJobWorkflowNodesList, self).get_queryset().order_by('id') @@ -3926,6 +3964,7 @@ class WorkflowJobNotificationsList(WorkflowsEnforcementMixin, SubListAPIView): serializer_class = NotificationSerializer parent_model = WorkflowJob relationship = 'notifications' + search_fields = ('subject', 'notification_type', 'body',) class WorkflowJobActivityStreamList(WorkflowsEnforcementMixin, ActivityStreamEnforcementMixin, SubListAPIView): @@ -3934,6 +3973,7 @@ class WorkflowJobActivityStreamList(WorkflowsEnforcementMixin, ActivityStreamEnf serializer_class = ActivityStreamSerializer parent_model = WorkflowJob relationship = 'activitystream_set' + search_fields = ('changes',) class SystemJobTemplateList(ListAPIView): @@ -4089,6 +4129,7 @@ class JobActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): serializer_class = ActivityStreamSerializer parent_model = Job relationship = 'activitystream_set' + search_fields = ('changes',) # TODO: remove endpoint in 3.3 @@ -4292,6 +4333,7 @@ class JobNotificationsList(SubListAPIView): serializer_class = NotificationSerializer parent_model = Job relationship = 'notifications' + search_fields = ('subject', 'notification_type', 'body',) class BaseJobHostSummariesList(SubListAPIView): @@ -4301,6 +4343,7 @@ class BaseJobHostSummariesList(SubListAPIView): parent_model = None # Subclasses must define this attribute. relationship = 'job_host_summaries' view_name = _('Job Host Summaries List') + search_fields = ('host_name',) def get_queryset(self): parent = self.get_parent_object() @@ -4333,6 +4376,7 @@ class JobEventList(ListAPIView): model = JobEvent serializer_class = JobEventSerializer + search_fields = ('stdout',) class JobEventDetail(RetrieveAPIView): @@ -4348,6 +4392,7 @@ class JobEventChildrenList(SubListAPIView): parent_model = JobEvent relationship = 'children' view_name = _('Job Event Children List') + search_fields = ('stdout',) class JobEventHostsList(HostRelatedSearchMixin, SubListAPIView): @@ -4561,6 +4606,7 @@ class AdHocCommandEventList(ListAPIView): model = AdHocCommandEvent serializer_class = AdHocCommandEventSerializer + search_fields = ('stdout',) class AdHocCommandEventDetail(RetrieveAPIView): @@ -4576,6 +4622,7 @@ class BaseAdHocCommandEventsList(SubListAPIView): parent_model = None # Subclasses must define this attribute. relationship = 'ad_hoc_command_events' view_name = _('Ad Hoc Command Events List') + search_fields = ('stdout',) class HostAdHocCommandEventsList(BaseAdHocCommandEventsList): @@ -4598,6 +4645,7 @@ class AdHocCommandActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIV serializer_class = ActivityStreamSerializer parent_model = AdHocCommand relationship = 'activitystream_set' + search_fields = ('changes',) class AdHocCommandNotificationsList(SubListAPIView): @@ -4606,6 +4654,7 @@ class AdHocCommandNotificationsList(SubListAPIView): serializer_class = NotificationSerializer parent_model = AdHocCommand relationship = 'notifications' + search_fields = ('subject', 'notification_type', 'body',) class SystemJobList(ListCreateAPIView): @@ -4646,6 +4695,7 @@ class SystemJobNotificationsList(SubListAPIView): serializer_class = NotificationSerializer parent_model = SystemJob relationship = 'notifications' + search_fields = ('subject', 'notification_type', 'body',) class UnifiedJobTemplateList(ListAPIView): @@ -4851,6 +4901,7 @@ class NotificationTemplateNotificationList(SubListAPIView): parent_model = NotificationTemplate relationship = 'notifications' parent_key = 'notification_template' + search_fields = ('subject', 'notification_type', 'body',) class NotificationTemplateCopy(CopyAPIView): @@ -4863,6 +4914,7 @@ class NotificationList(ListAPIView): model = Notification serializer_class = NotificationSerializer + search_fields = ('subject', 'notification_type', 'body',) class NotificationDetail(RetrieveAPIView): @@ -4887,6 +4939,7 @@ class ActivityStreamList(ActivityStreamEnforcementMixin, SimpleListAPIView): model = ActivityStream serializer_class = ActivityStreamSerializer + search_fields = ('changes',) class ActivityStreamDetail(ActivityStreamEnforcementMixin, RetrieveAPIView): @@ -4900,6 +4953,7 @@ class RoleList(ListAPIView): model = Role serializer_class = RoleSerializer permission_classes = (IsAuthenticated,) + search_fields = ('role_field', 'content_type__model',) def get_queryset(self): result = Role.visible_roles(self.request.user) @@ -5012,6 +5066,7 @@ class RoleParentsList(SubListAPIView): parent_model = Role relationship = 'parents' permission_classes = (IsAuthenticated,) + search_fields = ('role_field', 'content_type__model',) def get_queryset(self): role = Role.objects.get(pk=self.kwargs['pk']) @@ -5025,6 +5080,7 @@ class RoleChildrenList(SubListAPIView): parent_model = Role relationship = 'children' permission_classes = (IsAuthenticated,) + search_fields = ('role_field', 'content_type__model',) def get_queryset(self): role = Role.objects.get(pk=self.kwargs['pk']) diff --git a/awx/main/tests/unit/test_views.py b/awx/main/tests/unit/test_views.py index 6e3572dc7d..f1bad79400 100644 --- a/awx/main/tests/unit/test_views.py +++ b/awx/main/tests/unit/test_views.py @@ -3,6 +3,10 @@ import mock # Django REST Framework from rest_framework import exceptions +from rest_framework.generics import ListAPIView + +# Django +from django.core.urlresolvers import RegexURLResolver, RegexURLPattern # AWX from awx.main.views import ApiErrorView @@ -52,3 +56,44 @@ def test_disable_post_on_v1_inventory_source_list(version, supports_post): inv_source_list.request = mock.MagicMock() with mock.patch('awx.api.views.get_request_version', return_value=version): assert ('POST' in inv_source_list.allowed_methods) == supports_post + + +def test_views_have_search_fields(): + from awx.api.urls import urlpatterns as api_patterns + patterns = set([]) + url_views = set([]) + # Add recursive URL patterns + unprocessed = set(api_patterns) + while unprocessed: + to_process = unprocessed.copy() + unprocessed = set([]) + for pattern in to_process: + if hasattr(pattern, 'lookup_str') and not pattern.lookup_str.startswith('awx.api'): + continue + patterns.add(pattern) + if isinstance(pattern, RegexURLResolver): + for sub_pattern in pattern.url_patterns: + if sub_pattern not in patterns: + unprocessed.add(sub_pattern) + # Get view classes + for pattern in patterns: + if isinstance(pattern, RegexURLPattern) and hasattr(pattern.callback, 'view_class'): + cls = pattern.callback.view_class + if issubclass(cls, ListAPIView): + url_views.add(pattern.callback.view_class) + + # Gather any views that don't have search fields defined + views_missing_search = [] + for View in url_views: + view = View() + if not hasattr(view, 'search_fields') or len(view.search_fields) == 0: + views_missing_search.append(view) + + if views_missing_search: + raise Exception('{} views do not have search fields defined:\n{}'.format( + len(views_missing_search), + '\n'.join([ + v.__class__.__name__ + ' (model: {})'.format(getattr(v, 'model', type(None)).__name__) + for v in views_missing_search + ])) + )