diff --git a/Makefile b/Makefile index d74468a9e7..8b79000e06 100644 --- a/Makefile +++ b/Makefile @@ -81,7 +81,7 @@ SETUP_TAR_CHECKSUM=$(NAME)-setup-CHECKSUM # DEB build parameters DEBUILD_BIN ?= debuild -DEBUILD_OPTS = +DEBUILD_OPTS = DPUT_BIN ?= dput DPUT_OPTS ?= -c .dput.cf -u REPREPRO_BIN ?= reprepro @@ -184,7 +184,7 @@ UI_RELEASE_FLAG_FILE = awx/ui/.release_built deb deb-src debian debsign pbuilder reprepro setup_tarball \ virtualbox-ovf virtualbox-centos-7 virtualbox-centos-6 \ clean-bundle setup_bundle_tarball \ - ui-docker-machine ui-docker ui-release \ + ui-docker-machine ui-docker ui-release ui-devel \ ui-test ui-deps ui-test-ci ui-test-saucelabs jlaska @@ -229,6 +229,7 @@ clean-venv: # Remove temporary build files, compiled Python files. clean: clean-rpm clean-deb clean-ui clean-tar clean-packer clean-bundle + rm -rf awx/public rm -rf awx/lib/site-packages rm -rf dist/* rm -rf tmp @@ -506,6 +507,43 @@ test_jenkins : test_coverage # UI TASKS # -------------------------------------- +HAVE_PO := $(shell ls awx/ui/po/*.po 2>/dev/null) +check-po: +ifdef HAVE_PO + # Should be 'Language: zh-CN' but not 'Language: zh_CN' in zh_CN.po + for po in awx/ui/po/*.po ; do \ + echo $$po; \ + mo="awx/ui/po/`basename $$po .po`.mo"; \ + msgfmt --check --verbose $$po -o $$mo; \ + if test "$$?" -ne 0 ; then \ + exit -1; \ + fi; \ + rm $$mo; \ + name=`echo "$$po" | grep '-'`; \ + if test "x$$name" != x ; then \ + right_name=`echo $$language | sed -e 's/-/_/'`; \ + echo "ERROR: WRONG $$name CORRECTION: $$right_name"; \ + exit -1; \ + fi; \ + language=`grep '^"Language:' "$$po" | grep '_'`; \ + if test "x$$language" != x ; then \ + right_language=`echo $$language | sed -e 's/_/-/'`; \ + echo "ERROR: WRONG $$language CORRECTION: $$right_language in $$po"; \ + exit -1; \ + fi; \ + done; +else + @echo No PO files +endif + +# generate l10n .json +languages: $(UI_DEPS_FLAG_FILE) check-po + $(NPM_BIN) --prefix awx/ui run languages + +# generate .pot +pot: $(UI_DEPS_FLAG_FILE) + $(NPM_BIN) --prefix awx/ui run pot + ui-deps: $(UI_DEPS_FLAG_FILE) $(UI_DEPS_FLAG_FILE): awx/ui/package.json @@ -518,8 +556,13 @@ ui-docker-machine: $(UI_DEPS_FLAG_FILE) ui-docker: $(UI_DEPS_FLAG_FILE) $(NPM_BIN) --prefix awx/ui run build-docker-cid +# Builds UI with development/debug settings enabled. Does not raise browser-sync or filesystem polling. +ui-devel: $(UI_DEPS_FLAG_FILE) + $(NPM_BIN) --prefix awx/ui run build-devel + ui-release: $(UI_RELEASE_FLAG_FILE) +# todo: include languages target when .po deliverables are added to source control $(UI_RELEASE_FLAG_FILE): $(UI_DEPS_FLAG_FILE) $(NPM_BIN) --prefix awx/ui run build-release touch $(UI_RELEASE_FLAG_FILE) diff --git a/awx/main/scheduler/dag_workflow.py b/awx/main/scheduler/dag_workflow.py index c891b2ec32..6d2b349365 100644 --- a/awx/main/scheduler/dag_workflow.py +++ b/awx/main/scheduler/dag_workflow.py @@ -42,7 +42,9 @@ class WorkflowDAG(SimpleDAG): nodes.extend(children_all) elif job.status in ['successful']: children_success = self.get_dependencies(obj, 'success_nodes') - nodes.extend(children_success) + children_always = self.get_dependencies(obj, 'always_nodes') + children_all = children_success + children_always + nodes.extend(children_all) return [n['node_object'] for n in nodes_found] def is_workflow_done(self): @@ -67,6 +69,8 @@ class WorkflowDAG(SimpleDAG): nodes.extend(children_all) elif job.status in ['successful']: children_success = self.get_dependencies(obj, 'success_nodes') - nodes.extend(children_success) + children_always = self.get_dependencies(obj, 'always_nodes') + children_all = children_success + children_always + nodes.extend(children_all) return True diff --git a/awx/main/tests/unit/models/test_job_unit.py b/awx/main/tests/unit/models/test_job_unit.py index 9de1f77ee6..ff55d7103b 100644 --- a/awx/main/tests/unit/models/test_job_unit.py +++ b/awx/main/tests/unit/models/test_job_unit.py @@ -7,12 +7,14 @@ from awx.main.models import Job @pytest.fixture def job(mocker): - return mocker.MagicMock(**{ + ret = mocker.MagicMock(**{ 'display_extra_vars.return_value': '{\"secret_key\": \"$encrypted$\"}', 'extra_vars_dict': {"secret_key": "my_password"}, 'pk': 1, 'job_template.pk': 1, 'job_template.name': '', 'created_by.pk': 1, 'created_by.username': 'admin', 'launch_type': 'manual'}) + ret.project = mocker.MagicMock(scm_revision='asdf1234') + return ret @pytest.mark.survey def test_job_survey_password_redaction(): diff --git a/awx/main/tests/unit/scheduler/test_dag.py b/awx/main/tests/unit/scheduler/test_dag.py index 84fb2d37f2..ab1e45c3d8 100644 --- a/awx/main/tests/unit/scheduler/test_dag.py +++ b/awx/main/tests/unit/scheduler/test_dag.py @@ -154,7 +154,25 @@ def workflow_dag_finished(factory_node): expected = [] return (dag, expected, True) -@pytest.fixture(params=['workflow_dag_multiple_roots', 'workflow_dag_level_2', 'workflow_dag_multiple_edges_labeled', 'workflow_dag_finished']) +@pytest.fixture +def workflow_dag_always(factory_node): + dag = WorkflowDAG() + data = [ + factory_node(0, 'failed'), + factory_node(1, 'successful'), + factory_node(2, None), + ] + [dag.add_node(d) for d in data] + + dag.add_edge(data[0], data[1], 'always_nodes') + dag.add_edge(data[1], data[2], 'always_nodes') + + expected = data[2:3] + return (dag, expected, False) + +@pytest.fixture(params=['workflow_dag_multiple_roots', 'workflow_dag_level_2', + 'workflow_dag_multiple_edges_labeled', 'workflow_dag_finished', + 'workflow_dag_always']) def workflow_dag(request): return request.getfuncargvalue(request.param) diff --git a/awx/ui/Gruntfile.js b/awx/ui/Gruntfile.js index 57fddad6ed..a118409568 100644 --- a/awx/ui/Gruntfile.js +++ b/awx/ui/Gruntfile.js @@ -16,16 +16,26 @@ module.exports = function(grunt) { // Project configuration. grunt.initConfig(configs); grunt.loadNpmTasks('grunt-newer'); + grunt.loadNpmTasks('grunt-angular-gettext'); // writes environment variables for development. current manages: // browser-sync + websocket proxy + grunt.registerTask('sync', [ + 'browserSync:http', + 'concurrent:watch' + ]); + grunt.registerTask('dev', [ 'clean:tmp', 'clean:static', 'concurrent:dev', - 'browserSync:http', - 'concurrent:watch' + ]); + + grunt.registerTask('devNoSync', [ + 'clean:tmp', + 'clean:static', + 'concurrent:devNoSync', ]); grunt.registerTask('release', [ diff --git a/awx/ui/client/src/about/about.controller.js b/awx/ui/client/src/about/about.controller.js index 90824532db..2c821b9e09 100644 --- a/awx/ui/client/src/about/about.controller.js +++ b/awx/ui/client/src/about/about.controller.js @@ -1,5 +1,6 @@ export default - ['$scope', '$state', 'ConfigService', function($scope, $state, ConfigService){ + ['$scope', '$state', 'ConfigService', 'i18n', + function($scope, $state, ConfigService, i18n){ var processVersion = function(version){ // prettify version & calculate padding // e,g 3.0.0-0.git201602191743/ -> 3.0.0 @@ -20,6 +21,7 @@ export default .then(function(config){ $scope.subscription = config.license_info.subscription_name; $scope.version = processVersion(config.version); + $scope.version_str = i18n._("Version"); $('#about-modal').modal('show'); }); }; diff --git a/awx/ui/client/src/about/about.partial.html b/awx/ui/client/src/about/about.partial.html index 65cfd11f03..bcb2a5cd33 100644 --- a/awx/ui/client/src/about/about.partial.html +++ b/awx/ui/client/src/about/about.partial.html @@ -11,7 +11,7 @@
  ________________
-/  Tower Version \\
+/  Tower {{version_str}} \\
 \\{{version}}/
  ----------------
         \\   ^__^
diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js
index 6df9567b34..b7a3c25d43 100644
--- a/awx/ui/client/src/app.js
+++ b/awx/ui/client/src/app.js
@@ -7,6 +7,7 @@
 // Vendor dependencies
 import 'jquery';
 import 'angular';
+import 'angular-gettext';
 import 'bootstrap';
 import 'jquery-ui';
 import 'bootstrap-datepicker';
@@ -79,6 +80,7 @@ import config from './shared/config/main';
 import './login/authenticationServices/pendo/ng-pendo';
 import footer from './footer/main';
 import scheduler from './scheduler/main';
+import {N_} from './i18n';
 
 var tower = angular.module('Tower', [
     // how to add CommonJS / AMD  third-party dependencies:
@@ -203,6 +205,8 @@ var tower = angular.module('Tower', [
     scheduler.name,
     'ApiModelHelper',
     'ActivityStreamHelper',
+    'gettext',
+    'I18N',
 ])
 
 .constant('AngularScheduler.partials', urlPrefix + 'lib/angular-scheduler/lib/')
@@ -237,6 +241,10 @@ var tower = angular.module('Tower', [
                 $state.go('dashboard');
             });
 
+            /* Mark translatable strings with N_() and
+             * extract them by 'grunt nggettext_extract'
+             * but angular.config() cannot get gettextCatalog.
+             */
             $stateProvider.
             state('teams', {
                 url: '/teams',
@@ -248,7 +256,7 @@ var tower = angular.module('Tower', [
                 },
                 ncyBreadcrumb: {
                     parent: 'setup',
-                    label: 'TEAMS'
+                    label: N_("TEAMS")
                 }
             }).
 
@@ -258,7 +266,7 @@ var tower = angular.module('Tower', [
                 controller: TeamsAdd,
                 ncyBreadcrumb: {
                     parent: "teams",
-                    label: "CREATE TEAM"
+                    label: N_("CREATE TEAM")
                 }
             }).
 
@@ -333,7 +341,7 @@ var tower = angular.module('Tower', [
                 },
                 ncyBreadcrumb: {
                     parent: 'setup',
-                    label: 'CREDENTIALS'
+                    label: N_("CREDENTIALS")
                 }
             }).
 
@@ -343,7 +351,7 @@ var tower = angular.module('Tower', [
                 controller: CredentialsAdd,
                 ncyBreadcrumb: {
                     parent: "credentials",
-                    label: "CREATE CREDENTIAL"
+                    label: N_("CREATE CREDENTIAL")
                 }
             }).
 
@@ -370,7 +378,7 @@ var tower = angular.module('Tower', [
                 },
                 ncyBreadcrumb: {
                     parent: 'setup',
-                    label: 'USERS'
+                    label: N_("USERS")
                 }
             }).
 
@@ -380,7 +388,7 @@ var tower = angular.module('Tower', [
                 controller: UsersAdd,
                 ncyBreadcrumb: {
                     parent: "users",
-                    label: "CREATE USER"
+                    label: N_("CREATE USER")
                 }
             }).
 
@@ -420,7 +428,7 @@ var tower = angular.module('Tower', [
                 templateUrl: urlPrefix + 'partials/sockets.html',
                 controller: SocketsController,
                 ncyBreadcrumb: {
-                    label: 'SOCKETS'
+                    label: N_("SOCKETS")
                 }
             });
         }
@@ -443,13 +451,14 @@ var tower = angular.module('Tower', [
     'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'Timer',
     'ClearScope', 'LoadConfig', 'Store', 'pendoService', 'Prompt', 'Rest',
     'Wait', 'ProcessErrors', '$state', 'GetBasePath', 'ConfigService',
-    'FeaturesService', '$filter', 'SocketService',
+    'FeaturesService', '$filter', 'SocketService', 'I18NInit',
     function($stateExtender, $q, $compile, $cookieStore, $rootScope, $log,
         CheckLicense, $location, Authorization, LoadBasePaths, Timer,
         ClearScope, LoadConfig, Store, pendoService, Prompt, Rest, Wait,
         ProcessErrors, $state, GetBasePath, ConfigService, FeaturesService,
-        $filter, SocketService) {
+        $filter, SocketService, I18NInit) {
 
+        I18NInit();
         $stateExtender.addState({
             name: 'dashboard',
             url: '/home',
@@ -466,7 +475,7 @@ var tower = angular.module('Tower', [
                 refreshButton: true
             },
             ncyBreadcrumb: {
-                label: "DASHBOARD"
+                label: N_("DASHBOARD")
             },
             resolve: {
                 graphData: ['$q', 'jobStatusGraphData', '$rootScope',
@@ -487,7 +496,7 @@ var tower = angular.module('Tower', [
             templateUrl: urlPrefix + 'partials/jobs.html',
             controller: JobsListController,
             ncyBreadcrumb: {
-                label: "JOBS"
+                label: N_("JOBS")
             },
             params: {
                 search: {
@@ -512,7 +521,7 @@ var tower = angular.module('Tower', [
                 activityStreamTarget: 'project'
             },
             ncyBreadcrumb: {
-                label: "PROJECTS"
+                label: N_("PROJECTS")
             },
             socket: {
                 "groups":{
@@ -528,7 +537,7 @@ var tower = angular.module('Tower', [
             controller: ProjectsAdd,
             ncyBreadcrumb: {
                 parent: "projects",
-                label: "CREATE PROJECT"
+                label: N_("CREATE PROJECT")
             },
             socket: {
                 "groups":{
diff --git a/awx/ui/client/src/bread-crumb/bread-crumb.partial.html b/awx/ui/client/src/bread-crumb/bread-crumb.partial.html
index 8e661904ba..4500556e96 100644
--- a/awx/ui/client/src/bread-crumb/bread-crumb.partial.html
+++ b/awx/ui/client/src/bread-crumb/bread-crumb.partial.html
@@ -3,7 +3,7 @@