[IMP] website_slides: allow to mark content as "(un)done"
Purpose ======= Allow the users to themselves mark their course as "done" or "not done" Specifications ============== When the course is a "Training" in fullscreen or in non-fullscreen, or when the course is a "Documentation" in fullscreen mode, allow the user to click on the "done" button in the sidebar. When the course is a "Documentation" in non-fullscreen mode, add a button "Mark Not Done" when the slide is done. The quiz and the certification cannot be marked as done manually, the user has to complete the questions for that. A certification cannot be marked as "Not Done" (but a quiz can). In the non-fullscreen view, mark content as done automatically like we do for the fullscreen view. Technical ========= As the fullscreen view and the non-fullscreen view will share a lot of code, make a common class to avoid redundancy. Task-2604792 Part-of: odoo/odoo#75561
This commit is contained in:
committed by
Aurélien Warnon
parent
7d69bd893e
commit
e1ac0ed0a2
@@ -83,6 +83,7 @@ Featuring
|
||||
'website_slides/static/src/js/slides_slide_archive.js',
|
||||
'website_slides/static/src/js/slides_slide_toggle_is_preview.js',
|
||||
'website_slides/static/src/js/slides_slide_like.js',
|
||||
'website_slides/static/src/js/slides_course_page.js',
|
||||
'website_slides/static/src/js/slides_course_slides_list.js',
|
||||
'website_slides/static/src/js/slides_course_fullscreen_player.js',
|
||||
'website_slides/static/src/js/slides_course_join.js',
|
||||
|
||||
@@ -74,13 +74,18 @@ class WebsiteSlides(WebsiteProfile):
|
||||
slide.action_set_viewed(quiz_attempts_inc=quiz_attempts_inc)
|
||||
return True
|
||||
|
||||
def _set_completed_slide(self, slide):
|
||||
def _slide_mark_completed(self, slide):
|
||||
# quiz use their specific mechanism to be marked as done
|
||||
if slide.slide_category == 'quiz' or slide.question_ids:
|
||||
raise UserError(_("Slide with questions must be marked as done when submitting all good answers "))
|
||||
if slide.website_published and slide.channel_id.is_member:
|
||||
slide.action_set_completed()
|
||||
return True
|
||||
if not slide.can_self_mark_completed:
|
||||
raise werkzeug.exceptions.Forbidden(_("This slide can not be marked as completed."))
|
||||
slide.action_mark_completed()
|
||||
|
||||
def _slide_mark_uncompleted(self, slide):
|
||||
if not slide.can_self_mark_uncompleted:
|
||||
raise werkzeug.exceptions.Forbidden(_("This slide can not be marked as uncompleted."))
|
||||
slide.action_mark_uncompleted()
|
||||
|
||||
def _get_slide_detail(self, slide):
|
||||
base_domain = self._get_channel_slides_base_domain(slide.channel_id)
|
||||
@@ -744,7 +749,12 @@ class WebsiteSlides(WebsiteProfile):
|
||||
# redirection to channel's homepage for category slides
|
||||
if slide.is_category:
|
||||
return werkzeug.utils.redirect(slide.channel_id.website_url)
|
||||
self._set_viewed_slide(slide)
|
||||
|
||||
if slide.can_self_mark_completed and not slide.user_has_completed \
|
||||
and slide.channel_id.channel_type == 'training':
|
||||
self._slide_mark_completed(slide)
|
||||
else:
|
||||
self._set_viewed_slide(slide)
|
||||
|
||||
values = self._get_slide_detail(slide)
|
||||
# quiz-specific: update with karma and quiz information
|
||||
@@ -843,7 +853,7 @@ class WebsiteSlides(WebsiteProfile):
|
||||
|
||||
@http.route('/slides/slide/<model("slide.slide"):slide>/set_completed', website=True, type="http", auth="user")
|
||||
def slide_set_completed_and_redirect(self, slide, next_slide_id=None):
|
||||
self._set_completed_slide(slide)
|
||||
self._slide_mark_completed(slide)
|
||||
next_slide = None
|
||||
if next_slide_id:
|
||||
next_slide = self._fetch_slide(next_slide_id).get('slide', None)
|
||||
@@ -856,11 +866,28 @@ class WebsiteSlides(WebsiteProfile):
|
||||
fetch_res = self._fetch_slide(slide_id)
|
||||
if fetch_res.get('error'):
|
||||
return fetch_res
|
||||
self._set_completed_slide(fetch_res['slide'])
|
||||
self._slide_mark_completed(fetch_res['slide'])
|
||||
return {
|
||||
'channel_completion': fetch_res['slide'].channel_id.completion
|
||||
}
|
||||
|
||||
@http.route('/slides/slide/<model("slide.slide"):slide>/set_uncompleted', website=True, type='http', auth='user')
|
||||
def slide_set_uncompleted_and_redirect(self, slide):
|
||||
self._slide_mark_uncompleted(slide)
|
||||
return request.redirect(f'/slides/slide/{slug(slide)}')
|
||||
|
||||
@http.route('/slides/slide/set_uncompleted', website=True, type='json', auth='public')
|
||||
def slide_set_uncompleted(self, slide_id):
|
||||
if request.website.is_public_user():
|
||||
return {'error': 'public_user'}
|
||||
fetch_res = self._fetch_slide(slide_id)
|
||||
if fetch_res.get('error'):
|
||||
return fetch_res
|
||||
self._slide_mark_uncompleted(fetch_res['slide'])
|
||||
return {
|
||||
'channel_completion': fetch_res['slide'].channel_id.completion,
|
||||
}
|
||||
|
||||
@http.route('/slides/slide/like', type='json', auth="public", website=True)
|
||||
def slide_like(self, slide_id, upvote):
|
||||
if request.website.is_public_user():
|
||||
@@ -1038,8 +1065,7 @@ class WebsiteSlides(WebsiteProfile):
|
||||
rank_progress = {}
|
||||
if not user_bad_answers:
|
||||
rank_progress['previous_rank'] = self._get_rank_values(request.env.user)
|
||||
slide._action_set_quiz_done()
|
||||
slide.action_set_completed()
|
||||
slide._action_mark_completed()
|
||||
rank_progress['new_rank'] = self._get_rank_values(request.env.user)
|
||||
rank_progress.update({
|
||||
'description': request.env.user.rank_id.description,
|
||||
|
||||
@@ -60,16 +60,25 @@ class ChannelUsersRelation(models.Model):
|
||||
mapped_data[item['channel_id'][0]][item['partner_id'][0]] = item['__count']
|
||||
|
||||
completed_records = self.env['slide.channel.partner']
|
||||
uncompleted_records = self.env['slide.channel.partner']
|
||||
for record in self:
|
||||
record.completed_slides_count = mapped_data.get(record.channel_id.id, dict()).get(record.partner_id.id, 0)
|
||||
record.completion = 100.0 if record.completed else round(100.0 * record.completed_slides_count / (record.channel_id.total_slides or 1))
|
||||
if not record.completed and record.channel_id.active and record.completed_slides_count >= record.channel_id.total_slides:
|
||||
|
||||
if not record.channel_id.active:
|
||||
continue
|
||||
elif not record.completed and record.completed_slides_count >= record.channel_id.total_slides:
|
||||
completed_records += record
|
||||
elif record.completed and record.completed_slides_count < record.channel_id.total_slides:
|
||||
uncompleted_records += record
|
||||
|
||||
if completed_records:
|
||||
completed_records._set_as_completed()
|
||||
completed_records._set_as_completed(completed=True)
|
||||
completed_records._send_completed_mail()
|
||||
|
||||
if uncompleted_records:
|
||||
uncompleted_records._set_as_completed(completed=False)
|
||||
|
||||
def unlink(self):
|
||||
"""
|
||||
Override unlink method :
|
||||
@@ -87,11 +96,16 @@ class ChannelUsersRelation(models.Model):
|
||||
self.env['slide.slide.partner'].search(removed_slide_partner_domain).unlink()
|
||||
return super(ChannelUsersRelation, self).unlink()
|
||||
|
||||
def _set_as_completed(self):
|
||||
""" Set record as completed and compute karma gains """
|
||||
def _set_as_completed(self, completed=True):
|
||||
""" Set record as completed and compute karma gains
|
||||
|
||||
:param completed:
|
||||
True if we make the slide as completed
|
||||
False if we remove user completion
|
||||
"""
|
||||
partner_karma = dict.fromkeys(self.mapped('partner_id').ids, 0)
|
||||
for record in self:
|
||||
record.completed = True
|
||||
record.completed = completed
|
||||
partner_karma[record.partner_id.id] += record.channel_id.karma_gen_channel_finish
|
||||
|
||||
partner_karma = {
|
||||
@@ -102,7 +116,11 @@ class ChannelUsersRelation(models.Model):
|
||||
if partner_karma:
|
||||
users = self.env['res.users'].sudo().search([('partner_id', 'in', list(partner_karma.keys()))])
|
||||
for user in users:
|
||||
users.add_karma(partner_karma[user.partner_id.id])
|
||||
karma = partner_karma[user.partner_id.id]
|
||||
if not completed:
|
||||
# Mark the channel as not-completed, we remove the gained karma
|
||||
karma *= -1
|
||||
users.add_karma(karma)
|
||||
|
||||
def _send_completed_mail(self):
|
||||
""" Send an email to the attendee when they have successfully completed a course. """
|
||||
@@ -376,7 +394,7 @@ class Channel(models.Model):
|
||||
for record in self:
|
||||
record.rating_avg_stars = record.rating_avg
|
||||
|
||||
@api.depends('slide_partner_ids', 'total_slides')
|
||||
@api.depends('slide_partner_ids', 'slide_partner_ids.completed', 'total_slides')
|
||||
@api.depends_context('uid')
|
||||
def _compute_user_statistics(self):
|
||||
current_user_info = self.env['slide.channel.partner'].sudo().search(
|
||||
|
||||
@@ -54,16 +54,23 @@ class SlidePartnerRelation(models.Model):
|
||||
res = super().create(vals_list)
|
||||
completed = res.filtered('completed')
|
||||
if completed:
|
||||
completed._set_completed_callback()
|
||||
completed._recompute_completion()
|
||||
return res
|
||||
|
||||
def write(self, values):
|
||||
slides_completion_to_recompute = self.env['slide.slide.partner']
|
||||
if 'completed' in values:
|
||||
slides_completion_to_recompute = self.filtered(
|
||||
lambda slide_partner: slide_partner.completed != values['completed'])
|
||||
|
||||
res = super(SlidePartnerRelation, self).write(values)
|
||||
if values.get('completed'):
|
||||
self._set_completed_callback()
|
||||
|
||||
if slides_completion_to_recompute:
|
||||
slides_completion_to_recompute._recompute_completion()
|
||||
|
||||
return res
|
||||
|
||||
def _set_completed_callback(self):
|
||||
def _recompute_completion(self):
|
||||
self.env['slide.channel.partner'].search([
|
||||
('channel_id', 'in', self.channel_id.ids),
|
||||
('partner_id', 'in', self.partner_id.ids),
|
||||
@@ -140,6 +147,10 @@ class Slide(models.Model):
|
||||
quiz_third_attempt_reward = fields.Integer("Reward: third attempt", default=5,)
|
||||
quiz_fourth_attempt_reward = fields.Integer("Reward: every attempt after the third try", default=2)
|
||||
# content
|
||||
can_self_mark_completed = fields.Boolean('Can Mark Completed', compute='_compute_mark_complete_actions',
|
||||
help='The slide can be marked as completed even without opening it')
|
||||
can_self_mark_uncompleted = fields.Boolean('Can Mark Uncompleted', compute='_compute_mark_complete_actions',
|
||||
help='The slide can be marked as not completed and the progression')
|
||||
slide_category = fields.Selection([
|
||||
('infographic', 'Image'),
|
||||
('article', 'Article'),
|
||||
@@ -266,6 +277,24 @@ class Slide(models.Model):
|
||||
elif slide.category_id != current_category:
|
||||
slide.category_id = current_category.id
|
||||
|
||||
@api.depends('slide_category', 'question_ids', 'channel_id.is_member')
|
||||
@api.depends_context('uid')
|
||||
def _compute_mark_complete_actions(self):
|
||||
"""Determine if the slide can be marked as (un)completed.
|
||||
|
||||
We can't mark a slide with questions as completed manually because we need to
|
||||
complete the quiz first. But we can mark as uncompleted a slide with questions,
|
||||
and the answers will be reset, the karma removed, etc (see mark_uncompleted).
|
||||
"""
|
||||
for slide in self:
|
||||
slide.can_self_mark_uncompleted = slide.website_published and slide.channel_id.is_member
|
||||
slide.can_self_mark_completed = (
|
||||
slide.website_published
|
||||
and slide.channel_id.is_member
|
||||
and slide.slide_category != 'quiz'
|
||||
and not slide.question_ids
|
||||
)
|
||||
|
||||
@api.depends('question_ids')
|
||||
def _compute_questions_count(self):
|
||||
for slide in self:
|
||||
@@ -637,7 +666,7 @@ class Slide(models.Model):
|
||||
|
||||
if 'is_published' in values or 'active' in values:
|
||||
# if the slide is published/unpublished, recompute the completion for the partners
|
||||
self.slide_partner_ids._set_completed_callback()
|
||||
self.slide_partner_ids._recompute_completion()
|
||||
|
||||
return res
|
||||
|
||||
@@ -854,22 +883,25 @@ class Slide(models.Model):
|
||||
'quiz_attempts_count': 1 if quiz_attempts_inc else 0,
|
||||
'vote': 0} for new_slide in new_slides])
|
||||
|
||||
def action_set_completed(self):
|
||||
if any(not slide.channel_id.is_member for slide in self):
|
||||
def action_mark_completed(self):
|
||||
if any(not slide.can_self_mark_completed for slide in self):
|
||||
raise UserError(_('You cannot mark a slide as completed if you are not among its members.'))
|
||||
|
||||
return self._action_set_completed(self.env.user.partner_id)
|
||||
return self._action_mark_completed()
|
||||
|
||||
def _action_set_completed(self, target_partner):
|
||||
self_sudo = self.sudo()
|
||||
def _action_mark_completed(self):
|
||||
uncompleted_slides = self.filtered(lambda slide: not slide.user_has_completed)
|
||||
|
||||
target_partner = self.env.user.partner_id
|
||||
uncompleted_slides._action_set_quiz_done()
|
||||
SlidePartnerSudo = self.env['slide.slide.partner'].sudo()
|
||||
existing_sudo = SlidePartnerSudo.search([
|
||||
('slide_id', 'in', self.ids),
|
||||
('slide_id', 'in', uncompleted_slides.ids),
|
||||
('partner_id', '=', target_partner.id)
|
||||
])
|
||||
existing_sudo.write({'completed': True})
|
||||
|
||||
new_slides = self_sudo - existing_sudo.mapped('slide_id')
|
||||
new_slides = uncompleted_slides.sudo() - existing_sudo.mapped('slide_id')
|
||||
SlidePartnerSudo.create([{
|
||||
'slide_id': new_slide.id,
|
||||
'channel_id': new_slide.channel_id.id,
|
||||
@@ -877,16 +909,41 @@ class Slide(models.Model):
|
||||
'vote': 0,
|
||||
'completed': True} for new_slide in new_slides])
|
||||
|
||||
return True
|
||||
def action_mark_uncompleted(self):
|
||||
if any(not slide.can_self_mark_uncompleted for slide in self):
|
||||
raise UserError(_('You cannot mark a slide as uncompleted if you are not among its members.'))
|
||||
|
||||
def _action_set_quiz_done(self):
|
||||
if any(not slide.channel_id.is_member for slide in self):
|
||||
raise UserError(_('You cannot mark a slide quiz as completed if you are not among its members.'))
|
||||
completed_slides = self.filtered(lambda slide: slide.user_has_completed)
|
||||
|
||||
# Remove the Karma point gained
|
||||
completed_slides._action_set_quiz_done(completed=False)
|
||||
|
||||
self.env['slide.slide.partner'].sudo().search([
|
||||
('slide_id', 'in', completed_slides.ids),
|
||||
('partner_id', '=', self.env.user.partner_id.id),
|
||||
]).completed = False
|
||||
|
||||
def _action_set_quiz_done(self, completed=True):
|
||||
"""Add or remove karma point related to the quiz.
|
||||
|
||||
:param completed:
|
||||
True if the quiz will be marked as completed (karma will be increased)
|
||||
If set to False, we will remove the karma instead of increasing it,
|
||||
so that the user can take the quiz multiple times but not gain karma infinitely
|
||||
"""
|
||||
if any(not slide.channel_id.is_member or not slide.website_published for slide in self):
|
||||
raise UserError(
|
||||
_('You cannot mark a slide quiz as completed if you are not among its members.') if completed
|
||||
else _('You cannot mark a slide quiz as not completed if you are not among its members.')
|
||||
)
|
||||
|
||||
points = 0
|
||||
for slide in self:
|
||||
user_membership_sudo = slide.user_membership_id.sudo()
|
||||
if not user_membership_sudo or user_membership_sudo.completed or not user_membership_sudo.quiz_attempts_count:
|
||||
if not user_membership_sudo \
|
||||
or user_membership_sudo.completed == completed \
|
||||
or not user_membership_sudo.quiz_attempts_count \
|
||||
or not slide.question_ids:
|
||||
continue
|
||||
|
||||
gains = [slide.quiz_first_attempt_reward,
|
||||
@@ -895,6 +952,9 @@ class Slide(models.Model):
|
||||
slide.quiz_fourth_attempt_reward]
|
||||
points += gains[user_membership_sudo.quiz_attempts_count - 1] if user_membership_sudo.quiz_attempts_count <= len(gains) else gains[-1]
|
||||
|
||||
if not completed:
|
||||
points *= -1
|
||||
|
||||
return self.env.user.sudo().add_karma(points)
|
||||
|
||||
def action_view_embeds(self):
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import session from 'web.session';
|
||||
import { Quiz } from '@website_slides/js/slides_course_quiz';
|
||||
import { SlideCoursePage } from '@website_slides/js/slides_course_page';
|
||||
import Dialog from 'web.Dialog';
|
||||
import '@website_slides/js/slides_course_join';
|
||||
|
||||
@@ -114,7 +115,7 @@
|
||||
if (self.totalVideoTime && self.currentVideoTime > self.totalVideoTime - 30){
|
||||
clearInterval(self.tid);
|
||||
if (!self.slide.hasQuestion && !self.slide.completed){
|
||||
self.trigger_up('slide_to_complete', self.slide);
|
||||
self.trigger_up('slide_mark_completed', self.slide);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
@@ -216,7 +217,7 @@
|
||||
_onVideoTimeUpdate: async function (eventData) {
|
||||
if (eventData.seconds > (this.videoDuration - 30)) {
|
||||
if (!this.slide.hasQuestion && !this.slide.completed){
|
||||
this.trigger_up('slide_to_complete', this.slide);
|
||||
this.trigger_up('slide_mark_completed', this.slide);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,7 +235,7 @@
|
||||
*/
|
||||
var Sidebar = publicWidget.Widget.extend({
|
||||
events: {
|
||||
"click .o_wslides_fs_sidebar_list_item": '_onClickTab',
|
||||
'click .o_wslides_fs_sidebar_list_item .o_wslides_fs_slide_name': '_onClickTab',
|
||||
},
|
||||
init: function (parent, slideList, defaultSlide) {
|
||||
var result = this._super.apply(this, arguments);
|
||||
@@ -278,27 +279,6 @@
|
||||
this.set('slideEntry', this.slideEntries[currentIndex-1]);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Greens up the bullet when the slide is completed
|
||||
*
|
||||
* @public
|
||||
* @param {Integer} slideId
|
||||
*/
|
||||
setSlideCompleted: function (slideId) {
|
||||
var $elem = this.$('.fa-circle-thin[data-slide-id="'+slideId+'"]');
|
||||
$elem.removeClass('fa-circle-thin').addClass('fa-check text-success o_wslides_slide_completed');
|
||||
},
|
||||
/**
|
||||
* Updates the progressbar whenever a lesson is completed
|
||||
*
|
||||
* @public
|
||||
* @param {*} channelCompletion
|
||||
*/
|
||||
updateProgressbar: function (channelCompletion) {
|
||||
var completion = Math.min(100, channelCompletion);
|
||||
this.$('.progress-bar').css('width', completion + "%" );
|
||||
this.$('.o_wslides_progress_percentage').text(completion);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
@@ -341,7 +321,7 @@
|
||||
*/
|
||||
_onClickTab: function (ev) {
|
||||
ev.stopPropagation();
|
||||
var $elem = $(ev.currentTarget);
|
||||
const $elem = $(ev.currentTarget).closest('.o_wslides_fs_sidebar_list_item');
|
||||
if ($elem.data('canAccess') === 'True') {
|
||||
var isQuiz = $elem.data('isQuiz');
|
||||
var slideID = parseInt($elem.data('id'));
|
||||
@@ -486,16 +466,14 @@
|
||||
*
|
||||
* This widget is rendered sever side, and attached to the existing DOM.
|
||||
*/
|
||||
var Fullscreen = publicWidget.Widget.extend({
|
||||
events: {
|
||||
"click .o_wslides_fs_toggle_sidebar": '_onClickToggleSidebar',
|
||||
},
|
||||
custom_events: {
|
||||
var Fullscreen = SlideCoursePage.extend({
|
||||
events: _.extend({}, SlideCoursePage.prototype.events, {
|
||||
'click .o_wslides_fs_toggle_sidebar': '_onClickToggleSidebar',
|
||||
}),
|
||||
custom_events: _.extend({}, SlideCoursePage.prototype.custom_events, {
|
||||
'change_slide': '_onChangeSlideRequest',
|
||||
'slide_to_complete': '_onSlideToComplete',
|
||||
'slide_completed': '_onSlideCompleted',
|
||||
'slide_go_next': '_onSlideGoToNext',
|
||||
},
|
||||
}),
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} el
|
||||
@@ -579,12 +557,6 @@
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
_markAsCompleted: function (slideId, completion) {
|
||||
var slide = findSlide(this.slides, {id: slideId});
|
||||
slide.completed = true;
|
||||
this.sidebar.setSlideCompleted(slide.id);
|
||||
this.sidebar.updateProgressbar(completion);
|
||||
},
|
||||
/**
|
||||
* Extend the slide data list to add informations about rendering method, and other
|
||||
* specific values according to their slide_category.
|
||||
@@ -686,29 +658,6 @@
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
/**
|
||||
* Once the completion conditions are filled,
|
||||
* rpc call to set the the relation between the slide and the user as "completed"
|
||||
*
|
||||
* @private
|
||||
* @param {Integer} slideId: the id of slide to set as completed
|
||||
*/
|
||||
_setCompleted: function (slideId){
|
||||
var self = this;
|
||||
var slide = findSlide(this.slides, {id: slideId});
|
||||
if (!slide.completed) { // no useless RPC call
|
||||
return this._rpc({
|
||||
route: '/slides/slide/set_completed',
|
||||
params: {
|
||||
slide_id: slide.id,
|
||||
}
|
||||
}).then(function (data){
|
||||
self._markAsCompleted(slideId, data.channel_completion);
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -737,9 +686,9 @@
|
||||
if (slide._autoSetDone && !session.is_website_user) { // no useless RPC call
|
||||
if (slide.category === 'document') {
|
||||
// only set the slide as completed after iFrame is loaded to avoid concurrent execution with 'embedUrl' controller
|
||||
self.el.querySelector('iframe.o_wslides_iframe_viewer').addEventListener('load', () => self._setCompleted(slide.id));
|
||||
self.el.querySelector('iframe.o_wslides_iframe_viewer').addEventListener('load', () => self._toggleSlideCompleted(slide));
|
||||
} else {
|
||||
return self._setCompleted(slide.id);
|
||||
return self._toggleSlideCompleted(slide);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -760,25 +709,35 @@
|
||||
this.shareButton._onChangeSlide(newSlide);
|
||||
},
|
||||
/**
|
||||
* Triggered when subwidget has mark the slide as done, and the UI need to be adapted.
|
||||
* After a slide has been marked as completed / uncompleted, update the state
|
||||
* of this widget and reload the slide if needed (e.g. to re-show the questions
|
||||
* of a quiz).
|
||||
*
|
||||
* We might need to set multiple slide as completed, because of "isQuiz"
|
||||
* set to True / False
|
||||
*
|
||||
* @private
|
||||
* @param {Object} slide: slide to set as completed
|
||||
* @param {Boolean} completed: true to mark the slide as completed
|
||||
* false to mark the slide as not completed
|
||||
*/
|
||||
_onSlideCompleted: function (ev) {
|
||||
var slide = ev.data.slide;
|
||||
var completion = ev.data.completion;
|
||||
this._markAsCompleted(slide.id, completion);
|
||||
},
|
||||
/**
|
||||
* Triggered when sub widget business is done and that slide
|
||||
* can now be marked as done.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onSlideToComplete: function (ev) {
|
||||
if (!session.is_website_user) { // no useless RPC call
|
||||
var slideId = ev.data.id;
|
||||
this._setCompleted(slideId);
|
||||
_toggleSlideCompleted: async function (slide, completed = true) {
|
||||
await this._super(...arguments);
|
||||
|
||||
const slideMatch = _.matcher({id: slide.id});
|
||||
const fsSlides = _.filter(this.slides, slideMatch);
|
||||
|
||||
fsSlides.forEach(slide => slide.completed = completed);
|
||||
|
||||
const currentSlide = this.get('slide');
|
||||
if (currentSlide.id === slide.id) {
|
||||
currentSlide.completed = completed;
|
||||
this.set('slide', currentSlide);
|
||||
|
||||
if ((currentSlide.hasQuestion || currentSlide.type === 'quiz') && !completed) {
|
||||
// Reload the quiz
|
||||
this._renderSlide();
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import publicWidget from 'web.public.widget';
|
||||
import session from 'web.session';
|
||||
import { qweb as QWeb } from 'web.core';
|
||||
|
||||
|
||||
/**
|
||||
* Global widget for both fullscreen view and non-fullscreen view of a slide course.
|
||||
* Contains general methods to update the UI elements (progress bar, sidebar...) as well
|
||||
* as method to mark the slide as completed / uncompleted.
|
||||
*/
|
||||
export const SlideCoursePage = publicWidget.Widget.extend({
|
||||
events: {
|
||||
'click button.o_wslides_button_complete': '_onClickComplete',
|
||||
},
|
||||
|
||||
custom_events: {
|
||||
'slide_completed': '_onSlideCompleted',
|
||||
'slide_mark_completed': '_onSlideMarkCompleted',
|
||||
},
|
||||
|
||||
xmlDependencies: ['/website_slides/static/src/xml/website_slides_sidebar.xml'],
|
||||
|
||||
/**
|
||||
* Greens up the bullet when the slide is completed
|
||||
*
|
||||
* @public
|
||||
* @param {Object} slide
|
||||
* @param {Boolean} completed
|
||||
*/
|
||||
toggleCompletionButton: function (slide, completed = true) {
|
||||
const $button = this.$(`.o_wslides_sidebar_done_button[data-id="${slide.id}"]`);
|
||||
|
||||
if (!$button.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newButton = QWeb.render('website.slides.sidebar.done.button', {
|
||||
slideId: slide.id,
|
||||
slideCompleted: completed,
|
||||
canSelfMarkUncompleted: slide.canSelfMarkUncompleted,
|
||||
canSelfMarkCompleted: slide.canSelfMarkCompleted,
|
||||
isMember: slide.isMember,
|
||||
});
|
||||
$button.replaceWith(newButton);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the progressbar whenever a lesson is completed
|
||||
*
|
||||
* @public
|
||||
* @param {Integer} channelCompletion
|
||||
*/
|
||||
updateProgressbar: function (channelCompletion) {
|
||||
const completion = Math.min(100, channelCompletion);
|
||||
|
||||
const $completed = $('.o_wslides_channel_completion_completed');
|
||||
const $progressbar = $('.o_wslides_channel_completion_progressbar');
|
||||
|
||||
if (completion < 100) {
|
||||
// Hide the "Completed" text and show the progress bar
|
||||
$completed.addClass('d-none');
|
||||
$progressbar.removeClass('d-none').addClass('d-flex');
|
||||
} else {
|
||||
// Hide the progress bar and show the "Completed" text
|
||||
$completed.removeClass('d-none');
|
||||
$progressbar.addClass('d-none').removeClass('d-flex');
|
||||
}
|
||||
|
||||
$progressbar.find('.progress-bar').css('width', `${completion}%`);
|
||||
$progressbar.find('.o_wslides_progress_percentage').text(completion);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Once the completion conditions are filled,
|
||||
* rpc call to set the relation between the slide and the user as "completed"
|
||||
*
|
||||
* @private
|
||||
* @param {Object} slide: slide to set as completed
|
||||
* @param {Boolean} completed: true to mark the slide as completed
|
||||
* false to mark the slide as not completed
|
||||
*/
|
||||
_toggleSlideCompleted: async function (slide, completed = true) {
|
||||
if (!!slide.completed === !!completed || !slide.isMember) {
|
||||
// no useless RPC call
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await this._rpc({
|
||||
route: `/slides/slide/${completed ? 'set_completed' : 'set_uncompleted'}`,
|
||||
params: {slide_id: slide.id},
|
||||
});
|
||||
|
||||
this.toggleCompletionButton(slide, completed);
|
||||
this.updateProgressbar(data.channel_completion);
|
||||
},
|
||||
/**
|
||||
* Retrieve the slide data corresponding to the slide id given in argument.
|
||||
* This method used the "slide_sidebar_done_button" template.
|
||||
*
|
||||
* @private
|
||||
* @param {Integer} slideId
|
||||
*/
|
||||
_getSlide: function (slideId) {
|
||||
return $(`.o_wslides_sidebar_done_button[data-id="${slideId}"]`).data();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handler
|
||||
//--------------------------------------------------------------------------
|
||||
/**
|
||||
* We clicked on the "done" button.
|
||||
* It will make a RPC call to update the slide state and update the UI.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onClickComplete: function (ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
const $button = $(ev.currentTarget).closest('.o_wslides_sidebar_done_button');
|
||||
|
||||
const slideData = $button.data();
|
||||
const isCompleted = Boolean(slideData.completed);
|
||||
|
||||
this._toggleSlideCompleted(slideData, !isCompleted);
|
||||
},
|
||||
|
||||
/**
|
||||
* The slide has been completed, update the UI
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onSlideCompleted: function (ev) {
|
||||
const slideId = ev.data.slideId;
|
||||
const completed = ev.data.completed;
|
||||
const slide = this._getSlide(slideId);
|
||||
if (slide) {
|
||||
// Just joined the course (e.g. When "Submit & Join" action), update the UI
|
||||
this.toggleCompletionButton(slide, completed);
|
||||
}
|
||||
this.updateProgressbar(ev.data.channelCompletion);
|
||||
},
|
||||
|
||||
/**
|
||||
* Make a RPC call to complete the slide then update the UI
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onSlideMarkCompleted: function (ev) {
|
||||
if (!session.is_website_user) { // no useless RPC call
|
||||
const slide = this._getSlide(ev.data.id);
|
||||
this._toggleSlideCompleted(slide, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -8,6 +8,7 @@
|
||||
import CourseJoin from '@website_slides/js/slides_course_join';
|
||||
import QuestionFormWidget from '@website_slides/js/slides_course_quiz_question_form';
|
||||
import SlideQuizFinishModal from '@website_slides/js/slides_course_quiz_finish';
|
||||
import { SlideCoursePage } from '@website_slides/js/slides_course_page';
|
||||
|
||||
import SlideEnroll from '@website_slides/js/slides_course_enroll_email';
|
||||
|
||||
@@ -197,6 +198,7 @@
|
||||
'slide_id': self.slide.id,
|
||||
}
|
||||
}).then(function (quiz_data) {
|
||||
self.slide.sessionAnswers = quiz_data.session_answers;
|
||||
self.quiz = {
|
||||
questions: quiz_data.slide_questions || [],
|
||||
questionsCount: quiz_data.slide_questions.length,
|
||||
@@ -375,7 +377,11 @@
|
||||
userId: this.userId
|
||||
}).open();
|
||||
this.slide.completed = true;
|
||||
this.trigger_up('slide_completed', {slide: this.slide, completion});
|
||||
this.trigger_up('slide_completed', {
|
||||
slideId: this.slide.id,
|
||||
channelCompletion: completion,
|
||||
completed: true,
|
||||
});
|
||||
}
|
||||
this._hideEditOptions();
|
||||
this._renderAnswersHighlightingAndComments();
|
||||
@@ -480,16 +486,15 @@
|
||||
});
|
||||
},
|
||||
/**
|
||||
* After joining the course, we immediately submit the quiz and get the correction.
|
||||
* This allows a smooth onboarding when the user is logged in and the course is public.
|
||||
* After joining the course, we save the questions in the session
|
||||
* and reload the page to update the view.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_afterJoin: function () {
|
||||
this.isMember = true;
|
||||
this._renderValidationInfo();
|
||||
this._applySessionAnswers();
|
||||
this._submitQuiz();
|
||||
this._saveQuizAnswersToSession().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -679,12 +684,11 @@
|
||||
}
|
||||
});
|
||||
|
||||
publicWidget.registry.websiteSlidesQuizNoFullscreen = publicWidget.Widget.extend({
|
||||
publicWidget.registry.websiteSlidesQuizNoFullscreen = SlideCoursePage.extend({
|
||||
selector: '.o_wslides_lesson_main', // selector of complete page, as we need slide content and aside content table
|
||||
custom_events: {
|
||||
custom_events: _.extend({}, SlideCoursePage.prototype.custom_events, {
|
||||
slide_go_next: '_onQuizNextSlide',
|
||||
slide_completed: '_onQuizCompleted',
|
||||
},
|
||||
}),
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Public
|
||||
@@ -695,36 +699,32 @@
|
||||
* @param {Object} parent
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
this.quizWidgets = [];
|
||||
var defs = [this._super.apply(this, arguments)];
|
||||
this.$('.o_wslides_js_lesson_quiz').each(function () {
|
||||
var slideData = $(this).data();
|
||||
var channelData = self._extractChannelData(slideData);
|
||||
const ret = this._super(...arguments);
|
||||
|
||||
const $quiz = this.$('.o_wslides_js_lesson_quiz');
|
||||
if ($quiz.length) {
|
||||
const slideData = $quiz.data();
|
||||
const channelData = this._extractChannelData(slideData);
|
||||
slideData.quizData = {
|
||||
questions: self._extractQuestionsAndAnswers(),
|
||||
questions: this._extractQuestionsAndAnswers(),
|
||||
sessionAnswers: slideData.sessionAnswers || [],
|
||||
quizKarmaMax: slideData.quizKarmaMax,
|
||||
quizKarmaWon: slideData.quizKarmaWon || 0,
|
||||
quizKarmaGain: slideData.quizKarmaGain,
|
||||
quizAttemptsCount: slideData.quizAttemptsCount,
|
||||
};
|
||||
defs.push(new Quiz(self, slideData, channelData, slideData.quizData).attachTo($(this)));
|
||||
});
|
||||
return Promise.all(defs);
|
||||
|
||||
this.quiz = new Quiz(this, slideData, channelData, slideData.quizData);
|
||||
this.quiz.attachTo($quiz);
|
||||
} else {
|
||||
this.quiz = null;
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Handlers
|
||||
//---------------------------------------------------------------------
|
||||
_onQuizCompleted: function (ev) {
|
||||
var slide = ev.data.slide;
|
||||
var completion = ev.data.completion;
|
||||
this.$('#o_wslides_lesson_aside_slide_check_' + slide.id).addClass('text-success fa-check').removeClass('text-600 fa-circle-o');
|
||||
// need to use global selector as progress bar is outside this animation widget scope
|
||||
$('.o_wslides_lesson_header .progress-bar').css('width', completion + "%");
|
||||
$('.o_wslides_lesson_header .progress span').text(_.str.sprintf("%s %%", completion));
|
||||
},
|
||||
_onQuizNextSlide: function () {
|
||||
var url = this.$('.o_wslides_js_lesson_quiz').data('next-slide-url');
|
||||
window.location.replace(url);
|
||||
@@ -734,6 +734,63 @@
|
||||
// Private
|
||||
//---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get the slide data from the elements in the DOM.
|
||||
*
|
||||
* We need this overwrite because a documentation in non-fullscreen view
|
||||
* doesn't have the standard "done" button and so in that case the slide
|
||||
* data can not be retrieved.
|
||||
*
|
||||
* @override
|
||||
* @param {Integer} slideId
|
||||
*/
|
||||
_getSlide: function (slideId) {
|
||||
const slide = this._super(...arguments);
|
||||
if (slide) {
|
||||
return slide;
|
||||
}
|
||||
// A quiz in a documentation on non fullscreen view
|
||||
return $(`.o_wslides_js_lesson_quiz[data-id="${slideId}"`).data();
|
||||
},
|
||||
|
||||
/**
|
||||
* After a slide has been marked as completed / uncompleted, update the state
|
||||
* of this widget and reload the slide if needed (e.g. to re-show the questions
|
||||
* of a quiz).
|
||||
*
|
||||
* @override
|
||||
* @param {Object} slide
|
||||
* @param {Boolean} completed
|
||||
*/
|
||||
toggleCompletionButton: function (slide, completed = true) {
|
||||
this._super(...arguments);
|
||||
|
||||
if (this.quiz && this.quiz.slide.id === slide.id && !completed && this.quiz.quiz.questionsCount) {
|
||||
// The quiz has been marked as "Not Done", re-load the questions
|
||||
this.quiz.quiz.answers = null;
|
||||
this.quiz.quiz.sessionAnswers = null;
|
||||
this.quiz.slide.completed = false;
|
||||
this.quiz._fetchQuiz().then(() => {
|
||||
this.quiz.renderElement();
|
||||
this.quiz._renderValidationInfo();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// The quiz has been submitted in a documentation and in non fullscreen view,
|
||||
// should update the button "Mark Done" to "Mark To Do"
|
||||
const $doneButton = $('.o_wslides_done_button');
|
||||
if ($doneButton.length && completed) {
|
||||
$doneButton
|
||||
.removeClass('o_wslides_done_button disabled btn-primary text-white')
|
||||
.addClass('o_wslides_undone_button btn-light')
|
||||
.text(_t('Mark To Do'))
|
||||
.removeAttr('title')
|
||||
.removeAttr('aria-disabled')
|
||||
.attr('href', `/slides/slide/${slide.id}/set_uncompleted`);
|
||||
}
|
||||
},
|
||||
|
||||
_extractChannelData: function (slideData) {
|
||||
return {
|
||||
channelId: slideData.channelId,
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
|
||||
import publicWidget from 'web.public.widget';
|
||||
import { _t } from 'web.core';
|
||||
import { SlideCoursePage } from '@website_slides/js/slides_course_page';
|
||||
|
||||
publicWidget.registry.websiteSlidesCourseSlidesList = publicWidget.Widget.extend({
|
||||
publicWidget.registry.websiteSlidesCourseSlidesList = SlideCoursePage.extend({
|
||||
selector: '.o_wslides_slides_list',
|
||||
xmlDependencies: ['/website_slides/static/src/xml/website_slides_upload.xml'],
|
||||
|
||||
xmlDependencies: SlideCoursePage.prototype.xmlDependencies.concat([
|
||||
'/website_slides/static/src/xml/website_slides_upload.xml',
|
||||
]),
|
||||
|
||||
start: function () {
|
||||
this._super.apply(this,arguments);
|
||||
|
||||
@@ -115,6 +115,13 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
button.o_wslides_button_complete i {
|
||||
color: $gray-400;
|
||||
}
|
||||
div.o_wslides_button_complete i {
|
||||
color: $gray-600;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-open {
|
||||
|
||||
@@ -133,6 +133,13 @@ $truncate-limits: 2, 3, 10;
|
||||
}
|
||||
}
|
||||
|
||||
.o_wslides_nav_button.disabled {
|
||||
// show the "not-allowed" cursor while keeping the disabled style
|
||||
cursor: not-allowed;
|
||||
pointer-events: auto !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.o_wslides_js_lesson_quiz {
|
||||
i.o_wslides_js_quiz_icon {
|
||||
cursor: pointer;
|
||||
@@ -215,6 +222,10 @@ $truncate-limits: 2, 3, 10;
|
||||
background-color: $o-wslides-color-bg; // same as body
|
||||
}
|
||||
|
||||
.o_wslides_channel_completion_completed {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
// New home page
|
||||
// **************************************************
|
||||
.o_wslides_home_main {
|
||||
@@ -503,10 +514,15 @@ $truncate-limits: 2, 3, 10;
|
||||
&.active {
|
||||
box-shadow:inset 2px 0 theme-color('primary');
|
||||
}
|
||||
|
||||
&:hover .o_wslides_lesson_link_name {
|
||||
color: $headings-color;
|
||||
}
|
||||
}
|
||||
.o_wslides_button_complete {
|
||||
margin-left: 5px;
|
||||
}
|
||||
button.o_wslides_button_complete i {
|
||||
color: $gray-600;
|
||||
}
|
||||
div.o_wslides_button_complete i {
|
||||
color: $gray-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -526,8 +542,20 @@ $truncate-limits: 2, 3, 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_wslides_undone_button.disabled,
|
||||
.o_wslides_done_button.disabled {
|
||||
// Show title attribute on disabled link
|
||||
color: white !important;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce the height of the lesson header, so the DOM element size do not change
|
||||
// when the completion progressbar become visible / hidden
|
||||
.o_wslides_lesson_header_container {
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
// Modals
|
||||
// **************************************************
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<!-- JS Equivalent of the Python template "slide_sidebar_done_button" -->
|
||||
<t t-name="website.slides.sidebar.done.button">
|
||||
<div class="o_wslides_sidebar_done_button"
|
||||
t-att-data-id="slideId"
|
||||
t-att-data-completed="slideCompleted"
|
||||
t-att-data-can-self-mark-completed="canSelfMarkCompleted"
|
||||
t-att-data-can-self-mark-uncompleted="canSelfMarkUncompleted"
|
||||
t-att-data-is-member="isMember"> <!-- The template is only used for members -->
|
||||
<button class="o_wslides_button_complete btn btn-sm" t-if="slideCompleted and canSelfMarkUncompleted or !slideCompleted and canSelfMarkCompleted">
|
||||
<i t-if="slideCompleted" class="o_wslides_slide_completed fa fa-check-circle fa-fw text-success fa-lg" t-att-data-slide-id="slideId" title="Mark as not done"/>
|
||||
<i t-if="!slideCompleted" class="fa fa-circle-thin fa-fw fa-lg" t-att-data-slide-id="slideId" title="Mark as done"/>
|
||||
</button>
|
||||
<div class="o_wslides_button_complete btn-sm" t-else="">
|
||||
<i t-if="slideCompleted" class="o_wslides_slide_completed fa fa-check fa-fw text-success fa-lg" t-att-data-slide-id="slideId" title="Can not be marked as not done"/>
|
||||
<i t-if="!slideCompleted" class="fa fa-circle-thin fa-fw fa-lg" t-att-data-slide-id="slideId" title="Can not be marked as done"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -28,8 +28,10 @@ tour.register('course_member', {
|
||||
},
|
||||
// eLearning: follow course by cliking on first lesson and going to fullscreen player
|
||||
{
|
||||
trigger: '.o_wslides_fs_sidebar_list_item div:contains("Home Gardening")'
|
||||
}, {
|
||||
trigger: '.o_wslides_fs_slide_name:contains("Home Gardening")',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
trigger: '.o_wslides_fs_sidebar_header',
|
||||
run: function () {
|
||||
// check navigation with arrow keys
|
||||
@@ -87,7 +89,8 @@ tour.register('course_member', {
|
||||
extra_trigger: '.o_wslides_progress_percentage:contains("60")',
|
||||
run: function () {} // check that previous step succeeded
|
||||
}, {
|
||||
trigger: '.o_wslides_fs_sidebar_list_item div:contains("How to Grow and Harvest The Best Strawberries | Basics")'
|
||||
trigger: '.o_wslides_fs_slide_name:contains("How to Grow and Harvest The Best Strawberries | Basics")',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: '.o_wslides_fs_sidebar_section_slides li:contains("How to Grow and Harvest The Best Strawberries | Basics") .o_wslides_slide_completed',
|
||||
run: function () {} // check that video slide is marked as 'done'
|
||||
@@ -97,7 +100,8 @@ tour.register('course_member', {
|
||||
},
|
||||
// eLearning: last slide is a quiz, complete it
|
||||
{
|
||||
trigger: '.o_wslides_fs_sidebar_list_item div:contains("Test your knowledge")'
|
||||
trigger: '.o_wslides_fs_slide_name:contains("Test your knowledge")',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: '.o_wslides_js_lesson_quiz_question:first .list-group a:first'
|
||||
}, {
|
||||
|
||||
@@ -73,26 +73,39 @@ class TestKarmaGain(common.SlidesCase):
|
||||
self.assertEqual(user.karma, 0)
|
||||
|
||||
# Finish the Course
|
||||
self.slide.with_user(user).action_set_completed()
|
||||
self.slide.with_user(user).action_mark_completed()
|
||||
self.assertFalse(self.channel.with_user(user).completed)
|
||||
self.slide_2.with_user(user).action_set_completed()
|
||||
self.slide_2.with_user(user).action_mark_completed()
|
||||
|
||||
# answer a quizz question
|
||||
self.slide_3.with_user(user).action_set_viewed(quiz_attempts_inc=True)
|
||||
self.slide_3.with_user(user)._action_set_quiz_done()
|
||||
self.slide_3.with_user(user).action_set_completed()
|
||||
self.slide_3.with_user(user)._action_mark_completed()
|
||||
computed_karma += self.slide_3.quiz_first_attempt_reward
|
||||
computed_karma += self.channel.karma_gen_channel_finish
|
||||
self.assertTrue(self.channel.with_user(user).completed)
|
||||
self.assertEqual(user.karma, computed_karma)
|
||||
|
||||
# Mark the quiz as not completed
|
||||
self.slide_3.with_user(user).action_mark_uncompleted()
|
||||
computed_karma -= self.slide_3.quiz_first_attempt_reward
|
||||
computed_karma -= self.channel.karma_gen_channel_finish
|
||||
self.assertFalse(self.channel.with_user(user).completed)
|
||||
self.assertEqual(user.karma, computed_karma)
|
||||
|
||||
# Re-submit the quiz, we should consider it as the second attempt
|
||||
self.slide_3.with_user(user).action_set_viewed(quiz_attempts_inc=True)
|
||||
self.slide_3.with_user(user)._action_mark_completed()
|
||||
computed_karma += self.slide_3.quiz_second_attempt_reward
|
||||
computed_karma += self.channel.karma_gen_channel_finish
|
||||
self.assertTrue(self.channel.with_user(user).completed)
|
||||
self.assertEqual(user.karma, computed_karma)
|
||||
|
||||
# Begin then finish the second Course
|
||||
self.slide_2_0.with_user(user).action_set_completed()
|
||||
self.slide_2_0.with_user(user).action_mark_completed()
|
||||
self.assertFalse(self.channel_2.with_user(user).completed)
|
||||
self.assertEqual(user.karma, computed_karma)
|
||||
|
||||
self.slide_2_1.with_user(user).action_set_completed()
|
||||
self.slide_2_1.with_user(user).action_mark_completed()
|
||||
self.assertTrue(self.channel_2.with_user(user).completed)
|
||||
computed_karma += self.channel_2.karma_gen_channel_finish
|
||||
self.assertEqual(user.karma, computed_karma)
|
||||
@@ -128,7 +141,7 @@ class TestKarmaGain(common.SlidesCase):
|
||||
# Leave the finished course
|
||||
self.channel._remove_membership(user.partner_id.ids)
|
||||
computed_karma -= self.channel.karma_gen_channel_finish
|
||||
computed_karma -= self.slide_3.quiz_first_attempt_reward
|
||||
computed_karma -= self.slide_3.quiz_second_attempt_reward
|
||||
self.assertEqual(user.karma, computed_karma)
|
||||
|
||||
@mute_logger('odoo.models')
|
||||
@@ -142,5 +155,5 @@ class TestKarmaGain(common.SlidesCase):
|
||||
(self.channel | self.channel_2)._action_add_members(user.partner_id)
|
||||
|
||||
computed_karma += self.channel.karma_gen_channel_finish + self.channel_2.karma_gen_channel_finish
|
||||
(self.slide | self.slide_2 | self.slide_3 | self.slide_2_0 | self.slide_2_1).with_user(user).action_set_completed()
|
||||
(self.slide | self.slide_2 | self.slide_3 | self.slide_2_0 | self.slide_2_1).with_user(user)._action_mark_completed()
|
||||
self.assertEqual(user.karma, computed_karma)
|
||||
|
||||
@@ -25,7 +25,7 @@ class TestChannelStatistics(common.SlidesCase):
|
||||
|
||||
channel_aspublisher = self.channel.with_user(self.user_officer)
|
||||
self.assertTrue(channel_aspublisher.partner_has_new_content)
|
||||
(self.slide | self.slide_2).with_user(self.user_officer).action_set_completed()
|
||||
(self.slide | self.slide_2).with_user(self.user_officer).action_mark_completed()
|
||||
self.assertFalse(channel_aspublisher.partner_has_new_content)
|
||||
|
||||
channel_aspublisher._action_add_members(self.user_portal.partner_id)
|
||||
@@ -68,14 +68,14 @@ class TestChannelStatistics(common.SlidesCase):
|
||||
slides_emp.action_set_viewed()
|
||||
self.assertEqual(channel_emp.completion, 0)
|
||||
|
||||
slides_emp.action_set_completed()
|
||||
slides_emp.action_mark_completed()
|
||||
channel_emp.invalidate_cache()
|
||||
self.assertEqual(
|
||||
channel_emp.completion,
|
||||
math.ceil(100.0 * len(slides_emp) / len(channel_publisher.slide_content_ids)))
|
||||
self.assertFalse(channel_emp.completed)
|
||||
|
||||
self.slide_3.with_user(self.user_emp).action_set_completed()
|
||||
self.slide_3.with_user(self.user_emp)._action_mark_completed()
|
||||
self.assertEqual(channel_emp.completion, 100)
|
||||
self.assertTrue(channel_emp.completed)
|
||||
|
||||
@@ -96,7 +96,7 @@ class TestChannelStatistics(common.SlidesCase):
|
||||
slides_emp = slides.with_user(self.user_emp)
|
||||
slides_emp.read(['name'])
|
||||
with self.assertRaises(UserError):
|
||||
slides_emp.action_set_completed()
|
||||
slides_emp.action_mark_completed()
|
||||
|
||||
@mute_logger('odoo.models')
|
||||
def test_channel_user_statistics_view_check_member(self):
|
||||
|
||||
@@ -299,8 +299,7 @@
|
||||
t-att-data-channel-id="channel.id"
|
||||
t-att-data-channel-enroll="channel.enroll">
|
||||
<span class="cta-title text_small_caps">
|
||||
<t t-if="channel.channel_type == 'documentation'">Start Course</t>
|
||||
<t t-else="">Join Course</t>
|
||||
Join Course
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -339,17 +338,17 @@
|
||||
<i class="fa fa-times"/>
|
||||
</button>
|
||||
<div class="d-flex align-items-center pt-3">
|
||||
<t t-if="channel.completed">
|
||||
<span class="badge badge-pill badge-success py-1 px-2 mx-auto" style="font-size: 1em"><i class="fa fa-check"/> Completed</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-attf-class="o_wslides_channel_completion_completed badge badge-pill badge-success py-1 px-2 mx-auto #{'d-none' if not channel.completed else ''}">
|
||||
<i class="fa fa-check"/> Completed
|
||||
</span>
|
||||
<div t-attf-class="o_wslides_channel_completion_progressbar #{'d-none' if channel.completed else 'd-flex'} w-100 align-items-center">
|
||||
<div class="progress flex-grow-1 bg-black-50" style="height: 6px;">
|
||||
<div class="progress-bar" role="progressbar" t-attf-style="width: #{channel.completion}%" t-att-aria-valuenow="channel.completion" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
<div class="ml-3 small">
|
||||
<span class="o_wslides_progress_percentage" t-esc="channel.completion"/> %
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
@@ -539,9 +538,9 @@
|
||||
</div>
|
||||
|
||||
<div t-if="channel.is_member or channel.can_publish" class="pt-2 pb-2 border-left ml-2 mr-2 pl-2 d-flex flex-row align-items-center o_wslides_slides_list_slide_controls">
|
||||
<t t-if="channel.is_member">
|
||||
<i t-if="not channel_progress[slide.id].get('completed')" class="check-done fa fa-circle-o text-500 px-2"></i>
|
||||
<i t-else="" class="check-done text-success fa fa-check-circle px-2"></i>
|
||||
<t t-call="website_slides.slide_sidebar_done_button">
|
||||
<t t-set="is_member" t-value="channel.is_member"/>
|
||||
<t t-set="slide_completed" t-value="channel_progress[slide.id].get('completed')"/>
|
||||
</t>
|
||||
<span t-if="channel.can_publish" class="d-none d-md-flex">
|
||||
<a t-if="slide.slide_category == 'article'" class="px-2 o_text_link text-primary" target="_blank" t-attf-href="/slides/slide/#{slug(slide)}?enable_editor=1"><span class="fa fa-pencil"/></a>
|
||||
|
||||
@@ -26,19 +26,18 @@
|
||||
<span class="font-weight-normal">Last update:</span>
|
||||
<t t-esc="slide.date_published" t-options="{'widget': 'date'}"/>
|
||||
</div>
|
||||
|
||||
<div t-else="" t-if="not slide.channel_id.completed" class="d-flex align-items-center pb-3">
|
||||
<div t-else="" t-attf-class="o_wslides_channel_completion_progressbar #{'d-none' if slide.channel_id.completed else 'd-flex'} align-items-center pb-3">
|
||||
<div class="progress w-50 bg-black-25" style="height: 10px;">
|
||||
<div class="progress-bar rounded-left" role="progressbar"
|
||||
t-att-aria-valuenow="slide.channel_id.completion" aria-valuemin="0" aria-valuemax="100"
|
||||
t-attf-style="width: #{slide.channel_id.completion}%;">
|
||||
</div>
|
||||
</div>
|
||||
<i t-att-class="'fa fa-trophy m-0 ml-2 p-0 %s' % ('text-warning' if slide.channel_id.completed else 'text-black-50')"></i>
|
||||
<small class="ml-2 text-white-50"><t t-esc="slide.channel_id.completion"/> %</small>
|
||||
<i class="fa fa-trophy m-0 ml-2 p-0 text-black-50"></i>
|
||||
<small class="ml-2 text-white-50"><span class="o_wslides_progress_percentage" t-esc="slide.channel_id.completion"/> %</small>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="slide.channel_id.completed" class="col-12 col-sm-3">
|
||||
<div t-attf-class="o_wslides_channel_completion_completed col-12 col-sm-3 #{'d-none' if not slide.channel_id.completed else ''}">
|
||||
<h2>
|
||||
<small><span class="badge badge-pill badge-success font-weight-normal"><i class="fa fa-check"/> Completed</span></small>
|
||||
</h2>
|
||||
@@ -136,7 +135,7 @@
|
||||
<t t-if="category" t-set="category" t-value="category.get('category')"/>
|
||||
<li class="o_wslides_fs_sidebar_section mt-2">
|
||||
<a t-att-href="('#collapse-%s') % (category.id if category else 0)" data-toggle="collapse" role="button" aria-expanded="true"
|
||||
class="o_wslides_lesson_aside_list_link pl-2 text-600 text-uppercase text-decoration-none py-1 small d-block"
|
||||
class="o_wslides_lesson_aside_list_link pl-2 text-600 text-uppercase text-decoration-none p-1 small d-block"
|
||||
t-att-aria-controls="('collapse-%s') % (category.id if category else 0)">
|
||||
<t t-if="category">
|
||||
<b t-field="category.name"/>
|
||||
@@ -151,23 +150,28 @@
|
||||
<t t-set="is_member" t-value="slide.channel_id.is_member"/>
|
||||
<t t-set="can_access" t-value="aside_slide.is_preview or is_member or slide.channel_id.can_publish"/>
|
||||
<li class="p-0 pb-1">
|
||||
<a t-att-href="'/slides/slide/%s' % (slug(aside_slide)) if can_access else '#'"
|
||||
t-att-class="'o_wslides_lesson_aside_list_link d-flex align-items-center px-2 py-1 text-decoration-none %s%s' % (('bg-100 active' if aside_slide == slide else ''), 'text-muted' if not can_access else '')">
|
||||
<i t-if="is_member" t-att-id="'o_wslides_lesson_aside_slide_check_%s' % (aside_slide.id)"
|
||||
t-att-class="'mr-2 fa text-left %s' % ('text-success fa-check-circle' if channel_progress[aside_slide.id].get('completed') else 'text-600 fa-circle')">
|
||||
</i>
|
||||
<t t-call="website_slides.slide_icon">
|
||||
<div t-att-class="'o_wslides_lesson_aside_list_link d-flex align-items-center p-1 %s%s' % (('bg-100 active' if aside_slide == slide else ''), 'text-muted' if not can_access else '')"
|
||||
t-att-data-id="slide.id"
|
||||
t-att-data-completed="slide_completed">
|
||||
<t t-call="website_slides.slide_sidebar_done_button">
|
||||
<t t-set="slide" t-value="aside_slide"/>
|
||||
<t t-set="slide_completed" t-value="channel_progress[aside_slide.id].get('completed')"/>
|
||||
</t>
|
||||
<div class="o_wslides_lesson_link_name text-truncate" t-att-title="aside_slide.name">
|
||||
<span t-esc="aside_slide.name" class="mr-2 text-truncate"/>
|
||||
</div>
|
||||
<div class="ml-auto" t-if="aside_slide.question_ids">
|
||||
<span t-att-class="'badge badge-pill %s' % ('badge-success' if channel_progress[aside_slide.id].get('completed') else 'badge-light text-600')">
|
||||
<t t-esc="channel_progress[aside_slide.id].get('quiz_karma_won') if channel_progress[aside_slide.id].get('completed') else channel_progress[aside_slide.id].get('quiz_karma_gain')"/> xp
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<a t-att-href="'/slides/slide/%s' % (slug(aside_slide)) if can_access else '#'"
|
||||
t-attf-class="d-flex align-items-center text-decoration-none mw-100 overflow-hidden ml-2">
|
||||
<t t-call="website_slides.slide_icon">
|
||||
<t t-set="slide" t-value="aside_slide"/>
|
||||
</t>
|
||||
<div class="o_wslides_lesson_link_name text-truncate" t-att-title="aside_slide.name">
|
||||
<span t-esc="aside_slide.name" class="mr-2 text-truncate"/>
|
||||
</div>
|
||||
<div class="ml-auto" t-if="aside_slide.question_ids">
|
||||
<span t-att-class="'badge badge-pill %s' % ('badge-success' if channel_progress[aside_slide.id].get('completed') else 'badge-light text-600')">
|
||||
<t t-esc="channel_progress[aside_slide.id].get('quiz_karma_won') if channel_progress[aside_slide.id].get('completed') else channel_progress[aside_slide.id].get('quiz_karma_gain')"/> xp
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<ul t-if="aside_slide.slide_resource_ids or aside_slide.question_ids" class="list-group px-2 mb-1 list-unstyled">
|
||||
<t t-foreach="aside_slide.slide_resource_ids" t-as="resource" t-if="resource.resource_type == 'url'">
|
||||
<li class="pl-4">
|
||||
@@ -179,9 +183,10 @@
|
||||
</span>
|
||||
</li>
|
||||
</t>
|
||||
<div class="o_wslides_js_course_join pl-4" t-if="aside_slide.slide_resource_ids">
|
||||
<t t-set="resource_file_ids" t-value="aside_slide.slide_resource_ids.filtered(lambda res: res.resource_type == 'file')"/>
|
||||
<div class="o_wslides_js_course_join pl-4" t-if="resource_file_ids">
|
||||
<t t-if="is_member or aside_slide.channel_id.can_publish">
|
||||
<li t-foreach="aside_slide.slide_resource_ids" t-as="resource" t-if="resource.resource_type == 'file'">
|
||||
<li t-foreach="resource_file_ids" t-as="resource">
|
||||
<a t-attf-href="/web/content/slide.slide.resource/#{resource.id}/data?download=true" class="text-decoration-none small">
|
||||
<i class="fa fa-download mr-1"/><span t-field="resource.name"/>
|
||||
</a>
|
||||
@@ -210,40 +215,65 @@
|
||||
|
||||
<!-- Slide: all its content, not fullscreen mode -->
|
||||
<template id="slide_content_detailed" name="Slide: Detailed Content">
|
||||
<div class="row align-items-center my-3">
|
||||
<div class="row align-items-start my-3 w-100">
|
||||
<t t-set="slide_completed" t-value="channel_progress[slide.id].get('completed')"/>
|
||||
<div class="col-12 col-md order-2 order-md-1 d-flex">
|
||||
<div class="d-flex align-items-center">
|
||||
<h1 class="h4 my-0">
|
||||
<div class="d-flex align-items-start">
|
||||
<h1 class="h4 my-0 d-flex flex_row">
|
||||
<t t-call="website_slides.slide_icon">
|
||||
<t t-set="icon_class" t-valuef="mr-1"/>
|
||||
<t t-set="icon_class" t-valuef="mr-2"/>
|
||||
</t>
|
||||
<span t-field="slide.name"/>
|
||||
</h1>
|
||||
<span t-if="slide.question_ids"
|
||||
t-att-class="'ml-2 badge %s' % ('badge-success' if channel_progress[slide.id].get('completed') else 'badge-info')">
|
||||
<span t-if="channel_progress[slide.id].get('completed')">
|
||||
<i class="fa fa-check-circle"/>
|
||||
<t t-esc="channel_progress[slide.id].get('quiz_karma_won', 0)"/>
|
||||
</span>
|
||||
<span t-else="" t-esc="channel_progress[slide.id].get('quiz_karma_gain', 0)"/>
|
||||
<span>XP</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md order-1 order-md-2 text-nowrap flex-grow-0 d-flex justify-content-end mb-3 mb-md-0">
|
||||
<div class="col-12 col-md order-1 order-md-2 text-nowrap flex-grow-0 d-flex justify-content-end align-items-center mb-3 mb-md-0">
|
||||
<t t-set="quiz_karma_won" t-value="channel_progress[slide.id].get('quiz_karma_won', 0)"/>
|
||||
<t t-set="quiz_karma_gain" t-value="channel_progress[slide.id].get('quiz_karma_gain', 0)"/>
|
||||
<span t-if="slide.question_ids and (slide_completed or quiz_karma_gain)"
|
||||
t-attf-class="mx-2 badge #{'badge-success' if slide_completed else 'badge-info'}">
|
||||
<span t-if="slide_completed">
|
||||
<i class="fa fa-check-circle"/>
|
||||
<t t-if="quiz_karma_won">
|
||||
<t t-esc="quiz_karma_won" />
|
||||
<span>XP</span>
|
||||
</t>
|
||||
</span>
|
||||
<t t-else="">
|
||||
<span t-esc="quiz_karma_gain"/>
|
||||
<span>XP</span>
|
||||
</t>
|
||||
</span>
|
||||
<div class="btn-group flex-grow-1 flex-sm-0" role="group" aria-label="Lesson Nav">
|
||||
<a t-att-class="'btn btn-light border %s' % ('disabled' if not previous_slide else '')"
|
||||
<a t-attf-class="o_wslides_nav_button btn btn-light border #{'disabled' if not previous_slide else ''} mr-2"
|
||||
role="button" t-att-aria-disabled="'disabled' if not previous_slide else None"
|
||||
t-att-href="'/slides/slide/%s' % (slug(previous_slide)) if previous_slide else '#'">
|
||||
<i class="fa fa-chevron-left mr-2"></i> <span class="d-none d-sm-inline-block">Prev</span>
|
||||
</a>
|
||||
<t t-set="allow_done_btn" t-value="slide.slide_category in ['infographic', 'document', 'article', 'video'] and not slide.question_ids and not channel_progress[slide.id].get('completed') and slide.channel_id.is_member"/>
|
||||
<a t-att-class="'btn btn-primary border text-white %s' % ('disabled' if not allow_done_btn else '')"
|
||||
role="button" t-att-aria-disabled="'true' if not allow_done_btn else None"
|
||||
t-att-href="'/slides/slide/%s/set_completed?%s' % (slide.id, 'next_slide_id=%s' % (next_slide.id) if next_slide else '') if allow_done_btn else '#'">
|
||||
Set Done
|
||||
</a>
|
||||
<a t-att-class="'btn btn-light border %s' % ('disabled' if not next_slide else '')"
|
||||
<t t-if="slide.channel_id.channel_type == 'documentation' and slide.channel_id.is_member">
|
||||
<t t-set="is_quiz" t-value="slide.slide_category == 'quiz' or slide.question_ids"/>
|
||||
<a t-if="slide_completed and slide.can_self_mark_uncompleted" role="button"
|
||||
class="o_wslides_undone_button btn btn-light border mr-2"
|
||||
t-attf-href="/slides/slide/#{slide.id}/set_uncompleted">
|
||||
Mark To Do
|
||||
</a>
|
||||
<a t-elif="not slide_completed and is_quiz" role="button"
|
||||
class="o_wslides_done_button btn btn-primary border text-white mr-2"
|
||||
href="#quiz_container">
|
||||
Take Quiz
|
||||
</a>
|
||||
<a t-else="not slide_completed and slide.can_self_mark_completed" role="button"
|
||||
class="o_wslides_done_button btn btn-primary border text-white mr-2"
|
||||
t-attf-href="/slides/slide/#{slide.id}/set_completed?next_slide_id=#{next_slide.id if next_slide else ''}">
|
||||
Mark Done
|
||||
</a>
|
||||
</t>
|
||||
<div t-if="slide.channel_id.channel_type == 'documentation' and not slide.channel_id.is_member" class="mr-2">
|
||||
<t t-call="website_slides.course_join">
|
||||
<t t-set="channel" t-value="slide.channel_id"/>
|
||||
</t>
|
||||
</div>
|
||||
<a t-attf-class="o_wslides_nav_button btn btn-light border #{'disabled' if not next_slide else ''}"
|
||||
role="button" t-att-aria-disabled="'disabled' if not next_slide else None"
|
||||
t-att-href="'/slides/slide/%s' % (slug(next_slide)) if next_slide else '#'">
|
||||
<span class="d-none d-sm-inline-block">Next</span> <i class="fa fa-chevron-right ml-2"></i>
|
||||
@@ -389,7 +419,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_wslides_js_quiz_container" t-att-data-slide-id="slide.id">
|
||||
<div class="o_wslides_js_quiz_container" t-att-data-slide-id="slide.id" id="quiz_container">
|
||||
<div class="row" t-if="slide.slide_category != 'certification'">
|
||||
<t t-if="slide.question_ids">
|
||||
<t t-call="website_slides.lesson_content_quiz"/>
|
||||
@@ -436,7 +466,9 @@
|
||||
t-att-data-name="slide.name"
|
||||
t-att-data-slide-category="slide.slide_category"
|
||||
t-att-data-is-member="slide.channel_id.is_member"
|
||||
t-att-data-completed="1 if slide_completed else 0"
|
||||
t-att-data-can-self-mark-completed="slide.can_self_mark_completed"
|
||||
t-att-data-can-self-mark-uncompleted="slide.can_self_mark_uncompleted"
|
||||
t-att-data-completed="slide_completed"
|
||||
t-att-data-quiz-attempts-count="quiz_attempts_count"
|
||||
t-att-data-quiz-karma-max="quiz_karma_max"
|
||||
t-att-data-quiz-karma-gain="quiz_karma_gain"
|
||||
|
||||
@@ -61,18 +61,18 @@
|
||||
<a class="h5 d-block mb-1" t-attf-href="/slides/#{slug(slide.channel_id)}">
|
||||
<span t-field="slide.channel_id.name"/>
|
||||
</a>
|
||||
<div t-if="not is_public_user" class="d-flex align-items-center">
|
||||
<t t-if="slide.channel_id.completed">
|
||||
<span class="badge badge-pill badge-success py-1 px-2" style="font-size: 1em"><i class="fa fa-check"/> Completed</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div t-if="not is_public_user">
|
||||
<span t-attf-class="o_wslides_channel_completion_completed badge badge-pill badge-success py-1 px-2 #{'d-none' if not slide.channel_id.completed else ''}">
|
||||
<i class="fa fa-check"/> Completed
|
||||
</span>
|
||||
<div t-attf-class="o_wslides_channel_completion_progressbar #{'d-none' if slide.channel_id.completed else 'd-flex'} w-100 align-items-center">
|
||||
<div class="progress flex-grow-1 bg-black-50" style="height: 6px;">
|
||||
<div class="progress-bar" role="progressbar" t-attf-style="width: #{slide.channel_id.completion}%" t-att-aria-valuenow="slide.channel_id.completion" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
<div class="ml-3 small">
|
||||
<span class="o_wslides_progress_percentage" t-esc="slide.channel_id.completion"/> %
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="mx-n3 list-unstyled my-0 pb-2 overflow-auto">
|
||||
@@ -105,7 +105,7 @@
|
||||
<t t-set="slide_completed" t-value="channel_progress[slide.id].get('completed')"/>
|
||||
<t t-set="is_member" t-value="current_slide.channel_id.is_member"/>
|
||||
<t t-set="can_access" t-value="slide.is_preview or is_member or current_slide.channel_id.can_publish"/>
|
||||
<li t-att-class="'o_wslides_fs_sidebar_list_item d-flex align-items-top py-1 %s' % ('active' if slide.id == current_slide.id else '')"
|
||||
<li t-attf-class="o_wslides_fs_sidebar_list_item d-flex align-items-center py-1 #{'active' if slide.id == current_slide.id else ''}"
|
||||
t-att-data-id="slide.id"
|
||||
t-att-data-can-access="can_access"
|
||||
t-att-data-name="slide.name"
|
||||
@@ -114,15 +114,14 @@
|
||||
t-att-data-slug="slug(slide)"
|
||||
t-att-data-has-question="1 if slide.question_ids else 0"
|
||||
t-att-data-is-quiz="0"
|
||||
t-att-data-completed="1 if slide_completed else 0"
|
||||
t-att-data-completed="slide_completed"
|
||||
t-att-data-embed-code="slide.embed_code if slide.slide_category in ['video', 'document', 'infographic'] else False"
|
||||
t-att-data-can-self-mark-completed="slide.can_self_mark_completed"
|
||||
t-att-data-can-self-mark-uncompleted="slide.can_self_mark_uncompleted"
|
||||
t-att-data-is-member="is_member"
|
||||
t-att-data-session-answers="session_answers"
|
||||
t-att-data-website-share-url="slide.website_share_url">
|
||||
<span class="ml-3">
|
||||
<i t-if="slide_completed and is_member" class="o_wslides_slide_completed fa fa-check fa-fw text-success" t-att-data-slide-id="slide.id"/>
|
||||
<i t-if="not slide_completed and is_member" class="fa fa-circle-thin fa-fw" t-att-data-slide-id="slide.id"/>
|
||||
</span>
|
||||
<t t-call="website_slides.slide_sidebar_done_button"/>
|
||||
<div class="ml-2">
|
||||
<a t-if="can_access" class="d-block" href="#">
|
||||
<div class="d-flex ">
|
||||
@@ -171,11 +170,13 @@
|
||||
t-att-data-slug="slug(slide)"
|
||||
t-att-data-has-question="1 if slide.question_ids else 0"
|
||||
t-att-data-is-quiz="1"
|
||||
t-att-data-completed="1 if slide_completed else 0"
|
||||
t-att-data-completed="slide_completed"
|
||||
t-att-data-can-self-mark-completed="slide.can_self_mark_completed"
|
||||
t-att-data-can-self-mark-uncompleted="slide.can_self_mark_uncompleted"
|
||||
t-att-data-is-member="is_member"
|
||||
t-att-data-session-answers="session_answers"
|
||||
t-att-data-website-share-url="slide.website_share_url">
|
||||
<a t-if="can_access" class="o_wslides_fs_slide_quiz" href="#" t-att-index="i">
|
||||
<a t-if="can_access" class="o_wslides_fs_slide_quiz o_wslides_fs_slide_name" href="#" t-att-index="i">
|
||||
<i class="fa fa-flag-checkered text-warning mr-2"/>Quiz
|
||||
</a>
|
||||
<span t-else="" class="text-600">
|
||||
|
||||
@@ -96,4 +96,24 @@
|
||||
</a> to download resources
|
||||
</template>
|
||||
|
||||
<!-- Python equivalent of the JS template "website.slides.sidebar.done.button" -->
|
||||
<template id="slide_sidebar_done_button" name="Sidebar Done Button">
|
||||
<div t-if="is_member"
|
||||
class="o_wslides_sidebar_done_button"
|
||||
t-att-data-id="slide.id"
|
||||
t-att-data-completed="slide_completed"
|
||||
t-att-data-can-self-mark-completed="slide.can_self_mark_completed"
|
||||
t-att-data-can-self-mark-uncompleted="slide.can_self_mark_uncompleted"
|
||||
t-att-data-is-member="is_member">
|
||||
<button class="o_wslides_button_complete btn btn-sm" t-if="slide_completed and slide.can_self_mark_uncompleted or not slide_completed and slide.can_self_mark_completed">
|
||||
<i t-if="slide_completed" class="o_wslides_slide_completed fa fa-check-circle fa-fw text-success fa-lg" t-att-data-slide-id="slide.id" title="Mark as not done"/>
|
||||
<i t-else="" class="fa fa-circle-thin fa-fw fa-lg" t-att-data-slide-id="slide.id" title="Mark as done"/>
|
||||
</button>
|
||||
<div class="o_wslides_button_complete btn-sm" t-else="">
|
||||
<i t-if="not slide_completed" class="fa fa-circle-thin fa-fw fa-lg" t-att-data-slide-id="slide.id" title="Can not be marked as done"/>
|
||||
<i t-else="" class="o_wslides_slide_completed fa fa-check fa-fw text-success fa-lg" t-att-data-slide-id="slide.id" title="Can not be marked as not done"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</data></odoo>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
'views/website_slides_templates_lesson.xml',
|
||||
'views/website_slides_templates_lesson_fullscreen.xml',
|
||||
'views/website_slides_templates_homepage.xml',
|
||||
'views/website_slides_templates_utils.xml',
|
||||
'views/survey_templates.xml',
|
||||
'views/website_profile.xml',
|
||||
'data/mail_template_data.xml',
|
||||
|
||||
@@ -86,10 +86,10 @@ class WebsiteSlidesSurvey(WebsiteSlides):
|
||||
|
||||
# Utils
|
||||
# ---------------------------------------------------
|
||||
def _set_completed_slide(self, slide):
|
||||
def _slide_mark_completed(self, slide):
|
||||
if slide.slide_category == 'certification':
|
||||
raise werkzeug.exceptions.Forbidden(_("Certification slides are completed when the survey is succeeded."))
|
||||
return super(WebsiteSlidesSurvey, self)._set_completed_slide(slide)
|
||||
return super(WebsiteSlidesSurvey, self)._slide_mark_completed(slide)
|
||||
|
||||
def _get_valid_slide_post_values(self):
|
||||
result = super(WebsiteSlidesSurvey, self)._get_valid_slide_post_values()
|
||||
|
||||
@@ -53,6 +53,12 @@ class Slide(models.Model):
|
||||
if not slide.name and slide.survey_id:
|
||||
slide.name = slide.survey_id.title
|
||||
|
||||
def _compute_mark_complete_actions(self):
|
||||
slides_certification = self.filtered(lambda slide: slide.slide_category == 'certification')
|
||||
slides_certification.can_self_mark_uncompleted = False
|
||||
slides_certification.can_self_mark_completed = False
|
||||
super(Slide, self - slides_certification)._compute_mark_complete_actions()
|
||||
|
||||
@api.depends('slide_category')
|
||||
def _compute_is_preview(self):
|
||||
for slide in self:
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
<div t-if="widget.get('slide').category === 'certification' && !widget.get('slide').completed" class="">
|
||||
<a class="btn btn-primary" t-att-href="'/slides_survey/slide/get_certification_url?slide_id=' + widget.get('slide').id" target="_blank">
|
||||
<i class="fa fa-trophy"/>
|
||||
<span t-if="widget.get('slide').isMember"> Pass Certification</span>
|
||||
<span t-if="widget.get('slide').isMember"> Start Certification</span>
|
||||
<span t-else="">Test Certification</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="" t-if="widget.get('slide').category === 'certification' && widget.get('slide').completed">
|
||||
<div class="text-center" t-if="widget.get('slide').category === 'certification' && widget.get('slide').completed">
|
||||
<h4 class="mb-3 text-white">Congratulations, you passed the Certification!</h4>
|
||||
<a role="button" class="btn btn-primary" t-att-href="'/survey/' + widget.get('slide').certificationId + '/get_certification'">
|
||||
<i class="fa fa-fw fa-trophy" role="img" aria-label="Download certification" title="Download certification"/> Download certification
|
||||
</a>
|
||||
|
||||
@@ -6,18 +6,35 @@
|
||||
<div t-if="slide.slide_category == 'certification' and not channel_progress[slide.id].get('completed')" class="col mt32 mb8 d-flex justify-content-center">
|
||||
<a role="button"
|
||||
class="btn btn-primary btn-lg"
|
||||
id="start_certification"
|
||||
t-att-href="'/slides_survey/slide/get_certification_url?slide_id=%s' %(slide.id)">
|
||||
<i class="fa fa-fw fa-graduation-cap" role="img"/>
|
||||
<t t-if="slide.channel_id.is_member and slide.channel_id.active">Begin Certification</t>
|
||||
<i class="fa fa-trophy" role="img"/>
|
||||
<t t-if="slide.channel_id.is_member and slide.channel_id.active">Start Certification</t>
|
||||
<t t-else="">Test Certification</t>
|
||||
</a>
|
||||
</div>
|
||||
<div t-if="slide.slide_category == 'certification' and channel_progress[slide.id].get('completed')" class="col mt32 mb8 d-flex justify-content-center">
|
||||
<a role="button" class="btn btn-primary btn-lg" t-att-href="'/survey/%s/get_certification' % slide.survey_id.id">
|
||||
<div t-if="slide.slide_category == 'certification' and channel_progress[slide.id].get('completed')" class="col mt32 mb8 text-center">
|
||||
<h4 class="mb-3">Congratulations, you passed the Certification!</h4>
|
||||
<a role="button" class="btn btn-primary btn-lg mb-3" t-att-href="'/survey/%s/get_certification' % slide.survey_id.id">
|
||||
<i class="fa fa-fw fa-trophy" role="img" aria-label="Download certification" title="Download certification"/> Download certification
|
||||
</a>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//a[hasclass('o_wslides_done_button')][1]" position="after">
|
||||
<a t-elif="not slide_completed and slide.slide_category == 'certification'"
|
||||
role="button"
|
||||
t-attf-class="o_wslides_done_button btn btn-primary border text-white mr-2"
|
||||
href="#start_certification">
|
||||
Take Quiz
|
||||
</a>
|
||||
</xpath>
|
||||
<xpath expr="//a[hasclass('o_wslides_undone_button')][1]" position="after">
|
||||
<a t-elif="slide_completed and slide.slide_category == 'certification'"
|
||||
class="o_wslides_undone_button btn btn-primary border text-white mr-2 disabled"
|
||||
aria-disabled="true" title="Certifications you have passed cannot be marked as not done">
|
||||
Mark To Do
|
||||
</a>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="slide_sidebar_done_button" inherit_id="website_slides.slide_sidebar_done_button">
|
||||
<xpath expr="//div[hasclass('o_wslides_button_complete')]/i[1]" position="after">
|
||||
<i t-elif="slide_completed and slide.slide_category == 'certification'"
|
||||
class="o_wslides_slide_completed fa fa-check fa-fw text-success fa-lg"
|
||||
t-att-data-slide-id="slide.id"
|
||||
title="Certifications you have passed cannot be marked as not done"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user