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.