diff --git a/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js b/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js index 8f3d924ec2..5b6825676f 100644 --- a/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js +++ b/awx/ui/client/src/access/rbac-multiselect/rbac-multiselect-list.directive.js @@ -6,9 +6,9 @@ /* jshint unused: vars */ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateList', 'ProjectList', - 'InventoryList', 'CredentialList', '$compile', 'generateList', 'GetBasePath', 'SelectionInit', + 'InventoryList', 'CredentialList', '$compile', 'generateList', 'GetBasePath', function(addPermissionsTeamsList, addPermissionsUsersList, TemplateList, ProjectList, - InventoryList, CredentialList, $compile, generateList, GetBasePath, SelectionInit) { + InventoryList, CredentialList, $compile, generateList, GetBasePath) { return { restrict: 'E', scope: { diff --git a/awx/ui/client/src/activity-stream/get-target-title.factory.js b/awx/ui/client/src/activity-stream/get-target-title.factory.js new file mode 100644 index 0000000000..85c6c7a80b --- /dev/null +++ b/awx/ui/client/src/activity-stream/get-target-title.factory.js @@ -0,0 +1,51 @@ +export default + function GetTargetTitle(i18n) { + return function (target) { + + var rtnTitle = i18n._('ALL ACTIVITY'); + + switch(target) { + case 'project': + rtnTitle = i18n._('PROJECTS'); + break; + case 'inventory': + rtnTitle = i18n._('INVENTORIES'); + break; + case 'credential': + rtnTitle = i18n._('CREDENTIALS'); + break; + case 'user': + rtnTitle = i18n._('USERS'); + break; + case 'team': + rtnTitle = i18n._('TEAMS'); + break; + case 'notification_template': + rtnTitle = i18n._('NOTIFICATION TEMPLATES'); + break; + case 'organization': + rtnTitle = i18n._('ORGANIZATIONS'); + break; + case 'job': + rtnTitle = i18n._('JOBS'); + break; + case 'custom_inventory_script': + rtnTitle = i18n._('INVENTORY SCRIPTS'); + break; + case 'schedule': + rtnTitle = i18n._('SCHEDULES'); + break; + case 'host': + rtnTitle = i18n._('HOSTS'); + break; + case 'template': + rtnTitle = i18n._('TEMPLATES'); + break; + } + + return rtnTitle; + + }; + } + +GetTargetTitle.$inject = ['i18n']; diff --git a/awx/ui/client/src/activity-stream/main.js b/awx/ui/client/src/activity-stream/main.js index 44c82217ab..3cb93e3003 100644 --- a/awx/ui/client/src/activity-stream/main.js +++ b/awx/ui/client/src/activity-stream/main.js @@ -12,6 +12,8 @@ import BuildAnchor from './factories/build-anchor.factory'; import BuildDescription from './factories/build-description.factory'; import ShowDetail from './factories/show-detail.factory'; import Stream from './factories/stream.factory'; +import GetTargetTitle from './get-target-title.factory'; +import ModelToBasePathKey from './model-to-base-path-key.factory'; export default angular.module('activityStream', [streamDetailModal.name]) .controller('activityStreamController', activityStreamController) @@ -20,6 +22,8 @@ export default angular.module('activityStream', [streamDetailModal.name]) .factory('BuildDescription', BuildDescription) .factory('ShowDetail', ShowDetail) .factory('Stream', Stream) + .factory('GetTargetTitle', GetTargetTitle) + .factory('ModelToBasePathKey', ModelToBasePathKey) .run(['$stateExtender', function($stateExtender) { $stateExtender.addState(activityStreamRoute); }]); diff --git a/awx/ui/client/src/activity-stream/model-to-base-path-key.factory.js b/awx/ui/client/src/activity-stream/model-to-base-path-key.factory.js new file mode 100644 index 0000000000..bc6cfe74b8 --- /dev/null +++ b/awx/ui/client/src/activity-stream/model-to-base-path-key.factory.js @@ -0,0 +1,59 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + /** + * @ngdoc function + * @name helpers.function:ApiModel + * @description Helper functions to convert singular/plural versions of our models to the opposite +*/ + +export default + function ModelToBasePathKey() { + return function(model) { + // This function takes in the singular model string and returns the key needed + // to get the base path from $rootScope/local storage. + + var basePathKey; + + switch(model) { + case 'project': + basePathKey = 'projects'; + break; + case 'inventory': + basePathKey = 'inventory'; + break; + case 'job_template': + basePathKey = 'job_templates'; + break; + case 'credential': + basePathKey = 'credentials'; + break; + case 'user': + basePathKey = 'users'; + break; + case 'team': + basePathKey = 'teams'; + break; + case 'notification_template': + basePathKey = 'notification_templates'; + break; + case 'organization': + basePathKey = 'organizations'; + break; + case 'management_job': + basePathKey = 'management_jobs'; + break; + case 'custom_inventory_script': + basePathKey = 'inventory_scripts'; + break; + case 'workflow_job_template': + basePathKey = 'workflow_job_templates'; + break; + } + + return basePathKey; + }; + } diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index f17df84b89..dd0ce79cbe 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -37,7 +37,7 @@ if ($basePath) { } // Modules -import './helpers'; +import './forms'; import './lists'; import './filters'; import portalMode from './portal-mode/main'; @@ -71,7 +71,6 @@ import projects from './projects/main'; import RestServices from './rest/main'; import access from './access/main'; -import './shared/Modal'; import './shared/prompt-dialog'; import './shared/directives'; import './shared/filters'; @@ -91,6 +90,7 @@ var tower = angular.module('Tower', [ require('angular-sanitize'), require('angular-scheduler').name, require('angular-tz-extensions'), + require('angular-md5'), require('lr-infinite-scroll'), require('ng-toast'), 'gettext', @@ -138,66 +138,46 @@ var tower = angular.module('Tower', [ 'OrganizationListDefinition', 'templates', 'UserListDefinition', - 'UserHelper', 'PromptDialog', 'AWDirectives', 'InventoriesListDefinition', 'InventoryFormDefinition', - 'InventoryHelper', 'InventoryGroupsDefinition', 'InventoryHostsDefinition', - 'HostsHelper', 'AWFilters', 'HostFormDefinition', 'HostListDefinition', 'GroupFormDefinition', 'GroupListDefinition', - 'GroupsHelper', 'TeamsListDefinition', 'TeamFormDefinition', - 'TeamHelper', 'CredentialsListDefinition', 'CredentialFormDefinition', 'TemplatesListDefinition', 'PortalJobTemplatesListDefinition', 'JobTemplateFormDefinition', - 'JobTemplatesHelper', - 'JobSubmissionHelper', 'ProjectsListDefinition', 'ProjectFormDefinition', 'ProjectStatusDefinition', - 'ProjectsHelper', 'CompletedJobsDefinition', 'AllJobsDefinition', 'JobSummaryDefinition', - 'ParseHelper', - 'ChildrenHelper', - 'ProjectPathHelper', - 'md5Helper', - 'SelectionHelper', 'HostGroupsFormDefinition', - 'JobsHelper', - 'CredentialsHelper', 'StreamListDefinition', 'ActivityDetailDefinition', - 'VariablesHelper', 'SchedulesListDefinition', 'ScheduledJobsDefinition', //'Timezones', - 'SchedulesHelper', 'JobsListDefinition', 'LogViewerStatusDefinition', 'StandardOutHelper', 'LogViewerOptionsDefinition', 'lrInfiniteScroll', - 'LoadConfigHelper', 'PortalJobsListDefinition', 'features', 'longDateFilter', 'pendolytics', scheduler.name, - 'ApiModelHelper', - 'ActivityStreamHelper', 'WorkflowFormDefinition', 'InventorySourcesListDefinition', 'WorkflowMakerFormDefinition' diff --git a/awx/ui/client/src/credentials/add/credentials-add.controller.js b/awx/ui/client/src/credentials/add/credentials-add.controller.js index fa6c436e66..88f2a41e57 100644 --- a/awx/ui/client/src/credentials/add/credentials-add.controller.js +++ b/awx/ui/client/src/credentials/add/credentials-add.controller.js @@ -7,11 +7,11 @@ export default ['$scope', '$rootScope', '$compile', '$location', '$log', '$stateParams', 'CredentialForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'ClearScope', 'GetBasePath', 'GetChoices', 'Empty', 'KindChange', 'BecomeMethodChange', - 'OwnerChange', 'FormSave', '$state', 'CreateSelect2', 'i18n', + 'OwnerChange', 'CredentialFormSave', '$state', 'CreateSelect2', 'i18n', function($scope, $rootScope, $compile, $location, $log, $stateParams, CredentialForm, GenerateForm, Rest, Alert, ProcessErrors, ClearScope, GetBasePath, GetChoices, Empty, KindChange, BecomeMethodChange, - OwnerChange, FormSave, $state, CreateSelect2, i18n) { + OwnerChange, CredentialFormSave, $state, CreateSelect2, i18n) { ClearScope(); @@ -126,7 +126,7 @@ export default ['$scope', '$rootScope', '$compile', '$location', // Save $scope.formSave = function() { if ($scope[form.name + '_form'].$valid) { - FormSave({ scope: $scope, mode: 'add' }); + CredentialFormSave({ scope: $scope, mode: 'add' }); } }; diff --git a/awx/ui/client/src/credentials/edit/credentials-edit.controller.js b/awx/ui/client/src/credentials/edit/credentials-edit.controller.js index 2dd3ae1de2..9073b80bdb 100644 --- a/awx/ui/client/src/credentials/edit/credentials-edit.controller.js +++ b/awx/ui/client/src/credentials/edit/credentials-edit.controller.js @@ -8,10 +8,10 @@ export default ['$scope', '$rootScope', '$compile', '$location', '$log', '$stateParams', 'CredentialForm', 'Rest', 'Alert', 'ProcessErrors', 'ClearScope', 'Prompt', 'GetBasePath', 'GetChoices', 'KindChange', 'BecomeMethodChange', 'Empty', 'OwnerChange', - 'FormSave', 'Wait', '$state', 'CreateSelect2', 'Authorization', 'i18n', + 'CredentialFormSave', 'Wait', '$state', 'CreateSelect2', 'Authorization', 'i18n', function($scope, $rootScope, $compile, $location, $log, $stateParams, CredentialForm, Rest, Alert, ProcessErrors, ClearScope, Prompt, - GetBasePath, GetChoices, KindChange, BecomeMethodChange, Empty, OwnerChange, FormSave, Wait, + GetBasePath, GetChoices, KindChange, BecomeMethodChange, Empty, OwnerChange, CredentialFormSave, Wait, $state, CreateSelect2, Authorization, i18n) { ClearScope(); @@ -236,7 +236,7 @@ export default ['$scope', '$rootScope', '$compile', '$location', // Save changes to the parent $scope.formSave = function() { if ($scope[form.name + '_form'].$valid) { - FormSave({ scope: $scope, mode: 'edit' }); + CredentialFormSave({ scope: $scope, mode: 'edit' }); } }; diff --git a/awx/ui/client/src/credentials/factories/become-method-change.factory.js b/awx/ui/client/src/credentials/factories/become-method-change.factory.js new file mode 100644 index 0000000000..0727553528 --- /dev/null +++ b/awx/ui/client/src/credentials/factories/become-method-change.factory.js @@ -0,0 +1,105 @@ +export default + function BecomeMethodChange(Empty, i18n) { + return function(params) { + var scope = params.scope; + + if (!Empty(scope.kind)) { + // Apply kind specific settings + switch (scope.kind.value) { + case 'aws': + scope.aws_required = true; + break; + case 'rax': + scope.rackspace_required = true; + scope.username_required = true; + break; + case 'ssh': + scope.usernameLabel = i18n._('Username'); //formally 'SSH Username' + scope.becomeUsernameLabel = i18n._('Privilege Escalation Username'); + scope.becomePasswordLabel = i18n._('Privilege Escalation Password'); + break; + case 'scm': + scope.sshKeyDataLabel = i18n._('SCM Private Key'); + scope.passwordLabel = i18n._('Password'); + break; + case 'gce': + scope.usernameLabel = i18n._('Service Account Email Address'); + scope.sshKeyDataLabel = i18n._('RSA Private Key'); + scope.email_required = true; + scope.key_required = true; + scope.project_required = true; + scope.key_description = i18n._('Paste the contents of the PEM file associated with the service account email.'); + scope.projectLabel = i18n._("Project"); + scope.project_required = false; + scope.projectPopOver = "
" + i18n._("The Project ID is the " + + "GCE assigned identification. It is constructed as " + + "two words followed by a three digit number. Such " + + "as: ") + "
adjective-noun-000
"; + break; + case 'azure': + scope.sshKeyDataLabel = i18n._('Management Certificate'); + scope.subscription_required = true; + scope.key_required = true; + scope.key_description = i18n._("Paste the contents of the PEM file that corresponds to the certificate you uploaded in the Microsoft Azure console."); + break; + case 'azure_rm': + scope.usernameLabel = i18n._("Username"); + scope.subscription_required = true; + scope.passwordLabel = i18n._('Password'); + scope.azure_rm_required = true; + break; + case 'vmware': + scope.username_required = true; + scope.host_required = true; + scope.password_required = true; + scope.hostLabel = "vCenter Host"; + scope.passwordLabel = i18n._('Password'); + scope.hostPopOver = i18n._("Enter the hostname or IP address which corresponds to your VMware vCenter."); + break; + case 'openstack': + scope.hostLabel = i18n._("Host (Authentication URL)"); + scope.projectLabel = i18n._("Project (Tenant Name)"); + scope.domainLabel = i18n._("Domain Name"); + scope.password_required = true; + scope.project_required = true; + scope.host_required = true; + scope.username_required = true; + scope.projectPopOver = "" + i18n._("This is the tenant name. " + + " This value is usually the same " + + " as the username.") + "
"; + scope.hostPopOver = "" + i18n._("The host to authenticate with.") +
+ "
" + i18n.sprintf(i18n._("For example, %s"), "https://openstack.business.com/v2.0/");
+ break;
+ case 'satellite6':
+ scope.username_required = true;
+ scope.password_required = true;
+ scope.passwordLabel = i18n._('Password');
+ scope.host_required = true;
+ scope.hostLabel = i18n._("Satellite 6 URL");
+ scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL which corresponds to your %s" +
+ "Red Hat Satellite 6 server. %s" +
+ "For example, %s"), "
", "
", "https://satellite.example.org");
+ break;
+ case 'cloudforms':
+ scope.username_required = true;
+ scope.password_required = true;
+ scope.passwordLabel = i18n._('Password');
+ scope.host_required = true;
+ scope.hostLabel = i18n._("CloudForms URL");
+ scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL for the virtual machine which %s" +
+ "corresponds to your CloudForm instance. %s" +
+ "For example, %s"), "
", "
", "https://cloudforms.example.org");
+ break;
+ case 'net':
+ scope.username_required = true;
+ scope.password_required = false;
+ scope.passwordLabel = i18n._('Password');
+ scope.sshKeyDataLabel = i18n._('SSH Key');
+ break;
+ }
+ }
+ };
+ }
+
+BecomeMethodChange.$inject =
+ [ 'Empty', 'i18n' ];
diff --git a/awx/ui/client/src/credentials/factories/credential-form-save.factory.js b/awx/ui/client/src/credentials/factories/credential-form-save.factory.js
new file mode 100644
index 0000000000..d8efc83ffd
--- /dev/null
+++ b/awx/ui/client/src/credentials/factories/credential-form-save.factory.js
@@ -0,0 +1,105 @@
+export default
+ function CredentialFormSave($rootScope, $location, Alert, Rest, ProcessErrors, Empty, GetBasePath, CredentialForm, ReturnToCaller, Wait, $state, i18n) {
+ return function(params) {
+ var scope = params.scope,
+ mode = params.mode,
+ form = CredentialForm,
+ data = {}, fld, url;
+
+ for (fld in form.fields) {
+ if (fld !== 'access_key' && fld !== 'secret_key' && fld !== 'ssh_username' &&
+ fld !== 'ssh_password') {
+ if (fld === "organization" && !scope[fld]) {
+ data.user = $rootScope.current_user.id;
+ } else if (scope[fld] === null) {
+ data[fld] = "";
+ } else {
+ data[fld] = scope[fld];
+ }
+ }
+ }
+
+ data.kind = scope.kind.value;
+ if (scope.become_method === null || typeof scope.become_method === 'undefined') {
+ data.become_method = "";
+ data.become_username = "";
+ data.become_password = "";
+ } else {
+ data.become_method = (scope.become_method.value) ? scope.become_method.value : "";
+ }
+ switch (data.kind) {
+ case 'ssh':
+ data.password = scope.ssh_password;
+ break;
+ case 'aws':
+ data.username = scope.access_key;
+ data.password = scope.secret_key;
+ break;
+ case 'rax':
+ data.password = scope.api_key;
+ break;
+ case 'gce':
+ data.username = scope.email_address;
+ data.project = scope.project;
+ break;
+ case 'azure':
+ data.username = scope.subscription;
+ }
+
+ Wait('start');
+ if (mode === 'add') {
+ url = GetBasePath("credentials");
+ Rest.setUrl(url);
+ Rest.post(data)
+ .success(function (data) {
+ scope.addedItem = data.id;
+
+ Wait('stop');
+ var base = $location.path().replace(/^\//, '').split('/')[0];
+ if (base === 'credentials') {
+ $state.go('credentials.edit', {credential_id: data.id}, {reload: true});
+ }
+ else {
+ ReturnToCaller(1);
+ }
+ })
+ .error(function (data, status) {
+ Wait('stop');
+ // TODO: hopefully this conditional error handling will to away in a future version of tower. The reason why we cannot
+ // simply pass this error to ProcessErrors is because it will actually match the form element 'ssh_key_unlock' and show
+ // the error there. The ssh_key_unlock field is not shown when the kind of credential is gce/azure and as a result the
+ // error is never shown. In the future, the API will hopefully either behave or respond differently.
+ if(status && status === 400 && data && data.ssh_key_unlock && (scope.kind.value === 'gce' || scope.kind.value === 'azure')) {
+ scope.ssh_key_data_api_error = i18n._("Encrypted credentials are not supported.");
+ }
+ else {
+ ProcessErrors(scope, data, status, form, {
+ hdr: i18n._('Error!'),
+ msg: i18n._('Failed to create new Credential. POST status: ') + status
+ });
+ }
+ });
+ } else {
+ url = GetBasePath('credentials') + scope.id + '/';
+ Rest.setUrl(url);
+ Rest.put(data)
+ .success(function () {
+ Wait('stop');
+ $state.go($state.current, {}, {reload: true});
+ })
+ .error(function (data, status) {
+ Wait('stop');
+ ProcessErrors(scope, data, status, form, {
+ hdr: i18n._('Error!'),
+ msg: i18n._('Failed to update Credential. PUT status: ') + status
+ });
+ });
+ }
+ };
+ }
+
+CredentialFormSave.$inject =
+ [ '$rootScope', '$location', 'Alert', 'Rest',
+ 'ProcessErrors', 'Empty', 'GetBasePath', 'CredentialForm',
+ 'ReturnToCaller', 'Wait', '$state', 'i18n'
+ ];
diff --git a/awx/ui/client/src/credentials/factories/kind-change.factory.js b/awx/ui/client/src/credentials/factories/kind-change.factory.js
new file mode 100644
index 0000000000..e35bedc526
--- /dev/null
+++ b/awx/ui/client/src/credentials/factories/kind-change.factory.js
@@ -0,0 +1,192 @@
+export default
+ function KindChange(Empty, i18n) {
+ return function(params) {
+ var scope = params.scope,
+ reset = params.reset,
+ collapse, id;
+
+ $('.popover').each(function() {
+ // remove lingering popover
" + i18n._("The project value") + "
"; + scope.hostPopOver = "" + i18n._("The host value") + "
"; + scope.ssh_key_data_api_error = ''; + + if (!Empty(scope.kind)) { + // Apply kind specific settings + switch (scope.kind.value) { + case 'aws': + scope.aws_required = true; + break; + case 'rax': + scope.rackspace_required = true; + scope.username_required = true; + break; + case 'ssh': + scope.usernameLabel = i18n._('Username'); //formally 'SSH Username' + scope.becomeUsernameLabel = i18n._('Privilege Escalation Username'); + scope.becomePasswordLabel = i18n._('Privilege Escalation Password'); + break; + case 'scm': + scope.sshKeyDataLabel = i18n._('SCM Private Key'); + scope.passwordLabel = i18n._('Password'); + break; + case 'gce': + scope.usernameLabel = i18n._('Service Account Email Address'); + scope.sshKeyDataLabel = i18n._('RSA Private Key'); + scope.email_required = true; + scope.key_required = true; + scope.project_required = true; + scope.key_description = i18n._('Paste the contents of the PEM file associated with the service account email.'); + scope.projectLabel = i18n._("Project"); + scope.project_required = false; + scope.projectPopOver = "" + i18n._("The Project ID is the " + + "GCE assigned identification. It is constructed as " + + "two words followed by a three digit number. Such " + + "as: ") + "
adjective-noun-000
"; + break; + case 'azure': + scope.sshKeyDataLabel = i18n._('Management Certificate'); + scope.subscription_required = true; + scope.key_required = true; + scope.key_description = i18n._("Paste the contents of the PEM file that corresponds to the certificate you uploaded in the Microsoft Azure console."); + break; + case 'azure_rm': + scope.usernameLabel = i18n._("Username"); + scope.subscription_required = true; + scope.passwordLabel = i18n._('Password'); + scope.azure_rm_required = true; + break; + case 'vmware': + scope.username_required = true; + scope.host_required = true; + scope.password_required = true; + scope.hostLabel = "vCenter Host"; + scope.passwordLabel = i18n._('Password'); + scope.hostPopOver = i18n._("Enter the hostname or IP address which corresponds to your VMware vCenter."); + break; + case 'openstack': + scope.hostLabel = i18n._("Host (Authentication URL)"); + scope.projectLabel = i18n._("Project (Tenant Name)"); + scope.domainLabel = i18n._("Domain Name"); + scope.password_required = true; + scope.project_required = true; + scope.host_required = true; + scope.username_required = true; + scope.projectPopOver = "" + i18n._("This is the tenant name. " + + " This value is usually the same " + + " as the username.") + "
"; + scope.hostPopOver = "" + i18n._("The host to authenticate with.") +
+ "
" + i18n.sprintf(i18n._("For example, %s"), "https://openstack.business.com/v2.0/");
+ break;
+ case 'satellite6':
+ scope.username_required = true;
+ scope.password_required = true;
+ scope.passwordLabel = i18n._('Password');
+ scope.host_required = true;
+ scope.hostLabel = i18n._("Satellite 6 URL");
+ scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL which corresponds to your %s" +
+ "Red Hat Satellite 6 server. %s" +
+ "For example, %s"), "
", "
", "https://satellite.example.org");
+ break;
+ case 'cloudforms':
+ scope.username_required = true;
+ scope.password_required = true;
+ scope.passwordLabel = i18n._('Password');
+ scope.host_required = true;
+ scope.hostLabel = i18n._("CloudForms URL");
+ scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL for the virtual machine which %s" +
+ "corresponds to your CloudForm instance. %s" +
+ "For example, %s"), "
", "
", "https://cloudforms.example.org");
+ break;
+ case 'net':
+ scope.username_required = true;
+ scope.password_required = false;
+ scope.passwordLabel = i18n._('Password');
+ scope.sshKeyDataLabel = i18n._('SSH Key');
+ break;
+ }
+ }
+
+ // Reset all the field values related to Kind.
+ if (reset) {
+ scope.access_key = null;
+ scope.secret_key = null;
+ scope.api_key = null;
+ scope.username = null;
+ scope.password = null;
+ scope.password_confirm = null;
+ scope.ssh_key_data = null;
+ scope.ssh_key_unlock = null;
+ scope.ssh_key_unlock_confirm = null;
+ scope.become_username = null;
+ scope.become_password = null;
+ scope.authorize = false;
+ scope.authorize_password = null;
+ }
+
+ // Collapse or open help widget based on whether scm value is selected
+ collapse = $('#credential_kind').parent().find('.panel-collapse').first();
+ id = collapse.attr('id');
+ if (!Empty(scope.kind) && scope.kind.value !== '') {
+ if ($('#' + id + '-icon').hasClass('icon-minus')) {
+ scope.accordionToggle('#' + id);
+ }
+ } else {
+ if ($('#' + id + '-icon').hasClass('icon-plus')) {
+ scope.accordionToggle('#' + id);
+ }
+ }
+ };
+ }
+
+KindChange.$inject =
+ [ 'Empty', 'i18n' ];
diff --git a/awx/ui/client/src/credentials/factories/owner-change.factory.js b/awx/ui/client/src/credentials/factories/owner-change.factory.js
new file mode 100644
index 0000000000..60b77110bb
--- /dev/null
+++ b/awx/ui/client/src/credentials/factories/owner-change.factory.js
@@ -0,0 +1,18 @@
+export default
+ function OwnerChange() {
+ return function(params) {
+ var scope = params.scope,
+ owner = scope.owner;
+ if (owner === 'team') {
+ scope.team_required = true;
+ scope.user_required = false;
+ scope.user = null;
+ scope.user_username = null;
+ } else {
+ scope.team_required = false;
+ scope.user_required = true;
+ scope.team = null;
+ scope.team_name = null;
+ }
+ };
+ }
diff --git a/awx/ui/client/src/credentials/main.js b/awx/ui/client/src/credentials/main.js
index 16cc13aad0..e4bb2f6f2d 100644
--- a/awx/ui/client/src/credentials/main.js
+++ b/awx/ui/client/src/credentials/main.js
@@ -8,11 +8,19 @@ import ownerList from './ownerList.directive';
import CredentialsList from './list/credentials-list.controller';
import CredentialsAdd from './add/credentials-add.controller';
import CredentialsEdit from './edit/credentials-edit.controller';
+import BecomeMethodChange from './factories/become-method-change.factory';
+import CredentialFormSave from './factories/credential-form-save.factory';
+import KindChange from './factories/kind-change.factory';
+import OwnerChange from './factories/owner-change.factory';
import { N_ } from '../i18n';
export default
angular.module('credentials', [])
.directive('ownerList', ownerList)
+ .factory('BecomeMethodChange', BecomeMethodChange)
+ .factory('CredentialFormSave', CredentialFormSave)
+ .factory('KindChange', KindChange)
+ .factory('OwnerChange', OwnerChange)
.controller('CredentialsList', CredentialsList)
.controller('CredentialsAdd', CredentialsAdd)
.controller('CredentialsEdit', CredentialsEdit)
diff --git a/awx/ui/client/src/helpers.js b/awx/ui/client/src/helpers.js
deleted file mode 100644
index 765b81be5f..0000000000
--- a/awx/ui/client/src/helpers.js
+++ /dev/null
@@ -1,58 +0,0 @@
-/*************************************************
- * Copyright (c) 2015 Ansible, Inc.
- *
- * All Rights Reserved
- *************************************************/
-
-import './forms';
-import './lists';
-
-import Children from "./helpers/Children";
-import Credentials from "./helpers/Credentials";
-import Events from "./helpers/Events";
-import Groups from "./helpers/Groups";
-import Hosts from "./helpers/Hosts";
-import JobSubmission from "./helpers/JobSubmission";
-import JobTemplates from "./helpers/JobTemplates";
-import Jobs from "./helpers/Jobs";
-import LoadConfig from "./helpers/LoadConfig";
-import Parse from "./helpers/Parse";
-import ProjectPath from "./helpers/ProjectPath";
-import Projects from "./helpers/Projects";
-import Schedules from "./helpers/Schedules";
-import Selection from "./helpers/Selection";
-import Users from "./helpers/Users";
-import Variables from "./helpers/Variables";
-import ApiDefaults from "./helpers/api-defaults";
-import inventory from "./helpers/inventory";
-import MD5 from "./helpers/md5";
-import Teams from "./helpers/teams";
-import AdhocHelper from "./helpers/Adhoc";
-import ApiModelHelper from "./helpers/ApiModel";
-import ActivityStreamHelper from "./helpers/ActivityStream";
-
-export
- { Children,
- Credentials,
- Events,
- Groups,
- Hosts,
- JobSubmission,
- JobTemplates,
- Jobs,
- LoadConfig,
- Parse,
- ProjectPath,
- Projects,
- Schedules,
- Selection,
- Users,
- Variables,
- ApiDefaults,
- inventory,
- MD5,
- Teams,
- AdhocHelper,
- ApiModelHelper,
- ActivityStreamHelper
- };
diff --git a/awx/ui/client/src/helpers/ActivityStream.js b/awx/ui/client/src/helpers/ActivityStream.js
deleted file mode 100644
index 02bd972eee..0000000000
--- a/awx/ui/client/src/helpers/ActivityStream.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/*************************************************
- * Copyright (c) 2016 Ansible, Inc.
- *
- * All Rights Reserved
- *************************************************/
-
- /**
- * @ngdoc function
- * @name helpers.function:ActivityStream
- * @description Helper functions for the activity stream
-*/
-
-export default
- angular.module('ActivityStreamHelper', ['Utilities'])
- .factory('GetTargetTitle', ['i18n',
- function (i18n) {
- return function (target) {
-
- var rtnTitle = i18n._('ALL ACTIVITY');
-
- switch(target) {
- case 'project':
- rtnTitle = i18n._('PROJECTS');
- break;
- case 'inventory':
- rtnTitle = i18n._('INVENTORIES');
- break;
- case 'credential':
- rtnTitle = i18n._('CREDENTIALS');
- break;
- case 'user':
- rtnTitle = i18n._('USERS');
- break;
- case 'team':
- rtnTitle = i18n._('TEAMS');
- break;
- case 'notification_template':
- rtnTitle = i18n._('NOTIFICATION TEMPLATES');
- break;
- case 'organization':
- rtnTitle = i18n._('ORGANIZATIONS');
- break;
- case 'job':
- rtnTitle = i18n._('JOBS');
- break;
- case 'custom_inventory_script':
- rtnTitle = i18n._('INVENTORY SCRIPTS');
- break;
- case 'schedule':
- rtnTitle = i18n._('SCHEDULES');
- break;
- case 'host':
- rtnTitle = i18n._('HOSTS');
- break;
- case 'template':
- rtnTitle = i18n._('TEMPLATES');
- break;
- }
-
- return rtnTitle;
-
- };
- }
- ]);
diff --git a/awx/ui/client/src/helpers/Adhoc.js b/awx/ui/client/src/helpers/Adhoc.js
deleted file mode 100644
index d29e591ce5..0000000000
--- a/awx/ui/client/src/helpers/Adhoc.js
+++ /dev/null
@@ -1,172 +0,0 @@
-/*************************************************
- * Copyright (c) 2015 Ansible, Inc.
- *
- * All Rights Reserved
- *************************************************/
-
-/**
- * @ngdoc function
- * @name helpers.function:Adhoc
- * @description These routines are shared by adhoc command related controllers.
- * The content here is very similar to the JobSubmission helper, and in fact,
- * certain services are pulled from that helper. This leads to an important
- * point: if you need to create functionality that is shared between the command
- * and playbook run process, put that code in the JobSubmission helper and make
- * it into a reusable step (by specifying a callback parameter in the factory).
- * For a good example of this, please see how the AdhocLaunch factory in this
- * file utilizes the CheckPasswords factory from the JobSubmission helper.
- *
- * #AdhocRelaunch Step 1: preparing the GET to ad_hoc_commands/n/relaunch
- * The adhoc relaunch process is called from the JobSubmission helper. It is a
- * separate process from the initial adhoc run becuase of the way the API
- * endpoints work. For AdhocRelaunch, we have access to the original run and
- * we can pull the related relaunch URL by knowing the original Adhoc runs ID.
- *
- * #AdhocRelaunch Step 2: If we got passwords back, add them
- * The relaunch URL gives us back the passwords we need to prompt for (if any).
- * We'll go to step 3 if there are passwords, and step 4 if not.
- *
- * #AdhocRelaunch Step 3: PromptForPasswords and the CreateLaunchDialog
- *
- * #AdhocRelaunch Step 5: StartAdhocRun
- *
- * #AdhocRelaunch Step 6: LaunchJob and navigate to the standard out page.
-
- * **If you are
- * TODO: once the API endpoint is figured out for running an adhoc command
- * from the form is figured out, the rest work should probably be excised from
- * the controller and moved into here. See the todo statements in the
- * controller for more information about this.
- */
-
-export default
- angular.module('AdhocHelper', ['RestServices', 'Utilities',
- 'CredentialFormDefinition', 'CredentialsListDefinition',
- 'JobSubmissionHelper', 'JobTemplateFormDefinition', 'ModalDialog',
- 'FormGenerator', 'JobVarsPromptFormDefinition'])
-
- /**
- * @ngdoc method
- * @name helpers.function:JobSubmission#AdhocRun
- * @methodOf helpers.function:JobSubmission
- * @description The adhoc Run function is run when the user clicks the relaunch button
- *
- */
- // Submit request to run an adhoc comamand
- .factory('AdhocRun', ['$location','$stateParams', 'LaunchJob',
- 'PromptForPasswords', 'Rest', 'GetBasePath', 'Alert', 'ProcessErrors',
- 'Wait', 'Empty', 'CreateLaunchDialog', '$state',
- function ($location, $stateParams, LaunchJob, PromptForPasswords,
- Rest, GetBasePath, Alert, ProcessErrors, Wait, Empty, CreateLaunchDialog, $state) {
- return function (params) {
- var id = params.project_id,
- scope = params.scope.$new(),
- new_job_id,
- html,
- url;
-
- // this is used to cancel a running adhoc command from
- // the jobs page
- if (scope.removeCancelJob) {
- scope.removeCancelJob();
- }
- scope.removeCancelJob = scope.$on('CancelJob', function() {
- // Delete the job
- Wait('start');
- Rest.setUrl(GetBasePath('ad_hoc_commands') + new_job_id + '/');
- Rest.destroy()
- .success(function() {
- Wait('stop');
- })
- .error(function (data, status) {
- ProcessErrors(scope, data, status,
- null, { hdr: 'Error!',
- msg: 'Call to ' + url +
- ' failed. DELETE returned status: ' +
- status });
- });
- });
-
- if (scope.removeStartAdhocRun) {
- scope.removeStartAdhocRun();
- }
-
- scope.removeStartAdhocRun = scope.$on('StartAdhocRun', function() {
- var password,
- postData={};
- for (password in scope.passwords) {
- postData[scope.passwords[password]] = scope[
- scope.passwords[password]
- ];
- }
- // Re-launch the adhoc job
- Rest.setUrl(url);
- Rest.post(postData)
- .success(function (data) {
- Wait('stop');
- if($location.path().replace(/^\//, '').split('/')[0] !== 'jobs') {
- $state.go('adHocJobStdout', {id: data.id});
- }
- })
- .error(function (data, status) {
- ProcessErrors(scope, data, status, {
- hdr: 'Error!',
- msg: 'Failed to launch adhoc command. POST ' +
- 'returned status: ' + status });
- });
- });
-
- // start routine only if passwords need to be prompted
- if (scope.removeCreateLaunchDialog) {
- scope.removeCreateLaunchDialog();
- }
- scope.removeCreateLaunchDialog = scope.$on('CreateLaunchDialog',
- function(e, html, url) {
- CreateLaunchDialog({
- scope: scope,
- html: html,
- url: url,
- callback: 'StartAdhocRun'
- });
- });
-
- if (scope.removePromptForPasswords) {
- scope.removePromptForPasswords();
- }
- scope.removePromptForPasswords = scope.$on('PromptForPasswords',
- function(e, passwords_needed_to_start,html, url) {
- PromptForPasswords({
- scope: scope,
- passwords: passwords_needed_to_start,
- callback: 'CreateLaunchDialog',
- html: html,
- url: url
- });
- }); // end password prompting routine
-
- // start the adhoc relaunch routine
- Wait('start');
- url = GetBasePath('ad_hoc_commands') + id + '/relaunch/';
- Rest.setUrl(url);
- Rest.get()
- .success(function (data) {
- new_job_id = data.id;
-
- scope.passwords_needed_to_start = data.passwords_needed_to_start;
- if (!Empty(data.passwords_needed_to_start) &&
- data.passwords_needed_to_start.length > 0) {
- // go through the password prompt routine before
- // starting the adhoc run
- scope.$emit('PromptForPasswords', data.passwords_needed_to_start, html, url);
- }
- else {
- // no prompting of passwords needed
- scope.$emit('StartAdhocRun');
- }
- })
- .error(function (data, status) {
- ProcessErrors(scope, data, status, null, { hdr: 'Error!',
- msg: 'Failed to get job template details. GET returned status: ' + status });
- });
- };
- }]);
diff --git a/awx/ui/client/src/helpers/ApiModel.js b/awx/ui/client/src/helpers/ApiModel.js
deleted file mode 100644
index 20bed707f2..0000000000
--- a/awx/ui/client/src/helpers/ApiModel.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/*************************************************
- * Copyright (c) 2016 Ansible, Inc.
- *
- * All Rights Reserved
- *************************************************/
-
- /**
- * @ngdoc function
- * @name helpers.function:ApiModel
- * @description Helper functions to convert singular/plural versions of our models to the opposite
-*/
-
-export default
- angular.module('ApiModelHelper', ['Utilities'])
- .factory('ModelToBasePathKey', [
- function () {
- return function (model) {
- // This function takes in the singular model string and returns the key needed
- // to get the base path from $rootScope/local storage.
-
- var basePathKey;
-
- switch(model) {
- case 'project':
- basePathKey = 'projects';
- break;
- case 'inventory':
- basePathKey = 'inventory';
- break;
- case 'job_template':
- basePathKey = 'job_templates';
- break;
- case 'credential':
- basePathKey = 'credentials';
- break;
- case 'user':
- basePathKey = 'users';
- break;
- case 'team':
- basePathKey = 'teams';
- break;
- case 'notification_template':
- basePathKey = 'notification_templates';
- break;
- case 'organization':
- basePathKey = 'organizations';
- break;
- case 'management_job':
- basePathKey = 'management_jobs';
- break;
- case 'custom_inventory_script':
- basePathKey = 'inventory_scripts';
- break;
- case 'workflow_job_template':
- basePathKey = 'workflow_job_templates';
- break;
- }
-
- return basePathKey;
-
- };
- }
- ]);
diff --git a/awx/ui/client/src/helpers/Children.js b/awx/ui/client/src/helpers/Children.js
deleted file mode 100644
index 7c8246dd62..0000000000
--- a/awx/ui/client/src/helpers/Children.js
+++ /dev/null
@@ -1,111 +0,0 @@
-/*************************************************
- * Copyright (c) 2015 Ansible, Inc.
- *
- * All Rights Reserved
- *************************************************/
-
- /**
- * @ngdoc function
- * @name helpers.function:Children
- * @descriptionUsed in job_events to expand/collapse children by setting the
- * 'show' attribute of each job_event in the set of job_events.
- * See the filter in job_events.js list.
-*/
-
-export default
- angular.module('ChildrenHelper', ['RestServices', 'Utilities'])
- .factory('ToggleChildren', ['$location', 'Store', function ($location, Store) {
- return function (params) {
-
- var scope = params.scope,
- list = params.list,
- id = params.id,
- set = scope[list.name],
- clicked,
- //base = $location.path().replace(/^\//, '').split('/')[0],
- path = $location.path(),
- local_child_store;
-
- function updateExpand(key, expand) {
- var found = false;
- local_child_store.every(function(child, i) {
- if (child.key === key) {
- local_child_store[i].expand = expand;
- found = true;
- return false;
- }
- return true;
- });
- if (!found) {
- local_child_store.push({ key: key, expand: expand });
- }
- }
-
- function updateShow(key, show) {
- var found = false;
- local_child_store.every(function(child, i) {
- if (child.key === key) {
- local_child_store[i].show = show;
- found = true;
- return false;
- }
- return true;
- });
- if (!found) {
- local_child_store.push({ key: key, show: show });
- }
- }
-
- function expand(node) {
- var i, has_children = false;
- for (i = node + 1; i < set.length; i++) {
- if (set[i].parent === set[node].id) {
- updateShow(set[i].key, true);
- set[i].show = true;
- }
- }
- set[node].ngicon = (has_children) ? 'fa fa-minus-square-o node-toggle' : 'fa fa-minus-square-o node-toggle';
- }
-
- function collapse(node) {
- var i, has_children = false;
- for (i = node + 1; i < set.length; i++) {
- if (set[i].parent === set[node].id) {
- set[i].show = false;
- has_children = true;
- updateShow(set[i].key, false);
- if (set[i].related.children) {
- collapse(i);
- }
- }
- }
- set[node].ngicon = (has_children) ? 'fa fa-plus-square-o node-toggle' : 'fa fa-square-o node-toggle';
- }
-
- local_child_store = Store(path + '_children');
- if (!local_child_store) {
- local_child_store = [];
- }
-
- // Scan the array list and find the clicked element
- set.every(function(row, i) {
- if (row.id === id) {
- clicked = i;
- return false;
- }
- return true;
- });
-
- // Expand or collapse children based on clicked element's icon
- if (/plus-square-o/.test(set[clicked].ngicon)) {
- // Expand: lookup and display children
- expand(clicked);
- updateExpand(set[clicked].key, true);
- } else if (/minus-square-o/.test(set[clicked].ngicon)) {
- collapse(clicked);
- updateExpand(set[clicked].key, false);
- }
- Store(path + '_children', local_child_store);
- };
- }
- ]);
diff --git a/awx/ui/client/src/helpers/Credentials.js b/awx/ui/client/src/helpers/Credentials.js
deleted file mode 100644
index edb1e700ab..0000000000
--- a/awx/ui/client/src/helpers/Credentials.js
+++ /dev/null
@@ -1,432 +0,0 @@
-/*************************************************
- * Copyright (c) 2015 Ansible, Inc.
- *
- * All Rights Reserved
- *************************************************/
-
-/**
- * @ngdoc function
- * @name helpers.function:Credentials
- * @description Functions shared amongst Credential related controllers
- */
-
-export default
-angular.module('CredentialsHelper', ['Utilities'])
-
-.factory('KindChange', ['Empty', 'i18n',
- function (Empty, i18n) {
- return function (params) {
- var scope = params.scope,
- reset = params.reset,
- collapse, id;
-
- $('.popover').each(function() {
- // remove lingering popover
" + i18n._("The project value") + "
"; - scope.hostPopOver = "" + i18n._("The host value") + "
"; - scope.ssh_key_data_api_error = ''; - - if (!Empty(scope.kind)) { - // Apply kind specific settings - switch (scope.kind.value) { - case 'aws': - scope.aws_required = true; - break; - case 'rax': - scope.rackspace_required = true; - scope.username_required = true; - break; - case 'ssh': - scope.usernameLabel = i18n._('Username'); //formally 'SSH Username' - scope.becomeUsernameLabel = i18n._('Privilege Escalation Username'); - scope.becomePasswordLabel = i18n._('Privilege Escalation Password'); - break; - case 'scm': - scope.sshKeyDataLabel = i18n._('SCM Private Key'); - scope.passwordLabel = i18n._('Password'); - break; - case 'gce': - scope.usernameLabel = i18n._('Service Account Email Address'); - scope.sshKeyDataLabel = i18n._('RSA Private Key'); - scope.email_required = true; - scope.key_required = true; - scope.project_required = true; - scope.key_description = i18n._('Paste the contents of the PEM file associated with the service account email.'); - scope.projectLabel = i18n._("Project"); - scope.project_required = false; - scope.projectPopOver = "" + i18n._("The Project ID is the " + - "GCE assigned identification. It is constructed as " + - "two words followed by a three digit number. Such " + - "as: ") + "
adjective-noun-000
"; - break; - case 'azure': - scope.sshKeyDataLabel = i18n._('Management Certificate'); - scope.subscription_required = true; - scope.key_required = true; - scope.key_description = i18n._("Paste the contents of the PEM file that corresponds to the certificate you uploaded in the Microsoft Azure console."); - break; - case 'azure_rm': - scope.usernameLabel = i18n._("Username"); - scope.subscription_required = true; - scope.passwordLabel = i18n._('Password'); - scope.azure_rm_required = true; - break; - case 'vmware': - scope.username_required = true; - scope.host_required = true; - scope.password_required = true; - scope.hostLabel = "vCenter Host"; - scope.passwordLabel = i18n._('Password'); - scope.hostPopOver = i18n._("Enter the hostname or IP address which corresponds to your VMware vCenter."); - break; - case 'openstack': - scope.hostLabel = i18n._("Host (Authentication URL)"); - scope.projectLabel = i18n._("Project (Tenant Name)"); - scope.domainLabel = i18n._("Domain Name"); - scope.password_required = true; - scope.project_required = true; - scope.host_required = true; - scope.username_required = true; - scope.projectPopOver = "" + i18n._("This is the tenant name. " + - " This value is usually the same " + - " as the username.") + "
"; - scope.hostPopOver = "" + i18n._("The host to authenticate with.") +
- "
" + i18n.sprintf(i18n._("For example, %s"), "https://openstack.business.com/v2.0/");
- break;
- case 'satellite6':
- scope.username_required = true;
- scope.password_required = true;
- scope.passwordLabel = i18n._('Password');
- scope.host_required = true;
- scope.hostLabel = i18n._("Satellite 6 URL");
- scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL which corresponds to your %s" +
- "Red Hat Satellite 6 server. %s" +
- "For example, %s"), "
", "
", "https://satellite.example.org");
- break;
- case 'cloudforms':
- scope.username_required = true;
- scope.password_required = true;
- scope.passwordLabel = i18n._('Password');
- scope.host_required = true;
- scope.hostLabel = i18n._("CloudForms URL");
- scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL for the virtual machine which %s" +
- "corresponds to your CloudForm instance. %s" +
- "For example, %s"), "
", "
", "https://cloudforms.example.org");
- break;
- case 'net':
- scope.username_required = true;
- scope.password_required = false;
- scope.passwordLabel = i18n._('Password');
- scope.sshKeyDataLabel = i18n._('SSH Key');
- break;
- }
- }
-
- // Reset all the field values related to Kind.
- if (reset) {
- scope.access_key = null;
- scope.secret_key = null;
- scope.api_key = null;
- scope.username = null;
- scope.password = null;
- scope.password_confirm = null;
- scope.ssh_key_data = null;
- scope.ssh_key_unlock = null;
- scope.ssh_key_unlock_confirm = null;
- scope.become_username = null;
- scope.become_password = null;
- scope.authorize = false;
- scope.authorize_password = null;
- }
-
- // Collapse or open help widget based on whether scm value is selected
- collapse = $('#credential_kind').parent().find('.panel-collapse').first();
- id = collapse.attr('id');
- if (!Empty(scope.kind) && scope.kind.value !== '') {
- if ($('#' + id + '-icon').hasClass('icon-minus')) {
- scope.accordionToggle('#' + id);
- }
- } else {
- if ($('#' + id + '-icon').hasClass('icon-plus')) {
- scope.accordionToggle('#' + id);
- }
- }
-
- };
- }
-])
-
-.factory('BecomeMethodChange', ['Empty', 'i18n',
- function (Empty, i18n) {
- return function (params) {
- console.log('become method has changed');
- var scope = params.scope;
-
- if (!Empty(scope.kind)) {
- // Apply kind specific settings
- switch (scope.kind.value) {
- case 'aws':
- scope.aws_required = true;
- break;
- case 'rax':
- scope.rackspace_required = true;
- scope.username_required = true;
- break;
- case 'ssh':
- scope.usernameLabel = i18n._('Username'); //formally 'SSH Username'
- scope.becomeUsernameLabel = i18n._('Privilege Escalation Username');
- scope.becomePasswordLabel = i18n._('Privilege Escalation Password');
- break;
- case 'scm':
- scope.sshKeyDataLabel = i18n._('SCM Private Key');
- scope.passwordLabel = i18n._('Password');
- break;
- case 'gce':
- scope.usernameLabel = i18n._('Service Account Email Address');
- scope.sshKeyDataLabel = i18n._('RSA Private Key');
- scope.email_required = true;
- scope.key_required = true;
- scope.project_required = true;
- scope.key_description = i18n._('Paste the contents of the PEM file associated with the service account email.');
- scope.projectLabel = i18n._("Project");
- scope.project_required = false;
- scope.projectPopOver = "
" + i18n._("The Project ID is the " + - "GCE assigned identification. It is constructed as " + - "two words followed by a three digit number. Such " + - "as: ") + "
adjective-noun-000
"; - break; - case 'azure': - scope.sshKeyDataLabel = i18n._('Management Certificate'); - scope.subscription_required = true; - scope.key_required = true; - scope.key_description = i18n._("Paste the contents of the PEM file that corresponds to the certificate you uploaded in the Microsoft Azure console."); - break; - case 'azure_rm': - scope.usernameLabel = i18n._("Username"); - scope.subscription_required = true; - scope.passwordLabel = i18n._('Password'); - scope.azure_rm_required = true; - break; - case 'vmware': - scope.username_required = true; - scope.host_required = true; - scope.password_required = true; - scope.hostLabel = "vCenter Host"; - scope.passwordLabel = i18n._('Password'); - scope.hostPopOver = i18n._("Enter the hostname or IP address which corresponds to your VMware vCenter."); - break; - case 'openstack': - scope.hostLabel = i18n._("Host (Authentication URL)"); - scope.projectLabel = i18n._("Project (Tenant Name)"); - scope.domainLabel = i18n._("Domain Name"); - scope.password_required = true; - scope.project_required = true; - scope.host_required = true; - scope.username_required = true; - scope.projectPopOver = "" + i18n._("This is the tenant name. " + - " This value is usually the same " + - " as the username.") + "
"; - scope.hostPopOver = "" + i18n._("The host to authenticate with.") +
- "
" + i18n.sprintf(i18n._("For example, %s"), "https://openstack.business.com/v2.0/");
- break;
- case 'satellite6':
- scope.username_required = true;
- scope.password_required = true;
- scope.passwordLabel = i18n._('Password');
- scope.host_required = true;
- scope.hostLabel = i18n._("Satellite 6 URL");
- scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL which corresponds to your %s" +
- "Red Hat Satellite 6 server. %s" +
- "For example, %s"), "
", "
", "https://satellite.example.org");
- break;
- case 'cloudforms':
- scope.username_required = true;
- scope.password_required = true;
- scope.passwordLabel = i18n._('Password');
- scope.host_required = true;
- scope.hostLabel = i18n._("CloudForms URL");
- scope.hostPopOver = i18n.sprintf(i18n._("Enter the URL for the virtual machine which %s" +
- "corresponds to your CloudForm instance. %s" +
- "For example, %s"), "
", "
", "https://cloudforms.example.org");
- break;
- case 'net':
- scope.username_required = true;
- scope.password_required = false;
- scope.passwordLabel = i18n._('Password');
- scope.sshKeyDataLabel = i18n._('SSH Key');
- break;
- }
- }
- };
- }
-])
-
-
-.factory('OwnerChange', [
- function () {
- return function (params) {
- var scope = params.scope,
- owner = scope.owner;
- if (owner === 'team') {
- scope.team_required = true;
- scope.user_required = false;
- scope.user = null;
- scope.user_username = null;
- } else {
- scope.team_required = false;
- scope.user_required = true;
- scope.team = null;
- scope.team_name = null;
- }
- };
-}
-])
-
-.factory('FormSave', ['$rootScope', '$location', 'Alert', 'Rest', 'ProcessErrors', 'Empty', 'GetBasePath', 'CredentialForm', 'ReturnToCaller', 'Wait', '$state', 'i18n',
- function ($rootScope, $location, Alert, Rest, ProcessErrors, Empty, GetBasePath, CredentialForm, ReturnToCaller, Wait, $state, i18n) {
- return function (params) {
- var scope = params.scope,
- mode = params.mode,
- form = CredentialForm,
- data = {}, fld, url;
-
- for (fld in form.fields) {
- if (fld !== 'access_key' && fld !== 'secret_key' && fld !== 'ssh_username' &&
- fld !== 'ssh_password') {
- if (fld === "organization" && !scope[fld]) {
- data.user = $rootScope.current_user.id;
- } else if (scope[fld] === null) {
- data[fld] = "";
- } else {
- data[fld] = scope[fld];
- }
- }
- }
-
- data.kind = scope.kind.value;
- if (scope.become_method === null || typeof scope.become_method === 'undefined') {
- data.become_method = "";
- data.become_username = "";
- data.become_password = "";
- } else {
- data.become_method = (scope.become_method.value) ? scope.become_method.value : "";
- }
- switch (data.kind) {
- case 'ssh':
- data.password = scope.ssh_password;
- break;
- case 'aws':
- data.username = scope.access_key;
- data.password = scope.secret_key;
- break;
- case 'rax':
- data.password = scope.api_key;
- break;
- case 'gce':
- data.username = scope.email_address;
- data.project = scope.project;
- break;
- case 'azure':
- data.username = scope.subscription;
- }
-
- Wait('start');
- if (mode === 'add') {
- url = GetBasePath("credentials");
- Rest.setUrl(url);
- Rest.post(data)
- .success(function (data) {
- scope.addedItem = data.id;
- Wait('stop');
- var base = $location.path().replace(/^\//, '').split('/')[0];
- if (base === 'credentials') {
- $state.go('credentials.edit', {credential_id: data.id}, {reload: true});
- }
- else {
- ReturnToCaller(1);
- }
- })
- .error(function (data, status) {
- Wait('stop');
- // TODO: hopefully this conditional error handling will to away in a future version of tower. The reason why we cannot
- // simply pass this error to ProcessErrors is because it will actually match the form element 'ssh_key_unlock' and show
- // the error there. The ssh_key_unlock field is not shown when the kind of credential is gce/azure and as a result the
- // error is never shown. In the future, the API will hopefully either behave or respond differently.
- if(status && status === 400 && data && data.ssh_key_unlock && (scope.kind.value === 'gce' || scope.kind.value === 'azure')) {
- scope.ssh_key_data_api_error = i18n._("Encrypted credentials are not supported.");
- }
- else {
- ProcessErrors(scope, data, status, form, {
- hdr: i18n._('Error!'),
- msg: i18n._('Failed to create new Credential. POST status: ') + status
- });
- }
- });
- } else {
- url = GetBasePath('credentials') + scope.id + '/';
- Rest.setUrl(url);
- Rest.put(data)
- .success(function () {
- Wait('stop');
- $state.go($state.current, {}, {reload: true});
- })
- .error(function (data, status) {
- Wait('stop');
- ProcessErrors(scope, data, status, form, {
- hdr: i18n._('Error!'),
- msg: i18n._('Failed to update Credential. PUT status: ') + status
- });
- });
- }
- };
- }
-]);
diff --git a/awx/ui/client/src/helpers/Events.js b/awx/ui/client/src/helpers/Events.js
deleted file mode 100644
index be58e72661..0000000000
--- a/awx/ui/client/src/helpers/Events.js
+++ /dev/null
@@ -1,237 +0,0 @@
-/*************************************************
- * Copyright (c) 2015 Ansible, Inc.
- *
- * All Rights Reserved
- *************************************************/
-
- /**
- * @ngdoc function
- * @name helpers.function:Events
- * @description EventView - show the job_events form in a modal dialog
-*/
-
-export default
- angular.module('EventsHelper', ['RestServices', 'Utilities', 'JobEventDataDefinition', 'JobEventsFormDefinition'])
-
- .factory('EventView', ['$rootScope', '$location', '$log', '$stateParams', 'Rest', 'Alert', 'GenerateForm',
- 'Prompt', 'ProcessErrors', 'GetBasePath', 'FormatDate', 'JobEventDataForm', 'Empty', 'JobEventsForm',
- function ($rootScope, $location, $log, $stateParams, Rest, Alert, GenerateForm, Prompt, ProcessErrors, GetBasePath,
- FormatDate, JobEventDataForm, Empty, JobEventsForm) {
- return function (params) {
-
- var event_id = params.event_id,
- generator = GenerateForm,
- form = angular.copy(JobEventsForm),
- scope,
- defaultUrl = GetBasePath('base') + 'job_events/' + event_id + '/';
-
- // Retrieve detail record and prepopulate the form
- Rest.setUrl(defaultUrl);
- Rest.get()
- .success(function (data) {
- var i, n, fld, rows, txt, cDate;
-
- // If event_data is not available, remove fields that depend on it
- if ($.isEmptyObject(data.event_data) || !data.event_data.res || typeof data.event_data.res === 'string') {
- for (fld in form.fields) {
- switch (fld) {
- case 'start':
- case 'end':
- case 'delta':
- case 'msg':
- case 'stdout':
- case 'stderr':
- case 'msg':
- case 'results':
- case 'module_name':
- case 'module_args':
- case 'rc':
- delete form.fields[fld];
- break;
- }
- }
- }
-
- if ($.isEmptyObject(data.event_data) || !data.event_data.res || typeof data.event_data.res !== 'string') {
- delete form.fields.traceback;
- }
-
- // Remove remaining form fields that do not have a corresponding data value
- for (fld in form.fields) {
- switch (fld) {
- case 'start':
- case 'end':
- case 'delta':
- case 'msg':
- case 'stdout':
- case 'stderr':
- case 'msg':
- case 'rc':
- if (data.event_data && data.event_data.res && Empty(data.event_data.res[fld])) {
- delete form.fields[fld];
- } else {
- if (form.fields[fld].type === 'textarea') {
- n = data.event_data.res[fld].match(/\n/g);
- rows = (n) ? n.length : 1;
- rows = (rows > 10) ? 10 : rows;
- rows = (rows < 3) ? 3 : rows;
- form.fields[fld].rows = rows;
- }
- }
- break;
- case 'results':
- if (data.event_data && data.event_data.res && data.event_data.res[fld] === undefined) {
- // not defined
- delete form.fields[fld];
- } else if (!Array.isArray(data.event_data.res[fld]) || data.event_data.res[fld].length === 0) {
- // defined, but empty
- delete form.fields[fld];
- } else {
- // defined and not empty, so attempt to size the textarea field
- txt = '';
- for (i = 0; i < data.event_data.res[fld].length; i++) {
- txt += data.event_data.res[fld][i];
- }
- if (txt === '') {
- // there's an array, but the actual text is empty
- delete form.fields[fld];
- } else {
- n = txt.match(/\n/g);
- rows = (n) ? n.length : 1;
- rows = (rows > 10) ? 10 : rows;
- rows = (rows < 3) ? 3 : rows;
- form.fields[fld].rows = rows;
- }
- }
- break;
- case 'module_name':
- case 'module_args':
- if (data.event_data && data.event_data.res) {
- if (data.event_data.res.invocation === undefined ||
- data.event_data.res.invocation[fld] === undefined) {
- delete form.fields[fld];
- }
- }
- break;
- }
- }
-
- // load the form
- scope = generator.inject(form, {
- mode: 'edit',
- modal: true,
- related: false
- });
- generator.reset();
- scope.formModalAction = function () {
- $('#form-modal').modal("hide");
- };
- scope.formModalActionLabel = 'OK';
- scope.formModalCancelShow = false;
- scope.formModalInfo = 'View JSON';
- $('#form-modal .btn-success').removeClass('btn-success').addClass('btn-none');
- $('#form-modal').addClass('skinny-modal');
- scope.formModalHeader = data.event_display.replace(/^\u00a0*/g, '');
-
- // Respond to View JSON button
- scope.formModalInfoAction = function () {
- var generator = GenerateForm,
- scope = generator.inject(JobEventDataForm, {
- mode: 'edit',
- modal: true,
- related: false,
- modal_selector: '#form-modal2',
- modal_body_id: 'form-modal2-body',
- modal_title_id: 'formModal2Header'
- });
- generator.reset();
- scope.formModal2Header = data.event_display.replace(/^\u00a0*/g, '');
- scope.event_data = JSON.stringify(data.event_data, null, '\t');
- scope.formModal2ActionLabel = 'OK';
- scope.formModal2CancelShow = false;
- scope.formModal2Info = false;
- scope.formModalInfo = 'View JSON';
- scope.formModal2Action = function () {
- $('#form-modal2').modal("hide");
- };
- $('#form-modal2 .btn-success').removeClass('btn-success').addClass('btn-none');
- };
-
- if (typeof data.event_data.res === 'string') {
- scope.traceback = data.event_data.res;
- }
-
- for (fld in form.fields) {
- switch (fld) {
- case 'status':
- if (data.failed) {
- scope.status = 'error';
- } else if (data.changed) {
- scope.status = 'changed';
- } else {
- scope.status = 'success';
- }
- break;
- case 'created':
- cDate = new Date(data.created);
- scope.created = FormatDate(cDate);
- break;
- case 'host':
- if (data.summary_fields && data.summary_fields.host) {
- scope.host = data.summary_fields.host.name;
- }
- break;
- case 'id':
- case 'task':
- case 'play':
- scope[fld] = data[fld];
- break;
- case 'start':
- case 'end':
- if (data.event_data && data.event_data.res && !Empty(data.event_data.res[fld])) {
- scope[fld] = data.event_data.res[fld];
- }
-
- break;
- case 'results':
- if (Array.isArray(data.event_data.res[fld]) && data.event_data.res[fld].length > 0) {
- txt = '';
- for (i = 0; i < data.event_data.res[fld].length; i++) {
- txt += data.event_data.res[fld][i];
- }
- if (txt !== '') {
- scope[fld] = txt;
- }
- }
- break;
- case 'msg':
- case 'stdout':
- case 'stderr':
- case 'delta':
- case 'rc':
- if (data.event_data && data.event_data.res && data.event_data.res[fld] !== undefined) {
- scope[fld] = data.event_data.res[fld];
- }
- break;
- case 'module_name':
- case 'module_args':
- if (data.event_data.res && data.event_data.res.invocation) {
- scope[fld] = data.event_data.res.invocation[fld];
- }
- break;
- }
- }
-
- if (!scope.$$phase) {
- scope.$digest();
- }
-
- })
- .error(function (data, status) {
- $('#form-modal').modal("hide");
- ProcessErrors(scope, data, status, form, { hdr: 'Error!',
- msg: 'Failed to retrieve event: ' + event_id + '. GET status: ' + status });
- });
- };
- }
- ]);
diff --git a/awx/ui/client/src/helpers/Groups.js b/awx/ui/client/src/helpers/Groups.js
deleted file mode 100644
index 295af244a1..0000000000
--- a/awx/ui/client/src/helpers/Groups.js
+++ /dev/null
@@ -1,1028 +0,0 @@
-/*************************************************
- * Copyright (c) 2015 Ansible, Inc.
- *
- * All Rights Reserved
- *************************************************/
-
-'use strict';
-
-/**
- * @ngdoc function
- * @name helpers.function:Groups
- * @description inventory tree widget add/edit/delete
-*/
-
-import listGenerator from '../shared/list-generator/main';
-
-export default
-angular.module('GroupsHelper', [ 'RestServices', 'Utilities', listGenerator.name, 'GroupListDefinition', listGenerator.name, 'GroupsHelper', 'InventoryHelper', 'SelectionHelper',
- 'JobSubmissionHelper', 'PromptDialog', 'CredentialsListDefinition',
- 'InventoryStatusDefinition', 'VariablesHelper', 'SchedulesListDefinition', 'StandardOutHelper',
- 'SchedulesHelper'
-])
-
-/**
- *
- * Lookup options for group source and build an array of drop-down choices
- *
- */
-.factory('GetSourceTypeOptions', ['Rest', 'ProcessErrors', 'GetBasePath',
- function (Rest, ProcessErrors, GetBasePath) {
- return function (params) {
- var scope = params.scope,
- variable = params.variable;
-
- if (scope[variable] === undefined) {
- scope[variable] = [];
- Rest.setUrl(GetBasePath('inventory_sources'));
- Rest.options()
- .success(function (data) {
- var i, choices = data.actions.GET.source.choices;
- for (i = 0; i < choices.length; i++) {
- if (choices[i][0] !== 'file') {
- scope[variable].push({
- label: choices[i][1],
- value: choices[i][0]
- });
- }
- }
- scope.cloudCredentialRequired = false;
- scope.$emit('sourceTypeOptionsReady');
- })
- .error(function (data, status) {
- ProcessErrors(scope, data, status, null, { hdr: 'Error!',
- msg: 'Failed to retrieve options for inventory_sources.source. OPTIONS status: ' + status
- });
- });
- }
- };
- }
-])
-
-/**
- *
- * TODO: Document
- *
- */
-.factory('ViewUpdateStatus', ['$state', 'Rest', 'ProcessErrors', 'GetBasePath', 'Alert', 'Wait', 'Empty', 'Find',
- function ($state, Rest, ProcessErrors, GetBasePath, Alert, Wait, Empty, Find) {
- return function (params) {
-
- var scope = params.scope,
- group_id = params.group_id,
- group = Find({ list: scope.groups, key: 'id', val: group_id });
-
- if (scope.removeSourceReady) {
- scope.removeSourceReady();
- }
- scope.removeSourceReady = scope.$on('SourceReady', function(e, source) {
-
- // Get the ID from the correct summary field
- var update_id = (source.summary_fields.current_update) ? source.summary_fields.current_update.id : source.summary_fields.last_update.id;
-
- $state.go('inventorySyncStdout', {id: update_id});
-
- });
-
- if (group) {
- if (Empty(group.source)) {
- // do nothing
- } else if (Empty(group.status) || group.status === "never updated") {
- Alert('No Status Available', '
No recent job data available for this host.
\n"; - } - - function setMsg(host) { - var j, job, jobs; - - if (host.has_active_failures === true || (host.has_active_failures === false && host.last_job !== null)) { - if (host.has_active_failures === true) { - host.badgeToolTip = 'Most recent job failed. Click to view jobs.'; - host.active_failures = 'error'; - } - else { - host.badgeToolTip = "Most recent job successful. Click to view jobs."; - host.active_failures = 'successful'; - } - if (host.summary_fields.recent_jobs.length > 0) { - // build html table of job status info - jobs = host.summary_fields.recent_jobs.sort( - function(a,b) { - // reverse numerical order - return -1 * (a - b); - }); - title = "Recent Jobs"; - html = "| Status | \n"; - html += "Finished | \n"; - html += "Name | \n"; - html += "
|---|---|---|
| \n"; - - html += " | " + ($filter('longDate')(job.finished)).replace(/ /,' ') + " | \n";
-
- html += "" + ellipsis(job.name) + " | \n"; - - html += "
With a provisioning callback URL and a host config key a host can contact Tower and request a configuration update using this job " + - "template. The request from the host must be a POST. Here is an example using curl:
\n" + - "curl --data \"host_config_key=" + scope.example_config_key + "\" " +
- scope.callback_server_path + GetBasePath('job_templates') + scope.example_template_id + "/callback/\n" +
- "Note the requesting host must be defined in the inventory associated with the job template. If Tower fails to " + - "locate the host, the request will be denied.
" + - "Successful requests create an entry on the Jobs page, where results and history can be viewed.
"; - }; - - // The md5 helper emits NewMD5Generated whenever a new key is available - if (scope.removeNewMD5Generated) { - scope.removeNewMD5Generated(); - } - scope.removeNewMD5Generated = scope.$on('NewMD5Generated', function() { - scope.configKeyChange(); - }); - - // Fired when user enters a key value - scope.configKeyChange = function() { - scope.example_config_key = scope.host_config_key; - scope.setCallbackHelp(); - }; - - // Set initial values and construct help text - scope.callback_server_path = $location.protocol() + '://' + $location.host() + (($location.port()) ? ':' + $location.port() : ''); - scope.example_config_key = '5a8ec154832b780b9bdef1061764ae5a'; - scope.example_template_id = 'N'; - scope.setCallbackHelp(); - - // this fills the job template form both on copy of the job template - // and on edit - scope.fillJobTemplate = function(){ - // id = id || $rootScope.copy.id; - // Retrieve detail record and prepopulate the form - Rest.setUrl(defaultUrl + id); - Rest.get() - .success(function (data) { - scope.job_template_obj = data; - scope.name = data.name; - var fld, i; - for (fld in form.fields) { - if (fld !== 'variables' && fld !== 'survey' && data[fld] !== null && data[fld] !== undefined) { - if (form.fields[fld].type === 'select') { - if (scope[fld + '_options'] && scope[fld + '_options'].length > 0) { - for (i = 0; i < scope[fld + '_options'].length; i++) { - if (data[fld] === scope[fld + '_options'][i].value) { - scope[fld] = scope[fld + '_options'][i]; - } - } - } else { - scope[fld] = data[fld]; - } - } else { - scope[fld] = data[fld]; - if(!Empty(data.summary_fields.survey)) { - scope.survey_exists = true; - } - } - master[fld] = scope[fld]; - } - if (fld === 'variables') { - // Parse extra_vars, converting to YAML. - scope.variables = ParseVariableString(data.extra_vars); - master.variables = scope.variables; - } - if (form.fields[fld].type === 'lookup' && 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] = - scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField]; - } - if (form.fields[fld].type === 'checkbox_group') { - for(var j=0; jNo recent job data available for this host.
\n"; + } + + function setMsg(host) { + var j, job, jobs; + + if (host.has_active_failures === true || (host.has_active_failures === false && host.last_job !== null)) { + if (host.has_active_failures === true) { + host.badgeToolTip = 'Most recent job failed. Click to view jobs.'; + host.active_failures = 'error'; + } + else { + host.badgeToolTip = "Most recent job successful. Click to view jobs."; + host.active_failures = 'successful'; + } + if (host.summary_fields.recent_jobs.length > 0) { + // build html table of job status info + jobs = host.summary_fields.recent_jobs.sort( + function(a,b) { + // reverse numerical order + return -1 * (a - b); + }); + title = "Recent Jobs"; + html = "| Status | \n"; + html += "Finished | \n"; + html += "Name | \n"; + html += "
|---|---|---|
| \n"; + + html += " | " + ($filter('longDate')(job.finished)).replace(/ /,' ') + " | \n";
+
+ html += "" + ellipsis(job.name) + " | \n"; + + html += "
With a provisioning callback URL and a host config key a host can contact Tower and request a configuration update using this job " + + "template. The request from the host must be a POST. Here is an example using curl:
\n" + + "curl --data \"host_config_key=" + scope.example_config_key + "\" " +
+ scope.callback_server_path + GetBasePath('job_templates') + scope.example_template_id + "/callback/\n" +
+ "Note the requesting host must be defined in the inventory associated with the job template. If Tower fails to " + + "locate the host, the request will be denied.
" + + "Successful requests create an entry on the Jobs page, where results and history can be viewed.
"; + }; + + // The md5 helper emits NewMD5Generated whenever a new key is available + if (scope.removeNewMD5Generated) { + scope.removeNewMD5Generated(); + } + scope.removeNewMD5Generated = scope.$on('NewMD5Generated', function() { + scope.configKeyChange(); + }); + + // Fired when user enters a key value + scope.configKeyChange = function() { + scope.example_config_key = scope.host_config_key; + scope.setCallbackHelp(); + }; + + // Set initial values and construct help text + scope.callback_server_path = $location.protocol() + '://' + $location.host() + (($location.port()) ? ':' + $location.port() : ''); + scope.example_config_key = '5a8ec154832b780b9bdef1061764ae5a'; + scope.example_template_id = 'N'; + scope.setCallbackHelp(); + + // this fills the job template form both on copy of the job template + // and on edit + scope.fillJobTemplate = function(){ + // id = id || $rootScope.copy.id; + // Retrieve detail record and prepopulate the form + Rest.setUrl(defaultUrl + id); + Rest.get() + .success(function (data) { + scope.job_template_obj = data; + scope.name = data.name; + var fld, i; + for (fld in form.fields) { + if (fld !== 'variables' && fld !== 'survey' && data[fld] !== null && data[fld] !== undefined) { + if (form.fields[fld].type === 'select') { + if (scope[fld + '_options'] && scope[fld + '_options'].length > 0) { + for (i = 0; i < scope[fld + '_options'].length; i++) { + if (data[fld] === scope[fld + '_options'][i].value) { + scope[fld] = scope[fld + '_options'][i]; + } + } + } else { + scope[fld] = data[fld]; + } + } else { + scope[fld] = data[fld]; + if(!Empty(data.summary_fields.survey)) { + scope.survey_exists = true; + } + } + master[fld] = scope[fld]; + } + if (fld === 'variables') { + // Parse extra_vars, converting to YAML. + scope.variables = ParseVariableString(data.extra_vars); + master.variables = scope.variables; + } + if (form.fields[fld].type === 'lookup' && 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] = + scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField]; + } + if (form.fields[fld].type === 'checkbox_group') { + for(var j=0; j