[IMP] mail: send cache control header on avatars

Before this PR cache control header for avatar was set to no-cache which is not
the right value.
This PR mark the avatar routes response as immutable when a unique string is
present. We also use the partner write_date as unique string when possible.

task-3551636

closes odoo/odoo#141326

X-original-commit: 3de4f1f1225229cbdb0512df5790ce491f04ea7e
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
Didier (did)
2023-11-07 21:02:27 +00:00
parent 7b1dd79f46
commit 4064508af5
11 changed files with 98 additions and 19 deletions
@@ -39,9 +39,9 @@ class LivechatBinaryController(BinaryController):
auth="public",
cors="*",
)
def livechat_channel_partner_avatar_128(self, guest_token, channel_id, partner_id, **kwargs):
def livechat_channel_partner_avatar_128(self, guest_token, channel_id, partner_id, unique=False):
force_guest_env(guest_token)
return self.discuss_channel_partner_avatar_128(channel_id, partner_id, **kwargs)
return self.discuss_channel_partner_avatar_128(channel_id, partner_id, unique=unique)
@route(
"/im_livechat/cors/channel/<int:channel_id>/guest/<int:guest_id>/avatar_128",
@@ -50,6 +50,6 @@ class LivechatBinaryController(BinaryController):
auth="public",
cors="*",
)
def livechat_channel_guest_avatar_128(self, guest_token, channel_id, guest_id, **kwargs):
def livechat_channel_guest_avatar_128(self, guest_token, channel_id, guest_id, unique=False):
force_guest_env(guest_token)
return self.discuss_channel_guest_avatar_128(channel_id, guest_id, **kwargs)
return self.discuss_channel_guest_avatar_128(channel_id, guest_id, unique=unique)
@@ -48,6 +48,7 @@ class TestGetDiscussChannel(TestImLivechatCommon):
'name': 'Visitor',
'im_status': 'offline',
'type': "guest",
'write_date': fields.Datetime.to_string(self.env['discuss.channel'].browse(channel_info['id']).channel_member_ids.filtered(lambda m: m.guest_id)[0].guest_id.write_date),
}, {
'active': True,
'country': False,
+8 -8
View File
@@ -16,29 +16,29 @@ class BinaryController(Binary):
auth="public",
)
@add_guest_to_context
def discuss_channel_partner_avatar_128(self, channel_id, partner_id, **kwargs):
def discuss_channel_partner_avatar_128(self, channel_id, partner_id, unique=False):
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
partner = request.env["res.partner"].browse(partner_id).exists()
domain = [("channel_id", "=", channel_id), ("partner_id", "=", partner_id)]
if channel and partner and request.env["discuss.channel.member"].search(domain):
# sudo: res.partner - the partner is in the same channel as the current user, so they can see their avatar
return (
request.env["ir.binary"]._get_image_stream_from(partner.sudo(), field_name="avatar_128").get_response()
request.env["ir.binary"]._get_image_stream_from(partner.sudo(), field_name="avatar_128").get_response(immutable=True if unique else False)
)
return self.content_image(model="res.partner", id=partner_id, field="avatar_128")
return self.content_image(model="res.partner", id=partner_id, field="avatar_128", unique=unique)
@http.route(
"/discuss/channel/<int:channel_id>/guest/<int:guest_id>/avatar_128", methods=["GET"], type="http", auth="public"
)
@add_guest_to_context
def discuss_channel_guest_avatar_128(self, channel_id, guest_id, **kwargs):
def discuss_channel_guest_avatar_128(self, channel_id, guest_id, unique=False):
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
guest = request.env["mail.guest"].browse(guest_id).exists()
domain = [("channel_id", "=", channel_id), ("guest_id", "=", guest_id)]
if channel and guest and request.env["discuss.channel.member"].search(domain):
# sudo: mail.guest - the guest is in the same channel as the current user, so they can see their avatar
return request.env["ir.binary"]._get_image_stream_from(guest.sudo(), field_name="avatar_128").get_response()
return self.content_image(model="mail.guest", id=guest_id, field="avatar_128")
return request.env["ir.binary"]._get_image_stream_from(guest.sudo(), field_name="avatar_128").get_response(immutable=True if unique else False)
return self.content_image(model="mail.guest", id=guest_id, field="avatar_128", unique=unique)
@http.route(
"/discuss/channel/<int:channel_id>/attachment/<int:attachment_id>", methods=["GET"], type="http", auth="public"
@@ -61,8 +61,8 @@ class BinaryController(Binary):
@http.route("/discuss/channel/<int:channel_id>/avatar_128", methods=["GET"], type="http", auth="public")
@add_guest_to_context
def discuss_channel_avatar_128(self, channel_id, **kwargs):
return self.content_image(model="discuss.channel", id=channel_id, field="avatar_128")
def discuss_channel_avatar_128(self, channel_id, unique=False):
return self.content_image(model="discuss.channel", id=channel_id, field="avatar_128", unique=unique)
@http.route(
[
+4 -1
View File
@@ -6,6 +6,7 @@ from datetime import datetime, timedelta
from functools import wraps
from inspect import Parameter, signature
import odoo
from odoo.tools import consteq, get_lang
from odoo import _, api, fields, models
from odoo.http import request
@@ -163,7 +164,7 @@ class MailGuest(models.Model):
def _guest_format(self, fields=None):
if not fields:
fields = {'id': True, 'name': True, 'im_status': True}
fields = {'id': True, 'name': True, 'im_status': True, "write_date": True}
guests_formatted_data = {}
for guest in self:
data = {}
@@ -173,6 +174,8 @@ class MailGuest(models.Model):
data['name'] = guest.name
if 'im_status' in fields:
data['im_status'] = guest.im_status
if "write_date" in fields:
data["write_date"] = odoo.fields.Datetime.to_string(guest.write_date)
data['type'] = "guest"
guests_formatted_data[guest] = data
return guests_formatted_data
+4 -1
View File
@@ -3,6 +3,7 @@
import re
import odoo
from odoo import _, api, fields, models, tools
from odoo.osv import expression
@@ -213,7 +214,7 @@ class Partner(models.Model):
def mail_partner_format(self, fields=None):
partners_format = dict()
if not fields:
fields = {'id': True, 'name': True, 'email': True, 'active': True, 'im_status': True, 'is_company': True, 'user': {}}
fields = {'id': True, 'name': True, 'email': True, 'active': True, 'im_status': True, 'is_company': True, 'user': {}, "write_date": True}
for partner in self:
data = {}
if 'id' in fields:
@@ -228,6 +229,8 @@ class Partner(models.Model):
data['im_status'] = partner.im_status
if 'is_company' in fields:
data['is_company'] = partner.is_company
if "write_date" in fields:
data["write_date"] = odoo.fields.Datetime.to_string(partner.write_date)
if 'user' in fields:
internal_users = partner.user_ids - partner.user_ids.filtered('share')
main_user = internal_users[0] if len(internal_users) > 0 else partner.user_ids[0] if len(partner.user_ids) > 0 else self.env['res.users']
@@ -61,6 +61,8 @@ export class Persona extends Record {
/** @type {ImStatus} */
im_status;
isAdmin = false;
/** @type {string} */
write_date;
/**
* @returns {boolean}
@@ -854,15 +854,19 @@ export class ThreadService {
if (!persona) {
return DEFAULT_AVATAR;
}
const urlParams = {};
if (persona.write_date) {
urlParams.unique = persona.write_date;
}
if (persona.is_company === undefined && this.store.self?.user?.isInternalUser) {
this.personaService.fetchIsCompany(persona);
}
if (thread?.model === "discuss.channel") {
if (persona.type === "partner") {
return url(`/discuss/channel/${thread.id}/partner/${persona.id}/avatar_128`);
return url(`/discuss/channel/${thread.id}/partner/${persona.id}/avatar_128`, urlParams);
}
if (persona.type === "guest") {
return url(`/discuss/channel/${thread.id}/guest/${persona.id}/avatar_128`);
return url(`/discuss/channel/${thread.id}/guest/${persona.id}/avatar_128`, urlParams);
}
}
if (persona.type === "partner" && persona?.id) {
@@ -870,6 +874,7 @@ export class ThreadService {
field: "avatar_128",
id: persona.id,
model: "res.partner",
...urlParams,
});
return avatar;
}
@@ -878,6 +883,7 @@ export class ThreadService {
field: "avatar_128",
id: persona.user.id,
model: "res.users",
...urlParams,
});
return avatar;
}
@@ -66,7 +66,10 @@ patch(Thread.prototype, {
);
}
if (this.type === "chat") {
return `/web/image/res.partner/${this.chatPartner.id}/avatar_128`;
return url(
`/web/image/res.partner/${this.chatPartner.id}/avatar_128`,
assignDefined({}, { unique: this.chatPartner.write_date })
);
}
return super.imgUrl;
},
@@ -1000,7 +1000,7 @@ QUnit.test("chat - avatar: should have correct avatar", async () => {
openDiscuss();
await contains(".o-mail-DiscussSidebarChannel img");
await contains(`img[data-src='/web/image/res.partner/${partnerId}/avatar_128']`);
await contains(`img[data-src$='/web/image/res.partner/${partnerId}/avatar_128']`);
});
QUnit.test("chat should be sorted by last activity time [REQUIRE FOCUS]", async () => {
@@ -5,6 +5,8 @@ import json
import odoo
from odoo.tools import mute_logger, date_utils
from odoo.tests import HttpCase
from odoo.http import STATIC_CACHE_LONG
from odoo import Command
@odoo.tests.tagged("-at_install", "post_install")
@@ -342,3 +344,44 @@ class TestMessageController(HttpCase):
self.env["res.partner"].search_count([('email', '=', "john2@test.be")]),
"'mail/message/post' does not create another user if there's already a user with matching email",
)
def test_mail_cache_control_header(self):
testuser = self.env['res.users'].create({
'email': 'testuser@testuser.com',
'groups_id': [Command.set([self.ref('base.group_portal')])],
'name': 'Test User',
'login': 'testuser',
'password': 'testuser',
})
test_user = self.authenticate("testuser", "testuser")
partner = self.env["res.users"].browse(test_user.uid).partner_id
self.channel.add_members(testuser.partner_id.ids)
res = self.url_open(
url=f"/discuss/channel/{self.channel.id}/avatar_128?unique={self.channel._get_avatar_cache_key()}"
)
self.assertEqual(res.headers["Cache-Control"], f"public, max-age={STATIC_CACHE_LONG}")
res = self.url_open(
url=f"/discuss/channel/{self.channel.id}/avatar_128"
)
self.assertEqual(res.headers["Cache-Control"], "no-cache")
res = self.url_open(
url=f"/discuss/channel/{self.channel.id}/partner/{partner.id}/avatar_128?unique={partner.write_date.isoformat()}"
)
self.assertEqual(res.headers["Cache-Control"], f"public, max-age={STATIC_CACHE_LONG}")
res = self.url_open(
url=f"/discuss/channel/{self.channel.id}/partner/{partner.id}/avatar_128"
)
self.assertEqual(res.headers["Cache-Control"], "no-cache")
res = self.url_open(
url=f"/discuss/channel/{self.channel.id}/guest/{self.guest.id}/avatar_128?unique={self.guest.write_date.isoformat()}"
)
self.assertEqual(res.headers["Cache-Control"], f"public, max-age={STATIC_CACHE_LONG}")
res = self.url_open(
url=f"/discuss/channel/{self.channel.id}/guest/{self.guest.id}/avatar_128"
)
self.assertEqual(res.headers["Cache-Control"], "no-cache")
@@ -5,7 +5,7 @@ from datetime import date
from dateutil.relativedelta import relativedelta
from unittest.mock import patch, PropertyMock
from odoo import Command
from odoo import Command, fields
from odoo.tests.common import users, tagged, HttpCase, warmup
from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
@@ -157,6 +157,7 @@ class TestDiscussFullPerformance(HttpCase):
'id': self.users[0].id,
'isInternalUser': True,
},
'write_date': fields.Datetime.to_string(self.users[0].partner_id.write_date),
},
}], key=lambda member_data: member_data['id']))],
'custom_channel_name': False,
@@ -208,6 +209,7 @@ class TestDiscussFullPerformance(HttpCase):
'id': self.users[0].id,
'isInternalUser': True,
},
'write_date': fields.Datetime.to_string(self.users[0].partner_id.write_date),
},
}], key=lambda member_data: member_data['id']))],
'custom_channel_name': False,
@@ -259,6 +261,7 @@ class TestDiscussFullPerformance(HttpCase):
'id': self.users[0].id,
'isInternalUser': True,
},
'write_date': fields.Datetime.to_string(self.users[0].partner_id.write_date),
},
}], key=lambda member_data: member_data['id']))],
'custom_channel_name': False,
@@ -310,6 +313,7 @@ class TestDiscussFullPerformance(HttpCase):
'id': self.users[0].id,
'isInternalUser': True,
},
'write_date': fields.Datetime.to_string(self.users[0].partner_id.write_date),
},
}], key=lambda member_data: member_data['id']))],
'custom_channel_name': False,
@@ -361,6 +365,7 @@ class TestDiscussFullPerformance(HttpCase):
'id': self.users[0].id,
'isInternalUser': True,
},
'write_date': fields.Datetime.to_string(self.users[0].partner_id.write_date),
},
}], key=lambda member_data: member_data['id']))],
'custom_channel_name': False,
@@ -413,6 +418,7 @@ class TestDiscussFullPerformance(HttpCase):
'id': self.users[0].id,
'isInternalUser': True,
},
'write_date': fields.Datetime.to_string(self.users[0].partner_id.write_date),
},
},
{
@@ -434,6 +440,7 @@ class TestDiscussFullPerformance(HttpCase):
'id': self.users[12].id,
'isInternalUser': True,
},
'write_date': fields.Datetime.to_string(self.users[0].partner_id.write_date),
},
},
], key=lambda member_data: member_data['id']))],
@@ -501,6 +508,7 @@ class TestDiscussFullPerformance(HttpCase):
'id': self.users[0].id,
'isInternalUser': True,
},
'write_date': fields.Datetime.to_string(self.users[0].partner_id.write_date),
},
},
{
@@ -522,6 +530,7 @@ class TestDiscussFullPerformance(HttpCase):
'id': self.users[14].id,
'isInternalUser': True,
},
'write_date': fields.Datetime.to_string(self.users[0].partner_id.write_date),
},
},
], key=lambda member_data: member_data['id']))],
@@ -589,6 +598,7 @@ class TestDiscussFullPerformance(HttpCase):
'id': self.users[0].id,
'isInternalUser': True,
},
'write_date': fields.Datetime.to_string(self.users[0].partner_id.write_date),
},
},
{
@@ -610,6 +620,7 @@ class TestDiscussFullPerformance(HttpCase):
'id': self.users[15].id,
'isInternalUser': True,
},
'write_date': fields.Datetime.to_string(self.users[0].partner_id.write_date),
},
},
], key=lambda member_data: member_data['id']))],
@@ -677,6 +688,7 @@ class TestDiscussFullPerformance(HttpCase):
'id': self.users[0].id,
'isInternalUser': True,
},
'write_date': fields.Datetime.to_string(self.users[0].partner_id.write_date),
},
},
{
@@ -698,6 +710,7 @@ class TestDiscussFullPerformance(HttpCase):
'id': self.users[2].id,
'isInternalUser': True,
},
'write_date': fields.Datetime.to_string(self.users[0].partner_id.write_date),
},
},
], key=lambda member_data: member_data['id']))],
@@ -765,6 +778,7 @@ class TestDiscussFullPerformance(HttpCase):
'id': self.users[0].id,
'isInternalUser': True,
},
'write_date': fields.Datetime.to_string(self.users[0].partner_id.write_date),
},
},
{
@@ -786,6 +800,7 @@ class TestDiscussFullPerformance(HttpCase):
'id': self.users[3].id,
'isInternalUser': True,
},
'write_date': fields.Datetime.to_string(self.users[3].partner_id.write_date),
},
},
], key=lambda member_data: member_data['id']))],
@@ -952,6 +967,7 @@ class TestDiscussFullPerformance(HttpCase):
'im_status': self.channel_livechat_2.channel_member_ids.filtered(lambda m: m.guest_id).guest_id.im_status,
'name': self.channel_livechat_2.channel_member_ids.filtered(lambda m: m.guest_id).guest_id.name,
'type': "guest",
'write_date': fields.Datetime.to_string(self.channel_livechat_2.channel_member_ids.filtered(lambda m: m.guest_id).guest_id.write_date),
},
},
])],
@@ -1014,6 +1030,7 @@ class TestDiscussFullPerformance(HttpCase):
'out_of_office_date_end': False,
'type': "partner",
'user': False,
'write_date': fields.Datetime.to_string(self.user_root.partner_id.write_date),
},
'currentGuest': False,
'current_partner': {
@@ -1029,6 +1046,7 @@ class TestDiscussFullPerformance(HttpCase):
'id': self.users[0].id,
'isInternalUser': True,
},
'write_date': fields.Datetime.to_string(self.users[0].partner_id.write_date),
},
'current_user_id': self.users[0].id,
'current_user_settings': {