[IMP] website_slides: Add course tags creation to frontend
This commit makes it possible for course tags to be added via the frontend. To support this, course tags were also made viewable in a specific course's website since previously these tags were only visible in the courses frontpage (i.e. within the course cards). "Add Tag" is placed after course page's tags. Note that due to the ability to make new tags within this "Add Tag" modal restricts addition to only 1 tag at a time since new tags require a "Tap Group" to be assigned and handling a mix of new/existing tags would get confusing and complicated. Supports Task: 2128166 PR #44365
This commit is contained in:
committed by
Thibault Delavallée
parent
ff828a3e0c
commit
27005ebdbb
@@ -549,6 +549,69 @@ class WebsiteSlides(WebsiteProfile):
|
||||
'can_create': can_create,
|
||||
}
|
||||
|
||||
@http.route(['/slides/channel/tag/group/search_read'], type='json', auth='user', methods=['POST'], website=True)
|
||||
def slide_channel_tag_group_search_read(self, fields, domain):
|
||||
can_create = request.env['slide.channel.tag.group'].check_access_rights('create', raise_exception=False)
|
||||
return {
|
||||
'read_results': request.env['slide.channel.tag.group'].search_read(domain, fields),
|
||||
'can_create': can_create,
|
||||
}
|
||||
|
||||
@http.route('/slides/channel/tag/add', type='json', auth='user', methods=['POST'], website=True)
|
||||
def slide_channel_tag_add(self, channel_id, tag_id=None, group_id=None):
|
||||
""" Adds a slide channel tag to the specified slide channel.
|
||||
|
||||
:param integer channel_id: Channel ID
|
||||
:param list tag_id: Channel Tag ID as first value of list. If id=0, then this is a new tag to
|
||||
generate and expects a second list value of the name of the new tag.
|
||||
:param list group_id: Channel Tag Group ID as first value of list. If id=0, then this is a new
|
||||
tag group to generate and expects a second list value of the name of the
|
||||
new tag group. This value is required for when a new tag is being created.
|
||||
|
||||
tag_id and group_id values are provided by a Select2. Default "None" values allow for
|
||||
graceful failures in exceptional cases when values are not provided.
|
||||
|
||||
:return: channel's course page
|
||||
"""
|
||||
|
||||
# handle exception during addition of course tag and send error notification to the client
|
||||
# otherwise client slide create dialog box continue processing even server fail to create a slide
|
||||
try:
|
||||
channel = request.env['slide.channel'].browse(int(channel_id))
|
||||
can_upload = channel.can_upload
|
||||
can_publish = channel.can_publish
|
||||
except (UserError, AccessError) as e:
|
||||
_logger.error(e)
|
||||
return {'error': e.name}
|
||||
else:
|
||||
if not can_upload or not can_publish:
|
||||
return {'error': _('You cannot add tags to this course.')}
|
||||
|
||||
if tag_id:
|
||||
# handle creation of new channel tag
|
||||
if tag_id[0] == 0:
|
||||
if group_id:
|
||||
# handle creation of new channel tag group
|
||||
if group_id[0] == 0:
|
||||
tag_group = request.env['slide.channel.tag.group'].create({
|
||||
'name': group_id[1]['name'],
|
||||
})
|
||||
group_id = tag_group.id
|
||||
# use existing channel tag group
|
||||
else:
|
||||
group_id = group_id[0]
|
||||
else:
|
||||
return {'error': _('Missing "Tag Group" for creating a new "Tag".')}
|
||||
|
||||
request.env['slide.channel.tag'].create({'name': tag_id[1]['name'],
|
||||
'channel_ids': [channel.id],
|
||||
'group_id': group_id,
|
||||
})
|
||||
else:
|
||||
# use existing channel tag
|
||||
request.env['slide.channel.tag'].browse(tag_id[0]).write({'channel_ids': [(4, channel.id, 0)]})
|
||||
return {'url': "/slides/%s" % (slug(channel))}
|
||||
|
||||
@http.route(['/slides/channel/subscribe'], type='json', auth='user', website=True)
|
||||
def slide_channel_subscribe(self, channel_id):
|
||||
return request.env['slide.channel'].browse(channel_id).message_subscribe(partner_ids=[request.env.user.partner_id.id])
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
odoo.define('website_slides.channel_tag.add', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var Dialog = require('web.Dialog');
|
||||
var publicWidget = require('web.public.widget');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
var TagCourseDialog = Dialog.extend({
|
||||
template: 'website.slides.tag.add',
|
||||
events: _.extend({}, Dialog.prototype.events, {
|
||||
'change input#tag_id' : '_onChangeTag',
|
||||
}),
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} parent
|
||||
* @param {Object} options holding channelId
|
||||
*
|
||||
*/
|
||||
init: function (parent, options) {
|
||||
options = _.defaults(options || {}, {
|
||||
title: _t("Add a tag"),
|
||||
size: 'medium',
|
||||
buttons: [{
|
||||
text: _t("Add"),
|
||||
classes: 'btn-primary',
|
||||
click: this._onClickFormSubmit.bind(this)
|
||||
}, {
|
||||
text: _t("Discard"),
|
||||
close: true
|
||||
}]
|
||||
});
|
||||
|
||||
this.channelID = parseInt(options.channelId, 10);
|
||||
this.tagIds = options.channelTagIds || [];
|
||||
this._super(parent, options);
|
||||
},
|
||||
start: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
self._bindSelect2Dropdown();
|
||||
self._hideTagGroup();
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 'Tag' and 'Tag Group' management for select2
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_bindSelect2Dropdown: function () {
|
||||
var self = this;
|
||||
this.$('#tag_id').select2(this._select2Wrapper(_t('Tag'),
|
||||
function () {
|
||||
return self._rpc({
|
||||
route: '/slides/channel/tag/search_read',
|
||||
params: {
|
||||
fields: ['name'],
|
||||
domain: [['id','not in',self.tagIds]],
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
this.$('#tag_group_id').select2(this._select2Wrapper(_t('Tag Group (required for new tags)'),
|
||||
function () {
|
||||
return self._rpc({
|
||||
route: '/slides/channel/tag/group/search_read',
|
||||
params: {
|
||||
fields: ['name'],
|
||||
domain: [],
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Wrapper for select2 load data from server at once and store it.
|
||||
*
|
||||
* @private
|
||||
* @param {String} Placeholder for element.
|
||||
* @param {Function} Function to fetch data from remote location should return a Promise
|
||||
* resolved data should be array of object with id and name. eg. [{'id': id, 'name': 'text'}, ...]
|
||||
* @param {String} [nameKey='name'] (optional) the name key of the returned record
|
||||
* ('name' if not provided)
|
||||
* @returns {Object} select2 wrapper object
|
||||
*/
|
||||
_select2Wrapper: function (tag, fetchFNC, nameKey) {
|
||||
nameKey = nameKey || 'name';
|
||||
|
||||
var values = {
|
||||
width: '100%',
|
||||
placeholder: tag,
|
||||
allowClear: true,
|
||||
formatNoMatches: false,
|
||||
selection_data: false,
|
||||
fetch_rpc_fnc: fetchFNC,
|
||||
formatSelection: function (data) {
|
||||
if (data.tag) {
|
||||
data.text = data.tag;
|
||||
}
|
||||
return data.text;
|
||||
},
|
||||
createSearchChoice: function (term, data) {
|
||||
var addedTags = $(this.opts.element).select2('data');
|
||||
if (_.filter(_.union(addedTags, data), function (tag) {
|
||||
return tag.text.toLowerCase().localeCompare(term.toLowerCase()) === 0;
|
||||
}).length === 0) {
|
||||
if (this.opts.can_create) {
|
||||
return {
|
||||
id: _.uniqueId('tag_'),
|
||||
create: true,
|
||||
tag: term,
|
||||
text: _.str.sprintf(_t("Create new %s '%s'"), tag, term),
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
fill_data: function (query, data) {
|
||||
var that = this,
|
||||
tags = {results: []};
|
||||
_.each(data, function (obj) {
|
||||
if (that.matcher(query.term, obj[nameKey])) {
|
||||
tags.results.push({id: obj.id, text: obj[nameKey]});
|
||||
}
|
||||
});
|
||||
query.callback(tags);
|
||||
},
|
||||
query: function (query) {
|
||||
var that = this;
|
||||
// fetch data only once and store it
|
||||
if (!this.selection_data) {
|
||||
this.fetch_rpc_fnc().then(function (data) {
|
||||
that.can_create = data.can_create;
|
||||
that.fill_data(query, data.read_results);
|
||||
that.selection_data = data.read_results;
|
||||
});
|
||||
} else {
|
||||
this.fill_data(query, this.selection_data);
|
||||
}
|
||||
}
|
||||
};
|
||||
return values;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get value for tag_id and [when appropriate] tag_group_id to send to server
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_getSelect2DropdownValues: function () {
|
||||
var result = {};
|
||||
var tag = this.$('#tag_id').select2('data');
|
||||
if (tag) {
|
||||
if (tag.create) {
|
||||
// new tag
|
||||
var group = this.$('#tag_group_id').select2('data');
|
||||
if(group) {
|
||||
result['tag_id'] = [0, {'name': tag.text}]
|
||||
if (group.create) {
|
||||
// new tag group
|
||||
result['group_id'] = [0, {'name': group.text}];
|
||||
} else {
|
||||
result['group_id'] = [group.id];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result['tag_id'] = [tag.id];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Select2 fields makes the "required" input hidden on the interface.
|
||||
* Therefore we need to make a method to visually provide this requirement
|
||||
* feedback to users. "tag group" field should only need this when a new tag
|
||||
* is created.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_formValidate: function ($form) {
|
||||
$form.addClass('was-validated');
|
||||
var result = $form[0].checkValidity();
|
||||
|
||||
var $tagInput = this.$('#tag_id');
|
||||
if ($tagInput.length !== 0){
|
||||
var $tagSelect2Container = $tagInput
|
||||
.closest('.form-group')
|
||||
.find('.select2-container');
|
||||
$tagSelect2Container.removeClass('is-invalid is-valid');
|
||||
if ($tagInput.is(':invalid')) {
|
||||
$tagSelect2Container.addClass('is-invalid');
|
||||
} else if ($tagInput.is(':valid')) {
|
||||
$tagSelect2Container.addClass('is-valid');
|
||||
var $tagGroupInput = this.$('#tag_group_id');
|
||||
if ($tagGroupInput.length !== 0){
|
||||
var $tagGroupSelect2Container = $tagGroupInput
|
||||
.closest('.form-group')
|
||||
.find('.select2-container');
|
||||
if ($tagGroupInput.is(':invalid')) {
|
||||
$tagGroupSelect2Container.addClass('is-invalid');
|
||||
} else if ($tagGroupInput.is(':valid')) {
|
||||
$tagGroupSelect2Container.addClass('is-valid');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
_alertDisplay: function (message) {
|
||||
this._alertRemove();
|
||||
$('<div/>', {
|
||||
"class": 'alert alert-warning',
|
||||
role: 'alert'
|
||||
}).text(message).insertBefore(this.$('form'));
|
||||
},
|
||||
_alertRemove: function () {
|
||||
this.$('.alert-warning').remove();
|
||||
},
|
||||
|
||||
/**
|
||||
* When the user IS NOT creating a new tag, this function hides the group tag field
|
||||
* and makes it not required. Since the select2 field makes an extra container, this
|
||||
* needs to be hidden along with the group tag input field and its label.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_hideTagGroup: function () {
|
||||
var $tag_group_id = this.$('#tag_group_id');
|
||||
var $tagGroupSelect2Container = $tag_group_id.closest('.form-group');
|
||||
$tagGroupSelect2Container.hide();
|
||||
$tag_group_id.removeAttr("required");
|
||||
$tag_group_id.select2("val", "");
|
||||
},
|
||||
|
||||
/**
|
||||
* When the user IS creating a new tag, this function shows the field and
|
||||
* makes it required. Since the select2 field makes an extra container, this
|
||||
* needs to be shown along with the group input field and its label.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_showTagGroup: function () {
|
||||
var $tag_group_id = this.$('#tag_group_id');
|
||||
var $tagGroupSelect2Container = $tag_group_id.closest('.form-group');
|
||||
$tagGroupSelect2Container.show();
|
||||
$tag_group_id.attr("required", "required");
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handler
|
||||
//--------------------------------------------------------------------------
|
||||
_onClickFormSubmit: function () {
|
||||
var self = this;
|
||||
var $form = this.$('#slides_channel_tag_add_form');
|
||||
if (this._formValidate($form)) {
|
||||
var values = this._getSelect2DropdownValues();
|
||||
return this._rpc({
|
||||
route: '/slides/channel/tag/add',
|
||||
params: {'channel_id': this.channelID,
|
||||
'tag_id': values.tag_id,
|
||||
'group_id': values.group_id},
|
||||
}).then(function (data) {
|
||||
if (data.error) {
|
||||
self._alertDisplay(data.error);
|
||||
} else {
|
||||
window.location = data.url;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_onChangeTag: function (ev) {
|
||||
var self = this;
|
||||
var tag = $(ev.currentTarget).select2('data');
|
||||
if (tag && tag.create) {
|
||||
self._showTagGroup();
|
||||
} else {
|
||||
self._hideTagGroup();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
publicWidget.registry.websiteSlidesTag = publicWidget.Widget.extend({
|
||||
selector: '.o_wslides_js_channel_tag_add',
|
||||
xmlDependencies: ['/website_slides/static/src/xml/website_slides_channel_tag.xml'],
|
||||
events: {
|
||||
'click': '_onAddTagClick',
|
||||
},
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
_openDialog: function ($element) {
|
||||
var data = $element.data();
|
||||
return new TagCourseDialog(this, data).open();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onAddTagClick: function (ev) {
|
||||
ev.preventDefault();
|
||||
this._openDialog($(ev.currentTarget));
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
TagCourseDialog: TagCourseDialog,
|
||||
websiteSlidesTag: publicWidget.registry.websiteSlidesTag
|
||||
};
|
||||
|
||||
});
|
||||
@@ -75,6 +75,8 @@ tour.register('course_publisher', {
|
||||
run: function () {} // check membership
|
||||
}
|
||||
].concat(
|
||||
slidesTourTools.addExistingCourseTag(),
|
||||
slidesTourTools.addNewCourseTag('The Most Awesome Course'),
|
||||
slidesTourTools.addSection('Introduction'),
|
||||
slidesTourTools.addVideoToSection('Introduction'),
|
||||
[{
|
||||
|
||||
@@ -73,7 +73,7 @@ var addWebPageToSection = function (sectionName, pageName) {
|
||||
content: 'eLearning: click on tags',
|
||||
trigger: 'ul.select2-choices:first',
|
||||
}, {
|
||||
content: 'eLearning: select gardener tag',
|
||||
content: 'eLearning: select Theory tag',
|
||||
trigger: 'div.select2-result-label:contains("Theory")',
|
||||
in_modal: false,
|
||||
}, {
|
||||
@@ -83,10 +83,59 @@ var addWebPageToSection = function (sectionName, pageName) {
|
||||
}];
|
||||
};
|
||||
|
||||
var addExistingCourseTag = function () {
|
||||
return [
|
||||
{
|
||||
content: 'eLearning: click on Add Tag',
|
||||
trigger: 'a.o_wslides_js_channel_tag_add',
|
||||
}, {
|
||||
content: 'eLearning: click on tag dropdown',
|
||||
trigger: 'a.select2-choice:first',
|
||||
}, {
|
||||
content: 'eLearning: select advanced tag',
|
||||
trigger: 'div.select2-result-label:contains("Advanced")',
|
||||
in_modal: false,
|
||||
}, {
|
||||
content: 'eLearning: add existing course tag',
|
||||
trigger: 'footer.modal-footer button:contains("Add")'
|
||||
}];
|
||||
};
|
||||
|
||||
var addNewCourseTag = function (courseTagName) {
|
||||
return [
|
||||
{
|
||||
content: 'eLearning: click on Add Tag',
|
||||
trigger: 'a.o_wslides_js_channel_tag_add',
|
||||
}, {
|
||||
content: 'eLearning: click on tag dropdown',
|
||||
trigger: 'a.select2-choice:first',
|
||||
}, {
|
||||
content: 'eLearning: add a new course tag',
|
||||
trigger: 'a.select2-choice:first',
|
||||
run: function () {
|
||||
// directly add new tag since we can assume select2 works correctly
|
||||
$('#tag_id').select2('data', {id:'123', text: courseTagName, create: true});
|
||||
$('#tag_id').trigger('change');
|
||||
}
|
||||
}, {
|
||||
content: 'eLearning: click on tag group dropdown',
|
||||
trigger: 'a.select2-choice:last',
|
||||
}, {
|
||||
content: 'eLearning: select Tags tag group',
|
||||
trigger: 'div.select2-result-label:contains("Tags")',
|
||||
in_modal: false,
|
||||
}, {
|
||||
content: 'eLearning: add new course tag',
|
||||
trigger: 'footer.modal-footer button:contains("Add")'
|
||||
}];
|
||||
};
|
||||
|
||||
return {
|
||||
addSection: addSection,
|
||||
addVideoToSection: addVideoToSection,
|
||||
addWebPageToSection: addWebPageToSection,
|
||||
addExistingCourseTag: addExistingCourseTag,
|
||||
addNewCourseTag: addNewCourseTag,
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="website.slides.tag.add">
|
||||
<div class="form-group">
|
||||
<form action="/slides/channel/tag/add" method="POST" id="slides_channel_tag_add_form">
|
||||
<div class="form-group">
|
||||
<label for="tag_id" class="col-form-label">Tag</label>
|
||||
<input class="form-control" id="tag_id" required="required"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label id="tag_group_label" for="tag_group_id" class="col-form-label">Tag Group</label>
|
||||
<input class="form-control" id="tag_group_id"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -30,6 +30,7 @@
|
||||
<script type="text/javascript" src="/website_slides/static/src/js/slides_course_quiz.js"/>
|
||||
<script type="text/javascript" src="/website_slides/static/src/js/slides_course_quiz_question_form.js"/>
|
||||
<script type="text/javascript" src="/website_slides/static/src/js/slides_course_quiz_finish.js"/>
|
||||
<script type="text/javascript" src="/website_slides/static/src/js/slides_course_tag_add.js"/>
|
||||
<script type="text/javascript" src="/website_slides/static/src/js/slides_course_unsubscribe.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
@@ -193,6 +193,22 @@
|
||||
|
||||
<div class="tab-content py-4 o_wslides_tabs_content mb-4" id="courseMainTabContent">
|
||||
<div t-att-class="'tab-pane fade %s' % ('show active' if active_tab == 'home' else '')" id="home" role="tabpanel" aria-labelledby="home-tab">
|
||||
<div class="mb-2 pt-1">
|
||||
<t t-if="channel.tag_ids">
|
||||
<t t-foreach="channel.tag_ids" t-as="channel_tag">
|
||||
<span t-attf-class="badge badge-primary" t-esc="channel_tag.name"/>
|
||||
</t>
|
||||
</t>
|
||||
<a t-if="channel.can_upload"
|
||||
class="o_wslides_js_channel_tag_add border badge badge-light font-weight-normal py-1 m-1"
|
||||
role="button"
|
||||
aria-label="Add Tag"
|
||||
href="#"
|
||||
t-att-data-channel-id="channel.id"
|
||||
t-att-data-channel-tag-ids="channel.tag_ids.ids">
|
||||
<i class="fa fa-plus mr-1"/><span>Add Tag</span>
|
||||
</a>
|
||||
</div>
|
||||
<t t-if="channel.channel_type == 'training'" t-call="website_slides.course_slides_list"/>
|
||||
</div>
|
||||
<div t-if="channel.allow_comment" t-att-class="'tab-pane fade %s' % ('show active' if active_tab == 'review' else '')" id="review" role="tabpanel" aria-labelledby="review-tab">
|
||||
@@ -572,21 +588,42 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-if="channel.can_upload" class="text-right container pb-2">
|
||||
<a class="btn btn-primary py-1 o_wslides_js_slide_upload"
|
||||
title="Upload Presentation" role="button"
|
||||
aria-label="Upload Presentation" href="#"
|
||||
t-att-data-channel-id="channel.id"
|
||||
t-att-data-can-upload="channel.can_upload"
|
||||
t-att-data-can-publish="channel.can_publish">
|
||||
<i class="fa fa-cloud-upload mr-1"/>Upload new content
|
||||
</a>
|
||||
<a class="btn btn-secondary py-1 o_wslides_js_slide_section_add"
|
||||
title="Add Section" role="button"
|
||||
aria-label="Add Section" href="#"
|
||||
t-att-channel_id="channel.id">
|
||||
<i class="fa fa-folder-o mr-1"/>Add a section
|
||||
</a>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="mb-2 pt-1 text-left col">
|
||||
<t t-if="channel.tag_ids">
|
||||
<t t-foreach="channel.tag_ids" t-as="channel_tag">
|
||||
<span t-attf-class="badge badge-primary" t-esc="channel_tag.name"/>
|
||||
</t>
|
||||
</t>
|
||||
<a t-if="channel.can_upload"
|
||||
class="o_wslides_js_channel_tag_add border badge badge-light font-weight-normal py-1 m-1"
|
||||
role="button"
|
||||
aria-label="Add Tag"
|
||||
href="#"
|
||||
t-att-data-channel-id="channel.id"
|
||||
t-att-data-channel-tag-ids="channel.tag_ids.ids">
|
||||
<i class="fa fa-plus mr-1"/><span>Add Tag</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div t-if="channel.can_upload" class="text-right pb-2 col-auto">
|
||||
<a class="btn btn-primary py-1 o_wslides_js_slide_upload"
|
||||
title="Upload Presentation" role="button"
|
||||
aria-label="Upload Presentation" href="#"
|
||||
t-att-data-channel-id="channel.id"
|
||||
t-att-data-can-upload="channel.can_upload"
|
||||
t-att-data-can-publish="channel.can_publish">
|
||||
<i class="fa fa-cloud-upload mr-1"/>Upload new content
|
||||
</a>
|
||||
<a class="btn btn-secondary py-1 o_wslides_js_slide_section_add"
|
||||
title="Add Section" role="button"
|
||||
aria-label="Add Section" href="#"
|
||||
t-att-channel_id="channel.id">
|
||||
<i class="fa fa-folder-o mr-1"/>Add a section
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Featured lesson -->
|
||||
<t t-call="website_slides.course_promoted_slide"/>
|
||||
|
||||
Reference in New Issue
Block a user