diff --git a/addons/website_sale_slides/static/src/js/slides_course_quiz.js b/addons/website_sale_slides/static/src/js/slides_course_quiz.js index 9dc0154596a..1e601acc7a7 100644 --- a/addons/website_sale_slides/static/src/js/slides_course_quiz.js +++ b/addons/website_sale_slides/static/src/js/slides_course_quiz.js @@ -2,7 +2,7 @@ odoo.define('website_sale_slides.quiz', function (require) { "use strict"; var sAnimations = require('website.content.snippets.animation'); -var Quiz = require('website_slides.quiz'); +var Quiz = require('website_slides.quiz').Quiz; sAnimations.registry.websiteSlidesQuizNoFullscreen.include({ _extractChannelData: function (slideData){ diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index d501ff21e2a..a4bb141c3a6 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -127,10 +127,10 @@ class WebsiteSlides(WebsiteProfile): 'slide_questions': [{ 'id': question.id, 'question': question.question, - 'answers': [{ + 'answer_ids': [{ 'id': answer.id, 'text_value': answer.text_value, - 'is_correct': answer.is_correct if slide_completed else None + 'is_correct': answer.is_correct if slide_completed or request.website.is_publisher() else None } for answer in question.sudo().answer_ids], } for question in slide.question_ids] } @@ -700,6 +700,57 @@ class WebsiteSlides(WebsiteProfile): # QUIZZ SECTION # -------------------------------------------------- + @http.route('/slides/slide/quiz/question_add_or_update', type='json', methods=['POST'], auth='user', website=True) + def slide_quiz_question_add_or_update(self, slide_id, question, sequence, answer_ids, existing_question_id=None): + """ Add a new question to an existing slide. Completed field of slide.partner + link is set to False to make sure that the creator can take the quiz again. + + An optional question_id to udpate can be given. In this case question is + deleted first before creating a new one to simplify management. + + :param integer slide_id: Slide ID + :param string question: Question Title + :param integer sequence: Question Sequence + :param array answer_ids: Array containing all the answers : + [ + 'sequence': Answer Sequence (Integer), + 'text_value': Answer Title (String), + 'is_correct': Answer Is Correct (Boolean) + ] + :param integer existing_question_id: question ID if this is an update + + :return: rendered question template + """ + fetch_res = self._fetch_slide(slide_id) + if fetch_res.get('error'): + return fetch_res + slide = fetch_res['slide'] + if existing_question_id: + request.env['slide.question'].search([ + ('slide_id', '=', slide.id), + ('id', '=', int(existing_question_id)) + ]).unlink() + + request.env['slide.slide.partner'].search([ + ('slide_id', '=', slide_id), + ('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'] + }) for answer in answer_ids] + }) + return request.env.ref('website_slides.lesson_content_quiz_question').render({ + 'slide': slide, + 'question': slide_question, + }) + @http.route('/slides/slide/quiz/get', type="json", auth="public", website=True) def slide_quiz_get(self, slide_id): fetch_res = self._fetch_slide(slide_id) @@ -708,6 +759,16 @@ class WebsiteSlides(WebsiteProfile): slide = fetch_res['slide'] return self._get_slide_quiz_data(slide) + @http.route('/slides/slide/quiz/reset', type="json", auth="user", website=True) + def slide_quiz_reset(self, slide_id): + fetch_res = self._fetch_slide(slide_id) + if fetch_res.get('error'): + return fetch_res + request.env['slide.slide.partner'].search([ + ('slide_id', '=', fetch_res['slide'].id), + ('partner_id', '=', request.env.user.partner_id.id) + ]).write({'completed': False, 'quiz_attempts_count': 0}) + @http.route('/slides/slide/quiz/submit', type="json", auth="public", website=True) def slide_quiz_submit(self, slide_id, answer_ids): if request.website.is_public_user(): @@ -768,7 +829,6 @@ class WebsiteSlides(WebsiteProfile): 'motivational': next_rank.description_motivational, 'progress': progress } - # -------------------------------------------------- # CATEGORY MANAGEMENT # -------------------------------------------------- @@ -878,9 +938,6 @@ class WebsiteSlides(WebsiteProfile): redirect_url = "/slides/%s" % (slug(channel)) if slide.slide_type == 'webpage': redirect_url += "?enable_editor=1" - if slide.slide_type == "quiz": - action_id = request.env.ref('website_slides.slide_slide_action').id - redirect_url = '/web#id=%s&action=%s&model=slide.slide&view_type=form' %( slide.id, action_id) return { 'url': redirect_url, 'channel_type': channel.channel_type, diff --git a/addons/website_slides/data/slide_slide_demo.xml b/addons/website_slides/data/slide_slide_demo.xml index 6e00c21e9aa..3d6ae6aeec0 100644 --- a/addons/website_slides/data/slide_slide_demo.xml +++ b/addons/website_slides/data/slide_slide_demo.xml @@ -76,34 +76,41 @@ What is a strawberry ? + 1 A fruit + 1 A vegetable + 2 A table + 3 What is the best tool to dig a hole for your plants ? + 2 A shovel + 1 A spoon + 2 @@ -256,34 +263,41 @@ Do you think Harry Potted has a good name ? + 1 Yes + 1 No + 2 Did you read the whole article ? + 2 Yes + 1 No + 2 What was the question again ? + 3 @@ -371,34 +385,41 @@ Do you make beams out of lemon trees ? + 1 Yes + 1 No + 2 Do you make lemons out of beams ? + 2 Yes + 1 No + 2 And also bananas + 3 @@ -638,15 +659,18 @@ Do you want to reply correctly ? + 1 Yes + 1 No + 2 diff --git a/addons/website_slides/models/slide_question.py b/addons/website_slides/models/slide_question.py index 8258b26e71a..c612f90b371 100644 --- a/addons/website_slides/models/slide_question.py +++ b/addons/website_slides/models/slide_question.py @@ -9,8 +9,9 @@ class SlideQuestion(models.Model): _name = "slide.question" _rec_name = "question" _description = "Content Quiz Question" + _order = "sequence" - sequence = fields.Integer("Sequence", default=10) + sequence = fields.Integer("Sequence") question = fields.Char("Question Name", required=True, translate=True) slide_id = fields.Many2one('slide.slide', string="Content", required=True) answer_ids = fields.One2many('slide.answer', 'question_id', string="Answer") @@ -49,8 +50,9 @@ class SlideAnswer(models.Model): _name = "slide.answer" _rec_name = "text_value" _description = "Answer for a slide question" - _order = 'question_id, id' + _order = 'question_id, sequence' - question_id = fields.Many2one('slide.question', string="Question", required=True) + sequence = fields.Integer("Sequence") + question_id = fields.Many2one('slide.question', string="Question", required=True, ondelete='cascade') text_value = fields.Char("Answer", required=True, translate=True) is_correct = fields.Boolean("Is correct answer") diff --git a/addons/website_slides/static/src/js/slides_course_fullscreen_player.js b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js index e9dd0c8e2c3..ca8463a8f07 100644 --- a/addons/website_slides/static/src/js/slides_course_fullscreen_player.js +++ b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js @@ -11,7 +11,7 @@ odoo.define('website_slides.fullscreen', function (require) { var session = require('web.session'); - var Quiz = require('website_slides.quiz'); + var Quiz = require('website_slides.quiz').Quiz; var Dialog = require('web.Dialog'); diff --git a/addons/website_slides/static/src/js/slides_course_quiz.js b/addons/website_slides/static/src/js/slides_course_quiz.js index cf53fe1b236..d22c386f3cc 100644 --- a/addons/website_slides/static/src/js/slides_course_quiz.js +++ b/addons/website_slides/static/src/js/slides_course_quiz.js @@ -2,10 +2,12 @@ odoo.define('website_slides.quiz', function (require) { 'use strict'; var publicWidget = require('web.public.widget'); + var Dialog = require('web.Dialog'); var core = require('web.core'); var session = require('web.session'); var CourseJoinWidget = require('website_slides.course.join.widget').courseJoinWidget; + var QuestionFormWidget = require('website_slides.quiz.question.form'); var QWeb = core.qweb; var _t = core._t; @@ -28,7 +30,18 @@ odoo.define('website_slides.quiz', function (require) { "click .o_wslides_quiz_answer": '_onAnswerClick', "click .o_wslides_js_lesson_quiz_submit": '_onSubmitQuiz', "click .o_wslides_quiz_modal_btn": '_onClickNext', - "click .o_wslides_quiz_continue": '_onClickNext' + "click .o_wslides_quiz_continue": '_onClickNext', + "click .o_wslides_js_lesson_quiz_reset": '_onClickReset', + 'click .o_wslides_js_quiz_add': '_onCreateQuizClick', + 'click .o_wslides_js_quiz_edit_question': '_onEditQuestionClick', + 'click .o_wslides_js_quiz_delete_question': '_onDeleteQuestionClick', + }, + + custom_events: { + display_created_question: '_displayCreatedQuestion', + display_updated_question: '_displayUpdatedQuestion', + reset_display: '_resetDisplay', + delete_question: '_deleteQuestion', }, /** @@ -46,6 +59,9 @@ odoo.define('website_slides.quiz', function (require) { readonly: false, }); this.quiz = quiz_data || false; + if (this.quiz) { + this.quiz.questionsCount = quiz_data.questions.length; + } this.readonly = slide_data.readonly || false; this.publicUser = session.is_website_user; this.userId = session.user_id; @@ -74,6 +90,8 @@ odoo.define('website_slides.quiz', function (require) { self._renderAnswers(); self._renderAnswersHighlighting(); self._renderValidationInfo(); + self._bindSortable(); + self._checkLocationHref(); new CourseJoinWidget(self, self.channel.channelId).appendTo(self.$('.o_wslides_course_join_widget')); }); }, @@ -82,10 +100,6 @@ odoo.define('website_slides.quiz', function (require) { // Private //-------------------------------------------------------------------------- - _alertHide: function () { - this.$('.o_wslides_js_lesson_quiz_error').addClass('d-none'); - }, - _alertShow: function (alert_code) { var message = _t('There was an error validating this quiz.'); if (! alert_code || alert_code === 'slide_quiz_incomplete') { @@ -97,8 +111,63 @@ odoo.define('website_slides.quiz', function (require) { else if (alert_code === 'public_user') { message = _t('You must be logged to submit the quiz.'); } - this.$('.o_wslides_js_lesson_quiz_error span:first').html(message); - this.$('.o_wslides_js_lesson_quiz_error').removeClass('d-none'); + this.displayNotification({ + type: 'warning', + title: _t('Something went wrong'), + message: message, + sticky: true + }); + }, + + /** + * Allows to reorder the questions + * @private + */ + _bindSortable: function() { + this.$el.sortable({ + handle: '.o_wslides_js_quiz_sequence_handler', + items: '.o_wslides_js_lesson_quiz_question', + stop: this._reorderQuestions.bind(this), + placeholder: 'o_wslides_js_quiz_sequence_highlight position-relative my-3' + }); + }, + + /** + * Get all the questions ID from the displayed Quiz + * @returns {Array} + * @private + */ + _getQuestionsIds: function () { + return this.$('.o_wslides_js_lesson_quiz_question').map(function () { + return $(this).data('question-id'); + }).get(); + }, + + /** + * Modify visually the sequence of all the questions after + * calling the _reorderQuestions RPC call. + * @private + */ + _modifyQuestionsSequence: function () { + this.$('.o_wslides_js_lesson_quiz_question').each(function (index, question) { + $(question).find('span.o_wslides_quiz_question_sequence').text(index + 1); + }); + }, + + /** + * RPC call to resequence all the questions. It is called + * after modifying the sequence of a question and also after + * deleting a question. + * @private + */ + _reorderQuestions: function () { + this._rpc({ + route: '/web/dataset/resequence', + params: { + model: "slide.question", + ids: this._getQuestionsIds() + } + }).then(this._modifyQuestionsSequence.bind(this)) }, /* @@ -115,6 +184,7 @@ odoo.define('website_slides.quiz', function (require) { }).then(function (quiz_data) { self.quiz = { questions: quiz_data.slide_questions || [], + questionsCount: quiz_data.slide_questions.length, quizAttemptsCount: quiz_data.quiz_attempts_count || 0, quizKarmaGain: quiz_data.quiz_karma_gain || 0, quizKarmaWon: quiz_data.quiz_karma_won || 0, @@ -122,6 +192,16 @@ odoo.define('website_slides.quiz', function (require) { }); }, + /** + * Hide the edit and delete button and also the handler + * to resequence the question + * @private + */ + _hideEditOptions: function () { + this.$('.o_wslides_js_lesson_quiz_question .o_wslides_js_quiz_edit_del,' + + ' .o_wslides_js_lesson_quiz_question .o_wslides_js_quiz_sequence_handler').addClass('d-none'); + }, + /** * @private * Decorate the answers according to state @@ -308,12 +388,48 @@ odoo.define('website_slides.quiz', function (require) { self.slide.completed = true; self.trigger_up('slide_completed', {slide: self.slide, completion: data.channel_completion}); } + self._hideEditOptions(); self._renderAnswersHighlighting(); self._renderValidationInfo(); } }); }, + /** + * Get all the question information after clicking on + * the edit button + * @param $elem + * @returns {{id: *, sequence: number, text: *, answers: Array}} + * @private + */ + _getQuestionDetails: function ($elem) { + var answers = []; + $elem.find('.o_wslides_quiz_answer').each(function () { + answers.push({ + 'id': $(this).data('answerId'), + 'text_value': $(this).data('text'), + 'is_correct': $(this).data('isCorrect') + }); + }); + return { + 'id': $elem.data('questionId'), + 'sequence': parseInt($elem.find('.o_wslides_quiz_question_sequence').text()), + 'text': $elem.data('title'), + 'answers': answers, + }; + }, + + /** + * If the slides has been called with the Add Quiz button on the slide list + * it goes straight to the 'Add Quiz' button and clicks on it. + * @private + */ + _checkLocationHref: function() { + if (window.location.href.includes('quiz_quick_create')) { + this._onCreateQuizClick(); + } + }, + //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- @@ -329,7 +445,6 @@ odoo.define('website_slides.quiz', function (require) { if (! this.slide.readonly && ! this.slide.completed) { $(ev.currentTarget).find('input[type=radio]').prop('checked', true); } - this._alertHide(); }, /** * Triggering a event to switch to next slide @@ -345,6 +460,22 @@ odoo.define('website_slides.quiz', function (require) { this.trigger_up('slide_go_next'); } }, + /** + * Resets the completion of the slide so the user can take + * the quiz again + * + * @private + */ + _onClickReset: function () { + this._rpc({ + route: '/slides/slide/quiz/reset', + params: { + slide_id: this.slide.id + } + }).then(function() { + window.location.reload(); + }); + }, /** * Submit a quiz and get the correction. It will display messages * according to quiz result. @@ -359,13 +490,189 @@ odoo.define('website_slides.quiz', function (require) { values.push(parseInt($(inputs[i]).val())); } - if (values.length === this.quiz.questions.length){ - this._alertHide(); + if (values.length === this.quiz.questionsCount){ this._submitQuiz(values); } else { this._alertShow(); } }, + + /** + * When clicking on 'Add a Question' or 'Add Quiz' it + * initialize a new QuestionFormWidget to input the new + * question. + * @private + */ + _onCreateQuizClick: function () { + var $elem = this.$('.o_wslides_js_lesson_quiz_new_question'); + this.$('.o_wslides_js_quiz_add').addClass('d-none'); + new QuestionFormWidget(this, { + slideId: this.slide.id, + sequence: this.quiz.questionsCount + 1 + }).appendTo($elem); + }, + + /** + * When clicking on the edit button of a question it + * initialize a new QuestionFormWidget with the existing + * question as inputs. + * @param ev + * @private + */ + _onEditQuestionClick: function (ev) { + var $editedQuestion = $(ev.currentTarget).closest('.o_wslides_js_lesson_quiz_question'); + var question = this._getQuestionDetails($editedQuestion); + new QuestionFormWidget(this, { + editedQuestion: $editedQuestion, + question: question, + slideId: this.slide.id, + sequence: question.sequence, + update: true + }).insertAfter($editedQuestion); + $editedQuestion.hide(); + }, + + /** + * When clicking on the delete button of a question it + * toggles a modal to confirm the deletion + * @param ev + * @private + */ + _onDeleteQuestionClick: function (ev) { + var question = $(ev.currentTarget).closest('.o_wslides_js_lesson_quiz_question'); + new ConfirmationDialog(this, { + questionId: question.data('questionId'), + questionTitle: question.data('title') + }).open(); + }, + + /** + * Displays the created Question at the correct place (after the last question or + * at the first place if there is no questions yet) It also displays the 'Add Question' + * button or open a new QuestionFormWidget if the user wants to immediately add another one. + * + * @param event + * @private + */ + _displayCreatedQuestion: function (event) { + var $lastQuestion = this.$('.o_wslides_js_lesson_quiz_question:last'); + if ($lastQuestion.length !== 0) { + $lastQuestion.after(event.data.newQuestionRenderedTemplate); + } else { + this.$el.prepend(event.data.newQuestionRenderedTemplate); + } + this.quiz.questionsCount++; + event.data.questionFormWidget.destroy(); + if (event.data.save_and_new) { + this._onCreateQuizClick(); + } else { + this.$('.o_wslides_js_quiz_add_question').removeClass('d-none'); + } + }, + + /** + * Replace the edited question by the new question and destroy + * the QuestionFormWidget. + * @param event + * @private + */ + _displayUpdatedQuestion: function (event) { + var questionFormWidget = event.data.questionFormWidget; + event.data.$editedQuestion.replaceWith(event.data.newQuestionRenderedTemplate); + questionFormWidget.destroy(); + }, + + /** + * If the user cancels the creation or update of a Question it resets the display + * of the updated Question or it displays back the buttons. + * + * @param event + * @private + */ + _resetDisplay: function (event) { + var questionFormWidget = event.data.questionFormWidget; + if (questionFormWidget.update) { + questionFormWidget.$editedQuestion.show(); + } else { + if (this.quiz.questionsCount > 0) { + this.$('.o_wslides_js_quiz_add_question').removeClass('d-none'); + } else { + this.$('.o_wslides_js_quiz_add_quiz').removeClass('d-none'); + } + } + questionFormWidget.destroy(); + }, + + /** + * After deletion of a Question the display is refreshed with the removal of the Question + * the reordering of all the remaining Questions and the change of the new Question sequence + * if the QuestionFormWidget is initialized. + * + * @param event + * @private + */ + _deleteQuestion: function(event) { + var questionId = event.data.questionId; + this.$('.o_wslides_js_lesson_quiz_question[data-question-id=' + questionId + ']').remove(); + this.quiz.questionsCount--; + this._reorderQuestions(); + var $newQuestionSequence = this.$('.o_wslides_js_lesson_quiz_new_question .o_wslides_quiz_question_sequence'); + $newQuestionSequence.text(parseInt($newQuestionSequence.text()) - 1); + if (this.quiz.questionsCount === 0 && !this.$('.o_wsildes_quiz_question_input').length) { + this.$('.o_wslides_js_quiz_add_quiz').removeClass('d-none'); + this.$('.o_wslides_js_quiz_add_question').addClass('d-none'); + this.$('.o_wslides_js_lesson_quiz_validation').addClass('d-none'); + } + }, + }); + + /** + * Dialog box shown when clicking the deletion button on a Question. + * When confirming it sends a RPC request to delete the Question. + */ + var ConfirmationDialog = Dialog.extend({ + template: 'slide.quiz.confirm.deletion', + xmlDependencies: Dialog.prototype.xmlDependencies.concat( + ['/website_slides/static/src/xml/slide_quiz_create.xml'] + ), + + /** + * @override + * @param parent + * @param options + */ + init: function (parent, options) { + options = _.defaults(options || {}, { + title: _t('Delete Question'), + buttons: [ + { text: _t('Yes'), classes: 'btn-primary', click: this._onConfirmClick }, + { text: _t('No'), close: true} + ], + size: 'medium' + }); + this.questionId = options.questionId; + this.questionTitle = options.questionTitle; + this._super.apply(this, arguments); + }, + + /** + * Handler when the user confirm the deletion by clicking on 'Yes' + * it sends a RPC request to the server and triggers an event to + * visually delete the question. + * @private + */ + _onConfirmClick: function () { + var self = this; + this._rpc({ + model: 'slide.question', + method: 'unlink', + args: [this.questionId], + }).then(function () { + self.trigger_up('delete_question', { questionId: self.questionId }); + self.close(); + }); + } + }); publicWidget.registry.websiteSlidesQuizNoFullscreen = publicWidget.Widget.extend({ @@ -453,12 +760,16 @@ odoo.define('website_slides.quiz', function (require) { questions.push({ id: $question.data('questionId'), title: $question.data('title'), - answers: answers, + answer_ids: answers, }); }); return questions; }, }); - return Quiz; + return { + Quiz: Quiz, + ConfirmationDialog: ConfirmationDialog, + websiteSlidesQuizNoFullscreen: publicWidget.registry.websiteSlidesQuizNoFullscreen + }; }); diff --git a/addons/website_slides/static/src/js/slides_course_quiz_question_form.js b/addons/website_slides/static/src/js/slides_course_quiz_question_form.js new file mode 100644 index 00000000000..04f8532eca9 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_quiz_question_form.js @@ -0,0 +1,190 @@ +odoo.define('website_slides.quiz.question.form', function (require) { +'use strict'; + +var publicWidget = require('web.public.widget'); +var core = require('web.core'); + +var QWeb = core.qweb; +var _t = core._t; + +/** + * This Widget is responsible of displaying the question inputs when adding a new question or when updating an + * existing one. When validating the question it makes an RPC call to the server and trigger an event for + * displaying the question by the Quiz widget. + */ +var QuestionFormWidget = publicWidget.Widget.extend({ + template: 'slide.quiz.question.input', + xmlDependencies: ['/website_slides/static/src/xml/slide_quiz_create.xml'], + events: { + 'click .o_wslides_js_quiz_validate_question': '_validateQuestion', + 'click .o_wslides_js_quiz_cancel_question': '_cancelValidation', + 'click .o_wslides_js_quiz_add_answer': '_addAnswerLine', + 'click .o_wslides_js_quiz_remove_answer': '_removeAnswerLine', + }, + + /** + * @override + * @param parent + * @param options + */ + init: function (parent, options) { + this.$editedQuestion = options.editedQuestion; + this.question = options.question || {}; + this.update = options.update; + this.sequence = options.sequence; + this.slideId = options.slideId; + this._super.apply(this, arguments); + }, + + /** + * @override + * @returns {*} + */ + start: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + self.$('.o_wslides_quiz_question input').focus(); + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Adds a new answer line after the element the user clicked on + * e.g. If there is 3 answer lines and the user click on the add + * answer button on the second line, the new answer line will + * display between the second and the third line. + * @param ev + * @private + */ + _addAnswerLine: function (ev) { + $(ev.currentTarget).closest('.o_wslides_js_quiz_answer').after(QWeb.render('slide.quiz.answer.line')); + }, + + /** + * Removes an answer line. Can't remove the last answer line. + * @param ev + * @private + */ + _removeAnswerLine: function (ev) { + if (this.$('.o_wslides_js_quiz_answer').length > 1) { + $(ev.currentTarget).closest('.o_wslides_js_quiz_answer').remove(); + } + }, + + /** + * Handler when user click on 'Save & New', 'Save & Close' + * or 'Update' buttons. + * @param ev + * @private + */ + _validateQuestion: function (ev) { + this._createOrUpdateQuestion({ + save_and_new: $(ev.currentTarget).hasClass('o_wslides_js_quiz_create_next'), + update: $(ev.currentTarget).hasClass('o_wslides_js_quiz_update'), + }); + }, + + /** + * Handler when user click on the 'Cancel' button. + * Calls a method from slides_course_quiz.js widget + * which will handle the reset of the question display. + * @private + */ + _cancelValidation: function () { + this.trigger_up('reset_display', { + questionFormWidget: this, + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * RPC call to create or update a question. + * Triggers method from slides_course_quiz.js to + * correctly display the question. + * @param options + * @private + */ + _createOrUpdateQuestion: function (options) { + var self = this; + var $form = this.$('form'); + if (this._isValidForm($form)) { + var values = this._serializeForm($form); + 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, + save_and_new: options.save_and_new, + questionFormWidget: self + }); + } + }); + } else { + this.displayNotification({ + type: 'warning', + title: _t('Unable to create question'), + message: _t('Please fill up the question'), + sticky: true + }); + this.$('.o_wslides_quiz_question input').focus(); + } + }, + + /** + * Check if the Question has been filled up + * @param $form + * @returns {boolean} + * @private + */ + _isValidForm: function($form) { + return $form.find('.o_wslides_quiz_question input[type=text]').val().trim() !== ""; + }, + + /** + * Serialize the form into a JSON object to send it + * to the server through a RPC call. + * @param $form + * @returns {{id: *, sequence: *, question: *, slide_id: *, answer_ids: Array}} + * @private + */ + _serializeForm: function ($form) { + var answers = []; + var sequence = 1; + $form.find('.o_wslides_js_quiz_answer').each(function () { + var value = $(this).find('input[type=text]').val(); + if (value.trim() !== "") { + var answer = { + 'sequence': sequence++, + 'text_value': value, + 'is_correct': $(this).find('input[type=radio]').prop('checked') === true + }; + answers.push(answer); + } + }); + return { + 'existing_question_id': this.$el.data('id'), + 'sequence': this.sequence, + 'question': $form.find('.o_wslides_quiz_question input[type=text]').val(), + 'slide_id': this.slideId, + 'answer_ids': answers + }; + }, + +}); + +return QuestionFormWidget; +}); diff --git a/addons/website_slides/static/src/scss/website_slides.scss b/addons/website_slides/static/src/scss/website_slides.scss index cd02d6eaedd..52628a00342 100644 --- a/addons/website_slides/static/src/scss/website_slides.scss +++ b/addons/website_slides/static/src/scss/website_slides.scss @@ -83,6 +83,14 @@ $o-wslides-fs-side-width: 300px; } } + .o_wsildes_quiz_question_input { + .o_wslides_js_quiz_is_correct > label, + .o_wslides_js_quiz_add_answer, + .o_wslides_js_quiz_remove_answer { + cursor: pointer; + } + } + .o_wslides_js_lesson_quiz_question { .list-group-item { font-size: 1rem; @@ -97,11 +105,52 @@ $o-wslides-fs-side-width: 300px; pointer-events: none; } + .o_wslides_js_quiz_edit_question, + .o_wslides_js_quiz_delete_question { + cursor: pointer; + } + + .o_wslides_js_quiz_sequence_handler { + cursor: pointer; + opacity: 0.4; + } + + .o_wslides_js_quiz_sequence_handler:hover { + opacity: 1; + } + &.completed-disabled{ pointer-events: none; } } + a.o_wslides_js_quiz_is_correct { + color: black; + input:checked + i.fa-check-circle-o { + color: $primary; + } + } + + .o_wslides_js_quiz_sequence_highlight { + background-color: #1252F3; + height: 1px; + z-index: 3; + + &:before, &:after { + content: ""; + @include size(6px); + display: block; + border-radius: 100%; + background-color: inherit; + @include o-position-absolute(-2px, -2px); + } + + &:after { + right: auto; + left: -2px; + } + } + // tools // **************************************** .text_small_caps { diff --git a/addons/website_slides/static/src/xml/slide_quiz.xml b/addons/website_slides/static/src/xml/slide_quiz.xml index abf284ac61b..a57a10c5adb 100644 --- a/addons/website_slides/static/src/xml/slide_quiz.xml +++ b/addons/website_slides/static/src/xml/slide_quiz.xml @@ -11,7 +11,7 @@ .
- + @@ -30,12 +30,6 @@
-
@@ -77,9 +71,14 @@
- +
+ + +
diff --git a/addons/website_slides/static/src/xml/slide_quiz_create.xml b/addons/website_slides/static/src/xml/slide_quiz_create.xml new file mode 100644 index 00000000000..0d19371e0b3 --- /dev/null +++ b/addons/website_slides/static/src/xml/slide_quiz_create.xml @@ -0,0 +1,77 @@ + + + + +
+
+
+ + . + + +
+
+ Select the correct answer below : +
+
+ + + + + + + + + + +
+
+ + +
+
+ + +
+ + + + + + + + + Update + + + Cancel + + + + +
Are you sure you want to delete this question : ?
+
+ + diff --git a/addons/website_slides/tests/test_ui_wslides.py b/addons/website_slides/tests/test_ui_wslides.py index 4e60263553d..3a96a559677 100644 --- a/addons/website_slides/tests/test_ui_wslides.py +++ b/addons/website_slides/tests/test_ui_wslides.py @@ -67,10 +67,13 @@ class TestUICommon(HttpCaseWithUserDemo, HttpCaseWithUserPortal): (0, 0, { 'text_value': 'A fruit', 'is_correct': True, + 'sequence': 1, }), (0, 0, { 'text_value': 'A vegetable', + 'sequence': 2, }), (0, 0, { 'text_value': 'A table', + 'sequence': 3, }) ] }), (0, 0, { @@ -79,8 +82,10 @@ class TestUICommon(HttpCaseWithUserDemo, HttpCaseWithUserPortal): (0, 0, { 'text_value': 'A shovel', 'is_correct': True, + 'sequence': 1, }), (0, 0, { 'text_value': 'A spoon', + 'sequence': 2, }) ] }) diff --git a/addons/website_slides/views/assets.xml b/addons/website_slides/views/assets.xml index 2d214bfa17c..d4d037d8e80 100644 --- a/addons/website_slides/views/assets.xml +++ b/addons/website_slides/views/assets.xml @@ -28,6 +28,7 @@