From 54699b17ec45e14f5ae6604f15c943ae79d7d5a6 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 3 Mar 2016 08:19:45 -0500 Subject: [PATCH 01/63] Made list headers, buttons and search bars responsive down to mobile views. --- awx/ui/client/legacy-styles/ansible-ui.less | 17 ++------ awx/ui/client/legacy-styles/lists.less | 40 +++++++++++++++++-- awx/ui/client/src/controllers/Inventories.js | 2 +- awx/ui/client/src/helpers/Hosts.js | 2 +- .../list-generator/list-actions.partial.html | 7 ++-- .../list-generator/list-generator.factory.js | 2 + awx/ui/client/src/widgets/Stream.js | 2 +- 7 files changed, 48 insertions(+), 24 deletions(-) diff --git a/awx/ui/client/legacy-styles/ansible-ui.less b/awx/ui/client/legacy-styles/ansible-ui.less index 6df1c3bada..4adaeb09eb 100644 --- a/awx/ui/client/legacy-styles/ansible-ui.less +++ b/awx/ui/client/legacy-styles/ansible-ui.less @@ -919,15 +919,11 @@ input[type="checkbox"].checkbox-no-label { /* Display list actions next to search widget */ .list-actions { - text-align: right; + text-align: right; - button { - margin-left: 4px; - } - - .fa-lg { - vertical-align: -8%; - } + .fa-lg { + vertical-align: -8%; + } } .jqui-accordion { @@ -1952,11 +1948,6 @@ tr td button i { } } -button.dropdown-toggle, -.input-group-btn { - z-index: 1; -} - #login-modal-body { padding-bottom: 5px; } diff --git a/awx/ui/client/legacy-styles/lists.less b/awx/ui/client/legacy-styles/lists.less index e52ca73ec0..ba6adba673 100644 --- a/awx/ui/client/legacy-styles/lists.less +++ b/awx/ui/client/legacy-styles/lists.less @@ -143,7 +143,6 @@ table, tbody { .List-header { display: flex; - height: 34px; align-items: center; } @@ -151,7 +150,7 @@ table, tbody { align-items: center; flex: 1 0 auto; display: flex; - margin-top: -2px; + height: 34px; } .List-titleBadge { @@ -172,15 +171,22 @@ table, tbody { text-transform: uppercase; } -.List-actions { +.List-actionHolder { justify-content: flex-end; display: flex; + height: 34px; +} + +.List-actions { margin-top: -10px; +} + +.List-auxAction + .List-actions { margin-left: 10px; } .List-auxAction { - justify-content: flex-end; + align-items: center; display: flex; } @@ -188,6 +194,10 @@ table, tbody { width: 175px; } +.List-action:not(.ng-hide) ~ .List-action:not(.ng-hide) { + margin-left: 10px; +} + .List-buttonSubmit { background-color: @submit-button-bg; color: @submit-button-text; @@ -352,3 +362,25 @@ table, tbody { display: block; font-size: 13px; } + +@media (max-width: 991px) { + .List-searchWidget + .List-searchWidget { + margin-top: 20px; + } +} + +@media (max-width: 600px) { + .List-header { + flex-direction: column; + align-items: stretch; + } + .List-actionHolder { + justify-content: flex-start; + align-items: center; + flex: 1 0 auto; + margin-top: 12px; + } + .List-well { + margin-top: 20px; + } +} diff --git a/awx/ui/client/src/controllers/Inventories.js b/awx/ui/client/src/controllers/Inventories.js index 819baaea48..62dfb5b03b 100644 --- a/awx/ui/client/src/controllers/Inventories.js +++ b/awx/ui/client/src/controllers/Inventories.js @@ -922,7 +922,7 @@ export function InventoriesManage ($log, $scope, $rootScope, $location, generateList.inject(InventoryGroups, { mode: 'edit', id: 'group-list-container', - searchSize: 'col-lg-6 col-md-6 col-sm-6', + searchSize: 'col-lg-6 col-md-6 col-sm-6 col-xs-12', scope: $scope }); diff --git a/awx/ui/client/src/helpers/Hosts.js b/awx/ui/client/src/helpers/Hosts.js index b9bde5fe2c..83eef613f2 100644 --- a/awx/ui/client/src/helpers/Hosts.js +++ b/awx/ui/client/src/helpers/Hosts.js @@ -229,7 +229,7 @@ angular.module('HostsHelper', [ 'RestServices', 'Utilities', listGenerator.name, generator = GenerateList; // Inject the list html - generator.inject(InventoryHosts, { scope: host_scope, mode: 'edit', id: 'host-list-container', searchSize: 'col-lg-6 col-md-6 col-sm-6' }); + generator.inject(InventoryHosts, { scope: host_scope, mode: 'edit', id: 'host-list-container', searchSize: 'col-lg-6 col-md-6 col-sm-6 col-xs-12' }); // Load data HostsReload({ scope: host_scope, group_id: group_id, inventory_id: inventory_id, parent_scope: group_scope, pageSize: pageSize }); diff --git a/awx/ui/client/src/shared/list-generator/list-actions.partial.html b/awx/ui/client/src/shared/list-generator/list-actions.partial.html index 5a97698872..ed82c52c11 100644 --- a/awx/ui/client/src/shared/list-generator/list-actions.partial.html +++ b/awx/ui/client/src/shared/list-generator/list-actions.partial.html @@ -1,4 +1,6 @@ - + - - +
+ + +
+ + +
+
+ - -
-
- -
{{ job_status.status_label }}
-
-
- -
-
Previous Task Failed - - - - -
-
- -
- -
-
- -
- - -
- -
- -
{{ job_status.started | date:'MM/dd/yy HH:mm:ss' }}
-
- -
- -
{{ job_type }}
-
- -
- -
{{ job_status.finished | date:'MM/dd/yy HH:mm:ss' }}
-
- -
- - -
- -
- -
{{ job_status.elapsed }}
-
- -
- - -
- -
- - -
- -
- - -
- -
- -
{{ job.playbook }}
-
- -
- - -
- -
- - -
- -
- -
{{ job.forks }}
-
- -
- -
{{ job.limit }}
-
- -
- -
{{ verbosity }}
-
- -
- -
{{ job.job_tags }}
-
- -
- - -
- +
+ + +
- -
-
-
- - DETAILS - - - DETAILS +
+
+ +
{{ job_status.status_label }}
+
+ +
-
-
-
-
-
- - - -
-
-
-
- - -
-
-
- - -
- - - - - - - - -
PlaysStartedElapsed
-
-
- - - - - - - - - - - - - - - - - - -
{{ play.name }}{{ play.created | date: 'HH:mm:ss' }}{{ play.elapsed }}
Waiting...
Loading...
No matching plays
-
-
+
+ +
- -
+
+ + +
+
+ +
{{ job_status.started | date:'MM/dd/yy HH:mm:ss' }}
+
+ +
+ +
{{ job_type }}
+
+ +
+ +
{{ job_status.finished | date:'MM/dd/yy HH:mm:ss' }}
+
+ +
+ + +
+ +
+ +
{{ job_status.elapsed }}
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
{{ job.playbook }}
+
+ +
+ + +
+ +
+ + +
+ +
+ +
{{ job.forks }}
+
+ +
+ +
{{ job.limit }}
+
+ +
+ +
{{ verbosity }}
+
+ +
+ +
{{ job.job_tags }}
+
+ +
+ + +
+ +
+
+ + + +
+ + +
+
+
+
+
+ + + +
+
+
+
+ + +
+
+
+ + +
+ + + + + + + + +
PlaysStartedElapsed
+
+
+ + + + + + + + + + + + + + + + + + +
{{ play.name }}{{ play.created | date: 'HH:mm:ss' }}{{ play.elapsed }}
Waiting...
Loading...
No matching plays
+
+
+ +
+
+ + +
- - + + +
+
+
+
+ + +
+
+
+ +
+ + + + + + + + + +
TasksStartedElapsed
+
+
+ + + + + + + + + + + + + + + + + + + +
{{ task.name }}{{ task.created | date: 'HH:mm:ss' }}{{ task.elapsed }}
Waiting...
Loading...
No matching tasks
+
+
+
+ + +
+
+
+
+ + +
-
+
+ + +
+
+
+
+ + + + + + + + +
HostsItemMessage
+
+ +
+ + + + + + + + + + + + + + + + + +
{{ result.name }}{{ result.name }}{{ result.item }}{{ result.msg }}
Waiting...
Loading...
No matching host events
+
+
+
+ +
+
+ + + +
+ + + +
+ + +
+ + + - -
-
-
- -
- - -
-
-
-
-
- - - -
-
-
-
- - -
-
-
-
- - - - - - - - -
HostsCompleted TasksActions
-
-
+ +
+ +
+
-
- + +
+ +
+
+ +
+ + + +
+
+
STANDARD OUT
+
+ + + +
-
-
-
- + +
- +
+
diff --git a/awx/ui/client/src/shared/branding/colors.default.less b/awx/ui/client/src/shared/branding/colors.default.less index f63c3860f3..dd53943e88 100644 --- a/awx/ui/client/src/shared/branding/colors.default.less +++ b/awx/ui/client/src/shared/branding/colors.default.less @@ -161,9 +161,6 @@ @db-graph-axis: @default-border; @db-graph-axis-label: @default-interface-txt; -//job detail -@toggle-selected-text: #eeeeee; - // panel @panel-bg: @default-bg; @panel-border: @default-border; diff --git a/awx/ui/client/src/standard-out/log/standard-out-log.controller.js b/awx/ui/client/src/standard-out/log/standard-out-log.controller.js index 3316d088a9..ca3bff7947 100644 --- a/awx/ui/client/src/standard-out/log/standard-out-log.controller.js +++ b/awx/ui/client/src/standard-out/log/standard-out-log.controller.js @@ -21,6 +21,15 @@ export default ['$log', '$rootScope', '$scope', '$state', '$stateParams', 'Proce // Open up a socket for events depending on the type of job function openSockets() { + if ($state.current.name == 'jobDetail') { + $log.debug("socket watching on job_events-" + job_id); + $rootScope.event_socket.on("job_events-" + job_id, function() { + $log.debug("socket fired on job_events-" + job_id); + if (api_complete) { + event_queue++; + } + }); + } if ($state.current.name == 'adHocJobStdout') { $log.debug("socket watching on ad_hoc_command_events-" + job_id); $rootScope.adhoc_event_socket.on("ad_hoc_command_events-" + job_id, function() { @@ -108,7 +117,7 @@ export default ['$log', '$rootScope', '$scope', '$state', '$stateParams', 'Proce function getNextSection() { // get the next range of data from the API var start = loaded_sections[loaded_sections.length - 1].end, url; - url = stdout_url + '?format=json&start_line=' + start + '&end_line=' + (start + page_size); + url = $scope.stdoutEndpoint + '?format=json&start_line=' + start + '&end_line=' + (start + page_size); $('#stdoutMoreRowsBottom').fadeIn(); Rest.setUrl(url); Rest.get() From 09c5a189ed833fd9a19525e10fd217280b1c4d18 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 3 Mar 2016 13:58:11 -0500 Subject: [PATCH 04/63] remove forgotten print --- awx/main/tests/functional/api/test_fact_versions.py | 3 --- awx/main/tests/functional/commands/test_cleanup_facts.py | 5 +++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/awx/main/tests/functional/api/test_fact_versions.py b/awx/main/tests/functional/api/test_fact_versions.py index 04246aa890..26f552dc60 100644 --- a/awx/main/tests/functional/api/test_fact_versions.py +++ b/awx/main/tests/functional/api/test_fact_versions.py @@ -79,9 +79,6 @@ def test_basic_options_fields(hosts, fact_scans, options, user): url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,)) response = options(url, None, user('admin', True), pk=hosts[0].id) - import json - print(json.dumps(response.data)) - assert 'related' in response.data['actions']['GET'] assert 'module' in response.data['actions']['GET'] assert ("ansible", "Ansible") in response.data['actions']['GET']['module']['choices'] diff --git a/awx/main/tests/functional/commands/test_cleanup_facts.py b/awx/main/tests/functional/commands/test_cleanup_facts.py index 93aec40928..9582d6fa54 100644 --- a/awx/main/tests/functional/commands/test_cleanup_facts.py +++ b/awx/main/tests/functional/commands/test_cleanup_facts.py @@ -3,6 +3,7 @@ # Python import pytest +import mock from dateutil.relativedelta import relativedelta from datetime import timedelta @@ -91,7 +92,7 @@ def test_cleanup_logic(fact_scans, hosts): timestamp_pivot -= granularity assert fact.timestamp == timestamp_pivot -@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) +@mock.patch('awx.main.management.commands.cleanup_facts.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_parameters_ok(mocker): run = mocker.patch('awx.main.management.commands.cleanup_facts.CleanupFacts.run') @@ -162,7 +163,7 @@ def test_string_time_to_timestamp_invalid(): res = cmd.string_time_to_timestamp(kv['time']) assert res is None -@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) +@mock.patch('awx.main.management.commands.cleanup_facts.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_parameters_fail(mocker): # Mock run() just in case, but it should never get called because an error should be thrown From cfeae512546ea99a2353358a3138424e598b5a47 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Fri, 4 Mar 2016 09:11:39 -0500 Subject: [PATCH 05/63] deny endpoint access to system tracking feature based on license --- awx/api/serializers.py | 2 +- awx/api/views.py | 23 ++++++++------- .../functional/api/test_fact_versions.py | 29 +++++++++++++++++++ .../tests/functional/api/test_fact_view.py | 27 +++++++++++++++++ .../functional/commands/test_cleanup_facts.py | 12 ++++++++ pytest.ini | 1 + 6 files changed, 82 insertions(+), 12 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 7847e1e86c..f8ab5c4d73 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -483,7 +483,7 @@ class BaseFactSerializer(BaseSerializer): def get_fields(self): ret = super(BaseFactSerializer, self).get_fields() - if 'module' in ret and feature_enabled('system_tracking'): + if 'module' in ret: # TODO: the values_list may pull in a LOT of entries before the distinct is called modules = Fact.objects.all().values_list('module', flat=True).distinct() choices = [(o, o.title()) for o in modules] diff --git a/awx/api/views.py b/awx/api/views.py index fda294153f..333f2427ae 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1270,7 +1270,17 @@ class HostActivityStreamList(SubListAPIView): qs = self.request.user.get_queryset(self.model) return qs.filter(Q(host=parent) | Q(inventory=parent.inventory)) -class HostFactVersionsList(ListAPIView, ParentMixin): +class SystemTrackingEnforcementMixin(APIView): + ''' + Use check_permissions instead of initial() because it's in the OPTION's path as well + ''' + def check_permissions(self, request): + if not feature_enabled("system_tracking"): + raise LicenseForbids("Your license does not permit use " + "of system tracking.") + return super(SystemTrackingEnforcementMixin, self).check_permissions(request) + +class HostFactVersionsList(ListAPIView, ParentMixin, SystemTrackingEnforcementMixin): model = Fact serializer_class = FactVersionSerializer @@ -1278,10 +1288,6 @@ class HostFactVersionsList(ListAPIView, ParentMixin): new_in_220 = True def get_queryset(self): - if not feature_enabled("system_tracking"): - raise LicenseForbids("Your license does not permit use " - "of system tracking.") - from_spec = self.request.query_params.get('from', None) to_spec = self.request.query_params.get('to', None) module_spec = self.request.query_params.get('module', None) @@ -1299,7 +1305,7 @@ class HostFactVersionsList(ListAPIView, ParentMixin): queryset = self.get_queryset() or [] return Response(dict(results=self.serializer_class(queryset, many=True).data)) -class HostFactCompareView(SubDetailAPIView): +class HostFactCompareView(SubDetailAPIView, SystemTrackingEnforcementMixin): model = Fact new_in_220 = True @@ -1307,11 +1313,6 @@ class HostFactCompareView(SubDetailAPIView): serializer_class = FactSerializer def retrieve(self, request, *args, **kwargs): - # Sanity check: Does the license allow system tracking? - if not feature_enabled('system_tracking'): - raise LicenseForbids('Your license does not permit use ' - 'of system tracking.') - datetime_spec = request.query_params.get('datetime', None) module_spec = request.query_params.get('module', "ansible") datetime_actual = dateutil.parser.parse(datetime_spec) if datetime_spec is not None else now() diff --git a/awx/main/tests/functional/api/test_fact_versions.py b/awx/main/tests/functional/api/test_fact_versions.py index 26f552dc60..dfb067a1f8 100644 --- a/awx/main/tests/functional/api/test_fact_versions.py +++ b/awx/main/tests/functional/api/test_fact_versions.py @@ -16,6 +16,9 @@ from django.utils import timezone def mock_feature_enabled(feature, bypass_database=None): return True +def mock_feature_disabled(feature, bypass_database=None): + return False + def setup_common(hosts, fact_scans, get, user, epoch=timezone.now(), get_params={}, host_count=1): hosts = hosts(host_count=host_count) fact_scans(fact_scans=3, timestamp_epoch=epoch) @@ -42,8 +45,33 @@ def check_response_facts(facts_known, response): assert timestamp_apiformat(fact_known.timestamp) == response.data['results'][i]['timestamp'] check_url(response.data['results'][i]['related']['fact_view'], fact_known, fact_known.module) +def check_system_tracking_feature_forbidden(response): + assert 402 == response.status_code + assert 'Your license does not permit use of system tracking.' == response.data['detail'] + +@mock.patch('awx.api.views.feature_enabled', new=mock_feature_disabled) +@pytest.mark.django_db +@pytest.mark.license_feature +def test_system_tracking_license_get(hosts, get, user): + hosts = hosts(host_count=1) + url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,)) + response = get(url, user('admin', True)) + + check_system_tracking_feature_forbidden(response) + +@mock.patch('awx.api.views.feature_enabled', new=mock_feature_disabled) +@pytest.mark.django_db +@pytest.mark.license_feature +def test_system_tracking_license_options(hosts, options, user): + hosts = hosts(host_count=1) + url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,)) + response = options(url, None, user('admin', True)) + + check_system_tracking_feature_forbidden(response) + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db +@pytest.mark.license_feature def test_no_facts_db(hosts, get, user): hosts = hosts(host_count=1) url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,)) @@ -72,6 +100,7 @@ def test_basic_fields(hosts, fact_scans, get, user): @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db +@pytest.mark.license_feature def test_basic_options_fields(hosts, fact_scans, options, user): hosts = hosts(host_count=1) fact_scans(fact_scans=1) diff --git a/awx/main/tests/functional/api/test_fact_view.py b/awx/main/tests/functional/api/test_fact_view.py index d27a7d97db..ad96d48aee 100644 --- a/awx/main/tests/functional/api/test_fact_view.py +++ b/awx/main/tests/functional/api/test_fact_view.py @@ -9,6 +9,9 @@ from django.utils import timezone def mock_feature_enabled(feature, bypass_database=None): return True +def mock_feature_disabled(feature, bypass_database=None): + return False + # TODO: Consider making the fact_scan() fixture a Class, instead of a function, and move this method into it def find_fact(facts, host_id, module_name, timestamp): for f in facts: @@ -26,6 +29,30 @@ def setup_common(hosts, fact_scans, get, user, epoch=timezone.now(), module_name fact_known = find_fact(facts, hosts[0].id, module_name, epoch) return (fact_known, response) +def check_system_tracking_feature_forbidden(response): + assert 402 == response.status_code + assert 'Your license does not permit use of system tracking.' == response.data['detail'] + +@mock.patch('awx.api.views.feature_enabled', new=mock_feature_disabled) +@pytest.mark.django_db +@pytest.mark.license_feature +def test_system_tracking_license_get(hosts, get, user): + hosts = hosts(host_count=1) + url = reverse('api:host_fact_compare_view', args=(hosts[0].pk,)) + response = get(url, user('admin', True)) + + check_system_tracking_feature_forbidden(response) + +@mock.patch('awx.api.views.feature_enabled', new=mock_feature_disabled) +@pytest.mark.django_db +@pytest.mark.license_feature +def test_system_tracking_license_options(hosts, options, user): + hosts = hosts(host_count=1) + url = reverse('api:host_fact_compare_view', args=(hosts[0].pk,)) + response = options(url, None, user('admin', True)) + + check_system_tracking_feature_forbidden(response) + @mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_no_fact_found(hosts, get, user): diff --git a/awx/main/tests/functional/commands/test_cleanup_facts.py b/awx/main/tests/functional/commands/test_cleanup_facts.py index 9582d6fa54..93ddb72d14 100644 --- a/awx/main/tests/functional/commands/test_cleanup_facts.py +++ b/awx/main/tests/functional/commands/test_cleanup_facts.py @@ -19,6 +19,9 @@ from awx.main.models.inventory import Host def mock_feature_enabled(feature, bypass_database=None): return True +def mock_feature_disabled(feature, bypass_database=None): + return False + @pytest.mark.django_db def test_cleanup_granularity(fact_scans, hosts): epoch = timezone.now() @@ -92,6 +95,15 @@ def test_cleanup_logic(fact_scans, hosts): timestamp_pivot -= granularity assert fact.timestamp == timestamp_pivot +@mock.patch('awx.main.management.commands.cleanup_facts.feature_enabled', new=mock_feature_disabled) +@pytest.mark.django_db +@pytest.mark.license_feature +def test_system_tracking_feature_disabled(mocker): + cmd = Command() + with pytest.raises(CommandError) as err: + cmd.handle(None) + assert 'The System Tracking feature is not enabled for your Tower instance' in err.value + @mock.patch('awx.main.management.commands.cleanup_facts.feature_enabled', new=mock_feature_enabled) @pytest.mark.django_db def test_parameters_ok(mocker): diff --git a/pytest.ini b/pytest.ini index 748c2919fd..338e91c932 100644 --- a/pytest.ini +++ b/pytest.ini @@ -6,3 +6,4 @@ python_files = *.py addopts = --reuse-db markers = ac: access control test + license_feature: ensure license features are accessible or not depending on license From 656f77c0de455bf05f590710316651c22e4053b9 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Fri, 4 Mar 2016 07:42:56 -0800 Subject: [PATCH 06/63] clean up stale code --- awx/ui/client/src/helpers/JobDetail.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/src/helpers/JobDetail.js b/awx/ui/client/src/helpers/JobDetail.js index 121786de3a..689e7139b5 100644 --- a/awx/ui/client/src/helpers/JobDetail.js +++ b/awx/ui/client/src/helpers/JobDetail.js @@ -1142,7 +1142,6 @@ export default .factory('DrawGraph', ['DonutChart', function(DonutChart) { return function(params) { var scope = params.scope, - resize = params.resize, graph_data = []; // Ready the data @@ -1192,8 +1191,9 @@ export default element = $("#graph-section"), colors, total,job_detail_chart; - // colors = ['#60D66F', '#FF9900','#FF0000','#ff5850']; - colors = _.map(dataset, function(d){return d.color}); + colors = _.map(dataset, function(d){ + return d.color; + }); total = d3.sum(dataset.map(function(d) { return d.value; })); From d56dc253146a8ba3d3d362c869de8226ebc8e1f3 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Fri, 4 Mar 2016 10:52:12 -0500 Subject: [PATCH 07/63] do not require mongo to be on --- awx/fact/utils/connection.py | 28 ---------------------------- awx/main/tasks.py | 6 ------ 2 files changed, 34 deletions(-) delete mode 100644 awx/fact/utils/connection.py diff --git a/awx/fact/utils/connection.py b/awx/fact/utils/connection.py deleted file mode 100644 index 4c4019e24d..0000000000 --- a/awx/fact/utils/connection.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -from django.conf import settings -from mongoengine import connect -from mongoengine.connection import ConnectionError -from pymongo.errors import AutoReconnect - -def test_mongo_connection(): - # Connect to Mongo - try: - # Sanity check: If we have intentionally invalid settings, then we - # know we cannot connect. - if settings.MONGO_HOST == NotImplemented: - raise ConnectionError - - # Attempt to connect to the MongoDB database. - db = connect(settings.MONGO_DB, - host=settings.MONGO_HOST, - port=int(settings.MONGO_PORT), - username=settings.MONGO_USERNAME, - password=settings.MONGO_PASSWORD, - tz_aware=settings.USE_TZ) - db[settings.MONGO_DB].command('ping') - return True - except (ConnectionError, AutoReconnect): - return False - diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 509c5d1e7e..3942cc78bb 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -53,7 +53,6 @@ from awx.main.task_engine import TaskSerializer, TASK_TIMEOUT_INTERVAL from awx.main.utils import (get_ansible_version, get_ssh_version, decrypt_field, update_scm_url, ignore_inventory_computed_fields, emit_websocket_notification, check_proot_installed, build_proot_temp_dir, wrap_args_with_proot) -from awx.fact.utils.connection import test_mongo_connection __all__ = ['RunJob', 'RunSystemJob', 'RunProjectUpdate', 'RunInventoryUpdate', 'RunAdHocCommand', 'handle_work_error', 'handle_work_success', @@ -959,11 +958,6 @@ class RunJob(BaseTask): ''' return getattr(tower_settings, 'AWX_PROOT_ENABLED', False) - def pre_run_hook(self, job, **kwargs): - if job.job_type == PERM_INVENTORY_SCAN: - if not test_mongo_connection(): - raise RuntimeError("Fact Scan Database is offline") - def post_run_hook(self, job, **kwargs): ''' Hook for actions to run after job/task has completed. From cf5d718fa8730c715cc06906bb881fff4776c60a Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 26 Feb 2016 11:44:38 -0500 Subject: [PATCH 08/63] Fixed pagination nav issue Fixes #1021 --- awx/ui/client/src/helpers/PaginationHelpers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/helpers/PaginationHelpers.js b/awx/ui/client/src/helpers/PaginationHelpers.js index d2019af8fc..f141376d8f 100644 --- a/awx/ui/client/src/helpers/PaginationHelpers.js +++ b/awx/ui/client/src/helpers/PaginationHelpers.js @@ -32,14 +32,14 @@ export default // Which page are we on? if (Empty(next) && previous) { // no next page, but there is a previous page - scope[iterator + '_page'] = parseInt(previous.match(/page=\d+/)[0].replace(/page=/, '')) + 1; + scope[iterator + '_page'] = /page=\d+/.test(previous) ? parseInt(previous.match(/page=(\d+)/)[1]) + 1 : 2 } else if (next && Empty(previous)) { // next page available, but no previous page scope[iterator + '_page'] = 1; $('#'+iterator+'-pagination #pagination-links li:eq(1)').attr('class', 'disabled'); } else if (next && previous) { // we're in between next and previous - scope[iterator + '_page'] = parseInt(previous.match(/page=\d+/)[0].replace(/page=/, '')) + 1; + scope[iterator + '_page'] = /page=\d+/.test(previous) ? parseInt(previous.match(/page=(\d+)/)[1]) + 1 : 2 } // Calc the range of up to 10 pages to show From e28eec4a5663d714cdce81d3e734aa4112efad1f Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 4 Mar 2016 11:37:47 -0500 Subject: [PATCH 09/63] Fix missing semicolons --- awx/ui/client/src/helpers/PaginationHelpers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/helpers/PaginationHelpers.js b/awx/ui/client/src/helpers/PaginationHelpers.js index f141376d8f..2fd9d57bf2 100644 --- a/awx/ui/client/src/helpers/PaginationHelpers.js +++ b/awx/ui/client/src/helpers/PaginationHelpers.js @@ -32,14 +32,14 @@ export default // Which page are we on? if (Empty(next) && previous) { // no next page, but there is a previous page - scope[iterator + '_page'] = /page=\d+/.test(previous) ? parseInt(previous.match(/page=(\d+)/)[1]) + 1 : 2 + scope[iterator + '_page'] = /page=\d+/.test(previous) ? parseInt(previous.match(/page=(\d+)/)[1]) + 1 : 2; } else if (next && Empty(previous)) { // next page available, but no previous page scope[iterator + '_page'] = 1; $('#'+iterator+'-pagination #pagination-links li:eq(1)').attr('class', 'disabled'); } else if (next && previous) { // we're in between next and previous - scope[iterator + '_page'] = /page=\d+/.test(previous) ? parseInt(previous.match(/page=(\d+)/)[1]) + 1 : 2 + scope[iterator + '_page'] = /page=\d+/.test(previous) ? parseInt(previous.match(/page=(\d+)/)[1]) + 1 : 2; } // Calc the range of up to 10 pages to show From 7494b137040be3d5c8ef287e75b8c4bc7b621e7b Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Fri, 4 Mar 2016 11:41:08 -0500 Subject: [PATCH 10/63] find correct fact to update * The fact cache receiver needs to look for an exact match, rather than * a relative, most recent fact. --- awx/main/management/commands/run_fact_cache_receiver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/management/commands/run_fact_cache_receiver.py b/awx/main/management/commands/run_fact_cache_receiver.py index 42fc25a561..062cd39693 100644 --- a/awx/main/management/commands/run_fact_cache_receiver.py +++ b/awx/main/management/commands/run_fact_cache_receiver.py @@ -67,7 +67,7 @@ class FactCacheReceiver(object): self.timestamp = datetime.fromtimestamp(date_key, None) # Update existing Fact entry - fact_obj = Fact.get_host_fact(host_obj.id, module_name, self.timestamp) + fact_obj = Fact.objects.filter(host__id=host_obj.id, module=module_name, timestamp=self.timestamp) if fact_obj: fact_obj.facts = facts fact_obj.save() From 5a3180433056cc2f461c25f1d6d2b94b562aaac2 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 4 Mar 2016 13:37:07 -0500 Subject: [PATCH 11/63] Just use num_pages when we're at the end of a nav list --- awx/ui/client/src/helpers/PaginationHelpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/helpers/PaginationHelpers.js b/awx/ui/client/src/helpers/PaginationHelpers.js index 2fd9d57bf2..2b131c2dc0 100644 --- a/awx/ui/client/src/helpers/PaginationHelpers.js +++ b/awx/ui/client/src/helpers/PaginationHelpers.js @@ -32,7 +32,7 @@ export default // Which page are we on? if (Empty(next) && previous) { // no next page, but there is a previous page - scope[iterator + '_page'] = /page=\d+/.test(previous) ? parseInt(previous.match(/page=(\d+)/)[1]) + 1 : 2; + scope[iterator + '_page'] = scope[iterator + '_num_pages']; } else if (next && Empty(previous)) { // next page available, but no previous page scope[iterator + '_page'] = 1; From 5759db1229541f2755b548ff1d8baf4c103cd72c Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Fri, 4 Mar 2016 14:14:05 -0500 Subject: [PATCH 12/63] flake8 fix --- awx/main/management/commands/run_fact_cache_receiver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/management/commands/run_fact_cache_receiver.py b/awx/main/management/commands/run_fact_cache_receiver.py index 062cd39693..02a3b2e66c 100644 --- a/awx/main/management/commands/run_fact_cache_receiver.py +++ b/awx/main/management/commands/run_fact_cache_receiver.py @@ -67,7 +67,7 @@ class FactCacheReceiver(object): self.timestamp = datetime.fromtimestamp(date_key, None) # Update existing Fact entry - fact_obj = Fact.objects.filter(host__id=host_obj.id, module=module_name, timestamp=self.timestamp) + fact_obj = Fact.objects.filter(host__id=host_obj.id, module=module_name, timestamp=self.timestamp) if fact_obj: fact_obj.facts = facts fact_obj.save() From 08c33a8c6bbe6b91c6d67b6bb5d992963b62e220 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Fri, 4 Mar 2016 11:36:34 -0500 Subject: [PATCH 13/63] Refresh the job details when the job finishes running. This pulls in details not available at the beginning of a job. One plus two styling tweaks --- awx/ui/client/src/license/license.block.less | 26 +- .../client/src/license/license.partial.html | 87 +++---- .../src/shared/layouts/one-plus-two.less | 18 +- .../adhoc/standard-out-adhoc.partial.html | 2 +- .../standard-out-inventory-sync.partial.html | 2 +- .../standard-out-management-jobs.partial.html | 2 +- .../standard-out-scm-update.partial.html | 2 +- .../src/standard-out/standard-out.block.less | 48 ++-- .../standard-out/standard-out.controller.js | 222 +++++++++--------- 9 files changed, 206 insertions(+), 203 deletions(-) diff --git a/awx/ui/client/src/license/license.block.less b/awx/ui/client/src/license/license.block.less index 58ce83542a..bf8c005d08 100644 --- a/awx/ui/client/src/license/license.block.less +++ b/awx/ui/client/src/license/license.block.less @@ -33,6 +33,9 @@ .License-field{ .OnePlusTwo-left--detailsRow; } +.License-field + .License-field { + margin-top: 20px; +} .License-greenText{ color: @submit-button-bg; } @@ -40,16 +43,16 @@ color: #d9534f; } .License-fields{ - .OnePlusTwo-left--details; + .OnePlusTwo-left--details; } .License-details { - .OnePlusTwo-left--panel(600px); + .OnePlusTwo-left--panel(650px); } .License-titleText { .OnePlusTwo-panelHeader; } .License-management{ - .OnePlusTwo-right--panel(600px); + .OnePlusTwo-right--panel(650px); } .License-submit--container{ height: 33px; @@ -59,8 +62,21 @@ margin: 0 10px 0 0; } .License-file--container { - margin: 20px 0 20px 0; input[type=file] { display: none; } -} \ No newline at end of file +} +.License-upgradeText { + margin: 20px 0px; +} +.License-body { + margin-top: 25px; +} +.License-subTitleText { + text-transform: uppercase; + margin: 20px 0px 5px 0px; + color: @default-interface-txt; +} +.License-helperText { + color: @default-interface-txt; +} diff --git a/awx/ui/client/src/license/license.partial.html b/awx/ui/client/src/license/license.partial.html index dcbde280e7..38eeede726 100644 --- a/awx/ui/client/src/license/license.partial.html +++ b/awx/ui/client/src/license/license.partial.html @@ -5,95 +5,98 @@
License
-
+
Valid Invalid -
+
Version
-
+
{{license.version}}
-
License Type
+
License Type
{{license.license_info.license_type}} -
+
Subscription
-
+
{{license.license_info.subscription_name}} -
+
-
License Key
-
+
License Key
+
{{license.license_info.license_key}} -
+
Expires On
-
+
{{time.expiresOn}} -
+
Time Remaining
-
+
{{time.remaining}} Day -
+
-
Hosts Available
+
Hosts Available
{{license.license_info.available_instances}} -
+
Hosts Used
-
+
{{license.license_info.current_instances}} -
+
Hosts Remaining
-
+
{{license.license_info.free_instances}} -
+
-

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

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

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

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

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

+ +
License File
+
+ Browse... + + +
+
End User License Agreement
+
+ +
+
+
+
I agree to the End User License Agreement
+
+ Save successful! + +
-
- + +
-
\ No newline at end of file +
diff --git a/awx/ui/client/src/shared/layouts/one-plus-two.less b/awx/ui/client/src/shared/layouts/one-plus-two.less index fe43e40b65..87c44fe056 100644 --- a/awx/ui/client/src/shared/layouts/one-plus-two.less +++ b/awx/ui/client/src/shared/layouts/one-plus-two.less @@ -22,25 +22,24 @@ flex: 0 0; height: @height; width: 100%; + margin-right: 20px; .Panel{ height: 100%; } - @media screen and (min-width: @breakpoint){ - max-width: 400px; - } + @media screen and (max-width: @breakpoint){ + margin-right: 0px; + height: inherit; + } } .OnePlusTwo-right--panel(@height: 100%; @breakpoint: 900px) { height: @height; flex: 1 0; - margin-left: 20px; .Panel{ height: 100%; } @media screen and (max-width: @breakpoint){ flex-direction: column; - margin-left: 0px; - margin-top: 25px; } } @@ -50,6 +49,7 @@ font-weight: bold; margin-right: 10px; text-transform: uppercase; + display: flex; } .OnePlusTwo-left--details { @@ -58,9 +58,6 @@ .OnePlusTwo-left--detailsRow { display: flex; - :not(:last-child){ - margin-bottom: 20px; - } } .OnePlusTwo-left--detailsLabel { @@ -73,7 +70,6 @@ .OnePlusTwo-left--detailsContent { display: inline-block; - max-width: 220px; + width: 220px; word-wrap: break-word; } - diff --git a/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.partial.html b/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.partial.html index 92e4d5e762..e2a321bfbb 100644 --- a/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.partial.html +++ b/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.partial.html @@ -1,6 +1,6 @@
-
+
diff --git a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html index d16efeb007..1fb14f2131 100644 --- a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html +++ b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html @@ -1,6 +1,6 @@
-
+
diff --git a/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html b/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html index 3e035ddab5..27864d2ca2 100644 --- a/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html +++ b/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html @@ -1,6 +1,6 @@
-
+
diff --git a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html b/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html index 74e7e63925..ed14989c9a 100644 --- a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html +++ b/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html @@ -1,6 +1,6 @@
-
+
diff --git a/awx/ui/client/src/standard-out/standard-out.block.less b/awx/ui/client/src/standard-out/standard-out.block.less index 8d0e8a0219..ffdb158705 100644 --- a/awx/ui/client/src/standard-out/standard-out.block.less +++ b/awx/ui/client/src/standard-out/standard-out.block.less @@ -1,28 +1,22 @@ @import "../shared/branding/colors.default.less"; +@import "awx/ui/client/src/shared/layouts/one-plus-two.less"; /** @define StandardOut */ -.StandardOut { - height: 100%; - display: flex; - flex-direction: row; +.StandardOut-container { + .OnePlusTwo-container; } .StandardOut-leftPanel { - flex: 0 0 400px; - margin-right: 20px; + .OnePlusTwo-left--panel(590px); } .StandardOut-rightPanel { - flex: 1 0; + .OnePlusTwo-right--panel(590px); } .StandardOut-panelHeader { - color: @default-interface-txt; - font-size: 14px; - font-weight: bold; - text-transform: uppercase; - display: flex; + .OnePlusTwo-panelHeader } .StandardOut-consoleOutput { @@ -30,31 +24,28 @@ min-height: 200px; background-color: @default-secondary-bg; border-radius: 5px; - height: 300px; + height: ~"calc(100% - 74px)"; overflow: scroll; } .StandardOut-details { - margin-top: 25px; + .OnePlusTwo-left--details; } .StandardOut-detailsRow { - display: flex; + .OnePlusTwo-left--detailsRow; } -.StandardOut-detailsRow:not(:last-child) { - margin-bottom: 20px; +.StandardOut-detailsRow + .StandardOut-detailsRow { + margin-top: 20px; } .StandardOut-detailsLabel { - width: 130px; - flex: 0 0 130px; - color: @default-interface-txt; - text-transform: uppercase; + .OnePlusTwo-left--detailsLabel; } .StandardOut-detailsContent { - flex: 1 0; + .OnePlusTwo-left--detailsContent; } .StandardOut-statusText { @@ -66,7 +57,7 @@ } .StandardOut-preContainer { - height: 300px; + height: 100%; } .StandardOut-panelHeaderText { @@ -105,14 +96,3 @@ .StandardOut-actionButton + a { margin-left: 15px; } - -@standardout-breakpoint: 900px; - -@media screen and (max-width: @standardout-breakpoint) { - .StandardOut { - flex-direction: column; - } - .StandardOut-leftPanel { - margin-right: 0px; - } -} diff --git a/awx/ui/client/src/standard-out/standard-out.controller.js b/awx/ui/client/src/standard-out/standard-out.controller.js index 152d2a8a5d..b3b363e064 100644 --- a/awx/ui/client/src/standard-out/standard-out.controller.js +++ b/awx/ui/client/src/standard-out/standard-out.controller.js @@ -32,118 +32,124 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, C $scope.job.status = data.status; } - // TODO: when the job completes we should refresh the job data so that we pull in the finish - // timestamp as well as the run time. + if (data.status === 'failed' || data.status === 'canceled' || data.status === 'error' || data.status === 'successful') { + // Go out and refresh the job details + getJobDetails(); + } }); - // Go out and get the job details based on the job type. jobType gets defined - // in the data block of the route declaration for each of the different types - // of stdout jobs. - Rest.setUrl(GetBasePath('base') + jobType + '/' + job_id + '/'); - Rest.get() - .success(function(data) { - $scope.job = data; - $scope.job_template_name = data.name; - $scope.created_by = data.summary_fields.created_by; - $scope.project_name = (data.summary_fields.project) ? data.summary_fields.project.name : ''; - $scope.inventory_name = (data.summary_fields.inventory) ? data.summary_fields.inventory.name : ''; - $scope.job_template_url = '/#/job_templates/' + data.unified_job_template; - $scope.inventory_url = ($scope.inventory_name && data.inventory) ? '/#/inventories/' + data.inventory : ''; - $scope.project_url = ($scope.project_name && data.project) ? '/#/projects/' + data.project : ''; - $scope.credential_name = (data.summary_fields.credential) ? data.summary_fields.credential.name : ''; - $scope.credential_url = (data.credential) ? '/#/credentials/' + data.credential : ''; - $scope.cloud_credential_url = (data.cloud_credential) ? '/#/credentials/' + data.cloud_credential : ''; - $scope.playbook = data.playbook; - $scope.credential = data.credential; - $scope.cloud_credential = data.cloud_credential; - $scope.forks = data.forks; - $scope.limit = data.limit; - $scope.verbosity = data.verbosity; - $scope.job_tags = data.job_tags; + function getJobDetails() { - // If we have a source then we have to go get the source choices from the server - if (!Empty(data.source)) { - if ($scope.removeChoicesReady) { - $scope.removeChoicesReady(); - } - $scope.removeChoicesReady = $scope.$on('ChoicesReady', function() { - $scope.source_choices.every(function(e) { - if (e.value === data.source) { - $scope.source = e.label; - return false; - } - return true; + // Go out and get the job details based on the job type. jobType gets defined + // in the data block of the route declaration for each of the different types + // of stdout jobs. + Rest.setUrl(GetBasePath('base') + jobType + '/' + job_id + '/'); + Rest.get() + .success(function(data) { + $scope.job = data; + $scope.job_template_name = data.name; + $scope.created_by = data.summary_fields.created_by; + $scope.project_name = (data.summary_fields.project) ? data.summary_fields.project.name : ''; + $scope.inventory_name = (data.summary_fields.inventory) ? data.summary_fields.inventory.name : ''; + $scope.job_template_url = '/#/job_templates/' + data.unified_job_template; + $scope.inventory_url = ($scope.inventory_name && data.inventory) ? '/#/inventories/' + data.inventory : ''; + $scope.project_url = ($scope.project_name && data.project) ? '/#/projects/' + data.project : ''; + $scope.credential_name = (data.summary_fields.credential) ? data.summary_fields.credential.name : ''; + $scope.credential_url = (data.credential) ? '/#/credentials/' + data.credential : ''; + $scope.cloud_credential_url = (data.cloud_credential) ? '/#/credentials/' + data.cloud_credential : ''; + $scope.playbook = data.playbook; + $scope.credential = data.credential; + $scope.cloud_credential = data.cloud_credential; + $scope.forks = data.forks; + $scope.limit = data.limit; + $scope.verbosity = data.verbosity; + $scope.job_tags = data.job_tags; + + // If we have a source then we have to go get the source choices from the server + if (!Empty(data.source)) { + if ($scope.removeChoicesReady) { + $scope.removeChoicesReady(); + } + $scope.removeChoicesReady = $scope.$on('ChoicesReady', function() { + $scope.source_choices.every(function(e) { + if (e.value === data.source) { + $scope.source = e.label; + return false; + } + return true; + }); + }); + // GetChoices can be found in the helper: StandardOut.js + // It attaches the source choices to $scope.source_choices. + // Then, when the callback is fired, $scope.source is bound + // to the corresponding label. + GetChoices({ + scope: $scope, + url: GetBasePath('inventory_sources'), + field: 'source', + variable: 'source_choices', + choice_name: 'choices', + callback: 'ChoicesReady' }); - }); - // GetChoices can be found in the helper: StandardOut.js - // It attaches the source choices to $scope.source_choices. - // Then, when the callback is fired, $scope.source is bound - // to the corresponding label. - GetChoices({ - scope: $scope, - url: GetBasePath('inventory_sources'), - field: 'source', - variable: 'source_choices', - choice_name: 'choices', - callback: 'ChoicesReady' - }); - } - - // LookUpName can be found in the helper: StandardOut.js - // It attaches the name that it gets (based on the url) - // to the $scope variable defined by the attribute scope_var. - if (!Empty(data.credential)) { - LookUpName({ - scope: $scope, - scope_var: 'credential', - url: GetBasePath('credentials') + data.credential + '/' - }); - } - - if (!Empty(data.inventory)) { - LookUpName({ - scope: $scope, - scope_var: 'inventory', - url: GetBasePath('inventory') + data.inventory + '/' - }); - } - - if (!Empty(data.project)) { - LookUpName({ - scope: $scope, - scope_var: 'project', - url: GetBasePath('projects') + data.project + '/' - }); - } - - if (!Empty(data.cloud_credential)) { - LookUpName({ - scope: $scope, - scope_var: 'cloud_credential', - url: GetBasePath('credentials') + data.cloud_credential + '/' - }); - } - - if (!Empty(data.inventory_source)) { - LookUpName({ - scope: $scope, - scope_var: 'inventory_source', - url: GetBasePath('inventory_sources') + data.inventory_source + '/' - }); - } - - // If the job isn't running we want to clear out the interval that goes out and checks for stdout updates. - // This interval is defined in the standard out log directive controller. - if (data.status === 'successful' || data.status === 'failed' || data.status === 'error' || data.status === 'canceled') { - if ($rootScope.jobStdOutInterval) { - window.clearInterval($rootScope.jobStdOutInterval); } - } - }) - .error(function(data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve job: ' + job_id + '. GET returned: ' + status }); - }); + + // LookUpName can be found in the helper: StandardOut.js + // It attaches the name that it gets (based on the url) + // to the $scope variable defined by the attribute scope_var. + if (!Empty(data.credential)) { + LookUpName({ + scope: $scope, + scope_var: 'credential', + url: GetBasePath('credentials') + data.credential + '/' + }); + } + + if (!Empty(data.inventory)) { + LookUpName({ + scope: $scope, + scope_var: 'inventory', + url: GetBasePath('inventory') + data.inventory + '/' + }); + } + + if (!Empty(data.project)) { + LookUpName({ + scope: $scope, + scope_var: 'project', + url: GetBasePath('projects') + data.project + '/' + }); + } + + if (!Empty(data.cloud_credential)) { + LookUpName({ + scope: $scope, + scope_var: 'cloud_credential', + url: GetBasePath('credentials') + data.cloud_credential + '/' + }); + } + + if (!Empty(data.inventory_source)) { + LookUpName({ + scope: $scope, + scope_var: 'inventory_source', + url: GetBasePath('inventory_sources') + data.inventory_source + '/' + }); + } + + // If the job isn't running we want to clear out the interval that goes out and checks for stdout updates. + // This interval is defined in the standard out log directive controller. + if (data.status === 'successful' || data.status === 'failed' || data.status === 'error' || data.status === 'canceled') { + if ($rootScope.jobStdOutInterval) { + window.clearInterval($rootScope.jobStdOutInterval); + } + } + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Failed to retrieve job: ' + job_id + '. GET returned: ' + status }); + }); + + } // TODO: this is currently not used but is necessary for cases where sockets // are not available and a manual refresh trigger is needed. @@ -156,6 +162,8 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, C $scope.stdoutFullScreen = !$scope.stdoutFullScreen; } + getJobDetails(); + } JobStdoutController.$inject = [ '$rootScope', '$scope', '$state', '$stateParams', 'ClearScope', 'GetBasePath', 'Rest', 'ProcessErrors', 'Empty', 'GetChoices', 'LookUpName']; From f1c13e0837bcfe04b0725dacd7c30203317ad930 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Fri, 4 Mar 2016 15:41:40 -0500 Subject: [PATCH 14/63] Add migration to create system job templates for a new database. --- awx/main/migrations/0005_v300_changes.py | 115 +++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 awx/main/migrations/0005_v300_changes.py diff --git a/awx/main/migrations/0005_v300_changes.py b/awx/main/migrations/0005_v300_changes.py new file mode 100644 index 0000000000..e350c881f2 --- /dev/null +++ b/awx/main/migrations/0005_v300_changes.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.utils.timezone import now + +from awx.api.license import feature_enabled + + +def create_system_job_templates(apps, schema_editor): + ''' + Create default system job templates if not present. Create default schedules + only if new system job templates were created (i.e. new database). + ''' + + SystemJobTemplate = apps.get_model('main', 'SystemJobTemplate') + ContentType = apps.get_model('contenttypes', 'ContentType') + sjt_ct = ContentType.objects.get_for_model(SystemJobTemplate) + now_dt = now() + now_str = now_dt.strftime('%Y%m%dT%H%M%SZ') + + sjt, created = SystemJobTemplate.objects.get_or_create( + job_type='cleanup_jobs', + defaults=dict( + name='Cleanup Job Details', + description='Remove job history older than X days', + created=now_dt, + modified=now_dt, + polymorphic_ctype=sjt_ct, + ), + ) + if created: + sjt.schedules.create( + name='Cleanup Job Schedule', + rrule='DTSTART:%s RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=SU' % now_str, + description='Automatically Generated Schedule', + enabled=True, + extra_data={'days': '120'}, + created=now_dt, + modified=now_dt, + ) + + sjt, created = SystemJobTemplate.objects.get_or_create( + job_type='cleanup_deleted', + defaults=dict( + name='Cleanup Deleted Data', + description='Remove deleted object history older than X days', + created=now_dt, + modified=now_dt, + polymorphic_ctype=sjt_ct, + ), + ) + if created: + sjt.schedules.create( + name='Cleanup Deleted Data Schedule', + rrule='DTSTART:%s RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO' % now_str, + description='Automatically Generated Schedule', + enabled=True, + extra_data={'days': '30'}, + created=now_dt, + modified=now_dt, + ) + + sjt, created = SystemJobTemplate.objects.get_or_create( + job_type='cleanup_activitystream', + defaults=dict( + name='Cleanup Activity Stream', + description='Remove activity stream history older than X days', + created=now_dt, + modified=now_dt, + polymorphic_ctype=sjt_ct, + ), + ) + if created: + sjt.schedules.create( + name='Cleanup Activity Schedule', + rrule='DTSTART:%s RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=TU' % now_str, + description='Automatically Generated Schedule', + enabled=True, + extra_data={'days': '355'}, + created=now_dt, + modified=now_dt, + ) + + sjt, created = SystemJobTemplate.objects.get_or_create( + job_type='cleanup_facts', + defaults=dict( + name='Cleanup Fact Details', + description='Remove system tracking history', + created=now_dt, + modified=now_dt, + polymorphic_ctype=sjt_ct, + ), + ) + if created and feature_enabled('system_tracking', bypass_database=True): + sjt.schedules.create( + name='Cleanup Fact Schedule', + rrule='DTSTART:%s RRULE:FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=1' % now_str, + description='Automatically Generated Schedule', + enabled=True, + extra_data={'older_than': '120d', 'granularity': '1w'}, + created=now_dt, + modified=now_dt, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0004_v300_changes'), + ] + + operations = [ + migrations.RunPython(create_system_job_templates, migrations.RunPython.noop), + ] From ee5694b175e2e9a6aea776404269ed39c2ee3ea3 Mon Sep 17 00:00:00 2001 From: James Laska Date: Fri, 4 Mar 2016 16:19:00 -0500 Subject: [PATCH 15/63] Add ISSUE_TEMPLATE.md fixes #1106 --- ISSUE_TEMPLATE.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 ISSUE_TEMPLATE.md diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..c12528c389 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,31 @@ +### Summary + + + +### Environment + + + +### Steps To Reproduce: + + + +### Expected Results: + + + +### Actual Results: + + + +### Additional Information: + + From 60b6b6bfeab4a065d30babc8034aa8f9a0d31907 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Fri, 4 Mar 2016 16:36:02 -0500 Subject: [PATCH 16/63] Fix job template callback view to accept URL-encoded form data. --- awx/api/views.py | 2 ++ awx/main/tests/base.py | 3 +++ awx/main/tests/old/jobs/jobs_monolithic.py | 31 ++++++++++++++++------ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index 333f2427ae..02a10451d8 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -32,6 +32,7 @@ from django.http import HttpResponse # Django REST Framework from rest_framework.exceptions import PermissionDenied, ParseError +from rest_framework.parsers import FormParser from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.settings import api_settings @@ -2005,6 +2006,7 @@ class JobTemplateCallback(GenericAPIView): model = JobTemplate permission_classes = (JobTemplateCallbackPermission,) serializer_class = EmptySerializer + parser_classes = api_settings.DEFAULT_PARSER_CLASSES + [FormParser] @csrf_exempt @transaction.non_atomic_requests diff --git a/awx/main/tests/base.py b/awx/main/tests/base.py index 93bac00948..a0387079b6 100644 --- a/awx/main/tests/base.py +++ b/awx/main/tests/base.py @@ -11,6 +11,7 @@ import shutil import sys import tempfile import time +import urllib from multiprocessing import Process from subprocess import Popen import re @@ -463,6 +464,8 @@ class BaseTestMixin(QueueTestMixin, MockCommonlySlowTestMixin): response = method(url, json.dumps(data), 'application/json') elif data_type == 'yaml': response = method(url, yaml.safe_dump(data), 'application/yaml') + elif data_type == 'form': + response = method(url, urllib.urlencode(data), 'application/x-www-form-urlencoded') else: self.fail('Unsupported data_type %s' % data_type) else: diff --git a/awx/main/tests/old/jobs/jobs_monolithic.py b/awx/main/tests/old/jobs/jobs_monolithic.py index 1d36972245..9234f57a2b 100644 --- a/awx/main/tests/old/jobs/jobs_monolithic.py +++ b/awx/main/tests/old/jobs/jobs_monolithic.py @@ -803,6 +803,21 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase): self.assertEqual(job.hosts.count(), 1) self.assertEqual(job.hosts.all()[0], host) + # Create the job itself using URL-encoded form data instead of JSON. + result = self.post(url, data, expect=202, remote_addr=host_ip, data_type='form') + + # Establish that we got back what we expect, and made the changes + # that we expect. + self.assertTrue('Location' in result.response, result.response) + self.assertEqual(jobs_qs.count(), 2) + job = jobs_qs[0] + self.assertEqual(urlparse.urlsplit(result.response['Location']).path, + job.get_absolute_url()) + self.assertEqual(job.launch_type, 'callback') + self.assertEqual(job.limit, host.name) + self.assertEqual(job.hosts.count(), 1) + self.assertEqual(job.hosts.all()[0], host) + # Run the callback job again with extra vars and verify their presence data.update(dict(extra_vars=dict(key="value"))) result = self.post(url, data, expect=202, remote_addr=host_ip) @@ -853,9 +868,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase): if host_ip: break self.assertTrue(host) - self.assertEqual(jobs_qs.count(), 2) - self.post(url, data, expect=202, remote_addr=host_ip) self.assertEqual(jobs_qs.count(), 3) + self.post(url, data, expect=202, remote_addr=host_ip) + self.assertEqual(jobs_qs.count(), 4) job = jobs_qs[0] self.assertEqual(job.launch_type, 'callback') self.assertEqual(job.limit, host.name) @@ -878,9 +893,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase): if host_ip: break self.assertTrue(host) - self.assertEqual(jobs_qs.count(), 3) - self.post(url, data, expect=202, remote_addr=host_ip) self.assertEqual(jobs_qs.count(), 4) + self.post(url, data, expect=202, remote_addr=host_ip) + self.assertEqual(jobs_qs.count(), 5) job = jobs_qs[0] self.assertEqual(job.launch_type, 'callback') self.assertEqual(job.limit, host.name) @@ -892,9 +907,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase): host_qs = host_qs.filter(variables__icontains='ansible_ssh_host') host = host_qs[0] host_ip = host.variables_dict['ansible_ssh_host'] - self.assertEqual(jobs_qs.count(), 4) - self.post(url, data, expect=202, remote_addr=host_ip) self.assertEqual(jobs_qs.count(), 5) + self.post(url, data, expect=202, remote_addr=host_ip) + self.assertEqual(jobs_qs.count(), 6) job = jobs_qs[0] self.assertEqual(job.launch_type, 'callback') self.assertEqual(job.limit, host.name) @@ -926,9 +941,9 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase): host_ip = list(ips)[0] break self.assertTrue(host) - self.assertEqual(jobs_qs.count(), 5) - self.post(url, data, expect=202, remote_addr=host_ip) self.assertEqual(jobs_qs.count(), 6) + self.post(url, data, expect=202, remote_addr=host_ip) + self.assertEqual(jobs_qs.count(), 7) job = jobs_qs[0] self.assertEqual(job.launch_type, 'callback') self.assertEqual(job.limit, ':&'.join([job_template.limit, host.name])) From a35368f55884bfe12da98496107b949ad7d49944 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Fri, 4 Mar 2016 15:39:23 -0800 Subject: [PATCH 17/63] Adding initial notifications files --- awx/ui/client/src/app.js | 8 +- .../src/notifications/add/add.controller.js | 67 +++++++++++++ .../src/notifications/add/add.partial.html | 3 + .../client/src/notifications/add/add.route.js | 23 +++++ awx/ui/client/src/notifications/add/main.js | 15 +++ .../src/notifications/edit/edit.controller.js | 97 +++++++++++++++++++ .../src/notifications/edit/edit.partial.html | 3 + .../src/notifications/edit/edit.route.js | 23 +++++ awx/ui/client/src/notifications/edit/main.js | 15 +++ .../src/notifications/list/list.controller.js | 83 ++++++++++++++++ .../src/notifications/list/list.partial.html | 4 + .../src/notifications/list/list.route.js | 19 ++++ awx/ui/client/src/notifications/list/main.js | 15 +++ awx/ui/client/src/notifications/main.js | 22 +++++ .../src/notifications/notifications.form.js | 47 +++++++++ .../src/notifications/notifications.list.js | 63 ++++++++++++ .../src/setup-menu/setup-menu.partial.html | 6 ++ 17 files changed, 510 insertions(+), 3 deletions(-) create mode 100644 awx/ui/client/src/notifications/add/add.controller.js create mode 100644 awx/ui/client/src/notifications/add/add.partial.html create mode 100644 awx/ui/client/src/notifications/add/add.route.js create mode 100644 awx/ui/client/src/notifications/add/main.js create mode 100644 awx/ui/client/src/notifications/edit/edit.controller.js create mode 100644 awx/ui/client/src/notifications/edit/edit.partial.html create mode 100644 awx/ui/client/src/notifications/edit/edit.route.js create mode 100644 awx/ui/client/src/notifications/edit/main.js create mode 100644 awx/ui/client/src/notifications/list/list.controller.js create mode 100644 awx/ui/client/src/notifications/list/list.partial.html create mode 100644 awx/ui/client/src/notifications/list/list.route.js create mode 100644 awx/ui/client/src/notifications/list/main.js create mode 100644 awx/ui/client/src/notifications/main.js create mode 100644 awx/ui/client/src/notifications/notifications.form.js create mode 100644 awx/ui/client/src/notifications/notifications.list.js diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 3c12eadfb1..a37db8cbc4 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -30,6 +30,7 @@ import inventoryScripts from './inventory-scripts/main'; import permissions from './permissions/main'; import managementJobs from './management-jobs/main'; import jobDetail from './job-detail/main'; +import notifications from './notifications/main'; // modules import about from './about/main'; @@ -98,6 +99,7 @@ var tower = angular.module('Tower', [ activityStream.name, footer.name, jobDetail.name, + notifications.name, standardOut.name, 'templates', 'Utilities', @@ -882,13 +884,13 @@ var tower = angular.module('Tower', [ }]); }]) - .run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', '$state', 'CheckLicense', + .run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', '$state', 'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'Socket', 'LoadConfig', 'Store', 'ShowSocketHelp', 'pendoService', function ( - $q, $compile, $cookieStore, $rootScope, $log, $state, CheckLicense, + $q, $compile, $cookieStore, $rootScope, $log, $state, CheckLicense, $location, Authorization, LoadBasePaths, Timer, ClearScope, Socket, - LoadConfig, Store, ShowSocketHelp, pendoService) + LoadConfig, Store, ShowSocketHelp, pendoService) { var sock; diff --git a/awx/ui/client/src/notifications/add/add.controller.js b/awx/ui/client/src/notifications/add/add.controller.js new file mode 100644 index 0000000000..88048941ff --- /dev/null +++ b/awx/ui/client/src/notifications/add/add.controller.js @@ -0,0 +1,67 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default + [ '$rootScope', 'pagination', '$compile','SchedulerInit', 'Rest', 'Wait', + 'notificationsFormObject', 'ProcessErrors', 'GetBasePath', 'Empty', + 'GenerateForm', 'SearchInit' , 'PaginateInit', + 'LookUpInit', 'OrganizationList', '$scope', '$state', + function( + $rootScope, pagination, $compile, SchedulerInit, Rest, Wait, + notificationsFormObject, ProcessErrors, GetBasePath, Empty, + GenerateForm, SearchInit, PaginateInit, + LookUpInit, OrganizationList, $scope, $state + ) { + var scope = $scope, + generator = GenerateForm, + form = notificationsFormObject, + url = GetBasePath('notifications'); + + generator.inject(form, { + mode: 'add' , + scope:scope, + related: false + }); + generator.reset(); + + LookUpInit({ + url: GetBasePath('organization'), + scope: scope, + form: form, + list: OrganizationList, + field: 'organization', + input_type: 'radio' + }); + + // Save + scope.formSave = function () { + + generator.clearApiErrors(); + Wait('start'); + Rest.setUrl(url); + Rest.post({ + name: scope.name, + description: scope.description, + organization: scope.organization, + script: scope.script + }) + .success(function (data) { + $rootScope.addedItem = data.id; + $state.go('inventoryScripts', {}, {reload: true}); + Wait('stop'); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to add new inventory script. POST returned status: ' + status }); + }); + }; + + scope.formCancel = function () { + $state.transitionTo('inventoryScripts'); + }; + + } + ]; diff --git a/awx/ui/client/src/notifications/add/add.partial.html b/awx/ui/client/src/notifications/add/add.partial.html new file mode 100644 index 0000000000..65bfebbbe6 --- /dev/null +++ b/awx/ui/client/src/notifications/add/add.partial.html @@ -0,0 +1,3 @@ +
+
+
diff --git a/awx/ui/client/src/notifications/add/add.route.js b/awx/ui/client/src/notifications/add/add.route.js new file mode 100644 index 0000000000..6bad062844 --- /dev/null +++ b/awx/ui/client/src/notifications/add/add.route.js @@ -0,0 +1,23 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import {templateUrl} from '../../shared/template-url/template-url.factory'; + +export default { + name: 'notifications.add', + route: '/add', + templateUrl: templateUrl('notifications/add/add'), + controller: 'notificationsAddController', + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }] + }, + ncyBreadcrumb: { + parent: 'notifications', + label: 'Create Notification' + } +}; diff --git a/awx/ui/client/src/notifications/add/main.js b/awx/ui/client/src/notifications/add/main.js new file mode 100644 index 0000000000..f3101e402f --- /dev/null +++ b/awx/ui/client/src/notifications/add/main.js @@ -0,0 +1,15 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import route from './add.route'; +import controller from './add.controller'; + +export default + angular.module('notificationsAdd', []) + .controller('notificationsAddController', controller) + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(route); + }]); diff --git a/awx/ui/client/src/notifications/edit/edit.controller.js b/awx/ui/client/src/notifications/edit/edit.controller.js new file mode 100644 index 0000000000..a14b3334af --- /dev/null +++ b/awx/ui/client/src/notifications/edit/edit.controller.js @@ -0,0 +1,97 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default + [ 'Rest', 'Wait', + 'notificationsFormObject', 'ProcessErrors', 'GetBasePath', + 'GenerateForm', 'SearchInit' , 'PaginateInit', + 'LookUpInit', 'OrganizationList', 'inventory_script', + '$scope', '$state', + function( + Rest, Wait, + notificationsFormObject, ProcessErrors, GetBasePath, + GenerateForm, SearchInit, PaginateInit, + LookUpInit, OrganizationList, inventory_script, + $scope, $state + ) { + var generator = GenerateForm, + id = inventory_script.id, + form = notificationsFormObject, + master = {}, + url = GetBasePath('notifications'); + + $scope.inventory_script = inventory_script; + generator.inject(form, { + mode: 'edit' , + scope:$scope, + related: false, + activityStream: false + }); + generator.reset(); + LookUpInit({ + url: GetBasePath('organization'), + scope: $scope, + form: form, + // hdr: "Select Custom Inventory", + list: OrganizationList, + field: 'organization', + input_type: 'radio' + }); + + // Retrieve detail record and prepopulate the form + Wait('start'); + Rest.setUrl(url + id+'/'); + Rest.get() + .success(function (data) { + var fld; + for (fld in form.fields) { + if (data[fld]) { + $scope[fld] = data[fld]; + master[fld] = data[fld]; + } + + if (form.fields[fld].sourceModel && data.summary_fields && + data.summary_fields[form.fields[fld].sourceModel]) { + $scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; + master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; + } + } + Wait('stop'); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to retrieve inventory script: ' + id + '. GET status: ' + status }); + }); + + $scope.formSave = function () { + generator.clearApiErrors(); + Wait('start'); + Rest.setUrl(url+ id+'/'); + Rest.put({ + name: $scope.name, + description: $scope.description, + organization: $scope.organization, + script: $scope.script + }) + .success(function () { + $state.transitionTo('inventoryScriptsList'); + Wait('stop'); + + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to add new inventory script. PUT returned status: ' + status }); + }); + }; + + $scope.formCancel = function () { + $state.transitionTo('inventoryScripts'); + }; + + } + ]; diff --git a/awx/ui/client/src/notifications/edit/edit.partial.html b/awx/ui/client/src/notifications/edit/edit.partial.html new file mode 100644 index 0000000000..ebd7e80e80 --- /dev/null +++ b/awx/ui/client/src/notifications/edit/edit.partial.html @@ -0,0 +1,3 @@ +
+
+
diff --git a/awx/ui/client/src/notifications/edit/edit.route.js b/awx/ui/client/src/notifications/edit/edit.route.js new file mode 100644 index 0000000000..1987b32cad --- /dev/null +++ b/awx/ui/client/src/notifications/edit/edit.route.js @@ -0,0 +1,23 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import {templateUrl} from '../../shared/template-url/template-url.factory'; + +export default { + name: 'notifications.edit', + route: '/edit', + templateUrl: templateUrl('notifications/edit/edit'), + controller: 'notificationsEditController', + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }] + }, + ncyBreadcrumb: { + parent: 'notifications', + label: 'Edit Notification' + } +}; diff --git a/awx/ui/client/src/notifications/edit/main.js b/awx/ui/client/src/notifications/edit/main.js new file mode 100644 index 0000000000..8f0c62d7a1 --- /dev/null +++ b/awx/ui/client/src/notifications/edit/main.js @@ -0,0 +1,15 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import route from './edit.route'; +import controller from './edit.controller'; + +export default + angular.module('notificationsEdit', []) + .controller('notificationsEditController', controller) + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(route); + }]); diff --git a/awx/ui/client/src/notifications/list/list.controller.js b/awx/ui/client/src/notifications/list/list.controller.js new file mode 100644 index 0000000000..df0ef9f7d2 --- /dev/null +++ b/awx/ui/client/src/notifications/list/list.controller.js @@ -0,0 +1,83 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default + [ '$rootScope','Wait', 'generateList', 'notificationsListObject', + 'GetBasePath' , 'SearchInit' , 'PaginateInit', + 'Rest' , 'ProcessErrors', 'Prompt', '$state', + function( + $rootScope,Wait, GenerateList, notificationsListObject, + GetBasePath, SearchInit, PaginateInit, + Rest, ProcessErrors, Prompt, $state + ) { + var scope = $rootScope.$new(), + defaultUrl = GetBasePath('notifications'), + list = notificationsListObject, + view = GenerateList; + + view.inject( list, { + mode: 'edit', + scope: scope + }); + + // SearchInit({ + // scope: scope, + // set: 'notifications', + // list: list, + // url: defaultUrl + // }); + // + // if ($rootScope.addedItem) { + // scope.addedItem = $rootScope.addedItem; + // delete $rootScope.addedItem; + // } + // PaginateInit({ + // scope: scope, + // list: list, + // url: defaultUrl + // }); + // + // scope.search(list.iterator); + + scope.editNotification = function(){ + $state.transitionTo('notifications.edit',{ + inventory_script_id: this.inventory_script.id, + inventory_script: this.inventory_script + }); + }; + + scope.deleteNotification = function(id, name){ + + var action = function () { + $('#prompt-modal').modal('hide'); + Wait('start'); + var url = defaultUrl + id + '/'; + Rest.setUrl(url); + Rest.destroy() + .success(function () { + scope.search(list.iterator); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); + }); + }; + + var bodyHtml = '
Are you sure you want to delete the inventory script below?
' + name + '
'; + Prompt({ + hdr: 'Delete', + body: bodyHtml, + action: action, + actionText: 'DELETE' + }); + }; + + scope.addNotification = function(){ + $state.transitionTo('notifications.add'); + }; + + } + ]; diff --git a/awx/ui/client/src/notifications/list/list.partial.html b/awx/ui/client/src/notifications/list/list.partial.html new file mode 100644 index 0000000000..bad2fa18bb --- /dev/null +++ b/awx/ui/client/src/notifications/list/list.partial.html @@ -0,0 +1,4 @@ +
+
+
+
diff --git a/awx/ui/client/src/notifications/list/list.route.js b/awx/ui/client/src/notifications/list/list.route.js new file mode 100644 index 0000000000..da76880791 --- /dev/null +++ b/awx/ui/client/src/notifications/list/list.route.js @@ -0,0 +1,19 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import {templateUrl} from '../../shared/template-url/template-url.factory'; + +export default { + name: 'notifications', + route: '/notifications', + templateUrl: templateUrl('notifications/list/list'), + controller: 'notificationsListController', + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }] + } +}; diff --git a/awx/ui/client/src/notifications/list/main.js b/awx/ui/client/src/notifications/list/main.js new file mode 100644 index 0000000000..35cab03cef --- /dev/null +++ b/awx/ui/client/src/notifications/list/main.js @@ -0,0 +1,15 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import route from './list.route'; +import controller from './list.controller'; + +export default + angular.module('notificationsList', []) + .controller('notificationsListController', controller) + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(route); + }]); diff --git a/awx/ui/client/src/notifications/main.js b/awx/ui/client/src/notifications/main.js new file mode 100644 index 0000000000..147b3b8479 --- /dev/null +++ b/awx/ui/client/src/notifications/main.js @@ -0,0 +1,22 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + +import notificationsList from './list/main'; +import notificationsAdd from './add/main'; +import notificationsEdit from './edit/main'; + +import list from './notifications.list'; +import form from './notifications.form'; + +export default + angular.module('notifications', [ + notificationsList.name, + notificationsAdd.name, + notificationsEdit.name + ]) + .factory('notificationsListObject', list) + .factory('notificationsFormObject', form); diff --git a/awx/ui/client/src/notifications/notifications.form.js b/awx/ui/client/src/notifications/notifications.form.js new file mode 100644 index 0000000000..d8c49d00ba --- /dev/null +++ b/awx/ui/client/src/notifications/notifications.form.js @@ -0,0 +1,47 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + /** + * @ngdoc function + * @name forms.function:CustomInventory + * @description This form is for adding/editing an organization +*/ + +export default function() { + return { + + addTitle: 'New Notification', + editTitle: '{{ name }}', + name: 'notification', + showActions: true, + + fields: { + name: { + label: 'Name', + type: 'text', + addRequired: true, + editRequired: true, + capitalize: false + }, + description: { + label: 'Description', + type: 'text', + addRequired: false, + editRequired: false + } + }, + + buttons: { //for now always generates
- -
EXTRA VARS
-
- {{ job.extra_vars }} -
+
+ +
+
diff --git a/awx/ui/client/src/standard-out/standard-out.controller.js b/awx/ui/client/src/standard-out/standard-out.controller.js index 152d2a8a5d..83345d7132 100644 --- a/awx/ui/client/src/standard-out/standard-out.controller.js +++ b/awx/ui/client/src/standard-out/standard-out.controller.js @@ -11,7 +11,7 @@ */ -export function JobStdoutController ($rootScope, $scope, $state, $stateParams, ClearScope, GetBasePath, Rest, ProcessErrors, Empty, GetChoices, LookUpName) { +export function JobStdoutController ($rootScope, $scope, $state, $stateParams, ClearScope, GetBasePath, Rest, ProcessErrors, Empty, GetChoices, LookUpName, ParseTypeChange, ParseVariableString) { ClearScope(); @@ -36,6 +36,9 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, C // timestamp as well as the run time. }); + // Set the parse type so that CodeMirror knows how to display extra params YAML/JSON + $scope.parseType = 'yaml'; + // Go out and get the job details based on the job type. jobType gets defined // in the data block of the route declaration for each of the different types // of stdout jobs. @@ -132,6 +135,11 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, C }); } + if (!Empty(data.extra_vars)) { + $scope.variables = ParseVariableString(data.extra_vars); + ParseTypeChange({ scope: $scope, field_id: 'pre-formatted-variables' }); + } + // If the job isn't running we want to clear out the interval that goes out and checks for stdout updates. // This interval is defined in the standard out log directive controller. if (data.status === 'successful' || data.status === 'failed' || data.status === 'error' || data.status === 'canceled') { @@ -158,4 +166,4 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, C } -JobStdoutController.$inject = [ '$rootScope', '$scope', '$state', '$stateParams', 'ClearScope', 'GetBasePath', 'Rest', 'ProcessErrors', 'Empty', 'GetChoices', 'LookUpName']; +JobStdoutController.$inject = [ '$rootScope', '$scope', '$state', '$stateParams', 'ClearScope', 'GetBasePath', 'Rest', 'ProcessErrors', 'Empty', 'GetChoices', 'LookUpName', 'ParseTypeChange', 'ParseVariableString']; From 1bc3657fe7cce16e00f7529ba756dceef25b6576 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Mon, 7 Mar 2016 10:46:11 -0500 Subject: [PATCH 19/63] fixes demo feedback bugs in #1112 --- awx/ui/client/legacy-styles/ansible-ui.less | 3 +- awx/ui/client/src/about/about.controller.js | 34 +++++++++---------- awx/ui/client/src/about/about.partial.html | 2 +- awx/ui/client/src/app.js | 2 +- awx/ui/client/src/footer/footer.block.less | 3 +- awx/ui/client/src/license/license.block.less | 13 +++++++ .../client/src/license/license.controller.js | 24 ++++++++++--- .../client/src/license/license.partial.html | 22 ++++++------ .../client/src/shared/bootstrap-settings.less | 22 ++++++++++++ .../src/shared/layouts/one-plus-two.less | 1 + awx/ui/templates/ui/index.html | 2 -- 11 files changed, 89 insertions(+), 39 deletions(-) create mode 100644 awx/ui/client/src/shared/bootstrap-settings.less diff --git a/awx/ui/client/legacy-styles/ansible-ui.less b/awx/ui/client/legacy-styles/ansible-ui.less index 4adaeb09eb..717ab7bd30 100644 --- a/awx/ui/client/legacy-styles/ansible-ui.less +++ b/awx/ui/client/legacy-styles/ansible-ui.less @@ -44,7 +44,8 @@ @import "text-label.less"; @import "./bootstrap-datepicker.less"; @import "awx/ui/client/src/shared/branding/colors.default.less"; - +// Bootstrap default overrides +@import "awx/ui/client/src/shared/bootstrap-settings.less"; /* Bootstrap fix that's causing a right margin to appear whenver a modal is opened */ body.modal-open { diff --git a/awx/ui/client/src/about/about.controller.js b/awx/ui/client/src/about/about.controller.js index c35388e8ae..07bad2dfd6 100644 --- a/awx/ui/client/src/about/about.controller.js +++ b/awx/ui/client/src/about/about.controller.js @@ -1,20 +1,20 @@ export default ['$scope', '$state', 'CheckLicense', function($scope, $state, CheckLicense){ var processVersion = function(version){ - // prettify version & calculate padding - // e,g 3.0.0-0.git201602191743/ -> 3.0.0 - var split = version.split('-')[0] - var spaces = Math.floor((16-split.length)/2), - paddedStr = ""; - for(var i=0; i<=spaces; i++){ - paddedStr = paddedStr +" "; - } - paddedStr = paddedStr + split; - for(var j = paddedStr.length; j<16; j++){ - paddedStr = paddedStr + " "; - } - return paddedStr - } + // prettify version & calculate padding + // e,g 3.0.0-0.git201602191743/ -> 3.0.0 + var split = version.split('-')[0] + var spaces = Math.floor((16-split.length)/2), + paddedStr = ""; + for(var i=0; i<=spaces; i++){ + paddedStr = paddedStr +" "; + } + paddedStr = paddedStr + split; + for(var j = paddedStr.length; j<16; j++){ + paddedStr = paddedStr + " "; + } + return paddedStr + }; var init = function(){ CheckLicense.get() .then(function(res){ @@ -23,9 +23,9 @@ export default $('#about-modal').modal('show'); }); }; - var back = function(){ - $state.go('setup'); - } + $('#about-modal').on('hidden.bs.modal', function () { + $state.go('setup'); + }); init(); } ]; \ No newline at end of file diff --git a/awx/ui/client/src/about/about.partial.html b/awx/ui/client/src/about/about.partial.html index afc66724f4..44d4914402 100644 --- a/awx/ui/client/src/about/about.partial.html +++ b/awx/ui/client/src/about/about.partial.html @@ -3,7 +3,7 @@
Version
- {{license.version}} + {{license.version || "No result found"}}
License Type
- {{license.license_info.license_type}} + {{license.license_info.license_type || "No result found"}}
Subscription
- {{license.license_info.subscription_name}} + {{license.license_info.subscription_name || "No result found"}}
License Key
- {{license.license_info.license_key}} + {{license.license_info.license_key || "No result found"}}
@@ -43,25 +43,25 @@
Time Remaining
- {{time.remaining}} Day + {{time.remaining}} Days
Hosts Available
- {{license.license_info.available_instances}} + {{license.license_info.available_instances || "No result found"}}
Hosts Used
- {{license.license_info.current_instances}} + {{license.license_info.current_instances || "No result found"}}
Hosts Remaining
- {{license.license_info.free_instances}} + {{license.license_info.free_instances || "No result found"}}
@@ -76,7 +76,7 @@
Browse... - +
End User License Agreement
@@ -88,7 +88,7 @@
I agree to the End User License Agreement
- Save successful! + Save successful!
diff --git a/awx/ui/client/src/shared/bootstrap-settings.less b/awx/ui/client/src/shared/bootstrap-settings.less new file mode 100644 index 0000000000..c0c300c74a --- /dev/null +++ b/awx/ui/client/src/shared/bootstrap-settings.less @@ -0,0 +1,22 @@ +@import "awx/ui/client/src/shared/branding/colors.default.less"; +.btn-success{ + background: @default-succ; + border-color: transparent; + :hover{ + background: @default-succ-hov; + } + :disabled{ + background: @default-succ-disabled; + } +} +.btn-default{ + background: @btn-bg; + border-color: @btn-bord; + color: @btn-txt; + :hover{ + background: @btn-bg-hov; + } + :focus{ + background: @btn-bg-sel; + } +} \ No newline at end of file diff --git a/awx/ui/client/src/shared/layouts/one-plus-two.less b/awx/ui/client/src/shared/layouts/one-plus-two.less index fe43e40b65..e1e1653e8b 100644 --- a/awx/ui/client/src/shared/layouts/one-plus-two.less +++ b/awx/ui/client/src/shared/layouts/one-plus-two.less @@ -74,6 +74,7 @@ .OnePlusTwo-left--detailsContent { display: inline-block; max-width: 220px; + width: 220px; word-wrap: break-word; } diff --git a/awx/ui/templates/ui/index.html b/awx/ui/templates/ui/index.html index 79111c287d..6b5466b98d 100644 --- a/awx/ui/templates/ui/index.html +++ b/awx/ui/templates/ui/index.html @@ -154,8 +154,6 @@ - -
-
+
EXTRA VARS
diff --git a/awx/ui/client/src/standard-out/standard-out.block.less b/awx/ui/client/src/standard-out/standard-out.block.less index 8d0e8a0219..86621c3536 100644 --- a/awx/ui/client/src/standard-out/standard-out.block.less +++ b/awx/ui/client/src/standard-out/standard-out.block.less @@ -46,6 +46,10 @@ margin-bottom: 20px; } +.StandardOut-detailsRow--extraVars { + margin-bottom: 10px; +} + .StandardOut-detailsLabel { width: 130px; flex: 0 0 130px; From e5b50f182efa888c1cfac1c2543d50d00928a3aa Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Mon, 7 Mar 2016 11:36:28 -0500 Subject: [PATCH 22/63] remove ref to removed function #1112 --- awx/ui/client/src/about/about.partial.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/about/about.partial.html b/awx/ui/client/src/about/about.partial.html index 44d4914402..8d0c355e5c 100644 --- a/awx/ui/client/src/about/about.partial.html +++ b/awx/ui/client/src/about/about.partial.html @@ -3,7 +3,7 @@
- + From 84d7f6c13fba75e30653ac5c094fa0ca72e7a1d8 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 9 Mar 2016 14:24:14 -0500 Subject: [PATCH 26/63] Moved job template routes, controllers, and views out into its own module. --- awx/ui/client/src/app.js | 72 +- awx/ui/client/src/controllers/JobTemplates.js | 1273 ----------------- .../add/inventory-job-templates-add.route.js | 19 + .../add/job-templates-add.controller.js | 452 ++++++ .../add/job-templates-add.partial.html | 5 + .../add/job-templates-add.route.js | 23 + awx/ui/client/src/job-templates/add/main.js | 17 + .../inventory-job-templates-edit.route.js | 22 + .../edit/job-templates-edit.controller.js | 596 ++++++++ .../edit/job-templates-edit.partial.html | 5 + .../edit/job-templates-edit.route.js | 22 + awx/ui/client/src/job-templates/edit/main.js | 17 + .../list/job-templates-list.controller.js | 241 ++++ .../list/job-templates-list.partial.html} | 1 - .../list/job-templates-list.route.js | 26 + awx/ui/client/src/job-templates/list/main.js | 15 + awx/ui/client/src/job-templates/main.js | 8 +- .../survey-maker/surveys/show.factory.js | 47 +- 18 files changed, 1490 insertions(+), 1371 deletions(-) delete mode 100644 awx/ui/client/src/controllers/JobTemplates.js create mode 100644 awx/ui/client/src/job-templates/add/inventory-job-templates-add.route.js create mode 100644 awx/ui/client/src/job-templates/add/job-templates-add.controller.js create mode 100644 awx/ui/client/src/job-templates/add/job-templates-add.partial.html create mode 100644 awx/ui/client/src/job-templates/add/job-templates-add.route.js create mode 100644 awx/ui/client/src/job-templates/add/main.js create mode 100644 awx/ui/client/src/job-templates/edit/inventory-job-templates-edit.route.js create mode 100644 awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js create mode 100644 awx/ui/client/src/job-templates/edit/job-templates-edit.partial.html create mode 100644 awx/ui/client/src/job-templates/edit/job-templates-edit.route.js create mode 100644 awx/ui/client/src/job-templates/edit/main.js create mode 100644 awx/ui/client/src/job-templates/list/job-templates-list.controller.js rename awx/ui/client/src/{partials/job_templates.html => job-templates/list/job-templates-list.partial.html} (95%) create mode 100644 awx/ui/client/src/job-templates/list/job-templates-list.route.js create mode 100644 awx/ui/client/src/job-templates/list/main.js diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index a23e803de2..cbf50b22b6 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -47,7 +47,7 @@ import login from './login/main'; import activityStream from './activity-stream/main'; import standardOut from './standard-out/main'; import lookUpHelper from './lookup/main'; -import {JobTemplatesList, JobTemplatesAdd, JobTemplatesEdit} from './controllers/JobTemplates'; +import JobTemplates from './job-templates/main'; import {ScheduleEditController} from './controllers/Schedules'; import {ProjectsList, ProjectsAdd, ProjectsEdit} from './controllers/Projects'; import {OrganizationsList, OrganizationsAdd, OrganizationsEdit} from './controllers/Organizations'; @@ -65,7 +65,6 @@ import './shared/directives'; import './shared/filters'; import './shared/InventoryTree'; import './shared/Socket'; -import './job-templates/main'; import './shared/features/main'; import './login/authenticationServices/pendo/ng-pendo'; import footer from './footer/main'; @@ -101,6 +100,7 @@ var tower = angular.module('Tower', [ jobDetail.name, notifications.name, standardOut.name, + JobTemplates.name, 'templates', 'Utilities', 'OrganizationFormDefinition', @@ -297,52 +297,6 @@ var tower = angular.module('Tower', [ } }). - state('jobTemplates', { - url: '/job_templates', - templateUrl: urlPrefix + 'partials/job_templates.html', - controller: JobTemplatesList, - data: { - activityStream: true, - activityStreamTarget: 'job_template' - }, - ncyBreadcrumb: { - label: "JOB TEMPLATES" - }, - resolve: { - features: ['FeaturesService', function(FeaturesService) { - return FeaturesService.get(); - }] - } - }). - - state('jobTemplates.add', { - url: '/add', - templateUrl: urlPrefix + 'partials/job_templates.html', - controller: JobTemplatesAdd, - ncyBreadcrumb: { - parent: "jobTemplates", - label: "CREATE JOB TEMPLATE" - }, - resolve: { - features: ['FeaturesService', function(FeaturesService) { - return FeaturesService.get(); - }] - } - }). - - state('jobTemplates.edit', { - url: '/:template_id', - templateUrl: urlPrefix + 'partials/job_templates.html', - controller: JobTemplatesEdit, - data: { - activityStreamId: 'template_id' - }, - resolve: { - features: ['FeaturesService', function(FeaturesService) { - return FeaturesService.get(); - }] - } - }). state('projects', { url: '/projects', templateUrl: urlPrefix + 'partials/projects.html', @@ -458,28 +412,6 @@ var tower = angular.module('Tower', [ } }). - state('inventoryJobTemplateAdd', { - url: '/inventories/:inventory_id/job_templates/add', - templateUrl: urlPrefix + 'partials/job_templates.html', - controller: JobTemplatesAdd, - resolve: { - features: ['FeaturesService', function(FeaturesService) { - return FeaturesService.get(); - }] - } - }). - - state('inventoryJobTemplateEdit', { - url: '/inventories/:inventory_id/job_templates/:template_id', - templateUrl: urlPrefix + 'partials/job_templates.html', - controller: JobTemplatesEdit, - resolve: { - features: ['FeaturesService', function(FeaturesService) { - return FeaturesService.get(); - }] - } - }). - state('inventoryManage', { url: '/inventories/:inventory_id/manage?groups', templateUrl: urlPrefix + 'partials/inventory-manage.html', diff --git a/awx/ui/client/src/controllers/JobTemplates.js b/awx/ui/client/src/controllers/JobTemplates.js deleted file mode 100644 index b01dc04a48..0000000000 --- a/awx/ui/client/src/controllers/JobTemplates.js +++ /dev/null @@ -1,1273 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name controllers.function:JobTemplate - * @description This controller's for the Job Template page -*/ - - -export function JobTemplatesList($scope, $rootScope, $location, $log, - $stateParams, Rest, Alert, JobTemplateList, GenerateList, Prompt, - SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, - GetBasePath, JobTemplateForm, CredentialList, LookUpInit, PlaybookRun, - Wait, CreateDialog, $compile, $state) { - - ClearScope(); - - var list = JobTemplateList, - defaultUrl = GetBasePath('job_templates'), - view = GenerateList, - base = $location.path().replace(/^\//, '').split('/')[0], - mode = (base === 'job_templates') ? 'edit' : 'select'; - - view.inject(list, { mode: mode, scope: $scope }); - $rootScope.flashMessage = null; - - if ($scope.removePostRefresh) { - $scope.removePostRefresh(); - } - $scope.removePostRefresh = $scope.$on('PostRefresh', function () { - // Cleanup after a delete - Wait('stop'); - $('#prompt-modal').modal('hide'); - }); - - SearchInit({ - scope: $scope, - set: 'job_templates', - list: list, - url: defaultUrl - }); - PaginateInit({ - scope: $scope, - list: list, - url: defaultUrl - }); - - // Called from Inventories tab, host failed events link: - if ($stateParams.name) { - $scope[list.iterator + 'SearchField'] = 'name'; - $scope[list.iterator + 'SearchValue'] = $stateParams.name; - $scope[list.iterator + 'SearchFieldLabel'] = list.fields.name.label; - } - - $scope.search(list.iterator); - - $scope.addJobTemplate = function () { - $state.transitionTo('jobTemplates.add'); - }; - - $scope.editJobTemplate = function (id) { - $state.transitionTo('jobTemplates.edit', {template_id: id}); - }; - - $scope.deleteJobTemplate = function (id, name) { - var action = function () { - $('#prompt-modal').modal('hide'); - Wait('start'); - var url = defaultUrl + id + '/'; - Rest.setUrl(url); - Rest.destroy() - .success(function () { - $scope.search(list.iterator); - }) - .error(function (data) { - Wait('stop'); - ProcessErrors($scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); - }); - }; - - Prompt({ - hdr: 'Delete', - body: '
Are you sure you want to delete the job template below?
' + name + '
', - action: action, - actionText: 'DELETE' - }); - }; - - $scope.copyJobTemplate = function(id, name){ - var element, - buttons = [{ - "label": "Cancel", - "onClick": function() { - $(this).dialog('close'); - }, - "icon": "fa-times", - "class": "btn btn-default", - "id": "copy-close-button" - },{ - "label": "Copy", - "onClick": function() { - copyAction(); - // setTimeout(function(){ - // scope.$apply(function(){ - // if(mode==='survey-taker'){ - // scope.$emit('SurveyTakerCompleted'); - // } else{ - // scope.saveSurvey(); - // } - // }); - // }); - }, - "icon": "fa-copy", - "class": "btn btn-primary", - "id": "job-copy-button" - }], - copyAction = function () { - // retrieve the copy of the job template object from the api, then overwrite the name and throw away the id - Wait('start'); - var url = defaultUrl + id + '/'; - Rest.setUrl(url); - Rest.get() - .success(function (data) { - data.name = $scope.new_copy_name; - delete data.id; - $scope.$emit('GoToCopy', data); - }) - .error(function (data) { - Wait('stop'); - ProcessErrors($scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); - }); - }; - - - CreateDialog({ - id: 'copy-job-modal', - title: "Copy", - scope: $scope, - buttons: buttons, - width: 500, - height: 300, - minWidth: 200, - callback: 'CopyDialogReady' - }); - - $('#job_name').text(name); - $('#copy-job-modal').show(); - - - if ($scope.removeCopyDialogReady) { - $scope.removeCopyDialogReady(); - } - $scope.removeCopyDialogReady = $scope.$on('CopyDialogReady', function() { - //clear any old remaining text - $scope.new_copy_name = "" ; - $scope.copy_form.$setPristine(); - $('#copy-job-modal').dialog('open'); - $('#job-copy-button').attr('ng-disabled', "!copy_form.$valid"); - element = angular.element(document.getElementById('job-copy-button')); - $compile(element)($scope); - - }); - - if ($scope.removeGoToCopy) { - $scope.removeGoToCopy(); - } - $scope.removeGoToCopy = $scope.$on('GoToCopy', function(e, data) { - var url = defaultUrl, - old_survey_url = (data.related.survey_spec) ? data.related.survey_spec : "" ; - Rest.setUrl(url); - Rest.post(data) - .success(function (data) { - if(data.survey_enabled===true){ - $scope.$emit("CopySurvey", data, old_survey_url); - } - else { - $('#copy-job-modal').dialog('close'); - Wait('stop'); - $location.path($location.path() + '/' + data.id); - } - - }) - .error(function (data) { - Wait('stop'); - ProcessErrors($scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); - }); - }); - - if ($scope.removeCopySurvey) { - $scope.removeCopySurvey(); - } - $scope.removeCopySurvey = $scope.$on('CopySurvey', function(e, new_data, old_url) { - // var url = data.related.survey_spec; - Rest.setUrl(old_url); - Rest.get() - .success(function (survey_data) { - - Rest.setUrl(new_data.related.survey_spec); - Rest.post(survey_data) - .success(function () { - $('#copy-job-modal').dialog('close'); - Wait('stop'); - $location.path($location.path() + '/' + new_data.id); - }) - .error(function (data) { - Wait('stop'); - ProcessErrors($scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + new_data.related.survey_spec + ' failed. DELETE returned status: ' + status }); - }); - - - }) - .error(function (data) { - Wait('stop'); - ProcessErrors($scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + old_url + ' failed. DELETE returned status: ' + status }); - }); - - }); - - }; - - $scope.submitJob = function (id) { - PlaybookRun({ scope: $scope, id: id }); - }; - - $scope.scheduleJob = function (id) { - $state.go('jobTemplateSchedules', {id: id}); - }; -} - -JobTemplatesList.$inject = ['$scope', '$rootScope', '$location', '$log', - '$stateParams', 'Rest', 'Alert', 'JobTemplateList', 'generateList', - 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', - 'ProcessErrors', 'GetBasePath', 'JobTemplateForm', 'CredentialList', - 'LookUpInit', 'PlaybookRun', 'Wait', 'CreateDialog' , '$compile', - '$state' -]; - -export function JobTemplatesAdd(Refresh, $filter, $scope, $rootScope, $compile, - $location, $log, $stateParams, JobTemplateForm, GenerateForm, Rest, Alert, - ProcessErrors, ReturnToCaller, ClearScope, GetBasePath, InventoryList, - CredentialList, ProjectList, LookUpInit, md5Setup, ParseTypeChange, Wait, - Empty, ToJSON, CallbackHelpInit, SurveyControllerInit, Prompt, GetChoices, - $state, CreateSelect2) { - - ClearScope(); - - // Inject dynamic view - var defaultUrl = GetBasePath('job_templates'), - form = JobTemplateForm(), - generator = GenerateForm, - master = {}, - CloudCredentialList = {}, - selectPlaybook, checkSCMStatus, - callback, - base = $location.path().replace(/^\//, '').split('/')[0], - context = (base === 'job_templates') ? 'job_template' : 'inv'; - - CallbackHelpInit({ scope: $scope }); - $scope.can_edit = true; - generator.inject(form, { mode: 'add', related: false, scope: $scope }); - - callback = function() { - // Make sure the form controller knows there was a change - $scope[form.name + '_form'].$setDirty(); - }; - $scope.mode = "add"; - $scope.parseType = 'yaml'; - ParseTypeChange({ scope: $scope, field_id: 'job_templates_variables', onChange: callback }); - - $scope.playbook_options = []; - $scope.allow_callbacks = false; - - generator.reset(); - - md5Setup({ - scope: $scope, - master: master, - check_field: 'allow_callbacks', - default_val: false - }); - - LookUpInit({ - scope: $scope, - form: form, - current_item: ($stateParams.inventory_id !== undefined) ? $stateParams.inventory_id : null, - list: InventoryList, - field: 'inventory', - input_type: "radio" - }); - - - // Clone the CredentialList object for use with cloud_credential. Cloning - // and changing properties to avoid collision. - jQuery.extend(true, CloudCredentialList, CredentialList); - CloudCredentialList.name = 'cloudcredentials'; - CloudCredentialList.iterator = 'cloudcredential'; - - SurveyControllerInit({ - scope: $scope, - parent_scope: $scope - }); - - if ($scope.removeLookUpInitialize) { - $scope.removeLookUpInitialize(); - } - $scope.removeLookUpInitialize = $scope.$on('lookUpInitialize', function () { - LookUpInit({ - url: GetBasePath('credentials') + '?cloud=true', - scope: $scope, - form: form, - current_item: null, - list: CloudCredentialList, - field: 'cloud_credential', - hdr: 'Select Cloud Credential', - input_type: 'radio' - }); - - LookUpInit({ - url: GetBasePath('credentials') + '?kind=ssh', - scope: $scope, - form: form, - current_item: null, - list: CredentialList, - field: 'credential', - hdr: 'Select Machine Credential', - input_type: "radio" - }); - }); - - var selectCount = 0; - - if ($scope.removeChoicesReady) { - $scope.removeChoicesReady(); - } - $scope.removeChoicesReady = $scope.$on('choicesReadyVerbosity', function () { - selectCount++; - if (selectCount === 2) { - var verbosity; - // this sets the default options for the selects as specified by the controller. - for (verbosity in $scope.verbosity_options) { - if ($scope.verbosity_options[verbosity].isDefault) { - $scope.verbosity = $scope.verbosity_options[verbosity]; - } - } - $scope.job_type = $scope.job_type_options[$scope.job_type_field.default]; - - // if you're getting to the form from the scan job section on inventories, - // set the job type select to be scan - if ($stateParams.inventory_id) { - // This means that the job template form was accessed via inventory prop's - // This also means the job is a scan job. - $scope.job_type.value = 'scan'; - $scope.jobTypeChange(); - $scope.inventory = $stateParams.inventory_id; - Rest.setUrl(GetBasePath('inventory') + $stateParams.inventory_id + '/'); - Rest.get() - .success(function (data) { - $scope.inventory_name = data.name; - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to lookup inventory: ' + data.id + '. GET returned status: ' + status }); - }); - } - CreateSelect2({ - element:'#job_templates_job_type', - multiple: false - }); - - CreateSelect2({ - element:'#playbook-select', - multiple: false - }); - - CreateSelect2({ - element:'#job_templates_verbosity', - multiple: false - }); - - $scope.$emit('lookUpInitialize'); - } - }); - - // setup verbosity options select - GetChoices({ - scope: $scope, - url: defaultUrl, - field: 'verbosity', - variable: 'verbosity_options', - callback: 'choicesReadyVerbosity' - }); - - // setup job type options select - GetChoices({ - scope: $scope, - url: defaultUrl, - field: 'job_type', - variable: 'job_type_options', - callback: 'choicesReadyVerbosity' - }); - - // Update playbook select whenever project value changes - selectPlaybook = function (oldValue, newValue) { - var url; - if($scope.job_type.value === 'scan' && $scope.project_name === "Default"){ - $scope.playbook_options = ['Default']; - $scope.playbook = 'Default'; - Wait('stop'); - } - else if (oldValue !== newValue) { - if ($scope.project) { - Wait('start'); - url = GetBasePath('projects') + $scope.project + '/playbooks/'; - Rest.setUrl(url); - Rest.get() - .success(function (data) { - var i, opts = []; - for (i = 0; i < data.length; i++) { - opts.push(data[i]); - } - $scope.playbook_options = opts; - Wait('stop'); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to get playbook list for ' + url + '. GET returned status: ' + status }); - }); - } - } - }; - - $scope.jobTypeChange = function(){ - if($scope.job_type){ - if($scope.job_type.value === 'scan'){ - $scope.toggleScanInfo(); - } - else if($scope.project_name === "Default"){ - $scope.project_name = null; - $scope.playbook_options = []; - // $scope.playbook = 'null'; - $scope.job_templates_form.playbook.$setPristine(); - } - } - }; - - $scope.toggleScanInfo = function() { - $scope.project_name = 'Default'; - if($scope.project === null){ - selectPlaybook(); - } - else { - $scope.project = null; - } - }; - - // Detect and alert user to potential SCM status issues - checkSCMStatus = function (oldValue, newValue) { - if (oldValue !== newValue && !Empty($scope.project)) { - Rest.setUrl(GetBasePath('projects') + $scope.project + '/'); - Rest.get() - .success(function (data) { - var msg; - switch (data.status) { - case 'failed': - msg = "The selected project has a failed status. Review the project's SCM settings" + - " and run an update before adding it to a template."; - break; - case 'never updated': - msg = 'The selected project has a never updated status. You will need to run a successful' + - ' update in order to selected a playbook. Without a valid playbook you will not be able ' + - ' to save this template.'; - break; - case 'missing': - msg = 'The selected project has a status of missing. Please check the server and make sure ' + - ' the directory exists and file permissions are set correctly.'; - break; - } - if (msg) { - Alert('Warning', msg, 'alert-info', null, null, null, null, true); - } - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to get project ' + $scope.project + '. GET returned status: ' + status }); - }); - } - }; - - - // $scope.selectPlaybookUnregister = $scope.$watch('project_name', function (newval, oldval) { - // selectPlaybook(oldval, newval); - // checkSCMStatus(oldval, newval); - // }); - - // Register a watcher on project_name - if ($scope.selectPlaybookUnregister) { - $scope.selectPlaybookUnregister(); - } - $scope.selectPlaybookUnregister = $scope.$watch('project', function (newValue, oldValue) { - if (newValue !== oldValue) { - selectPlaybook(oldValue, newValue); - checkSCMStatus(); - } - }); - - LookUpInit({ - scope: $scope, - form: form, - current_item: null, - list: ProjectList, - field: 'project', - input_type: "radio", - autopopulateLookup: (context === 'inv') ? false : true - }); - - if ($scope.removeSurveySaved) { - $scope.rmoveSurveySaved(); - } - $scope.removeSurveySaved = $scope.$on('SurveySaved', function() { - Wait('stop'); - $scope.survey_exists = true; - $scope.invalid_survey = false; - $('#job_templates_survey_enabled_chbox').attr('checked', true); - $('#job_templates_delete_survey_btn').show(); - $('#job_templates_edit_survey_btn').show(); - $('#job_templates_create_survey_btn').hide(); - - }); - - - function saveCompleted() { - setTimeout(function() { - $scope.$apply(function() { - var base = $location.path().replace(/^\//, '').split('/')[0]; - if (base === 'job_templates') { - ReturnToCaller(); - } - else { - ReturnToCaller(1); - } - }); - }, 500); - } - - if ($scope.removeTemplateSaveSuccess) { - $scope.removeTemplateSaveSuccess(); - } - $scope.removeTemplateSaveSuccess = $scope.$on('templateSaveSuccess', function(e, data) { - Wait('stop'); - if (data.related && data.related.callback) { - Alert('Callback URL', '

Host callbacks are enabled for this template. The callback URL is:

'+ - '

' + $scope.callback_server_path + data.related.callback + '

'+ - '

The host configuration key is: ' + $filter('sanitize')(data.host_config_key) + '

', 'alert-info', saveCompleted, null, null, null, true); - } - else { - saveCompleted(); - } - }); - - // Save - $scope.formSave = function () { - $scope.invalid_survey = false; - if ($scope.removeGatherFormFields) { - $scope.removeGatherFormFields(); - } - $scope.removeGatherFormFields = $scope.$on('GatherFormFields', function(e, data) { - generator.clearApiErrors(); - Wait('start'); - data = {}; - var fld; - try { - for (fld in form.fields) { - if (form.fields[fld].type === 'select' && fld !== 'playbook') { - data[fld] = $scope[fld].value; - } else { - if (fld !== 'variables') { - data[fld] = $scope[fld]; - } - } - } - data.extra_vars = ToJSON($scope.parseType, $scope.variables, true); - if(data.job_type === 'scan' && $scope.default_scan === true){ - data.project = ""; - data.playbook = ""; - } - Rest.setUrl(defaultUrl); - Rest.post(data) - .success(function(data) { - $scope.$emit('templateSaveSuccess', data); - - $scope.addedItem = data.id; - - Refresh({ - scope: $scope, - set: 'job_templates', - iterator: 'job_template', - url: $scope.current_url - }); - - if(data.survey_enabled===true){ - //once the job template information is saved we submit the survey info to the correct endpoint - var url = data.url+ 'survey_spec/'; - Rest.setUrl(url); - Rest.post({ name: $scope.survey_name, description: $scope.survey_description, spec: $scope.survey_questions }) - .success(function () { - Wait('stop'); - - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to add new survey. Post returned status: ' + status }); - }); - } - - - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to add new job template. POST returned status: ' + status - }); - }); - - } catch (err) { - Wait('stop'); - Alert("Error", "Error parsing extra variables. Parser returned: " + err); - } - }); - - - if ($scope.removePromptForSurvey) { - $scope.removePromptForSurvey(); - } - $scope.removePromptForSurvey = $scope.$on('PromptForSurvey', function() { - var action = function () { - // $scope.$emit("GatherFormFields"); - Wait('start'); - $('#prompt-modal').modal('hide'); - $scope.addSurvey(); - - }; - Prompt({ - hdr: 'Incomplete Survey', - body: '
Do you want to create a survey before proceeding?
', - action: action - }); - }); - - // users can't save a survey with a scan job - if($scope.job_type.value === "scan" && $scope.survey_enabled === true){ - $scope.survey_enabled = false; - } - if($scope.survey_enabled === true && $scope.survey_exists!==true){ - // $scope.$emit("PromptForSurvey"); - - // The original design for this was a pop up that would prompt the user if they wanted to create a - // survey, because they had enabled one but not created it yet. We switched this for now so that - // an error message would be displayed by the survey buttons that tells the user to add a survey or disabled - // surveys. - $scope.invalid_survey = true; - return; - } else { - $scope.$emit("GatherFormFields"); - } - - - }; - - $scope.formCancel = function () { - $state.transitionTo('jobTemplates'); - }; -} - -JobTemplatesAdd.$inject = ['Refresh', '$filter', '$scope', '$rootScope', '$compile', - '$location', '$log', '$stateParams', 'JobTemplateForm', 'GenerateForm', - 'Rest', 'Alert', 'ProcessErrors', 'ReturnToCaller', 'ClearScope', - 'GetBasePath', 'InventoryList', 'CredentialList', 'ProjectList', - 'LookUpInit', 'md5Setup', 'ParseTypeChange', 'Wait', 'Empty', 'ToJSON', - 'CallbackHelpInit', 'initSurvey', 'Prompt', 'GetChoices', '$state', - 'CreateSelect2' -]; - - -export function JobTemplatesEdit($filter, $scope, $rootScope, $compile, - $location, $log, $stateParams, JobTemplateForm, GenerateForm, Rest, Alert, - ProcessErrors, RelatedSearchInit, RelatedPaginateInit, ReturnToCaller, - ClearScope, InventoryList, CredentialList, ProjectList, LookUpInit, - GetBasePath, md5Setup, ParseTypeChange, JobStatusToolTip, FormatDate, Wait, - Empty, Prompt, ParseVariableString, ToJSON, SchedulesControllerInit, - JobsControllerInit, JobsListUpdate, GetChoices, SchedulesListInit, - SchedulesList, CallbackHelpInit, PlaybookRun, SurveyControllerInit, $state, - CreateSelect2){ - - ClearScope(); - - var defaultUrl = GetBasePath('job_templates'), - generator = GenerateForm, - form = JobTemplateForm(), - base = $location.path().replace(/^\//, '').split('/')[0], - master = {}, - id = $stateParams.template_id, - relatedSets = {}, - checkSCMStatus, getPlaybooks, callback, - choicesCount = 0; - - - CallbackHelpInit({ scope: $scope }); - - SchedulesList.well = false; - generator.inject(form, { mode: 'edit', related: true, scope: $scope }); - $scope.mode = 'edit'; - $scope.parseType = 'yaml'; - $scope.showJobType = false; - - SurveyControllerInit({ - scope: $scope, - parent_scope: $scope, - id: id - }); - - callback = function() { - // Make sure the form controller knows there was a change - $scope[form.name + '_form'].$setDirty(); - }; - - $scope.playbook_options = null; - $scope.playbook = null; - generator.reset(); - - getPlaybooks = function (project) { - var url; - if($scope.job_type.value === 'scan' && $scope.project_name === "Default"){ - $scope.playbook_options = ['Default']; - $scope.playbook = 'Default'; - Wait('stop'); - } - else if (!Empty(project)) { - url = GetBasePath('projects') + project + '/playbooks/'; - Wait('start'); - Rest.setUrl(url); - Rest.get() - .success(function (data) { - var i; - $scope.playbook_options = []; - for (i = 0; i < data.length; i++) { - $scope.playbook_options.push(data[i]); - if (data[i] === $scope.playbook) { - $scope.job_templates_form.playbook.$setValidity('required', true); - } - } - if ($scope.playbook) { - $scope.$emit('jobTemplateLoadFinished'); - } else { - Wait('stop'); - } - }) - .error(function () { - Wait('stop'); - Alert('Missing Playbooks', 'Unable to retrieve the list of playbooks for this project. Choose a different ' + - ' project or make the playbooks available on the file system.', 'alert-info'); - }); - } - else { - Wait('stop'); - } - }; - - $scope.jobTypeChange = function(){ - if($scope.job_type){ - if($scope.job_type.value === 'scan'){ - $scope.toggleScanInfo(); - } - else if($scope.project_name === "Default"){ - $scope.project_name = null; - $scope.playbook_options = []; - // $scope.playbook = 'null'; - $scope.job_templates_form.playbook.$setPristine(); - } - - } - }; - - $scope.toggleScanInfo = function() { - $scope.project_name = 'Default'; - if($scope.project === null){ - getPlaybooks(); - } - else { - $scope.project = null; - } - }; - - // Detect and alert user to potential SCM status issues - checkSCMStatus = function () { - if (!Empty($scope.project)) { - Wait('start'); - Rest.setUrl(GetBasePath('projects') + $scope.project + '/'); - Rest.get() - .success(function (data) { - var msg; - switch (data.status) { - case 'failed': - msg = "The selected project has a failed status. Review the project's SCM settings" + - " and run an update before adding it to a template."; - break; - case 'never updated': - msg = 'The selected project has a never updated status. You will need to run a successful' + - ' update in order to selected a playbook. Without a valid playbook you will not be able ' + - ' to save this template.'; - break; - case 'missing': - msg = 'The selected project has a status of missing. Please check the server and make sure ' + - ' the directory exists and file permissions are set correctly.'; - break; - } - Wait('stop'); - if (msg) { - Alert('Warning', msg, 'alert-info', null, null, null, null, true); - } - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', msg: 'Failed to get project ' + $scope.project + - '. GET returned status: ' + status }); - }); - } - }; - - if ($scope.removerelatedschedules) { - $scope.removerelatedschedules(); - } - $scope.removerelatedschedules = $scope.$on('relatedschedules', function() { - SchedulesListInit({ - scope: $scope, - list: SchedulesList, - choices: null, - related: true - }); - }); - - // Register a watcher on project_name. Refresh the playbook list on change. - if ($scope.watchProjectUnregister) { - $scope.watchProjectUnregister(); - } - $scope.watchProjectUnregister = $scope.$watch('project', function (newValue, oldValue) { - if (newValue !== oldValue) { - getPlaybooks($scope.project); - checkSCMStatus(); - } - }); - - - - // Turn off 'Wait' after both cloud credential and playbook list come back - if ($scope.removeJobTemplateLoadFinished) { - $scope.removeJobTemplateLoadFinished(); - } - $scope.removeJobTemplateLoadFinished = $scope.$on('jobTemplateLoadFinished', function () { - CreateSelect2({ - element:'#job_templates_job_type', - multiple: false - }); - - CreateSelect2({ - element:'#playbook-select', - multiple: false - }); - - CreateSelect2({ - element:'#job_templates_verbosity', - multiple: false - }); - - for (var set in relatedSets) { - $scope.search(relatedSets[set].iterator); - } - SchedulesControllerInit({ - scope: $scope, - parent_scope: $scope, - iterator: 'schedule' - }); - - }); - - // Set the status/badge for each related job - if ($scope.removeRelatedCompletedJobs) { - $scope.removeRelatedCompletedJobs(); - } - $scope.removeRelatedCompletedJobs = $scope.$on('relatedcompleted_jobs', function () { - JobsControllerInit({ - scope: $scope, - parent_scope: $scope, - iterator: form.related.completed_jobs.iterator - }); - JobsListUpdate({ - scope: $scope, - parent_scope: $scope, - list: form.related.completed_jobs - }); - }); - - if ($scope.cloudCredentialReadyRemove) { - $scope.cloudCredentialReadyRemove(); - } - $scope.cloudCredentialReadyRemove = $scope.$on('cloudCredentialReady', function (e, name) { - var CloudCredentialList = {}; - $scope.cloud_credential_name = name; - master.cloud_credential_name = name; - // Clone the CredentialList object for use with cloud_credential. Cloning - // and changing properties to avoid collision. - jQuery.extend(true, CloudCredentialList, CredentialList); - CloudCredentialList.name = 'cloudcredentials'; - CloudCredentialList.iterator = 'cloudcredential'; - LookUpInit({ - url: GetBasePath('credentials') + '?cloud=true', - scope: $scope, - form: form, - current_item: $scope.cloud_credential, - list: CloudCredentialList, - field: 'cloud_credential', - hdr: 'Select Cloud Credential', - input_type: "radio" - }); - $scope.$emit('jobTemplateLoadFinished'); - }); - - - // Retrieve each related set and populate the playbook list - if ($scope.jobTemplateLoadedRemove) { - $scope.jobTemplateLoadedRemove(); - } - $scope.jobTemplateLoadedRemove = $scope.$on('jobTemplateLoaded', function (e, related_cloud_credential, masterObject, relatedSets) { - var dft, set; - master = masterObject; - getPlaybooks($scope.project); - - for (set in relatedSets) { - $scope.search(relatedSets[set].iterator); - } - - dft = ($scope.host_config_key === "" || $scope.host_config_key === null) ? false : true; - md5Setup({ - scope: $scope, - master: master, - check_field: 'allow_callbacks', - default_val: dft - }); - - ParseTypeChange({ scope: $scope, field_id: 'job_templates_variables', onChange: callback }); - - if (related_cloud_credential) { - Rest.setUrl(related_cloud_credential); - Rest.get() - .success(function (data) { - $scope.$emit('cloudCredentialReady', data.name); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, {hdr: 'Error!', - msg: 'Failed to related cloud credential. GET returned status: ' + status }); - }); - } else { - // No existing cloud credential - $scope.$emit('cloudCredentialReady', null); - } - }); - - Wait('start'); - - if ($scope.removeEnableSurvey) { - $scope.removeEnableSurvey(); - } - $scope.removeEnableSurvey = $scope.$on('EnableSurvey', function(fld) { - - $('#job_templates_survey_enabled_chbox').attr('checked', $scope[fld]); - Rest.setUrl(defaultUrl + id+ '/survey_spec/'); - Rest.get() - .success(function (data) { - if(!data || !data.name){ - $('#job_templates_delete_survey_btn').hide(); - $('#job_templates_edit_survey_btn').hide(); - $('#job_templates_create_survey_btn').show(); - } - else { - $scope.survey_exists = true; - $('#job_templates_delete_survey_btn').show(); - $('#job_templates_edit_survey_btn').show(); - $('#job_templates_create_survey_btn').hide(); - } - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { - hdr: 'Error!', - msg: 'Failed to retrieve job template: ' + $stateParams.template_id + '. GET status: ' + status - }); - }); - }); - - if ($scope.removeSurveySaved) { - $scope.rmoveSurveySaved(); - } - $scope.removeSurveySaved = $scope.$on('SurveySaved', function() { - Wait('stop'); - $scope.survey_exists = true; - $scope.invalid_survey = false; - $('#job_templates_survey_enabled_chbox').attr('checked', true); - $('#job_templates_delete_survey_btn').show(); - $('#job_templates_edit_survey_btn').show(); - $('#job_templates_create_survey_btn').hide(); - - }); - - if ($scope.removeLoadJobs) { - $scope.rmoveLoadJobs(); - } - $scope.removeLoadJobs = $scope.$on('LoadJobs', function() { - $scope.fillJobTemplate(); - }); - - if ($scope.removeChoicesReady) { - $scope.removeChoicesReady(); - } - $scope.removeChoicesReady = $scope.$on('choicesReady', function() { - choicesCount++; - if (choicesCount === 4) { - $scope.$emit('LoadJobs'); - } - }); - - GetChoices({ - scope: $scope, - url: GetBasePath('unified_jobs'), - field: 'status', - variable: 'status_choices', - callback: 'choicesReady' - }); - - GetChoices({ - scope: $scope, - url: GetBasePath('unified_jobs'), - field: 'type', - variable: 'type_choices', - callback: 'choicesReady' - }); - - // setup verbosity options lookup - GetChoices({ - scope: $scope, - url: defaultUrl, - field: 'verbosity', - variable: 'verbosity_options', - callback: 'choicesReady' - }); - - // setup job type options lookup - GetChoices({ - scope: $scope, - url: defaultUrl, - field: 'job_type', - variable: 'job_type_options', - callback: 'choicesReady' - }); - - function saveCompleted() { - setTimeout(function() { - $scope.$apply(function() { - var base = $location.path().replace(/^\//, '').split('/')[0]; - if (base === 'job_templates') { - ReturnToCaller(); - } - else { - ReturnToCaller(1); - } - }); - }, 500); - } - - if ($scope.removeTemplateSaveSuccess) { - $scope.removeTemplateSaveSuccess(); - } - $scope.removeTemplateSaveSuccess = $scope.$on('templateSaveSuccess', function(e, data) { - Wait('stop'); - if ($scope.allow_callbacks && ($scope.host_config_key !== master.host_config_key || $scope.callback_url !== master.callback_url)) { - if (data.related && data.related.callback) { - Alert('Callback URL', '

Host callbacks are enabled for this template. The callback URL is:

'+ - '

' + $scope.callback_server_path + data.related.callback + '

'+ - '

The host configuration key is: ' + $filter('sanitize')(data.host_config_key) + '

', 'alert-info', saveCompleted, null, null, null, true); - } - else { - saveCompleted(); - } - } - else { - saveCompleted(); - } - }); - - - - // Save changes to the parent - $scope.formSave = function () { - $scope.invalid_survey = false; - if ($scope.removeGatherFormFields) { - $scope.removeGatherFormFields(); - } - $scope.removeGatherFormFields = $scope.$on('GatherFormFields', function(e, data) { - generator.clearApiErrors(); - Wait('start'); - data = {}; - var fld; - try { - // Make sure we have valid variable data - data.extra_vars = ToJSON($scope.parseType, $scope.variables, true); - if(data.extra_vars === undefined ){ - throw 'undefined variables'; - } - for (fld in form.fields) { - if (form.fields[fld].type === 'select' && fld !== 'playbook') { - data[fld] = $scope[fld].value; - } else { - if (fld !== 'variables' && fld !== 'callback_url') { - data[fld] = $scope[fld]; - } - } - } - Rest.setUrl(defaultUrl + id + '/'); - Rest.put(data) - .success(function (data) { - $scope.$emit('templateSaveSuccess', data); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to update job template. PUT returned status: ' + status }); - }); - - } catch (err) { - Wait('stop'); - Alert("Error", "Error parsing extra variables. Parser returned: " + err); - } - }); - - - if ($scope.removePromptForSurvey) { - $scope.removePromptForSurvey(); - } - $scope.removePromptForSurvey = $scope.$on('PromptForSurvey', function() { - var action = function () { - // $scope.$emit("GatherFormFields"); - Wait('start'); - $('#prompt-modal').modal('hide'); - $scope.addSurvey(); - - }; - Prompt({ - hdr: 'Incomplete Survey', - body: '
Do you want to create a survey before proceeding?
', - action: action - }); - }); - - // users can't save a survey with a scan job - if($scope.job_type.value === "scan" && $scope.survey_enabled === true){ - $scope.survey_enabled = false; - } - if($scope.survey_enabled === true && $scope.survey_exists!==true){ - // $scope.$emit("PromptForSurvey"); - - // The original design for this was a pop up that would prompt the user if they wanted to create a - // survey, because they had enabled one but not created it yet. We switched this for now so that - // an error message would be displayed by the survey buttons that tells the user to add a survey or disabled - // surveys. - $scope.invalid_survey = true; - return; - } else { - $scope.$emit("GatherFormFields"); - } - - }; - - $scope.formCancel = function () { - $state.transitionTo('jobTemplates'); - }; - - // Related set: Add button - $scope.add = function (set) { - $rootScope.flashMessage = null; - $location.path('/' + base + '/' + $stateParams.template_id + '/' + set); - }; - - // Related set: Edit button - $scope.edit = function (set, id) { - $rootScope.flashMessage = null; - $location.path('/' + set + '/' + id); - }; - - // Launch a job using the selected template - $scope.launch = function() { - - if ($scope.removePromptForSurvey) { - $scope.removePromptForSurvey(); - } - $scope.removePromptForSurvey = $scope.$on('PromptForSurvey', function() { - var action = function () { - // $scope.$emit("GatherFormFields"); - Wait('start'); - $('#prompt-modal').modal('hide'); - $scope.addSurvey(); - - }; - Prompt({ - hdr: 'Incomplete Survey', - body: '
Do you want to create a survey before proceeding?
', - action: action - }); - }); - if($scope.survey_enabled === true && $scope.survey_exists!==true){ - $scope.$emit("PromptForSurvey"); - } - else { - - PlaybookRun({ - scope: $scope, - id: id - }); - } - }; - - // handler for 'Enable Survey' button - $scope.surveyEnabled = function(){ - Rest.setUrl(defaultUrl + id+ '/'); - Rest.patch({"survey_enabled": $scope.survey_enabled}) - .success(function (data) { - - if(Empty(data.summary_fields.survey)){ - $('#job_templates_delete_survey_btn').hide(); - $('#job_templates_edit_survey_btn').hide(); - $('#job_templates_create_survey_btn').show(); - } - else{ - $scope.survey_exists = true; - $('#job_templates_delete_survey_btn').show(); - $('#job_templates_edit_survey_btn').show(); - $('#job_templates_create_survey_btn').hide(); - } - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { - hdr: 'Error!', - msg: 'Failed to retrieve save survey_enabled: ' + $stateParams.template_id + '. GET status: ' + status - }); - }); - }; - - -} - -JobTemplatesEdit.$inject = ['$filter', '$scope', '$rootScope', '$compile', - '$location', '$log', '$stateParams', 'JobTemplateForm', 'GenerateForm', - 'Rest', 'Alert', 'ProcessErrors', 'RelatedSearchInit', - 'RelatedPaginateInit','ReturnToCaller', 'ClearScope', 'InventoryList', - 'CredentialList', 'ProjectList', 'LookUpInit', 'GetBasePath', 'md5Setup', - 'ParseTypeChange', 'JobStatusToolTip', 'FormatDate', 'Wait', - 'Empty', 'Prompt', 'ParseVariableString', 'ToJSON', - 'SchedulesControllerInit', 'JobsControllerInit', 'JobsListUpdate', - 'GetChoices', 'SchedulesListInit', 'SchedulesList', 'CallbackHelpInit', - 'PlaybookRun' , 'initSurvey', '$state', 'CreateSelect2' -]; diff --git a/awx/ui/client/src/job-templates/add/inventory-job-templates-add.route.js b/awx/ui/client/src/job-templates/add/inventory-job-templates-add.route.js new file mode 100644 index 0000000000..c273fcbbf3 --- /dev/null +++ b/awx/ui/client/src/job-templates/add/inventory-job-templates-add.route.js @@ -0,0 +1,19 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import {templateUrl} from '../../shared/template-url/template-url.factory'; + +export default { + name: 'inventoryJobTemplateAdd', + url: '/inventories/:inventory_id/job_templates/add', + templateUrl: templateUrl('job-templates/add/job-templates-add'), + controller: 'JobTemplatesAdd', + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }] + } +}; diff --git a/awx/ui/client/src/job-templates/add/job-templates-add.controller.js b/awx/ui/client/src/job-templates/add/job-templates-add.controller.js new file mode 100644 index 0000000000..1c7000d399 --- /dev/null +++ b/awx/ui/client/src/job-templates/add/job-templates-add.controller.js @@ -0,0 +1,452 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + export default + [ 'Refresh', '$filter', '$scope', '$rootScope', '$compile', + '$location', '$log', '$stateParams', 'JobTemplateForm', 'GenerateForm', + 'Rest', 'Alert', 'ProcessErrors', 'ReturnToCaller', 'ClearScope', + 'GetBasePath', 'InventoryList', 'CredentialList', 'ProjectList', + 'LookUpInit', 'md5Setup', 'ParseTypeChange', 'Wait', 'Empty', 'ToJSON', + 'CallbackHelpInit', 'initSurvey', 'Prompt', 'GetChoices', '$state', + 'CreateSelect2', + function( + Refresh, $filter, $scope, $rootScope, $compile, + $location, $log, $stateParams, JobTemplateForm, GenerateForm, Rest, Alert, + ProcessErrors, ReturnToCaller, ClearScope, GetBasePath, InventoryList, + CredentialList, ProjectList, LookUpInit, md5Setup, ParseTypeChange, Wait, + Empty, ToJSON, CallbackHelpInit, SurveyControllerInit, Prompt, GetChoices, + $state, CreateSelect2 + ) { + + ClearScope(); + + // Inject dynamic view + var defaultUrl = GetBasePath('job_templates'), + form = JobTemplateForm(), + generator = GenerateForm, + master = {}, + CloudCredentialList = {}, + selectPlaybook, checkSCMStatus, + callback, + base = $location.path().replace(/^\//, '').split('/')[0], + context = (base === 'job_templates') ? 'job_template' : 'inv'; + + CallbackHelpInit({ scope: $scope }); + $scope.can_edit = true; + generator.inject(form, { mode: 'add', related: false, scope: $scope }); + + callback = function() { + // Make sure the form controller knows there was a change + $scope[form.name + '_form'].$setDirty(); + }; + $scope.mode = "add"; + $scope.parseType = 'yaml'; + ParseTypeChange({ scope: $scope, field_id: 'job_templates_variables', onChange: callback }); + + $scope.playbook_options = []; + $scope.allow_callbacks = false; + + generator.reset(); + + md5Setup({ + scope: $scope, + master: master, + check_field: 'allow_callbacks', + default_val: false + }); + + LookUpInit({ + scope: $scope, + form: form, + current_item: ($stateParams.inventory_id !== undefined) ? $stateParams.inventory_id : null, + list: InventoryList, + field: 'inventory', + input_type: "radio" + }); + + + // Clone the CredentialList object for use with cloud_credential. Cloning + // and changing properties to avoid collision. + jQuery.extend(true, CloudCredentialList, CredentialList); + CloudCredentialList.name = 'cloudcredentials'; + CloudCredentialList.iterator = 'cloudcredential'; + + SurveyControllerInit({ + scope: $scope, + parent_scope: $scope + }); + + if ($scope.removeLookUpInitialize) { + $scope.removeLookUpInitialize(); + } + $scope.removeLookUpInitialize = $scope.$on('lookUpInitialize', function () { + LookUpInit({ + url: GetBasePath('credentials') + '?cloud=true', + scope: $scope, + form: form, + current_item: null, + list: CloudCredentialList, + field: 'cloud_credential', + hdr: 'Select Cloud Credential', + input_type: 'radio' + }); + + LookUpInit({ + url: GetBasePath('credentials') + '?kind=ssh', + scope: $scope, + form: form, + current_item: null, + list: CredentialList, + field: 'credential', + hdr: 'Select Machine Credential', + input_type: "radio" + }); + }); + + var selectCount = 0; + + if ($scope.removeChoicesReady) { + $scope.removeChoicesReady(); + } + $scope.removeChoicesReady = $scope.$on('choicesReadyVerbosity', function () { + selectCount++; + if (selectCount === 2) { + var verbosity; + // this sets the default options for the selects as specified by the controller. + for (verbosity in $scope.verbosity_options) { + if ($scope.verbosity_options[verbosity].isDefault) { + $scope.verbosity = $scope.verbosity_options[verbosity]; + } + } + $scope.job_type = $scope.job_type_options[$scope.job_type_field.default]; + + // if you're getting to the form from the scan job section on inventories, + // set the job type select to be scan + if ($stateParams.inventory_id) { + // This means that the job template form was accessed via inventory prop's + // This also means the job is a scan job. + $scope.job_type.value = 'scan'; + $scope.jobTypeChange(); + $scope.inventory = $stateParams.inventory_id; + Rest.setUrl(GetBasePath('inventory') + $stateParams.inventory_id + '/'); + Rest.get() + .success(function (data) { + $scope.inventory_name = data.name; + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to lookup inventory: ' + data.id + '. GET returned status: ' + status }); + }); + } + CreateSelect2({ + element:'#job_templates_job_type', + multiple: false + }); + + CreateSelect2({ + element:'#playbook-select', + multiple: false + }); + + CreateSelect2({ + element:'#job_templates_verbosity', + multiple: false + }); + + $scope.$emit('lookUpInitialize'); + } + }); + + // setup verbosity options select + GetChoices({ + scope: $scope, + url: defaultUrl, + field: 'verbosity', + variable: 'verbosity_options', + callback: 'choicesReadyVerbosity' + }); + + // setup job type options select + GetChoices({ + scope: $scope, + url: defaultUrl, + field: 'job_type', + variable: 'job_type_options', + callback: 'choicesReadyVerbosity' + }); + + // Update playbook select whenever project value changes + selectPlaybook = function (oldValue, newValue) { + var url; + if($scope.job_type.value === 'scan' && $scope.project_name === "Default"){ + $scope.playbook_options = ['Default']; + $scope.playbook = 'Default'; + Wait('stop'); + } + else if (oldValue !== newValue) { + if ($scope.project) { + Wait('start'); + url = GetBasePath('projects') + $scope.project + '/playbooks/'; + Rest.setUrl(url); + Rest.get() + .success(function (data) { + var i, opts = []; + for (i = 0; i < data.length; i++) { + opts.push(data[i]); + } + $scope.playbook_options = opts; + Wait('stop'); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to get playbook list for ' + url + '. GET returned status: ' + status }); + }); + } + } + }; + + $scope.jobTypeChange = function(){ + if($scope.job_type){ + if($scope.job_type.value === 'scan'){ + $scope.toggleScanInfo(); + } + else if($scope.project_name === "Default"){ + $scope.project_name = null; + $scope.playbook_options = []; + // $scope.playbook = 'null'; + $scope.job_templates_form.playbook.$setPristine(); + } + } + }; + + $scope.toggleScanInfo = function() { + $scope.project_name = 'Default'; + if($scope.project === null){ + selectPlaybook(); + } + else { + $scope.project = null; + } + }; + + // Detect and alert user to potential SCM status issues + checkSCMStatus = function (oldValue, newValue) { + if (oldValue !== newValue && !Empty($scope.project)) { + Rest.setUrl(GetBasePath('projects') + $scope.project + '/'); + Rest.get() + .success(function (data) { + var msg; + switch (data.status) { + case 'failed': + msg = "The selected project has a failed status. Review the project's SCM settings" + + " and run an update before adding it to a template."; + break; + case 'never updated': + msg = 'The selected project has a never updated status. You will need to run a successful' + + ' update in order to selected a playbook. Without a valid playbook you will not be able ' + + ' to save this template.'; + break; + case 'missing': + msg = 'The selected project has a status of missing. Please check the server and make sure ' + + ' the directory exists and file permissions are set correctly.'; + break; + } + if (msg) { + Alert('Warning', msg, 'alert-info', null, null, null, null, true); + } + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to get project ' + $scope.project + '. GET returned status: ' + status }); + }); + } + }; + + + // $scope.selectPlaybookUnregister = $scope.$watch('project_name', function (newval, oldval) { + // selectPlaybook(oldval, newval); + // checkSCMStatus(oldval, newval); + // }); + + // Register a watcher on project_name + if ($scope.selectPlaybookUnregister) { + $scope.selectPlaybookUnregister(); + } + $scope.selectPlaybookUnregister = $scope.$watch('project', function (newValue, oldValue) { + if (newValue !== oldValue) { + selectPlaybook(oldValue, newValue); + checkSCMStatus(); + } + }); + + LookUpInit({ + scope: $scope, + form: form, + current_item: null, + list: ProjectList, + field: 'project', + input_type: "radio", + autopopulateLookup: (context === 'inv') ? false : true + }); + + if ($scope.removeSurveySaved) { + $scope.rmoveSurveySaved(); + } + $scope.removeSurveySaved = $scope.$on('SurveySaved', function() { + Wait('stop'); + $scope.survey_exists = true; + $scope.invalid_survey = false; + $('#job_templates_survey_enabled_chbox').attr('checked', true); + $('#job_templates_delete_survey_btn').show(); + $('#job_templates_edit_survey_btn').show(); + $('#job_templates_create_survey_btn').hide(); + + }); + + + function saveCompleted() { + setTimeout(function() { + $scope.$apply(function() { + var base = $location.path().replace(/^\//, '').split('/')[0]; + if (base === 'job_templates') { + ReturnToCaller(); + } + else { + ReturnToCaller(1); + } + }); + }, 500); + } + + if ($scope.removeTemplateSaveSuccess) { + $scope.removeTemplateSaveSuccess(); + } + $scope.removeTemplateSaveSuccess = $scope.$on('templateSaveSuccess', function(e, data) { + Wait('stop'); + if (data.related && data.related.callback) { + Alert('Callback URL', '

Host callbacks are enabled for this template. The callback URL is:

'+ + '

' + $scope.callback_server_path + data.related.callback + '

'+ + '

The host configuration key is: ' + $filter('sanitize')(data.host_config_key) + '

', 'alert-info', saveCompleted, null, null, null, true); + } + else { + saveCompleted(); + } + }); + + // Save + $scope.formSave = function () { + $scope.invalid_survey = false; + if ($scope.removeGatherFormFields) { + $scope.removeGatherFormFields(); + } + $scope.removeGatherFormFields = $scope.$on('GatherFormFields', function(e, data) { + generator.clearApiErrors(); + Wait('start'); + data = {}; + var fld; + try { + for (fld in form.fields) { + if (form.fields[fld].type === 'select' && fld !== 'playbook') { + data[fld] = $scope[fld].value; + } else { + if (fld !== 'variables') { + data[fld] = $scope[fld]; + } + } + } + data.extra_vars = ToJSON($scope.parseType, $scope.variables, true); + if(data.job_type === 'scan' && $scope.default_scan === true){ + data.project = ""; + data.playbook = ""; + } + Rest.setUrl(defaultUrl); + Rest.post(data) + .success(function(data) { + $scope.$emit('templateSaveSuccess', data); + + $scope.addedItem = data.id; + + Refresh({ + scope: $scope, + set: 'job_templates', + iterator: 'job_template', + url: $scope.current_url + }); + + if(data.survey_enabled===true){ + //once the job template information is saved we submit the survey info to the correct endpoint + var url = data.url+ 'survey_spec/'; + Rest.setUrl(url); + Rest.post({ name: $scope.survey_name, description: $scope.survey_description, spec: $scope.survey_questions }) + .success(function () { + Wait('stop'); + + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to add new survey. Post returned status: ' + status }); + }); + } + + + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to add new job template. POST returned status: ' + status + }); + }); + + } catch (err) { + Wait('stop'); + Alert("Error", "Error parsing extra variables. Parser returned: " + err); + } + }); + + + if ($scope.removePromptForSurvey) { + $scope.removePromptForSurvey(); + } + $scope.removePromptForSurvey = $scope.$on('PromptForSurvey', function() { + var action = function () { + // $scope.$emit("GatherFormFields"); + Wait('start'); + $('#prompt-modal').modal('hide'); + $scope.addSurvey(); + + }; + Prompt({ + hdr: 'Incomplete Survey', + body: '
Do you want to create a survey before proceeding?
', + action: action + }); + }); + + // users can't save a survey with a scan job + if($scope.job_type.value === "scan" && $scope.survey_enabled === true){ + $scope.survey_enabled = false; + } + if($scope.survey_enabled === true && $scope.survey_exists!==true){ + // $scope.$emit("PromptForSurvey"); + + // The original design for this was a pop up that would prompt the user if they wanted to create a + // survey, because they had enabled one but not created it yet. We switched this for now so that + // an error message would be displayed by the survey buttons that tells the user to add a survey or disabled + // surveys. + $scope.invalid_survey = true; + return; + } else { + $scope.$emit("GatherFormFields"); + } + + + }; + + $scope.formCancel = function () { + $state.transitionTo('jobTemplates'); + }; + } + + ]; diff --git a/awx/ui/client/src/job-templates/add/job-templates-add.partial.html b/awx/ui/client/src/job-templates/add/job-templates-add.partial.html new file mode 100644 index 0000000000..6c3956cfb8 --- /dev/null +++ b/awx/ui/client/src/job-templates/add/job-templates-add.partial.html @@ -0,0 +1,5 @@ +
+
+
+
+
diff --git a/awx/ui/client/src/job-templates/add/job-templates-add.route.js b/awx/ui/client/src/job-templates/add/job-templates-add.route.js new file mode 100644 index 0000000000..7d79f00763 --- /dev/null +++ b/awx/ui/client/src/job-templates/add/job-templates-add.route.js @@ -0,0 +1,23 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import {templateUrl} from '../../shared/template-url/template-url.factory'; + +export default { + name: 'jobTemplates.add', + url: '/add', + templateUrl: templateUrl('job-templates/add/job-templates-add'), + controller: 'JobTemplatesAdd', + ncyBreadcrumb: { + parent: "jobTemplates", + label: "CREATE JOB TEMPLATE" + }, + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }] + } +}; diff --git a/awx/ui/client/src/job-templates/add/main.js b/awx/ui/client/src/job-templates/add/main.js new file mode 100644 index 0000000000..b618873933 --- /dev/null +++ b/awx/ui/client/src/job-templates/add/main.js @@ -0,0 +1,17 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import jobTemplateAddRoute from './job-templates-add.route'; +import inventoryJobTemplateAddRoute from './inventory-job-templates-add.route'; +import controller from './job-templates-add.controller'; + +export default + angular.module('jobTemplatesAdd', []) + .controller('JobTemplatesAdd', controller) + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(jobTemplateAddRoute); + $stateExtender.addState(inventoryJobTemplateAddRoute); + }]); diff --git a/awx/ui/client/src/job-templates/edit/inventory-job-templates-edit.route.js b/awx/ui/client/src/job-templates/edit/inventory-job-templates-edit.route.js new file mode 100644 index 0000000000..6b0d476302 --- /dev/null +++ b/awx/ui/client/src/job-templates/edit/inventory-job-templates-edit.route.js @@ -0,0 +1,22 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import {templateUrl} from '../../shared/template-url/template-url.factory'; + +export default { + name: 'inventoryJobTemplateEdit', + url: '/inventories/:inventory_id/job_templates/:template_id', + templateUrl: templateUrl('job-templates/edit/job-templates-edit'), + controller: 'JobTemplatesEdit', + data: { + activityStreamId: 'template_id' + }, + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }] + } +}; diff --git a/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js b/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js new file mode 100644 index 0000000000..786358d52d --- /dev/null +++ b/awx/ui/client/src/job-templates/edit/job-templates-edit.controller.js @@ -0,0 +1,596 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +/** + * @ngdoc function + * @name controllers.function:JobTemplatesEdit + * @description This controller's for Job Template Edit +*/ + +export default + [ '$filter', '$scope', '$rootScope', '$compile', + '$location', '$log', '$stateParams', 'JobTemplateForm', 'GenerateForm', + 'Rest', 'Alert', 'ProcessErrors', 'RelatedSearchInit', + 'RelatedPaginateInit','ReturnToCaller', 'ClearScope', 'InventoryList', + 'CredentialList', 'ProjectList', 'LookUpInit', 'GetBasePath', 'md5Setup', + 'ParseTypeChange', 'JobStatusToolTip', 'FormatDate', 'Wait', + 'Empty', 'Prompt', 'ParseVariableString', 'ToJSON', + 'SchedulesControllerInit', 'JobsControllerInit', 'JobsListUpdate', + 'GetChoices', 'SchedulesListInit', 'SchedulesList', 'CallbackHelpInit', + 'PlaybookRun' , 'initSurvey', '$state', 'CreateSelect2', + function( + $filter, $scope, $rootScope, $compile, + $location, $log, $stateParams, JobTemplateForm, GenerateForm, Rest, Alert, + ProcessErrors, RelatedSearchInit, RelatedPaginateInit, ReturnToCaller, + ClearScope, InventoryList, CredentialList, ProjectList, LookUpInit, + GetBasePath, md5Setup, ParseTypeChange, JobStatusToolTip, FormatDate, Wait, + Empty, Prompt, ParseVariableString, ToJSON, SchedulesControllerInit, + JobsControllerInit, JobsListUpdate, GetChoices, SchedulesListInit, + SchedulesList, CallbackHelpInit, PlaybookRun, SurveyControllerInit, $state, + CreateSelect2 + ) { + + ClearScope(); + + var defaultUrl = GetBasePath('job_templates'), + generator = GenerateForm, + form = JobTemplateForm(), + base = $location.path().replace(/^\//, '').split('/')[0], + master = {}, + id = $stateParams.template_id, + relatedSets = {}, + checkSCMStatus, getPlaybooks, callback, + choicesCount = 0; + + + CallbackHelpInit({ scope: $scope }); + + SchedulesList.well = false; + generator.inject(form, { mode: 'edit', related: true, scope: $scope }); + $scope.mode = 'edit'; + $scope.parseType = 'yaml'; + $scope.showJobType = false; + + SurveyControllerInit({ + scope: $scope, + parent_scope: $scope, + id: id + }); + + callback = function() { + // Make sure the form controller knows there was a change + $scope[form.name + '_form'].$setDirty(); + }; + + $scope.playbook_options = null; + $scope.playbook = null; + generator.reset(); + + getPlaybooks = function (project) { + var url; + if($scope.job_type.value === 'scan' && $scope.project_name === "Default"){ + $scope.playbook_options = ['Default']; + $scope.playbook = 'Default'; + Wait('stop'); + } + else if (!Empty(project)) { + url = GetBasePath('projects') + project + '/playbooks/'; + Wait('start'); + Rest.setUrl(url); + Rest.get() + .success(function (data) { + var i; + $scope.playbook_options = []; + for (i = 0; i < data.length; i++) { + $scope.playbook_options.push(data[i]); + if (data[i] === $scope.playbook) { + $scope.job_templates_form.playbook.$setValidity('required', true); + } + } + if ($scope.playbook) { + $scope.$emit('jobTemplateLoadFinished'); + } else { + Wait('stop'); + } + }) + .error(function () { + Wait('stop'); + Alert('Missing Playbooks', 'Unable to retrieve the list of playbooks for this project. Choose a different ' + + ' project or make the playbooks available on the file system.', 'alert-info'); + }); + } + else { + Wait('stop'); + } + }; + + $scope.jobTypeChange = function(){ + if($scope.job_type){ + if($scope.job_type.value === 'scan'){ + $scope.toggleScanInfo(); + } + else if($scope.project_name === "Default"){ + $scope.project_name = null; + $scope.playbook_options = []; + // $scope.playbook = 'null'; + $scope.job_templates_form.playbook.$setPristine(); + } + + } + }; + + $scope.toggleScanInfo = function() { + $scope.project_name = 'Default'; + if($scope.project === null){ + getPlaybooks(); + } + else { + $scope.project = null; + } + }; + + // Detect and alert user to potential SCM status issues + checkSCMStatus = function () { + if (!Empty($scope.project)) { + Wait('start'); + Rest.setUrl(GetBasePath('projects') + $scope.project + '/'); + Rest.get() + .success(function (data) { + var msg; + switch (data.status) { + case 'failed': + msg = "The selected project has a failed status. Review the project's SCM settings" + + " and run an update before adding it to a template."; + break; + case 'never updated': + msg = 'The selected project has a never updated status. You will need to run a successful' + + ' update in order to selected a playbook. Without a valid playbook you will not be able ' + + ' to save this template.'; + break; + case 'missing': + msg = 'The selected project has a status of missing. Please check the server and make sure ' + + ' the directory exists and file permissions are set correctly.'; + break; + } + Wait('stop'); + if (msg) { + Alert('Warning', msg, 'alert-info', null, null, null, null, true); + } + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: 'Error!', msg: 'Failed to get project ' + $scope.project + + '. GET returned status: ' + status }); + }); + } + }; + + if ($scope.removerelatedschedules) { + $scope.removerelatedschedules(); + } + $scope.removerelatedschedules = $scope.$on('relatedschedules', function() { + SchedulesListInit({ + scope: $scope, + list: SchedulesList, + choices: null, + related: true + }); + }); + + // Register a watcher on project_name. Refresh the playbook list on change. + if ($scope.watchProjectUnregister) { + $scope.watchProjectUnregister(); + } + $scope.watchProjectUnregister = $scope.$watch('project', function (newValue, oldValue) { + if (newValue !== oldValue) { + getPlaybooks($scope.project); + checkSCMStatus(); + } + }); + + + + // Turn off 'Wait' after both cloud credential and playbook list come back + if ($scope.removeJobTemplateLoadFinished) { + $scope.removeJobTemplateLoadFinished(); + } + $scope.removeJobTemplateLoadFinished = $scope.$on('jobTemplateLoadFinished', function () { + CreateSelect2({ + element:'#job_templates_job_type', + multiple: false + }); + + CreateSelect2({ + element:'#playbook-select', + multiple: false + }); + + CreateSelect2({ + element:'#job_templates_verbosity', + multiple: false + }); + + for (var set in relatedSets) { + $scope.search(relatedSets[set].iterator); + } + SchedulesControllerInit({ + scope: $scope, + parent_scope: $scope, + iterator: 'schedule' + }); + + }); + + // Set the status/badge for each related job + if ($scope.removeRelatedCompletedJobs) { + $scope.removeRelatedCompletedJobs(); + } + $scope.removeRelatedCompletedJobs = $scope.$on('relatedcompleted_jobs', function () { + JobsControllerInit({ + scope: $scope, + parent_scope: $scope, + iterator: form.related.completed_jobs.iterator + }); + JobsListUpdate({ + scope: $scope, + parent_scope: $scope, + list: form.related.completed_jobs + }); + }); + + if ($scope.cloudCredentialReadyRemove) { + $scope.cloudCredentialReadyRemove(); + } + $scope.cloudCredentialReadyRemove = $scope.$on('cloudCredentialReady', function (e, name) { + var CloudCredentialList = {}; + $scope.cloud_credential_name = name; + master.cloud_credential_name = name; + // Clone the CredentialList object for use with cloud_credential. Cloning + // and changing properties to avoid collision. + jQuery.extend(true, CloudCredentialList, CredentialList); + CloudCredentialList.name = 'cloudcredentials'; + CloudCredentialList.iterator = 'cloudcredential'; + LookUpInit({ + url: GetBasePath('credentials') + '?cloud=true', + scope: $scope, + form: form, + current_item: $scope.cloud_credential, + list: CloudCredentialList, + field: 'cloud_credential', + hdr: 'Select Cloud Credential', + input_type: "radio" + }); + $scope.$emit('jobTemplateLoadFinished'); + }); + + + // Retrieve each related set and populate the playbook list + if ($scope.jobTemplateLoadedRemove) { + $scope.jobTemplateLoadedRemove(); + } + $scope.jobTemplateLoadedRemove = $scope.$on('jobTemplateLoaded', function (e, related_cloud_credential, masterObject, relatedSets) { + var dft, set; + master = masterObject; + getPlaybooks($scope.project); + + for (set in relatedSets) { + $scope.search(relatedSets[set].iterator); + } + + dft = ($scope.host_config_key === "" || $scope.host_config_key === null) ? false : true; + md5Setup({ + scope: $scope, + master: master, + check_field: 'allow_callbacks', + default_val: dft + }); + + ParseTypeChange({ scope: $scope, field_id: 'job_templates_variables', onChange: callback }); + + if (related_cloud_credential) { + Rest.setUrl(related_cloud_credential); + Rest.get() + .success(function (data) { + $scope.$emit('cloudCredentialReady', data.name); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, null, {hdr: 'Error!', + msg: 'Failed to related cloud credential. GET returned status: ' + status }); + }); + } else { + // No existing cloud credential + $scope.$emit('cloudCredentialReady', null); + } + }); + + Wait('start'); + + if ($scope.removeEnableSurvey) { + $scope.removeEnableSurvey(); + } + $scope.removeEnableSurvey = $scope.$on('EnableSurvey', function(fld) { + + $('#job_templates_survey_enabled_chbox').attr('checked', $scope[fld]); + Rest.setUrl(defaultUrl + id+ '/survey_spec/'); + Rest.get() + .success(function (data) { + if(!data || !data.name){ + $('#job_templates_delete_survey_btn').hide(); + $('#job_templates_edit_survey_btn').hide(); + $('#job_templates_create_survey_btn').show(); + } + else { + $scope.survey_exists = true; + $('#job_templates_delete_survey_btn').show(); + $('#job_templates_edit_survey_btn').show(); + $('#job_templates_create_survey_btn').hide(); + } + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { + hdr: 'Error!', + msg: 'Failed to retrieve job template: ' + $stateParams.template_id + '. GET status: ' + status + }); + }); + }); + + if ($scope.removeSurveySaved) { + $scope.rmoveSurveySaved(); + } + $scope.removeSurveySaved = $scope.$on('SurveySaved', function() { + Wait('stop'); + $scope.survey_exists = true; + $scope.invalid_survey = false; + $('#job_templates_survey_enabled_chbox').attr('checked', true); + $('#job_templates_delete_survey_btn').show(); + $('#job_templates_edit_survey_btn').show(); + $('#job_templates_create_survey_btn').hide(); + + }); + + if ($scope.removeLoadJobs) { + $scope.rmoveLoadJobs(); + } + $scope.removeLoadJobs = $scope.$on('LoadJobs', function() { + $scope.fillJobTemplate(); + }); + + if ($scope.removeChoicesReady) { + $scope.removeChoicesReady(); + } + $scope.removeChoicesReady = $scope.$on('choicesReady', function() { + choicesCount++; + if (choicesCount === 4) { + $scope.$emit('LoadJobs'); + } + }); + + GetChoices({ + scope: $scope, + url: GetBasePath('unified_jobs'), + field: 'status', + variable: 'status_choices', + callback: 'choicesReady' + }); + + GetChoices({ + scope: $scope, + url: GetBasePath('unified_jobs'), + field: 'type', + variable: 'type_choices', + callback: 'choicesReady' + }); + + // setup verbosity options lookup + GetChoices({ + scope: $scope, + url: defaultUrl, + field: 'verbosity', + variable: 'verbosity_options', + callback: 'choicesReady' + }); + + // setup job type options lookup + GetChoices({ + scope: $scope, + url: defaultUrl, + field: 'job_type', + variable: 'job_type_options', + callback: 'choicesReady' + }); + + function saveCompleted() { + setTimeout(function() { + $scope.$apply(function() { + var base = $location.path().replace(/^\//, '').split('/')[0]; + if (base === 'job_templates') { + ReturnToCaller(); + } + else { + ReturnToCaller(1); + } + }); + }, 500); + } + + if ($scope.removeTemplateSaveSuccess) { + $scope.removeTemplateSaveSuccess(); + } + $scope.removeTemplateSaveSuccess = $scope.$on('templateSaveSuccess', function(e, data) { + Wait('stop'); + if ($scope.allow_callbacks && ($scope.host_config_key !== master.host_config_key || $scope.callback_url !== master.callback_url)) { + if (data.related && data.related.callback) { + Alert('Callback URL', '

Host callbacks are enabled for this template. The callback URL is:

'+ + '

' + $scope.callback_server_path + data.related.callback + '

'+ + '

The host configuration key is: ' + $filter('sanitize')(data.host_config_key) + '

', 'alert-info', saveCompleted, null, null, null, true); + } + else { + saveCompleted(); + } + } + else { + saveCompleted(); + } + }); + + + + // Save changes to the parent + $scope.formSave = function () { + $scope.invalid_survey = false; + if ($scope.removeGatherFormFields) { + $scope.removeGatherFormFields(); + } + $scope.removeGatherFormFields = $scope.$on('GatherFormFields', function(e, data) { + generator.clearApiErrors(); + Wait('start'); + data = {}; + var fld; + try { + // Make sure we have valid variable data + data.extra_vars = ToJSON($scope.parseType, $scope.variables, true); + if(data.extra_vars === undefined ){ + throw 'undefined variables'; + } + for (fld in form.fields) { + if (form.fields[fld].type === 'select' && fld !== 'playbook') { + data[fld] = $scope[fld].value; + } else { + if (fld !== 'variables' && fld !== 'callback_url') { + data[fld] = $scope[fld]; + } + } + } + Rest.setUrl(defaultUrl + id + '/'); + Rest.put(data) + .success(function (data) { + $scope.$emit('templateSaveSuccess', data); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to update job template. PUT returned status: ' + status }); + }); + + } catch (err) { + Wait('stop'); + Alert("Error", "Error parsing extra variables. Parser returned: " + err); + } + }); + + + if ($scope.removePromptForSurvey) { + $scope.removePromptForSurvey(); + } + $scope.removePromptForSurvey = $scope.$on('PromptForSurvey', function() { + var action = function () { + // $scope.$emit("GatherFormFields"); + Wait('start'); + $('#prompt-modal').modal('hide'); + $scope.addSurvey(); + + }; + Prompt({ + hdr: 'Incomplete Survey', + body: '
Do you want to create a survey before proceeding?
', + action: action + }); + }); + + // users can't save a survey with a scan job + if($scope.job_type.value === "scan" && $scope.survey_enabled === true){ + $scope.survey_enabled = false; + } + if($scope.survey_enabled === true && $scope.survey_exists!==true){ + // $scope.$emit("PromptForSurvey"); + + // The original design for this was a pop up that would prompt the user if they wanted to create a + // survey, because they had enabled one but not created it yet. We switched this for now so that + // an error message would be displayed by the survey buttons that tells the user to add a survey or disabled + // surveys. + $scope.invalid_survey = true; + return; + } else { + $scope.$emit("GatherFormFields"); + } + + }; + + $scope.formCancel = function () { + $state.transitionTo('jobTemplates'); + }; + + // Related set: Add button + $scope.add = function (set) { + $rootScope.flashMessage = null; + $location.path('/' + base + '/' + $stateParams.template_id + '/' + set); + }; + + // Related set: Edit button + $scope.edit = function (set, id) { + $rootScope.flashMessage = null; + $location.path('/' + set + '/' + id); + }; + + // Launch a job using the selected template + $scope.launch = function() { + + if ($scope.removePromptForSurvey) { + $scope.removePromptForSurvey(); + } + $scope.removePromptForSurvey = $scope.$on('PromptForSurvey', function() { + var action = function () { + // $scope.$emit("GatherFormFields"); + Wait('start'); + $('#prompt-modal').modal('hide'); + $scope.addSurvey(); + + }; + Prompt({ + hdr: 'Incomplete Survey', + body: '
Do you want to create a survey before proceeding?
', + action: action + }); + }); + if($scope.survey_enabled === true && $scope.survey_exists!==true){ + $scope.$emit("PromptForSurvey"); + } + else { + + PlaybookRun({ + scope: $scope, + id: id + }); + } + }; + + // handler for 'Enable Survey' button + $scope.surveyEnabled = function(){ + Rest.setUrl(defaultUrl + id+ '/'); + Rest.patch({"survey_enabled": $scope.survey_enabled}) + .success(function (data) { + + if(Empty(data.summary_fields.survey)){ + $('#job_templates_delete_survey_btn').hide(); + $('#job_templates_edit_survey_btn').hide(); + $('#job_templates_create_survey_btn').show(); + } + else{ + $scope.survey_exists = true; + $('#job_templates_delete_survey_btn').show(); + $('#job_templates_edit_survey_btn').show(); + $('#job_templates_create_survey_btn').hide(); + } + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { + hdr: 'Error!', + msg: 'Failed to retrieve save survey_enabled: ' + $stateParams.template_id + '. GET status: ' + status + }); + }); + }; + + + } + ]; diff --git a/awx/ui/client/src/job-templates/edit/job-templates-edit.partial.html b/awx/ui/client/src/job-templates/edit/job-templates-edit.partial.html new file mode 100644 index 0000000000..cb55a8a700 --- /dev/null +++ b/awx/ui/client/src/job-templates/edit/job-templates-edit.partial.html @@ -0,0 +1,5 @@ +
+
+
+
+
diff --git a/awx/ui/client/src/job-templates/edit/job-templates-edit.route.js b/awx/ui/client/src/job-templates/edit/job-templates-edit.route.js new file mode 100644 index 0000000000..2a4ab960e4 --- /dev/null +++ b/awx/ui/client/src/job-templates/edit/job-templates-edit.route.js @@ -0,0 +1,22 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import {templateUrl} from '../../shared/template-url/template-url.factory'; + +export default { + name: 'jobTemplates.edit', + url: '/:template_id', + templateUrl: templateUrl('job-templates/edit/job-templates-edit'), + controller: 'JobTemplatesEdit', + data: { + activityStreamId: 'template_id' + }, + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }] + } +}; diff --git a/awx/ui/client/src/job-templates/edit/main.js b/awx/ui/client/src/job-templates/edit/main.js new file mode 100644 index 0000000000..b7a48404ec --- /dev/null +++ b/awx/ui/client/src/job-templates/edit/main.js @@ -0,0 +1,17 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import jobTemplateEditRoute from './job-templates-edit.route'; +import inventoryJobTemplateEditRoute from './inventory-job-templates-edit.route'; +import controller from './job-templates-edit.controller'; + +export default + angular.module('jobTemplatesEdit', []) + .controller('JobTemplatesEdit', controller) + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(jobTemplateEditRoute); + $stateExtender.addState(inventoryJobTemplateEditRoute); + }]); diff --git a/awx/ui/client/src/job-templates/list/job-templates-list.controller.js b/awx/ui/client/src/job-templates/list/job-templates-list.controller.js new file mode 100644 index 0000000000..c23258017f --- /dev/null +++ b/awx/ui/client/src/job-templates/list/job-templates-list.controller.js @@ -0,0 +1,241 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default + [ '$scope', '$rootScope', '$location', '$log', + '$stateParams', 'Rest', 'Alert', 'JobTemplateList', 'generateList', + 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', + 'ProcessErrors', 'GetBasePath', 'JobTemplateForm', 'CredentialList', + 'LookUpInit', 'PlaybookRun', 'Wait', 'CreateDialog' , '$compile', + '$state', + + function( + $scope, $rootScope, $location, $log, + $stateParams, Rest, Alert, JobTemplateList, GenerateList, Prompt, + SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, + GetBasePath, JobTemplateForm, CredentialList, LookUpInit, PlaybookRun, + Wait, CreateDialog, $compile, $state + ) { + + ClearScope(); + + var list = JobTemplateList, + defaultUrl = GetBasePath('job_templates'), + view = GenerateList, + base = $location.path().replace(/^\//, '').split('/')[0], + mode = (base === 'job_templates') ? 'edit' : 'select'; + + view.inject(list, { mode: mode, scope: $scope }); + $rootScope.flashMessage = null; + + if ($scope.removePostRefresh) { + $scope.removePostRefresh(); + } + $scope.removePostRefresh = $scope.$on('PostRefresh', function () { + // Cleanup after a delete + Wait('stop'); + $('#prompt-modal').modal('hide'); + }); + + SearchInit({ + scope: $scope, + set: 'job_templates', + list: list, + url: defaultUrl + }); + PaginateInit({ + scope: $scope, + list: list, + url: defaultUrl + }); + + // Called from Inventories tab, host failed events link: + if ($stateParams.name) { + $scope[list.iterator + 'SearchField'] = 'name'; + $scope[list.iterator + 'SearchValue'] = $stateParams.name; + $scope[list.iterator + 'SearchFieldLabel'] = list.fields.name.label; + } + + $scope.search(list.iterator); + + $scope.addJobTemplate = function () { + $state.transitionTo('jobTemplates.add'); + }; + + $scope.editJobTemplate = function (id) { + $state.transitionTo('jobTemplates.edit', {template_id: id}); + }; + + $scope.deleteJobTemplate = function (id, name) { + var action = function () { + $('#prompt-modal').modal('hide'); + Wait('start'); + var url = defaultUrl + id + '/'; + Rest.setUrl(url); + Rest.destroy() + .success(function () { + $scope.search(list.iterator); + }) + .error(function (data) { + Wait('stop'); + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); + }); + }; + + Prompt({ + hdr: 'Delete', + body: '
Are you sure you want to delete the job template below?
' + name + '
', + action: action, + actionText: 'DELETE' + }); + }; + + $scope.copyJobTemplate = function(id, name){ + var element, + buttons = [{ + "label": "Cancel", + "onClick": function() { + $(this).dialog('close'); + }, + "icon": "fa-times", + "class": "btn btn-default", + "id": "copy-close-button" + },{ + "label": "Copy", + "onClick": function() { + copyAction(); + // setTimeout(function(){ + // scope.$apply(function(){ + // if(mode==='survey-taker'){ + // scope.$emit('SurveyTakerCompleted'); + // } else{ + // scope.saveSurvey(); + // } + // }); + // }); + }, + "icon": "fa-copy", + "class": "btn btn-primary", + "id": "job-copy-button" + }], + copyAction = function () { + // retrieve the copy of the job template object from the api, then overwrite the name and throw away the id + Wait('start'); + var url = defaultUrl + id + '/'; + Rest.setUrl(url); + Rest.get() + .success(function (data) { + data.name = $scope.new_copy_name; + delete data.id; + $scope.$emit('GoToCopy', data); + }) + .error(function (data) { + Wait('stop'); + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); + }); + }; + + + CreateDialog({ + id: 'copy-job-modal', + title: "Copy", + scope: $scope, + buttons: buttons, + width: 500, + height: 300, + minWidth: 200, + callback: 'CopyDialogReady' + }); + + $('#job_name').text(name); + $('#copy-job-modal').show(); + + + if ($scope.removeCopyDialogReady) { + $scope.removeCopyDialogReady(); + } + $scope.removeCopyDialogReady = $scope.$on('CopyDialogReady', function() { + //clear any old remaining text + $scope.new_copy_name = "" ; + $scope.copy_form.$setPristine(); + $('#copy-job-modal').dialog('open'); + $('#job-copy-button').attr('ng-disabled', "!copy_form.$valid"); + element = angular.element(document.getElementById('job-copy-button')); + $compile(element)($scope); + + }); + + if ($scope.removeGoToCopy) { + $scope.removeGoToCopy(); + } + $scope.removeGoToCopy = $scope.$on('GoToCopy', function(e, data) { + var url = defaultUrl, + old_survey_url = (data.related.survey_spec) ? data.related.survey_spec : "" ; + Rest.setUrl(url); + Rest.post(data) + .success(function (data) { + if(data.survey_enabled===true){ + $scope.$emit("CopySurvey", data, old_survey_url); + } + else { + $('#copy-job-modal').dialog('close'); + Wait('stop'); + $location.path($location.path() + '/' + data.id); + } + + }) + .error(function (data) { + Wait('stop'); + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); + }); + }); + + if ($scope.removeCopySurvey) { + $scope.removeCopySurvey(); + } + $scope.removeCopySurvey = $scope.$on('CopySurvey', function(e, new_data, old_url) { + // var url = data.related.survey_spec; + Rest.setUrl(old_url); + Rest.get() + .success(function (survey_data) { + + Rest.setUrl(new_data.related.survey_spec); + Rest.post(survey_data) + .success(function () { + $('#copy-job-modal').dialog('close'); + Wait('stop'); + $location.path($location.path() + '/' + new_data.id); + }) + .error(function (data) { + Wait('stop'); + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + new_data.related.survey_spec + ' failed. DELETE returned status: ' + status }); + }); + + + }) + .error(function (data) { + Wait('stop'); + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + old_url + ' failed. DELETE returned status: ' + status }); + }); + + }); + + }; + + $scope.submitJob = function (id) { + PlaybookRun({ scope: $scope, id: id }); + }; + + $scope.scheduleJob = function (id) { + $state.go('jobTemplateSchedules', {id: id}); + }; + } + ]; diff --git a/awx/ui/client/src/partials/job_templates.html b/awx/ui/client/src/job-templates/list/job-templates-list.partial.html similarity index 95% rename from awx/ui/client/src/partials/job_templates.html rename to awx/ui/client/src/job-templates/list/job-templates-list.partial.html index 9aa018d9bc..4fcfdeb6e9 100644 --- a/awx/ui/client/src/partials/job_templates.html +++ b/awx/ui/client/src/job-templates/list/job-templates-list.partial.html @@ -3,7 +3,6 @@
-
diff --git a/awx/ui/client/src/job-templates/main.js b/awx/ui/client/src/job-templates/main.js index bba8038410..4bbf4c1125 100644 --- a/awx/ui/client/src/job-templates/main.js +++ b/awx/ui/client/src/job-templates/main.js @@ -10,7 +10,10 @@ import surveyMaker from './survey-maker/main'; import jobTemplatesList from './list/main'; import jobTemplatesAdd from './add/main'; import jobTemplatesEdit from './edit/main'; +import jobTemplatesCopy from './copy/main'; export default - angular.module('jobTemplates', [surveyMaker.name, jobTemplatesList.name, jobTemplatesAdd.name, jobTemplatesEdit.name]) + angular.module('jobTemplates', + [surveyMaker.name, jobTemplatesList.name, jobTemplatesAdd.name, + jobTemplatesEdit.name, jobTemplatesCopy.name]) .service('deleteJobTemplate', deleteJobTemplate); diff --git a/awx/ui/client/src/lists/JobTemplates.js b/awx/ui/client/src/lists/JobTemplates.js index 55ed708df2..d7331d62d5 100644 --- a/awx/ui/client/src/lists/JobTemplates.js +++ b/awx/ui/client/src/lists/JobTemplates.js @@ -71,7 +71,7 @@ export default }, copy: { label: 'Copy', - ngClick: "copyJobTemplate(job_template.id, job_template.name)", + 'ui-sref': 'jobTemplates.copy({id: job_template.id})', "class": 'btn-danger btn-xs', awToolTip: 'Copy template', dataPlacement: 'top', diff --git a/awx/ui/client/src/lists/ScanJobs.js b/awx/ui/client/src/lists/ScanJobs.js index 457a7d2d1e..93eaa9486c 100644 --- a/awx/ui/client/src/lists/ScanJobs.js +++ b/awx/ui/client/src/lists/ScanJobs.js @@ -58,8 +58,7 @@ export default }, copy: { label: 'Copy', - ngClick: "copyJobTemplate(job_template.id, job_template.name)", - "class": 'btn-danger btn-xs', + 'ui-sref': 'jobTemplates.copy({id: job_template.id})', "class": 'btn-danger btn-xs', awToolTip: 'Copy template', dataPlacement: 'top', ngHide: 'job_template.summary_fields.can_copy===false' diff --git a/awx/ui/client/tests/job-templates/delete-job-template.service-test.js b/awx/ui/client/tests/job-templates/delete-job-template.service-test.js index f301995c57..014d46651f 100644 --- a/awx/ui/client/tests/job-templates/delete-job-template.service-test.js +++ b/awx/ui/client/tests/job-templates/delete-job-template.service-test.js @@ -1,26 +1,44 @@ import '../support/node'; -import jobTemplates from 'job-templates/main'; -import {describeModule} from '../support/describe-module'; +import jobTemplatesModule from 'job-templates/main'; +import RestStub from '../support/rest-stub'; -describeModule(jobTemplates.name) - .testService('deleteJobTemplate', function(test, restStub) { +//import RestStub from '../support/rest-stub'; - var service; +describe('jobTemplates.service', function(){ + var $httpBackend, jobTemplates, service, Rest, $q, $stateExtender; - test.withService(function(_service) { - service = _service; - }); + before('instantiate RestStub', function(){ + Rest = new RestStub(); + }); - it('deletes the job template', function() { - var result = {}; + beforeEach('instantiate the jobTemplates module', function(){ + angular.mock.module(jobTemplatesModule.name); + }); - var actual = service(); + beforeEach('mock dependencies', angular.mock.module(['$provide', function(_$provide_){ + var $provide = _$provide_; + $provide.value('GetBasePath', angular.noop); + $provide.value('$stateExtender', {addState: angular.noop}); + $provide.value('Rest', Rest); + }])); - restStub.succeedOn('destroy', result); - restStub.flush(); + beforeEach('put $q into the scope', window.inject(['$q', function($q){ + Rest.$q = $q; + }])) - expect(actual).to.eventually.equal(result); + beforeEach('inject real dependencies', inject(function($injector){ + $httpBackend = $injector.get('$httpBackend'); + service = $injector.get('deleteJobTemplate'); + })); + describe('deleteJobTemplate', function(){ + it('deletes a job template', function() { + var result = {}; + var actual = service.deleteJobTemplate(1); + + $httpBackend.when('DELETE', 'url').respond(200) + expect(actual).to.eventually.equal(result); }); }); +}); diff --git a/config/awx-munin.conf b/config/awx-munin.conf index 90c479f77a..833a6f36bf 100644 --- a/config/awx-munin.conf +++ b/config/awx-munin.conf @@ -1,17 +1,12 @@ - -Alias /munin /var/cache/munin/www - +Alias /munin /var/www/html/munin/ + Order Allow,Deny Allow from all - Options FollowSymLinks AuthUserFile /var/lib/awx/.munin_htpasswd AuthName "Munin" AuthType Basic require valid-user - - ExpiresActive On - ExpiresDefault M310 - +ScriptAlias /munin-cgi/munin-cgi-graph /var/www/cgi-bin/munin-cgi-graph \ No newline at end of file From eab223d229c30bd4d1b2bb62ce6ece19e22bdb68 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Fri, 11 Mar 2016 15:11:08 -0500 Subject: [PATCH 39/63] Make sure we are covering system jobs and template on notifications --- awx/api/serializers.py | 5 ++++ awx/api/urls.py | 4 +++ awx/api/views.py | 27 +++++++++++++++++++ .../management/commands/run_task_system.py | 2 ++ awx/main/models/jobs.py | 7 +++++ awx/main/signals.py | 4 ++- awx/main/tasks.py | 10 +++++++ 7 files changed, 58 insertions(+), 1 deletion(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index f8ab5c4d73..9aed65dbf4 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1852,6 +1852,10 @@ class SystemJobTemplateSerializer(UnifiedJobTemplateSerializer): jobs = reverse('api:system_job_template_jobs_list', args=(obj.pk,)), schedules = reverse('api:system_job_template_schedules_list', args=(obj.pk,)), launch = reverse('api:system_job_template_launch', args=(obj.pk,)), + notifiers_any = reverse('api:system_job_template_notifiers_any_list', args=(obj.pk,)), + notifiers_success = reverse('api:system_job_template_notifiers_success_list', args=(obj.pk,)), + notifiers_error = reverse('api:system_job_template_notifiers_error_list', args=(obj.pk,)), + )) return res @@ -1866,6 +1870,7 @@ class SystemJobSerializer(UnifiedJobSerializer): if obj.system_job_template and obj.system_job_template.active: res['system_job_template'] = reverse('api:system_job_template_detail', args=(obj.system_job_template.pk,)) + res['notifications'] = reverse('api:system_job_notifications_list', args=(obj.pk,)) if obj.can_cancel or True: res['cancel'] = reverse('api:system_job_cancel', args=(obj.pk,)) return res diff --git a/awx/api/urls.py b/awx/api/urls.py index ed36f4e057..394ff3639e 100644 --- a/awx/api/urls.py +++ b/awx/api/urls.py @@ -218,12 +218,16 @@ system_job_template_urls = patterns('awx.api.views', url(r'^(?P[0-9]+)/launch/$', 'system_job_template_launch'), url(r'^(?P[0-9]+)/jobs/$', 'system_job_template_jobs_list'), url(r'^(?P[0-9]+)/schedules/$', 'system_job_template_schedules_list'), + url(r'^(?P[0-9]+)/notifiers_any/$', 'system_job_template_notifiers_any_list'), + url(r'^(?P[0-9]+)/notifiers_error/$', 'system_job_template_notifiers_error_list'), + url(r'^(?P[0-9]+)/notifiers_success/$', 'system_job_template_notifiers_success_list'), ) system_job_urls = patterns('awx.api.views', url(r'^$', 'system_job_list'), url(r'^(?P[0-9]+)/$', 'system_job_detail'), url(r'^(?P[0-9]+)/cancel/$', 'system_job_cancel'), + url(r'^(?P[0-9]+)/notifications/$', 'system_job_notifications_list'), ) notifier_urls = patterns('awx.api.views', diff --git a/awx/api/views.py b/awx/api/views.py index 9b330805aa..b203015770 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2223,6 +2223,27 @@ class SystemJobTemplateJobsList(SubListAPIView): relationship = 'jobs' parent_key = 'system_job_template' +class SystemJobTemplateNotifiersAnyList(SubListCreateAttachDetachAPIView): + + model = Notifier + serializer_class = NotifierSerializer + parent_model = SystemJobTemplate + relationship = 'notifiers_any' + +class SystemJobTemplateNotifiersErrorList(SubListCreateAttachDetachAPIView): + + model = Notifier + serializer_class = NotifierSerializer + parent_model = SystemJobTemplate + relationship = 'notifiers_error' + +class SystemJobTemplateNotifiersSuccessList(SubListCreateAttachDetachAPIView): + + model = Notifier + serializer_class = NotifierSerializer + parent_model = SystemJobTemplate + relationship = 'notifiers_success' + class JobList(ListCreateAPIView): model = Job @@ -2903,6 +2924,12 @@ class SystemJobCancel(RetrieveAPIView): else: return self.http_method_not_allowed(request, *args, **kwargs) +class SystemJobNotificationsList(SubListAPIView): + + model = Notification + serializer_class = NotificationSerializer + parent_model = SystemJob + relationship = 'notifications' class UnifiedJobTemplateList(ListAPIView): diff --git a/awx/main/management/commands/run_task_system.py b/awx/main/management/commands/run_task_system.py index 5b5dd3bff0..9e933a0507 100644 --- a/awx/main/management/commands/run_task_system.py +++ b/awx/main/management/commands/run_task_system.py @@ -108,6 +108,8 @@ class SimpleDAG(object): return "inventory_update" elif type(obj) == ProjectUpdate: return "project_update" + elif type(obj) == SystemJob: + return "system_job" return "unknown" def get_dependencies(self, obj): diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 9c1ba1c50e..e8a13d7737 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -1065,6 +1065,13 @@ class SystemJobTemplate(UnifiedJobTemplate, SystemJobOptions): def cache_timeout_blocked(self): return False + @property + def notifiers(self): + base_notifiers = Notifier.objects.filter(active=True) + error_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_errors__in=[self])) + success_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_success__in=[self])) + any_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_any__in=[self])) + return dict(error=list(error_notifiers), success=list(success_notifiers), any=list(any_notifiers)) class SystemJob(UnifiedJob, SystemJobOptions): diff --git a/awx/main/signals.py b/awx/main/signals.py index 29c5c7d016..5a633ee0f6 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -393,9 +393,11 @@ def activity_stream_associate(sender, instance, **kwargs): obj2_id = entity_acted obj2_actual = obj2.objects.get(id=obj2_id) object2 = camelcase_to_underscore(obj2.__name__) - # Skip recording any inventory source changes here. + # Skip recording any inventory source, or system job template changes here. if isinstance(obj1, InventorySource) or isinstance(obj2_actual, InventorySource): continue + if isinstance(obj1, SystemJobTemplate) or isinstance(obj2_actual, SystemJobTemplate): + continue activity_entry = ActivityStream( operation=action, object1=object1, diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 1ad8524240..ec34886632 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -201,6 +201,11 @@ def handle_work_success(self, result, task_actual): instance_name = instance.module_name notifiers = [] # TODO: Ad-hoc commands need to notify someone friendly_name = "AdHoc Command" + elif task_actual['type'] == 'system_job': + instance = SystemJob.objects.get(id=task_actual['id']) + instance_name = instance.system_job_template.name + notifiers = instance.system_job_template.notifiers + friendly_name = "System Job" else: return notification_body = instance.notification_data() @@ -244,6 +249,11 @@ def handle_work_error(self, task_id, subtasks=None): instance_name = instance.module_name notifiers = [] friendly_name = "AdHoc Command" + elif task_actual['type'] == 'system_job': + instance = SystemJob.objects.get(id=task_actual['id']) + instance_name = instance.system_job_template.name + notifiers = instance.system_job_template.notifiers + friendly_name = "System Job" else: # Unknown task type break From ff1c41e8dcbe243c92e9014b591fbe155c5c3e7f Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Fri, 11 Mar 2016 15:33:16 -0500 Subject: [PATCH 40/63] revert unintended change to awx-munin.conf --- config/awx-munin.conf | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/config/awx-munin.conf b/config/awx-munin.conf index 833a6f36bf..5cfffe573e 100644 --- a/config/awx-munin.conf +++ b/config/awx-munin.conf @@ -1,12 +1,17 @@ -Alias /munin /var/www/html/munin/ - + +Alias /munin /var/cache/munin/www + Order Allow,Deny Allow from all + Options FollowSymLinks AuthUserFile /var/lib/awx/.munin_htpasswd AuthName "Munin" AuthType Basic require valid-user - -ScriptAlias /munin-cgi/munin-cgi-graph /var/www/cgi-bin/munin-cgi-graph \ No newline at end of file + + ExpiresActive On + ExpiresDefault M310 + + \ No newline at end of file From 765dcd33185224f9dad0db87c7b3588592642c13 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 11 Mar 2016 08:47:33 -0500 Subject: [PATCH 41/63] Added queries that calculate counts for organization resources --- awx/api/serializers.py | 8 ++ awx/api/views.py | 78 +++++++++++++++++++ .../api/test_organization_counts.py | 54 +++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 awx/main/tests/functional/api/test_organization_counts.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 9aed65dbf4..743ca1c054 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -798,6 +798,14 @@ class OrganizationSerializer(BaseSerializer): )) return res + def get_summary_fields(self, obj): + summary_dict = super(OrganizationSerializer, self).get_summary_fields(obj) + counts_dict = self.context.get('counts', None) + if counts_dict is not None and summary_dict is not None: + print 'counts_dict: ' + str(counts_dict) + summary_dict['counts'] = counts_dict[obj.id] + return summary_dict + class ProjectOptionsSerializer(BaseSerializer): diff --git a/awx/api/views.py b/awx/api/views.py index b203015770..09a14f5381 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -596,6 +596,15 @@ class OrganizationList(ListCreateAPIView): model = Organization serializer_class = OrganizationSerializer + # @paginated + # def get(self, *args, **kwargs): + # # self.paginated_params = {'limit': limit, 'offset': offset, 'ordering': ordering} + # limit = kwargs.pop('limit') + # offset = kwargs.pop('offset') + # ordering = kwargs.pop('ordering') + # # qs[offset:offset + limit] + # return (super(OrganizationList, self).get(*args, **kwargs), 5, None) + def create(self, request, *args, **kwargs): """Create a new organzation. @@ -614,6 +623,75 @@ class OrganizationList(ListCreateAPIView): # Okay, create the organization as usual. return super(OrganizationList, self).create(request, *args, **kwargs) + def get_serializer_context(self, *args, **kwargs): + full_context = super(OrganizationList, self).get_serializer_context(*args, **kwargs) + + if self.request is None: + return full_context + + db_results = {} + org_qs = self.request.user.get_queryset(self.model) + org_id_list = org_qs.values('id') + if len(org_id_list) == 0: + return full_context + + # Produce counts of Foreign Key relationships + db_results['inventories'] = self.request.user.get_queryset(Inventory)\ + .values('organization').annotate(Count('organization')).order_by('organization') + + db_results['teams'] = self.request.user.get_queryset(Team)\ + .values('organization').annotate(Count('organization')).order_by('organization') + + JT_reference = 'inventory__organization' + db_JT_results = self.request.user.get_queryset(JobTemplate)\ + .values(JT_reference).annotate(Count(JT_reference)).\ + order_by(JT_reference) + + # Produce counts of m2m relationships + project_qs = self.request.user.get_queryset(Project) + db_results['projects'] = Organization.projects.through.objects\ + .filter( + project_id__in=project_qs.values_list('pk', flat=True), + organization_id__in=org_qs.values_list('pk', flat=True))\ + .values('organization')\ + .annotate(Count('organization')).order_by('organization') + + # TODO: When RBAC branch merges, change these to role relation + user_qs = self.request.user.get_queryset(User) + db_results['users'] = Organization.users.through.objects\ + .filter( + user_id__in=user_qs.values_list('pk', flat=True), + organization_id__in=org_qs.values_list('pk', flat=True))\ + .values('organization')\ + .annotate(Count('organization')).order_by('organization') + + db_results['admins'] = Organization.admins.through.objects\ + .filter( + user_id__in=user_qs.values_list('pk', flat=True), + organization_id__in=org_qs.values_list('pk', flat=True))\ + .values('organization')\ + .annotate(Count('organization')).order_by('organization') + + count_context = {} + for org in org_id_list: + org_id = org['id'] + count_context[org_id] = {'inventories': 0, 'teams': 0, 'users': 0, + 'job_templates': 0, 'admins': 0, + 'projects': 0} + + for res in db_results: + for entry in db_results[res]: + org_id = entry['organization'] + count_context[org_id][res] = entry['organization__count'] + + for entry in db_JT_results: + org_id = entry[JT_reference] + count_context[org_id]['job_templates'] = entry['%s__count' % JT_reference] + + full_context['counts'] = count_context + + return full_context + class OrganizationDetail(RetrieveUpdateDestroyAPIView): model = Organization diff --git a/awx/main/tests/functional/api/test_organization_counts.py b/awx/main/tests/functional/api/test_organization_counts.py new file mode 100644 index 0000000000..6a31bf1151 --- /dev/null +++ b/awx/main/tests/functional/api/test_organization_counts.py @@ -0,0 +1,54 @@ +import pytest + +from django.core.urlresolvers import reverse + +@pytest.fixture +def resourced_organization(organization, project, user): + admin_user = user('test-admin', True) + member_user = user('org-member') + + # Associate one resource of every type with the organization + organization.users.add(member_user) + organization.admins.add(admin_user) + organization.projects.add(project) + organization.teams.create(name='org-team') + inventory = organization.inventories.create(name="associated-inv") + inventory.jobtemplates.create(name="test-jt", + description="test-job-template-desc", + project=project, + playbook="test_playbook.yml") + + return organization + +@pytest.mark.django_db +def test_org_counts_admin(resourced_organization, user, get): + # Check that all types of resources are counted by a superuser + external_admin = user('admin', True) + response = get(reverse('api:organization_list', args=[]), external_admin) + counts = response.data['results'][0]['summary_fields']['counts'] + + assert counts == { + 'users': 1, + 'admins': 1, + 'job_templates': 1, + 'projects': 1, + 'inventories': 1, + 'teams': 1 + } + +@pytest.mark.django_db +def test_org_counts_member(resourced_organization, get): + # Check that a non-admin user can only see the full project and + # user count, consistent with the RBAC rules + member_user = resourced_organization.users.get(username='org-member') + response = get(reverse('api:organization_list', args=[]), member_user) + counts = response.data['results'][0]['summary_fields']['counts'] + + assert counts == { + 'users': 1, # User can see themselves + 'admins': 0, + 'job_templates': 0, + 'projects': 1, # Projects are shared with all the organization + 'inventories': 0, + 'teams': 0 + } From 9f25a48936413c072c0b97d6048abd3eb9952b90 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Sun, 13 Mar 2016 11:45:08 -0400 Subject: [PATCH 42/63] fix for POST scenario --- awx/api/views.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index 09a14f5381..7d7e5b34ad 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -596,15 +596,6 @@ class OrganizationList(ListCreateAPIView): model = Organization serializer_class = OrganizationSerializer - # @paginated - # def get(self, *args, **kwargs): - # # self.paginated_params = {'limit': limit, 'offset': offset, 'ordering': ordering} - # limit = kwargs.pop('limit') - # offset = kwargs.pop('offset') - # ordering = kwargs.pop('ordering') - # # qs[offset:offset + limit] - # return (super(OrganizationList, self).get(*args, **kwargs), 5, None) - def create(self, request, *args, **kwargs): """Create a new organzation. @@ -673,11 +664,16 @@ class OrganizationList(ListCreateAPIView): .annotate(Count('organization')).order_by('organization') count_context = {} + zeroed_dict = {'inventories': 0, 'teams': 0, 'users': 0, + 'job_templates': 0, 'admins': 0, 'projects': 0} for org in org_id_list: org_id = org['id'] - count_context[org_id] = {'inventories': 0, 'teams': 0, 'users': 0, - 'job_templates': 0, 'admins': 0, - 'projects': 0} + count_context[org_id] = zeroed_dict.copy() + if self.request.method == 'POST': + org_id = max([int(k) for k in count_context.keys()]) + 1 + # org_id = instance = self.get_object().id + # self.request.data['id'] + count_context[org_id] = zeroed_dict for res in db_results: for entry in db_results[res]: From 39c956335243832688db842558108bd8f984418b Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Sun, 13 Mar 2016 20:40:21 -0400 Subject: [PATCH 43/63] test and fix for POST to empty list scenaro and JT count fix --- awx/api/serializers.py | 8 ++- awx/api/views.py | 19 +++---- .../api/test_organization_counts.py | 51 +++++++++++++++++++ 3 files changed, 67 insertions(+), 11 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 743ca1c054..514075bc29 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -802,8 +802,12 @@ class OrganizationSerializer(BaseSerializer): summary_dict = super(OrganizationSerializer, self).get_summary_fields(obj) counts_dict = self.context.get('counts', None) if counts_dict is not None and summary_dict is not None: - print 'counts_dict: ' + str(counts_dict) - summary_dict['counts'] = counts_dict[obj.id] + if obj.id not in counts_dict: + summary_dict['counts'] = { + 'inventories': 0, 'teams': 0, 'users': 0, + 'job_templates': 0, 'admins': 0, 'projects': 0} + else: + summary_dict['counts'] = counts_dict[obj.id] return summary_dict diff --git a/awx/api/views.py b/awx/api/views.py index 7d7e5b34ad..9bf2acbf40 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -624,17 +624,23 @@ class OrganizationList(ListCreateAPIView): org_qs = self.request.user.get_queryset(self.model) org_id_list = org_qs.values('id') if len(org_id_list) == 0: + if self.request.method == 'POST': + full_context['counts'] = {} return full_context # Produce counts of Foreign Key relationships - db_results['inventories'] = self.request.user.get_queryset(Inventory)\ + inv_qs = self.request.user.get_queryset(Inventory) + db_results['inventories'] = inv_qs\ .values('organization').annotate(Count('organization')).order_by('organization') db_results['teams'] = self.request.user.get_queryset(Team)\ .values('organization').annotate(Count('organization')).order_by('organization') JT_reference = 'inventory__organization' + # Extra filter is applied on the inventory, because this catches + # the case of deleted (and purged) inventory db_JT_results = self.request.user.get_queryset(JobTemplate)\ + .filter(inventory_id__in=inv_qs.values_list('pk', flat=True))\ .values(JT_reference).annotate(Count(JT_reference)).\ order_by(JT_reference) @@ -664,16 +670,11 @@ class OrganizationList(ListCreateAPIView): .annotate(Count('organization')).order_by('organization') count_context = {} - zeroed_dict = {'inventories': 0, 'teams': 0, 'users': 0, - 'job_templates': 0, 'admins': 0, 'projects': 0} for org in org_id_list: org_id = org['id'] - count_context[org_id] = zeroed_dict.copy() - if self.request.method == 'POST': - org_id = max([int(k) for k in count_context.keys()]) + 1 - # org_id = instance = self.get_object().id - # self.request.data['id'] - count_context[org_id] = zeroed_dict + count_context[org_id] = { + 'inventories': 0, 'teams': 0, 'users': 0, 'job_templates': 0, + 'admins': 0, 'projects': 0} for res in db_results: for entry in db_results[res]: diff --git a/awx/main/tests/functional/api/test_organization_counts.py b/awx/main/tests/functional/api/test_organization_counts.py index 6a31bf1151..694eb24468 100644 --- a/awx/main/tests/functional/api/test_organization_counts.py +++ b/awx/main/tests/functional/api/test_organization_counts.py @@ -52,3 +52,54 @@ def test_org_counts_member(resourced_organization, get): 'inventories': 0, 'teams': 0 } + +@pytest.mark.django_db +def test_new_org_zero_counts(user, post): + # Check that a POST to the organization list endpoint returns + # correct counts, including the new record + org_list_url = reverse('api:organization_list', args=[]) + post_response = post(url=org_list_url, data={'name': 'test organization', + 'description': ''}, user=user('admin', True)) + new_org_list = post_response.render().data + counts_dict = new_org_list['summary_fields']['counts'] + + assert counts_dict == { + 'users': 0, + 'admins': 0, + 'job_templates': 0, + 'projects': 0, + 'inventories': 0, + 'teams': 0 + } + +@pytest.mark.django_db +def test_two_organizations(resourced_organization, organizations, user, get): + # Check correct results for two organizations are returned + external_admin = user('admin', True) + organization_zero = organizations(1)[0] + response = get(reverse('api:organization_list', args=[]), external_admin) + org_id_full = resourced_organization.id + org_id_zero = organization_zero.id + print ' ids: ' + str(org_id_full) + " : " + str(org_id_zero) + print ' counts_dict: ' + str(response.data['results']) + counts = {} + for i in range(2): + org_id = response.data['results'][i]['id'] + counts[org_id] = response.data['results'][i]['summary_fields']['counts'] + + assert counts[org_id_full] == { + 'users': 1, + 'admins': 1, + 'job_templates': 1, + 'projects': 1, + 'inventories': 1, + 'teams': 1 + } + assert counts[org_id_zero] == { + 'users': 0, + 'admins': 0, + 'job_templates': 0, + 'projects': 0, + 'inventories': 0, + 'teams': 0 + } From 6996ea22b00019eba7be4a61c150032b7549377c Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Sun, 13 Mar 2016 22:39:52 -0400 Subject: [PATCH 44/63] style tweaks, add one more assertion --- awx/api/views.py | 4 ++-- awx/main/tests/functional/api/test_organization_counts.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index 9bf2acbf40..ebdb57098d 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -641,8 +641,8 @@ class OrganizationList(ListCreateAPIView): # the case of deleted (and purged) inventory db_JT_results = self.request.user.get_queryset(JobTemplate)\ .filter(inventory_id__in=inv_qs.values_list('pk', flat=True))\ - .values(JT_reference).annotate(Count(JT_reference)).\ - order_by(JT_reference) + .values(JT_reference).annotate(Count(JT_reference))\ + .order_by(JT_reference) # Produce counts of m2m relationships project_qs = self.request.user.get_queryset(Project) diff --git a/awx/main/tests/functional/api/test_organization_counts.py b/awx/main/tests/functional/api/test_organization_counts.py index 694eb24468..56fdb8215e 100644 --- a/awx/main/tests/functional/api/test_organization_counts.py +++ b/awx/main/tests/functional/api/test_organization_counts.py @@ -63,6 +63,7 @@ def test_new_org_zero_counts(user, post): new_org_list = post_response.render().data counts_dict = new_org_list['summary_fields']['counts'] + assert post_response.status_code == 201 assert counts_dict == { 'users': 0, 'admins': 0, @@ -80,8 +81,6 @@ def test_two_organizations(resourced_organization, organizations, user, get): response = get(reverse('api:organization_list', args=[]), external_admin) org_id_full = resourced_organization.id org_id_zero = organization_zero.id - print ' ids: ' + str(org_id_full) + " : " + str(org_id_zero) - print ' counts_dict: ' + str(response.data['results']) counts = {} for i in range(2): org_id = response.data['results'][i]['id'] From 938772544206d24c7c5b23f4e4f74c433ba76db0 Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Mon, 14 Mar 2016 09:26:43 -0400 Subject: [PATCH 45/63] Lookup modal feedback from demo prep --- awx/ui/client/legacy-styles/jquery-ui-overrides.less | 3 +++ awx/ui/client/src/lookup/lookup.factory.js | 1 + 2 files changed, 4 insertions(+) diff --git a/awx/ui/client/legacy-styles/jquery-ui-overrides.less b/awx/ui/client/legacy-styles/jquery-ui-overrides.less index c40440bf8a..2027e218e9 100644 --- a/awx/ui/client/legacy-styles/jquery-ui-overrides.less +++ b/awx/ui/client/legacy-styles/jquery-ui-overrides.less @@ -31,6 +31,9 @@ table.ui-datepicker-calendar { opacity: .7; text-shadow: 0 1px 0 @white; } + .ui-dialog-content { + overflow: hidden; + } .ui-widget-header { border-radius: 0; border: none; diff --git a/awx/ui/client/src/lookup/lookup.factory.js b/awx/ui/client/src/lookup/lookup.factory.js index e52dfb4876..cf8507a941 100644 --- a/awx/ui/client/src/lookup/lookup.factory.js +++ b/awx/ui/client/src/lookup/lookup.factory.js @@ -175,6 +175,7 @@ export default ['Rest', 'ProcessErrors', 'generateList', width: 600, height: (instructions) ? 625 : 450, minWidth: 500, + resizable: false, title: hdr, id: 'LookupModal-dialog', onClose: function() { From 36182109e248f2073d5c81e6a1fc7f532448285e Mon Sep 17 00:00:00 2001 From: kensible Date: Mon, 14 Mar 2016 10:27:25 -0400 Subject: [PATCH 46/63] Revert "Lookup modal feedback from demo prep" --- awx/ui/client/legacy-styles/jquery-ui-overrides.less | 3 --- awx/ui/client/src/lookup/lookup.factory.js | 1 - 2 files changed, 4 deletions(-) diff --git a/awx/ui/client/legacy-styles/jquery-ui-overrides.less b/awx/ui/client/legacy-styles/jquery-ui-overrides.less index 2027e218e9..c40440bf8a 100644 --- a/awx/ui/client/legacy-styles/jquery-ui-overrides.less +++ b/awx/ui/client/legacy-styles/jquery-ui-overrides.less @@ -31,9 +31,6 @@ table.ui-datepicker-calendar { opacity: .7; text-shadow: 0 1px 0 @white; } - .ui-dialog-content { - overflow: hidden; - } .ui-widget-header { border-radius: 0; border: none; diff --git a/awx/ui/client/src/lookup/lookup.factory.js b/awx/ui/client/src/lookup/lookup.factory.js index cf8507a941..e52dfb4876 100644 --- a/awx/ui/client/src/lookup/lookup.factory.js +++ b/awx/ui/client/src/lookup/lookup.factory.js @@ -175,7 +175,6 @@ export default ['Rest', 'ProcessErrors', 'generateList', width: 600, height: (instructions) ? 625 : 450, minWidth: 500, - resizable: false, title: hdr, id: 'LookupModal-dialog', onClose: function() { From 5fdab74ae75c2af7851a5ace864ec8dc0e090835 Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Mon, 14 Mar 2016 11:36:29 -0400 Subject: [PATCH 47/63] Increase specificity and keep y-scrollbar for long name edge cases. --- awx/ui/client/legacy-styles/jquery-ui-overrides.less | 3 --- awx/ui/client/src/lookup/lookup.block.less | 4 ++++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/legacy-styles/jquery-ui-overrides.less b/awx/ui/client/legacy-styles/jquery-ui-overrides.less index 2027e218e9..c40440bf8a 100644 --- a/awx/ui/client/legacy-styles/jquery-ui-overrides.less +++ b/awx/ui/client/legacy-styles/jquery-ui-overrides.less @@ -31,9 +31,6 @@ table.ui-datepicker-calendar { opacity: .7; text-shadow: 0 1px 0 @white; } - .ui-dialog-content { - overflow: hidden; - } .ui-widget-header { border-radius: 0; border: none; diff --git a/awx/ui/client/src/lookup/lookup.block.less b/awx/ui/client/src/lookup/lookup.block.less index 9f2dea25a9..25c05ef803 100644 --- a/awx/ui/client/src/lookup/lookup.block.less +++ b/awx/ui/client/src/lookup/lookup.block.less @@ -19,4 +19,8 @@ .List-tableCell { color: @default-interface-txt; } + + &.ui-dialog-content { + overflow-x: hidden; + } } From b43f7e8c7f88d09164faf524d65b6cf5bba21958 Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Mon, 14 Mar 2016 11:42:44 -0400 Subject: [PATCH 48/63] Lookup modal not-resizable --- awx/ui/client/src/lookup/lookup.factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/lookup/lookup.factory.js b/awx/ui/client/src/lookup/lookup.factory.js index cf8507a941..ef4cfafba6 100644 --- a/awx/ui/client/src/lookup/lookup.factory.js +++ b/awx/ui/client/src/lookup/lookup.factory.js @@ -175,9 +175,9 @@ export default ['Rest', 'ProcessErrors', 'generateList', width: 600, height: (instructions) ? 625 : 450, minWidth: 500, - resizable: false, title: hdr, id: 'LookupModal-dialog', + resizable: false, onClose: function() { setTimeout(function() { scope.$apply(function() { From 001eb1f69f546fe83e5e84a22cde5576f4409558 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 14 Mar 2016 11:56:51 -0400 Subject: [PATCH 49/63] Handle a mongo OperationFailure This seems to happenw hen the database is up, allows us to connect but we don't have permission to access the objects (or they don't exist yet). --- awx/main/migrations/_system_tracking.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/awx/main/migrations/_system_tracking.py b/awx/main/migrations/_system_tracking.py index e5be20f4ef..936786609b 100644 --- a/awx/main/migrations/_system_tracking.py +++ b/awx/main/migrations/_system_tracking.py @@ -1,6 +1,7 @@ from awx.fact.models import FactVersion from mongoengine.connection import ConnectionError +from pymongo.errors import OperationFailure from django.conf import settings def drop_system_tracking_db(): @@ -11,6 +12,9 @@ def drop_system_tracking_db(): # TODO: Log this. Not a deal-breaker. Just let the user know they # may need to manually drop/delete the database. pass + except OperationFailure: + # TODO: This means the database was up but something happened when we tried to query it + return pass def migrate_facts(apps, schema_editor): Fact = apps.get_model('main', "Fact") @@ -22,6 +26,9 @@ def migrate_facts(apps, schema_editor): # TODO: Let the user know about the error. Likely this is # a new install and we just don't need to do this return (0, 0) + except OperationFailure: + # TODO: This means the database was up but something happened when we tried to query it + return (0, 0) migrated_count = 0 not_migrated_count = 0 From 7e818319b55b97e4c30fe4b608984b971e2e9523 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 14 Mar 2016 13:53:34 -0400 Subject: [PATCH 50/63] Pass, don't return pass when checking mongo connectivity --- awx/main/migrations/_system_tracking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/migrations/_system_tracking.py b/awx/main/migrations/_system_tracking.py index 936786609b..71206d253e 100644 --- a/awx/main/migrations/_system_tracking.py +++ b/awx/main/migrations/_system_tracking.py @@ -14,7 +14,7 @@ def drop_system_tracking_db(): pass except OperationFailure: # TODO: This means the database was up but something happened when we tried to query it - return pass + pass def migrate_facts(apps, schema_editor): Fact = apps.get_model('main', "Fact") From af9882af97790c920bf3602b8f766f835c42d067 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Mon, 14 Mar 2016 21:16:44 -0400 Subject: [PATCH 51/63] Yoink REST calls from Job Details controller into modular service, resolves #1239 --- .../host-event.route.js} | 0 .../src/job-detail/job-detail.controller.js | 97 ++++++-------- .../src/job-detail/job-detail.partial.html | 2 +- .../src/job-detail/job-detail.service.js | 118 ++++++++++++++++++ awx/ui/client/src/job-detail/main.js | 2 + 5 files changed, 160 insertions(+), 59 deletions(-) rename awx/ui/client/src/job-detail/{job-detail.factory.js => host-event/host-event.route.js} (100%) create mode 100644 awx/ui/client/src/job-detail/job-detail.service.js diff --git a/awx/ui/client/src/job-detail/job-detail.factory.js b/awx/ui/client/src/job-detail/host-event/host-event.route.js similarity index 100% rename from awx/ui/client/src/job-detail/job-detail.factory.js rename to awx/ui/client/src/job-detail/host-event/host-event.route.js diff --git a/awx/ui/client/src/job-detail/job-detail.controller.js b/awx/ui/client/src/job-detail/job-detail.controller.js index 447cec8d8c..e36dbb13de 100644 --- a/awx/ui/client/src/job-detail/job-detail.controller.js +++ b/awx/ui/client/src/job-detail/job-detail.controller.js @@ -1,5 +1,5 @@ /************************************************* - * Copyright (c) 2015 Ansible, Inc. + * Copyright (c) 2016 Ansible, Inc. * * All Rights Reserved *************************************************/ @@ -12,23 +12,22 @@ export default [ '$location', '$rootScope', '$filter', '$scope', '$compile', - '$stateParams', '$log', 'ClearScope', 'GetBasePath', 'Wait', 'Rest', + '$stateParams', '$log', 'ClearScope', 'GetBasePath', 'Wait', 'ProcessErrors', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed', 'DrawGraph', 'LoadHostSummary', 'ReloadHostSummaryList', - 'JobIsFinished', 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', - 'EventViewer', 'DeleteJob', 'PlaybookRun', 'HostEventsViewer', + 'JobIsFinished', 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'DeleteJob', 'PlaybookRun', 'HostEventsViewer', 'LoadPlays', 'LoadTasks', 'LoadHosts', 'HostsEdit', 'ParseVariableString', 'GetChoices', 'fieldChoices', 'fieldLabels', - 'EditSchedule', 'ParseTypeChange', + 'EditSchedule', 'ParseTypeChange', 'JobDetailService', 'EventViewer', function( $location, $rootScope, $filter, $scope, $compile, $stateParams, - $log, ClearScope, GetBasePath, Wait, Rest, ProcessErrors, + $log, ClearScope, GetBasePath, Wait, ProcessErrors, SelectPlay, SelectTask, Socket, GetElapsed, DrawGraph, LoadHostSummary, ReloadHostSummaryList, JobIsFinished, - SetTaskStyles, DigestEvent, UpdateDOM, EventViewer, DeleteJob, + SetTaskStyles, DigestEvent, UpdateDOM, DeleteJob, PlaybookRun, HostEventsViewer, LoadPlays, LoadTasks, LoadHosts, HostsEdit, ParseVariableString, GetChoices, fieldChoices, - fieldLabels, EditSchedule, ParseTypeChange + fieldLabels, EditSchedule, ParseTypeChange, JobDetailService, EventViewer ) { ClearScope(); @@ -283,15 +282,15 @@ export default scope.removeInitialLoadComplete(); } scope.removeInitialLoadComplete = scope.$on('InitialLoadComplete', function() { - var url; Wait('stop'); if (JobIsFinished(scope)) { scope.liveEventProcessing = false; // signal that event processing is over and endless scroll scope.pauseLiveEvents = false; // should be enabled - url = scope.job.related.job_events + '?event=playbook_on_stats'; - Rest.setUrl(url); - Rest.get() + var params = { + event: 'playbook_on_stats' + }; + JobDetailService.getRelatedJobEvents(scope.job.id, params) .success(function(data) { if (data.results.length > 0) { LoadHostSummary({ @@ -327,11 +326,11 @@ export default } scope.removeHostSummaries = scope.$on('LoadHostSummaries', function() { if(scope.job){ - var url = scope.job.related.job_host_summaries + '?'; - url += '&page_size=' + scope.hostSummariesMaxRows + '&order=host_name'; - - Rest.setUrl(url); - Rest.get() + var params = { + page_size: scope.hostSummariesMaxRows, + order: 'host_name' + }; + JobDetailService.getJobHostSummaries(scope.job.id, params) .success(function(data) { scope.next_host_summaries = data.next; if (data.results.length > 0) { @@ -357,10 +356,6 @@ export default }; }); scope.$emit('InitialLoadComplete'); - }) - .error(function(data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + '. GET returned: ' + status }); }); } @@ -373,17 +368,17 @@ export default if (scope.activeTask) { var play = scope.jobData.plays[scope.activePlay], - task, // = play.tasks[scope.activeTask], - url; + task; if(play){ task = play.tasks[scope.activeTask]; } if (play && task) { - url = scope.job.related.job_events + '?parent=' + task.id + '&'; - url += 'event__startswith=runner&page_size=' + scope.hostResultsMaxRows + '&order=host_name,counter'; - - Rest.setUrl(url); - Rest.get() + var params = { + parent: task.id, + event__startswith: 'runner', + page_size: scope.hostResultsMaxRows + }; + JobDetailService.getRelatedJobEvents(scope.job.id, params) .success(function(data) { var idx, event, status, status_text, item, msg; if (data.results.length > 0) { @@ -450,10 +445,6 @@ export default } } scope.$emit('LoadHostSummaries'); - }) - .error(function(data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + '. GET returned: ' + status }); }); } else { scope.$emit('LoadHostSummaries'); @@ -468,14 +459,15 @@ export default } scope.removeLoadTasks = scope.$on('LoadTasks', function() { if (scope.activePlay) { - var play = scope.jobData.plays[scope.activePlay], url; + var play = scope.jobData.plays[scope.activePlay]; if (play) { - url = scope.job.url + 'job_tasks/?event_id=' + play.id; - url += '&page_size=' + scope.tasksMaxRows + '&order=id'; - - Rest.setUrl(url); - Rest.get() + var params = { + event_id: play.id, + page_size: scope.tasksMaxRows, + order: 'id' + } + JobDetailService.getJobTasks(scope.job.id, params) .success(function(data) { scope.next_tasks = data.next; if (data.results.length > 0) { @@ -585,12 +577,10 @@ export default scope.host_summary.failed = 0; scope.host_summary.total = 0; scope.jobData.plays = {}; - - var url = scope.job.url + 'job_plays/?order_by=id'; - url += '&page_size=' + scope.playsMaxRows + '&order_by=id'; - - Rest.setUrl(url); - Rest.get() + var params = { + order_by: 'id' + }; + JobDetailService.getJobPlays(scope.job.id, params) .success( function(data) { scope.next_plays = data.next; if (data.results.length > 0) { @@ -681,10 +671,6 @@ export default scope.jobData.plays[scope.activePlay].playActiveClass = 'JobDetail-tableRow--selected'; } scope.$emit('LoadTasks', events_url); - }) - .error( function(data, status) { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + '. GET returned: ' + status }); }); }); @@ -702,8 +688,7 @@ export default scope.LoadHostSummaries = true; // Load the job record - Rest.setUrl(GetBasePath('jobs') + job_id + '/'); - Rest.get() + JobDetailService.getJob(job_id) .success(function(data) { var i; scope.job = data; @@ -1177,8 +1162,7 @@ export default if (((!scope.liveEventProcessing) || (scope.liveEventProcessing && scope.pauseLiveEvents)) && scope.next_plays) { $('#playsMoreRows').fadeIn(); scope.playsLoading = true; - Rest.setUrl(scope.next_plays); - Rest.get() + JobDetailService.getNextPage(scope.next_plays) .success( function(data) { scope.next_plays = data.next; data.results.forEach(function(event, idx) { @@ -1243,8 +1227,7 @@ export default if (((!scope.liveEventProcessing) || (scope.liveEventProcessing && scope.pauseLiveEvents)) && scope.next_tasks) { $('#tasksMoreRows').fadeIn(); scope.tasksLoading = true; - Rest.setUrl(scope.next_tasks); - Rest.get() + JobDetailService.getNextPage(scope.next_tasks) .success(function(data) { scope.next_tasks = data.next; data.results.forEach(function(event, idx) { @@ -1315,8 +1298,7 @@ export default if (((!scope.liveEventProcessing) || (scope.liveEventProcessing && scope.pauseLiveEvents)) && scope.next_host_results) { $('#hostResultsMoreRows').fadeIn(); scope.hostResultsLoading = true; - Rest.setUrl(scope.next_host_results); - Rest.get() + JobDetailService.getNextPage(scope.next_host_results) .success(function(data) { scope.next_host_results = data.next; data.results.forEach(function(row) { @@ -1387,8 +1369,7 @@ export default // check for more hosts when user scrolls to bottom of host summaries list... if (((!scope.liveEventProcessing) || (scope.liveEventProcessing && scope.pauseLiveEvents)) && scope.next_host_summaries) { scope.hostSummariesLoading = true; - Rest.setUrl(scope.next_host_summaries); - Rest.get() + JobDetailService.getNextPage(scope.next_host_summaries) .success(function(data) { scope.next_host_summaries = data.next; data.results.forEach(function(row) { diff --git a/awx/ui/client/src/job-detail/job-detail.partial.html b/awx/ui/client/src/job-detail/job-detail.partial.html index 8bbbc4aaaa..3ff7262d1c 100644 --- a/awx/ui/client/src/job-detail/job-detail.partial.html +++ b/awx/ui/client/src/job-detail/job-detail.partial.html @@ -344,7 +344,7 @@ - + diff --git a/awx/ui/client/src/job-detail/job-detail.service.js b/awx/ui/client/src/job-detail/job-detail.service.js new file mode 100644 index 0000000000..8597fff9f1 --- /dev/null +++ b/awx/ui/client/src/job-detail/job-detail.service.js @@ -0,0 +1,118 @@ +export default + ['$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors', function($rootScope, Rest, GetBasePath, ProcessErrors){ + return { + + /* + For ES6 + it might be useful to set some default params here, e.g. + getJobHostSummaries: function(id, page_size=200, order='host_name'){} + without ES6, we'd have to supply defaults like this: + this.page_size = params.page_size ? params.page_size : 200; + */ + + // GET events related to a job run + // e.g. + // ?event=playbook_on_stats + // ?parent=206&event__startswith=runner&page_size=200&order=host_name,counter + getRelatedJobEvents: function(id, params){ + var url = GetBasePath('jobs'); + url = url + id + '/job_events/?'; + Object.keys(params).forEach(function(key, index) { + // the API is tolerant of extra ampersands + // ?&event=playbook_on_start == ?event=playbook_on_stats + url = url + '&' + key + '=' + params[key]; + }); + Rest.setUrl(url); + return Rest.get() + .success(function(data){ + return data + }) + .error(function(data, status) { + ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + }, + // GET job host summaries related to a job run + // e.g. ?page_size=200&order=host_name + getJobHostSummaries: function(id, params){ + var url = GetBasePath('jobs'); + url = url + id + '/job_host_summaries/?' + Object.keys(params).forEach(function(key, index) { + // the API is tolerant of extra ampersands + url = url + '&' + key + '=' + params[key]; + }); + Rest.setUrl(url); + return Rest.get() + .success(function(data){ + return data + }) + .error(function(data, status) { + ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + }, + // GET job plays related to a job run + // e.g. ?page_size=200 + getJobPlays: function(id, params){ + var url = GetBasePath('jobs'); + url = url + id + '/job_plays/?'; + Object.keys(params).forEach(function(key, index) { + // the API is tolerant of extra ampersands + url = url + '&' + key + '=' + params[key]; + }); + Rest.setUrl(url); + return Rest.get() + .success(function(data){ + return data + }) + .error(function(data, status) { + ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + }, + getJobTasks: function(id, params){ + var url = GetBasePath('jobs'); + url = url + id + '/job_tasks/?'; + Object.keys(params).forEach(function(key, index) { + // the API is tolerant of extra ampersands + url = url + '&' + key + '=' + params[key]; + }); + Rest.setUrl(url); + return Rest.get() + .success(function(data){ + return data + }) + .error(function(data, status) { + ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + }, + getJob: function(id){ + var url = GetBasePath('jobs'); + url = url + id; + Rest.setUrl(url); + return Rest.get() + .success(function(data){ + return data + }) + .error(function(data, status) { + ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + }, + // GET next set of paginated results + // expects 'next' param returned by the API e.g. + // "/api/v1/jobs/51/job_plays/?order_by=id&page=2&page_size=1" + getNextPage: function(url){ + return Rest.get() + .success(function(data){ + return data + }) + .error(function(data, status) { + ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + '. GET returned: ' + status }); + }); + } + } + } + ]; \ No newline at end of file diff --git a/awx/ui/client/src/job-detail/main.js b/awx/ui/client/src/job-detail/main.js index 42e9cae45c..d985a310e6 100644 --- a/awx/ui/client/src/job-detail/main.js +++ b/awx/ui/client/src/job-detail/main.js @@ -6,10 +6,12 @@ import route from './job-detail.route'; import controller from './job-detail.controller'; +import service from './job-detail.service'; export default angular.module('jobDetail', []) .controller('JobDetailController', controller) + .service('JobDetailService', service) .run(['$stateExtender', function($stateExtender) { $stateExtender.addState(route); }]); From da39f1269a4e97f1bf03806d1264ca791ec2f4db Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 15 Mar 2016 14:26:50 -0400 Subject: [PATCH 52/63] org counts code restructing to better prepare for RBAC merge --- awx/api/serializers.py | 6 +- awx/api/views.py | 41 +++++----- .../api/test_organization_counts.py | 77 +++++++++++++++---- 3 files changed, 86 insertions(+), 38 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 514075bc29..6ca73cf6a0 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -800,14 +800,14 @@ class OrganizationSerializer(BaseSerializer): def get_summary_fields(self, obj): summary_dict = super(OrganizationSerializer, self).get_summary_fields(obj) - counts_dict = self.context.get('counts', None) + counts_dict = self.context.get('related_field_counts', None) if counts_dict is not None and summary_dict is not None: if obj.id not in counts_dict: - summary_dict['counts'] = { + summary_dict['related_field_counts'] = { 'inventories': 0, 'teams': 0, 'users': 0, 'job_templates': 0, 'admins': 0, 'projects': 0} else: - summary_dict['counts'] = counts_dict[obj.id] + summary_dict['related_field_counts'] = counts_dict[obj.id] return summary_dict diff --git a/awx/api/views.py b/awx/api/views.py index ebdb57098d..b50ba0497c 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -625,47 +625,43 @@ class OrganizationList(ListCreateAPIView): org_id_list = org_qs.values('id') if len(org_id_list) == 0: if self.request.method == 'POST': - full_context['counts'] = {} + full_context['related_field_counts'] = {} return full_context - # Produce counts of Foreign Key relationships inv_qs = self.request.user.get_queryset(Inventory) + project_qs = self.request.user.get_queryset(Project) + user_qs = self.request.user.get_queryset(User) + + # Produce counts of Foreign Key relationships db_results['inventories'] = inv_qs\ .values('organization').annotate(Count('organization')).order_by('organization') db_results['teams'] = self.request.user.get_queryset(Team)\ .values('organization').annotate(Count('organization')).order_by('organization') + # TODO: When RBAC branch merges, change this to project relationship JT_reference = 'inventory__organization' # Extra filter is applied on the inventory, because this catches # the case of deleted (and purged) inventory - db_JT_results = self.request.user.get_queryset(JobTemplate)\ - .filter(inventory_id__in=inv_qs.values_list('pk', flat=True))\ + db_results['job_templates'] = self.request.user.get_queryset(JobTemplate)\ + .filter(inventory__in=inv_qs)\ .values(JT_reference).annotate(Count(JT_reference))\ .order_by(JT_reference) # Produce counts of m2m relationships - project_qs = self.request.user.get_queryset(Project) db_results['projects'] = Organization.projects.through.objects\ - .filter( - project_id__in=project_qs.values_list('pk', flat=True), - organization_id__in=org_qs.values_list('pk', flat=True))\ + .filter(project__in=project_qs, organization__in=org_qs)\ .values('organization')\ .annotate(Count('organization')).order_by('organization') # TODO: When RBAC branch merges, change these to role relation - user_qs = self.request.user.get_queryset(User) db_results['users'] = Organization.users.through.objects\ - .filter( - user_id__in=user_qs.values_list('pk', flat=True), - organization_id__in=org_qs.values_list('pk', flat=True))\ + .filter(user__in=user_qs, organization__in=org_qs)\ .values('organization')\ .annotate(Count('organization')).order_by('organization') db_results['admins'] = Organization.admins.through.objects\ - .filter( - user_id__in=user_qs.values_list('pk', flat=True), - organization_id__in=org_qs.values_list('pk', flat=True))\ + .filter(user__in=user_qs, organization__in=org_qs)\ .values('organization')\ .annotate(Count('organization')).order_by('organization') @@ -677,15 +673,16 @@ class OrganizationList(ListCreateAPIView): 'admins': 0, 'projects': 0} for res in db_results: + if res == 'job_templates': + org_reference = JT_reference + else: + org_reference = 'organization' for entry in db_results[res]: - org_id = entry['organization'] - count_context[org_id][res] = entry['organization__count'] + org_id = entry[org_reference] + if org_id in count_context: + count_context[org_id][res] = entry['%s__count' % org_reference] - for entry in db_JT_results: - org_id = entry[JT_reference] - count_context[org_id]['job_templates'] = entry['%s__count' % JT_reference] - - full_context['counts'] = count_context + full_context['related_field_counts'] = count_context return full_context diff --git a/awx/main/tests/functional/api/test_organization_counts.py b/awx/main/tests/functional/api/test_organization_counts.py index 56fdb8215e..de629dbcf4 100644 --- a/awx/main/tests/functional/api/test_organization_counts.py +++ b/awx/main/tests/functional/api/test_organization_counts.py @@ -3,7 +3,7 @@ import pytest from django.core.urlresolvers import reverse @pytest.fixture -def resourced_organization(organization, project, user): +def resourced_organization(organization, project, team, inventory, user): admin_user = user('test-admin', True) member_user = user('org-member') @@ -11,12 +11,12 @@ def resourced_organization(organization, project, user): organization.users.add(member_user) organization.admins.add(admin_user) organization.projects.add(project) - organization.teams.create(name='org-team') - inventory = organization.inventories.create(name="associated-inv") - inventory.jobtemplates.create(name="test-jt", - description="test-job-template-desc", - project=project, - playbook="test_playbook.yml") + # organization.teams.create(name='org-team') + # inventory = organization.inventories.create(name="associated-inv") + project.jobtemplates.create(name="test-jt", + description="test-job-template-desc", + inventory=inventory, + playbook="test_playbook.yml") return organization @@ -25,8 +25,9 @@ def test_org_counts_admin(resourced_organization, user, get): # Check that all types of resources are counted by a superuser external_admin = user('admin', True) response = get(reverse('api:organization_list', args=[]), external_admin) - counts = response.data['results'][0]['summary_fields']['counts'] + assert response.status_code == 200 + counts = response.data['results'][0]['summary_fields']['related_field_counts'] assert counts == { 'users': 1, 'admins': 1, @@ -42,7 +43,9 @@ def test_org_counts_member(resourced_organization, get): # user count, consistent with the RBAC rules member_user = resourced_organization.users.get(username='org-member') response = get(reverse('api:organization_list', args=[]), member_user) - counts = response.data['results'][0]['summary_fields']['counts'] + assert response.status_code == 200 + + counts = response.data['results'][0]['summary_fields']['related_field_counts'] assert counts == { 'users': 1, # User can see themselves @@ -60,10 +63,10 @@ def test_new_org_zero_counts(user, post): org_list_url = reverse('api:organization_list', args=[]) post_response = post(url=org_list_url, data={'name': 'test organization', 'description': ''}, user=user('admin', True)) - new_org_list = post_response.render().data - counts_dict = new_org_list['summary_fields']['counts'] - assert post_response.status_code == 201 + + new_org_list = post_response.render().data + counts_dict = new_org_list['summary_fields']['related_field_counts'] assert counts_dict == { 'users': 0, 'admins': 0, @@ -79,12 +82,14 @@ def test_two_organizations(resourced_organization, organizations, user, get): external_admin = user('admin', True) organization_zero = organizations(1)[0] response = get(reverse('api:organization_list', args=[]), external_admin) + assert response.status_code == 200 + org_id_full = resourced_organization.id org_id_zero = organization_zero.id counts = {} for i in range(2): org_id = response.data['results'][i]['id'] - counts[org_id] = response.data['results'][i]['summary_fields']['counts'] + counts[org_id] = response.data['results'][i]['summary_fields']['related_field_counts'] assert counts[org_id_full] == { 'users': 1, @@ -102,3 +107,49 @@ def test_two_organizations(resourced_organization, organizations, user, get): 'inventories': 0, 'teams': 0 } + +@pytest.mark.django_db +def test_overlapping_project(resourced_organization, organizations, user, get): + # Check correct results for two organizations are returned + external_admin = user('admin', True) + organization2 = organizations(1)[0] + the_project = resourced_organization.projects.all()[0] + organization2.projects.add(the_project) + organization2.projects.create(name="second-project", + description="test-proj-desc", + scm_type="git", + scm_url="https://github.com/jlaska/ansible-playbooks") + inventory = organization2.inventories.create(name="second-inventory") + organization2.projects.get(name="second-project").jobtemplates.create( + name="second-job-template", + inventory=inventory, + playbook="hello.yml" + ) + + response = get(reverse('api:organization_list', args=[]), external_admin) + assert response.status_code == 200 + + org_id_full = resourced_organization.id + org_id2 = organization2.id + counts = {} + for i in range(2): + org_id = response.data['results'][i]['id'] + counts[org_id] = response.data['results'][i]['summary_fields']['related_field_counts'] + + assert counts[org_id_full] == { + 'users': 1, + 'admins': 1, + 'job_templates': 1, + 'projects': 1, + 'inventories': 1, + 'teams': 1 + } + assert counts[org_id2] == { + 'users': 0, + 'admins': 0, + 'job_templates': 2, + 'projects': 2, + 'inventories': 1, + 'teams': 0 + } + assert False From 85a9e14ced14e23c27af0349234606103147a096 Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Tue, 15 Mar 2016 14:55:12 -0400 Subject: [PATCH 53/63] Split up and modularized organizations --- awx/ui/client/src/app.js | 112 ++--- .../client/src/controllers/Organizations.js | 382 ------------------ awx/ui/client/src/organizations/add/main.js | 14 + .../add/organizations-add.controller.js | 66 +++ .../add/organizations-add.partial.html | 4 + .../add/organizations-add.route.js | 24 ++ awx/ui/client/src/organizations/edit/main.js | 15 + .../edit/organizations-edit.controller.js | 150 +++++++ .../edit/organizations-edit.route.js | 29 ++ awx/ui/client/src/organizations/list/main.js | 14 + .../list/organizations-list.controller.js | 182 +++++++++ .../list/organizations-list.partial.html | 62 +++ .../list/organizations-list.route.js | 31 ++ awx/ui/client/src/organizations/main.js | 16 + 14 files changed, 665 insertions(+), 436 deletions(-) delete mode 100644 awx/ui/client/src/controllers/Organizations.js create mode 100644 awx/ui/client/src/organizations/add/main.js create mode 100644 awx/ui/client/src/organizations/add/organizations-add.controller.js create mode 100644 awx/ui/client/src/organizations/add/organizations-add.partial.html create mode 100644 awx/ui/client/src/organizations/add/organizations-add.route.js create mode 100644 awx/ui/client/src/organizations/edit/main.js create mode 100644 awx/ui/client/src/organizations/edit/organizations-edit.controller.js create mode 100644 awx/ui/client/src/organizations/edit/organizations-edit.route.js create mode 100644 awx/ui/client/src/organizations/list/main.js create mode 100644 awx/ui/client/src/organizations/list/organizations-list.controller.js create mode 100644 awx/ui/client/src/organizations/list/organizations-list.partial.html create mode 100644 awx/ui/client/src/organizations/list/organizations-list.route.js create mode 100644 awx/ui/client/src/organizations/main.js diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index cbf50b22b6..a64b8c9d62 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -27,6 +27,7 @@ import {JobsListController} from './controllers/Jobs'; import {PortalController} from './controllers/Portal'; import systemTracking from './system-tracking/main'; import inventoryScripts from './inventory-scripts/main'; +import organizations from './organizations/main'; import permissions from './permissions/main'; import managementJobs from './management-jobs/main'; import jobDetail from './job-detail/main'; @@ -50,7 +51,9 @@ import lookUpHelper from './lookup/main'; import JobTemplates from './job-templates/main'; import {ScheduleEditController} from './controllers/Schedules'; import {ProjectsList, ProjectsAdd, ProjectsEdit} from './controllers/Projects'; -import {OrganizationsList, OrganizationsAdd, OrganizationsEdit} from './controllers/Organizations'; +import OrganizationsList from './organizations/list/organizations-list.controller'; +import OrganizationsAdd from './organizations/add/organizations-add.controller'; +import OrganizationsEdit from './organizations/edit/organizations-edit.controller'; import {InventoriesList, InventoriesAdd, InventoriesEdit, InventoriesManage} from './controllers/Inventories'; import {AdminsList} from './controllers/Admins'; import {UsersList, UsersAdd, UsersEdit} from './controllers/Users'; @@ -85,6 +88,7 @@ var tower = angular.module('Tower', [ browserData.name, systemTracking.name, inventoryScripts.name, + organizations.name, permissions.name, managementJobs.name, setupMenu.name, @@ -428,60 +432,60 @@ var tower = angular.module('Tower', [ } }). - state('organizations', { - url: '/organizations', - templateUrl: urlPrefix + 'partials/organizations.html', - controller: OrganizationsList, - data: { - activityStream: true, - activityStreamTarget: 'organization' - }, - ncyBreadcrumb: { - parent: function($scope) { - $scope.$parent.$emit("ReloadOrgListView"); - return "setup"; - }, - label: "ORGANIZATIONS" - }, - resolve: { - features: ['FeaturesService', function(FeaturesService) { - return FeaturesService.get(); - }] - } - }). + // state('organizations', { + // url: '/organizations', + // templateUrl: urlPrefix + 'partials/organizations.html', + // controller: OrganizationsList, + // data: { + // activityStream: true, + // activityStreamTarget: 'organization' + // }, + // ncyBreadcrumb: { + // parent: function($scope) { + // $scope.$parent.$emit("ReloadOrgListView"); + // return "setup"; + // }, + // label: "ORGANIZATIONS" + // }, + // resolve: { + // features: ['FeaturesService', function(FeaturesService) { + // return FeaturesService.get(); + // }] + // } + // }). - state('organizations.add', { - url: '/add', - templateUrl: urlPrefix + 'partials/organizations.crud.html', - controller: OrganizationsAdd, - ncyBreadcrumb: { - parent: "organizations", - label: "CREATE ORGANIZATION" - }, - resolve: { - features: ['FeaturesService', function(FeaturesService) { - return FeaturesService.get(); - }] - } - }). - - state('organizations.edit', { - url: '/:organization_id', - templateUrl: urlPrefix + 'partials/organizations.crud.html', - controller: OrganizationsEdit, - data: { - activityStreamId: 'organization_id' - }, - ncyBreadcrumb: { - parent: "organizations", - label: "{{name}}" - }, - resolve: { - features: ['FeaturesService', function(FeaturesService) { - return FeaturesService.get(); - }] - } - }). + // state('organizations.add', { + // url: '/add', + // templateUrl: urlPrefix + 'partials/organizations.crud.html', + // controller: OrganizationsAdd, + // ncyBreadcrumb: { + // parent: "organizations", + // label: "CREATE ORGANIZATION" + // }, + // resolve: { + // features: ['FeaturesService', function(FeaturesService) { + // return FeaturesService.get(); + // }] + // } + // }). + // + // state('organizations.edit', { + // url: '/:organization_id', + // templateUrl: urlPrefix + 'partials/organizations.crud.html', + // controller: OrganizationsEdit, + // data: { + // activityStreamId: 'organization_id' + // }, + // ncyBreadcrumb: { + // parent: "organizations", + // label: "{{name}}" + // }, + // resolve: { + // features: ['FeaturesService', function(FeaturesService) { + // return FeaturesService.get(); + // }] + // } + // }). state('organizationAdmins', { url: '/organizations/:organization_id/admins', diff --git a/awx/ui/client/src/controllers/Organizations.js b/awx/ui/client/src/controllers/Organizations.js deleted file mode 100644 index fb1ebecfa1..0000000000 --- a/awx/ui/client/src/controllers/Organizations.js +++ /dev/null @@ -1,382 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name controllers.function:Organizations - * @description This controller's for the Organizations page -*/ - - -export function OrganizationsList($stateParams, $scope, $rootScope, $location, - $log, $compile, Rest, PaginateWidget, PaginateInit, SearchInit, OrganizationList, Alert, Prompt, ClearScope, ProcessErrors, GetBasePath, Wait, - $state) { - - ClearScope(); - - var defaultUrl = GetBasePath('organizations'), - list = OrganizationList, - pageSize = $scope.orgCount; - - PaginateInit({ - scope: $scope, - list: list, - url: defaultUrl, - pageSize: pageSize, - }); - SearchInit({ - scope: $scope, - list: list, - url: defaultUrl, - }); - - $scope.search(list.iterator); - - $scope.PaginateWidget = PaginateWidget({ - iterator: list.iterator, - set: 'organizations' - }); - - var paginationContainer = $('#pagination-container'); - paginationContainer.html($scope.PaginateWidget); - $compile(paginationContainer.contents())($scope) - - var parseCardData = function (cards) { - return cards.map(function (card) { - var val = {}; - val.name = card.name; - val.id = card.id; - if (card.id + "" === cards.activeCard) { - val.isActiveCard = true; - } - val.description = card.description || undefined; - val.links = []; - val.links.push({ - href: card.related.users, - name: "USERS" - }); - val.links.push({ - href: card.related.teams, - name: "TEAMS" - }); - val.links.push({ - href: card.related.inventories, - name: "INVENTORIES" - }); - val.links.push({ - href: card.related.projects, - name: "PROJECTS" - }); - val.links.push({ - href: card.related.job_templates, - name: "JOB TEMPLATES" - }); - val.links.push({ - href: card.related.admins, - name: "ADMINS" - }); - return val; - }); - }; - - var getOrganization = function (id) { - Rest.setUrl(defaultUrl); - Rest.get() - .success(function (data) { - data.results.activeCard = id; - $scope.orgCount = data.count; - $scope.orgCards = parseCardData(data.results); - Wait("stop"); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + defaultUrl + ' failed. DELETE returned status: ' + status }); - }); - }; - - $scope.$on("ReloadOrgListView", function() { - if ($state.$current.self.name === "organizations") { - delete $scope.activeCard; - if ($scope.orgCards) { - $scope.orgCards = $scope.orgCards.map(function (card) { - delete card.isActiveCard; - return card; - }); - } - $scope.hideListHeader = false; - } - }); - - $scope.$on("ReloadOrganzationCards", function(e, id) { - $scope.activeCard = id; - getOrganization(id); - }); - - $scope.$on("HideOrgListHeader", function() { - $scope.hideListHeader = true; - }); - - $scope.$on("ShowOrgListHeader", function() { - $scope.hideListHeader = false; - }); - - getOrganization(); - - $rootScope.flashMessage = null; - - if ($scope.removePostRefresh) { - $scope.removePostRefresh(); - } - $scope.removePostRefresh = $scope.$on('PostRefresh', function () { - // Cleanup after a delete - Wait('stop'); - $('#prompt-modal').modal('hide'); - }); - - $scope.addOrganization = function () { - $state.transitionTo('organizations.add'); - }; - - $scope.editOrganization = function (id) { - $scope.activeCard = id; - $state.transitionTo('organizations.edit', {organization_id: id}); - }; - - $scope.deleteOrganization = function (id, name) { - - var action = function () { - $('#prompt-modal').modal('hide'); - Wait('start'); - var url = defaultUrl + id + '/'; - Rest.setUrl(url); - Rest.destroy() - .success(function () { - if ($state.current.name !== "organzations") { - $state.transitionTo("organizations"); - } - $scope.$emit("ReloadOrganzationCards"); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); - }); - }; - - Prompt({ - hdr: 'Delete', - body: '
Are you sure you want to delete the organization below?
' + name + '
', - action: action, - actionText: 'DELETE' - }); - }; -} - -OrganizationsList.$inject = ['$stateParams', '$scope', '$rootScope', - '$location', '$log', '$compile', 'Rest', 'PaginateWidget', 'PaginateInit', 'SearchInit', 'OrganizationList', 'Alert', 'Prompt', 'ClearScope', - 'ProcessErrors', 'GetBasePath', 'Wait', - '$state' -]; - - -export function OrganizationsAdd($scope, $rootScope, $compile, $location, $log, - $stateParams, OrganizationForm, GenerateForm, Rest, Alert, ProcessErrors, - ClearScope, GetBasePath, ReturnToCaller, Wait, $state) { - - ClearScope(); - - // Inject dynamic view - var generator = GenerateForm, - form = OrganizationForm, - base = $location.path().replace(/^\//, '').split('/')[0]; - - generator.inject(form, { mode: 'add', related: false, scope: $scope}); - generator.reset(); - - $scope.$emit("HideOrgListHeader"); - - // Save - $scope.formSave = function () { - generator.clearApiErrors(); - Wait('start'); - var url = GetBasePath(base); - url += (base !== 'organizations') ? $stateParams.project_id + '/organizations/' : ''; - Rest.setUrl(url); - Rest.post({ name: $scope.name, description: $scope.description }) - .success(function (data) { - Wait('stop'); - $scope.$emit("ReloadOrganzationCards", data.id); - if (base === 'organizations') { - $rootScope.flashMessage = "New organization successfully created!"; - $location.path('/organizations/' + data.id); - } else { - ReturnToCaller(1); - } - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to add new organization. Post returned status: ' + status }); - }); - }; - - $scope.formCancel = function () { - $scope.$emit("ReloadOrganzationCards"); - $scope.$emit("ShowOrgListHeader"); - $state.transitionTo('organizations'); - }; -} - -OrganizationsAdd.$inject = ['$scope', '$rootScope', '$compile', '$location', - '$log', '$stateParams', 'OrganizationForm', 'GenerateForm', 'Rest', 'Alert', - 'ProcessErrors', 'ClearScope', 'GetBasePath', 'ReturnToCaller', 'Wait', - '$state' -]; - - -export function OrganizationsEdit($scope, $rootScope, $compile, $location, $log, - $stateParams, OrganizationForm, GenerateForm, Rest, Alert, ProcessErrors, - RelatedSearchInit, RelatedPaginateInit, Prompt, ClearScope, GetBasePath, - Wait, $state) { - - ClearScope(); - - // Inject dynamic view - var form = OrganizationForm, - generator = GenerateForm, - defaultUrl = GetBasePath('organizations'), - base = $location.path().replace(/^\//, '').split('/')[0], - master = {}, - id = $stateParams.organization_id, - relatedSets = {}; - - $scope.$emit("HideOrgListHeader"); - - $scope.$emit("ReloadOrganzationCards", id); - - $scope.organization_id = id; - - generator.inject(form, { mode: 'edit', related: true, scope: $scope}); - generator.reset(); - - // After the Organization is loaded, retrieve each related set - if ($scope.organizationLoadedRemove) { - $scope.organizationLoadedRemove(); - } - $scope.organizationLoadedRemove = $scope.$on('organizationLoaded', function () { - for (var set in relatedSets) { - $scope.search(relatedSets[set].iterator); - } - Wait('stop'); - }); - - // Retrieve detail record and prepopulate the form - Wait('start'); - Rest.setUrl(defaultUrl + id + '/'); - Rest.get() - .success(function (data) { - var fld, related, set; - $scope.organization_name = data.name; - for (fld in form.fields) { - if (data[fld]) { - $scope[fld] = data[fld]; - master[fld] = data[fld]; - } - } - related = data.related; - for (set in form.related) { - if (related[set]) { - relatedSets[set] = { - url: related[set], - iterator: form.related[set].iterator - }; - } - } - // Initialize related search functions. Doing it here to make sure relatedSets object is populated. - RelatedSearchInit({ scope: $scope, form: form, relatedSets: relatedSets }); - RelatedPaginateInit({ scope: $scope, relatedSets: relatedSets }); - $scope.$emit('organizationLoaded'); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to retrieve organization: ' + $stateParams.id + '. GET status: ' + status }); - }); - - - // Save changes to the parent - $scope.formSave = function () { - var fld, params = {}; - generator.clearApiErrors(); - Wait('start'); - for (fld in form.fields) { - params[fld] = $scope[fld]; - } - Rest.setUrl(defaultUrl + id + '/'); - Rest.put(params) - .success(function (data) { - Wait('stop'); - $scope.organization_name = $scope.name; - master = params; - $rootScope.flashMessage = "Your changes were successfully saved!"; - $scope.$emit("ReloadOrganzationCards", data.id); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, OrganizationForm, { hdr: 'Error!', - msg: 'Failed to update organization: ' + id + '. PUT status: ' + status }); - }); - }; - - $scope.formCancel = function () { - $scope.$emit("ReloadOrganzationCards"); - $scope.$emit("ShowOrgListHeader"); - $state.transitionTo('organizations'); - }; - - // Related set: Add button - $scope.add = function (set) { - $rootScope.flashMessage = null; - $location.path('/' + base + '/' + $stateParams.organization_id + '/' + set); - }; - - // Related set: Edit button - $scope.edit = function (set, id) { - $rootScope.flashMessage = null; - $location.path('/' + set + '/' + id); - }; - - // Related set: Delete button - $scope['delete'] = function (set, itm_id, name, title) { - $rootScope.flashMessage = null; - - var action = function () { - Wait('start'); - var url = defaultUrl + $stateParams.organization_id + '/' + set + '/'; - Rest.setUrl(url); - Rest.post({ id: itm_id, disassociate: 1 }) - .success(function () { - $('#prompt-modal').modal('hide'); - $scope.search(form.related[set].iterator); - }) - .error(function (data, status) { - $('#prompt-modal').modal('hide'); - ProcessErrors($scope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. POST returned status: ' + status }); - }); - }; - - Prompt({ - hdr: 'Delete', - body: '
Are you sure you want to remove the ' + title + ' below from ' + $scope.name + '?
' + name + '
', - action: action, - actionText: 'DELETE' - }); - - }; -} - -OrganizationsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', - '$log', '$stateParams', 'OrganizationForm', 'GenerateForm', 'Rest', 'Alert', - 'ProcessErrors', 'RelatedSearchInit', 'RelatedPaginateInit', 'Prompt', - 'ClearScope', 'GetBasePath', 'Wait', '$state' -]; \ No newline at end of file diff --git a/awx/ui/client/src/organizations/add/main.js b/awx/ui/client/src/organizations/add/main.js new file mode 100644 index 0000000000..27b8406e0b --- /dev/null +++ b/awx/ui/client/src/organizations/add/main.js @@ -0,0 +1,14 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import route from './organizations-add.route'; +import controller from './organizations-add.controller'; + +export default + angular.module('organizationsAdd', []) + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(route); + }]); diff --git a/awx/ui/client/src/organizations/add/organizations-add.controller.js b/awx/ui/client/src/organizations/add/organizations-add.controller.js new file mode 100644 index 0000000000..00c7ad9579 --- /dev/null +++ b/awx/ui/client/src/organizations/add/organizations-add.controller.js @@ -0,0 +1,66 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$scope', '$rootScope', '$compile', '$location', + '$log', '$stateParams', 'OrganizationForm', 'GenerateForm', 'Rest', 'Alert', + 'ProcessErrors', 'ClearScope', 'GetBasePath', 'ReturnToCaller', 'Wait', + '$state', + function($scope, $rootScope, $compile, $location, $log, + $stateParams, OrganizationForm, GenerateForm, Rest, Alert, ProcessErrors, + ClearScope, GetBasePath, ReturnToCaller, Wait, $state) { + + ClearScope(); + + // Inject dynamic view + var generator = GenerateForm, + form = OrganizationForm, + base = $location.path().replace(/^\//, '').split('/')[0]; + + generator.inject(form, { + mode: 'add', + related: false, + scope: $scope + }); + generator.reset(); + + $scope.$emit("HideOrgListHeader"); + + // Save + $scope.formSave = function() { + generator.clearApiErrors(); + Wait('start'); + var url = GetBasePath(base); + url += (base !== 'organizations') ? $stateParams.project_id + '/organizations/' : ''; + Rest.setUrl(url); + Rest.post({ + name: $scope.name, + description: $scope.description + }) + .success(function(data) { + Wait('stop'); + $scope.$emit("ReloadOrganzationCards", data.id); + if (base === 'organizations') { + $rootScope.flashMessage = "New organization successfully created!"; + $location.path('/organizations/' + data.id); + } else { + ReturnToCaller(1); + } + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, form, { + hdr: 'Error!', + msg: 'Failed to add new organization. Post returned status: ' + status + }); + }); + }; + + $scope.formCancel = function() { + $scope.$emit("ReloadOrganzationCards"); + $scope.$emit("ShowOrgListHeader"); + $state.transitionTo('organizations'); + }; + } +] diff --git a/awx/ui/client/src/organizations/add/organizations-add.partial.html b/awx/ui/client/src/organizations/add/organizations-add.partial.html new file mode 100644 index 0000000000..5db1583d13 --- /dev/null +++ b/awx/ui/client/src/organizations/add/organizations-add.partial.html @@ -0,0 +1,4 @@ +
+
+
+
diff --git a/awx/ui/client/src/organizations/add/organizations-add.route.js b/awx/ui/client/src/organizations/add/organizations-add.route.js new file mode 100644 index 0000000000..9deab323b7 --- /dev/null +++ b/awx/ui/client/src/organizations/add/organizations-add.route.js @@ -0,0 +1,24 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import {templateUrl} from '../../shared/template-url/template-url.factory'; +import OrganizationsAdd from './organizations-add.controller'; + +export default { + name: 'organizations.add', + route: '/add', + templateUrl: templateUrl('organizations/add/organizations-add'), + controller: OrganizationsAdd, + ncyBreadcrumb: { + parent: "organizations", + label: "CREATE ORGANIZATION" + }, + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }] + } +}; diff --git a/awx/ui/client/src/organizations/edit/main.js b/awx/ui/client/src/organizations/edit/main.js new file mode 100644 index 0000000000..8f2a825df9 --- /dev/null +++ b/awx/ui/client/src/organizations/edit/main.js @@ -0,0 +1,15 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import route from './organizations-edit.route'; +import controller from './organizations-edit.controller'; + +export default + angular.module('organizationsEdit', []) + .controller('organizationsEditController', controller) + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(route); + }]); diff --git a/awx/ui/client/src/organizations/edit/organizations-edit.controller.js b/awx/ui/client/src/organizations/edit/organizations-edit.controller.js new file mode 100644 index 0000000000..f9178a1fb1 --- /dev/null +++ b/awx/ui/client/src/organizations/edit/organizations-edit.controller.js @@ -0,0 +1,150 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$scope', '$rootScope', '$compile', '$location', + '$log', '$stateParams', 'OrganizationForm', 'GenerateForm', 'Rest', 'Alert', + 'ProcessErrors', 'RelatedSearchInit', 'RelatedPaginateInit', 'Prompt', + 'ClearScope', 'GetBasePath', 'Wait', '$state', + function($scope, $rootScope, $compile, $location, $log, + $stateParams, OrganizationForm, GenerateForm, Rest, Alert, ProcessErrors, + RelatedSearchInit, RelatedPaginateInit, Prompt, ClearScope, GetBasePath, + Wait, $state) { + + ClearScope(); + + // Inject dynamic view + var form = OrganizationForm, + generator = GenerateForm, + defaultUrl = GetBasePath('organizations'), + base = $location.path().replace(/^\//, '').split('/')[0], + master = {}, + id = $stateParams.organization_id, + relatedSets = {}; + + $scope.$emit("HideOrgListHeader"); + + $scope.$emit("ReloadOrganzationCards", id); + + $scope.organization_id = id; + + generator.inject(form, { mode: 'edit', related: true, scope: $scope}); + generator.reset(); + + // After the Organization is loaded, retrieve each related set + if ($scope.organizationLoadedRemove) { + $scope.organizationLoadedRemove(); + } + $scope.organizationLoadedRemove = $scope.$on('organizationLoaded', function () { + for (var set in relatedSets) { + $scope.search(relatedSets[set].iterator); + } + Wait('stop'); + }); + + // Retrieve detail record and prepopulate the form + Wait('start'); + Rest.setUrl(defaultUrl + id + '/'); + Rest.get() + .success(function (data) { + var fld, related, set; + $scope.organization_name = data.name; + for (fld in form.fields) { + if (data[fld]) { + $scope[fld] = data[fld]; + master[fld] = data[fld]; + } + } + related = data.related; + for (set in form.related) { + if (related[set]) { + relatedSets[set] = { + url: related[set], + iterator: form.related[set].iterator + }; + } + } + // Initialize related search functions. Doing it here to make sure relatedSets object is populated. + RelatedSearchInit({ scope: $scope, form: form, relatedSets: relatedSets }); + RelatedPaginateInit({ scope: $scope, relatedSets: relatedSets }); + $scope.$emit('organizationLoaded'); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to retrieve organization: ' + $stateParams.id + '. GET status: ' + status }); + }); + + + // Save changes to the parent + $scope.formSave = function () { + var fld, params = {}; + generator.clearApiErrors(); + Wait('start'); + for (fld in form.fields) { + params[fld] = $scope[fld]; + } + Rest.setUrl(defaultUrl + id + '/'); + Rest.put(params) + .success(function (data) { + Wait('stop'); + $scope.organization_name = $scope.name; + master = params; + $rootScope.flashMessage = "Your changes were successfully saved!"; + $scope.$emit("ReloadOrganzationCards", data.id); + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, OrganizationForm, { hdr: 'Error!', + msg: 'Failed to update organization: ' + id + '. PUT status: ' + status }); + }); + }; + + $scope.formCancel = function () { + $scope.$emit("ReloadOrganzationCards"); + $scope.$emit("ShowOrgListHeader"); + $state.transitionTo('organizations'); + }; + + // Related set: Add button + $scope.add = function (set) { + $rootScope.flashMessage = null; + $location.path('/' + base + '/' + $stateParams.organization_id + '/' + set); + }; + + // Related set: Edit button + $scope.edit = function (set, id) { + $rootScope.flashMessage = null; + $location.path('/' + set + '/' + id); + }; + + // Related set: Delete button + $scope['delete'] = function (set, itm_id, name, title) { + $rootScope.flashMessage = null; + + var action = function () { + Wait('start'); + var url = defaultUrl + $stateParams.organization_id + '/' + set + '/'; + Rest.setUrl(url); + Rest.post({ id: itm_id, disassociate: 1 }) + .success(function () { + $('#prompt-modal').modal('hide'); + $scope.search(form.related[set].iterator); + }) + .error(function (data, status) { + $('#prompt-modal').modal('hide'); + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. POST returned status: ' + status }); + }); + }; + + Prompt({ + hdr: 'Delete', + body: '
Are you sure you want to remove the ' + title + ' below from ' + $scope.name + '?
' + name + '
', + action: action, + actionText: 'DELETE' + }); + + }; +} +] diff --git a/awx/ui/client/src/organizations/edit/organizations-edit.route.js b/awx/ui/client/src/organizations/edit/organizations-edit.route.js new file mode 100644 index 0000000000..ad546e71cc --- /dev/null +++ b/awx/ui/client/src/organizations/edit/organizations-edit.route.js @@ -0,0 +1,29 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import { + templateUrl +} from '../../shared/template-url/template-url.factory'; +import OrganizationsEdit from './organizations-edit.controller'; + +export default { + name: 'organizations.edit', + route: '/:organization_id', + templateUrl: templateUrl('organizations/add/organizations-add'), + controller: OrganizationsEdit, + data: { + activityStreamId: 'organization_id' + }, + ncyBreadcrumb: { + parent: "organizations", + label: "{{name}}" + }, + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }] + } +}; diff --git a/awx/ui/client/src/organizations/list/main.js b/awx/ui/client/src/organizations/list/main.js new file mode 100644 index 0000000000..0250a5f5f8 --- /dev/null +++ b/awx/ui/client/src/organizations/list/main.js @@ -0,0 +1,14 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import route from './organizations-list.route'; +import controller from './organizations-list.controller'; + +export default + angular.module('organizationsList', []) + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(route); + }]); diff --git a/awx/ui/client/src/organizations/list/organizations-list.controller.js b/awx/ui/client/src/organizations/list/organizations-list.controller.js new file mode 100644 index 0000000000..2a21b03ffb --- /dev/null +++ b/awx/ui/client/src/organizations/list/organizations-list.controller.js @@ -0,0 +1,182 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['$stateParams', '$scope', '$rootScope', '$location', + '$log', '$compile', 'Rest', 'PaginateWidget', 'PaginateInit', + 'SearchInit', 'OrganizationList', 'Alert', 'Prompt', 'ClearScope', + 'ProcessErrors', 'GetBasePath', 'Wait', + '$state', + function($stateParams, $scope, $rootScope, $location, + $log, $compile, Rest, PaginateWidget, PaginateInit, + SearchInit, OrganizationList, Alert, Prompt, ClearScope, + ProcessErrors, GetBasePath, Wait, + $state) { + + ClearScope(); + + var defaultUrl = GetBasePath('organizations'), + list = OrganizationList, + pageSize = $scope.orgCount; + + PaginateInit({ + scope: $scope, + list: list, + url: defaultUrl, + pageSize: pageSize, + }); + SearchInit({ + scope: $scope, + list: list, + url: defaultUrl, + }); + + $scope.search(list.iterator); + + $scope.PaginateWidget = PaginateWidget({ + iterator: list.iterator, + set: 'organizations' + }); + + var paginationContainer = $('#pagination-container'); + paginationContainer.html($scope.PaginateWidget); + $compile(paginationContainer.contents())($scope) + + var parseCardData = function(cards) { + return cards.map(function(card) { + var val = {}; + val.name = card.name; + val.id = card.id; + if (card.id + "" === cards.activeCard) { + val.isActiveCard = true; + } + val.description = card.description || undefined; + val.links = []; + val.links.push({ + href: card.related.users, + name: "USERS" + }); + val.links.push({ + href: card.related.teams, + name: "TEAMS" + }); + val.links.push({ + href: card.related.inventories, + name: "INVENTORIES" + }); + val.links.push({ + href: card.related.projects, + name: "PROJECTS" + }); + val.links.push({ + href: card.related.job_templates, + name: "JOB TEMPLATES" + }); + val.links.push({ + href: card.related.admins, + name: "ADMINS" + }); + return val; + }); + }; + + var getOrganization = function(id) { + Rest.setUrl(defaultUrl); + Rest.get() + .success(function(data) { + data.results.activeCard = id; + $scope.orgCount = data.count; + $scope.orgCards = parseCardData(data.results); + Wait("stop"); + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, null, { + hdr: 'Error!', + msg: 'Call to ' + defaultUrl + ' failed. DELETE returned status: ' + status + }); + }); + }; + + $scope.$on("ReloadOrgListView", function() { + if ($state.$current.self.name === "organizations") { + delete $scope.activeCard; + if ($scope.orgCards) { + $scope.orgCards = $scope.orgCards.map(function(card) { + delete card.isActiveCard; + return card; + }); + } + $scope.hideListHeader = false; + } + }); + + $scope.$on("ReloadOrganzationCards", function(e, id) { + $scope.activeCard = id; + getOrganization(id); + }); + + $scope.$on("HideOrgListHeader", function() { + $scope.hideListHeader = true; + }); + + $scope.$on("ShowOrgListHeader", function() { + $scope.hideListHeader = false; + }); + + getOrganization(); + + $rootScope.flashMessage = null; + + if ($scope.removePostRefresh) { + $scope.removePostRefresh(); + } + $scope.removePostRefresh = $scope.$on('PostRefresh', function() { + // Cleanup after a delete + Wait('stop'); + $('#prompt-modal').modal('hide'); + }); + + $scope.addOrganization = function() { + $state.transitionTo('organizations.add'); + }; + + $scope.editOrganization = function(id) { + $scope.activeCard = id; + $state.transitionTo('organizations.edit', { + organization_id: id + }); + }; + + $scope.deleteOrganization = function(id, name) { + + var action = function() { + $('#prompt-modal').modal('hide'); + Wait('start'); + var url = defaultUrl + id + '/'; + Rest.setUrl(url); + Rest.destroy() + .success(function() { + if ($state.current.name !== "organzations") { + $state.transitionTo("organizations"); + } + $scope.$emit("ReloadOrganzationCards"); + }) + .error(function(data, status) { + ProcessErrors($scope, data, status, null, { + hdr: 'Error!', + msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status + }); + }); + }; + + Prompt({ + hdr: 'Delete', + body: '
Are you sure you want to delete the organization below?
' + name + '
', + action: action, + actionText: 'DELETE' + }); + }; + } +] diff --git a/awx/ui/client/src/organizations/list/organizations-list.partial.html b/awx/ui/client/src/organizations/list/organizations-list.partial.html new file mode 100644 index 0000000000..b6d531897a --- /dev/null +++ b/awx/ui/client/src/organizations/list/organizations-list.partial.html @@ -0,0 +1,62 @@ +
+
+
+
+
+
+ organizations +
+ + {{ orgCount }} + +
+
+ +
+
+
+
+
+
+

{{ card.name }}

+
+ + +
+
+

{{ card.description || "Place organization description here" }}

+ +
+
+
+
+
diff --git a/awx/ui/client/src/organizations/list/organizations-list.route.js b/awx/ui/client/src/organizations/list/organizations-list.route.js new file mode 100644 index 0000000000..a97b939c01 --- /dev/null +++ b/awx/ui/client/src/organizations/list/organizations-list.route.js @@ -0,0 +1,31 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import {templateUrl} from '../../shared/template-url/template-url.factory'; +import OrganizationsList from './organizations-list.controller'; + +export default { + name: 'organizations', + route: '/organizations', + templateUrl: templateUrl('organizations/list/organizations-list'), + controller: OrganizationsList, + data: { + activityStream: true, + activityStreamTarget: 'organization' + }, + ncyBreadcrumb: { + parent: function($scope) { + $scope.$parent.$emit("ReloadOrgListView"); + return "setup"; + }, + label: "ORGANIZATIONS" + }, + resolve: { + features: ['FeaturesService', function(FeaturesService) { + return FeaturesService.get(); + }] + } +}; diff --git a/awx/ui/client/src/organizations/main.js b/awx/ui/client/src/organizations/main.js new file mode 100644 index 0000000000..b846961f8d --- /dev/null +++ b/awx/ui/client/src/organizations/main.js @@ -0,0 +1,16 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import organizationsList from './list/main'; +import organizationsAdd from './add/main'; +import organizationsEdit from './edit/main'; + +export default +angular.module('organizations', [ + organizationsList.name, + organizationsAdd.name, + organizationsEdit.name, +]); From 52cd4f5ef94a303ef8b839300165f233a77c4822 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 15 Mar 2016 15:06:00 -0400 Subject: [PATCH 54/63] reduce test to only check project inventory connection --- .../api/test_organization_counts.py | 53 ++++++++----------- 1 file changed, 21 insertions(+), 32 deletions(-) diff --git a/awx/main/tests/functional/api/test_organization_counts.py b/awx/main/tests/functional/api/test_organization_counts.py index de629dbcf4..8d881fe8a0 100644 --- a/awx/main/tests/functional/api/test_organization_counts.py +++ b/awx/main/tests/functional/api/test_organization_counts.py @@ -109,47 +109,36 @@ def test_two_organizations(resourced_organization, organizations, user, get): } @pytest.mark.django_db -def test_overlapping_project(resourced_organization, organizations, user, get): - # Check correct results for two organizations are returned +def test_JT_associated_with_project(organizations, project, user, get): + # Check that adding a project to an organization gets the project's JT + # included in the organization's JT count external_admin = user('admin', True) - organization2 = organizations(1)[0] - the_project = resourced_organization.projects.all()[0] - organization2.projects.add(the_project) - organization2.projects.create(name="second-project", - description="test-proj-desc", - scm_type="git", - scm_url="https://github.com/jlaska/ansible-playbooks") - inventory = organization2.inventories.create(name="second-inventory") - organization2.projects.get(name="second-project").jobtemplates.create( - name="second-job-template", - inventory=inventory, - playbook="hello.yml" - ) + two_orgs = organizations(2) + organization = two_orgs[0] + other_org = two_orgs[1] + + unrelated_inv = other_org.inventories.create(name='not-in-organization') + project.jobtemplates.create(name="test-jt", + description="test-job-template-desc", + inventory=unrelated_inv, + playbook="test_playbook.yml") + organization.projects.add(project) response = get(reverse('api:organization_list', args=[]), external_admin) assert response.status_code == 200 - org_id_full = resourced_organization.id - org_id2 = organization2.id + org_id = organization.id counts = {} for i in range(2): - org_id = response.data['results'][i]['id'] - counts[org_id] = response.data['results'][i]['summary_fields']['related_field_counts'] + working_id = response.data['results'][i]['id'] + counts[working_id] = response.data['results'][i]['summary_fields']['related_field_counts'] - assert counts[org_id_full] == { - 'users': 1, - 'admins': 1, - 'job_templates': 1, - 'projects': 1, - 'inventories': 1, - 'teams': 1 - } - assert counts[org_id2] == { + assert counts[org_id] == { 'users': 0, 'admins': 0, - 'job_templates': 2, - 'projects': 2, - 'inventories': 1, + 'job_templates': 1, + 'projects': 1, + 'inventories': 0, 'teams': 0 } - assert False + From 63b01bb04b98b8a7bd00590fdea0686b561bafc8 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 16 Mar 2016 13:06:54 -0400 Subject: [PATCH 55/63] Fix up the docker-refresh Makefile target This allows you to cleanup the images without requiring a rebuild necessarily --- Makefile | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index a6461e8e66..df28fdb381 100644 --- a/Makefile +++ b/Makefile @@ -802,13 +802,15 @@ docker-compose-test: cd tools && docker-compose run --rm --service-ports tower /bin/bash MACHINE?=default -docker-refresh: +docker-clean: rm -f awx/lib/.deps_built + rm -f awx/lib/site-packages eval $$(docker-machine env $(MACHINE)) docker stop $$(docker ps -a -q) - docker rm $$(docker ps -f name=tools_tower -a -q) - docker rmi tools_tower - docker-compose -f tools/docker-compose.yml up + -docker rm $$(docker ps -f name=tools_tower -a -q) + -docker rmi tools_tower + +docker-refresh: docker-clean docker-compose mongo-debug-ui: docker run -it --rm --name mongo-express --link tools_mongo_1:mongo -e ME_CONFIG_OPTIONS_EDITORTHEME=ambiance -e ME_CONFIG_BASICAUTH_USERNAME=admin -e ME_CONFIG_BASICAUTH_PASSWORD=password -p 8081:8081 knickers/mongo-express From 45f95bf2b257f92fa0afd3ae1251a03b91e30726 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 16 Mar 2016 13:08:50 -0400 Subject: [PATCH 56/63] Disallow related elements to be treated as choices DRF will try to resolve potential candidates into the OPTIONS endpoint. This is mainly to support their POST field in the browseable API. We don't need this and it can yield some expensive queries so we bypass generating choices for any RelatedField fields --- awx/api/metadata.py | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/awx/api/metadata.py b/awx/api/metadata.py index 01f8fe306e..3ec4c6d4d1 100644 --- a/awx/api/metadata.py +++ b/awx/api/metadata.py @@ -1,6 +1,8 @@ # Copyright (c) 2016 Ansible, Inc. # All Rights Reserved. +from collections import OrderedDict + # Django from django.core.exceptions import PermissionDenied from django.http import Http404 @@ -10,6 +12,7 @@ from django.utils.encoding import force_text from rest_framework import exceptions from rest_framework import metadata from rest_framework import serializers +from rest_framework.relations import RelatedField from rest_framework.request import clone_request # Ansible Tower @@ -37,9 +40,20 @@ class Metadata(metadata.SimpleMetadata): return field_info def get_field_info(self, field): - field_info = super(Metadata, self).get_field_info(field) - if hasattr(field, 'choices') and field.choices: - field_info = self._render_read_only_choices(field, field_info) + field_info = OrderedDict() + field_info['type'] = self.label_lookup[field] + field_info['required'] = getattr(field, 'required', False) + + text_attrs = [ + 'read_only', 'label', 'help_text', + 'min_length', 'max_length', + 'min_value', 'max_value' + ] + + for attr in text_attrs: + value = getattr(field, attr, None) + if value is not None and value != '': + field_info[attr] = force_text(value, strings_only=True) # Indicate if a field has a default value. # FIXME: Still isn't showing all default values? @@ -48,21 +62,18 @@ class Metadata(metadata.SimpleMetadata): except serializers.SkipField: pass + if getattr(field, 'child', None): + field_info['child'] = self.get_field_info(field.child) + elif getattr(field, 'fields', None): + field_info['children'] = self.get_serializer_info(field) + + if hasattr(field, 'choices') and not isinstance(field, RelatedField): + field_info['choices'] = [(choice_value, choice_name) for choice_value, choice_name in field.choices.items()] + # Indicate if a field is write-only. if getattr(field, 'write_only', False): field_info['write_only'] = True - # Update choices to be a list of 2-tuples instead of list of dicts with - # value/display_name. - if 'choices' in field_info: - choices = [] - for choice in field_info['choices']: - if isinstance(choice, dict): - choices.append((choice.get('value'), choice.get('display_name'))) - else: - choices.append(choice) - field_info['choices'] = choices - # Special handling of inventory source_region choices that vary based on # selected inventory source. if field.field_name == 'source_regions': From 526a6ec7dd25b3b2aece7a89a6f75371e27ff3d0 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 16 Mar 2016 13:12:13 -0400 Subject: [PATCH 57/63] Remove unneeded fetch for r/o fields --- awx/api/metadata.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/awx/api/metadata.py b/awx/api/metadata.py index 3ec4c6d4d1..6fccdb887d 100644 --- a/awx/api/metadata.py +++ b/awx/api/metadata.py @@ -21,24 +21,6 @@ from awx.main.models import InventorySource, Notifier class Metadata(metadata.SimpleMetadata): - # DRF 3.3 doesn't render choices for read-only fields - # - # We want to render choices for read-only fields - # - # Note: This works in conjuction with logic in serializers.py that sets - # field property editable=True before calling DRF build_standard_field() - # Note: Consider expanding this rendering for more than just choices fields - def _render_read_only_choices(self, field, field_info): - if field_info.get('read_only') and hasattr(field, 'choices'): - field_info['choices'] = [ - { - 'value': choice_value, - 'display_name': force_text(choice_name, strings_only=True) - } - for choice_value, choice_name in field.choices.items() - ] - return field_info - def get_field_info(self, field): field_info = OrderedDict() field_info['type'] = self.label_lookup[field] From 8ca3a6b2bfd5d130af947e9820a745ffede6fcea Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Wed, 16 Mar 2016 13:23:37 -0400 Subject: [PATCH 58/63] Added counts to organizations listing --- .../list/organizations-list.controller.js | 18 ++++++++++++------ .../list/organizations-list.partial.html | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/awx/ui/client/src/organizations/list/organizations-list.controller.js b/awx/ui/client/src/organizations/list/organizations-list.controller.js index 2a21b03ffb..7316f145b9 100644 --- a/awx/ui/client/src/organizations/list/organizations-list.controller.js +++ b/awx/ui/client/src/organizations/list/organizations-list.controller.js @@ -56,27 +56,33 @@ export default ['$stateParams', '$scope', '$rootScope', '$location', val.links = []; val.links.push({ href: card.related.users, - name: "USERS" + name: "USERS", + count: card.summary_fields.related_field_counts.users }); val.links.push({ href: card.related.teams, - name: "TEAMS" + name: "TEAMS", + count: card.summary_fields.related_field_counts.teams }); val.links.push({ href: card.related.inventories, - name: "INVENTORIES" + name: "INVENTORIES", + count: card.summary_fields.related_field_counts.inventories }); val.links.push({ href: card.related.projects, - name: "PROJECTS" + name: "PROJECTS", + count: card.summary_fields.related_field_counts.projects }); val.links.push({ href: card.related.job_templates, - name: "JOB TEMPLATES" + name: "JOB TEMPLATES", + count: card.summary_fields.related_field_counts.job_templates }); val.links.push({ href: card.related.admins, - name: "ADMINS" + name: "ADMINS", + count: card.summary_fields.related_field_counts.admins }); return val; }); diff --git a/awx/ui/client/src/organizations/list/organizations-list.partial.html b/awx/ui/client/src/organizations/list/organizations-list.partial.html index b6d531897a..73bfa908e9 100644 --- a/awx/ui/client/src/organizations/list/organizations-list.partial.html +++ b/awx/ui/client/src/organizations/list/organizations-list.partial.html @@ -47,7 +47,7 @@
{{ result.name }}{{ result.name }}{{ result.name }}{{ result.name }} {{ result.item }} {{ result.msg }}