Skip to content

Commit 2a3d33a

Browse files
[FIX] mail: send is_main with the attachments
task-2783076 closes odoo#85765 Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
1 parent db6994c commit 2a3d33a

File tree

8 files changed

+119
-34
lines changed

8 files changed

+119
-34
lines changed

addons/mail/controllers/discuss.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,14 @@ def channel_ping(self, channel_id, rtc_session_id=None, check_rtc_session_ids=No
405405
# Chatter API
406406
# --------------------------------------------------------------------------
407407

408+
@http.route('/mail/thread/data', methods=['POST'], type='json', auth='user')
409+
def mail_thread_data(self, thread_model, thread_id, request_list, **kwargs):
410+
res = {}
411+
thread = request.env[thread_model].with_context(active_test=False).search([('id', '=', thread_id)])
412+
if 'attachments' in request_list:
413+
res['attachments'] = thread.env['ir.attachment'].search([('res_id', '=', thread.id), ('res_model', '=', thread._name)], order='id desc')._attachment_format(commands=True)
414+
return res
415+
408416
@http.route('/mail/thread/messages', methods=['POST'], type='json', auth='user')
409417
def mail_thread_messages(self, thread_model, thread_id, max_id=None, min_id=None, limit=30, **kwargs):
410418
return request.env['mail.message']._message_fetch(domain=[

addons/mail/models/ir_attachment.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ def _attachment_format(self, commands=False):
5858
'name': attachment.name,
5959
'mimetype': 'application/octet-stream' if safari and attachment.mimetype and 'video' in attachment.mimetype else attachment.mimetype,
6060
}
61+
if attachment.res_id and issubclass(self.pool[attachment.res_model], self.pool['mail.thread']):
62+
main_attachment = self.env[attachment.res_model].sudo().browse(attachment.res_id).message_main_attachment_id
63+
res['is_main'] = attachment == main_attachment
6164
if commands:
6265
res['originThread'] = [('insert', {
6366
'id': attachment.res_id,

addons/mail/models/mail_message.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -839,13 +839,6 @@ def _message_format(self, fnames, format_reply=True):
839839
else:
840840
author = (0, message_sudo.email_from)
841841

842-
# Attachments
843-
main_attachment = self.env['ir.attachment']
844-
if message_sudo.attachment_ids and message_sudo.res_id and issubclass(self.pool[message_sudo.model], self.pool['mail.thread']):
845-
main_attachment = self.env[message_sudo.model].sudo().browse(message_sudo.res_id).message_main_attachment_id
846-
attachments_formatted = message_sudo.attachment_ids._attachment_format()
847-
for attachment in attachments_formatted:
848-
attachment['is_main'] = attachment['id'] == main_attachment.id
849842
# Tracking values
850843
tracking_value_ids = []
851844
for tracking in message_sudo.tracking_value_ids:
@@ -890,7 +883,7 @@ def _message_format(self, fnames, format_reply=True):
890883
vals['parentMessage'] = message_sudo.parent_id.message_format(format_reply=False)[0]
891884
vals.update({
892885
'notifications': message_sudo.notification_ids._filtered_for_web_client()._notification_format(),
893-
'attachment_ids': attachments_formatted,
886+
'attachment_ids': message_sudo.attachment_ids._attachment_format(),
894887
'tracking_value_ids': tracking_value_ids,
895888
'messageReactionGroups': reaction_groups,
896889
'record_name': record_name,

addons/mail/static/src/components/attachment_box/tests/attachment_box_tests.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,9 @@ QUnit.test('base non-empty rendering', async function (assert) {
102102
}
103103
);
104104
await this.start({
105-
async mockRPC(route, args) {
106-
if (route.includes('ir.attachment/search_read')) {
107-
assert.step('ir.attachment/search_read');
105+
async mockRPC(route) {
106+
if (route.includes('/mail/thread/data')) {
107+
assert.step('/mail/thread/data');
108108
}
109109
return this._super(...arguments);
110110
},
@@ -115,7 +115,7 @@ QUnit.test('base non-empty rendering', async function (assert) {
115115
});
116116
await this.createAttachmentBoxComponent(thread);
117117
assert.verifySteps(
118-
['ir.attachment/search_read'],
118+
['/mail/thread/data'],
119119
"should have fetched attachments"
120120
);
121121
assert.strictEqual(

addons/mail/static/src/components/chatter_topbar/tests/chatter_topbar_tests.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ QUnit.test('attachment loading is delayed', async function (assert) {
148148
hasTimeControl: true,
149149
loadingBaseDelayDuration: 100,
150150
async mockRPC(route) {
151-
if (route.includes('ir.attachment/search_read')) {
151+
if (route.includes('/mail/thread/data')) {
152152
await makeTestPromise(); // simulate long loading
153153
}
154154
return this._super(...arguments);
@@ -191,7 +191,7 @@ QUnit.test('attachment counter while loading attachments', async function (asser
191191
this.data['res.partner'].records.push({ id: 100 });
192192
await this.start({
193193
async mockRPC(route) {
194-
if (route.includes('ir.attachment/search_read')) {
194+
if (route.includes('/mail/thread/data')) {
195195
await makeTestPromise(); // simulate long loading
196196
}
197197
return this._super(...arguments);
@@ -234,7 +234,7 @@ QUnit.test('attachment counter transition when attachments become loaded)', asyn
234234
await this.start({
235235
async mockRPC(route) {
236236
const _super = this._super.bind(this, ...arguments); // limitation of class.js
237-
if (route.includes('ir.attachment/search_read')) {
237+
if (route.includes('/mail/thread/data')) {
238238
await attachmentPromise;
239239
}
240240
return _super();

addons/mail/static/src/models/attachment/attachment.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ function factory(dependencies) {
3434
if ('id' in data) {
3535
data2.id = data.id;
3636
}
37+
if ('is_main' in data) {
38+
data2.is_main = data.is_main;
39+
}
3740
if ('mimetype' in data) {
3841
data2.mimetype = data.mimetype;
3942
}

addons/mail/static/src/models/thread/thread.js

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -707,25 +707,49 @@ function factory(dependencies) {
707707
)];
708708
}
709709

710+
/**
711+
* Fetch attachments linked to a record. Useful for populating the store
712+
* with these attachments, which are used by attachment box in the chatter.
713+
*/
714+
async fetchAttachments() {
715+
return this.fetchData(['attachments']);
716+
}
717+
710718
/**
711-
* Fetch attachments linked to a record. Useful for populating the store
712-
* with these attachments, which are used by attachment box in the chatter.
719+
* Requests the given `requestList` data from the server.
720+
*
721+
* @param {string[]} requestList
713722
*/
714-
async fetchAttachments() {
715-
const attachmentsData = await this.async(() => this.env.services.rpc({
716-
model: 'ir.attachment',
717-
method: 'search_read',
718-
domain: [
719-
['res_id', '=', this.id],
720-
['res_model', '=', this.model],
721-
],
722-
fields: ['id', 'name', 'mimetype'],
723-
orderBy: [{ name: 'id', asc: false }],
724-
}, { shadow: true }));
725-
this.update({
726-
originThreadAttachments: insertAndReplace(attachmentsData),
727-
});
728-
this.update({ areAttachmentsLoaded: true });
723+
async fetchData(requestList) {
724+
if (this.isTemporary) {
725+
return;
726+
}
727+
const requestSet = new Set(requestList);
728+
if (requestSet.has('attachments')) {
729+
this.update({ isLoadingAttachments: true });
730+
}
731+
const {
732+
attachments: attachmentsData,
733+
} = await this.env.services.rpc({
734+
route: '/mail/thread/data',
735+
params: {
736+
request_list: [...requestSet],
737+
thread_id: this.id,
738+
thread_model: this.model,
739+
},
740+
}, { shadow: true });
741+
if (!this.exists()) {
742+
return;
743+
}
744+
const values = {};
745+
if (attachmentsData) {
746+
Object.assign(values, {
747+
areAttachmentsLoaded: true,
748+
isLoadingAttachments: false,
749+
originThreadAttachments: insertAndReplace(attachmentsData),
750+
});
751+
}
752+
this.update(values);
729753
}
730754

731755
/**
@@ -1074,9 +1098,7 @@ function factory(dependencies) {
10741098
return;
10751099
}
10761100
this.loadNewMessages();
1077-
this.update({ isLoadingAttachments: true });
10781101
await this.async(() => this.fetchAttachments());
1079-
this.update({ isLoadingAttachments: false });
10801102
}
10811103

10821104
async refreshActivities() {

addons/mail/static/tests/helpers/mock_server.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ MockServer.include({
127127
const follower_id = args.follower_id;
128128
return this._mockRouteMailReadSubscriptionData(follower_id);
129129
}
130+
if (route === '/mail/thread/data') {
131+
return this._mockRouteMailThreadData(args.thread_model, args.thread_id, args.request_list);
132+
}
130133
if (route === '/mail/thread/messages') {
131134
const { min_id, max_id, limit, thread_model, thread_id } = args;
132135
return this._mockRouteMailThreadFetchMessages(thread_model, thread_id, max_id, min_id, limit);
@@ -502,6 +505,26 @@ MockServer.include({
502505
return subtypes_list;
503506
},
504507

508+
/**
509+
* Simulates the `/mail/thread/data` route.
510+
*
511+
* @param {string} thread_model
512+
* @param {integer} thread_id
513+
* @param {string[]} request_list
514+
* @returns {Object}
515+
*/
516+
async _mockRouteMailThreadData(thread_model, thread_id, request_list) {
517+
const res = {};
518+
const thread = this._mockSearchRead(thread_model, [[['id', '=', thread_id]]], {})[0];
519+
if (request_list.includes('attachments')) {
520+
const attachments = this._mockSearchRead('ir.attachment', [
521+
[['res_id', '=', thread.id], ['res_model', '=', thread_model]],
522+
], {}); // order not done for simplicity
523+
res['attachments'] = this._mockIrAttachment_attachmentFormat(attachments.map(attachment => attachment.id), true);
524+
}
525+
return res;
526+
},
527+
505528
/**
506529
* Simulates the `/mail/thread/messages` route.
507530
*
@@ -526,6 +549,39 @@ MockServer.include({
526549
// Private Mocked Methods
527550
//--------------------------------------------------------------------------
528551

552+
/**
553+
* Simulates `_attachment_format` on `ir.attachment`.
554+
*
555+
* @private
556+
* @param {string} res_model
557+
* @param {string} domain
558+
* @returns {Object}
559+
*/
560+
_mockIrAttachment_attachmentFormat(ids, commands = false) {
561+
const attachments = this._mockRead('ir.attachment', [ids]);
562+
return attachments.map(attachment => {
563+
const res = {
564+
'checksum': attachment.checksum,
565+
'filename': attachment.name,
566+
'id': attachment.id,
567+
'mimetype': attachment.mimetype,
568+
'name': attachment.name,
569+
};
570+
if (commands) {
571+
res['originThread'] = [['insert', {
572+
'id': attachment.res_id,
573+
'model': attachment.res_model,
574+
}]];
575+
} else {
576+
Object.assign(res, {
577+
'res_id': attachment.res_id,
578+
'res_model': attachment.res_model,
579+
});
580+
}
581+
return res;
582+
});
583+
},
584+
529585
/**
530586
* Simulates `get_activity_data` on `mail.activity`.
531587
*

0 commit comments

Comments
 (0)