diff --git a/awx/api/views.py b/awx/api/views.py index 20a6681d36..5c177dbebf 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -282,10 +282,16 @@ class DashboardView(APIView): user_hosts = get_user_queryset(request.user, Host) user_hosts_failed = user_hosts.filter(has_active_failures=True) + try: + user_hosts_count = user_hosts.distinct('name').count() + user_hosts_failed_count = user_hosts_failed.distinct('name').count() + except NotImplementedError: # For unit tests only, SQLite doesn't support distinct('name') + user_hosts_count = len(set(user_hosts.values_list('name', flat=True))) + user_hosts_failed_count = len(set(user_hosts_failed.values_list('name', flat=True))) data['hosts'] = {'url': reverse('api:host_list'), 'failures_url': reverse('api:host_list') + "?has_active_failures=True", - 'total': user_hosts.count(), - 'failed': user_hosts_failed.count()} + 'total': user_hosts_count, + 'failed': user_hosts_failed_count} user_projects = get_user_queryset(request.user, Project) user_projects_failed = user_projects.filter(last_job_failed=True) @@ -2352,7 +2358,7 @@ class HostAdHocCommandsList(AdHocCommandList, SubListCreateAPIView): relationship = 'ad_hoc_commands' -class AdHocCommandDetail(RetrieveAPIView): +class AdHocCommandDetail(RetrieveDestroyAPIView): model = AdHocCommand serializer_class = AdHocCommandSerializer @@ -2399,6 +2405,21 @@ class AdHocCommandRelaunch(GenericAPIView): if not request.user.can_access(self.model, 'start', obj): raise PermissionDenied() + # Re-validate ad hoc command against serializer to check if module is + # still allowed. + data = {} + for field in ('job_type', 'inventory_id', 'limit', 'credential_id', + 'module_name', 'module_args', 'forks', 'verbosity', + 'become_enabled'): + if field.endswith('_id'): + data[field[:-3]] = getattr(obj, field) + else: + data[field] = getattr(obj, field) + serializer = self.get_serializer(data=data) + if not serializer.is_valid(): + return Response(serializer.errors, + status=status.HTTP_400_BAD_REQUEST) + # Check for passwords needed before copying ad hoc command. needed = obj.passwords_needed_to_start provided = dict([(field, request.DATA.get(field, '')) for field in needed]) diff --git a/awx/main/access.py b/awx/main/access.py index c487187941..1cbc50bf09 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1208,7 +1208,7 @@ class AdHocCommandAccess(BaseAccess): # If a credential is provided, the user should have read access to it. credential_pk = get_pk_from_dict(data, 'credential') if credential_pk: - credential = get_object_or_400(Credential, pk=credential_pk) + credential = get_object_or_400(Credential, pk=credential_pk, active=True) if not self.user.can_access(Credential, 'read', credential): return False @@ -1216,7 +1216,7 @@ class AdHocCommandAccess(BaseAccess): # given inventory. inventory_pk = get_pk_from_dict(data, 'inventory') if inventory_pk: - inventory = get_object_or_400(Inventory, pk=inventory_pk) + inventory = get_object_or_400(Inventory, pk=inventory_pk, active=True) if not self.user.can_access(Inventory, 'run_ad_hoc_commands', inventory): return False @@ -1226,7 +1226,7 @@ class AdHocCommandAccess(BaseAccess): return False def can_delete(self, obj): - return False + return self.can_read(obj) def can_start(self, obj): return self.can_add({ diff --git a/awx/main/management/commands/cleanup_deleted.py b/awx/main/management/commands/cleanup_deleted.py index af0266a2be..4c84a695cd 100644 --- a/awx/main/management/commands/cleanup_deleted.py +++ b/awx/main/management/commands/cleanup_deleted.py @@ -40,6 +40,29 @@ class Command(BaseCommand): yield submodel def cleanup_model(self, model): + + n_deleted_items = 0 + pks_to_delete = set() + for asobj in ActivityStream.objects.iterator(): + asobj_disp = '"%s" id: %s' % (unicode(asobj), asobj.id) + if asobj.timestamp >= self.cutoff: + if self.dry_run: + self.logger.info("would skip %s" % asobj_disp) + else: + if self.dry_run: + self.logger.info("would delete %s" % asobj_disp) + else: + pks_to_delete.add(asobj.pk) + # Cleanup objects in batches instead of deleting each one individually. + if len(pks_to_delete) >= 500: + ActivityStream.objects.filter(pk__in=pks_to_delete).delete() + n_deleted_items += len(pks_to_delete) + pks_to_delete.clear() + if len(pks_to_delete): + ActivityStream.objects.filter(pk__in=pks_to_delete).delete() + n_deleted_items += len(pks_to_delete) + print("Removed %s items" % str(n_deleted_items)) + name_field = None active_field = None n_deleted_items = 0 @@ -63,7 +86,8 @@ class Command(BaseCommand): '%s__startswith' % name_field: name_prefix, }) self.logger.debug('cleaning up model %s', model) - for instance in qs: + pks_to_delete = set() + for instance in qs.iterator(): dt = parse_datetime(getattr(instance, name_field).split('_')[2]) if not is_aware(dt): dt = make_aware(dt, self.cutoff.tzinfo) @@ -76,10 +100,17 @@ class Command(BaseCommand): else: action_text = 'would delete' if self.dry_run else 'deleting' self.logger.info('%s %s', action_text, instance) - n_deleted_items += 1 if not self.dry_run: - instance.delete() + pks_to_delete.add(instance.pk) + # Cleanup objects in batches instead of deleting each one individually. + if len(pks_to_delete) >= 500: + model.objects.filter(pk__in=pks_to_delete).delete() + n_deleted_items += len(pks_to_delete) + pks_to_delete.clear() + if len(pks_to_delete): + model.objects.filter(pk__in=pks_to_delete).delete() + n_deleted_items += len(pks_to_delete) return n_deleted_items def init_logging(self): diff --git a/awx/main/tests/ad_hoc.py b/awx/main/tests/ad_hoc.py index 2b729a17ba..b5155ed155 100644 --- a/awx/main/tests/ad_hoc.py +++ b/awx/main/tests/ad_hoc.py @@ -560,12 +560,14 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): def test_ad_hoc_command_detail(self): with self.current_user('admin'): - response = self.run_test_ad_hoc_command() + response1 = self.run_test_ad_hoc_command() + response2 = self.run_test_ad_hoc_command() + response3 = self.run_test_ad_hoc_command() # Retrieve detail for ad hoc command. Only GET is supported. - url = reverse('api:ad_hoc_command_detail', args=(response['id'],)) - self.assertEqual(url, response['url']) with self.current_user('admin'): + url = reverse('api:ad_hoc_command_detail', args=(response1['id'],)) + self.assertEqual(url, response1['url']) response = self.get(url, expect=200) self.assertEqual(response['credential'], self.credential.pk) self.assertEqual(response['related']['credential'], @@ -580,22 +582,28 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): self.assertTrue(response['related']['activity_stream']) self.put(url, {}, expect=405) self.patch(url, {}, expect=405) - self.delete(url, expect=405) + self.delete(url, expect=204) + self.delete(url, expect=404) with self.current_user('normal'): + url = reverse('api:ad_hoc_command_detail', args=(response2['id'],)) + self.assertEqual(url, response2['url']) response = self.get(url, expect=200) self.put(url, {}, expect=405) self.patch(url, {}, expect=405) - self.delete(url, expect=405) + self.delete(url, expect=204) + self.delete(url, expect=404) + url = reverse('api:ad_hoc_command_detail', args=(response3['id'],)) + self.assertEqual(url, response3['url']) with self.current_user('other'): response = self.get(url, expect=403) self.put(url, {}, expect=405) self.patch(url, {}, expect=405) - self.delete(url, expect=405) + self.delete(url, expect=403) with self.current_user('nobody'): response = self.get(url, expect=403) self.put(url, {}, expect=405) self.patch(url, {}, expect=405) - self.delete(url, expect=405) + self.delete(url, expect=403) with self.current_user(None): response = self.get(url, expect=401) self.put(url, {}, expect=401) @@ -603,13 +611,16 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): self.delete(url, expect=401) # Verify that the credential and inventory are null when they have - # been deleted. + # been deleted, can delete an ad hoc command without inventory or + # credential. self.credential.mark_inactive() self.inventory.mark_inactive() with self.current_user('admin'): response = self.get(url, expect=200) self.assertEqual(response['credential'], None) self.assertEqual(response['inventory'], None) + self.delete(url, expect=204) + self.delete(url, expect=404) def test_ad_hoc_command_cancel(self): # Override setting so that ad hoc command isn't actually started. @@ -708,6 +719,21 @@ class AdHocCommandApiTest(BaseAdHocCommandTest): self.patch(url, {}, expect=401) self.delete(url, expect=401) + # Try to relaunch ad hoc command when module has been removed from + # allowed list of modules. + with self.settings(AD_HOC_COMMANDS=[]): + with self.current_user('admin'): + response = self.get(url, expect=200) + self.assertEqual(response['passwords_needed_to_start'], []) + response = self.post(url, {}, expect=400) + + # Try to relaunch after the inventory has been marked inactive. + self.inventory.mark_inactive() + with self.current_user('admin'): + response = self.get(url, expect=200) + self.assertEqual(response['passwords_needed_to_start'], []) + response = self.post(url, {}, expect=400) + def test_ad_hoc_command_events_list(self): with self.current_user('admin'): response = self.run_test_ad_hoc_command() diff --git a/awx/main/tests/commands/commands_monolithic.py b/awx/main/tests/commands/commands_monolithic.py index 8988bdae30..972ca8ac9b 100644 --- a/awx/main/tests/commands/commands_monolithic.py +++ b/awx/main/tests/commands/commands_monolithic.py @@ -252,6 +252,28 @@ class CleanupDeletedTest(BaseCommandMixin, BaseTest): self.assertNotEqual(counts_before, counts_after) self.assertFalse(sum(x[1] for x in counts_after.values())) + # Create lots of hosts already marked as deleted. + t = time.time() + dtnow = now() + for x in xrange(1000): + hostname = "_deleted_%s_host-%d" % (dtnow.isoformat(), x) + host = self.inventories[0].hosts.create(name=hostname, active=False) + create_elapsed = time.time() - t + + # Time how long it takes to cleanup deleted items, should be no more + # then the time taken to create them. + counts_before = self.get_model_counts() + self.assertTrue(sum(x[1] for x in counts_before.values())) + t = time.time() + result, stdout, stderr = self.run_command('cleanup_deleted', days=0) + cleanup_elapsed = time.time() - t + self.assertEqual(result, None) + counts_after = self.get_model_counts() + self.assertNotEqual(counts_before, counts_after) + self.assertFalse(sum(x[1] for x in counts_after.values())) + self.assertTrue(cleanup_elapsed < create_elapsed, + 'create took %0.3fs, cleanup took %0.3fs, expected < %0.3fs' % (create_elapsed, cleanup_elapsed, create_elapsed)) + def get_user_counts(self): active = User.objects.filter(is_active=True).count() inactive = User.objects.filter(is_active=False).count() diff --git a/awx/main/tests/fact/fact_api.py b/awx/main/tests/fact/fact_api.py index 55156d1436..7447f04e6d 100644 --- a/awx/main/tests/fact/fact_api.py +++ b/awx/main/tests/fact/fact_api.py @@ -93,7 +93,7 @@ class FactVersionApiTest(FactApiBaseTest): response = self.get(url, expect=200) for entry in response['results']: self.assertIn('fact_view', entry['related']) - r = self.get(entry['related']['fact_view'], expect=200) + self.get(entry['related']['fact_view'], expect=200) def test_list(self): self.setup_facts(2) @@ -207,7 +207,7 @@ class SingleFactApiTest(FactApiBaseTest): for entry in response['results']: self.assertIn('single_fact', entry['related']) # Requires fields - r = self.get(entry['related']['single_fact'], expect=400) + self.get(entry['related']['single_fact'], expect=400) def test_related_host_list(self): self.setup_facts(2) diff --git a/awx/main/tests/inventory.py b/awx/main/tests/inventory.py index 62741c1678..8771501062 100644 --- a/awx/main/tests/inventory.py +++ b/awx/main/tests/inventory.py @@ -1089,6 +1089,45 @@ class InventoryTest(BaseTest): self.assertEqual(set(h_e.all_groups.values_list('pk', flat=True)), set([g_e.pk])) + def test_dashboard_hosts_count(self): + url = reverse('api:dashboard_view') + + # Test with zero hosts. + with self.current_user(self.super_django_user): + response = self.get(url, expect=200) + self.assertEqual(response['hosts']['total'], 0) + self.assertEqual(response['hosts']['failed'], 0) + + # Create hosts with the same name in different inventories. + for x in xrange(4): + hostname = 'host-%d' % x + self.inventory_a.hosts.create(name=hostname) + self.inventory_b.hosts.create(name=hostname) + with self.current_user(self.super_django_user): + response = self.get(url, expect=200) + self.assertEqual(response['hosts']['total'], 4) + self.assertEqual(response['hosts']['failed'], 0) + + # Mark all hosts in one inventory as failed. Failed count should + # reflect unique hostnames. + for host in self.inventory_a.hosts.all(): + host.has_active_failures = True + host.save() + with self.current_user(self.super_django_user): + response = self.get(url, expect=200) + self.assertEqual(response['hosts']['total'], 4) + self.assertEqual(response['hosts']['failed'], 4) + + # Mark all hosts in the other inventory as failed. Failed count + # should reflect unique hostnames and never be greater than total. + for host in self.inventory_b.hosts.all(): + host.has_active_failures = True + host.save() + with self.current_user(self.super_django_user): + response = self.get(url, expect=200) + self.assertEqual(response['hosts']['total'], 4) + self.assertEqual(response['hosts']['failed'], 4) + def test_dashboard_inventory_graph_view(self): url = reverse('api:dashboard_inventory_graph_view') # Test with zero hosts. diff --git a/awx/ui/static/js/controllers/JobTemplates.js b/awx/ui/static/js/controllers/JobTemplates.js index 7563b987c4..443ab095ee 100644 --- a/awx/ui/static/js/controllers/JobTemplates.js +++ b/awx/ui/static/js/controllers/JobTemplates.js @@ -346,6 +346,25 @@ export function JobTemplatesAdd($scope, $rootScope, $compile, $location, $log, $ // this sets the default options for the selects as specified by the controller. $scope.verbosity = $scope.verbosity_options[$scope.verbosity_field.default]; $scope.job_type = $scope.job_type_options[$scope.job_type_field.default]; + + // if you're getting to the form from the scan job section on inventories, + // set the job type select to be scan + if ($routeParams.inventory_id) { + // This means that the job template form was accessed via inventory prop's + // This also means the job is a scan job. + $scope.job_type.value = 'scan'; + $scope.jobTypeChange(); + $scope.inventory = $routeParams.inventory_id; + Rest.setUrl(GetBasePath('inventory') + $routeParams.inventory_id + '/'); + Rest.get() + .success(function (data) { + $scope.inventory_name = data.name; + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to lookup inventory: ' + data.id + '. GET returned status: ' + status }); + }); + } $scope.$emit('lookUpInitialize'); } }); @@ -424,23 +443,6 @@ export function JobTemplatesAdd($scope, $rootScope, $compile, $location, $log, $ } }; - if ($routeParams.inventory_id) { - // This means that the job template form was accessed via inventory prop's - // This also means the job is a scan job. - $scope.job_type.value = 'scan'; - $scope.jobTypeChange(); - $scope.inventory = $routeParams.inventory_id; - Rest.setUrl(GetBasePath('inventory') + $routeParams.inventory_id + '/'); - Rest.get() - .success(function (data) { - $scope.inventory_name = data.name; - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to lookup inventory: ' + data.id + '. GET returned status: ' + status }); - }); - } - // Detect and alert user to potential SCM status issues checkSCMStatus = function (oldValue, newValue) { if (oldValue !== newValue && !Empty($scope.project)) { diff --git a/awx/ui/static/js/filters/sanitize/xss-sanitizer.filter.js b/awx/ui/static/js/filters/sanitize/xss-sanitizer.filter.js index 40689805d1..0ed767aacb 100644 --- a/awx/ui/static/js/filters/sanitize/xss-sanitizer.filter.js +++ b/awx/ui/static/js/filters/sanitize/xss-sanitizer.filter.js @@ -1,6 +1,6 @@ angular.module('sanitizeFilter', []).filter('sanitize', function() { return function(input) { - input = input.replace(//g, ">"); + input = input.replace(//g, ">").replace(/'/g, "'").replace(/"/g, """); return input; }; }); diff --git a/awx/ui/static/js/helpers/EventViewer.js b/awx/ui/static/js/helpers/EventViewer.js index 6e8c13802a..ff76bffc3d 100644 --- a/awx/ui/static/js/helpers/EventViewer.js +++ b/awx/ui/static/js/helpers/EventViewer.js @@ -437,8 +437,7 @@ export default else { if( typeof itm === "string"){ if(itm.indexOf('<') > -1 || itm.indexOf('>') > -1){ - itm = itm.replace(//g, ">"); + itm = $filter('sanitize')(itm); } } html += "" + itm + ""; @@ -547,15 +546,14 @@ export default }; }]) - .factory('EventAddPreFormattedText', [function() { + .factory('EventAddPreFormattedText', ['$filter', function($filter) { return function(params) { var id = params.id, val = params.val, html; if( typeof val === "string"){ if(val.indexOf('<') > -1 || val.indexOf('>') > -1){ - val = val.replace(//g, ">"); + val = $filter('sanitize')(val); } } html = "
" + val + "
\n"; diff --git a/awx/ui/static/js/helpers/JobSubmission.js b/awx/ui/static/js/helpers/JobSubmission.js index f0d9f4bc83..3ec4b2f8ef 100644 --- a/awx/ui/static/js/helpers/JobSubmission.js +++ b/awx/ui/static/js/helpers/JobSubmission.js @@ -497,9 +497,9 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm, }; }]) - .factory('PromptForSurvey', ['$compile', 'Wait', 'Alert', 'CredentialForm', 'CreateLaunchDialog', 'SurveyControllerInit' , 'GetBasePath', 'Rest' , 'Empty', + .factory('PromptForSurvey', ['$filter', '$compile', 'Wait', 'Alert', 'CredentialForm', 'CreateLaunchDialog', 'SurveyControllerInit' , 'GetBasePath', 'Rest' , 'Empty', 'GenerateForm', 'ShowSurveyModal', 'ProcessErrors', '$routeParams' , - function($compile, Wait, Alert, CredentialForm, CreateLaunchDialog, SurveyControllerInit, GetBasePath, Rest, Empty, + function($filter, $compile, Wait, Alert, CredentialForm, CreateLaunchDialog, SurveyControllerInit, GetBasePath, Rest, Empty, GenerateForm, ShowSurveyModal, ProcessErrors, $routeParams) { return function(params) { var html = params.html || "", @@ -519,10 +519,8 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm, function buildHtml(question, index){ question.index = index; - question.question_name = question.question_name.replace(//g, ">"); - question.question_description = (question.question_description) ? question.question_description.replace(//g, ">") : undefined; + question.question_name = $filter('sanitize')(question.question_name); + question.question_description = (question.question_description) ? $filter('sanitize')(question.question_description) : undefined; requiredAsterisk = (question.required===true) ? "prepend-asterisk" : ""; @@ -603,8 +601,7 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm, html+='
'; for( j = 0; j/g, ">"); + choices[j] = $filter('sanitize')(choices[j]); html+= '' + ''+choices[j] +'
' ; } diff --git a/awx/ui/static/js/helpers/Schedules.js b/awx/ui/static/js/helpers/Schedules.js index ba0aa4eeb7..a32ea1abc2 100644 --- a/awx/ui/static/js/helpers/Schedules.js +++ b/awx/ui/static/js/helpers/Schedules.js @@ -341,7 +341,7 @@ export default Wait('stop'); } }) - .error( function() { + .error( function(data, status) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to update schedule ' + id + ' PUT returned: ' + status }); }); diff --git a/awx/ui/static/js/helpers/Survey.js b/awx/ui/static/js/helpers/Survey.js index ab7a922bd0..0f1f494ed6 100644 --- a/awx/ui/static/js/helpers/Survey.js +++ b/awx/ui/static/js/helpers/Survey.js @@ -15,7 +15,7 @@ import listGenerator from 'tower/shared/list-generator/main'; export default angular.module('SurveyHelper', [ 'Utilities', 'RestServices', 'SchedulesHelper', 'SearchHelper', 'PaginationHelpers', listGenerator.name, 'ModalDialog' , - 'GeneratorHelpers']) + 'GeneratorHelpers', 'sanitizeFilter']) .factory('ShowSurveyModal', ['Wait', 'CreateDialog', 'Empty', '$compile' , function(Wait, CreateDialog, Empty, $compile) { @@ -253,8 +253,8 @@ angular.module('SurveyHelper', [ 'Utilities', 'RestServices', 'SchedulesHelper', * }) * */ - .factory('FinalizeQuestion', ['GetBasePath','Rest', 'Wait', 'ProcessErrors', '$compile', 'Empty', - function(GetBasePath, Rest, Wait, ProcessErrors, $compile, Empty) { + .factory('FinalizeQuestion', ['GetBasePath','Rest', 'Wait', 'ProcessErrors', '$compile', 'Empty', '$filter', + function(GetBasePath, Rest, Wait, ProcessErrors, $compile, Empty, $filter) { return function(params) { var scope = params.scope, @@ -272,10 +272,8 @@ angular.module('SurveyHelper', [ 'Utilities', 'RestServices', 'SchedulesHelper', html = ""; question.index = index; - question.question_name = question.question_name.replace(//g, ">"); - question.question_description = (question.question_description) ? question.question_description.replace(//g, ">") : undefined; + question.question_name = $filter('sanitize')(question.question_name); + question.question_description = (question.question_description) ? $filter('sanitize')(question.question_description) : undefined; if(!$('#question_'+question.index+':eq(0)').is('div')){ @@ -291,8 +289,7 @@ angular.module('SurveyHelper', [ 'Utilities', 'RestServices', 'SchedulesHelper', if(question.type === 'text' ){ defaultValue = (question.default) ? question.default : ""; - defaultValue = defaultValue.replace(//g, ">"); + defaultValue = $filter('sanitize')(defaultValue); defaultValue = scope.serialize(defaultValue); html+='
'+ '
'+ @@ -301,8 +298,7 @@ angular.module('SurveyHelper', [ 'Utilities', 'RestServices', 'SchedulesHelper', } if(question.type === "textarea"){ defaultValue = (question.default) ? question.default : (question.default_textarea) ? question.default_textarea: "" ; - defaultValue = defaultValue.replace(//g, ">"); + defaultValue = $filter('sanitize')(defaultValue); defaultValue = scope.serialize(defaultValue); html+='
'+ '
'+ @@ -317,8 +313,7 @@ angular.module('SurveyHelper', [ 'Utilities', 'RestServices', 'SchedulesHelper', html += '
'; for( i = 0; i/g, ">"); + choices[i] = $filter('sanitize')(choices[i]); choices[i] = scope.serialize(choices[i]); html+= '' + ''+choices[i] +'
' ; @@ -328,8 +323,7 @@ angular.module('SurveyHelper', [ 'Utilities', 'RestServices', 'SchedulesHelper', if(question.type === 'password'){ defaultValue = (question.default) ? question.default : ""; - defaultValue = defaultValue.replace(//g, ">"); + defaultValue = $filter('defaultValue')(choices[i]); defaultValue = scope.serialize(defaultValue); html+='
'+ '
'+ diff --git a/awx/ui/static/js/shared/Utilities.js b/awx/ui/static/js/shared/Utilities.js index a09f730feb..9731dde633 100644 --- a/awx/ui/static/js/shared/Utilities.js +++ b/awx/ui/static/js/shared/Utilities.js @@ -590,22 +590,20 @@ angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter']) .factory('Wait', ['$rootScope', function ($rootScope) { return function (directive) { - var docw, doch, spinnyw, spinnyh, x, y; + var docw, doch, spinnyw, spinnyh; if (directive === 'start' && !$rootScope.waiting) { $rootScope.waiting = true; docw = $(window).width(); doch = $(window).height(); spinnyw = $('.spinny').width(); spinnyh = $('.spinny').height(); - x = (docw - spinnyw) / 2; - y = (doch - spinnyh) / 2; $('.overlay').css({ width: $(document).width(), height: $(document).height() }).fadeIn(); $('.spinny').css({ - top: y, - left: x + bottom: 15, + right: 15 }).fadeIn(400); } else if (directive === 'stop' && $rootScope.waiting) { $('.spinny, .overlay').fadeOut(400, function () { diff --git a/awx/ui/static/js/shared/form-generator.js b/awx/ui/static/js/shared/form-generator.js index e8f89f3e56..f49fe1764b 100644 --- a/awx/ui/static/js/shared/form-generator.js +++ b/awx/ui/static/js/shared/form-generator.js @@ -197,6 +197,10 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat this.scope = element.scope(); } + if (options.mode) { + this.scope.mode = options.mode; + } + for (fld in form.fields) { this.scope[fld + '_field'] = form.fields[fld]; this.scope[fld + '_field'].name = fld; @@ -752,7 +756,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += "'"; html += (field.ngShow) ? this.attr(field, 'ngShow') : ""; html += (field.ngHide) ? this.attr(field, 'ngHide') : ""; - html += (field.awFeature) ? "aw-feature=\"" + field.awFeature + "\" " : ""; + html += (field.awFeature) ? "aw-feature=\"" + field.awFeature + "\" " : ""; html += ">\n"; //text fields diff --git a/awx/ui/static/js/widgets/Stream.js b/awx/ui/static/js/widgets/Stream.js index afa564c2cd..3000100012 100644 --- a/awx/ui/static/js/widgets/Stream.js +++ b/awx/ui/static/js/widgets/Stream.js @@ -176,8 +176,8 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti } ]) -.factory('BuildDescription', ['FixUrl', 'BuildUrl','$sce', - function (FixUrl, BuildUrl, $sce) { +.factory('BuildDescription', ['$filter', 'FixUrl', 'BuildUrl','$sce', + function ($filter, FixUrl, BuildUrl, $sce) { return function (activity) { function stripDeleted(s) { @@ -210,9 +210,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti // The block until line 221 is for associative/disassociative operations, such as adding/removing a user to a team or vise versa if (obj2_obj && obj2_obj.name && !/^_delete/.test(obj2_obj.name)) { obj2_obj.base = obj2; - obj2_obj.name = obj2_obj.name.replace(//g, ">"); - obj2_obj.name = $sce.getTrustedHtml(obj2_obj.name); + obj2_obj.name = $filter('sanitize')(obj2_obj.name); descr += obj2 + " " + obj2_obj.name + '' + ((activity.operation === 'disassociate') ? ' from ' : ' to '); descr_nolink += obj2 + ' ' + obj2_obj.name + ((activity.operation === 'disassociate') ? ' from ' : ' to '); } else if (obj2) { @@ -227,8 +225,7 @@ angular.module('StreamWidget', ['RestServices', 'Utilities', 'StreamListDefiniti obj1_obj.base = obj1; // Need to character escape the link names, as a malicious url or piece of html could be inserted here that could take the // user to a unknown location. - obj1_obj.name = obj1_obj.name.replace(//g, ">"); + obj1_obj.name = $filter('sanitize')(obj1_obj.name); obj1_obj.name = $sce.getTrustedHtml(obj1_obj.name); descr += obj1 + " " + obj1_obj.name + ''; descr_nolink += obj1 + ' ' + obj1_obj.name; diff --git a/awx/ui/static/less/ansible-ui.less b/awx/ui/static/less/ansible-ui.less index ddc5e628bc..15f460f09d 100644 --- a/awx/ui/static/less/ansible-ui.less +++ b/awx/ui/static/less/ansible-ui.less @@ -361,8 +361,8 @@ textarea.allowresize { display: none; position: fixed; z-index: 2000; - width: 75px; - height: 75px; + width: 138px; + height: 50px; text-align:center; color: #eee; background-color: @black; @@ -371,8 +371,15 @@ textarea.allowresize { padding-top: 10px; p { - padding-top: 10px; - font-size: 11px; + padding-top: 0px; + font-size: 18px; + text-align: right; + margin-right: 10px; + } + + i { + float: left; + margin-left: 10px; } } diff --git a/awx/ui/static/lib/angular-moment/.bower.json b/awx/ui/static/lib/angular-moment/.bower.json new file mode 100644 index 0000000000..aa74eee08f --- /dev/null +++ b/awx/ui/static/lib/angular-moment/.bower.json @@ -0,0 +1,32 @@ +{ + "name": "angular-moment", + "version": "0.10.1", + "description": "Moment.JS directives & filters for AngularJS (timeago alternative)", + "author": "Uri Shaked", + "license": "MIT", + "homepage": "http://github.com/urish/angular-moment", + "main": "./angular-moment.js", + "ignore": [], + "dependencies": { + "angular": ">=1.2.0 <1.5.0", + "moment": ">=2.8.0 <2.11.0" + }, + "devDependencies": { + "angular-mocks": "1.3.x", + "moment-timezone": "0.3.1" + }, + "repository": { + "type": "git", + "url": "git://github.com/urish/angular-moment.git" + }, + "_release": "0.10.1", + "_resolution": { + "type": "version", + "tag": "0.10.1", + "commit": "8910240ee1872478a1b318d2d800c1c073526c37" + }, + "_source": "git://github.com/urish/angular-moment.git", + "_target": "~0.10.1", + "_originalSource": "angular-moment", + "_direct": true +} \ No newline at end of file diff --git a/awx/ui/static/lib/angular-moment/.editorconfig b/awx/ui/static/lib/angular-moment/.editorconfig new file mode 100644 index 0000000000..297368244a --- /dev/null +++ b/awx/ui/static/lib/angular-moment/.editorconfig @@ -0,0 +1,24 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] + +# Change these settings to your own preference +indent_style = tab +indent_size = 4 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[{package.json,bower.json}] +indent_style=space +indent_size=2 + +[*.md] +trim_trailing_whitespace = false diff --git a/awx/ui/static/lib/angular-moment/.gitignore b/awx/ui/static/lib/angular-moment/.gitignore new file mode 100644 index 0000000000..fccb56b324 --- /dev/null +++ b/awx/ui/static/lib/angular-moment/.gitignore @@ -0,0 +1,4 @@ +/.idea +/bower_components +/node_modules +/coverage \ No newline at end of file diff --git a/awx/ui/static/lib/angular-moment/.jshintrc b/awx/ui/static/lib/angular-moment/.jshintrc new file mode 100644 index 0000000000..d430ef2a6b --- /dev/null +++ b/awx/ui/static/lib/angular-moment/.jshintrc @@ -0,0 +1,26 @@ +{ + "node": true, + "browser": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 2, + "latedef": true, + "newcap": true, + "noarg": true, + "quotmark": "single", + "regexp": true, + "undef": true, + "unused": true, + "strict": true, + "trailing": true, + "smarttabs": true, + "maxdepth": 2, + "maxcomplexity": 10, + "globals": { + "angular": false + } +} diff --git a/awx/ui/static/lib/angular-moment/.npmignore b/awx/ui/static/lib/angular-moment/.npmignore new file mode 100644 index 0000000000..2a9c3935f7 --- /dev/null +++ b/awx/ui/static/lib/angular-moment/.npmignore @@ -0,0 +1,4 @@ +.idea +bower_components +node_modules +coverage diff --git a/awx/ui/static/lib/angular-moment/.travis.yml b/awx/ui/static/lib/angular-moment/.travis.yml new file mode 100644 index 0000000000..c85bdee3c0 --- /dev/null +++ b/awx/ui/static/lib/angular-moment/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: + - "0.10" +before_script: + - npm run bower +after_success: + - cat ./coverage/*/lcov.info | ./node_modules/coveralls/bin/coveralls.js diff --git a/awx/ui/static/lib/angular-moment/CHANGELOG.md b/awx/ui/static/lib/angular-moment/CHANGELOG.md new file mode 100644 index 0000000000..669057ba3b --- /dev/null +++ b/awx/ui/static/lib/angular-moment/CHANGELOG.md @@ -0,0 +1,131 @@ +# Changelog + +## 0.10.1 - 2015-05-01 +- Fix broken SystemJS/JSPM support (see [#104](https://github.com/urish/angular-moment/issues/104)) + +## 0.10.0 - 2015-04-10 +- Breaking change: removed one-time binding for `am-time-ago` in favor of AngularJS 1.3's one time binding ([#122](https://github.com/urish/angular-moment/issues/122)) +- Remove support for AngularJS 1.0.x and 1.1.x. +- Support moment.js v2.10.x +- Support for displaying full dates in `am-time-ago` (see [#75](https://github.com/urish/angular-moment/issues/75)) +- Support Angular Core's style CommonJS standard ([#123](https://github.com/urish/angular-moment/pull/123), contributed by [seanhealy](https://github.com/seanhealy)) +- Added an optional timezone parameter to amDateFormat ([#90](https://github.com/urish/angular-moment/pull/90), contributed by [robertbrooker](https://github.com/robertbrooker)) + +## 0.9.2 - 2015-03-17 +- Critical fix: npm install angular-moment fails ([#121](https://github.com/urish/angular-moment/issues/121)) + +## 0.9.1 - 2015-03-17 +- Add support for locale strings customization ([#102](https://github.com/urish/angular-moment/pull/102), contributed by [vosi](https://github.com/vosi)) +- Add `amDifference` filter ([#120](https://github.com/urish/angular-moment/pull/120), contributed by [ajhodges](https://github.com/ajhodges)) +- Support for changing the timezone via `amMoment.changeTimezone()` ([#92](https://github.com/urish/angular-moment/issues/92)) +- Support for AngularJS 1.4.x +- Remove explicit module name for RequireJS ([#112](https://github.com/urish/angular-moment/pull/112), contributed by [WilliamCarter](https://github.com/WilliamCarter)) + +## 0.9.0 - 2015-01-11 +- Support moment.js v2.9.0. See [here](https://gist.github.com/ichernev/0c9a9b49951111a27ce7) for changelog. +- Removed support for older moment.js versions. Only 2.8.0 and newer versions are now supported. +- Removed deprecated method: `amMoment.changeLanguage()`. Use `amMoment.changeLocale()` instead. +- Removed deprecated event: `amMoment:languageChange`. Listen for `amMoment:localeChange` instead. +- Filters are now stateful by default (fixes [#97](https://github.com/urish/angular-moment/issues/97)). +- The project is now available on [NuGet](https://www.nuget.org/packages/angular-moment/) ([#99](https://github.com/urish/angular-moment/pull/99), contributed by [markvp](https://github.com/markvp)). + +## 0.8.3 - 2014-12-08 +- `amTimeAgo` filter ([#96](https://github.com/urish/angular-moment/pull/96), contributed by [maxklenk](https://github.com/maxklenk)) +- Show formatted time as element title ([#78](https://github.com/urish/angular-moment/pull/78), contributed by [ctesene](https://github.com/ctesene)) +- Support commonjs and browserify ([#95](https://github.com/urish/angular-moment/pull/95), contributed by [Pencroff](https://github.com/Pencroff)) +- SystemJS Loader support ([#85](https://github.com/urish/angular-moment/pull/85), contributed by [capaj](https://github.com/capaj)) + +## 0.8.2 - 2014-09-07 +- `amMoment.changeLanguage()` was deprecated in favor of `amMoment.changeLocale()` (following [a change](http://momentjs.com/docs/#/i18n/changing-locale/) introduced in moment v2.8.1) +- Bugfix: changing the locale emitted a deprecation warning (see [#76](https://github.com/urish/angular-moment/issues/76) for details). + +## 0.8.1 - 2014-09-01 +- Support moment.js v2.8.0. See [here](https://gist.github.com/ichernev/ac3899324a5fa6c8c9b4) for changelog. +- Support moment-timezone v0.2.1. See [here](https://github.com/moment/moment-timezone/blob/develop/changelog.md#021-2014-08-02) for changelog. +- Bugfix: `updateTime()` is called too often for future dates ([#73](https://github.com/urish/angular-moment/issues/73)) + +## 0.8.0 - 2014-07-26 +- Generate source map for the minified version ([#50](https://github.com/urish/angular-moment/issues/50)) +- Add support HTML `