Merge branch 'release_3.1.0' into jobResultsPerf

This commit is contained in:
jlmitch5
2017-01-12 16:15:09 -05:00
committed by GitHub
80 changed files with 18728 additions and 267 deletions

View File

@@ -251,6 +251,7 @@ class BaseSerializer(serializers.ModelSerializer):
'inventory_update': _('Inventory Sync'),
'system_job': _('Management Job'),
'workflow_job': _('Workflow Job'),
'workflow_job_template': _('Workflow Template'),
}
choices = []
for t in self.get_types():
@@ -2708,18 +2709,15 @@ class WorkflowJobLaunchSerializer(BaseSerializer):
variables_needed_to_start = serializers.ReadOnlyField()
survey_enabled = serializers.SerializerMethodField()
extra_vars = VerbatimField(required=False, write_only=True)
warnings = serializers.SerializerMethodField()
workflow_job_template_data = serializers.SerializerMethodField()
class Meta:
model = WorkflowJobTemplate
fields = ('can_start_without_user_input', 'extra_vars', 'warnings',
fields = ('can_start_without_user_input', 'extra_vars',
'survey_enabled', 'variables_needed_to_start',
'node_templates_missing', 'node_prompts_rejected',
'workflow_job_template_data')
def get_warnings(self, obj):
return obj.get_warnings()
def get_survey_enabled(self, obj):
if obj:
return obj.survey_enabled and 'spec' in obj.survey_spec

View File

@@ -1553,12 +1553,14 @@ class InventoryScriptList(ListCreateAPIView):
model = CustomInventoryScript
serializer_class = CustomInventoryScriptSerializer
new_in_210 = True
class InventoryScriptDetail(RetrieveUpdateDestroyAPIView):
model = CustomInventoryScript
serializer_class = CustomInventoryScriptSerializer
new_in_210 = True
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
@@ -2904,10 +2906,16 @@ class WorkflowJobTemplateCopy(WorkflowsEnforcementMixin, GenericAPIView):
def get(self, request, *args, **kwargs):
obj = self.get_object()
data = {}
copy_TF, messages = request.user.can_access_with_errors(self.model, 'copy', obj)
data['can_copy'] = copy_TF
data['warnings'] = messages
can_copy, messages = request.user.can_access_with_errors(self.model, 'copy', obj)
data = OrderedDict([
('can_copy', can_copy), ('can_copy_without_user_input', can_copy),
('templates_unable_to_copy', [] if can_copy else ['all']),
('credentials_unable_to_copy', [] if can_copy else ['all']),
('inventories_unable_to_copy', [] if can_copy else ['all'])
])
if messages and can_copy:
data['can_copy_without_user_input'] = False
data.update(messages)
return Response(data)
def post(self, request, *args, **kwargs):
@@ -2938,7 +2946,10 @@ class WorkflowJobTemplateLaunch(WorkflowsEnforcementMixin, RetrieveAPIView):
always_allow_superuser = False
def update_raw_data(self, data):
obj = self.get_object()
try:
obj = self.get_object()
except PermissionDenied:
return data
extra_vars = data.pop('extra_vars', None) or {}
if obj:
for v in obj.variables_needed_to_start:

View File

@@ -328,7 +328,7 @@ class BaseCallbackModule(CallbackBase):
ok=stats.ok,
processed=stats.processed,
skipped=stats.skipped,
artifact_data=stats.custom.get('_run', {})
artifact_data=stats.custom.get('_run', {}) if hasattr(stats, 'custom') else {}
)
with self.capture_event_data('playbook_on_stats', **event_data):

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1043,7 +1043,7 @@ class JobTemplateAccess(BaseAccess):
Project.accessible_objects(self.user, 'use_role').exists() or
Inventory.accessible_objects(self.user, 'use_role').exists())
# if reference_obj is provided, determine if it can be coppied
# if reference_obj is provided, determine if it can be copied
reference_obj = data.get('reference_obj', None)
if 'job_type' in data and data['job_type'] == PERM_INVENTORY_SCAN:
@@ -1537,22 +1537,28 @@ class WorkflowJobTemplateAccess(BaseAccess):
def can_copy(self, obj):
if self.save_messages:
wfjt_errors = {}
missing_ujt = []
missing_credentials = []
missing_inventories = []
qs = obj.workflow_job_template_nodes
qs.select_related('unified_job_template', 'inventory', 'credential')
for node in qs.all():
node_errors = {}
if node.inventory and self.user not in node.inventory.use_role:
node_errors['inventory'] = 'Prompted inventory %s can not be coppied.' % node.inventory.name
missing_inventories.append(node.inventory.name)
if node.credential and self.user not in node.credential.use_role:
node_errors['credential'] = 'Prompted credential %s can not be coppied.' % node.credential.name
missing_credentials.append(node.credential.name)
ujt = node.unified_job_template
if ujt and not self.user.can_access(UnifiedJobTemplate, 'start', ujt, validate_license=False):
node_errors['unified_job_template'] = (
'Prompted %s %s can not be coppied.' % (ujt._meta.verbose_name_raw, ujt.name))
missing_ujt.append(ujt.name)
if node_errors:
wfjt_errors[node.id] = node_errors
self.messages.update(wfjt_errors)
if missing_ujt:
self.messages['templates_unable_to_copy'] = missing_ujt
if missing_credentials:
self.messages['credentials_unable_to_copy'] = missing_credentials
if missing_inventories:
self.messages['inventories_unable_to_copy'] = missing_inventories
return self.check_related('organization', Organization, {'reference_obj': obj}, mandatory=True)

View File

@@ -96,12 +96,12 @@ class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option('--older_than',
dest='older_than',
default=None,
help='Specify the relative time to consider facts older than (w)eek (d)ay or (y)ear (i.e. 5d, 2w, 1y).'),
default='30d',
help='Specify the relative time to consider facts older than (w)eek (d)ay or (y)ear (i.e. 5d, 2w, 1y). Defaults to 30d.'),
make_option('--granularity',
dest='granularity',
default=None,
help='Window duration to group same hosts by for deletion (w)eek (d)ay or (y)ear (i.e. 5d, 2w, 1y).'),
default='1w',
help='Window duration to group same hosts by for deletion (w)eek (d)ay or (y)ear (i.e. 5d, 2w, 1y). Defaults to 1w.'),
make_option('--module',
dest='module',
default=None,

View File

@@ -20,7 +20,7 @@ from django.core.urlresolvers import reverse
# AWX
from awx.main.models.base import * # noqa
from awx.main.models.unified_jobs import * # noqa
from awx.main.models.notifications import JobNotificationMixin
from awx.main.models.notifications import JobNotificationMixin, NotificationTemplate
from awx.main.fields import JSONField
logger = logging.getLogger('awx.main.models.ad_hoc_commands')
@@ -157,18 +157,20 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin):
@property
def notification_templates(self):
all_inventory_sources = set()
all_orgs = set()
for h in self.hosts.all():
for invsrc in h.inventory_sources.all():
all_inventory_sources.add(invsrc)
all_orgs.add(h.inventory.organization)
active_templates = dict(error=set(),
success=set(),
any=set())
for invsrc in all_inventory_sources:
notifications_dict = invsrc.notification_templates
for notification_type in active_templates.keys():
for templ in notifications_dict[notification_type]:
active_templates[notification_type].add(templ)
base_notification_templates = NotificationTemplate.objects
for org in all_orgs:
for templ in base_notification_templates.filter(organization_notification_templates_for_errors=org):
active_templates['error'].add(templ)
for templ in base_notification_templates.filter(organization_notification_templates_for_success=org):
active_templates['success'].add(templ)
for templ in base_notification_templates.filter(organization_notification_templates_for_any=org):
active_templates['any'].add(templ)
active_templates['error'] = list(active_templates['error'])
active_templates['any'] = list(active_templates['any'])
active_templates['success'] = list(active_templates['success'])

View File

@@ -134,7 +134,7 @@ class WorkflowNodeBase(CreatedModifiedModel):
scan_errors = ujt_obj._extra_job_type_errors(accepted_fields)
ignored_dict.update(scan_errors)
for fd in ['inventory', 'credential']:
if getattr(ujt_obj, fd) is None and not (ask_for_vars_dict.get(fd, False) and fd in prompts_dict):
if getattr(ujt_obj, "{}_id".format(fd)) is None and not (ask_for_vars_dict.get(fd, False) and fd in prompts_dict):
missing_dict[fd] = 'Job Template does not have this field and workflow node does not provide it'
data = {}
@@ -421,18 +421,22 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
def can_start_without_user_input(self):
'''Return whether WFJT can be launched without survey passwords.'''
return not bool(self.variables_needed_to_start)
return not bool(
self.variables_needed_to_start or
self.node_templates_missing() or
self.node_prompts_rejected())
def get_warnings(self):
warning_data = {}
for node in self.workflow_job_template_nodes.all():
if node.unified_job_template is None:
warning_data[node.pk] = 'Node is missing a linked unified_job_template'
continue
def node_templates_missing(self):
return [node.pk for node in self.workflow_job_template_nodes.filter(
unified_job_template__isnull=True).all()]
def node_prompts_rejected(self):
node_list = []
for node in self.workflow_job_template_nodes.select_related('unified_job_template').all():
node_prompts_warnings = node.get_prompts_warnings()
if node_prompts_warnings:
warning_data[node.pk] = node_prompts_warnings
return warning_data
node_list.append(node.pk)
return node_list
def user_copy(self, user):
new_wfjt = self.copy_unified_jt()

View File

@@ -114,6 +114,8 @@ class TaskManager():
dag = WorkflowDAG(workflow_job)
spawn_nodes = dag.bfs_nodes_to_run()
for spawn_node in spawn_nodes:
if spawn_node.unified_job_template is None:
continue
kv = spawn_node.get_job_kwargs()
job = spawn_node.unified_job_template.create_unified_job(**kv)
spawn_node.job = job

View File

@@ -67,6 +67,8 @@ class WorkflowDAG(SimpleDAG):
obj = n['node_object']
job = obj.job
if obj.unified_job_template is None:
continue
if not job:
return False
# Job is about to run or is running. Hold our horses and wait for

View File

@@ -120,13 +120,11 @@ class TestWorkflowJobAccess:
access = WorkflowJobTemplateAccess(rando, save_messages=True)
assert not access.can_copy(wfjt)
warnings = access.messages
assert 1 in warnings
assert 'inventory' in warnings[1]
assert 'inventories_unable_to_copy' in warnings
def test_workflow_copy_warnings_jt(self, wfjt, rando, job_template):
wfjt.workflow_job_template_nodes.create(unified_job_template=job_template)
access = WorkflowJobTemplateAccess(rando, save_messages=True)
assert not access.can_copy(wfjt)
warnings = access.messages
assert 1 in warnings
assert 'unified_job_template' in warnings[1]
assert 'templates_unable_to_copy' in warnings

View File

@@ -5,7 +5,7 @@ import pytest
# AWX
from awx.main.scheduler.dag_simple import SimpleDAG
from awx.main.scheduler.dag_workflow import WorkflowDAG
from awx.main.models import Job
from awx.main.models import Job, JobTemplate
from awx.main.models.workflow import WorkflowJobNode
@@ -72,6 +72,7 @@ def factory_node():
if status:
j = Job(status=status)
wfn.job = j
wfn.unified_job_template = JobTemplate(name='JT{}'.format(id))
return wfn
return fn

View File

@@ -73,7 +73,7 @@ DATABASES = {
# timezone as the operating system.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'America/New_York'
TIME_ZONE = None
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
@@ -163,6 +163,12 @@ MAX_EVENT_RES_DATA = 700000
# Note: This setting may be overridden by database settings.
EVENT_STDOUT_MAX_BYTES_DISPLAY = 1024
# Disallow sending session cookies over insecure connections
SESSION_COOKIE_SECURE = True
# Disallow sending csrf cookies over insecure connections
CSRF_COOKIE_SECURE = True
TEMPLATE_CONTEXT_PROCESSORS = ( # NOQA
'django.contrib.auth.context_processors.auth',
'django.core.context_processors.debug',

View File

@@ -24,11 +24,11 @@ ALLOWED_HOSTS = ['*']
mimetypes.add_type("image/svg+xml", ".svg", True)
mimetypes.add_type("image/svg+xml", ".svgz", True)
MONGO_HOST = '127.0.0.1'
MONGO_PORT = 27017
MONGO_USERNAME = None
MONGO_PASSWORD = None
MONGO_DB = 'system_tracking_dev'
# Disallow sending session cookies over insecure connections
SESSION_COOKIE_SECURE = False
# Disallow sending csrf cookies over insecure connections
CSRF_COOKIE_SECURE = False
# Override django.template.loaders.cached.Loader in defaults.py
TEMPLATE_LOADERS = (

View File

@@ -114,7 +114,7 @@ SYSTEM_UUID = '00000000-0000-0000-0000-000000000000'
# timezone as the operating system.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'America/New_York'
TIME_ZONE = None
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html

View File

@@ -71,7 +71,7 @@ SYSTEM_UUID = '00000000-0000-0000-0000-000000000000'
# timezone as the operating system.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'America/New_York'
TIME_ZONE = None
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html

View File

@@ -44,11 +44,10 @@
color: @list-header-txt;
font-size: 14px;
font-weight: bold;
padding-bottom: 25px;
min-height: 45px;
word-break: break-all;
max-width: 90%;
word-wrap: break-word;
margin-bottom: 20px;
}
.Form-secondaryTitle{

View File

@@ -15,7 +15,8 @@ export default ['templateUrl', '$state',
usersDataset: '=',
teamsDataset: '=',
resourceData: '=',
withoutTeamPermissions: '@'
withoutTeamPermissions: '@',
title: '@'
},
controller: controller,
templateUrl: templateUrl('access/add-rbac-resource/rbac-resource'),

View File

@@ -8,7 +8,7 @@
<div class="List-titleText ng-binding">
{{ object.name || object.username }}
<div class="List-titleLockup"></div>
Add Permissions
{{ title }}
</div>
</div>
<div class="Form-exitHolder">

View File

@@ -11,7 +11,8 @@ export default ['templateUrl',
return {
restrict: 'E',
scope: {
resolve: "="
resolve: "=",
title: "@",
},
controller: controller,
templateUrl: templateUrl('access/add-rbac-user-team/rbac-user-team'),

View File

@@ -9,7 +9,7 @@
<div class="List-titleText ng-binding">
{{ owner.name || owner.username }}
<div class="List-titleLockup"></div>
Add Permissions
{{ title }}
</div>
</div>
<div class="Form-exitHolder">

View File

@@ -51,6 +51,10 @@
padding-top: 20px;
}
.AddPermissions-list {
margin-bottom: 20px;
}
.AddPermissions-list .List-searchRow {
height: 0px;
}

View File

@@ -88,6 +88,14 @@ export default ['addPermissionsTeamsList', 'addPermissionsUsersList', 'TemplateL
list.fields.first_name.columnClass = 'col-md-3 col-sm-3 hidden-xs';
list.fields.last_name.columnClass = 'col-md-3 col-sm-3 hidden-xs';
break;
case 'Teams':
list.fields = {
name: list.fields.name,
organization: list.fields.organization,
};
list.fields.name.columnClass = 'col-md-6 col-sm-6 col-xs-11';
list.fields.organization.columnClass = 'col-md-5 col-sm-5 hidden-xs';
break;
default:
list.fields = {
name: list.fields.name,

View File

@@ -16,7 +16,8 @@
reset: 'SOCIAL_AUTH_AZUREAD_OAUTH2_KEY'
},
SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET'
},
SOCIAL_AUTH_AZUREAD_OAUTH2_ORGANIZATION_MAP: {
@@ -38,8 +39,8 @@
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@@ -16,7 +16,8 @@ export default ['i18n', function(i18n) {
reset: 'SOCIAL_AUTH_GITHUB_ORG_KEY'
},
SOCIAL_AUTH_GITHUB_ORG_SECRET: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'SOCIAL_AUTH_GITHUB_ORG_SECRET'
},
SOCIAL_AUTH_GITHUB_ORG_NAME: {
@@ -28,8 +29,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@@ -16,7 +16,8 @@ export default ['i18n', function(i18n) {
reset: 'SOCIAL_AUTH_GITHUB_TEAM_KEY'
},
SOCIAL_AUTH_GITHUB_TEAM_SECRET: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'SOCIAL_AUTH_GITHUB_TEAM_SECRET'
},
SOCIAL_AUTH_GITHUB_TEAM_ID: {
@@ -28,8 +29,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@@ -16,7 +16,8 @@ export default ['i18n', function(i18n) {
reset: 'SOCIAL_AUTH_GITHUB_KEY'
},
SOCIAL_AUTH_GITHUB_SECRET: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'SOCIAL_AUTH_GITHUB_SECRET'
}
},
@@ -24,8 +25,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@@ -16,7 +16,8 @@ export default ['i18n', function(i18n) {
reset: 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY'
},
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET'
},
SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS: {
@@ -36,8 +37,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@@ -21,7 +21,8 @@ export default ['i18n', function(i18n) {
reset: 'AUTH_LDAP_BIND_DN'
},
AUTH_LDAP_BIND_PASSWORD: {
type: 'password'
type: 'sensitive',
hasShowInputButton: true,
},
AUTH_LDAP_USER_SEARCH: {
type: 'textarea',
@@ -84,8 +85,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@@ -21,7 +21,8 @@ export default ['i18n', function(i18n) {
reset: 'RADIUS_PORT'
},
RADIUS_SECRET: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'RADIUS_SECRET'
}
},
@@ -29,8 +30,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@@ -20,7 +20,8 @@ export default ['i18n', function(i18n) {
reset: 'SOCIAL_AUTH_SAML_SP_PUBLIC_CERT'
},
SOCIAL_AUTH_SAML_SP_PRIVATE_KEY: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'SOCIAL_AUTH_SAML_SP_PRIVATE_KEY'
},
SOCIAL_AUTH_SAML_ORG_INFO: {
@@ -56,8 +57,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@@ -12,6 +12,19 @@
float: right
}
.Form-resetAll {
border: none;
padding: 0;
background-color: @white;
margin-right: auto;
color: @default-link;
font-size: 12px;
&:hover {
color: @default-link-hov;
}
}
.Form-tab {
min-width: 77px;
}

View File

@@ -64,8 +64,8 @@
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@@ -22,8 +22,8 @@
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@@ -30,7 +30,8 @@
reset: 'LOG_AGGREGATOR_USERNAME'
},
LOG_AGGREGATOR_PASSWORD: {
type: 'text',
type: 'sensitive',
hasShowInputButton: true,
reset: 'LOG_AGGREGATOR_PASSWORD'
},
LOG_AGGREGATOR_LOGGERS: {
@@ -48,8 +49,8 @@
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@@ -26,8 +26,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@@ -32,8 +32,8 @@ export default ['i18n', function(i18n) {
buttons: {
reset: {
ngClick: 'vm.resetAllConfirm()',
label: i18n._('Reset All'),
class: 'Form-button--left Form-cancelButton'
label: i18n._('Revert all to default'),
class: 'Form-resetAll'
},
cancel: {
ngClick: 'vm.formCancel()',

View File

@@ -283,7 +283,7 @@ CredentialsAdd.$inject = ['$scope', '$rootScope', '$compile', '$location',
export function CredentialsEdit($scope, $rootScope, $compile, $location, $log,
$stateParams, CredentialForm, Rest, Alert, ProcessErrors, ClearScope, Prompt,
GetBasePath, GetChoices, KindChange, Empty, OwnerChange, FormSave, Wait,
$state, CreateSelect2, Authorization) {
$state, CreateSelect2, Authorization, i18n) {
ClearScope();
@@ -336,13 +336,14 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log,
});
}
// if the credential is assigned to an organization, allow permission delegation
// do NOT use $scope.organization in a view directive to determine if a credential is associated with an org
// @todo why not? ^ and what is this type check for a number doing - should this be a type check for undefined?
$scope.disablePermissionAssignment = typeof($scope.organization) === 'number' ? false : true;
if ($scope.disablePermissionAssignment) {
$scope.permissionsTooltip = 'Credentials are only shared within an organization. Assign credentials to an organization to delegate credential permissions. The organization cannot be edited after credentials are assigned.';
}
$scope.$watch('organization', function(val) {
if (val === undefined) {
$scope.permissionsTooltip = i18n._('Credentials are only shared within an organization. Assign credentials to an organization to delegate credential permissions. The organization cannot be edited after credentials are assigned.');
} else {
$scope.permissionsTooltip = '';
}
});
setAskCheckboxes();
KindChange({
scope: $scope,
@@ -613,5 +614,5 @@ CredentialsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location',
'$log', '$stateParams', 'CredentialForm', 'Rest', 'Alert',
'ProcessErrors', 'ClearScope', 'Prompt', 'GetBasePath', 'GetChoices',
'KindChange', 'Empty', 'OwnerChange',
'FormSave', 'Wait', '$state', 'CreateSelect2', 'Authorization'
'FormSave', 'Wait', '$state', 'CreateSelect2', 'Authorization', 'i18n',
];

View File

@@ -420,9 +420,12 @@ export default
related: {
permissions: {
disabled: 'disablePermissionAssignment',
disabled: '(organization === undefined ? true : false)',
// Do not transition the state if organization is undefined
ngClick: `(organization === undefined ? true : false)||$state.go('credentials.edit.permissions')`,
awToolTip: '{{permissionsTooltip}}',
dataTipWatch: 'permissionsTooltip',
awToolTipTabEnabledInEditMode: true,
dataPlacement: 'top',
basePath: 'api/v1/credentials/{{$stateParams.credential_id}}/access_list/',
search: {

View File

@@ -103,7 +103,7 @@ angular.module('InventoryFormDefinition', ['ScanJobsListDefinition'])
add: {
label: i18n._('Add'),
ngClick: "$state.go('.add')",
awToolTip: 'Add a permission',
awToolTip: i18n._('Add a permission'),
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD',
ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)'

View File

@@ -68,7 +68,7 @@ export default
searchType: 'select',
actions: {
add: {
ngClick: "addPermission",
ngClick: "$state.go('.add')",
label: i18n._('Add'),
awToolTip: i18n._('Add a permission'),
actionClass: 'btn List-buttonSubmit',

View File

@@ -121,6 +121,7 @@ export default
organizations: {
awToolTip: i18n._('Please save before assigning to organizations'),
basePath: 'api/v1/users/{{$stateParams.user_id}}/organizations',
emptyListText: i18n._('Please add user to an Organization.'),
search: {
page_size: '10'
},

View File

@@ -122,7 +122,7 @@ export default
add: {
ngClick: "$state.go('.add')",
label: i18n._('Add'),
awToolTip: 'Add a permission',
awToolTip: i18n._('Add a permission'),
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; '+ i18n._('ADD'),
ngShow: '(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)'

View File

@@ -66,7 +66,19 @@ angular.module('inventory', [
],
ParentObject: ['groupData', function(groupData) {
return groupData;
}]
}],
UnifiedJobsOptions: ['Rest', 'GetBasePath', '$stateParams', '$q',
function(Rest, GetBasePath, $stateParams, $q) {
Rest.setUrl(GetBasePath('unified_jobs'));
var val = $q.defer();
Rest.options()
.then(function(data) {
val.resolve(data.data);
}, function(data) {
val.reject(data);
});
return val.promise;
}]
},
views: {
// clear form template when views render in this substate

View File

@@ -120,7 +120,7 @@ export default
Rest.post(job_launch_data)
.success(function(data) {
Wait('stop');
var job = data.job || data.system_job || data.project_update || data.inventory_update || data.ad_hoc_command || data.workflow_job;
var job = data.job || data.system_job || data.project_update || data.inventory_update || data.ad_hoc_command;
if($rootScope.portalMode===false && Empty(data.system_job) || (base === 'home')){
// use $state.go with reload: true option to re-instantiate sockets in
@@ -131,7 +131,8 @@ export default
if(_.has(data, 'job')) {
goToJobDetails('jobDetail');
}
else if(_.has(data, 'workflow_job')) {
else if(data.type && data.type === 'workflow_job') {
job = data.id;
goToJobDetails('workflowResults');
}
else if(_.has(data, 'ad_hoc_command')) {

View File

@@ -7,7 +7,8 @@
export default
angular.module('AllJobsDefinition', ['sanitizeFilter', 'capitalizeFilter'])
.value( 'AllJobsList', {
.factory('AllJobsList', ['i18n', function(i18n) {
return {
name: 'jobs',
basePath: 'unified_jobs',
@@ -16,6 +17,7 @@ export default
index: false,
hover: true,
well: false,
emptyListText: i18n._('No jobs have yet run.'),
title: false,
fields: {
@@ -115,4 +117,5 @@ export default
ngShow: "(job.status !== 'running' && job.status !== 'waiting' && job.status !== 'pending') && job.summary_fields.user_capabilities.delete"
}
}
});
};
}]);

View File

@@ -39,18 +39,18 @@ export default
columnClass: "col-lg-4 col-md-4 col-sm-5 col-xs-7 List-staticColumnAdjacent",
modalColumnClass: 'col-md-8'
},
scm_revision: {
label: i18n._('Revision'),
excludeModal: true,
columnClass: 'col-lg-4 col-md-2 col-sm-3 hidden-xs',
class: 'List-staticColumnAdjacent--monospace'
},
scm_type: {
label: i18n._('Type'),
ngBind: 'project.type_label',
excludeModal: true,
columnClass: 'col-lg-2 col-md-2 col-sm-3 hidden-xs'
},
scm_revision: {
label: i18n._('Revision'),
excludeModal: true,
columnClass: 'col-lg-4 col-md-2 col-sm-3 hidden-xs',
class: 'List-staticColumnAdjacent--monospace'
},
last_updated: {
label: i18n._('Last Updated'),
filter: "longDate",
@@ -61,6 +61,14 @@ export default
},
actions: {
refresh: {
mode: 'all',
awToolTip: i18n._("Refresh the page"),
ngClick: "refresh()",
ngShow: "socketStatus === 'error'",
actionClass: 'btn List-buttonDefault',
buttonContent: i18n._('REFRESH')
},
add: {
mode: 'all', // One of: edit, select, all
ngClick: 'addProject()',
@@ -68,14 +76,6 @@ export default
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ' + i18n._('ADD'),
ngShow: "canAdd"
},
refresh: {
mode: 'all',
awToolTip: i18n._("Refresh the page"),
ngClick: "refresh()",
ngShow: "socketStatus == 'error'",
actionClass: 'btn List-buttonDefault',
buttonContent: i18n._('REFRESH')
}
},

View File

@@ -323,6 +323,7 @@ export default ['i18n', function(i18n) {
headers: {
label: i18n._('HTTP Headers'),
type: 'textarea',
name: 'headers',
rows: 5,
'class': 'Form-formGroup--fullWidth',
awRequiredWhen: {

View File

@@ -127,6 +127,16 @@ export default ['$stateParams', '$scope', '$rootScope', '$location',
});
};
function isDeletedOrganizationBeingEdited(deleted_organization_id, editing_organization_id) {
if (editing_organization_id === undefined) {
return false;
}
if (deleted_organization_id === editing_organization_id) {
return true;
}
return false;
}
$scope.deleteOrganization = function(id, name) {
var action = function() {
@@ -137,7 +147,11 @@ export default ['$stateParams', '$scope', '$rootScope', '$location',
Rest.destroy()
.success(function() {
Wait('stop');
$state.reload('organizations');
if (isDeletedOrganizationBeingEdited(id, parseInt($stateParams.organization_id)) === true) {
$state.go('^', null, { reload: true });
} else {
$state.reload('organizations');
}
})
.error(function(data, status) {
ProcessErrors($scope, data, status, null, {

View File

@@ -285,9 +285,7 @@ export default
}
},
data: {
activityStream: true,
activityStreamTarget: 'job',
activityStreamId: 'id'
activityStream: false,
},
ncyBreadcrumb: {
parent: 'jobs',

View File

@@ -198,6 +198,8 @@ angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter'])
msg += 'Please contact your system administrator.';
}
Alert(defaultMsg.hdr, msg);
} else if (status === 409) {
Alert('Conflict', data.conflict || "Resource currently in use.");
} else if (status === 410) {
Alert('Deleted Object', 'The requested object was previously deleted and can no longer be accessed.');
} else if ((status === 'Token is expired') || (status === 401 && data.detail && data.detail === 'Token is expired') ||

View File

@@ -484,7 +484,7 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper'])
function applyValidation(viewValue) {
basePath = GetBasePath(elm.attr('data-basePath')) || elm.attr('data-basePath');
query = elm.attr('data-query');
query = query.replace(/\:value/, encodeURI(viewValue));
query = query.replace(/\:value/, encodeURIComponent(viewValue));
Rest.setUrl(`${basePath}${query}`);
// https://github.com/ansible/ansible-tower/issues/3549
// capturing both success/failure conditions in .then() promise
@@ -620,12 +620,10 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper'])
if (attrs.tipWatch) {
// Add dataTipWatch: 'variable_name'
scope.$watch(attrs.tipWatch, function(newVal, oldVal) {
if (newVal !== oldVal) {
// Where did fixTitle come from?:
// http://stackoverflow.com/questions/9501921/change-twitter-bootstrap-tooltip-content-on-click
$(element).tooltip('hide').attr('data-original-title', newVal).tooltip('fixTitle');
}
scope.$watch(attrs.tipWatch, function(newVal) {
// Where did fixTitle come from?:
// http://stackoverflow.com/questions/9501921/change-twitter-bootstrap-tooltip-content-on-click
$(element).tooltip('hide').attr('data-original-title', newVal).tooltip('fixTitle');
});
}
}

View File

@@ -1479,7 +1479,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += `<div id="${itm}_tab" `+
`class="Form-tab" `;
html += (this.form.related[itm].ngClick) ? `ng-click="` + this.form.related[itm].ngClick + `" ` : `ng-click="$state.go('${this.form.stateTree}.edit.${itm}')" `;
if (collection.awToolTip){
if (collection.awToolTip && collection.awToolTipTabEnabledInEditMode === true) {
html += `aw-tool-tip="${collection.awToolTip}" ` +
`aw-tip-placement="${collection.dataPlacement}" ` +
`data-tip-watch="${collection.dataTipWatch}" `;
@@ -1830,7 +1830,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
// smart-search directive
html += `
<div
ng-hide="${itm}.length === 0 && (${collection.iterator}_searchTags | isEmpty)">
ng-hide="${itm}.length === 0 && (searchTags | isEmpty)">
<smart-search
django-model="${itm}"
search-size="${width}"
@@ -1855,7 +1855,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += `
<div
class="row"
ng-show="${itm}.length === 0 && !(${collection.iterator}_searchTags | isEmpty)">
ng-show="${itm}.length === 0 && !(searchTags | isEmpty)">
<div class="col-lg-12 List-searchNoResults">
No records matched your search.
</div>
@@ -1865,7 +1865,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
// Show the "no items" box when loading is done and the user isn't actively searching and there are no results
var emptyListText = (collection.emptyListText) ? collection.emptyListText : i18n._("PLEASE ADD ITEMS TO THIS LIST");
html += `<div ng-hide="is_superuser">`;
html += `<div class="List-noItems" ng-show="${itm}.length === 0 && (${collection.iterator}_searchTags | isEmpty)"> ${emptyListText} </div>`;
html += `<div class="List-noItems" ng-show="${itm}.length === 0 && (searchTags | isEmpty)"> ${emptyListText} </div>`;
html += '</div>';
html += `

View File

@@ -18,7 +18,7 @@ export default ['$scope', '$stateParams', '$state', '$filter', 'GetBasePath', 'Q
return;
}
path = GetBasePath($scope.basePath) || $scope.basePath;
queryset = _.merge($stateParams[`${$scope.iterator}_search`], { page: page });
queryset = _.merge($stateParams[`${$scope.iterator}_search`], { page: page.toString() });
$state.go('.', {
[$scope.iterator + '_search']: queryset
});

View File

@@ -2,11 +2,12 @@ import directive from './smart-search.directive';
import controller from './smart-search.controller';
import service from './queryset.service';
import DjangoSearchModel from './django-search-model.class';
import smartSearchService from './smart-search.service';
export default
angular.module('SmartSearchModule', [])
.directive('smartSearch', directive)
.controller('SmartSearchController', controller)
.service('QuerySet', service)
.service('SmartSearchService', smartSearchService)
.constant('DjangoSearchModel', DjangoSearchModel);

View File

@@ -1,5 +1,5 @@
export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSearchModel', '$cacheFactory',
function($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearchModel, $cacheFactory) {
export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSearchModel', '$cacheFactory', 'SmartSearchService',
function($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearchModel, $cacheFactory, SmartSearchService) {
return {
// kick off building a model for a specific endpoint
// this is usually a list's basePath
@@ -67,29 +67,120 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear
return angular.isObject(params) ? `?${queryset}` : '';
function encodeTerm(value, key){
key = key.replace(/__icontains_DEFAULT/g, "__icontains");
key = key.replace(/__search_DEFAULT/g, "__search");
if (Array.isArray(value)){
return _.map(value, (item) => `${key}=${item}`).join('&') + '&';
let concated = '';
angular.forEach(value, function(item){
item = item.replace(/"|'/g, "");
concated += `${key}=${item}&`;
});
return concated;
}
else {
value = value.replace(/"|'/g, "");
return `${key}=${value}&`;
}
}
},
// encodes a ui smart-search param to a django-friendly param
// operand:key:comparator:value => {operand__key__comparator: value}
encodeParam(param){
let split = param.split(':');
return {[split.slice(0,split.length -1).join('__')] : split[split.length-1]};
encodeParam(params){
// Assumption here is that we have a key and a value so the length
// of the paramParts array will be 2. [0] is the key and [1] the value
let paramParts = SmartSearchService.splitTermIntoParts(params.term);
let keySplit = paramParts[0].split('.');
let exclude = false;
let lessThanGreaterThan = paramParts[1].match(/^(>|<).*$/) ? true : false;
if(keySplit[0].match(/^-/g)) {
exclude = true;
keySplit[0] = keySplit[0].replace(/^-/, '');
}
let paramString = exclude ? "not__" : "";
let valueString = paramParts[1];
if(keySplit.length === 1) {
if(params.searchTerm && !lessThanGreaterThan) {
paramString += keySplit[0] + '__icontains_DEFAULT';
}
else if(params.relatedSearchTerm) {
paramString += keySplit[0] + '__search_DEFAULT';
}
else {
paramString += keySplit[0];
}
}
else {
paramString += keySplit.join('__');
}
if(lessThanGreaterThan) {
if(paramParts[1].match(/^>=.*$/)) {
paramString += '__gte';
valueString = valueString.replace(/^(>=)/,"");
}
else if(paramParts[1].match(/^<=.*$/)) {
paramString += '__lte';
valueString = valueString.replace(/^(<=)/,"");
}
else if(paramParts[1].match(/^<.*$/)) {
paramString += '__lt';
valueString = valueString.replace(/^(<)/,"");
}
else if(paramParts[1].match(/^>.*$/)) {
paramString += '__gt';
valueString = valueString.replace(/^(>)/,"");
}
}
return {[paramString] : valueString};
},
// decodes a django queryset param into a ui smart-search tag or set of tags
decodeParam(value, key){
let decodeParamString = function(searchString) {
if(key === 'search') {
// Don't include 'search:' in the search tag
return decodeURIComponent(`${searchString}`);
}
else {
key = key.replace(/__icontains_DEFAULT/g, "");
key = key.replace(/__search_DEFAULT/g, "");
let split = key.split('__');
let decodedParam = searchString;
let exclude = false;
if(key.startsWith('not__')) {
exclude = true;
split = split.splice(1, split.length);
}
if(key.endsWith('__gt')) {
decodedParam = '>' + decodedParam;
split = split.splice(0, split.length-1);
}
else if(key.endsWith('__lt')) {
decodedParam = '<' + decodedParam;
split = split.splice(0, split.length-1);
}
else if(key.endsWith('__gte')) {
decodedParam = '>=' + decodedParam;
split = split.splice(0, split.length-1);
}
else if(key.endsWith('__lte')) {
decodedParam = '<=' + decodedParam;
split = split.splice(0, split.length-1);
}
return exclude ? `-${split.join('.')}:${decodedParam}` : `${split.join('.')}:${decodedParam}`;
}
};
if (Array.isArray(value)){
return _.map(value, (item) => {
return `${key.split('__').join(':')}:${item}`;
return decodeParamString(item);
});
}
else {
return `${key.split('__').join(':')}:${value}`;
return decodeParamString(value);
}
},

View File

@@ -1,5 +1,5 @@
export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', 'QuerySet',
function($stateParams, $scope, $state, QuerySet, GetBasePath, qs) {
export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', 'QuerySet', 'SmartSearchService',
function($stateParams, $scope, $state, QuerySet, GetBasePath, qs, SmartSearchService) {
let path, relations,
// steps through the current tree of $state configurations, grabs default search params
@@ -17,6 +17,7 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', '
$scope.searchTags = stripDefaultParams($state.params[`${$scope.iterator}_search`]);
qs.initFieldset(path, $scope.djangoModel, relations).then((data) => {
$scope.models = data.models;
$scope.options = data.options.data;
$scope.$emit(`${$scope.list.iterator}_options`, data.options);
});
}
@@ -27,7 +28,20 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', '
// setting the default value of a term to null in a state definition is a very explicit way to ensure it will NEVER generate a search tag, even with a non-default value
return defaults[key] !== value && key !== 'order_by' && key !== 'page' && key !== 'page_size' && defaults[key] !== null;
});
return _(stripped).map(qs.decodeParam).flatten().value();
let strippedCopy = _.cloneDeep(stripped);
if(_.keys(_.pick(defaults, _.keys(strippedCopy))).length > 0){
for (var key in strippedCopy) {
if (strippedCopy.hasOwnProperty(key)) {
let value = strippedCopy[key];
if(_.isArray(value)){
let index = _.indexOf(value, defaults[key]);
value = value.splice(index, 1)[0];
}
}
}
stripped = strippedCopy;
}
return _(strippedCopy).map(qs.decodeParam).flatten().value();
}
// searchable relationships
@@ -38,6 +52,16 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', '
return flat;
}
function setDefaults(term) {
if ($scope.list.defaultSearchParams) {
return $scope.list.defaultSearchParams(term);
} else {
return {
search: encodeURIComponent(term)
};
}
}
$scope.toggleKeyPane = function() {
$scope.showKeyPane = !$scope.showKeyPane;
};
@@ -56,10 +80,37 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', '
// remove tag, merge new queryset, $state.go
$scope.remove = function(index) {
let removed = qs.encodeParam($scope.searchTags.splice(index, 1)[0]);
let tagToRemove = $scope.searchTags.splice(index, 1)[0];
let termParts = SmartSearchService.splitTermIntoParts(tagToRemove);
let removed;
if (termParts.length === 1) {
removed = setDefaults(tagToRemove);
}
else {
let root = termParts[0].split(".")[0].replace(/^-/, '');
let encodeParams = {
term: tagToRemove
};
if(_.has($scope.options.actions.GET, root)) {
if($scope.options.actions.GET[root].type && $scope.options.actions.GET[root].type === 'field') {
encodeParams.relatedSearchTerm = true;
}
else {
encodeParams.searchTerm = true;
}
removed = qs.encodeParam(encodeParams);
}
else {
removed = setDefaults(tagToRemove);
}
}
_.each(removed, (value, key) => {
if (Array.isArray(queryset[key])){
_.remove(queryset[key], (item) => item === value);
// If the array is now empty, remove that key
if(queryset[key].length === 0) {
delete queryset[key];
}
}
else {
delete queryset[key];
@@ -79,26 +130,46 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', '
let params = {},
origQueryset = _.clone(queryset);
function setDefaults(term) {
// "name" and "description" are sane defaults for MOST models, but not ALL!
// defaults may be configured in ListDefinition.defaultSearchParams
if ($scope.list.defaultSearchParams) {
return $scope.list.defaultSearchParams(term);
} else {
return {
or__name__icontains: term,
or__description__icontains: term
};
}
}
// Remove leading/trailing whitespace if there is any
terms = terms.trim();
if(terms && terms !== '') {
_.forEach(terms.split(' '), (term) => {
// Split the terms up
let splitTerms = SmartSearchService.splitSearchIntoTerms(terms);
_.forEach(splitTerms, (term) => {
let termParts = SmartSearchService.splitTermIntoParts(term);
function combineSameSearches(a,b){
if (_.isArray(a)) {
return a.concat(b);
}
else {
if(a) {
return [a,b];
}
}
}
// if only a value is provided, search using default keys
if (term.split(':').length === 1) {
params = _.merge(params, setDefaults(term));
if (termParts.length === 1) {
params = _.merge(params, setDefaults(term), combineSameSearches);
} else {
params = _.merge(params, qs.encodeParam(term));
// Figure out if this is a search term
let root = termParts[0].split(".")[0].replace(/^-/, '');
if(_.has($scope.options.actions.GET, root)) {
if($scope.options.actions.GET[root].type && $scope.options.actions.GET[root].type === 'field') {
params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true}), combineSameSearches);
}
else {
params = _.merge(params, qs.encodeParam({term: term, searchTerm: true}), combineSameSearches);
}
}
// Its not a search term or a related search term - treat it as a string
else {
params = _.merge(params, setDefaults(term), combineSameSearches);
}
}
});

View File

@@ -0,0 +1,19 @@
export default [function() {
return {
splitSearchIntoTerms(searchString) {
return searchString.match(/(?:[^\s("')]+|"[^"]*"|'[^']*')+/g);
},
splitTermIntoParts(searchTerm) {
let breakOnColon = searchTerm.match(/(?:[^:"]+|"[^"]*")+/g);
if(breakOnColon.length > 2) {
// concat all the strings after the first one together
let stringsToJoin = breakOnColon.slice(1,breakOnColon.length);
return [breakOnColon[0], stringsToJoin.join(':')];
}
else {
return breakOnColon;
}
}
};
}];

View File

@@ -267,7 +267,7 @@ export default ['$injector', '$stateExtender', '$log', function($injector, $stat
},
views: {
[`modal@${formStateDefinition.name}`]: {
template: `<add-rbac-user-team resolve="$resolve"></add-rbac-user-team>`
template: `<add-rbac-user-team resolve="$resolve" title="Add Permissions"></add-rbac-user-team>`
}
},
resolve: {
@@ -332,7 +332,7 @@ export default ['$injector', '$stateExtender', '$log', function($injector, $stat
},
views: {
[`modal@${formStateDefinition.name}`]: {
template: `<add-rbac-resource users-dataset="$resolve.usersDataset" teams-dataset="$resolve.teamsDataset" selected="allSelected" resource-data="$resolve.resourceData"></add-rbac-resource>`
template: `<add-rbac-resource users-dataset="$resolve.usersDataset" teams-dataset="$resolve.teamsDataset" selected="allSelected" resource-data="$resolve.resourceData" title="Add Users / Teams"></add-rbac-resource>`
}
},
resolve: {
@@ -501,7 +501,7 @@ export default ['$injector', '$stateExtender', '$log', function($injector, $stat
},
views: {
[`modal@${formStateDefinition.name}`]: {
template: `<add-rbac-resource users-dataset="$resolve.usersDataset" selected="allSelected" resource-data="$resolve.resourceData" without-team-permissions="true"></add-rbac-resource>`
template: `<add-rbac-resource users-dataset="$resolve.usersDataset" selected="allSelected" resource-data="$resolve.resourceData" without-team-permissions="true" title="Add Users"></add-rbac-resource>`
}
},
resolve: {

View File

@@ -287,20 +287,18 @@
if (data.related &&
data.related.callback) {
Alert('Callback URL',
`<div>
<p>Host callbacks are enabled for this template. The callback URL is:</p>
<p style=\"padding: 10px 0;\">
<strong>
${$scope.callback_server_path}
${data.related.callback}
</string>
</p>
<p>The host configuration key is:
<strong>
${$filter('sanitize')(data.host_config_key)}
</string>
</p>
</div>`,
`Host callbacks are enabled for this template. The callback URL is:
<p style=\"padding: 10px 0;\">
<strong>
${$scope.callback_server_path}
${data.related.callback}
</strong>
</p>
<p class="break">The host configuration key is:
<strong>
${$filter('sanitize')(data.host_config_key)}
</strong>
</p>`,
'alert-danger', saveCompleted, null, null,
null, true);
}

View File

@@ -423,23 +423,20 @@ export default
if (data.related &&
data.related.callback) {
Alert('Callback URL',
`
<div>
<p>Host callbacks are enabled for this template. The callback URL is:</p>
<p style=\"padding: 10px 0;\">
<strong>
${$scope.callback_server_path}
${data.related.callback}
</string>
</p>
<p>The host configuration key is:
<strong>
${$filter('sanitize')(data.host_config_key)}
</string>
</p>
</div>
`Host callbacks are enabled for this template. The callback URL is:
<p style=\"padding: 10px 0;\">
<strong>
${$scope.callback_server_path}
${data.related.callback}
</strong>
</p>
<p class="break">The host configuration key is:
<strong>
${$filter('sanitize')(data.host_config_key)}
</strong>
</p>
`,
'alert-info', saveCompleted, null, null,
'alert-danger', saveCompleted, null, null,
null, true);
}
var orgDefer = $q.defer();

View File

@@ -131,7 +131,7 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest',
handleSuccessfulDelete();
}, function (data) {
Wait('stop');
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
ProcessErrors($scope, data, data.status, null, { hdr: 'Error!',
msg: 'Call to delete workflow job template failed. DELETE returned status: ' + status });
});
}
@@ -141,8 +141,8 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest',
handleSuccessfulDelete();
}, function (data) {
Wait('stop');
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Call to delete job template failed. DELETE returned status: ' + status });
ProcessErrors($scope, data, data.status, null, { hdr: 'Error!',
msg: 'Call to delete job template failed. DELETE returned status: ' + data.status });
});
}
else {
@@ -220,7 +220,7 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest',
.then(function(result) {
if(result.data.can_copy) {
if(!result.data.warnings || _.isEmpty(result.data.warnings)) {
if(result.data.can_copy_without_user_input) {
// Go ahead and copy the workflow - the user has full priveleges on all the resources
TemplateCopyService.copyWorkflow(template.id)
.then(function(result) {
@@ -235,18 +235,40 @@ export default ['$scope', '$rootScope', '$location', '$stateParams', 'Rest',
let bodyHtml = `
<div class="Prompt-bodyQuery">
You may not have access to all resources used by this workflow. Resources that you don\'t have access to will not be copied and may result in an incomplete workflow.
You do not have access to all resources used by this workflow. Resources that you don\'t have access to will not be copied and will result in an incomplete workflow.
</div>
<div class="Prompt-bodyTarget">`;
// Go and grab all of the warning strings
_.forOwn(result.data.warnings, function(warning) {
if(warning) {
_.forOwn(warning, function(warningString) {
bodyHtml += '<div>' + warningString + '</div>';
});
}
} );
// List the unified job templates user can not access
if (result.data.templates_unable_to_copy.length > 0) {
bodyHtml += '<div>Unified Job Templates that can not be copied<ul>';
_.forOwn(result.data.templates_unable_to_copy, function(ujt) {
if(ujt) {
bodyHtml += '<li>' + ujt + '</li>';
}
});
bodyHtml += '</ul></div>';
}
// List the prompted inventories user can not access
if (result.data.inventories_unable_to_copy.length > 0) {
bodyHtml += '<div>Node prompted inventories that can not be copied<ul>';
_.forOwn(result.data.inventories_unable_to_copy, function(inv) {
if(inv) {
bodyHtml += '<li>' + inv + '</li>';
}
});
bodyHtml += '</ul></div>';
}
// List the prompted credentials user can not access
if (result.data.credentials_unable_to_copy.length > 0) {
bodyHtml += '<div>Node prompted credentials that can not be copied<ul>';
_.forOwn(result.data.credentials_unable_to_copy, function(cred) {
if(cred) {
bodyHtml += '<li>' + cred + '</li>';
}
});
bodyHtml += '</ul></div>';
}
bodyHtml += '</div>';

View File

@@ -174,7 +174,20 @@ export default [ '$state','moment',
nodeEnter.each(function(d) {
let thisNode = d3.select(this);
if(d.isStartNode) {
if(d.isStartNode && scope.mode === 'details') {
// Overwrite the default root height and width and replace it with a small blue square
rootW = 25;
rootH = 25;
thisNode.append("rect")
.attr("width", rootW)
.attr("height", rootH)
.attr("y", 10)
.attr("rx", 5)
.attr("ry", 5)
.attr("fill", "#337ab7")
.attr("class", "WorkflowChart-rootNode");
}
else if(d.isStartNode && scope.mode !== 'details') {
thisNode.append("rect")
.attr("width", rootW)
.attr("height", rootH)
@@ -190,7 +203,6 @@ export default [ '$state','moment',
.attr("dy", ".35em")
.attr("class", "WorkflowChart-startText")
.text(function () { return "START"; })
.attr("display", function() { return scope.mode === 'details' ? 'none' : null;})
.call(add_node);
}
else {
@@ -200,15 +212,15 @@ export default [ '$state','moment',
.attr("rx", 5)
.attr("ry", 5)
.attr('stroke', function(d) {
if(d.edgeType) {
if(d.edgeType === "failure") {
return "#d9534f";
}
else if(d.edgeType === "success") {
if(d.job && d.job.status) {
if(d.job.status === "successful"){
return "#5cb85c";
}
else if(d.edgeType === "always"){
return "#337ab7";
else if (d.job.status === "failed" || d.job.status === "error" || d.job.status === "cancelled") {
return "#d9534f";
}
else {
return "#D7D7D7";
}
}
else {
@@ -593,15 +605,15 @@ export default [ '$state','moment',
t.selectAll(".rect")
.attr('stroke', function(d) {
if(d.edgeType) {
if(d.edgeType === "failure") {
return "#d9534f";
}
else if(d.edgeType === "success") {
if(d.job && d.job.status) {
if(d.job.status === "successful"){
return "#5cb85c";
}
else if(d.edgeType === "always"){
return "#337ab7";
else if (d.job.status === "failed" || d.job.status === "error" || d.job.status === "cancelled") {
return "#d9534f";
}
else {
return "#D7D7D7";
}
}
else {

View File

@@ -126,7 +126,7 @@ export default ['$q', 'Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErr
},
relaunchJob: function(scope) {
InitiatePlaybookRun({ scope: scope, id: scope.workflow.id,
relaunch: true, job_type: 'workflow_job_template' });
relaunch: true, job_type: 'workflow_job' });
}
};
return val;

View File

@@ -81,7 +81,7 @@
<div id="alert-modal-msg" class="alert" ng-bind-html="alertBody"></div>
</div>
<div class="modal-footer">
<a href="#" ng-hide="disableButtons" data-target="#form-modal" data-dismiss="modal" id="alert_ok_btn" class="btn btn-primary">{% trans 'OK' %}</a>
<a href="#" ng-hide="disableButtons" data-target="#form-modal" data-dismiss="modal" id="alert_ok_btn" class="btn btn-default">{% trans 'OK' %}</a>
</div>
</div>
<!-- modal-content -->

View File

@@ -3,7 +3,8 @@
describe('Service: QuerySet', () => {
let $httpBackend,
QuerySet,
Authorization;
Authorization,
SmartSearchService;
beforeEach(angular.mock.module('Tower', ($provide) =>{
// @todo: improve app source / write testing utilities for interim
@@ -17,9 +18,10 @@ describe('Service: QuerySet', () => {
}));
beforeEach(angular.mock.module('RestServices'));
beforeEach(angular.mock.inject((_$httpBackend_, _QuerySet_) => {
beforeEach(angular.mock.inject((_$httpBackend_, _QuerySet_, _SmartSearchService_) => {
$httpBackend = _$httpBackend_;
QuerySet = _QuerySet_;
SmartSearchService = _SmartSearchService_;
// @todo: improve app source
// config.js / local_settings emit $http requests in the app's run block
@@ -33,24 +35,27 @@ describe('Service: QuerySet', () => {
.respond(200, '');
}));
describe('fn encodeQuery', () => {
xit('null/undefined params should return an empty string', () => {
expect(QuerySet.encodeQuery(null)).toEqual('');
expect(QuerySet.encodeQuery(undefined)).toEqual('');
describe('fn encodeParam', () => {
it('should encode parameters properly', () =>{
expect(QuerySet.encodeParam({term: "name:foo", searchTerm: true})).toEqual({"name__icontains_DEFAULT" : "foo"});
expect(QuerySet.encodeParam({term: "-name:foo", searchTerm: true})).toEqual({"not__name__icontains_DEFAULT" : "foo"});
expect(QuerySet.encodeParam({term: "name:'foo bar'", searchTerm: true})).toEqual({"name__icontains_DEFAULT" : "'foo bar'"});
expect(QuerySet.encodeParam({term: "-name:'foo bar'", searchTerm: true})).toEqual({"not__name__icontains_DEFAULT" : "'foo bar'"});
expect(QuerySet.encodeParam({term: "organization:foo", relatedSearchTerm: true})).toEqual({"organization__search_DEFAULT" : "foo"});
expect(QuerySet.encodeParam({term: "-organization:foo", relatedSearchTerm: true})).toEqual({"not__organization__search_DEFAULT" : "foo"});
expect(QuerySet.encodeParam({term: "organization.name:foo", relatedSearchTerm: true})).toEqual({"organization__name" : "foo"});
expect(QuerySet.encodeParam({term: "-organization.name:foo", relatedSearchTerm: true})).toEqual({"not__organization__name" : "foo"});
expect(QuerySet.encodeParam({term: "id:11", searchTerm: true})).toEqual({"id__icontains_DEFAULT" : "11"});
expect(QuerySet.encodeParam({term: "-id:11", searchTerm: true})).toEqual({"not__id__icontains_DEFAULT" : "11"});
expect(QuerySet.encodeParam({term: "id:>11", searchTerm: true})).toEqual({"id__gt" : "11"});
expect(QuerySet.encodeParam({term: "-id:>11", searchTerm: true})).toEqual({"not__id__gt" : "11"});
expect(QuerySet.encodeParam({term: "id:>=11", searchTerm: true})).toEqual({"id__gte" : "11"});
expect(QuerySet.encodeParam({term: "-id:>=11", searchTerm: true})).toEqual({"not__id__gte" : "11"});
expect(QuerySet.encodeParam({term: "id:<11", searchTerm: true})).toEqual({"id__lt" : "11"});
expect(QuerySet.encodeParam({term: "-id:<11", searchTerm: true})).toEqual({"not__id__lt" : "11"});
expect(QuerySet.encodeParam({term: "id:<=11", searchTerm: true})).toEqual({"id__lte" : "11"});
expect(QuerySet.encodeParam({term: "-id:<=11", searchTerm: true})).toEqual({"not__id__lte" : "11"});
});
xit('should encode params to a string', () => {
let params = {
or__created_by: 'Jenkins',
or__modified_by: 'Jenkins',
and__not__status: 'success',
},
result = '?or__created_by=Jenkins&or__modified_by=Jenkins&and__not__status=success';
expect(QuerySet.encodeQuery(params)).toEqual(result);
});
});
xdescribe('fn decodeQuery', () => {
});

View File

@@ -0,0 +1,43 @@
'use strict';
describe('Service: SmartSearch', () => {
let SmartSearchService;
beforeEach(angular.mock.module('Tower'));
beforeEach(angular.mock.module('SmartSearchModule'));
beforeEach(angular.mock.inject((_SmartSearchService_) => {
SmartSearchService = _SmartSearchService_;
}));
describe('fn splitSearchIntoTerms', () => {
it('should convert the search string to an array tag strings', () =>{
expect(SmartSearchService.splitSearchIntoTerms('foo')).toEqual(["foo"]);
expect(SmartSearchService.splitSearchIntoTerms('foo bar')).toEqual(["foo", "bar"]);
expect(SmartSearchService.splitSearchIntoTerms('name:foo bar')).toEqual(["name:foo", "bar"]);
expect(SmartSearchService.splitSearchIntoTerms('name:foo description:bar')).toEqual(["name:foo", "description:bar"]);
expect(SmartSearchService.splitSearchIntoTerms('name:"foo bar"')).toEqual(["name:\"foo bar\""]);
expect(SmartSearchService.splitSearchIntoTerms('name:"foo bar" description:"bar foo"')).toEqual(["name:\"foo bar\"", "description:\"bar foo\""]);
expect(SmartSearchService.splitSearchIntoTerms('name:"foo bar" description:"bar foo"')).toEqual(["name:\"foo bar\"", "description:\"bar foo\""]);
expect(SmartSearchService.splitSearchIntoTerms('name:\'foo bar\'')).toEqual(["name:\'foo bar\'"]);
expect(SmartSearchService.splitSearchIntoTerms('name:\'foo bar\' description:\'bar foo\'')).toEqual(["name:\'foo bar\'", "description:\'bar foo\'"]);
expect(SmartSearchService.splitSearchIntoTerms('name:\'foo bar\' description:\'bar foo\'')).toEqual(["name:\'foo bar\'", "description:\'bar foo\'"]);
expect(SmartSearchService.splitSearchIntoTerms('name:\"foo bar\" description:\'bar foo\'')).toEqual(["name:\"foo bar\"", "description:\'bar foo\'"]);
expect(SmartSearchService.splitSearchIntoTerms('name:\"foo bar\" foo')).toEqual(["name:\"foo bar\"", "foo"]);
});
});
describe('fn splitTermIntoParts', () => {
it('should convert the search term to a key and value', () =>{
expect(SmartSearchService.splitTermIntoParts('foo')).toEqual(["foo"]);
expect(SmartSearchService.splitTermIntoParts('foo:bar')).toEqual(["foo", "bar"]);
expect(SmartSearchService.splitTermIntoParts('foo:bar:foobar')).toEqual(["foo", "bar:foobar"]);
expect(SmartSearchService.splitTermIntoParts('name:\"foo bar\"')).toEqual(["name", "\"foo bar\""]);
expect(SmartSearchService.splitTermIntoParts('name:\"foo:bar\"')).toEqual(["name", "\"foo:bar\""]);
expect(SmartSearchService.splitTermIntoParts('name:\'foo bar\'')).toEqual(["name", "\'foo bar\'"]);
expect(SmartSearchService.splitTermIntoParts('name:\'foo:bar\'')).toEqual(["name", "\'foo:bar\'"]);
});
});
});