From f932bf440d93176f52d960955540515621b7e0e0 Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Sat, 4 Feb 2017 12:04:24 -0800 Subject: [PATCH 01/27] added a simple "revert_to" method for testing form builder changes --- kpi/models/asset.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/kpi/models/asset.py b/kpi/models/asset.py index ba1e5bd342..6e94dc6b8e 100644 --- a/kpi/models/asset.py +++ b/kpi/models/asset.py @@ -482,6 +482,11 @@ def clone(self, version_uid=None): # not currently used, but this is how "to_clone_dict" should work Asset.objects.create(**self.to_clone_dict(version_uid)) + def revert_to(self, version_uid): + av = self.asset_versions.get(uid=version_uid) + self.content = av.version_content + self.save() + def _populate_report_styles(self): default = self.report_styles.get(DEFAULT_REPORTS_KEY, {}) specifieds = self.report_styles.get(SPECIFIC_REPORTS_KEY, {}) From 8756a790e92d381ee177e6291b1dd0e4a1b68e82 Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Sat, 4 Feb 2017 12:37:58 -0800 Subject: [PATCH 02/27] test asset.revert_to_version(...) method --- kpi/models/asset.py | 2 +- kpi/tests/test_assets.py | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/kpi/models/asset.py b/kpi/models/asset.py index 6e94dc6b8e..7243eddb69 100644 --- a/kpi/models/asset.py +++ b/kpi/models/asset.py @@ -482,7 +482,7 @@ def clone(self, version_uid=None): # not currently used, but this is how "to_clone_dict" should work Asset.objects.create(**self.to_clone_dict(version_uid)) - def revert_to(self, version_uid): + def revert_to_version(self, version_uid): av = self.asset_versions.get(uid=version_uid) self.content = av.version_content self.save() diff --git a/kpi/tests/test_assets.py b/kpi/tests/test_assets.py index adab9f30ee..699b5e00d9 100644 --- a/kpi/tests/test_assets.py +++ b/kpi/tests/test_assets.py @@ -2,7 +2,9 @@ # -*- coding: utf-8 -*- import re +import json from collections import OrderedDict +from copy import deepcopy from django.contrib.auth.models import User, AnonymousUser from django.core.exceptions import ValidationError @@ -60,7 +62,6 @@ def setUp(self): ]}, owner=self.user, asset_type='survey') self.sa = self.asset - class CreateAssetVersions(AssetsTestCase): def test_asset_with_versions(self): @@ -84,6 +85,42 @@ def _list_tag_names(): self.asset.tags.add('tag2') self.assertEqual(_list_tag_names(), ['tag1', 'tag2']) + def test_asset_can_be_reverted(self): + # TODO: figure out why kuids are changing + # note: this is fixed by calling `self.asset.save()` + # at the beginning of this method + _content = deepcopy(self.asset.content) + # _kuid1 = _content['survey'][0]['$kuid'] + _content_copy = deepcopy(_content) + # remove this next line when todo is fixed + self.asset._strip_kuids(_content_copy) + _c1 = json.dumps(_content_copy, sort_keys=True) + surv_l = len(_content['survey']) + self.assertEqual(surv_l, 2) + self.asset.content['survey'].append({ + 'type': 'integer', + 'label': 'Number' + }) + av1_uid = self.asset.asset_versions.all()[0].uid + self.asset.save() + aa = Asset.objects.get(uid=self.asset.uid) + surv_l_2 = len(aa.content['survey']) + self.assertEqual(surv_l_2, 3) + aa.revert_to_version(av1_uid) + + aa = Asset.objects.get(uid=self.asset.uid) + _content_copy2 = deepcopy(aa.content) + # remove this next line when todo is fixed + self.asset._strip_kuids(_content_copy2) + _c3 = json.dumps(_content_copy2, sort_keys=True) + # _kuid3 = aa.content['survey'][0]['$kuid'] + surv_l_3 = len(aa.content['survey']) + + # self.assertEqual(_kuid1, _kuid3) + self.assertEqual(surv_l_3, 2) + self.assertEqual(_c1, _c3) + + def test_asset_can_be_anonymous(self): anon_asset = Asset.objects.create(content=self.asset.content) self.assertEqual(anon_asset.owner, None) From c9d3e96ddd4d111db98df90fe19a790f4d2d9289 Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Sun, 5 Feb 2017 13:11:19 -0800 Subject: [PATCH 03/27] asset.clone() returns new asset --- kpi/models/asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kpi/models/asset.py b/kpi/models/asset.py index 7243eddb69..af8535b4c5 100644 --- a/kpi/models/asset.py +++ b/kpi/models/asset.py @@ -480,7 +480,7 @@ def to_clone_dict(self, version_uid=None): def clone(self, version_uid=None): # not currently used, but this is how "to_clone_dict" should work - Asset.objects.create(**self.to_clone_dict(version_uid)) + return Asset.objects.create(**self.to_clone_dict(version_uid)) def revert_to_version(self, version_uid): av = self.asset_versions.get(uid=version_uid) From 6a01e3576eaf61f58c3f2dd2a89e7896726ba8df Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Tue, 7 Feb 2017 14:14:59 -0800 Subject: [PATCH 04/27] refactoring form builder translations with thorough tests --- jsapp/xlform/src/model.survey.coffee | 20 ++++-- test/index.js | 1 + test/xlform/translations.tests.coffee | 100 ++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 test/xlform/translations.tests.coffee diff --git a/jsapp/xlform/src/model.survey.coffee b/jsapp/xlform/src/model.survey.coffee index c7423f626f..899c5d51f3 100644 --- a/jsapp/xlform/src/model.survey.coffee +++ b/jsapp/xlform/src/model.survey.coffee @@ -32,14 +32,14 @@ module.exports = do -> @choices = new $choices.ChoiceLists([], _parent: @) $inputParser.loadChoiceLists(options.choices || [], @choices) - @translations = options.translations or [null] - if @translations + if options.translations + @translations = options.translations # if 'preferred_translation' is set, then any reference to a null translation # is actually a reference to the preferred translation. if @translations.preferred_translation _pt_index = @translations.indexOf(@translations.preferred_translation) if _pt_index is -1 - throw new Error("Translation #{@translations.preferred_translation} " + throw new Error("Translation #{@translations.preferred_translation} " + "not found in list: #{@translations.join(', ')}") if not @translations.secondary_translation # the secondary_translation is the next available translation in the list @@ -56,6 +56,14 @@ module.exports = do -> @translations.preferred_translation = @translations[_null_index] _next_translation_index = (_null_index + 1) % @translations.length @translations.secondary_translation = @translations[_next_translation_index] + else + @translations = [null] + + if options['#null_translation'] + @null_translation = options['#null_translation'] + + @_preferred_translation = @translations[0] + @_secondary_translation = @translations[1] if options.survey if !$inputParser.hasBeenParsed(options) @@ -154,10 +162,8 @@ module.exports = do -> addlSheets = choices: new $choices.ChoiceLists() - - # pass interface parameters back to the server - if @translations and @translations.preferred_translation isnt null - obj['#null_translation'] = @translations.preferred_translation + if @null_translation + obj['#null_translation'] = @null_translation # in case we had to rename the null translation to "UNNAMED" in order # to "focus" the form builder on a named translation diff --git a/test/index.js b/test/index.js index f9f9795d71..2be7c4fb67 100644 --- a/test/index.js +++ b/test/index.js @@ -8,6 +8,7 @@ require('./xlform/csv.tests') require('./xlform/deserializer.tests') require('./xlform/group.tests') require('./xlform/inputParser.tests') +require('./xlform/translations.tests') // require('./xlform/integration.tests') require('./xlform/model.tests') require('./xlform/survey.tests') diff --git a/test/xlform/translations.tests.coffee b/test/xlform/translations.tests.coffee new file mode 100644 index 0000000000..b525ce553c --- /dev/null +++ b/test/xlform/translations.tests.coffee @@ -0,0 +1,100 @@ +{expect} = require('../helper/fauxChai') + +$inputParser = require("../../jsapp/xlform/src/model.inputParser") +$survey = require("../../jsapp/xlform/src/model.survey") + +describe " translations set proper values ", -> + process = (src)-> + parsed = $inputParser.parse(src) + new $survey.Survey(parsed) + + it 'example 0', -> + survey1 = process( + survey: [ + type: "text" + label: "VAL1", + name: "val1", + ] + ) + survey2 = process( + survey: [ + type: "text" + label: ["VAL1"], + name: "val1", + ] + translations: [null] + ) + + expect(survey1._preferred_translation).toEqual(null) + expect(survey1._secondary_translation).toEqual(undefined) + + expect(survey2._preferred_translation).toEqual(null) + expect(survey2._secondary_translation).toEqual(undefined) + + it 'does not have null_translation value when none set', -> + survey_json = process( + survey: [type: "text", label: ["VAL1"], name: "val1"] + translations: [null] + ).toJSON() + expect(survey_json['#null_translation']).toBeUndefined() + + it 'passes thru null_translation', -> + survey = process( + survey: [ + type: "text" + label: ["VAL1_NULL", "VAL2_L2"], + name: "val1", + ] + translations: [null, "L2"] + '#null_translation': 'XYZ' + ) + expect(survey.null_translation).toEqual('XYZ') + _json = survey.toJSON() + expect(_json['#null_translation']).toEqual('XYZ') + + it 'example 1', -> + survey = process( + survey: [ + type: "text" + label: ["VAL1_NULL", "VAL2_L2"], + name: "val1", + ] + translations: [null, "L2"] + ) + expect(survey._preferred_translation).toEqual(null) + expect(survey._secondary_translation).toEqual("L2") + r0 = survey.toJSON().survey[0] + expect(r0['label']).toBeDefined() + expect(r0['label::L2']).toBeDefined() + + it 'example 2', -> + survey = process( + survey: [ + type: "text" + label: ["VAL1_L1", "VAL2_L2"], + name: "val1", + ] + translations: ["L1", "L2"] + ) + expect(survey._preferred_translation).toEqual("L1") + expect(survey._secondary_translation).toEqual("L2") + r0 = survey.toJSON().survey[0] + expect(r0['label::L1']).toBeDefined() + expect(r0['label::L2']).toBeDefined() + expect(r0.label).toBeUndefined() + + it 'example 3', -> + survey = process( + survey: [ + type: "text" + label: ["VAL1_L2", "VAL2_NULL"], + name: "val1", + ] + translations: ["L2", null] + ) + expect(survey._preferred_translation).toEqual("L2") + expect(survey._secondary_translation).toEqual(null) + r0 = survey.toJSON().survey[0] + expect(r0['label::L2']).toBeDefined() + expect(r0['label']).toBeDefined() + From 88029ae8f22494cc5d789f1c6d6d8da369931c23 Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Tue, 7 Feb 2017 14:34:46 -0800 Subject: [PATCH 05/27] translations cont'd --- jsapp/xlform/src/model.inputParser.coffee | 15 ++++++++++----- test/xlform/translations.tests.coffee | 21 +++++++++++++++++---- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/jsapp/xlform/src/model.inputParser.coffee b/jsapp/xlform/src/model.inputParser.coffee index e09726098f..53c6a10f30 100644 --- a/jsapp/xlform/src/model.inputParser.coffee +++ b/jsapp/xlform/src/model.inputParser.coffee @@ -95,11 +95,13 @@ module.exports = do -> inputParser.parseArr = parseArr inputParser.parse = (o)-> - translations = o.translations or [null] - if translations and '#null_translation' of o - translations.preferred_translation = o['#null_translation'] - if o['#null_translation'] isnt null and null in translations - translations[translations.indexOf(null)] = 'UNNAMED' + translations = o.translations + if translations + if translations.indexOf(null) is -1 + o.null_translation = translations[0] + translations[0] = null + else + translations = [null] # sorts groups and repeats into groups and repeats (recreates the structure) if o.survey @@ -111,6 +113,9 @@ module.exports = do -> # settings is sometimes packaged as an array length=1 if o.settings and _.isArray(o.settings) and o.settings.length is 1 o.settings = o.settings[0] + + o.translations = translations + o inputParser.loadChoiceLists = (passedChoices, choices)-> diff --git a/test/xlform/translations.tests.coffee b/test/xlform/translations.tests.coffee index b525ce553c..ddd46fa792 100644 --- a/test/xlform/translations.tests.coffee +++ b/test/xlform/translations.tests.coffee @@ -76,12 +76,25 @@ describe " translations set proper values ", -> ] translations: ["L1", "L2"] ) - expect(survey._preferred_translation).toEqual("L1") + src = $inputParser.parse( + survey: [ + type: "text" + label: ["VAL1_L1", "VAL2_L2"], + name: "val1", + ] + translations: ["L1", "L2"] + ) + expect(src['null_translation']).toEqual("L1") + expect(src.translations[0]).toEqual(null) + + # expect(survey._preferred_translation).toEqual("L1") expect(survey._secondary_translation).toEqual("L2") - r0 = survey.toJSON().survey[0] - expect(r0['label::L1']).toBeDefined() + _sjson = survey.toJSON() + r0 = _sjson.survey[0] + expect(r0['label']).toBeDefined() expect(r0['label::L2']).toBeDefined() - expect(r0.label).toBeUndefined() + # expect(r0.label).toBeUndefined() + expect(_sjson['#null_translation']).toEqual('L1') it 'example 3', -> survey = process( From 3046e2f873572271cc53c25734fe3de77c624de9 Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Wed, 8 Feb 2017 02:41:06 -0800 Subject: [PATCH 06/27] more refactoring of how translations were handled --- jsapp/xlform/src/model.inputParser.coffee | 10 +++++++- jsapp/xlform/src/model.survey.coffee | 31 ++--------------------- test/xlform/translations.tests.coffee | 20 ++++++++++++--- 3 files changed, 28 insertions(+), 33 deletions(-) diff --git a/jsapp/xlform/src/model.inputParser.coffee b/jsapp/xlform/src/model.inputParser.coffee index 53c6a10f30..737bea8dc2 100644 --- a/jsapp/xlform/src/model.inputParser.coffee +++ b/jsapp/xlform/src/model.inputParser.coffee @@ -96,10 +96,18 @@ module.exports = do -> inputParser.parseArr = parseArr inputParser.parse = (o)-> translations = o.translations + if o['#null_translation'] + _existing_null_translation = o['#null_translation'] + delete o['#null_translation'] + if translations if translations.indexOf(null) is -1 - o.null_translation = translations[0] + if _existing_null_translation + throw new Error('null_translation set, but cannot be found') + o._null_translation = translations[0] translations[0] = null + else if _existing_null_translation + o._null_translation = _existing_null_translation else translations = [null] diff --git a/jsapp/xlform/src/model.survey.coffee b/jsapp/xlform/src/model.survey.coffee index 899c5d51f3..7316f9f5cd 100644 --- a/jsapp/xlform/src/model.survey.coffee +++ b/jsapp/xlform/src/model.survey.coffee @@ -34,33 +34,11 @@ module.exports = do -> if options.translations @translations = options.translations - # if 'preferred_translation' is set, then any reference to a null translation - # is actually a reference to the preferred translation. - if @translations.preferred_translation - _pt_index = @translations.indexOf(@translations.preferred_translation) - if _pt_index is -1 - throw new Error("Translation #{@translations.preferred_translation} " + - "not found in list: #{@translations.join(', ')}") - if not @translations.secondary_translation - # the secondary_translation is the next available translation in the list - @translations.secondary_translation = @translations[if _pt_index is 0 then 1 else 0] - else if -1 is @translations.indexOf(null) - # if no null translation exists, set the first two translations as the - # preferred and secondary - @translations.preferred_translation = @translations[0] - @translations.secondary_translation = @translations[1] - else - # if a null translation exists (e.g. with a column "label" in a survey with - # other "label::lang" columns) then it is the "preferred_translation" by default - _null_index = @translations.indexOf(null) - @translations.preferred_translation = @translations[_null_index] - _next_translation_index = (_null_index + 1) % @translations.length - @translations.secondary_translation = @translations[_next_translation_index] else @translations = [null] - if options['#null_translation'] - @null_translation = options['#null_translation'] + if options['_null_translation'] + @null_translation = options['_null_translation'] @_preferred_translation = @translations[0] @_secondary_translation = @translations[1] @@ -165,11 +143,6 @@ module.exports = do -> if @null_translation obj['#null_translation'] = @null_translation - # in case we had to rename the null translation to "UNNAMED" in order - # to "focus" the form builder on a named translation - if @translations and 'UNNAMED' in @translations - obj['#replace_with_null_translation'] = 'UNNAMED' - obj.translations = [].concat(@translations) obj.survey = do => diff --git a/test/xlform/translations.tests.coffee b/test/xlform/translations.tests.coffee index ddd46fa792..5003d26087 100644 --- a/test/xlform/translations.tests.coffee +++ b/test/xlform/translations.tests.coffee @@ -52,6 +52,22 @@ describe " translations set proper values ", -> _json = survey.toJSON() expect(_json['#null_translation']).toEqual('XYZ') + it 'fails with invalid null_translation', -> + run = -> + survey = process( + survey: [ + type: "text" + label: ["VAL1_NULL", "VAL2_L2"], + name: "val1", + ] + translations: ["L1", "L2"] + '#null_translation': 'XYZ' + ) + # "#null_translation" is set, but refers to a value in "translations" + # but in this case there is no null in the translations list so it should + # throw an error + expect(run).toThrow() + it 'example 1', -> survey = process( survey: [ @@ -84,16 +100,14 @@ describe " translations set proper values ", -> ] translations: ["L1", "L2"] ) - expect(src['null_translation']).toEqual("L1") + expect(src['_null_translation']).toEqual("L1") expect(src.translations[0]).toEqual(null) - # expect(survey._preferred_translation).toEqual("L1") expect(survey._secondary_translation).toEqual("L2") _sjson = survey.toJSON() r0 = _sjson.survey[0] expect(r0['label']).toBeDefined() expect(r0['label::L2']).toBeDefined() - # expect(r0.label).toBeUndefined() expect(_sjson['#null_translation']).toEqual('L1') it 'example 3', -> From 6b1d3eeed442812162838f90534c73f4ae11a4d8 Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Wed, 8 Feb 2017 17:42:53 -0800 Subject: [PATCH 07/27] adding es6-map polyfill as a dependency --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index d3d6654420..cdc60c5c4a 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "coffeeify": "^1.1.0", "css-loader": "^0.18.0", "diff": "^1.4.0", + "es6-map": "^0.1.4", "es6-promise": "^3.3.1", "eslint": "^1.3.1", "eslint-loader": "^1.0.0", From 42c5ac92411e0867ed7004da81fd97e6d967eadc Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Thu, 9 Feb 2017 19:41:16 -0800 Subject: [PATCH 08/27] Handling all types of translated xlsforms except those where: > survey_content.translations.indexOf(null) > 0 --- .../partials/form_builder/_card.scss | 10 +++++ jsapp/xlform/src/model.inputParser.coffee | 12 +++-- jsapp/xlform/src/model.row.coffee | 13 ++++++ jsapp/xlform/src/view.choices.coffee | 12 +++-- jsapp/xlform/src/view.row.coffee | 4 ++ jsapp/xlform/src/view.row.templates.coffee | 1 + .../src/view.surveyApp.templates.coffee | 4 +- test/xlform/translations.tests.coffee | 45 +++++++++++-------- 8 files changed, 70 insertions(+), 31 deletions(-) diff --git a/jsapp/scss/stylesheets/partials/form_builder/_card.scss b/jsapp/scss/stylesheets/partials/form_builder/_card.scss index e3f02ba3ec..2eb6e549af 100644 --- a/jsapp/scss/stylesheets/partials/form_builder/_card.scss +++ b/jsapp/scss/stylesheets/partials/form_builder/_card.scss @@ -65,6 +65,16 @@ cursor:text; // white-space: pre; } + .card__header-subtitle { + margin: 10px 0 0 0; + color: #aaa; + font-size: 12px; + + &:before { + content: '🌐 - '; + opacity: 0.5; + } + } &.card--shaded .card__header-title { opacity:0.3; } diff --git a/jsapp/xlform/src/model.inputParser.coffee b/jsapp/xlform/src/model.inputParser.coffee index 737bea8dc2..d93e4ef07d 100644 --- a/jsapp/xlform/src/model.inputParser.coffee +++ b/jsapp/xlform/src/model.inputParser.coffee @@ -33,7 +33,7 @@ module.exports = do -> delete item[key] _.map(translations, (_t, i)-> _translated_val = val[i] - if _t and _t isnt translations.preferred_translation + if _t lang_str = "#{key}::#{_t}" else lang_str = key @@ -101,12 +101,18 @@ module.exports = do -> delete o['#null_translation'] if translations - if translations.indexOf(null) is -1 + if translations.indexOf(null) is -1 # there is no unnamed translation if _existing_null_translation throw new Error('null_translation set, but cannot be found') o._null_translation = translations[0] translations[0] = null - else if _existing_null_translation + else if translations.indexOf(null) > 0 + throw new Error(""" + null_translation must be the first (primary) translation + translations need to be reordered or unnamed translation needs + to be given a name + """) + else if _existing_null_translation # there is already an active null translation o._null_translation = _existing_null_translation else translations = [null] diff --git a/jsapp/xlform/src/model.row.coffee b/jsapp/xlform/src/model.row.coffee index 441ab9c134..2ea115263a 100644 --- a/jsapp/xlform/src/model.row.coffee +++ b/jsapp/xlform/src/model.row.coffee @@ -398,6 +398,19 @@ module.exports = do -> return newRow + getTranslatedColumnKey: (col, whichone="primary")-> + if whichone is "secondary" + _t = @getSurvey()._secondary_translation + else + _t = @getSurvey()._preferred_translation + _key = "#{col}" + if _t isnt null + _key += "::#{_t}" + _key + + getLabel: (whichone="primary")-> + @getValue @getTranslatedColumnKey("label", whichone) + finalize: -> existing_name = @getValue("name") unless existing_name diff --git a/jsapp/xlform/src/view.choices.coffee b/jsapp/xlform/src/view.choices.coffee index be441c9f3f..6ae4662730 100644 --- a/jsapp/xlform/src/view.choices.coffee +++ b/jsapp/xlform/src/view.choices.coffee @@ -129,14 +129,12 @@ module.exports = do -> @d.append(@t) @d.append(@c) @$el.html(@d) - try - _tt = @model.getSurvey().getSurvey().translations - catch err - _tt = false - if _tt and _tt.secondary_translation - _t_opt = @model.get("label::#{_tt.secondary_translation}") - $("").html(""" + _survey = @model.getSurvey().getSurvey() + _secondary_translation = _survey._secondary_translation + if _secondary_translation isnt undefined + _t_opt = @model.get("label::#{_secondary_translation}") + $("", {className: 'secondary-translation'}).html(""" 🌐 -  #{_t_opt} """).appendTo(@$el) diff --git a/jsapp/xlform/src/view.row.coffee b/jsapp/xlform/src/view.row.coffee index e9650b10b4..4a2ea53fc3 100644 --- a/jsapp/xlform/src/view.row.coffee +++ b/jsapp/xlform/src/view.row.coffee @@ -65,6 +65,7 @@ module.exports = do -> _renderRow: -> @$el.html $viewTemplates.$$render('row.xlfRowView', @surveyView) @$label = @$('.card__header-title') + @$sub_label = @$('.card__header-subtitle') @$card = @$('.card') @$header = @$('.card__header') context = {warnings: []} @@ -75,6 +76,9 @@ module.exports = do -> @listView = new $viewChoices.ListView(model: cl, rowView: @).render() @cardSettingsWrap = @$('.card__settings').eq(0) + _second_translation = @surveyView.survey._secondary_translation + if _second_translation isnt undefined + @$sub_label.html(@model.getLabel('secondary')).show() @defaultRowDetailParent = @cardSettingsWrap.find('.card__settings__fields--question-options').eq(0) for [key, val] in @model.attributesArray() when key is 'label' or key is 'type' view = new $viewRowDetail.DetailView(model: val, rowView: @) diff --git a/jsapp/xlform/src/view.row.templates.coffee b/jsapp/xlform/src/view.row.templates.coffee index db8ae92b79..6238909789 100644 --- a/jsapp/xlform/src/view.row.templates.coffee +++ b/jsapp/xlform/src/view.row.templates.coffee @@ -62,6 +62,7 @@ module.exports = do ->
+
diff --git a/jsapp/xlform/src/view.surveyApp.templates.coffee b/jsapp/xlform/src/view.surveyApp.templates.coffee index be757e1758..42d1a58bb0 100644 --- a/jsapp/xlform/src/view.surveyApp.templates.coffee +++ b/jsapp/xlform/src/view.surveyApp.templates.coffee @@ -29,8 +29,8 @@ module.exports = do -> warnings_html += """

#{warning}

""" warnings_html += """
""" if survey.translations - t0 = survey.translations.preferred_translation - t1 = survey.translations.secondary_translation + t0 = survey._preferred_translation + t1 = survey._secondary_translation print_translation = (tx)-> if tx is null then "Unnamed translation" else tx translations_content = "#{print_translation(t0)}" if t1 diff --git a/test/xlform/translations.tests.coffee b/test/xlform/translations.tests.coffee index 5003d26087..899fce8848 100644 --- a/test/xlform/translations.tests.coffee +++ b/test/xlform/translations.tests.coffee @@ -79,9 +79,13 @@ describe " translations set proper values ", -> ) expect(survey._preferred_translation).toEqual(null) expect(survey._secondary_translation).toEqual("L2") - r0 = survey.toJSON().survey[0] - expect(r0['label']).toBeDefined() - expect(r0['label::L2']).toBeDefined() + r0 = survey.rows.at(0) + expect(r0.getLabel('primary')).toEqual('VAL1_NULL') + expect(r0.getLabel('secondary')).toEqual('VAL2_L2') + + rj0 = survey.toJSON().survey[0] + expect(rj0['label']).toBeDefined() + expect(rj0['label::L2']).toBeDefined() it 'example 2', -> survey = process( @@ -105,23 +109,26 @@ describe " translations set proper values ", -> expect(survey._secondary_translation).toEqual("L2") _sjson = survey.toJSON() - r0 = _sjson.survey[0] - expect(r0['label']).toBeDefined() - expect(r0['label::L2']).toBeDefined() + + r0 = survey.rows.at(0) + expect(r0.getLabel('primary')).toEqual('VAL1_L1') + expect(r0.getLabel('secondary')).toEqual('VAL2_L2') + + rj0 = _sjson.survey[0] + expect(rj0['label']).toBeDefined() + expect(rj0['label::L2']).toBeDefined() expect(_sjson['#null_translation']).toEqual('L1') it 'example 3', -> - survey = process( - survey: [ - type: "text" - label: ["VAL1_L2", "VAL2_NULL"], - name: "val1", - ] - translations: ["L2", null] - ) - expect(survey._preferred_translation).toEqual("L2") - expect(survey._secondary_translation).toEqual(null) - r0 = survey.toJSON().survey[0] - expect(r0['label::L2']).toBeDefined() - expect(r0['label']).toBeDefined() + run = -> + survey = process( + survey: [ + type: "text" + label: ["VAL1_L2", "VAL2_NULL"], + name: "val1", + ] + translations: ["L2", null] + ) + # run() + expect(run).toThrow('translations need to be reordered') From db3fcd800a836f85ec1ab3d551ca40f15c4ffe52 Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Fri, 10 Feb 2017 14:46:33 -0800 Subject: [PATCH 09/27] renaming "null_translation" to "active_translation_name" which describes the value more clearly --- jsapp/xlform/src/model.inputParser.coffee | 18 +++++++++--------- jsapp/xlform/src/model.survey.coffee | 8 ++++---- kpi/models/asset.py | 2 +- test/xlform/translations.tests.coffee | 22 +++++++++++----------- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/jsapp/xlform/src/model.inputParser.coffee b/jsapp/xlform/src/model.inputParser.coffee index d93e4ef07d..4571e0df27 100644 --- a/jsapp/xlform/src/model.inputParser.coffee +++ b/jsapp/xlform/src/model.inputParser.coffee @@ -96,24 +96,24 @@ module.exports = do -> inputParser.parseArr = parseArr inputParser.parse = (o)-> translations = o.translations - if o['#null_translation'] - _existing_null_translation = o['#null_translation'] - delete o['#null_translation'] + if o['#active_translation_name'] + _existing_active_translation_name = o['#active_translation_name'] + delete o['#active_translation_name'] if translations if translations.indexOf(null) is -1 # there is no unnamed translation - if _existing_null_translation - throw new Error('null_translation set, but cannot be found') - o._null_translation = translations[0] + if _existing_active_translation_name + throw new Error('active translation set, but cannot be found') + o._active_translation_name = translations[0] translations[0] = null else if translations.indexOf(null) > 0 throw new Error(""" - null_translation must be the first (primary) translation + unnamed translation must be the first (primary) translation translations need to be reordered or unnamed translation needs to be given a name """) - else if _existing_null_translation # there is already an active null translation - o._null_translation = _existing_null_translation + else if _existing_active_translation_name # there is already an active null translation + o._active_translation_name = _existing_active_translation_name else translations = [null] diff --git a/jsapp/xlform/src/model.survey.coffee b/jsapp/xlform/src/model.survey.coffee index 7316f9f5cd..956c15eb1e 100644 --- a/jsapp/xlform/src/model.survey.coffee +++ b/jsapp/xlform/src/model.survey.coffee @@ -37,8 +37,8 @@ module.exports = do -> else @translations = [null] - if options['_null_translation'] - @null_translation = options['_null_translation'] + if options['_active_translation_name'] + @active_translation_name = options['_active_translation_name'] @_preferred_translation = @translations[0] @_secondary_translation = @translations[1] @@ -140,8 +140,8 @@ module.exports = do -> addlSheets = choices: new $choices.ChoiceLists() - if @null_translation - obj['#null_translation'] = @null_translation + if @active_translation_name + obj['#active_translation_name'] = @active_translation_name obj.translations = [].concat(@translations) diff --git a/kpi/models/asset.py b/kpi/models/asset.py index af8535b4c5..136531d8da 100644 --- a/kpi/models/asset.py +++ b/kpi/models/asset.py @@ -398,7 +398,7 @@ def adjust_content_on_save(self): # the interface focuses on the "null translation" and shows other ones # in advanced settings, we allow the builder to attach a parameter # which says what to name the null translation. - _null_translation_as = self.content.pop('#null_translation', None) + _null_translation_as = self.content.pop('#active_translation_name', None) if _null_translation_as: self._rename_translation(self.content, None, _null_translation_as) diff --git a/test/xlform/translations.tests.coffee b/test/xlform/translations.tests.coffee index 899fce8848..120bbde71d 100644 --- a/test/xlform/translations.tests.coffee +++ b/test/xlform/translations.tests.coffee @@ -31,14 +31,14 @@ describe " translations set proper values ", -> expect(survey2._preferred_translation).toEqual(null) expect(survey2._secondary_translation).toEqual(undefined) - it 'does not have null_translation value when none set', -> + it 'does not have active_translation_name value when none set', -> survey_json = process( survey: [type: "text", label: ["VAL1"], name: "val1"] translations: [null] ).toJSON() - expect(survey_json['#null_translation']).toBeUndefined() + expect(survey_json['#active_translation_name']).toBeUndefined() - it 'passes thru null_translation', -> + it 'passes thru active_translation_name', -> survey = process( survey: [ type: "text" @@ -46,13 +46,13 @@ describe " translations set proper values ", -> name: "val1", ] translations: [null, "L2"] - '#null_translation': 'XYZ' + '#active_translation_name': 'XYZ' ) - expect(survey.null_translation).toEqual('XYZ') + expect(survey.active_translation_name).toEqual('XYZ') _json = survey.toJSON() - expect(_json['#null_translation']).toEqual('XYZ') + expect(_json['#active_translation_name']).toEqual('XYZ') - it 'fails with invalid null_translation', -> + it 'fails with invalid active_translation_name', -> run = -> survey = process( survey: [ @@ -61,9 +61,9 @@ describe " translations set proper values ", -> name: "val1", ] translations: ["L1", "L2"] - '#null_translation': 'XYZ' + '#active_translation_name': 'XYZ' ) - # "#null_translation" is set, but refers to a value in "translations" + # "#active_translation_name" is set, but refers to a value in "translations" # but in this case there is no null in the translations list so it should # throw an error expect(run).toThrow() @@ -104,7 +104,7 @@ describe " translations set proper values ", -> ] translations: ["L1", "L2"] ) - expect(src['_null_translation']).toEqual("L1") + expect(src['_active_translation_name']).toEqual("L1") expect(src.translations[0]).toEqual(null) expect(survey._secondary_translation).toEqual("L2") @@ -117,7 +117,7 @@ describe " translations set proper values ", -> rj0 = _sjson.survey[0] expect(rj0['label']).toBeDefined() expect(rj0['label::L2']).toBeDefined() - expect(_sjson['#null_translation']).toEqual('L1') + expect(_sjson['#active_translation_name']).toEqual('L1') it 'example 3', -> run = -> From 4737c52169de59011c8aa5e9bd52df8fe13a3200 Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Fri, 10 Feb 2017 14:54:34 -0800 Subject: [PATCH 10/27] renamed "_preferred_translation" and "_secondary_translation" variables --- jsapp/xlform/src/model.row.coffee | 4 ++-- jsapp/xlform/src/model.survey.coffee | 4 ++-- jsapp/xlform/src/view.choices.coffee | 6 +++--- jsapp/xlform/src/view.row.coffee | 2 +- jsapp/xlform/src/view.surveyApp.templates.coffee | 4 ++-- test/xlform/translations.tests.coffee | 14 +++++++------- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/jsapp/xlform/src/model.row.coffee b/jsapp/xlform/src/model.row.coffee index 2ea115263a..b910afb7bb 100644 --- a/jsapp/xlform/src/model.row.coffee +++ b/jsapp/xlform/src/model.row.coffee @@ -400,9 +400,9 @@ module.exports = do -> getTranslatedColumnKey: (col, whichone="primary")-> if whichone is "secondary" - _t = @getSurvey()._secondary_translation + _t = @getSurvey()._translation_2 else - _t = @getSurvey()._preferred_translation + _t = @getSurvey()._translation_1 _key = "#{col}" if _t isnt null _key += "::#{_t}" diff --git a/jsapp/xlform/src/model.survey.coffee b/jsapp/xlform/src/model.survey.coffee index 956c15eb1e..828b39b994 100644 --- a/jsapp/xlform/src/model.survey.coffee +++ b/jsapp/xlform/src/model.survey.coffee @@ -40,8 +40,8 @@ module.exports = do -> if options['_active_translation_name'] @active_translation_name = options['_active_translation_name'] - @_preferred_translation = @translations[0] - @_secondary_translation = @translations[1] + @_translation_1 = @translations[0] + @_translation_2 = @translations[1] if options.survey if !$inputParser.hasBeenParsed(options) diff --git a/jsapp/xlform/src/view.choices.coffee b/jsapp/xlform/src/view.choices.coffee index 6ae4662730..84a2af3b46 100644 --- a/jsapp/xlform/src/view.choices.coffee +++ b/jsapp/xlform/src/view.choices.coffee @@ -131,9 +131,9 @@ module.exports = do -> @$el.html(@d) _survey = @model.getSurvey().getSurvey() - _secondary_translation = _survey._secondary_translation - if _secondary_translation isnt undefined - _t_opt = @model.get("label::#{_secondary_translation}") + _translation_2 = _survey._translation_2 + if _translation_2 isnt undefined + _t_opt = @model.get("label::#{_translation_2}") $("", {className: 'secondary-translation'}).html(""" 🌐 -  #{_t_opt} diff --git a/jsapp/xlform/src/view.row.coffee b/jsapp/xlform/src/view.row.coffee index 4a2ea53fc3..19b3ef8a86 100644 --- a/jsapp/xlform/src/view.row.coffee +++ b/jsapp/xlform/src/view.row.coffee @@ -76,7 +76,7 @@ module.exports = do -> @listView = new $viewChoices.ListView(model: cl, rowView: @).render() @cardSettingsWrap = @$('.card__settings').eq(0) - _second_translation = @surveyView.survey._secondary_translation + _second_translation = @surveyView.survey._translation_2 if _second_translation isnt undefined @$sub_label.html(@model.getLabel('secondary')).show() @defaultRowDetailParent = @cardSettingsWrap.find('.card__settings__fields--question-options').eq(0) diff --git a/jsapp/xlform/src/view.surveyApp.templates.coffee b/jsapp/xlform/src/view.surveyApp.templates.coffee index 42d1a58bb0..e65245b99a 100644 --- a/jsapp/xlform/src/view.surveyApp.templates.coffee +++ b/jsapp/xlform/src/view.surveyApp.templates.coffee @@ -29,8 +29,8 @@ module.exports = do -> warnings_html += """

#{warning}

""" warnings_html += """""" if survey.translations - t0 = survey._preferred_translation - t1 = survey._secondary_translation + t0 = survey._translation_1 + t1 = survey._translation_2 print_translation = (tx)-> if tx is null then "Unnamed translation" else tx translations_content = "#{print_translation(t0)}" if t1 diff --git a/test/xlform/translations.tests.coffee b/test/xlform/translations.tests.coffee index 120bbde71d..aa9356daa2 100644 --- a/test/xlform/translations.tests.coffee +++ b/test/xlform/translations.tests.coffee @@ -25,11 +25,11 @@ describe " translations set proper values ", -> translations: [null] ) - expect(survey1._preferred_translation).toEqual(null) - expect(survey1._secondary_translation).toEqual(undefined) + expect(survey1._translation_1).toEqual(null) + expect(survey1._translation_2).toEqual(undefined) - expect(survey2._preferred_translation).toEqual(null) - expect(survey2._secondary_translation).toEqual(undefined) + expect(survey2._translation_1).toEqual(null) + expect(survey2._translation_2).toEqual(undefined) it 'does not have active_translation_name value when none set', -> survey_json = process( @@ -77,8 +77,8 @@ describe " translations set proper values ", -> ] translations: [null, "L2"] ) - expect(survey._preferred_translation).toEqual(null) - expect(survey._secondary_translation).toEqual("L2") + expect(survey._translation_1).toEqual(null) + expect(survey._translation_2).toEqual("L2") r0 = survey.rows.at(0) expect(r0.getLabel('primary')).toEqual('VAL1_NULL') expect(r0.getLabel('secondary')).toEqual('VAL2_L2') @@ -107,7 +107,7 @@ describe " translations set proper values ", -> expect(src['_active_translation_name']).toEqual("L1") expect(src.translations[0]).toEqual(null) - expect(survey._secondary_translation).toEqual("L2") + expect(survey._translation_2).toEqual("L2") _sjson = survey.toJSON() r0 = survey.rows.at(0) From 9d35c701834b5012fe93844ee71057cbcb66f81d Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Fri, 10 Feb 2017 15:45:48 -0800 Subject: [PATCH 11/27] changed `primary_translation` and `secondary_translation` to `_1` and `_2` --- jsapp/xlform/src/model.row.coffee | 2 +- jsapp/xlform/src/view.row.coffee | 2 +- test/xlform/translations.tests.coffee | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/jsapp/xlform/src/model.row.coffee b/jsapp/xlform/src/model.row.coffee index b910afb7bb..97745058ae 100644 --- a/jsapp/xlform/src/model.row.coffee +++ b/jsapp/xlform/src/model.row.coffee @@ -399,7 +399,7 @@ module.exports = do -> return newRow getTranslatedColumnKey: (col, whichone="primary")-> - if whichone is "secondary" + if whichone is "_2" _t = @getSurvey()._translation_2 else _t = @getSurvey()._translation_1 diff --git a/jsapp/xlform/src/view.row.coffee b/jsapp/xlform/src/view.row.coffee index 19b3ef8a86..5fe29183cd 100644 --- a/jsapp/xlform/src/view.row.coffee +++ b/jsapp/xlform/src/view.row.coffee @@ -78,7 +78,7 @@ module.exports = do -> @cardSettingsWrap = @$('.card__settings').eq(0) _second_translation = @surveyView.survey._translation_2 if _second_translation isnt undefined - @$sub_label.html(@model.getLabel('secondary')).show() + @$sub_label.html(@model.getLabel('_2')).show() @defaultRowDetailParent = @cardSettingsWrap.find('.card__settings__fields--question-options').eq(0) for [key, val] in @model.attributesArray() when key is 'label' or key is 'type' view = new $viewRowDetail.DetailView(model: val, rowView: @) diff --git a/test/xlform/translations.tests.coffee b/test/xlform/translations.tests.coffee index aa9356daa2..84d071b7a5 100644 --- a/test/xlform/translations.tests.coffee +++ b/test/xlform/translations.tests.coffee @@ -80,8 +80,8 @@ describe " translations set proper values ", -> expect(survey._translation_1).toEqual(null) expect(survey._translation_2).toEqual("L2") r0 = survey.rows.at(0) - expect(r0.getLabel('primary')).toEqual('VAL1_NULL') - expect(r0.getLabel('secondary')).toEqual('VAL2_L2') + expect(r0.getLabel('_1')).toEqual('VAL1_NULL') + expect(r0.getLabel('_2')).toEqual('VAL2_L2') rj0 = survey.toJSON().survey[0] expect(rj0['label']).toBeDefined() @@ -111,8 +111,8 @@ describe " translations set proper values ", -> _sjson = survey.toJSON() r0 = survey.rows.at(0) - expect(r0.getLabel('primary')).toEqual('VAL1_L1') - expect(r0.getLabel('secondary')).toEqual('VAL2_L2') + expect(r0.getLabel('_1')).toEqual('VAL1_L1') + expect(r0.getLabel('_2')).toEqual('VAL2_L2') rj0 = _sjson.survey[0] expect(rj0['label']).toBeDefined() From 0fd9e91415da2f320560b279d869820874f1a46c Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Sat, 11 Feb 2017 18:57:44 -0800 Subject: [PATCH 12/27] asset_translation_utils --- kpi/models/asset.py | 24 +++++++++ kpi/serializers.py | 51 ++++++++++++++++++ kpi/tests/test_asset_translation_utils.py | 59 +++++++++++++++++++++ kpi/utils/asset_translation_utils.py | 64 +++++++++++++++++++++++ 4 files changed, 198 insertions(+) create mode 100644 kpi/tests/test_asset_translation_utils.py create mode 100644 kpi/utils/asset_translation_utils.py diff --git a/kpi/models/asset.py b/kpi/models/asset.py index 136531d8da..98d5384577 100644 --- a/kpi/models/asset.py +++ b/kpi/models/asset.py @@ -235,6 +235,30 @@ def _rename_null_translation(self, content, new_name): def _has_translations(self, content, min_count=1): return len(content.get('translations', [])) >= min_count + def _prioritize_translation(self, content, translation_name): + _content = self.content + _translations = _content.get('translations') + if translation_name not in _translations: + return + _tindex = _translations.index(translation_name) + _translated = _content.get('translated', []) + if _tindex > 0: + for row in _content.get('survey'): + for col in _translated: + _translated_cells = row[col] + _translated_cells.insert(0, _translated_cells.pop(_tindex)) + for row in _content.get('choices'): + for col in _translated: + _translated_cells = row[col] + _translated_cells.insert(0, _translated_cells.pop(_tindex)) + _translations.insert(0, _translations.pop(_tindex)) + + def _reorder_translations(self, content, translations): + _ts = translations[:] + _ts.reverse() + for _tname in _ts: + self._prioritize_translation(content, _tname) + def _rename_translation(self, content, _from, _to): _ts = self.content.get('translations') if _to in _ts: diff --git a/kpi/serializers.py b/kpi/serializers.py index 0cf9066adf..5d80018682 100644 --- a/kpi/serializers.py +++ b/kpi/serializers.py @@ -30,6 +30,18 @@ from .forms import USERNAME_REGEX, USERNAME_MAX_LENGTH from .forms import USERNAME_INVALID_MESSAGE from .utils.gravatar_url import gravatar_url +from .utils.asset_translation_utils import ( + compare_translations, + # TRANSLATIONS_EQUAL, + TRANSLATIONS_OUT_OF_ORDER, + TRANSLATION_RENAMED, + TRANSLATION_DELETED, + TRANSLATION_ADDED, + TRANSLATION_CHANGE_UNSUPPORTED, + TRANSLATIONS_MULTIPLE_CHANGES, + ) + + from .deployment_backends.kc_reader.utils import get_kc_profile_data from .deployment_backends.kc_reader.utils import set_kc_require_auth @@ -445,6 +457,45 @@ class Meta: }, } + def update(self, asset, validated_data): + asset_content = asset.content + _req_data = self.context['request'].data + _has_translations = 'translations' in _req_data + _has_content = 'content' in _req_data + if _has_translations and not _has_content: + # if 'translations' in content and 'survey' not in content: + existing_ts = asset_content.get('translations', []) + suggested_ts = json.loads(_req_data['translations']) + params = compare_translations( + existing_ts, + suggested_ts) + if TRANSLATIONS_OUT_OF_ORDER in params: + asset._reorder_translations(asset_content, suggested_ts) + elif TRANSLATION_RENAMED in params: + _change = params[TRANSLATION_RENAMED]['changes'][0] + asset._rename_translation(asset_content, _change['from'], + _change['to']) + elif TRANSLATIONS_MULTIPLE_CHANGES in params: + raise serializers.ValidationError( + 'Too many translation changes: {} > 1'.format( + params['diff_count']) + ) + else: + for chg in [ + TRANSLATION_CHANGE_UNSUPPORTED, + TRANSLATION_DELETED, + TRANSLATION_ADDED, + ]: + if chg in params: + raise serializers.ValidationError( + 'Unsupported change: "{}": {}'.format( + chg, + params[chg] + ) + ) + validated_data['content'] = asset_content + return super(AssetSerializer, self).update(asset, validated_data) + def get_fields(self, *args, **kwargs): fields = super(AssetSerializer, self).get_fields(*args, **kwargs) user = self.context['request'].user diff --git a/kpi/tests/test_asset_translation_utils.py b/kpi/tests/test_asset_translation_utils.py new file mode 100644 index 0000000000..0006180f8d --- /dev/null +++ b/kpi/tests/test_asset_translation_utils.py @@ -0,0 +1,59 @@ +from django.test import TestCase + + +from kpi.utils.asset_translation_utils import ( + compare_translations, + TRANSLATIONS_EQUAL, + TRANSLATIONS_OUT_OF_ORDER, + TRANSLATIONS_MULTIPLE_CHANGES, + TRANSLATION_RENAMED, + ) + + +class AssetTranslationTests(TestCase): + def test_equality(self): + self.assertTrue(TRANSLATIONS_EQUAL in + compare_translations( + ['a', 'b', 'c'], + ['a', 'b', 'c'], + ), + ) + self.assertTrue(TRANSLATIONS_EQUAL in + compare_translations( + [None, 'a', 'b', 'c'], + [None, 'a', 'b', 'c'], + ), + ) + + def test_out_of_order(self): + self.assertTrue(TRANSLATIONS_OUT_OF_ORDER in + compare_translations( + [None, 'a', 'b', 'c'], + ['a', 'b', 'c', None], + ), + ) + self.assertTrue(TRANSLATIONS_OUT_OF_ORDER in + compare_translations( + [None, 'a', 'b', 'c'], + ['a', None, 'b', 'c'], + ), + ) + + def test_translation_renamed(self): + _renamed_params = compare_translations( + [None, 'a', 'b', 'c'], + [None, 'a', 'b', 'Z'], + ) + + self.assertTrue(TRANSLATION_RENAMED in _renamed_params) + first_change = _renamed_params[TRANSLATION_RENAMED]['changes'][0] + self.assertEqual(first_change['index'], 3) + self.assertEqual(first_change['from'], 'c') + self.assertEqual(first_change['to'], 'Z') + + def test_translations_too_changed(self): + _params = compare_translations( + [None, 'a', 'b', 'c'], + [None, 'a', 'y', 'z'], + ) + self.assertTrue(TRANSLATIONS_MULTIPLE_CHANGES in _params) diff --git a/kpi/utils/asset_translation_utils.py b/kpi/utils/asset_translation_utils.py new file mode 100644 index 0000000000..3467f26abc --- /dev/null +++ b/kpi/utils/asset_translation_utils.py @@ -0,0 +1,64 @@ +import json + +TRANSLATIONS_EQUAL = 'equal' +TRANSLATIONS_OUT_OF_ORDER = 'out_of_order' +TRANSLATION_RENAMED = 'translation_renamed' +TRANSLATION_ADDED = 'translation_added' +TRANSLATION_CHANGE_UNSUPPORTED = 'translation_change_unsupported' +TRANSLATION_DELETED = 'translation_deleted' +TRANSLATIONS_MULTIPLE_CHANGES = 'multiple_changes' + + +def _track_changes(t1, t2): + _num = 0 + params = { + 'diff_count': 0, + 'changes': [] + } + for (i, vs) in enumerate(zip(t1, t2)): + (v1, v2) = vs + if v1 != v2: + params['changes'].append({ + 'index': i, + 'from': v1, + 'to': v2 + }) + params['diff_count'] += 1 + _num += 1 + return params + + +def compare_translations(t1, t2): + if json.dumps(t1) == json.dumps(t2): + return { + TRANSLATIONS_EQUAL: True, + } + if json.dumps(sorted(t1)) == json.dumps(sorted(t2)): + return { + TRANSLATIONS_OUT_OF_ORDER: True, + } + if len(t1) == len(t2): + params = _track_changes(t1, t2) + if params['diff_count'] == 1: + return { + TRANSLATION_RENAMED: params + } + elif params['diff_count'] > 1: + return { + TRANSLATIONS_MULTIPLE_CHANGES: params + } + if len(t1) == len(t2) - 1: + _added = list(set(t2) - set(t1)) + if len(_added) == 1: + return { + TRANSLATION_ADDED: _added[0], + } + elif len(t1) == len(t2) + 1: + _removed = list(set(t1) - set(t2)) + if len(_removed) == 1: + return { + TRANSLATION_DELETED: _removed[0] + } + return { + TRANSLATION_CHANGE_UNSUPPORTED: True + } From 6bf724b9cb1004db6cde6d1865e71502c569db8b Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Sun, 12 Feb 2017 21:38:17 -0800 Subject: [PATCH 13/27] restructured asset_translation_utils and asset helper methods to allow adding and removing a translation. --- kpi/models/asset.py | 97 +++++++++++++++++++++++----- kpi/serializers.py | 48 ++------------ kpi/utils/asset_translation_utils.py | 24 +++---- 3 files changed, 101 insertions(+), 68 deletions(-) diff --git a/kpi/models/asset.py b/kpi/models/asset.py index 98d5384577..feb986d7b5 100644 --- a/kpi/models/asset.py +++ b/kpi/models/asset.py @@ -38,6 +38,16 @@ expand_rank_and_score_in_place, replace_with_autofields, remove_empty_expressions_in_place) +from ..utils.asset_translation_utils import ( + compare_translations, + # TRANSLATIONS_EQUAL, + TRANSLATIONS_OUT_OF_ORDER, + TRANSLATION_RENAMED, + TRANSLATION_DELETED, + TRANSLATION_ADDED, + TRANSLATION_CHANGE_UNSUPPORTED, + TRANSLATIONS_MULTIPLE_CHANGES, + ) from ..utils.random_id import random_id from ..deployment_backends.mixin import DeployableMixin from kobo.apps.reports.constants import (SPECIFIC_REPORTS_KEY, @@ -235,23 +245,80 @@ def _rename_null_translation(self, content, new_name): def _has_translations(self, content, min_count=1): return len(content.get('translations', [])) >= min_count - def _prioritize_translation(self, content, translation_name): - _content = self.content - _translations = _content.get('translations') - if translation_name not in _translations: - return - _tindex = _translations.index(translation_name) - _translated = _content.get('translated', []) - if _tindex > 0: - for row in _content.get('survey'): + def update_translation_list(self, translation_list): + existing_ts = self.content.get('translations', []) + params = compare_translations(existing_ts, + translation_list) + if None in translation_list and translation_list[0] is not None: + raise ValueError('Unnamed translation must be first in ' + 'list of translations') + if TRANSLATIONS_OUT_OF_ORDER in params: + self._reorder_translations(self.content, translation_list) + elif TRANSLATION_RENAMED in params: + _change = params[TRANSLATION_RENAMED]['changes'][0] + self._rename_translation(self.content, _change['from'], + _change['to']) + elif TRANSLATION_ADDED in params: + if None in existing_ts: + raise ValueError('cannot add translation if an unnamed translation exists') + self._prepend_translation(self.content, params[TRANSLATION_ADDED]) + elif TRANSLATION_DELETED in params: + if params[TRANSLATION_DELETED] != existing_ts[-1]: + raise ValueError('you can only delete the last translation of the asset') + self._remove_last_translation(self.content) + else: + for chg in [ + TRANSLATIONS_MULTIPLE_CHANGES, + TRANSLATION_CHANGE_UNSUPPORTED, + ]: + if chg in params: + raise ValueError( + 'Unsupported change: "{}": {}'.format( + chg, + params[chg] + ) + ) + + def _prioritize_translation(self, content, translation_name, is_new=False): + _translations = content.get('translations') + _translated = content.get('translated', []) + if is_new and (translation_name in _translations): + raise ValueError('cannot add existing translation') + elif (not is_new) and (translation_name not in _translations): + raise ValueError('translation cannot be found') + _tindex = -1 if is_new else _translations.index(translation_name) + if is_new or (_tindex > 0): + for row in content.get('survey'): for col in _translated: - _translated_cells = row[col] - _translated_cells.insert(0, _translated_cells.pop(_tindex)) - for row in _content.get('choices'): + if is_new: + val = '[{}] {}'.format(translation_name, row[col][0]) + else: + val = row[col].pop(_tindex) + row[col].insert(0, val) + for row in content.get('choices'): for col in _translated: - _translated_cells = row[col] - _translated_cells.insert(0, _translated_cells.pop(_tindex)) - _translations.insert(0, _translations.pop(_tindex)) + if is_new: + val = '[{}] {}'.format(translation_name, row[col][0]) + else: + val = row[col].pop(_tindex) + row[col].insert(0, val) + if is_new: + _translations.insert(0, translation_name) + else: + _translations.insert(0, _translations.pop(_tindex)) + + def _remove_last_translation(self, content): + content.get('translations').pop() + _translated = content.get('translated', []) + for row in content.get('survey'): + for col in _translated: + row[col].pop() + for row in content.get('choices'): + for col in _translated: + row[col].pop() + + def _prepend_translation(self, content, translation_name): + self._prioritize_translation(content, translation_name, is_new=True) def _reorder_translations(self, content, translations): _ts = translations[:] diff --git a/kpi/serializers.py b/kpi/serializers.py index 5d80018682..6c6cd6d862 100644 --- a/kpi/serializers.py +++ b/kpi/serializers.py @@ -30,17 +30,6 @@ from .forms import USERNAME_REGEX, USERNAME_MAX_LENGTH from .forms import USERNAME_INVALID_MESSAGE from .utils.gravatar_url import gravatar_url -from .utils.asset_translation_utils import ( - compare_translations, - # TRANSLATIONS_EQUAL, - TRANSLATIONS_OUT_OF_ORDER, - TRANSLATION_RENAMED, - TRANSLATION_DELETED, - TRANSLATION_ADDED, - TRANSLATION_CHANGE_UNSUPPORTED, - TRANSLATIONS_MULTIPLE_CHANGES, - ) - from .deployment_backends.kc_reader.utils import get_kc_profile_data from .deployment_backends.kc_reader.utils import set_kc_require_auth @@ -60,7 +49,7 @@ class WritableJSONField(serializers.Field): """ Serializer for JSONField -- required to make field writable""" def __init__(self, **kwargs): - self.allow_blank= kwargs.pop('allow_blank', False) + self.allow_blank = kwargs.pop('allow_blank', False) super(WritableJSONField, self).__init__(**kwargs) def to_internal_value(self, data): @@ -463,36 +452,11 @@ def update(self, asset, validated_data): _has_translations = 'translations' in _req_data _has_content = 'content' in _req_data if _has_translations and not _has_content: - # if 'translations' in content and 'survey' not in content: - existing_ts = asset_content.get('translations', []) - suggested_ts = json.loads(_req_data['translations']) - params = compare_translations( - existing_ts, - suggested_ts) - if TRANSLATIONS_OUT_OF_ORDER in params: - asset._reorder_translations(asset_content, suggested_ts) - elif TRANSLATION_RENAMED in params: - _change = params[TRANSLATION_RENAMED]['changes'][0] - asset._rename_translation(asset_content, _change['from'], - _change['to']) - elif TRANSLATIONS_MULTIPLE_CHANGES in params: - raise serializers.ValidationError( - 'Too many translation changes: {} > 1'.format( - params['diff_count']) - ) - else: - for chg in [ - TRANSLATION_CHANGE_UNSUPPORTED, - TRANSLATION_DELETED, - TRANSLATION_ADDED, - ]: - if chg in params: - raise serializers.ValidationError( - 'Unsupported change: "{}": {}'.format( - chg, - params[chg] - ) - ) + translations_list = json.loads(_req_data['translations']) + try: + asset.update_translation_list(translations_list) + except ValueError as err: + raise serializers.ValidationError(err.message) validated_data['content'] = asset_content return super(AssetSerializer, self).update(asset, validated_data) diff --git a/kpi/utils/asset_translation_utils.py b/kpi/utils/asset_translation_utils.py index 3467f26abc..333911cd12 100644 --- a/kpi/utils/asset_translation_utils.py +++ b/kpi/utils/asset_translation_utils.py @@ -29,31 +29,33 @@ def _track_changes(t1, t2): def compare_translations(t1, t2): - if json.dumps(t1) == json.dumps(t2): - return { - TRANSLATIONS_EQUAL: True, - } - if json.dumps(sorted(t1)) == json.dumps(sorted(t2)): - return { - TRANSLATIONS_OUT_OF_ORDER: True, - } + _s1 = set(t1) + _s2 = set(t2) if len(t1) == len(t2): params = _track_changes(t1, t2) + if params['diff_count'] == 0: + return { + TRANSLATIONS_EQUAL: True, + } + if _s1 == _s2: + return { + TRANSLATIONS_OUT_OF_ORDER: True, + } if params['diff_count'] == 1: return { TRANSLATION_RENAMED: params } - elif params['diff_count'] > 1: + if params['diff_count'] > 1: return { TRANSLATIONS_MULTIPLE_CHANGES: params } if len(t1) == len(t2) - 1: - _added = list(set(t2) - set(t1)) + _added = list(_s2 - _s1) if len(_added) == 1: return { TRANSLATION_ADDED: _added[0], } - elif len(t1) == len(t2) + 1: + if len(t1) == len(t2) + 1: _removed = list(set(t1) - set(t2)) if len(_removed) == 1: return { From d50a90a985efe6d9b46cf250c6ee51f1bcc24679 Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Mon, 13 Feb 2017 13:53:07 -0800 Subject: [PATCH 14/27] * handling cases where translations do not yet exist --- .../stylesheets/partials/form_builder/_card.scss | 11 +++++++++++ jsapp/xlform/src/model.row.coffee | 6 +++++- jsapp/xlform/src/view.choices.coffee | 13 +++++++++---- jsapp/xlform/src/view.row.coffee | 6 +++++- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/jsapp/scss/stylesheets/partials/form_builder/_card.scss b/jsapp/scss/stylesheets/partials/form_builder/_card.scss index 2eb6e549af..d0c8fa8f80 100644 --- a/jsapp/scss/stylesheets/partials/form_builder/_card.scss +++ b/jsapp/scss/stylesheets/partials/form_builder/_card.scss @@ -75,6 +75,17 @@ opacity: 0.5; } } + .card__header-subtitle-empty-value { + border: 1px solid #ffdbdb; + padding: 2px 3px; + border-radius: 3px; + background-color: #fff8f8; + } + .card__option-translation { + &--empty { + opacity: 0.6; + } + } &.card--shaded .card__header-title { opacity:0.3; } diff --git a/jsapp/xlform/src/model.row.coffee b/jsapp/xlform/src/model.row.coffee index 3bd0b8041d..aab9e64758 100644 --- a/jsapp/xlform/src/model.row.coffee +++ b/jsapp/xlform/src/model.row.coffee @@ -410,7 +410,11 @@ module.exports = do -> _key getLabel: (whichone="primary")-> - @getValue @getTranslatedColumnKey("label", whichone) + _col = @getTranslatedColumnKey("label", whichone) + if _col of @attributes + @getValue _col + else + null finalize: -> existing_name = @getValue("name") diff --git a/jsapp/xlform/src/view.choices.coffee b/jsapp/xlform/src/view.choices.coffee index 84a2af3b46..446dfe96ec 100644 --- a/jsapp/xlform/src/view.choices.coffee +++ b/jsapp/xlform/src/view.choices.coffee @@ -54,7 +54,8 @@ module.exports = do -> addEmptyOption: (label)-> emptyOpt = new $choices.Option(label: label) @model.options.add(emptyOpt) - new OptionView(model: emptyOpt, cl: @model).render().$el.appendTo @ul + _translations = @model.getSurvey().translations + new OptionView(model: emptyOpt, cl: @model, translations: _translations).render().$el.appendTo @ul lis = @ul.find('li') if lis.length == 2 lis.find('.js-remove-option').removeClass('hidden') @@ -73,7 +74,7 @@ module.exports = do -> class OptionView extends $baseView tagName: "li" - className: "multioptions__option xlf-option-view xlf-option-view--depr" + className: "multioptions__option xlf-option-view xlf-option-view--depr" events: "keyup input": "keyupinput" "click .js-remove-option": "remove" @@ -130,10 +131,14 @@ module.exports = do -> @d.append(@c) @$el.html(@d) - _survey = @model.getSurvey().getSurvey() - _translation_2 = _survey._translation_2 + _translation_2 = @options.translations[1] if _translation_2 isnt undefined _t_opt = @model.get("label::#{_translation_2}") + if !_t_opt + _no_t = _t("No translation") + _klss = ["card__option-translation", + "card__option-translation--empty"].join(" ") + _t_opt = """#{_no_t}""" $("", {className: 'secondary-translation'}).html(""" 🌐 -  #{_t_opt} diff --git a/jsapp/xlform/src/view.row.coffee b/jsapp/xlform/src/view.row.coffee index 5fe29183cd..deea769c0e 100644 --- a/jsapp/xlform/src/view.row.coffee +++ b/jsapp/xlform/src/view.row.coffee @@ -78,7 +78,11 @@ module.exports = do -> @cardSettingsWrap = @$('.card__settings').eq(0) _second_translation = @surveyView.survey._translation_2 if _second_translation isnt undefined - @$sub_label.html(@model.getLabel('_2')).show() + _second_val = @model.getLabel('_2') + if !_second_val + _no_t = _t("No translation") + _second_val = """#{_no_t}""" + @$sub_label.html(_second_val).show() @defaultRowDetailParent = @cardSettingsWrap.find('.card__settings__fields--question-options').eq(0) for [key, val] in @model.attributesArray() when key is 'label' or key is 'type' view = new $viewRowDetail.DetailView(model: val, rowView: @) From 745a7a8b35e2d64f605f731a6ea478913afa29f3 Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Mon, 13 Feb 2017 13:54:12 -0800 Subject: [PATCH 15/27] * handling cases where no choice list is present --- kpi/models/asset.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/kpi/models/asset.py b/kpi/models/asset.py index feb986d7b5..51d7c74048 100644 --- a/kpi/models/asset.py +++ b/kpi/models/asset.py @@ -288,17 +288,17 @@ def _prioritize_translation(self, content, translation_name, is_new=False): raise ValueError('translation cannot be found') _tindex = -1 if is_new else _translations.index(translation_name) if is_new or (_tindex > 0): - for row in content.get('survey'): + for row in content.get('survey', []): for col in _translated: if is_new: - val = '[{}] {}'.format(translation_name, row[col][0]) + val = '{}'.format(row[col][0]) else: val = row[col].pop(_tindex) row[col].insert(0, val) - for row in content.get('choices'): + for row in content.get('choices', []): for col in _translated: if is_new: - val = '[{}] {}'.format(translation_name, row[col][0]) + val = '{}'.format(row[col][0]) else: val = row[col].pop(_tindex) row[col].insert(0, val) @@ -310,10 +310,10 @@ def _prioritize_translation(self, content, translation_name, is_new=False): def _remove_last_translation(self, content): content.get('translations').pop() _translated = content.get('translated', []) - for row in content.get('survey'): + for row in content.get('survey', []): for col in _translated: row[col].pop() - for row in content.get('choices'): + for row in content.get('choices', []): for col in _translated: row[col].pop() From ae97f8541f79a767d1d91c50b875626841d29f72 Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Wed, 15 Feb 2017 16:01:13 -0800 Subject: [PATCH 16/27] fix missing 'translations' parameter --- jsapp/xlform/src/view.choices.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jsapp/xlform/src/view.choices.coffee b/jsapp/xlform/src/view.choices.coffee index 446dfe96ec..8ded0c17c2 100644 --- a/jsapp/xlform/src/view.choices.coffee +++ b/jsapp/xlform/src/view.choices.coffee @@ -20,9 +20,10 @@ module.exports = do -> if cardText.find('.card__buttons__multioptions.js-expand-multioptions').length is 0 cardText.prepend $.parseHTML($viewTemplates.row.expandChoiceList()) @$el.html (@ul = $("
    ", class: @ulClasses)) + _ts = @model.getSurvey().translations if @row.get("type").get("rowType").specifyChoice for option, i in @model.options.models - new OptionView(model: option, cl: @model).render().$el.appendTo @ul + new OptionView(model: option, cl: @model, translations: _ts).render().$el.appendTo @ul if i == 0 while i < 2 @addEmptyOption("Option #{++i}") From c0af16e8ab3aa9675b0c75e5d4b1a3509cffdc54 Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Thu, 16 Feb 2017 12:39:17 -0800 Subject: [PATCH 17/27] Putting first and second translations above the form builder --- jsapp/js/editorMixins/editableForm.es6 | 17 +++++++++++++++++ jsapp/scss/components/_kobo.form-builder.scss | 15 +++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/jsapp/js/editorMixins/editableForm.es6 b/jsapp/js/editorMixins/editableForm.es6 index 0b4754e401..c657fecf30 100644 --- a/jsapp/js/editorMixins/editableForm.es6 +++ b/jsapp/js/editorMixins/editableForm.es6 @@ -450,6 +450,8 @@ export default assign({ saveButtonText, } = this.buttonStates(); + let translations = this.state.translations || []; + return ( @@ -550,6 +552,21 @@ export default assign({ : null } + + { + (translations.length < 2) ? +

    + {translations[0]} +

    + : +

    + {translations[0]} + + {translations[1]} + +

    + } +
    Date: Tue, 21 Feb 2017 11:34:03 -0800 Subject: [PATCH 18/27] prevents mis-ordered elements while editing labels --- jsapp/xlform/src/view.utils.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jsapp/xlform/src/view.utils.coffee b/jsapp/xlform/src/view.utils.coffee index 8d421d70e6..8caa1a195a 100644 --- a/jsapp/xlform/src/view.utils.coffee +++ b/jsapp/xlform/src/view.utils.coffee @@ -39,8 +39,8 @@ module.exports = do -> parent_element.find('.error-message').remove() current_value = selector.text().replace new RegExp(String.fromCharCode(160), 'g'), '' - edit_box = $('', type:'text', value:current_value, class:'js-cancel-sort js-blur-on-select-row') - selector.parent().append edit_box + edit_box = $('', type:'text', value: current_value, class:'js-cancel-sort js-blur-on-select-row') + selector.after edit_box selector.hide() edit_box.focus() From e90be42126410a605352c15333ea65a348a794f7 Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Tue, 21 Feb 2017 12:20:54 -0800 Subject: [PATCH 19/27] preserving translation list in the asset's state --- jsapp/js/editorMixins/existingForm.es6 | 1 + jsapp/scss/components/_kobo.form-builder.scss | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/jsapp/js/editorMixins/existingForm.es6 b/jsapp/js/editorMixins/existingForm.es6 index 9c622e5871..189ee4a371 100644 --- a/jsapp/js/editorMixins/existingForm.es6 +++ b/jsapp/js/editorMixins/existingForm.es6 @@ -20,6 +20,7 @@ export default { stores.allAssets.whenLoaded(uid, (asset) => { this.launchAppForSurveyContent(asset.content, { name: asset.name, + translations: asset.content && asset.content.translations.slice(0), settings__style: asset.settings__style, asset_uid: asset.uid, asset_type: asset.asset_type, diff --git a/jsapp/scss/components/_kobo.form-builder.scss b/jsapp/scss/components/_kobo.form-builder.scss index f6ffcde578..ca7a090624 100644 --- a/jsapp/scss/components/_kobo.form-builder.scss +++ b/jsapp/scss/components/_kobo.form-builder.scss @@ -147,9 +147,10 @@ } &--translations { p { - margin: 0; - line-height: 17px; + margin: 0 0 0 10px; + line-height: 15px; padding-top: 10px; + font-size: 11px; small { display: block; From 3879b0f8456e4bd01dacfc8a795bc71e39d41a49 Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Wed, 22 Feb 2017 15:34:26 -0800 Subject: [PATCH 20/27] changing test to use '#active_translation_name' rather than '#null_translation' --- kpi/tests/test_assets.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/kpi/tests/test_assets.py b/kpi/tests/test_assets.py index 699b5e00d9..1c09d8e239 100644 --- a/kpi/tests/test_assets.py +++ b/kpi/tests/test_assets.py @@ -153,9 +153,10 @@ def test_rename_null_translation(self): {'label': ['lang1', 'lang2'], 'type': 'text', 'name': 'q1'}, ], 'translations': ['lang1', None], - '#null_translation': 'lang2', + '#active_translation_name': 'lang2', }) self.assertEqual(self.asset.content['translations'], ['lang1', 'lang2']) + self.assertTrue('#active_translation_name' not in self.asset.content) self.assertTrue('#null_translation' not in self.asset.content) def test_rename_translation(self): @@ -168,6 +169,10 @@ def test_rename_translation(self): ], 'translations': ['lang1', None], }) + _content = self.asset.content + self.assertTrue('translated' in _content) + self.assertEqual(_content['translated'], ['label']) + self.asset.rename_translation(None, 'lang2') self.assertEqual(self.asset.content['translations'], ['lang1', 'lang2']) From 945863fb67cdfe84251517c2495eececb7427c4e Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Thu, 23 Feb 2017 10:48:04 -0800 Subject: [PATCH 21/27] * added comments * removed test of feature that had to be removed for compatibility with old forms --- kpi/tests/test_asset_content.py | 11 ++++++----- kpi/utils/autoname.py | 6 ++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/kpi/tests/test_asset_content.py b/kpi/tests/test_asset_content.py index efdd5722ef..e328ce9ba2 100644 --- a/kpi/tests/test_asset_content.py +++ b/kpi/tests/test_asset_content.py @@ -226,11 +226,12 @@ def _name_to_autoname(rows): # if there is a name conflict we should throw a meaningful error # however, since this behavior is already present, it might be - with pytest.raises(ValueError): - _name_to_autoname([ - {'name': LONG_NAME}, - {'name': LONG_NAME}, - ]) + # impossible to transition existing valid forms + # with pytest.raises(ValueError): + # _name_to_autoname([ + # {'name': LONG_NAME}, + # {'name': LONG_NAME}, + # ]) long_label = ('Four score and seven years ago, our fathers brought forth' ' on this contintent') diff --git a/kpi/utils/autoname.py b/kpi/utils/autoname.py index af05907899..90983987fe 100644 --- a/kpi/utils/autoname.py +++ b/kpi/utils/autoname.py @@ -110,6 +110,12 @@ def _assign_row_to_name(row, suggested_name): row['$given_name'] = _name _name = sluggify_label(_name, other_names=other_names.keys()) + # We might be able to remove these next 4 lines because + # sluggify_label shouldn't be returning an empty string + # and these fields already have names (_has_name(r)==True). + # However, these lines were added when testing a large set + # of forms so it's possible some edge cases (e.g. arabic) + # still permit it if _name == '' and '$kuid' in row: _name = '{}_{}'.format(row['type'], row['$kuid']) elif _name == '': From 7961b593bfedaac56d66fd854b3e3a2a7c22c7cd Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Fri, 24 Feb 2017 10:44:07 -0800 Subject: [PATCH 22/27] rest framework template fix / simplification --- kpi/templates/rest_framework/api.html | 19 ++----------------- .../muted_readonly_content_field.html | 13 ------------- 2 files changed, 2 insertions(+), 30 deletions(-) delete mode 100644 kpi/templates/rest_framework/horizontal/muted_readonly_content_field.html diff --git a/kpi/templates/rest_framework/api.html b/kpi/templates/rest_framework/api.html index 6de6258388..9b6b90bff0 100644 --- a/kpi/templates/rest_framework/api.html +++ b/kpi/templates/rest_framework/api.html @@ -1,24 +1,9 @@ {% extends "rest_framework/base.html" %} -{# Override this template in your own templates directory to customize #} -{% block title %}KoBoForm API{% endblock %} +{% block title %}KoBoToolbox Form Building API{% endblock %} {% block branding %} - KoBoForm API 0.1 + KoBoToolbox Form Building API {% endblock %} - -{% block head %} - - {{ block.super }} -{% endblock %} -{% block bootstrap_theme %} - {% load staticfiles %} - {% load render_bundle from webpack_loader %} - {% render_bundle 'app' 'css' %} -{% endblock %} - -{% block script %} -{% endblock %} - diff --git a/kpi/templates/rest_framework/horizontal/muted_readonly_content_field.html b/kpi/templates/rest_framework/horizontal/muted_readonly_content_field.html deleted file mode 100644 index bbe7fa7ce0..0000000000 --- a/kpi/templates/rest_framework/horizontal/muted_readonly_content_field.html +++ /dev/null @@ -1,13 +0,0 @@ -
    - - -
    - - {{ field.value }} - - - {% if field.errors %} - {% for error in field.errors %}{{ error }}{% endfor %} - {% endif %} -
    -
    From 074cde8baa9ef0c2c02c78d99379e3d21df2a8f4 Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Fri, 24 Feb 2017 10:44:59 -0800 Subject: [PATCH 23/27] asset snapshot preparation matches that of assets --- kpi/models/asset.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/kpi/models/asset.py b/kpi/models/asset.py index 3a3b255ee2..2b51fe02ec 100644 --- a/kpi/models/asset.py +++ b/kpi/models/asset.py @@ -212,10 +212,18 @@ def _unlink_list_items(self, content): if '$kuid' in row: del row['$kuid'] - def _remove_empty_expressions(self, content): remove_empty_expressions_in_place(content) + def _adjust_active_translation(self, content): + # to get around the form builder's way of handling translations where + # the interface focuses on the "null translation" and shows other ones + # in advanced settings, we allow the builder to attach a parameter + # which says what to name the null translation. + _null_translation_as = content.pop('#active_translation_name', None) + if _null_translation_as: + self._rename_translation(content, None, _null_translation_as) + def _strip_empty_rows(self, content, vals=None): if vals is None: vals = { @@ -327,7 +335,7 @@ def _reorder_translations(self, content, translations): self._prioritize_translation(content, _tname) def _rename_translation(self, content, _from, _to): - _ts = self.content.get('translations') + _ts = content.get('translations') if _to in _ts: raise ValueError('Duplicate translation: {}'.format(_to)) _ts[_ts.index(_from)] = _to @@ -485,14 +493,8 @@ def adjust_content_on_save(self): asset.save(adjust_content=False) ''' self._standardize(self.content) - # to get around the form builder's way of handling translations where - # the interface focuses on the "null translation" and shows other ones - # in advanced settings, we allow the builder to attach a parameter - # which says what to name the null translation. - _null_translation_as = self.content.pop('#active_translation_name', None) - if _null_translation_as: - self._rename_translation(self.content, None, _null_translation_as) + self._adjust_active_translation(self.content) self._strip_empty_rows(self.content) self._assign_kuids(self.content) self._autoname(self.content) @@ -708,6 +710,7 @@ def save(self, *args, **kwargs): if _source is None: _source = {} self._standardize(_source) + self._adjust_active_translation(_source) self._strip_empty_rows(_source) self._autoname(_source) self._remove_empty_expressions(_source) From 8e2754da1edd129669e100df575ae7a9d8e91539 Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Fri, 24 Feb 2017 10:46:57 -0800 Subject: [PATCH 24/27] forms with 0 translations don't break --- jsapp/js/editorMixins/existingForm.es6 | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/jsapp/js/editorMixins/existingForm.es6 b/jsapp/js/editorMixins/existingForm.es6 index 189ee4a371..f0b14d1d6a 100644 --- a/jsapp/js/editorMixins/existingForm.es6 +++ b/jsapp/js/editorMixins/existingForm.es6 @@ -18,9 +18,11 @@ export default { componentDidMount () { let uid = this.props.params.assetid; stores.allAssets.whenLoaded(uid, (asset) => { + let translations = (asset.content && asset.content.translations + && asset.content.translations.slice(0)) || []; this.launchAppForSurveyContent(asset.content, { name: asset.name, - translations: asset.content && asset.content.translations.slice(0), + translations: translations, settings__style: asset.settings__style, asset_uid: asset.uid, asset_type: asset.asset_type, @@ -33,9 +35,11 @@ export default { if (isLibrary(this.context.router)) { routeName = 'library'; } else { - if (stores.history.previousRoute == 'form-landing') { + if (stores.history.currentRoute === 'form-edit') { routeName = 'form-landing'; - params = {assetid: this.props.params.assetid}; + params = { + assetid: this.props.params.assetid, + }; } } From aeeb037727f0820ab3d484a9ee45cd8fa1183382 Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Fri, 24 Feb 2017 10:48:11 -0800 Subject: [PATCH 25/27] * Some minor navigation & css changes * "X" button from the form builder goes back to the previous page (as was originally coded, but broke at some point) --- jsapp/js/mixins.es6 | 7 ++++++- jsapp/scss/components/_kobo.asset-row.scss | 6 +++--- jsapp/scss/components/_kobo.form-view.scss | 6 ++++++ jsapp/scss/stylesheets/partials/form_builder/_card.scss | 2 +- jsapp/xlform/src/view.choices.coffee | 2 +- jsapp/xlform/src/view.surveyApp.templates.coffee | 2 +- 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/jsapp/js/mixins.es6 b/jsapp/js/mixins.es6 index 9ccd74bfc9..16a869875c 100644 --- a/jsapp/js/mixins.es6 +++ b/jsapp/js/mixins.es6 @@ -135,7 +135,8 @@ var dmix = { }, survey: { innerRender: function () { - var docTitle = this.state.name || t('Untitled'); + let docTitle = this.state.name || t('Untitled'); + let formList = this.makeHref('forms'); return ( @@ -143,6 +144,10 @@ var dmix = { + {t('Form Overview')} {this.renderDeployments()} diff --git a/jsapp/scss/components/_kobo.asset-row.scss b/jsapp/scss/components/_kobo.asset-row.scss index 833fd7e21b..a4acdc6e60 100644 --- a/jsapp/scss/components/_kobo.asset-row.scss +++ b/jsapp/scss/components/_kobo.asset-row.scss @@ -44,13 +44,13 @@ } .asset-row__celllink--name { - display: inline-block; - max-width:96%; + display: block; + max-width: 96%; text-overflow: ellipsis; overflow: hidden; font-size: 15px; vertical-align: top; - padding:4px 0px; + padding: 4px 0px; } .asset-type-icon + .asset-row__celllink--name { diff --git a/jsapp/scss/components/_kobo.form-view.scss b/jsapp/scss/components/_kobo.form-view.scss index 5240870cde..eaa10322a1 100644 --- a/jsapp/scss/components/_kobo.form-view.scss +++ b/jsapp/scss/components/_kobo.form-view.scss @@ -55,6 +55,12 @@ } } +.form-view__link--close { + @extend .k-icon, .k-icon-close; + float: right; + margin-right: -10px; +} + // FIXME: rename this .form-view__cell { .asset-view__label { diff --git a/jsapp/scss/stylesheets/partials/form_builder/_card.scss b/jsapp/scss/stylesheets/partials/form_builder/_card.scss index d0c8fa8f80..3a94f818e7 100644 --- a/jsapp/scss/stylesheets/partials/form_builder/_card.scss +++ b/jsapp/scss/stylesheets/partials/form_builder/_card.scss @@ -71,7 +71,7 @@ font-size: 12px; &:before { - content: '🌐 - '; + content: '+ '; opacity: 0.5; } } diff --git a/jsapp/xlform/src/view.choices.coffee b/jsapp/xlform/src/view.choices.coffee index 8ded0c17c2..27fdbace04 100644 --- a/jsapp/xlform/src/view.choices.coffee +++ b/jsapp/xlform/src/view.choices.coffee @@ -141,7 +141,7 @@ module.exports = do -> "card__option-translation--empty"].join(" ") _t_opt = """#{_no_t}""" $("", {className: 'secondary-translation'}).html(""" - 🌐 -  + #{_t_opt} """).appendTo(@$el) @ diff --git a/jsapp/xlform/src/view.surveyApp.templates.coffee b/jsapp/xlform/src/view.surveyApp.templates.coffee index e65245b99a..b97e8fe813 100644 --- a/jsapp/xlform/src/view.surveyApp.templates.coffee +++ b/jsapp/xlform/src/view.surveyApp.templates.coffee @@ -36,7 +36,7 @@ module.exports = do -> if t1 translations_content += " [#{print_translation(t1)}]" else - translations_content = "1×🌐" + translations_content = "" """
    From 1cc4e588d50458c3d02f10e58bf7af9b51d327e8 Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Fri, 24 Feb 2017 10:50:52 -0800 Subject: [PATCH 26/27] gitignoring some generated files --- .gitignore | 3 ++- jsapp/fonts/.gitignore | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 829760b096..234cbf325f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,8 @@ jsapp/js/refactored.es6 whoosh_index tmp staticfiles +.cache .sass-cache webpack-stats.json sensitive_data.json - +test/compiled/* diff --git a/jsapp/fonts/.gitignore b/jsapp/fonts/.gitignore index c3022ba016..14ac882592 100644 --- a/jsapp/fonts/.gitignore +++ b/jsapp/fonts/.gitignore @@ -3,3 +3,9 @@ *.ttf *.woff *.woff2 +*.scss +*.css +*.md +*.ijmap +*.otf +codepoints \ No newline at end of file From 55578ae7f9902f03e5550fd8f6ca741528a10ecd Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Fri, 24 Feb 2017 11:00:46 -0800 Subject: [PATCH 27/27] double save to preserve original asset content --- kpi/management/commands/sync_kobocat_xforms.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kpi/management/commands/sync_kobocat_xforms.py b/kpi/management/commands/sync_kobocat_xforms.py index b57f769047..dcd4e6db6c 100644 --- a/kpi/management/commands/sync_kobocat_xforms.py +++ b/kpi/management/commands/sync_kobocat_xforms.py @@ -389,7 +389,10 @@ def handle(self, *args, **options): continue if content_changed or metadata_changed: + # preserve the original "asset.content" asset.save(adjust_content=False) + # save a new version with standardized content + asset.save() if content_changed: asset._mark_latest_version_as_deployed() self._print_tabular(