[IMP] website_slides: improve quiz error management

This commit slightly improves the quiz error management when building a quiz
directly on the website or when answering to the quiz.

When there is a validation or submission error, instead of displaying a toaster
very far from the user focus, we add the error next to the button he just
clicked.

For the question validation, this requires a small trick in the controller,
where we catch the ValidationError to return the message to the JavaScript.

Task-2678061

closes odoo/odoo#79081

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Aurélien Warnon
2021-11-24 13:16:45 +00:00
parent db27973180
commit 4a329da6a0
6 changed files with 94 additions and 55 deletions
+26 -13
View File
@@ -2,7 +2,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from ast import literal_eval
from collections import defaultdict
import base64
import json
@@ -15,7 +14,7 @@ from odoo.addons.http_routing.models.ir_http import slug, unslug
from odoo.addons.website.controllers.main import QueryURL
from odoo.addons.website.models.ir_http import sitemap_qs2dom
from odoo.addons.website_profile.controllers.main import WebsiteProfile
from odoo.exceptions import AccessError, UserError
from odoo.exceptions import AccessError, ValidationError, UserError
from odoo.http import request
from odoo.osv import expression
@@ -879,6 +878,30 @@ class WebsiteSlides(WebsiteProfile):
:return: rendered question template
"""
new_question_values = {
'sequence': sequence,
'question': question,
'slide_id': slide_id,
'answer_ids': [(0, 0, {
'sequence': answer['sequence'],
'text_value': answer['text_value'],
'is_correct': answer['is_correct'],
'comment': answer['comment']
}) for answer in answer_ids]
}
try:
# Attempt to create the question and validate the fields.
# We want to return the error to have a nice display instead of the default mechanism
# of exception handling that shows sticky toasters.
# (Use a 'new' and not a create to avoid having to rollback anything if an error is
# raised)
slide_question = request.env['slide.question'].new(new_question_values)
slide_question._validate_fields(new_question_values.keys())
except ValidationError as e:
return {'error': e.args[0]}
fetch_res = self._fetch_slide(slide_id)
if fetch_res.get('error'):
return fetch_res
@@ -894,17 +917,7 @@ class WebsiteSlides(WebsiteProfile):
('partner_id', '=', request.env.user.partner_id.id)
]).write({'completed': False})
slide_question = request.env['slide.question'].create({
'sequence': sequence,
'question': question,
'slide_id': slide_id,
'answer_ids': [(0, 0, {
'sequence': answer['sequence'],
'text_value': answer['text_value'],
'is_correct': answer['is_correct'],
'comment': answer['comment']
}) for answer in answer_ids]
})
slide_question = request.env['slide.question'].create(new_question_values)
return request.env.ref('website_slides.lesson_content_quiz_question')._render({
'slide': slide,
'question': slide_question,
@@ -114,21 +114,25 @@
// Private
//--------------------------------------------------------------------------
_alertShow: function (alertCode) {
_showErrorMessage: function (errorCode) {
var message = _t('There was an error validating this quiz.');
if (alertCode === 'slide_quiz_incomplete') {
if (errorCode === 'slide_quiz_incomplete') {
message = _t('All questions must be answered !');
} else if (alertCode === 'slide_quiz_done') {
} else if (errorCode === 'slide_quiz_done') {
message = _t('This quiz is already done. Retaking it is not possible.');
} else if (alertCode === 'public_user') {
} else if (errorCode === 'public_user') {
message = _t('You must be logged to submit the quiz.');
}
this.displayNotification({
type: 'warning',
message: message,
sticky: true
});
this.$('.o_wslides_js_quiz_submit_error')
.removeClass('d-none')
.find('.o_wslides_js_quiz_submit_error_text')
.text(message);
},
_hideErrorMessage: function () {
this.$('.o_wslides_js_quiz_submit_error')
.addClass('d-none');
},
/**
@@ -350,8 +354,10 @@
}
});
if (data.error) {
this._alertShow(data.error);
this._showErrorMessage(data.error);
return;
} else {
this._hideErrorMessage();
}
Object.assign(this.quiz, data);
const {rankProgress, completed, channel_completion: completion} = this.quiz;
@@ -466,6 +472,8 @@
_saveQuizAnswersToSession: function () {
var quizAnswers = this._getQuizAnswers();
if (quizAnswers.length === this.quiz.questions.length) {
this._hideErrorMessage();
return this._rpc({
route: '/slides/slide/quiz/save_to_session',
params: {
@@ -473,7 +481,7 @@
}
});
} else {
this._alertShow('slide_quiz_incomplete');
this._showErrorMessage('slide_quiz_incomplete');
return Promise.reject('The quiz is incomplete');
}
},
@@ -148,34 +148,40 @@ var QuestionFormWidget = publicWidget.Widget.extend({
* @param options
* @private
*/
_createOrUpdateQuestion: function (options) {
var self = this;
_createOrUpdateQuestion: async function (options) {
var $form = this.$('form');
if (this._isValidForm($form)) {
var values = this._serializeForm($form);
this._rpc({
var renderedQuestion = await this._rpc({
route: '/slides/slide/quiz/question_add_or_update',
params: values
}).then(function (renderedQuestion) {
if (options.update) {
self.trigger_up('display_updated_question', {
newQuestionRenderedTemplate: renderedQuestion,
$editedQuestion: self.$editedQuestion,
questionFormWidget: self,
});
} else {
self.trigger_up('display_created_question', {
newQuestionRenderedTemplate: renderedQuestion,
questionFormWidget: self
});
}
});
if (typeof renderedQuestion === 'object' && renderedQuestion.error) {
this.$('.o_wslides_js_quiz_validation_error')
.removeClass('d-none')
.find('.o_wslides_js_quiz_validation_error_text')
.text(renderedQuestion.error);
} else if (options.update) {
this.$('.o_wslides_js_quiz_validation_error').addClass('d-none');
this.trigger_up('display_updated_question', {
newQuestionRenderedTemplate: renderedQuestion,
$editedQuestion: this.$editedQuestion,
questionFormWidget: this,
});
} else {
this.$('.o_wslides_js_quiz_validation_error').addClass('d-none');
this.trigger_up('display_created_question', {
newQuestionRenderedTemplate: renderedQuestion,
questionFormWidget: this
});
}
} else {
this.displayNotification({
type: 'warning',
message: _t('Please fill in the question'),
sticky: true
});
this.$('.o_wslides_js_quiz_validation_error')
.removeClass('d-none')
.find('.o_wslides_js_quiz_validation_error_text')
.text(_t('Please fill in the question'));
this.$('.o_wslides_quiz_question input').focus();
}
},
@@ -139,6 +139,10 @@ $truncate-limits: 2, 3, 10;
i.o_wslides_js_quiz_icon:hover {
color: black !important;
}
.o_wslides_js_quiz_validation_error, .o_wslides_js_quiz_submit_error {
display: inline-block; // not using d-inline-block to avoid messing with d-none
}
}
.o_wslides_js_lesson_quiz_question {
@@ -105,6 +105,10 @@
+ <t t-if="!widget.slide.completed" t-esc="widget.quiz.quizKarmaGain"/><t t-else="" t-esc="widget.quiz.quizKarmaWon"/> XP
</span>
</span>
<div class="ml-3 d-none text-danger o_wslides_js_quiz_submit_error">
<i class="fa fa-close mr-1"/>
<span class="o_wslides_js_quiz_submit_error_text"></span>
</div>
</div>
<div class="ml-auto mt-3 mt-md-0">
<button t-if="widget.quiz.quizAttemptsCount > 0 &amp;&amp; widget.slide.channelCanUpload" class="btn btn-light border o_wslides_js_lesson_quiz_reset">
@@ -29,8 +29,14 @@
</t>
</div>
</form>
<t t-if="widget.update" t-call="slide.quiz.update.buttons"/>
<t t-else="" t-call="slide.quiz.create.buttons"/>
<div>
<t t-if="widget.update" t-call="slide.quiz.update.buttons"/>
<t t-else="" t-call="slide.quiz.create.buttons"/>
<div class="ml-2 d-none text-danger o_wslides_js_quiz_validation_error">
<i class="fa fa-close mr-1"/>
<span class="o_wslides_js_quiz_validation_error_text"></span>
</div>
</div>
</div>
</t>
@@ -66,14 +72,12 @@
</t>
<t t-name="slide.quiz.create.buttons">
<div>
<a class="o_wslides_js_quiz_validate_question btn btn-primary text-white border" role="button">
<span>Save</span>
</a>
<a class="o_wslides_js_quiz_cancel_question btn btn-light border" role="button">
<span>Cancel</span>
</a>
</div>
<a class="o_wslides_js_quiz_validate_question btn btn-primary text-white border" role="button">
<span>Save</span>
</a>
<a class="o_wslides_js_quiz_cancel_question btn btn-light border" role="button">
<span>Cancel</span>
</a>
</t>
<t t-name="slide.quiz.update.buttons">