" will be accepted
+ if self.validation_email:
if not email_validator.match(answer):
- errors.update({answer_tag: _('This answer must be an email address')})
+ return {self.id: _('This answer must be an email address')}
+
# Answer validation (if properly defined)
# Length of the answer must be in a range
- if answer and self.validation_required:
+ if self.validation_required:
if not (self.validation_length_min <= len(answer) <= self.validation_length_max):
- errors.update({answer_tag: self.validation_error_msg})
- return errors
+ return {self.id: self.validation_error_msg}
+ return {}
- def validate_numerical_box(self, post, answer_tag):
- self.ensure_one()
- errors = {}
- answer = post[answer_tag].strip()
- # Empty answer to mandatory question
- if self.constr_mandatory and not answer:
- errors.update({answer_tag: self.constr_error_msg})
- # Checks if user input is a number
- if answer:
- try:
- floatanswer = float(answer)
- except ValueError:
- errors.update({answer_tag: _('This is not a number')})
- # Answer validation (if properly defined)
- if answer and self.validation_required:
+ def _validate_numerical_box(self, answer):
+ try:
+ floatanswer = float(answer)
+ except ValueError:
+ return {self.id: _('This is not a number')}
+
+ if self.validation_required:
# Answer is not in the right range
with tools.ignore(Exception):
- floatanswer = float(answer) # check that it is a float has been done hereunder
if not (self.validation_min_float_value <= floatanswer <= self.validation_max_float_value):
- errors.update({answer_tag: self.validation_error_msg})
- return errors
+ return {self.id: self.validation_error_msg}
+ return {}
- def date_validation(self, date_type, post, answer_tag, min_value, max_value):
- self.ensure_one()
- errors = {}
- if date_type not in ('date', 'datetime'):
- raise ValueError("Unexpected date type value")
- answer = post[answer_tag].strip()
- # Empty answer to mandatory question
- if self.constr_mandatory and not answer:
- errors.update({answer_tag: self.constr_error_msg})
+ def _validate_date(self, answer):
+ isDatetime = self.question_type == 'datetime'
# Checks if user input is a date
- if answer:
- try:
- if date_type == 'datetime':
- dateanswer = fields.Datetime.from_string(answer)
- else:
- dateanswer = fields.Date.from_string(answer)
- except ValueError:
- errors.update({answer_tag: _('This is not a date')})
- return errors
- # Answer validation (if properly defined)
- if answer and self.validation_required:
- # Answer is not in the right range
- try:
- if date_type == 'datetime':
- date_from_string = fields.Datetime.from_string
- else:
- date_from_string = fields.Date.from_string
- dateanswer = date_from_string(answer)
- min_date = date_from_string(min_value)
- max_date = date_from_string(max_value)
-
- if min_date and max_date and not (min_date <= dateanswer <= max_date):
- # If Minimum and Maximum Date are entered
- errors.update({answer_tag: self.validation_error_msg})
- elif min_date and not min_date <= dateanswer:
- # If only Minimum Date is entered and not Define Maximum Date
- errors.update({answer_tag: self.validation_error_msg})
- elif max_date and not dateanswer <= max_date:
- # If only Maximum Date is entered and not Define Minimum Date
- errors.update({answer_tag: self.validation_error_msg})
- except ValueError: # check that it is a date has been done hereunder
- pass
- return errors
-
- def validate_date(self, post, answer_tag):
- return self.date_validation('date', post, answer_tag, self.validation_min_date, self.validation_max_date)
-
- def validate_datetime(self, post, answer_tag):
- return self.date_validation('datetime', post, answer_tag, self.validation_min_datetime, self.validation_max_datetime)
-
- def validate_simple_choice(self, post, answer_tag):
- self.ensure_one()
- errors = {}
- if self.comments_allowed:
- comment_tag = "%s_%s" % (answer_tag, 'comment')
- # Empty answer to mandatory self
- if self.constr_mandatory and answer_tag not in post:
- errors.update({answer_tag: self.constr_error_msg})
- if self.constr_mandatory and answer_tag in post and not post[answer_tag].strip():
- errors.update({answer_tag: self.constr_error_msg})
- # Answer is a comment and is empty
- if self.constr_mandatory and answer_tag in post and post[answer_tag] == "-1" and self.comment_count_as_answer and comment_tag in post and not post[comment_tag].strip():
- errors.update({answer_tag: self.constr_error_msg})
- return errors
-
- def validate_multiple_choice(self, post, answer_tag):
- self.ensure_one()
- errors = {}
- if self.constr_mandatory:
- answer_candidates = dict_keys_startswith(post, answer_tag)
- comment_flag = answer_candidates.pop(("%s_%s" % (answer_tag, -1)), None)
- if self.comments_allowed:
- comment_answer = answer_candidates.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
- # Preventing answers with blank value
- if all(not answer.strip() for answer in answer_candidates.values()) and answer_candidates:
- errors.update({answer_tag: self.constr_error_msg})
- # There is no answer neither comments (if comments count as answer)
- if not answer_candidates and self.comment_count_as_answer and (not comment_flag or not comment_answer):
- errors.update({answer_tag: self.constr_error_msg})
- # There is no answer at all
- if not answer_candidates and not self.comment_count_as_answer:
- errors.update({answer_tag: self.constr_error_msg})
- return errors
-
- def validate_matrix(self, post, answer_tag):
- self.ensure_one()
- errors = {}
- if self.constr_mandatory:
- lines_number = len(self.labels_ids_2)
- answer_candidates = dict_keys_startswith(post, answer_tag)
- answer_candidates.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
- # Number of lines that have been answered
- if self.matrix_subtype == 'simple':
- answer_number = len(answer_candidates)
- elif self.matrix_subtype == 'multiple':
- answer_number = len({sk.rsplit('_', 1)[0] for sk in answer_candidates})
+ try:
+ dateanswer = fields.Datetime.from_string(answer) if isDatetime else fields.Date.from_string(answer)
+ except ValueError:
+ return {self.id: _('This is not a date')}
+ if self.validation_required:
+ # Check if answer is in the right range
+ if isDatetime:
+ min_date = fields.Datetime.from_string(self.validation_min_datetime)
+ max_date = fields.Datetime.from_string(self.validation_max_datetime)
+ dateanswer = fields.Datetime.from_string(answer)
else:
- raise RuntimeError("Invalid matrix subtype")
- # Validate that each line has been answered
- if answer_number != lines_number:
- errors.update({answer_tag: self.constr_error_msg})
- return errors
+ min_date = fields.Date.from_string(self.validation_min_date)
+ max_date = fields.Date.from_string(self.validation_max_date)
+ dateanswer = fields.Date.from_string(answer)
+
+ if (min_date and max_date and not (min_date <= dateanswer <= max_date))\
+ or (min_date and not min_date <= dateanswer)\
+ or (max_date and not dateanswer <= max_date):
+ return {self.id: self.validation_error_msg}
+ return {}
+
+ def _validate_choice(self, answer, comment):
+ # Empty comment
+ if self.constr_mandatory \
+ and not answer \
+ and not (self.comments_allowed and self.comment_count_as_answer and comment):
+ return {self.id: self.constr_error_msg}
+ return {}
+
+ def _validate_matrix(self, answers):
+ # Validate that each line has been answered
+ if self.constr_mandatory and len(self.labels_ids_2) != len(answers):
+ return {self.id: self.constr_error_msg}
+ return {}
@api.depends('survey_id.question_and_page_ids.is_page', 'survey_id.question_and_page_ids.sequence')
def _compute_question_ids(self):
diff --git a/addons/survey/models/survey_user.py b/addons/survey/models/survey_user.py
index 33fc0c8776a..37e822da1ba 100644
--- a/addons/survey/models/survey_user.py
+++ b/addons/survey/models/survey_user.py
@@ -267,240 +267,96 @@ class SurveyUserInputLine(models.Model):
vals.update({'answer_score': self.env['survey.label'].browse(int(value_suggested)).answer_score})
return super(SurveyUserInputLine, self).write(vals)
+ def _get_save_line_values(self, answer, answer_type):
+ if not answer or (isinstance(answer, str) and not answer.strip()):
+ return {'answer_type': None, 'skipped': True}
+ if answer_type == 'suggestion':
+ return {'answer_type': answer_type, 'value_suggested': answer}
+ value = float(answer) if answer_type == 'number' else answer
+ return {'answer_type': answer_type, 'value_' + answer_type: value}
+
@api.model
- def save_lines(self, user_input_id, question, post, answer_tag):
+ def save_lines(self, user_input_id, question, answer, comment=None):
""" Save answers to questions, depending on question type
If an answer already exists for question and user_input_id, it will be
- overwritten (in order to maintain data consistency).
+ overwritten (or deleted for 'choice' questions) (in order to maintain data consistency).
"""
- try:
- saver = getattr(self, 'save_line_' + question.question_type)
- except AttributeError:
- _logger.error(question.question_type + ": This type of question has no saving function")
- return False
- else:
- saver(user_input_id, question, post, answer_tag)
-
- @api.model
- def save_line_free_text(self, user_input_id, question, post, answer_tag):
vals = {
'user_input_id': user_input_id,
'question_id': question.id,
'survey_id': question.survey_id.id,
'skipped': False,
}
- if answer_tag in post and post[answer_tag].strip():
- vals.update({'answer_type': 'free_text', 'value_free_text': post[answer_tag]})
- else:
- vals.update({'answer_type': None, 'skipped': True})
- old_uil = self.search([
+ old_answers = self.search([
('user_input_id', '=', user_input_id),
('survey_id', '=', question.survey_id.id),
('question_id', '=', question.id)
])
- if old_uil:
- old_uil.write(vals)
+
+ if question.question_type in ['textbox', 'free_text', 'numerical_box', 'date', 'datetime']:
+ self._save_line_simple_answer(vals, old_answers, question, answer)
+ elif question.question_type in ['simple_choice', 'multiple_choice']:
+ self._save_line_choice(vals, old_answers, question, answer, comment)
+ elif question.question_type == 'matrix':
+ self._save_line_matrix(vals, old_answers, answer, comment)
else:
- old_uil.create(vals)
- return True
+ raise AttributeError(question.question_type + ": This type of question has no saving function")
@api.model
- def save_line_textbox(self, user_input_id, question, post, answer_tag):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False
- }
- if answer_tag in post and post[answer_tag].strip():
- vals.update({'answer_type': 'text', 'value_text': post[answer_tag]})
+ def _save_line_simple_answer(self, vals, old_answers, question, answer):
+ answer_type = question.question_type
+ if question.question_type == 'textbox':
+ answer_type = 'text'
+ elif question.question_type == 'numerical_box':
+ answer_type = 'number'
+
+ vals.update(self._get_save_line_values(answer, answer_type))
+ if old_answers:
+ old_answers.write(vals)
else:
- vals.update({'answer_type': None, 'skipped': True})
- old_uil = self.search([
- ('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)
- ])
- if old_uil:
- old_uil.write(vals)
- else:
- old_uil.create(vals)
- return True
-
- @api.model
- def save_line_numerical_box(self, user_input_id, question, post, answer_tag):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False
- }
- if answer_tag in post and post[answer_tag].strip():
- vals.update({'answer_type': 'number', 'value_number': float(post[answer_tag])})
- else:
- vals.update({'answer_type': None, 'skipped': True})
- old_uil = self.search([
- ('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)
- ])
- if old_uil:
- old_uil.write(vals)
- else:
- old_uil.create(vals)
- return True
-
- @api.model
- def save_line_date(self, user_input_id, question, post, answer_tag):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False
- }
- if answer_tag in post and post[answer_tag].strip():
- vals.update({'answer_type': 'date', 'value_date': post[answer_tag]})
- else:
- vals.update({'answer_type': None, 'skipped': True})
- old_uil = self.search([
- ('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)
- ])
- if old_uil:
- old_uil.write(vals)
- else:
- old_uil.create(vals)
- return True
-
- @api.model
- def save_line_datetime(self, user_input_id, question, post, answer_tag):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False
- }
- if answer_tag in post and post[answer_tag].strip():
- vals.update({'answer_type': 'datetime', 'value_datetime': post[answer_tag]})
- else:
- vals.update({'answer_type': None, 'skipped': True})
- old_uil = self.search([
- ('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)
- ])
- if old_uil:
- old_uil.write(vals)
- else:
- old_uil.create(vals)
- return True
-
- @api.model
- def save_line_simple_choice(self, user_input_id, question, post, answer_tag):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False
- }
- old_uil = self.search([
- ('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)
- ])
- old_uil.sudo().unlink()
-
- if answer_tag in post and post[answer_tag].strip():
- vals.update({'answer_type': 'suggestion', 'value_suggested': int(post[answer_tag])})
- else:
- vals.update({'answer_type': None, 'skipped': True})
-
- # '-1' indicates 'comment count as an answer' so do not need to record it
- if post.get(answer_tag) and post.get(answer_tag) != '-1':
- self.create(vals)
-
- comment_answer = post.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
- if comment_answer:
- vals.update({'answer_type': 'text', 'value_text': comment_answer, 'skipped': False, 'value_suggested': False})
- self.create(vals)
-
- return True
-
- @api.model
- def save_line_multiple_choice(self, user_input_id, question, post, answer_tag):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False
- }
- old_uil = self.search([
- ('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)
- ])
- old_uil.sudo().unlink()
-
- ca_dict = dict_keys_startswith(post, answer_tag + '_')
- comment_answer = ca_dict.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
- if len(ca_dict) > 0:
- for key in ca_dict:
- # '-1' indicates 'comment count as an answer' so do not need to record it
- if key != ('%s_%s' % (answer_tag, '-1')):
- val = ca_dict[key]
- vals.update({'answer_type': 'suggestion', 'value_suggested': bool(val) and int(val)})
- self.create(vals)
- if comment_answer:
- vals.update({'answer_type': 'text', 'value_text': comment_answer, 'value_suggested': False})
- self.create(vals)
- if not ca_dict and not comment_answer:
- vals.update({'answer_type': None, 'skipped': True})
self.create(vals)
return True
@api.model
- def save_line_matrix(self, user_input_id, question, post, answer_tag):
- vals = {
- 'user_input_id': user_input_id,
- 'question_id': question.id,
- 'survey_id': question.survey_id.id,
- 'skipped': False
- }
- old_uil = self.search([
- ('user_input_id', '=', user_input_id),
- ('survey_id', '=', question.survey_id.id),
- ('question_id', '=', question.id)
- ])
- old_uil.sudo().unlink()
+ def _save_line_choice(self, vals, old_answers, question, answers, comment):
+ if not (isinstance(answers, list)):
+ answers = [answers]
- no_answers = True
- ca_dict = dict_keys_startswith(post, answer_tag + '_')
+ vals_list = []
+ if question.question_type == 'simple_choice':
+ if not (question.comment_count_as_answer and question.comments_allowed and comment):
+ for answer in answers:
+ vals.update(self._get_save_line_values(answer, 'suggestion'))
+ vals_list.append(vals.copy())
+ elif question.question_type == 'multiple_choice':
+ for answer in answers:
+ vals.update(self._get_save_line_values(answer, 'suggestion'))
+ vals_list.append(vals.copy())
- comment_answer = ca_dict.pop(("%s_%s" % (answer_tag, 'comment')), '').strip()
- if comment_answer:
- vals.update({'answer_type': 'text', 'value_text': comment_answer})
- self.create(vals)
- no_answers = False
+ if comment:
+ vals.update({'answer_type': 'text', 'value_text': comment, 'skipped': False, 'value_suggested': False})
+ vals_list.append(vals.copy())
- if question.matrix_subtype == 'simple':
- for row in question.labels_ids_2:
- a_tag = "%s_%s" % (answer_tag, row.id)
- if a_tag in ca_dict:
- no_answers = False
- vals.update({'answer_type': 'suggestion', 'value_suggested': ca_dict[a_tag], 'value_suggested_row': row.id})
- self.create(vals)
+ old_answers.sudo().unlink()
+ self.create(vals_list)
+
+ return True
+
+ @api.model
+ def _save_line_matrix(self, vals, old_answers, answers, comment):
+ vals_list = []
+
+ for row_key, row_answer in answers.items():
+ for answer in row_answer:
+ vals.update({'answer_type': 'suggestion', 'value_suggested': answer, 'value_suggested_row': row_key})
+ vals_list.append(vals.copy())
+
+ if comment:
+ vals.update({'answer_type': 'text', 'value_text': comment, 'skipped': False, 'value_suggested': False})
+ vals_list.append(vals.copy())
+
+ old_answers.sudo().unlink()
+ self.create(vals_list)
- elif question.matrix_subtype == 'multiple':
- for col in question.labels_ids:
- for row in question.labels_ids_2:
- a_tag = "%s_%s_%s" % (answer_tag, row.id, col.id)
- if a_tag in ca_dict:
- no_answers = False
- vals.update({'answer_type': 'suggestion', 'value_suggested': col.id, 'value_suggested_row': row.id})
- self.create(vals)
- if no_answers:
- vals.update({'answer_type': None, 'skipped': True})
- self.create(vals)
return True
diff --git a/addons/survey/static/src/js/survey_form.js b/addons/survey/static/src/js/survey_form.js
index 23ee96d71ea..a8ccd8324b0 100644
--- a/addons/survey/static/src/js/survey_form.js
+++ b/addons/survey/static/src/js/survey_form.js
@@ -10,7 +10,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
selector: '.o_survey_form',
events: {
'change .o_survey_form_choice_item': '_onChangeChoiceItem',
- 'click button[name="button_submit"]': '_onSubmit',
+ 'click button[type="submit"]': '_onSubmit',
'click .o_survey_header .breadcrumb-item a': '_onBreadcrumbClick',
},
@@ -30,7 +30,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
this.surveyTimerWidget = new publicWidget.registry.SurveyTimerWidget(this);
this.surveyTimerWidget.attachTo($timer);
this.surveyTimerWidget.on('time_up', this, function (ev) {
- self.$el.find('button[name="button_submit"]').click();
+ self.$el.find('button[type="submit"]').click();
});
}
@@ -101,9 +101,11 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
_onSubmit: function (event) {
event.preventDefault();
- var $target = $(event.currentTarget);
- var submitValue = $target.val();
- this._submitForm({'button_submit': submitValue})
+ if ($(event.currentTarget).val() === 'previous') {
+ this._submitForm({'previous_id': $(event.currentTarget).data('previousId')});
+ } else {
+ this._submitForm({});
+ }
},
_submitForm: function (params) {
@@ -111,9 +113,6 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
var $form = this.$('form');
var formData = new FormData($form[0]);
- this.$('div.o_survey_form_date').each(function () {
- self._updateDateForSubmit($(this), formData);
- });
this._prepareSubmitValues(formData, params);
this._resetErrors();
@@ -149,64 +148,113 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
}
},
- // INIT FIELDS
+ // PREPARE SUBMIT TOOLS
// -------------------------------------------------------------------------
/*
- * Convert the client side date format into server side date format before submit
+ * For each type of question, extract the answer from inputs or textarea (comment or answer)
+ *
+ *
+ * @private
+ * @param {Event} event
*/
- _updateDateForSubmit: function (dateGroup, formData) {
- var input = $(dateGroup).find('input');
- var dateValue = input.val();
- var questionType = $(input).closest('.o_survey_form_date').data('questiontype');
- if (dateValue) {
- var momentDate = questionType === 'datetime' ? field_utils.parse.datetime(dateValue, null, {timezone: true}) : field_utils.parse.date(dateValue);
- var newDate = momentDate ? momentDate.toJSON() : '';
- formData.set(input.attr('name'), newDate);
- input.val(newDate);
- }
- },
-
- // TOOLS
- // -------------------------------------------------------------------------
_prepareSubmitValues: function (formData, params) {
var self = this;
+ // Get all context params -- TODO : Use formData instead (test if input with no name are in formData)
formData.forEach(function(value, key){
- if (value !== -1) {
- // Handles Comment
- if (key.indexOf('_comment') !== -1){
- key = key.split('_comment')[0];
- value = {'comment': value};
- }
- // Handles Matrix - Matrix answer_tag are composed like : 'questionId_rowId_colId'
- // and are the only ones with this structure.
- var splitKey = key.split('_');
- if (splitKey.length === 3 && splitKey[2] === value) {
- params = self._prepareSubmitMatrix(params, splitKey, value);
- }
- // Handles the rest
- else {
- params = self._prepareSubmitOther(params, key, value);
- }
+ switch (key) {
+ case 'csrf_token':
+ case 'token':
+ case 'page_id':
+ case 'question_id':
+ params[key] = value;
+ break;
+ }
+ });
+
+ // Get all question answers by question type
+ this.$('[data-question-type]').each(function () {
+ switch ($(this).data('questionType')) {
+ case 'free_text':
+ case 'textbox':
+ case 'numerical_box':
+ params[this.name] = this.value;
+ break;
+ case 'date':
+ params = self._prepareSubmitDates(params, this.name, this.value, false);
+ break;
+ case 'datetime':
+ params = self._prepareSubmitDates(params, this.name, this.value, true);
+ break;
+ case 'simple_choice_dropdown':
+ params = self._prepareSubmitChoices(params, $(this), $(this).data('name'), 'option:selected');
+ break;
+ case 'simple_choice_radio':
+ case 'multiple_choice':
+ params = self._prepareSubmitChoices(params, $(this), $(this).data('name'), 'input:checked');
+ break;
+ case 'matrix':
+ params = self._prepareSubmitAnswersMatrix(params, $(this));
+ break;
}
});
},
+ /**
+ * Prepare date answer before submitting form.
+ * Convert date value from client current timezone to UTC Date to correspond to the server format.
+ * return params = { 'dateQuestionId' : '2019-05-23', 'datetimeQuestionId' : '2019-05-23 14:05:12' }
+ */
+ _prepareSubmitDates: function(params, questionId, value, isDateTime) {
+ var momentDate = isDateTime ? field_utils.parse.datetime(value, null, {timezone: true}) : field_utils.parse.date(value);
+ var formattedDate = momentDate ? momentDate.toJSON() : '';
+ params[questionId] = formattedDate;
+ return params;
+ },
+
+ /**
+ * Prepare choice answer before submitting form.
+ * If the answer is not the 'comment selection' (=Other), calls the _prepareSubmitAnswer method to add the answer to the params
+ * If there is a comment linked to that question, calls the _prepareSubmitComment method to add the comment to the params
+ */
+ _prepareSubmitChoices: function(params, $parent, questionId, selector) {
+ var self = this;
+ $parent.find(selector).each(function () {
+ if (this.value != '-1') {
+ params = self._prepareSubmitAnswer(params, questionId, this.value);
+ }
+ });
+ params = self._prepareSubmitComment(params, $parent, questionId, false);
+ return params;
+ },
+
+
+ /**
+ * Prepare matrix answers before submitting form.
+ * This method adds matrix answers one by one and add comment if any to a params key,value like :
+ * params = { 'matrixQuestionId' : {'rowId1': [colId1, colId2,...], 'rowId2': [colId1, colId3, ...], 'comment': comment }}
+ */
+ _prepareSubmitAnswersMatrix: function(params, $matrixDiv) {
+ var self = this;
+ $matrixDiv.find('input:checked').each(function () {
+ params = self._prepareSubmitAnswerMatrix(params, $matrixDiv.data('name'), $(this).data('rowId'), this.value);
+ });
+ params = self._prepareSubmitComment(params, $matrixDiv, $matrixDiv.data('name'), true);
+ return params;
+ },
+
/**
* Prepare answer before submitting form if question type is matrix.
- * This method regroups answers by question and by row to make a object like :
+ * This method regroups answers by question and by row to make an object like :
* params = { 'matrixQuestionId' : { 'rowId1' : [colId1, colId2,...], 'rowId2' : [colId1, colId3, ...] } }
*/
- _prepareSubmitMatrix: function(params, splitKey, value) {
- var key = splitKey[0];
- var rowId = splitKey[1];
- var colId = splitKey[2];
- value = key in params ? params[key] : {};
+ _prepareSubmitAnswerMatrix: function(params, questionId, rowId, colId) {
+ var value = questionId in params ? params[questionId] : {};
if (rowId in value) {
value[rowId].push(colId);
} else {
value[rowId] = [colId];
}
- params[key] = value;
+ params[questionId] = value;
return params;
},
@@ -216,37 +264,43 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
* Lonely answer are directly assigned to questionId. Multiple answers are regrouped in an array:
* params = { 'questionId1' : lonelyAnswer, 'questionId2' : [multipleAnswer1, multipleAnswer2, ...] }
*/
- _prepareSubmitOther: function(params, key, value) {
- if (key in params) {
- if (params[key].constructor === Array) {
- params[key].push(value);
+ _prepareSubmitAnswer: function(params, questionId, value) {
+ if (questionId in params) {
+ if (params[questionId].constructor === Array) {
+ params[questionId].push(value);
} else {
- params[key] = [params[key], value];
+ params[questionId] = [params[questionId], value];
}
} else {
- params[key] = value;
+ params[questionId] = value;
}
return params;
},
/**
- * Convert date value in client current timezone (if already answered)
+ * Prepare comment before submitting form.
+ * This method extract the comment, encapsulate it in a dict and calls the _prepareSubmitAnswer methods
+ * with the new value. At the end, the result looks like :
+ * params = { 'questionId1' : {'comment': commentValue}, 'questionId2' : [multipleAnswer1, {'comment': commentValue}, ...] }
*/
- _formatDateValue: function ($dateGroup) {
- var input = $dateGroup.find('input');
- var dateValue = input.val();
- if (dateValue !== '') {
- var momentDate = field_utils.parse.date(dateValue);
- if ($dateGroup.data('questiontype') === 'datetime') {
- dateValue = field_utils.format.datetime(momentDate, null, {timezone: true});
- } else {
- dateValue = field_utils.format.date(momentDate, null, {timezone: true});
+ _prepareSubmitComment: function(params, $parent, questionId, isMatrix) {
+ var self = this;
+ $parent.find('textarea').each(function () {
+ if (this.value) {
+ var value = {'comment': this.value};
+ if (isMatrix) {
+ params = self._prepareSubmitAnswerMatrix(params, questionId, this.name, value);
+ } else {
+ params = self._prepareSubmitAnswer(params, questionId, value);
+ }
}
- }
- input.val(dateValue);
- return dateValue
+ });
+ return params
},
+ // INIT FIELDS TOOLS
+ // -------------------------------------------------------------------------
+
/**
* Initialize datetimepicker in correct format and with constraints
*/
@@ -259,7 +313,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
var maxDate = maxDateData ? this._formatDateTime(maxDateData) : moment().add(200, "y");
var datetimepickerFormat = time.getLangDateFormat()
- if ($dateGroup.data('questiontype') === 'datetime') {
+ if ($dateGroup.find('input').data('questionType') === 'datetime') {
datetimepickerFormat = time.getLangDatetimeFormat()
} else {
// Include min and max date in selectable values
@@ -296,6 +350,9 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend({
return field_utils.format.datetime(moment(datetimeValue), null, {timezone: true});
},
+ // ERRORS TOOLS
+ // -------------------------------------------------------------------------
+
_scrollToError: function ($target) {
var scrollLocation = $target.offset().top;
var navbarHeight = $('.o_main_navbar').height();
diff --git a/addons/survey/static/tests/tours/certification_failure.js b/addons/survey/static/tests/tours/certification_failure.js
index e26ac61b17c..5b5fb1c9ea0 100644
--- a/addons/survey/static/tests/tours/certification_failure.js
+++ b/addons/survey/static/tests/tours/certification_failure.js
@@ -62,7 +62,7 @@ var failSteps = [{ // Page-1
}
}, {
content: "Finish Survey",
- trigger: 'button[value="finish"]',
+ trigger: 'button[type="submit"]',
}];
var retrySteps = [{
diff --git a/addons/survey/static/tests/tours/certification_success.js b/addons/survey/static/tests/tours/certification_success.js
index afefe3648eb..03d3a22a9d7 100644
--- a/addons/survey/static/tests/tours/certification_success.js
+++ b/addons/survey/static/tests/tours/certification_success.js
@@ -62,7 +62,7 @@ tour.register('test_certification_success', {
}
}, {
content: "Finish Survey",
- trigger: 'button[value="finish"]',
+ trigger: 'button[type="submit"]',
}, {
content: "Thank you",
trigger: 'h1:contains("Thank you!")',
diff --git a/addons/survey/static/tests/tours/survey.js b/addons/survey/static/tests/tours/survey.js
index 422ee75a68e..e4540fba1ab 100644
--- a/addons/survey/static/tests/tours/survey.js
+++ b/addons/survey/static/tests/tours/survey.js
@@ -31,7 +31,7 @@ tour.register('test_survey', {
trigger: 'div.js_question-wrapper:contains("How many times did you order products on our website") input',
run: 'text 12',
}, {
- content: 'Click on Next Page',
+ content: 'Submit and go to Next Page',
trigger: 'button[value="next"]',
},
// Page-2
@@ -61,7 +61,7 @@ tour.register('test_survey', {
trigger: 'div.js_question-wrapper:contains("Do you have any other comments, questions, or concerns") textarea',
run: 'text This is great. Really.',
}, {
- content: 'Click Submit',
+ content: 'Click Submit and finish the survey',
trigger: 'button[value="finish"]',
},
// Final page
diff --git a/addons/survey/static/tests/tours/survey_prefill.js b/addons/survey/static/tests/tours/survey_prefill.js
index 845a3bdf53a..d93f3d96210 100644
--- a/addons/survey/static/tests/tours/survey_prefill.js
+++ b/addons/survey/static/tests/tours/survey_prefill.js
@@ -55,23 +55,16 @@ tour.register('test_survey_prefill', {
trigger: 'div.js_question-wrapper:contains("Do you have any other comments, questions, or concerns") textarea',
run: 'text Is the prefill working?',
}, {
- content: 'Click on Previous Page',
- trigger: 'button[value="previous"]',
+ // Go back to previous page
+ content: 'Click on the previous page name in the breadcrumb',
+ trigger: 'ol.breadcrumb a:first',
}, {
trigger: 'div.js_question-wrapper:contains("How many times did you order products on our website ?") input',
run: function () {
- // wait for prefill
- // booo ugly (but will be removed when ajax prefill is removed)
- var maxAttempts = 0;
- var checkPrefillLoaded = setInterval(function () {
- if ($('div.js_question-wrapper:contains("How many times did you order products on our website ?") input').val() === '42.0') {
- $('.o_survey_title').addClass('prefilled');
- clearInterval(checkPrefillLoaded);
- } else if (maxAttempts >= 50) {
- clearInterval(checkPrefillLoaded);
- }
- maxAttempts++;
- }, 100); // check every 100ms
+ var $inputQ3 = $('div.js_question-wrapper:contains("How many times did you order products on our website ?") input');
+ if ($inputQ3.val() === '42.0') {
+ $('.o_survey_title').addClass('prefilled');
+ }
}
}, {
trigger: '.o_survey_title.prefilled',
@@ -107,18 +100,10 @@ tour.register('test_survey_prefill', {
}, {
trigger: 'div.js_question-wrapper:contains("Do you have any other comments, questions, or concerns") textarea',
run: function () {
- // wait for prefill
- // booo ugly (but will be removed when ajax prefill is removed)
- var maxAttempts = 0;
- var checkPrefillLoaded = setInterval(function () {
- if ($('div.js_question-wrapper:contains("Do you have any other comments, questions, or concerns") textarea').val() === "Is the prefill working?") {
- $('.o_survey_title').addClass('prefilled2');
- clearInterval(checkPrefillLoaded);
- } else if (maxAttempts >= 50) {
- clearInterval(checkPrefillLoaded);
- }
- maxAttempts++;
- }, 100); // check every 100ms
+ var $inputQ3 = $('div.js_question-wrapper:contains("Do you have any other comments, questions, or concerns") textarea');
+ if ($inputQ3.val() === "Is the prefill working?") {
+ $('.o_survey_title').addClass('prefilled2');
+ }
}
}, {
trigger: '.o_survey_title.prefilled2',
diff --git a/addons/survey/tests/common.py b/addons/survey/tests/common.py
index 25f600ef5a1..f279ef9e445 100644
--- a/addons/survey/tests/common.py
+++ b/addons/survey/tests/common.py
@@ -137,7 +137,10 @@ class TestSurveyCommon(common.SavepointCase):
else:
[value] = user_input['value']
answer_fname = self._type_match[question.question_type][1]
- self.assertEqual(getattr(answer_lines, answer_fname), value)
+ if question.question_type == 'numerical_box':
+ self.assertEqual(getattr(answer_lines, answer_fname), float(value))
+ else:
+ self.assertEqual(getattr(answer_lines, answer_fname), value)
def assertResponse(self, response, status_code, text_bits=None):
self.assertEqual(response.status_code, status_code)
@@ -222,3 +225,20 @@ class TestSurveyCommon(common.SavepointCase):
def _find_csrf_token(self, text):
csrf_token_re = re.compile("(input.+csrf_token.+value=\")([_a-zA-Z0-9]{51})", re.MULTILINE)
return csrf_token_re.search(text).groups()[1]
+
+ def _prepare_post_data(self, question, answers, post_data):
+ values = answers if isinstance(answers, list) else [answers]
+ if question.question_type == 'multiple_choice':
+ for value in values:
+ value = str(value)
+ if question.id in post_data:
+ if isinstance(post_data[question.id], list):
+ post_data[question.id].append(value)
+ else:
+ post_data[question.id] = [post_data[question.id], value]
+ else:
+ post_data[question.id] = value
+ else:
+ [values] = values
+ post_data[question.id] = str(values)
+ return post_data
diff --git a/addons/survey/tests/test_certification_flow.py b/addons/survey/tests/test_certification_flow.py
index d1c6f39405f..f9a9010b68e 100644
--- a/addons/survey/tests/test_certification_flow.py
+++ b/addons/survey/tests/test_certification_flow.py
@@ -24,14 +24,7 @@ class TestCertificationFlow(common.TestSurveyCommon, HttpCase):
def _format_submission_data(self, question, answer, additional_post_data):
post_data = {}
post_data['question_id'] = question.id
- if question.question_type == 'multiple_choice':
- values = answer
- for value in values:
- key = "%s_%s_%s" % (question.survey_id.id, question.id, value)
- post_data[key] = value
- else:
- key = "%s_%s" % (question.survey_id.id, question.id)
- post_data[key] = answer
+ post_data.update(self._prepare_post_data(question, answer, post_data))
post_data.update(**additional_post_data)
return post_data
diff --git a/addons/survey/tests/test_survey.py b/addons/survey/tests/test_survey.py
index 67e44eb33de..6c64d47f763 100644
--- a/addons/survey/tests/test_survey.py
+++ b/addons/survey/tests/test_survey.py
@@ -29,10 +29,9 @@ class TestSurveyInternals(common.TestSurveyCommon):
kwargs['labels_2'] = [{'value': 'Row0'}, {'value': 'Row1'}]
question = self._add_question(self.page_0, 'Q0', question_type, **kwargs)
- answer_tag = '%s_%s' % (self.survey.id, question.id)
self.assertDictEqual(
- question.validate_question({answer_tag: ''}, answer_tag),
- {answer_tag: 'TestError'}
+ question.validate_question(''),
+ {question.id: 'TestError'}
)
@users('survey_manager')
@@ -40,25 +39,24 @@ class TestSurveyInternals(common.TestSurveyCommon):
question = self._add_question(
self.page_0, 'Q0', 'date', validation_required=True,
validation_min_date='2015-03-20', validation_max_date='2015-03-25', validation_error_msg='ValidationError')
- answer_tag = '%s_%s' % (self.survey.id, question.id)
self.assertEqual(
- question.validate_question({answer_tag: 'Is Alfred an answer ?'}, answer_tag),
- {answer_tag: _('This is not a date')}
+ question.validate_question('Is Alfred an answer ?'),
+ {question.id: _('This is not a date')}
)
self.assertEqual(
- question.validate_question({answer_tag: '2015-03-19'}, answer_tag),
- {answer_tag: 'ValidationError'}
+ question.validate_question('2015-03-19'),
+ {question.id: 'ValidationError'}
)
self.assertEqual(
- question.validate_question({answer_tag: '2015-03-26'}, answer_tag),
- {answer_tag: 'ValidationError'}
+ question.validate_question('2015-03-26'),
+ {question.id: 'ValidationError'}
)
self.assertEqual(
- question.validate_question({answer_tag: '2015-03-25'}, answer_tag),
+ question.validate_question('2015-03-25'),
{}
)
@@ -67,40 +65,38 @@ class TestSurveyInternals(common.TestSurveyCommon):
question = self._add_question(
self.page_0, 'Q0', 'numerical_box', validation_required=True,
validation_min_float_value=2.2, validation_max_float_value=3.3, validation_error_msg='ValidationError')
- answer_tag = '%s_%s' % (self.survey.id, question.id)
self.assertEqual(
- question.validate_question({answer_tag: 'Is Alfred an answer ?'}, answer_tag),
- {answer_tag: _('This is not a number')}
+ question.validate_question('Is Alfred an answer ?'),
+ {question.id: _('This is not a number')}
)
self.assertEqual(
- question.validate_question({answer_tag: '2.0'}, answer_tag),
- {answer_tag: 'ValidationError'}
+ question.validate_question('2.0'),
+ {question.id: 'ValidationError'}
)
self.assertEqual(
- question.validate_question({answer_tag: '4.0'}, answer_tag),
- {answer_tag: 'ValidationError'}
+ question.validate_question('4.0'),
+ {question.id: 'ValidationError'}
)
self.assertEqual(
- question.validate_question({answer_tag: '2.9'}, answer_tag),
+ question.validate_question('2.9'),
{}
)
@users('survey_manager')
def test_answer_validation_textbox_email(self):
question = self._add_question(self.page_0, 'Q0', 'textbox', validation_email=True)
- answer_tag = '%s_%s' % (self.survey.id, question.id)
self.assertEqual(
- question.validate_question({answer_tag: 'not an email'}, answer_tag),
- {answer_tag: _('This answer must be an email address')}
+ question.validate_question('not an email'),
+ {question.id: _('This answer must be an email address')}
)
self.assertEqual(
- question.validate_question({answer_tag: 'email@example.com'}, answer_tag),
+ question.validate_question('email@example.com'),
{}
)
@@ -109,20 +105,19 @@ class TestSurveyInternals(common.TestSurveyCommon):
question = self._add_question(
self.page_0, 'Q0', 'textbox', validation_required=True,
validation_length_min=2, validation_length_max=8, validation_error_msg='ValidationError')
- answer_tag = '%s_%s' % (self.survey.id, question.id)
self.assertEqual(
- question.validate_question({answer_tag: 'l'}, answer_tag),
- {answer_tag: 'ValidationError'}
+ question.validate_question('l'),
+ {question.id: 'ValidationError'}
)
self.assertEqual(
- question.validate_question({answer_tag: 'waytoomuchlonganswer'}, answer_tag),
- {answer_tag: 'ValidationError'}
+ question.validate_question('waytoomuchlonganswer'),
+ {question.id: 'ValidationError'}
)
self.assertEqual(
- question.validate_question({answer_tag: 'valid'}, answer_tag),
+ question.validate_question('valid'),
{}
)
diff --git a/addons/survey/tests/test_survey_flow.py b/addons/survey/tests/test_survey_flow.py
index 5712682df3f..472eaff55fd 100644
--- a/addons/survey/tests/test_survey_flow.py
+++ b/addons/survey/tests/test_survey_flow.py
@@ -13,15 +13,7 @@ class TestSurveyFlow(common.TestSurveyCommon, HttpCase):
post_data['page_id'] = page.id
for question_id, answer_vals in answer_data.items():
question = page.question_ids.filtered(lambda q: q.id == question_id)
- if question.question_type == 'multiple_choice':
- values = answer_vals['value']
- for value in values:
- key = "%s_%s_%s" % (page.survey_id.id, question.id, value)
- post_data[key] = value
- else:
- [value] = answer_vals['value']
- key = "%s_%s" % (page.survey_id.id, question.id)
- post_data[key] = value
+ post_data.update(self._prepare_post_data(question, answer_vals['value'], post_data))
post_data.update(**additional_post_data)
return post_data
@@ -96,7 +88,7 @@ class TestSurveyFlow(common.TestSurveyCommon, HttpCase):
# Customer submit first page answers
answer_data = {
page0_q0.id: {'value': ['Alfred Poilvache']},
- page0_q1.id: {'value': [44.0]},
+ page0_q1.id: {'value': ['44.0']},
}
post_data = self._format_submission_data(page_0, answer_data, {'csrf_token': csrf_token, 'token': answer_token, 'button_submit': 'next'})
r = self._access_submit(survey, answer_token, post_data)
diff --git a/addons/survey/views/survey_templates.xml b/addons/survey/views/survey_templates.xml
index 4604b3ddd12..494c667d23c 100644
--- a/addons/survey/views/survey_templates.xml
+++ b/addons/survey/views/survey_templates.xml
@@ -269,7 +269,7 @@
-
+
@@ -285,8 +285,9 @@
-
-
+
@@ -297,8 +298,9 @@
-
-
+
@@ -319,9 +321,8 @@
-
-