[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:
@@ -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 && 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">
|
||||
|
||||
Reference in New Issue
Block a user