[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:
committed by
Siddarth Gajjar
co-authored by
ktr-odoo
Siddarth Gajjar
parent
0f3467587e
commit
75b4e2dd38
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
}, {
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user