diff --git a/addons/survey/__openerp__.py b/addons/survey/__openerp__.py index 3bf3183e21b..603f2ed142b 100644 --- a/addons/survey/__openerp__.py +++ b/addons/survey/__openerp__.py @@ -38,8 +38,8 @@ sent mails with user name and password for the invitation of the survey. 'website': 'https://www.openerp.com/', 'depends': ['email_template', 'mail', 'website'], 'data': [ - #'security/survey_security.xml', - #'security/ir.model.access.csv', + 'security/survey_security.xml', + 'security/ir.model.access.csv', 'survey_view.xml', 'views/survey_templates.xml', #'wizard/survey_answer.xml', diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index 8d691661974..db74c0ff6c0 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -24,7 +24,9 @@ from openerp.addons.web.http import request from openerp.addons.website.models import website import werkzeug +import json import logging +import re _logger = logging.getLogger(__name__) @@ -57,7 +59,7 @@ class WebsiteSurvey(http.Controller): if survey_obj.exists(cr, uid, survey.id, context=context) == []: return werkzeug.utils.redirect("/survey/") - _logger.debug('Post request data: %s', post) + _logger.debug('Post data: %s', post) # Store answer data # (/!\ assumes JavaScript validation of answers has succeeded!) @@ -96,14 +98,25 @@ class WebsiteSurvey(http.Controller): # return {'valid': True, 'errors': None} @website.route(['/survey/submit/'], - type='json', auth='public', multilang=True) - def submit(self, survey=None, **post): - _logger.debug("Incoming json data: " + post.__str__()) + type='http', auth='public', multilang=True) + def submit(self, survey, **post): + _logger.debug("Incoming data: " + post.__str__()) + page_nr = int(post['current']) + questions = survey.page_ids[page_nr].question_ids - # transform post data into dictionary - # for each {K:V} in post, check + errors = {} + for question in questions: + answer_tag = "%s_%s_%s" % (survey.id, page_nr, question.id) + errors.update(self.validate_question(survey.id, page_nr, question, post, answer_tag)) - return {'redirect': '/survey/'} + ret = {} + if (len(errors) != 0): + ret['errors'] = errors + else: + cr, uid, context = request.cr, request.uid, request.context + # Store here data if allowed + ret['redirect'] = "nexturl" + return json.dumps(ret) # Printing routes @website.route(['/survey/print//'], @@ -114,3 +127,130 @@ class WebsiteSurvey(http.Controller): return request.website.render('survey.survey_print', {'survey': survey, 'pagination': pagination}) + + # Validation methods + + def validate_question(self, survey_id, page_nr, question, post, answer_tag): + ''' Routing to the right question valider, depending on question type ''' + try: + checker = getattr(self, 'validate_' + question.type) + except AttributeError: + _logger.warning(question.type + ": This type of question has no validation method") + return {} + else: + return checker(survey_id, page_nr, question, post, answer_tag) + + def validate_free_text(self, survey_id, page_nr, question, post, answer_tag): + errors = {} + answer = post[answer_tag].strip() + # Empty answer to mandatory question + if question.constr_mandatory and not answer: + errors.update({answer_tag: question.constr_error_msg}) + return errors + + def validate_textbox(self, survey_id, page_nr, question, post, answer_tag): + errors = {} + answer = post[answer_tag].strip() + # Empty answer to mandatory question + if question.constr_mandatory and not answer: + errors.update({answer_tag: question.constr_error_msg}) + # Answer validation (if properly defined) + if answer and question.validation_required and question.validation_type: + # Length of the answer must be in a range + if question.validation_type == "has_length": + if not (question.validation_length_min <= len(answer) <= question.validation_length_max): + errors.update({answer_tag: question.validation_error_msg}) + + # Answer must be an integer in a particular range + elif question.validation_type == "is_integer": + try: + intanswer = int(answer) + # Answer is not an integer + except ValueError: + errors.update({answer_tag: question.validation_error_msg}) + else: + # Answer is not in the right range + if not (question.validation_min_int_value <= intanswer <= question.validation_max_int_value): + errors.update({answer_tag: question.validation_error_msg}) + # Answer must be a float in a particular range + elif question.validation_type == "is_decimal": + try: + floatanswer = float(answer) + # Answer is not an integer + except ValueError: + errors.update({answer_tag: question.validation_error_msg}) + else: + # Answer is not in the right range + if not (question.validation_min_float_value <= floatanswer <= question.validation_max_float_value): + errors.update({answer_tag: question.validation_error_msg}) + + # Answer must be a date in a particular range + elif question.validation_type == "is_date": + raise Exception("Not implemented") + # Answer must be an email address + # Note: this validation is very basic: + # all the strings of the form + # @. + # will be accepted + elif question.validation_type == "is_email": + if not re.match(r"[^@]+@[^@]+\.[^@]+", answer): + errors.update({answer_tag: question.validation_error_msg}) + else: + pass + return errors + + def validate_numerical_box(self, survey_id, page_nr, question, post, answer_tag): + errors = {} + answer = post[answer_tag].strip() + # Empty answer to mandatory question + if question.constr_mandatory and not answer: + errors.update({answer_tag: question.constr_error_msg}) + # Checks if user input is a number + if answer: + try: + float(answer) + except ValueError: + errors.update({answer_tag: question.constr_error_msg}) + return errors + + def validate_datetime(self, survey_id, page_nr, question, post, answer_tag): + errors = {} + answer = post[answer_tag].strip() + # Empty answer to mandatory question + if question.constr_mandatory and not answer: + errors.update({answer_tag: question.constr_error_msg}) + # Checks if user input is a datetime + # TODO when datepicker will be available + return errors + + # def validate_simple_choice_scale(self, survey_id, page_nr, question, post, answer_tag): + # answer_tag = survey_id.__str__() + '*' + page_nr + '*' + question.id.__str__() + # problems = [] + # if question.constr_mandatory: + # problems = problems + self.__has_empty_input(question, post, answer_tag) + # return problems + + # def validate_simple_choice_dropdown(self, survey_id, page_nr, question, post, answer_tag): + # answer_tag = survey_id.__str__() + '*' + page_nr + '*' + question.id.__str__() + # problems = [] + # if question.constr_mandatory: + # problems = problems + self.__has_empty_input(question, post, answer_tag) + # return problems + + # def validate_multiple_choice(self, survey_id, page_nr, question, post, answer_tag): + # answer_tag = survey_id.__str__() + '*' + page_nr + '*' + question.id.__str__() + # problems = [] + # return problems + + # def validate_matrix(self, survey_id, page_nr, question, post, answer_tag): + # answer_tag = survey_id.__str__() + '*' + page_nr + '*' + question.id.__str__() + # problems = [] + # return problems + +def dict_keys_startswith(self, dictionary, string): + '''Returns a dictionary containing the elements of whose keys start + with . + + .. note:: + This function uses dictionary comprehensions (Python >= 2.7)''' + return {k: dictionary[k] for k in filter(lambda key: key.startswith(string), dictionary.keys())} diff --git a/addons/survey/security/ir.model.access.csv b/addons/survey/security/ir.model.access.csv index 93c80c8f7f8..6b4e4f6a95e 100644 --- a/addons/survey/security/ir.model.access.csv +++ b/addons/survey/security/ir.model.access.csv @@ -3,6 +3,6 @@ access_survey_user,survey.user,model_survey_survey,base.group_survey_user,1,0,0, access_survey_manager,survey.manager,model_survey_survey,base.group_survey_manager,1,1,1,1 access_survey_page_manager,survey.page manager,model_survey_page,base.group_survey_manager,1,1,1,1 access_survey_question_manager,survey.question manager,model_survey_question,base.group_survey_manager,1,1,1,1 -access_survey_suggestion_manager,survey.suggestion manager,model_survey_suggestion,base.group_survey_manager,1,1,1,1 +access_survey_label_manager,survey.label manager,model_survey_label,base.group_survey_manager,1,1,1,1 access_survey_user_input_manager,survey.user_input manager,model_survey_user_input,base.group_survey_manager,1,1,1,1 access_survey_user_input_line_manager,survey.user_input_line manager,model_survey_user_input_line,base.group_survey_manager,1,1,1,1 diff --git a/addons/survey/static/src/js/survey.js b/addons/survey/static/src/js/survey.js index 9b072e51b35..72ee3c7945c 100644 --- a/addons/survey/static/src/js/survey.js +++ b/addons/survey/static/src/js/survey.js @@ -1,15 +1,12 @@ $(document).ready(function () { + console.debug("[survey] Custom JS for survey is loading..."); + var the_form = $('.js_surveyform'); var prefill_controller = the_form.attr("data-prefill"); var validate_controller = the_form.attr("data-validate"); var submit_controller = the_form.attr("data-submit"); - console.log(prefill_controller); - console.log(validate_controller); - console.log(submit_controller); - - // function prefill(form){ // return false; // } @@ -22,29 +19,30 @@ $(document).ready(function () { // return false; // } - - - // startsWith compatibility patch with old browsers - // if (typeof String.prototype.startsWith != 'function') { - // String.prototype.startsWith = function (str){ - // return this.slice(0, str.length) == str; - // }; - // } - - // function that display error messages - var display_form_error = function (question_id, err_msg){ - $('#survey_' + question_id + '>.js_errzone').append('

' + err_msg + '

').show(); - }; - - //$('.js_errzone').hide(); - - console.debug("[survey] Custom JS for survey loaded"); - - // for each question - // check if answer exists and is not empty and is mandatory - // check if answer type is ok - // check if number of answers is ok - // check if answer has good qualities + // Parameters for form submission + $('.js_surveyform').ajaxForm({ + url: submit_controller, + type: 'POST', + dataType: 'json', // answer expected type + beforeSubmit: function(){ + $('.js_errzone').html("").hide(); + }, + success: function(response, status, xhr, wfe){ + if(_.has(response, 'errors')){ + _.each(_.keys(response.errors), function(key){ + $("#" + key + '>.js_errzone').append('

' + response.errors[key] + '

').show(); + }); + return false; + } + else if (_.has(response, 'redirect')){ + return true; + } + else { + console.error("Something bad happened during AJAX request :("); + return false; + } + } + }); // Handles the event when a question is focused out $('.question-wrapper').focusout( @@ -52,114 +50,6 @@ $(document).ready(function () { console.debug("[survey] Focus lost on question " + $(this).attr("id")); }); - // Parameters for form submission - $('.js_surveyform').ajaxForm({ - type: 'POST', - contentType: 'application/json', // request mimetype - dataType: 'json', // answer expected type - //success: - //error: - url: submit_controller - }); - - $('.js_surveyform').submit(function(){ - $(this).ajaxSubmit(); - return false; - }); - - // $('form').submit( - // function(){ - // console.debug("[survey] Submit button clicked"); - // var valid = true; - - // $('.js_errzone').html("").hide(); - - // $('.question-wrapper').each( - // function(){ - // var qparams = jQuery.parseJSON($(this).attr('data-oe-survey-qparams')); - // var question_id = $(this).attr('id').slice(7); //all the ids of question containers start with "survey_" - // var candidate_answers = _.filter(answers, function(item){return item.name.startsWith(question_id);}); - // var answer_value; - - // if(qparams.type === 'free_text'){ - // answer_value = candidate_answers[0].value.trim(); - // if(qparams.constr_mandatory === true && answer_value === ''){ - // valid = false; - // display_form_error(question_id, "This question is mandatory"); - // } - - - // } else if (qparams.type === 'textbox'){ - // answer_value = candidate_answers[0].value.trim(); - // if(qparams.constr_mandatory === true && answer_value === ''){ - // valid = false; - // display_form_error(question_id, "This question is mandatory"); - // } - - - // } else if (qparams.type === 'numerical_box'){ - // answer_value = candidate_answers[0].value.trim(); - // if(qparams.constr_mandatory === true && answer_value === ''){ - // valid = false; - // display_form_error(question_id, "This question is mandatory"); - - // } - // if(answer_value !== ''){ - // if(_.isNaN(parseFloat(answer_value))){ - // valid = false; - // display_form_error(question_id, "You must answer with a number"); - // } - // } - - - // } else if (qparams.type === 'datetime'){ - // answer_value = candidate_answers[0].value.trim(); - // if(qparams.constr_mandatory === true && answer_value === ''){ - // valid = false; - // display_form_error(question_id, "This question is mandatory"); - // } - // if(answer_value !== ''){ - // console.warning("Date format validation is not yet implemented"); - // } - - // } else if (qparams.type === 'simple_choice_scale' || qparams.type === 'simple_choice_dropdown'){ - // if(qparams.constr_mandatory === true && _.size(candidate_answers) === 0){ - // valid = false; - // display_form_error(question_id, "This question is mandatory"); - // } - // // if answers: - // // answer_value = candidate_answers[0].value; - - - - // } else if (qparams.type === 'multiple_choice'){ - // var answ_nbr = _.size(candidate_answers); - // if(qparams.constr_mandatory === true && answ_nbr <= 0){ - // valid = false; - // display_form_error(question_id, "This question is mandatory"); - // } - - - // // } else if (qparams.type === 'vector'){ - - - - // // } else if (qparams.type === 'matrix'){ - - - - // } else { - // console.warning('Not supported question type!'); - // } - - - // }); - - // return valid; - - // // _.filter(answers, function(item){return item.name.startsWith(question_id);}) - // return; - // }); - + console.debug("[survey] Custom JS for survey loaded!"); }); \ No newline at end of file diff --git a/addons/survey/survey.py b/addons/survey/survey.py index cfcdb99bb7f..67339d3b0b6 100644 --- a/addons/survey/survey.py +++ b/addons/survey/survey.py @@ -345,24 +345,31 @@ class survey_question(osv.osv): oldname='descriptive_text'), # Answer - 'type': fields.selection([('free_text', 'Free Text'), + 'type': fields.selection([('free_text', 'Long text zone'), ('textbox', 'Text box'), ('numerical_box', 'Numerical box'), ('datetime', 'Date and Time'), - ('simple_choice_scale', 'One choice on a scale'), - ('simple_choice_dropdown', 'One choice in a menu'), - ('multiple_choice', 'Some choices in checkboxes'), + ('simple_choice', 'Multiple choice (one answer)'), + ('multiple_choice', 'Multiple choice (multiple answers)'), ('matrix', 'Matrix')], 'Question Type', required=1), - 'suggested_answers_ids': fields.one2many('survey.suggestion', + 'labels_ids': fields.one2many('survey.label', 'question_id', 'Suggested answers', oldname='answer_choice_ids'), + # Display options + 'simple_choice_display' : fields.selection([('1col', 'Buttons (1 column)'), + ('2col', 'Buttons (2 columns)'), + ('3col', 'Buttons (3 columns)'), + ('horizontal', 'Buttons (horizontal)'), + ('dropdown', 'Dropdown menu'),], + 'Display mode', ), + # Comments 'comments_allowed': fields.boolean('Allow comments', oldname="allow_comment"), - 'comment_children_ids': fields.one2many('survey.question_id', - 'parent_id', 'Comment question'), # one2one in fact - 'comment_count_as_answer': fields.boolean('Make Comment Field an \ - Answer Choice', oldname='make_comment_field'), + #'comment_children_ids': fields.one2many('survey.question_id', + # 'parent_id', 'Comment question'), # one2one in fact + 'comment_count_as_answer': fields.boolean('Comment field is an answer choice', + oldname='make_comment_field'), # Validation 'validation_required': fields.boolean('Validate entry', @@ -371,7 +378,7 @@ class survey_question(osv.osv): ('has_length', 'Must have a specific length'), ('is_integer', 'Must be an integer'), ('is_decimal', 'Must be a decimal number'), - ('is_date', 'Must be a date'), + #('is_date', 'Must be a date'), ('is_email', 'Must be an email address') ], 'Validation type'), 'validation_length_min': fields.integer('Minimum length'), @@ -382,8 +389,7 @@ class survey_question(osv.osv): 'validation_max_int_value': fields.integer('Maximum value'), 'validation_min_date': fields.date('Start date range'), 'validation_max_date': fields.date('End date range'), - 'validation_error_msg': fields.char("Error message if validation \ - fails", oldname='validation_valid_err_msg'), + 'validation_error_msg': fields.char("Error message", oldname='validation_valid_err_msg'), # Constraints on number of answers 'constr_mandatory': fields.boolean('Mandatory question', @@ -398,23 +404,28 @@ class survey_question(osv.osv): oldname='maximum_req_ans'), 'constr_minimum_req_ans': fields.integer('Minimum Required Answer', oldname='minimum_req_ans'), - 'constr_error_msg': fields.char("Error message if constraints fails", + 'constr_error_msg': fields.char("Error message", oldname='req_error_msg'), } _defaults = { 'page_id': lambda s, cr, uid, c: c.get('page_id'), - 'sequence': 1, 'type': 'free_text', - 'validation_error_msg': lambda s, cr, uid, c: _('The answer you entered has an invalid format.'), + 'simple_choice_display': '1col', 'constr_type': 'at least', 'constr_minimum_req_ans': 1, 'constr_error_msg': lambda s, cr, uid, c: _('This question requires an answer.'), + 'validation_error_msg': lambda s, cr, uid, c: _('The answer you entered has an invalid format.'), 'validation_required': 'False', - 'validation_length_min': 0, - 'validation_length_max': 5000, } - _sql_constraints = [('validation_length', 'CHECK (validation_length_min <= validation_length_max)', 'Max length cannot be smaller than min length!')] + _sql_constraints = [ + ('positive_len_min', 'CHECK (validation_length_min >= 0)', 'A length must be positive!'), + ('positive_len_max', 'CHECK (validation_length_max >= 0)', 'A length must be positive!'), + ('validation_length', 'CHECK (validation_length_min <= validation_length_max)', 'Max length cannot be smaller than min length!'), + ('validation_float', 'CHECK (validation_min_float_value <= validation_max_float_value)', 'Max value cannot be smaller than min value!'), + ('validation_int', 'CHECK (validation_min_int_value <= validation_max_int_value)', 'Max value cannot be smaller than min value!'), + ('validation_date', 'CHECK (validation_min_date <= validation_max_date)', 'Max date cannot be smaller than min date!') + ] def on_change_page_id(self, cr, uid, ids, page_id, context=None): if page_id: @@ -527,9 +538,9 @@ class survey_question(osv.osv): } -class survey_suggestion(osv.osv): +class survey_label(osv.osv): ''' A suggested answer for a question ''' - _name = 'survey.suggestion' + _name = 'survey.label' _rec_name = 'value' _columns = { @@ -678,7 +689,7 @@ class survey_user_input_line(osv.osv): 'value_number': fields.float("Numerical answer"), 'value_date': fields.datetime("Date answer"), 'value_free_text': fields.text("Free Text answer"), - 'value_suggested': fields.many2one('survey.suggestion'), + 'value_suggested': fields.many2one('survey.label'), } _defaults = { 'skipped': False, diff --git a/addons/survey/survey_view.xml b/addons/survey/survey_view.xml index 275b159330b..da9d2fd3eae 100644 --- a/addons/survey/survey_view.xml +++ b/addons/survey/survey_view.xml @@ -402,14 +402,14 @@ - - - + + + + @@ -422,7 +422,7 @@ - + @@ -436,7 +436,7 @@ - + @@ -464,7 +464,7 @@ - + @@ -518,9 +518,9 @@ {'search_default_group_by_survey': True, 'search_default_group_by_page': True} - - Survey Suggestions - survey.suggestion + + Survey labels + survey.label form tree,form diff --git a/addons/survey/views/survey_templates.xml b/addons/survey/views/survey_templates.xml index 128962301d5..a0a486e5391 100644 --- a/addons/survey/views/survey_templates.xml +++ b/addons/survey/views/survey_templates.xml @@ -9,11 +9,10 @@

Open surveys

- + - @@ -112,6 +111,10 @@ + + + + @@ -120,41 +123,19 @@ + + +
TitleDescriptionCategory
TitleCategory