[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:
@@ -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"/>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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"><p>Test your vendor skills!.</p></field>
|
||||
<field name="thank_you_message"><p></p></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"><p>Test your knowledge of your products!</p></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"><p>Test your knowledge of our prices.</p></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>
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 |
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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&model=survey.survey&id=#{survey.id}&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&model=survey.survey&id=#{survey.id}&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"> × </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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user