Skip to content

Commit 8b3c1b3

Browse files
committed
[IMP] survey: add a "performance per section" chart
This commit adds a new chart at the end of a scored survey. Previously, you could see your "Overall Performance", meaning what was your percentage of correct/partially correct/incorrect/skipped answers for all the survey scored questions. Now, we added a new chart new to it that shows the percentage of correct/partially correct/incorrect/skipped answers but for each section of the survey. This allows the user to see in which section(s) he did the most mistakes. Task-2484885
1 parent 6d42540 commit 8b3c1b3

File tree

5 files changed

+214
-32
lines changed

5 files changed

+214
-32
lines changed

addons/survey/controllers/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def _prepare_survey_finished_values(self, survey, answer, token=False):
194194
if token:
195195
values['token'] = token
196196
if survey.scoring_type != 'no_scoring' and survey.certification:
197-
values['graph_data'] = json.dumps(answer._prepare_statistics()[0])
197+
values['graph_data'] = json.dumps(answer._prepare_statistics()[answer])
198198
return values
199199

200200
# ------------------------------------------------------------

addons/survey/models/survey_user.py

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -332,31 +332,100 @@ def _get_line_comment_values(self, question, comment):
332332
# ------------------------------------------------------------
333333

334334
def _prepare_statistics(self):
335+
""" Prepares survey.user_input's statistics to display various charts on the frontend.
336+
Returns a structure containing answers statistics "by section" and "totals" for every input in self.
337+
338+
e.g returned structure:
339+
{
340+
survey.user_input(1,): {
341+
'by_section': {
342+
'Uncategorized': {
343+
'question_count': 2,
344+
'correct': 2,
345+
'partial': 0,
346+
'incorrect': 0,
347+
'skipped': 0,
348+
},
349+
'Mathematics': {
350+
'question_count': 3,
351+
'correct': 1,
352+
'partial': 1,
353+
'incorrect': 0,
354+
'skipped': 1,
355+
},
356+
'Geography': {
357+
'question_count': 4,
358+
'correct': 2,
359+
'partial': 0,
360+
'incorrect': 2,
361+
'skipped': 0,
362+
}
363+
},
364+
'totals' [{
365+
'text': 'Correct',
366+
'count': 5,
367+
}, {
368+
'text': 'Partially',
369+
'count': 1,
370+
}, {
371+
'text': 'Incorrect',
372+
'count': 2,
373+
}, {
374+
'text': 'Unanswered',
375+
'count': 1,
376+
}]
377+
}
378+
}"""
335379
res = dict((user_input, {
336-
'correct': 0,
337-
'incorrect': 0,
338-
'partial': 0,
339-
'skipped': 0,
380+
'by_section': {}
340381
}) for user_input in self)
341382

342383
scored_questions = self.mapped('predefined_question_ids').filtered(lambda question: question.is_scored_question)
343384

344385
for question in scored_questions:
345386
if question.question_type in ['simple_choice', 'multiple_choice']:
346387
question_correct_suggested_answers = question.suggested_answer_ids.filtered(lambda answer: answer.is_correct)
388+
389+
question_section = question.page_id.title or _('Uncategorized')
347390
for user_input in self:
348391
user_input_lines = user_input.user_input_line_ids.filtered(lambda line: line.question_id == question)
349392
if question.question_type in ['simple_choice', 'multiple_choice']:
350-
res[user_input][self._choice_question_answer_result(user_input_lines, question_correct_suggested_answers)] += 1
393+
answer_result_key = self._choice_question_answer_result(user_input_lines, question_correct_suggested_answers)
351394
else:
352-
res[user_input][self._simple_question_answer_result(user_input_lines)] += 1
353-
354-
return [[
355-
{'text': _("Correct"), 'count': res[user_input]['correct']},
356-
{'text': _("Partially"), 'count': res[user_input]['partial']},
357-
{'text': _("Incorrect"), 'count': res[user_input]['incorrect']},
358-
{'text': _("Unanswered"), 'count': res[user_input]['skipped']}
359-
] for user_input in self]
395+
answer_result_key = self._simple_question_answer_result(user_input_lines)
396+
397+
if question_section not in res[user_input]['by_section']:
398+
res[user_input]['by_section'][question_section] = {
399+
'question_count': 0,
400+
'correct': 0,
401+
'partial': 0,
402+
'incorrect': 0,
403+
'skipped': 0,
404+
}
405+
406+
res[user_input]['by_section'][question_section]['question_count'] += 1
407+
res[user_input]['by_section'][question_section][answer_result_key] += 1
408+
409+
for user_input in self:
410+
correct_count = 0
411+
partial_count = 0
412+
incorrect_count = 0
413+
skipped_count = 0
414+
415+
for section_counts in res[user_input]['by_section'].values():
416+
correct_count += section_counts.get('correct', 0)
417+
partial_count += section_counts.get('partial', 0)
418+
incorrect_count += section_counts.get('incorrect', 0)
419+
skipped_count += section_counts.get('skipped', 0)
420+
421+
res[user_input]['totals'] = [
422+
{'text': _("Correct"), 'count': correct_count},
423+
{'text': _("Partially"), 'count': partial_count},
424+
{'text': _("Incorrect"), 'count': incorrect_count},
425+
{'text': _("Unanswered"), 'count': skipped_count}
426+
]
427+
428+
return res
360429

361430
def _choice_question_answer_result(self, user_input_lines, question_correct_suggested_answers):
362431
correct_user_input_lines = user_input_lines.filtered(lambda line: line.answer_is_correct and not line.skipped).mapped('suggested_answer_id')

addons/survey/static/src/js/survey_result.js

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ publicWidget.registry.SurveyResultChart = publicWidget.Widget.extend({
107107
case 'doughnut':
108108
self.chartConfig = self._getDoughnutChartConfig();
109109
break;
110+
case 'by_section':
111+
self.chartConfig = self._getSectionResultsChartConfig();
112+
break;
110113
}
111114

112115
self._loadChart();
@@ -240,15 +243,15 @@ publicWidget.registry.SurveyResultChart = publicWidget.Widget.extend({
240243
},
241244

242245
_getDoughnutChartConfig: function () {
243-
var scoring_percentage = this.$el.data("scoring_percentage") || 0.0;
244-
var counts = this.graphData.map(function (point) {
246+
var totalsGraphData = this.graphData.totals;
247+
var counts = totalsGraphData.map(function (point) {
245248
return point.count;
246249
});
247250

248251
return {
249252
type: 'doughnut',
250253
data: {
251-
labels: this.graphData.map(function (point) {
254+
labels: totalsGraphData.map(function (point) {
252255
return point.text;
253256
}),
254257
datasets: [{
@@ -257,17 +260,116 @@ publicWidget.registry.SurveyResultChart = publicWidget.Widget.extend({
257260
backgroundColor: counts.map(function (val, index) {
258261
return D3_COLORS[index % 20];
259262
}),
263+
borderColor: 'rgba(0, 0, 0, 0.1)'
260264
}]
261265
},
262266
options: {
263267
title: {
264268
display: true,
265-
text: _.str.sprintf(_t("Overall Performance %.2f%s"), parseFloat(scoring_percentage), '%'),
269+
text: _t("Overall Performance"),
266270
},
267271
}
268272
};
269273
},
270274

275+
/**
276+
* Displays the survey results grouped by section.
277+
* For each section, user can see the percentage of answers
278+
* - Correct
279+
* - Partially correct (multiple choices and not all correct answers ticked)
280+
* - Incorrect
281+
* - Unanswered
282+
*
283+
* e.g:
284+
*
285+
* Mathematics:
286+
* - Correct 75%
287+
* - Incorrect 25%
288+
* - Partially correct 0%
289+
* - Unanswered 0%
290+
*
291+
* Geography:
292+
* - Correct 0%
293+
* - Incorrect 0%
294+
* - Partially correct 50%
295+
* - Unanswered 50%
296+
*
297+
*
298+
* @private
299+
*/
300+
_getSectionResultsChartConfig: function () {
301+
var sectionGraphData = this.graphData.by_section;
302+
303+
var resultKeys = {
304+
'correct': _t('Correct'),
305+
'partial': _t('Partially'),
306+
'incorrect': _t('Incorrect'),
307+
'skipped': _t('Unanswered'),
308+
};
309+
var resultColorIndex = 0;
310+
var datasets = [];
311+
for (var resultKey in resultKeys) {
312+
var data = [];
313+
for (var section in sectionGraphData) {
314+
data.push((sectionGraphData[section][resultKey]) / sectionGraphData[section]['question_count'] * 100);
315+
}
316+
datasets.push({
317+
label: resultKeys[resultKey],
318+
data: data,
319+
backgroundColor: D3_COLORS[resultColorIndex % 20],
320+
});
321+
resultColorIndex++;
322+
}
323+
324+
return {
325+
type: 'bar',
326+
data: {
327+
labels: Object.keys(sectionGraphData),
328+
datasets: datasets
329+
},
330+
options: {
331+
title: {
332+
display: true,
333+
text: _t("Performance by Section"),
334+
},
335+
legend: {
336+
display: true,
337+
},
338+
scales: {
339+
xAxes: [{
340+
ticks: {
341+
callback: this._customTick(20),
342+
},
343+
}],
344+
yAxes: [{
345+
gridLines: {
346+
display: false,
347+
},
348+
ticks: {
349+
precision: 0,
350+
callback: function (label) {
351+
return label + '%';
352+
},
353+
suggestedMin: 0,
354+
suggestedMax: 100,
355+
maxTicksLimit: 5,
356+
stepSize: 25
357+
},
358+
}],
359+
},
360+
tooltips: {
361+
callbacks: {
362+
label: function (tooltipItem, data) {
363+
var datasetLabel = data.datasets[tooltipItem.datasetIndex].label || '';
364+
var roundedValue = Math.round(tooltipItem.yLabel * 100) / 100;
365+
return `${datasetLabel}: ${roundedValue}%`;
366+
}
367+
}
368+
}
369+
},
370+
};
371+
},
372+
271373
/**
272374
* Custom Tick function to replace overflowing text with '...'
273375
*

addons/survey/tests/test_certification_flow.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,22 @@ def test_randomized_certification(self):
190190
# Whatever which question was selected, the correct answer is the first one
191191
self._answer_question(question_ids, question_ids.suggested_answer_ids.ids[0], answer_token, csrf_token)
192192

193-
statistics = user_inputs._prepare_statistics()
194-
self.assertEqual(statistics, [[
193+
statistics = user_inputs._prepare_statistics()[user_inputs]
194+
total_statistics = statistics['totals']
195+
self.assertEqual(total_statistics, [
195196
{'text': 'Correct', 'count': 1},
196197
{'text': 'Partially', 'count': 0},
197198
{'text': 'Incorrect', 'count': 0},
198199
{'text': 'Unanswered', 'count': 0},
199-
]], "With the configured randomization, there should be exactly 1 correctly answered question and none skipped.")
200+
], "With the configured randomization, there should be exactly 1 correctly answered question and none skipped.")
201+
202+
section_statistics = statistics['by_section']
203+
self.assertEqual(section_statistics, {
204+
'Page 1': {
205+
'question_count': 1,
206+
'correct': 1,
207+
'partial': 0,
208+
'incorrect': 0,
209+
'skipped': 0,
210+
}
211+
}, "With the configured randomization, there should be exactly 1 correctly answered question in the 'Page 1' section.")

addons/survey/views/survey_templates.xml

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -271,17 +271,16 @@
271271
</div>
272272
</div>
273273
</div>
274-
<div class="container o_survey_result p-4" t-if="graph_data">
275-
<div class="tab-content">
276-
<div role="tabpanel" class="tab-pane active survey_graph"
277-
t-att-data-scoring_percentage="answer.scoring_percentage"
278-
t-att-id="'survey_graph_question_%d' % answer.id"
279-
t-att-data-question_id="answer.id"
280-
data-graph-type="doughnut"
281-
t-att-data-graph-data="graph_data">
282-
<canvas id="doughnut_chart"></canvas>
283-
<span class="o_overall_performance"></span>
284-
</div>
274+
<div class="o_survey_result p-4 col-12 row" t-if="graph_data">
275+
<div t-if="survey.page_ids" class="survey_graph col-lg-6 d-none d-md-block"
276+
data-graph-type="by_section"
277+
t-att-data-graph-data="graph_data">
278+
<canvas id="by_section_chart"></canvas>
279+
</div>
280+
<div t-attf-class="survey_graph col-lg-6 #{'offset-lg-3' if not survey.page_ids else ''}"
281+
data-graph-type="doughnut"
282+
t-att-data-graph-data="graph_data">
283+
<canvas id="doughnut_chart"></canvas>
285284
</div>
286285
</div>
287286
</div>

0 commit comments

Comments
 (0)