[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:
committed by
Sébastien Theys
parent
22d905bbb5
commit
e8922db3da
@@ -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,
|
||||
|
||||
+52
-2
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+4
-1
@@ -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`.
|
||||
*
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'>"
|
||||
|
||||
Reference in New Issue
Block a user