From 0d55f476f0bc931762799d24c71e14e6ab0c7dd2 Mon Sep 17 00:00:00 2001 From: "Richard Mathot (OpenERP)" Date: Thu, 12 Dec 2013 12:04:35 +0100 Subject: [PATCH] [IMP] Deadlines on invitations [REF] Survey auth refactoring [REF/IMP] Pagination system refactoring + add go back function [FIX] Small glitches [REF] Code refactoring bzr revid: rim@openerp.com-20131212110435-he9h9dkeyb722l1a --- addons/survey/controllers/main.py | 177 ++++++++++++----------- addons/survey/survey.py | 108 ++++++++++---- addons/survey/views/survey_templates.xml | 6 +- 3 files changed, 173 insertions(+), 118 deletions(-) diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py index e5d488831b5..67e388f45ea 100644 --- a/addons/survey/controllers/main.py +++ b/addons/survey/controllers/main.py @@ -23,16 +23,56 @@ from openerp.addons.web import http from openerp.addons.web.http import request from openerp.addons.website.models import website from openerp import SUPERUSER_ID - +from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT as DTF +from datetime import datetime import werkzeug import json import logging + _logger = logging.getLogger(__name__) class WebsiteSurvey(http.Controller): + ## HELPER METHODS ## + + def _check_bad_cases(self, cr, uid, request, survey_obj, survey, user_input_obj, context=None): + # In case of bad survey, redirect to surveys list + if survey_obj.exists(cr, SUPERUSER_ID, survey.id, context=context) == []: + return werkzeug.utils.redirect("/survey/") + + # In case of auth required, block public user + if survey.auth_required and uid == request.registry['website'].get_public_user(cr, uid, context): + return request.website.render("website.401") + + # In case of non open surveys + if survey.state != 'open': + return request.website.render("survey.notopen") + + # If enough surveys completed + if survey.user_input_limit > 0: + completed = user_input_obj.search(cr, uid, [('state', '=', 'done')], count=True) + if completed >= survey.user_input_limit: + return request.website.render("survey.notopen") + + # Everything seems to be ok + return None + + def _check_deadline(self, cr, uid, user_input, context=None): + '''Prevent opening of the survey if the deadline has turned out + + ! This will NOT disallow access to users who have already partially filled the survey !''' + if user_input.deadline: + dt_deadline = datetime.strptime(user_input.deadline, DTF) + dt_now = datetime.now() + if dt_now > dt_deadline: # survey is not open anymore + return request.website.render("survey.notopen") + + return None + + ## ROUTES HANDLERS ## + # Survey list @website.route(['/survey/', '/survey/list/'], @@ -56,23 +96,10 @@ class WebsiteSurvey(http.Controller): survey_obj = request.registry['survey.survey'] user_input_obj = request.registry['survey.user_input'] - # In case of bad survey, redirect to surveys list - if survey_obj.exists(cr, uid, survey.id, context=context) == []: - return werkzeug.utils.redirect("/survey/") - - # In case of auth required, block public user - if survey.auth_required and uid == request.registry['website'].get_public_user(request.cr, SUPERUSER_ID, request.context): - return request.website.render("website.401") - - # In case of non open surveys - if survey.state != 'open': - return request.website.render("survey.notopen") - - # If enough surveys completed - if survey.user_input_limit > 0: - completed = user_input_obj.search(cr, uid, [('state', '=', 'done')], count=True) - if completed >= survey.user_input_limit: - return request.website.render("survey.notopen") + # Controls if the survey can be displayed + errpage = self._check_bad_cases(cr, uid, request, survey_obj, survey, user_input_obj, context=context) + if errpage: + return errpage # Manual surveying if not token: @@ -89,73 +116,50 @@ class WebsiteSurvey(http.Controller): else: user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0] - # Prevent opening of the survey if the deadline has turned out - # ! This will NOT disallow access to users who have already partially filled the survey ! - # if user_input.deadline > fields.date.now() and user_input.state == 'new': - # return request.website.render("survey.notopen") - # TODO check if this is ok + # Do not open expired survey + errpage = self._check_deadline(cr, uid, user_input, context=context) + if errpage: + return errpage # Select the right page - if user_input.state == 'new': # Intro page data = {'survey': survey, 'page': None, 'token': user_input.token} return request.website.render('survey.survey_init', data) else: return request.redirect('/survey/fill/%s/%s' % (survey.id, user_input.token)) - # Survey displaying - @website.route(['/survey/fill//'], + @website.route(['/survey/fill//', + '/survey/fill///'], type='http', auth='public', multilang=True) - def fill_survey(self, survey, token, **post): + def fill_survey(self, survey, token, prev=None, **post): '''Display and validates a survey''' cr, uid, context = request.cr, request.uid, request.context survey_obj = request.registry['survey.survey'] user_input_obj = request.registry['survey.user_input'] - # In case of bad survey, redirect to surveys list - if survey_obj.exists(cr, uid, survey.id, context=context) == []: - return werkzeug.utils.redirect("/survey/") + # Controls if the survey can be displayed + errpage = self._check_bad_cases(cr, uid, request, survey_obj, survey, user_input_obj, context=context) + if errpage: + return errpage - # In case of auth required, block public user - if survey.auth_required and uid == request.registry['website'].get_public_user(request.cr, SUPERUSER_ID, request.context): - return request.website.render("website.401") - - # In case of non open surveys - if survey.state != 'open': - return request.website.render("survey.notopen") - - # If enough surveys completed - if survey.user_input_limit > 0: - completed = user_input_obj.search(cr, uid, [('state', '=', 'done')], count=True) - if completed >= survey.user_input_limit: - return request.website.render("survey.notopen") - - # Manual surveying - if not token: - if survey.visible_to_user: - user_input_id = user_input_obj.create(cr, uid, {'survey_id': survey.id}) - user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0] - else: # An user cannot open hidden surveys without token - return request.website.render("website.403") + # Load the user_input + try: + user_input_id = user_input_obj.search(cr, uid, [('token', '=', token)])[0] + except IndexError: # Invalid token + return request.website.render("website.403") else: - try: - user_input_id = user_input_obj.search(cr, uid, [('token', '=', token)])[0] - except IndexError: # Invalid token - return request.website.render("website.403") - else: - user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0] + user_input = user_input_obj.browse(cr, uid, [user_input_id], context=context)[0] - # Prevent opening of the survey if the deadline has turned out - # ! This will NOT disallow access to users who have already partially filled the survey ! - # if user_input.deadline > fields.date.now() and user_input.state == 'new': - # return request.website.render("survey.notopen") - # TODO check if this is ok + # Do not display expired survey (even if some pages have already been + # displayed -- There's a time for everything!) + errpage = self._check_deadline(cr, uid, user_input, context=context) + if errpage: + return errpage # Select the right page - if user_input.state == 'new': # First page - page, page_nr, last = self.find_next_page(survey, user_input) + page, page_nr, last = survey_obj.next_page(cr, uid, user_input, 0, go_back=False, context=context) data = {'survey': survey, 'page': page, 'page_nr': page_nr, 'token': user_input.token} if last: data.update({'last': True}) @@ -163,39 +167,33 @@ class WebsiteSurvey(http.Controller): elif user_input.state == 'done': # Display success message return request.website.render('survey.finished', {'survey': survey}) elif user_input.state == 'skip': - page, page_nr, last = self.find_next_page(survey, user_input) + flag = (True if prev and prev == 'prev' else False) + page, page_nr, last = survey_obj.next_page(cr, uid, user_input, user_input.last_displayed_page_id.id, go_back=flag, context=context) data = {'survey': survey, 'page': page, 'page_nr': page_nr, 'token': user_input.token} if last: data.update({'last': True}) + return request.website.render('survey.survey', data) else: return request.website.render("website.403") - # @website.route(['/survey/prefill/'], type='json', auth='public', multilang=True): + # AJAX prefilling of a survey + # @website.route(['/survey/prefill/'], + # type='json', auth='public', multilang=True): + # def prefill(self, survey, **post): + # AJAX validation of some questions # @website.route(['/survey/validate/'], # type='http', auth='public', multilang=True) # def validate(self, survey, **post): - # _logger.debug('Incoming data: %s', post) - # page_id = int(post['page_id']) - # cr, uid, context = request.cr, request.uid, request.context - # questions_obj = request.registry['survey.question'] - # questions_ids = questions_obj.search(cr, uid, [('page_id', '=', page_id)], context=context) - # questions = questions_obj.browse(cr, uid, questions_ids, context=context) - # errors = {} - # for question in questions: - # answer_tag = "%s_%s_%s" % (survey.id, page_id, question.id) - # errors.update(self.validate_question(question, post, answer_tag)) - # ret = {} - # if (len(errors) != 0): - # ret['errors'] = errors - # return json.dumps(ret) + # AJAX submission of a page @website.route(['/survey/submit/'], type='http', auth='public', multilang=True) def submit(self, survey, **post): _logger.debug('Incoming data: %s', post) page_id = int(post['page_id']) cr, uid, context = request.cr, request.uid, request.context + survey_obj = request.registry['survey.survey'] questions_obj = request.registry['survey.question'] questions_ids = questions_obj.search(cr, uid, [('page_id', '=', page_id)], context=context) questions = questions_obj.browse(cr, uid, questions_ids, context=context) @@ -213,6 +211,7 @@ class WebsiteSurvey(http.Controller): else: # Store answers into database user_input_obj = request.registry['survey.user_input'] + user_input_line_obj = request.registry['survey.user_input_line'] try: user_input_id = user_input_obj.search(cr, uid, [('token', '=', post['token'])], context=context)[0] @@ -222,12 +221,18 @@ class WebsiteSurvey(http.Controller): answer_tag = "%s_%s_%s" % (survey.id, page_id, question.id) user_input_line_obj.save_lines(cr, uid, user_input_id, question, post, answer_tag, context=context) - # page, _, _ = self.find_next_page(survey, user_input_obj.browse(cr, uid, [user_input_id], context=context)) - # if page: - # user_input_obj.write(cr, uid, user_input_id, {'state': 'skip'}) - # else: - user_input_obj.write(cr, uid, user_input_id, {'state': 'done'}) + user_input = user_input_obj.browse(cr, uid, user_input_id, context=context) + go_back = post['button_submit'] == 'previous' + next_page, _, last = survey_obj.next_page(cr, uid, user_input, page_id, go_back=go_back, context=context) + vals = {'last_displayed_page_id': page_id} + if next_page is None and not go_back: + vals.update({'state': 'done'}) + else: + vals.update({'state': 'skip'}) + user_input_obj.write(cr, uid, user_input_id, vals, context=context) ret['redirect'] = '/survey/fill/%s/%s' % (survey.id, post['token']) + if go_back: + ret['redirect'] += '/prev' return json.dumps(ret) # Printing routes @@ -256,7 +261,7 @@ class WebsiteSurvey(http.Controller): page_nr = page_nr + 1 else: nextpage = page - if page_nr == len(survey.page_ids): + if page_nr == len(survey.page_ids) - 1: last = True return nextpage, page_nr, last diff --git a/addons/survey/survey.py b/addons/survey/survey.py index b4a0523c6a4..bcdfb136052 100644 --- a/addons/survey/survey.py +++ b/addons/survey/survey.py @@ -150,6 +150,45 @@ class survey_survey(osv.osv): return super(survey_survey, self).copy(cr, uid, ids, vals, context=context) + def next_page(self, cr, uid, user_input, page_id, go_back=False, context=None): + '''The next page to display to the user, knowing that page_id is the id + of the last displayed page. + + If page_id == 0, it will always return the first page of the survey. + + If all the pages have been displayed and go_back == False, it will + return None + + If go_back == True, it will return the *previous* page instead of the + next page. + + .. note:: + It is assumed here that a careful user will not try to set go_back + to True if she knows that the page to display is the first one! + (doing this will probably cause a giant worm to eat her house)''' + survey = user_input.survey_id + pages = list(enumerate(survey.page_ids)) + + # First page + if page_id == 0: + return (pages[0][1], 0, len(pages) == 1) + + current_page_index = pages.index((filter(lambda p: p[1].id == page_id, pages))[0]) + + # All the pages have been displayed + if current_page_index == len(pages) - 1 and not go_back: + return (None, -1, False) + # Let's get back, baby! + elif go_back and survey.users_can_go_back: + return (pages[current_page_index - 1][1], current_page_index - 1, False) + else: + # This will show the last page + if current_page_index == len(pages) - 2: + return (pages[current_page_index + 1][1], current_page_index + 1, True) + # This will show a regular page + else: + return (pages[current_page_index + 1][1], current_page_index + 1, False) + def action_print_survey_questions(self, cr, uid, ids, context=None): ''' Generates a printable view of an empty survey ''' pass @@ -287,6 +326,9 @@ class survey_page(osv.osv): help="An introductory text to your page", translate=True, oldname="note"), } + _defaults = { + 'sequence': 10 + } # Public methods # @@ -408,6 +450,7 @@ class survey_question(osv.osv): } _defaults = { 'page_id': lambda s, cr, uid, c: c.get('page_id'), + 'sequence': 10, 'type': 'free_text', 'matrix_subtype': 'simple', 'column_nb': '12', @@ -661,7 +704,7 @@ class survey_question(osv.osv): if question.constr_mandatory: lines_number = len(question.labels_ids_2) answer_candidates = dict_keys_startswith(post, answer_tag) - comment_answer = answer_candidates.pop(("%s_%s" % (answer_tag, question.comment_children_ids[0].id)), None) + #comment_answer = answer_candidates.pop(("%s_%s" % (answer_tag, question.comment_children_ids[0].id)), None) # Number of lines that have been answered if question.matrix_subtype == 'simple': answer_number = len(answer_candidates) @@ -701,6 +744,9 @@ class survey_label(osv.osv): 'value': fields.char("Suggested value", translate=True, required=True) } + defaults = { + 'sequence': 10 + } class survey_user_input(osv.osv): @@ -714,8 +760,9 @@ class survey_user_input(osv.osv): readonly=1, ondelete='restrict'), 'date_create': fields.datetime('Creation Date', required=True, readonly=1), - 'deadline': fields.date("Deadline", - help="Date by which the person can take part to the survey", + 'deadline': fields.datetime("Deadline", + help="Date by which the person can open the survey and submit answers.\ + Warning: ", oldname="date_deadline"), 'type': fields.selection([('manually', 'Manually'), ('link', 'Link')], 'Answer Type', required=1, readonly=1, @@ -732,6 +779,9 @@ class survey_user_input(osv.osv): 'partner_id': fields.many2one('res.partner', 'Partner', readonly=1), 'email': fields.char("E-mail", readonly=1), + # Displaying data + 'last_displayed_page_id': fields.many2one('survey.page', + 'Last displayed page'), # The answers ! 'user_input_line_ids': fields.one2many('survey.user_input_line', 'user_input_id', 'Answers'), @@ -872,7 +922,7 @@ class survey_user_input_line(osv.osv): 'survey_id': fields.related('user_input_id', 'survey_id', type="many2one", relation="survey.survey", string='Survey'), - 'date_create': fields.datetime('Create Date', required=1), # drop + 'date_create': fields.datetime('Create Date', required=1), 'skipped': fields.boolean('Skipped'), 'answer_type': fields.selection([('text', 'Text'), ('number', 'Number'), @@ -888,7 +938,7 @@ class survey_user_input_line(osv.osv): } _defaults = { 'skipped': False, - 'date_create': fields.datetime.now + 'date_create': fields.datetime.now() } def save_lines(self, cr, uid, user_input_id, question, post, answer_tag, @@ -912,7 +962,7 @@ class survey_user_input_line(osv.osv): 'page_id': question.page_id.id, 'survey_id': question.survey_id.id, } - if answer_tag in post: + 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({'skipped': True}) @@ -933,7 +983,7 @@ class survey_user_input_line(osv.osv): 'page_id': question.page_id.id, 'survey_id': question.survey_id.id, } - if answer_tag in post: + if answer_tag in post and post[answer_tag].strip() != '': vals.update({'answer_type': 'text', 'value_text': post[answer_tag]}) else: vals.update({'skipped': True}) @@ -954,10 +1004,10 @@ class survey_user_input_line(osv.osv): 'page_id': question.page_id.id, 'survey_id': question.survey_id.id, } - if answer_tag in post: + if answer_tag in post and post[answer_tag].strip() != '': vals.update({'answer_type': 'number', 'value_number': float(post[answer_tag])}) else: - vals.update({'skipped': True}) + vals.update({'skipped': True, 'answer_type': None}) old_uil = self.search(cr, uid, [('user_input_id', '=', user_input_id), ('survey_id', '=', question.survey_id.id), ('question_id', '=', question.id)], @@ -989,26 +1039,26 @@ class survey_user_input_line(osv.osv): self.create(cr, uid, vals, context=context) return True - def save_line_simple_choice(self, cr, uid, user_input_id, question, post, answer_tag, context=None): - vals = { - 'user_input_id': user_input_id, - 'question_id': question.id, - 'page_id': question.page_id.id, - 'survey_id': question.survey_id.id, - } - if answer_tag in post: - vals.update({'answer_type': 'date', 'value_date': post[answer_tag]}) - else: - vals.update({'skipped': True}) - old_uil = self.search(cr, uid, [('user_input_id', '=', user_input_id), - ('survey_id', '=', question.survey_id.id), - ('question_id', '=', question.id)], - context=context) - if old_uil: - self.write(cr, uid, old_uil[0], vals, context=context) - else: - self.create(cr, uid, vals, context=context) - return True + # def save_line_simple_choice(self, cr, uid, user_input_id, question, post, answer_tag, context=None): + # vals = { + # 'user_input_id': user_input_id, + # 'question_id': question.id, + # 'page_id': question.page_id.id, + # 'survey_id': question.survey_id.id, + # } + # if answer_tag in post: + # vals.update({'answer_type': 'date', 'value_date': post[answer_tag]}) + # else: + # vals.update({'skipped': True}) + # old_uil = self.search(cr, uid, [('user_input_id', '=', user_input_id), + # ('survey_id', '=', question.survey_id.id), + # ('question_id', '=', question.id)], + # context=context) + # if old_uil: + # self.write(cr, uid, old_uil[0], vals, context=context) + # else: + # self.create(cr, uid, vals, context=context) + # return True def dict_keys_startswith(dictionary, string): diff --git a/addons/survey/views/survey_templates.xml b/addons/survey/views/survey_templates.xml index fa57efa8f19..a3055f701c6 100644 --- a/addons/survey/views/survey_templates.xml +++ b/addons/survey/views/survey_templates.xml @@ -115,9 +115,9 @@
- - - + + +