[REF] survey: allow more question type scoring
Previously only the question_types 'simple_choice' and 'multiple_choice' were scoreable and could have "correct" answer(s). In this commit we change this behavior so that question_types 'numerical_box', 'date', and 'datetime' can also have a "correct" answer and an associated answer_score. Task ID: 2061937 Community PR #43014 Upgrade PR odoo/upgrade#835
This commit is contained in:
committed by
Thibault Delavallée
parent
0beebad0ff
commit
fb7b30b2e1
@@ -80,6 +80,15 @@ class SurveyQuestion(models.Model):
|
|||||||
('simple_choice', 'Multiple choice: only one answer'),
|
('simple_choice', 'Multiple choice: only one answer'),
|
||||||
('multiple_choice', 'Multiple choice: multiple answers allowed'),
|
('multiple_choice', 'Multiple choice: multiple answers allowed'),
|
||||||
('matrix', 'Matrix')], string='Question Type')
|
('matrix', 'Matrix')], string='Question Type')
|
||||||
|
is_scored_question = fields.Boolean(
|
||||||
|
'Scored', compute='_compute_is_scored_question',
|
||||||
|
readonly=False, store=True, copy=True,
|
||||||
|
help="Include this question as part of quiz scoring. Requires an answer and answer score to be taken into account.")
|
||||||
|
# -- scoreable/answerable simple answer_types: numerical_box / date / datetime
|
||||||
|
answer_numerical_box = fields.Float('Correct numerical answer', help="Correct number answer for this question.")
|
||||||
|
answer_date = fields.Date('Correct date answer', help="Correct date answer for this question.")
|
||||||
|
answer_datetime = fields.Datetime('Correct datetime answer', help="Correct date and time answer for this question.")
|
||||||
|
answer_score = fields.Float('Score', help="Score value for a correct answer to this question.")
|
||||||
# -- char_box
|
# -- char_box
|
||||||
save_as_email = fields.Boolean(
|
save_as_email = fields.Boolean(
|
||||||
"Save as user email", compute='_compute_save_as_email', readonly=False, store=True, copy=True,
|
"Save as user email", compute='_compute_save_as_email', readonly=False, store=True, copy=True,
|
||||||
@@ -153,7 +162,12 @@ class SurveyQuestion(models.Model):
|
|||||||
('validation_length', 'CHECK (validation_length_min <= validation_length_max)', 'Max length cannot be smaller than min length!'),
|
('validation_length', 'CHECK (validation_length_min <= validation_length_max)', 'Max length cannot be smaller than min length!'),
|
||||||
('validation_float', 'CHECK (validation_min_float_value <= validation_max_float_value)', 'Max value cannot be smaller than min value!'),
|
('validation_float', 'CHECK (validation_min_float_value <= validation_max_float_value)', 'Max value cannot be smaller than min value!'),
|
||||||
('validation_date', 'CHECK (validation_min_date <= validation_max_date)', 'Max date cannot be smaller than min date!'),
|
('validation_date', 'CHECK (validation_min_date <= validation_max_date)', 'Max date cannot be smaller than min date!'),
|
||||||
('validation_datetime', 'CHECK (validation_min_datetime <= validation_max_datetime)','Max datetime cannot be smaller than min datetime!')
|
('validation_datetime', 'CHECK (validation_min_datetime <= validation_max_datetime)', 'Max datetime cannot be smaller than min datetime!'),
|
||||||
|
('positive_answer_score', 'CHECK (answer_score >= 0)', 'An answer score for a non-multiple choice question cannot be negative!'),
|
||||||
|
('scored_datetime_have_answers', "CHECK (is_scored_question != True OR question_type != 'datetime' OR answer_datetime is not null)",
|
||||||
|
'All "Is a scored question = True" and "Question Type: Datetime" questions need an answer'),
|
||||||
|
('scored_date_have_answers', "CHECK (is_scored_question != True OR question_type != 'date' OR answer_date is not null)",
|
||||||
|
'All "Is a scored question = True" and "Question Type: Date" questions need an answer')
|
||||||
]
|
]
|
||||||
|
|
||||||
@api.onchange('validation_email')
|
@api.onchange('validation_email')
|
||||||
@@ -229,7 +243,34 @@ class SurveyQuestion(models.Model):
|
|||||||
or question.triggering_answer_id is None:
|
or question.triggering_answer_id is None:
|
||||||
question.triggering_answer_id = False
|
question.triggering_answer_id = False
|
||||||
|
|
||||||
# Validation methods
|
@api.depends('question_type', 'scoring_type', 'answer_date', 'answer_datetime', 'answer_numerical_box')
|
||||||
|
def _compute_is_scored_question(self):
|
||||||
|
""" Computes whether a question "is scored" or not. Handles following cases:
|
||||||
|
- inconsistent Boolean=None edge case that breaks tests => False
|
||||||
|
- survey is not scored => False
|
||||||
|
- 'date'/'datetime'/'numerical_box' question types w/correct answer => True
|
||||||
|
(implied without user having to activate, except for numerical whose correct value is 0.0)
|
||||||
|
- 'simple_choice / multiple_choice': set to True even if logic is a bit different (coming from answers)
|
||||||
|
- question_type isn't scoreable (note: choice questions scoring logic handled separately) => False
|
||||||
|
"""
|
||||||
|
for question in self:
|
||||||
|
if question.is_scored_question is None or question.scoring_type == 'no_scoring':
|
||||||
|
question.is_scored_question = False
|
||||||
|
elif question.question_type == 'date':
|
||||||
|
question.is_scored_question = bool(question.answer_date)
|
||||||
|
elif question.question_type == 'datetime':
|
||||||
|
question.is_scored_question = bool(question.answer_datetime)
|
||||||
|
elif question.question_type == 'numerical_box' and question.answer_numerical_box:
|
||||||
|
question.is_scored_question = True
|
||||||
|
elif question.question_type in ['simple_choice', 'multiple_choice']:
|
||||||
|
question.is_scored_question = True
|
||||||
|
else:
|
||||||
|
question.is_scored_question = False
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# VALIDATION
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
def validate_question(self, answer, comment=None):
|
def validate_question(self, answer, comment=None):
|
||||||
""" Validate question, depending on question type and parameters
|
""" Validate question, depending on question type and parameters
|
||||||
for simple choice, text, date and number, answer is simply the answer of the question.
|
for simple choice, text, date and number, answer is simply the answer of the question.
|
||||||
@@ -379,12 +420,12 @@ class SurveyQuestion(models.Model):
|
|||||||
return all_questions_data
|
return all_questions_data
|
||||||
|
|
||||||
def _get_stats_data(self, user_input_lines):
|
def _get_stats_data(self, user_input_lines):
|
||||||
if self.question_type in ['simple_choice']:
|
if self.question_type == 'simple_choice':
|
||||||
return self._get_stats_data_answers(user_input_lines)
|
return self._get_stats_data_answers(user_input_lines)
|
||||||
elif self.question_type in ['multiple_choice']:
|
elif self.question_type == 'multiple_choice':
|
||||||
table_data, graph_data = self._get_stats_data_answers(user_input_lines)
|
table_data, graph_data = self._get_stats_data_answers(user_input_lines)
|
||||||
return table_data, [{'key': self.title, 'values': graph_data}]
|
return table_data, [{'key': self.title, 'values': graph_data}]
|
||||||
elif self.question_type in ['matrix']:
|
elif self.question_type == 'matrix':
|
||||||
return self._get_stats_graph_data_matrix(user_input_lines)
|
return self._get_stats_graph_data_matrix(user_input_lines)
|
||||||
return [line for line in user_input_lines], []
|
return [line for line in user_input_lines], []
|
||||||
|
|
||||||
@@ -445,11 +486,13 @@ class SurveyQuestion(models.Model):
|
|||||||
return table_data, graph_data
|
return table_data, graph_data
|
||||||
|
|
||||||
def _get_stats_summary_data(self, user_input_lines):
|
def _get_stats_summary_data(self, user_input_lines):
|
||||||
|
stats = {}
|
||||||
if self.question_type in ['simple_choice', 'multiple_choice']:
|
if self.question_type in ['simple_choice', 'multiple_choice']:
|
||||||
return self._get_stats_summary_data_choice(user_input_lines)
|
stats.update(self._get_stats_summary_data_choice(user_input_lines))
|
||||||
if self.question_type in ['numerical_box']:
|
elif self.question_type == 'numerical_box':
|
||||||
return self._get_stats_summary_data_numerical(user_input_lines)
|
stats.update(self._get_stats_summary_data_numerical(user_input_lines))
|
||||||
return {}
|
|
||||||
|
return stats
|
||||||
|
|
||||||
def _get_stats_summary_data_choice(self, user_input_lines):
|
def _get_stats_summary_data_choice(self, user_input_lines):
|
||||||
right_inputs, partial_inputs = self.env['survey.user_input'], self.env['survey.user_input']
|
right_inputs, partial_inputs = self.env['survey.user_input'], self.env['survey.user_input']
|
||||||
@@ -465,8 +508,8 @@ class SurveyQuestion(models.Model):
|
|||||||
right_inputs = user_input_lines.filtered(lambda line: line.answer_is_correct).mapped('user_input_id')
|
right_inputs = user_input_lines.filtered(lambda line: line.answer_is_correct).mapped('user_input_id')
|
||||||
return {
|
return {
|
||||||
'right_answers': right_answers,
|
'right_answers': right_answers,
|
||||||
'right_inputs': right_inputs,
|
'right_inputs_count': len(right_inputs),
|
||||||
'partial_inputs': partial_inputs,
|
'partial_inputs_count': len(partial_inputs),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_stats_summary_data_numerical(self, user_input_lines):
|
def _get_stats_summary_data_numerical(self, user_input_lines):
|
||||||
|
|||||||
@@ -55,13 +55,16 @@ class SurveyUserInput(models.Model):
|
|||||||
('unique_token', 'UNIQUE (access_token)', 'An access token must be unique!'),
|
('unique_token', 'UNIQUE (access_token)', 'An access token must be unique!'),
|
||||||
]
|
]
|
||||||
|
|
||||||
@api.depends('user_input_line_ids.answer_score', 'user_input_line_ids.question_id', 'predefined_question_ids')
|
@api.depends('user_input_line_ids.answer_score', 'user_input_line_ids.question_id', 'predefined_question_ids.answer_score')
|
||||||
def _compute_scoring_values(self):
|
def _compute_scoring_values(self):
|
||||||
for user_input in self:
|
for user_input in self:
|
||||||
total_possible_score = sum([
|
# sum(multi-choice question scores) + sum(simple answer_type scores)
|
||||||
answer_score if answer_score > 0 else 0
|
total_possible_score = 0
|
||||||
for answer_score in user_input.predefined_question_ids.mapped('suggested_answer_ids.answer_score')
|
for question in user_input.predefined_question_ids:
|
||||||
])
|
if question.question_type in ['simple_choice', 'multiple_choice']:
|
||||||
|
total_possible_score += sum(score for score in question.mapped('suggested_answer_ids.answer_score') if score > 0)
|
||||||
|
elif question.is_scored_question:
|
||||||
|
total_possible_score += question.answer_score
|
||||||
|
|
||||||
if total_possible_score == 0:
|
if total_possible_score == 0:
|
||||||
user_input.scoring_percentage = 0
|
user_input.scoring_percentage = 0
|
||||||
@@ -335,25 +338,17 @@ class SurveyUserInput(models.Model):
|
|||||||
'skipped': 0,
|
'skipped': 0,
|
||||||
}) for user_input in self)
|
}) for user_input in self)
|
||||||
|
|
||||||
scored_questions = self.mapped('predefined_question_ids').filtered(
|
scored_questions = self.mapped('predefined_question_ids').filtered(lambda question: question.is_scored_question)
|
||||||
lambda question: question.question_type in ['simple_choice', 'multiple_choice']
|
|
||||||
)
|
|
||||||
|
|
||||||
for question in scored_questions:
|
for question in scored_questions:
|
||||||
question_answer_correct = question.suggested_answer_ids.filtered(lambda answer: answer.is_correct)
|
if question.question_type in ['simple_choice', 'multiple_choice']:
|
||||||
|
question_correct_suggested_answers = question.suggested_answer_ids.filtered(lambda answer: answer.is_correct)
|
||||||
for user_input in self:
|
for user_input in self:
|
||||||
user_answer_lines_question = user_input.user_input_line_ids.filtered(lambda line: line.question_id == question)
|
user_input_lines = user_input.user_input_line_ids.filtered(lambda line: line.question_id == question)
|
||||||
user_answer_correct = user_answer_lines_question.filtered(lambda line: line.answer_is_correct and not line.skipped).mapped('suggested_answer_id')
|
if question.question_type in ['simple_choice', 'multiple_choice']:
|
||||||
user_answer_incorrect = user_answer_lines_question.filtered(lambda line: not line.answer_is_correct and not line.skipped)
|
res[user_input][self._choice_question_answer_result(user_input_lines, question_correct_suggested_answers)] += 1
|
||||||
|
else:
|
||||||
if user_answer_correct == question_answer_correct:
|
res[user_input][self._simple_question_answer_result(user_input_lines)] += 1
|
||||||
res[user_input]['correct'] += 1
|
|
||||||
elif user_answer_correct and user_answer_correct < question_answer_correct:
|
|
||||||
res[user_input]['partial'] += 1
|
|
||||||
if not user_answer_correct and user_answer_incorrect:
|
|
||||||
res[user_input]['incorrect'] += 1
|
|
||||||
if not user_answer_correct and not user_answer_incorrect:
|
|
||||||
res[user_input]['skipped'] += 1
|
|
||||||
|
|
||||||
return [[
|
return [[
|
||||||
{'text': _("Correct"), 'count': res[user_input]['correct']},
|
{'text': _("Correct"), 'count': res[user_input]['correct']},
|
||||||
@@ -362,6 +357,26 @@ class SurveyUserInput(models.Model):
|
|||||||
{'text': _("Unanswered"), 'count': res[user_input]['skipped']}
|
{'text': _("Unanswered"), 'count': res[user_input]['skipped']}
|
||||||
] for user_input in self]
|
] for user_input in self]
|
||||||
|
|
||||||
|
def _choice_question_answer_result(self, user_input_lines, question_correct_suggested_answers):
|
||||||
|
correct_user_input_lines = user_input_lines.filtered(lambda line: line.answer_is_correct and not line.skipped).mapped('suggested_answer_id')
|
||||||
|
incorrect_user_input_lines = user_input_lines.filtered(lambda line: not line.answer_is_correct and not line.skipped)
|
||||||
|
if correct_user_input_lines == question_correct_suggested_answers:
|
||||||
|
return 'correct'
|
||||||
|
elif correct_user_input_lines and correct_user_input_lines < question_correct_suggested_answers:
|
||||||
|
return 'partial'
|
||||||
|
elif not correct_user_input_lines and incorrect_user_input_lines:
|
||||||
|
return 'incorrect'
|
||||||
|
else:
|
||||||
|
return 'skipped'
|
||||||
|
|
||||||
|
def _simple_question_answer_result(self, user_input_line):
|
||||||
|
if user_input_line.skipped:
|
||||||
|
return 'skipped'
|
||||||
|
elif user_input_line.answer_is_correct:
|
||||||
|
return 'correct'
|
||||||
|
else:
|
||||||
|
return 'incorrect'
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# Conditional Questions Management
|
# Conditional Questions Management
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
@@ -477,15 +492,7 @@ class SurveyUserInputLine(models.Model):
|
|||||||
matrix_row_id = fields.Many2one('survey.question.answer', string="Row answer")
|
matrix_row_id = fields.Many2one('survey.question.answer', string="Row answer")
|
||||||
# scoring
|
# scoring
|
||||||
answer_score = fields.Float('Score')
|
answer_score = fields.Float('Score')
|
||||||
answer_is_correct = fields.Boolean('Correct', compute='_compute_answer_is_correct')
|
answer_is_correct = fields.Boolean('Correct')
|
||||||
|
|
||||||
@api.depends('suggested_answer_id', 'question_id')
|
|
||||||
def _compute_answer_is_correct(self):
|
|
||||||
for answer in self:
|
|
||||||
if answer.suggested_answer_id and answer.question_id.question_type in ['simple_choice', 'multiple_choice']:
|
|
||||||
answer.answer_is_correct = answer.suggested_answer_id.is_correct
|
|
||||||
else:
|
|
||||||
answer.answer_is_correct = False
|
|
||||||
|
|
||||||
@api.constrains('skipped', 'answer_type')
|
@api.constrains('skipped', 'answer_type')
|
||||||
def _check_answer_type_skipped(self):
|
def _check_answer_type_skipped(self):
|
||||||
@@ -509,53 +516,90 @@ class SurveyUserInputLine(models.Model):
|
|||||||
@api.model_create_multi
|
@api.model_create_multi
|
||||||
def create(self, vals_list):
|
def create(self, vals_list):
|
||||||
for vals in vals_list:
|
for vals in vals_list:
|
||||||
score = self._get_answer_score(vals.get('user_input_id'), vals.get('suggested_answer_id'))
|
score_vals = self._get_answer_score_values(vals)
|
||||||
if score and not vals.get('answer_score'):
|
if not vals.get('answer_score'):
|
||||||
vals.update({'answer_score': score})
|
vals.update(score_vals)
|
||||||
return super(SurveyUserInputLine, self).create(vals_list)
|
return super(SurveyUserInputLine, self).create(vals_list)
|
||||||
|
|
||||||
def write(self, vals):
|
def write(self, vals):
|
||||||
score = self._get_answer_score(
|
score_vals = self._get_answer_score_values(vals, compute_speed_score=False)
|
||||||
vals.get('user_input_id'),
|
if not vals.get('answer_score'):
|
||||||
vals.get('suggested_answer_id'),
|
vals.update(score_vals)
|
||||||
compute_speed_score=False)
|
|
||||||
if score and not vals.get('answer_score'):
|
|
||||||
vals.update({'answer_score': score})
|
|
||||||
return super(SurveyUserInputLine, self).write(vals)
|
return super(SurveyUserInputLine, self).write(vals)
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _get_answer_score(self, user_input_id, suggested_answer_id, compute_speed_score=True):
|
def _get_answer_score_values(self, vals, compute_speed_score=True):
|
||||||
""" If score depends on the speed of the answer, we need to compute it.
|
""" Get values for: answer_is_correct and associated answer_score.
|
||||||
If the user answers in less than 2 seconds, he gets 100% of the points.
|
|
||||||
If he answers after that, he gets minimum 50% of the points.
|
|
||||||
The 50 other % are ponderated between the time limit and the time it took him to answer.
|
|
||||||
|
|
||||||
If the score does not depend on the speed, we just return the answer_score field of the
|
Requires vals to contain 'answer_type', 'question_id', and 'user_input_id'.
|
||||||
suggested survey.question.answer. """
|
Depending on 'answer_type' additional value of 'suggested_answer_id' may also be
|
||||||
|
required.
|
||||||
|
|
||||||
if not suggested_answer_id:
|
Calculates whether an answer_is_correct and its score based on 'answer_type' and
|
||||||
return None
|
corresponding question. Handles choice (answer_type == 'suggestion') questions
|
||||||
|
separately from other question types. Each selected choice answer is handled as an
|
||||||
|
individual answer.
|
||||||
|
|
||||||
answer = self.env['survey.question.answer'].search([('id', '=', suggested_answer_id)], limit=1)
|
If score depends on the speed of the answer, it is adjusted as follows:
|
||||||
answer_score = answer.answer_score
|
- If the user answers in less than 2 seconds, they receive 100% of the possible points.
|
||||||
|
- If user answers after that, they receive 50% of the possible points + the remaining
|
||||||
|
50% scaled by the time limit and time taken to answer [i.e. a minimum of 50% of the
|
||||||
|
possible points is given to all correct answers]
|
||||||
|
|
||||||
if compute_speed_score:
|
Example of returned values:
|
||||||
|
* {'answer_is_correct': False, 'answer_score': 0} (default)
|
||||||
|
* {'answer_is_correct': True, 'answer_score': 2.0}
|
||||||
|
"""
|
||||||
|
user_input_id = vals.get('user_input_id')
|
||||||
|
answer_type = vals.get('answer_type')
|
||||||
|
question_id = vals.get('question_id')
|
||||||
|
if not question_id:
|
||||||
|
raise ValueError(_('Computing score requires a question in arguments.'))
|
||||||
|
question = self.env['survey.question'].browse(int(question_id))
|
||||||
|
|
||||||
|
# default and non-scored questions
|
||||||
|
answer_is_correct = False
|
||||||
|
answer_score = 0
|
||||||
|
|
||||||
|
# record selected suggested choice answer_score (can be: pos, neg, or 0)
|
||||||
|
if question.question_type in ['simple_choice', 'multiple_choice']:
|
||||||
|
if answer_type == 'suggestion':
|
||||||
|
suggested_answer_id = vals.get('suggested_answer_id')
|
||||||
|
if suggested_answer_id:
|
||||||
|
question_answer = self.env['survey.question.answer'].browse(int(suggested_answer_id))
|
||||||
|
answer_score = question_answer.answer_score
|
||||||
|
answer_is_correct = question_answer.is_correct
|
||||||
|
# for all other scored question cases, record question answer_score (can be: pos or 0)
|
||||||
|
elif question.is_scored_question:
|
||||||
|
answer = vals.get('value_%s' % answer_type)
|
||||||
|
if answer_type == 'numerical_box':
|
||||||
|
answer = float(answer)
|
||||||
|
elif answer_type == 'date':
|
||||||
|
answer = fields.Date.from_string(answer)
|
||||||
|
elif answer_type == 'datetime':
|
||||||
|
answer = fields.Datetime.from_string(answer)
|
||||||
|
if answer and answer == question['answer_%s' % answer_type]:
|
||||||
|
answer_is_correct = True
|
||||||
|
answer_score = question.answer_score
|
||||||
|
|
||||||
|
if compute_speed_score and answer_score > 0:
|
||||||
user_input = self.env['survey.user_input'].browse(user_input_id)
|
user_input = self.env['survey.user_input'].browse(user_input_id)
|
||||||
session_speed_rating = user_input.exists() and user_input.is_session_answer and user_input.survey_id.session_speed_rating
|
session_speed_rating = user_input.exists() and user_input.is_session_answer and user_input.survey_id.session_speed_rating
|
||||||
if session_speed_rating and answer.answer_score and answer.answer_score > 0:
|
if session_speed_rating:
|
||||||
max_score_delay = 2
|
max_score_delay = 2
|
||||||
time_limit = answer.question_id.time_limit
|
time_limit = question.time_limit
|
||||||
now = fields.Datetime.now()
|
now = fields.Datetime.now()
|
||||||
seconds_to_answer = (now - user_input.survey_id.session_question_start_time).total_seconds()
|
seconds_to_answer = (now - user_input.survey_id.session_question_start_time).total_seconds()
|
||||||
question_remaining_time = time_limit - seconds_to_answer
|
question_remaining_time = time_limit - seconds_to_answer
|
||||||
if seconds_to_answer < max_score_delay: # if answered within the max_score_delay
|
# if answered within the max_score_delay => leave score as is
|
||||||
answer_score = answer.answer_score
|
if question_remaining_time < 0: # if no time left
|
||||||
elif question_remaining_time < 0: # if no time left
|
answer_score /= 2
|
||||||
answer_score = answer.answer_score / 2
|
elif seconds_to_answer > max_score_delay:
|
||||||
else:
|
|
||||||
time_limit -= max_score_delay # we remove the max_score_delay to have all possible values
|
time_limit -= max_score_delay # we remove the max_score_delay to have all possible values
|
||||||
question_remaining_time -= max_score_delay
|
|
||||||
score_proportion = (time_limit - seconds_to_answer) / time_limit
|
score_proportion = (time_limit - seconds_to_answer) / time_limit
|
||||||
answer_score = (answer.answer_score / 2) * (1 + score_proportion)
|
answer_score = (answer_score / 2) * (1 + score_proportion)
|
||||||
|
|
||||||
return answer_score
|
return {
|
||||||
|
'answer_is_correct': answer_is_correct,
|
||||||
|
'answer_score': answer_score
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,8 +8,10 @@
|
|||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form string="Survey Question" create="false">
|
<form string="Survey Question" create="false">
|
||||||
<field name="is_page" invisible="1"/>
|
<field name="is_page" invisible="1"/>
|
||||||
|
<field name="page_id" invisible="1" required="False"/>
|
||||||
<field name="survey_id" invisible="1"/>
|
<field name="survey_id" invisible="1"/>
|
||||||
<field name="sequence" invisible="1"/>
|
<field name="sequence" invisible="1"/>
|
||||||
|
<field name="scoring_type" invisible="1"/>
|
||||||
<sheet>
|
<sheet>
|
||||||
<div class="oe_title" style="width: 100%;">
|
<div class="oe_title" style="width: 100%;">
|
||||||
<label for="title" string="Section" attrs="{'invisible': [('is_page', '=', False)]}"/>
|
<label for="title" string="Section" attrs="{'invisible': [('is_page', '=', False)]}"/>
|
||||||
@@ -96,63 +98,76 @@
|
|||||||
</group>
|
</group>
|
||||||
<notebook>
|
<notebook>
|
||||||
<page string="Answers" name="answers" attrs="{'invisible': [('is_page', '=', True)]}">
|
<page string="Answers" name="answers" attrs="{'invisible': [('is_page', '=', True)]}">
|
||||||
<field name="validation_email" attrs="{'invisible': [('question_type', '!=', 'char_box')]}"/>
|
<div class="row">
|
||||||
<label for="validation_email" attrs="{'invisible': [('question_type', '!=', 'char_box')]}"/><br/>
|
<div class="col-6"
|
||||||
<field name="save_as_email" attrs="{'invisible': ['|', ('question_type', '!=', 'char_box'), ('validation_email', '=', False)]}"/>
|
attrs="{'invisible': [('question_type', 'not in', ['char_box', 'numerical_box', 'date', 'datetime'])]}">
|
||||||
<label for="save_as_email" attrs="{'invisible': ['|', ('question_type', '!=', 'char_box'), ('validation_email', '=', False)]}"/>
|
<div attrs="{'invisible': [('question_type', '!=', 'char_box')]}">
|
||||||
<div attrs="{'invisible': [('question_type', '!=', 'char_box')]}">
|
<field name="validation_email" nolabel="1"/>
|
||||||
<field name="save_as_nickname"/>
|
<label for="validation_email"/><br />
|
||||||
<label for="save_as_nickname"/>
|
|
||||||
|
<field name="save_as_email" nolabel="1" attrs="{'invisible': [('validation_email', '=', False)]}"/>
|
||||||
|
<label for="save_as_email" attrs="{'invisible': [('validation_email', '=', False)]}"/>
|
||||||
|
<br attrs="{'invisible': [('validation_email', '=', False)]}"/>
|
||||||
|
|
||||||
|
<field name="save_as_nickname" nolabel="1"/>
|
||||||
|
<label for="save_as_nickname"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<field name="validation_required" nolabel="1"/>
|
||||||
|
<label for="validation_required"/>
|
||||||
|
<group attrs="{'invisible': [('validation_required', '=', False)]}">
|
||||||
|
<field name="validation_length_min" attrs="{'invisible': [('question_type', '!=', 'char_box')]}"/>
|
||||||
|
<field name="validation_length_max" attrs="{'invisible': [('question_type', '!=', 'char_box')]}"/>
|
||||||
|
<field name="validation_min_float_value" attrs="{'invisible': [('question_type', '!=', 'numerical_box')]}"/>
|
||||||
|
<field name="validation_max_float_value" attrs="{'invisible': [('question_type', '!=', 'numerical_box')]}"/>
|
||||||
|
<field name="validation_min_date" attrs="{'invisible': [('question_type', '!=', 'date')]}"/>
|
||||||
|
<field name="validation_max_date" attrs="{'invisible': [('question_type', '!=', 'date')]}"/>
|
||||||
|
<field name="validation_min_datetime" widget="datetime" attrs="{'invisible': [('question_type', '!=', 'datetime')]}"/>
|
||||||
|
<field name="validation_max_datetime" widget="datetime" attrs="{'invisible': [('question_type', '!=', 'datetime')]}"/>
|
||||||
|
<field name="validation_error_msg"/>
|
||||||
|
</group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6" attrs="{'invisible': [
|
||||||
|
'|', ('scoring_type', '=', 'no_scoring'),
|
||||||
|
('question_type', 'not in', ['numerical_box', 'date', 'datetime'])]
|
||||||
|
}">
|
||||||
|
<group>
|
||||||
|
<field name="answer_numerical_box" string="Correct Answer" attrs="{'invisible': [('question_type', '!=', 'numerical_box')]}"/>
|
||||||
|
<field name="answer_date" string="Correct Answer" attrs="{'invisible': [('question_type', '!=', 'date')]}"/>
|
||||||
|
<field name="answer_datetime" string="Correct Answer" attrs="{'invisible': [('question_type', '!=', 'datetime')]}"/>
|
||||||
|
<field name="is_scored_question"/>
|
||||||
|
<field name="answer_score"/>
|
||||||
|
</group>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<field name="page_id" invisible="1" required="0"/>
|
<group attrs="{'invisible': [('question_type', 'not in', ['simple_choice', 'multiple_choice', 'matrix'])]}">
|
||||||
<field name="survey_id" invisible="1" readonly="1"/>
|
<field name="suggested_answer_ids" string="Answers" context="{'default_question_id': active_id}">
|
||||||
<field name="scoring_type" invisible="1"/>
|
<tree editable="bottom">
|
||||||
<separator />
|
<field name="sequence" widget="handle"/>
|
||||||
<label for="suggested_answer_ids" string="Columns of the Matrix" attrs="{'invisible': [('question_type', '!=', 'matrix')]}" />
|
<field name="value" string="Choices"/>
|
||||||
<field name="suggested_answer_ids" string="Type of answers" context="{'default_question_id': active_id}" attrs="{'invisible': [('question_type', 'not in', ['simple_choice', 'multiple_choice', 'matrix'])]}">
|
<field name="value_image" options="{'accepted_file_extensions': 'image/*'}"
|
||||||
<tree editable="bottom">
|
attrs="{'column_invisible': ['|', ('parent.allow_value_image', '=', False), ('parent.question_type', '=', 'matrix')]}"/>
|
||||||
<field name="sequence" widget="handle"/>
|
<field name="is_correct" attrs="{'column_invisible': ['|', ('parent.scoring_type', '=', 'no_scoring'), ('parent.question_type', '=', 'matrix')]}"/>
|
||||||
<field name="value" string="Choices"/>
|
<field name="answer_score" attrs="{'column_invisible': ['|', ('parent.scoring_type', '=', 'no_scoring'), ('parent.question_type', '=', 'matrix')]}"/>
|
||||||
<field name="value_image" options="{'accepted_file_extensions': 'image/*'}"
|
</tree>
|
||||||
attrs="{'column_invisible': ['|', ('parent.allow_value_image', '=', False), ('parent.question_type', '=', 'matrix')]}"/>
|
</field>
|
||||||
<field name="is_correct" attrs="{'column_invisible': ['|', ('parent.scoring_type', '=', 'no_scoring'), ('parent.question_type', '=', 'matrix')]}"/>
|
</group>
|
||||||
<field name="answer_score" attrs="{'column_invisible': ['|', ('parent.scoring_type', '=', 'no_scoring'), ('parent.question_type', '=', 'matrix')]}"/>
|
<group attrs="{'invisible': [('question_type', '!=', 'matrix')]}">
|
||||||
</tree>
|
<field name="matrix_row_ids" context="{'default_matrix_question_id': active_id}" attrs="{'invisible': [('question_type', '!=', 'matrix')]}">
|
||||||
</field>
|
<tree editable="bottom">
|
||||||
<separator />
|
<field name="sequence" widget="handle"/>
|
||||||
<label for="matrix_row_ids" attrs="{'invisible': [('question_type', '!=', 'matrix')]}" />
|
<field name="value" string="Rows"/>
|
||||||
<field name="matrix_row_ids" context="{'default_matrix_question_id': active_id}" attrs="{'invisible': [('question_type', '!=', 'matrix')]}">
|
</tree>
|
||||||
<tree editable="bottom">
|
</field>
|
||||||
<field name="sequence" widget="handle"/>
|
</group>
|
||||||
<field name="value" string="Rows"/>
|
|
||||||
</tree>
|
|
||||||
</field>
|
|
||||||
</page>
|
</page>
|
||||||
<page string="Description" name="survey_description">
|
<page string="Description" name="survey_description">
|
||||||
<field name="description" widget="html"/>
|
<field name="description" widget="html"/>
|
||||||
</page>
|
</page>
|
||||||
<page string="Options" name="options" attrs="{'invisible': [('is_page', '=', True)]}">
|
<page string="Options" name="options" attrs="{'invisible': [('is_page', '=', True)]}">
|
||||||
<group string="Constraints">
|
<group string="Constraints">
|
||||||
<group colspan="2" col="4">
|
|
||||||
<field name="constr_mandatory" string="Mandatory Answer"/>
|
|
||||||
<field name="constr_error_msg" attrs="{'invisible': [('constr_mandatory', '=', False)]}"/>
|
|
||||||
</group>
|
|
||||||
<div colspan="2" attrs="{'invisible': [('question_type', 'not in', ['char_box', 'numerical_box', 'date', 'datetime'])]}">
|
|
||||||
<group>
|
|
||||||
<field name="validation_required" attrs="{'invisible': [('validation_email', '=', True), ('question_type', '=', 'char_box')]}"/>
|
|
||||||
</group>
|
|
||||||
<group col="4" attrs="{'invisible': [('validation_required', '=', False)]}">
|
|
||||||
<field name="validation_length_min" attrs="{'invisible': [('question_type', '!=', 'char_box')]}"/>
|
|
||||||
<field name="validation_length_max" attrs="{'invisible': [('question_type', '!=', 'char_box')]}"/>
|
|
||||||
<field name="validation_min_float_value" attrs="{'invisible': [('question_type', '!=', 'numerical_box')]}"/>
|
|
||||||
<field name="validation_max_float_value" attrs="{'invisible': [('question_type', '!=', 'numerical_box')]}"/>
|
|
||||||
<field name="validation_min_date" attrs="{'invisible': [('question_type', '!=', 'date')]}"/>
|
|
||||||
<field name="validation_max_date" attrs="{'invisible': [('question_type', '!=', 'date')]}"/>
|
|
||||||
<field name="validation_min_datetime" widget="datetime" attrs="{'invisible': [('question_type', '!=', 'datetime')]}"/>
|
|
||||||
<field name="validation_max_datetime" widget="datetime" attrs="{'invisible': [('question_type', '!=', 'datetime')]}"/>
|
|
||||||
<field name="validation_error_msg" colspan="4"/>
|
|
||||||
</group>
|
|
||||||
</div>
|
|
||||||
<group>
|
<group>
|
||||||
<field name="matrix_subtype" attrs="{'invisible':[('question_type','not in',['matrix'])],'required':[('question_type','=','matrix')]}"/>
|
<field name="matrix_subtype" attrs="{'invisible':[('question_type','not in',['matrix'])],'required':[('question_type','=','matrix')]}"/>
|
||||||
</group>
|
</group>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template id="survey_page_statistics_question" name="Question: result statistics">
|
<template id="survey_page_statistics_question" name="Question: result statistics">
|
||||||
<t t-set="question_scoring" t-value="survey.scoring_type in ['scoring_with_answers', 'scoring_without_answers'] and question.question_type in ['simple_choice', 'multiple_choice']"/>
|
<t t-set="question_scoring" t-value="survey.scoring_type in ['scoring_with_answers', 'scoring_without_answers']"/>
|
||||||
<t t-set="comment_lines" t-value="question_data['comment_line_ids']"/>
|
<t t-set="comment_lines" t-value="question_data['comment_line_ids']"/>
|
||||||
<t t-set="graph_data" t-value="question_data['graph_data']"/>
|
<t t-set="graph_data" t-value="question_data['graph_data']"/>
|
||||||
<t t-set="table_data" t-value="question_data['table_data']"/>
|
<t t-set="table_data" t-value="question_data['table_data']"/>
|
||||||
@@ -75,13 +75,33 @@
|
|||||||
<div class="mr-auto">
|
<div class="mr-auto">
|
||||||
<h5 t-field="question.title" class="mb-1"/>
|
<h5 t-field="question.title" class="mb-1"/>
|
||||||
<div t-if="question_scoring">
|
<div t-if="question_scoring">
|
||||||
<t t-if="len(question_data['right_answers']) >= 1">
|
<t t-if="question.question_type in ['simple_choice', 'multiple_choice']">
|
||||||
<span t-if="len(question_data['right_answers']) == 1">Right answer: </span>
|
<t t-if="len(question_data['right_answers']) >= 1">
|
||||||
<span t-if="len(question_data['right_answers']) > 1">Right answers: </span>
|
<span t-if="len(question_data['right_answers']) == 1">Right answer: </span>
|
||||||
|
<span t-if="len(question_data['right_answers']) > 1">Right answers: </span>
|
||||||
|
<strong>
|
||||||
|
<t t-foreach="question_data['right_answers']" t-as="answer">
|
||||||
|
<span t-if="answer_index != 0">, </span><span t-esc="answer.value"/>
|
||||||
|
</t>
|
||||||
|
</strong>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
<t t-if="question.is_scored_question">
|
||||||
|
<span>Right answer:</span>
|
||||||
<strong>
|
<strong>
|
||||||
<t t-foreach="question_data['right_answers']" t-as="answer">
|
<t t-if="question.question_type == 'numerical_box'">
|
||||||
<span t-if="answer_index != 0">, </span><span t-esc="answer.value"/>
|
<span t-esc="question.answer_numerical_box"/>
|
||||||
</t>
|
</t>
|
||||||
|
<t t-if="question.question_type == 'date'">
|
||||||
|
<span t-field="question.answer_date"/>
|
||||||
|
</t>
|
||||||
|
<t t-if="question.question_type == 'datetime'">
|
||||||
|
<span t-field="question.answer_datetime"/>
|
||||||
|
</t>
|
||||||
|
</strong><br/>
|
||||||
|
<span>Score:</span>
|
||||||
|
<strong>
|
||||||
|
<span t-esc="question.answer_score"/>
|
||||||
</strong>
|
</strong>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,10 +113,15 @@
|
|||||||
</t>
|
</t>
|
||||||
<!-- Scoring info -->
|
<!-- Scoring info -->
|
||||||
<t t-if="question_scoring">
|
<t t-if="question_scoring">
|
||||||
<span class="badge badge-success ml-3"><span t-esc="len(question_data['right_inputs'])"></span> Correct</span>
|
<t t-if="question.question_type in ['simple_choice', 'multiple_choice']">
|
||||||
<span class="badge badge-warning ml-1" t-if="question.question_type == 'multiple_choice'">
|
<span class="badge badge-success ml-3"><span t-esc="question_data['right_inputs_count']"></span> Correct</span>
|
||||||
<span t-esc="len(question_data['partial_inputs'])"></span> Partial
|
<span class="badge badge-warning ml-1" t-if="question.question_type == 'multiple_choice'">
|
||||||
</span>
|
<span t-esc="question_data['partial_inputs_count']"></span> Partial
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
<t t-if="question.is_scored_question">
|
||||||
|
<span class="badge badge-success ml-3"><span t-esc="question_data['right_inputs_count']"></span> Correct</span>
|
||||||
|
</t>
|
||||||
</t>
|
</t>
|
||||||
<!-- Inputs info -->
|
<!-- Inputs info -->
|
||||||
<span class="badge badge-info ml-3"><span t-esc="len(question_data['answer_input_done_ids'])"></span> Answered</span>
|
<span class="badge badge-info ml-3"><span t-esc="len(question_data['answer_input_done_ids'])"></span> Answered</span>
|
||||||
@@ -105,19 +130,18 @@
|
|||||||
|
|
||||||
<!-- Question Description -->
|
<!-- Question Description -->
|
||||||
<div class="ml-3 text-muted" t-field="question.description"/>
|
<div class="ml-3 text-muted" t-field="question.description"/>
|
||||||
|
<t t-if="question.question_type in ['text_box', 'char_box', 'date', 'datetime']">
|
||||||
<t t-if="question.question_type in ['text_box', 'char_box', 'date', 'datetime']">
|
<t t-call="survey.question_result_text"/>
|
||||||
<t t-call="survey.question_result_text"/>
|
</t>
|
||||||
</t>
|
<t t-elif="question.question_type in ['numerical_box']">
|
||||||
<t t-elif="question.question_type in ['numerical_box']">
|
<t t-call="survey.question_result_number"></t>
|
||||||
<t t-call="survey.question_result_number"></t>
|
</t>
|
||||||
</t>
|
<t t-elif="question.question_type in ['simple_choice', 'multiple_choice']">
|
||||||
<t t-elif="question.question_type in ['simple_choice', 'multiple_choice']">
|
<t t-call="survey.question_result_choice"></t>
|
||||||
<t t-call="survey.question_result_choice"></t>
|
</t>
|
||||||
</t>
|
<t t-elif="question.question_type in ['matrix']">
|
||||||
<t t-elif="question.question_type in ['matrix']">
|
<t t-call="survey.question_result_matrix"></t>
|
||||||
<t t-call="survey.question_result_matrix"></t>
|
</t>
|
||||||
</t>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user