diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py
index 045661c5d75..7eabe92f733 100644
--- a/addons/survey/controllers/main.py
+++ b/addons/survey/controllers/main.py
@@ -20,6 +20,10 @@ _logger = logging.getLogger(__name__)
class Survey(http.Controller):
+ # ------------------------------------------------------------
+ # ACCESS
+ # ------------------------------------------------------------
+
def _fetch_from_access_token(self, survey_token, answer_token):
""" Check that given token matches an answer from the given survey_id.
Returns a sudo-ed browse record of survey in order to avoid access rights
@@ -182,6 +186,10 @@ class Survey(http.Controller):
'deadline': answer.deadline,
}
+ # ------------------------------------------------------------
+ # TAKING SURVEY ROUTES
+ # ------------------------------------------------------------
+
@http.route('/survey/start/', type='http', auth='public', website=True)
def survey_start(self, survey_token, answer_token=None, email=False, **post):
""" Start a survey by providing
@@ -423,6 +431,10 @@ class Survey(http.Controller):
return json.dumps(ret)
+ # ------------------------------------------------------------
+ # COMPLETED SURVEY ROUTES
+ # ------------------------------------------------------------
+
@http.route('/survey/print/', type='http', auth='public', website=True)
def survey_print(self, survey_token, review=False, answer_token=None, **post):
'''Display an survey in printable view; if is set, it will
@@ -639,4 +651,12 @@ class Survey(http.Controller):
values = {'survey': survey, 'answer': answer}
if token:
values['token'] = token
+ if survey.scoring_type != 'no_scoring' and survey.certificate:
+ answer_perf = survey._get_answers_correctness(answer)[answer]
+ values['graph_data'] = json.dumps([
+ {"text": "Correct", "count": answer_perf['correct']},
+ {"text": "Partially", "count": answer_perf['partial']},
+ {"text": "Incorrect", "count": answer_perf['incorrect']},
+ {"text": "Unanswered", "count": answer_perf['skipped']}
+ ])
return values
diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py
index 64a74f57652..5418d414603 100644
--- a/addons/survey/models/survey_survey.py
+++ b/addons/survey/models/survey_survey.py
@@ -8,7 +8,7 @@ from itertools import product
from werkzeug import urls
import random
-from odoo import api, fields, models, SUPERUSER_ID, _
+from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.osv import expression
@@ -25,7 +25,7 @@ class Survey(models.Model):
return str(uuid.uuid4())
# description
- title = fields.Char('Title', required=True, translate=True)
+ title = fields.Char('Survey Title', required=True, translate=True)
description = fields.Html("Description", translate=True,
help="The description will be displayed on the home page of the survey. You can use this to give the purpose and guidelines to your candidates before they start it.")
color = fields.Integer('Color Index', default=0)
@@ -188,7 +188,10 @@ class Survey(models.Model):
if not self.users_login_required or not self.certificate:
self.certification_give_badge = False
+ # ------------------------------------------------------------
# CRUD
+ # ------------------------------------------------------------
+
@api.model
def create(self, vals):
survey = super(Survey, self).create(vals)
@@ -202,12 +205,15 @@ class Survey(models.Model):
return self.sudo()._handle_certification_badges(vals)
return result
- # Public methods #
def copy_data(self, default=None):
title = _("%s (copy)") % (self.title)
default = dict(default or {}, title=title)
return super(Survey, self).copy_data(default)
+ # ------------------------------------------------------------
+ # TECHNICAL
+ # ------------------------------------------------------------
+
def _create_answer(self, user=False, partner=False, email=False, test_entry=False, check_attempts=True, **additional_vals):
""" Main entry point to get a token back or create a new one. This method
does check for current user access in order to explicitely validate
@@ -278,6 +284,60 @@ class Survey(models.Model):
if check_attempts and not self._has_attempts_left(partner or (user and user.partner_id), email, invite_token):
raise UserError(_('No attempts left.'))
+ def _prepare_answer_questions(self):
+ """ Will generate the questions for a randomized survey.
+ It uses the random_questions_count of every sections of the survey to
+ pick a random number of questions and returns the merged recordset """
+ self.ensure_one()
+
+ questions = self.env['survey.question']
+
+ for page in self.page_ids:
+ if self.questions_selection == 'all':
+ questions |= page.question_ids
+ else:
+ if page.random_questions_count > 0 and len(page.question_ids) > page.random_questions_count:
+ questions = questions.concat(*random.sample(page.question_ids, page.random_questions_count))
+ else:
+ questions |= page.question_ids
+
+ if not questions:
+ questions = self.question_ids
+
+ return questions
+
+ def _has_attempts_left(self, partner, email, invite_token):
+ self.ensure_one()
+
+ if (self.access_mode != 'public' or self.users_login_required) and self.is_attempts_limited:
+ return self._get_number_of_attempts_lefts(partner, email, invite_token) > 0
+
+ return True
+
+ def _get_number_of_attempts_lefts(self, partner, email, invite_token):
+ """ Returns the number of attempts left. """
+ self.ensure_one()
+
+ domain = [
+ ('survey_id', '=', self.id),
+ ('test_entry', '=', False),
+ ('state', '=', 'done')
+ ]
+
+ if partner:
+ domain = expression.AND([domain, [('partner_id', '=', partner.id)]])
+ else:
+ domain = expression.AND([domain, [('email', '=', email)]])
+
+ if invite_token:
+ domain = expression.AND([domain, [('invite_token', '=', invite_token)]])
+
+ return self.attempts_limit - self.env['survey.user_input'].search_count(domain)
+
+ # ------------------------------------------------------------
+ # ACTIONS
+ # ------------------------------------------------------------
+
@api.model
def next_page_or_question(self, user_input, page_or_question_id, go_back=False):
""" The next page to display to the user, knowing that page_id is the id
@@ -329,6 +389,115 @@ class Survey(models.Model):
else:
return (pages_or_questions[current_page_index + 1][1], False)
+ def action_draft(self):
+ self.write({'state': 'draft'})
+
+ def action_open(self):
+ self.write({'state': 'open'})
+
+ def action_close(self):
+ self.write({'state': 'closed'})
+
+ def action_start_survey(self):
+ """ Open the website page with the survey form """
+ self.ensure_one()
+ token = self.env.context.get('survey_token')
+ trail = "?answer_token=%s" % token if token else ""
+ return {
+ 'type': 'ir.actions.act_url',
+ 'name': "Start Survey",
+ 'target': 'self',
+ 'url': self.public_url + trail
+ }
+
+ def action_send_survey(self):
+ """ Open a window to compose an email, pre-filled with the survey message """
+ # Ensure that this survey has at least one page with at least one question.
+ if (not self.page_ids and self.questions_layout == 'page_per_section') or not self.question_ids:
+ raise UserError(_('You cannot send an invitation for a survey that has no questions.'))
+
+ if self.state == 'closed':
+ raise UserError(_("You cannot send invitations for closed surveys."))
+
+ template = self.env.ref('survey.mail_template_user_input_invite', raise_if_not_found=False)
+
+ local_context = dict(
+ self.env.context,
+ default_survey_id=self.id,
+ default_use_template=bool(template),
+ default_template_id=template and template.id or False,
+ notif_layout='mail.mail_notification_light',
+ )
+ return {
+ 'type': 'ir.actions.act_window',
+ 'view_mode': 'form',
+ 'res_model': 'survey.invite',
+ 'target': 'new',
+ 'context': local_context,
+ }
+
+ def action_print_survey(self):
+ """ Open the website page with the survey printable view """
+ self.ensure_one()
+ token = self.env.context.get('survey_token')
+ trail = "?answer_token=%s" % token if token else ""
+ return {
+ 'type': 'ir.actions.act_url',
+ 'name': "Print Survey",
+ 'target': 'self',
+ 'url': '/survey/print/%s%s' % (self.access_token, trail)
+ }
+
+ def action_result_survey(self):
+ """ Open the website page with the survey results view """
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_url',
+ 'name': "Results of the Survey",
+ 'target': 'self',
+ 'url': '/survey/results/%s' % self.id
+ }
+
+ def action_test_survey(self):
+ ''' Open the website page with the survey form into test mode'''
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_url',
+ 'name': "Test Survey",
+ 'target': 'self',
+ 'url': '/survey/test/%s' % self.access_token,
+ }
+
+ def action_survey_user_input_completed(self):
+ action_rec = self.env.ref('survey.action_survey_user_input_notest')
+ action = action_rec.read()[0]
+ ctx = dict(self.env.context)
+ ctx.update({'search_default_survey_id': self.ids[0],
+ 'search_default_completed': 1})
+ action['context'] = ctx
+ return action
+
+ def action_survey_user_input_certified(self):
+ action_rec = self.env.ref('survey.action_survey_user_input_notest')
+ action = action_rec.read()[0]
+ ctx = dict(self.env.context)
+ ctx.update({'search_default_survey_id': self.ids[0],
+ 'search_default_quizz_passed': 1})
+ action['context'] = ctx
+ return action
+
+ def action_survey_user_input(self):
+ action_rec = self.env.ref('survey.action_survey_user_input_notest')
+ action = action_rec.read()[0]
+ ctx = dict(self.env.context)
+ ctx.update({'search_default_survey_id': self.ids[0]})
+ action['context'] = ctx
+ return action
+
+ # ------------------------------------------------------------
+ # GRAPH / RESULTS
+ # ------------------------------------------------------------
+
def filter_input_ids(self, filters, finished=False):
"""If user applies any filters, then this function returns list of
filtered user_input_id and label's strings for display data in web.
@@ -453,6 +622,43 @@ class Survey(models.Model):
return result
+ def _get_answers_correctness(self, user_answers):
+ if not user_answers.mapped('survey_id') == self:
+ raise UserError(_('Invalid performance computation'))
+
+ res = dict((user_answer, {
+ 'correct': 0,
+ 'incorrect': 0,
+ 'partial': 0,
+ 'skipped': 0,
+ }) for user_answer in user_answers)
+
+ scored_questions = self.question_ids.filtered(
+ lambda question: question.question_type in ['simple_choice', 'multiple_choice']
+ )
+
+ for question in scored_questions:
+ question_answer_correct = question.labels_ids.filtered(lambda answer: answer.is_correct)
+ for user_answer in user_answers:
+ user_answer_lines_question = user_answer.user_input_line_ids.filtered(lambda line: line.question_id == question)
+ user_answer_correct = user_answer_lines_question.filtered(lambda line: line.answer_is_correct and not line.skipped).mapped('value_suggested')
+ user_answer_incorrect = user_answer_lines_question.filtered(lambda line: not line.answer_is_correct and not line.skipped)
+
+ if user_answer_correct == question_answer_correct:
+ res[user_answer]['correct'] += 1
+ elif user_answer_correct and user_answer_correct < question_answer_correct:
+ res[user_answer]['partial'] += 1
+ if not user_answer_correct and user_answer_incorrect:
+ res[user_answer]['incorrect'] += 1
+ if not user_answer_correct and not user_answer_incorrect:
+ res[user_answer]['skipped'] += 1
+
+ return res
+
+ # ------------------------------------------------------------
+ # GAMIFICATION / BADGES
+ # ------------------------------------------------------------
+
def _create_certification_badge_trigger(self):
self.ensure_one()
goal = self.env['gamification.goal.definition'].create({
@@ -502,160 +708,3 @@ class Survey(models.Model):
# delete all challenges and goals because not needed anymore (challenge lines are deleted in cascade)
challenges_to_delete.unlink()
goals_to_delete.unlink()
-
- # Actions
-
- def action_draft(self):
- self.write({'state': 'draft'})
-
- def action_open(self):
- self.write({'state': 'open'})
-
- def action_close(self):
- self.write({'state': 'closed'})
-
- def action_start_survey(self):
- """ Open the website page with the survey form """
- self.ensure_one()
- token = self.env.context.get('survey_token')
- trail = "?answer_token=%s" % token if token else ""
- return {
- 'type': 'ir.actions.act_url',
- 'name': "Start Survey",
- 'target': 'self',
- 'url': self.public_url + trail
- }
-
- def action_send_survey(self):
- """ Open a window to compose an email, pre-filled with the survey message """
- # Ensure that this survey has at least one page with at least one question.
- if (not self.page_ids and self.questions_layout == 'page_per_section') or not self.question_ids:
- raise UserError(_('You cannot send an invitation for a survey that has no questions.'))
-
- if self.state == 'closed':
- raise UserError(_("You cannot send invitations for closed surveys."))
-
- template = self.env.ref('survey.mail_template_user_input_invite', raise_if_not_found=False)
-
- local_context = dict(
- self.env.context,
- default_survey_id=self.id,
- default_use_template=bool(template),
- default_template_id=template and template.id or False,
- notif_layout='mail.mail_notification_light',
- )
- return {
- 'type': 'ir.actions.act_window',
- 'view_mode': 'form',
- 'res_model': 'survey.invite',
- 'target': 'new',
- 'context': local_context,
- }
-
- def action_print_survey(self):
- """ Open the website page with the survey printable view """
- self.ensure_one()
- token = self.env.context.get('survey_token')
- trail = "?answer_token=%s" % token if token else ""
- return {
- 'type': 'ir.actions.act_url',
- 'name': "Print Survey",
- 'target': 'self',
- 'url': '/survey/print/%s%s' % (self.access_token, trail)
- }
-
- def action_result_survey(self):
- """ Open the website page with the survey results view """
- self.ensure_one()
- return {
- 'type': 'ir.actions.act_url',
- 'name': "Results of the Survey",
- 'target': 'self',
- 'url': '/survey/results/%s' % self.id
- }
-
- def action_test_survey(self):
- ''' Open the website page with the survey form into test mode'''
- self.ensure_one()
- return {
- 'type': 'ir.actions.act_url',
- 'name': "Test Survey",
- 'target': 'self',
- 'url': '/survey/test/%s' % self.access_token,
- }
-
- def action_survey_user_input_completed(self):
- action_rec = self.env.ref('survey.action_survey_user_input_notest')
- action = action_rec.read()[0]
- ctx = dict(self.env.context)
- ctx.update({'search_default_survey_id': self.ids[0],
- 'search_default_completed': 1})
- action['context'] = ctx
- return action
-
- def action_survey_user_input_certified(self):
- action_rec = self.env.ref('survey.action_survey_user_input_notest')
- action = action_rec.read()[0]
- ctx = dict(self.env.context)
- ctx.update({'search_default_survey_id': self.ids[0],
- 'search_default_quizz_passed': 1})
- action['context'] = ctx
- return action
-
- def action_survey_user_input(self):
- action_rec = self.env.ref('survey.action_survey_user_input_notest')
- action = action_rec.read()[0]
- ctx = dict(self.env.context)
- ctx.update({'search_default_survey_id': self.ids[0]})
- action['context'] = ctx
- return action
-
- def _has_attempts_left(self, partner, email, invite_token):
- self.ensure_one()
-
- if (self.access_mode != 'public' or self.users_login_required) and self.is_attempts_limited:
- return self._get_number_of_attempts_lefts(partner, email, invite_token) > 0
-
- return True
-
- def _get_number_of_attempts_lefts(self, partner, email, invite_token):
- """ Returns the number of attempts left. """
- self.ensure_one()
-
- domain = [
- ('survey_id', '=', self.id),
- ('test_entry', '=', False),
- ('state', '=', 'done')
- ]
-
- if partner:
- domain = expression.AND([domain, [('partner_id', '=', partner.id)]])
- else:
- domain = expression.AND([domain, [('email', '=', email)]])
-
- if invite_token:
- domain = expression.AND([domain, [('invite_token', '=', invite_token)]])
-
- return self.attempts_limit - self.env['survey.user_input'].search_count(domain)
-
- def _prepare_answer_questions(self):
- """ Will generate the questions for a randomized survey.
- It uses the random_questions_count of every sections of the survey to
- pick a random number of questions and returns the merged recordset """
- self.ensure_one()
-
- questions = self.env['survey.question']
-
- for page in self.page_ids:
- if self.questions_selection == 'all':
- questions |= page.question_ids
- else:
- if page.random_questions_count > 0 and len(page.question_ids) > page.random_questions_count:
- questions = questions.concat(*random.sample(page.question_ids, page.random_questions_count))
- else:
- questions |= page.question_ids
-
- if not questions:
- questions = self.question_ids
-
- return questions
diff --git a/addons/survey/models/survey_user.py b/addons/survey/models/survey_user.py
index 3a53d73f439..8cfa9e9dde4 100644
--- a/addons/survey/models/survey_user.py
+++ b/addons/survey/models/survey_user.py
@@ -199,11 +199,15 @@ class SurveyUserInputLine(models.Model):
_name = 'survey.user_input_line'
_description = 'Survey User Input Line'
_rec_name = 'user_input_id'
+ _order = 'question_sequence,id'
+ # survey data
user_input_id = fields.Many2one('survey.user_input', string='User Input', ondelete='cascade', required=True)
- question_id = fields.Many2one('survey.question', string='Question', ondelete='cascade', required=True)
- page_id = fields.Many2one(related='question_id.page_id', string="Page", readonly=False)
survey_id = fields.Many2one(related='user_input_id.survey_id', string='Survey', store=True, readonly=False)
+ question_id = fields.Many2one('survey.question', string='Question', ondelete='cascade', required=True)
+ page_id = fields.Many2one(related='question_id.page_id', string="Section", readonly=False)
+ question_sequence = fields.Integer('Sequence', related='question_id.sequence', store=True)
+ # answer
skipped = fields.Boolean('Skipped')
answer_type = fields.Selection([
('text', 'Text'),
@@ -219,7 +223,16 @@ class SurveyUserInputLine(models.Model):
value_free_text = fields.Text('Free Text answer')
value_suggested = fields.Many2one('survey.label', string="Suggested answer")
value_suggested_row = fields.Many2one('survey.label', string="Row answer")
- answer_score = fields.Float('Score given for this choice')
+ answer_score = fields.Float('Score')
+ answer_is_correct = fields.Boolean('Correct', compute='_compute_answer_is_correct')
+
+ @api.depends('value_suggested', 'question_id')
+ def _compute_answer_is_correct(self):
+ for answer in self:
+ if answer.value_suggested and answer.question_id.question_type in ['simple_choice', 'multiple_choice']:
+ answer.answer_is_correct = answer.value_suggested.is_correct
+ else:
+ answer.answer_is_correct = False
@api.constrains('skipped', 'answer_type')
def _answered_or_skipped(self):
diff --git a/addons/survey/static/src/js/survey_result.js b/addons/survey/static/src/js/survey_result.js
index 6e63d473ea4..380a5c893a8 100644
--- a/addons/survey/static/src/js/survey_result.js
+++ b/addons/survey/static/src/js/survey_result.js
@@ -2,6 +2,7 @@ odoo.define('survey.result', function (require) {
'use strict';
require('web.dom_ready');
+var _t = require('web.core')._t;
if(!$('.js_surveyresult').length) {
return Promise.reject("DOM doesn't contain '.js_surveyresult'");
@@ -159,6 +160,35 @@ if(!$('.js_surveyresult').length) {
return chartConfig;
}
+ //initialize doughnut Chart
+ function init_doughnut_chart(graph_data, quizz_score){
+ var data = graph_data.map(function (point) {
+ return point.count;
+ });
+ var chartConfig = {
+ type: 'doughnut',
+ data: {
+ labels: graph_data.map(function (point) {
+ return point.text;
+ }),
+ datasets: [{
+ label: '',
+ data: data,
+ backgroundColor: data.map(function (val, index) {
+ return D3_COLORS[index % 20];
+ }),
+ }]
+ },
+ options: {
+ title: {
+ display: true,
+ text: _.str.sprintf(_t("Overall Performance %.2f%s"), parseFloat(quizz_score), '%'),
+ },
+ }
+ };
+ return chartConfig;
+ }
+
//load chart to svg element chart:initialized chart, response:AJAX response, quistion_id:if of survey question, tick_limit:text length limit
function load_chart(chartConfig, containerSelector){
var $container = $(containerSelector).css({position: 'relative'});
@@ -187,6 +217,11 @@ if(!$('.js_surveyresult').length) {
chartConfig = init_pie_chart(graph_data);
return load_chart(chartConfig, containerSelector);
}
+ else if (graph_type === 'doughnut') {
+ var quizz_score = $(graph).attr("quizz-score") || 0.0;
+ chartConfig = init_doughnut_chart(graph_data, quizz_score);
+ return load_chart(chartConfig, containerSelector);
+ }
});
var $scoringResultsChart = $('#scoring_results_chart');
diff --git a/addons/survey/views/survey_survey_views.xml b/addons/survey/views/survey_survey_views.xml
index 2c16f8f5992..19249cc12bd 100644
--- a/addons/survey/views/survey_survey_views.xml
+++ b/addons/survey/views/survey_survey_views.xml
@@ -44,7 +44,7 @@
-
+
@@ -257,7 +257,7 @@
You can create surveys for different purposes: customer opinion, services feedback, recruitment interviews, employee's periodical evaluations, marketing campaigns, etc.
- Design easily your survey, send invitations to answer by email and analyze answers.
+ Design easily your survey, send invitations and analyze answers.