[MERGE][REF] website_slides: allow / improve channel and slide creation
This merge provides new modal to create slide channel from the 'New' website menu, like blogpost or forum. It also improves the upload slide widget from channel homepage. This merge also brings new slide type 'webpage'. This allow user to create completely customized content as a slide. This merge is linked to Task-1938643. More generally it is linked to the eLearning feature [1]. For more details see subcommits. [1] see task ID 1902304 (main eLearning task) PR #29876 closes odoo/odoo#30991
This commit is contained in:
@@ -147,15 +147,15 @@
|
||||
</a>
|
||||
</div>
|
||||
<div groups="base.group_system" name="module_website_slides" t-att-data-module-id="env.ref('base.module_website_slides').id" t-att-data-module-shortdesc="env.ref('base.module_website_slides').shortdesc" class="col-md-4 mb8 o_new_content_element">
|
||||
<a href="#" data-action="new_slide">
|
||||
<a href="#" data-action="new_slide_channel">
|
||||
<i class="fa fa-youtube-play"/>
|
||||
<p>New Slide</p>
|
||||
<p>New Slide Channel</p>
|
||||
</a>
|
||||
</div>
|
||||
<div groups="base.group_system" name="module_website_livechat" t-att-data-module-id="env.ref('base.module_website_livechat').id" t-att-data-module-shortdesc="env.ref('base.module_website_livechat').shortdesc" class="col-md-4 mb8 o_new_content_element">
|
||||
<a href="#" data-action="new_channel">
|
||||
<i class="fa fa-hashtag"/>
|
||||
<p>New Channel</p>
|
||||
<p>New Livechat Channel</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -114,10 +114,11 @@ class WebsiteSlides(http.Controller):
|
||||
|
||||
if search:
|
||||
domain += [
|
||||
'|', '|',
|
||||
'|', '|', '|',
|
||||
('name', 'ilike', search),
|
||||
('description', 'ilike', search),
|
||||
('index_content', 'ilike', search)]
|
||||
('index_content', 'ilike', search),
|
||||
('html_content', 'ilike', search)]
|
||||
pager_args['search'] = search
|
||||
else:
|
||||
if category:
|
||||
@@ -183,6 +184,23 @@ class WebsiteSlides(http.Controller):
|
||||
|
||||
return request.render('website_slides.home', values)
|
||||
|
||||
@http.route(['/slides/channel/add'], type='http', auth='user', methods=['POST'], website=True)
|
||||
def slide_channel_create(self, *args, **kw):
|
||||
# `tag_ids` is a string representing a list of int with coma. i.e.: '2,5,7'
|
||||
# We don't want to allow user to create tags and tag groups on the fly.
|
||||
tag_ids = []
|
||||
if kw.get('tag_ids'):
|
||||
tag_ids = [int(item) for item in kw['tag_ids'].split(',')]
|
||||
|
||||
channel = request.env['slide.channel'].create({
|
||||
'name': kw['name'],
|
||||
'description': kw.get('description'),
|
||||
'channel_type': kw.get('channel_type', 'documentation'),
|
||||
'user_id': request.env.user.id,
|
||||
'tag_ids': [(6, 0, tag_ids)],
|
||||
})
|
||||
return werkzeug.utils.redirect("/slides/%s" % (slug(channel)))
|
||||
|
||||
# --------------------------------------------------
|
||||
# SLIDE.SLIDE CONTOLLERS
|
||||
# --------------------------------------------------
|
||||
@@ -257,8 +275,8 @@ class WebsiteSlides(http.Controller):
|
||||
return {'error': 'join_done'}
|
||||
return success
|
||||
|
||||
@http.route(['/slides/dialog_preview'], type='json', auth='user', methods=['POST'], website=True)
|
||||
def dialog_preview(self, **data):
|
||||
@http.route(['/slides/prepare_preview'], type='json', auth='user', methods=['POST'], website=True)
|
||||
def prepare_preview(self, **data):
|
||||
Slide = request.env['slide.slide']
|
||||
document_type, document_id = Slide._find_document_data_from_url(data['url'])
|
||||
preview = {}
|
||||
@@ -279,13 +297,14 @@ class WebsiteSlides(http.Controller):
|
||||
def create_slide(self, *args, **post):
|
||||
# check the size only when we upload a file.
|
||||
if post.get('datas'):
|
||||
file_size = len(post['datas']) * 3 / 4 # base64
|
||||
file_size = len(post['datas']) * 3 / 4 # base64
|
||||
if (file_size / 1024.0 / 1024.0) > 25:
|
||||
return {'error': _('File is too big. File size cannot exceed 25MB')}
|
||||
|
||||
values = dict((fname, post[fname]) for fname in [
|
||||
'name', 'url', 'tag_ids', 'slide_type', 'channel_id',
|
||||
'mime_type', 'datas', 'description', 'image', 'index_content', 'website_published'] if post.get(fname))
|
||||
|
||||
if post.get('category_id'):
|
||||
if post['category_id'][0] == 0:
|
||||
values['category_id'] = request.env['slide.category'].create({
|
||||
@@ -317,7 +336,19 @@ class WebsiteSlides(http.Controller):
|
||||
except Exception as e:
|
||||
_logger.error(e)
|
||||
return {'error': _('Internal server error, please try again later or contact administrator.\nHere is the error message: %s') % e}
|
||||
return {'url': "/slides/slide/%s" % (slide.id)}
|
||||
|
||||
redirect_url = "/slides/slide/%s" % (slide.id)
|
||||
if slide.slide_type == 'webpage':
|
||||
redirect_url += "?enable_editor=1"
|
||||
return {'url': redirect_url}
|
||||
|
||||
@http.route(['/slides/channel/tag/search_read'], type='json', auth='user', methods=['POST'], website=True)
|
||||
def slide_channel_tag_search_read(self, fields, domain):
|
||||
can_create = request.env['slide.channel.tag'].check_access_rights('create', raise_exception=False)
|
||||
return {
|
||||
'read_results': request.env['slide.channel.tag'].search_read(domain, fields),
|
||||
'can_create': can_create,
|
||||
}
|
||||
|
||||
@http.route(['/slides/tag/search_read'], type='json', auth='user', methods=['POST'], website=True)
|
||||
def slide_tag_search_read(self, fields, domain):
|
||||
|
||||
@@ -67,6 +67,7 @@ class Channel(models.Model):
|
||||
('documentation', 'Documentation'), ('training', 'Training')],
|
||||
string="Course type", default="documentation", required=True)
|
||||
sequence = fields.Integer(default=10, help='Display order')
|
||||
user_id = fields.Many2one('res.users', string='Responsible', default=lambda self: self.env.uid)
|
||||
tag_ids = fields.Many2many(
|
||||
'slide.channel.tag', 'slide_channel_tag_rel', 'channel_id', 'tag_id',
|
||||
string='Tags', help='Used to categorize and filter displayed channels/courses')
|
||||
@@ -90,6 +91,7 @@ class Channel(models.Model):
|
||||
nbr_documents = fields.Integer('Number of Documents', compute='_compute_slides_statistics', store=True)
|
||||
nbr_videos = fields.Integer('Number of Videos', compute='_compute_slides_statistics', store=True)
|
||||
nbr_infographics = fields.Integer('Number of Infographics', compute='_compute_slides_statistics', store=True)
|
||||
nbr_webpages = fields.Integer("Number of Webpages", compute='_compute_slides_statistics', store=True)
|
||||
total_slides = fields.Integer('# Slides', compute='_compute_slides_statistics', store=True, oldname='total')
|
||||
total_views = fields.Integer('# Views', compute='_compute_slides_statistics', store=True)
|
||||
total_votes = fields.Integer('# Votes', compute='_compute_slides_statistics', store=True)
|
||||
@@ -149,7 +151,7 @@ class Channel(models.Model):
|
||||
'slide_ids.likes', 'slide_ids.dislikes', 'slide_ids.total_views')
|
||||
def _compute_slides_statistics(self):
|
||||
result = dict.fromkeys(self.ids, dict(
|
||||
nbr_presentations=0, nbr_documents=0, nbr_videos=0, nbr_infographics=0,
|
||||
nbr_presentations=0, nbr_documents=0, nbr_videos=0, nbr_infographics=0, nbr_webpages=0,
|
||||
total_slides=0, total_views=0, total_votes=0, total_time=0))
|
||||
read_group_res = self.env['slide.slide'].read_group(
|
||||
[('is_published', '=', True), ('channel_id', 'in', self.ids)],
|
||||
@@ -162,6 +164,7 @@ class Channel(models.Model):
|
||||
result[cid]['nbr_documents'] += res_group.get('slide_type', '') == 'document' and res_group['__count'] or 0
|
||||
result[cid]['nbr_videos'] += res_group.get('slide_type', '') == 'video' and res_group['__count'] or 0
|
||||
result[cid]['nbr_infographics'] += res_group.get('slide_type', '') == 'infographic' and res_group['__count'] or 0
|
||||
result[cid]['nbr_webpages'] += res_group.get('slide_type', '') == 'webpage' and res_group['__count'] or 0
|
||||
result[cid]['total_slides'] += res_group['__count']
|
||||
result[cid]['total_views'] += res_group.get('total_views', 0)
|
||||
result[cid]['total_votes'] += res_group.get('likes', 0)
|
||||
@@ -363,6 +366,7 @@ class Category(models.Model):
|
||||
nbr_documents = fields.Integer("Number of Documents", compute='_count_presentations', store=True)
|
||||
nbr_videos = fields.Integer("Number of Videos", compute='_count_presentations', store=True)
|
||||
nbr_infographics = fields.Integer("Number of Infographics", compute='_count_presentations', store=True)
|
||||
nbr_webpages = fields.Integer("Number of Webpages", compute='_count_presentations', store=True)
|
||||
total_slides = fields.Integer(compute='_count_presentations', store=True, oldname='total')
|
||||
|
||||
@api.depends('slide_ids.slide_type', 'slide_ids.is_published')
|
||||
@@ -379,4 +383,5 @@ class Category(models.Model):
|
||||
record.nbr_documents = result[record.id].get('document', 0)
|
||||
record.nbr_videos = result[record.id].get('video', 0)
|
||||
record.nbr_infographics = result[record.id].get('infographic', 0)
|
||||
record.total_slides = record.nbr_presentations + record.nbr_documents + record.nbr_videos + record.nbr_infographics
|
||||
record.nbr_webpages = result[record.id].get('webpage', 0)
|
||||
record.total_slides = record.nbr_presentations + record.nbr_documents + record.nbr_videos + record.nbr_infographics + record.nbr_webpages
|
||||
|
||||
@@ -31,6 +31,15 @@ class SlidePartnerRelation(models.Model):
|
||||
completed = fields.Boolean('Completed')
|
||||
|
||||
|
||||
class SlideLink(models.Model):
|
||||
_name = 'slide.slide.link'
|
||||
_description = "External URL for a particular slide"
|
||||
|
||||
slide_id = fields.Many2one('slide.slide', required=True)
|
||||
name = fields.Char('Title', required=True)
|
||||
link = fields.Char("External Link", required=True)
|
||||
|
||||
|
||||
class EmbeddedSlide(models.Model):
|
||||
""" Embedding in third party websites. Track view count, generate statistics. """
|
||||
_name = 'slide.embed'
|
||||
@@ -74,6 +83,7 @@ class Slide(models.Model):
|
||||
- Document
|
||||
- Infographic
|
||||
- Video
|
||||
- Webpage
|
||||
|
||||
Slide has various statistics like view count, embed count, like, dislikes """
|
||||
|
||||
@@ -93,7 +103,7 @@ class Slide(models.Model):
|
||||
'website_published', 'website_url', 'website_meta_title', 'website_meta_description', 'website_meta_keywords', 'website_meta_og_img']
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'UNIQUE(channel_id, name)', 'The slide name must be unique within a channel')
|
||||
('exclusion_html_content_and_url', "CHECK(html_content IS NULL OR url IS NULL)", "A slide is either filled with a document url or HTML content. Not both.")
|
||||
]
|
||||
|
||||
# description
|
||||
@@ -122,15 +132,18 @@ class Slide(models.Model):
|
||||
('infographic', 'Infographic'),
|
||||
('presentation', 'Presentation'),
|
||||
('document', 'Document'),
|
||||
('webpage', 'Web Page'),
|
||||
('video', 'Video')],
|
||||
string='Type', required=True,
|
||||
default='document',
|
||||
default='document', readonly=True,
|
||||
help="The document type will be set automatically based on the document URL and properties (e.g. height and width for presentation and document).")
|
||||
index_content = fields.Text('Transcript')
|
||||
datas = fields.Binary('Content', attachment=True)
|
||||
url = fields.Char('Document URL', help="Youtube or Google Document URL")
|
||||
document_id = fields.Char('Document ID', help="Youtube or Google Document ID")
|
||||
link_ids = fields.One2many('slide.slide.link', 'slide_id', string="External URL for this slide")
|
||||
mime_type = fields.Char('Mime-type')
|
||||
html_content = fields.Html("HTML Content", help="Custom HTML content for slides of type 'Web Page'.", translate=True)
|
||||
# website
|
||||
website_id = fields.Many2one(related='channel_id.website_id', readonly=True)
|
||||
date_published = fields.Datetime('Publish Date')
|
||||
|
||||
@@ -17,3 +17,5 @@ access_slide_category_all,slide.category.all,model_slide_category,,1,0,0,0
|
||||
access_slide_category_publisher,slide.category.publisher,model_slide_category,website.group_website_publisher,1,1,1,1
|
||||
access_slide_embed_all,slide.embed.all,model_slide_embed,,1,0,0,0
|
||||
access_slide_embed_user,slide.embed.user,model_slide_embed,base.group_user,1,1,1,1
|
||||
access_slide_slide_link_all,slide.slide.link.all,model_slide_slide_link,,1,0,0,0
|
||||
access_slide_slide_link_publisher,slide.slide.link.publisher,model_slide_slide_link,website.group_website_publisher,1,1,1,1
|
||||
|
@@ -1,50 +1,51 @@
|
||||
odoo.define('website_slides.upload', function (require) {
|
||||
odoo.define('website_slides.upload_modal', function (require) {
|
||||
'use strict';
|
||||
|
||||
var sAnimations = require('website.content.snippets.animation');
|
||||
var core = require('web.core');
|
||||
var Widget = require('web.Widget');
|
||||
var Dialog = require('web.Dialog');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
var SlideDialog = Widget.extend({
|
||||
template: 'website.slide.upload',
|
||||
events: {
|
||||
'hidden.bs.modal': 'destroy',
|
||||
'click button.save': '_save',
|
||||
'click button[data-dismiss="modal"]': '_cancel',
|
||||
'change input#upload': '_slideUpload',
|
||||
'change input#url': '_slideUrl',
|
||||
'click .list-group-item': function (ev) {
|
||||
this.$('.list-group-item').removeClass('active');
|
||||
$(ev.target).closest('li').addClass('active');
|
||||
}
|
||||
},
|
||||
|
||||
var SlideUploadDialog = Dialog.extend({
|
||||
template: 'website.slide.upload.modal',
|
||||
events: _.extend({}, Dialog.prototype.events, {
|
||||
'click .o_wslides_select_type': '_onClickSlideTypeIcon',
|
||||
'change input#upload': '_onChangeSlideUpload',
|
||||
'change input#url': '_onChangeSlideUrl',
|
||||
}),
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} el
|
||||
* @param {Object} data holding channelId and optionally upload and publish control parameters
|
||||
* @param {Object} parent
|
||||
* @param {Object} options holding channelId and optionally upload and publish control parameters
|
||||
*/
|
||||
init: function (el, data) {
|
||||
this._super(el, data);
|
||||
this.channelId = parseInt(data.channelId, 10);
|
||||
this.canUpload = data.canUpload === 'True';
|
||||
this.canPublish = data.canPublish === 'True';
|
||||
console.log(this.channelId, this.canUpload, this.canPublish);
|
||||
this.file = {};
|
||||
this.index_content = '';
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this.$el.modal({
|
||||
backdrop: 'static'
|
||||
init: function (parent, options) {
|
||||
options = _.defaults(options || {}, {
|
||||
title: _t("Upload a document"),
|
||||
size: 'medium',
|
||||
});
|
||||
this._super(parent, options);
|
||||
this._setup();
|
||||
|
||||
this.channelID = parseInt(options.channelId, 10);
|
||||
this.canUpload = options.canUpload === 'True';
|
||||
this.canPublish = options.canPublish === 'True';
|
||||
|
||||
this.set('state', '_select');
|
||||
this.on('change:state', this, this._onChangeType);
|
||||
this.set('can_submit_form', false);
|
||||
this.on('change:can_submit_form', this, this._onChangeCanSubmitForm);
|
||||
|
||||
this.file = {};
|
||||
this.isValidUrl = true;
|
||||
},
|
||||
start: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
self._resetModalButton();
|
||||
});
|
||||
this._setCategoryId();
|
||||
this._setTagIds();
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -53,32 +54,167 @@ var SlideDialog = Widget.extend({
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} file_name
|
||||
* @param {string} message
|
||||
*/
|
||||
_checkUniqueSlide: function (fileName) {
|
||||
_alertDisplay: function (message) {
|
||||
this._alertRemove();
|
||||
$('<div/>', {
|
||||
"class": 'alert alert-warning',
|
||||
role: 'alert'
|
||||
}).text(message).insertBefore(this.$('form'));
|
||||
},
|
||||
_alertRemove: function () {
|
||||
this.$('.alert-warning').remove();
|
||||
},
|
||||
/**
|
||||
* Category and tags management from select2
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_bindSelect2Dropdown: function () {
|
||||
var self = this;
|
||||
this.$('#category_id').select2(this._select2Wrapper(_t('Category'), false,
|
||||
function () {
|
||||
return self._rpc({
|
||||
route: '/slides/category/search_read',
|
||||
params: {
|
||||
fields: ['name'],
|
||||
domain: [['channel_id', '=', self.channelID]],
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
this.$('#tag_ids').select2(this._select2Wrapper(_t('Tags'), true, function () {
|
||||
return self._rpc({
|
||||
route: '/slides/tag/search_read',
|
||||
params: {
|
||||
fields: ['name'],
|
||||
domain: [],
|
||||
}
|
||||
});
|
||||
}));
|
||||
},
|
||||
_fetchUrlPreview: function (url) {
|
||||
return this._rpc({
|
||||
model: 'slide.slide',
|
||||
method: 'search_count',
|
||||
args: [[['channel_id', '=', self.channelId], ['name', '=', fileName]]],
|
||||
route: '/slides/prepare_preview/',
|
||||
params: {
|
||||
'url': url,
|
||||
'channel_id': this.channelID
|
||||
},
|
||||
});
|
||||
},
|
||||
_formSetFieldValue: function (fieldId, value) {
|
||||
this.$('form').find('#'+fieldId).val(value);
|
||||
},
|
||||
_formGetFieldValue: function (fieldId) {
|
||||
return this.$('#'+fieldId).val();
|
||||
},
|
||||
_formValidate: function () {
|
||||
var form = this.$("form");
|
||||
form.addClass('was-validated');
|
||||
return form[0].checkValidity() && this.isValidUrl;
|
||||
},
|
||||
/**
|
||||
* Extract values to submit from form, force the slide_type according to
|
||||
* filled values.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_formValidateGetValues: function (forcePublished) {
|
||||
var canvas = this.$('#data_canvas')[0];
|
||||
var values = _.extend({
|
||||
'channel_id': this.channelID,
|
||||
'name': this._formGetFieldValue('name'),
|
||||
'url': this._formGetFieldValue('url'),
|
||||
'description': this._formGetFieldValue('description'),
|
||||
'index_content': this._formGetFieldValue('index_content'),
|
||||
'website_published': forcePublished,
|
||||
}, this._getSelect2DropdownValues()); // add tags and category
|
||||
|
||||
// default slide_type (for webpage for instance)
|
||||
if (_.contains(this.slide_type_data), this.get('state')) {
|
||||
values['slide_type'] = this.get('state');
|
||||
}
|
||||
|
||||
if (this.file.type === 'application/pdf') {
|
||||
_.extend(values, {
|
||||
'image': canvas.toDataURL().split(',')[1],
|
||||
'slide_type': canvas.height > canvas.width ? 'document' : 'presentation',
|
||||
'mime_type': this.file.type,
|
||||
'datas': this.file.data
|
||||
});
|
||||
} else if (/^image\/.*/.test(this.file.type)) {
|
||||
_.extend(values, {
|
||||
'slide_type': 'infographic',
|
||||
'mime_type': this.file.type === 'image/svg+xml' ? 'image/png' : this.file.type,
|
||||
'datas': this.file.type === 'image/svg+xml' ? this._svgToPng() : this.file.data
|
||||
});
|
||||
}
|
||||
return values;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_resetFile: function () {
|
||||
_fileReset: function () {
|
||||
var control = this.$('#upload');
|
||||
control.replaceWith(control = control.clone(true));
|
||||
this.file.name = false;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} message
|
||||
*/
|
||||
_displayAlert: function (message) {
|
||||
this.$('.alert-warning').remove();
|
||||
$('<div class="alert alert-warning" role="alert">' + message + '</div>').insertBefore(this.$('form'));
|
||||
},
|
||||
|
||||
_getModalButtons: function () {
|
||||
var btnList = [];
|
||||
var state = this.get('state');
|
||||
if (state === '_select') {
|
||||
btnList.push({text: _t("Cancel"), classes: 'o_w_slide_cancel', close: true});
|
||||
} else if (state !== '_upload') { // no button when uploading
|
||||
if (this.canUpload) {
|
||||
if (this.canPublish) {
|
||||
btnList.push({text: _t("Save as Draft"), classes: 'o_w_slide_upload', click: this._onClickFormSubmit.bind(this)});
|
||||
btnList.push({text: _t("Publish"), classes: 'btn-primary o_w_slide_upload o_w_slide_upload_published', click: this._onClickFormSubmit.bind(this)});
|
||||
} else {
|
||||
btnList.push({text: _t("Save as Draft"), classes: 'btn-primary o_w_slide_upload', click: this._onClickFormSubmit.bind(this)});
|
||||
}
|
||||
}
|
||||
btnList.push({text: _t("Go Back"), classes: 'o_w_slide_go_back', click: this._onClickGoBack.bind(this)});
|
||||
}
|
||||
return btnList;
|
||||
},
|
||||
/**
|
||||
* Get value for category_id and tag_ids (ORM cmd) to send to server
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_getSelect2DropdownValues: function (){
|
||||
var result = {};
|
||||
// tags
|
||||
var tagValues = [];
|
||||
_.each(this.$('#tag_ids').select2('data'), function (val) {
|
||||
if (val.create) {
|
||||
tagValues.push([0, 0, {'name': val.text}]);
|
||||
} else {
|
||||
tagValues.push([4, val.id]);
|
||||
}
|
||||
});
|
||||
if (tagValues) {
|
||||
result['tag_ids'] = tagValues;
|
||||
}
|
||||
// category
|
||||
var categoryValue = this.$('#category_id').select2('data');
|
||||
if (categoryValue && categoryValue.create) {
|
||||
result['category_id'] = [0, {'name': categoryValue.text}];
|
||||
} else if (categoryValue) {
|
||||
result['category_id'] = [categoryValue.id];
|
||||
}
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* Reset the footer buttons, according to current state of modal
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_resetModalButton: function () {
|
||||
this.set_buttons(this._getModalButtons());
|
||||
},
|
||||
/**
|
||||
* Wrapper for select2 load data from server at once and store it.
|
||||
*
|
||||
@@ -114,7 +250,7 @@ var SlideDialog = Widget.extend({
|
||||
id: _.uniqueId('tag_'),
|
||||
create: true,
|
||||
tag: term,
|
||||
text: _.str.sprintf(_t("Create new tag '%s'"), term),
|
||||
text: _.str.sprintf(_t("Create new %s '%s'"), tag, term),
|
||||
};
|
||||
} else {
|
||||
return undefined;
|
||||
@@ -147,64 +283,28 @@ var SlideDialog = Widget.extend({
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Category management from select2
|
||||
* Init the data relative to the support slide type to upload
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_setCategoryId: function () {
|
||||
var self = this;
|
||||
$('#category_id').select2(this._select2Wrapper(_t('Category'), false,
|
||||
function () {
|
||||
return self._rpc({
|
||||
route: '/slides/category/search_read',
|
||||
params: {
|
||||
fields: ['name'],
|
||||
domain: [['channel_id', '=', self.channelId]],
|
||||
}
|
||||
});
|
||||
}));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_getCategoryId: function () {
|
||||
var value = $('#category_id').select2('data');
|
||||
if (value && value.create) {
|
||||
return [0, {'name': value.text}];
|
||||
}
|
||||
return [value ? value.id : null];
|
||||
},
|
||||
/**
|
||||
* Tags management from select2
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_setTagIds: function () {
|
||||
var self = this;
|
||||
$('#tag_ids').select2(this._select2Wrapper(_t('Tags'), true, function () {
|
||||
return self._rpc({
|
||||
route: '/slides/tag/search_read',
|
||||
params: {
|
||||
fields: ['name'],
|
||||
domain: [],
|
||||
}
|
||||
});
|
||||
}));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_getTagIds: function () {
|
||||
var res = [];
|
||||
_.each($('#tag_ids').select2('data'),
|
||||
function (val) {
|
||||
if (val.create) {
|
||||
res.push([0, 0, {'name': val.text}]);
|
||||
} else {
|
||||
res.push([4, val.id]);
|
||||
}
|
||||
});
|
||||
return res;
|
||||
_setup: function () {
|
||||
this.slide_type_data = {
|
||||
presentation: {
|
||||
icon: 'fa-file-pdf-o',
|
||||
label: _t('Presentation (PDF)'),
|
||||
template: 'website.slide.upload.modal.presentation',
|
||||
},
|
||||
webpage: {
|
||||
icon: 'fa-file-text',
|
||||
label: _t('Web Page'),
|
||||
template: 'website.slide.upload.modal.webpage',
|
||||
},
|
||||
video: {
|
||||
icon: 'fa-video-camera',
|
||||
label: _t('Video'),
|
||||
template: 'website.slide.upload.modal.video',
|
||||
},
|
||||
};
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
@@ -219,99 +319,35 @@ var SlideDialog = Widget.extend({
|
||||
canvas.getContext('2d').drawImage(img, 0, 0);
|
||||
return canvas.toDataURL('image/png').split(',')[1];
|
||||
},
|
||||
/**
|
||||
* Values and save
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_getValue: function () {
|
||||
var canvas = this.$('#data_canvas')[0],
|
||||
values = {
|
||||
'channel_id': this.channelId || '',
|
||||
'name': this.$('#name').val(),
|
||||
'url': this.$('#url').val(),
|
||||
'description': this.$('#description').val(),
|
||||
'tag_ids': this._getTagIds(),
|
||||
'category_id': this._getCategoryId()
|
||||
};
|
||||
if (this.file.type === 'application/pdf') {
|
||||
_.extend(values, {
|
||||
'image': canvas.toDataURL().split(',')[1],
|
||||
'index_content': this.index_content,
|
||||
'slide_type': canvas.height > canvas.width ? 'document' : 'presentation',
|
||||
'mime_type': this.file.type,
|
||||
'datas': this.file.data
|
||||
});
|
||||
}
|
||||
if (/^image\/.*/.test(this.file.type)) {
|
||||
_.extend(values, {
|
||||
'slide_type': 'infographic',
|
||||
'mime_type': this.file.type === 'image/svg+xml' ? 'image/png' : this.file.type,
|
||||
'datas': this.file.type === 'image/svg+xml' ? this._svgToPng() : this.file.data
|
||||
});
|
||||
}
|
||||
return values;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_validate: function () {
|
||||
this.$('.form-group').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid');
|
||||
if (!this.$('#name').val()) {
|
||||
this.$('#name').closest('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');
|
||||
return false;
|
||||
}
|
||||
var url = this.$('#url').val() ? this.is_valid_url : false;
|
||||
if (!(this.file.name || url)) {
|
||||
this.$('#url').closest('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
// Handler
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} ev
|
||||
*/
|
||||
_save: function (ev) {
|
||||
var self = this;
|
||||
if (this._validate()) {
|
||||
var values = this._getValue();
|
||||
if ($(ev.target).data('published')) {
|
||||
values.website_published = true;
|
||||
}
|
||||
this.$('.oe_slides_upload_loading').show();
|
||||
this.$('.modal-footer, .modal-body').hide();
|
||||
this._rpc({
|
||||
route: '/slides/add_slide',
|
||||
params: values,
|
||||
}).then(function (data) {
|
||||
if (data.error) {
|
||||
self._displayAlert(data.error);
|
||||
self.$('.oe_slides_upload_loading').hide();
|
||||
self.$('.modal-footer, .modal-body').show();
|
||||
_onChangeType: function () {
|
||||
var currentType = this.get('state');
|
||||
var tmpl;
|
||||
if (currentType === '_select') {
|
||||
tmpl = 'website.slide.upload.modal.select';
|
||||
} else if (currentType === '_upload') {
|
||||
tmpl = 'website.slide.upload.modal.uploading';
|
||||
} else {
|
||||
tmpl = this.slide_type_data[currentType]['template'];
|
||||
}
|
||||
this.$('.o_w_slide_upload_modal_container').empty();
|
||||
this.$('.o_w_slide_upload_modal_container').append(QWeb.render(tmpl, {widget: this}));
|
||||
|
||||
} else {
|
||||
window.location = data.url;
|
||||
}
|
||||
});
|
||||
this._resetModalButton();
|
||||
},
|
||||
_onChangeCanSubmitForm: function (ev) {
|
||||
if (this.get('can_submit_form')) {
|
||||
this.$('.o_w_slide_upload').button('reset');
|
||||
} else {
|
||||
this.$('.o_w_slide_upload').button('loading');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_cancel: function () {
|
||||
this.trigger('cancel');
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} ev
|
||||
*/
|
||||
_slideUpload: function (ev) {
|
||||
_onChangeSlideUpload: function (ev) {
|
||||
this._alertRemove();
|
||||
|
||||
var self = this;
|
||||
var file = ev.target.files[0];
|
||||
var isImage = /^image\/.*/.test(file.type);
|
||||
@@ -319,16 +355,15 @@ var SlideDialog = Widget.extend({
|
||||
this.file.name = file.name;
|
||||
this.file.type = file.type;
|
||||
if (!(isImage || this.file.type === 'application/pdf')) {
|
||||
this._displayAlert(_t("Invalid file type. Please select pdf or image file"));
|
||||
this._resetFile();
|
||||
this._alertDisplay(_t("Invalid file type. Please select pdf or image file"));
|
||||
this._fileReset();
|
||||
return;
|
||||
}
|
||||
if (file.size / 1024 / 1024 > 25) {
|
||||
this._displayAlert(_t("File is too big. File size cannot exceed 25MB"));
|
||||
this._resetFile();
|
||||
this._alertDisplay(_t("File is too big. File size cannot exceed 25MB"));
|
||||
this._fileReset();
|
||||
return;
|
||||
}
|
||||
this.$('.alert-warning').remove();
|
||||
var BinaryReader = new FileReader();
|
||||
// file read as DataURL
|
||||
BinaryReader.readAsDataURL(file);
|
||||
@@ -343,15 +378,15 @@ var SlideDialog = Widget.extend({
|
||||
|
||||
if (file.type === 'application/pdf') {
|
||||
var ArrayReader = new FileReader();
|
||||
this.$('.save').button('loading');
|
||||
this.set('can_submit_form', false);
|
||||
// file read as ArrayBuffer for PDFJS get_Document API
|
||||
ArrayReader.readAsArrayBuffer(file);
|
||||
ArrayReader.onload = function (evt) {
|
||||
var buffer = evt.target.result;
|
||||
var passwordNeeded = function () {
|
||||
self._displayAlert(_t("You can not upload password protected file."));
|
||||
self._resetFile();
|
||||
self.$('.save').button('reset');
|
||||
self._alertDisplay(_t("You can not upload password protected file."));
|
||||
self._fileReset();
|
||||
self.set('can_submit_form', true);
|
||||
};
|
||||
PDFJS.getDocument(new Uint8Array(buffer), null, passwordNeeded).then(function getPdf(pdf) {
|
||||
pdf.getPage(1).then(function getFirstPage(page) {
|
||||
@@ -369,10 +404,9 @@ var SlideDialog = Widget.extend({
|
||||
var imageData = self.$('#data_canvas')[0].toDataURL();
|
||||
self.$('#slide-image').attr('src', imageData);
|
||||
if (loaded) {
|
||||
self.$('.save').button('reset');
|
||||
self.set('can_submit_form', true);
|
||||
}
|
||||
loaded = true;
|
||||
|
||||
});
|
||||
});
|
||||
var maxPages = pdf.pdfInfo.numPages;
|
||||
@@ -389,10 +423,12 @@ var SlideDialog = Widget.extend({
|
||||
});
|
||||
// page_content may contain null characters
|
||||
pageContent = pageContent.replace(/\0/g, '');
|
||||
self.index_content = self.index_content + pageNumber + '. ' + pageContent + '\n';
|
||||
var indexContent = self._formGetFieldValue('index_content') + pageNumber + '. ' + pageContent + '\n';
|
||||
self._formSetFieldValue('index_content', indexContent);
|
||||
|
||||
if (maxPages === pageNumber) {
|
||||
if (loaded) {
|
||||
self.$('.save').button('reset');
|
||||
self.set('can_submit_form', false);
|
||||
}
|
||||
loaded = true;
|
||||
}
|
||||
@@ -405,48 +441,65 @@ var SlideDialog = Widget.extend({
|
||||
|
||||
var input = file.name;
|
||||
var inputVal = input.substr(0, input.lastIndexOf('.')) || input;
|
||||
this._checkUniqueSlide(inputVal).then(function (exist) {
|
||||
if (exist) {
|
||||
var message = _t("Channel contains the given title, please change before Save or Publish.");
|
||||
self._displayAlert(message);
|
||||
}
|
||||
self.$('#name').val(inputVal);
|
||||
});
|
||||
this._formSetFieldValue('name', inputVal);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} ev
|
||||
*/
|
||||
_slideUrl: function (ev) {
|
||||
var self = this,
|
||||
value = {
|
||||
'url': $(ev.target).val(),
|
||||
'channel_id': self.channelId
|
||||
};
|
||||
this.$('.alert-warning').remove();
|
||||
this.is_valid_url = false;
|
||||
this.$('.save').button('loading');
|
||||
this._rpc({
|
||||
route: '/slides/dialog_preview/',
|
||||
params: value,
|
||||
}).then(function (data) {
|
||||
self.$('.save').button('reset');
|
||||
_onChangeSlideUrl: function (ev) {
|
||||
var self = this;
|
||||
var url = $(ev.target).val();
|
||||
|
||||
this._alertRemove();
|
||||
this.isValidUrl = false;
|
||||
this.set('can_submit_form', false);
|
||||
this._fetchUrlPreview(url).then(function (data) {
|
||||
this.set('can_submit_form', true);
|
||||
if (data.error) {
|
||||
self._displayAlert(data.error);
|
||||
self._alertDisplay(data.error);
|
||||
} else {
|
||||
self.$('#slide-image').attr('src', data.url_src);
|
||||
self.$('#name').val(data.title);
|
||||
self.$('#description').val(data.description);
|
||||
self.is_valid_url = true;
|
||||
self._formSetFieldValue('name', data.title);
|
||||
self._formSetFieldValue('description', data.description);
|
||||
|
||||
self.isValidUrl = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
_onClickGoBack: function (ev) {
|
||||
this.set('state', '_select');
|
||||
this.isValidUrl = true;
|
||||
},
|
||||
_onClickFormSubmit: function (ev) {
|
||||
var self = this;
|
||||
var $btn = $(ev.currentTarget);
|
||||
if (this._formValidate()) {
|
||||
var values = this._formValidateGetValues($btn.hasClass('o_w_slide_upload_published')); // get info before changing state
|
||||
var oldType = this.get('state');
|
||||
this.set('state', '_upload');
|
||||
return this._rpc({
|
||||
route: '/slides/add_slide',
|
||||
params: values,
|
||||
}).then(function (data) {
|
||||
if (data.error) {
|
||||
self.set('state', oldType);
|
||||
self._alertDisplay(data.error);
|
||||
} else {
|
||||
window.location = data.url;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
_onClickSlideTypeIcon: function (ev) {
|
||||
var $elem = this.$(ev.currentTarget);
|
||||
var slideType = $elem.data('slideType');
|
||||
this.set('state', slideType);
|
||||
|
||||
this._bindSelect2Dropdown(); // rebind select2 at each modal body rendering
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
sAnimations.registry.websiteSlidesUpload = sAnimations.Class.extend({
|
||||
selector: '.oe_slide_js_upload',
|
||||
xmlDependencies: ['/website_slides/static/src/xml/website_slides.xml'],
|
||||
xmlDependencies: ['/website_slides/static/src/xml/website_slides_upload.xml'],
|
||||
read_events: {
|
||||
'click': '_onUploadClick',
|
||||
},
|
||||
@@ -468,7 +521,9 @@ sAnimations.registry.websiteSlidesUpload = sAnimations.Class.extend({
|
||||
|
||||
_openDialog: function ($element) {
|
||||
var data = $element.data();
|
||||
new SlideDialog(this, data).appendTo(document.body);
|
||||
var dialog = new SlideUploadDialog(this, data)
|
||||
dialog.appendTo(document.body);
|
||||
return dialog.open();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -483,4 +538,7 @@ sAnimations.registry.websiteSlidesUpload = sAnimations.Class.extend({
|
||||
this._openDialog($(ev.currentTarget));
|
||||
},
|
||||
});
|
||||
|
||||
return SlideUploadDialog;
|
||||
|
||||
});
|
||||
|
||||
@@ -8,12 +8,83 @@ var WebsiteNewMenu = require('website.newMenu');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
|
||||
var ChannelCreateDialog = Dialog.extend({
|
||||
template: 'website.slide.channel.create',
|
||||
/**
|
||||
* @override
|
||||
* @param {Object} parent
|
||||
* @param {Object} options
|
||||
*/
|
||||
init: function (parent, options) {
|
||||
options = _.defaults(options || {}, {
|
||||
title: _t("New Channel Slide"),
|
||||
size: 'medium',
|
||||
buttons: [{
|
||||
text: _t("Create"),
|
||||
classes: 'btn-primary',
|
||||
click: this._onClickFormSubmit.bind(this)
|
||||
}, {
|
||||
text: _t("Discard"),
|
||||
close: true
|
||||
},]
|
||||
});
|
||||
this._super(parent, options);
|
||||
},
|
||||
start: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
var $input = self.$('#tag_ids');
|
||||
$input.select2({
|
||||
width: '100%',
|
||||
allowClear: true,
|
||||
formatNoMatches: false,
|
||||
multiple: true,
|
||||
selection_data: false,
|
||||
fill_data: function (query, data) {
|
||||
var that = this,
|
||||
tags = {results: []};
|
||||
_.each(data, function (obj) {
|
||||
if (that.matcher(query.term, obj.name)) {
|
||||
tags.results.push({id: obj.id, text: obj.name});
|
||||
}
|
||||
});
|
||||
query.callback(tags);
|
||||
},
|
||||
query: function (query) {
|
||||
var that = this;
|
||||
// fetch data only once and store it
|
||||
if (!this.selection_data) {
|
||||
self._rpc({
|
||||
route: '/slides/channel/tag/search_read',
|
||||
params: {
|
||||
fields: ['name'],
|
||||
domain: [],
|
||||
}
|
||||
}).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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
_onClickFormSubmit: function (ev) {
|
||||
var $form = this.$("#slide_channel_add_form");
|
||||
$form.submit()
|
||||
},
|
||||
});
|
||||
|
||||
WebsiteNewMenu.include({
|
||||
actions: _.extend({}, WebsiteNewMenu.prototype.actions || {}, {
|
||||
new_slide: '_createNewSlide',
|
||||
new_slide_channel: '_createNewSlideChannel',
|
||||
}),
|
||||
xmlDependencies: WebsiteNewMenu.prototype.xmlDependencies.concat(
|
||||
['/website_slides/static/src/xml/website_slides.xml']
|
||||
['/website_slides/static/src/xml/website_slides_channel.xml']
|
||||
),
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -21,44 +92,20 @@ WebsiteNewMenu.include({
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Asks the user information about in which channel to create a new slide,
|
||||
* and redirects the user to this channel with the "new slide" popup open.
|
||||
* Displays the popup to create a new slide channel,
|
||||
* and redirects the user to this channel.
|
||||
*
|
||||
* @private
|
||||
* @returns {Deferred} Unresolved if there is a redirection
|
||||
*/
|
||||
_createNewSlide: function () {
|
||||
var self = this;
|
||||
return this._rpc({
|
||||
model: 'slide.channel',
|
||||
method: 'list_all',
|
||||
args: [[]],
|
||||
}).then(function (data) {
|
||||
var def = $.Deferred();
|
||||
new Dialog(self, {
|
||||
title: _t("New slide"),
|
||||
subtitle: _t("On which channel do you want to add a slide?"),
|
||||
size: 'medium',
|
||||
$content: QWeb.render('website.slide.create', data),
|
||||
buttons: [{
|
||||
text: _t("Select"),
|
||||
classes: 'btn-primary',
|
||||
click: function () {
|
||||
var channel_url = this.$("option:selected").val();
|
||||
if (channel_url) {
|
||||
window.location.href = channel_url + '?enable_slide_upload';
|
||||
} else {
|
||||
def.reject();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
text: _t("Cancel"), close: true
|
||||
},]
|
||||
}).open()
|
||||
.on('closed', def.resolve.bind(def));
|
||||
|
||||
return def;
|
||||
_createNewSlideChannel: function () {
|
||||
var def = $.Deferred();
|
||||
var dialog = new ChannelCreateDialog(this, {});
|
||||
dialog.open();
|
||||
dialog.on('closed', this, function() {
|
||||
def.resolve();
|
||||
});
|
||||
},
|
||||
return def;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -172,3 +172,9 @@ div.embed-responsive {
|
||||
.oe_show_footer {
|
||||
z-index: $MAX-Z-INDEX; // Looks terrible but seems necessary due to fullscreen & canvas in PDFSlidesViewer
|
||||
}
|
||||
|
||||
|
||||
// Upload Modal
|
||||
.o_wslides_select_type {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="website.slide.upload">
|
||||
<div role="dialog" class="modal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<header class="modal-header">
|
||||
<h3 class="modal-title">Upload Presentation</h3>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button>
|
||||
</header>
|
||||
<div class="oe_slides_upload_loading text-center" style="display:none" aria-hidden="true" role="status">
|
||||
<h4><i class='fa fa-spinner fa-spin'></i> Uploading presentation... </h4>
|
||||
</div>
|
||||
<main class="modal-body">
|
||||
<form class="clearfix">
|
||||
<div class="form-group row">
|
||||
<div class="col-md-4">
|
||||
<div class="img-thumbnail">
|
||||
<div class="o_slide_preview">
|
||||
<img src="/website_slides/static/src/img/document.png" id="slide-image" title="Content Preview" alt="Content Preview" class="img-fluid"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item active">
|
||||
<h5 class="list-group-item-heading">
|
||||
<label for="upload" class="col-form-label">PDF or Image File</label>
|
||||
</h5>
|
||||
<input id="upload" name="file" class="form-control" accept="image/*,application/pdf" type="file"/>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<h5 class="list-group-item-heading">
|
||||
<label for="url" class="col-form-label">Youtube or Google Doc URL</label>
|
||||
</h5>
|
||||
<input id="url" name="url" class="form-control" placeholder="Youtube Video URL"/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="name" class="col-form-label col-md-2">Title</label>
|
||||
<div class="col-md-10">
|
||||
<input id="name" placeholder="Title" class="form-control" required="required"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="tag_ids" class="col-form-label col-md-2">Tags</label>
|
||||
<div class="controls col-md-10">
|
||||
<input id="tag_ids" type="hidden"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="category_id" class="col-form-label col-md-2">Category</label>
|
||||
<div class="controls col-md-10">
|
||||
<input id="category_id" type="hidden"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="description" class="col-form-label col-md-2">Description</label>
|
||||
<div class="controls col-md-10">
|
||||
<textarea id="description" rows="4" placeholder="Description" class="form-control"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="data_canvas" style="display: none;"></canvas>
|
||||
</form>
|
||||
</main>
|
||||
<footer class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Discard</button>
|
||||
<button type="button" data-loading-text="Loading..." t-if="widget.canUpload && widget.canPublish" class="btn btn-primary save">Save as Draft</button>
|
||||
<button type="button" data-loading-text="Loading..." t-if="widget.canUpload && ! widget.canPublish" class="btn btn-primary save">Submit</button>
|
||||
<button type="button" data-loading-text="Loading..." t-if="widget.canUpload && widget.canPublish" data-published="true" class="btn btn-primary save">Publish</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="website.slide.create">
|
||||
<form>
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label col-md-3" for="slide_channel_id">Channel</label>
|
||||
<div class="col-md-9">
|
||||
<select class="form-control" id="slide_channel_id" required="">
|
||||
<t t-foreach="channels" t-as="channel">
|
||||
<option t-att-value="channel.website_url"><t t-esc="channel.name"/></option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="website.slide.channel.create">
|
||||
<div>
|
||||
<form action="/slides/channel/add" method="POST" id="slide_channel_add_form">
|
||||
<input type="hidden" name="csrf_token" t-att-value="csrf_token"/>
|
||||
<div class="form-group row">
|
||||
<label for="title" class="col-sm-2 col-form-label">Title</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" name="name" id="title" placeholder="Title"/>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="form-group">
|
||||
<div class="row">
|
||||
<legend class="col-form-label col-sm-2">Type</legend>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="channel_type" id="channel_type1" value="training" checked="checked"/>
|
||||
<label class="form-check-label" for="channel_type1">
|
||||
Training (with progress)
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="channel_type" id="channel_type2" value="documentation"/>
|
||||
<label class="form-check-label" for="channel_type2">
|
||||
Documentation
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="form-group row">
|
||||
<label for="title" class="col-sm-2 col-form-label">Description</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea name="description" id="description" style="height:8em" class="form-control" placeholder="Write here a short description of your first course"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="title" class="col-sm-2 col-form-label">Tags</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" name="tag_ids" id="tag_ids" placeholder="Tags"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="website.slide.upload.modal">
|
||||
<div>
|
||||
<div class="o_w_slide_upload_modal_container">
|
||||
<t t-call="website.slide.upload.modal.select"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
Slide Type Selection template
|
||||
-->
|
||||
<t t-name="website.slide.upload.modal.select">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-stretch text-primary p-1 mt-4">
|
||||
<t t-foreach="widget.slide_type_data" t-as="slide_type">
|
||||
<t t-set="type_data" t-value="widget.slide_type_data[slide_type]"/>
|
||||
|
||||
<div class="content-type d-flex flex-column align-items-center p-1 mb-4 o_wslides_select_type" t-att-data-slide-type="slide_type">
|
||||
<i t-attf-class="fa #{type_data['icon']} mb-2" style="font-size:4rem;"></i>
|
||||
<t t-esc="type_data['label']"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
Uploading template
|
||||
-->
|
||||
<t t-name="website.slide.upload.modal.uploading">
|
||||
<div class="oe_slides_upload_loading text-center" aria-hidden="true" role="status">
|
||||
<div class="fa-3x">
|
||||
<i class="fas fa-spinner fa-pulse"></i>
|
||||
</div>
|
||||
<h4>Uploading document ...</h4>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
Slide Type common form part template
|
||||
-->
|
||||
<t t-name="website.slide.upload.modal.common">
|
||||
<div class="form-group row">
|
||||
<label for="name" class="col-form-label col-md-2">Title</label>
|
||||
<div class="col-md-10">
|
||||
<input id="name" name="name" placeholder="Title" class="form-control" required="required"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="category_id" class="col-form-label col-md-2">Category</label>
|
||||
<div class="controls col-md-10">
|
||||
<input class="form-control" id="category_id"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="tag_ids" class="col-form-label col-md-2">Tags</label>
|
||||
<div class="controls col-md-10">
|
||||
<input id="tag_ids" name="tag_ids" type="hidden"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
Slide Type templates
|
||||
-->
|
||||
<t t-name="website.slide.upload.modal.presentation">
|
||||
<div>
|
||||
<form class="clearfix">
|
||||
<div class="form-group row">
|
||||
<div class="col-md-4">
|
||||
<div class="img-thumbnail">
|
||||
<div class="o_slide_preview">
|
||||
<img src="/website_slides/static/src/img/document.png" id="slide-image" title="Content Preview" alt="Content Preview" class="img-fluid"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<h5 class="list-group-item-heading">
|
||||
<label for="upload" class="col-form-label">PDF or Image File</label>
|
||||
</h5>
|
||||
<input id="upload" name="file" class="form-control" accept="image/*,application/pdf" type="file" required="required"/>
|
||||
<input id="index_content" name="index_content" type="hidden"/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="data_canvas" class="d-none"></canvas>
|
||||
<t t-call="website.slide.upload.modal.common"/>
|
||||
</form>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="website.slide.upload.modal.webpage">
|
||||
<div>
|
||||
<form class="clearfix">
|
||||
<t t-call="website.slide.upload.modal.common"/>
|
||||
</form>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="website.slide.upload.modal.video">
|
||||
<div>
|
||||
<form class="clearfix">
|
||||
<div class="form-group row">
|
||||
<div class="col-md-4">
|
||||
<div class="img-thumbnail">
|
||||
<div class="o_slide_preview">
|
||||
<img src="/website_slides/static/src/img/document.png" id="slide-image" title="Content Preview" alt="Content Preview" class="img-fluid"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<h5 class="list-group-item-heading">
|
||||
<label for="url" class="col-form-label">Youtube Link</label>
|
||||
</h5>
|
||||
<input id="url" name="url" class="form-control" placeholder="Youtube Video URL" required="required"/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="data_canvas" class="d-none"></canvas>
|
||||
<t t-call="website.slide.upload.modal.common"/>
|
||||
</form>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -79,6 +79,7 @@
|
||||
context="{'default_channel_id': active_id}"
|
||||
domain="[('channel_id','=', active_id)]"/>
|
||||
<field name="tag_ids" widget="many2many_tags" placeholder="Tags"/>
|
||||
<field name="user_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook colspan="4">
|
||||
@@ -88,7 +89,7 @@
|
||||
<page string="Options">
|
||||
<group>
|
||||
<group>
|
||||
<field name="channel_type"/>
|
||||
<field name="channel_type" widget="radio"/>
|
||||
<field name="website_id" options="{'no_create': True}" groups="website.group_multi_website"/>
|
||||
<field name="promote_strategy" widget="radio"/>
|
||||
</group>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<group>
|
||||
<field name="url" attrs="{'required': [('image', '=', False)]}"/>
|
||||
<field name="slide_type" readonly="1"/>
|
||||
<field name="document_id" readonly="1"/>
|
||||
<field name="document_id" readonly="1" attrs="{'invisible': [('document_id', '=', False)]}"/>
|
||||
<field name="mime_type" readonly="1"/>
|
||||
<field name="datas" readonly="1"/>
|
||||
</group>
|
||||
@@ -92,6 +92,16 @@
|
||||
<label for="index_content"/>
|
||||
<field name="index_content"/>
|
||||
</page>
|
||||
<page string="External Links">
|
||||
<group>
|
||||
<field name="link_ids" widget="one2many" nolabel="1">
|
||||
<tree editable="top">
|
||||
<field name="name"/>
|
||||
<field name="link" widget="url"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
|
||||
@@ -182,6 +182,12 @@
|
||||
<span class="badge badge-pill" t-esc="header_object.nbr_infographics"/> Infographics
|
||||
</a>
|
||||
</li>
|
||||
<li t-if="header_object.nbr_webpages" class="nav-item">
|
||||
<a t-attf-href="/slides/#{slug(channel)}#{category and '/category/' + slug(category) or ''}/webpage"
|
||||
t-attf-class="nav-link#{slide_type == 'webpage' and ' active' or ''}">
|
||||
<span class="badge badge-pill" t-esc="header_object.nbr_webpages"/> Pages
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Sort options -->
|
||||
@@ -411,7 +417,10 @@
|
||||
<div t-if="slide.slide_type == 'video' and slide.document_id" class="embed-responsive embed-responsive-16by9 embed-responsive-item mb8">
|
||||
<t t-raw="slide.embed_code"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div t-if="slide.slide_type == 'webpage'" class="border border-light rounded">
|
||||
<div t-field="slide.html_content"/>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<h4 t-field="slide.name" class="col-lg-6"/>
|
||||
<div class="col-lg-6">
|
||||
<div clas="row">
|
||||
@@ -513,6 +522,11 @@
|
||||
<i class="fa fa-bar-chart"></i> Statistics
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" t-if="slide.link_ids">
|
||||
<a href="#external-links" aria-controls="external-links" class="nav-link" role="tab" data-toggle="tab">
|
||||
<i class="fa fa-link"></i> External Links
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" style="padding: 20px 5px 5px 5px; word-wrap: break-word;">
|
||||
<div role="tabpanel" t-att-class="not comments and 'tab-pane fade in show active' or 'tab-pane fade'" id="about">
|
||||
@@ -628,6 +642,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane fade oe_slides_transcript" id="external-links" t-if="slide.link_ids">
|
||||
<ul>
|
||||
<t t-foreach="slide.link_ids" t-as="link">
|
||||
<li><a t-att-href="link.link" target="_blank"><t t-esc="link.name"/></a></li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user