summaryrefslogtreecommitdiff
path: root/addons/survey/controllers
diff options
context:
space:
mode:
authorAurélien Warnon <awa@odoo.com>2019-02-06 09:29:37 +0000
committerAurélien Warnon <awa@odoo.com>2019-02-06 10:34:40 +0000
commitafb1abbb60e5cdc4f76ab7b5a1b54b5e8a9283d5 (patch)
tree351e5ef81e76689378b77739af1adfe0b1efcd02 /addons/survey/controllers
parentdba478332681dd21a39c4a183c002b1cf6e046d2 (diff)
[IMP] survey: scoring & certification
Task #1902306 Purpose ======= This commit adds certification capabilities to the survey module. A certification is a survey with the "certification" flag set to true than can be linked to a certification email template that contains a certification document (PDF). This template can be edited by the user in the technical settings to customize the certification email/document. To be able to implement this certification concept, we also have to add scoring mechanisms to surveys. survey.questions of type 'simple_choice' and 'multiple_choice' now have scores for the suggested answers. These question scores allow to compute a global score that is used to determine whether the user has successfully passed the certification or not. As additional features, we also have: - A time limit with an interface timer that limits the test to X minutes When reached, the survey is automatically submitted and unregistered (= unsubmitted) answers are not taken into account. - A limited number of attempts for the survey/certification If reached, the user can't take the survey/certification anymore The 'survey result' layout was adapted accordingly to show the success rate of participants and the correct answers to the survey questions. Specs ======= - Create a new survey : - Add a description field for the survey - Options on a survey : - Passing score : (sum of all good answers) in % - If No scoring => No passing score, no certificate - If Scoring with answers => Passing score and can see the answers (can create certificate) - If Scoring without answers => Passing score but can't review the answers at the end (can create certificate) - for the questions, if "no scoring" selected, can 't see the option "good answer" and "score" on the questions - If the 2 others options, can see the options "good answer" and "score" on the questions - all the types of questions are available for each option. - Questions - Add the option correct answer on the multiples questions (one or more good answers) - Certifications - If scoring, force "mandatory" for "mutliple choices (1or multiple answers)" - On the dashboard => visual information that this specific survey is a certification - Time Limit : The student is informed on the home screen of the survey of the time limit. The clock start when he clicks on "start survey" - Template of the certificate : send email with attachment PDF - Front-end : - Add some margin - Replace "Back to survey" with the blue-bar from the portal - Add a timer (start when the survey starts) - Add a progress bar (number of section and number of question inside the section) - Analyse of the results : - First a global graph with the number of people who''ve participated and passed the test - Stages of a survey : - Remove the stage "Permanent" - 3 stages : - draft : not on-line but can be tested (with phantom token) - In progress : on-line - closed : not on-line - Who can test a survey : the manager and the user. Add this condition to the phantom token.
Diffstat (limited to 'addons/survey/controllers')
-rw-r--r--addons/survey/controllers/main.py167
1 files changed, 140 insertions, 27 deletions
diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py
index 8111459f11d..35d505721de 100644
--- a/addons/survey/controllers/main.py
+++ b/addons/survey/controllers/main.py
@@ -9,7 +9,7 @@ from datetime import datetime
from dateutil.relativedelta import relativedelta
from math import ceil
-from odoo import fields, http
+from odoo import fields, http, _
from odoo.addons.base.models.ir_ui_view import keep_query
from odoo.exceptions import UserError
from odoo.http import request
@@ -120,7 +120,7 @@ class Survey(http.Controller):
answer_sudo = access_data['answer_sudo']
if error_key == 'survey_void' and access_data['can_answer']:
- return request.render("survey.survey_void", {'survey': survey_sudo})
+ return request.render("survey.survey_void", {'survey': survey_sudo, 'answer': answer_sudo})
elif error_key == 'survey_closed' and access_data['can_answer']:
return request.render("survey.survey_expired", {'survey': survey_sudo})
elif error_key == 'survey_auth' and answer_sudo.token:
@@ -136,7 +136,7 @@ class Survey(http.Controller):
elif error_key == 'answer_deadline' and answer_sudo.token:
return request.render("survey.survey_expired", {'survey': survey_sudo})
elif error_key == 'answer_done' and answer_sudo.token:
- return request.render("survey.sfinished", {'survey': survey_sudo, 'token': answer_sudo.token, 'user_input': answer_sudo})
+ return request.render("survey.sfinished", {'survey': survey_sudo, 'token': answer_sudo.token, 'answer': answer_sudo})
return werkzeug.utils.redirect("/")
@@ -151,6 +151,25 @@ class Survey(http.Controller):
return werkzeug.utils.redirect('/')
return request.redirect('/survey/start/%s?%s' % (survey_sudo.access_token, keep_query('*', answer_token=answer_sudo.token)))
+ @http.route('/survey/retry/<string:survey_token>/<string:answer_token>', type='http', auth='public', website=True)
+ def survey_retry(self, survey_token, answer_token, **post):
+ """ This route is called whenever the user has attempts left and hits the 'Retry' button
+ after failing the survey."""
+ access_data = self._get_access_data(survey_token, answer_token, ensure_token=True)
+ if access_data['validity_code'] is not True and access_data['validity_code'] != 'answer_done':
+ return self._redirect_with_error(access_data, access_data['validity_code'])
+
+ survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
+ if not answer_sudo:
+ # attempts to 'retry' without having tried first
+ return werkzeug.utils.redirect("/")
+
+ try:
+ retry_answer_sudo = survey_sudo._create_answer(user=request.env.user, partner=answer_sudo.partner_id, email=answer_sudo.email)
+ except:
+ return werkzeug.utils.redirect("/")
+ return request.redirect('/survey/start/%s?%s' % (survey_sudo.access_token, keep_query('*', answer_token=retry_answer_sudo.token)))
+
@http.route('/survey/start/<string:survey_token>', type='http', auth='public', website=True)
def survey_start(self, survey_token, answer_token=None, email=False, **post):
""" Start a survey by providing
@@ -179,11 +198,34 @@ class Survey(http.Controller):
# Select the right page
if answer_sudo.state == 'new': # Intro page
- data = {'survey': survey_sudo, 'page': None, 'answer': answer_sudo}
+ data = {'survey': survey_sudo, 'answer': answer_sudo, 'page': 0}
return request.render('survey.survey_init', data)
else:
return request.redirect('/survey/fill/%s/%s' % (survey_sudo.access_token, answer_sudo.token))
+ # Survey direct link to a specific page
+ @http.route('/survey/page/<string:survey_token>/<string:answer_token>/<int:page_id>',
+ type='http', auth='public', website=True)
+ def survey_change_page(self, survey_token, answer_token, page_id, **post):
+ """ Method called when the user switches from one page to another using the breadcrumbs links
+ in the survey layout.
+ TODO: Right now, the answers that are not submitted are LOST when changing from one page to another
+ using this method.
+
+ The survey "submit" mechanism needs to be refactored entirely to make this more user-friendly."""
+ # Controls if the survey can be displayed
+ access_data = self._get_access_data(survey_token, answer_token, ensure_token=False)
+ if access_data['validity_code'] is not True:
+ return self._redirect_with_error(access_data, access_data['validity_code'])
+
+ survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
+
+ return request.render('survey.survey', {
+ 'survey': survey_sudo,
+ 'page': request.env['survey.question'].sudo().browse(page_id),
+ 'answer': answer_sudo
+ })
+
@http.route('/survey/fill/<string:survey_token>/<string:answer_token>', type='http', auth='public', website=True)
def survey_display_page(self, survey_token, answer_token, prev=None, **post):
access_data = self._get_access_data(survey_token, answer_token, ensure_token=True)
@@ -192,27 +234,42 @@ class Survey(http.Controller):
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
+ if survey_sudo.is_time_limited and not answer_sudo.start_datetime:
+ # init start date when user starts filling in the survey
+ answer_sudo.write({
+ 'start_datetime': fields.Datetime.now()
+ })
+
# Select the right page
if answer_sudo.state == 'new': # First page
- page, page_nr, last = survey_sudo.next_page(answer_sudo, 0, go_back=False)
- data = {'survey': survey_sudo, 'page': page, 'page_nr': page_nr, 'answer': answer_sudo}
+ page, last = survey_sudo.next_page(answer_sudo, 0, go_back=False)
+ data = {
+ 'survey': survey_sudo,
+ 'page': page,
+ 'answer': answer_sudo
+ }
if last:
data.update({'last': True})
return request.render('survey.survey', data)
elif answer_sudo.state == 'done': # Display success message
return request.render('survey.sfinished', {'survey': survey_sudo,
- 'user_input': answer_sudo})
+ 'answer': answer_sudo})
elif answer_sudo.state == 'skip':
flag = (True if prev and prev == 'prev' else False)
- page, page_nr, last = survey_sudo.next_page(answer_sudo, answer_sudo.last_displayed_page_id.id, go_back=flag)
+ page, last = survey_sudo.next_page(answer_sudo, answer_sudo.last_displayed_page_id.id, go_back=flag)
#special case if you click "previous" from the last page, then leave the survey, then reopen it from the URL, avoid crash
if not page:
- page, page_nr, last = survey_sudo.next_page(answer_sudo, answer_sudo.last_displayed_page_id.id, go_back=True)
+ page, last = survey_sudo.next_page(answer_sudo, answer_sudo.last_displayed_page_id.id, go_back=True)
- data = {'survey': survey_sudo, 'page': page, 'page_nr': page_nr, 'answer': answer_sudo}
+ data = {
+ 'survey': survey_sudo,
+ 'page': page,
+ 'answer': answer_sudo
+ }
if last:
data.update({'last': True})
+
return request.render('survey.survey', data)
else:
return request.render("survey.403", {'survey': survey_sudo})
@@ -280,17 +337,25 @@ class Survey(http.Controller):
ret = {}
for answer in answer_sudo.user_input_line_ids:
tmp_score = ret.get(answer.question_id.id, 0.0)
- ret.update({answer.question_id.id: tmp_score + answer.quizz_mark})
+ ret.update({answer.question_id.id: tmp_score + answer.answer_score})
return json.dumps(ret)
@http.route('/survey/submit/<string:survey_token>/<string:answer_token>', type='http', methods=['POST'], auth='public', website=True)
def survey_submit(self, survey_token, answer_token, **post):
- """ TDE NOTE: original comment: # AJAX submission of a page -> AJAX / http ?? """
+ """ Submit a page from the survey.
+ This will take into account the validation errors and store the answers to the questions.
+ If the time limit is reached, errors will be skipped, answers wil be ignored and
+ survey state will be forced to 'done'
+
+ TDE NOTE: original comment: # AJAX submission of a page -> AJAX / http ?? """
access_data = self._get_access_data(survey_token, answer_token, ensure_token=True)
if access_data['validity_code'] is not True:
return {}
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
+ if not survey_sudo._has_attempts_left(answer_sudo.partner_id, answer_sudo.email):
+ # prevent cheating with users creating multiple 'user_input' before their last attempt
+ return {}
page_id = int(post['page_id'])
questions = request.env['survey.question'].sudo().search([
@@ -298,32 +363,41 @@ class Survey(http.Controller):
('page_id', '=', page_id)
])
- # Answer validation
errors = {}
- for question in questions:
- answer_tag = "%s_%s_%s" % (survey_sudo.id, page_id, question.id)
- errors.update(question.validate_question(post, answer_tag))
+ # Answer validation
+ if not answer_sudo.is_time_limit_reached:
+ for question in questions:
+ answer_tag = "%s_%s_%s" % (survey_sudo.id, page_id, question.id)
+ errors.update(question.validate_question(post, answer_tag))
ret = {}
if len(errors):
# Return errors messages to webpage
ret['errors'] = errors
else:
- for question in questions:
- answer_tag = "%s_%s_%s" % (survey_sudo.id, page_id, question.id)
- request.env['survey.user_input_line'].sudo().save_lines(answer_sudo.id, question, post, answer_tag)
-
- go_back = post['button_submit'] == 'previous'
- next_page, _, last = request.env['survey.survey'].next_page(answer_sudo, page_id, go_back=go_back)
- vals = {'last_displayed_page_id': page_id}
- if next_page is None and not go_back:
- vals.update({'state': 'done'})
+ if answer_sudo.is_time_limit_reached:
+ go_back = False
+ answer_sudo._send_certification()
+ vals = {'state': 'done'}
else:
- vals.update({'state': 'skip'})
+ for question in questions:
+ answer_tag = "%s_%s_%s" % (survey_sudo.id, page_id, question.id)
+ request.env['survey.user_input_line'].sudo().save_lines(answer_sudo.id, question, post, answer_tag)
+
+ go_back = post['button_submit'] == 'previous'
+ next_page, last = request.env['survey.survey'].next_page(answer_sudo, page_id, go_back=go_back)
+ vals = {'last_displayed_page_id': page_id}
+ if next_page is None and not go_back:
+ answer_sudo._send_certification()
+ vals.update({'state': 'done'})
+ else:
+ vals.update({'state': 'skip'})
+
answer_sudo.write(vals)
ret['redirect'] = '/survey/fill/%s/%s' % (survey_sudo.access_token, answer_token)
if go_back:
ret['redirect'] += '?prev=prev'
+
return json.dumps(ret)
@http.route('/survey/print/<string:survey_token>', type='http', auth='public', website=True)
@@ -342,7 +416,7 @@ class Survey(http.Controller):
'survey': survey_sudo,
'answer': answer_sudo,
'page_nr': 0,
- })
+ 'quizz_correction': survey_sudo.scoring_type != 'scoring_without_answers' and answer_sudo})
@http.route('/survey/results/<model("survey.survey"):survey>', type='http', auth='user', website=True)
def survey_report(self, survey, answer_token=None, **post):
@@ -424,6 +498,12 @@ class Survey(http.Controller):
page_dict['question_ids'].append(question_dict)
result['page_ids'].append(page_dict)
+
+ if survey.scoring_type in ['scoring_with_answers', 'scoring_without_answers']:
+ scoring_data = self._get_scoring_data(survey)
+ result['success_rate'] = scoring_data['success_rate']
+ result['scoring_graph_data'] = json.dumps(scoring_data['graph_data'])
+
return result
def _get_filter_data(self, post):
@@ -463,3 +543,36 @@ class Survey(http.Controller):
values.append({'text': data['rows'].get(row), 'count': data['result'].get((row, answer))})
result.append({'key': data['answers'].get(answer), 'values': values})
return json.dumps(result)
+
+ def _get_scoring_data(self, survey):
+ """Performs a read_group to fetch the count of failed/passed tests in a single query."""
+
+ count_data = request.env['survey.user_input'].read_group(
+ [('survey_id', '=', survey.id), ('state', '=', 'done'), ('test_entry', '=', False)],
+ ['quizz_passed', 'id:count_distinct'],
+ ['quizz_passed']
+ )
+
+ quizz_passed_count = 0
+ quizz_failed_count = 0
+ for count_data_item in count_data:
+ if count_data_item['quizz_passed']:
+ quizz_passed_count = count_data_item['quizz_passed_count']
+ else:
+ quizz_failed_count = count_data_item['quizz_passed_count']
+
+ graph_data = [{
+ 'text': _('Passed'),
+ 'count': quizz_passed_count,
+ 'color': '#2E7D32'
+ }, {
+ 'text': _('Missed'),
+ 'count': quizz_failed_count,
+ 'color': '#C62828'
+ }]
+
+ total_quizz_passed = quizz_passed_count + quizz_failed_count
+ return {
+ 'success_rate': round((quizz_passed_count / total_quizz_passed) * 100, 1) if total_quizz_passed > 0 else 0,
+ 'graph_data': graph_data
+ }