diff --git a/MANIFEST.in b/MANIFEST.in index 36d627dc10..ce0b9f5e12 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ recursive-include awx *.py -recursive-include awx/static *.ico +recursive-include awx/static * recursive-include awx/templates *.html recursive-include awx/api/templates *.md *.html recursive-include awx/ui/templates *.html diff --git a/awx/api/metadata.py b/awx/api/metadata.py index 46ea3f36da..713f9f83a2 100644 --- a/awx/api/metadata.py +++ b/awx/api/metadata.py @@ -128,9 +128,10 @@ class Metadata(metadata.SimpleMetadata): metadata['added_in_version'] = added_in_version # Add type(s) handled by this view/serializer. - serializer = view.get_serializer() - if hasattr(serializer, 'get_types'): - metadata['types'] = serializer.get_types() + if hasattr(view, 'get_serializer'): + serializer = view.get_serializer() + if hasattr(serializer, 'get_types'): + metadata['types'] = serializer.get_types() # Add search fields if available from the view. if getattr(view, 'search_fields', None): diff --git a/awx/api/serializers.py b/awx/api/serializers.py index f655b35f4e..49dd88c4f0 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -772,13 +772,16 @@ class OrganizationSerializer(BaseSerializer): class ProjectOptionsSerializer(BaseSerializer): + scm_clean = serializers.NullBooleanField(default=False) + scm_delete_on_update = serializers.NullBooleanField(default=False) + class Meta: fields = ('*', 'local_path', 'scm_type', 'scm_url', 'scm_branch', 'scm_clean', 'scm_delete_on_update', 'credential') extra_kwargs = { 'scm_type': { - 'allow_null': True - } + 'allow_null': True, + }, } def get_related(self, obj): @@ -791,6 +794,12 @@ class ProjectOptionsSerializer(BaseSerializer): def validate_scm_type(self, value): return value or u'' + def validate_scm_clean(self, value): + return bool(value) + + def validate_scm_delete_on_update(self, value): + return bool(value) + def validate(self, attrs): errors = {} @@ -822,18 +831,20 @@ class ProjectOptionsSerializer(BaseSerializer): class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer): - playbooks = serializers.ReadOnlyField(help_text='Array of playbooks available within this project.') scm_delete_on_next_update = serializers.BooleanField(read_only=True) + scm_update_on_launch = serializers.NullBooleanField(default=False) status = serializers.ChoiceField(choices=Project.PROJECT_STATUS_CHOICES, read_only=True, required=False) last_update_failed = serializers.BooleanField(read_only=True) last_updated = serializers.DateTimeField(read_only=True) class Meta: model = Project - fields = ('*', 'playbooks', 'scm_delete_on_next_update', 'scm_update_on_launch', + fields = ('*', 'scm_delete_on_next_update', 'scm_update_on_launch', 'scm_update_cache_timeout') + \ ('last_update_failed', 'last_updated') # Backwards compatibility - + + def clean_scm_update_on_launch(self, value): + return bool(value) def get_related(self, obj): res = super(ProjectSerializer, self).get_related(obj) @@ -858,6 +869,8 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer): class ProjectPlaybooksSerializer(ProjectSerializer): + playbooks = serializers.ReadOnlyField(help_text='Array of playbooks available within this project.') + class Meta: model = Project fields = ('playbooks',) @@ -1736,6 +1749,12 @@ class AdHocCommandSerializer(UnifiedJobSerializer): }, } + def get_field_names(self, declared_fields, info): + field_names = super(AdHocCommandSerializer, self).get_field_names(declared_fields, info) + # Meta inheritance and -field_name options don't seem to be taking + # effect above, so remove the undesired fields here. + return tuple(x for x in field_names if x not in ('unified_job_template', 'description')) + def build_standard_field(self, field_name, model_field): field_class, field_kwargs = super(AdHocCommandSerializer, self).build_standard_field(field_name, model_field) # Load module name choices dynamically from DB settings. diff --git a/awx/main/access.py b/awx/main/access.py index e17fc59b02..0cafdb918e 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1379,14 +1379,15 @@ class UnifiedJobTemplateAccess(BaseAccess): qs = qs.select_related( 'created_by', 'modified_by', - 'project', - 'inventory', - 'credential', - 'cloud_credential', + #'project', + #'inventory', + #'credential', + #'cloud_credential', 'next_schedule', 'last_job', 'current_job', ) + # FIXME: Figure out how to do select/prefetch on related project/inventory/credential/cloud_credential. return qs class UnifiedJobAccess(BaseAccess): @@ -1412,18 +1413,19 @@ class UnifiedJobAccess(BaseAccess): qs = qs.select_related( 'created_by', 'modified_by', - 'project', - 'inventory', - 'credential', - 'project___credential', - 'inventory_source___credential', - 'inventory_source___inventory', - 'job_template___inventory', - 'job_template___project', - 'job_template___credential', - 'job_template___cloud_credential', + #'project', + #'inventory', + #'credential', + #'project___credential', + #'inventory_source___credential', + #'inventory_source___inventory', + #'job_template___inventory', + #'job_template___project', + #'job_template___credential', + #'job_template___cloud_credential', ) qs = qs.prefetch_related('unified_job_template') + # FIXME: Figure out how to do select/prefetch on related project/inventory/credential/cloud_credential. return qs class ScheduleAccess(BaseAccess): diff --git a/awx/ui/client/src/activity-stream/activitystream.controller.js b/awx/ui/client/src/activity-stream/activitystream.controller.js index 13d2ccecce..0c16f304b7 100644 --- a/awx/ui/client/src/activity-stream/activitystream.controller.js +++ b/awx/ui/client/src/activity-stream/activitystream.controller.js @@ -9,13 +9,21 @@ * @name controllers.function:Activity Stream * @description This controller controls the activity stream. */ -function activityStreamController($scope, Stream) { +function activityStreamController($scope, $state, subTitle, Stream, GetTargetTitle) { + + // subTitle is passed in via a resolve on the route. If there is no subtitle + // generated in the resolve then we go get the targets generic title. + + // Get the streams sub-title based on the target. This scope variable is leveraged + // when we define the activity stream list. Specifically it is included in the list + // title. + $scope.streamSubTitle = subTitle ? subTitle : GetTargetTitle($state.params.target); // Open the stream Stream({ scope: $scope }); - + } -export default ['$scope', 'Stream', activityStreamController]; +export default ['$scope', '$state', 'subTitle', 'Stream', 'GetTargetTitle', activityStreamController]; diff --git a/awx/ui/client/src/activity-stream/activitystream.route.js b/awx/ui/client/src/activity-stream/activitystream.route.js index e334363e9c..74151e32df 100644 --- a/awx/ui/client/src/activity-stream/activitystream.route.js +++ b/awx/ui/client/src/activity-stream/activitystream.route.js @@ -14,4 +14,39 @@ export default { ncyBreadcrumb: { label: "ACTIVITY STREAM" }, + resolve: { + subTitle: + [ '$stateParams', + 'Rest', + 'ModelToPlural', + 'GetBasePath', + 'ProcessErrors', + function($stateParams, rest, ModelToPlural, getBasePath, ProcessErrors) { + // If we have a target and an ID then we want to go grab the name of the object + // that we're examining with the activity stream. This name will be used in the + // subtitle. + if ($stateParams.target && $stateParams.id) { + var target = $stateParams.target; + var id = $stateParams.id; + + var url = getBasePath(ModelToPlural(target)) + id + '/'; + rest.setUrl(url); + return rest.get() + .then(function(data) { + // Return the name or the username depending on which is available. + return (data.data.name || data.data.username); + }).catch(function (response) { + ProcessErrors(null, response.data, response.status, null, { + hdr: 'Error!', + msg: 'Failed to get title info. GET returned status: ' + + response.status + }); + }); + } + else { + return null; + } + } + ] + } }; diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 00b19eda61..4bcb2f8e10 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -185,7 +185,9 @@ var tower = angular.module('Tower', [ 'pendolytics', 'ui.router', 'ncy-angular-breadcrumb', - 'scheduler' + 'scheduler', + 'ApiModelHelper', + 'ActivityStreamHelper' ]) .constant('AngularScheduler.partials', urlPrefix + 'lib/angular-scheduler/lib/') diff --git a/awx/ui/client/src/helpers.js b/awx/ui/client/src/helpers.js index be09e70be5..4a277970da 100644 --- a/awx/ui/client/src/helpers.js +++ b/awx/ui/client/src/helpers.js @@ -41,6 +41,8 @@ import RelatedSearch from "./helpers/related-search"; import Search from "./helpers/search"; import Teams from "./helpers/teams"; import AdhocHelper from "./helpers/Adhoc"; +import ApiModelHelper from "./helpers/ApiModel"; +import ActivityStreamHelper from "./helpers/ActivityStream"; export { AboutAnsible, @@ -76,5 +78,7 @@ export RelatedSearch, Search, Teams, - AdhocHelper + AdhocHelper, + ApiModelHelper, + ActivityStreamHelper }; diff --git a/awx/ui/client/src/helpers/ActivityStream.js b/awx/ui/client/src/helpers/ActivityStream.js new file mode 100644 index 0000000000..6a59bee789 --- /dev/null +++ b/awx/ui/client/src/helpers/ActivityStream.js @@ -0,0 +1,58 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + /** + * @ngdoc function + * @name helpers.function:ActivityStream + * @description Helper functions for the activity stream +*/ + +export default + angular.module('ActivityStreamHelper', ['Utilities']) + .factory('GetTargetTitle', [ + function () { + return function (target) { + + var rtnTitle = 'DASHBOARD'; + + switch(target) { + case 'project': + rtnTitle = 'PROJECTS'; + break; + case 'inventory': + rtnTitle = 'INVENTORIES'; + break; + case 'job_template': + rtnTitle = 'JOB TEMPLATES'; + break; + case 'credential': + rtnTitle = 'CREDENTIALS'; + break; + case 'user': + rtnTitle = 'USERS'; + break; + case 'team': + rtnTitle = 'TEAMS'; + break; + case 'organization': + rtnTitle = 'ORGANIZATIONS'; + break; + case 'management_job': + rtnTitle = 'MANAGEMENT JOBS'; + break; + case 'inventory_script': + rtnTitle = 'INVENTORY SCRIPTS'; + break; + case 'schedule': + rtnTitle = 'SCHEDULES'; + break; + } + + return rtnTitle; + + }; + } + ]); diff --git a/awx/ui/client/src/helpers/ApiModel.js b/awx/ui/client/src/helpers/ApiModel.js new file mode 100644 index 0000000000..6a63d24583 --- /dev/null +++ b/awx/ui/client/src/helpers/ApiModel.js @@ -0,0 +1,100 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + /** + * @ngdoc function + * @name helpers.function:ApiModel + * @description Helper functions to convert singular/plural versions of our models to the opposite +*/ + +export default + angular.module('ApiModelHelper', ['Utilities']) + .factory('ModelToSingular', [ + function () { + return function (model) { + // This function takes in the plural model string and spits out the singular + // version. + + var singularModel; + + switch(model) { + case 'projects': + singularModel = 'project'; + break; + case 'inventories': + singularModel = 'inventory'; + break; + case 'job_templates': + singularModel = 'job_template'; + break; + case 'credentials': + singularModel = 'credential'; + break; + case 'users': + singularModel = 'user'; + break; + case 'teams': + singularModel = 'team'; + break; + case 'organizations': + singularModel = 'organization'; + break; + case 'management_jobs': + singularModel = 'management_job'; + break; + case 'inventory_scripts': + singularModel = 'inventory_script'; + break; + } + + return singularModel; + + }; + } + ]) + .factory('ModelToPlural', [ + function () { + return function (model) { + // This function takes in the singular model string and spits out the plural + // version. + + var pluralModel; + + switch(model) { + case 'project': + pluralModel = 'projects'; + break; + case 'inventory': + pluralModel = 'inventories'; + break; + case 'job_template': + pluralModel = 'job_templates'; + break; + case 'credential': + pluralModel = 'credentials'; + break; + case 'user': + pluralModel = 'users'; + break; + case 'team': + pluralModel = 'teams'; + break; + case 'organization': + pluralModel = 'organizations'; + break; + case 'management_job': + pluralModel = 'management_jobs'; + break; + case 'inventory_script': + pluralModel = 'inventory_scripts'; + break; + } + + return pluralModel; + + }; + } + ]); diff --git a/awx/ui/client/src/lists/Streams.js b/awx/ui/client/src/lists/Streams.js index 9aa718a77b..1cc99c4d7c 100644 --- a/awx/ui/client/src/lists/Streams.js +++ b/awx/ui/client/src/lists/Streams.js @@ -12,7 +12,7 @@ export default name: 'activities', iterator: 'activity', editTitle: 'Activity Stream', - listTitle: 'Activity Stream', + listTitle: 'Activity Stream
{{streamSubTitle}}', listTitleBadge: false, selectInstructions: '', index: false, diff --git a/awx/ui/client/src/shared/prompt-dialog.js b/awx/ui/client/src/shared/prompt-dialog.js index 1ee2ad1fa7..afe6ac51a3 100644 --- a/awx/ui/client/src/shared/prompt-dialog.js +++ b/awx/ui/client/src/shared/prompt-dialog.js @@ -119,7 +119,7 @@ angular.module('PromptDialog', ['Utilities', 'sanitizeFilter']) $('#prompt-modal').off('hidden.bs.modal'); $('#prompt-modal').modal({ - backdrop: 'local_backdrop', + backdrop: 'static', keyboard: true, show: true });