From 25bd28e48e654c3fcd3b37541606468a81eeb9d3 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Wed, 5 Dec 2018 11:58:07 +0100 Subject: [PATCH 01/44] listen to all 403/401 ajax errors and display notification --- jsapp/js/dataInterface.es6 | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/jsapp/js/dataInterface.es6 b/jsapp/js/dataInterface.es6 index b1727717b9..697ca93b7c 100644 --- a/jsapp/js/dataInterface.es6 +++ b/jsapp/js/dataInterface.es6 @@ -1,7 +1,9 @@ import $ from 'jquery'; import { + t, assign, + notify } from './utils'; var dataInterface; @@ -25,6 +27,16 @@ var dataInterface; })(); this.rootUrl = rootUrl; + $(document).ajaxError((event, request, settings) => { + if (request.status === 403 || request.status === 401) { + let errorMessage = t("It seems you're not logged in anymore. Try reloading the page") + if (request.responseJSON && request.responseJSON.detail) { + errorMessage = request.responseJSON.detail; + } + notify(errorMessage, 'error'); + } + }); + assign(this, { selfProfile: ()=> $ajax({ url: `${rootUrl}/me/` }), serverEnvironment: ()=> $ajax({ url: `${rootUrl}/environment/` }), From 2d5f1b25977a1d55a4153c7e1998e80145a9660b Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Wed, 5 Dec 2018 18:07:12 +0100 Subject: [PATCH 02/44] start working on accepted files view --- jsapp/xlform/src/model.row.coffee | 2 ++ jsapp/xlform/src/view.acceptedFiles.coffee | 29 +++++++++++++++++++ .../src/view.acceptedFiles.templates.coffee | 12 ++++++++ jsapp/xlform/src/view.row.coffee | 8 +++++ jsapp/xlform/src/view.rowDetail.coffee | 5 ++++ jsapp/xlform/src/view.templates.coffee | 2 ++ 6 files changed, 58 insertions(+) create mode 100644 jsapp/xlform/src/view.acceptedFiles.coffee create mode 100644 jsapp/xlform/src/view.acceptedFiles.templates.coffee diff --git a/jsapp/xlform/src/model.row.coffee b/jsapp/xlform/src/model.row.coffee index fddb82ebfc..60be9f9587 100644 --- a/jsapp/xlform/src/model.row.coffee +++ b/jsapp/xlform/src/model.row.coffee @@ -420,6 +420,8 @@ module.exports = do -> # TODO [ald]: pull this from $aliases @get('type').get('typeId') in ['select_one', 'select_multiple'] + getAcceptedFiles: -> return @attributes['body::accept']? + getParameters: -> readParameters(@attributes.parameters?.attributes?.value) setParameters: (paramObject) -> diff --git a/jsapp/xlform/src/view.acceptedFiles.coffee b/jsapp/xlform/src/view.acceptedFiles.coffee new file mode 100644 index 0000000000..55bcd83dbe --- /dev/null +++ b/jsapp/xlform/src/view.acceptedFiles.coffee @@ -0,0 +1,29 @@ +Backbone = require 'backbone' +$baseView = require './view.pluggedIn.backboneView' +$viewTemplates = require './view.templates' + +module.exports = do -> + class AcceptedFilesView extends $baseView + className: 'param-option' + events: { + 'input input': 'onChange' + } + + initialize: ({@rowView, @acceptedFiles=''}) -> return + + render: -> + template = $($viewTemplates.$$render("AcceptedFilesView.input", @acceptedFiles)) + @$el.html(template) + return @ + + insertInDOM: (rowView)-> + @$el.appendTo(rowView.defaultRowDetailParent) + return + + onChange: (evt) -> + @acceptedFiles = evt.currentTarget.value + @rowView.model.setAcceptedFiles(@acceptedFiles) + @rowView.model.getSurvey().trigger('change') + return + + AcceptedFilesView: AcceptedFilesView diff --git a/jsapp/xlform/src/view.acceptedFiles.templates.coffee b/jsapp/xlform/src/view.acceptedFiles.templates.coffee new file mode 100644 index 0000000000..caa9152ec9 --- /dev/null +++ b/jsapp/xlform/src/view.acceptedFiles.templates.coffee @@ -0,0 +1,12 @@ +module.exports = do -> + _t = require('utils').t + + acceptedFilesInput = (value) -> + return """ + + """ + + acceptedFilesInput: acceptedFilesInput diff --git a/jsapp/xlform/src/view.row.coffee b/jsapp/xlform/src/view.row.coffee index dbc4d8a1a0..2f93eba91d 100644 --- a/jsapp/xlform/src/view.row.coffee +++ b/jsapp/xlform/src/view.row.coffee @@ -9,6 +9,7 @@ $viewTemplates = require './view.templates' $viewUtils = require './view.utils' $viewChoices = require './view.choices' $viewParams = require './view.params' +$acceptedFilesView = require './view.acceptedFiles' $viewRowDetail = require './view.rowDetail' renderKobomatrix = require('js/formbuild/renderInBackbone').renderKobomatrix _t = require('utils').t @@ -212,6 +213,7 @@ module.exports = do -> new $viewRowDetail.DetailView(model: val, rowView: @).render().insertInDOM(@) questionType = @model.get('type').get('typeId') + paramsConfig = $configs.questionParams[questionType] if paramsConfig and 'getParameters' of @model @paramsView = new $viewParams.ParamsView({ @@ -220,6 +222,12 @@ module.exports = do -> paramsConfig: paramsConfig }).render().insertInDOM(@) + if questionType is 'file' + @acceptedFilesView = new $acceptedFilesView.AcceptedFilesView({ + rowView: @, + acceptedFiles: @model.getAcceptedFiles() + }).render().insertInDOM(@) + return @ hideMultioptions: -> diff --git a/jsapp/xlform/src/view.rowDetail.coffee b/jsapp/xlform/src/view.rowDetail.coffee index 3c1162e5c5..13310b5bc2 100644 --- a/jsapp/xlform/src/view.rowDetail.coffee +++ b/jsapp/xlform/src/view.rowDetail.coffee @@ -188,6 +188,11 @@ module.exports = do -> html: -> false insertInDOM: (rowView)-> return + # body::accept is handled in custom view + viewRowDetail.DetailViewMixins['body::accept'] = + html: -> false + insertInDOM: (rowView)-> return + viewRowDetail.DetailViewMixins.relevant = html: -> @$el.addClass("card__settings__fields--active") diff --git a/jsapp/xlform/src/view.templates.coffee b/jsapp/xlform/src/view.templates.coffee index 07f68d9487..1dc4fa3eb4 100644 --- a/jsapp/xlform/src/view.templates.coffee +++ b/jsapp/xlform/src/view.templates.coffee @@ -1,6 +1,7 @@ _ = require 'underscore' choices_templates = require './view.choices.templates' +accepted_files_templates = require './view.acceptedFiles.templates' params_templates = require './view.params.templates' row_templates = require './view.row.templates' rowDetail_templates = require './view.rowDetail.templates' @@ -18,6 +19,7 @@ module.exports = do -> surveyApp: surveyApp_templates surveyDetails: surveyDetails_templates + templates['AcceptedFilesView.input'] = accepted_files_templates.acceptedFilesInput templates['ParamsView.numberParam'] = params_templates.numberParam templates['ParamsView.booleanParam'] = params_templates.booleanParam templates['xlfListView.addOptionButton'] = choices_templates.addOptionButton From 3965b182a5588bd9c242417c72a4044ec9d8ca98 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Thu, 13 Dec 2018 18:46:55 +0100 Subject: [PATCH 03/44] better auth error handling --- jsapp/js/dataInterface.es6 | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/jsapp/js/dataInterface.es6 b/jsapp/js/dataInterface.es6 index ad60dcde32..f89ed8f69e 100644 --- a/jsapp/js/dataInterface.es6 +++ b/jsapp/js/dataInterface.es6 @@ -1,5 +1,5 @@ import $ from 'jquery'; - +import alertify from 'alertifyjs'; import { t, assign, @@ -27,13 +27,20 @@ var dataInterface; })(); this.rootUrl = rootUrl; + // hook up to all AJAX requests to check auth problems $(document).ajaxError((event, request, settings) => { if (request.status === 403 || request.status === 401) { - let errorMessage = t("It seems you're not logged in anymore. Try reloading the page") - if (request.responseJSON && request.responseJSON.detail) { - errorMessage = request.responseJSON.detail; - } - notify(errorMessage, 'error'); + dataInterface.selfProfile().done((data) => { + if (data.message === 'user is not logged in') { + let errorMessage = t("It seems you're not logged in anymore. Try reloading the page. The server said: ##server_message##") + if (request.responseJSON && request.responseJSON.detail) { + errorMessage = errorMessage.replace('##server_message##', request.responseJSON.detail); + } else { + errorMessage = errorMessage.replace('##server_message##', request.status); + } + alertify.alert(t('Auth Error'), errorMessage); + } + }); } }); From e4387c7c831abef1b5fc9ff7299b133639ac4e19 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Mon, 17 Dec 2018 16:58:57 +0100 Subject: [PATCH 04/44] finish up acceptedFiles --- jsapp/xlform/src/model.row.coffee | 6 +++++- jsapp/xlform/src/view.acceptedFiles.coffee | 5 +++-- jsapp/xlform/src/view.acceptedFiles.templates.coffee | 12 +++++++----- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/jsapp/xlform/src/model.row.coffee b/jsapp/xlform/src/model.row.coffee index 60be9f9587..ef0c2af687 100644 --- a/jsapp/xlform/src/model.row.coffee +++ b/jsapp/xlform/src/model.row.coffee @@ -420,7 +420,11 @@ module.exports = do -> # TODO [ald]: pull this from $aliases @get('type').get('typeId') in ['select_one', 'select_multiple'] - getAcceptedFiles: -> return @attributes['body::accept']? + getAcceptedFiles: -> return @attributes['body::accept']?.attributes?.value + + setAcceptedFiles: (bodyAcceptString) -> + @setDetail('body::accept', bodyAcceptString) + return getParameters: -> readParameters(@attributes.parameters?.attributes?.value) diff --git a/jsapp/xlform/src/view.acceptedFiles.coffee b/jsapp/xlform/src/view.acceptedFiles.coffee index 55bcd83dbe..b4cf298f20 100644 --- a/jsapp/xlform/src/view.acceptedFiles.coffee +++ b/jsapp/xlform/src/view.acceptedFiles.coffee @@ -4,15 +4,16 @@ $viewTemplates = require './view.templates' module.exports = do -> class AcceptedFilesView extends $baseView - className: 'param-option' + className: 'accepted-files card__settings__fields__field' events: { 'input input': 'onChange' } + placeholder: '.pdf,.doc,.odt,.docx' initialize: ({@rowView, @acceptedFiles=''}) -> return render: -> - template = $($viewTemplates.$$render("AcceptedFilesView.input", @acceptedFiles)) + template = $($viewTemplates.$$render("AcceptedFilesView.input", @acceptedFiles, @placeholder)) @$el.html(template) return @ diff --git a/jsapp/xlform/src/view.acceptedFiles.templates.coffee b/jsapp/xlform/src/view.acceptedFiles.templates.coffee index caa9152ec9..8739ec8ec3 100644 --- a/jsapp/xlform/src/view.acceptedFiles.templates.coffee +++ b/jsapp/xlform/src/view.acceptedFiles.templates.coffee @@ -1,12 +1,14 @@ module.exports = do -> _t = require('utils').t - acceptedFilesInput = (value) -> + acceptedFilesInput = (value, placeholder) -> return """ - +
+ + + + +
""" acceptedFilesInput: acceptedFilesInput From 2e6029b5aeba30aa0fe98c9889895667ea24e560 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Tue, 18 Dec 2018 22:57:01 +0100 Subject: [PATCH 05/44] account menu is scrollable and language menu toggles on click --- jsapp/js/components/header.es6 | 14 ++++++++++---- jsapp/js/ui.es6 | 12 ++++++++++++ jsapp/scss/components/_kobo.navigation.scss | 18 +++--------------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/jsapp/js/components/header.es6 b/jsapp/js/components/header.es6 index 7c278da12c..7b777026b4 100644 --- a/jsapp/js/components/header.es6 +++ b/jsapp/js/components/header.es6 @@ -64,6 +64,9 @@ class MainHeader extends Reflux.Component { logout () { actions.auth.logout(); } + toggleLanguageSelector() { + this.setState({isLanguageSelectorVisible: !this.state.isLanguageSelectorVisible}) + } accountSettings () { // verifyLogin also refreshes stored profile data actions.auth.verifyLogin.triggerAsync().then(() => { @@ -139,13 +142,16 @@ class MainHeader extends Reflux.Component { } - + {t('Language')} -
    - {langs.map(this.renderLangItem)} -
+ + {this.state.isLanguageSelectorVisible && +
    + {langs.map(this.renderLangItem)} +
+ }
diff --git a/jsapp/js/ui.es6 b/jsapp/js/ui.es6 index 178e668392..59ce77820c 100644 --- a/jsapp/js/ui.es6 +++ b/jsapp/js/ui.es6 @@ -230,6 +230,18 @@ class PopoverMenu extends React.Component { if (isBlur && this.props.blurEventDisabled) return false; + if ( + isBlur && + evt.relatedTarget && + evt.relatedTarget.dataset && + evt.relatedTarget.dataset.popoverMenuStopBlur + ) { + // bring back focus to trigger to still enable this toggle callback + // but don't close the menu + evt.target.focus(); + return false; + } + if (this.state.popoverVisible || isBlur) { $popoverMenu = $(evt.target).parents('.popover-menu').find('.popover-menu__content'); this.setState({ diff --git a/jsapp/scss/components/_kobo.navigation.scss b/jsapp/scss/components/_kobo.navigation.scss index 40dab018db..8a6423eb1c 100644 --- a/jsapp/scss/components/_kobo.navigation.scss +++ b/jsapp/scss/components/_kobo.navigation.scss @@ -142,6 +142,8 @@ $headerAccountTextColor: #dbedf7; .account-box__menu { min-width: 270px; + max-height: 80vh; + overflow-y: auto; .account-box__menu-item--avatar { display: inline-block; @@ -215,17 +217,10 @@ $headerAccountTextColor: #dbedf7; padding-bottom: 5px; ul { - position: absolute; - right: 100%; - top: 0px; background-color: #FFF; color: $cool-gray; min-width: 140px; - transition: all 0.5s; - max-height: 0px; - opacity: 0; - overflow: hidden; - padding: 8px 0px; + padding: 0; @include box-shadow; @@ -239,13 +234,6 @@ $headerAccountTextColor: #dbedf7; } } } - - &:hover > ul { - transition: all 0.5s; - max-height: 1000px; - opacity: 1; - overflow: visible; - } } .account-box__menu-li--logout { From c73cfe2fa033c6e395cb693b9016316d3bca37f8 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Tue, 18 Dec 2018 23:02:51 +0100 Subject: [PATCH 06/44] add default bool to state --- jsapp/js/components/header.es6 | 1 + 1 file changed, 1 insertion(+) diff --git a/jsapp/js/components/header.es6 b/jsapp/js/components/header.es6 index 7b777026b4..6080ad0337 100644 --- a/jsapp/js/components/header.es6 +++ b/jsapp/js/components/header.es6 @@ -29,6 +29,7 @@ class MainHeader extends Reflux.Component { this.state = assign({ asset: false, currentLang: currentLang(), + isLanguageSelectorVisible: false, libraryFiltersContext: searches.getSearchContext('library', { filterParams: { assetType: 'asset_type:question OR asset_type:block OR asset_type:template', From 0acec571b4baa637bf68462a05d45b0c3385e6b0 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Wed, 19 Dec 2018 12:28:33 +0100 Subject: [PATCH 07/44] add strings for password strength to translations --- jsapp/js/components/passwordStrength.es6 | 4 +-- jsapp/js/i18nMissingStrings.es6 | 31 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/jsapp/js/components/passwordStrength.es6 b/jsapp/js/components/passwordStrength.es6 index 047eb5e268..47f2fddc65 100644 --- a/jsapp/js/components/passwordStrength.es6 +++ b/jsapp/js/components/passwordStrength.es6 @@ -31,7 +31,7 @@ class PasswordStrength extends React.Component { {report.feedback.warning && - {report.feedback.warning} + {t(report.feedback.warning)} } @@ -39,7 +39,7 @@ class PasswordStrength extends React.Component { report.feedback.suggestions.map((suggestion, index) => { return ( - {suggestion} + {t(suggestion)} ) }) diff --git a/jsapp/js/i18nMissingStrings.es6 b/jsapp/js/i18nMissingStrings.es6 index 04edbbdd93..8120e847eb 100644 --- a/jsapp/js/i18nMissingStrings.es6 +++ b/jsapp/js/i18nMissingStrings.es6 @@ -5,6 +5,37 @@ import { t, } from './utils'; +// strings for zxcvbn package +// copied from https://github.com/dropbox/zxcvbn/blob/master/src/feedback.coffee +test = t('Use a few words, avoid common phrases'); +test = t('No need for symbols, digits, or uppercase letters'); +test = t('Add another word or two. Uncommon words are better.'); +test = t('Straight rows of keys are easy to guess'); +test = t('Short keyboard patterns are easy to guess'); +test = t('Use a longer keyboard pattern with more turns'); +test = t('Repeats like "aaa" are easy to guess'); +test = t('Repeats like "abcabcabc" are only slightly harder to guess than "abc"'); +test = t('Avoid repeated words and characters'); +test = t('Sequences like abc or 6543 are easy to guess'); +test = t('Avoid sequences'); +test = t('Recent years are easy to guess'); +test = t('Avoid recent years'); +test = t('Avoid years that are associated with you'); +test = t('Dates are often easy to guess'); +test = t('Avoid dates and years that are associated with you'); +test = t('This is a top-10 common password'); +test = t('This is a top-100 common password'); +test = t('This is a very common password'); +test = t('This is similar to a commonly used password'); +test = t('A word by itself is easy to guess'); +test = t('Names and surnames by themselves are easy to guess'); +test = t('Common names and surnames are easy to guess'); +test = t("Capitalization doesn't help very much"); +test = t('All-uppercase is almost as easy to guess as all-lowercase'); +test = t("Reversed words aren't much harder to guess"); +test = t("Predictable substitutions like '@' instead of 'a' don't help very much"); + +// misc strings test = t('Add another condition'); test = t('Question should match all of these criteria'); From 286676ef353764993ed4a1e8a73d3b2d94c563f9 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Wed, 19 Dec 2018 12:31:11 +0100 Subject: [PATCH 08/44] note version number for copied strings --- jsapp/js/i18nMissingStrings.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsapp/js/i18nMissingStrings.es6 b/jsapp/js/i18nMissingStrings.es6 index 8120e847eb..f815f4c65f 100644 --- a/jsapp/js/i18nMissingStrings.es6 +++ b/jsapp/js/i18nMissingStrings.es6 @@ -5,7 +5,7 @@ import { t, } from './utils'; -// strings for zxcvbn package +// strings for zxcvbn 4.4.2 package // copied from https://github.com/dropbox/zxcvbn/blob/master/src/feedback.coffee test = t('Use a few words, avoid common phrases'); test = t('No need for symbols, digits, or uppercase letters'); From a7884e7e7801d6545aa0ac0e32fa8e9d7e9a81e6 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Wed, 19 Dec 2018 21:00:11 +0100 Subject: [PATCH 09/44] start reworking params view for special range type display --- jsapp/xlform/src/model.configs.coffee | 36 ++++++++++++++----- jsapp/xlform/src/view.params.coffee | 12 ++++--- jsapp/xlform/src/view.params.templates.coffee | 7 ++-- jsapp/xlform/src/view.row.coffee | 5 ++- 4 files changed, 42 insertions(+), 18 deletions(-) diff --git a/jsapp/xlform/src/model.configs.coffee b/jsapp/xlform/src/model.configs.coffee index b5c5e9c7f6..5acec3219f 100644 --- a/jsapp/xlform/src/model.configs.coffee +++ b/jsapp/xlform/src/model.configs.coffee @@ -184,20 +184,40 @@ module.exports = do -> configs.questionParams = { range: { - start: configs.paramTypes.number - end: configs.paramTypes.number - step: configs.paramTypes.number + start: { + type: configs.paramTypes.number + defaultValue: 0 + } + end: { + type: configs.paramTypes.number + defaultValue: 10 + } + step: { + type: configs.paramTypes.number + defaultValue: 1 + } } image: { - 'max-pixels': configs.paramTypes.number + 'max-pixels': { + type: configs.paramTypes.number + defaultValue: 1024 + } } select_one: { - randomize: configs.paramTypes.boolean - seed: configs.paramTypes.number + randomize: { + type: configs.paramTypes.boolean + } + seed: { + type: configs.paramTypes.number + } } select_multiple: { - randomize: configs.paramTypes.boolean - seed: configs.paramTypes.number + randomize: { + type: configs.paramTypes.boolean + } + seed: { + type: configs.paramTypes.number + } } } diff --git a/jsapp/xlform/src/view.params.coffee b/jsapp/xlform/src/view.params.coffee index 1eb8d3dc5d..c9bf38ff9d 100644 --- a/jsapp/xlform/src/view.params.coffee +++ b/jsapp/xlform/src/view.params.coffee @@ -6,16 +6,18 @@ $viewTemplates = require './view.templates' module.exports = do -> class ParamsView extends $baseView - initialize: ({@rowView, @parameters={}, @paramsConfig}) -> + initialize: ({@rowView, @parameters={}, @questionType}) -> + @typeConfig = $configs.questionParams[@questionType] @$el = $($.parseHTML($viewTemplates.row.paramsSettingsField())) @$paramsViewEl = @$el.find('.params-view') return render: -> - for paramName, paramType of @paramsConfig + for paramName, paramConfig of @typeConfig new ParamOption( paramName, - paramType, + paramConfig.type, + paramConfig.defaultValue, @parameters[paramName], @onParamChange.bind(@) ).render().$el.appendTo(@$paramsViewEl) @@ -37,10 +39,10 @@ module.exports = do -> 'input input': 'onChange' } - initialize: (@paramName, @paramType, @paramValue='', @onParamChange) -> return + initialize: (@paramName, @paramType, @paramDefault, @paramValue='', @onParamChange) -> return render: -> - template = $($viewTemplates.$$render("ParamsView.#{@paramType}Param", @paramName, @paramValue)) + template = $($viewTemplates.$$render("ParamsView.#{@paramType}Param", @paramName, @paramValue, @paramDefault)) @$el.html(template) return @ diff --git a/jsapp/xlform/src/view.params.templates.coffee b/jsapp/xlform/src/view.params.templates.coffee index 16e9912909..cc22c5a730 100644 --- a/jsapp/xlform/src/view.params.templates.coffee +++ b/jsapp/xlform/src/view.params.templates.coffee @@ -1,9 +1,12 @@ module.exports = do -> - numberParam = (label, number) -> + numberParam = (label, number, defaultValue) -> + if defaultValue + defaultValueAttr = "placeholder='#{defaultValue}'" + return """ """ diff --git a/jsapp/xlform/src/view.row.coffee b/jsapp/xlform/src/view.row.coffee index a85d1af433..cf1b825af5 100644 --- a/jsapp/xlform/src/view.row.coffee +++ b/jsapp/xlform/src/view.row.coffee @@ -212,12 +212,11 @@ module.exports = do -> new $viewRowDetail.DetailView(model: val, rowView: @).render().insertInDOM(@) questionType = @model.get('type').get('typeId') - paramsConfig = $configs.questionParams[questionType] - if paramsConfig and 'getParameters' of @model + if $configs.questionParams[questionType] and 'getParameters' of @model @paramsView = new $viewParams.ParamsView({ rowView: @, parameters: @model.getParameters(), - paramsConfig: paramsConfig + questionType: questionType }).render().insertInDOM(@) return @ From 47edbd495401f8694e4e0c4e9c50aeee77b8dab7 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Wed, 19 Dec 2018 21:14:24 +0100 Subject: [PATCH 10/44] work out splitting --- jsapp/xlform/src/view.params.coffee | 14 ++++++++++++-- jsapp/xlform/src/view.row.coffee | 18 +++++++++++++++++- jsapp/xlform/src/view.row.templates.coffee | 8 ++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/jsapp/xlform/src/view.params.coffee b/jsapp/xlform/src/view.params.coffee index c9bf38ff9d..d597cce70d 100644 --- a/jsapp/xlform/src/view.params.coffee +++ b/jsapp/xlform/src/view.params.coffee @@ -8,7 +8,13 @@ module.exports = do -> class ParamsView extends $baseView initialize: ({@rowView, @parameters={}, @questionType}) -> @typeConfig = $configs.questionParams[@questionType] - @$el = $($.parseHTML($viewTemplates.row.paramsSettingsField())) + + if @questionType is 'range' + template = $viewTemplates.row.paramsSimple() + else + template = $viewTemplates.row.paramsSettingsField() + + @$el = $($.parseHTML(template)) @$paramsViewEl = @$el.find('.params-view') return @@ -29,10 +35,14 @@ module.exports = do -> @rowView.model.getSurvey().trigger('change') return - insertInDOM: (rowView)-> + insertInDOM: (rowView) -> @$el.appendTo(rowView.defaultRowDetailParent) return + insertInDOMAfter: ($el) -> + @$el.insertAfter($el) + return + class ParamOption extends $baseView className: 'param-option' events: { diff --git a/jsapp/xlform/src/view.row.coffee b/jsapp/xlform/src/view.row.coffee index cf1b825af5..bf22b049a4 100644 --- a/jsapp/xlform/src/view.row.coffee +++ b/jsapp/xlform/src/view.row.coffee @@ -72,6 +72,18 @@ module.exports = do -> @$header = @$('.card__header') context = {warnings: []} + questionType = @model.get('type').get('typeId') + if ( + $configs.questionParams[questionType] and + 'getParameters' of @model and + questionType is 'range' + ) + @paramsView = new $viewParams.ParamsView({ + rowView: @, + parameters: @model.getParameters(), + questionType: questionType + }).render().insertInDOMAfter(@$header) + if 'getList' of @model and (cl = @model.getList()) @$card.addClass('card--selectquestion card--expandedchoices') @is_expanded = true @@ -212,7 +224,11 @@ module.exports = do -> new $viewRowDetail.DetailView(model: val, rowView: @).render().insertInDOM(@) questionType = @model.get('type').get('typeId') - if $configs.questionParams[questionType] and 'getParameters' of @model + if ( + $configs.questionParams[questionType] and + 'getParameters' of @model and + questionType isnt 'range' + ) @paramsView = new $viewParams.ParamsView({ rowView: @, parameters: @model.getParameters(), diff --git a/jsapp/xlform/src/view.row.templates.coffee b/jsapp/xlform/src/view.row.templates.coffee index 07a7068f6c..4ad76ef3e0 100644 --- a/jsapp/xlform/src/view.row.templates.coffee +++ b/jsapp/xlform/src/view.row.templates.coffee @@ -252,6 +252,13 @@ module.exports = do -> """ + paramsSimple = -> + """ +
+
+
+ """ + selectQuestionExpansion = -> """
@@ -278,6 +285,7 @@ module.exports = do -> xlfRowView: xlfRowView expandChoiceList: expandChoiceList paramsSettingsField: paramsSettingsField + paramsSimple: paramsSimple selectQuestionExpansion: selectQuestionExpansion groupView: groupView rowErrorView: rowErrorView From 8cc129ce9825e3afdcfc7a945052cbcaf2c43822 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Wed, 19 Dec 2018 21:29:49 +0100 Subject: [PATCH 11/44] bring back old look --- .../partials/form_builder/_params_view.scss | 33 ++++++++++++++++--- jsapp/xlform/src/view.params.coffee | 2 +- jsapp/xlform/src/view.params.templates.coffee | 2 +- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/jsapp/scss/stylesheets/partials/form_builder/_params_view.scss b/jsapp/scss/stylesheets/partials/form_builder/_params_view.scss index bb668d363c..90a24f3197 100644 --- a/jsapp/scss/stylesheets/partials/form_builder/_params_view.scss +++ b/jsapp/scss/stylesheets/partials/form_builder/_params_view.scss @@ -1,8 +1,31 @@ -.params-view { - .param-option { - display: block; - &:not(:last-child) { - margin-bottom: 10px; +.params-view__simple-wrapper { + @extend %card-content; + + .params-view { + padding: 10px; + + .param-option { + padding: 5px 10px; + display: inline-block; + } + } +} + +.survey__row--selected { + .params-view__simple-wrapper { + border-left-color: $linkColor; + border-right-color: $linkColor; + border-bottom-color: $linkColor; + } +} + +.card__settings__fields__field { + .params-view { + .param-option { + display: block; + &:not(:last-child) { + margin-bottom: 10px; + } } } } diff --git a/jsapp/xlform/src/view.params.coffee b/jsapp/xlform/src/view.params.coffee index d597cce70d..2d78dfe336 100644 --- a/jsapp/xlform/src/view.params.coffee +++ b/jsapp/xlform/src/view.params.coffee @@ -44,7 +44,7 @@ module.exports = do -> return class ParamOption extends $baseView - className: 'param-option' + className: 'param-option js-cancel-sort js-cancel-select-row' events: { 'input input': 'onChange' } diff --git a/jsapp/xlform/src/view.params.templates.coffee b/jsapp/xlform/src/view.params.templates.coffee index cc22c5a730..c339f5cfec 100644 --- a/jsapp/xlform/src/view.params.templates.coffee +++ b/jsapp/xlform/src/view.params.templates.coffee @@ -1,6 +1,6 @@ module.exports = do -> numberParam = (label, number, defaultValue) -> - if defaultValue + if typeof defaultValue isnt 'undefined' defaultValueAttr = "placeholder='#{defaultValue}'" return """ From 6426e9f666669ca647e3acd0ce7ca6150ad1cabf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20L=C3=A9ger?= Date: Wed, 19 Dec 2018 18:54:23 -0500 Subject: [PATCH 12/44] Monkey Patch django-storage 'flush_buffer' method --- kpi/models/import_export_task.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/kpi/models/import_export_task.py b/kpi/models/import_export_task.py index 295f46f1e4..11bf102ea4 100644 --- a/kpi/models/import_export_task.py +++ b/kpi/models/import_export_task.py @@ -35,6 +35,30 @@ from ..deployment_backends.mock_backend import MockDeploymentBackend +# TODO: Remove lines below (38:58) when django and django-storages are upgraded +# to latest version. +# Because current version of Django is 1.8, we can't upgrade `django-storages`. +# `Django 1.8` has been dropped in v1.6.6. Latest version (v1.7.2) requires `Django 1.11` +from storages.backends.s3boto3 import S3Boto3StorageFile + + +def _flush_write_buffer(self): + """ + Flushes the write buffer. + """ + if self._buffer_file_size: + self._write_counter += 1 + self.file.seek(0) + part = self._multipart.Part(self._write_counter) + part.upload(Body=self.file.read()) + self.file.seek(0) + self.file.truncate() + + +# Monkey Patch S3Boto3StorageFile class with 1.7.1 version of the method +S3Boto3StorageFile._flush_write_buffer = _flush_write_buffer + + def utcnow(*args, **kwargs): ''' Stupid, and exists only to facilitate mocking during unit testing. @@ -115,7 +139,7 @@ def run(self): # This method must be implemented by a subclass self._run_task(msgs) self.status = self.COMPLETE - except Exception, err: + except Exception as err: msgs['error_type'] = type(err).__name__ msgs['error'] = err.message self.status = self.ERROR @@ -131,7 +155,7 @@ def run(self): ).total_seconds() try: self.save(update_fields=['status', 'messages', 'data']) - except TypeError, e: + except TypeError as e: self.status = self.ERROR logging.error('Failed to save %s: %s' % (self._meta.model_name, repr(e)), @@ -576,6 +600,8 @@ def _run_task(self, messages): # TODO: chunk again once # https://github.com/jschneier/django-storages/issues/449 # is fixed + # TODO: Check if monkey-patch (line 57) can restore writing + # by chunk ''' while True: chunk = xlsx_output_file.read(5 * 1024 * 1024) From 2f642fb894150951ef34ada43c0a271e53fe9e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20L=C3=A9ger?= Date: Thu, 20 Dec 2018 17:04:25 -0500 Subject: [PATCH 13/44] Fixed typo --- kpi/models/import_export_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kpi/models/import_export_task.py b/kpi/models/import_export_task.py index 11bf102ea4..a2a414207c 100644 --- a/kpi/models/import_export_task.py +++ b/kpi/models/import_export_task.py @@ -38,7 +38,7 @@ # TODO: Remove lines below (38:58) when django and django-storages are upgraded # to latest version. # Because current version of Django is 1.8, we can't upgrade `django-storages`. -# `Django 1.8` has been dropped in v1.6.6. Latest version (v1.7.2) requires `Django 1.11` +# `Django 1.8` has been dropped in v1.6.6. Latest version (v1.7.1) requires `Django 1.11` from storages.backends.s3boto3 import S3Boto3StorageFile From c8969aedf9d5aab68aa309677b1c90f6883feeef Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Fri, 21 Dec 2018 14:43:14 +0100 Subject: [PATCH 14/44] display reports styes settings even for no report data --- jsapp/js/components/reports.es6 | 71 ++++++++++++++++----------------- 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/jsapp/js/components/reports.es6 b/jsapp/js/components/reports.es6 index 80393abbf8..ee07caf527 100644 --- a/jsapp/js/components/reports.es6 +++ b/jsapp/js/components/reports.es6 @@ -1099,7 +1099,6 @@ class Reports extends React.Component { var reportData = this.state.reportData || []; if (reportData.length) { - if (currentCustomReport && currentCustomReport.questions.length) { const currentQuestions = currentCustomReport.questions; const fullReportData = this.state.reportData; @@ -1109,7 +1108,6 @@ class Reports extends React.Component { if (this.state.reportLimit && reportData.length > this.state.reportLimit) { reportData = reportData.slice(0, this.state.reportLimit); } - } if (this.state.reportData === undefined) { @@ -1133,20 +1131,6 @@ class Reports extends React.Component { ); } - if (this.state.reportData && reportData.length === 0) { - return ( - - - - - {t('This report has no data.')} - - - - - ); - } - const formViewModifiers = []; if (this.state.isFullscreen) { formViewModifiers.push('fullscreen'); @@ -1158,27 +1142,41 @@ class Reports extends React.Component { {this.renderReportButtons()} - - -

{asset.name}

-
- {!this.state.currentCustomReport && this.state.reportLimit && reportData.length && this.state.reportData.length > this.state.reportLimit && - -
- {t('For performance reasons, this report only includes the first ## questions.').replace('##', this.state.reportLimit)} -
- -
- } + {this.state.reportData && reportData.length === 0 && + + + + {t('This report has no data.')} - -

{t('Warning')}

-

{t('This is an automated report based on raw data submitted to this project. Please conduct proper data cleaning prior to using the graphs and figures used on this page. ')}

-
- -
+ {this.state.groupBy !== '' && ' ' + t('Try changing Report Style to "No grouping".')} + + +
+ } + + {this.state.reportData && reportData.length !== 0 && + + +

{asset.name}

+
+ {!this.state.currentCustomReport && this.state.reportLimit && reportData.length && this.state.reportData.length > this.state.reportLimit && + +
+ {t('For performance reasons, this report only includes the first ## questions.').replace('##', this.state.reportLimit)} +
+ +
+ } + + +

{t('Warning')}

+

{t('This is an automated report based on raw data submitted to this project. Please conduct proper data cleaning prior to using the graphs and figures used on this page. ')}

+
+ +
+ } {this.state.showReportGraphSettings && @@ -1197,7 +1195,6 @@ class Reports extends React.Component { } -
From df34b276a30df4c5f4d4061ca19ca4c32f11d7ef Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Fri, 21 Dec 2018 16:13:12 +0100 Subject: [PATCH 15/44] check if provided data --- jsapp/js/components/reports.es6 | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/jsapp/js/components/reports.es6 b/jsapp/js/components/reports.es6 index ee07caf527..dba09233d3 100644 --- a/jsapp/js/components/reports.es6 +++ b/jsapp/js/components/reports.es6 @@ -911,6 +911,15 @@ class Reports extends React.Component { showReportGraphSettings: !this.state.showReportGraphSettings, }); } + hasAnyProvidedData (reportData) { + let hasAny = false; + reportData.map((rowContent, i)=>{ + if (rowContent.data.provided) { + hasAny = true; + } + }); + return hasAny; + } setCustomReport (e) { var crid = e ? e.target.getAttribute('data-crid') : false; @@ -1136,25 +1145,27 @@ class Reports extends React.Component { formViewModifiers.push('fullscreen'); } + const hasAnyProvidedData = this.hasAnyProvidedData(reportData); + return ( {this.renderReportButtons()} - {this.state.reportData && reportData.length === 0 && + {!hasAnyProvidedData && {t('This report has no data.')} - {this.state.groupBy !== '' && ' ' + t('Try changing Report Style to "No grouping".')} + {this.state.groupBy && ' ' + t('Try changing Report Style to "No grouping".')} } - {this.state.reportData && reportData.length !== 0 && + {hasAnyProvidedData &&

{asset.name}

@@ -1174,6 +1185,7 @@ class Reports extends React.Component {

{t('Warning')}

{t('This is an automated report based on raw data submitted to this project. Please conduct proper data cleaning prior to using the graphs and figures used on this page. ')}

+
} From c1af019578838695587060de13c71f41d9712737 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Sat, 22 Dec 2018 12:50:42 +0100 Subject: [PATCH 16/44] better groupBy check --- jsapp/js/components/reports.es6 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jsapp/js/components/reports.es6 b/jsapp/js/components/reports.es6 index dba09233d3..8637c3167e 100644 --- a/jsapp/js/components/reports.es6 +++ b/jsapp/js/components/reports.es6 @@ -1146,6 +1146,7 @@ class Reports extends React.Component { } const hasAnyProvidedData = this.hasAnyProvidedData(reportData); + const hasGroupBy = this.state.groupBy.length !== 0; return ( @@ -1159,7 +1160,7 @@ class Reports extends React.Component { {t('This report has no data.')} - {this.state.groupBy && ' ' + t('Try changing Report Style to "No grouping".')} + {hasGroupBy && ' ' + t('Try changing Report Style to "No grouping".')} From 168b4fcd0500ee094c9d01a9ef61d200c466aef3 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Thu, 27 Dec 2018 18:08:35 +0100 Subject: [PATCH 17/44] render editable hint under question label, use input --- .../partials/form_builder/_card.scss | 9 ++++++++- jsapp/xlform/src/view.row.coffee | 6 ++++-- jsapp/xlform/src/view.row.templates.coffee | 1 + jsapp/xlform/src/view.rowDetail.coffee | 17 +++++++++++++---- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/jsapp/scss/stylesheets/partials/form_builder/_card.scss b/jsapp/scss/stylesheets/partials/form_builder/_card.scss index 13dfdd8295..316328e151 100644 --- a/jsapp/scss/stylesheets/partials/form_builder/_card.scss +++ b/jsapp/scss/stylesheets/partials/form_builder/_card.scss @@ -20,7 +20,7 @@ } .card__header { border: $cardBorderStyle; - padding: 30px 40px 30px 75px; + padding: 20px 40px 20px 75px; display: block; background: white; cursor: move; @@ -65,6 +65,13 @@ cursor:text; // white-space: pre; } + .card__header-hint { + color: inherit; + font-weight: 400; + &::placeholder { + color: $cool-silver; + } + } .card__header-subtitle { margin: 10px 0 0 0; color: #aaa; diff --git a/jsapp/xlform/src/view.row.coffee b/jsapp/xlform/src/view.row.coffee index a85d1af433..9488a7b2b6 100644 --- a/jsapp/xlform/src/view.row.coffee +++ b/jsapp/xlform/src/view.row.coffee @@ -68,6 +68,7 @@ module.exports = do -> _renderRow: -> @$el.html $viewTemplates.$$render('row.xlfRowView', @surveyView) @$label = @$('.card__header-title') + @$hint = @$('.card__header-hint') @$card = @$('.card') @$header = @$('.card__header') context = {warnings: []} @@ -79,7 +80,7 @@ module.exports = do -> @cardSettingsWrap = @$('.card__settings').eq(0) @defaultRowDetailParent = @cardSettingsWrap.find('.card__settings__fields--question-options').eq(0) - for [key, val] in @model.attributesArray() when key is 'label' or key is 'type' + for [key, val] in @model.attributesArray() when key in ['label', 'hint', 'type'] view = new $viewRowDetail.DetailView(model: val, rowView: @) if key == 'label' and @model.get('type').get('value') == 'calculate' view.model = @model.get('calculation') @@ -208,7 +209,8 @@ module.exports = do -> @defaultRowDetailParent = @cardSettingsWrap.find('.card__settings__fields--question-options').eq(0) # don't display columns that start with a $ - for [key, val] in @model.attributesArray() when !key.match(/^\$/) and key not in ["label", "type", "select_from_list_name", 'kobo--matrix_list', 'parameters'] + hiddenFields = ['label', 'hint', 'type', 'select_from_list_name', 'kobo--matrix_list', 'parameters'] + for [key, val] in @model.attributesArray() when !key.match(/^\$/) and key not in hiddenFields new $viewRowDetail.DetailView(model: val, rowView: @).render().insertInDOM(@) questionType = @model.get('type').get('typeId') diff --git a/jsapp/xlform/src/view.row.templates.coffee b/jsapp/xlform/src/view.row.templates.coffee index 07a7068f6c..b80b2c591e 100644 --- a/jsapp/xlform/src/view.row.templates.coffee +++ b/jsapp/xlform/src/view.row.templates.coffee @@ -63,6 +63,7 @@ module.exports = do ->
+
diff --git a/jsapp/xlform/src/view.rowDetail.coffee b/jsapp/xlform/src/view.rowDetail.coffee index 3c1162e5c5..fa74f4554a 100644 --- a/jsapp/xlform/src/view.rowDetail.coffee +++ b/jsapp/xlform/src/view.rowDetail.coffee @@ -96,6 +96,11 @@ module.exports = do -> if transformFn $elVal = transformFn($elVal) changeModelValue($elVal) + + $el.on('keyup', (evt) => + if evt.key is 'Enter' or evt.keyCode is 13 + $el.blur() + ) return _insertInDOM: (where, how) -> @@ -161,11 +166,15 @@ module.exports = do -> @ viewRowDetail.DetailViewMixins.hint = - html: -> - @$el.addClass("card__settings__fields--active") - viewRowDetail.Templates.textbox @cid, @model.key, _t("Question hint"), 'text' + html: -> false + insertInDOM: (rowView) -> + hintEl = rowView.$hint + return @ afterRender: -> - @listenForInputChange() + @listenForInputChange({ + el: this.rowView.$hint + }) + return viewRowDetail.DetailViewMixins.guidance_hint = html: -> From 5053316eeaacbe202625d47600b2a81d0f47b7ae Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Thu, 27 Dec 2018 18:13:38 +0100 Subject: [PATCH 18/44] better differentiation for placeholder --- jsapp/scss/stylesheets/partials/form_builder/_card.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/jsapp/scss/stylesheets/partials/form_builder/_card.scss b/jsapp/scss/stylesheets/partials/form_builder/_card.scss index 316328e151..555c0335d3 100644 --- a/jsapp/scss/stylesheets/partials/form_builder/_card.scss +++ b/jsapp/scss/stylesheets/partials/form_builder/_card.scss @@ -70,6 +70,7 @@ font-weight: 400; &::placeholder { color: $cool-silver; + font-style: italic; } } .card__header-subtitle { From 2810ccd0a6550d890ebbee410fe622fb3e819e20 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Fri, 28 Dec 2018 19:10:49 +0100 Subject: [PATCH 19/44] cleanup project deletion flow plus display asset name during deletion --- jsapp/js/components/assetrow.es6 | 9 ++++---- jsapp/js/components/header.es6 | 2 +- .../components/modalForms/projectSettings.es6 | 1 + jsapp/js/mixins.es6 | 22 ++++++++++--------- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/jsapp/js/components/assetrow.es6 b/jsapp/js/components/assetrow.es6 index 0111060db0..4ce09ffb84 100644 --- a/jsapp/js/components/assetrow.es6 +++ b/jsapp/js/components/assetrow.es6 @@ -424,10 +424,11 @@ class AssetRow extends React.Component { } {userCanEdit && + m={'delete'} + data-action={'delete'} + data-asset-type={this.props.kind} + data-asset-name={this.props.name} + > {t('Delete')} diff --git a/jsapp/js/components/header.es6 b/jsapp/js/components/header.es6 index 7c278da12c..34221ebc4d 100644 --- a/jsapp/js/components/header.es6 +++ b/jsapp/js/components/header.es6 @@ -270,7 +270,7 @@ class MainHeader extends Reflux.Component { name='title' placeholder={t('Project title')} value={this.state.asset.name ? this.state.asset.name : ''} - onChange={this.assetTitleChange} + onChange={this.assetTitleChange.bind(this)} onKeyDown={this.assetTitleKeyDown} disabled={!userCanEditAsset} /> diff --git a/jsapp/js/components/modalForms/projectSettings.es6 b/jsapp/js/components/modalForms/projectSettings.es6 index bf1e447e0c..c820c927a0 100644 --- a/jsapp/js/components/modalForms/projectSettings.es6 +++ b/jsapp/js/components/modalForms/projectSettings.es6 @@ -234,6 +234,7 @@ class ProjectSettings extends React.Component { deleteProject() { this.deleteAsset( this.state.formAsset.uid, + this.state.formAsset.name, this.goToProjectsList.bind(this) ); } diff --git a/jsapp/js/mixins.es6 b/jsapp/js/mixins.es6 index a3a0351c26..491d6a4d5c 100644 --- a/jsapp/js/mixins.es6 +++ b/jsapp/js/mixins.es6 @@ -163,8 +163,8 @@ mixins.dmix = { mixins.clickAssets.click.asset.unarchive(uid, callback); } }, - deleteAsset (uid, callback) { - mixins.clickAssets.click.asset.delete(uid, callback); + deleteAsset (uid, name, callback) { + mixins.clickAssets.click.asset.delete(uid, name, callback); }, toggleDeploymentHistory () { this.setState({ @@ -513,11 +513,11 @@ mixins.clickAssets = { else hashHistory.push(`/forms/${uid}/edit`); }, - delete: function(uid, callback){ - let asset = stores.selectedAsset.asset || stores.allAssets.byUid[uid]; - var assetTypeLabel = t('project'); + delete: function(uid, name, callback) { + const asset = stores.selectedAsset.asset || stores.allAssets.byUid[uid]; + let assetTypeLabel = ASSET_TYPES.survey.label; - if (asset.asset_type != 'survey') { + if (asset.asset_type != ASSET_TYPES.survey.id) { assetTypeLabel = t('library item'); } @@ -528,7 +528,6 @@ mixins.clickAssets = { actions.resources.deleteAsset({uid: uid}, { onComplete: ()=> { notify(`${assetTypeLabel} ${t('deleted permanently')}`); - $('.alertify-toggle input').prop('checked', false); if (typeof callback === 'function') { callback(); } @@ -537,7 +536,7 @@ mixins.clickAssets = { }; if (!deployed) { - if (asset.asset_type != 'survey') + if (asset.asset_type != ASSET_TYPES.survey.id) msg = t('You are about to permanently delete this item from your library.'); else msg = t('You are about to permanently delete this draft.'); @@ -552,10 +551,13 @@ mixins.clickAssets = { onshow = (evt) => { let ok_button = dialog.elements.buttons.primary.firstChild; let $els = $('.alertify-toggle input'); + ok_button.disabled = true; + $els.each(function () {$(this).prop('checked', false);}); + $els.change(function () { ok_button.disabled = false; - $els.each(function ( index ) { + $els.each(function () { if (!$(this).prop('checked')) { ok_button.disabled = true; } @@ -564,7 +566,7 @@ mixins.clickAssets = { }; } let opts = { - title: `${t('Delete')} ${assetTypeLabel}`, + title: `${t('Delete')} ${assetTypeLabel} "${name}"`, message: msg, labels: { ok: t('Delete'), From f52b4419a526a9f0df2dc2befaa76a7569992b68 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Fri, 28 Dec 2018 20:57:30 +0100 Subject: [PATCH 20/44] display asset type label instead of vague "library item" --- jsapp/js/mixins.es6 | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/jsapp/js/mixins.es6 b/jsapp/js/mixins.es6 index 491d6a4d5c..8712260b4e 100644 --- a/jsapp/js/mixins.es6 +++ b/jsapp/js/mixins.es6 @@ -515,11 +515,7 @@ mixins.clickAssets = { }, delete: function(uid, name, callback) { const asset = stores.selectedAsset.asset || stores.allAssets.byUid[uid]; - let assetTypeLabel = ASSET_TYPES.survey.label; - - if (asset.asset_type != ASSET_TYPES.survey.id) { - assetTypeLabel = t('library item'); - } + let assetTypeLabel = ASSET_TYPES[asset.asset_type].label; let dialog = alertify.dialog('confirm'); let deployed = asset.has_deployment; From 356fae0b30264ceb8578f8043a5ed4f2949fb993 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Fri, 28 Dec 2018 21:10:43 +0100 Subject: [PATCH 21/44] pass firstQuestionName to assetRow and use it plus use ASSET_TYPES constant --- jsapp/js/components/searchcollectionlist.es6 | 14 ++++++++++++++ jsapp/js/ui.es6 | 1 + 2 files changed, 15 insertions(+) diff --git a/jsapp/js/components/searchcollectionlist.es6 b/jsapp/js/components/searchcollectionlist.es6 index 965a251ec3..5cc55b4acf 100644 --- a/jsapp/js/components/searchcollectionlist.es6 +++ b/jsapp/js/components/searchcollectionlist.es6 @@ -13,6 +13,7 @@ import DocumentTitle from 'react-document-title'; import $ from 'jquery'; import Dropzone from 'react-dropzone'; import {t, validFileTypes} from '../utils'; +import {ASSET_TYPES} from '../constants'; class SearchCollectionList extends Reflux.Component { constructor(props) { @@ -78,6 +79,18 @@ class SearchCollectionList extends Reflux.Component { var isSelected = stores.selectedAsset.uid === resource.uid; var ownedCollections = this.state.ownedCollections; + // for unnamed assets, we try to display first question name + let firstQuestionName; + if ( + resource.asset_type !== ASSET_TYPES.survey.id && + resource.name === '' && + resource.summary && + resource.summary.labels && + resource.summary.labels.length > 0 + ) { + firstQuestionName = resource.summary.labels[0] + } + return ( ); diff --git a/jsapp/js/ui.es6 b/jsapp/js/ui.es6 index 178e668392..5a1be05a20 100644 --- a/jsapp/js/ui.es6 +++ b/jsapp/js/ui.es6 @@ -181,6 +181,7 @@ class AssetName extends React.Component { var row_count; if (!name) { row_count = summary.row_count; + // for unnamed assets, we try to display first question name name = summary.labels ? summary.labels[0] : false; if (!name) { isEmpty = true; From b16d88ce774cd8a8df8e2f487493121cdd18b620 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Fri, 28 Dec 2018 21:10:58 +0100 Subject: [PATCH 22/44] forgot to commit file --- jsapp/js/components/assetrow.es6 | 38 +++++++++++++++++--------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/jsapp/js/components/assetrow.es6 b/jsapp/js/components/assetrow.es6 index 4ce09ffb84..0ebbdb3040 100644 --- a/jsapp/js/components/assetrow.es6 +++ b/jsapp/js/components/assetrow.es6 @@ -95,10 +95,12 @@ class AssetRow extends React.Component { ownedCollections = [], parent = undefined; - var isDeployable = this.props.asset_type && this.props.asset_type === 'survey' && this.props.deployed_version_id === null; + var isDeployable = this.props.asset_type && this.props.asset_type === ASSET_TYPES.survey.id && this.props.deployed_version_id === null; const userCanEdit = this.userCan('change_asset', this.props); + const assetName = this.props.name || this.props.firstQuestionName; + if (this.props.has_deployment && this.props.deployment__submission_count && this.userCan('view_submissions', this.props)) { hrefTo = `/forms/${this.props.uid}/summary`; @@ -160,7 +162,7 @@ class AssetRow extends React.Component { - { this.props.asset_type && this.props.asset_type === 'survey' && this.props.settings.description && + { this.props.asset_type && this.props.asset_type === ASSET_TYPES.survey.id && this.props.settings.description && {this.props.settings.description} @@ -187,19 +189,19 @@ class AssetRow extends React.Component { key={'userlink'} className={[ 'mdl-cell', - this.props.asset_type == 'survey' ? 'mdl-cell--2-col mdl-cell--1-col-tablet mdl-cell--hide-phone' : 'mdl-cell--2-col mdl-cell--2-col-tablet mdl-cell--1-col-phone' + this.props.asset_type == ASSET_TYPES.survey.id ? 'mdl-cell--2-col mdl-cell--1-col-tablet mdl-cell--hide-phone' : 'mdl-cell--2-col mdl-cell--2-col-tablet mdl-cell--1-col-phone' ]} > - { this.props.asset_type == 'survey' && + { this.props.asset_type == ASSET_TYPES.survey.id && { selfowned ? ' ' : this.props.owner__username } } - { this.props.asset_type != 'survey' && + { this.props.asset_type != ASSET_TYPES.survey.id && {selfowned ? t('me') : this.props.owner__username} } {/* "date created" column for surveys */} - { this.props.asset_type == 'survey' && + { this.props.asset_type == ASSET_TYPES.survey.id && {/* "submission count" column for surveys */} - { this.props.asset_type == 'survey' && + { this.props.asset_type == ASSET_TYPES.survey.id && @@ -298,7 +300,7 @@ class AssetRow extends React.Component { data-action={'cloneAsSurvey'} data-tip={t('Create project')} data-asset-type={this.props.kind} - data-asset-name={this.props.name} + data-asset-name={assetName} data-disabled={false} > @@ -327,7 +329,7 @@ class AssetRow extends React.Component { clearPopover={this.state.clearPopover} popoverSetVisible={this.popoverSetVisible} > - { this.props.asset_type && this.props.asset_type === 'survey' && userCanEdit && isDeployable && + { this.props.asset_type && this.props.asset_type === ASSET_TYPES.survey.id && userCanEdit && isDeployable && } - { this.props.asset_type && this.props.asset_type === 'survey' && this.props.has_deployment && !this.props.deployment__active && userCanEdit && + { this.props.asset_type && this.props.asset_type === ASSET_TYPES.survey.id && this.props.has_deployment && !this.props.deployment__active && userCanEdit && } - { this.props.asset_type && this.props.asset_type === 'survey' && userCanEdit && + { this.props.asset_type && this.props.asset_type === ASSET_TYPES.survey.id && userCanEdit && ); })} - { this.props.asset_type && this.props.asset_type != 'survey' && ownedCollections.length > 0 && + { this.props.asset_type && this.props.asset_type != ASSET_TYPES.survey.id && ownedCollections.length > 0 && {t('Move to')} } - { this.props.asset_type && this.props.asset_type != 'survey' && ownedCollections.length > 0 && + { this.props.asset_type && this.props.asset_type != ASSET_TYPES.survey.id && ownedCollections.length > 0 && {ownedCollections.map((col)=>{ return ( @@ -401,7 +403,7 @@ class AssetRow extends React.Component { })} } - { this.props.asset_type && this.props.asset_type === 'survey' && this.props.has_deployment && this.props.deployment__active && userCanEdit && + { this.props.asset_type && this.props.asset_type === ASSET_TYPES.survey.id && this.props.has_deployment && this.props.deployment__active && userCanEdit && } - { this.props.asset_type && this.props.asset_type === 'survey' && userCanEdit && + { this.props.asset_type && this.props.asset_type === ASSET_TYPES.survey.id && userCanEdit && {t('Create template')} @@ -427,7 +429,7 @@ class AssetRow extends React.Component { m={'delete'} data-action={'delete'} data-asset-type={this.props.kind} - data-asset-name={this.props.name} + data-asset-name={assetName} > {t('Delete')} From 6617c383457ddc3d9fdb68778b9fda9c2eeb5977 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Sat, 29 Dec 2018 21:00:39 +0100 Subject: [PATCH 23/44] display stringified values instead of crashing --- jsapp/xlform/src/view.row.coffee | 3 ++- jsapp/xlform/src/view.surveyApp.coffee | 11 ++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/jsapp/xlform/src/view.row.coffee b/jsapp/xlform/src/view.row.coffee index a85d1af433..26be0b50bf 100644 --- a/jsapp/xlform/src/view.row.coffee +++ b/jsapp/xlform/src/view.row.coffee @@ -12,6 +12,7 @@ $viewParams = require './view.params' $viewRowDetail = require './view.rowDetail' renderKobomatrix = require('js/formbuild/renderInBackbone').renderKobomatrix _t = require('utils').t +alertify = require 'alertifyjs' module.exports = do -> class BaseRowView extends Backbone.View @@ -179,7 +180,7 @@ module.exports = do -> $appearanceField.find('input:checkbox').prop('checked', false) appearanceModel = @model.get('appearance') if appearanceModel.getValue() - @surveyView.ngScope.miscUtils.alert(_t("You can't display nested groups on the same screen - the setting has been removed from the parent group")) + alertify.warning(_t("You can't display nested groups on the same screen - the setting has been removed from the parent group")) appearanceModel.set('value', '') @model.on 'remove', (row) => diff --git a/jsapp/xlform/src/view.surveyApp.coffee b/jsapp/xlform/src/view.surveyApp.coffee index 86624f84ab..aca3f1e58e 100644 --- a/jsapp/xlform/src/view.surveyApp.coffee +++ b/jsapp/xlform/src/view.surveyApp.coffee @@ -11,6 +11,7 @@ $rowView = require './view.row' $baseView = require './view.pluggedIn.backboneView' $viewUtils = require './view.utils' _t = require('utils').t +alertify = require 'alertifyjs' module.exports = do -> surveyApp = {} @@ -681,16 +682,8 @@ module.exports = do -> enketoServer: window.koboConfigs?.enketoServer or false enketoPreviewUri: window.koboConfigs?.enketoPreviewUri or false onSuccess: => @onEscapeKeydown = $viewUtils.enketoIframe.close - onError: (message, opts)=> @alert message, opts + onError: (message)=> alertify.error(message) return - - alert: (message, opts={}) -> - title = opts.title or 'Error' - $('.alert-modal').html(message).dialog('option', { - title: title, - width: 500, - dialogClass: 'surveyapp__alert' - }).dialog 'open' downloadButtonClick: (evt)-> # Download = save a CSV file to the disk surveyCsv = @survey.toCSV() From ddd1a9c9d5a92a3889760998dcaf00b200e33fcd Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Sat, 29 Dec 2018 21:00:52 +0100 Subject: [PATCH 24/44] display stringified values instead of crashing --- jsapp/js/components/modalForms/submission.es6 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/jsapp/js/components/modalForms/submission.es6 b/jsapp/js/components/modalForms/submission.es6 index 6c66d6ffb3..4beb3a6164 100644 --- a/jsapp/js/components/modalForms/submission.es6 +++ b/jsapp/js/components/modalForms/submission.es6 @@ -263,6 +263,13 @@ class Submission extends React.Component { case 'video': return this.renderAttachment(submissionValue, q.type); break; + case 'begin_repeat': + const list = submissionValue.map((r) => { + const stringified = JSON.stringify(r); + return
  • {stringified}
  • + }); + return
      {list}
    + break; default: return submissionValue; break; From 4d18de3ba9fc87cf23e0148d757dc6f111e825f8 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Wed, 2 Jan 2019 17:21:43 +0100 Subject: [PATCH 25/44] define read_only as visible property --- jsapp/xlform/src/model.base.coffee | 17 ++++++++++++++--- jsapp/xlform/src/model.configs.coffee | 21 ++++++++++++--------- jsapp/xlform/src/view.rowDetail.coffee | 7 +++++++ 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/jsapp/xlform/src/model.base.coffee b/jsapp/xlform/src/model.base.coffee index 37cc5638d6..2c9f2cd2bf 100644 --- a/jsapp/xlform/src/model.base.coffee +++ b/jsapp/xlform/src/model.base.coffee @@ -161,9 +161,20 @@ module.exports = do -> # @_parent.trigger "change", @key, val, ctxt # when attributes change, register changes with parent survey - if @key in ["name", "label", "hint", "guidance_hint", "required", - "calculation", "default", "appearance", - "constraint_message", "tags"] or @key.match(/^.+::.+/) + observedAttrs = [ + 'name', + 'label', + 'hint', + 'guidance_hint', + 'required', + 'read_only' + 'calculation', + 'default', + 'appearance', + 'constraint_message', + 'tags' + ] + if @key in observedAttrs or @key.match(/^.+::.+/) @on "change", (changes)=> @getSurvey().trigger "change", changes diff --git a/jsapp/xlform/src/model.configs.coffee b/jsapp/xlform/src/model.configs.coffee index b5c5e9c7f6..caa04a765a 100644 --- a/jsapp/xlform/src/model.configs.coffee +++ b/jsapp/xlform/src/model.configs.coffee @@ -202,15 +202,16 @@ module.exports = do -> } configs.columns = [ - "type", - "name", - "label", - "hint", - "guidance_hint", - "required", - "relevant", - "default", - "constraint" + 'type', + 'name', + 'label', + 'hint', + 'guidance_hint', + 'required', + 'read_only', + 'relevant', + 'default', + 'constraint' ] configs.lookupRowType = do-> @@ -280,6 +281,8 @@ module.exports = do -> required: value: false _hideUnlessChanged: true + read_only: + value: false relevant: value: "" _hideUnlessChanged: true diff --git a/jsapp/xlform/src/view.rowDetail.coffee b/jsapp/xlform/src/view.rowDetail.coffee index 3c1162e5c5..94dd304540 100644 --- a/jsapp/xlform/src/view.rowDetail.coffee +++ b/jsapp/xlform/src/view.rowDetail.coffee @@ -377,6 +377,13 @@ module.exports = do -> afterRender: -> @listenForCheckboxChange() + viewRowDetail.DetailViewMixins.read_only = + html: -> + @$el.addClass("card__settings__fields--active") + viewRowDetail.Templates.checkbox @cid, @model.key, _t("Read Only") + afterRender: -> + @listenForCheckboxChange() + viewRowDetail.DetailViewMixins.appearance = getTypes: () -> types = From 1000a9e0f4b5e68c4894585361788969bd3b9e9c Mon Sep 17 00:00:00 2001 From: "John N. Milner" Date: Wed, 2 Jan 2019 22:49:17 -0500 Subject: [PATCH 26/44] Define shadow UserObjectPermission; fixes #2148 Also removes django-guardian dependency --- dependencies/pip/dev_requirements.txt | 1 - dependencies/pip/external_services.txt | 3 +- dependencies/pip/requirements.in | 3 - dependencies/pip/requirements.txt | 1 - kobo/settings.py | 1 - .../kc_access/shadow_models.py | 74 +++++++++++++++++-- kpi/deployment_backends/kc_access/utils.py | 27 +------ .../commands/sync_kobocat_xforms.py | 3 +- 8 files changed, 72 insertions(+), 41 deletions(-) diff --git a/dependencies/pip/dev_requirements.txt b/dependencies/pip/dev_requirements.txt index 23cb02ca6a..b4114d3881 100644 --- a/dependencies/pip/dev_requirements.txt +++ b/dependencies/pip/dev_requirements.txt @@ -29,7 +29,6 @@ django-celery-beat==1.1.1 django-constance[database]==2.2.0 django-debug-toolbar==1.6 django-extensions==1.7.6 -django-guardian==1.4.1 django-haystack==2.6.0 django-jsonbfield==0.1.0 django-loginas==0.2.3 diff --git a/dependencies/pip/external_services.txt b/dependencies/pip/external_services.txt index 24586d8ef3..c23ceb0e69 100644 --- a/dependencies/pip/external_services.txt +++ b/dependencies/pip/external_services.txt @@ -30,7 +30,6 @@ django-celery-beat==1.1.1 django-constance[database]==2.2.0 django-debug-toolbar==1.4 django-extensions==1.6.7 -django-guardian==1.4.1 django-haystack==2.6.0 django-jsonbfield==0.1.0 django-loginas==0.2.3 @@ -85,7 +84,7 @@ pytest==3.0.3 # via pytest-django python-dateutil==2.6.0 python-digest==1.7 pytz==2016.4 -pyxform==0.11.5 +pyxform==0.12.0 raven==5.32.0 requests==2.10.0 responses==0.9.0 diff --git a/dependencies/pip/requirements.in b/dependencies/pip/requirements.in index c691da0f35..78b4935dcb 100644 --- a/dependencies/pip/requirements.in +++ b/dependencies/pip/requirements.in @@ -9,9 +9,6 @@ python-digest==1.7 -e git+https://github.com/dimagi/django-digest@0eb1c921329dd187c343b61acfbec4e98450136e#egg=django_digest -# django-guardian must match KoBoCAT's version -django-guardian==1.4.1 - # Regular PyPI packages Django<1.9 Markdown diff --git a/dependencies/pip/requirements.txt b/dependencies/pip/requirements.txt index 0d0b74424b..589058c7d0 100644 --- a/dependencies/pip/requirements.txt +++ b/dependencies/pip/requirements.txt @@ -29,7 +29,6 @@ django-celery-beat==1.1.1 django-constance[database]==2.2.0 django-debug-toolbar==1.4 django-extensions==1.6.7 -django-guardian==1.4.1 django-haystack==2.6.0 django-jsonbfield==0.1.0 django-loginas==0.2.3 diff --git a/kobo/settings.py b/kobo/settings.py index e55b8ca2b9..0568515bf5 100644 --- a/kobo/settings.py +++ b/kobo/settings.py @@ -98,7 +98,6 @@ 'kobo.apps.service_health', 'constance', 'constance.backends.database', - 'guardian', # For access to KC permissions ONLY 'kobo.apps.hook', 'django_celery_beat', ) diff --git a/kpi/deployment_backends/kc_access/shadow_models.py b/kpi/deployment_backends/kc_access/shadow_models.py index cbac408bc4..2a48a30112 100644 --- a/kpi/deployment_backends/kc_access/shadow_models.py +++ b/kpi/deployment_backends/kc_access/shadow_models.py @@ -1,9 +1,16 @@ -from django.contrib.auth.models import User -from django.contrib.contenttypes.models import ContentType +from hashlib import md5 + from django.db import models +from django.conf import settings from django.db import ProgrammingError -from django.utils.translation import ugettext_lazy -from hashlib import md5 +from django.utils.translation import ugettext_lazy as _ +from django.contrib.auth.models import User, Permission +from django.contrib.contenttypes.models import ContentType + +try: + from django.contrib.contenttypes.fields import GenericForeignKey +except ImportError: + from django.contrib.contenttypes.generic import GenericForeignKey from jsonfield import JSONField @@ -42,12 +49,19 @@ def UserProfile(self): self._define() return self._UserProfile + @property + def UserObjectPermission(self): + if not hasattr(self, '_UserObjectPermission'): + self._define() + return self._UserObjectPermission + @staticmethod def get_content_type_for_model(model): MODEL_NAME_MAPPING = { '_readonlyxform': ('logger', 'xform'), '_readonlyinstance': ('logger', 'instance'), - '_userprofile': ('main', 'userprofile') + '_userprofile': ('main', 'userprofile'), + '_userobjectpermission': ('guardian', 'userobjectpermission'), } try: app_label, model_name = MODEL_NAME_MAPPING[model._meta.model_name] @@ -88,6 +102,7 @@ def prefixed_hash(self): ''' Matches what's returned by the KC API ''' return u"md5:%s" % self.hash + class _ReadOnlyInstance(_ReadOnlyModel): class Meta: managed = False @@ -105,6 +120,7 @@ class Meta: default=u'submitted_via_web') uuid = models.CharField(max_length=249, default=u'') + class _UserProfile(models.Model): ''' From onadata/apps/main/models/user_profile.py @@ -129,7 +145,7 @@ class Meta: description = models.CharField(max_length=255, blank=True) require_auth = models.BooleanField( default=False, - verbose_name=ugettext_lazy( + verbose_name=_( "Require authentication to see forms and submit data" ) ) @@ -139,9 +155,55 @@ class Meta: num_of_submissions = models.IntegerField(default=0) metadata = JSONField(default={}, blank=True) + + class _UserObjectPermission(models.Model): + ''' + For the _sole purpose_ of letting us manipulate KoBoCAT + permissions, this comprises the following django-guardian classes + all condensed into one: + + * UserObjectPermission + * UserObjectPermissionBase + * BaseGenericObjectPermission + * BaseObjectPermission + + CAVEAT LECTOR: The django-guardian custom manager, + UserObjectPermissionManager, is NOT included! + ''' + permission = models.ForeignKey(Permission) + content_type = models.ForeignKey(ContentType) + object_pk = models.CharField(_('object ID'), max_length=255) + content_object = GenericForeignKey(fk_field='object_pk') + user = models.ForeignKey( + getattr(settings, 'AUTH_USER_MODEL', 'auth.User')) + + class Meta: + db_table = 'guardian_userobjectpermission' + unique_together = ['user', 'permission', 'object_pk'] + + def __unicode__(self): + return '%s | %s | %s' % ( + unicode(self.content_object), + unicode(getattr(self, 'user', False) or self.group), + unicode(self.permission.codename)) + + def save(self, *args, **kwargs): + content_type = ContentType.objects.get_for_model( + self.content_object) + if content_type != self.permission.content_type: + raise ValidationError( + "Cannot persist permission not designed for this " + "class (permission's type is %r and object's type is " + "%r)" + % (self.permission.content_type, content_type) + ) + return super(UserObjectPermission, self).save(*args, **kwargs) + + self._XForm = _ReadOnlyXform self._Instance = _ReadOnlyInstance self._UserProfile = _UserProfile + self._UserObjectPermission = _UserObjectPermission _models = LazyModelGroup() diff --git a/kpi/deployment_backends/kc_access/utils.py b/kpi/deployment_backends/kc_access/utils.py index 70a920cf51..fe319a5747 100644 --- a/kpi/deployment_backends/kc_access/utils.py +++ b/kpi/deployment_backends/kc_access/utils.py @@ -6,7 +6,6 @@ from django.contrib.contenttypes.models import ContentType from django.core.checks import Warning, register as register_check from django.db import ProgrammingError, transaction -from guardian.models import UserObjectPermission from rest_framework.authtoken.models import Token import requests @@ -250,15 +249,12 @@ def assign_applicable_kc_permissions(obj, user, kpi_codenames): return set_kc_anonymous_permissions_xform_flags( obj, kpi_codenames, xform_id) xform_content_type = ContentType.objects.get(**obj.KC_CONTENT_TYPE_KWARGS) + UserObjectPermission = _models.UserObjectPermission kc_permissions_already_assigned = UserObjectPermission.objects.filter( user=user, permission__in=permissions, object_pk=xform_id, ).values_list('permission__codename', flat=True) permissions_to_create = [] for permission in permissions: - # Since `logger` isn't in `INSTALLED_APPS`, `get_or_create()` raises - # `AttributeError: 'NoneType' object has no attribute '_base_manager'`. - # We hack around this with `bulk_create()`, which bypasses - # `UserObjectPermission.save()` if permission.codename in kc_permissions_already_assigned: continue permissions_to_create.append(UserObjectPermission( @@ -291,27 +287,8 @@ def remove_applicable_kc_permissions(obj, user, kpi_codenames): return set_kc_anonymous_permissions_xform_flags( obj, kpi_codenames, xform_id, remove=True) content_type_kwargs = _get_content_type_kwargs(obj) - # Do NOT try to `print` or do anything else that would `repr()` this - # queryset, or you'll be greeted by - # `AttributeError: 'NoneType' object has no attribute '_base_manager'` - UserObjectPermission.objects.filter( + _models.UserObjectPermission.objects.filter( user=user, permission__in=permissions, object_pk=xform_id, # `permission` has a FK to `ContentType`, but I'm paranoid **content_type_kwargs ).delete() - - -@register_check() -def guardian_message(app_configs, **kwargs): - r""" - Including `guardian` in `INSTALLED_APPS` but not using its - authentication backend causes Guardian to raise a warning through the - Django system check framework. Here we raise our own warning - instructing the administrator to ignore Guardian's warning. - """ - return [ - Warning( - '*** Please disregard warning guardian.W001. ***', - id='guardian.W001' - ) - ] diff --git a/kpi/management/commands/sync_kobocat_xforms.py b/kpi/management/commands/sync_kobocat_xforms.py index cb0ed0ff7e..401ddf16a1 100644 --- a/kpi/management/commands/sync_kobocat_xforms.py +++ b/kpi/management/commands/sync_kobocat_xforms.py @@ -16,7 +16,6 @@ from django.core.files.storage import get_storage_class from django.core.management.base import BaseCommand from django.db import models, transaction -from guardian.models import UserObjectPermission from rest_framework.authtoken.models import Token from formpack.utils.xls_to_ss_structure import xls_to_dicts @@ -303,7 +302,7 @@ def _sync_permissions(asset, xform): return [] # Get all applicable KC permissions set for this xform - xform_user_perms = UserObjectPermission.objects.filter( + xform_user_perms = _models.UserObjectPermission.objects.filter( permission_id__in=PERMISSIONS_MAP.keys(), content_type=XFORM_CT, object_pk=xform.pk From 60e3b11e8567ab28fc0c9522d80aca21a2338040 Mon Sep 17 00:00:00 2001 From: "John N. Milner" Date: Thu, 3 Jan 2019 01:15:50 -0500 Subject: [PATCH 27/44] Stop trying to remove shadow model instances ...and fix a troublesome `__unicode__()` method copied from django-guardian --- hub/actions.py | 4 ++-- .../kc_access/shadow_models.py | 22 +++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/hub/actions.py b/hub/actions.py index 9f1404c316..13aba4ff4c 100644 --- a/hub/actions.py +++ b/hub/actions.py @@ -13,7 +13,7 @@ from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy, ugettext as _ -from kpi.deployment_backends.kc_access.shadow_models import _ReadOnlyModel +from kpi.deployment_backends.kc_access.shadow_models import _ShadowModel def delete_related_objects(modeladmin, request, queryset): """ @@ -47,7 +47,7 @@ def delete_related_objects(modeladmin, request, queryset): # element. We can skip it since delete() on the first # level of related objects will cascade. continue - elif not isinstance(obj, _ReadOnlyModel): + elif not isinstance(obj, _ShadowModel): first_level_related_objects.append(obj) # Populate deletable_objects, a data structure of (string representations diff --git a/kpi/deployment_backends/kc_access/shadow_models.py b/kpi/deployment_backends/kc_access/shadow_models.py index 2a48a30112..4c402586d7 100644 --- a/kpi/deployment_backends/kc_access/shadow_models.py +++ b/kpi/deployment_backends/kc_access/shadow_models.py @@ -19,7 +19,12 @@ class ReadOnlyModelError(ValueError): pass -class _ReadOnlyModel(models.Model): +class _ShadowModel(models.Model): + ''' Allows identification of writeable and read-only shadow models ''' + class Meta: + abstract = True + +class _ReadOnlyModel(_ShadowModel): class Meta: abstract = True @@ -121,7 +126,7 @@ class Meta: uuid = models.CharField(max_length=249, default=u'') - class _UserProfile(models.Model): + class _UserProfile(_ShadowModel): ''' From onadata/apps/main/models/user_profile.py Not read-only because we need write access to `require_auth` @@ -156,7 +161,7 @@ class Meta: metadata = JSONField(default={}, blank=True) - class _UserObjectPermission(models.Model): + class _UserObjectPermission(_ShadowModel): ''' For the _sole purpose_ of letting us manipulate KoBoCAT permissions, this comprises the following django-guardian classes @@ -180,10 +185,19 @@ class _UserObjectPermission(models.Model): class Meta: db_table = 'guardian_userobjectpermission' unique_together = ['user', 'permission', 'object_pk'] + verbose_name = 'user object permission' def __unicode__(self): + # `unicode(self.content_object)` fails when the object's model + # isn't known to this Django project. Let's use something more + # benign instead. + content_object_str = '{app_label}_{model} ({pk})'.format( + app_label=self.content_type.app_label, + model=self.content_type.model, + pk=self.object_pk) return '%s | %s | %s' % ( - unicode(self.content_object), + #unicode(self.content_object), + content_object_str, unicode(getattr(self, 'user', False) or self.group), unicode(self.permission.codename)) From 55e129d8d6c4d37cf1f9f1447f6f92379ced2e5f Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Fri, 4 Jan 2019 23:06:01 +0100 Subject: [PATCH 28/44] fix tests --- test/xlform/survey.tests.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/xlform/survey.tests.coffee b/test/xlform/survey.tests.coffee index 5ab1929c0f..34b67c2464 100644 --- a/test/xlform/survey.tests.coffee +++ b/test/xlform/survey.tests.coffee @@ -122,7 +122,8 @@ do -> 'select_from_list_name': 'yesno', 'name': 'yn', 'label': 'YesNo', - 'required': 'false' + 'required': 'false', + 'read_only': 'false' } ], 'choices': { From 39878cf8c6c2433daa280ec345a268a953872060 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Mon, 7 Jan 2019 15:48:35 +0100 Subject: [PATCH 29/44] prepopulate hint value --- jsapp/xlform/src/view.rowDetail.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/jsapp/xlform/src/view.rowDetail.coffee b/jsapp/xlform/src/view.rowDetail.coffee index fa74f4554a..675676956f 100644 --- a/jsapp/xlform/src/view.rowDetail.coffee +++ b/jsapp/xlform/src/view.rowDetail.coffee @@ -169,6 +169,7 @@ module.exports = do -> html: -> false insertInDOM: (rowView) -> hintEl = rowView.$hint + hintEl.value = @model.get("value") return @ afterRender: -> @listenForInputChange({ From b5def26d66c6beff60e145fba1d8abf5e790b680 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Sun, 13 Jan 2019 19:58:27 +0100 Subject: [PATCH 30/44] add trailing slash in missing data interface calls --- jsapp/js/dataInterface.es6 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/jsapp/js/dataInterface.es6 b/jsapp/js/dataInterface.es6 index 2ae3faf62c..c8784598c9 100644 --- a/jsapp/js/dataInterface.es6 +++ b/jsapp/js/dataInterface.es6 @@ -293,7 +293,7 @@ var dataInterface; }, getAssetXformView (uid) { return $ajax({ - url: `${rootUrl}/assets/${uid}/xform`, + url: `${rootUrl}/assets/${uid}/xform/`, dataType: 'html' }); }, @@ -417,7 +417,7 @@ var dataInterface; filter += '&count=1'; return $ajax({ - url: `${rootUrl}/assets/${uid}/submissions?${query}${s}${f}${filter}`, + url: `${rootUrl}/assets/${uid}/submissions/?${query}${s}${f}${filter}`, method: 'GET' }); }, @@ -443,7 +443,7 @@ var dataInterface; }, getSubmissionsQuery(uid, query='') { return $ajax({ - url: `${rootUrl}/assets/${uid}/submissions?${query}`, + url: `${rootUrl}/assets/${uid}/submissions/?${query}`, method: 'GET' }); }, @@ -455,7 +455,7 @@ var dataInterface; }, getEnketoEditLink(uid, sid) { return $ajax({ - url: `${rootUrl}/assets/${uid}/submissions/${sid}/edit?return_url=false`, + url: `${rootUrl}/assets/${uid}/submissions/${sid}/edit/?return_url=false`, method: 'GET' }); }, @@ -475,13 +475,13 @@ var dataInterface; }, getAssetFiles(uid) { return $ajax({ - url: `${rootUrl}/assets/${uid}/files`, + url: `${rootUrl}/assets/${uid}/files/`, method: 'GET' }); }, deleteAssetFile(assetUid, uid) { return $ajax({ - url: `${rootUrl}/assets/${assetUid}/files/${uid}`, + url: `${rootUrl}/assets/${assetUid}/files/${uid}/`, method: 'DELETE' }); }, From ed1d906533d1f6e94527ab224ee8f4981fe78b81 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Thu, 17 Jan 2019 09:35:22 +0100 Subject: [PATCH 31/44] fix validation status filtering --- jsapp/js/components/table.es6 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jsapp/js/components/table.es6 b/jsapp/js/components/table.es6 index f4c8807786..f59a30a02e 100644 --- a/jsapp/js/components/table.es6 +++ b/jsapp/js/components/table.es6 @@ -74,6 +74,8 @@ export class DataTable extends React.Component { filter.forEach(function(f, i) { if (f.id === '_id') { filterQuery += `"${f.id}":{"$in":[${f.value}]}`; + } else if (f.id === '__ValidationStatus') { + filterQuery += `"_validation_status.uid":"${f.value}"`; } else { filterQuery += `"${f.id}":{"$regex":"${f.value}","$options":"i"}`; } From 324da748f11e0ae4a4bb8cbf2f06044faaa04bba Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Thu, 17 Jan 2019 09:38:33 +0100 Subject: [PATCH 32/44] Rename "Replace project" to "Replace form" --- jsapp/js/components/assetrow.es6 | 2 +- jsapp/js/components/formLanding.es6 | 2 +- jsapp/js/components/modalForms/projectSettings.es6 | 2 +- jsapp/js/mixins.es6 | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jsapp/js/components/assetrow.es6 b/jsapp/js/components/assetrow.es6 index 0111060db0..cff72045cd 100644 --- a/jsapp/js/components/assetrow.es6 +++ b/jsapp/js/components/assetrow.es6 @@ -353,7 +353,7 @@ class AssetRow extends React.Component { data-asset-type={this.props.kind} > - {t('Replace project')} + {t('Replace form')}
    } { userCanEdit && diff --git a/jsapp/js/components/formLanding.es6 b/jsapp/js/components/formLanding.es6 index 966e17c3a7..3ee7d5975d 100644 --- a/jsapp/js/components/formLanding.es6 +++ b/jsapp/js/components/formLanding.es6 @@ -362,7 +362,7 @@ export class FormLanding extends React.Component { {userCanEdit && diff --git a/jsapp/js/components/modalForms/projectSettings.es6 b/jsapp/js/components/modalForms/projectSettings.es6 index bf1e447e0c..6747b23d92 100644 --- a/jsapp/js/components/modalForms/projectSettings.es6 +++ b/jsapp/js/components/modalForms/projectSettings.es6 @@ -126,7 +126,7 @@ class ProjectSettings extends React.Component { case PROJECT_SETTINGS_CONTEXTS.NEW: return t('Create project'); case PROJECT_SETTINGS_CONTEXTS.REPLACE: - return t('Replace project'); + return t('Replace form'); case PROJECT_SETTINGS_CONTEXTS.EXISTING: case PROJECT_SETTINGS_CONTEXTS.BUILDER: default: diff --git a/jsapp/js/mixins.es6 b/jsapp/js/mixins.es6 index a3a0351c26..381c6eb194 100644 --- a/jsapp/js/mixins.es6 +++ b/jsapp/js/mixins.es6 @@ -353,7 +353,7 @@ mixins.droppable = { } else { if (!assetUid) { // TODO: use a more specific error message here - alertify.error(t('XLSForm Import failed. Check that the XLSForm and/or the URL are valid, and try again using the "Replace project" icon.')); + alertify.error(t('XLSForm Import failed. Check that the XLSForm and/or the URL are valid, and try again using the "Replace form" icon.')); if (params.assetUid) hashHistory.push(`/forms/${params.assetUid}`); } else { From 703a9d5fc0c784a89c056f17a097224eb49ae490 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20L=C3=A9ger?= Date: Thu, 17 Jan 2019 10:54:37 -0500 Subject: [PATCH 33/44] Update to Django 1.8.19 and sync all requirements files according to production file (external_service) --- dependencies/pip/dev_requirements.txt | 72 +++++++++++++------------- dependencies/pip/external_services.txt | 2 +- dependencies/pip/requirements.txt | 2 +- 3 files changed, 39 insertions(+), 37 deletions(-) diff --git a/dependencies/pip/dev_requirements.txt b/dependencies/pip/dev_requirements.txt index b4114d3881..74d131655d 100644 --- a/dependencies/pip/dev_requirements.txt +++ b/dependencies/pip/dev_requirements.txt @@ -6,48 +6,49 @@ # -e git+https://github.com/dimagi/django-digest@0eb1c921329dd187c343b61acfbec4e98450136e#egg=django_digest -e git+https://github.com/kobotoolbox/formpack.git@6f39abc642920964b6fbcfae16deb4b935956ad3#egg=formpack -amqp==2.1.4 +amqp==2.3.2 anyjson==0.3.3 argparse==1.4.0 # via unittest2 asn1crypto==0.24.0 # via cryptography +bcrypt==3.1.6 # via paramiko begins==0.9 -billiard==3.5.0.2 +billiard==3.5.0.4 boto3==1.5.8 -boto==2.45.0 +boto==2.40.0 botocore==1.8.22 # via boto3, s3transfer -celery==4.0.2 -cffi==1.9.1 # via cryptography +celery==4.2.1 +cffi==1.8.3 # via bcrypt, cryptography, pynacl cookies==2.2.1 # via responses cryptography==2.2.2 # via paramiko, pyopenssl cssselect==1.0.3 # via pyquery cyordereddict==1.0.0 defusedxml==0.5.0 # via djangorestframework-xml -dj-database-url==0.4.2 +dj-database-url==0.4.1 dj-static==0.0.6 -django-braces==1.11.0 +django-braces==1.8.1 django-celery-beat==1.1.1 django-constance[database]==2.2.0 -django-debug-toolbar==1.6 -django-extensions==1.7.6 +django-debug-toolbar==1.4 +django-extensions==1.6.7 django-haystack==2.6.0 django-jsonbfield==0.1.0 django-loginas==0.2.3 django-markitup==3.0.0 django-mptt==0.8.7 -django-oauth-toolkit==0.11.0 +django-oauth-toolkit==0.10.0 django-picklefield==1.0.0 # via django-constance django-private-storage==2.1.2 django-registration-redux==1.3 django-reversion==2.0.8 -django-ses==0.8.1 +django-ses==0.7.1 django-storages==1.6.5 django-taggit==0.22.0 django-toolbelt==0.0.1 -django-webpack-loader==0.4.1 -django==1.8.17 +django-webpack-loader==0.3.0 +django==1.8.19 djangorestframework-xml==1.3.0 -djangorestframework==3.5.4 -docutils==0.13.1 # via botocore, statistics +djangorestframework==3.3.3 +docutils==0.12 # via botocore, statistics drf-extensions==0.3.1 enum34==1.1.6 # via cryptography fabric==1.13.1 @@ -55,53 +56,54 @@ formencode==1.3.1 # via pyxform funcsigs==1.0.2 # via begins, mock functools32==3.2.3.post2 # via jsonschema futures==3.1.1 # via s3transfer -gunicorn==19.6.0 -idna==2.2 # via cryptography -ipaddress==1.0.18 # via cryptography +gunicorn==19.4.5 +idna==2.1 # via cryptography +ipaddress==1.0.17 # via cryptography jmespath==0.9.3 # via boto3, botocore jsonfield==1.0.3 jsonschema==2.6.0 -kombu==4.0.2 +kombu==4.2.1 linecache2==1.0.0 # via traceback2 lxml==4.2.1 -markdown==2.6.8 +markdown==2.6.6 mock==2.0.0 ndg-httpsclient==0.4.2 -oauthlib==1.1.2 -paramiko==2.1.1 # via fabric +oauthlib==1.0.3 +paramiko==2.4.2 # via fabric path.py==11.0.1 pbr==4.0.2 # via mock psycopg2==2.7.3.2 -py==1.4.32 # via pytest -pyasn1==0.2.2 -pycparser==2.17 # via cffi -pygments==2.2.0 -pymongo==3.4.0 +py==1.4.31 # via pytest +pyasn1==0.1.9 +pycparser==2.14 # via cffi +pygments==2.1.3 +pymongo==3.3.0 +pynacl==1.3.0 # via paramiko pyopenssl==18.0.0 pyquery==1.4.0 pytest-django==3.1.2 -pytest==3.0.6 # via pytest-django +pytest==3.0.3 # via pytest-django python-dateutil==2.6.0 python-digest==1.7 -pytz==2016.10 +pytz==2016.4 pyxform==0.12.0 -requests==2.13.0 +requests==2.10.0 responses==0.9.0 s3transfer==0.1.11 # via boto3 shortuuid==0.4.3 six==1.10.0 -sqlparse==0.2.2 +sqlparse==0.1.19 static3==0.7.0 statistics==1.0.3.5 -tabulate==0.7.7 +tabulate==0.7.5 traceback2==1.4.0 # via unittest2 unicodecsv==0.14.1 unittest2==1.1.0 # via pyxform uwsgi==2.0.17 -vine==1.1.3 # via amqp -werkzeug==0.11.15 +vine==1.1.4 # via amqp +werkzeug==0.14.1 whitenoise==3.3.1 whoosh==2.7.4 xlrd==1.1.0 xlsxwriter==1.0.4 -xlwt==1.2.0 +xlwt==1.0.0 diff --git a/dependencies/pip/external_services.txt b/dependencies/pip/external_services.txt index c23ceb0e69..f62457e2bc 100644 --- a/dependencies/pip/external_services.txt +++ b/dependencies/pip/external_services.txt @@ -45,7 +45,7 @@ django-storages==1.6.5 django-taggit==0.22.0 django-toolbelt==0.0.1 django-webpack-loader==0.3.0 -django==1.8.13 +django==1.8.19 djangorestframework-xml==1.3.0 djangorestframework==3.3.3 docutils==0.12 # via botocore, statistics diff --git a/dependencies/pip/requirements.txt b/dependencies/pip/requirements.txt index 589058c7d0..513ceeffb5 100644 --- a/dependencies/pip/requirements.txt +++ b/dependencies/pip/requirements.txt @@ -44,7 +44,7 @@ django-storages==1.6.5 django-taggit==0.22.0 django-toolbelt==0.0.1 django-webpack-loader==0.3.0 -django==1.8.13 +django==1.8.19 djangorestframework-xml==1.3.0 djangorestframework==3.3.3 docutils==0.12 # via botocore, statistics From 1c6a2560715c3d4b62e3d57dbd5fb3d5d3a69c04 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Thu, 17 Jan 2019 17:34:57 +0100 Subject: [PATCH 34/44] one more slash missing --- jsapp/js/dataInterface.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsapp/js/dataInterface.es6 b/jsapp/js/dataInterface.es6 index c8784598c9..4d5014afb4 100644 --- a/jsapp/js/dataInterface.es6 +++ b/jsapp/js/dataInterface.es6 @@ -423,7 +423,7 @@ var dataInterface; }, getSubmission(uid, sid) { return $ajax({ - url: `${rootUrl}/assets/${uid}/submissions/${sid}`, + url: `${rootUrl}/assets/${uid}/submissions/${sid}/`, method: 'GET' }); }, From 31801e29815cf1a86d1bbfa7ef40ceadf5927f92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20L=C3=A9ger?= Date: Thu, 17 Jan 2019 14:27:15 -0500 Subject: [PATCH 35/44] Updated several pip dependencies --- dependencies/pip/dev_requirements.txt | 61 ++++++++++++++------------ dependencies/pip/external_services.txt | 59 +++++++++++++------------ dependencies/pip/requirements.txt | 56 ++++++++++++----------- 3 files changed, 94 insertions(+), 82 deletions(-) diff --git a/dependencies/pip/dev_requirements.txt b/dependencies/pip/dev_requirements.txt index 74d131655d..ace644e80f 100644 --- a/dependencies/pip/dev_requirements.txt +++ b/dependencies/pip/dev_requirements.txt @@ -2,30 +2,32 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --output-file dependencies/pip/dev_requirements.txt dependencies/pip/dev_requirements.in +# pip-compile --output-file dev_requirements.txt dev_requirements.in # -e git+https://github.com/dimagi/django-digest@0eb1c921329dd187c343b61acfbec4e98450136e#egg=django_digest -e git+https://github.com/kobotoolbox/formpack.git@6f39abc642920964b6fbcfae16deb4b935956ad3#egg=formpack -amqp==2.3.2 +amqp==2.4.0 anyjson==0.3.3 argparse==1.4.0 # via unittest2 asn1crypto==0.24.0 # via cryptography bcrypt==3.1.6 # via paramiko begins==0.9 -billiard==3.5.0.4 -boto3==1.5.8 -boto==2.40.0 -botocore==1.8.22 # via boto3, s3transfer +billiard==3.5.0.5 +boto3==1.9.80 +boto==2.49.0 +botocore==1.12.80 # via boto3, s3transfer celery==4.2.1 +certifi==2018.11.29 # via requests cffi==1.8.3 # via bcrypt, cryptography, pynacl +chardet==3.0.4 # via requests cookies==2.2.1 # via responses -cryptography==2.2.2 # via paramiko, pyopenssl +cryptography==2.2.2 # via fabric, paramiko, pyopenssl cssselect==1.0.3 # via pyquery cyordereddict==1.0.0 defusedxml==0.5.0 # via djangorestframework-xml dj-database-url==0.4.1 dj-static==0.0.6 -django-braces==1.8.1 +django-braces==1.13.0 django-celery-beat==1.1.1 django-constance[database]==2.2.0 django-debug-toolbar==1.4 @@ -40,70 +42,73 @@ django-picklefield==1.0.0 # via django-constance django-private-storage==2.1.2 django-registration-redux==1.3 django-reversion==2.0.8 -django-ses==0.7.1 +django-ses==0.8.9 django-storages==1.6.5 django-taggit==0.22.0 django-toolbelt==0.0.1 django-webpack-loader==0.3.0 django==1.8.19 -djangorestframework-xml==1.3.0 -djangorestframework==3.3.3 -docutils==0.12 # via botocore, statistics +djangorestframework-xml==1.4.0 +djangorestframework==3.6.4 +docutils==0.14 # via botocore, statistics drf-extensions==0.3.1 enum34==1.1.6 # via cryptography -fabric==1.13.1 +fabric==2.4.0 formencode==1.3.1 # via pyxform funcsigs==1.0.2 # via begins, mock functools32==3.2.3.post2 # via jsonschema -futures==3.1.1 # via s3transfer +future==0.17.1 # via django-ses +futures==3.2.0 # via s3transfer gunicorn==19.4.5 -idna==2.1 # via cryptography +idna==2.8 # via cryptography, requests +invoke==1.2.0 # via fabric ipaddress==1.0.17 # via cryptography jmespath==0.9.3 # via boto3, botocore jsonfield==1.0.3 jsonschema==2.6.0 -kombu==4.2.1 +kombu==4.2.2.post1 linecache2==1.0.0 # via traceback2 lxml==4.2.1 -markdown==2.6.6 +markdown==3.0.1 mock==2.0.0 ndg-httpsclient==0.4.2 oauthlib==1.0.3 paramiko==2.4.2 # via fabric path.py==11.0.1 pbr==4.0.2 # via mock -psycopg2==2.7.3.2 +psycopg2==2.7.6.1 py==1.4.31 # via pytest pyasn1==0.1.9 pycparser==2.14 # via cffi pygments==2.1.3 -pymongo==3.3.0 +pymongo==3.7.2 pynacl==1.3.0 # via paramiko pyopenssl==18.0.0 pyquery==1.4.0 pytest-django==3.1.2 pytest==3.0.3 # via pytest-django -python-dateutil==2.6.0 +python-dateutil==2.7.5 python-digest==1.7 -pytz==2016.4 +pytz==2018.9 pyxform==0.12.0 -requests==2.10.0 +requests==2.21.0 responses==0.9.0 -s3transfer==0.1.11 # via boto3 +s3transfer==0.1.13 # via boto3 shortuuid==0.4.3 -six==1.10.0 +six==1.12.0 sqlparse==0.1.19 static3==0.7.0 statistics==1.0.3.5 -tabulate==0.7.5 +tabulate==0.8.2 traceback2==1.4.0 # via unittest2 unicodecsv==0.14.1 unittest2==1.1.0 # via pyxform +urllib3==1.24.1 # via botocore, requests uwsgi==2.0.17 -vine==1.1.4 # via amqp +vine==1.2.0 # via amqp werkzeug==0.14.1 whitenoise==3.3.1 whoosh==2.7.4 xlrd==1.1.0 -xlsxwriter==1.0.4 -xlwt==1.0.0 +xlsxwriter==1.1.2 +xlwt==1.3.0 diff --git a/dependencies/pip/external_services.txt b/dependencies/pip/external_services.txt index f62457e2bc..0032edeb58 100644 --- a/dependencies/pip/external_services.txt +++ b/dependencies/pip/external_services.txt @@ -2,22 +2,24 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --output-file dependencies/pip/external_services.txt dependencies/pip/external_services.in +# pip-compile --output-file external_services.txt external_services.in # -e git+https://github.com/dimagi/django-digest@0eb1c921329dd187c343b61acfbec4e98450136e#egg=django_digest -e git+https://github.com/kobotoolbox/formpack.git@6f39abc642920964b6fbcfae16deb4b935956ad3#egg=formpack -amqp==2.3.2 +amqp==2.4.0 anyjson==0.3.3 argparse==1.4.0 # via unittest2 asn1crypto==0.24.0 # via cryptography begins==0.9 -billiard==3.5.0.4 -boto3==1.5.8 -boto==2.40.0 -botocore==1.8.22 # via boto3, s3transfer +billiard==3.5.0.5 +boto3==1.9.80 +boto==2.49.0 +botocore==1.12.80 # via boto3, s3transfer celery==4.2.1 +certifi==2018.11.29 # via requests cffi==1.8.3 # via cryptography -contextlib2==0.5.4 # via raven +chardet==3.0.4 # via requests +contextlib2==0.5.5 # via raven cookies==2.2.1 # via responses cryptography==2.2.2 # via pyopenssl cssselect==1.0.3 # via pyquery @@ -25,7 +27,7 @@ cyordereddict==1.0.0 defusedxml==0.5.0 # via djangorestframework-xml dj-database-url==0.4.1 dj-static==0.0.6 -django-braces==1.8.1 +django-braces==1.13.0 django-celery-beat==1.1.1 django-constance[database]==2.2.0 django-debug-toolbar==1.4 @@ -40,70 +42,71 @@ django-picklefield==1.0.0 # via django-constance django-private-storage==2.1.2 django-registration-redux==1.3 django-reversion==2.0.8 -django-ses==0.7.1 +django-ses==0.8.9 django-storages==1.6.5 django-taggit==0.22.0 django-toolbelt==0.0.1 django-webpack-loader==0.3.0 django==1.8.19 -djangorestframework-xml==1.3.0 -djangorestframework==3.3.3 -docutils==0.12 # via botocore, statistics +djangorestframework-xml==1.4.0 +djangorestframework==3.6.4 +docutils==0.14 # via botocore, statistics drf-extensions==0.3.1 enum34==1.1.6 # via cryptography formencode==1.3.1 # via pyxform funcsigs==1.0.2 # via begins, mock functools32==3.2.3.post2 # via jsonschema -futures==3.1.1 # via s3transfer +future==0.17.1 # via django-ses +futures==3.2.0 # via s3transfer gunicorn==19.4.5 -idna==2.1 # via cryptography +idna==2.8 # via cryptography, requests ipaddress==1.0.17 # via cryptography jmespath==0.9.3 # via boto3, botocore jsonfield==1.0.3 jsonschema==2.6.0 -kombu==4.2.1 +kombu==4.2.2.post1 linecache2==1.0.0 # via traceback2 lxml==4.2.1 -markdown==2.6.6 +markdown==3.0.1 mock==2.0.0 ndg-httpsclient==0.4.2 newrelic==2.84.0.64 oauthlib==1.0.3 path.py==11.0.1 pbr==4.0.2 # via mock -psycopg2==2.7.3.2 +psycopg2==2.7.6.1 py==1.4.31 # via pytest pyasn1==0.1.9 pycparser==2.14 # via cffi pygments==2.1.3 -pymongo==3.3.0 +pymongo==3.7.2 pyopenssl==18.0.0 pyquery==1.4.0 pytest-django==3.1.2 pytest==3.0.3 # via pytest-django -python-dateutil==2.6.0 +python-dateutil==2.7.5 python-digest==1.7 -pytz==2016.4 +pytz==2018.9 pyxform==0.12.0 raven==5.32.0 -requests==2.10.0 +requests==2.21.0 responses==0.9.0 -s3transfer==0.1.11 # via boto3 +s3transfer==0.1.13 # via boto3 shortuuid==0.4.3 -six==1.10.0 +six==1.12.0 sqlparse==0.1.19 static3==0.7.0 statistics==1.0.3.5 -tabulate==0.7.5 +tabulate==0.8.2 traceback2==1.4.0 # via unittest2 transifex-client==0.11 unicodecsv==0.14.1 unittest2==1.1.0 # via pyxform -urllib3==1.15.1 # via transifex-client +urllib3==1.24.1 # via botocore, requests, transifex-client uwsgi==2.0.17 -vine==1.1.4 # via amqp +vine==1.2.0 # via amqp whitenoise==3.3.1 whoosh==2.7.4 xlrd==1.1.0 -xlsxwriter==1.0.4 -xlwt==1.0.0 +xlsxwriter==1.1.2 +xlwt==1.3.0 diff --git a/dependencies/pip/requirements.txt b/dependencies/pip/requirements.txt index 513ceeffb5..22ebdf6e5d 100644 --- a/dependencies/pip/requirements.txt +++ b/dependencies/pip/requirements.txt @@ -2,21 +2,23 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --output-file dependencies/pip/requirements.txt dependencies/pip/requirements.in +# pip-compile --output-file requirements.txt requirements.in # -e git+https://github.com/dimagi/django-digest@0eb1c921329dd187c343b61acfbec4e98450136e#egg=django_digest -e git+https://github.com/kobotoolbox/formpack.git@6f39abc642920964b6fbcfae16deb4b935956ad3#egg=formpack -amqp==2.3.2 +amqp==2.4.0 anyjson==0.3.3 argparse==1.4.0 # via unittest2 asn1crypto==0.24.0 # via cryptography begins==0.9 -billiard==3.5.0.4 -boto3==1.5.8 -boto==2.40.0 -botocore==1.8.22 # via boto3, s3transfer +billiard==3.5.0.5 +boto3==1.9.80 +boto==2.49.0 +botocore==1.12.80 # via boto3, s3transfer celery==4.2.1 +certifi==2018.11.29 # via requests cffi==1.8.3 # via cryptography +chardet==3.0.4 # via requests cookies==2.2.1 # via responses cryptography==2.2.2 # via pyopenssl cssselect==1.0.3 # via pyquery @@ -24,7 +26,7 @@ cyordereddict==1.0.0 defusedxml==0.5.0 # via djangorestframework-xml dj-database-url==0.4.1 dj-static==0.0.6 -django-braces==1.8.1 +django-braces==1.13.0 django-celery-beat==1.1.1 django-constance[database]==2.2.0 django-debug-toolbar==1.4 @@ -39,66 +41,68 @@ django-picklefield==1.0.0 # via django-constance django-private-storage==2.1.2 django-registration-redux==1.3 django-reversion==2.0.8 -django-ses==0.7.1 +django-ses==0.8.9 django-storages==1.6.5 django-taggit==0.22.0 django-toolbelt==0.0.1 django-webpack-loader==0.3.0 django==1.8.19 -djangorestframework-xml==1.3.0 -djangorestframework==3.3.3 -docutils==0.12 # via botocore, statistics +djangorestframework-xml==1.4.0 +djangorestframework==3.6.4 +docutils==0.14 # via botocore, statistics drf-extensions==0.3.1 enum34==1.1.6 # via cryptography formencode==1.3.1 # via pyxform funcsigs==1.0.2 # via begins, mock functools32==3.2.3.post2 # via jsonschema -futures==3.1.1 # via s3transfer +future==0.17.1 # via django-ses +futures==3.2.0 # via s3transfer gunicorn==19.4.5 -idna==2.1 # via cryptography +idna==2.8 # via cryptography, requests ipaddress==1.0.17 # via cryptography jmespath==0.9.3 # via boto3, botocore jsonfield==1.0.3 jsonschema==2.6.0 -kombu==4.2.1 +kombu==4.2.2.post1 linecache2==1.0.0 # via traceback2 lxml==4.2.1 -markdown==2.6.6 +markdown==3.0.1 mock==2.0.0 ndg-httpsclient==0.4.2 oauthlib==1.0.3 path.py==11.0.1 pbr==4.0.2 # via mock -psycopg2==2.7.3.2 +psycopg2==2.7.6.1 py==1.4.31 # via pytest pyasn1==0.1.9 pycparser==2.14 # via cffi pygments==2.1.3 -pymongo==3.3.0 +pymongo==3.7.2 pyopenssl==18.0.0 pyquery==1.4.0 pytest-django==3.1.2 pytest==3.0.3 # via pytest-django -python-dateutil==2.6.0 +python-dateutil==2.7.5 python-digest==1.7 -pytz==2016.4 +pytz==2018.9 pyxform==0.12.0 -requests==2.10.0 +requests==2.21.0 responses==0.9.0 -s3transfer==0.1.11 # via boto3 +s3transfer==0.1.13 # via boto3 shortuuid==0.4.3 -six==1.10.0 +six==1.12.0 sqlparse==0.1.19 static3==0.7.0 statistics==1.0.3.5 -tabulate==0.7.5 +tabulate==0.8.2 traceback2==1.4.0 # via unittest2 unicodecsv==0.14.1 unittest2==1.1.0 # via pyxform +urllib3==1.24.1 # via botocore, requests uwsgi==2.0.17 -vine==1.1.4 # via amqp +vine==1.2.0 # via amqp whitenoise==3.3.1 whoosh==2.7.4 xlrd==1.1.0 -xlsxwriter==1.0.4 -xlwt==1.0.0 +xlsxwriter==1.1.2 +xlwt==1.3.0 From b430b2e90ab9138a9f8f2b3d2a4a3cb81190ea60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20L=C3=A9ger?= Date: Thu, 17 Jan 2019 14:41:54 -0500 Subject: [PATCH 36/44] Replaced psycopg2 with -psycopg2-binary --- dependencies/pip/dev_requirements.txt | 2 +- dependencies/pip/external_services.txt | 2 +- dependencies/pip/requirements.in | 2 +- dependencies/pip/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dependencies/pip/dev_requirements.txt b/dependencies/pip/dev_requirements.txt index ace644e80f..e8e017a9c2 100644 --- a/dependencies/pip/dev_requirements.txt +++ b/dependencies/pip/dev_requirements.txt @@ -76,7 +76,7 @@ oauthlib==1.0.3 paramiko==2.4.2 # via fabric path.py==11.0.1 pbr==4.0.2 # via mock -psycopg2==2.7.6.1 +psycopg2-binary==2.7.6.1 py==1.4.31 # via pytest pyasn1==0.1.9 pycparser==2.14 # via cffi diff --git a/dependencies/pip/external_services.txt b/dependencies/pip/external_services.txt index 0032edeb58..25910be8f3 100644 --- a/dependencies/pip/external_services.txt +++ b/dependencies/pip/external_services.txt @@ -74,7 +74,7 @@ newrelic==2.84.0.64 oauthlib==1.0.3 path.py==11.0.1 pbr==4.0.2 # via mock -psycopg2==2.7.6.1 +psycopg2-binary==2.7.6.1 py==1.4.31 # via pytest pyasn1==0.1.9 pycparser==2.14 # via cffi diff --git a/dependencies/pip/requirements.in b/dependencies/pip/requirements.in index 78b4935dcb..db942f34b4 100644 --- a/dependencies/pip/requirements.in +++ b/dependencies/pip/requirements.in @@ -49,7 +49,7 @@ kombu lxml mock oauthlib -psycopg2 +psycopg2-binary pymongo pytest-django python-dateutil diff --git a/dependencies/pip/requirements.txt b/dependencies/pip/requirements.txt index 22ebdf6e5d..b5734536c9 100644 --- a/dependencies/pip/requirements.txt +++ b/dependencies/pip/requirements.txt @@ -72,7 +72,7 @@ ndg-httpsclient==0.4.2 oauthlib==1.0.3 path.py==11.0.1 pbr==4.0.2 # via mock -psycopg2==2.7.6.1 +psycopg2-binary==2.7.6.1 py==1.4.31 # via pytest pyasn1==0.1.9 pycparser==2.14 # via cffi From 624c003cd9b53d4663e0032bd32519c83e7129d8 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Fri, 18 Jan 2019 23:58:25 +0100 Subject: [PATCH 37/44] fix validation statuses column ordering --- jsapp/js/components/modalForms/submission.es6 | 4 +-- jsapp/js/components/table.es6 | 28 +++++++++---------- jsapp/js/constants.es6 | 15 +++++++--- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/jsapp/js/components/modalForms/submission.es6 b/jsapp/js/components/modalForms/submission.es6 index 6c66d6ffb3..4ba9fae555 100644 --- a/jsapp/js/components/modalForms/submission.es6 +++ b/jsapp/js/components/modalForms/submission.es6 @@ -15,7 +15,7 @@ import stores from 'js/stores'; import ui from 'js/ui'; import icons from '../../../xlform/src/view.icons'; import { - VALIDATION_STATUSES, + VALIDATION_STATUSES_LIST, MODAL_TYPES } from 'js/constants'; @@ -445,7 +445,7 @@ class Submission extends React.Component { isDisabled={!this.userCan('validate_submissions', this.props.asset)} isClearable={false} value={s._validation_status && s._validation_status.uid ? s._validation_status : false} - options={VALIDATION_STATUSES} + options={VALIDATION_STATUSES_LIST} onChange={this.validationStatusChange} className='kobo-select' classNamePrefix='kobo-select' diff --git a/jsapp/js/components/table.es6 b/jsapp/js/components/table.es6 index f59a30a02e..03ba8f28de 100644 --- a/jsapp/js/components/table.es6 +++ b/jsapp/js/components/table.es6 @@ -20,6 +20,7 @@ import {DebounceInput} from 'react-debounce-input'; import { VALIDATION_STATUSES, + VALIDATION_STATUSES_LIST, MODAL_TYPES } from '../constants'; @@ -74,8 +75,8 @@ export class DataTable extends React.Component { filter.forEach(function(f, i) { if (f.id === '_id') { filterQuery += `"${f.id}":{"$in":[${f.value}]}`; - } else if (f.id === '__ValidationStatus') { - filterQuery += `"_validation_status.uid":"${f.value}"`; + } else if (f.id === '_validation_status.uid') { + filterQuery += `"${f.id}":"${f.value}"`; } else { filterQuery += `"${f.id}":{"$regex":"${f.value}","$options":"i"}`; } @@ -128,12 +129,9 @@ export class DataTable extends React.Component { this.setState({error: t('Error: could not load data.'), loading: false}); }); } - getValidationStatusOption(rowIndex) { - if (this.state.tableData[rowIndex]._validation_status) { - const optionVal = this.state.tableData[rowIndex]._validation_status.uid; - return _.find(VALIDATION_STATUSES, (option) => { - return option.value === optionVal; - }); + getValidationStatusOption(originalRow) { + if (originalRow._validation_status.uid) { + return VALIDATION_STATUSES[originalRow._validation_status.uid]; } else { return null; } @@ -259,7 +257,7 @@ export class DataTable extends React.Component { }, accessor: '_validation_status.uid', index: '__2', - id: '__ValidationStatus', + id: '_validation_status.uid', minWidth: 130, className: 'rt-status', Filter: ({ filter, onChange }) => @@ -268,7 +266,7 @@ export class DataTable extends React.Component { style={{ width: '100%' }} value={filter ? filter.value : ''}> - {VALIDATION_STATUSES.map((item, n) => { + {VALIDATION_STATUSES_LIST.map((item, n) => { return ( ); @@ -278,8 +276,8 @@ export class DataTable extends React.Component { + """ From a7ea6ae9b28768bed4b9ed76787ca6ade1279419 Mon Sep 17 00:00:00 2001 From: Leszek Pietrzak Date: Wed, 23 Jan 2019 20:45:54 +0100 Subject: [PATCH 41/44] # make sure that params without values use default one --- jsapp/xlform/src/view.params.coffee | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/jsapp/xlform/src/view.params.coffee b/jsapp/xlform/src/view.params.coffee index 2d78dfe336..a15881dc7a 100644 --- a/jsapp/xlform/src/view.params.coffee +++ b/jsapp/xlform/src/view.params.coffee @@ -49,7 +49,11 @@ module.exports = do -> 'input input': 'onChange' } - initialize: (@paramName, @paramType, @paramDefault, @paramValue='', @onParamChange) -> return + initialize: (@paramName, @paramType, @paramDefault, @paramValue='', @onParamChange) -> + if @paramValue is '' and typeof @paramDefault isnt 'undefined' + # make sure that params without values use default one + @onParamChange(@paramName, @paramDefault) + return render: -> template = $($viewTemplates.$$render("ParamsView.#{@paramType}Param", @paramName, @paramValue, @paramDefault)) @@ -59,6 +63,9 @@ module.exports = do -> onChange: (evt) -> if @paramType is $configs.paramTypes.number val = evt.currentTarget.value + # make sure that params without removed values keep using default one + if val is '' and typeof @paramDefault isnt 'undefined' + val = "#{@paramDefault}" else if @paramType is $configs.paramTypes.boolean val = evt.currentTarget.checked @onParamChange(@paramName, val) From 6e2c03b3d5a24430c58ab5868ed3631b14a344ec Mon Sep 17 00:00:00 2001 From: "John N. Milner" Date: Thu, 24 Jan 2019 13:50:09 -0500 Subject: [PATCH 42/44] Change authenticaion error text --- jsapp/js/dataInterface.es6 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/jsapp/js/dataInterface.es6 b/jsapp/js/dataInterface.es6 index 66194d7a95..a893fac14f 100644 --- a/jsapp/js/dataInterface.es6 +++ b/jsapp/js/dataInterface.es6 @@ -32,13 +32,13 @@ var dataInterface; if (request.status === 403 || request.status === 401 || request.status === 404) { dataInterface.selfProfile().done((data) => { if (data.message === 'user is not logged in') { - let errorMessage = t("It seems you're not logged in anymore. Try reloading the page. The server said: ##server_message##") + let errorMessage = t("Please try reloading the page. If you need to contact support, note the following message:
    ##server_message##
    ") + let serverMessage = request.status.toString(); if (request.responseJSON && request.responseJSON.detail) { - errorMessage = errorMessage.replace('##server_message##', request.responseJSON.detail); - } else { - errorMessage = errorMessage.replace('##server_message##', request.status); + serverMessage += ": " + request.responseJSON.detail; } - alertify.alert(t('Auth Error'), errorMessage); + errorMessage = errorMessage.replace('##server_message##', serverMessage); + alertify.alert(t('You are not logged in'), errorMessage); } }); } From cbef0008e77edf1bd090e3f0d56650be9853a227 Mon Sep 17 00:00:00 2001 From: "John N. Milner" Date: Sun, 27 Jan 2019 22:02:28 -0500 Subject: [PATCH 43/44] Change `firstQuestionName` to `firstQuestionLabel` --- jsapp/js/components/assetrow.es6 | 2 +- jsapp/js/components/searchcollectionlist.es6 | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/jsapp/js/components/assetrow.es6 b/jsapp/js/components/assetrow.es6 index 0ebbdb3040..5ab8c5bbb9 100644 --- a/jsapp/js/components/assetrow.es6 +++ b/jsapp/js/components/assetrow.es6 @@ -99,7 +99,7 @@ class AssetRow extends React.Component { const userCanEdit = this.userCan('change_asset', this.props); - const assetName = this.props.name || this.props.firstQuestionName; + const assetName = this.props.name || this.props.firstQuestionLabel; if (this.props.has_deployment && this.props.deployment__submission_count && this.userCan('view_submissions', this.props)) { diff --git a/jsapp/js/components/searchcollectionlist.es6 b/jsapp/js/components/searchcollectionlist.es6 index 5cc55b4acf..c3f6e4aea0 100644 --- a/jsapp/js/components/searchcollectionlist.es6 +++ b/jsapp/js/components/searchcollectionlist.es6 @@ -79,8 +79,8 @@ class SearchCollectionList extends Reflux.Component { var isSelected = stores.selectedAsset.uid === resource.uid; var ownedCollections = this.state.ownedCollections; - // for unnamed assets, we try to display first question name - let firstQuestionName; + // for unnamed assets, we try to display first question label + let firstQuestionLabel; if ( resource.asset_type !== ASSET_TYPES.survey.id && resource.name === '' && @@ -88,7 +88,7 @@ class SearchCollectionList extends Reflux.Component { resource.summary.labels && resource.summary.labels.length > 0 ) { - firstQuestionName = resource.summary.labels[0] + firstQuestionLabel = resource.summary.labels[0] } return ( @@ -98,7 +98,7 @@ class SearchCollectionList extends Reflux.Component { isSelected={isSelected} ownedCollections={ownedCollections} deleting={resource.deleting} - firstQuestionName={firstQuestionName} + firstQuestionLabel={firstQuestionLabel} {...resource} /> ); From 77234e5e52e056d1ea4bd94d7fef92c314fe955e Mon Sep 17 00:00:00 2001 From: "John N. Milner" Date: Sun, 27 Jan 2019 23:56:19 -0500 Subject: [PATCH 44/44] Correct capitalization per existing convention --- jsapp/xlform/src/view.rowDetail.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsapp/xlform/src/view.rowDetail.coffee b/jsapp/xlform/src/view.rowDetail.coffee index 94dd304540..87b7c9d0b2 100644 --- a/jsapp/xlform/src/view.rowDetail.coffee +++ b/jsapp/xlform/src/view.rowDetail.coffee @@ -380,7 +380,7 @@ module.exports = do -> viewRowDetail.DetailViewMixins.read_only = html: -> @$el.addClass("card__settings__fields--active") - viewRowDetail.Templates.checkbox @cid, @model.key, _t("Read Only") + viewRowDetail.Templates.checkbox @cid, @model.key, _t("Read only") afterRender: -> @listenForCheckboxChange()