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/app.js b/awx/ui/client/src/app.js
index 3c12eadfb1..a23e803de2 100644
--- a/awx/ui/client/src/app.js
+++ b/awx/ui/client/src/app.js
@@ -30,6 +30,7 @@ import inventoryScripts from './inventory-scripts/main';
import permissions from './permissions/main';
import managementJobs from './management-jobs/main';
import jobDetail from './job-detail/main';
+import notifications from './notifications/main';
// modules
import about from './about/main';
@@ -76,7 +77,7 @@ __deferLoadIfEnabled();
/*#endif#*/
var tower = angular.module('Tower', [
- // 'ngAnimate',
+ //'ngAnimate',
'ngSanitize',
'ngCookies',
about.name,
@@ -98,6 +99,7 @@ var tower = angular.module('Tower', [
activityStream.name,
footer.name,
jobDetail.name,
+ notifications.name,
standardOut.name,
'templates',
'Utilities',
@@ -882,13 +884,13 @@ var tower = angular.module('Tower', [
}]);
}])
- .run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', '$state', 'CheckLicense',
+ .run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', '$state', 'CheckLicense',
'$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'Socket',
'LoadConfig', 'Store', 'ShowSocketHelp', 'pendoService',
function (
- $q, $compile, $cookieStore, $rootScope, $log, $state, CheckLicense,
+ $q, $compile, $cookieStore, $rootScope, $log, $state, CheckLicense,
$location, Authorization, LoadBasePaths, Timer, ClearScope, Socket,
- LoadConfig, Store, ShowSocketHelp, pendoService)
+ LoadConfig, Store, ShowSocketHelp, pendoService)
{
var sock;
diff --git a/awx/ui/client/src/footer/footer.block.less b/awx/ui/client/src/footer/footer.block.less
index 057d926568..ebe652d1c3 100644
--- a/awx/ui/client/src/footer/footer.block.less
+++ b/awx/ui/client/src/footer/footer.block.less
@@ -1,12 +1,11 @@
/** @define DashboardCounts */
-
.Footer {
height: 40px;
background-color: #f6f6f6;
color: #848992;
width: 100%;
z-index: 1040;
- position: fixed;
+ position: absolute;
right: 0;
left: 0;
bottom: 0;
diff --git a/awx/ui/client/src/footer/footer.partial.html b/awx/ui/client/src/footer/footer.partial.html
index a55076dc50..32ec05bb05 100644
--- a/awx/ui/client/src/footer/footer.partial.html
+++ b/awx/ui/client/src/footer/footer.partial.html
@@ -4,5 +4,5 @@
-
+
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
-
-
-
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"}}
+
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.
-
\ 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