diff --git a/Makefile b/Makefile index 48333cfaca..00fd4dc9e0 100644 --- a/Makefile +++ b/Makefile @@ -937,7 +937,7 @@ install: $(PYTHON) setup.py install $(SETUP_INSTALL_ARGS) docker-auth: - docker login -e 1234@5678.com -u oauth2accesstoken -p "$(GCLOUD_AUTH)" https://gcr.io + docker login -u oauth2accesstoken -p "$(GCLOUD_AUTH)" https://gcr.io # Docker isolated rampart docker-isolated: diff --git a/awx/api/views.py b/awx/api/views.py index a63f762297..b0ec02791e 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -59,7 +59,7 @@ import ansiconv from social.backends.utils import load_backends # AWX -from awx.main.tasks import send_notifications, update_host_smart_inventory_memberships, delete_inventory +from awx.main.tasks import send_notifications, update_host_smart_inventory_memberships from awx.main.access import get_user_queryset from awx.main.ha import is_ha_environment from awx.api.authentication import TaskAuthentication, TokenGetAuthentication @@ -1839,15 +1839,13 @@ class InventoryDetail(ControlledByScmMixin, RetrieveUpdateDestroyAPIView): def destroy(self, request, *args, **kwargs): obj = self.get_object() - if obj.pending_deletion is True: - return Response(dict(error=_("Inventory is already being deleted.")), status=status.HTTP_400_BAD_REQUEST) if not request.user.can_access(self.model, 'delete', obj): raise PermissionDenied() - obj.websocket_emit_status('pending_deletion') - delete_inventory.delay(obj.id) - obj.pending_deletion = True - obj.save(update_fields=['pending_deletion']) - return Response(status=status.HTTP_202_ACCEPTED) + try: + obj.schedule_deletion() + return Response(status=status.HTTP_202_ACCEPTED) + except RuntimeError, e: + return Response(dict(error=_("{0}".format(e))), status=status.HTTP_400_BAD_REQUEST) class InventoryActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): diff --git a/awx/main/isolated/isolated_manager.py b/awx/main/isolated/isolated_manager.py index d8764ac086..0f2a6b6837 100644 --- a/awx/main/isolated/isolated_manager.py +++ b/awx/main/isolated/isolated_manager.py @@ -78,7 +78,8 @@ class IsolatedManager(object): 'CALLBACK_QUEUE': '', 'CALLBACK_CONNECTION': '', 'ANSIBLE_RETRY_FILES_ENABLED': 'False', - 'ANSIBLE_HOST_KEY_CHECKING': 'False' + 'ANSIBLE_HOST_KEY_CHECKING': 'False', + 'ANSIBLE_LIBRARY': os.path.join(os.path.dirname(awx.__file__), 'plugins', 'isolated') } @classmethod @@ -204,7 +205,7 @@ class IsolatedManager(object): os.symlink(self.cwd, self.path_to('project')) # create directories for build artifacts to live in - os.makedirs(self.path_to('artifacts', 'job_events'), mode=stat.S_IRUSR | stat.S_IWUSR) + os.makedirs(self.path_to('artifacts', 'job_events'), mode=stat.S_IXUSR + stat.S_IWUSR + stat.S_IRUSR) def _missing_artifacts(self, path_list, buff): missing_artifacts = filter(lambda path: not os.path.exists(path), path_list) @@ -347,7 +348,6 @@ class IsolatedManager(object): args = ['ansible-playbook', '-u', settings.AWX_ISOLATED_USERNAME, '-i', hostname_string, 'heartbeat_isolated.yml'] env = cls._base_management_env() - env['ANSIBLE_LIBRARY'] = os.path.join(os.path.dirname(awx.__file__), 'lib', 'management_modules') env['ANSIBLE_STDOUT_CALLBACK'] = 'json' buff = cStringIO.StringIO() diff --git a/awx/main/migrations/0038_v320_release.py b/awx/main/migrations/0038_v320_release.py index 049151f709..ec9f7a24b7 100644 --- a/awx/main/migrations/0038_v320_release.py +++ b/awx/main/migrations/0038_v320_release.py @@ -76,6 +76,11 @@ class Migration(migrations.Migration): name='pending_deletion', field=models.BooleanField(default=False, help_text='Flag indicating the inventory is being deleted.', editable=False), ), + migrations.AlterField( + model_name='inventory', + name='organization', + field=models.ForeignKey(related_name='inventories', on_delete=models.deletion.SET_NULL, to='main.Organization', help_text='Organization containing this inventory.', null=True), + ), # Facts migrations.AlterField( diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 6190ebbb5c..73fc884f83 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -63,7 +63,8 @@ class Inventory(CommonModelNameNotUnique, ResourceMixin): 'Organization', related_name='inventories', help_text=_('Organization containing this inventory.'), - on_delete=models.CASCADE, + on_delete=models.SET_NULL, + null=True, ) variables = models.TextField( blank=True, @@ -373,6 +374,16 @@ class Inventory(CommonModelNameNotUnique, ResourceMixin): raise ValidationError(_("Credential kind must be 'insights'.")) return self.insights_credential + @transaction.atomic + def schedule_deletion(self): + from awx.main.tasks import delete_inventory + if self.pending_deletion is True: + raise RuntimeError("Inventory is already pending deletion.") + self.websocket_emit_status('pending_deletion') + delete_inventory.delay(self.pk) + self.pending_deletion = True + self.save(update_fields=['pending_deletion']) + class SmartInventoryMembership(BaseModel): ''' diff --git a/awx/main/signals.py b/awx/main/signals.py index 3dda0873b1..da477706e2 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -510,3 +510,13 @@ def get_current_user_from_drf_request(sender, **kwargs): request = get_current_request() drf_request = getattr(request, 'drf_request', None) return (getattr(drf_request, 'user', False), 0) + + +@receiver(pre_delete, sender=Organization) +def delete_inventory_for_org(sender, instance, **kwargs): + inventories = Inventory.objects.filter(organization__pk=instance.pk) + for inventory in inventories: + try: + inventory.schedule_deletion() + except RuntimeError, e: + logger.debug(e) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 27ad41faa7..331bbc76ef 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -360,18 +360,17 @@ def update_host_smart_inventory_memberships(): def delete_inventory(inventory_id): with ignore_inventory_computed_fields(), \ ignore_inventory_group_removal(): - with transaction.atomic(): - try: - i = Inventory.objects.get(id=inventory_id) - except Inventory.DoesNotExist: - logger.error("Delete Inventory failed due to missing inventory: " + str(inventory_id)) - return + try: + i = Inventory.objects.get(id=inventory_id) i.delete() - emit_channel_notification( - 'inventories-status_changed', - {'group_name': 'inventories', 'inventory_id': inventory_id, 'status': 'deleted'} - ) - logger.debug('Deleted inventory: %s' % inventory_id) + emit_channel_notification( + 'inventories-status_changed', + {'group_name': 'inventories', 'inventory_id': inventory_id, 'status': 'deleted'} + ) + logger.debug('Deleted inventory: %s' % inventory_id) + except Inventory.DoesNotExist: + logger.error("Delete Inventory failed due to missing inventory: " + str(inventory_id)) + return class BaseTask(Task): diff --git a/awx/main/tests/functional/api/test_inventory.py b/awx/main/tests/functional/api/test_inventory.py index 3266c3f27c..1cd93182f9 100644 --- a/awx/main/tests/functional/api/test_inventory.py +++ b/awx/main/tests/functional/api/test_inventory.py @@ -59,7 +59,7 @@ def test_async_inventory_duplicate_deletion_prevention(delete, get, inventory, a resp = delete(reverse('api:inventory_detail', kwargs={'pk': inventory.id}), alice) assert resp.status_code == 400 - assert resp.data['error'] == 'Inventory is already being deleted.' + assert resp.data['error'] == 'Inventory is already pending deletion.' @pytest.mark.parametrize('order_by', ('script', '-script', 'script,pk', '-script,pk')) @@ -314,12 +314,12 @@ class TestControlledBySCM: @pytest.mark.django_db class TestInsightsCredential: def test_insights_credential(self, patch, insights_inventory, admin_user, insights_credential): - patch(insights_inventory.get_absolute_url(), + patch(insights_inventory.get_absolute_url(), {'insights_credential': insights_credential.id}, admin_user, expect=200) def test_non_insights_credential(self, patch, insights_inventory, admin_user, scm_credential): - patch(insights_inventory.get_absolute_url(), + patch(insights_inventory.get_absolute_url(), {'insights_credential': scm_credential.id}, admin_user, expect=400) diff --git a/awx/main/tests/unit/isolated/test_expect.py b/awx/main/tests/unit/isolated/test_expect.py index a021b8fdc4..5d5c79f150 100644 --- a/awx/main/tests/unit/isolated/test_expect.py +++ b/awx/main/tests/unit/isolated/test_expect.py @@ -149,7 +149,7 @@ def test_build_isolated_job_data(private_data_dir, rsa_key): path = os.path.join(private_data_dir, 'artifacts') assert os.path.isdir(path) - assert stat.S_IMODE(os.stat(path).st_mode) == stat.S_IRUSR + stat.S_IWUSR # user rw + assert stat.S_IMODE(os.stat(path).st_mode) == stat.S_IXUSR + stat.S_IWUSR + stat.S_IRUSR # user rwx path = os.path.join(private_data_dir, 'args') with open(path, 'r') as f: diff --git a/awx/playbooks/library/mkfifo.py b/awx/plugins/isolated/mkfifo.py similarity index 100% rename from awx/playbooks/library/mkfifo.py rename to awx/plugins/isolated/mkfifo.py diff --git a/awx/lib/management_modules/tower_capacity.py b/awx/plugins/isolated/tower_capacity.py similarity index 100% rename from awx/lib/management_modules/tower_capacity.py rename to awx/plugins/isolated/tower_capacity.py diff --git a/awx/lib/management_modules/tower_isolated_cleanup.py b/awx/plugins/isolated/tower_isolated_cleanup.py similarity index 100% rename from awx/lib/management_modules/tower_isolated_cleanup.py rename to awx/plugins/isolated/tower_isolated_cleanup.py diff --git a/awx/ui/client/src/inventories-hosts/hosts/host.form.js b/awx/ui/client/src/inventories-hosts/hosts/host.form.js index 9cfec370d2..a736fd5448 100644 --- a/awx/ui/client/src/inventories-hosts/hosts/host.form.js +++ b/awx/ui/client/src/inventories-hosts/hosts/host.form.js @@ -106,14 +106,14 @@ function(i18n) { related: { ansible_facts: { name: 'ansible_facts', - awToolTip: i18n._('Please save before viewing facts'), + awToolTip: i18n._('Please save before viewing facts.'), dataPlacement: 'top', title: i18n._('Facts'), skipGenerator: true }, groups: { name: 'groups', - awToolTip: i18n._('Please save before defining groups'), + awToolTip: i18n._('Please save before defining groups.'), dataPlacement: 'top', ngClick: "$state.go('hosts.edit.groups')", title: i18n._('Groups'), @@ -122,7 +122,7 @@ function(i18n) { }, insights: { name: 'insights', - awToolTip: i18n._('Please save before viewing Insights'), + awToolTip: i18n._('Please save before viewing Insights.'), dataPlacement: 'top', title: i18n._('Insights'), skipGenerator: true, diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/completed-jobs/completed-jobs.list.js b/awx/ui/client/src/inventories-hosts/inventories/related/completed-jobs/completed-jobs.list.js index b8990de7f9..c69a7889e2 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/completed-jobs/completed-jobs.list.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/completed-jobs/completed-jobs.list.js @@ -8,7 +8,7 @@ export default ['i18n', function(i18n) { return { // These tooltip fields are consumed to build disabled related tabs tooltips in the form > add view - awToolTip: i18n._('Please save and run a job to view'), + awToolTip: i18n._('Please save and run a job to view.'), dataPlacement: 'top', name: 'completed_jobs', basePath: 'unified_jobs', diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/groups/groups.form.js b/awx/ui/client/src/inventories-hosts/inventories/related/groups/groups.form.js index eb52db6ad7..cefa0fbff9 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/groups/groups.form.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/groups/groups.form.js @@ -83,7 +83,7 @@ function(i18n){ related: { nested_groups: { name: 'nested_groups', - awToolTip: i18n._('Please save before defining groups'), + awToolTip: i18n._('Please save before defining groups.'), dataPlacement: 'top', ngClick: "$state.go('inventories.edit.groups.edit.nested_groups')", title: i18n._('Groups'), @@ -91,7 +91,7 @@ function(i18n){ }, nested_hosts: { name: 'nested_hosts', - awToolTip: i18n._('Please save before defining hosts'), + awToolTip: i18n._('Please save before defining hosts.'), dataPlacement: 'top', ngClick: "$state.go('inventories.edit.groups.edit.nested_hosts')", include: "NestedHostsListDefinition", diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/groups/related/nested-hosts/group-nested-hosts.form.js b/awx/ui/client/src/inventories-hosts/inventories/related/groups/related/nested-hosts/group-nested-hosts.form.js index 37e207ce9f..f46c4e2176 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/groups/related/nested-hosts/group-nested-hosts.form.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/groups/related/nested-hosts/group-nested-hosts.form.js @@ -107,14 +107,14 @@ function(i18n) { related: { ansible_facts: { name: 'ansible_facts', - awToolTip: i18n._('Please save before viewing facts'), + awToolTip: i18n._('Please save before viewing facts.'), dataPlacement: 'top', title: i18n._('Facts'), skipGenerator: true }, nested_groups: { name: 'nested_groups', - awToolTip: i18n._('Please save before defining groups'), + awToolTip: i18n._('Please save before defining groups.'), dataPlacement: 'top', ngClick: "$state.go('inventories.edit.groups.edit.nested_hosts.edit.nested_groups')", title: i18n._('Groups'), diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/hosts/related-host.form.js b/awx/ui/client/src/inventories-hosts/inventories/related/hosts/related-host.form.js index 0ad88d8835..8ddc10a5a4 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/hosts/related-host.form.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/hosts/related-host.form.js @@ -107,14 +107,14 @@ function(i18n) { related: { ansible_facts: { name: 'ansible_facts', - awToolTip: i18n._('Please save before viewing facts'), + awToolTip: i18n._('Please save before viewing facts.'), dataPlacement: 'top', title: i18n._('Facts'), skipGenerator: true }, nested_groups: { name: 'nested_groups', - awToolTip: i18n._('Please save before defining groups'), + awToolTip: i18n._('Please save before defining groups.'), dataPlacement: 'top', ngClick: "$state.go('inventories.edit.hosts.edit.nested_groups')", title: i18n._('Groups'), @@ -122,7 +122,7 @@ function(i18n) { }, insights: { name: 'insights', - awToolTip: i18n._('Please save before viewing Insights'), + awToolTip: i18n._('Please save before viewing Insights.'), dataPlacement: 'top', title: i18n._('Insights'), skipGenerator: true, diff --git a/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory.form.js b/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory.form.js index ecf59a02f0..0e864fc4a1 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory.form.js +++ b/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory.form.js @@ -110,7 +110,7 @@ export default ['i18n', 'InventoryCompletedJobsList', function(i18n, InventoryCo related: { permissions: { name: 'permissions', - awToolTip: i18n._('Please save before assigning permissions'), + awToolTip: i18n._('Please save before assigning permissions.'), dataPlacement: 'top', basePath: 'api/v2/inventories/{{$stateParams.smartinventory_id}}/access_list/', type: 'collection', diff --git a/awx/ui/client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js b/awx/ui/client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js index 673896e994..5d4c24d811 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js +++ b/awx/ui/client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js @@ -127,7 +127,7 @@ function(i18n, InventoryCompletedJobsList) { related: { permissions: { name: 'permissions', - awToolTip: i18n._('Please save before assigning permissions'), + awToolTip: i18n._('Please save before assigning permissions.'), dataPlacement: 'top', basePath: 'api/v2/inventories/{{$stateParams.inventory_id}}/access_list/', type: 'collection', @@ -172,7 +172,7 @@ function(i18n, InventoryCompletedJobsList) { }, groups: { name: 'groups', - awToolTip: i18n._('Please save before creating groups'), + awToolTip: i18n._('Please save before creating groups.'), dataPlacement: 'top', include: "GroupList", title: i18n._('Groups'), @@ -181,7 +181,7 @@ function(i18n, InventoryCompletedJobsList) { }, hosts: { name: 'hosts', - awToolTip: i18n._('Please save before creating hosts'), + awToolTip: i18n._('Please save before creating hosts.'), dataPlacement: 'top', include: "RelatedHostsListDefinition", title: i18n._('Hosts'), @@ -190,7 +190,7 @@ function(i18n, InventoryCompletedJobsList) { }, inventory_sources: { name: 'inventory_sources', - awToolTip: i18n._('Please save before defining inventory sources'), + awToolTip: i18n._('Please save before defining inventory sources.'), dataPlacement: 'top', title: i18n._('Sources'), iterator: 'inventory_source', diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index dedc200007..ee39e70cc0 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -50,7 +50,7 @@ data-placement="top" ng-click="deleteJob()" ng-hide="job_status == 'running' || - job_status == 'pending' " + job_status == 'pending' || !job.summary_fields.user_capabilities.delete" aw-tool-tip="Delete" data-original-title="" title=""> diff --git a/awx/ui/client/src/notifications/notifications.list.js b/awx/ui/client/src/notifications/notifications.list.js index ad554ac3f2..c836e87476 100644 --- a/awx/ui/client/src/notifications/notifications.list.js +++ b/awx/ui/client/src/notifications/notifications.list.js @@ -12,7 +12,7 @@ export default ['i18n', function(i18n){ return { // These tooltip fields are consumed to build disabled related tabs tooltips in the form > add view dataPlacement: 'top', - awToolTip: i18n._('Please save before adding notifications'), + awToolTip: i18n._('Please save before adding notifications.'), name: 'notifications' , title: i18n._('Notifications'), iterator: 'notification', diff --git a/awx/ui/client/src/organizations/organizations.form.js b/awx/ui/client/src/organizations/organizations.form.js index 588523b677..42bda9b716 100644 --- a/awx/ui/client/src/organizations/organizations.form.js +++ b/awx/ui/client/src/organizations/organizations.form.js @@ -65,7 +65,7 @@ export default ['NotificationsList', 'i18n', users: { name: 'users', dataPlacement: 'top', - awToolTip: i18n._('Please save before adding users'), + awToolTip: i18n._('Please save before adding users.'), basePath: 'api/v2/organizations/{{$stateParams.organization_id}}/access_list/', search: { order_by: 'username' diff --git a/awx/ui/client/src/projects/projects.form.js b/awx/ui/client/src/projects/projects.form.js index 021242b4bf..af5b6b47a9 100644 --- a/awx/ui/client/src/projects/projects.form.js +++ b/awx/ui/client/src/projects/projects.form.js @@ -221,7 +221,7 @@ export default ['i18n', 'NotificationsList', function(i18n, NotificationsList) { related: { permissions: { name: 'permissions', - awToolTip: i18n._('Please save before assigning permissions'), + awToolTip: i18n._('Please save before assigning permissions.'), djangoModel: 'access_list', dataPlacement: 'top', basePath: 'api/v2/projects/{{$stateParams.project_id}}/access_list/', diff --git a/awx/ui/client/src/teams/teams.form.js b/awx/ui/client/src/teams/teams.form.js index 78ba95f159..ca4e8dc4c3 100644 --- a/awx/ui/client/src/teams/teams.form.js +++ b/awx/ui/client/src/teams/teams.form.js @@ -66,7 +66,7 @@ export default ['i18n', function(i18n) { users: { name: 'users', dataPlacement: 'top', - awToolTip: i18n._('Please save before adding users'), + awToolTip: i18n._('Please save before adding users.'), basePath: 'api/v2/teams/{{$stateParams.team_id}}/access_list/', search: { order_by: 'username' @@ -110,7 +110,7 @@ export default ['i18n', function(i18n) { // @todo ask about name field / serializer on this endpoint order_by: 'id' }, - awToolTip: i18n._('Please save before assigning permissions'), + awToolTip: i18n._('Please save before assigning permissions.'), dataPlacement: 'top', hideSearchAndActions: true, type: 'collection', diff --git a/awx/ui/client/src/templates/completed-jobs.list.js b/awx/ui/client/src/templates/completed-jobs.list.js index 56a0f76f52..a370145c96 100644 --- a/awx/ui/client/src/templates/completed-jobs.list.js +++ b/awx/ui/client/src/templates/completed-jobs.list.js @@ -8,7 +8,7 @@ export default ['i18n', function(i18n) { return { // These tooltip fields are consumed to build disabled related tabs tooltips in the form > add view - awToolTip: i18n._('Please save and run a job to view'), + awToolTip: i18n._('Please save and run a job to view.'), dataPlacement: 'top', name: 'completed_jobs', basePath: 'api/v2/job_templates/{{$stateParams.job_template_id}}/jobs', diff --git a/awx/ui/client/src/templates/job_templates/add-job-template/job-template-add.controller.js b/awx/ui/client/src/templates/job_templates/add-job-template/job-template-add.controller.js index e9b2176471..f2ab5e3792 100644 --- a/awx/ui/client/src/templates/job_templates/add-job-template/job-template-add.controller.js +++ b/awx/ui/client/src/templates/job_templates/add-job-template/job-template-add.controller.js @@ -17,7 +17,7 @@ Empty, ToJSON, CallbackHelpInit, GetChoices, $state, availableLabels, CreateSelect2, $q, i18n, Inventory, Project, InstanceGroupsService, MultiCredentialService ) { - + // Inject dynamic view let defaultUrl = GetBasePath('job_templates'), form = JobTemplateForm(), @@ -45,6 +45,8 @@ default_val: false }); CallbackHelpInit({ scope: $scope }); + + $scope.surveyTooltip = i18n._('Please save before adding a survey to this job template.'); } callback = function() { diff --git a/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js b/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js index ff88cf9473..8a3b5b9a75 100644 --- a/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js +++ b/awx/ui/client/src/templates/job_templates/edit-job-template/job-template-edit.controller.js @@ -14,7 +14,7 @@ export default [ '$filter', '$scope', '$rootScope', '$location', '$stateParams', 'JobTemplateForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'GetBasePath', 'md5Setup', - 'ParseTypeChange', 'Wait', 'selectedLabels', + 'ParseTypeChange', 'Wait', 'selectedLabels', 'i18n', 'Empty', 'Prompt', 'ToJSON', 'GetChoices', 'CallbackHelpInit', 'InitiatePlaybookRun' , 'initSurvey', '$state', 'CreateSelect2', 'ToggleNotification','$q', 'InstanceGroupsService', 'InstanceGroupsData', 'MultiCredentialService', 'availableLabels', @@ -22,7 +22,7 @@ export default $filter, $scope, $rootScope, $location, $stateParams, JobTemplateForm, GenerateForm, Rest, Alert, ProcessErrors, GetBasePath, md5Setup, - ParseTypeChange, Wait, selectedLabels, + ParseTypeChange, Wait, selectedLabels, i18n, Empty, Prompt, ToJSON, GetChoices, CallbackHelpInit, InitiatePlaybookRun, SurveyControllerInit, $state, CreateSelect2, ToggleNotification, $q, InstanceGroupsService, InstanceGroupsData, MultiCredentialService, availableLabels ) { @@ -54,6 +54,7 @@ export default $scope.showJobType = false; $scope.instance_groups = InstanceGroupsData; $scope.credentialNotPresent = false; + $scope.surveyTooltip = i18n._('Surveys allow users to be prompted at job launch with a series of questions related to the job. This allows for variables to be defined that affect the playbook run at time of launch.'); SurveyControllerInit({ scope: $scope, diff --git a/awx/ui/client/src/templates/job_templates/job-template.form.js b/awx/ui/client/src/templates/job_templates/job-template.form.js index fe10b8e952..e2b076d60b 100644 --- a/awx/ui/client/src/templates/job_templates/job-template.form.js +++ b/awx/ui/client/src/templates/job_templates/job-template.form.js @@ -386,7 +386,7 @@ function(NotificationsList, CompletedJobsList, i18n) { }, permissions: { name: 'permissions', - awToolTip: i18n._('Please save before assigning permissions'), + awToolTip: i18n._('Please save before assigning permissions.'), dataPlacement: 'top', basePath: 'api/v2/job_templates/{{$stateParams.job_template_id}}/access_list/', search: { @@ -447,7 +447,7 @@ function(NotificationsList, CompletedJobsList, i18n) { ngClick: 'addSurvey()', ngShow: '($state.is(\'templates.addJobTemplate\') || $state.is(\'templates.editJobTemplate\')) && !survey_exists && (job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)', awFeature: 'surveys', - awToolTip: 'Surveys allow users to be prompted at job launch with a series of questions related to the job. This allows for variables to be defined that affect the playbook run at time of launch.', + awToolTip: '{{surveyTooltip}}', dataPlacement: 'top', label: i18n._('Add Survey'), class: 'Form-primaryButton' @@ -457,7 +457,9 @@ function(NotificationsList, CompletedJobsList, i18n) { awFeature: 'surveys', ngShow: '($state.is(\'templates.addJobTemplate\') || $state.is(\'templates.editJobTemplate\')) && survey_exists && (job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)', label: i18n._('Edit Survey'), - class: 'Form-primaryButton' + class: 'Form-primaryButton', + awToolTip: '{{surveyTooltip}}', + dataPlacement: 'top' } } }; diff --git a/awx/ui/client/src/templates/workflows.form.js b/awx/ui/client/src/templates/workflows.form.js index dab524019a..5a709e1067 100644 --- a/awx/ui/client/src/templates/workflows.form.js +++ b/awx/ui/client/src/templates/workflows.form.js @@ -106,7 +106,7 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n) { related: { permissions: { name: 'permissions', - awToolTip: i18n._('Please save before assigning permissions'), + awToolTip: i18n._('Please save before assigning permissions.'), dataPlacement: 'top', basePath: 'api/v2/workflow_job_templates/{{$stateParams.workflow_job_template_id}}/access_list/', search: { @@ -167,7 +167,7 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n) { ngClick: 'addSurvey()', ngShow: '!survey_exists && ($state.includes(\'templates.addWorkflowJobTemplate\') || $state.includes(\'templates.editWorkflowJobTemplate\'))', awFeature: 'surveys', - awToolTip: 'Surveys allow users to be prompted at job launch with a series of questions related to the job. This allows for variables to be defined that affect the playbook run at time of launch.', + awToolTip: '{{surveyTooltip}}', dataPlacement: 'top', label: i18n._('Add Survey'), class: 'Form-primaryButton' @@ -177,12 +177,14 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n) { awFeature: 'surveys', ngShow: 'survey_exists && ($state.includes(\'templates.addWorkflowJobTemplate\') || $state.includes(\'templates.editWorkflowJobTemplate\'))', label: i18n._('Edit Survey'), - class: 'Form-primaryButton' + class: 'Form-primaryButton', + awToolTip: '{{surveyTooltip}}', + dataPlacement: 'top' }, workflow_editor: { ngClick: 'openWorkflowMaker()', ngShow: '$state.includes(\'templates.addWorkflowJobTemplate\') || $state.includes(\'templates.editWorkflowJobTemplate\')', - awToolTip: i18n._('Please save before defining the workflow graph'), + awToolTip: '{{workflowEditorTooltip}}', dataPlacement: 'top', label: i18n._('Workflow Editor'), class: 'Form-primaryButton' diff --git a/awx/ui/client/src/templates/workflows/add-workflow/workflow-add.controller.js b/awx/ui/client/src/templates/workflows/add-workflow/workflow-add.controller.js index 9c32fe652a..6fdbff391d 100644 --- a/awx/ui/client/src/templates/workflows/add-workflow/workflow-add.controller.js +++ b/awx/ui/client/src/templates/workflows/add-workflow/workflow-add.controller.js @@ -7,10 +7,10 @@ export default [ '$scope', 'WorkflowForm', 'GenerateForm', 'Alert', 'ProcessErrors', 'Wait', '$state', 'CreateSelect2', 'TemplatesService', - 'ToJSON', 'ParseTypeChange', '$q', 'Rest', 'GetBasePath', 'availableLabels', + 'ToJSON', 'ParseTypeChange', '$q', 'Rest', 'GetBasePath', 'availableLabels', 'i18n', function($scope, WorkflowForm, GenerateForm, Alert, ProcessErrors, Wait, $state, CreateSelect2, TemplatesService, ToJSON, - ParseTypeChange, $q, Rest, GetBasePath, availableLabels) { + ParseTypeChange, $q, Rest, GetBasePath, availableLabels, i18n) { // Inject dynamic view let form = WorkflowForm(), @@ -41,6 +41,9 @@ export default [ multiple: true, addNew: true }); + + $scope.workflowEditorTooltip = i18n._("Please save before defining the workflow graph."); + $scope.surveyTooltip = i18n._('Please save before adding a survey to this workflow.'); } $scope.formSave = function () { diff --git a/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js b/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js index d61beadd78..71257b99e2 100644 --- a/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js +++ b/awx/ui/client/src/templates/workflows/edit-workflow/workflow-edit.controller.js @@ -9,12 +9,12 @@ export default [ 'ProcessErrors', 'GetBasePath', '$q', 'ParseTypeChange', 'Wait', 'Empty', 'ToJSON', 'initSurvey', '$state', 'CreateSelect2', 'ParseVariableString', 'TemplatesService', 'Rest', 'ToggleNotification', - 'OrgAdminLookup', 'availableLabels', 'selectedLabels', 'workflowJobTemplateData', + 'OrgAdminLookup', 'availableLabels', 'selectedLabels', 'workflowJobTemplateData', 'i18n', function($scope, $stateParams, WorkflowForm, GenerateForm, Alert, ProcessErrors, GetBasePath, $q, ParseTypeChange, Wait, Empty, ToJSON, SurveyControllerInit, $state, CreateSelect2, ParseVariableString, - TemplatesService, Rest, ToggleNotification, OrgAdminLookup, availableLabels, selectedLabels, workflowJobTemplateData) { - + TemplatesService, Rest, ToggleNotification, OrgAdminLookup, availableLabels, selectedLabels, workflowJobTemplateData, i18n) { + $scope.$watch('workflow_job_template_obj.summary_fields.user_capabilities.edit', function(val) { if (val === false) { $scope.canAddWorkflowJobTemplate = false; @@ -60,6 +60,9 @@ export default [ opts: opts }); + $scope.workflowEditorTooltip = i18n._("Click here to open the workflow graph editor."); + $scope.surveyTooltip = i18n._('Surveys allow users to be prompted at job launch with a series of questions related to the job. This allows for variables to be defined that affect the playbook run at time of launch.'); + $scope.workflow_job_template_obj = workflowJobTemplateData; $scope.name = workflowJobTemplateData.name; $scope.can_edit = workflowJobTemplateData.summary_fields.user_capabilities.edit; diff --git a/awx/ui/client/src/users/users.form.js b/awx/ui/client/src/users/users.form.js index 931d26e4a2..08f0fc543f 100644 --- a/awx/ui/client/src/users/users.form.js +++ b/awx/ui/client/src/users/users.form.js @@ -118,7 +118,7 @@ export default ['i18n', function(i18n) { related: { organizations: { name: 'organizations', - awToolTip: i18n._('Please save before assigning to organizations'), + awToolTip: i18n._('Please save before assigning to organizations.'), basePath: 'api/v2/users/{{$stateParams.user_id}}/organizations', emptyListText: i18n._('Please add user to an Organization.'), search: { @@ -146,7 +146,7 @@ export default ['i18n', function(i18n) { }, teams: { name: 'teams', - awToolTip: i18n._('Please save before assigning to teams'), + awToolTip: i18n._('Please save before assigning to teams.'), basePath: 'api/v2/users/{{$stateParams.user_id}}/teams', search: { page_size: '10' @@ -177,7 +177,7 @@ export default ['i18n', function(i18n) { page_size: '10', order_by: 'id' }, - awToolTip: i18n._('Please save before assigning to organizations'), + awToolTip: i18n._('Please save before assigning to organizations.'), dataPlacement: 'top', hideSearchAndActions: true, type: 'collection',