diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..c12528c389 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,31 @@ +### Summary + + + +### Environment + + + +### Steps To Reproduce: + + + +### Expected Results: + + + +### Actual Results: + + + +### Additional Information: + + diff --git a/awx/api/views.py b/awx/api/views.py index 959b4dd2cf..04a7785cd3 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -32,6 +32,7 @@ from django.http import HttpResponse # Django REST Framework from rest_framework.exceptions import PermissionDenied, ParseError +from rest_framework.parsers import FormParser from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.settings import api_settings @@ -2064,6 +2065,7 @@ class JobTemplateCallback(GenericAPIView): model = JobTemplate permission_classes = (JobTemplateCallbackPermission,) serializer_class = EmptySerializer + parser_classes = api_settings.DEFAULT_PARSER_CLASSES + [FormParser] @csrf_exempt @transaction.non_atomic_requests diff --git a/awx/main/management/commands/run_fact_cache_receiver.py b/awx/main/management/commands/run_fact_cache_receiver.py index 062cd39693..02a3b2e66c 100644 --- a/awx/main/management/commands/run_fact_cache_receiver.py +++ b/awx/main/management/commands/run_fact_cache_receiver.py @@ -67,7 +67,7 @@ class FactCacheReceiver(object): self.timestamp = datetime.fromtimestamp(date_key, None) # Update existing Fact entry - fact_obj = Fact.objects.filter(host__id=host_obj.id, module=module_name, timestamp=self.timestamp) + fact_obj = Fact.objects.filter(host__id=host_obj.id, module=module_name, timestamp=self.timestamp) if fact_obj: fact_obj.facts = facts fact_obj.save() diff --git a/awx/main/migrations/0005_v300_changes.py b/awx/main/migrations/0005_v300_changes.py new file mode 100644 index 0000000000..e350c881f2 --- /dev/null +++ b/awx/main/migrations/0005_v300_changes.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.utils.timezone import now + +from awx.api.license import feature_enabled + + +def create_system_job_templates(apps, schema_editor): + ''' + Create default system job templates if not present. Create default schedules + only if new system job templates were created (i.e. new database). + ''' + + SystemJobTemplate = apps.get_model('main', 'SystemJobTemplate') + ContentType = apps.get_model('contenttypes', 'ContentType') + sjt_ct = ContentType.objects.get_for_model(SystemJobTemplate) + now_dt = now() + now_str = now_dt.strftime('%Y%m%dT%H%M%SZ') + + sjt, created = SystemJobTemplate.objects.get_or_create( + job_type='cleanup_jobs', + defaults=dict( + name='Cleanup Job Details', + description='Remove job history older than X days', + created=now_dt, + modified=now_dt, + polymorphic_ctype=sjt_ct, + ), + ) + if created: + sjt.schedules.create( + name='Cleanup Job Schedule', + rrule='DTSTART:%s RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=SU' % now_str, + description='Automatically Generated Schedule', + enabled=True, + extra_data={'days': '120'}, + created=now_dt, + modified=now_dt, + ) + + sjt, created = SystemJobTemplate.objects.get_or_create( + job_type='cleanup_deleted', + defaults=dict( + name='Cleanup Deleted Data', + description='Remove deleted object history older than X days', + created=now_dt, + modified=now_dt, + polymorphic_ctype=sjt_ct, + ), + ) + if created: + sjt.schedules.create( + name='Cleanup Deleted Data Schedule', + rrule='DTSTART:%s RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO' % now_str, + description='Automatically Generated Schedule', + enabled=True, + extra_data={'days': '30'}, + created=now_dt, + modified=now_dt, + ) + + sjt, created = SystemJobTemplate.objects.get_or_create( + job_type='cleanup_activitystream', + defaults=dict( + name='Cleanup Activity Stream', + description='Remove activity stream history older than X days', + created=now_dt, + modified=now_dt, + polymorphic_ctype=sjt_ct, + ), + ) + if created: + sjt.schedules.create( + name='Cleanup Activity Schedule', + rrule='DTSTART:%s RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=TU' % now_str, + description='Automatically Generated Schedule', + enabled=True, + extra_data={'days': '355'}, + created=now_dt, + modified=now_dt, + ) + + sjt, created = SystemJobTemplate.objects.get_or_create( + job_type='cleanup_facts', + defaults=dict( + name='Cleanup Fact Details', + description='Remove system tracking history', + created=now_dt, + modified=now_dt, + polymorphic_ctype=sjt_ct, + ), + ) + if created and feature_enabled('system_tracking', bypass_database=True): + sjt.schedules.create( + name='Cleanup Fact Schedule', + rrule='DTSTART:%s RRULE:FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=1' % now_str, + description='Automatically Generated Schedule', + enabled=True, + extra_data={'older_than': '120d', 'granularity': '1w'}, + created=now_dt, + modified=now_dt, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0004_v300_changes'), + ] + + operations = [ + migrations.RunPython(create_system_job_templates, migrations.RunPython.noop), + ] diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 3942cc78bb..b8bb60905b 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -233,7 +233,8 @@ def handle_work_success(self, result, task_actual): task_actual['id'], instance_name, notification_body['url']) - send_notifications.delay([n.generate_notification(notification_subject, notification_body) + notification_body['friendly_name'] = friendly_name + send_notifications.delay([n.generate_notification(notification_subject, notification_body).id for n in set(notifiers.get('success', []) + notifiers.get('any', []))], job_id=task_actual['id']) diff --git a/awx/main/tests/base.py b/awx/main/tests/base.py index 93bac00948..a0387079b6 100644 --- a/awx/main/tests/base.py +++ b/awx/main/tests/base.py @@ -11,6 +11,7 @@ import shutil import sys import tempfile import time +import urllib from multiprocessing import Process from subprocess import Popen import re @@ -463,6 +464,8 @@ class BaseTestMixin(QueueTestMixin, MockCommonlySlowTestMixin): response = method(url, json.dumps(data), 'application/json') elif data_type == 'yaml': response = method(url, yaml.safe_dump(data), 'application/yaml') + elif data_type == 'form': + response = method(url, urllib.urlencode(data), 'application/x-www-form-urlencoded') else: self.fail('Unsupported data_type %s' % data_type) else: diff --git a/awx/main/tests/old/jobs/jobs_monolithic.py b/awx/main/tests/old/jobs/jobs_monolithic.py index 0e16afe0b2..9286b93084 100644 --- a/awx/main/tests/old/jobs/jobs_monolithic.py +++ b/awx/main/tests/old/jobs/jobs_monolithic.py @@ -803,6 +803,21 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase): self.assertEqual(job.hosts.count(), 1) self.assertEqual(job.hosts.all()[0], host) + # Create the job itself using URL-encoded form data instead of JSON. + result = self.post(url, data, expect=202, remote_addr=host_ip, data_type='form') + + # Establish that we got back what we expect, and made the changes + # that we expect. + self.assertTrue('Location' in result.response, result.response) + self.assertEqual(jobs_qs.count(), 2) + job = jobs_qs[0] + self.assertEqual(urlparse.urlsplit(result.response['Location']).path, + job.get_absolute_url()) + self.assertEqual(job.launch_type, 'callback') + self.assertEqual(job.limit, host.name) + self.assertEqual(job.hosts.count(), 1) + self.assertEqual(job.hosts.all()[0], host) + # Run the callback job again with extra vars and verify their presence data.update(dict(extra_vars=dict(key="value"))) result = self.post(url, data, expect=202, remote_addr=host_ip) @@ -853,9 +868,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase): if host_ip: break self.assertTrue(host) - self.assertEqual(jobs_qs.count(), 2) - self.post(url, data, expect=202, remote_addr=host_ip) self.assertEqual(jobs_qs.count(), 3) + self.post(url, data, expect=202, remote_addr=host_ip) + self.assertEqual(jobs_qs.count(), 4) job = jobs_qs[0] self.assertEqual(job.launch_type, 'callback') self.assertEqual(job.limit, host.name) @@ -878,9 +893,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase): if host_ip: break self.assertTrue(host) - self.assertEqual(jobs_qs.count(), 3) - self.post(url, data, expect=202, remote_addr=host_ip) self.assertEqual(jobs_qs.count(), 4) + self.post(url, data, expect=202, remote_addr=host_ip) + self.assertEqual(jobs_qs.count(), 5) job = jobs_qs[0] self.assertEqual(job.launch_type, 'callback') self.assertEqual(job.limit, host.name) @@ -892,9 +907,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase): host_qs = host_qs.filter(variables__icontains='ansible_ssh_host') host = host_qs[0] host_ip = host.variables_dict['ansible_ssh_host'] - self.assertEqual(jobs_qs.count(), 4) - self.post(url, data, expect=202, remote_addr=host_ip) self.assertEqual(jobs_qs.count(), 5) + self.post(url, data, expect=202, remote_addr=host_ip) + self.assertEqual(jobs_qs.count(), 6) job = jobs_qs[0] self.assertEqual(job.launch_type, 'callback') self.assertEqual(job.limit, host.name) @@ -926,9 +941,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase): host_ip = list(ips)[0] break self.assertTrue(host) - self.assertEqual(jobs_qs.count(), 5) - self.post(url, data, expect=202, remote_addr=host_ip) self.assertEqual(jobs_qs.count(), 6) + self.post(url, data, expect=202, remote_addr=host_ip) + self.assertEqual(jobs_qs.count(), 7) job = jobs_qs[0] self.assertEqual(job.launch_type, 'callback') self.assertEqual(job.limit, ':&'.join([job_template.limit, host.name])) diff --git a/awx/ui/client/legacy-styles/ansible-ui.less b/awx/ui/client/legacy-styles/ansible-ui.less index 4adaeb09eb..717ab7bd30 100644 --- a/awx/ui/client/legacy-styles/ansible-ui.less +++ b/awx/ui/client/legacy-styles/ansible-ui.less @@ -44,7 +44,8 @@ @import "text-label.less"; @import "./bootstrap-datepicker.less"; @import "awx/ui/client/src/shared/branding/colors.default.less"; - +// Bootstrap default overrides +@import "awx/ui/client/src/shared/bootstrap-settings.less"; /* Bootstrap fix that's causing a right margin to appear whenver a modal is opened */ body.modal-open { diff --git a/awx/ui/client/src/about/about.controller.js b/awx/ui/client/src/about/about.controller.js index c35388e8ae..07bad2dfd6 100644 --- a/awx/ui/client/src/about/about.controller.js +++ b/awx/ui/client/src/about/about.controller.js @@ -1,20 +1,20 @@ export default ['$scope', '$state', 'CheckLicense', function($scope, $state, CheckLicense){ var processVersion = function(version){ - // prettify version & calculate padding - // e,g 3.0.0-0.git201602191743/ -> 3.0.0 - var split = version.split('-')[0] - var spaces = Math.floor((16-split.length)/2), - paddedStr = ""; - for(var i=0; i<=spaces; i++){ - paddedStr = paddedStr +" "; - } - paddedStr = paddedStr + split; - for(var j = paddedStr.length; j<16; j++){ - paddedStr = paddedStr + " "; - } - return paddedStr - } + // prettify version & calculate padding + // e,g 3.0.0-0.git201602191743/ -> 3.0.0 + var split = version.split('-')[0] + var spaces = Math.floor((16-split.length)/2), + paddedStr = ""; + for(var i=0; i<=spaces; i++){ + paddedStr = paddedStr +" "; + } + paddedStr = paddedStr + split; + for(var j = paddedStr.length; j<16; j++){ + paddedStr = paddedStr + " "; + } + return paddedStr + }; var init = function(){ CheckLicense.get() .then(function(res){ @@ -23,9 +23,9 @@ export default $('#about-modal').modal('show'); }); }; - var back = function(){ - $state.go('setup'); - } + $('#about-modal').on('hidden.bs.modal', function () { + $state.go('setup'); + }); init(); } ]; \ No newline at end of file diff --git a/awx/ui/client/src/about/about.partial.html b/awx/ui/client/src/about/about.partial.html index afc66724f4..8d0c355e5c 100644 --- a/awx/ui/client/src/about/about.partial.html +++ b/awx/ui/client/src/about/about.partial.html @@ -3,7 +3,7 @@ - + diff --git a/awx/ui/client/src/license/license.block.less b/awx/ui/client/src/license/license.block.less index 58ce83542a..becc149b7e 100644 --- a/awx/ui/client/src/license/license.block.less +++ b/awx/ui/client/src/license/license.block.less @@ -20,6 +20,16 @@ display: block; width: 100%; } +.License-submit--success.ng-hide-add, .License-submit--success.ng-hide-remove { + transition: all ease-in-out 0.5s; +} +.License-submit--success{ + opacity: 1; + transition: all ease-in-out 0.5s; +} +.License-submit--success.ng-hide{ + opacity: 0; +} .License-eula textarea{ width: 100%; height: 300px; @@ -33,6 +43,9 @@ .License-field{ .OnePlusTwo-left--detailsRow; } +.License-field + .License-field { + margin-top: 20px; +} .License-greenText{ color: @submit-button-bg; } @@ -40,16 +53,16 @@ color: #d9534f; } .License-fields{ - .OnePlusTwo-left--details; + .OnePlusTwo-left--details; } .License-details { - .OnePlusTwo-left--panel(600px); + .OnePlusTwo-left--panel(650px); } .License-titleText { .OnePlusTwo-panelHeader; } .License-management{ - .OnePlusTwo-right--panel(600px); + .OnePlusTwo-right--panel(650px); } .License-submit--container{ height: 33px; @@ -59,8 +72,25 @@ margin: 0 10px 0 0; } .License-file--container { - margin: 20px 0 20px 0; input[type=file] { display: none; } -} \ No newline at end of file +} +.License-upgradeText { + margin: 20px 0px; +} +.License-body { + margin-top: 25px; +} +.License-subTitleText { + text-transform: uppercase; + margin: 20px 0px 5px 0px; + color: @default-interface-txt; +} +.License-helperText { + color: @default-interface-txt; +} +.License-input--fake{ + border-top-right-radius: 4px !important; + border-bottom-right-radius: 4px !important; +} diff --git a/awx/ui/client/src/license/license.controller.js b/awx/ui/client/src/license/license.controller.js index 6e2801a85f..187fa8883c 100644 --- a/awx/ui/client/src/license/license.controller.js +++ b/awx/ui/client/src/license/license.controller.js @@ -5,9 +5,9 @@ *************************************************/ export default - [ 'Wait', '$state', '$scope', '$location', + [ 'Wait', '$state', '$scope', '$rootScope', '$location', 'GetBasePath', 'Rest', 'ProcessErrors', 'CheckLicense', 'moment', - function( Wait, $state, $scope, $location, + function( Wait, $state, $scope, $rootScope, $location, GetBasePath, Rest, ProcessErrors, CheckLicense, moment){ $scope.getKey = function(event){ // Mimic HTML5 spec, show filename @@ -16,9 +16,19 @@ export default var raw = new FileReader(); // readAsFoo runs async raw.onload = function(){ - $scope.newLicense.file = JSON.parse(raw.result); + try { + $scope.newLicense.file = JSON.parse(raw.result); + } + catch(err) { + ProcessErrors($rootScope, null, null, null, {msg: 'Invalid file format. Please upload valid JSON.'}); + } + } + try { + raw.readAsText(event.target.files[0]); + } + catch(err) { + ProcessErrors($rootScope, null, null, null, {msg: 'Invalid file format. Please upload valid JSON.'}); } - raw.readAsText(event.target.files[0]); }; // HTML5 spec doesn't provide a way to customize file input css // So we hide the default input, show our own, and simulate clicks to the hidden input @@ -33,6 +43,11 @@ export default reset(); init(); $scope.success = true; + // for animation purposes + var successTimeout = setTimeout(function(){ + $scope.success = false; + clearTimeout(successTimeout); + }, 4000); }); }; var calcDaysRemaining = function(ms){ @@ -51,6 +66,7 @@ export default CheckLicense.get() .then(function(res){ $scope.license = res.data; + $scope.license.version = res.data.version.split('-')[0]; $scope.time = {}; $scope.time.remaining = calcDaysRemaining($scope.license.license_info.time_remaining); $scope.time.expiresOn = calcExpiresOn($scope.time.remaining); diff --git a/awx/ui/client/src/license/license.partial.html b/awx/ui/client/src/license/license.partial.html index dcbde280e7..8f7b7b81f5 100644 --- a/awx/ui/client/src/license/license.partial.html +++ b/awx/ui/client/src/license/license.partial.html @@ -5,95 +5,98 @@
License
-
+
Valid Invalid -
-
-
-
Version
-
- {{license.version}}
-
License Type
+
Version
- {{license.license_info.license_type}} -
+ {{license.version || "No result found"}} +
+
+
+
License Type
+
+ {{license.license_info.license_type || "No result found"}} +
Subscription
-
- {{license.license_info.subscription_name}} -
+
+ {{license.license_info.subscription_name || "No result found"}} +
-
License Key
-
- {{license.license_info.license_key}} -
+
License Key
+
+ {{license.license_info.license_key || "No result found"}} +
Expires On
-
+
{{time.expiresOn}} -
+
Time Remaining
-
- {{time.remaining}} Day -
+
+ {{time.remaining}} Days +
-
Hosts Available
+
Hosts Available
- {{license.license_info.available_instances}} -
+ {{license.license_info.available_instances || "No result found"}} +
Hosts Used
-
- {{license.license_info.current_instances}} -
+
+ {{license.license_info.current_instances || "No result found"}} +
Hosts Remaining
-
- {{license.license_info.free_instances}} -
+
+ {{license.license_info.free_instances || "No result found"}} +
-

If you are ready to upgrade, please contact us by clicking the button below

+
If you are ready to upgrade, please contact us by clicking the button below
License Management
-

Choose your license file, agree to the End User License Agreement, and click submit.

-
-
- Browse... - - -
-
End User License Agreement
-
- -
-
-
-
I agree to the End User License Agreement
-
- Save successful! - +
+

Choose your license file, agree to the End User License Agreement, and click submit.

+ +
License File
+
+ Browse... + + +
+
End User License Agreement
+
+ +
+
+
+
I agree to the End User License Agreement
+
+ Save successful! + +
-
- + +
-
\ No newline at end of file +
diff --git a/awx/ui/client/src/notifications/add/add.controller.js b/awx/ui/client/src/notifications/add/add.controller.js new file mode 100644 index 0000000000..88048941ff --- /dev/null +++ b/awx/ui/client/src/notifications/add/add.controller.js @@ -0,0 +1,67 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default + [ '$rootScope', 'pagination', '$compile','SchedulerInit', 'Rest', 'Wait', + 'notificationsFormObject', 'ProcessErrors', 'GetBasePath', 'Empty', + 'GenerateForm', 'SearchInit' , 'PaginateInit', + 'LookUpInit', 'OrganizationList', '$scope', '$state', + function( + $rootScope, pagination, $compile, SchedulerInit, Rest, Wait, + notificationsFormObject, ProcessErrors, GetBasePath, Empty, + GenerateForm, SearchInit, PaginateInit, + LookUpInit, OrganizationList, $scope, $state + ) { + var scope = $scope, + generator = GenerateForm, + form = notificationsFormObject, + url = GetBasePath('notifications'); + + generator.inject(form, { + mode: 'add' , + scope:scope, + related: false + }); + generator.reset(); + + LookUpInit({ + url: GetBasePath('organization'), + scope: scope, + form: form, + list: OrganizationList, + field: 'organization', + input_type: 'radio' + }); + + // Save + scope.formSave = function () { + + generator.clearApiErrors(); + Wait('start'); + Rest.setUrl(url); + Rest.post({ + name: scope.name, + description: scope.description, + organization: scope.organization, + script: scope.script + }) + .success(function (data) { + $rootScope.addedItem = data.id; + $state.go('inventoryScripts', {}, {reload: true}); + Wait('stop'); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to add new inventory script. POST returned status: ' + status }); + }); + }; + + scope.formCancel = function () { + $state.transitionTo('inventoryScripts'); + }; + + } + ]; diff --git a/awx/ui/client/src/notifications/add/add.partial.html b/awx/ui/client/src/notifications/add/add.partial.html new file mode 100644 index 0000000000..65bfebbbe6 --- /dev/null +++ b/awx/ui/client/src/notifications/add/add.partial.html @@ -0,0 +1,3 @@ +
+
+
diff --git a/awx/ui/client/src/notifications/add/add.route.js b/awx/ui/client/src/notifications/add/add.route.js new file mode 100644 index 0000000000..6bad062844 --- /dev/null +++ b/awx/ui/client/src/notifications/add/add.route.js @@ -0,0 +1,23 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import {templateUrl} from '../../shared/template-url/template-url.factory'; + +export default { + name: 'notifications.add', + route: '/add', + templateUrl: templateUrl('notifications/add/add'), + controller: 'notificationsAddController', + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }] + }, + ncyBreadcrumb: { + parent: 'notifications', + label: 'Create Notification' + } +}; diff --git a/awx/ui/client/src/notifications/add/main.js b/awx/ui/client/src/notifications/add/main.js new file mode 100644 index 0000000000..f3101e402f --- /dev/null +++ b/awx/ui/client/src/notifications/add/main.js @@ -0,0 +1,15 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import route from './add.route'; +import controller from './add.controller'; + +export default + angular.module('notificationsAdd', []) + .controller('notificationsAddController', controller) + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(route); + }]); diff --git a/awx/ui/client/src/notifications/edit/edit.controller.js b/awx/ui/client/src/notifications/edit/edit.controller.js new file mode 100644 index 0000000000..a14b3334af --- /dev/null +++ b/awx/ui/client/src/notifications/edit/edit.controller.js @@ -0,0 +1,97 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default + [ 'Rest', 'Wait', + 'notificationsFormObject', 'ProcessErrors', 'GetBasePath', + 'GenerateForm', 'SearchInit' , 'PaginateInit', + 'LookUpInit', 'OrganizationList', 'inventory_script', + '$scope', '$state', + function( + Rest, Wait, + notificationsFormObject, ProcessErrors, GetBasePath, + GenerateForm, SearchInit, PaginateInit, + LookUpInit, OrganizationList, inventory_script, + $scope, $state + ) { + var generator = GenerateForm, + id = inventory_script.id, + form = notificationsFormObject, + master = {}, + url = GetBasePath('notifications'); + + $scope.inventory_script = inventory_script; + generator.inject(form, { + mode: 'edit' , + scope:$scope, + related: false, + activityStream: false + }); + generator.reset(); + LookUpInit({ + url: GetBasePath('organization'), + scope: $scope, + form: form, + // hdr: "Select Custom Inventory", + list: OrganizationList, + field: 'organization', + input_type: 'radio' + }); + + // Retrieve detail record and prepopulate the form + Wait('start'); + Rest.setUrl(url + id+'/'); + Rest.get() + .success(function (data) { + var fld; + for (fld in form.fields) { + if (data[fld]) { + $scope[fld] = data[fld]; + master[fld] = data[fld]; + } + + if (form.fields[fld].sourceModel && data.summary_fields && + data.summary_fields[form.fields[fld].sourceModel]) { + $scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; + master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; + } + } + Wait('stop'); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to retrieve inventory script: ' + id + '. GET status: ' + status }); + }); + + $scope.formSave = function () { + generator.clearApiErrors(); + Wait('start'); + Rest.setUrl(url+ id+'/'); + Rest.put({ + name: $scope.name, + description: $scope.description, + organization: $scope.organization, + script: $scope.script + }) + .success(function () { + $state.transitionTo('inventoryScriptsList'); + Wait('stop'); + + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to add new inventory script. PUT returned status: ' + status }); + }); + }; + + $scope.formCancel = function () { + $state.transitionTo('inventoryScripts'); + }; + + } + ]; diff --git a/awx/ui/client/src/notifications/edit/edit.partial.html b/awx/ui/client/src/notifications/edit/edit.partial.html new file mode 100644 index 0000000000..ebd7e80e80 --- /dev/null +++ b/awx/ui/client/src/notifications/edit/edit.partial.html @@ -0,0 +1,3 @@ +
+
+
diff --git a/awx/ui/client/src/notifications/edit/edit.route.js b/awx/ui/client/src/notifications/edit/edit.route.js new file mode 100644 index 0000000000..1987b32cad --- /dev/null +++ b/awx/ui/client/src/notifications/edit/edit.route.js @@ -0,0 +1,23 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import {templateUrl} from '../../shared/template-url/template-url.factory'; + +export default { + name: 'notifications.edit', + route: '/edit', + templateUrl: templateUrl('notifications/edit/edit'), + controller: 'notificationsEditController', + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }] + }, + ncyBreadcrumb: { + parent: 'notifications', + label: 'Edit Notification' + } +}; diff --git a/awx/ui/client/src/notifications/edit/main.js b/awx/ui/client/src/notifications/edit/main.js new file mode 100644 index 0000000000..8f0c62d7a1 --- /dev/null +++ b/awx/ui/client/src/notifications/edit/main.js @@ -0,0 +1,15 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import route from './edit.route'; +import controller from './edit.controller'; + +export default + angular.module('notificationsEdit', []) + .controller('notificationsEditController', controller) + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(route); + }]); diff --git a/awx/ui/client/src/notifications/list/list.controller.js b/awx/ui/client/src/notifications/list/list.controller.js new file mode 100644 index 0000000000..df0ef9f7d2 --- /dev/null +++ b/awx/ui/client/src/notifications/list/list.controller.js @@ -0,0 +1,83 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default + [ '$rootScope','Wait', 'generateList', 'notificationsListObject', + 'GetBasePath' , 'SearchInit' , 'PaginateInit', + 'Rest' , 'ProcessErrors', 'Prompt', '$state', + function( + $rootScope,Wait, GenerateList, notificationsListObject, + GetBasePath, SearchInit, PaginateInit, + Rest, ProcessErrors, Prompt, $state + ) { + var scope = $rootScope.$new(), + defaultUrl = GetBasePath('notifications'), + list = notificationsListObject, + view = GenerateList; + + view.inject( list, { + mode: 'edit', + scope: scope + }); + + // SearchInit({ + // scope: scope, + // set: 'notifications', + // list: list, + // url: defaultUrl + // }); + // + // if ($rootScope.addedItem) { + // scope.addedItem = $rootScope.addedItem; + // delete $rootScope.addedItem; + // } + // PaginateInit({ + // scope: scope, + // list: list, + // url: defaultUrl + // }); + // + // scope.search(list.iterator); + + scope.editNotification = function(){ + $state.transitionTo('notifications.edit',{ + inventory_script_id: this.inventory_script.id, + inventory_script: this.inventory_script + }); + }; + + scope.deleteNotification = function(id, name){ + + var action = function () { + $('#prompt-modal').modal('hide'); + Wait('start'); + var url = defaultUrl + id + '/'; + Rest.setUrl(url); + Rest.destroy() + .success(function () { + scope.search(list.iterator); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); + }); + }; + + var bodyHtml = '
Are you sure you want to delete the inventory script below?
' + name + '
'; + Prompt({ + hdr: 'Delete', + body: bodyHtml, + action: action, + actionText: 'DELETE' + }); + }; + + scope.addNotification = function(){ + $state.transitionTo('notifications.add'); + }; + + } + ]; diff --git a/awx/ui/client/src/notifications/list/list.partial.html b/awx/ui/client/src/notifications/list/list.partial.html new file mode 100644 index 0000000000..bad2fa18bb --- /dev/null +++ b/awx/ui/client/src/notifications/list/list.partial.html @@ -0,0 +1,4 @@ +
+
+
+
diff --git a/awx/ui/client/src/notifications/list/list.route.js b/awx/ui/client/src/notifications/list/list.route.js new file mode 100644 index 0000000000..da76880791 --- /dev/null +++ b/awx/ui/client/src/notifications/list/list.route.js @@ -0,0 +1,19 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import {templateUrl} from '../../shared/template-url/template-url.factory'; + +export default { + name: 'notifications', + route: '/notifications', + templateUrl: templateUrl('notifications/list/list'), + controller: 'notificationsListController', + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }] + } +}; diff --git a/awx/ui/client/src/notifications/list/main.js b/awx/ui/client/src/notifications/list/main.js new file mode 100644 index 0000000000..35cab03cef --- /dev/null +++ b/awx/ui/client/src/notifications/list/main.js @@ -0,0 +1,15 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import route from './list.route'; +import controller from './list.controller'; + +export default + angular.module('notificationsList', []) + .controller('notificationsListController', controller) + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(route); + }]); diff --git a/awx/ui/client/src/notifications/main.js b/awx/ui/client/src/notifications/main.js new file mode 100644 index 0000000000..147b3b8479 --- /dev/null +++ b/awx/ui/client/src/notifications/main.js @@ -0,0 +1,22 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + +import notificationsList from './list/main'; +import notificationsAdd from './add/main'; +import notificationsEdit from './edit/main'; + +import list from './notifications.list'; +import form from './notifications.form'; + +export default + angular.module('notifications', [ + notificationsList.name, + notificationsAdd.name, + notificationsEdit.name + ]) + .factory('notificationsListObject', list) + .factory('notificationsFormObject', form); diff --git a/awx/ui/client/src/notifications/notifications.form.js b/awx/ui/client/src/notifications/notifications.form.js new file mode 100644 index 0000000000..d8c49d00ba --- /dev/null +++ b/awx/ui/client/src/notifications/notifications.form.js @@ -0,0 +1,47 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + /** + * @ngdoc function + * @name forms.function:CustomInventory + * @description This form is for adding/editing an organization +*/ + +export default function() { + return { + + addTitle: 'New Notification', + editTitle: '{{ name }}', + name: 'notification', + showActions: true, + + fields: { + name: { + label: 'Name', + type: 'text', + addRequired: true, + editRequired: true, + capitalize: false + }, + description: { + label: 'Description', + type: 'text', + addRequired: false, + editRequired: false + } + }, + + buttons: { //for now always generates