diff --git a/addons/account/tests/test_portal_attachment.py b/addons/account/tests/test_portal_attachment.py index 2340fc75f2a..374ccdfe2da 100644 --- a/addons/account/tests/test_portal_attachment.py +++ b/addons/account/tests/test_portal_attachment.py @@ -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) diff --git a/addons/portal/controllers/mail.py b/addons/portal/controllers/mail.py index af000897113..619f75723cf 100644 --- a/addons/portal/controllers/mail.py +++ b/addons/portal/controllers/mail.py @@ -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): diff --git a/addons/portal/static/src/js/portal_chatter.js b/addons/portal/static/src/js/portal_chatter.js index 134efbf6097..0cda6860598 100644 --- a/addons/portal/static/src/js/portal_chatter.js +++ b/addons/portal/static/src/js/portal_chatter.js @@ -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} diff --git a/addons/portal/static/src/js/portal_composer.js b/addons/portal/static/src/js/portal_composer.js index 68fb9189207..8d8cabecb28 100644 --- a/addons/portal/static/src/js/portal_composer.js +++ b/addons/portal/static/src/js/portal_composer.js @@ -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 { diff --git a/addons/portal/static/src/xml/portal_chatter.xml b/addons/portal/static/src/xml/portal_chatter.xml index ae3a2374913..cad73f0bcc0 100644 --- a/addons/portal/static/src/xml/portal_chatter.xml +++ b/addons/portal/static/src/xml/portal_chatter.xml @@ -28,39 +28,28 @@

You must be logged in to post a comment.

+
Avatar
-
- -
+
+
- - - - - - - - - - -
- +
- -
+
+
- +
diff --git a/addons/portal_rating/controllers/portal_chatter.py b/addons/portal_rating/controllers/portal_chatter.py index c57a6d347fc..746bc068aa4 100644 --- a/addons/portal_rating/controllers/portal_chatter.py +++ b/addons/portal_rating/controllers/portal_chatter.py @@ -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 diff --git a/addons/portal_rating/static/src/js/portal_chatter.js b/addons/portal_rating/static/src/js/portal_chatter.js index fdf0bc8db3b..adefa727e97 100644 --- a/addons/portal_rating/static/src/js/portal_chatter.js +++ b/addons/portal_rating/static/src/js/portal_chatter.js @@ -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 diff --git a/addons/portal_rating/static/src/js/portal_composer.js b/addons/portal_rating/static/src/js/portal_composer.js index 3524a0a83cd..3d1bb0ea7fc 100644 --- a/addons/portal_rating/static/src/js/portal_composer.js +++ b/addons/portal_rating/static/src/js/portal_composer.js @@ -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'); + }); + }, }); }); diff --git a/addons/portal_rating/static/src/js/portal_rating_composer.js b/addons/portal_rating/static/src/js/portal_rating_composer.js index 517b18aad6f..71c53f98c39 100644 --- a/addons/portal_rating/static/src/js/portal_rating_composer.js +++ b/addons/portal_rating/static/src/js/portal_rating_composer.js @@ -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; diff --git a/addons/portal_rating/static/src/xml/portal_chatter.xml b/addons/portal_rating/static/src/xml/portal_chatter.xml index adb0174b2d8..4be6daa5920 100644 --- a/addons/portal_rating/static/src/xml/portal_chatter.xml +++ b/addons/portal_rating/static/src/xml/portal_chatter.xml @@ -5,14 +5,13 @@ --> - - #{widget.options['force_submit_url'] ? widget.options['force_submit_url'] : '/mail/chatter_post'} + + #{widget.options['force_submit_url'] || '/mail/chatter_post'} - + - diff --git a/addons/portal_rating/views/rating_templates.xml b/addons/portal_rating/views/rating_templates.xml index 7125f283f5d..e7701970630 100644 --- a/addons/portal_rating/views/rating_templates.xml +++ b/addons/portal_rating/views/rating_templates.xml @@ -34,8 +34,8 @@ -->