[IMP] mail: add default avatar for channels

- Channels
    * "hash tag" avatar with different backgroud colors
    * only private channels have lock thread icon
- DM groups
    * "users" avatar with different background colors
    * no thread icon

task-2584994

closes odoo/odoo#75653

Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
Qiuyu (QHO)
2021-08-27 20:53:44 +00:00
committed by Sébastien Theys
parent 22d905bbb5
commit e8922db3da
13 changed files with 213 additions and 38 deletions
+45 -13
View File
@@ -3,15 +3,26 @@
import base64
import logging
from hashlib import sha512
from uuid import uuid4
from odoo import _, api, fields, models, modules, tools, Command
from odoo import _, api, fields, models, tools, Command
from odoo.addons.base.models.avatar_mixin import get_hsl_from_seed
from odoo.exceptions import UserError, ValidationError
from odoo.osv import expression
from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
_logger = logging.getLogger(__name__)
channel_avatar = '''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 530.06 530.06">
<circle cx="265.03" cy="265.03" r="265.03" fill="#875a7b"/>
<path d="M416.74,217.29l5-28a8.4,8.4,0,0,0-8.27-9.88H361.09l10.24-57.34a8.4,8.4,0,0,0-8.27-9.88H334.61a8.4,8.4,0,0,0-8.27,6.93L315.57,179.4H246.5l10.24-57.34a8.4,8.4,0,0,0-8.27-9.88H220a8.4,8.4,0,0,0-8.27,6.93L201,179.4H145.6a8.42,8.42,0,0,0-8.28,6.93l-5,28a8.4,8.4,0,0,0,8.27,9.88H193l-16,89.62H121.59a8.4,8.4,0,0,0-8.27,6.93l-5,28a8.4,8.4,0,0,0,8.27,9.88H169L158.73,416a8.4,8.4,0,0,0,8.27,9.88h28.45a8.42,8.42,0,0,0,8.28-6.93l10.76-60.29h69.07L273.32,416a8.4,8.4,0,0,0,8.27,9.88H310a8.4,8.4,0,0,0,8.27-6.93l10.77-60.29h55.38a8.41,8.41,0,0,0,8.28-6.93l5-28a8.4,8.4,0,0,0-8.27-9.88H337.08l16-89.62h55.38A8.4,8.4,0,0,0,416.74,217.29ZM291.56,313.84H222.5l16-89.62h69.07Z" fill="#ffffff"/>
</svg>'''
group_avatar = '''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 530.06 530.06">
<circle cx="265.03" cy="265.03" r="265.03" fill="#875a7b"/>
<path d="m184.356059,265.030004c-23.740561,0.73266 -43.157922,10.11172 -58.252302,28.136961l-29.455881,0c-12.0169,0 -22.128621,-2.96757 -30.335161,-8.90271s-12.309921,-14.618031 -12.309921,-26.048671c0,-51.730902 9.08582,-77.596463 27.257681,-77.596463c0.87928,0 4.06667,1.53874 9.56217,4.61622s12.639651,6.19167 21.432451,9.34235s17.512401,4.72613 26.158581,4.72613c9.8187,0 19.563981,-1.68536 29.236061,-5.05586c-0.73266,5.4223 -1.0991,10.25834 -1.0991,14.508121c0,20.370061 5.93514,39.127962 17.805421,56.273922zm235.42723,140.025346c0,17.585601 -5.34888,31.470971 -16.046861,41.655892s-24.912861,15.277491 -42.645082,15.277491l-192.122688,0c-17.732221,0 -31.947101,-5.09257 -42.645082,-15.277491s-16.046861,-24.070291 -16.046861,-41.655892c0,-7.7669 0.25653,-15.350691 0.76937,-22.751371s1.53874,-15.387401 3.07748,-23.960381s3.48041,-16.523211 5.82523,-23.850471s5.4955,-14.471411 9.45226,-21.432451s8.49978,-12.89618 13.628841,-17.805421c5.12906,-4.90924 11.393931,-8.82951 18.794611,-11.76037s15.570511,-4.3964 24.509931,-4.3964c1.46554,0 4.61622,1.57545 9.45226,4.72613s10.18492,6.6678 16.046861,10.55136c5.86194,3.88356 13.702041,7.40068 23.520741,10.55136s19.710601,4.72613 29.675701,4.72613s19.857001,-1.57545 29.675701,-4.72613s17.658801,-6.6678 23.520741,-10.55136c5.86194,-3.88356 11.21082,-7.40068 16.046861,-10.55136s7.98672,-4.72613 9.45226,-4.72613c8.93942,0 17.109251,1.46554 24.509931,4.3964s13.665551,6.85113 18.794611,11.76037c5.12906,4.90924 9.67208,10.844381 13.628841,17.805421s7.10744,14.105191 9.45226,21.432451s4.28649,15.277491 5.82523,23.850471s2.56464,16.559701 3.07748,23.960381s0.76937,14.984471 0.76937,22.751371zm-225.095689,-280.710152c0,15.534021 -5.4955,28.796421 -16.486501,39.787422s-24.253401,16.486501 -39.787422,16.486501s-28.796421,-5.4955 -39.787422,-16.486501s-16.486501,-24.253401 -16.486501,-39.787422s5.4955,-28.796421 16.486501,-39.787422s24.253401,-16.486501 39.787422,-16.486501s28.796421,5.4955 39.787422,16.486501s16.486501,24.253401 16.486501,39.787422zm154.753287,84.410884c0,23.300921 -8.24325,43.194632 -24.729751,59.681133s-36.380212,24.729751 -59.681133,24.729751s-43.194632,-8.24325 -59.681133,-24.729751s-24.729751,-36.380212 -24.729751,-59.681133s8.24325,-43.194632 24.729751,-59.681133s36.380212,-24.729751 59.681133,-24.729751s43.194632,8.24325 59.681133,24.729751s24.729751,36.380212 24.729751,59.681133zm126.616325,49.459502c0,11.43064 -4.10338,20.113531 -12.309921,26.048671s-18.318261,8.90271 -30.335161,8.90271l-29.455881,0c-15.094381,-18.025241 -34.511741,-27.404301 -58.252302,-28.136961c11.87028,-17.145961 17.805421,-35.903862 17.805421,-56.273922c0,-4.24978 -0.36644,-9.08582 -1.0991,-14.508121c9.67208,3.3705 19.417361,5.05586 29.236061,5.05586c8.64618,0 17.365781,-1.57545 26.158581,-4.72613s15.936951,-6.26487 21.432451,-9.34235s8.68289,-4.61622 9.56217,-4.61622c18.171861,0 27.257681,25.865561 27.257681,77.596463zm-28.136961,-133.870386c0,15.534021 -5.4955,28.796421 -16.486501,39.787422s-24.253401,16.486501 -39.787422,16.486501s-28.796421,-5.4955 -39.787422,-16.486501s-16.486501,-24.253401 -16.486501,-39.787422s5.4955,-28.796421 16.486501,-39.787422s24.253401,-16.486501 39.787422,-16.486501s28.796421,5.4955 39.787422,16.486501s16.486501,24.253401 16.486501,39.787422z" fill="#ffffff"/>
</svg>'''
class Channel(models.Model):
""" A mail.channel is a discussion group that may behave like a listener
@@ -31,10 +42,6 @@ class Channel(models.Model):
res['alias_contact'] = 'everyone' if res.get('public', 'private') == 'public' else 'followers'
return res
def _get_default_image(self):
image_path = modules.get_module_resource('mail', 'static/src/img', 'groupdefault.png')
return base64.b64encode(open(image_path, 'rb').read())
# description
name = fields.Char('Name', required=True, translate=True)
active = fields.Boolean(default=True, help="Set active to false to hide the channel without removing it.")
@@ -45,7 +52,8 @@ class Channel(models.Model):
string='Channel Type', default='channel', help="Chat is private and unique between 2 persons. Group is private among invited persons. Channel can be freely joined (depending on its configuration).")
is_chat = fields.Boolean(string='Is a chat', compute='_compute_is_chat')
description = fields.Text('Description')
image_128 = fields.Image("Image", max_width=128, max_height=128, default=_get_default_image)
image_128 = fields.Image("Image", max_width=128, max_height=128)
avatar_128 = fields.Image("Avatar", max_width=128, max_height=128, compute='_compute_avatar_128')
channel_partner_ids = fields.Many2many(
'res.partner', string='Members',
compute='_compute_channel_partner_ids', inverse='_inverse_channel_partner_ids',
@@ -77,6 +85,19 @@ class Channel(models.Model):
for record in self:
record.is_chat = record.channel_type == 'chat'
@api.depends('channel_type', 'image_128', 'uuid')
def _compute_avatar_128(self):
for record in self:
record.avatar_128 = record.image_128 or record._generate_avatar()
def _generate_avatar(self):
if self.channel_type not in ('channel', 'group'):
return False
avatar = group_avatar if self.channel_type == 'group' else channel_avatar
bgcolor = get_hsl_from_seed(self.uuid)
avatar = avatar.replace('fill="#875a7b"', f'fill="{bgcolor}"')
return base64.b64encode(avatar.encode())
@api.depends('channel_last_seen_partner_ids.partner_id')
def _compute_channel_partner_ids(self):
for channel in self:
@@ -128,7 +149,7 @@ class Channel(models.Model):
@api.model_create_multi
def create(self, vals_list):
defaults = self.default_get(['image_128', 'public'])
defaults = self.default_get(['public'])
access_types = []
for vals in vals_list:
@@ -153,10 +174,6 @@ class Channel(models.Model):
for pid in partner_ids_to_add if pid not in membership_pids
]
# ensure image at quick create
if not vals.get('image_128'):
vals['image_128'] = defaults['image_128']
# save visibility, apply public visibility for create then set back after creation
# to avoid ACLS issue
access_type = vals.pop('public', defaults['public'])
@@ -191,10 +208,19 @@ class Channel(models.Model):
def write(self, vals):
result = super(Channel, self).write(vals)
if vals.get('group_ids'):
self._subscribe_users_automatically()
if 'image_128' in vals:
notifications = []
for channel in self:
notifications.append([(self._cr.dbname, 'mail.channel', channel.id), {
'type': 'mail.channel_update',
'payload': {
'id': channel.id,
'avatarCacheKey': channel._get_avatar_cache_key(),
},
}])
self.env['bus.bus'].sendmany(notifications)
return result
def init(self):
@@ -557,6 +583,7 @@ class Channel(models.Model):
channel_last_message_ids = dict((r['id'], r['message_id']) for r in self._channel_last_message_ids())
for channel in self:
info = {
'avatarCacheKey': channel._get_avatar_cache_key(),
'id': channel.id,
'name': channel.name,
'description': channel.description,
@@ -969,6 +996,11 @@ class Channel(models.Model):
)
return [('insert', partners)]
def _get_avatar_cache_key(self):
if not self.avatar_128:
return 'no-avatar'
return sha512(self.avatar_128).hexdigest()
# ------------------------------------------------------------
# COMMANDS
# ------------------------------------------------------------
@@ -434,7 +434,7 @@ QUnit.test('sidebar: add channel', async function (assert) {
});
QUnit.test('sidebar: basic channel rendering', async function (assert) {
assert.expect(13);
assert.expect(12);
// channel expected to be found in the sidebar,
// with a random unique id and name that will be referenced in the test
@@ -460,11 +460,6 @@ QUnit.test('sidebar: basic channel rendering', async function (assert) {
channel.classList.contains('o-active'),
"should not be active by default"
);
assert.strictEqual(
channel.querySelectorAll(`:scope .o_ThreadIcon`).length,
1,
"should have an icon"
);
assert.strictEqual(
channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_name`).length,
1,
@@ -623,10 +618,9 @@ QUnit.test('sidebar: public/private channel rendering', async function (assert)
}).localId
}"]
`);
assert.strictEqual(
channel1.querySelectorAll(`:scope .o_ThreadIcon_channelPublic`).length,
1,
"channel1 (public) has hashtag icon"
assert.notOk(
channel1.querySelectorAll(`:scope .o_ThreadIcon`).length,
"channel1 (public) should not have any icon"
);
assert.strictEqual(
channel2.querySelectorAll(`:scope .o_ThreadIcon_channelPrivate`).length,
@@ -34,7 +34,11 @@ QUnit.module('discuss_sidebar_category_item_tests.js', {
QUnit.test('channel - avatar: should have correct avatar', async function (assert) {
assert.expect(2);
this.data['mail.channel'].records.push({ id: 20 });
this.data['mail.channel'].records.push({
avatarCacheKey: '100111',
id: 20,
});
await this.start();
const channelItem = document.querySelector(`
@@ -53,9 +57,55 @@ QUnit.test('channel - avatar: should have correct avatar', async function (asser
assert.strictEqual(
channelItem.querySelector(`:scope .o_DiscussSidebarCategoryItem_image`).dataset.src,
'/web/image/mail.channel/20/image_128',
'/web/image/mail.channel/20/avatar_128?unique=100111',
'should link to the correct picture source'
);
});
QUnit.test('channel - avatar: should update avatar url from bus', async function (assert) {
assert.expect(2);
this.data['mail.channel'].records.push({
avatarCacheKey: '101010',
id: 20,
});
await this.start();
const channelItemAvatar = document.querySelector(`
.o_DiscussSidebarCategoryItem[data-thread-local-id="${
this.messaging.models['mail.thread'].findFromIdentifyingData({
id: 20,
model: 'mail.channel',
}).localId
}"] .o_DiscussSidebarCategoryItem_image
`);
assert.strictEqual(
channelItemAvatar.dataset.src,
'/web/image/mail.channel/20/avatar_128?unique=101010',
);
await afterNextRender(() => {
this.env.services.rpc({
model: 'mail.channel',
method: 'write',
args: [[20], { image_128: 'This field does not matter' }],
});
});
const result = await this.env.services.rpc({
model: 'mail.channel',
method: 'read',
args: [[20], ['avatarCacheKey']],
});
const newCacheKey = result[0]['avatarCacheKey'];
// FIXME: current test framework does not replace `src` with `data-src` during the re-rendering.
assert.strictEqual(
channelItemAvatar.getAttribute('src'),
`/web/image/mail.channel/20/avatar_128?unique=${newCacheKey}`,
);
});
QUnit.test('chat - avatar: should have correct avatar', async function (assert) {
@@ -38,7 +38,7 @@ export class ThreadNeedactionPreview extends Component {
return this.thread.correspondent.avatarUrl;
}
if (this.thread.model === 'mail.channel') {
return `/web/image/mail.channel/${this.thread.id}/image_128`;
return `/web/image/mail.channel/${this.thread.id}/avatar_128?unique=${this.thread.avatarCacheKey}`;
}
return '/mail/static/src/img/smiley/avatar.jpg';
}
@@ -34,7 +34,7 @@ export class ThreadPreview extends Component {
if (this.thread.correspondent) {
return this.thread.correspondent.avatarUrl;
}
return `/web/image/mail.channel/${this.thread.id}/image_128`;
return `/web/image/mail.channel/${this.thread.id}/avatar_128?unique=${this.thread.avatarCacheKey}`;
}
/**
@@ -39,7 +39,7 @@ function factory(dependencies) {
switch (this.channelType) {
case 'channel':
case 'group':
return `/web/image/mail.channel/${this.channelId}/image_128`;
return `/web/image/mail.channel/${this.channelId}/avatar_128?unique=${this.channel.avatarCacheKey}`;
case 'chat':
return this.channel.correspondent.avatarUrl;
}
@@ -124,9 +124,12 @@ function factory(dependencies) {
_computeHasThreadIcon() {
switch (this.channelType) {
case 'channel':
return this.channel.public === 'private';
case 'chat':
case 'group':
return true;
case 'group':
return false;
}
}
@@ -168,6 +168,9 @@ function factory(dependencies) {
*/
static convertData(data) {
const data2 = {};
if ('avatarCacheKey' in data) {
data2.avatarCacheKey = data.avatarCacheKey;
}
if ('description' in data) {
data2.description = data.description;
}
@@ -1809,6 +1812,11 @@ function factory(dependencies) {
attachments: many2many('mail.attachment', {
inverse: 'threads',
}),
/**
* Cache key to force a reload of the avatar when avatar is changed.
* It only makes sense for channels.
*/
avatarCacheKey: attr(),
cache: one2one('mail.thread_cache', {
default: create(),
inverse: 'thread',
@@ -68,6 +68,8 @@ export class MockModels {
},
'mail.channel': {
fields: {
// In python this is not a real field but a compute. Here for simplicity.
avatarCacheKey: { string: "Avatar Cache Key", type: "datetime", default() { moment.utc().format("YYYYMMDDHHmmss"); } },
channel_type: { string: "Channel Type", type: "selection", default: 'channel' },
// In python this belongs to mail.channel.partner. Here for simplicity.
custom_channel_name: { string: "Custom channel name", type: 'char' },
@@ -223,6 +223,10 @@ MockServer.include({
const context = args.kwargs.context;
return this._mockMailChannelNotifyTyping(ids, is_typing, context);
}
if (args.model === 'mail.channel' && args.method === 'write' && 'image_128' in args.args[1]) {
const ids = args.args[0];
return this._mockMailChannelWriteImage128(ids[0]);
}
// mail.message methods
if (args.model === 'mail.message' && args.method === 'mark_all_as_read') {
const domain = args.args[0] || args.kwargs.domain;
@@ -994,6 +998,31 @@ MockServer.include({
return mentionSuggestions;
},
/**
* Simulates `write` on `mail.channel` when `image_128` changes.
*
* @param {integer} id
*/
_mockMailChannelWriteImage128(id) {
this._mockWrite('mail.channel', [
[id],
{
avatarCacheKey: moment.utc().format("YYYYMMDDHHmmss"),
},
]);
const avatarCacheKey = this._getRecords('mail.channel', [['id', '=', id]])[0].avatarCacheKey;
const notification = [
['dbName', 'mail.channel', id],
{
type: 'mail.channel_update',
payload: {
id: 20,
avatarCacheKey: avatarCacheKey,
},
},
];
this._widget.call('bus_service', 'trigger', 'notification', [notification]);
},
/**
* Simulates `message_post` on `mail.channel`.
*
+37 -1
View File
@@ -1,16 +1,18 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from datetime import datetime
from unittest.mock import patch
from odoo import Command, fields
from odoo.addons.mail.models.mail_channel import channel_avatar, group_avatar
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.mail.tests.common import MailCommon
from odoo.exceptions import AccessError
from odoo.tests import tagged, Form
from odoo.tests.common import users
from odoo.tools import mute_logger
from odoo.tools import html_escape, mute_logger
from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
@@ -437,6 +439,40 @@ class TestChannelInternals(MailCommon):
self.assertEqual(len(messages_2), 1)
self.assertEqual(messages_1, messages_2)
def test_channel_should_generate_correct_default_avatar(self):
channel = self.env['mail.channel'].create({'name': '', 'uuid': 'test-uuid'})
bgcolor = html_escape('hsl(288, 51%, 45%)') # depends on uuid
expceted_avatar_channel = (channel_avatar.replace('fill="#875a7b"', f'fill="{bgcolor}"')).encode()
expected_avatar_group = (group_avatar.replace('fill="#875a7b"', f'fill="{bgcolor}"')).encode()
channel.channel_type = 'group'
self.assertEqual(base64.b64decode(channel.avatar_128), expected_avatar_group)
channel.channel_type = 'channel'
self.assertEqual(base64.b64decode(channel.avatar_128), expceted_avatar_channel)
channel.image_128 = base64.b64encode(("<svg/>").encode())
self.assertEqual(channel.avatar_128, channel.image_128)
def test_channel_write_should_send_notification_if_image_128_changed(self):
channel = self.env['mail.channel'].create({'name': '', 'uuid': 'test-uuid'})
# do the operation once before the assert to grab the value to expect
channel.image_128 = base64.b64encode(("<svg/>").encode())
avatar_cache_key = channel._get_avatar_cache_key()
channel.image_128 = False
self.env['bus.bus'].search([]).unlink()
with self.assertBus(
[(self.cr.dbname, 'mail.channel', channel.id)],
[{
"type": "mail.channel_update",
"payload": {
"id": channel.id,
"avatarCacheKey": avatar_cache_key,
},
}]
):
channel.image_128 = base64.b64encode(("<svg/>").encode())
def test_multi_company_chat(self):
self._activate_multi_company()
self.assertEqual(self.env.user.company_id, self.company_admin)
+3 -2
View File
@@ -21,7 +21,7 @@
</t>
<t t-name="kanban-box">
<div class="oe_module_vignette oe_kanban_global_click">
<img t-att-src="kanban_image('mail.channel', 'image_128', record.id.raw_value)" class="oe_module_icon" alt="Channel"/>
<img t-att-src="kanban_image('mail.channel', 'avatar_128', record.id.raw_value)" class="oe_module_icon" alt="Channel"/>
<div class="oe_module_desc">
<h4 class="o_kanban_record_title">#<field name="name"/></h4>
<p class="oe_module_name">
@@ -45,7 +45,8 @@
<form string="Mail Channel Form">
<sheet>
<div class="oe_button_box" name="button_box"/>
<field name="image_128" widget="image" class="oe_avatar" options="{'size': [90, 90]}"/>
<field name="avatar_128" invisible="1"/>
<field name="image_128" widget="image" class="oe_avatar" options="{'size': [90, 90], 'preview_image':'avatar_128'}"/>
<div class="oe_title">
<label for="name" string="Group Name"/>
<h1>
@@ -99,7 +99,7 @@ class TestDiscussFullPerformance(TransactionCase):
self.maxDiff = None
self.users[0].flush()
self.users[0].invalidate_cache()
with self.assertQueryCount(emp=48):
with self.assertQueryCount(emp=49):
init_messaging = self.users[0].with_user(self.users[0])._init_messaging()
self.assertEqual(init_messaging, {
@@ -107,6 +107,7 @@ class TestDiscussFullPerformance(TransactionCase):
'starred_counter': 1,
'channels': [
{
'avatarCacheKey': channel_general._get_avatar_cache_key(),
'channel_type': 'channel',
'create_uid': user_root.id,
'custom_channel_name': False,
@@ -127,6 +128,7 @@ class TestDiscussFullPerformance(TransactionCase):
'uuid': channel_general.uuid,
},
{
'avatarCacheKey': channel_channel_public_1._get_avatar_cache_key(),
'channel_type': 'channel',
'create_uid': self.env.user.id,
'custom_channel_name': False,
@@ -147,6 +149,7 @@ class TestDiscussFullPerformance(TransactionCase):
'uuid': channel_channel_public_1.uuid,
},
{
'avatarCacheKey': channel_channel_public_2._get_avatar_cache_key(),
'channel_type': 'channel',
'create_uid': self.env.user.id,
'custom_channel_name': False,
@@ -167,6 +170,7 @@ class TestDiscussFullPerformance(TransactionCase):
'uuid': channel_channel_public_2.uuid,
},
{
'avatarCacheKey': channel_channel_group_1._get_avatar_cache_key(),
'channel_type': 'channel',
'create_uid': self.env.user.id,
'custom_channel_name': False,
@@ -187,6 +191,7 @@ class TestDiscussFullPerformance(TransactionCase):
'uuid': channel_channel_group_1.uuid,
},
{
'avatarCacheKey': channel_channel_group_2._get_avatar_cache_key(),
'channel_type': 'channel',
'create_uid': self.env.user.id,
'custom_channel_name': False,
@@ -207,6 +212,7 @@ class TestDiscussFullPerformance(TransactionCase):
'uuid': channel_channel_group_2.uuid,
},
{
'avatarCacheKey': channel_channel_private_1._get_avatar_cache_key(),
'channel_type': 'channel',
'create_uid': self.env.user.id,
'custom_channel_name': False,
@@ -227,6 +233,7 @@ class TestDiscussFullPerformance(TransactionCase):
'uuid': channel_channel_private_1.uuid,
},
{
'avatarCacheKey': channel_channel_private_2._get_avatar_cache_key(),
'channel_type': 'channel',
'create_uid': self.env.user.id,
'custom_channel_name': False,
@@ -247,6 +254,7 @@ class TestDiscussFullPerformance(TransactionCase):
'uuid': channel_channel_private_2.uuid,
},
{
'avatarCacheKey': channel_group_1._get_avatar_cache_key(),
'channel_type': 'group',
'create_uid': self.env.user.id,
'custom_channel_name': False,
@@ -305,6 +313,7 @@ class TestDiscussFullPerformance(TransactionCase):
'uuid': channel_group_1.uuid,
},
{
'avatarCacheKey': channel_chat_1._get_avatar_cache_key(),
'channel_type': 'chat',
'create_uid': self.env.user.id,
'custom_channel_name': False,
@@ -363,6 +372,7 @@ class TestDiscussFullPerformance(TransactionCase):
'uuid': channel_chat_1.uuid,
},
{
'avatarCacheKey': channel_chat_2._get_avatar_cache_key(),
'channel_type': 'chat',
'create_uid': self.env.user.id,
'custom_channel_name': False,
@@ -421,6 +431,7 @@ class TestDiscussFullPerformance(TransactionCase):
'uuid': channel_chat_2.uuid,
},
{
'avatarCacheKey': channel_chat_3._get_avatar_cache_key(),
'channel_type': 'chat',
'create_uid': self.env.user.id,
'custom_channel_name': False,
@@ -479,6 +490,7 @@ class TestDiscussFullPerformance(TransactionCase):
'uuid': channel_chat_3.uuid,
},
{
'avatarCacheKey': channel_chat_4._get_avatar_cache_key(),
'channel_type': 'chat',
'create_uid': self.env.user.id,
'custom_channel_name': False,
@@ -537,6 +549,7 @@ class TestDiscussFullPerformance(TransactionCase):
'uuid': channel_chat_4.uuid,
},
{
'avatarCacheKey': channel_livechat_1._get_avatar_cache_key(),
'channel_type': 'livechat',
'create_uid': self.env.user.id,
'custom_channel_name': False,
@@ -601,6 +614,7 @@ class TestDiscussFullPerformance(TransactionCase):
'uuid': channel_livechat_1.uuid,
},
{
'avatarCacheKey': channel_livechat_2._get_avatar_cache_key(),
'channel_type': 'livechat',
'create_uid': self.env.ref('base.public_user').id,
'custom_channel_name': False,
+12 -6
View File
@@ -7,6 +7,17 @@ from odoo import models, fields, api
from odoo.tools import html_escape, file_open
def get_hsl_from_seed(seed):
hashed_seed = sha512(seed.encode()).hexdigest()
# full range of colors, in degree
hue = int(hashed_seed[0:2], 16) * 360 / 255
# colorful result but not too flashy, in percent
sat = int(hashed_seed[2:4], 16) * ((70 - 40) / 255) + 40
# not too bright and not too dark, in percent
lig = 45
return f'hsl({hue:.0f}, {sat:.0f}%, {lig:.0f}%)'
class AvatarMixin(models.AbstractModel):
_name = 'avatar.mixin'
_inherit = ['image.mixin']
@@ -52,12 +63,7 @@ class AvatarMixin(models.AbstractModel):
def _avatar_generate_svg(self):
initial = html_escape(self[self._avatar_name_field][0].upper())
seed = self[self._avatar_name_field] + str(self.create_date.timestamp())
hashed_seed = sha512(seed.encode()).hexdigest()
hue = int(hashed_seed[0:2], 16) * 360 / 255
sat = int(hashed_seed[2:4], 16) * ((70 - 40) / 255) + 40
lig = 45
bgcolor = html_escape(f'hsl({hue}, {sat}%, {lig}%)')
bgcolor = get_hsl_from_seed(self[self._avatar_name_field] + str(self.create_date.timestamp()))
return b64encode((
"<?xml version='1.0' encoding='UTF-8' ?>"
"<svg height='180' width='180' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'>"