From 52ab418abbcd661e9cccb04d92ad0d68a5d7810a Mon Sep 17 00:00:00 2001 From: Chris Church Date: Fri, 28 Mar 2014 01:25:19 -0400 Subject: [PATCH] AC-1040 Unified job template and unified job views. --- awx/api/filters.py | 42 +++++++- awx/api/generics.py | 2 +- awx/api/serializers.py | 99 ++++++++++++++++-- awx/api/urls.py | 3 +- awx/api/views.py | 216 +++------------------------------------ awx/main/access.py | 42 ++++++++ awx/settings/defaults.py | 1 + 7 files changed, 191 insertions(+), 214 deletions(-) diff --git a/awx/api/filters.py b/awx/api/filters.py index 2e3e18fd4f..358bb62c67 100644 --- a/awx/api/filters.py +++ b/awx/api/filters.py @@ -10,11 +10,15 @@ from django.db import models from django.db.models import Q from django.db.models.related import RelatedObject from django.db.models.fields import FieldDoesNotExist +from django.contrib.contenttypes.models import ContentType # Django REST Framework from rest_framework.exceptions import ParseError from rest_framework.filters import BaseFilterBackend +# Ansible Tower +from awx.main.utils import camelcase_to_underscore + class ActiveOnlyBackend(BaseFilterBackend): ''' Filter to show only objects where is_active/active is True. @@ -28,13 +32,49 @@ class ActiveOnlyBackend(BaseFilterBackend): queryset = queryset.filter(active=True) return queryset +class TypeFilterBackend(BaseFilterBackend): + ''' + Filter on type field now returned with all objects. + ''' + + def filter_queryset(self, request, queryset, view): + try: + types = None + for key, value in request.QUERY_PARAMS.items(): + if key == 'type': + if ',' in value: + types = value.split(',') + else: + types = (value,) + if types: + types_map = {} + for ct in ContentType.objects.filter(Q(app_label='main') | Q(app_label='auth', model='user')): + ct_model = ct.model_class() + if not ct_model: + continue + ct_type = camelcase_to_underscore(ct_model._meta.object_name) + types_map[ct_type] = ct.pk + model = queryset.model + model_type = camelcase_to_underscore(model._meta.object_name) + if 'polymorphic_ctype' in model._meta.get_all_field_names(): + types_pks = set([v for k,v in types_map.items() if k in types]) + queryset = queryset.filter(polymorphic_ctype_id__in=types_pks) + elif model_type in types: + queryset = queryset + else: + queryset = queryset.none() + return queryset + except FieldError, e: + # Return a 400 for invalid field names. + raise ParseError(*e.args) + class FieldLookupBackend(BaseFilterBackend): ''' Filter using field lookups provided via query string parameters. ''' RESERVED_NAMES = ('page', 'page_size', 'format', 'order', 'order_by', - 'search') + 'search', 'type') SUPPORTED_LOOKUPS = ('exact', 'iexact', 'contains', 'icontains', 'startswith', 'istartswith', 'endswith', 'iendswith', diff --git a/awx/api/generics.py b/awx/api/generics.py index 39dccb4cfd..2ce3c3ab83 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -197,7 +197,7 @@ class GenericAPIView(generics.GenericAPIView, APIView): # appropriate metadata about the fields that should be supplied. serializer = self.get_serializer() actions['GET'] = serializer.metadata() - ret['types'] = [serializer.get_type(None)] # FIXME: Support multiple types? + ret['types'] = serializer.get_types() if actions: ret['actions'] = actions if getattr(self, 'search_fields', None): diff --git a/awx/api/serializers.py b/awx/api/serializers.py index f8afa02e2e..57fea1994b 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -148,9 +148,9 @@ class BaseSerializerMetaclass(serializers.SerializerMetaclass): def __new__(cls, name, bases, attrs): meta = type('Meta', (object,), {}) - for base in bases: + for base in bases[::-1]: cls._update_meta(base, meta, getattr(base, 'Meta', None)) - cls._update_meta(None, meta, attrs.get('Meta', None)) + cls._update_meta(None, meta, attrs.get('Meta', meta)) attrs['Meta'] = meta return super(BaseSerializerMetaclass, cls).__new__(cls, name, bases, attrs) @@ -215,6 +215,9 @@ class BaseSerializer(serializers.ModelSerializer): opts = get_concrete_model(self.opts.model)._meta return camelcase_to_underscore(opts.object_name) + def get_types(self): + return [self.get_type(None)] + def get_url(self, obj): if obj is None: return '' @@ -304,6 +307,27 @@ class UnifiedJobTemplateSerializer(BaseSerializer): res['next_schedule'] = obj.next_schedule.get_absolute_url() return res + def get_types(self): + if type(self) is UnifiedJobTemplateSerializer: + return ['project', 'inventory_source', 'job_template'] + else: + return super(UnifiedJobTemplateSerializer, self).get_types() + + def to_native(self, obj): + serializer_class = None + if type(self) is UnifiedJobTemplateSerializer: + if isinstance(obj, Project): + serializer_class = ProjectSerializer + elif isinstance(obj, InventorySource): + serializer_class = InventorySourceSerializer + elif isinstance(obj, JobTemplate): + serializer_class = JobTemplateSerializer + if serializer_class: + serializer = serializer_class(instance=obj) + return serializer.to_native(obj) + else: + return super(UnifiedJobTemplateSerializer, self).to_native(obj) + class UnifiedJobSerializer(BaseSerializer): @@ -315,6 +339,12 @@ class UnifiedJobSerializer(BaseSerializer): 'failed', 'started', 'finished', 'elapsed', 'job_args', 'job_cwd', 'job_env', 'result_stdout', 'result_traceback') + def get_types(self): + if type(self) is UnifiedJobSerializer: + return ['project_update', 'inventory_update', 'job'] + else: + return super(UnifiedJobSerializer, self).get_types() + def get_related(self, obj): res = super(UnifiedJobSerializer, self).get_related(obj) if obj.unified_job_template and obj.unified_job_template.active: @@ -324,7 +354,50 @@ class UnifiedJobSerializer(BaseSerializer): return res def to_native(self, obj): - ret = super(UnifiedJobSerializer, self).to_native(obj) + serializer_class = None + if type(self) is UnifiedJobSerializer: + if isinstance(obj, ProjectUpdate): + serializer_class = ProjectUpdateSerializer + elif isinstance(obj, InventoryUpdate): + serializer_class = InventoryUpdateSerializer + elif isinstance(obj, Job): + serializer_class = JobSerializer + if serializer_class: + serializer = serializer_class(instance=obj) + ret = serializer.to_native(obj) + else: + ret = super(UnifiedJobSerializer, self).to_native(obj) + if 'elapsed' in ret: + ret['elapsed'] = float(ret['elapsed']) + return ret + + +class UnifiedJobListSerializer(UnifiedJobSerializer): + + class Meta: + exclude = ('*', 'job_args', 'job_cwd', 'job_env', 'result_traceback', + 'result_stdout') + + def get_types(self): + if type(self) is UnifiedJobListSerializer: + return ['project_update', 'inventory_update', 'job'] + else: + return super(UnifiedJobListSerializer, self).get_types() + + def to_native(self, obj): + serializer_class = None + if type(self) is UnifiedJobListSerializer: + if isinstance(obj, ProjectUpdate): + serializer_class = ProjectUpdateListSerializer + elif isinstance(obj, InventoryUpdate): + serializer_class = InventoryUpdateListSerializer + elif isinstance(obj, Job): + serializer_class = JobListSerializer + if serializer_class: + serializer = serializer_class(instance=obj) + ret = serializer.to_native(obj) + else: + ret = super(UnifiedJobListSerializer, self).to_native(obj) if 'elapsed' in ret: ret['elapsed'] = float(ret['elapsed']) return ret @@ -543,6 +616,11 @@ class ProjectUpdateSerializer(UnifiedJobSerializer, ProjectOptionsSerializer): return res +class ProjectUpdateListSerializer(ProjectUpdateSerializer, UnifiedJobListSerializer): + + pass + + class BaseSerializerWithVariables(BaseSerializer): def validate_variables(self, attrs, source): @@ -907,6 +985,11 @@ class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSeri return res +class InventoryUpdateListSerializer(InventoryUpdateSerializer, UnifiedJobListSerializer): + + pass + + class TeamSerializer(BaseSerializer): class Meta: @@ -1151,12 +1234,9 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer): ret['job_template'] = None return ret +class JobListSerializer(JobSerializer, UnifiedJobListSerializer): -class JobListSerializer(JobSerializer): - - class Meta: - model = Job - fields = ('*', '-result_stdout') + pass class JobHostSummarySerializer(BaseSerializer): @@ -1225,8 +1305,7 @@ class ScheduleSerializer(BaseSerializer): class Meta: model = Schedule - fields = ('*', 'unified_job_template', 'enabled', 'dtstart', 'dtend', - 'rrule', 'next_run') + fields = ('*', 'enabled', 'dtstart', 'dtend', 'rrule', 'next_run') def get_related(self, obj): res = super(ScheduleSerializer, self).get_related(obj) diff --git a/awx/api/urls.py b/awx/api/urls.py index 8ff1ab0d1e..9153e60ad1 100644 --- a/awx/api/urls.py +++ b/awx/api/urls.py @@ -172,7 +172,6 @@ v1_urls = patterns('awx.api.views', url(r'^me/$', 'user_me_list'), url(r'^dashboard/$', 'dashboard_view'), url(r'^schedules/$', include(schedule_urls)), - url(r'^unified_jobs/$','unified_jobs_list'), url(r'^organizations/', include(organization_urls)), url(r'^users/', include(user_urls)), url(r'^projects/', include(project_urls)), @@ -189,6 +188,8 @@ v1_urls = patterns('awx.api.views', url(r'^jobs/', include(job_urls)), url(r'^job_host_summaries/', include(job_host_summary_urls)), url(r'^job_events/', include(job_event_urls)), + url(r'^unified_job_templates/$', 'unified_job_template_list'), + url(r'^unified_jobs/$', 'unified_job_list'), url(r'^activity_stream/', include(activity_stream_urls)), ) diff --git a/awx/api/views.py b/awx/api/views.py index cf63a2f9ae..bec10a824e 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -93,7 +93,8 @@ class ApiV1RootView(APIView): data['job_templates'] = reverse('api:job_template_list') data['jobs'] = reverse('api:job_list') data['schedules'] = reverse('api:schedule_list') - data['unified_jobs'] = reverse('api:unified_jobs_list') + data['unified_job_templates'] = reverse('api:unified_job_template_list') + data['unified_jobs'] = reverse('api:unified_job_list') data['activity_stream'] = reverse('api:activity_stream_list') return Response(data) @@ -240,215 +241,16 @@ class DashboardView(APIView): class ScheduleList(ListCreateAPIView): view_name = "Schedules" - new_in_148 = True - model = Schedule serializer_class = ScheduleSerializer + new_in_148 = True class ScheduleDetail(RetrieveUpdateDestroyAPIView): - new_in_148 = True model = Schedule serializer_class = ScheduleSerializer - -class UnifiedJobsList(APIView): - - view_name = "Unified Job Templates" new_in_148 = True - def get(self, request, format=None): - data = { - 'count': 3, - 'next': None, - 'previous': None, - 'results': [ - - {"id": 39, - "url": "/api/v1/inventory_updates/39/", - "related": { - "cancel": "/api/v1/inventory_updates/39/cancel/", - "inventory_source": "/api/v1/inventory_sources/11/" - }, - "summary_fields": { - "inventory_source": { - "source": "ec2", - "last_updated": "2014-03-19T17:44:30.726Z", - "status": "successful" - } - }, - "created": "2014-03-19T17:44:13.894Z", - "modified": "2014-03-19T17:44:30.726Z", - "inventory_source": 11, - "status": "successful", - "failed": False, - "type": "inventory_sync", - "result_stdout": "there will be stdout here", - "result_traceback": "", - "job_args": "[\"awx-manage\", \"inventory_import\", \"--inventory-id\", \"6\", \"--source\", \"/home/ubuntu/projects/tower/awx/plugins/inventory/ec2.py\", \"--enabled-var\", \"ec2_state\", \"--enabled-value\", \"running\", \"-v1\", \"--traceback\"]", - "job_cwd": "/home/ubuntu/projects/tower/awx/plugins/inventory", - "job_env": { - "CELERY_LOG_REDIRECT_LEVEL": "WARNING", - "ANSIBLE_PARAMIKO_RECORD_HOST_KEYS": "False", - "DJANGO_LIVE_TEST_SERVER_ADDRESS": "localhost:9013-9199", - "LESSOPEN": "| /usr/bin/lesspipe %s", - "_MP_FORK_LOGFILE_": "", - "SSH_CLIENT": "65.190.173.93 52056 22", - "CELERY_LOG_REDIRECT": "1", - "LOGNAME": "ubuntu", - "USER": "ubuntu", - "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games", - "HOME": "/home/ubuntu", - "MAKEFLAGS": "w", - "LANG": "en_US.UTF-8", - "TERM": "screen", - "SHELL": "/bin/bash", - "TZ": "America/New_York", - "_MP_FORK_LOGFORMAT_": "[%(asctime)s: %(levelname)s/%(processName)s] %(message)s", - "SHLVL": "1", - "CELERY_LOG_FILE": "", - "DJANGO_PROJECT_DIR": "/home/ubuntu/projects/tower", - "MFLAGS": "-w", - "ANSIBLE_HOST_KEY_CHECKING": "False", - "PYTHONPATH": "/home/ubuntu/projects/tower/awx/lib/site-packages:", - "COMP_WORDBREAKS": " \t\n\"'><;|&(:", - "TMUX": "/tmp/tmux-1000/default,13862,0", - "CELERY_LOADER": "djcelery.loaders.DjangoLoader", - "_MP_FORK_LOGLEVEL_": "10", - "ANSIBLE_NOCOLOR": "1", - "AWS_ACCESS_KEY_ID": "AKIAJZFZXKDVUHDDCZSA", - "_": "/usr/bin/make", - "SSH_CONNECTION": "65.190.173.93 52056 10.157.37.183 22", - "LESSCLOSE": "/usr/bin/lesspipe %s %s", - "EC2_INI_PATH": "/tmp/tmpdDHKKH", - "SSH_TTY": "/dev/pts/0", - "OLDPWD": "/home/ubuntu", - "MAKELEVEL": "2", - "PWD": "/home/ubuntu/projects/tower", - "DJANGO_SETTINGS_MODULE": "awx.settings.development", - "AWS_SECRET_ACCESS_KEY": "****************************************", - "INVENTORY_SOURCE_ID": "11", - "MAIL": "/var/mail/ubuntu", - "LS_COLORS": "rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arj=01;31:*.taz=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lz=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.axa=00;36:*.oga=00;36:*.spx=00;36:*.xspf=00;36:", - "CELERY_LOG_LEVEL": "10", - "TMUX_PANE": "%1" - }, - "license_error": False}, - { - "id": 1, - "url": "/api/v1/jobs/1/", - "related": { - "job_host_summaries": "/api/v1/jobs/1/job_host_summaries/", - "activity_stream": "/api/v1/jobs/1/activity_stream/", - "job_events": "/api/v1/jobs/1/job_events/", - "job_template": "/api/v1/job_templates/1/", - "inventory": "/api/v1/inventories/1/", - "project": "/api/v1/projects/1/", - "credential": "/api/v1/credentials/1/", - "start": "/api/v1/jobs/1/start/", - "cancel": "/api/v1/jobs/1/cancel/" - }, - "summary_fields": { - "credential": { - "name": "testcred", - "description": "", - "kind": "ssh", - "cloud": False - }, - "job_template": { - "name": "ping1", - "description": "" - }, - "project": { - "name": "Test Projects", - "description": "", - "status": "ok" - }, - "inventory": { - "name": "Test Inventory", - "description": "", - "has_active_failures": False, - "total_hosts": 1000, - "hosts_with_active_failures": 0, - "total_groups": 1, - "groups_with_active_failures": 0, - "has_inventory_sources": False, - "total_inventory_sources": 0, - "inventory_sources_with_failures": 0 - } - }, - "created": "2014-02-10T19:28:43.886Z", - "modified": "2014-03-14T16:48:26.222Z", - "job_template": 1, - "job_type": "run", - "type": "playbook_run", - "inventory": 1, - "project": 1, - "playbook": "ping.yml", - "credential": 1, - "cloud_credential": None, - "forks": 0, - "limit": "host-1", - "verbosity": 0, - "extra_vars": "{\n\t\"ansible_connection\": \"local\"\n}", - "job_tags": "", - "launch_type": "manual", - "status": "successful", - "failed": False, - "result_traceback": "", - "passwords_needed_to_start": [], - "job_args": "[\"ansible-playbook\", \"-i\", \"/home/ubuntu/projects/tower/awx/plugins/inventory/awxrest.py\", \"-u\", \"admin\", \"--ask-pass\", \"-l\", \"host-1\", \"-e\", \"{\\\"ansible_connection\\\": \\\"local\\\"}\", \"ping.yml\"]", - "job_cwd": "/home/ubuntu/projects/tower/awx/projects/test", - "job_env": { - "CELERY_LOG_REDIRECT_LEVEL": "WARNING", - "ANSIBLE_PARAMIKO_RECORD_HOST_KEYS": "False", - "DJANGO_LIVE_TEST_SERVER_ADDRESS": "localhost:9013-9199", - "LESSOPEN": "| /usr/bin/lesspipe %s", - "_MP_FORK_LOGFILE_": "", - "SSH_CLIENT": "65.190.173.93 38699 22", - "CELERY_LOG_REDIRECT": "1", - "LOGNAME": "ubuntu", - "USER": "ubuntu", - "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games", - "HOME": "/home/ubuntu", - "REST_API_TOKEN": "**********************************", - "CALLBACK_CONSUMER_PORT": "tcp://127.0.0.1:5557", - "MAKEFLAGS": "", - "ANSIBLE_CALLBACK_PLUGINS": "/home/ubuntu/projects/tower/awx/plugins/callback", - "LANG": "en_US.UTF-8", - "TERM": "screen", - "SHELL": "/bin/bash", - "TZ": "America/New_York", - "_MP_FORK_LOGFORMAT_": "[%(asctime)s: %(levelname)s/%(processName)s] %(message)s", - "SHLVL": "2", - "CELERY_LOG_FILE": "", - "DJANGO_PROJECT_DIR": "/home/ubuntu/projects/tower", - "MFLAGS": "", - "ANSIBLE_HOST_KEY_CHECKING": "False", - "JOB_ID": "1", - "PYTHONPATH": "/home/ubuntu/projects/tower/awx/lib/site-packages:", - "COMP_WORDBREAKS": " \t\n\"'><;|&(:", - "TMUX": "/tmp/tmux-1000/default,1205,0", - "CELERY_LOADER": "djcelery.loaders.DjangoLoader", - "_MP_FORK_LOGLEVEL_": "10", - "ANSIBLE_NOCOLOR": "1", - "JOB_CALLBACK_DEBUG": "1", - "REST_API_URL": "http://127.0.0.1:8013", - "_": "/usr/bin/make", - "SSH_CONNECTION": "65.190.173.93 38699 10.157.37.183 22", - "LESSCLOSE": "/usr/bin/lesspipe %s %s", - "INVENTORY_HOSTVARS": "True", - "SSH_TTY": "/dev/pts/0", - "CELERY_LOG_LEVEL": "10", - "INVENTORY_ID": "1", - "MAKELEVEL": "1", - "PWD": "/home/ubuntu/projects/tower", - "DJANGO_SETTINGS_MODULE": "awx.settings.development", - "MAIL": "/var/mail/ubuntu", - "LS_COLORS": "rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arj=01;31:*.taz=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lz=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.axa=00;36:*.oga=00;36:*.spx=00;36:*.xspf=00;36:", - "TMUX_PANE": "%1" - }},]} - return Response(data) - class AuthTokenView(APIView): authentication_classes = [] @@ -1554,6 +1356,18 @@ class JobJobEventsList(BaseJobEventsList): headers=headers) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +class UnifiedJobTemplateList(ListAPIView): + + model = UnifiedJobTemplate + serializer_class = UnifiedJobTemplateSerializer + new_in_148 = True + +class UnifiedJobList(ListAPIView): + + model = UnifiedJob + serializer_class = UnifiedJobListSerializer + new_in_148 = True + class ActivityStreamList(SimpleListAPIView): model = ActivityStream diff --git a/awx/main/access.py b/awx/main/access.py index 7fb63f24a2..a1ef779f7b 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1043,6 +1043,46 @@ class JobEventAccess(BaseAccess): def can_delete(self, obj): return False +class UnifiedJobTemplateAccess(BaseAccess): + ''' + I can see a unified job template whenever I can see the same project, + inventory source or job template. Unified job templates do not include + projects without SCM configured or inventory sources without a cloud + source. + ''' + + model = UnifiedJobTemplate + + def get_queryset(self): + qs = self.model.objects.filter(active=True).distinct() + project_qs = self.user.get_queryset(Project).filter(scm_type__in=('',)) + inventory_source_qs = self.user.get_queryset(InventorySource).filter(source__in=CLOUD_INVENTORY_SOURCES) + job_template_qs = self.user.get_queryset(JobTemplate) + qs = qs.filter(Q(Project___in=project_qs) | + Q(InventorySource___in=inventory_source_qs) | + Q(JobTemplate___in=job_template_qs)) + # FIXME: select/prefetch to optimize! + return qs + +class UnifiedJobAccess(BaseAccess): + ''' + I can see a unified job whenever I can see the same project update, + inventory update or job. + ''' + + model = UnifiedJob + + def get_queryset(self): + qs = self.model.objects.filter(active=True).distinct() + project_update_qs = self.user.get_queryset(ProjectUpdate) + inventory_update_qs = self.user.get_queryset(InventoryUpdate).filter(source__in=CLOUD_INVENTORY_SOURCES) + job_qs = self.user.get_queryset(Job) + qs = qs.filter(Q(ProjectUpdate___in=project_update_qs) | + Q(InventoryUpdate___in=inventory_update_qs) | + Q(Job___in=job_qs)) + # FIXME: select/prefetch to optimize! + return qs + class ScheduleAccess(BaseAccess): ''' I can see a schedule if I can see it's related unified job, I can create them or update them if I have write access @@ -1230,4 +1270,6 @@ register_access(Job, JobAccess) register_access(JobHostSummary, JobHostSummaryAccess) register_access(JobEvent, JobEventAccess) register_access(Schedule, ScheduleAccess) +register_access(UnifiedJobTemplate, UnifiedJobTemplateAccess) +register_access(UnifiedJob, UnifiedJobAccess) register_access(ActivityStream, ActivityStreamAccess) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 6bc0a674d4..9b7d2365cb 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -160,6 +160,7 @@ REST_FRAMEWORK = { ), 'DEFAULT_FILTER_BACKENDS': ( 'awx.api.filters.ActiveOnlyBackend', + 'awx.api.filters.TypeFilterBackend', 'awx.api.filters.FieldLookupBackend', 'rest_framework.filters.SearchFilter', 'awx.api.filters.OrderByBackend',