[IMP] portal, website_slides: send message without reloading page

1) in this commit, improved portal chatter for messaging. when the user sends a
message, the new message is updated in history without reloading page.
instead of submitting a form which reloads page to send message we added rpc call
to send message.
2) rating on courses is implemented by extending portal chatter, applied changes in
website_slides to do reviews without reloading page
3) refactoring: cleaned or removed code
eg. removed form and some input fields as data is not submitted as form anymore,
introduced some methods to avoid duplication of code

task-2054662
Closes https://github.com/odoo/odoo/pull/47746

closes odoo/odoo#47746

Signed-off-by: Samuel Degueldre <sdegueldre@users.noreply.github.com>
Co-authored-by: ktr-odoo <ktr@odoo.com>
Co-authored-by: Siddarth Gajjar <sga@odoo.com>
This commit is contained in:
jpr-odoo
2020-11-30 11:22:13 +00:00
committed by Siddarth Gajjar
co-authored by ktr-odoo Siddarth Gajjar
parent 0f3467587e
commit 75b4e2dd38
17 changed files with 330 additions and 147 deletions
+55 -45
View File
@@ -167,48 +167,54 @@ class TestPortalAttachment(AccountTestInvoicingHttpCommon):
message.sudo().unlink()
# Test attachment can't be associated if no attachment token.
res = self.url_open(
res = self.opener.post(
url='%s/mail/chatter_post' % self.base_url,
data={
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'message': "test message 1",
'attachment_ids': attachment.id,
'attachment_tokens': 'false',
'csrf_token': http.WebRequest.csrf_token(self),
json={
'params': {
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'message': "test message 1",
'attachment_ids': [attachment.id],
'attachment_tokens': ['false'],
'csrf_token': http.WebRequest.csrf_token(self),
},
},
)
self.assertEqual(res.status_code, 400)
self.assertEqual(res.status_code, 200)
self.assertIn("The attachment %s does not exist or you do not have the rights to access it." % attachment.id, res.text)
# Test attachment can't be associated if no main document token
res = self.url_open(
res = self.opener.post(
url='%s/mail/chatter_post' % self.base_url,
data={
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'message': "test message 1",
'attachment_ids': attachment.id,
'attachment_tokens': attachment.access_token,
'csrf_token': http.WebRequest.csrf_token(self),
json={
'params': {
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'message': "test message 1",
'attachment_ids': [attachment.id],
'attachment_tokens': [attachment.access_token],
'csrf_token': http.WebRequest.csrf_token(self),
},
},
)
self.assertEqual(res.status_code, 403)
self.assertEqual(res.status_code, 200)
self.assertIn("You are not allowed to access 'Journal Entry' (account.move) records.", res.text)
# Test attachment can't be associated if not "pending" state
self.assertFalse(self.out_invoice.message_ids)
attachment.write({'res_model': 'model'})
res = self.url_open(
res = self.opener.post(
url='%s/mail/chatter_post' % self.base_url,
data={
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'message': "test message 1",
'attachment_ids': attachment.id,
'attachment_tokens': attachment.access_token,
'csrf_token': http.WebRequest.csrf_token(self),
'token': self.out_invoice._portal_ensure_token(),
json={
'params': {
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'message': "test message 1",
'attachment_ids': [attachment.id],
'attachment_tokens': [attachment.access_token],
'csrf_token': http.WebRequest.csrf_token(self),
'token': self.out_invoice._portal_ensure_token(),
},
},
)
self.assertEqual(res.status_code, 200)
@@ -219,16 +225,18 @@ class TestPortalAttachment(AccountTestInvoicingHttpCommon):
# Test attachment can't be associated if not correct user
attachment.write({'res_model': 'mail.compose.message'})
res = self.url_open(
res = self.opener.post(
url='%s/mail/chatter_post' % self.base_url,
data={
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'message': "test message 2",
'attachment_ids': attachment.id,
'attachment_tokens': attachment.access_token,
'csrf_token': http.WebRequest.csrf_token(self),
'token': self.out_invoice._portal_ensure_token(),
json={
'params': {
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'message': "test message 2",
'attachment_ids': [attachment.id],
'attachment_tokens': [attachment.access_token],
'csrf_token': http.WebRequest.csrf_token(self),
'token': self.out_invoice._portal_ensure_token(),
},
},
)
self.assertEqual(res.status_code, 200)
@@ -253,16 +261,18 @@ class TestPortalAttachment(AccountTestInvoicingHttpCommon):
create_res = json.loads(res.content.decode('utf-8'))
self.assertEqual(create_res['name'], "final attachment")
res = self.url_open(
res = self.opener.post(
url='%s/mail/chatter_post' % self.base_url,
data={
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'message': "test message 3",
'attachment_ids': create_res['id'],
'attachment_tokens': create_res['access_token'],
'csrf_token': http.WebRequest.csrf_token(self),
'token': self.out_invoice._portal_ensure_token(),
json={
'params': {
'res_model': self.out_invoice._name,
'res_id': self.out_invoice.id,
'message': "test message 3",
'attachment_ids': [create_res['id']],
'attachment_tokens': [create_res['access_token']],
'csrf_token': http.WebRequest.csrf_token(self),
'token': self.out_invoice._portal_ensure_token(),
},
},
)
self.assertEqual(res.status_code, 200)
+7 -9
View File
@@ -113,24 +113,20 @@ class PortalChatter(http.Controller):
except (AccessError, MissingError):
raise UserError(_("The attachment %s does not exist or you do not have the rights to access it.", attachment_id))
@http.route(['/mail/chatter_post'], type='http', methods=['POST'], auth='public', website=True)
def portal_chatter_post(self, res_model, res_id, message, redirect=None, attachment_ids='', attachment_tokens='', **kw):
"""Create a new `mail.message` with the given `message` and/or
`attachment_ids` and redirect the user to the newly created message.
@http.route(['/mail/chatter_post'], type='json', methods=['POST'], auth='public', website=True)
def portal_chatter_post(self, res_model, res_id, message, attachment_ids=None, attachment_tokens=None, **kw):
"""Create a new `mail.message` with the given `message` and/or `attachment_ids` and return new message values.
The message will be associated to the record `res_id` of the model
`res_model`. The user must have access rights on this target document or
must provide valid identifiers through `kw`. See `_message_post_helper`.
"""
url = redirect or (request.httprequest.referrer and request.httprequest.referrer + "#discussion") or '/my'
res_id = int(res_id)
attachment_ids = [int(attachment_id) for attachment_id in attachment_ids.split(',') if attachment_id]
attachment_tokens = [attachment_token for attachment_token in attachment_tokens.split(',') if attachment_token]
self._portal_post_check_attachments(attachment_ids, attachment_tokens)
if message or attachment_ids:
result = {'default_message': message}
# message is received in plaintext and saved in html
if message:
message = plaintext2html(message)
@@ -143,6 +139,7 @@ class PortalChatter(http.Controller):
}
post_values.update((fname, kw.get(fname)) for fname in self._portal_post_filter_params())
message = _message_post_helper(**post_values)
result.update({'default_message_id': message.id})
if attachment_ids:
# sudo write the attachment to bypass the read access
@@ -154,7 +151,8 @@ class PortalChatter(http.Controller):
if attachments.get('attachment_ids'):
message.sudo().write(attachments)
return request.redirect(url)
result.update({'default_attachment_ids': message.attachment_ids.sudo().read(['id', 'name', 'mimetype', 'file_size', 'access_token'])})
return result
@http.route('/mail/chatter_init', type='json', auth='public', website=True)
def portal_chatter_init(self, res_model, res_id, domain=False, limit=False, **kwargs):
+29 -11
View File
@@ -84,18 +84,12 @@ var PortalChatter = publicWidget.Widget.extend({
// set options and parameters
this.set('message_count', this.options['message_count']);
this.set('messages', this.preprocessMessages(this.result['messages']));
// bind bus event: this (portal.chatter) and 'portal.rating.composer' in portal_rating
// are separate and sibling widgets, this event is to be triggered from portal.rating.composer,
// hence bus event is bound to achieve usage of the event in another widget.
core.bus.on('reload_chatter_content', this, this._reloadChatterContent);
var defs = [];
defs.push(this._super.apply(this, arguments));
// instanciate and insert composer widget
if (this.options['display_composer']) {
this._composer = new portalComposer.PortalComposer(this, this.options);
defs.push(this._composer.replace(this.$('.o_portal_chatter_composer')));
}
return Promise.all(defs);
return Promise.all([this._super.apply(this, arguments), this._reloadComposer()]);
},
//--------------------------------------------------------------------------
@@ -117,6 +111,7 @@ var PortalChatter = publicWidget.Widget.extend({
}).then(function (result) {
self.set('messages', self.preprocessMessages(result['messages']));
self.set('message_count', result['message_count']);
return result;
});
},
/**
@@ -137,6 +132,29 @@ var PortalChatter = publicWidget.Widget.extend({
// Private
//--------------------------------------------------------------------------
/**
* Reloads chatter and message count after posting message
*
* @private
*/
_reloadChatterContent: function (data) {
this.messageFetch();
this._reloadComposer();
},
/**
* Destroy current composer widget and initialize and insert new widget
*
* @private
*/
_reloadComposer: async function () {
if (this._composer) {
this._composer.destroy();
}
if (this.options.display_composer) {
this._composer = new portalComposer.PortalComposer(this, this.options);
await this._composer.appendTo(this.$('.o_portal_chatter_composer'));
}
},
/**
* @private
* @returns {Deferred}
+39 -11
View File
@@ -47,9 +47,8 @@ var PortalComposer = publicWidget.Widget.extend({
this.$attachmentButton = this.$('.o_portal_chatter_attachment_btn');
this.$fileInput = this.$('.o_portal_chatter_file_input');
this.$sendButton = this.$('.o_portal_chatter_composer_btn');
this.$attachments = this.$('.o_portal_chatter_composer_form .o_portal_chatter_attachments');
this.$attachmentIds = this.$('.o_portal_chatter_attachment_ids');
this.$attachmentTokens = this.$('.o_portal_chatter_attachment_tokens');
this.$attachments = this.$('.o_portal_chatter_composer_input .o_portal_chatter_attachments');
this.$inputTextarea = this.$('.o_portal_chatter_composer_input textarea[name="message"]');
return this._super.apply(this, arguments).then(function () {
if (self.options.default_attachment_ids) {
@@ -139,15 +138,31 @@ var PortalComposer = publicWidget.Widget.extend({
});
},
/**
* Returns a Promise that is never resolved to prevent sending the form
* twice when clicking twice on the button, in combination with the `async`
* in the event definition.
* prepares data to send message
*
* @private
* @returns {Promise}
*/
_onSubmitButtonClick: function () {
return new Promise(function (resolve, reject) {});
_prepareMessageData: function () {
return Object.assign(this.options || {}, {
'message': this.$('textarea[name="message"]').val(),
'attachment_ids': _.pluck(this.attachments, 'id'),
'attachment_tokens': _.pluck(this.attachments, 'access_token'),
});
},
/**
* @private
* @param {Event} ev
*/
_onSubmitButtonClick: function (ev) {
ev.preventDefault();
if (!this.$inputTextarea.val().trim() && !this.attachments.length) {
this.$inputTextarea.addClass('border-danger');
const error = _t('Some fields are required. Please make sure to write a message or attach a document');
this.$(".o_portal_chatter_composer_error").text(error).removeClass('d-none');
return Promise.reject();
} else {
return this._chatterPostMessage(ev.currentTarget.getAttribute('data-action'));
}
},
//--------------------------------------------------------------------------
@@ -158,13 +173,26 @@ var PortalComposer = publicWidget.Widget.extend({
* @private
*/
_updateAttachments: function () {
this.$attachmentIds.val(_.pluck(this.attachments, 'id'));
this.$attachmentTokens.val(_.pluck(this.attachments, 'access_token'));
this.$attachments.html(qweb.render('portal.Chatter.Attachments', {
attachments: this.attachments,
showDelete: true,
}));
},
/**
* post message using rpc call and display new message and message count
*
* @private
* @param {String} route
* @returns {Promise}
*/
_chatterPostMessage: async function (route) {
const result = await this._rpc({
route: route,
params: this._prepareMessageData(),
});
core.bus.trigger('reload_chatter_content', result);
return result;
},
});
return {
@@ -28,39 +28,28 @@
<p>You must be <a t-attf-href="/web/login?redirect=#{discussion_url}">logged in</a> to post a comment.</p>
</t>
<t t-if="widget.options['display_composer']">
<div class="alert alert-danger mb8 d-none o_portal_chatter_composer_error" role="alert">
Oops! Something went wrong. Try to reload the page and log in.
</div>
<div class="media">
<img alt="Avatar" class="o_portal_chatter_avatar" t-attf-src="/web/image/res.partner/#{widget.options['partner_id']}/image_128/50x50"
t-if="!widget.options['is_user_public'] or !widget.options['token']"/>
<div class="media-body">
<form class="o_portal_chatter_composer_form" t-attf-action="/mail/chatter_post" method="POST">
<input type="hidden" name="csrf_token" t-att-value="widget.options['csrf_token']"/>
<div class="mb32">
<div class="o_portal_chatter_composer_input">
<div class="o_portal_chatter_composer_body mb32">
<textarea rows="4" name="message" class="form-control" placeholder="Write a message..."></textarea>
<input type="hidden" name="res_model" t-att-value="widget.options['res_model']"/>
<input type="hidden" name="res_id" t-att-value="widget.options['res_id']"/>
<input type="hidden" name="token" t-att-value="widget.options['token']" t-if="widget.options['token']"/>
<input type='hidden' name="pid" t-att-value="widget.options['pid']" t-if="widget.options['pid']"/>
<input type='hidden' name="hash" t-att-value="widget.options['hash']" t-if="widget.options['hash']"/>
<input type="hidden" name="sha_in" t-att-value="widget.options['sha_in']" t-if="widget.options['sha_in']"/>
<input type="hidden" name="sha_time" t-att-value="widget.options['sha_time']" t-if="widget.options['sha_time']"/>
<input type="hidden" name="redirect" t-att-value="discussion_url"/>
<input type="hidden" name="attachment_ids" class="o_portal_chatter_attachment_ids"/>
<input type="hidden" name="attachment_tokens" class="o_portal_chatter_attachment_tokens"/>
<div class="alert alert-danger mt8 mb0 o_portal_chatter_composer_error" style="display:none;" role="alert">
Oops! Something went wrong. Try to reload the page and log in.
</div>
<div class="o_portal_chatter_attachments mt-3"/>
<div class="mt8">
<button t-attf-class="o_portal_chatter_composer_btn btn btn-primary" type="submit">Send</button>
<button t-attf-data-action="/mail/chatter_post" class="o_portal_chatter_composer_btn btn btn-primary" type="submit">Send</button>
<button class="o_portal_chatter_attachment_btn btn btn-secondary" type="button" title="Add attachment">
<i class="fa fa-paperclip"/>
</button>
</div>
</div>
</form>
<form class="d-none">
</div>
<div class="d-none">
<input type="file" class="o_portal_chatter_file_input" multiple="multiple"/>
</form>
</div>
</div>
</div>
</t>
@@ -14,20 +14,24 @@ class PortalChatter(mail.PortalChatter):
fields += ['rating_value', 'rating_feedback']
return fields
def _portal_rating_stats(self, res_model, res_id, **kwargs):
# get the rating statistics for the record
if kwargs.get('rating_include'):
record = request.env[res_model].browse(res_id)
if hasattr(record, 'rating_get_stats'):
return {'rating_stats': record.sudo().rating_get_stats()}
return {}
@http.route()
def portal_chatter_post(self, res_model, res_id, message, redirect=None, attachment_ids='', attachment_tokens='', **kwargs):
def portal_chatter_post(self, res_model, res_id, message, attachment_ids='', attachment_tokens='', **kwargs):
if kwargs.get('rating_value'):
kwargs['rating_feedback'] = kwargs.pop('rating_feedback', message)
return super(PortalChatter, self).portal_chatter_post(res_model, res_id, message, redirect=redirect, attachment_ids=attachment_ids, attachment_tokens=attachment_tokens, **kwargs)
return super(PortalChatter, self).portal_chatter_post(res_model, res_id, message, attachment_ids=attachment_ids, attachment_tokens=attachment_tokens, **kwargs)
@http.route()
def portal_chatter_init(self, res_model, res_id, domain=False, limit=False, **kwargs):
result = super(PortalChatter, self).portal_chatter_init(res_model, res_id, domain=domain, limit=limit, **kwargs)
# get the rating statistics about the record
if kwargs.get('rating_include'):
record = request.env[res_model].browse(res_id)
if hasattr(record, 'rating_get_stats'):
result['rating_stats'] = record.sudo().rating_get_stats()
result.update(self._portal_rating_stats(res_model, res_id, **kwargs))
return result
@http.route()
@@ -37,4 +41,6 @@ class PortalChatter(mail.PortalChatter):
context = dict(request.context)
context['rating_include'] = True
request.context = context
return super(PortalChatter, self).portal_message_fetch(res_model, res_id, domain=domain, limit=limit, offset=offset, **kw)
result = super(PortalChatter, self).portal_message_fetch(res_model, res_id, domain=domain, limit=limit, offset=offset, **kw)
result.update(self._portal_rating_stats(res_model, res_id, **kw))
return result
@@ -50,6 +50,13 @@ PortalChatter.include({
this.set('rating_value', false);
this.on("change:rating_value", this, this._onChangeRatingDomain);
},
/**
* @override
*/
start: function () {
this._super.apply(this, arguments);
this.on("change:rating_card_values", this, this._renderRatingCard);
},
//--------------------------------------------------------------------------
// Public
@@ -105,25 +112,43 @@ PortalChatter.include({
/**
* @override
* @returns {Promise}
*/
_chatterInit: function () {
var self = this;
return this._super.apply(this, arguments).then(function (result) {
if (!result['rating_stats']) {
return;
}
var ratingData = {
'avg': Math.round(result['rating_stats']['avg'] * 100) / 100,
'percent': [],
};
_.each(_.keys(result['rating_stats']['percent']).reverse(), function (rating) {
ratingData['percent'].push({
'num': rating,
'percent': utils.round_precision(result['rating_stats']['percent'][rating], 0.01),
});
_chatterInit: async function () {
const result = await this._super(...arguments);
this._updateRatingCardValues(result);
return result;
},
/**
* @override
* @param {Array} domain
* @returns {Promise}
*/
messageFetch: async function (domain) {
const result = await this._super(...arguments);
this._updateRatingCardValues(result);
return result;
},
/**
* Calculates and Updates rating values i.e. average, percentage
*
* @private
*/
_updateRatingCardValues: function (result) {
if (!result['rating_stats']) {
return;
}
const ratingData = {
'avg': Math.round(result['rating_stats']['avg'] * 100) / 100,
'percent': [],
};
_.each(_.keys(result['rating_stats']['percent']).reverse(), function (rating) {
ratingData['percent'].push({
'num': rating,
'percent': utils.round_precision(result['rating_stats']['percent'][rating], 0.01),
});
self.set('rating_card_values', ratingData);
});
this.set('rating_card_values', ratingData);
},
/**
* @override
@@ -136,6 +161,14 @@ PortalChatter.include({
return params;
},
/**
* render rating card
*
* @private
*/
_renderRatingCard: function () {
this.$('.o_website_rating_card_container').replaceWith(qweb.render("portal_rating.rating_card", {widget: this}));
},
/**
* Default rating data for publisher comment qweb template
* @private
@@ -72,6 +72,16 @@ PortalComposer.include({
// Handlers
//--------------------------------------------------------------------------
/**
* @override
* @private
*/
_prepareMessageData: function () {
return Object.assign(this._super(...arguments) || {}, {
'message_id': this.options.default_message_id,
'rating_value': this.$input.val()
});
},
/**
* @private
*/
@@ -121,5 +131,23 @@ PortalComposer.include({
}
this.user_click = false;
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
* @private
*/
_onSubmitButtonClick: function (ev) {
return this._super(...arguments).then((result) => {
const $modal = this.$el.closest('#ratingpopupcomposer');
$modal.on('hidden.bs.modal', () => {
this.trigger_up('reload_rating_popup_composer', result);
});
$modal.modal('hide');
});
},
});
});
@@ -23,8 +23,8 @@ var RatingPopupComposer = publicWidget.Widget.extend({
init: function (parent, options) {
this._super.apply(this, arguments);
this.rating_avg = Math.round(options['ratingAvg'] * 100) / 100 || 0.0;
this.rating_total = options['ratingTotal'] || 0.0;
this.rating_avg = Math.round(options['rating_avg'] * 100) / 100 || 0.0;
this.rating_count = options['rating_count'] || 0.0;
this.options = _.defaults({}, options, {
'token': false,
@@ -54,18 +54,54 @@ var RatingPopupComposer = publicWidget.Widget.extend({
publicWidget.registry.RatingPopupComposer = publicWidget.Widget.extend({
selector: '.o_rating_popup_composer',
custom_events: {
reload_rating_popup_composer: '_onReloadRatingPopupComposer',
},
/**
* @override
*/
start: function () {
var ratingPopupData = this.$el.data();
var ratingPopup = new RatingPopupComposer(this, ratingPopupData);
this.ratingPopupData = this.$el.data();
this.ratingPopupData.display_composer = !this.ratingPopupData.disable_composer && !session.is_website_user;
this.ratingPopup = new RatingPopupComposer(this, this.ratingPopupData);
return Promise.all([
this._super.apply(this, arguments),
ratingPopup.appendTo(this.$el)
this.ratingPopup.appendTo(this.$el)
]);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Destroy existing ratingPopup and insert new ratingPopup widget
*
* @private
* @param {Object} data
*/
_reloadRatingPopupComposer: function (data) {
if (this.ratingPopup) {
this.ratingPopup.destroy();
}
if (this.ratingPopupData.display_composer) {
this.ratingPopup = new RatingPopupComposer(this, Object.assign(this.ratingPopupData, data));
this.ratingPopup.appendTo(this.$el);
}
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {OdooEvent} ev
*/
_onReloadRatingPopupComposer: function (ev) {
this._reloadRatingPopupComposer(ev.data);
}
});
return RatingPopupComposer;
@@ -5,14 +5,13 @@
-->
<t t-extend="portal.Composer">
<t t-jquery="textarea" t-operation="inner"><t t-esc="widget.options['default_message'] ? _.str.trim(widget.options['default_message']) : ''"/></t><!-- need to be one line to avoid \t in textarea -->
<t t-jquery="form" t-operation="attributes">
<attribute name="t-attf-action">#{widget.options['force_submit_url'] ? widget.options['force_submit_url'] : '/mail/chatter_post'}</attribute>
<t t-jquery="button.o_portal_chatter_composer_btn" t-operation="attributes">
<attribute name="t-attf-data-action">#{widget.options['force_submit_url'] || '/mail/chatter_post'}</attribute>
</t>
<t t-jquery=".o_portal_chatter_composer_form input[name='csrf_token']" t-operation="after">
<t t-jquery=".o_portal_chatter_composer_input div.o_portal_chatter_composer_body" t-operation="before">
<t t-call="portal_rating.rating_star_input">
<t t-set="default_rating" t-value="widget.options['default_rating_value']"/>
</t>
<input type="hidden" name="message_id" t-att-value="widget.options['default_message_id']" t-if="widget.options['default_message_id']"/>
</t>
</t>
@@ -34,8 +34,8 @@
-->
<template id="rating_stars_static_popup_composer" name="Rating: rating composer in popup">
<div class="d-print-none o_rating_popup_composer o_not_editable p-0"
t-att-data-rating-avg="rating_avg or 0.0"
t-att-data-rating-total="rating_total or 0.0"
t-att-data-rating_avg="rating_avg or 0.0"
t-att-data-rating_count="rating_count or 0.0"
t-att-data-token="token"
t-att-data-hash="hash"
t-att-data-pid="pid"
@@ -82,8 +82,8 @@ class TestWebsiteBlogFlow(TestWebsiteBlogCommon):
'blog.post',
self.test_blog_post.id,
'Test message blog post',
attachment_ids=str(attachment.id),
attachment_tokens=attachment.access_token
attachment_ids=[attachment.id],
attachment_tokens=[attachment.access_token]
)
self.assertTrue(self.env['mail.message'].sudo().search(
@@ -102,8 +102,8 @@ class TestWebsiteBlogFlow(TestWebsiteBlogCommon):
'blog.post',
self.test_blog_post.id,
'Test message blog post',
attachment_ids=str(second_attachment.id),
attachment_tokens='wrong_token'
attachment_ids=[second_attachment.id],
attachment_tokens=['wrong_token']
)
self.assertFalse(self.env['mail.message'].sudo().search(
+19 -9
View File
@@ -13,7 +13,7 @@ from odoo.tools import plaintext2html, html2plaintext
class SlidesPortalChatter(PortalChatter):
@http.route(['/mail/chatter_post'], type='http', methods=['POST'], auth='public', website=True)
@http.route(['/mail/chatter_post'], type='json', methods=['POST'], auth='public', website=True)
def portal_chatter_post(self, res_model, res_id, message, **kw):
result = super(SlidesPortalChatter, self).portal_chatter_post(res_model, res_id, message, **kw)
if res_model == 'slide.channel':
@@ -22,21 +22,25 @@ class SlidesPortalChatter(PortalChatter):
if rating_value and slide_channel and request.env.user.partner_id.id == int(kw.get('pid')):
# apply karma gain rule only once
request.env.user.add_karma(slide_channel.karma_gen_channel_rank)
result.update({
'default_rating_value': rating_value,
'rating_avg': slide_channel.rating_avg,
'rating_count': slide_channel.rating_count,
'force_submit_url': result.get('default_message_id') and '/slides/mail/update_comment',
})
return result
@http.route([
'/slides/mail/update_comment',
'/mail/chatter_update',
], type='http', auth="user")
def mail_update_message(self, res_model, res_id, message, message_id, redirect=None, attachment_ids='', attachment_tokens='', **post):
], type='json', auth="user")
def mail_update_message(self, res_model, res_id, message, message_id, attachment_ids=None, attachment_tokens=None, **post):
# keep this mechanism intern to slide currently (saas 12.5) as it is
# considered experimental
if res_model != 'slide.channel':
raise Forbidden()
res_id = int(res_id)
attachment_ids = [int(attachment_id) for attachment_id in attachment_ids.split(',') if attachment_id]
attachment_tokens = [attachment_token for attachment_token in attachment_tokens.split(',') if attachment_token]
self._portal_post_check_attachments(attachment_ids, attachment_tokens)
pid = int(post['pid']) if post.get('pid') else False
@@ -70,7 +74,13 @@ class SlidesPortalChatter(PortalChatter):
'rating': float(post['rating_value']),
'feedback': html2plaintext(message.body),
})
# redirect to specified or referrer or simply channel page as fallback
redirect_url = redirect or (request.httprequest.referrer and request.httprequest.referrer + '#review') or '/slides/%s' % res_id
return werkzeug.utils.redirect(redirect_url, 302)
channel = request.env[res_model].browse(res_id)
return {
'default_message_id': message.id,
'default_message': html2plaintext(message.body),
'default_rating_value': message.rating_value,
'rating_avg': channel.rating_avg,
'rating_count': channel.rating_count,
'default_attachment_ids': message.attachment_ids.sudo().read(['id', 'name', 'mimetype', 'file_size', 'access_token']),
'force_submit_url': '/slides/mail/update_comment',
}
@@ -0,0 +1,27 @@
odoo.define('website_slides.portal_chatter', function (require) {
'use strict';
const { _t } = require('web.core');
const PortalChatter = require('portal.chatter').PortalChatter;
/**
* PortalChatter
*
* Extends Frontend Chatter to handle rating count on review tab
*/
PortalChatter.include({
/**
* Update review count on review tab in courses
*
* @override
* @private
*/
_reloadChatterContent: async function (data) {
await this._super(...arguments);
if (this.options.res_model === "slide.channel") {
$('#review-tab').text(_.str.sprintf(_t('Reviews (%d)'), data.rating_count));
}
},
});
});
@@ -119,12 +119,12 @@ tour.register('course_member', {
}, {
trigger: 'button[data-target="#ratingpopupcomposer"]'
}, {
trigger: 'form.o_portal_chatter_composer_form i.fa:eq(4)',
trigger: 'div.o_portal_chatter_composer_input i.fa:eq(2)',
extra_trigger: 'div.modal_shown',
run: 'click',
in_modal: false,
}, {
trigger: 'form.o_portal_chatter_composer_form textarea',
trigger: 'div.o_portal_chatter_composer_input textarea',
run: 'text This is a great course. Top !',
in_modal: false,
}, {
+1
View File
@@ -36,6 +36,7 @@
<script type="text/javascript" src="/website_slides/static/src/js/slides_course_tag_add.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_course_unsubscribe.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/tours/slides_tour.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/portal_chatter.js"/>
</xpath>
</template>
@@ -143,7 +143,7 @@
<div class="d-flex flex-column justify-content-center h5 flex-grow-1 mb-md-5" t-if="channel.allow_comment">
<t t-call="portal_rating.rating_stars_static_popup_composer">
<t t-set="rating_avg" t-value="rating_avg"/>
<t t-set="rating_total" t-value="rating_count"/>
<t t-set="rating_count" t-value="rating_count"/>
<t t-set="object" t-value="channel"/>
<t t-set="token" t-value="channel.access_token"/>
<t t-set="hash" t-value="message_post_hash"/>