diff --git a/Dashboard/app/js/lib/controllers/data-controllers.js b/Dashboard/app/js/lib/controllers/data-controllers.js index d1276d80ab..f128df9bfb 100644 --- a/Dashboard/app/js/lib/controllers/data-controllers.js +++ b/Dashboard/app/js/lib/controllers/data-controllers.js @@ -283,10 +283,14 @@ FLOW.questionAnswerControl = Ember.ArrayController.create({ // over a set of responses to questions in a specific question group, ordered // by question order. For repeat question groups two adjacent sub lists // represent two iterations of responses for that group - contentByGroup: Ember.computed('content.isLoaded', function(key, value) { - var content = Ember.get(this, 'content'), - self = this; - if (content) { + contentByGroup: Ember.computed('content.isLoaded', + 'FLOW.questionContol.content.isLoaded', + 'FLOW.questionContol.content.isLoaded', function(key, value) { + var content = Ember.get(this, 'content'), self = this; + var questions = FLOW.questionControl.get('content'); + var questionGroups = FLOW.questionGroupControl.get('content'); + + if (content && questions && questionGroups) { var surveyQuestions = FLOW.questionControl.get('content'); var groups = FLOW.questionGroupControl.get('content'); var allResponses = []; @@ -347,9 +351,18 @@ FLOW.questionAnswerControl = Ember.ArrayController.create({ return allIterations; }, - doQuestionAnswerQuery: function (surveyInstanceId) { + doQuestionAnswerQuery: function (surveyInstance) { + var formId = surveyInstance.get('surveyId'); + var form = FLOW.surveyControl.filter(function (form) { + return form.get('keyId') === formId; + }).get(0); + + if (formId !== FLOW.selectedControl.selectedSurvey.get('keyId')) { + FLOW.selectedControl.set('selectedSurvey', form); + } + this.set('content', FLOW.store.findQuery(FLOW.QuestionAnswer, { - 'surveyInstanceId': surveyInstanceId + 'surveyInstanceId': surveyInstance.get('keyId') })); }, }); diff --git a/Dashboard/app/js/lib/controllers/languages.js b/Dashboard/app/js/lib/controllers/languages.js index 1a3adfafff..c7396c7105 100644 --- a/Dashboard/app/js/lib/controllers/languages.js +++ b/Dashboard/app/js/lib/controllers/languages.js @@ -499,6 +499,10 @@ FLOW.isoLanguagesDict = { "name": "Persian", "nativeName": "فارسی" }, + "pis": { + "name": "Pijin", + "nativeName": "Pijin" + }, "pl": { "name": "Polish", "nativeName": "polski" @@ -635,6 +639,10 @@ FLOW.isoLanguagesDict = { "name": "Tibetan Standard, Tibetan, Central", "nativeName": "བོད་ཡིག" }, + "tpi": { + "name": "Tok Pisin", + "nativeName": "Tok Pisin" + }, "tk": { "name": "Turkmen", "nativeName": "Türkmen, Түркмен" diff --git a/Dashboard/app/js/lib/controllers/survey-controllers.js b/Dashboard/app/js/lib/controllers/survey-controllers.js index 0dfd5d894b..8e8f0afb01 100644 --- a/Dashboard/app/js/lib/controllers/survey-controllers.js +++ b/Dashboard/app/js/lib/controllers/survey-controllers.js @@ -632,6 +632,7 @@ FLOW.surveyControl = Ember.ArrayController.create({ }.observes('FLOW.selectedControl.selectedSurveyGroup'), selectFirstForm: function() { + if(FLOW.selectedControl.selectedSurvey) return; // ignore if form is already selected if (this.get('content') && this.content.get('isLoaded')) { var form = this.content.get('firstObject'); if (form) { @@ -814,29 +815,50 @@ FLOW.questionGroupControl = Ember.ArrayController.create({ // execute group delete deleteQuestionGroup: function (questionGroupId) { - var questionGroup, questionsGroupsInSurvey, sId, qgOrder; + var questionGroup, sId, qgOrder; sId = FLOW.selectedControl.selectedSurvey.get('keyId'); questionGroup = FLOW.store.find(FLOW.QuestionGroup, questionGroupId); qgOrder = questionGroup.get('order'); questionGroup.deleteRecord(); - // restore order of remaining groups - questionGroupsInSurvey = FLOW.store.filter(FLOW.QuestionGroup, function (item) { - return item.get('surveyId') == sId; + // reorder the rest of the question groups + this.reorderQuestionGroups(sId, qgOrder, "decrement"); + this.submitBulkQuestionGroupsReorder(sId); + + FLOW.selectedControl.selectedSurvey.set('status', 'NOT_PUBLISHED'); + FLOW.store.commit(); + }, + + reorderQuestionGroups: function (surveyId, reorderPoint, reorderOperation) { + var questionGroupsInSurvey = FLOW.store.filter(FLOW.QuestionGroup, function (item) { + return item.get('surveyId') == surveyId; }); - // restore order + // move items up to make space questionGroupsInSurvey.forEach(function (item) { - if (item.get('order') > qgOrder) { - item.set('order', item.get('order') - 1); + if (reorderOperation == "increment") { + if (item.get('order') > reorderPoint) { + item.set('order', item.get('order') + 1); + } + } else if (reorderOperation == "decrement") { + if (item.get('order') > reorderPoint) { + item.set('order', item.get('order') - 1); + } } }); + }, + submitBulkQuestionGroupsReorder: function (surveyId) { + FLOW.questionControl.set('bulkCommit', true); + var questionGroupsInSurvey = FLOW.store.filter(FLOW.QuestionGroup, function (item) { + return item.get('surveyId') == surveyId; + }); // restore order in case the order has gone haywire FLOW.questionControl.restoreOrder(questionGroupsInSurvey); - FLOW.selectedControl.selectedSurvey.set('status', 'NOT_PUBLISHED'); FLOW.store.commit(); + FLOW.store.adapter.set('bulkCommit', false); + FLOW.questionControl.set('bulkCommit', false); } }); @@ -850,6 +872,7 @@ FLOW.questionControl = Ember.ArrayController.create({ sortProperties: ['order'], sortAscending: true, preflightQId: null, + bulkCommit: false, populateAllQuestions: function () { var sId; @@ -889,24 +912,16 @@ FLOW.questionControl = Ember.ArrayController.create({ }, deleteQuestion: function (questionId) { - qgId = this.content.get('questionGroupId'); + var question, qgId, qOrder; question = FLOW.store.find(FLOW.Question, questionId); qgId = question.get('questionGroupId'); qOrder = question.get('order'); question.deleteRecord(); - // restore order - questionsInGroup = FLOW.store.filter(FLOW.Question, function (item) { - return item.get('questionGroupId') == qgId; - }); + //reorder the rest of the questions + this.reorderQuestions(qgId, qOrder, "decrement"); + this.submitBulkQuestionsReorder([qgId]); - questionsInGroup.forEach(function (item) { - if (item.get('order') > qOrder) { - item.set('order', item.get('order') - 1); - } - }); - // restore order in case the order has gone haywire - this.restoreOrder(questionsInGroup); FLOW.selectedControl.selectedSurvey.set('status', 'NOT_PUBLISHED'); FLOW.store.commit(); }, @@ -977,6 +992,38 @@ FLOW.questionControl = Ember.ArrayController.create({ } }.observes('FLOW.selectedControl.selectedQuestion'), + reorderQuestions: function (qgId, reorderPoint, reorderOperation) { + var questionsInGroup = FLOW.store.filter(FLOW.Question, function (item) { + return item.get('questionGroupId') == qgId; + }); + + // move items up to make space + questionsInGroup.forEach(function (item) { + if (reorderOperation == "increment") { + if (item.get('order') > reorderPoint) { + item.set('order', item.get('order') + 1); + } + } else if (reorderOperation == "decrement") { + if (item.get('order') > reorderPoint) { + item.set('order', item.get('order') - 1); + } + } + }); + }, + + submitBulkQuestionsReorder: function (qgIds) { + this.set('bulkCommit', true); + for (var i=0; i>qgIds.length; i++) { + var questionsInGroup = FLOW.store.filter(FLOW.Question, function (item) { + return item.get('questionGroupId') == qgIds[i]; + }); + // restore order in case the order has gone haywire + FLOW.questionControl.restoreOrder(questionsInGroup); + } + FLOW.store.commit(); + FLOW.store.adapter.set('bulkCommit', false); + this.set('bulkCommit', false); + }, // true if all items have been saved diff --git a/Dashboard/app/js/lib/models/FLOWrest-adapter-v2-common.js b/Dashboard/app/js/lib/models/FLOWrest-adapter-v2-common.js index 4a1fb54593..6ce2f9f0d4 100644 --- a/Dashboard/app/js/lib/models/FLOWrest-adapter-v2-common.js +++ b/Dashboard/app/js/lib/models/FLOWrest-adapter-v2-common.js @@ -185,6 +185,11 @@ DS.FLOWRESTAdapter = DS.RESTAdapter.extend({ // includes 'bulk' in the POST call, to allign // with updateRecords and deleteRecords behaviour. createRecords: function (store, type, records) { + //do not bulk commit when creating questions and question groups + if (FLOW.questionControl.get('bulkCommit')) { + this.set('bulkCommit', false); + } + if (get(this, 'bulkCommit') === false) { return this._super(store, type, records); } @@ -207,5 +212,22 @@ DS.FLOWRESTAdapter = DS.RESTAdapter.extend({ this.didCreateRecords(store, type, records, json); } }); + }, + + + updateRecords: function(store, type, records) { + //if updating questions and question groups ordering, enable bulkCommit + if (FLOW.questionControl.get('bulkCommit')) { + this.set('bulkCommit', true); + } + this._super(store, type, records); + }, + + deleteRecords: function(store, type, records) { + //do not bulk commit when deleting questions and question groups + if (FLOW.questionControl.get('bulkCommit')) { + this.set('bulkCommit', false); + } + this._super(store, type, records); } }); diff --git a/Dashboard/app/js/lib/views/data/inspect-data-table-views.js b/Dashboard/app/js/lib/views/data/inspect-data-table-views.js index 2158c42b95..36b289df9b 100644 --- a/Dashboard/app/js/lib/views/data/inspect-data-table-views.js +++ b/Dashboard/app/js/lib/views/data/inspect-data-table-views.js @@ -29,6 +29,7 @@ FLOW.inspectDataTableView = FLOW.View.extend({ FLOW.dateControl.set('toDate', null); FLOW.dateControl.set('fromDate', null); FLOW.surveyInstanceControl.set('pageNumber', 0); + FLOW.surveyInstanceControl.set('currentContents', null); FLOW.locationControl.set('selectedLevel1', null); FLOW.locationControl.set('selectedLevel2', null); }, @@ -149,7 +150,7 @@ FLOW.inspectDataTableView = FLOW.View.extend({ // Survey instance edit popup window // TODO solve when popup is open, no new surveyIdQuery is done showEditSurveyInstanceWindow: function (event) { - FLOW.questionAnswerControl.doQuestionAnswerQuery(event.context.get('keyId')); + FLOW.questionAnswerControl.doQuestionAnswerQuery(event.context); this.get('alreadyLoaded').push(event.context.get('surveyId')); this.set('selectedSurveyInstanceId', event.context.get('keyId')); this.set('selectedSurveyInstanceNum', event.context.clientId); @@ -181,12 +182,13 @@ FLOW.inspectDataTableView = FLOW.View.extend({ return false; } }); - nextSIkeyId = filtered.objectAt(0).get('keyId'); + var nextSI = filtered.objectAt(0); + nextSIkeyId = nextSI.get('keyId'); this.set('selectedSurveyInstanceId', nextSIkeyId); this.set('selectedSurveyInstanceNum', nextItem); this.createSurveyInstanceString(); this.downloadQuestionsIfNeeded(); - FLOW.questionAnswerControl.doQuestionAnswerQuery(nextSIkeyId); + FLOW.questionAnswerControl.doQuestionAnswerQuery(nextSI); } }, @@ -208,12 +210,13 @@ FLOW.inspectDataTableView = FLOW.View.extend({ return false; } }); - nextSIkeyId = filtered.objectAt(0).get('keyId'); + var nextSI = filtered.objectAt(0); + nextSIkeyId = nextSI.get('keyId'); this.set('selectedSurveyInstanceId', nextSIkeyId); this.set('selectedSurveyInstanceNum', nextItem); this.createSurveyInstanceString(); this.downloadQuestionsIfNeeded(); - FLOW.questionAnswerControl.doQuestionAnswerQuery(nextSIkeyId); + FLOW.questionAnswerControl.doQuestionAnswerQuery(nextSI); } }, diff --git a/Dashboard/app/js/lib/views/data/monitoring-data-table-view.js b/Dashboard/app/js/lib/views/data/monitoring-data-table-view.js index ff11e152af..3d0aa51c94 100644 --- a/Dashboard/app/js/lib/views/data/monitoring-data-table-view.js +++ b/Dashboard/app/js/lib/views/data/monitoring-data-table-view.js @@ -22,7 +22,7 @@ FLOW.MonitoringDataTableView = FLOW.View.extend({ }, showSurveyInstanceDetails: function (evt) { - FLOW.questionAnswerControl.doQuestionAnswerQuery(evt.context.get('keyId')); + FLOW.questionAnswerControl.doQuestionAnswerQuery(evt.context); $('.si_details').hide(); $('tr[data-flow-id="si_details_' + evt.context.get('keyId') + '"]').show(); }, @@ -98,6 +98,12 @@ FLOW.MonitoringDataTableView = FLOW.View.extend({ hasPrevPage: function () { return FLOW.router.surveyedLocaleController.get('pageNumber'); }.property('FLOW.router.surveyedLocaleController.pageNumber'), + + willDestroyElement: function () { + FLOW.router.surveyedLocaleController.set('currentContents', null); + FLOW.metaControl.set('numSLLoaded',null) + FLOW.router.surveyedLocaleController.set('pageNumber',0) + } }); /** diff --git a/Dashboard/app/js/lib/views/surveys/question-view.js b/Dashboard/app/js/lib/views/surveys/question-view.js index 6161aa5b1f..62e4c57dcf 100644 --- a/Dashboard/app/js/lib/views/surveys/question-view.js +++ b/Dashboard/app/js/lib/views/surveys/question-view.js @@ -447,7 +447,8 @@ FLOW.QuestionView = FLOW.View.extend({ throttleTimer: null, validateVariableName: function(args) { - var selectedQuestion = FLOW.selectedControl.selectedQuestion + var self = this; + var selectedQuestion = FLOW.selectedControl.selectedQuestion; var questionKeyId = selectedQuestion.get('keyId'); var variableName = this.get('variableName') || ""; if (FLOW.Env.mandatoryQuestionID && variableName.match(/^\s*$/)) { @@ -464,7 +465,12 @@ FLOW.QuestionView = FLOW.View.extend({ type: 'POST', success: function(data) { if (data.success) { - args.success(); + //check for special characters once more + if (!self.get('variableName').match(/^[A-Za-z0-9_\-]*$/)) { + args.failure(Ember.String.loc('_variable_name_only_alphanumeric')); + } else { + args.success(); + } } else { args.failure(data.reason); } @@ -559,7 +565,7 @@ FLOW.QuestionView = FLOW.View.extend({ // move question to selected location doQuestionMoveHere: function () { - var selectedOrder, insertAfterOrder, selectedQ, useMoveQuestion; + var selectedOrder, insertAfterOrder, selectedQ, useMoveQuestion, qgIdSource, qgIdDest; selectedOrder = FLOW.selectedControl.selectedForMoveQuestion.get('order'); if (this.get('zeroItemQuestion')) { @@ -580,47 +586,18 @@ FLOW.QuestionView = FLOW.View.extend({ selectedQ = FLOW.store.find(FLOW.Question, FLOW.selectedControl.selectedForMoveQuestion.get('keyId')); if (selectedQ !== null) { - // restore order qgIdSource = FLOW.selectedControl.selectedForMoveQuestion.get('questionGroupId'); qgIdDest = FLOW.selectedControl.selectedQuestionGroup.get('keyId'); - questionsInSourceGroup = FLOW.store.filter(FLOW.Question, function (item) { - return item.get('questionGroupId') == qgIdSource; - }); - - questionsInDestGroup = FLOW.store.filter(FLOW.Question, function (item) { - return item.get('questionGroupId') == qgIdDest; - }); - - // restore order in source group, where the question dissapears - questionsInSourceGroup.forEach(function (item) { - if (item.get('order') > selectedOrder) { - item.set('order', item.get('order') - 1); - } - }); - - // make room in destination group - questionsInDestGroup.forEach(function (item) { - if (item.get('order') > insertAfterOrder) { - item.set('order', item.get('order') + 1); - } - }); + // restore order + FLOW.questionControl.reorderQuestions(qgIdSource, selectedOrder, "decrement"); + FLOW.questionControl.reorderQuestions(qgIdDest, insertAfterOrder, "increment"); // move question selectedQ.set('order', insertAfterOrder + 1); selectedQ.set('questionGroupId', qgIdDest); - // recompute questions in groups so we can correct any order problems - questionsInSourceGroup = FLOW.store.filter(FLOW.Question, function (item) { - return item.get('questionGroupId') == qgIdSource; - }); - - questionsInDestGroup = FLOW.store.filter(FLOW.Question, function (item) { - return item.get('questionGroupId') == qgIdDest; - }); - - FLOW.questionControl.restoreOrder(questionsInSourceGroup); - FLOW.questionControl.restoreOrder(questionsInDestGroup); + FLOW.questionControl.submitBulkQuestionsReorder([qgIdSource, qgIdDest]); } // if we are not moving to another group, we must be moving inside a group // only do something if we are not moving to the same place @@ -658,13 +635,8 @@ FLOW.QuestionView = FLOW.View.extend({ } }); - questionsInGroup = FLOW.store.filter(FLOW.Question, function (item) { - return item.get('questionGroupId') == qgId; - }); - - // restore order in case the order has gone haywire - FLOW.questionControl.restoreOrder(questionsInGroup); - } + FLOW.questionControl.submitBulkQuestionsReorder([qgId]); + } } FLOW.selectedControl.selectedSurvey.set('status', 'NOT_PUBLISHED'); FLOW.store.commit(); @@ -673,7 +645,7 @@ FLOW.QuestionView = FLOW.View.extend({ // execute question copy to selected location doQuestionCopyHere: function () { - var insertAfterOrder, path, qgId, questionsInGroup, question; + var insertAfterOrder, path, qgId, question; //path = FLOW.selectedControl.selectedSurveyGroup.get('code') + "/" + FLOW.selectedControl.selectedSurvey.get('name') + "/" + FLOW.selectedControl.selectedQuestionGroup.get('code'); if (this.get('zeroItemQuestion')) { @@ -689,17 +661,10 @@ FLOW.QuestionView = FLOW.View.extend({ return; } - // restore order qgId = FLOW.selectedControl.selectedQuestionGroup.get('keyId'); - questionsInGroup = FLOW.store.filter(FLOW.Question, function (item) { - return item.get('questionGroupId') == qgId; - }); - // move items up to make space - questionsInGroup.forEach(function (item) { - if (item.get('order') > insertAfterOrder) { - item.set('order', item.get('order') + 1); - } - }); + + // restore order + FLOW.questionControl.reorderQuestions(qgId, insertAfterOrder, "increment"); question = FLOW.selectedControl.get('selectedForCopyQuestion'); // create copy of Question item in the store @@ -710,21 +675,16 @@ FLOW.QuestionView = FLOW.View.extend({ "sourceId":question.get('keyId') }); - questionsInGroup = FLOW.store.filter(FLOW.Question, function (item) { - return item.get('questionGroupId') == qgId; - }); + FLOW.questionControl.submitBulkQuestionsReorder([qgId]); - // restore order in case the order has gone haywire - FLOW.questionControl.restoreOrder(questionsInGroup); FLOW.selectedControl.selectedSurvey.set('status', 'NOT_PUBLISHED'); FLOW.store.commit(); - FLOW.selectedControl.set('selectedForCopyQuestion', null); }, // create new question doInsertQuestion: function () { - var insertAfterOrder, path, qgId, questionsInGroup; + var insertAfterOrder, path, qgId; path = FLOW.selectedControl.selectedSurveyGroup.get('code') + "/" + FLOW.selectedControl.selectedSurvey.get('name') + "/" + FLOW.selectedControl.selectedQuestionGroup.get('code'); if (this.get('zeroItemQuestion')) { @@ -741,18 +701,10 @@ FLOW.QuestionView = FLOW.View.extend({ } - // restore order qgId = FLOW.selectedControl.selectedQuestionGroup.get('keyId'); - questionsInGroup = FLOW.store.filter(FLOW.Question, function (item) { - return item.get('questionGroupId') == qgId; - }); - // move items up to make space - questionsInGroup.forEach(function (item) { - if (item.get('order') > insertAfterOrder) { - item.set('order', item.get('order') + 1); - } - }); + // reorder the rest of the questions + FLOW.questionControl.reorderQuestions(qgId, insertAfterOrder, "increment"); // create new Question item in the store FLOW.store.createRecord(FLOW.Question, { @@ -761,14 +713,11 @@ FLOW.QuestionView = FLOW.View.extend({ "path": path, "text": Ember.String.loc('_new_question_please_change_name'), "surveyId": FLOW.selectedControl.selectedSurvey.get('keyId'), - "questionGroupId": FLOW.selectedControl.selectedQuestionGroup.get('keyId') + "questionGroupId": qgId }); - questionsInGroup = FLOW.store.filter(FLOW.Question, function (item) { - return item.get('questionGroupId') == qgId; - }); - // restore order in case the order has gone haywire - FLOW.questionControl.restoreOrder(questionsInGroup); + FLOW.questionControl.submitBulkQuestionsReorder([qgId]); + FLOW.selectedControl.selectedSurvey.set('status', 'NOT_PUBLISHED'); FLOW.store.commit(); }, diff --git a/Dashboard/app/js/lib/views/surveys/survey-details-views.js b/Dashboard/app/js/lib/views/surveys/survey-details-views.js index 05e0b746de..17b706316b 100644 --- a/Dashboard/app/js/lib/views/surveys/survey-details-views.js +++ b/Dashboard/app/js/lib/views/surveys/survey-details-views.js @@ -371,7 +371,7 @@ FLOW.QuestionGroupItemView = FLOW.View.extend({ // insert group doInsertQuestionGroup: function () { - var insertAfterOrder, path, sId, questionGroupsInSurvey; + var insertAfterOrder, path, sId; path = FLOW.selectedControl.selectedSurveyGroup.get('code') + "/" + FLOW.selectedControl.selectedSurvey.get('name'); if (FLOW.selectedControl.selectedSurvey.get('keyId')) { @@ -383,16 +383,9 @@ FLOW.QuestionGroupItemView = FLOW.View.extend({ // restore order sId = FLOW.selectedControl.selectedSurvey.get('keyId'); - questionGroupsInSurvey = FLOW.store.filter(FLOW.QuestionGroup, function (item) { - return item.get('surveyId') == sId; - }); - // move items up to make space - questionGroupsInSurvey.forEach(function (item) { - if (item.get('order') > insertAfterOrder) { - item.set('order', item.get('order') + 1); - } - }); + // reorder the rest of the question groups + FLOW.questionGroupControl.reorderQuestionGroups(sId, insertAfterOrder, "increment"); // create new QuestionGroup item in the store FLOW.store.createRecord(FLOW.QuestionGroup, { @@ -404,13 +397,7 @@ FLOW.QuestionGroupItemView = FLOW.View.extend({ "surveyId": FLOW.selectedControl.selectedSurvey.get('keyId') }); - // get the question groups again, now it contains the new one as well - questionGroupsInSurvey = FLOW.store.filter(FLOW.QuestionGroup, function (item) { - return item.get('surveyId') == sId; - }); - - // restore order in case the order has gone haywire - FLOW.questionControl.restoreOrder(questionGroupsInSurvey); + FLOW.questionGroupControl.submitBulkQuestionGroupsReorder(sId); FLOW.selectedControl.selectedSurvey.set('status', 'NOT_PUBLISHED'); FLOW.store.commit(); @@ -496,8 +483,7 @@ FLOW.QuestionGroupItemView = FLOW.View.extend({ } }); - // restore order in case the order has gone haywire - FLOW.questionControl.restoreOrder(questionGroupsInSurvey); + FLOW.questionGroupControl.submitBulkQuestionGroupsReorder(sId); FLOW.selectedControl.selectedSurvey.set('status', 'NOT_PUBLISHED'); FLOW.store.commit(); @@ -548,7 +534,7 @@ FLOW.QuestionGroupItemView = FLOW.View.extend({ // execute group copy to selected location doQGroupCopyHere: function () { - var insertAfterOrder, path, sId, questionGroupsInSurvey; + var insertAfterOrder, path, sId; path = FLOW.selectedControl.selectedSurveyGroup.get('code') + "/" + FLOW.selectedControl.selectedSurvey.get('name'); if (this.get('zeroItem')) { @@ -558,16 +544,9 @@ FLOW.QuestionGroupItemView = FLOW.View.extend({ } sId = FLOW.selectedControl.selectedSurvey.get('keyId'); - questionGroupsInSurvey = FLOW.store.filter(FLOW.QuestionGroup, function (item) { - return item.get('surveyId') === sId; - }); - // restore order - move items up to make space - questionGroupsInSurvey.forEach(function (item) { - if (item.get('order') > insertAfterOrder) { - item.set('order', item.get('order') + 1); - } - }); + // restore order + FLOW.questionGroupControl.reorderQuestionGroups(sId, insertAfterOrder, "increment"); FLOW.store.createRecord(FLOW.QuestionGroup, { "order": insertAfterOrder + 1, @@ -580,6 +559,8 @@ FLOW.QuestionGroupItemView = FLOW.View.extend({ "repeatable":FLOW.selectedControl.selectedForCopyQuestionGroup.get('repeatable') }); + FLOW.questionGroupControl.submitBulkQuestionGroupsReorder(sId); + FLOW.selectedControl.selectedSurvey.set('status', 'NOT_PUBLISHED'); FLOW.store.commit(); FLOW.selectedControl.set('selectedForCopyQuestionGroup', null); diff --git a/Dashboard/app/js/templates/navDevices/bootstrap-tab/survey-bootstrap.handlebars b/Dashboard/app/js/templates/navDevices/bootstrap-tab/survey-bootstrap.handlebars index fd9506e531..7a2a7db699 100644 --- a/Dashboard/app/js/templates/navDevices/bootstrap-tab/survey-bootstrap.handlebars +++ b/Dashboard/app/js/templates/navDevices/bootstrap-tab/survey-bootstrap.handlebars @@ -2,7 +2,8 @@
-
+
+

01. {{t _select_survey}}:

{{t _cant_find_your_survey_}}
@@ -30,7 +31,9 @@ {{t _add_selected_forms}}
-
+
+
+

{{t _preview_survey_selection}}:

@@ -63,6 +66,7 @@
+
diff --git a/GAE/src/com/gallatinsystems/framework/dao/BaseDAO.java b/GAE/src/com/gallatinsystems/framework/dao/BaseDAO.java index 6cc0a96f3d..004cb87a2d 100644 --- a/GAE/src/com/gallatinsystems/framework/dao/BaseDAO.java +++ b/GAE/src/com/gallatinsystems/framework/dao/BaseDAO.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010-2017 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2010-2018 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo FLOW. * @@ -28,7 +28,9 @@ import com.google.appengine.api.datastore.Cursor; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.KeyFactory; + import net.sf.jsr107cache.CacheException; + import org.akvo.flow.domain.SecuredObject; import org.datanucleus.store.appengine.query.JDOCursorHelper; import org.springframework.security.core.Authentication; @@ -36,6 +38,7 @@ import javax.jdo.JDOObjectNotFoundException; import javax.jdo.PersistenceManager; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -87,6 +90,7 @@ public void setDomainClass(Class e) { this.concreteClass = e; } + /** * saves an object to the data store. This method will set the lastUpdateDateTime on the domain * object prior to saving and will set the createdDateTime (if it is null). @@ -97,9 +101,12 @@ public void setDomainClass(Class e) { */ public E save(E obj) { PersistenceManager pm = PersistenceFilter.getManager(); + Long who = org.waterforpeople.mapping.app.web.CurrentUserServlet.getCurrentUserId(); obj.setLastUpdateDateTime(new Date()); + obj.setLastUpdateUserId(who); if (obj.getCreatedDateTime() == null) { obj.setCreatedDateTime(obj.getLastUpdateDateTime()); + obj.setCreateUserId(who); } obj = pm.makePersistent(obj); return obj; @@ -115,10 +122,13 @@ public E save(E obj) { */ public Collection save(Collection objList) { if (objList != null) { + Long who = org.waterforpeople.mapping.app.web.CurrentUserServlet.getCurrentUserId(); for (E item : objList) { item.setLastUpdateDateTime(new Date()); + item.setLastUpdateUserId(who); if (item.getCreatedDateTime() == null) { item.setCreatedDateTime(item.getLastUpdateDateTime()); + item.setCreateUserId(who); } } PersistenceManager pm = PersistenceFilter.getManager(); diff --git a/GAE/src/com/gallatinsystems/survey/dao/QuestionDao.java b/GAE/src/com/gallatinsystems/survey/dao/QuestionDao.java index 349aa9fe34..504f6be967 100644 --- a/GAE/src/com/gallatinsystems/survey/dao/QuestionDao.java +++ b/GAE/src/com/gallatinsystems/survey/dao/QuestionDao.java @@ -124,7 +124,7 @@ public void delete(List qList) { * @param question */ public void delete(Question question) throws IllegalDeletionException { - delete(question, Boolean.TRUE); + delete(question, Boolean.FALSE); } /** diff --git a/GAE/src/com/gallatinsystems/survey/domain/Question.java b/GAE/src/com/gallatinsystems/survey/domain/Question.java index b05f573fb7..c9c81e08ca 100644 --- a/GAE/src/com/gallatinsystems/survey/domain/Question.java +++ b/GAE/src/com/gallatinsystems/survey/domain/Question.java @@ -21,7 +21,6 @@ import javax.jdo.annotations.NotPersistent; import javax.jdo.annotations.PersistenceCapable; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -183,12 +182,6 @@ public void setTranslationMap(Map translationMap) { this.translationMap = translationMap; } - public void setTranslationMap(HashMap transMap) { - if (transMap != null) { - translationMap = new TreeMap(transMap); - } - } - public void addQuestionOption(QuestionOption questionOption) { if (getQuestionOptionMap() == null) setQuestionOptionMap(new TreeMap()); @@ -396,6 +389,7 @@ public String getVariableName() { public void setVariableName(String variableName) { this.variableName = variableName; + questionId = null; //ensure correct fallback } public Long getCascadeResourceId() { diff --git a/GAE/src/org/waterforpeople/mapping/app/web/CurrentUserServlet.java b/GAE/src/org/waterforpeople/mapping/app/web/CurrentUserServlet.java index dbdc369210..2ee08a7e69 100644 --- a/GAE/src/org/waterforpeople/mapping/app/web/CurrentUserServlet.java +++ b/GAE/src/org/waterforpeople/mapping/app/web/CurrentUserServlet.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2016 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2012-2016, 2018 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo FLOW. * @@ -113,6 +113,14 @@ public static User getCurrentUser() { return uDao.findUserByEmail(currentUserEmail); } + public static Long getCurrentUserId() { + User u = getCurrentUser(); + if (u == null || u.getKey() == null) { + return null; + } + return u.getKey().getId(); + } + /** * Retrieve a javascript map of the paths and corresponding permissions for the current user * diff --git a/GAE/src/org/waterforpeople/mapping/app/web/rest/QuestionGroupRestService.java b/GAE/src/org/waterforpeople/mapping/app/web/rest/QuestionGroupRestService.java index 3a9621501a..e228d6237c 100644 --- a/GAE/src/org/waterforpeople/mapping/app/web/rest/QuestionGroupRestService.java +++ b/GAE/src/org/waterforpeople/mapping/app/web/rest/QuestionGroupRestService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2017 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2012-2018 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo FLOW. * @@ -34,6 +34,7 @@ import org.waterforpeople.mapping.app.gwt.client.survey.QuestionGroupDto; import org.waterforpeople.mapping.app.web.dto.DataProcessorRequest; import org.waterforpeople.mapping.app.web.dto.SurveyTaskRequest; +import org.waterforpeople.mapping.app.web.rest.dto.QuestionGroupListPayload; import org.waterforpeople.mapping.app.web.rest.dto.QuestionGroupPayload; import org.waterforpeople.mapping.app.web.rest.dto.RestStatusDto; import org.waterforpeople.mapping.dao.QuestionAnswerStoreDao; @@ -52,7 +53,6 @@ @Controller @RequestMapping("/question_groups") public class QuestionGroupRestService { - private QuestionGroupDao questionGroupDao = new QuestionGroupDao(); private QuestionDao questionDao = new QuestionDao(); @@ -231,6 +231,56 @@ public Map saveExistingQuestionGroup( return response; } + // update several existing question groups + @RequestMapping(method = RequestMethod.PUT, value = "/bulk") + @ResponseBody + public Map saveExistingQuestionGroups( + @RequestBody QuestionGroupListPayload payLoad) { + final Map response = new HashMap(); + RestStatusDto statusDto = new RestStatusDto(); + statusDto.setStatus("failed"); + statusDto.setMessage("No question groups to change"); + List saveList = new ArrayList<>(); + + //Loop over question groups + final List requestList = payLoad.getQuestion_groups(); + if (requestList != null && requestList.size() > 0) { + for (final QuestionGroupDto questionGroupDto : requestList) { + + if (questionGroupDto != null) { + Long keyId = questionGroupDto.getKeyId(); + QuestionGroup qg; + + // if the questionGroupDto has a key, try to get the question group. + if (keyId != null) { + qg = questionGroupDao.getByKey(keyId); + // if we find the question, update it's properties + if (qg != null) { + // copy the properties, except the createdDateTime property, + // because it is set in the Dao. + BeanUtils.copyProperties(questionGroupDto, qg, + new String[] {"createdDateTime", "status"}); + saveList.add(qg); + } else { //missing in db - fail + statusDto.setMessage("Cannot change unknown question group " + keyId); + response.put("meta", statusDto); + return response; + } + } else { //no db key - fail + statusDto.setMessage("Cannot change question group without id"); + response.put("meta", statusDto); + return response; + } + } + } + questionGroupDao.save(saveList); + statusDto.setStatus("ok"); + statusDto.setMessage(""); + } + response.put("meta", statusDto); + return response; + } + // create new questionGroup @RequestMapping(method = RequestMethod.POST, value = "") @ResponseBody diff --git a/GAE/src/org/waterforpeople/mapping/app/web/rest/QuestionRestService.java b/GAE/src/org/waterforpeople/mapping/app/web/rest/QuestionRestService.java index b221f0ca46..6b3343e601 100644 --- a/GAE/src/org/waterforpeople/mapping/app/web/rest/QuestionRestService.java +++ b/GAE/src/org/waterforpeople/mapping/app/web/rest/QuestionRestService.java @@ -42,6 +42,7 @@ import org.waterforpeople.mapping.app.gwt.client.survey.QuestionDto; import org.waterforpeople.mapping.app.gwt.client.survey.QuestionOptionDto; import org.waterforpeople.mapping.app.web.dto.SurveyTaskRequest; +import org.waterforpeople.mapping.app.web.rest.dto.QuestionListPayload; import org.waterforpeople.mapping.app.web.rest.dto.QuestionPayload; import org.waterforpeople.mapping.app.web.rest.dto.RestStatusDto; import org.waterforpeople.mapping.dao.QuestionAnswerStoreDao; @@ -275,6 +276,62 @@ public Map saveExistingQuestion( return response; } + // update several existing questions + // questionOptions are saved and updated on their own + @RequestMapping(method = RequestMethod.PUT, value = "/bulk") + @ResponseBody + public Map saveExistingQuestions( + @RequestBody QuestionListPayload payLoad) { + final Map response = new HashMap(); + RestStatusDto statusDto = new RestStatusDto(); + statusDto.setStatus("failed"); + statusDto.setMessage("No questions to change"); + List saveList = new ArrayList<>(); + + //Loop over questions + final List requestList = payLoad.getQuestions(); + if (requestList != null && requestList.size() > 0) { + for (final QuestionDto questionDto : requestList) { + + if (questionDto != null) { + Long keyId = questionDto.getKeyId(); + Question q; + + // if the questionDto has a key, try to get the question. + if (keyId != null) { + q = questionDao.getByKey(keyId); + // if we find the question, update it's properties + if (q != null) { + // copy the properties, except the createdDateTime property, + // because it is set in the Dao. + BeanUtils.copyProperties(questionDto, q, new String[] { + "createdDateTime", "type", "optionList" + }); + if (questionDto.getType() != null) + q.setType(Question.Type.valueOf(questionDto.getType() + .toString())); + saveList.add(q); + + } else { //missing in db - fail + statusDto.setMessage("Cannot change unknown question " + keyId); + response.put("meta", statusDto); + return response; + } + } else { //no db key - fail + statusDto.setMessage("Cannot change question without id"); + response.put("meta", statusDto); + return response; + } + } + } + questionDao.save(saveList); + statusDto.setStatus("ok"); + statusDto.setMessage(""); + } + response.put("meta", statusDto); + return response; + } + // create new question // questionOptions are saved and updated on their own @RequestMapping(method = RequestMethod.POST, value = "") diff --git a/GAE/src/org/waterforpeople/mapping/app/web/rest/dto/QuestionGroupListPayload.java b/GAE/src/org/waterforpeople/mapping/app/web/rest/dto/QuestionGroupListPayload.java new file mode 100644 index 0000000000..951df9d27f --- /dev/null +++ b/GAE/src/org/waterforpeople/mapping/app/web/rest/dto/QuestionGroupListPayload.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018 Stichting Akvo (Akvo Foundation) + * + * This file is part of Akvo FLOW. + * + * Akvo FLOW is free software: you can redistribute it and modify it under the terms of + * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, + * either version 3 of the License or any later version. + * + * Akvo FLOW is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License included below for more details. + * + * The full license text can also be seen at . + */ + +package org.waterforpeople.mapping.app.web.rest.dto; + +import java.io.Serializable; +import java.util.List; + +import org.waterforpeople.mapping.app.gwt.client.survey.QuestionGroupDto; + +public class QuestionGroupListPayload implements Serializable { + + /** + * + */ + private static final long serialVersionUID = 1L; + List question_groups = null; + + public List getQuestion_groups() { + return question_groups; + } + + public void setQuestion_groups(List question_groups) { + this.question_groups = question_groups; + } +} diff --git a/GAE/src/org/waterforpeople/mapping/app/web/rest/dto/QuestionListPayload.java b/GAE/src/org/waterforpeople/mapping/app/web/rest/dto/QuestionListPayload.java new file mode 100644 index 0000000000..79ca33d9d1 --- /dev/null +++ b/GAE/src/org/waterforpeople/mapping/app/web/rest/dto/QuestionListPayload.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018 Stichting Akvo (Akvo Foundation) + * + * This file is part of Akvo FLOW. + * + * Akvo FLOW is free software: you can redistribute it and modify it under the terms of + * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, + * either version 3 of the License or any later version. + * + * Akvo FLOW is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License included below for more details. + * + * The full license text can also be seen at . + */ + +package org.waterforpeople.mapping.app.web.rest.dto; + +import java.io.Serializable; +import java.util.List; + +import org.waterforpeople.mapping.app.gwt.client.survey.QuestionDto; + +public class QuestionListPayload implements Serializable { + + /** + * + */ + private static final long serialVersionUID = 1L; + List questions = null; + + public List getQuestions() { + return questions; + } + + public void setQuestions(List questions) { + this.questions = questions; + } +} diff --git a/GAE/src/org/waterforpeople/mapping/dao/SurveyInstanceDAO.java b/GAE/src/org/waterforpeople/mapping/dao/SurveyInstanceDAO.java index 696a1ad9a1..712dc2a39c 100644 --- a/GAE/src/org/waterforpeople/mapping/dao/SurveyInstanceDAO.java +++ b/GAE/src/org/waterforpeople/mapping/dao/SurveyInstanceDAO.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010-2015, 2017 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2010-2015, 2017, 2018 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo FLOW. * @@ -145,17 +145,28 @@ public SurveyInstance save(SurveyInstance si, DeviceFiles deviceFile) { String deviceId = d == null ? "null" : String.valueOf(d.getKey().getId()); for (QuestionAnswerStore qas : images) { - String filename = qas.getValue().substring( - qas.getValue().lastIndexOf("/") + 1); - - Queue queue = QueueFactory.getQueue("background-processing"); - TaskOptions to = TaskOptions.Builder - .withUrl("/app_worker/imagecheck") - .param(ImageCheckRequest.FILENAME_PARAM, filename) - .param(ImageCheckRequest.DEVICE_ID_PARAM, deviceId) - .param(ImageCheckRequest.QAS_ID_PARAM, String.valueOf(qas.getKey().getId())) - .param(ImageCheckRequest.ATTEMPT_PARAM, "1"); - queue.add(to); + String value = qas.getValue(); + String filename = null; + if (value.startsWith("{")) { //JSON + final String key = "\"filename\":\""; + int i = value.indexOf(key); + if (i > -1) { //key found, grab all until next " + filename = value.substring(i + key.length()).split("\"", 2)[0]; + } + } else { //legacy: naked filename + filename = value; + } + if (filename != null) { + filename = filename.substring(filename.lastIndexOf("/") + 1); //strip path + Queue queue = QueueFactory.getQueue("background-processing"); + TaskOptions to = TaskOptions.Builder + .withUrl("/app_worker/imagecheck") + .param(ImageCheckRequest.FILENAME_PARAM, filename) + .param(ImageCheckRequest.DEVICE_ID_PARAM, deviceId) + .param(ImageCheckRequest.QAS_ID_PARAM, String.valueOf(qas.getKey().getId())) + .param(ImageCheckRequest.ATTEMPT_PARAM, "1"); + queue.add(to); + } } } diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index b1941f4234..2d8aa775d5 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,20 @@ # Akvo Flow Release Notes ---- +# Akvo Flow Dashboard v1.9.30 - Deterministic Daisy +Date: 03 April 2018 + +## New and noteworthy +* **Translations** - Add translations for Pijin and Tok Pisin [#2202] +* **Data security** - Log who changed entity [#2525] +* **Data point location** - Created a script to fix data point location [#2520]. This stems from issue [#2518] where a data point's location was being overriden by GEO type question answers from monitoring forms + +## Resolved issues +* **Questions reordering** - Changed how questions and question groups are reordered [#1393],[#2523]. Reordering is now done in bulk for all affected so that order does not break in case of unstable network issues +* **User interface** - Remove submissions list from monitoring tab after users navigates away [#2531]. Fixed alignment on manual survey transfers tab [#2276]. Load questions and answers for monitoring forms when viewing data in monitoring tab [#2567] +* **Variable name validation** - Recheck variable name for special characters after successful validation [#2563]. Preempt returning of old values when variable name is set to null [#2551] +* **Code cleanup** - Deleted code causing exception to be thrown when listing questions [#2421] +* **Performance improvement** - No longer putting bad file names in device file job queue [#2539] + # Akvo Flow Dashboard v1.9.29 - Cosmic Coconut Date: 08 March 2018 diff --git a/scripts/data/check-datapoint-location.sh b/scripts/data/check-datapoint-location.sh new file mode 100755 index 0000000000..6e64238145 --- /dev/null +++ b/scripts/data/check-datapoint-location.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# USAGE: ./check-datapoint-location.sh akvoflow-uat1 [surveyId] [FIX] +# FIX to actually fix the datapoint location +APP_ID=$1 +SERVICE_ACCOUNT="sa-$APP_ID@$APP_ID.iam.gserviceaccount.com" +REPOS_HOME="$(cd $(dirname "$THIS_SCRIPT")/../../.. && pwd)" +P12_FILE_PATH="$REPOS_HOME/akvo-flow-server-config/$1/$1.p12" + +java -cp bin:"lib/*" \ + org.akvo.gae.remoteapi.RemoteAPI \ + CheckDataPointLocation \ + $APP_ID \ + $SERVICE_ACCOUNT \ + $P12_FILE_PATH \ + $2 $3 diff --git a/scripts/data/src/org/akvo/gae/remoteapi/CheckDataPointLocation.java b/scripts/data/src/org/akvo/gae/remoteapi/CheckDataPointLocation.java new file mode 100644 index 0000000000..b31710faba --- /dev/null +++ b/scripts/data/src/org/akvo/gae/remoteapi/CheckDataPointLocation.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2018 Stichting Akvo (Akvo Foundation) + * + * This file is part of Akvo FLOW. + * + * Akvo FLOW is free software: you can redistribute it and modify it under the terms of + * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, + * either version 3 of the License or any later version. + * + * Akvo FLOW is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License included below for more details. + * + * The full license text can also be seen at . + */ + +package org.akvo.gae.remoteapi; + +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.EntityNotFoundException; +import com.google.appengine.api.datastore.FetchOptions; +import com.google.appengine.api.datastore.Key; +import com.google.appengine.api.datastore.KeyFactory; +import com.google.appengine.api.datastore.PreparedQuery; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.datastore.Query.FilterOperator; +import com.google.appengine.api.datastore.Query.FilterPredicate; + +import java.util.ArrayList; +import java.util.List; + +/* + * - Checks that the DataPoint (SurveyedLocale) location is correct and updates it using data from + * the localeGeolocation field of each SurveyInstance. + * + * This may take a very long time so it is better to provide the surveyId (the id of SurveyGroup) + */ +public class CheckDataPointLocation implements Process { + + private static final long MISSING_SURVEY_ID = -1; + + @Override + public void execute(DatastoreService ds, String[] args) { + long timeStart = System.currentTimeMillis(); + System.out.println( + "#Arguments: survey id to fix one survey, FIX to correct data point location"); + + boolean fixDataPointLocation = dataPointFixRequested(args); + long surveyId = getRequestedSurveyId(args); + + List dataPointsToFix = getDataToFix(ds, surveyId); + + long timeEnd = System.currentTimeMillis(); + System.out.println("Getting data to fix took: " + (timeEnd - timeStart) + " ms"); + System.out.println(dataPointsToFix.size() + " data points need update"); + + fixDataPoints(ds, timeStart, fixDataPointLocation, dataPointsToFix); + } + + private void fixDataPoints(DatastoreService ds, long timeStart, boolean fixDataPointLocation, + List dataPointsToFix) { + if (fixDataPointLocation) { + if (!dataPointsToFix.isEmpty()) { + System.out.println("Will fix data..."); + DataUtils.batchSaveEntities(ds, dataPointsToFix); + long timeEnd = System.currentTimeMillis(); + System.out.println("Fixing data took: " + (timeEnd - timeStart) + " ms"); + } else { + System.out.println("No data to fix..."); + } + } + } + + private long getRequestedSurveyId(String[] args) { + long surveyId = MISSING_SURVEY_ID; + if (args == null) { + return surveyId; + } + for (String arg : args) { + surveyId = safeParseSurveyId(arg); + if (surveyId != MISSING_SURVEY_ID) { + return surveyId; + } + } + return surveyId; + } + + private boolean dataPointFixRequested(String[] args) { + if (args == null) { + return false; + } + for (String arg : args) { + if ("FIX".equalsIgnoreCase(arg)) { + return true; + } + } + return false; + } + + private long safeParseSurveyId(String longAsString) { + if (longAsString == null || longAsString.isEmpty()) { + return MISSING_SURVEY_ID; + } + try { + return Long.parseLong(longAsString); + } catch (NumberFormatException e) { + return MISSING_SURVEY_ID; + } + } + + private List getDataToFix(DatastoreService ds, long surveyId) { + List brokenDataPoints = new ArrayList<>(); + if (surveyId == MISSING_SURVEY_ID) { + Iterable entities = getSurveys(ds); + int surveyCounter = 0; + for (Entity survey : entities) { + surveyCounter++; + brokenDataPoints.addAll(getDataPointsToFixForSurvey(ds, survey)); + } + System.out.println("Found " + surveyCounter + " monitored SurveyGroups to check"); + } else { + Key key = KeyFactory.createKey("SurveyGroup", surveyId); + try { + System.out.println("Checking data points for one survey: "+surveyId); + Entity entity = ds.get(key); + brokenDataPoints = getDataPointsToFixForSurvey(ds, entity); + } catch (EntityNotFoundException e) { + e.printStackTrace(); + } + } + return brokenDataPoints; + } + + private List getDataPointsToFixForSurvey(DatastoreService ds, Entity survey) { + List brokenDataPoints = new ArrayList<>(); + long surveyId = survey.getKey().getId(); + Long registrationFormId = (Long) survey.getProperty("newLocaleSurveyId"); + if (registrationFormId != null) { + List list = getForms(ds, surveyId); + if (list.size() > 1) { + Entity geoQuestion = getGeoQuestion(ds, registrationFormId); + if (geoQuestion != null) { + List dataPoints = getDataPoints(ds, surveyId); + if (dataPoints.size() > 0) { + long geoQuestionId = geoQuestion.getKey().getId(); + for (Entity dataPoint : dataPoints) { + Entity dataPointToUpdate = getDataPointToUpdate(ds, dataPoint, + registrationFormId, geoQuestionId); + if (dataPointToUpdate != null) { + brokenDataPoints.add(dataPointToUpdate); + } + } + } + } + } + } + return brokenDataPoints; + } + + private Entity getDataPointToUpdate(DatastoreService ds, Entity dataPoint, + Long registrationFormId, long geoQuestionId) { + long dataPointId = dataPoint.getKey().getId(); + long surveyInstanceId = getSurveyInstanceId(ds, registrationFormId, + (String) dataPoint.getProperty("identifier"), dataPointId); + if (surveyInstanceId != -1) { + Entity questionAnswer = getQuestionAnswer(ds, surveyInstanceId, geoQuestionId); + if (questionAnswer != null) { + Double dataPointLatitude = (Double) dataPoint.getProperty("latitude"); + Double dataPointLongitude = (Double) dataPoint.getProperty("longitude"); + + String answerValue = (String) questionAnswer.getProperty("value"); + if (answerValue != null && !answerValue.isEmpty()) { + String[] answerBits = answerValue.split("\\|"); + if (answerBits.length > 1) { + Double answerLatitude = safeParseDouble(answerBits[0]); + Double answerLongitude = safeParseDouble(answerBits[1]); + + boolean dataPointLocationNeedsUpdate = + answerLatitude != null && answerLongitude != null + && (!answerLatitude.equals(dataPointLatitude) + || !answerLongitude.equals(dataPointLongitude)); + if (dataPointLocationNeedsUpdate) { + dataPoint.setProperty("latitude", answerLatitude); + dataPoint.setProperty("longitude", answerLongitude); + + System.out.println("Data point " + dataPointId + " needs fixing"); + return dataPoint; + } + } + } + } + } + return null; + } + + private Double safeParseDouble(String doubleAsString) { + if (doubleAsString == null || doubleAsString.isEmpty()) { + return null; + } + try { + return Double.parseDouble(doubleAsString); + } catch (NumberFormatException e) { + return null; + } + } + + private Entity getQuestionAnswer(DatastoreService ds, long surveyInstanceId, long questionId) { + Query.Filter f1 = new FilterPredicate("surveyInstanceId", FilterOperator.EQUAL, + surveyInstanceId); + Query.Filter f2 = new FilterPredicate("questionID", FilterOperator.EQUAL, + questionId + ""); + Query q = new Query("QuestionAnswerStore"); + q.setFilter(Query.CompositeFilterOperator.and(f1, f2)); + PreparedQuery pq = ds.prepare(q); + return pq.asSingleEntity(); + } + + private long getSurveyInstanceId(DatastoreService ds, long formId, + String surveyedLocaleIdentifier, long surveyedLocaleId) { + Query.Filter f1 = new FilterPredicate("surveyId", FilterOperator.EQUAL, formId); + Query.Filter f3 = Query.CompositeFilterOperator + .or(new FilterPredicate("surveyedLocaleId", FilterOperator.EQUAL, surveyedLocaleId), + new FilterPredicate("surveyedLocaleIdentifier", FilterOperator.EQUAL, + surveyedLocaleIdentifier)); + Query q = new Query("SurveyInstance"); + q.setFilter(Query.CompositeFilterOperator.and(f1, f3)); + q.setKeysOnly(); + PreparedQuery pq = ds.prepare(q); + Entity surveyInstance = pq.asSingleEntity(); + if (surveyInstance == null) { + return -1; + } + return surveyInstance.getKey().getId(); + } + + private Entity getGeoQuestion(DatastoreService ds, long surveyId) { + Query.Filter f1 = new FilterPredicate("surveyId", FilterOperator.EQUAL, surveyId); + Query.Filter f2 = new FilterPredicate("type", FilterOperator.EQUAL, "GEO"); + Query.Filter f3 = new FilterPredicate("localeLocationFlag", FilterOperator.EQUAL, + Boolean.TRUE); + Query q = new Query("Question"); + q.setFilter(Query.CompositeFilterOperator.and(f1, f2, f3)); + PreparedQuery pq = ds.prepare(q); + List entities = pq.asList(FetchOptions.Builder.withLimit(1)); + if (entities == null || entities.size() == 0) { + return null; + } + return entities.get(0); + } + + private List getDataPoints(DatastoreService ds, long surveyId) { + Query.Filter f1 = new FilterPredicate("surveyGroupId", FilterOperator.EQUAL, surveyId); + Query q = new Query("SurveyedLocale"); + q.setFilter(f1); + PreparedQuery pq = ds.prepare(q); + return pq.asList(FetchOptions.Builder.withDefaults()); + } + + private List getForms(DatastoreService ds, long surveyId) { + Query.Filter f1 = new FilterPredicate("surveyGroupId", FilterOperator.EQUAL, surveyId); + Query.Filter f2 = new FilterPredicate("status", FilterOperator.EQUAL, + "PUBLISHED"); + Query.CompositeFilter compositeFilter = + Query.CompositeFilterOperator.and(f1, f2); + Query q = new Query("Survey").setFilter(compositeFilter).setKeysOnly(); + + PreparedQuery pq = ds.prepare(q); + return pq.asList(FetchOptions.Builder.withLimit(2)); + } + + private Iterable getSurveys(DatastoreService ds) { + Query.Filter f = new FilterPredicate("monitoringGroup", FilterOperator.EQUAL, true); + Query q = new Query("SurveyGroup").setFilter(f); + PreparedQuery pq = ds.prepare(q); + return pq.asIterable(); + } +}