Some models would like to use the 'mail.alias.mixin' but it creates an alias for each record in the parent model. This leads to a lot of unused aliases if only a subset of those records really use aliases i.e. a lot of aliases with 'alias_name' being 'False'. In this commit we introduce a new mixin 'mail.alias.mixin.optional' that behaves like the old 'mail.alias.mixin' but without having the 'alias_id' field required i.e. without the "inherits". When creating a record without giving an 'alias_name' no alias is created. In future commit, we plan to use it notably to remove custom code in account journal model and make it more standard. Using it in more models will be done later, but it is a candidate to cleanup unused aliases related to discuss channel model. Task-3453343 (Mail: Cleanup Alias Usage) Prepares Task-36879 (Mail: Support MultiCompany Aliases) Part-of: odoo/odoo#130632
375 lines
17 KiB
Python
375 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import psycopg2
|
|
|
|
from ast import literal_eval
|
|
|
|
from odoo import exceptions
|
|
from odoo.addons.mail.tests.common import MailCommon
|
|
from odoo.tests import tagged
|
|
from odoo.tests.common import users
|
|
from odoo.tools import mute_logger
|
|
|
|
|
|
class TestMailAliasCommon(MailCommon):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls._activate_multi_company()
|
|
|
|
cls.test_alias_mc = cls.env['mail.alias'].create({
|
|
'alias_model_id': cls.env['ir.model']._get('mail.test.container.mc').id,
|
|
'alias_name': 'test.alias',
|
|
})
|
|
|
|
|
|
@tagged('mail_gateway', 'mail_alias', 'multi_company')
|
|
class TestMailAlias(TestMailAliasCommon):
|
|
""" Test alias model features, constraints and behavior. """
|
|
|
|
@users('admin')
|
|
def test_alias_domain_allowed_validation(self):
|
|
""" Check the validation of `mail.catchall.domain.allowed` system parameter"""
|
|
for value in [',', ',,', ', ,']:
|
|
with self.assertRaises(exceptions.ValidationError,
|
|
msg="The value '%s' should not be allowed" % value):
|
|
self.env['ir.config_parameter'].set_param('mail.catchall.domain.allowed', value)
|
|
|
|
for value, expected in [
|
|
('', False),
|
|
('hello.com', 'hello.com'),
|
|
('hello.com,,', 'hello.com'),
|
|
('hello.com,bonjour.com', 'hello.com,bonjour.com'),
|
|
('hello.COM, BONJOUR.com', 'hello.com,bonjour.com'),
|
|
]:
|
|
self.env['ir.config_parameter'].set_param('mail.catchall.domain.allowed', value)
|
|
self.assertEqual(self.env['ir.config_parameter'].get_param('mail.catchall.domain.allowed'), expected)
|
|
|
|
@users('admin')
|
|
@mute_logger('odoo.models.unlink')
|
|
def test_alias_domain_parameters(self):
|
|
""" Check the validation of ``mail.bounce.alias`` and ``mail.catchall.alias``
|
|
parameters. """
|
|
ICP = self.env['ir.config_parameter']
|
|
# sanitization
|
|
for (bounce_value, catchall_value), (expected_bounce, expected_catchall) in zip(
|
|
[
|
|
('bounce+b4r=*R3wl_#_-$€{}[]()~|\\/!?&%^\'"`~', 'catchall+b4r=*R3wl_#_-$€{}[]()~|\\/!?&%^\'"`~'),
|
|
('bounce+😊', 'catchall+😊'),
|
|
('Bouncâïde 😊', 'Catchôïee 😊'),
|
|
('ぁ', 'ぁぁ'),
|
|
],
|
|
[
|
|
('bounce+b4r=*r3wl_#_-$-{}-~|-/!?&%^\'-`~', 'catchall+b4r=*r3wl_#_-$-{}-~|-/!?&%^\'-`~'),
|
|
('bounce+-', 'catchall+-'),
|
|
('bouncaide-', 'catchoiee-'),
|
|
('?', '??'),
|
|
]
|
|
):
|
|
with self.subTest(bounce_value=bounce_value):
|
|
ICP.set_param('mail.bounce.alias', bounce_value)
|
|
self.assertEqual(ICP.get_param('mail.bounce.alias'), expected_bounce)
|
|
with self.subTest(catchall_value=catchall_value):
|
|
ICP.set_param('mail.catchall.alias', catchall_value)
|
|
self.assertEqual(ICP.get_param('mail.catchall.alias'), expected_catchall)
|
|
|
|
# falsy values
|
|
for config_value in [False, None, '', ' ']:
|
|
with self.subTest(config_value=config_value):
|
|
ICP.set_param('mail.bounce.alias', config_value)
|
|
self.assertFalse(ICP.get_param('mail.bounce.alias'))
|
|
ICP.set_param('mail.catchall.alias', config_value)
|
|
self.assertFalse(ICP.get_param('mail.catchall.alias'))
|
|
|
|
# check successive param set, should not raise for unicity against itself
|
|
for _ in range(2):
|
|
ICP.set_param('mail.bounce.alias', 'bounce+double.test')
|
|
ICP.set_param('mail.catchall.alias', 'catchall+double.test')
|
|
self.assertEqual(ICP.get_param('mail.bounce.alias'), 'bounce+double.test')
|
|
self.assertEqual(ICP.get_param('mail.catchall.alias'), 'catchall+double.test')
|
|
|
|
@users('admin')
|
|
def test_alias_name_unique(self):
|
|
alias_model_id = self.env['ir.model']._get('mail.test.gateway').id
|
|
catchall_alias = self.env['ir.config_parameter'].sudo().get_param('mail.catchall.alias')
|
|
bounce_alias = self.env['ir.config_parameter'].sudo().get_param('mail.bounce.alias')
|
|
|
|
# test you cannot create aliases matching bounce / catchall
|
|
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
|
|
self.env['mail.alias'].create({'alias_model_id': alias_model_id, 'alias_name': catchall_alias})
|
|
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
|
|
self.env['mail.alias'].create({'alias_model_id': alias_model_id, 'alias_name': bounce_alias})
|
|
|
|
new_mail_alias = self.env['mail.alias'].create({
|
|
'alias_model_id': alias_model_id,
|
|
'alias_name': 'unused.test.alias'
|
|
})
|
|
other_alias = self.env['mail.alias'].create({
|
|
'alias_model_id': alias_model_id,
|
|
'alias_name': 'other.test.alias'
|
|
})
|
|
|
|
# test that alias names should be unique
|
|
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
|
|
self.env['mail.alias'].create({
|
|
'alias_model_id': alias_model_id,
|
|
'alias_name': 'unused.test.alias',
|
|
})
|
|
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
|
|
self.env['mail.alias'].create([
|
|
{
|
|
'alias_model_id': alias_model_id,
|
|
'alias_name': alias_name,
|
|
}
|
|
for alias_name in ('new.alias.1', 'new.alias.2', 'new.alias.1')
|
|
])
|
|
with self.assertRaises(psycopg2.errors.UniqueViolation), self.cr.savepoint(), mute_logger('odoo.sql_db'):
|
|
new_mail_alias.copy({'alias_name': 'unused.test.alias'})
|
|
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
|
|
other_alias.write({'alias_name': 'unused.test.alias'})
|
|
|
|
# test that re-using catchall and bounce alias raises UserError
|
|
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
|
|
new_mail_alias.write({'alias_name': catchall_alias})
|
|
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
|
|
new_mail_alias.write({'alias_name': bounce_alias})
|
|
|
|
new_mail_alias.write({'alias_name': 'another.unused.test.alias'})
|
|
|
|
# test that duplicating an alias should have blank name
|
|
copy_1 = new_mail_alias.copy()
|
|
self.assertFalse(copy_1.alias_name)
|
|
# test sanitize of copy with new name
|
|
copy_2 = new_mail_alias.copy({'alias_name': 'test.alias.2.éè#'})
|
|
self.assertEqual(copy_2.alias_name, 'test.alias.2.ee#')
|
|
|
|
# cannot batch update, would create duplicates
|
|
with self.assertRaises(exceptions.UserError):
|
|
(copy_1 + copy_2).write({'alias_name': 'test.alias.other'})
|
|
|
|
# cannot set catchall / bounce to used alias
|
|
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
|
|
self.env['ir.config_parameter'].sudo().set_param('mail.catchall.alias', new_mail_alias.alias_name)
|
|
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
|
|
self.env['ir.config_parameter'].sudo().set_param('mail.bounce.alias', new_mail_alias.alias_name)
|
|
|
|
@users('admin')
|
|
@mute_logger('odoo.models.unlink')
|
|
def test_alias_name_sanitize(self):
|
|
""" Check sanitizer, at both create, copy and write on alias name. """
|
|
alias_names = [
|
|
'bidule...inc.',
|
|
'b4r+=*R3wl_#_-$€{}[]()~|\\/!?&%^\'"`~',
|
|
'hélène.prôvâïder',
|
|
'😊',
|
|
'Déboulonneur 😊',
|
|
'ぁ',
|
|
]
|
|
expected_names = [
|
|
'bidule.inc',
|
|
'b4r+=*r3wl_#_-$-{}-~|-/!?&%^\'-`~',
|
|
'helene.provaider',
|
|
'-',
|
|
'deboulonneur-',
|
|
'?',
|
|
]
|
|
msgs = [
|
|
'Emails cannot start or end with a dot, there cannot be a sequence of dots.',
|
|
'Disallowed chars should be replaced by hyphens',
|
|
'Email alias should be unaccented',
|
|
'Only a subset of unaccented latin chars are valid, others are replaced',
|
|
'Only a subset of unaccented latin chars are valid, others are replaced',
|
|
'Only a subset of unaccented latin chars are valid, others are replaced',
|
|
]
|
|
for alias_name, expected, msg in zip(alias_names, expected_names, msgs):
|
|
with self.subTest(alias_name=alias_name):
|
|
alias = self.env['mail.alias'].create({
|
|
'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
|
|
'alias_name': alias_name,
|
|
})
|
|
self.assertEqual(alias.alias_name, expected, msg)
|
|
alias.unlink()
|
|
|
|
alias = self.env['mail.alias'].create({
|
|
'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
|
|
})
|
|
# check at write
|
|
for alias_name, expected, msg in zip(alias_names, expected_names, msgs):
|
|
with self.subTest(alias_name=alias_name):
|
|
alias.write({'alias_name': alias_name})
|
|
self.assertEqual(alias.alias_name, expected, msg)
|
|
|
|
@users('admin')
|
|
def test_alias_name_sanitize_false(self):
|
|
""" Check empty-like aliases are forced to False, as otherwise unique
|
|
constraint might fail with empty strings. """
|
|
aliases = self.env['mail.alias'].create([
|
|
{
|
|
'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
|
|
'alias_name': falsy_name,
|
|
}
|
|
# '.' -> not allowed to start with a "." hence False
|
|
for falsy_name in [False, None, '', ' ', '.']
|
|
])
|
|
for alias in aliases:
|
|
with self.subTest(alias_name=alias.alias_name):
|
|
self.assertFalse(alias.alias_name, 'Void values should resolve to False')
|
|
|
|
# try to reset names in batch: should work
|
|
for idx, alias in enumerate(aliases):
|
|
alias.write({'alias_name': f'unique-{idx}'})
|
|
aliases.write({'alias_name': ''})
|
|
for alias in aliases:
|
|
self.assertEqual(alias.alias_name, False)
|
|
|
|
@users('admin')
|
|
def test_alias_setup(self):
|
|
""" Test various constraints / configuration of alias model"""
|
|
alias = self.env['mail.alias'].create({
|
|
'alias_model_id': self.env['ir.model']._get('mail.test.container.mc').id,
|
|
'alias_name': 'unused.test.alias'
|
|
})
|
|
self.assertEqual(alias.alias_status, 'not_tested')
|
|
|
|
# validation of alias_defaults
|
|
with self.assertRaises(exceptions.ValidationError):
|
|
alias.write({'alias_defaults': "{'custom_field': brokendict"})
|
|
alias.write({'alias_defaults': "{'custom_field': 'validdict'}"})
|
|
|
|
|
|
@tagged('mail_gateway', 'mail_alias', 'multi_company')
|
|
class TestMailAliasMixin(TestMailAliasCommon):
|
|
""" Test alias mixin implementation, synchornization of alias records
|
|
based on owner records. """
|
|
|
|
@users('employee')
|
|
@mute_logger('odoo.addons.base.models.ir_model')
|
|
def test_alias_mixin(self):
|
|
record = self.env['mail.test.container'].create({
|
|
'name': 'Test Record',
|
|
'alias_name': 'alias.test',
|
|
'alias_contact': 'followers',
|
|
})
|
|
self.assertEqual(record.alias_id.alias_model_id, self.env['ir.model']._get('mail.test.container'))
|
|
self.assertEqual(record.alias_id.alias_force_thread_id, record.id)
|
|
self.assertEqual(record.alias_id.alias_parent_model_id, self.env['ir.model']._get('mail.test.container'))
|
|
self.assertEqual(record.alias_id.alias_parent_thread_id, record.id)
|
|
self.assertEqual(record.alias_id.alias_name, 'alias.test')
|
|
self.assertEqual(record.alias_id.alias_contact, 'followers')
|
|
|
|
record.write({
|
|
'alias_name': 'better.alias.test',
|
|
'alias_defaults': "{'default_name': 'defaults'}"
|
|
})
|
|
self.assertEqual(record.alias_id.alias_name, 'better.alias.test')
|
|
self.assertEqual(record.alias_id.alias_defaults, "{'default_name': 'defaults'}")
|
|
|
|
with self.assertRaises(exceptions.AccessError):
|
|
record.write({
|
|
'alias_force_thread_id': 0,
|
|
})
|
|
|
|
with self.assertRaises(exceptions.AccessError):
|
|
record.write({
|
|
'alias_model_id': self.env['ir.model']._get('mail.test.gateway').id,
|
|
})
|
|
|
|
with self.assertRaises(exceptions.ValidationError):
|
|
record.write({'alias_defaults': "{'custom_field': brokendict"})
|
|
|
|
@users('employee')
|
|
@mute_logger('odoo.addons.base.models.ir_model')
|
|
def test_alias_mixin_alias_id_management(self):
|
|
""" Test alias_id being not mandatory """
|
|
record_wo_alias, record_w_alias = self.env['mail.test.alias.optional'].create([
|
|
{
|
|
'name': 'Test WoAlias Name',
|
|
}, {
|
|
'alias_name': 'Alias Name',
|
|
'name': 'Test WoAlias Name',
|
|
}
|
|
])
|
|
self.assertFalse(record_wo_alias.alias_id, 'Alias record not created if not necessary (no alias_name)')
|
|
self.assertFalse(record_wo_alias.alias_id.alias_name)
|
|
self.assertFalse(record_wo_alias.alias_id.alias_defaults)
|
|
self.assertFalse(record_wo_alias.alias_name)
|
|
self.assertTrue(record_w_alias.alias_id, 'Alias record created as alias_name was given')
|
|
self.assertEqual(record_w_alias.alias_id.alias_name, 'alias-name', 'Alias name should go through sanitize')
|
|
self.assertEqual(
|
|
literal_eval(record_w_alias.alias_id.alias_defaults),
|
|
{'company_id': self.env.company.id}
|
|
)
|
|
self.assertEqual(record_w_alias.alias_name, 'alias-name', 'Alias name should go through sanitize')
|
|
self.assertEqual(
|
|
literal_eval(record_w_alias.alias_defaults),
|
|
{'company_id': self.env.company.id}
|
|
)
|
|
|
|
# update existing alias
|
|
record_w_alias.write({'alias_contact': 'followers', 'alias_name': 'Updated Alias Name'})
|
|
self.assertEqual(record_w_alias.alias_id.alias_contact, 'followers')
|
|
self.assertEqual(record_w_alias.alias_id.alias_name, 'updated-alias-name')
|
|
self.assertEqual(record_w_alias.alias_name, 'updated-alias-name')
|
|
|
|
# update non existing alias -> creates alias
|
|
record_wo_alias.write({'alias_name': 'trying a name'})
|
|
self.assertTrue(record_wo_alias.alias_id, 'Alias record should have been created to store the name')
|
|
self.assertEqual(record_wo_alias.alias_id.alias_name, 'trying-a-name')
|
|
self.assertEqual(
|
|
literal_eval(record_wo_alias.alias_id.alias_defaults),
|
|
{'company_id': self.env.company.id}
|
|
)
|
|
self.assertEqual(record_wo_alias.alias_name, 'trying-a-name')
|
|
self.assertEqual(
|
|
literal_eval(record_wo_alias.alias_defaults),
|
|
{'company_id': self.env.company.id}
|
|
)
|
|
|
|
# reset alias -> keep the alias as void, don't remove it
|
|
existing_aliases = record_wo_alias.alias_id + record_w_alias.alias_id
|
|
(record_wo_alias + record_w_alias).write({'alias_name': False})
|
|
self.assertEqual((record_wo_alias + record_w_alias).alias_id, existing_aliases)
|
|
self.assertFalse(list(filter(None, existing_aliases.mapped('alias_name'))))
|
|
|
|
@users('employee')
|
|
def test_alias_mixin_copy_content(self):
|
|
self.assertFalse(self.env.user.has_group('base.group_system'), 'Test user should not have Administrator access')
|
|
|
|
record = self.env['mail.test.container'].create({
|
|
'name': 'Test Record',
|
|
'alias_name': 'test.record',
|
|
'alias_contact': 'followers',
|
|
'alias_bounced_content': False,
|
|
})
|
|
self.assertFalse(record.alias_bounced_content)
|
|
record_copy = record.copy()
|
|
self.assertFalse(record_copy.alias_bounced_content)
|
|
|
|
new_content = '<p>Bounced Content</p>'
|
|
record_copy.write({'alias_bounced_content': new_content})
|
|
self.assertEqual(record_copy.alias_bounced_content, new_content)
|
|
record_copy2 = record_copy.copy()
|
|
self.assertEqual(record_copy2.alias_bounced_content, new_content)
|
|
|
|
@users('erp_manager')
|
|
def test_alias_mixin_mc(self):
|
|
""" Test company change does not impact anything at alias domain level """
|
|
record = self.env['mail.test.container.mc'].create({
|
|
'name': 'Test Record',
|
|
'alias_name': 'alias.test',
|
|
'alias_contact': 'followers',
|
|
'company_id': self.env.user.company_id.id,
|
|
})
|
|
self.assertEqual(record.alias_domain, self.alias_domain)
|
|
self.assertEqual(record.company_id, self.company_2)
|
|
|
|
record.write({'company_id': self.company_admin.id})
|
|
self.assertEqual(record.alias_domain, self.alias_domain)
|
|
self.assertEqual(record.company_id, self.company_admin)
|
|
|
|
record.write({'company_id': False})
|
|
self.assertEqual(record.alias_domain, self.alias_domain)
|
|
self.assertFalse(record.company_id)
|