mirror of
https://github.com/ansible/awx.git
synced 2026-01-14 03:10:42 -03:30
Merge branch 'release_3.1.0' into jobResultsPerf
This commit is contained in:
commit
7c8c42bfc1
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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):
|
||||
|
||||
3637
awx/locale/en-us/LC_MESSAGES/django.po
Normal file
3637
awx/locale/en-us/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
2963
awx/locale/fr/LC_MESSAGES/ansible-tower-ui.po
Normal file
2963
awx/locale/fr/LC_MESSAGES/ansible-tower-ui.po
Normal file
File diff suppressed because it is too large
Load Diff
4303
awx/locale/fr/LC_MESSAGES/django.po
Normal file
4303
awx/locale/fr/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
2803
awx/locale/ja/LC_MESSAGES/ansible-tower-ui.po
Normal file
2803
awx/locale/ja/LC_MESSAGES/ansible-tower-ui.po
Normal file
File diff suppressed because it is too large
Load Diff
3984
awx/locale/ja/LC_MESSAGES/django.po
Normal file
3984
awx/locale/ja/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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'])
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 = (
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -15,7 +15,8 @@ export default ['templateUrl', '$state',
|
||||
usersDataset: '=',
|
||||
teamsDataset: '=',
|
||||
resourceData: '=',
|
||||
withoutTeamPermissions: '@'
|
||||
withoutTeamPermissions: '@',
|
||||
title: '@'
|
||||
},
|
||||
controller: controller,
|
||||
templateUrl: templateUrl('access/add-rbac-resource/rbac-resource'),
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -51,6 +51,10 @@
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.AddPermissions-list {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.AddPermissions-list .List-searchRow {
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()',
|
||||
|
||||
@ -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()',
|
||||
|
||||
@ -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()',
|
||||
|
||||
@ -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()',
|
||||
|
||||
@ -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()',
|
||||
|
||||
@ -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()',
|
||||
|
||||
@ -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()',
|
||||
|
||||
@ -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()',
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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()',
|
||||
|
||||
@ -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()',
|
||||
|
||||
@ -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()',
|
||||
|
||||
@ -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()',
|
||||
|
||||
@ -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()',
|
||||
|
||||
@ -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',
|
||||
];
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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: '+ ADD',
|
||||
ngShow: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd)'
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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'
|
||||
},
|
||||
|
||||
@ -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: '+ '+ i18n._('ADD'),
|
||||
ngShow: '(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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')) {
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}]);
|
||||
|
||||
@ -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: '+ ' + i18n._('ADD'),
|
||||
ngShow: "canAdd"
|
||||
},
|
||||
refresh: {
|
||||
mode: 'all',
|
||||
awToolTip: i18n._("Refresh the page"),
|
||||
ngClick: "refresh()",
|
||||
ngShow: "socketStatus == 'error'",
|
||||
actionClass: 'btn List-buttonDefault',
|
||||
buttonContent: i18n._('REFRESH')
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -285,9 +285,7 @@ export default
|
||||
}
|
||||
},
|
||||
data: {
|
||||
activityStream: true,
|
||||
activityStreamTarget: 'job',
|
||||
activityStreamId: 'id'
|
||||
activityStream: false,
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
parent: 'jobs',
|
||||
|
||||
@ -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') ||
|
||||
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 += `
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}];
|
||||
@ -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: {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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>';
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
43
awx/ui/tests/spec/smart-search/smart-search.service-test.js
Normal file
43
awx/ui/tests/spec/smart-search/smart-search.service-test.js
Normal 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\'"]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
BIN
docs/licenses/ntlm-auth-1.0.2.zip
Normal file
BIN
docs/licenses/ntlm-auth-1.0.2.zip
Normal file
Binary file not shown.
163
docs/licenses/ntlm-auth.txt
Normal file
163
docs/licenses/ntlm-auth.txt
Normal file
@ -0,0 +1,163 @@
|
||||
GNU Lesser General Public License
|
||||
=================================
|
||||
|
||||
_Version 3, 29 June 2007_
|
||||
_Copyright © 2007 Free Software Foundation, Inc. <<http://fsf.org/>>_
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
### 0. Additional Definitions
|
||||
|
||||
As used herein, “this License” refers to version 3 of the GNU Lesser
|
||||
General Public License, and the “GNU GPL” refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
“The Library” refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An “Application” is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A “Combined Work” is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the “Linked
|
||||
Version”.
|
||||
|
||||
The “Minimal Corresponding Source” for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The “Corresponding Application Code” for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
### 1. Exception to Section 3 of the GNU GPL
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
### 2. Conveying Modified Versions
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
* **a)** under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
* **b)** under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
### 3. Object Code Incorporating Material from Library Header Files
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
* **a)** Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
* **b)** Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
### 4. Combined Works
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
* **a)** Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
* **b)** Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
* **c)** For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
* **d)** Do one of the following:
|
||||
- **0)** Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
- **1)** Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that **(a)** uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and **(b)** will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
* **e)** Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option **4d0**, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option **4d1**, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
### 5. Combined Libraries
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
* **a)** Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
* **b)** Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
### 6. Revised Versions of the GNU Lesser General Public License
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License “or any later version”
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
202
docs/licenses/pykerberos.txt
Normal file
202
docs/licenses/pykerberos.txt
Normal file
@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@ -1,9 +1,19 @@
|
||||
MIT License
|
||||
Copyright (c) 2013 Alexey Diyan
|
||||
|
||||
Copyright (c) <year> <copyright holders>
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
15
docs/licenses/requests-kerberos.txt
Normal file
15
docs/licenses/requests-kerberos.txt
Normal file
@ -0,0 +1,15 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2012 Kenneth Reitz
|
||||
|
||||
Permission to use, copy, modify and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS-IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
15
docs/licenses/requests-ntlm.txt
Normal file
15
docs/licenses/requests-ntlm.txt
Normal file
@ -0,0 +1,15 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2013 Ben Toews
|
||||
|
||||
Permission to use, copy, modify and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS-IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
@ -32,7 +32,7 @@ As stated, workflow job templates can be created with populated `extra_vars`. Th
|
||||
|
||||
Job resources spawned by workflow jobs are needed by workflow to run correctly. Therefore deletion of spawned job resources is blocked while the underlying workflow job is executing.
|
||||
|
||||
Other than success and failure, a workflow spawned job resource can also end with status 'error' and 'canceled'. When a workflow spawned job resource errors, all branches starting from that job will stop executing while the rest keep their own paces. Canceling a workflow spawned job resource follows the same rules.
|
||||
Other than success and failure, a workflow spawned job resource can also end with status 'error' and 'canceled'. When a workflow spawned job resource errors, all branches starting from that job will stop executing while the rest continue executing. Canceling a workflow spawned job resource follows the same rules. If the unified job template of the node is null (which could be a result of deleting the unified job template or copying a workflow when the user lacks necessary permissions to use the resource), then the branch should stop executing in this case as well.
|
||||
|
||||
A workflow job itself can also be canceled. In this case all its spawned job resources will be canceled if cancelable and following paths stop executing.
|
||||
|
||||
@ -47,11 +47,11 @@ Workflow job summary:
|
||||
```
|
||||
|
||||
### Workflow Copy and Relaunch
|
||||
Other than the normal way of creating workflow job templates, it is also possible to copy existing workflow job templates. The resulting new workflow job template will be identical to which it copies from, except for `name` field which will be appended a text to indicate it's a copy.
|
||||
Other than the normal way of creating workflow job templates, it is also possible to copy existing workflow job templates. The resulting new workflow job template will be mostly identical to the original, except for `name` field which will be appended a text to indicate it's a copy.
|
||||
|
||||
Workflow job templates can be copied by POSTing to endpoint `/workflow_job_templates/\d+/copy/`. After copy finished, the resulting new workflow job template will have identical fields including description, extra_vars, labels, 'launch_type' and survey-related fields (survey_passwords, survey_spec and survey_enabled). More importantly, workflow job template node of the original workflow job template, as well as the topology they bear, will be copied. Note there are RBAC restrictions on determining which workflow job template node is copied. In specific, a workflow job template is allowed to be copied if the user has at least read permission on all related resources like credential and job template. On the other hand, schedules and notification templates of the original workflow job template will not be copied nor shared, and the name of the created workflow job template is the original name plus a special-formatted suffix to indicate its copy origin as well as the copy time, such as 'copy_from_name@10:30:00 am'.
|
||||
Workflow job templates can be copied by POSTing to endpoint `/workflow_job_templates/\d+/copy/`. After copy finished, the resulting new workflow job template will have identical fields including description, extra_vars, and survey-related fields (survey_spec and survey_enabled). More importantly, workflow job template node of the original workflow job template, as well as the topology they bear, will be copied. Note there are RBAC restrictions on copying workflow job template nodes. A workflow job template is allowed to be copied if the user has permission to add an equivalent workflow job template. If the user performing the copy does not have access to a node's related resources (job template, inventory, or credential), those related fields will be null in the copy's version of the node. Schedules and notification templates of the original workflow job template will not be copied nor shared, and the name of the created workflow job template is the original name plus a special-formatted suffix to indicate its copy origin as well as the copy time, such as 'copy_from_name@10:30:00 am'.
|
||||
|
||||
Worflow jobs cannot be copied directly, instead a workflow job is implicitly copied when it needs to relaunch. Relaunching an existing workflow job is done by POSTing to endpoint `/workflow_jobs/\d+/relaunch/`. What happens next is the original workflow job is copied to create a new workflow job. The new workflow job then gets a copy of all nodes of the original as well as the topology they bear. Finally the full-fledged new workflow job is triggered to run, thus fulfilling the purpose of relaunch.
|
||||
Workflow jobs cannot be copied directly, instead a workflow job is implicitly copied when it needs to relaunch. Relaunching an existing workflow job is done by POSTing to endpoint `/workflow_jobs/\d+/relaunch/`. What happens next is the original workflow job is copied to create a new workflow job. The new workflow job then gets a copy of all nodes of the original as well as the topology they bear. Finally the full-fledged new workflow job is triggered to run, thus fulfilling the purpose of relaunch. Survey password-type answers should also be redacted in the relaunched version of the workflow job.
|
||||
|
||||
## Test Coverage
|
||||
### CRUD-related
|
||||
|
||||
@ -7,5 +7,6 @@ boto==2.45.0
|
||||
psphere==0.5.2
|
||||
psutil==5.0.0
|
||||
pyvmomi==6.5
|
||||
pywinrm[kerberos]==0.2.2
|
||||
secretstorage==2.3.1
|
||||
shade==1.13.1
|
||||
|
||||
@ -64,8 +64,10 @@ msrestazure==0.4.6 # via azure-common
|
||||
munch==2.0.4 # via shade
|
||||
netaddr==0.7.18 # via oslo.config, oslo.utils, python-neutronclient
|
||||
netifaces==0.10.5 # via oslo.utils, shade
|
||||
ntlm-auth==1.0.2 # via requests-ntlm
|
||||
oauthlib==2.0.1 # via requests-oauthlib
|
||||
openstacksdk==0.9.11 # via python-openstackclient
|
||||
ordereddict==1.1 # via ntlm-auth
|
||||
os-client-config==1.24.0 # via openstacksdk, osc-lib, python-magnumclient, python-neutronclient, shade
|
||||
os-diskconfig-python-novaclient-ext==0.1.3 # via rackspace-novaclient
|
||||
os-networksv2-python-novaclient-ext==0.26 # via rackspace-novaclient
|
||||
@ -83,6 +85,7 @@ psutil==5.0.0
|
||||
pyasn1==0.1.9 # via cryptography
|
||||
pycparser==2.17 # via cffi
|
||||
PyJWT==1.4.2 # via adal
|
||||
pykerberos==1.1.13 # via requests-kerberos
|
||||
pyparsing==2.1.10 # via cliff, cmd2, oslo.utils
|
||||
python-cinderclient==1.9.0 # via python-openstackclient, shade
|
||||
python-dateutil==2.6.0 # via adal, azure-storage
|
||||
@ -100,24 +103,28 @@ python-swiftclient==3.2.0 # via python-heatclient, python-troveclient, shade
|
||||
python-troveclient==2.7.0 # via shade
|
||||
pytz==2016.10 # via babel, oslo.serialization, oslo.utils
|
||||
pyvmomi==6.5
|
||||
pywinrm[kerberos]==0.2.2
|
||||
PyYAML==3.12 # via cliff, os-client-config, psphere, python-heatclient, python-ironicclient, python-mistralclient
|
||||
rackspace-auth-openstack==1.3 # via rackspace-novaclient
|
||||
rackspace-novaclient==2.1
|
||||
rax-default-network-flags-python-novaclient-ext==0.4.0 # via rackspace-novaclient
|
||||
rax-scheduled-images-python-novaclient-ext==0.3.1 # via rackspace-novaclient
|
||||
requests-kerberos==0.11.0 # via pywinrm
|
||||
requests-ntlm==1.0.0 # via pywinrm
|
||||
requests-oauthlib==0.7.0 # via msrest
|
||||
requests==2.11.1 # via adal, azure-servicebus, azure-servicemanagement-legacy, azure-storage, keystoneauth1, msrest, python-cinderclient, python-designateclient, python-glanceclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-mistralclient, python-neutronclient, python-novaclient, python-swiftclient, python-troveclient, pyvmomi, requests-oauthlib
|
||||
requests==2.11.1 # via adal, azure-servicebus, azure-servicemanagement-legacy, azure-storage, keystoneauth1, msrest, python-cinderclient, python-designateclient, python-glanceclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-mistralclient, python-neutronclient, python-novaclient, python-swiftclient, python-troveclient, pyvmomi, pywinrm, requests-kerberos, requests-ntlm, requests-oauthlib
|
||||
requestsexceptions==1.1.3 # via os-client-config, shade
|
||||
rfc3986==0.4.1 # via oslo.config
|
||||
secretstorage==2.3.1
|
||||
shade==1.13.1
|
||||
simplejson==3.10.0 # via osc-lib, python-cinderclient, python-neutronclient, python-novaclient, python-troveclient
|
||||
six==1.10.0 # via cliff, cryptography, debtcollector, keystoneauth1, mock, openstacksdk, osc-lib, oslo.config, oslo.i18n, oslo.serialization, oslo.utils, python-cinderclient, python-dateutil, python-designateclient, python-glanceclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-mistralclient, python-neutronclient, python-novaclient, python-openstackclient, python-swiftclient, python-troveclient, pyvmomi, shade, stevedore, warlock
|
||||
six==1.10.0 # via cliff, cryptography, debtcollector, keystoneauth1, mock, ntlm-auth, openstacksdk, osc-lib, oslo.config, oslo.i18n, oslo.serialization, oslo.utils, python-cinderclient, python-dateutil, python-designateclient, python-glanceclient, python-heatclient, python-ironicclient, python-keystoneclient, python-magnumclient, python-mistralclient, python-neutronclient, python-novaclient, python-openstackclient, python-swiftclient, python-troveclient, pyvmomi, pywinrm, shade, stevedore, warlock
|
||||
stevedore==1.19.1 # via cliff, keystoneauth1, openstacksdk, osc-lib, oslo.config, python-designateclient, python-keystoneclient, python-magnumclient
|
||||
suds==0.4 # via psphere
|
||||
unicodecsv==0.14.1 # via cliff
|
||||
warlock==1.2.0 # via python-glanceclient
|
||||
wrapt==1.10.8 # via debtcollector, positional
|
||||
xmltodict==0.10.2 # via pywinrm
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# setuptools # via cryptography
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user