[REF] website_slides: refactor the course reordering of slides and categories

Purpose
=======

This commit refactors the way slides and categories are reordered in the
frontend course view (training courses). The use of jqueryui's "sortable"
widget makes it easier and more user friendly to drag and drop both sections
and slides to reorganize your course.

Commit linked to task ID 1942852 and PR #31394.
This commit is contained in:
Aurélien Warnon
2019-02-26 16:07:57 +00:00
committed by Thibault Delavallée
parent 76bfcef0b9
commit 5fbfd2a7a4
7 changed files with 177 additions and 340 deletions
+18 -15
View File
@@ -644,6 +644,24 @@ class WebsiteSlides(WebsiteProfile):
return {'error': 'join_done'}
return success
@http.route('/slides/channel/resequence', type="json", website=True, auth="user")
def resequence_slides(self, channel_id, slides_data):
"""" Reorder the slides within the channel by reassigning their 'sequence' field.
This method also handles slides that are put in a new category. """
channel = request.env['slide.channel'].browse(int(channel_id))
if not channel.can_publish:
return {'error': 'Only the publishers of the channel can edit it'}
slides = request.env['slide.slide'].search([
('id', 'in', [int(key) for key in slides_data.keys()]),
('channel_id', '=', channel.id)
])
for slide in slides:
slide_key = str(slide.id)
slide.sequence = slides_data[slide_key]['sequence']
slide.category_id = slides_data[slide_key]['category_id']
@http.route(['/slides/prepare_preview'], type='json', auth='user', methods=['POST'], website=True)
def prepare_preview(self, **data):
Slide = request.env['slide.slide']
@@ -741,21 +759,6 @@ class WebsiteSlides(WebsiteProfile):
'can_create': can_create,
}
#Not using the /web/dataset/resequence route as a slide can be dragged into another category at the same time
@http.route('/slides/resequence_slides', type="json", website=True, auth="user")
def resequence_slides(self, slides_data=None, **kw):
channel_user = None
for slide in slides_data:
s = request.env['slide.slide'].browse(slide['id'])
if not channel_user:
channel_user = s.channel_id.user_id
if request.env.user != channel_user:
return {'error': 'Only the responsible of the channel can edit it'}
s.write({
'sequence': slide['sequence'],
'category_id': slide['category_id']
})
# --------------------------------------------------
# EMBED IN THIRD PARTY WEBSITES
# --------------------------------------------------
@@ -1,251 +1,127 @@
odoo.define('website_slides.slideslist', function (require) {
'use strict';
var sAnimations = require('website.content.snippets.animation');
var Widget = require('web.Widget');
'use strict';
var SlideUpload = require('website_slides.upload_modal');
var sAnimations = require('website.content.snippets.animation');
var List = Widget.extend({
init: function (el){
this._super.apply(this,arguments);
this.draggedElement = undefined;
this.dropTarget = undefined;
this.slideCount = undefined;
this.slides = [];
this.categories = [];
},
start: function (){
this._super.apply(this,arguments);
this.slideCount = $('li.o_wslides_content_slide').length;
//Change links HREF to fullscreen mode for SEO
var links = $('.o_wslides_course_slide_link');
for (var i = 0; i < links.length; i++){
$(links[i]).attr('href', $(links[i]).attr('href') + "?fullscreen=1");
}
this._bindEvents();
},
_bindEvents: function (){
var self = this;
$('.slide-draggable').each(function (){
self._addSlideDragAndDropHandlers($(this));
});
$('.section-draggable').each(function (){
self._addSectionDragAndDropHandlers($(this));
});
$('.course-section').each(function (){
self._addDropSlideOnSectionHandler($(this));
});
},
_unbind: function (className){
$("."+className).each(function (){
$(this).unbind();
});
},
_unbindAll: function (){
this._unbind('slide-draggable');
this._unbind('section-draggable');
this._unbind('course-section');
},
_getSlides: function (){
var self = this;
var slides = $('li.o_wsldies_content_slide');
for (var i = 0; i < slides.length;i++){
var slide = $(slides[i]);
self.slides.push({
id: parseInt(slide.attr('slide_id')),
category_id: parseInt(slide.attr('category_id')),
sequence: i
});
}
},
_getCategories: function (){
var self = this;
self.categories = [];
var categories = $('.course-section');
for (var i = 0; i < categories.length;i++){
var category = $(categories[i]);
self.categories.push(parseInt(category.attr('category_id')));
}
},
_addDropSlideOnSectionHandler: function (target){
var self = this;
target.on('drop', function (ev){
if (ev.preventDefault){
ev.preventDefault();
}
self.dropTarget = $(ev.currentTarget);
self.draggedElement[0].parentNode.removeChild(self.draggedElement[0]);
$('ul[category_id='+target.attr('category_id')+']').append(self.draggedElement);
self._addSlideDragAndDropHandlers(self.draggedElement);
self._reorderSlides();
});
target.on('dragover', function (ev){
if (ev.preventDefault){
ev.preventDefault();
}
});
},
_addSlideDragAndDropHandlers: function (target){
var self = this;
target.on('dragstart', function (ev){
$('.section-draggable').removeClass('hold');
self._unbind('section-draggable');
ev.originalEvent.dataTransfer.effectAllowed = 'move';
ev.originalEvent.dataTransfer.setData('text/html', this.outerHTML);
self.draggedElement = target;
self.draggedElement.addClass('hold');
});
target.on('dragover', function (ev){
if ($(ev.currentTarget) !== self.draggedElement){
if (ev.preventDefault){
ev.preventDefault();
}
target.addClass('slide-hovered');
}
});
target.on('dragleave', function (ev){
if (ev.preventDefault){
ev.preventDefault();
}
target.removeClass('slide-hovered');
});
target.on('drop', function (ev){
if (self.draggedElement.hasClass('slide-draggable') && target.hasClass('slide-draggable')){
if (ev.preventDefault){
ev.preventDefault();
}
target.removeClass('slide-hovered');
target.removeClass('hold');
if (target !== self.draggedElement){
self.dropTarget = $(ev.currentTarget);
self.draggedElement[0].parentNode.removeChild(self.draggedElement[0]);
var dropHTML = ev.originalEvent.dataTransfer.getData('text/html');
target[0].insertAdjacentHTML('beforebegin',dropHTML);
self.draggedElement = $(target[0].previousSibling);
self._reorderSlides();
}
self._unbindAll();
self._bindEvents();
}
});
target.on('dragend', function (ev){
if (ev.preventDefault){
ev.preventDefault();
}
target.removeClass('slide-hovered');
target.removeClass('hold');
});
},
_addSectionDragAndDropHandlers: function (target){
var self = this;
target.on('dragstart', function (ev){
self._unbind('slide-draggable');
self._unbind('course-section');
ev.originalEvent.dataTransfer.effectAllowed = 'move';
ev.originalEvent.dataTransfer.setData('text/html', this.outerHTML);
self.draggedElement = target;
self.draggedElement.addClass('hold');
});
target.on('dragover', function (ev){
if (target.hasClass('section-draggable') && self.draggedElement.hasClass('section-draggable')){
if (ev.preventDefault){
ev.preventDefault();
}
target.addClass('slide-hovered');
}
});
target.on('dragleave', function (ev){
if (ev.preventDefault){
ev.preventDefault();
}
target.removeClass('slide-hovered');
});
target.on('drop', function (ev){
if (ev.preventDefault){
ev.preventDefault();
}
if (self.draggedElement.hasClass('section-draggable') && target.hasClass('section-draggable')){
target.removeClass('slide-hovered');
target.removeClass('hold');
self.dropTarget = $(ev.currentTarget);
if (target !== self.draggedElement && $(ev.currentTarget).hasClass('section-draggable')){
self.draggedElement[0].parentNode.removeChild(self.draggedElement[0]);
var dropHTML = ev.originalEvent.dataTransfer.getData('text/html');
target[0].insertAdjacentHTML('beforebegin',dropHTML);
self.draggedElement = $(target[0].previousSibling);
self._reorderCategories();
self._reorderSlides();
self._rebindUploadButton(self.draggedElement.attr('category_id'));
}
self._unbindAll();
self._bindEvents();
}
});
target.on('dragend', function (ev){
if (ev.preventDefault){
ev.preventDefault();
}
target.removeClass('slide-hovered');
target.removeClass('hold');
});
},
_reorderCategories: function (){
var self = this;
self._getCategories();
self._rpc({
route: '/web/dataset/resequence',
params: {
model: "slide.category",
ids: self.categories
}
}).then(function (){
self._resetCategoriesIndex();
});
},
_resetCategoriesIndex: function (){
var categoriesIndexes = $('.section-index');
for (var i = 0; i < categoriesIndexes.length; i++){
$(categoriesIndexes[i]).text(i+1);
}
},
_reorderSlides: function (){
var self = this;
// In case the slide was transfered to another section
if (self.draggedElement.hasClass('slide-draggable')){
self.draggedElement.attr('category_id', parseInt(self.dropTarget.attr('category_id')));
}
self.slides = [];
self._getSlides();
self._rpc({
route: "/slides/resequence_slides",
params: {
slides_data: self.slides
}
}).then(function (){
});
},
_rebindUploadButton: function (categoryID){
var self = this;
this.$('.oe_slide_js_upload[data-category-id='+categoryID+']').click(function (ev){
ev.preventDefault();
var data = $(ev.currentTarget).data();
var dialog = new SlideUpload.SlideUploadDialog(self, data);
dialog.appendTo(document.body);
dialog.open();
});
}
});
sAnimations.registry.websiteSlidesCourseSlidesList = sAnimations.Class.extend({
selector: '.o_wslides_slides_list',
xmlDependencies: ['/website_slides/static/src/xml/website_slides_upload.xml'],
start: function () {
this._super.apply(this,arguments);
this.channelId = this.$el.data('channelId');
this._updateHref();
this._bindSortable();
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------,
/**
* Bind the sortable jQuery widget to both
* - course sections
* - course slides
*
* @private
*/
_bindSortable: function () {
this.$('ul.o_wslides_slides_list_container').sortable({
handle: '.fa-arrows',
stop: this._reorderCategories.bind(this)
});
this.$('.o_wslides_slides_list_container ul').sortable({
handle: '.fa-arrows',
connectWith: '.o_wslides_slides_list_container ul',
stop: this._reorderSlides.bind(this),
items: '.o_wslides_slides_list_slide'
});
},
_getCategories: function (){
var categories = [];
this.$('.o_wslides_slide_list_category_container').each(function (){
categories.push(parseInt($(this).attr('category_id')));
});
return categories;
},
/**
* Returns a slides dict in the form:
* {slide_id: {'sequence': slide_sequence, 'category_id': slide.category_id.id}}
*
* @private
*/
_getSlides: function (){
var slides = {};
this.$('li.o_wslides_slides_list_slide').each(function (index){
var $slide = $(this);
slides[$slide.attr('slide_id')] = {
category_id: parseInt(
$slide.closest('.o_wslides_slide_list_category').attr('category_id')
),
sequence: index
};
});
return slides;
},
_reorderCategories: function (){
var self = this;
self._rpc({
route: '/web/dataset/resequence',
params: {
model: "slide.category",
ids: self._getCategories()
}
}).then(function (){
self._resetCategoriesIndex();
});
},
_reorderSlides: function (){
this._rpc({
route: "/slides/channel/resequence",
params: {
channel_id: this.channelId,
slides_data: this._getSlides()
}
});
},
/**
* Used to reset the categories numbering (1, 2, 3, ...) in the UI
*/
_resetCategoriesIndex: function (){
this.$('.section-index').each(function (index){
$(this).text(index + 1);
});
},
/**
* Change links href to fullscreen mode for SEO.
*
* Specifications demand that links are generated (xml) without the "fullscreen"
* parameter for SEO purposes.
*
* This method then adds the parameter as soon as the page is loaded.
*
* @private
*/
_updateHref: function () {
this.$(".o_wslides_slides_list_slide_link").each(function (){
var href = $(this).attr('href');
var operator = href.indexOf('?') !== -1 ? '&' : '?';
$(this).attr('href', href + operator + "fullscreen=1");
});
}
});
return sAnimations.registry.websiteSlidesCourseSlidesList;
sAnimations.registry.websiteSlidesCourseSlidesList = Widget.extend({
selector: '.oe_js_course_slides_list',
xmlDependencies: ['/website_slides/static/src/xml/website_slides_upload.xml'],
init: function (el){
this._super.apply(this, arguments);
},
start: function (){
this._super.apply(this, arguments);
var list = new List(this);
list.appendTo(".oe_js_course_slides_list");
}
});
});
@@ -47,7 +47,7 @@ var SlideArchiveDialog = Dialog.extend({
slide_id: this.slideId
},
}).then(function () {
self.$slideTarget.closest('.o_wslides_content_slide').remove();
self.$slideTarget.closest('.o_wslides_slides_list_slide').remove();
self.close();
});
}
@@ -311,25 +311,6 @@ var SlideUploadDialog = Dialog.extend({
return values;
},
_reorderSlidesSequence: function (){
var self = this;
var slidesElement = $('li.o_wslides_content_slide');
var slides = [];
for (var i = 0; i < slidesElement.length;i++){
slides.push({
id: parseInt($(slidesElement[i]).attr('slide_id')),
category_id: parseInt($(slidesElement[i]).attr('category_id')),
sequence: i
});
}
self._rpc({
route: '/slides/resequence_slides',
params: {
slides_data: slides
}
}).then(function (){
});
},
/**
* Init the data relative to the support slide type to upload
*
@@ -539,12 +520,6 @@ var SlideUploadDialog = Dialog.extend({
self.set('state', oldType);
self._alertDisplay(data.error);
} else {
//Quick and really dirty fix for reordering issues
if (data.channel_type === 'training' && self.categoryID){
var categoryElement = $('ul[category_id='+self.categoryID+']');
$('<li hidden class="o_wslides_content_slide" slide_id="'+data.slide_id+'" category_id="'+self.categoryID+'">temp</li>').appendTo(categoryElement);
self._reorderSlidesSequence();
}
window.location = data.url;
}
});
@@ -1,49 +1,32 @@
.o_wslides_course_content {
.o_wslides_slides_list {
padding: 0;
ul {
list-style: none;
padding: 0
padding: 0;
}
.o_wslides_slide_list_category_container {
margin: 0;
background-color: #ddd;
display: flex;
list-style: none;
font-size: 1.05rem;
border-bottom: 1px solid #ccc;
}
.o_wslides_slides_list_slide:nth-child(odd) {
background-color: #f6f6f6;
}
.o_wslides_slides_list_slide:nth-child(even) {
background-color: #f9f9f9;
}
.o_wslides_slides_list_slide_controls {
i:hover {
color: $primary !important;
cursor: pointer;
}
}
}
.o_wslides_course_section {
margin: 0;
background-color: #ddd;
display: flex;
list-style: none;
font-size: 1.05rem;
border-bottom: 1px solid #ccc
}
.o_wslides_content_tab{
.active {
border: none;
border-bottom: 1px solid $secondary;
}
}
.o_wslides_content_tab a:hover{
border:none;
border-bottom: 1px solid $secondary;
}
.o_wslides_content_container{
border:none;
}
.o_wslides_content_slide:nth-child(odd){
background-color: #f6f6f6;
}
.o_wslides_content_slide:nth-child(even){
background-color: #f9f9f9;
}
.o_wslides_content_slide_controls {
i:hover{
color: $primary !important;
cursor: pointer;
}
}
@@ -245,15 +245,15 @@
<template id="course_content_training" name="Content of a training channel">
<div class="col-8 d-flex flex-column ml-4">
<div class="o_wslides_course_content oe_js_course_slides_list w-100 mt8">
<ul>
<div class="o_wslides_slides_list w-100 mt8" t-att-data-channel-id="channel.id">
<ul class="o_wslides_slides_list_container">
<t t-set="i" t-value="1"/>
<t t-set="j" t-value="0"/>
<t t-foreach="channel.category_ids" t-as="category">
<li t-attf-class="#{'section-draggable' if user == user_id else ''}" t-attf-category_id="#{category.id}">
<div t-attf-category_id="#{category.id}" class="o_wslides_course_section text-muted font-weight-bold pt-0 pb-0 pl-2 pr-2 d-flex justify-content-between align-items-center">
<li class="o_wslides_slide_list_category" t-attf-category_id="#{category.id}">
<div t-attf-category_id="#{category.id}" class="o_wslides_slide_list_category_container text-muted font-weight-bold pt-0 pb-0 pl-2 pr-2 d-flex justify-content-between align-items-center">
<div style="width:50%;" class="d-flex align-items-center">
<i t-if="channel.user_id == user" class="fa fa-arrows mr-3 text-muted"></i>
<i t-if="channel.can_publish" class="fa fa-arrows mr-3 text-muted"></i>
<div class="mr-2">Section <span class="section-index" t-esc="i"/>:</div>
<span t-field="category.name"/>
</div>
@@ -271,9 +271,9 @@
<ul t-attf-category_id="#{category.id}" >
<t t-foreach="category.slide_ids" t-as="slide">
<li t-att-index="j" t-attf-slide_id="#{slide.id}" t-attf-category_id="#{category.id}"
t-att-class="'o_wslides_content_slide %s d-flex justify-content-between align-items-center p-2' % ('slide-draggable' if channel.user_id == user else '')}">
class="o_wslides_slides_list_slide d-flex justify-content-between align-items-center p-2">
<div class="ml-2">
<i t-if="channel.user_id == user" class="fa fa-arrows mr-2 text-muted"></i>
<i t-if="channel.can_publish" class="fa fa-arrows mr-2 text-muted"></i>
<i t-if="slide.slide_type == 'document'" class="fa fa-file-pdf-o mr-2 text-muted"></i>
<i t-if="slide.slide_type == 'infographic'" class="fa fa-file-picture-o mr-2 text-muted"></i>
<i t-if="slide.slide_type == 'video'" class="fa fa-play-circle mr-2 text-muted"></i>
@@ -281,9 +281,9 @@
<i t-if="slide.slide_type == 'webpage'" class="fa fa-file-text mr-2 text-muted"></i>
<i t-if="slide.slide_type == 'quiz'" class="fa fa-question-circle mr-2 text-muted"></i>
<i t-if="slide.slide_type == 'certification'" class="fa fa-trophy mr-2 text-muted"></i>
<a class="o_wslides_course_slide_link" t-attf-href="/slides/slide/#{slug(slide)}"><span t-field="slide.name"/></a>
<a class="o_wslides_slides_list_slide_link" t-attf-href="/slides/slide/#{slug(slide)}"><span t-field="slide.name"/></a>
</div>
<div class="o_wslides_content_slide_controls mr-2">
<div class="o_wslides_slides_list_slide_controls mr-2">
<i t-if="not slide.id in user_progress or not user_progress[slide.id].completed" class="check-done fa fa-check-circle text-muted mr-1"></i>
<i t-if="slide.id in user_progress and user_progress[slide.id].completed" class="check-done text-primary fa fa-check-circle mr-1"></i>
<a t-if="channel.can_publish and not slide.slide_type == 'webpage'" t-attf-href="/web#id=#{slide.id}&amp;action=#{slide_action}&amp;model=slide.slide&amp;view_type=form"><i class="fa fa-pencil text-muted mr-1"></i></a>
@@ -49,7 +49,7 @@
<i t-if="course_slide.slide_type == 'quiz'" class="fa fa-question-circle mr-2 text-muted"></i>
<t t-esc="course_slide.name"/>
</div>
<div class="o_wslides_content_slide_controls mr-2">
<div class="o_wslides_slides_list_slide_controls mr-2">
<i t-attf-id="check-#{course_slide.id}" t-if="course_slide.id in user_progress and not user_progress[course_slide.id].completed" class="check-done fa fa-check-circle text-muted mr-1"></i>
<i t-attf-id="check-#{course_slide.id}" t-if="course_slide.id in user_progress and user_progress[course_slide.id].completed" class="check-done text-primary fa fa-check-circle mr-1"></i>
</div>