From 5ca25b924efdb77da2c04d306effc3bb5a8d97c5 Mon Sep 17 00:00:00 2001 From: Aaron Tan Date: Mon, 24 Oct 2016 11:06:25 -0400 Subject: [PATCH 01/21] Add missing always_node check in bfs --- awx/main/scheduler/dag_workflow.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 From bb34ca662f402b1cbbd91577437bbceea3c34eb2 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 24 Oct 2016 16:45:37 -0400 Subject: [PATCH 02/21] update job unit tests to HA project update changes --- awx/main/tests/unit/models/test_job_unit.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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(): From 56a9978d560ca96c428429de6f153104adc05f6f Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 13 Oct 2016 12:44:13 -0400 Subject: [PATCH 03/21] Introduce workflow failure condition --- awx/api/serializers.py | 6 ++- .../0041_v310_workflow_failure_condition.py | 24 ++++++++++++ awx/main/models/workflow.py | 9 ++++- awx/main/scheduler/__init__.py | 8 ++-- .../tests/functional/models/test_workflow.py | 39 +++++++++++++++++++ 5 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 awx/main/migrations/0041_v310_workflow_failure_condition.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index d6aa39f706..4b5b411a08 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2249,11 +2249,15 @@ class WorkflowNodeBaseSerializer(BaseSerializer): success_nodes = serializers.PrimaryKeyRelatedField(many=True, read_only=True) failure_nodes = serializers.PrimaryKeyRelatedField(many=True, read_only=True) always_nodes = serializers.PrimaryKeyRelatedField(many=True, read_only=True) + fail_on_job_failure = serializers.BooleanField( + help_text=('If set to true, and if the job runs and fails, ' + 'the workflow will also be marked as failed.'), + default=True) class Meta: fields = ('*', '-name', '-description', 'id', 'url', 'related', 'unified_job_template', 'success_nodes', 'failure_nodes', 'always_nodes', - 'inventory', 'credential', 'job_type', 'job_tags', 'skip_tags', 'limit', 'skip_tags') + 'inventory', 'credential', 'job_type', 'job_tags', 'skip_tags', 'limit', 'skip_tags', 'fail_on_job_failure') def get_related(self, obj): res = super(WorkflowNodeBaseSerializer, self).get_related(obj) diff --git a/awx/main/migrations/0041_v310_workflow_failure_condition.py b/awx/main/migrations/0041_v310_workflow_failure_condition.py new file mode 100644 index 0000000000..8544f9884a --- /dev/null +++ b/awx/main/migrations/0041_v310_workflow_failure_condition.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0040_v310_artifacts'), + ] + + operations = [ + migrations.AddField( + model_name='workflowjobnode', + name='fail_on_job_failure', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='workflowjobtemplatenode', + name='fail_on_job_failure', + field=models.BooleanField(default=True), + ), + ] diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 2848b38a4a..6ef5e88956 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -60,6 +60,10 @@ class WorkflowNodeBase(CreatedModifiedModel): default=None, on_delete=models.SET_NULL, ) + fail_on_job_failure = models.BooleanField( + blank=True, + default=True, + ) # Prompting-related fields inventory = models.ForeignKey( 'Inventory', @@ -137,7 +141,7 @@ class WorkflowNodeBase(CreatedModifiedModel): Return field names that should be copied from template node to job node. ''' return ['workflow_job', 'unified_job_template', - 'inventory', 'credential', 'char_prompts'] + 'inventory', 'credential', 'char_prompts', 'fail_on_job_failure'] class WorkflowJobTemplateNode(WorkflowNodeBase): # TODO: Ensure the API forces workflow_job_template being set @@ -383,6 +387,9 @@ class WorkflowJob(UnifiedJob, WorkflowJobOptions, JobNotificationMixin, Workflow from awx.main.tasks import RunWorkflowJob return RunWorkflowJob + def _has_failed(self): + return self.workflow_job_nodes.filter(job__status='failed', fail_on_job_failure=True).exists() + def socketio_emit_data(self): return {} diff --git a/awx/main/scheduler/__init__.py b/awx/main/scheduler/__init__.py index b93ce6956e..6ecdc09b37 100644 --- a/awx/main/scheduler/__init__.py +++ b/awx/main/scheduler/__init__.py @@ -73,10 +73,12 @@ def process_finished_workflow_jobs(workflow_jobs): dag = WorkflowDAG(workflow_job) if dag.is_workflow_done(): with transaction.atomic(): - # TODO: detect if wfj failed - workflow_job.status = 'completed' + if workflow_job._has_failed(): + workflow_job.status = 'failed' + else: + workflow_job.status = 'successful' workflow_job.save() - workflow_job.websocket_emit_status('completed') + workflow_job.websocket_emit_status(workflow_job.status) def rebuild_graph(): """Regenerate the task graph by refreshing known tasks from Tower, purging diff --git a/awx/main/tests/functional/models/test_workflow.py b/awx/main/tests/functional/models/test_workflow.py index cc61c34ed9..aa622d996a 100644 --- a/awx/main/tests/functional/models/test_workflow.py +++ b/awx/main/tests/functional/models/test_workflow.py @@ -90,3 +90,42 @@ class TestWorkflowJobTemplate: assert len(parent_qs) == 1 assert parent_qs[0] == wfjt.workflow_job_template_nodes.all()[1] +@pytest.mark.django_db +class TestWorkflowJobFailure: + @pytest.fixture + def wfj(self): + return WorkflowJob.objects.create(name='test-wf-job') + + def test_workflow_has_failed(self, wfj): + """ + Test that a single failed node with fail_on_job_failure = true + leads to the entire WF being marked as failed + """ + job = Job.objects.create(name='test-job', status='failed') + # Node has a failed job connected + WorkflowJobNode.objects.create(workflow_job=wfj, job=job) + assert wfj._has_failed() + + def test_workflow_not_failed_unran_job(self, wfj): + """ + Test that an un-ran node will not mark workflow job as failed + """ + WorkflowJobNode.objects.create(workflow_job=wfj) + assert not wfj._has_failed() + + def test_workflow_not_failed_successful_job(self, wfj): + """ + Test that a sucessful node will not mark workflow job as failed + """ + job = Job.objects.create(name='test-job', status='successful') + WorkflowJobNode.objects.create(workflow_job=wfj, job=job) + assert not wfj._has_failed() + + def test_workflow_not_failed_failed_job_but_okay(self, wfj): + """ + Test that a failed node will not mark workflow job as failed + if the fail_on_job_failure is set to false + """ + job = Job.objects.create(name='test-job', status='failed') + WorkflowJobNode.objects.create(workflow_job=wfj, job=job, fail_on_job_failure=False) + assert not wfj._has_failed() From 48fc9eb773663cbc9db445948fdc2cfd0d85bae4 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 14 Oct 2016 16:30:58 -0400 Subject: [PATCH 04/21] update workflow model unit tests --- awx/main/tests/unit/models/test_workflow_unit.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awx/main/tests/unit/models/test_workflow_unit.py b/awx/main/tests/unit/models/test_workflow_unit.py index 9df61ffe97..bcbec85c81 100644 --- a/awx/main/tests/unit/models/test_workflow_unit.py +++ b/awx/main/tests/unit/models/test_workflow_unit.py @@ -139,6 +139,7 @@ class TestWorkflowJobCreate: char_prompts=wfjt_node_no_prompts.char_prompts, inventory=None, credential=None, unified_job_template=wfjt_node_no_prompts.unified_job_template, + fail_on_job_failure=True, workflow_job=workflow_job_unit) def test_create_with_prompts(self, wfjt_node_with_prompts, workflow_job_unit, mocker): @@ -150,6 +151,7 @@ class TestWorkflowJobCreate: inventory=wfjt_node_with_prompts.inventory, credential=wfjt_node_with_prompts.credential, unified_job_template=wfjt_node_with_prompts.unified_job_template, + fail_on_job_failure=True, workflow_job=workflow_job_unit) @mock.patch('awx.main.models.workflow.WorkflowNodeBase.get_parent_nodes', lambda self: []) From 10c847e8048055e37be2517a11425ea3425b02eb Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 17 Oct 2016 08:35:26 -0400 Subject: [PATCH 05/21] docs feedback for fojf incorporated --- awx/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 4b5b411a08..a463a06064 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2251,7 +2251,7 @@ class WorkflowNodeBaseSerializer(BaseSerializer): always_nodes = serializers.PrimaryKeyRelatedField(many=True, read_only=True) fail_on_job_failure = serializers.BooleanField( help_text=('If set to true, and if the job runs and fails, ' - 'the workflow will also be marked as failed.'), + 'the workflow is marked as failed.'), default=True) class Meta: From e0173a5ca2174fc07bb07ba367b16e597da27048 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 25 Oct 2016 09:16:09 -0400 Subject: [PATCH 06/21] bump migration file number --- ...ilure_condition.py => 0045_v310_workflow_failure_condition.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename awx/main/migrations/{0041_v310_workflow_failure_condition.py => 0045_v310_workflow_failure_condition.py} (100%) diff --git a/awx/main/migrations/0041_v310_workflow_failure_condition.py b/awx/main/migrations/0045_v310_workflow_failure_condition.py similarity index 100% rename from awx/main/migrations/0041_v310_workflow_failure_condition.py rename to awx/main/migrations/0045_v310_workflow_failure_condition.py From 836c13f8571a3be69cfcc7b597eff31fb39097d9 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 25 Oct 2016 09:18:14 -0400 Subject: [PATCH 07/21] bump migration dependency --- awx/main/migrations/0045_v310_workflow_failure_condition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/migrations/0045_v310_workflow_failure_condition.py b/awx/main/migrations/0045_v310_workflow_failure_condition.py index 8544f9884a..9bafa0feb6 100644 --- a/awx/main/migrations/0045_v310_workflow_failure_condition.py +++ b/awx/main/migrations/0045_v310_workflow_failure_condition.py @@ -7,7 +7,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('main', '0040_v310_artifacts'), + ('main', '0044_v310_project_playbook_files'), ] operations = [ From 04013e4d9f9daef86e0d82c33fdfae8f250d0a8c Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 25 Oct 2016 16:34:36 -0400 Subject: [PATCH 08/21] fix problems with OPTIONS for char_prompts --- awx/api/serializers.py | 26 +++++++++++--------------- awx/api/views.py | 5 +++++ awx/main/models/workflow.py | 16 ++++++++++++++++ 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index a463a06064..70e37b8220 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2242,10 +2242,10 @@ class WorkflowJobListSerializer(WorkflowJobSerializer, UnifiedJobListSerializer) pass class WorkflowNodeBaseSerializer(BaseSerializer): - job_type = serializers.SerializerMethodField() - job_tags = serializers.SerializerMethodField() - limit = serializers.SerializerMethodField() - skip_tags = serializers.SerializerMethodField() + job_type = serializers.CharField(allow_blank=True, required=False, default=None) + job_tags = serializers.CharField(allow_blank=True, required=False, default=None) + limit = serializers.CharField(allow_blank=True, required=False, default=None) + skip_tags = serializers.CharField(allow_blank=True, required=False, default=None) success_nodes = serializers.PrimaryKeyRelatedField(many=True, read_only=True) failure_nodes = serializers.PrimaryKeyRelatedField(many=True, read_only=True) always_nodes = serializers.PrimaryKeyRelatedField(many=True, read_only=True) @@ -2265,17 +2265,13 @@ class WorkflowNodeBaseSerializer(BaseSerializer): res['unified_job_template'] = obj.unified_job_template.get_absolute_url() return res - def get_job_type(self, obj): - return obj.char_prompts.get('job_type', None) - - def get_job_tags(self, obj): - return obj.char_prompts.get('job_tags', None) - - def get_skip_tags(self, obj): - return obj.char_prompts.get('skip_tags', None) - - def get_limit(self, obj): - return obj.char_prompts.get('limit', None) + def validate(self, attrs): + # char_prompts go through different validation, so remove them here + print ' attrs: ' + str(attrs) + for fd in ['job_type', 'job_tags', 'skip_tags', 'limit']: + if fd in attrs: + attrs.pop(fd) + return super(WorkflowNodeBaseSerializer, self).validate(attrs) class WorkflowJobTemplateNodeSerializer(WorkflowNodeBaseSerializer): diff --git a/awx/api/views.py b/awx/api/views.py index 66980e141d..2327f11b6a 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2751,6 +2751,11 @@ class WorkflowJobTemplateWorkflowNodesList(SubListCreateAPIView): relationship = 'workflow_job_template_nodes' parent_key = 'workflow_job_template' + def update_raw_data(self, data): + for fd in ['job_type', 'job_tags', 'skip_tags', 'limit', 'skip_tags']: + data[fd] = None + return super(WorkflowJobTemplateWorkflowNodesList, self).update_raw_data(data) + # TODO: class WorkflowJobTemplateJobsList(SubListAPIView): diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 6ef5e88956..3bc0608cfc 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -97,6 +97,22 @@ class WorkflowNodeBase(CreatedModifiedModel): data[fd] = self.char_prompts[fd] return data + @property + def job_type(self): + return self.char_prompts.get('job_type', None) + + @property + def job_tags(self): + return self.char_prompts.get('job_tags', None) + + @property + def skip_tags(self): + return self.char_prompts.get('skip_tags', None) + + @property + def limit(self): + return self.char_prompts.get('limit', None) + def get_prompts_warnings(self): ujt_obj = self.unified_job_template if ujt_obj is None: From 40b94a38a86aa0d2a4cc38861c3d1bd64e3feb3e Mon Sep 17 00:00:00 2001 From: Takao Fujiwara Date: Fri, 7 Oct 2016 18:43:33 +0900 Subject: [PATCH 09/21] Enable message i18n with angular-gettext Signed-off-by: Takao Fujiwara --- Makefile | 41 +- awx/ui/Gruntfile.js | 1 + awx/ui/client/src/about/about.controller.js | 4 +- awx/ui/client/src/about/about.partial.html | 2 +- awx/ui/client/src/app.js | 36 +- .../src/bread-crumb/bread-crumb.partial.html | 2 +- awx/ui/client/src/controllers/Projects.js | 112 +- awx/ui/client/src/controllers/Users.js | 34 +- .../counts/dashboard-counts.directive.js | 15 +- .../graphs/dashboard-graphs.partial.html | 34 +- .../job-status/job-status-graph.directive.js | 11 +- .../dashboard/hosts/dashboard-hosts.list.js | 8 +- .../job-templates-list.partial.html | 14 +- .../lists/jobs/jobs-list.partial.html | 12 +- awx/ui/client/src/forms/Credentials.js | 111 +- awx/ui/client/src/forms/Inventories.js | 35 +- awx/ui/client/src/forms/JobTemplates.js | 154 +- awx/ui/client/src/forms/Organizations.js | 25 +- awx/ui/client/src/forms/Projects.js | 78 +- awx/ui/client/src/forms/Teams.js | 41 +- awx/ui/client/src/forms/Users.js | 47 +- awx/ui/client/src/helpers/Credentials.js | 92 +- awx/ui/client/src/i18n.js | 97 + .../inventory-scripts.form.js | 20 +- .../inventory-scripts.list.js | 28 +- .../add/job-templates-add.route.js | 3 +- .../client/src/license/license.controller.js | 11 +- .../client/src/license/license.partial.html | 52 +- awx/ui/client/src/lists/CompletedJobs.js | 21 +- awx/ui/client/src/lists/Credentials.js | 37 +- awx/ui/client/src/lists/Inventories.js | 41 +- awx/ui/client/src/lists/JobTemplates.js | 47 +- awx/ui/client/src/lists/PortalJobTemplates.js | 19 +- awx/ui/client/src/lists/PortalJobs.js | 15 +- awx/ui/client/src/lists/Projects.js | 34 +- awx/ui/client/src/lists/ScheduledJobs.js | 29 +- awx/ui/client/src/lists/Teams.js | 33 +- awx/ui/client/src/lists/Users.js | 33 +- .../login/loginModal/loginModal.partial.html | 8 +- .../src/main-menu/main-menu.partial.html | 34 +- .../management-jobs/card/card.partial.html | 8 +- .../notificationTemplates.form.js | 108 +- .../notificationTemplates.list.js | 30 +- .../src/notifications/notifications.list.js | 20 +- .../shared/type-change.service.js | 18 +- .../list/organizations-list.partial.html | 11 +- awx/ui/client/src/partials/breadcrumb.html | 4 +- awx/ui/client/src/partials/jobs.html | 8 +- .../portal-mode/portal-mode-jobs.partial.html | 6 +- .../src/setup-menu/setup-menu.partial.html | 36 +- awx/ui/client/src/shared/form-generator.js | 98 +- .../list-generator/list-generator.factory.js | 13 +- .../src/shared/socket/socket.service.js | 6 +- awx/ui/grunt-tasks/concurrent.js | 4 +- awx/ui/grunt-tasks/copy.js | 8 + awx/ui/grunt-tasks/nggettext_compile.js | 15 + awx/ui/grunt-tasks/nggettext_extract.js | 11 + awx/ui/package.json | 4 + awx/ui/po/ansible-tower.pot | 2041 +++++++++++++++++ awx/ui/webpack.config.js | 1 + 60 files changed, 3116 insertions(+), 805 deletions(-) create mode 100644 awx/ui/client/src/i18n.js create mode 100644 awx/ui/grunt-tasks/nggettext_compile.js create mode 100644 awx/ui/grunt-tasks/nggettext_extract.js create mode 100644 awx/ui/po/ansible-tower.pot diff --git a/Makefile b/Makefile index d74468a9e7..3112a2f4bd 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 @@ -506,6 +506,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: awx/ui/package.json check-po + $(NPM_BIN) --prefix awx/ui run languages + +# generate .pot +pot: awx/ui/package.json + $(NPM_BIN) --prefix awx/ui run pot + ui-deps: $(UI_DEPS_FLAG_FILE) $(UI_DEPS_FLAG_FILE): awx/ui/package.json @@ -518,7 +555,7 @@ ui-docker-machine: $(UI_DEPS_FLAG_FILE) ui-docker: $(UI_DEPS_FLAG_FILE) $(NPM_BIN) --prefix awx/ui run build-docker-cid -ui-release: $(UI_RELEASE_FLAG_FILE) +ui-release: languages $(UI_RELEASE_FLAG_FILE) $(UI_RELEASE_FLAG_FILE): $(UI_DEPS_FLAG_FILE) $(NPM_BIN) --prefix awx/ui run build-release diff --git a/awx/ui/Gruntfile.js b/awx/ui/Gruntfile.js index 57fddad6ed..16b61f0405 100644 --- a/awx/ui/Gruntfile.js +++ b/awx/ui/Gruntfile.js @@ -16,6 +16,7 @@ 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 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..4e4f5b0b92 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,12 +451,12 @@ 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) {
 
         $stateExtender.addState({
             name: 'dashboard',
@@ -466,7 +474,7 @@ var tower = angular.module('Tower', [
                 refreshButton: true
             },
             ncyBreadcrumb: {
-                label: "DASHBOARD"
+                label: N_("DASHBOARD")
             },
             resolve: {
                 graphData: ['$q', 'jobStatusGraphData', '$rootScope',
@@ -487,7 +495,7 @@ var tower = angular.module('Tower', [
             templateUrl: urlPrefix + 'partials/jobs.html',
             controller: JobsListController,
             ncyBreadcrumb: {
-                label: "JOBS"
+                label: N("JOBS")
             },
             params: {
                 search: {
@@ -512,7 +520,7 @@ var tower = angular.module('Tower', [
                 activityStreamTarget: 'project'
             },
             ncyBreadcrumb: {
-                label: "PROJECTS"
+                label: N("PROJECTS")
             },
             socket: {
                 "groups":{
@@ -528,7 +536,7 @@ var tower = angular.module('Tower', [
             controller: ProjectsAdd,
             ncyBreadcrumb: {
                 parent: "projects",
-                label: "CREATE PROJECT"
+                label: N("CREATE PROJECT")
             },
             socket: {
                 "groups":{
@@ -570,6 +578,8 @@ var tower = angular.module('Tower', [
             controller: OrganizationsAdd
         });
 
+        I18NInit();
+
         $rootScope.addPermission = function(scope) {
             $compile("")(scope);
         };
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 @@