[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.
This commit is contained in:
Aurélien Warnon
2019-02-06 10:34:40 +00:00
parent dba4783326
commit afb1abbb60
25 changed files with 1077 additions and 102 deletions
@@ -2,7 +2,7 @@
<odoo><data noupdate="1">
<record id="survey_recruitment_form" model="survey.survey">
<field name="title">Recruitment Form</field>
<field name="stage_id" ref="survey.stage_permanent"/>
<field name="stage_id" ref="survey.stage_in_progress"/>
<field name="access_mode">token</field>
<field name="category">hr_recruitment</field>
<field name="users_can_go_back" eval="True"/>
+3
View File
@@ -22,6 +22,8 @@ sent mails with personal token for the invitation of the survey.
'mail',
'web_tour'],
'data': [
'views/survey_report_templates.xml',
'views/survey_reports.xml',
'data/mail_template_data.xml',
'data/survey_data.xml',
'data/ir_actions_data.xml',
@@ -39,6 +41,7 @@ sent mails with personal token for the invitation of the survey.
'demo': [
'data/survey_demo_user.xml',
'data/survey_demo_feedback.xml',
'data/survey_demo_certification.xml',
'data/survey.user_input_line.csv'
],
'installable': True,
+140 -27
View File
@@ -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
}
+38
View File
@@ -27,5 +27,43 @@
<field name="user_signature" eval="False"/>
</record>
<!-- Certification Email template -->
<record id="mail_template_certification" model="mail.template">
<field name="name">Certification: Send by email</field>
<field name="model_id" ref="survey.model_survey_user_input"/>
<field name="email_to">${(object.partner_id.email_formatted or object.email) |safe}</field>
<field name="partner_to">${object.partner_id.id}</field>
<field name="subject">Certification: ${object.survey_id.display_name}</field>
<field name="report_template" ref="certification_report"/>
<field name="report_name">Certification Document</field>
<field name="lang">${object.partner_id.lang}</field>
<field name="auto_delete" eval="True"/>
<field name="body_html" type="xml">
<div style="background:#F0F0F0;color:#515166;padding:10px 0px;font-family:Arial,Helvetica,sans-serif;font-size:14px;">
<table style="width:600px;margin:5px auto;">
<tbody>
<tr><td>
<!-- We use the logo of the company that created the survey (to handle multi company cases) -->
<a href="/"><img src="/logo.png?company=${object.survey_id.create_uid.company_id.id}" style="vertical-align:baseline;max-width:100px;" /></a>
</td><td style="text-align:right;vertical-align:middle;">
Certification: ${object.survey_id.display_name}
</td></tr>
</tbody>
</table>
<table style="width:600px;margin:0px auto;background:white;border:1px solid #e1e1e1;">
<tbody>
<tr><td style="padding:15px 20px 10px 20px;">
<p>Dear <span>${object.partner_id.name or 'participant'}</span></p>
<p>
Here is, in attachment, your certification document for
<strong>${object.survey_id.display_name}</strong>
</p>
<p>Congratulations for succeeding the test!</p>
</td></tr>
</tbody>
</table>
</div>
</field>
</record>
</data>
</odoo>
+1 -7
View File
@@ -5,7 +5,7 @@
<record id="stage_draft" model="survey.stage">
<field name="name">Draft</field>
<field name="sequence" eval="1" />
<field name="closed" eval="False" />
<field name="closed" eval="True" />
<field name="fold" eval="False" />
</record>
<record id="stage_in_progress" model="survey.stage">
@@ -20,11 +20,5 @@
<field name="closed" eval="True" />
<field name="fold" eval="True" />
</record>
<record id="stage_permanent" model="survey.stage">
<field name="name">Permanent</field>
<field name="sequence" eval="4" />
<field name="closed" eval="False" />
<field name="fold" eval="True" />
</record>
</data>
</odoo>
@@ -0,0 +1,302 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Odoo Vendor Certification -->
<record model="survey.survey" id="vendor_certification">
<field name="title">MyCompany Vendor Certification</field>
<field name="access_token">4ead4bc8-b8f2-4760-a682-1fde8ddb95ac</field>
<field name="stage_id" ref="survey.stage_in_progress" />
<field name="access_mode">public</field>
<field name="users_can_go_back" eval="True" />
<field name="users_login_required" eval="True" />
<field name="scoring_type" >scoring_with_answers</field>
<field name="certificate" eval="True"></field>
<field name="certification_mail_template_id" ref="mail_template_certification"></field>
<field name="is_time_limited" >limited</field>
<field name="time_limit" >10.0</field>
<field name="is_attempts_limited" eval="True" />
<field name="attempts_limit">2</field>
<field name="description">&lt;p&gt;Test your vendor skills!.&lt;/p&gt;</field>
<field name="thank_you_message">&lt;p&gt;&lt;/p&gt;</field>
</record>
<!-- Page 1 -->
<record model="survey.question" id="vendor_certification_page_1">
<field name="title">Products</field>
<field name="survey_id" ref="vendor_certification" />
<field name="sequence">1</field>
<field name="is_page" eval="True"/>
<field name="description">&lt;p&gt;Test your knowledge of your products!&lt;/p&gt;</field>
</record>
<!-- Question and predefined answer 1 -->
<record model="survey.question" id="vendor_certification_page_1_question_1">
<field name="survey_id" ref="vendor_certification" />
<field name="sequence">2</field>
<field name="title">Do we sell Acoustic Bloc Screens?</field>
<field name="question_type">simple_choice</field>
<field name="display_mode">dropdown</field>
<field name="constr_mandatory" eval="True" />
</record>
<record model="survey.label" id="vendor_certification_page_1_question_1_choice_1">
<field name="question_id" ref="vendor_certification_page_1_question_1"/>
<field name="sequence">1</field>
<field name="value">No</field>
</record>
<record model="survey.label" id="vendor_certification_page_1_question_1_choice_2">
<field name="question_id" ref="vendor_certification_page_1_question_1"/>
<field name="sequence">2</field>
<field name="value">Yes</field>
<field name="is_correct" eval="True" />
<field name="answer_score">2.0</field>
</record>
<!-- Question and predefined answer 2 -->
<record model="survey.question" id="vendor_certification_page_1_question_2">
<field name="survey_id" ref="vendor_certification" />
<field name="sequence">3</field>
<field name="title">Select all the existing products</field>
<field name="question_type">multiple_choice</field>
<field name="column_nb">4</field>
</record>
<record model="survey.label" id="vendor_certification_page_1_question_2_choice_1">
<field name="question_id" ref="vendor_certification_page_1_question_2"/>
<field name="sequence">1</field>
<field name="value">Chair floor protection</field>
<field name="is_correct" eval="True" />
<field name="answer_score">1.0</field>
</record>
<record model="survey.label" id="vendor_certification_page_1_question_2_choice_2">
<field name="question_id" ref="vendor_certification_page_1_question_2"/>
<field name="sequence">2</field>
<field name="value">Fanta</field>
<field name="answer_score">-1.0</field>
</record>
<record model="survey.label" id="vendor_certification_page_1_question_2_choice_3">
<field name="question_id" ref="vendor_certification_page_1_question_2"/>
<field name="sequence">3</field>
<field name="value">Conference chair</field>
<field name="is_correct" eval="True" />
<field name="answer_score">1.0</field>
</record>
<record model="survey.label" id="vendor_certification_page_1_question_2_choice_4">
<field name="question_id" ref="vendor_certification_page_1_question_2"/>
<field name="sequence">4</field>
<field name="value">Drawer</field>
<field name="is_correct" eval="True" />
<field name="answer_score">1.0</field>
</record>
<record model="survey.label" id="vendor_certification_page_1_question_2_choice_5">
<field name="question_id" ref="vendor_certification_page_1_question_2"/>
<field name="sequence">5</field>
<field name="value">Customizable Lamp</field>
<field name="answer_score">-1.0</field>
</record>
<!-- Question and predefined answer 3 -->
<record model="survey.question" id="vendor_certification_page_1_question_3">
<field name="survey_id" ref="vendor_certification" />
<field name="sequence">4</field>
<field name="title">Select all the available customizations for our Customizable Desk</field>
<field name="question_type">multiple_choice</field>
<field name="column_nb">4</field>
</record>
<record model="survey.label" id="vendor_certification_page_1_question_3_choice_1">
<field name="question_id" ref="vendor_certification_page_1_question_3"/>
<field name="sequence">1</field>
<field name="value">Color</field>
<field name="is_correct" eval="True" />
<field name="answer_score">1.0</field>
</record>
<record model="survey.label" id="vendor_certification_page_1_question_3_choice_2">
<field name="question_id" ref="vendor_certification_page_1_question_3"/>
<field name="sequence">2</field>
<field name="value">Height</field>
<field name="answer_score">-1.0</field>
</record>
<record model="survey.label" id="vendor_certification_page_1_question_3_choice_3">
<field name="question_id" ref="vendor_certification_page_1_question_3"/>
<field name="sequence">3</field>
<field name="value">Width</field>
<field name="is_correct" eval="True" />
<field name="answer_score">-1.0</field>
</record>
<record model="survey.label" id="vendor_certification_page_1_question_3_choice_4">
<field name="question_id" ref="vendor_certification_page_1_question_3"/>
<field name="sequence">4</field>
<field name="value">Legs</field>
<field name="is_correct" eval="True" />
<field name="answer_score">1.0</field>
</record>
<record model="survey.label" id="vendor_certification_page_1_question_3_choice_5">
<field name="question_id" ref="vendor_certification_page_1_question_3"/>
<field name="sequence">5</field>
<field name="value">Number of drawers</field>
<field name="answer_score">-1.0</field>
</record>
<!-- Question and predefined answer 4 -->
<record model="survey.question" id="vendor_certification_page_1_question_4">
<field name="survey_id" ref="vendor_certification" />
<field name="sequence">5</field>
<field name="title">How many versions of the Corner Desk do we have?</field>
<field name="question_type">simple_choice</field>
<field name="display_mode">dropdown</field>
<field name="constr_mandatory" eval="True" />
</record>
<record model="survey.label" id="vendor_certification_page_1_question_4_choice_1">
<field name="question_id" ref="vendor_certification_page_1_question_4"/>
<field name="sequence">1</field>
<field name="value">1</field>
</record>
<record model="survey.label" id="vendor_certification_page_1_question_4_choice_2">
<field name="question_id" ref="vendor_certification_page_1_question_4"/>
<field name="sequence">2</field>
<field name="value">2</field>
<field name="is_correct" eval="True" />
<field name="answer_score">2.0</field>
</record>
<record model="survey.label" id="vendor_certification_page_1_question_4_choice_3">
<field name="question_id" ref="vendor_certification_page_1_question_4"/>
<field name="sequence">3</field>
<field name="value">3</field>
</record>
<record model="survey.label" id="vendor_certification_page_1_question_4_choice_4">
<field name="question_id" ref="vendor_certification_page_1_question_4"/>
<field name="sequence">4</field>
<field name="value">4</field>
</record>
<!-- Question and predefined answer 5 -->
<record model="survey.question" id="vendor_certification_page_1_question_5">
<field name="survey_id" ref="vendor_certification" />
<field name="sequence">6</field>
<field name="title">Do you think we have missing products in our catalog? (not rated)</field>
<field name="question_type">free_text</field>
</record>
<!-- Page 2 -->
<record model="survey.question" id="vendor_certification_page_2">
<field name="title">Prices</field>
<field name="survey_id" ref="vendor_certification" />
<field name="is_page" eval="True"/>
<field name="sequence">7</field>
<field name="description">&lt;p&gt;Test your knowledge of our prices.&lt;/p&gt;</field>
</record>
<!-- Question and predefined answer 6 -->
<record model="survey.question" id="vendor_certification_page_2_question_1">
<field name="survey_id" ref="vendor_certification" />
<field name="sequence">8</field>
<field name="title">How much do we sell our Cable Management Box?</field>
<field name="question_type">simple_choice</field>
<field name="display_mode">dropdown</field>
<field name="constr_mandatory" eval="True" />
</record>
<record model="survey.label" id="vendor_certification_page_2_question_1_choice_1">
<field name="question_id" ref="vendor_certification_page_2_question_1"/>
<field name="sequence">1</field>
<field name="value">20$</field>
</record>
<record model="survey.label" id="vendor_certification_page_2_question_1_choice_2">
<field name="question_id" ref="vendor_certification_page_2_question_1"/>
<field name="sequence">2</field>
<field name="value">50$</field>
</record>
<record model="survey.label" id="vendor_certification_page_2_question_1_choice_3">
<field name="question_id" ref="vendor_certification_page_2_question_1"/>
<field name="sequence">3</field>
<field name="value">80$</field>
</record>
<record model="survey.label" id="vendor_certification_page_2_question_1_choice_4">
<field name="question_id" ref="vendor_certification_page_2_question_1"/>
<field name="sequence">4</field>
<field name="value">100$</field>
<field name="is_correct" eval="True" />
<field name="answer_score">2.0</field>
</record>
<record model="survey.label" id="vendor_certification_page_2_question_1_choice_5">
<field name="question_id" ref="vendor_certification_page_2_question_1"/>
<field name="sequence">5</field>
<field name="value">200$</field>
</record>
<record model="survey.label" id="vendor_certification_page_2_question_1_choice_6">
<field name="question_id" ref="vendor_certification_page_2_question_1"/>
<field name="sequence">6</field>
<field name="value">300$</field>
</record>
<!-- Question and predefined answer 7 -->
<record model="survey.question" id="vendor_certification_page_2_question_2">
<field name="survey_id" ref="vendor_certification" />
<field name="sequence">9</field>
<field name="title">Select all the the products that sell for 100$ or more</field>
<field name="question_type">multiple_choice</field>
<field name="column_nb">2</field>
</record>
<record model="survey.label" id="vendor_certification_page_2_question_2_choice_1">
<field name="question_id" ref="vendor_certification_page_2_question_2"/>
<field name="sequence">1</field>
<field name="value">Corner Desk Right Sit</field>
<field name="answer_score">1.0</field>
<field name="is_correct" eval="True" />
</record>
<record model="survey.label" id="vendor_certification_page_2_question_2_choice_2">
<field name="question_id" ref="vendor_certification_page_2_question_2"/>
<field name="sequence">2</field>
<field name="value">Desk Combination</field>
<field name="answer_score">1.0</field>
<field name="is_correct" eval="True" />
</record>
<record model="survey.label" id="vendor_certification_page_2_question_2_choice_3">
<field name="question_id" ref="vendor_certification_page_2_question_2"/>
<field name="sequence">3</field>
<field name="value">Cabinet with Doors</field>
<field name="answer_score">-1.0</field>
</record>
<record model="survey.label" id="vendor_certification_page_2_question_2_choice_4">
<field name="question_id" ref="vendor_certification_page_2_question_2"/>
<field name="sequence">4</field>
<field name="value">Large Desk</field>
<field name="answer_score">1.0</field>
<field name="is_correct" eval="True" />
</record>
<record model="survey.label" id="vendor_certification_page_2_question_2_choice_5">
<field name="question_id" ref="vendor_certification_page_2_question_2"/>
<field name="sequence">5</field>
<field name="value">Letter Tray</field>
<field name="answer_score">-1.0</field>
</record>
<record model="survey.label" id="vendor_certification_page_2_question_2_choice_5">
<field name="question_id" ref="vendor_certification_page_2_question_2"/>
<field name="sequence">6</field>
<field name="value">Office Chair Black</field>
<field name="answer_score">-1.0</field>
</record>
<!-- Question and predefined answer 8 -->
<record model="survey.question" id="vendor_certification_page_2_question_3">
<field name="survey_id" ref="vendor_certification" />
<field name="sequence">10</field>
<field name="title">What do you think about our prices (not rated)?</field>
<field name="question_type">simple_choice</field>
<field name="display_mode">dropdown</field>
<field name="constr_mandatory" eval="True" />
</record>
<record model="survey.label" id="vendor_certification_page_2_question_3_choice_1">
<field name="question_id" ref="vendor_certification_page_2_question_3"/>
<field name="sequence">1</field>
<field name="value">Very underpriced</field>
</record>
<record model="survey.label" id="vendor_certification_page_2_question_3_choice_2">
<field name="question_id" ref="vendor_certification_page_2_question_3"/>
<field name="sequence">2</field>
<field name="value">Underpriced</field>
</record>
<record model="survey.label" id="vendor_certification_page_2_question_3_choice_3">
<field name="question_id" ref="vendor_certification_page_2_question_3"/>
<field name="sequence">3</field>
<field name="value">Correctly priced</field>
</record>
<record model="survey.label" id="vendor_certification_page_2_question_3_choice_4">
<field name="question_id" ref="vendor_certification_page_2_question_3"/>
<field name="sequence">4</field>
<field name="value">A little bit overpriced</field>
</record>
<record model="survey.label" id="vendor_certification_page_2_question_3_choice_5">
<field name="question_id" ref="vendor_certification_page_2_question_3"/>
<field name="sequence">5</field>
<field name="value">A lot overpriced</field>
</record>
</data>
</odoo>
+9 -1
View File
@@ -54,6 +54,7 @@ class SurveyQuestion(models.Model):
survey_id = fields.Many2one('survey.survey', string='Survey', ondelete='cascade')
page_id = fields.Many2one('survey.question', string='Page', compute="_compute_page_id", store=True)
question_ids = fields.One2many('survey.question', string='Questions', compute="_compute_question_ids")
scoring_type = fields.Selection(related='survey_id.scoring_type', string='Scoring Type', readonly=True)
sequence = fields.Integer('Sequence', default=10)
# Question
is_page = fields.Boolean('Is a page?')
@@ -350,6 +351,11 @@ class SurveyQuestion(models.Model):
self.ensure_one()
return list(self.survey_id.question_and_page_ids).index(self)
def get_correct_answer_ids(self):
self.ensure_one()
return self.labels_ids.filtered(lambda label: label.is_correct)
class SurveyLabel(models.Model):
""" A suggested answer for a question """
_name = 'survey.label'
@@ -361,7 +367,9 @@ class SurveyLabel(models.Model):
question_id_2 = fields.Many2one('survey.question', string='Question 2', ondelete='cascade')
sequence = fields.Integer('Label Sequence order', default=10)
value = fields.Char('Suggested value', translate=True, required=True)
quizz_mark = fields.Float('Score for this choice', help="A positive score indicates a correct choice; a negative or null score indicates a wrong answer")
is_correct = fields.Boolean('Is a correct answer')
answer_score = fields.Float('Score for this choice',
help="A positive score indicates a correct choice; a negative or null score indicates a wrong answer")
@api.one
@api.constrains('question_id', 'question_id_2')
+93 -11
View File
@@ -31,7 +31,6 @@ class Survey(models.Model):
description = fields.Html("Description", translate=True, help="A long description of the purpose of the survey")
color = fields.Integer('Color Index', default=0)
thank_you_message = fields.Html("Thanks Message", translate=True, help="This message will be displayed when survey is completed")
quizz_mode = fields.Boolean("Quizz Mode")
active = fields.Boolean("Active", default=True)
question_and_page_ids = fields.One2many('survey.question', 'survey_id', string='Pages and Questions', copy=True)
page_ids = fields.One2many('survey.question', string='Pages', compute="_compute_page_and_question_ids")
@@ -60,8 +59,29 @@ class Survey(models.Model):
answer_count = fields.Integer("Started", compute="_compute_survey_statistic")
answer_done_count = fields.Integer("Completed", compute="_compute_survey_statistic")
# scoring and certification fields
scoring_type = fields.Selection([
('no_scoring', 'No scoring'),
('scoring_with_answers', 'Scoring with answers at the end'),
('scoring_without_answers', 'Scoring without answers at the end')],
string="Scoring", required=True, default='no_scoring')
passing_score = fields.Float('Passing score (%)', required=True, default=80.0)
is_attempts_limited = fields.Boolean('Limited number of attempts',
help="Check this option if you want to limit the number of attempts per user")
attempts_limit = fields.Integer('Number of attempts', default=1)
is_time_limited = fields.Boolean('The survey is limited in time')
time_limit = fields.Float("Time limit (minutes)")
certificate = fields.Boolean('Certificate')
certification_mail_template_id = fields.Many2one(
'mail.template', 'Certification Email Template',
domain="[('model', '=', 'survey.user_input')]",
help="Automated email sent to the user when he succeeds the certification, containing his certification document.")
_sql_constraints = [
('access_token_unique', 'unique(access_token)', 'Access token should be unique')
('access_token_unique', 'unique(access_token)', 'Access token should be unique'),
('certificate_check', "CHECK( scoring_type!='no_scoring' OR certificate=False )", 'You can only create certifications for surveys that have a scoring mechanism.'),
('time_limit_check', "CHECK( (is_time_limited=False) OR (time_limit is not null AND time_limit > 0) )", 'The time limit needs to be a positive number if the survey is time limited.'),
('attempts_limit_check', "CHECK( (is_attempts_limited=False) OR (attempts_limit is not null AND attempts_limit > 0) )", 'The attempts limit needs to be a positive number if the survey has a limited number of attempts.')
]
@api.multi
@@ -93,12 +113,45 @@ class Survey(models.Model):
for survey in self:
survey.public_url = urls.url_join(base_url, "survey/start/%s" % (survey.access_token))
@api.multi
@api.depends('question_and_page_ids.labels_ids.answer_score')
def _compute_total_possible_score(self):
for survey in self:
survey.total_possible_score = sum([
answer_score if answer_score > 0 else 0
for answer_score in survey.question_and_page_ids.mapped('labels_ids.answer_score')])
@api.depends('question_and_page_ids')
def _compute_page_and_question_ids(self):
for survey in self:
survey.page_ids = survey.question_and_page_ids.filtered(lambda question: question.is_page)
survey.question_ids = survey.question_and_page_ids - survey.page_ids
@api.onchange('passing_score')
def _onchange_passing_score(self):
if self.passing_score < 0 or self.passing_score > 100:
self.passing_score = 80.0
@api.onchange('scoring_type')
def _onchange_scoring_type(self):
if self.scoring_type != 'no_scoring':
self.certificate = False
@api.onchange('users_login_required', 'access_mode')
def _onchange_access_mode(self):
if self.access_mode == 'public' and not self.users_login_required:
self.is_attempts_limited = False
@api.onchange('attempts_limit')
def _onchange_attempts_limit(self):
if self.attempts_limit <= 0:
self.attempts_limit = 1
@api.onchange('is_time_limited', 'time_limit')
def _onchange_time_limit(self):
if self.is_time_limited and (not self.time_limit or self.time_limit <= 0):
self.time_limit = 10
@api.model
def _read_group_stage_ids(self, stages, domain, order):
""" Read group customization in order to display all the stages in the
@@ -126,7 +179,7 @@ class Survey(models.Model):
self.check_access_rights('read')
self.check_access_rule('read')
tokens = self.env['survey.user_input']
answers = self.env['survey.user_input']
for survey in self:
if partner and not user and partner.user_ids:
user = partner.user_ids[0]
@@ -146,9 +199,9 @@ class Survey(models.Model):
answer_vals['email'] = email
answer_vals.update(additional_vals)
tokens += tokens.create(answer_vals)
answers += answers.create(answer_vals)
return tokens
return answers
@api.multi
def _check_answer_creation(self, user, partner, email, test_entry=False):
@@ -171,6 +224,8 @@ class Survey(models.Model):
raise UserError(_('Creating token for external people is not allowed for surveys requesting authentication.'))
if self.access_mode == 'internal' and (not user or not user.has_group('base.group_user')):
raise UserError(_('Creating token for anybody else than employees is not allowed for internal surveys.'))
if not self._has_attempts_left(partner or (user and user.partner_id), email):
raise UserError(_('No attempts left.'))
@api.model
def next_page(self, user_input, page_id, go_back=False):
@@ -195,23 +250,23 @@ class Survey(models.Model):
# First page
if page_id == 0:
return (pages[0][1], 0, len(pages) == 1)
return (pages[0][1], len(pages) == 1)
current_page_index = pages.index(next(p for p in pages if p[1].id == page_id))
# All the pages have been displayed
if current_page_index == len(pages) - 1 and not go_back:
return (None, -1, False)
return (None, 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)
return (pages[current_page_index - 1][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)
return (pages[current_page_index + 1][1], True)
# This will show a regular page
else:
return (pages[current_page_index + 1][1], current_page_index + 1, False)
return (pages[current_page_index + 1][1], False)
@api.multi
def filter_input_ids(self, filters, finished=False):
@@ -278,7 +333,7 @@ class Survey(models.Model):
# Calculate and return statistics for choice
if question.question_type in ['simple_choice', 'multiple_choice']:
comments = []
answers = OrderedDict((label.id, {'text': label.value, 'count': 0, 'answer_id': label.id}) for label in question.labels_ids)
answers = OrderedDict((label.id, {'text': label.value, 'count': 0, 'answer_id': label.id, 'answer_score': label.answer_score}) for label in question.labels_ids)
for input_line in input_lines:
if input_line.answer_type == 'suggestion' and answers.get(input_line.value_suggested.id) and (not(current_filters) or input_line.user_input_id.id in current_filters):
answers[input_line.value_suggested.id]['count'] += 1
@@ -435,3 +490,30 @@ class Survey(models.Model):
'search_default_invite': 1})
action['context'] = ctx
return action
@api.multi
def _has_attempts_left(self, partner, email):
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) > 0
return True
@api.multi
def _get_number_of_attempts_lefts(self, partner, email):
""" 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)]])
return self.attempts_limit - self.env['survey.user_input'].search_count(domain)
+43 -7
View File
@@ -9,6 +9,8 @@ import uuid
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from dateutil.relativedelta import relativedelta
email_validator = re.compile(r"[^@]+@[^@]+\.[^@]+")
_logger = logging.getLogger(__name__)
@@ -30,6 +32,8 @@ class SurveyUserInput(models.Model):
# description
survey_id = fields.Many2one('survey.survey', string='Survey', required=True, readonly=True, ondelete='cascade')
start_datetime = fields.Datetime('Start date and time', readonly=True)
is_time_limit_reached = fields.Boolean("Is time limit reached?", compute='_compute_is_time_limit_reached')
input_type = fields.Selection([
('manually', 'Manual'), ('link', 'Invitation')],
string='Answer Type', default='manually', required=True, readonly=True,
@@ -49,12 +53,27 @@ class SurveyUserInput(models.Model):
# answers
user_input_line_ids = fields.One2many('survey.user_input_line', 'user_input_id', string='Answers', copy=True)
deadline = fields.Datetime('Deadline', help="Datetime until customer can open the survey and submit answers")
quizz_score = fields.Float("Score for the quiz", compute="_compute_quizz_score", default=0.0)
@api.depends('user_input_line_ids.quizz_mark')
quizz_score = fields.Float("Score for the quiz (%)", compute="_compute_quizz_score", default=0.0)
# Stored for performance reasons while displaying results page
quizz_passed = fields.Boolean('Quizz Passed', compute='_compute_quizz_passed', store=True)
@api.multi
@api.depends('user_input_line_ids.answer_score', 'survey_id.total_possible_score')
def _compute_quizz_score(self):
for user_input in self:
user_input.quizz_score = sum(user_input.user_input_line_ids.mapped('quizz_mark'))
total_possible_score = user_input.survey_id.total_possible_score
if total_possible_score == 0:
user_input.quizz_score = 0
else:
score = (sum(user_input.user_input_line_ids.mapped('answer_score')) / total_possible_score) * 100
user_input.quizz_score = round(score, 2) if score > 0 else 0
@api.multi
@api.depends('quizz_score', 'survey_id.passing_score')
def _compute_quizz_passed(self):
for user_input in self:
user_input.quizz_passed = user_input.quizz_score >= user_input.survey_id.passing_score
_sql_constraints = [
('unique_token', 'UNIQUE (token)', 'A token must be unique!'),
@@ -97,6 +116,23 @@ class SurveyUserInput(models.Model):
'url': '/survey/print/%s?answer_token=%s' % (self.survey_id.access_token, self.token)
}
@api.depends('start_datetime', 'survey_id.is_time_limited', 'survey_id.time_limit')
def _compute_is_time_limit_reached(self):
""" Checks that the user_input is not exceeding the survey's time limit. """
for user_input in self:
user_input.is_time_limit_reached = user_input.survey_id.is_time_limited and fields.Datetime.now() \
> user_input.start_datetime + relativedelta(minutes=user_input.survey_id.time_limit)
@api.multi
def _send_certification(self):
""" Will send the certification email with attached document if
- The survey is a certification
- It has a certification_mail_template_id set
- The user succeeded the test """
for user_input in self:
if user_input.survey_id.certificate and user_input.quizz_passed and user_input.survey_id.certification_mail_template_id:
user_input.survey_id.certification_mail_template_id.send_mail(user_input.id, notif_layout="mail.mail_notification_light")
class SurveyUserInputLine(models.Model):
_name = 'survey.user_input_line'
@@ -122,7 +158,7 @@ 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")
quizz_mark = fields.Float('Score given for this choice')
answer_score = fields.Float('Score given for this choice')
@api.constrains('skipped', 'answer_type')
def _answered_or_skipped(self):
@@ -145,7 +181,7 @@ class SurveyUserInputLine(models.Model):
def _get_mark(self, value_suggested):
label = self.env['survey.label'].browse(int(value_suggested))
mark = label.quizz_mark if label.exists() else 0.0
mark = label.answer_score if label.exists() else 0.0
return mark
@api.model_create_multi
@@ -153,14 +189,14 @@ class SurveyUserInputLine(models.Model):
for vals in vals_list:
value_suggested = vals.get('value_suggested')
if value_suggested:
vals.update({'quizz_mark': self._get_mark(value_suggested)})
vals.update({'answer_score': self._get_mark(value_suggested)})
return super(SurveyUserInputLine, self).create(vals_list)
@api.multi
def write(self, vals):
value_suggested = vals.get('value_suggested')
if value_suggested:
vals.update({'quizz_mark': self._get_mark(value_suggested)})
vals.update({'answer_score': self._get_mark(value_suggested)})
return super(SurveyUserInputLine, self).write(vals)
@api.model
Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

+14 -5
View File
@@ -67,7 +67,7 @@ if(!$('.js_surveyresult').length) {
}
//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(chart, response, question_id, tick_limit, graph_type){
function load_chart(chart, response, svgSelector, tick_limit, graph_type){
// Custom Tick fuction for replacing long text with '...'
var customtick_function = function(d){
if(! this || d.length <= tick_limit){
@@ -83,7 +83,7 @@ if(!$('.js_surveyresult').length) {
chart.yAxis
.tickFormat(d3.format('d'));
}
d3.select('#graph_question_' + question_id + ' svg')
d3.select(svgSelector)
.datum(response)
.transition().duration(500).call(chart);
nv.utils.windowResize(chart.update);
@@ -95,26 +95,35 @@ if(!$('.js_surveyresult').length) {
var question_id = $(graph).attr("data-question_id");
var graph_type = $(graph).attr("data-graph_type");
var graph_data = JSON.parse($(graph).attr("graph-data"));
var svgSelector = '#graph_question_' + question_id + ' svg';
if(graph_type == 'multi_bar'){
nv.addGraph(function(){
var chart = init_multibar_chart();
return load_chart(chart, graph_data, question_id, 25);
return load_chart(chart, graph_data, svgSelector, 25);
});
}
else if(graph_type == 'bar'){
nv.addGraph(function() {
var chart = init_bar_chart();
return load_chart(chart, graph_data, question_id, 35);
return load_chart(chart, graph_data, svgSelector, 35);
});
}
else if(graph_type == 'pie'){
nv.addGraph(function() {
var chart = init_pie_chart();
return load_chart(chart, graph_data, question_id, 25, 'pie');
return load_chart(chart, graph_data, svgSelector, 25, 'pie');
});
}
});
var $scoringResultsChart = $('#scoring_results_chart');
if ($scoringResultsChart.length > 0) {
nv.addGraph(function () {
var chart = init_pie_chart();
return load_chart(chart, $scoringResultsChart.data('graph_data'), '#scoring_results_chart svg', 25, 'pie');
});
}
// Script for filter
$('td.survey_answer').hover(function(){
$(this).find('i.fa-filter').removeClass('invisible');
@@ -0,0 +1,45 @@
odoo.define('survey.timer', function (require) {
'use strict';
require('web.dom_ready');
if (!$('.js_survey_timer').length) {
return $.Deferred().reject("DOM doesn't contain '.js_survey_timer'");
}
var $parent = $('.js_survey_timer');
var timeLimitMinutes = parseInt($parent.data('time_limit_minutes'));
if (timeLimitMinutes <= 0) {
return $.Deferred().reject("Timer is not positive");
}
var countDownDate = moment.utc($parent.data('timer')).add(timeLimitMinutes, 'minutes');
if (countDownDate.diff(moment.utc(), 'seconds') < 0) {
return $.Deferred().reject("Timer is already finished");
}
var formatTime = function (time) {
return time > 9 ? time : '0' + time;
};
var $timer = $parent.find('.timer');
var interval = null;
var updateTimer = function () {
var timeLeft = countDownDate.diff(moment.utc(), 'seconds');
if (timeLeft >= 0) {
var timeLeftMinutes = parseInt(timeLeft / 60);
var timeLeftSeconds = timeLeft - (timeLeftMinutes * 60);
$timer.html(formatTime(timeLeftMinutes) + ':' + formatTime(timeLeftSeconds));
} else {
clearInterval(interval);
$('.js_surveyform').submit();
}
};
updateTimer();
interval = setInterval(updateTimer, 1000);
});
@@ -0,0 +1,17 @@
.certification-wrapper {
background: url(/survey/static/src/img/certification_background.png) no-repeat;
background-size: 100% 100%;
height: 610px;
width: 860px;
.certification {
margin-left: 350px;
padding-top: 140px;
width: 510px;
text-align: center;
h2 {
color: #876B81;
}
}
}
@@ -0,0 +1,8 @@
.o_survey_header {
padding-top: 15px;
padding-bottom: 25px;
}
.js_question-wrapper {
padding-bottom: 25px;
}
@@ -0,0 +1,5 @@
.o_kanban_card_survey {
.certification_icon {
font-size: 20px;
}
}
+9 -3
View File
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
from collections import Counter
from contextlib import contextmanager
from functools import partial
@@ -156,17 +158,17 @@ class SurveyCase(common.SavepointCase):
}
if qtype in ('simple_choice', 'multiple_choice'):
base_qvalues['labels_ids'] = [
(0, 0, {'value': label['value'], 'quizz_mark': label.get('quizz_mark', 0)})
(0, 0, {'value': label['value'], 'answer_score': label.get('answer_score', 0)})
for label in kwargs.pop('labels')
]
elif qtype == 'matrix':
base_qvalues['matrix_subtype'] = kwargs.pop('matrix_subtype', 'simple')
base_qvalues['labels_ids'] = [
(0, 0, {'value': label['value'], 'quizz_mark': label.get('quizz_mark', 0)})
(0, 0, {'value': label['value'], 'answer_score': label.get('answer_score', 0)})
for label in kwargs.pop('labels')
]
base_qvalues['labels_ids_2'] = [
(0, 0, {'value': label['value'], 'quizz_mark': label.get('quizz_mark', 0)})
(0, 0, {'value': label['value'], 'answer_score': label.get('answer_score', 0)})
for label in kwargs.pop('labels_2')
]
else:
@@ -199,3 +201,7 @@ class SurveyCase(common.SavepointCase):
base_alvals[answer_fname] = answer_value
base_alvals.update(kwargs)
return self.env['survey.user_input_line'].create(base_alvals)
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]
@@ -0,0 +1,148 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import patch
from odoo.addons.base.models.ir_mail_server import IrMailServer
from odoo.addons.survey.tests import common
from odoo.tests import tagged
from odoo.tests.common import HttpCase
@tagged('functional')
class TestCertificationFlow(common.SurveyCase, HttpCase):
def _answer_question(self, question, answer, answer_token, csrf_token, button_submit='next'):
# Employee submits the question answer
post_data = self._format_submission_data(question, answer, {'csrf_token': csrf_token, 'token': answer_token, 'button_submit': button_submit})
response = self._access_submit(question.survey_id, answer_token, post_data)
self.assertResponse(response, 200)
# Employee is redirected on next question
response = self._access_page(question.survey_id, answer_token)
self.assertResponse(response, 200)
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(**additional_post_data)
return post_data
def test_flow_certificate(self):
# Step: survey user creates the certification
# --------------------------------------------------
with self.sudo(self.survey_user):
certification = self.env['survey.survey'].create({
'title': 'User Certification for SO lines',
'access_mode': 'public',
'users_login_required': True,
'questions_layout': 'page_per_question',
'users_can_go_back': True,
'scoring_type': 'scoring_with_answers',
'passing_score': 85.0,
'certificate': True,
'certification_mail_template_id': self.env.ref('survey.mail_template_certification').id,
'is_time_limited': True,
'time_limit': 10,
'stage_id': self.env['survey.stage'].search([('closed', '=', False)]).id
})
q01 = self._add_question(
None, 'When do you know it\'s the right time to use the SO line model?', 'simple_choice',
sequence=1,
constr_mandatory=True, constr_error_msg='Please select an answer', survey_id=certification.id,
labels=[
{'value': 'Please stop'},
{'value': 'Only on the SO form'},
{'value': 'Only on the Survey form'},
{'value': 'Easy, all the time!!!', 'is_correct': True, 'answer_score': 2.0}
])
q02 = self._add_question(
None, 'On average, how many lines of code do you need when you use SO line widgets?', 'simple_choice',
sequence=2,
constr_mandatory=True, constr_error_msg='Please select an answer', survey_id=certification.id,
labels=[
{'value': '1'},
{'value': '5', 'is_correct': True, 'answer_score': 2.0},
{'value': '100'},
{'value': '1000'}
])
q03 = self._add_question(
None, 'What do you think about SO line widgets (not rated)?', 'free_text',
sequence=3,
constr_mandatory=True, constr_error_msg='Please tell us what you think', survey_id=certification.id)
q04 = self._add_question(
None, 'On a scale of 1 to 10, how much do you like SO line widgets (not rated)?', 'simple_choice',
sequence=4,
constr_mandatory=True, constr_error_msg='Please tell us what you think', survey_id=certification.id,
labels=[
{'value': '-1'},
{'value': '0'},
{'value': '100'}
])
q05 = self._add_question(
None, 'Select all the correct "types" of SO lines', 'multiple_choice',
sequence=5,
constr_mandatory=False, survey_id=certification.id,
labels=[
{'value': 'sale_order', 'is_correct': True, 'answer_score': 1.0},
{'value': 'survey_page', 'is_correct': True, 'answer_score': 1.0},
{'value': 'survey_question', 'is_correct': True, 'answer_score': 1.0},
{'value': 'a_future_and_yet_unknown_model', 'is_correct': True, 'answer_score': 1.0},
{'value': 'none', 'answer_score': -1.0}
])
# Step: employee takes the certification
# --------------------------------------------------
self.authenticate('user_emp', 'user_emp')
# Employee opens start page
response = self._access_start(certification)
self.assertResponse(response, 200, [certification.title, 'Time limit for this survey', '10 minutes'])
# -> this should have generated a new user_input with a token
user_inputs = self.env['survey.user_input'].search([('survey_id', '=', certification.id)])
self.assertEqual(len(user_inputs), 1)
self.assertEqual(user_inputs.partner_id, self.user_emp.partner_id)
answer_token = user_inputs.token
# Employee begins survey with first page
response = self._access_page(certification, answer_token)
self.assertResponse(response, 200)
csrf_token = self._find_csrf_token(response.text)
with patch.object(IrMailServer, 'connect'):
self._answer_question(q01, q01.labels_ids.ids[3], answer_token, csrf_token)
self._answer_question(q02, q02.labels_ids.ids[1], answer_token, csrf_token)
self._answer_question(q03, "I think they're great!", answer_token, csrf_token)
self._answer_question(q04, q04.labels_ids.ids[0], answer_token, csrf_token, button_submit='previous')
self._answer_question(q03, "Just kidding, I don't like it...", answer_token, csrf_token)
self._answer_question(q04, q04.labels_ids.ids[0], answer_token, csrf_token)
self._answer_question(q05, [q05.labels_ids.ids[0], q05.labels_ids.ids[1], q05.labels_ids.ids[3]], answer_token, csrf_token)
user_inputs.invalidate_cache()
# Check that certification is successfully passed
self.assertEqual(user_inputs.quizz_score, 87.5)
self.assertTrue(user_inputs.quizz_passed)
# Check answer correction is taken into account
self.assertNotIn("I think they're great!", user_inputs.mapped('user_input_line_ids.value_free_text'))
self.assertIn("Just kidding, I don't like it...", user_inputs.mapped('user_input_line_ids.value_free_text'))
certification_email = self.env['mail.mail'].search([], limit=1, order="create_date desc")
# Check certification email correctly sent and contains document
self.assertIn("User Certification for SO lines", certification_email.subject)
self.assertIn("employee@example.com", certification_email.email_to)
self.assertEqual(len(certification_email.attachment_ids), 1)
self.assertEqual(certification_email.attachment_ids[0].name, 'Certification Document.html')
-4
View File
@@ -20,10 +20,6 @@ class TestSurveyFlow(common.SurveyCase, HttpCase):
def _access_submit(self, survey, token, post_data):
return self.url_open('/survey/submit/%s/%s' % (survey.access_token, token), data=post_data)
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 _format_submission_data(self, page, answer_data, additional_post_data):
post_data = {}
post_data['page_id'] = page.id
+13
View File
@@ -11,6 +11,7 @@
<script type="text/javascript" src="/survey/static/src/js/survey_result.js" />
<script type="text/javascript" src="/survey/static/src/js/survey.js" />
<script type="text/javascript" src="/survey/static/src/js/survey_timer.js" />
<script type="text/javascript" src="/survey/static/src/js/tour_test_survey.js"></script>
<t t-call="web._assets_helpers">
@@ -25,10 +26,22 @@
<link href="/survey/static/src/css/survey_result.css" rel="stylesheet" type="text/css"></link>
</template>
<template id="survey_report_assets_pdf" inherit_id="web.report_assets_pdf">
<xpath expr="link[last()]" position="after">
<link rel="stylesheet" type="text/scss" href="/survey/static/src/scss/survey_reports.scss"/>
</xpath>
</template>
<template id="assets_backend" name="survey assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" href="/survey/static/src/css/survey_result.css"/>
</xpath>
</template>
<template id="assets_backend_inherit_survey" inherit_id="web.assets_backend" name="Survey backend assets">
<xpath expr="link[last()]" position="after">
<link rel="stylesheet" type="text/scss" href="/survey/static/src/scss/survey_views.scss"/>
</xpath>
</template>
</data>
</odoo>
@@ -95,12 +95,16 @@
<page string="Answers">
<field name="validation_email" attrs="{'invisible': [('question_type', '!=', 'textbox')]}"/>
<label for="validation_email" attrs="{'invisible': [('question_type', '!=', 'textbox')]}"/>
<field name="page_id" invisible="1" required="0"/>
<field name="survey_id" invisible="1" readonly="1"/>
<field name="scoring_type" invisible="1"/>
<separator />
<field name="labels_ids" string="Type of answers" context="{'default_question_id': active_id}" attrs="{'invisible': [('question_type', 'not in', ['simple_choice', 'multiple_choice', 'matrix'])]}">
<tree editable="bottom">
<field name="sequence" widget="handle"/>
<field name="value" string="Choices"/>
<field name="quizz_mark" groups="base.group_no_one"/>
<field name="is_correct" attrs="{'column_invisible': ['|', ('parent.scoring_type', '=', 'no_scoring'), ('parent.question_type', '=', 'matrix')]}"/>
<field name="answer_score" attrs="{'column_invisible': ['|', ('parent.scoring_type', '=', 'no_scoring'), ('parent.question_type', '=', 'matrix')]}"/>
</tree>
</field>
<separator />
@@ -204,7 +208,7 @@
<field name="question_id"/>
<field name="question_id_2"/>
<field name="value"/>
<field name="quizz_mark" groups="base.group_no_one"/>
<field name="answer_score" groups="base.group_no_one"/>
</tree>
</field>
</record>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<template id="certification_report_view">
<t t-set="data_report_landscape" t-value="True"/>
<t t-set="full_width" t-value="True"/>
<t t-call="web.html_container">
<t t-foreach="docs" t-as="user_input">
<div t-att-data-oe-model="user_input._name" t-att-data-oe-id="user_input.id" class="article certification-wrapper">
<div class="certification">
<h2>
<span t-field="user_input.survey_id.create_uid.company_id.display_name"/>
</h2>
<p>Certifies that</p>
<h2>
<span t-esc="user_input.partner_id.name or user_input.email"/>
</h2>
<p>Successfully achieved</p>
<h3>
<span t-field="user_input.survey_id.display_name"/>
</h3>
<p>
<strong>Date of Certification:</strong>
<span t-field="user_input.create_date"/>
</p>
</div>
</div>
</t>
</t>
</template>
</data>
</odoo>
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- QWeb Reports -->
<report
id="certification_report"
model="survey.user_input"
string="Certifications"
report_type="qweb-pdf"
name="survey.certification_report_view"
file="survey.certification_report_view"
attachment="'certification.pdf'"
print_report_name="'Certification - %s' % (object.survey_id.name)"
/>
</data>
</odoo>
+19 -4
View File
@@ -61,17 +61,27 @@
<field name="question_ids"/>
</tree>
</field>
<group string="Description" name="description" >
<field name="description" nolabel="1"></field>
</group>
</page>
<page string="Options">
<page string="Options">
<group name="options">
<group string="Candidates" name="candidates">
<field name="users_login_required"/>
<field name="is_attempts_limited" attrs="{'invisible': [('access_mode', '=', 'public'), ('users_login_required', '=', False)]}" />
<field name="attempts_limit" attrs="{'invisible': [('is_attempts_limited', '=', False)]}" />
</group>
<group string="Questions" name="questions">
<field name="users_can_go_back" string="Enable back button"/>
<field name="is_time_limited" attrs="{'invisible': [('scoring_type', '=', 'no_scoring')]}" string="Time limit" />
<field name="time_limit" widget="float_time" nolabel="1" attrs="{'invisible': [('is_time_limited', '=', False)]}" /><span attrs="{'invisible': [('is_time_limited', '=', False)]}"> minutes</span>
</group>
<group string="Scoring Informations" name="scoring">
<field name="quizz_mode"/>
<field name="scoring_type" widget="radio" />
<field name="passing_score" attrs="{'invisible': [('scoring_type', '=', 'no_scoring')]}" />
<field name="certificate" attrs="{'invisible': [('scoring_type', '=', 'no_scoring')]}" />
<field name="certification_mail_template_id" attrs="{'invisible': [('certificate', '=', False)]}" />
</group>
</group>
</page>
@@ -107,12 +117,13 @@
<field name="stage_id" />
<field name="title" />
<field name="answer_done_count" />
<field name="certificate" />
<field name="color" />
<field name="access_mode"/>
<field name="activity_ids" />
<field name="activity_state" />
<templates>
<div t-name="kanban-box" t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click">
<div t-name="kanban-box" t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click o_kanban_card_survey">
<div class="o_dropdown_kanban dropdown" t-if="widget.editable">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
@@ -127,7 +138,10 @@
</div>
</div>
<div class="o_kanban_record_top">
<h4 class="o_kanban_record_title mb4"><field name="title" /></h4>
<div class="col-10 p-0">
<h4 class="o_kanban_record_title mb4"><field name="title" /></h4>
</div>
<div class="col-2"><i t-if="record.certificate.raw_value === true" title="Certification" class="certification_icon fa fa-trophy"></i></div>
</div>
<div class="o_kanban_record_bottom">
<ul class="list-unstyled">
@@ -160,6 +174,7 @@
<field name="arch" type="xml">
<search string="Survey">
<field string="Title" name="title"/>
<filter string="Is a certification?" name="certificate" domain="[('certificate','=',True)]"/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<separator/>
<filter string="My Activities" name="activities_my"
+112 -27
View File
@@ -21,15 +21,39 @@
<t t-call="survey.layout">
<div class="wrap">
<div class="container">
<div class="alert alert-danger text-center no_print" t-if="user_input.test_entry">
<span>This is a test survey. Your answer will not be registered in the answers. </span>
</div>
<t t-call="survey.back" />
<t t-if="answer.test_entry" t-call="survey.back" />
<div class="jumbotron mt32">
<h1>Thank you!</h1>
<div t-field="survey.thank_you_message" class="oe_no_empty" />
<div t-if='survey.quizz_mode'>You scored <t t-esc="user_input.quizz_score" /> points.</div>
<div>If you wish, you can <a t-att-href="'/survey/print/%s?answer_token=%s' % (survey.access_token, user_input.token)">review your answers</a>.</div>
<t t-if="survey.scoring_type != 'no_scoring'">
<div>You scored <t t-esc="answer.quizz_score" />%</div>
<t t-if="answer.quizz_passed">
<div>Congratulations, you have passed the test!</div>
</t>
<t t-else="">
<div>Unfortunately, you have failed the test.</div>
<t t-if="survey.is_attempts_limited">
<t t-set="attempts_left" t-value="survey._get_number_of_attempts_lefts(answer.partner_id, answer.email)" />
<t t-if="attempts_left > 0">
<p><span>Number of attemps left</span>: <span t-esc="attempts_left"></span></p>
<p>
<a role="button" class="btn btn-primary btn-lg" t-att-href="'/survey/retry/%s/%s' % (survey.access_token, answer.token)">
Retry
</a>
</p>
</t>
</t>
<t t-else="">
<br/>
<p>
<a role="button" class="btn btn-primary btn-lg" t-att-href="'/survey/retry/%s/%s' % (survey.access_token, answer.token)">
Retry
</a>
</p>
</t>
</t>
</t>
<div t-if="survey.scoring_type != 'scoring_without_answers'">If you wish, you can <a t-att-href="'/survey/print/%s?answer_token=%s' % (survey.access_token, answer.token)">review your answers</a>.</div>
</div>
</div>
</div>
@@ -71,7 +95,7 @@
<t t-call="survey.layout">
<div class="wrap">
<div class="container">
<t t-call="survey.back" />
<t t-if="answer.test_entry" t-call="survey.back" />
<div class="jumbotron mt32">
<h1><span t-field="survey.title"/> survey is void</h1>
<p>This survey has no question defined currently.<br />
@@ -87,8 +111,11 @@
<!-- Back Button to redirect in form view of survey -->
<template id="back" name="Back">
<div groups="website.group_website_publisher" t-ignore="true" class="float-right">
<a role="button" t-attf-href="/web#view_type=form&amp;model=survey.survey&amp;id=#{survey.id}&amp;action=survey.action_survey_form" class="btn btn-secondary">Back to Survey</a>
<div groups="survey.group_survey_manager" t-ignore="true" class="alert alert-info alert-dismissible rounded-0 fade show d-print-none css_editable_mode_hidden">
<div t-ignore="true" class="text-center">
<a t-attf-href="/web#view_type=form&amp;model=survey.survey&amp;id=#{survey.id}&amp;action=survey.action_survey_form"><i class="fa fa-fw fa-arrow-right"/><span t-if="answer and answer.test_entry">This is a test survey. </span>Edit Survey.</a>
</div>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> &#215; </button>
</div>
</template>
@@ -140,13 +167,16 @@
<div class="wrap">
<div class="oe_structure" id="oe_structure_survey_init_1"/>
<div class="container">
<div class="alert alert-danger text-center no_print" t-if="answer.test_entry">
<span>This is a test survey. Your answer will not be registered in the answers. </span>
</div>
<t t-call="survey.back" />
<t t-if="answer.test_entry" t-call="survey.back" />
<div class='jumbotron mt32'>
<h1 t-field='survey.title' />
<div t-field='survey.description' class="oe_no_empty"/>
<div t-if="survey.is_time_limited">
<p>
<span>Time limit for this survey: </span>
<span class="font-weight-bold text-danger" t-field="survey.time_limit" t-options="{'widget': 'duration', 'unit': 'minute'}"></span>
</p>
</div>
<a role="button" class="btn btn-primary btn-lg" t-att-href="'/survey/fill/%s/%s' % (survey.access_token, answer.token)">
Start Survey
</a>
@@ -163,7 +193,7 @@
<div class="wrap">
<div class="oe_structure" id="oe_structure_survey_survey_1"/>
<div class="container">
<t t-call="survey.back" />
<t t-if="answer.test_entry" t-call="survey.back" />
<t t-call="survey.page" />
</div>
<div class="oe_structure" id="oe_structure_survey_survey_2"/>
@@ -173,11 +203,39 @@
<!-- A page -->
<template id="page" name="Page">
<div class="o_page_header">
<div class="alert alert-danger text-center no_print" t-if="answer.test_entry">
<span>This is a test survey. Your answer will not be registered in the answers. </span>
<div class="o_page_header o_survey_header">
<div class="container">
<div class="row">
<div class="col-lg-8"><h1 t-esc="survey.title"></h1></div>
<div
t-if="survey.is_time_limited"
class="js_survey_timer col-lg-4"
t-att-data-timer="answer.start_datetime.isoformat()"
t-att-data-time_limit_minutes="survey.time_limit">
<h1 class="timer text-right">00:00</h1>
</div>
</div>
</div>
<p class="float-right">Page <span t-raw='page_nr + 1'/> of <span t-raw="len(survey.page_ids)"/></p>
<ol class="breadcrumb mt8 justify-content-end">
<t t-set="can_go_back" t-value="survey.users_can_go_back" />
<t t-foreach='survey.page_ids' t-as='breadcrumb_page'>
<li t-attf-class="breadcrumb-item #{'active' if breadcrumb_page == page else ''}">
<t t-if="page == breadcrumb_page">
<!-- Users can only go back and not forward -->
<!-- As soon as we reach the current page, set "can_go_back" to False -->
<t t-set="can_go_back" t-value="False" />
</t>
<t t-if="can_go_back">
<a t-att-href="'/survey/page/%s/%s/%s' % (survey.access_token, answer.token, breadcrumb_page.id)">
<span t-field='breadcrumb_page.title' />
</a>
</t>
<t t-else="">
<span t-field='breadcrumb_page.title' />
</t>
</li>
</t>
</ol>
<h1 t-field='page.title' />
<div t-field='page.description' class="oe_no_empty"/>
</div>
@@ -210,7 +268,7 @@
</div>
</t>
<div class="text-center mt16 mb16">
<button t-if="survey.users_can_go_back and page_nr > 0" type="submit" class="btn btn-secondary" name="button_submit" value="previous">Previous page</button>
<button t-if="survey.users_can_go_back and page != survey.page_ids[0]" type="submit" class="btn btn-secondary" name="button_submit" value="previous">Previous page</button>
<button t-if="not last" type="submit" class="btn btn-primary" name="button_submit" value="next">Next page</button>
<button t-if="last" type="submit" class="btn btn-primary" name="button_submit" value="finish">Submit survey</button>
</div>
@@ -295,7 +353,7 @@
</div>
<div t-if="question.display_mode == 'columns' or not answer.token" class="row js_radio">
<div t-foreach='question.labels_ids' t-as='label' t-attf-class="col-lg-#{question.column_nb}">
<label t-att-class="' bg-success ' if quizz_correction and label.quizz_mark > 0.0 else None">
<label t-att-class="' bg-success ' if quizz_correction and label.answer_score > 0.0 else None">
<input type="radio" t-att-name="prefix" t-att-value='label.id' />
<span t-field='label.value'/>
</label>
@@ -317,7 +375,7 @@
<template id="multiple_choice" name="Multiple choice">
<div class="row">
<div t-foreach='question.labels_ids' t-as='label' t-attf-class="col-lg-#{question.column_nb}">
<label t-att-class="' bg-success ' if quizz_correction and label.quizz_mark > 0.0 else None">
<label t-att-class="' bg-success ' if quizz_correction and label.answer_score > 0.0 else None">
<input type="checkbox" t-att-name="'%s_%s' % (prefix, label.id)" t-att-value='label.id' />
<span t-field='label.value'/>
</label>
@@ -365,10 +423,7 @@
<t t-call="survey.layout">
<div class="wrap">
<div class="container">
<div class="alert alert-danger text-center no_print" t-if="answer and answer.test_entry">
<span>This is a test survey. Your answer will not be registered in the answers. </span>
</div>
<t t-call="survey.back" />
<t t-if="answer.test_entry" t-call="survey.back" />
<div class='jumbotron mt32'>
<h1><span t-field='survey.title'/></h1>
<t t-if="survey.description"><div t-field='survey.description' class="oe_no_empty"/></t>
@@ -413,7 +468,8 @@
<div class="container js_surveyresult">
<t t-call="survey.back" />
<div class="jumbotron mt32">
<h1><span t-field="survey.title" /> <span style="font-size:1.5em;" class="fa fa-bar-chart-o float-right " role="img" aria-label="Chart" title="Chart"/></h1>
<h1><span t-field="survey.title" />
<span style="font-size:1.5em;" t-attf-class="fa fa-bar-chart-o #{'fa-bar-chart-o' if survey.scoring_type == 'no_scoring' else 'fa-trophy' if survey.certificate else 'fa-question-circle-o'} float-right " role="img" aria-label="Chart" title="Chart"/></h1>
<div t-field="survey.description" class="oe_no_empty" />
<h2 t-if="not answers">
Sorry, no one answered this survey yet.
@@ -433,6 +489,15 @@
</span>
</div>
</div>
<div t-if="survey.scoring_type in ['scoring_with_answers', 'scoring_without_answers']">
<h1 class="mt16">Results Overview</h1>
<div>Success rate: <mark class="font-weight-bold"><t t-esc="survey_dict['success_rate']"></t>%</mark></div>
<div id="scoring_results_chart" t-att-data-graph_data="survey_dict['scoring_graph_data']">
<!-- svg element for drawing pie chart -->
<svg style="height:20em"></svg>
</div>
<hr/>
</div>
<div t-if="answers" t-foreach="survey_dict['page_ids']" t-as='page_ids'>
<t t-set="page" t-value="page_ids['page']"/>
<h1 class="mt16" t-field='page.title'></h1>
@@ -443,6 +508,7 @@
<t t-set="question" t-value="question_ids['question']"/>
<t t-set="graph_data" t-value="question_ids['graph_data']"/>
<t t-set="prepare_result" t-value="question_ids['prepare_result']"/>
<t t-set="question_with_correct_answers" t-value="survey.scoring_type in ['scoring_with_answers', 'scoring_without_answers'] and question.question_type in ['simple_choice', 'multiple_choice']" />
<h4>
<b>Question </b>
<span t-field='question.question'></span>
@@ -454,9 +520,25 @@
</t>
<span class="float-right">
<span class="badge badge-success"><span t-esc="input_summary['answered']"></span> Answered</span>
<span class="badge badge-danger"><span t-esc="input_summary['skipped']"></span> Skipped</span>
<span class="badge badge-warning"><span t-esc="input_summary['skipped']"></span> Skipped</span>
</span>
</h4>
<t t-if="question_with_correct_answers">
<t t-set="correct_answers" t-value="question.get_correct_answer_ids()" />
<div t-if="correct_answers">
<t t-if="question.question_type == 'simple_choice'">
<p><span>Correct answer</span>: <span class="font-weight-bold" t-esc="correct_answers[0].value"></span></p>
</t>
<t t-if="question.question_type == 'multiple_choice'">
<span>Correct answers</span>:
<ul>
<li t-foreach="correct_answers" t-as="correct_answer">
<t t-esc="correct_answer.value" />
</li>
</ul>
</t>
</div>
</t>
<t t-if="input_summary['answered'] != 0">
<t t-if="question.description">
<div class="text-muted oe_no_empty" t-field="question.description" />
@@ -575,6 +657,7 @@
<tr>
<th>Answer Choices</th>
<th>User Responses</th>
<th t-if="question_with_correct_answers">Answer Score</th>
</tr>
</thead>
<tbody>
@@ -587,6 +670,8 @@
<span t-esc="user_input['count']" class="badge badge-primary">Vote</span>
<i class="fa fa-filter text-primary invisible survey_filter" t-att-data-question_id="question.id" t-att-data-answer_id="user_input['answer_id']" role="img" aria-label="Filter question" title="Filter question"/>
</td>
<td t-if="question_with_correct_answers" t-esc="user_input['answer_score']">
</td>
</tr>
</tbody>
</table>
+3 -3
View File
@@ -59,7 +59,7 @@
<field name="answer_type"/>
<field name="skipped"/>
<field name="create_date"/>
<field name="quizz_mark" groups="base.group_no_one"/>
<field name="answer_score" groups="base.group_no_one"/>
</tree>
</field>
</sheet>
@@ -160,7 +160,7 @@
<field name="create_date"/>
<field name="answer_type"/>
<field name="skipped" />
<field name="quizz_mark" groups="base.group_no_one"/>
<field name="answer_score" groups="base.group_no_one"/>
</group>
<group>
<field name="value_text" colspan='2' attrs="{'invisible': [('answer_type','!=','text')]}"/>
@@ -185,7 +185,7 @@
<field name="create_date"/>
<field name="answer_type"/>
<field name="skipped"/>
<field name="quizz_mark" groups="base.group_no_one"/>
<field name="answer_score" groups="base.group_no_one"/>
</tree>
</field>
</record>