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 @@
.
-
-
-
-
-
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 @@
+
diff --git a/addons/website_slides/views/website_slides_templates_course.xml b/addons/website_slides/views/website_slides_templates_course.xml
index e9ce3e7dbdb..e9bc8668a82 100644
--- a/addons/website_slides/views/website_slides_templates_course.xml
+++ b/addons/website_slides/views/website_slides_templates_course.xml
@@ -431,6 +431,9 @@
xp
+
+ Add Quiz
+
Unpublished
diff --git a/addons/website_slides/views/website_slides_templates_lesson.xml b/addons/website_slides/views/website_slides_templates_lesson.xml
index d20aaacfc8e..c3c0db11b57 100644
--- a/addons/website_slides/views/website_slides_templates_lesson.xml
+++ b/addons/website_slides/views/website_slides_templates_lesson.xml
@@ -255,7 +255,8 @@
-
-
-
+
@@ -418,42 +426,68 @@
t-att-data-next-slide-url="'/slides/slide/%s' % (slug(next_slide)) if next_slide else None"
t-att-data-channel-id="slide.channel_id.id"
t-att-data-channel-enroll="slide.channel_id.enroll"
+ t-att-data-channel-can-upload="slide.channel_id.can_upload"
t-att-data-signup-allowed="signup_allowed">
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ .
+ .
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Share by mail
diff --git a/addons/website_slides_survey/views/website_slides_templates_course.xml b/addons/website_slides_survey/views/website_slides_templates_course.xml
index bac6288713a..7dcd3d79586 100644
--- a/addons/website_slides_survey/views/website_slides_templates_course.xml
+++ b/addons/website_slides_survey/views/website_slides_templates_course.xml
@@ -28,6 +28,9 @@
#{'/slides_survey/slide/get_certification_url?slide_id=%s' %(slide.id) if slide.slide_type == 'certification' and slide.channel_id.total_slides == 1 else '/slides/slide/%s' %(slug(slide))}
+
+ channel.can_upload and not slide.question_ids and slide.slide_type != 'certification'
+