diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py
index ee023857b95..d6bb401c7e2 100644
--- a/addons/website_slides/controllers/main.py
+++ b/addons/website_slides/controllers/main.py
@@ -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])
diff --git a/addons/website_slides/data/slide_channel_tag_demo.xml b/addons/website_slides/data/slide_channel_tag_demo.xml
index aa4d31c33e7..cf522d9877d 100644
--- a/addons/website_slides/data/slide_channel_tag_demo.xml
+++ b/addons/website_slides/data/slide_channel_tag_demo.xml
@@ -6,14 +6,17 @@
Basic
+ 10
Intermediate
+ 3
Advanced
+ 1
@@ -23,14 +26,17 @@
Gardener
+ 10
Carpenter
+ 11
Furniture Designer
+ 3
@@ -40,10 +46,11 @@
Certification
+ 7
Dog Friendly
-
\ No newline at end of file
+
diff --git a/addons/website_slides/models/slide_channel_tag.py b/addons/website_slides/models/slide_channel_tag.py
index d49491ed7e6..67c62cf742d 100644
--- a/addons/website_slides/models/slide_channel_tag.py
+++ b/addons/website_slides/models/slide_channel_tag.py
@@ -6,11 +6,11 @@ from odoo import fields, models
class SlideChannelTagGroup(models.Model):
_name = 'slide.channel.tag.group'
- _description = 'Channel/Course Tag'
+ _description = 'Channel/Course Groups'
_inherit = 'website.published.mixin'
_order = 'sequence asc'
- name = fields.Char('Name', required=True, translate=True)
+ name = fields.Char('Group Name', required=True, translate=True)
sequence = fields.Integer('Sequence', default=10, index=True, required=True)
tag_ids = fields.One2many('slide.channel.tag', 'group_id', string='Tags')
@@ -30,3 +30,4 @@ class SlideChannelTag(models.Model):
'Group sequence', related='group_id.sequence',
index=True, readonly=True, store=True)
channel_ids = fields.Many2many('slide.channel', 'slide_channel_tag_rel', 'tag_id', 'channel_id', string='Channels')
+ color = fields.Integer(string='Color Index', help="Color to apply to this tag (including in website).")
diff --git a/addons/website_slides/static/src/js/slides_course_tag_add.js b/addons/website_slides/static/src/js/slides_course_tag_add.js
new file mode 100644
index 00000000000..dcab7176a93
--- /dev/null
+++ b/addons/website_slides/static/src/js/slides_course_tag_add.js
@@ -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();
+ $('
', {
+ "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
+};
+
+});
diff --git a/addons/website_slides/static/src/scss/website_slides.scss b/addons/website_slides/static/src/scss/website_slides.scss
index b6a87ef61ca..815ae9aa050 100644
--- a/addons/website_slides/static/src/scss/website_slides.scss
+++ b/addons/website_slides/static/src/scss/website_slides.scss
@@ -58,6 +58,36 @@ $o-wslides-fs-side-width: 300px;
}
}
+// Color tags according to assigned background color.
+.o_wslides_channel_tag {
+ vertical-align: middle;
+ @for $size from 1 through length($o-colors) {
+ &.o_tag_color_#{$size - 1} {
+ $background-color: white;
+ // no color selected
+ @if $size == 1 {
+ & {
+ color: black;
+ background-color: $background-color;
+ box-shadow: inset 0 0 0 1px nth($o-colors, $size);
+ }
+ } @else {
+ $background-color: nth($o-colors, $size);
+ & {
+ color: white;
+ background-color: $background-color;
+ }
+ }
+ @at-root a#{&} {
+ &:hover {
+ color: color-yiq($background-color);
+ background-color: darken($background-color, 10%);
+ }
+ }
+ }
+ }
+}
+
.o_wslides_body {
background-color: $o-wslides-color-bg;
@@ -163,7 +193,7 @@ $o-wslides-fs-side-width: 300px;
@each $limit in $truncate-limits {
.o_wslides_desc_truncate_#{$limit} {
$line-height: 1.3;
- max-height: $limit * $line-height * 1em;
+ max-height: $limit * $line-height * 1.2em;
overflow: hidden;
text-overflow: ellipsis;
white-space: normal;
diff --git a/addons/website_slides/static/src/tests/tours/slides_course_publisher.js b/addons/website_slides/static/src/tests/tours/slides_course_publisher.js
index 2b9aad17f92..f357a2296c8 100644
--- a/addons/website_slides/static/src/tests/tours/slides_course_publisher.js
+++ b/addons/website_slides/static/src/tests/tours/slides_course_publisher.js
@@ -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'),
[{
diff --git a/addons/website_slides/static/src/tests/tours/slides_tour_tools.js b/addons/website_slides/static/src/tests/tours/slides_tour_tools.js
index 2290cd4e982..5b5f525cfeb 100644
--- a/addons/website_slides/static/src/tests/tours/slides_tour_tools.js
+++ b/addons/website_slides/static/src/tests/tours/slides_tour_tools.js
@@ -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,
};
});
diff --git a/addons/website_slides/static/src/xml/website_slides_channel_tag.xml b/addons/website_slides/static/src/xml/website_slides_channel_tag.xml
new file mode 100644
index 00000000000..081032e0a4e
--- /dev/null
+++ b/addons/website_slides/static/src/xml/website_slides_channel_tag.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
diff --git a/addons/website_slides/views/assets.xml b/addons/website_slides/views/assets.xml
index a078d02a3d3..3d896e8473e 100644
--- a/addons/website_slides/views/assets.xml
+++ b/addons/website_slides/views/assets.xml
@@ -30,6 +30,7 @@
+
diff --git a/addons/website_slides/views/slide_channel_tag_views.xml b/addons/website_slides/views/slide_channel_tag_views.xml
index 5607dd3d7a0..b4c4e9ab12e 100644
--- a/addons/website_slides/views/slide_channel_tag_views.xml
+++ b/addons/website_slides/views/slide_channel_tag_views.xml
@@ -39,7 +39,7 @@
-
+
Course Tags
slide.channel.tag
@@ -64,9 +64,23 @@
@@ -86,7 +100,7 @@
- Course Tags
+ Course Groups
slide.channel.tag.group
tree,form
diff --git a/addons/website_slides/views/slide_channel_views.xml b/addons/website_slides/views/slide_channel_views.xml
index a0922b398fe..c2df312e124 100644
--- a/addons/website_slides/views/slide_channel_views.xml
+++ b/addons/website_slides/views/slide_channel_views.xml
@@ -57,7 +57,7 @@
-
+
diff --git a/addons/website_slides/views/website_slides_menu_views.xml b/addons/website_slides/views/website_slides_menu_views.xml
index aa228171e9b..2d19f5462d9 100644
--- a/addons/website_slides/views/website_slides_menu_views.xml
+++ b/addons/website_slides/views/website_slides_menu_views.xml
@@ -67,8 +67,8 @@
sequence="1"
action="website_slides_action_settings"
groups="base.group_system"/>
-
@@ -78,4 +78,4 @@
sequence="3"
action="action_slide_tag"/>
-
\ No newline at end of file
+
diff --git a/addons/website_slides/views/website_slides_templates_course.xml b/addons/website_slides/views/website_slides_templates_course.xml
index eb276c03e7f..75396abceb0 100644
--- a/addons/website_slides/views/website_slides_templates_course.xml
+++ b/addons/website_slides/views/website_slides_templates_course.xml
@@ -193,6 +193,22 @@
-
-
- Upload new content
-
-
- Add a section
-
+
diff --git a/addons/website_slides/views/website_slides_templates_homepage.xml b/addons/website_slides/views/website_slides_templates_homepage.xml
index 19b35ed6e69..1389aeda8a5 100644
--- a/addons/website_slides/views/website_slides_templates_homepage.xml
+++ b/addons/website_slides/views/website_slides_templates_homepage.xml
@@ -360,7 +360,12 @@
diff --git a/addons/website_slides/views/website_slides_templates_profile.xml b/addons/website_slides/views/website_slides_templates_profile.xml
index e310abf9b30..d1a5a3f60e4 100644
--- a/addons/website_slides/views/website_slides_templates_profile.xml
+++ b/addons/website_slides/views/website_slides_templates_profile.xml
@@ -50,7 +50,7 @@