diff --git a/addons/website/views/website_navbar_templates.xml b/addons/website/views/website_navbar_templates.xml index 34623a7c3a1..1c65928f56b 100644 --- a/addons/website/views/website_navbar_templates.xml +++ b/addons/website/views/website_navbar_templates.xml @@ -147,15 +147,15 @@
diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index 3ea51a7395e..bd8d49b2ed0 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -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): diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py index c2e653411c1..53e46f6d7ee 100644 --- a/addons/website_slides/models/slide_channel.py +++ b/addons/website_slides/models/slide_channel.py @@ -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 diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index 908513c5da8..4b85660885b 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -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') diff --git a/addons/website_slides/security/ir.model.access.csv b/addons/website_slides/security/ir.model.access.csv index 6f12b3236c7..9dbfa916ff6 100644 --- a/addons/website_slides/security/ir.model.access.csv +++ b/addons/website_slides/security/ir.model.access.csv @@ -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 \ No newline at end of file diff --git a/addons/website_slides/static/src/js/slides_upload.js b/addons/website_slides/static/src/js/slides_upload.js index 30dc8c9547a..3fbc7b94ab4 100644 --- a/addons/website_slides/static/src/js/slides_upload.js +++ b/addons/website_slides/static/src/js/slides_upload.js @@ -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(); + $('', { + "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(); - $('