Files
odoo_source/odoo/addons/test_new_api/tests/test_properties.py
T
Ipsita Borisagar b2103b06f7 [FIX] base, web: allow reading property fields when no access to parent record
This commit fixes a use case where a user would not be able to access the
property field values of a record because he did not have access to that
record's parent (from the property field definition configuration).

In a concrete example:
- In the Knowledge App
- Marc Demo has access to the "Article B".
- The parent article of "Article B" is "Article A".
- Marc Demo does not have access to "Article A".
- Article B has some property fields values configured.
- If Marc Demo tries to open "Article B" -> crash

This is caused by the framework having to read the property definition from the
parent record, which we don't have access to.
A "sudo" was added to allow that use case (which is supported).

In addition, this commit also adds further checks on the web client side to
gracefully handle a special use case where the end-user could not write on the
parent record because of record rules.
(It only checked *access rules*).

To avoid making an extra RPC every time we load the record (to check if we can
write on the parent), we only make that check when the user tries to modify
(add/edit/remove) a property and show a proper error message.
(This also applies when creating new property field tags).

Task-3062064

closes odoo/odoo#118929

X-original-commit: bc538f6944461a642bac0f757ec96d7f8cc14c9a
Related: odoo/enterprise#39946
Signed-off-by: Warnon Aurélien (awa) <awa@odoo.com>
2023-04-18 19:29:03 +02:00

1812 lines
74 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import unittest
from unittest.mock import patch
from odoo import Command
from odoo.exceptions import AccessError, UserError
from odoo.tests.common import Form, TransactionCase, users
from odoo.tools import mute_logger
class PropertiesCase(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user = cls.env.user
cls.partner = cls.env['test_new_api.partner'].create({'name': 'Test Partner Properties'})
cls.partner_2 = cls.env['test_new_api.partner'].create({'name': 'Test Partner Properties 2'})
cls.test_user = cls.env['res.users'].create({
'name': 'Test',
'login': 'test',
'company_id': cls.env.company.id,
})
attributes_definition_1 = [{
'name': 'discussion_color_code',
'string': 'Color Code',
'type': 'char',
'default': 'blue',
}, {
'name': 'moderator_partner_id',
'string': 'Partner',
'type': 'many2one',
'comodel': 'test_new_api.partner',
}]
attributes_definition_2 = [{
'name': 'state',
'type': 'selection',
'string': 'Status',
'selection': [('draft', 'Draft'), ('progress', 'In Progress'), ('done', 'Done')],
'default': 'draft',
}]
cls.discussion_1 = cls.env['test_new_api.discussion'].create({
'name': 'Test Discussion',
'attributes_definition': attributes_definition_1,
'participants': [Command.link(cls.user.id)],
})
cls.discussion_2 = cls.env['test_new_api.discussion'].create({
'name': 'Test Discussion',
'attributes_definition': attributes_definition_2,
'participants': [Command.link(cls.user.id)],
})
cls.message_1 = cls.env['test_new_api.message'].create({
'name': 'Test Message',
'discussion': cls.discussion_1.id,
'author': cls.user.id,
'attributes': {
'discussion_color_code': 'Test',
'moderator_partner_id': cls.partner.id,
},
})
cls.message_2 = cls.env['test_new_api.message'].create({
'name': 'Test Message',
'discussion': cls.discussion_1.id,
'author': cls.user.id,
})
cls.message_3 = cls.env['test_new_api.message'].create({
'name': 'Test Message',
'discussion': cls.discussion_2.id,
'author': cls.user.id,
})
def test_properties_field(self):
self.assertTrue(isinstance(self.message_1.attributes, list))
# testing assigned value
self.assertEqual(self.message_1.attributes, [{
'name': 'discussion_color_code',
'string': 'Color Code',
'type': 'char',
'default': 'blue',
'value': 'Test',
}, {
'name': 'moderator_partner_id',
'string': 'Partner',
'type': 'many2one',
'comodel': 'test_new_api.partner',
'value': self.partner.id,
}])
self.assertEqual(self.message_2.attributes[0]['value'], 'blue')
self.assertFalse(self.message_2.attributes[1]['value'])
# testing default value
self.assertEqual(
self.message_3.attributes[0]['value'], 'draft',
msg='Should have taken the default value')
self.message_1.attributes = [
{'name': 'discussion_color_code', 'value': 'red'},
{'name': 'moderator_partner_id', 'value': self.partner_2.id},
]
self.assertEqual(self.message_1.attributes[0]['value'], 'red')
self.env.invalidate_all()
self.assertEqual(self.message_1.attributes[0]['value'], 'red')
self.assertEqual(self.message_1.attributes[1]['value'], self.partner_2.id)
# check that the value has been updated in the database
database_values = self._get_sql_properties(self.message_1)
self.assertTrue(isinstance(database_values, dict))
self.assertEqual(
database_values.get('discussion_color_code'), 'red',
msg='Value must be updated in the database')
# if we write False on the field, it should still
# return the properties definition for the web client
self.message_3.attributes = False
self.env.invalidate_all()
expected = self.discussion_2.attributes_definition
for property_definition in expected:
property_definition['value'] = False
self.assertEqual(self.message_3.read(['attributes'])[0]['attributes'], expected)
self.assertEqual(self.message_3.attributes, expected)
@mute_logger('odoo.fields')
def test_properties_field_write_batch(self):
"""Test the behavior of the write called in batch.
Simulate a write operation done by the web client.
"""
# mix both properties
properties_values = (self.message_1 | self.message_3).read(['attributes'])
properties_values = properties_values[0]['attributes'] + properties_values[1]['attributes']
for properties in properties_values:
if properties['name'] == 'discussion_color_code':
properties['value'] = 'orange'
elif properties['name'] == 'state':
properties['value'] = 'done'
elif properties['name'] == 'moderator_partner_id':
properties['value'] = self.partner_2.id
properties['definition_changed'] = True
(self.message_1 | self.message_3).write({'attributes': properties_values})
sql_values_1 = self._get_sql_properties(self.message_1)
sql_values_3 = self._get_sql_properties(self.message_3)
# definition of both child has been changed
self.assertEqual(sql_values_1, {'discussion_color_code': 'orange', 'moderator_partner_id': self.partner_2.id, 'state': 'done'})
self.assertEqual(sql_values_3, {'discussion_color_code': 'orange', 'moderator_partner_id': self.partner_2.id, 'state': 'done'})
@mute_logger('odoo.models.unlink', 'odoo.fields')
def test_properties_field_read_batch(self):
values = self.message_1.read(['attributes'])[0]['attributes']
self.assertEqual(len(values), 2)
self.assertEqual(values[0]['type'], 'char')
self.assertEqual(values[1]['type'], 'many2one')
message_2_values = self.message_1.attributes
message_2_values[1]['value'] = [self.partner_2.id, "Bob"]
self.message_2.attributes = message_2_values
expected_queries = [
# read the properties field value
''' SELECT "test_new_api_message"."id",
"test_new_api_message"."attributes"
FROM "test_new_api_message"
WHERE ("test_new_api_message"."id" IN %s)
''',
''' SELECT "test_new_api_message"."id",
"test_new_api_message"."discussion",
"test_new_api_message"."body",
"test_new_api_message"."author",
"test_new_api_message"."name",
"test_new_api_message"."important",
"test_new_api_message"."label"->>'en_US',
"test_new_api_message"."priority",
"test_new_api_message"."create_uid",
"test_new_api_message"."create_date",
"test_new_api_message"."write_uid",
"test_new_api_message"."write_date"
FROM "test_new_api_message"
WHERE ("test_new_api_message"."id" IN %s)
''',
# read the definition on the definition record
''' SELECT "test_new_api_discussion"."id",
"test_new_api_discussion"."name",
"test_new_api_discussion"."moderator",
"test_new_api_discussion"."message_concat",
"test_new_api_discussion"."history",
"test_new_api_discussion"."attributes_definition",
"test_new_api_discussion"."create_uid",
"test_new_api_discussion"."create_date",
"test_new_api_discussion"."write_uid",
"test_new_api_discussion"."write_date"
FROM "test_new_api_discussion"
WHERE ("test_new_api_discussion"."id" IN %s)
''',
# check the many2one existence
''' SELECT "test_new_api_partner"."id"
FROM "test_new_api_partner"
WHERE "test_new_api_partner".id IN %s
''',
''' SELECT "test_new_api_partner"."id",
"test_new_api_partner"."name",
"test_new_api_partner"."create_uid",
"test_new_api_partner"."create_date",
"test_new_api_partner"."write_uid",
"test_new_api_partner"."write_date"
FROM "test_new_api_partner"
WHERE ("test_new_api_partner"."id" IN %s)
''',
]
self.env.invalidate_all()
with self.assertQueryCount(5), self.assertQueries(expected_queries):
self.message_1.read(['attributes'])
# read in batch a lot of records
discussions = [self.discussion_1, self.discussion_2]
partners = self.env['test_new_api.partner'].create([{'name': f'Test {i}'} for i in range(50)])
messages = self.env['test_new_api.message'].create([{
'name': f'Test Message {i}',
'discussion': discussions[i % 2].id,
'author': self.user.id,
'attributes': [{
'name': 'partner_id',
'type': 'many2one',
'comodel': 'test_new_api.partner',
'value': partner.id,
'definition_changed': True,
}]
} for i, partner in enumerate(partners)])
self.env.invalidate_all()
with self.assertQueryCount(5), self.assertQueries(expected_queries):
values = messages.read(['attributes'])
# remove some partners in the list
partners[:20].unlink()
self.env.invalidate_all()
# 5 queries instead of 25 queries, thanks to the cache values that has been
# cleaned (the properties field can trust the cached value, the deleted ids
# are not in the cache even if they still exists in the database)
with self.assertQueryCount(5):
values = messages.read(['attributes'])
@mute_logger('odoo.fields')
def test_properties_field_delete(self):
"""Test to delete a property using the flag "definition_deleted"."""
self.message_1.attributes = [{
'name': 'discussion_color_code',
'string': 'Test color code',
'type': 'char',
'default': 'blue',
'value': 'purple',
}, {
'name': 'moderator_partner_id',
'string': 'Partner',
'type': 'many2one',
'comodel': 'test_new_api.partner',
'value': [self.partner.id, 'Bob'],
'definition_deleted': True,
}]
sql_definition = self._get_sql_definition(self.discussion_1)
self.assertEqual(
sql_definition, [{
'name': 'discussion_color_code',
'type': 'char',
'string': 'Test color code',
'default': 'blue',
}])
self.assertEqual(len(self.message_1.attributes), 1)
self.assertEqual(self.message_1.attributes[0]['value'], 'purple')
@mute_logger('odoo.fields')
def test_properties_field_create_batch(self):
# first create to cache the access rights
self.env['test_new_api.message'].create({'name': 'test'})
with self.assertQueryCount(2):
messages = self.env['test_new_api.message'].create([{
'name': 'Test Message',
'discussion': False,
'author': self.user.id,
}, {
'name': 'Test Message',
'discussion': False,
'author': self.user.id,
}])
self.env.invalidate_all()
with self.assertQueryCount(8):
messages = self.env['test_new_api.message'].create([{
'name': 'Test Message',
'discussion': self.discussion_1.id,
'author': self.user.id,
'attributes': [{
# no name, should be automatically generated
'string': 'Discussion Color code',
'type': 'char',
'default': 'blue',
'value': 'purple',
'definition_changed': True,
}, {
# the name is already set and shouldn't be re-generated
'name': 'moderator_partner_id',
'string': 'Partner',
'type': 'many2one',
'comodel': 'test_new_api.partner',
'value': self.partner.id,
'definition_changed': True,
}],
}, {
'name': 'Test Message',
'discussion': self.discussion_2.id,
'author': self.user.id,
'attributes': [{
'type': 'selection',
'string': 'Status',
'selection': [
('draft', 'Draft'),
('progress', 'In Progress'),
('done', 'Done'),
],
'default': 'draft',
'definition_changed': True,
}],
}])
self.env.invalidate_all()
sql_definition = self._get_sql_definition(self.discussion_1)
self.assertEqual(len(sql_definition), 2)
# check the generated name
property_color_name = sql_definition[0]['name']
self.assertTrue(property_color_name, msg="Property name must have been generated")
self.assertEqual(sql_definition, [
{
'name': property_color_name,
'default': 'blue',
'string': 'Discussion Color code',
'type': 'char',
}, {
'name': 'moderator_partner_id',
'type': 'many2one',
'comodel': 'test_new_api.partner',
'string': 'Partner',
}
])
self.assertEqual(
self.discussion_1.attributes_definition[0]['string'], 'Discussion Color code',
msg='Should have updated the definition record')
self.assertEqual(len(messages), 2)
sql_properties_1 = self._get_sql_properties(messages[0])
self.assertEqual(
sql_properties_1,
{'moderator_partner_id': self.partner.id,
property_color_name: 'purple'})
sql_properties_2 = self._get_sql_properties(messages[1])
status_name = self.discussion_2.attributes_definition[0]['name']
self.assertEqual(
sql_properties_2,
{status_name: 'draft'})
properties_values_1 = messages[0].attributes
properties_values_2 = messages[1].attributes
self.assertEqual(len(properties_values_1), 2, msg='Discussion 1 has 2 properties')
self.assertEqual(len(properties_values_2), 1, msg='Discussion 2 has 1 property')
self.assertEqual(properties_values_1[0]['value'], 'purple')
self.assertEqual(properties_values_1[1]['value'], self.partner.id)
self.assertEqual(properties_values_2[0]['value'], 'draft',
msg='Should have taken the default value')
def test_properties_field_default(self):
message = self.env['test_new_api.message'].create({
'name': 'Test Message',
'discussion': self.discussion_2.id,
'author': self.user.id,
})
self.assertEqual(
message.attributes[0]['value'],
'draft',
msg='Should have taken the default value')
message.attributes = [{'name': 'state', 'value': None}]
self.assertFalse(
message.attributes[0]['value'],
msg='Writing None should not reset to the default value')
# test the case where the definition record come from a default as well
def default_discussion(_record):
return self.discussion_2.id
with patch.object(self.env['test_new_api.message']._fields['discussion'], 'default', default_discussion):
message = self.env['test_new_api.message'].create({
'name': 'Test Message',
'author': self.user.id,
})
self.assertEqual(message.discussion, self.discussion_2)
self.assertEqual(
message.attributes[0]['value'],
'draft',
msg='Should have taken the default value')
# the definition record come from a default value
self.discussion_2.attributes_definition = [{
'name': 'test',
'type': 'char',
'default': 'default char',
}]
message = self.env['test_new_api.message'] \
.with_context(default_discussion=self.discussion_2) \
.create({'name': 'Test Message', 'author': self.user.id})
self.assertEqual(message.discussion, self.discussion_2)
self.assertEqual(message.attributes, [{
'name': 'test',
'type': 'char',
'default': 'default char',
'value': 'default char',
}])
# test a default many2one
self.discussion_1.attributes_definition = [
{
'name': 'my_many2one',
'string': 'Partner',
'comodel': 'test_new_api.partner',
'type': 'many2one',
# send the value like the web client does
'default': [self.partner.id, 'Bob'],
},
]
sql_definition = self._get_sql_definition(self.discussion_1)
self.assertEqual(sql_definition[0]['default'], self.partner.id)
read_values = self.discussion_1.read(['attributes_definition'])[0]['attributes_definition']
self.assertEqual(
read_values[0]['default'],
(self.partner.id, self.partner.display_name),
msg='When reading many2one default, it should return the display name',
)
# read the default many2one and deactivate the name_get
read_values = self.discussion_1.read(['attributes_definition'], load=None)[0]['attributes_definition']
self.assertEqual(
read_values[0]['default'],
self.partner.id,
msg='If the name_get is deactivate, it should not return the display name',
)
message = self.env['test_new_api.message'].create({
'name': 'Test Message',
'author': self.user.id,
'discussion': self.discussion_1.id,
})
properties = message.read(['attributes'])[0]['attributes']
self.assertEqual(properties[0]['value'], (self.partner.id, self.partner.display_name))
self.assertEqual(message.attributes[0]['value'], self.partner.id)
# give a default value and a value for a many2one
# the default value must be ignored
property_definition = self.discussion_1.read(['attributes_definition'])[0]['attributes_definition']
property_definition[0]['value'] = (self.partner_2.id, 'Alice')
message = self.env['test_new_api.message'].create({
'name': 'Test Message',
'author': self.user.id,
'discussion': self.discussion_1.id,
'attributes': property_definition,
})
self.assertEqual(
message.attributes[0]['value'],
self.partner_2.id,
msg='Should not take the default value',
)
def test_properties_field_read(self):
"""Test the behavior of the read method.
In comparison with a simple "record.properties", the read method should not
record a recordset for the many2one, but a tuple with the record id and
the record name_get.
"""
properties_values = (self.message_1 | self.message_3).read(['attributes'])
self.assertEqual(len(properties_values), 2)
properties_message_1 = properties_values[0]['attributes']
properties_message_3 = properties_values[1]['attributes']
self.assertTrue(isinstance(properties_message_1, list))
self.assertTrue(isinstance(properties_message_3, list))
self.assertEqual(len(properties_message_1), 2, msg="Message 1 has 2 properties")
self.assertEqual(len(properties_message_3), 1, msg="Message 3 has 1 property")
self.assertEqual(
properties_message_1[0]['name'], 'discussion_color_code',
msg='First message 1 property should be "discussion_color_code"')
self.assertEqual(
properties_message_1[1]['name'], 'moderator_partner_id',
msg='Second message 1 property should be "moderator_partner_id"')
self.assertEqual(
properties_message_3[0]['name'], 'state',
msg='First message 3 property should be "state"')
many2one_property = properties_message_1[1]
self.assertEqual(
many2one_property['string'], 'Partner',
msg='Definition must be present when reading child')
self.assertEqual(
many2one_property['type'], 'many2one',
msg='Definition must be present when reading child')
self.assertEqual(
many2one_property['comodel'], 'test_new_api.partner',
msg='Definition must be present when reading child')
self.assertEqual(many2one_property['value'], (self.partner.id, self.partner.display_name))
# disable the name_get
properties_values = (self.message_1 | self.message_3).read(['attributes'], load=None)
many2one_property = properties_values[0]['attributes'][1]
self.assertEqual(
many2one_property['value'], self.partner.id,
msg='If name_get is disable, should only return the record id')
def test_properties_field_many2one_basic(self):
"""Test the basic (read, write...) of the many2one property."""
self.message_2.attributes = [
{
"name": "discussion_color_code",
"type": "char",
"string": "Color Code",
"default": "blue",
"value": False,
}, {
"name": "moderator_partner_id",
"type": "many2one",
"string": "Partner",
"comodel": "test_new_api.partner",
"value": self.partner_2.id,
},
]
self.assertFalse(self.message_2.attributes[0]['value'])
self.assertEqual(self.message_2.attributes[1]['value'], self.partner_2.id)
sql_values = self._get_sql_properties(self.message_2)
self.assertEqual(
sql_values,
{'moderator_partner_id': self.partner_2.id,
'discussion_color_code': False})
# read the many2one
properties = self.message_2.read(['attributes'])[0]['attributes']
self.assertEqual(properties[1]['value'], (self.partner_2.id, self.partner_2.display_name))
self.assertEqual(properties[1]['comodel'], 'test_new_api.partner')
@mute_logger('odoo.models.unlink', 'odoo.fields')
def test_properties_field_many2one_unlink(self):
"""Test the case where we unlink the many2one record."""
self.message_2.attributes = [{
'name': 'moderator_partner_id',
'value': self.partner.id,
}]
# remove the partner on message 2
self.partner.unlink()
with self.assertQueryCount(4):
# 1 query to read the field
# 1 query to read the definition
# 2 queries to check if the many2one still exists / name_get
self.assertFalse(self.message_2.read(['attributes'])[0]['attributes'][0]['value'])
# remove the partner, and use the read method
self.message_2.attributes = [{
'name': 'moderator_partner_id',
'value': self.partner_2.id,
}]
self.partner_2.unlink()
with self.assertQueryCount(4):
value = self.message_2.read(['attributes'])
value = value[0]['attributes']
self.assertFalse(value[1]['value'])
self.assertEqual(value[1]['comodel'], 'test_new_api.partner')
# many2one properties in a default value
partner = self.env['res.partner'].create({'name': 'test unlink'})
self.message_2.attributes = [{
'name': 'moderator_partner_id',
'type': 'many2one',
'comodel': 'res.partner',
'default': [partner.id, 'Bob'],
'definition_changed': True,
}]
self.assertEqual(
self.message_2.read(['attributes'])[0]['attributes'],
[{
'name': 'moderator_partner_id',
'type': 'many2one',
'comodel': 'res.partner',
'default': (partner.id, partner.display_name),
'value': False,
}],
)
partner.unlink()
self.assertEqual(
self.message_2.read(['attributes'])[0]['attributes'],
[{
'name': 'moderator_partner_id',
'type': 'many2one',
'comodel': 'res.partner',
'default': False,
'value': False,
}],
)
def test_properties_field_many2one_model_removed(self):
"""Test the case where we uninstall a module, and the model does not exist anymore."""
# simulate a module uninstall, the model is not available now
# when reading the model / many2one, it should return False
self.message_1.attributes = [{
'name': 'message',
'value': self.message_3.id,
}]
self.env.flush_all()
self.env.cr.execute(
"""
UPDATE test_new_api_discussion
SET attributes_definition = '[{"name": "message", "comodel": "wrong_model", "type": "many2one"}]'
WHERE id = %s
""", (self.discussion_1.id, ),
)
self.env.invalidate_all()
values = self.discussion_1.read(['attributes_definition'])[0]
self.assertFalse(values['attributes_definition'][0]['comodel'])
attributes_definition = self.discussion_1.attributes_definition
self.assertEqual(
attributes_definition,
[{'name': 'message', 'comodel': False, 'type': 'many2one'}],
msg='The model does not exist anymore, it should return false',
)
# read the many2one on the child, should return False as well
self.assertFalse(self.message_1.attributes[0]['value'])
values = self.message_1.read(['attributes'])[0]['attributes']
self.assertEqual(values[0]['type'], 'many2one', msg='Property type should be preserved')
self.assertFalse(values[0]['value'])
self.assertFalse(values[0]['comodel'])
sql_definition = self._get_sql_definition(self.discussion_1)
self.assertEqual(
sql_definition,
[{'name': 'message', 'comodel': 'wrong_model', 'type': 'many2one'}],
msg='Do not clean the definition until we write on the field'
)
# write on the properties definition must clean the wrong model name
self.discussion_1.attributes_definition = self.discussion_1.attributes_definition
sql_definition = self._get_sql_definition(self.discussion_1)
self.assertEqual(
sql_definition,
[{'name': 'message', 'comodel': False, 'type': 'many2one'}],
msg='Should have cleaned the model key',
)
def test_properties_field_domain(self):
self.discussion_1.attributes_definition = [{
'name': 'message',
'comodel': 'test_new_api.message',
'type': 'many2one',
'domain': "[('name', 'ilike', 'message')]",
}]
domain = self.message_1.attributes[0]['domain']
self.assertEqual(domain, "[('name', 'ilike', 'message')]")
# set a wrong domain, it can happen if we uninstall a module
# and if a field defined in this module was used in the domain
self.env.flush_all()
new_properties = json.dumps([{
'name': 'message',
'comodel': 'test_new_api.message',
'type': 'many2one',
'domain': "[('wrong_field', 'ilike', 'test')]",
}])
self.env.cr.execute(
"""
UPDATE test_new_api_discussion
SET attributes_definition = %s
WHERE id = %s
""", (new_properties, self.discussion_1.id, ),
)
self.env.flush_all()
self.env.invalidate_all()
definition = self.discussion_1.read(['attributes_definition'])[0]['attributes_definition']
self.assertNotIn('domain', definition)
properties = self.message_1.read(['attributes'])[0]['attributes']
self.assertNotIn('domain', properties)
sql_definition = self._get_sql_definition(self.discussion_1)
self.assertIn(
'domain',
sql_definition[0],
msg='The domain should remain in database until we write on the properties definition',
)
def test_properties_field_integer_float_boolean(self):
self.discussion_1.attributes_definition = [
{
'name': 'int_value',
'string': 'Int Value',
'type': 'integer',
}, {
'name': 'float_value',
'string': 'Float Value',
'type': 'float',
}, {
'name': 'boolean_value',
'string': 'Boolean Value',
'type': 'boolean',
},
]
self.message_1.attributes = [{
'name': 'int_value',
'value': 55555555555,
}, {
'name': 'float_value',
'value': 1.337,
}, {
'name': 'boolean_value',
'value': 77777, # should be converted into True
}]
self.env.invalidate_all()
self.assertEqual(len(self.message_1.attributes), 3)
self.assertEqual(self.message_1.attributes[0]['value'], 55555555555)
self.assertEqual(self.message_1.attributes[1]['value'], 1.337)
self.assertEqual(self.message_1.attributes[2]['value'], True)
self.message_1.attributes = [{'name': 'boolean_value', 'value': 0}]
self.assertEqual(
self.message_1.attributes[2]['value'], False,
msg='Boolean value must have been converted to False')
# When the user sets the value 0 for the property fields of type integer
# and float, the system should store the value 0 and shouldn't transform
# 0 to False (-> unset value).
self.message_1.attributes = {'int_value': 0, 'float_value': 0}
self.assertEqual(len(self.message_1.attributes), 3)
self.assertEqual(self.message_1.attributes[0]['value'], 0)
self.assertEqual(self.message_1.attributes[1]['value'], 0)
self.assertEqual(self.message_1.attributes[2]['value'], False)
self.assertTrue(isinstance(self.message_1.attributes[0]['value'], int))
self.assertTrue(isinstance(self.message_1.attributes[1]['value'], int))
self.assertTrue(isinstance(self.message_1.attributes[2]['value'], bool))
self.assertEqual(self._get_sql_properties(self.message_1), {'int_value': 0, 'float_value': 0, 'boolean_value': False})
def test_properties_field_integer_float_falsy_value_edge_cases(self):
self.discussion_1.attributes_definition = [
{
'name': 'int_value',
'string': 'Int Value',
'type': 'integer',
'default': 42
}, {
'name': 'float_value',
'string': 'Float Value',
'type': 'float',
'default': 0.42
}
]
message_1 = self.env['test_new_api.message'].create({
'discussion': self.discussion_1.id,
'author': self.user.id,
'attributes': {'int_value': 0, 'float_value': 0}
})
# When the user sets the value 0 for the property fields of type integer
# and float, the system shouldn't consider 0 as a falsy value and fallback
# to the default value.
self.assertEqual(len(message_1.attributes), 2)
self.assertEqual(message_1.attributes[0]['value'], 0)
self.assertEqual(message_1.attributes[1]['value'], 0)
self.assertTrue(isinstance(message_1.attributes[0]['value'], int))
self.assertTrue(isinstance(message_1.attributes[1]['value'], int))
self.assertEqual(self._get_sql_properties(message_1), {'int_value': 0, 'float_value': 0})
def test_properties_field_selection(self):
self.message_3.attributes = [{'name': 'state', 'value': 'done'}]
self.env.invalidate_all()
self.assertEqual(self.message_3.attributes[0]['value'], 'done')
# the option might have been removed on the definition, write False
self.message_3.attributes = [{'name': 'state', 'value': 'unknown_selection'}]
self.env.invalidate_all()
self.assertFalse(self.message_3.attributes[0]['value'])
with self.assertRaises(ValueError):
# check that 2 options can not have the same id
self.discussion_1.attributes_definition = [
{
'name': 'option',
'type': 'selection',
'selection': [['a', 'A'], ['b', 'B'], ['a', 'C']],
}
]
def test_properties_field_tags(self):
"""Test the behavior of the tag property.
The tags properties is basically the same as the selection property,
but you can select multiple values. It should work like the selection
(if we remove a value on the definition record, it should remove the value on each
child the next time we read, etc).
Each tags has a color index defined on the definition record.
"""
self.discussion_1.attributes_definition = [
{
'name': 'my_tags',
'string': 'My Tags',
'type': 'tags',
'tags': [
('be', 'BE', 1),
('fr', 'FR', 2),
('de', 'DE', 3),
('it', 'IT', 1),
],
'default': ['be', 'de'],
},
]
message = self.env['test_new_api.message'].create(
{'discussion': self.discussion_1.id, 'author': self.user.id})
self.assertEqual(message.attributes[0]['value'], ['be', 'de'])
self.assertEqual(self._get_sql_properties(message), {'my_tags': ['be', 'de']})
self.env.invalidate_all()
# remove the DE tags on the definition
self.discussion_1.attributes_definition = [
{
'name': 'my_tags',
'string': 'My Tags',
'type': 'tags',
'tags': [
('be', 'BE', 1),
('fr', 'FR', 2),
('it', 'IT', 1),
],
'default': ['be', 'de'],
},
]
# the value must remain in the database until the next write on the child
self.assertEqual(self._get_sql_properties(message), {'my_tags': ['be', 'de']})
attributes = message.read(['attributes'])[0]['attributes']
self.assertEqual(
attributes[0]['value'],
['be'],
msg='The tag has been removed on the definition, should be removed when reading the child')
self.assertEqual(
message.attributes[0]['tags'],
[['be', 'BE', 1], ['fr', 'FR', 2], ['it', 'IT', 1]])
# next write on the child must update the value
message.attributes = message.read(['attributes'])[0]['attributes']
self.assertEqual(self._get_sql_properties(message), {'my_tags': ['be']})
with self.assertRaises(ValueError):
# it should detect that the tag is duplicated
self.discussion_1.attributes_definition = [
{
'name': 'my_tags',
'type': 'tags',
'tags': [
('be', 'BE', 1),
('be', 'FR', 2),
],
},
]
@mute_logger('odoo.models.unlink', 'odoo.fields')
def test_properties_field_many2many_basic(self):
"""Test the basic operation on a many2many properties (read, write...).
Check also that if we remove some record,
those are filtered when we read the child.
"""
partners = self.env['test_new_api.partner'].create([
{'name': f'Partner {i}'}
for i in range(20)
])
self.discussion_1.attributes_definition = [{
'name': 'moderator_partner_ids',
'string': 'Partners',
'type': 'many2many',
'comodel': 'test_new_api.partner',
}]
with self.assertQueryCount(5):
self.message_1.attributes = [
{
"name": "moderator_partner_ids",
"string": "Partners",
"type": "many2many",
"comodel": "test_new_api.partner",
"value": partners[:10].name_get(),
}
]
attributes = self.message_1.read(['attributes'], load=None)[0]['attributes']
self.assertEqual(attributes[0]['value'], partners[:10].ids)
partners[:5].unlink()
with self.assertQueryCount(4):
attributes = self.message_1.read(['attributes'], load=None)[0]['attributes']
self.assertEqual(attributes[0]['value'], partners[5:10].ids)
partners[5].unlink()
with self.assertQueryCount(5):
properties = self.message_1.read(['attributes'])[0]['attributes']
self.assertEqual(properties[0]['value'], partners[6:10].name_get())
# need to wait next write to clean data in database
# a single read won't clean the removed many2many
attributes = self.message_1.read(['attributes'])[0]['attributes']
self.message_1.invalidate_recordset()
self.message_1.attributes = attributes
sql_values = self._get_sql_properties(self.message_1)
self.assertEqual(sql_values, {'moderator_partner_ids': partners[6:10].ids})
# read and disable name_get
properties = self.message_1.read(['attributes'], load=None)[0]['attributes']
self.assertEqual(
properties[0]['value'],
partners[6:10].ids,
msg='Should not return the partners name',
)
# Check that duplicated ids are removed
self.env.flush_all()
moderator_partner_ids = partners[6:10].ids
moderator_partner_ids += moderator_partner_ids[2:]
new_value = json.dumps({"moderator_partner_ids": moderator_partner_ids})
self.env.cr.execute(
"""
UPDATE test_new_api_message
SET attributes = %s
WHERE id = %s
""", (new_value, self.message_1.id, ),
)
self.env.invalidate_all()
properties = self.message_1.read(['attributes'], load=None)[0]['attributes']
self.assertEqual(
properties[0]['value'],
partners[6:10].ids,
msg='Should removed duplicated ids',
)
# write a list with many2many values
self.message_1.attributes = [{
'name': 'partner_ids',
'string': 'Partners',
'type': 'many2many',
'comodel': 'test_new_api.partner',
'default': [(partners[8].id, 'Alice')],
'value': [(partners[9].id, 'Bob')],
'definition_changed': True,
}]
sql_properties = self._get_sql_properties(self.message_1)
self.assertEqual(sql_properties, {'partner_ids': [partners[9].id]})
sql_definition = self._get_sql_definition(self.discussion_1)
self.assertEqual(sql_definition, [{
'name': 'partner_ids',
'string': 'Partners',
'type': 'many2many',
'comodel': 'test_new_api.partner',
'default': [partners[8].id],
}])
properties = self.message_1.read(['attributes'])[0]['attributes']
self.assertEqual(
properties,
[{
'name': 'partner_ids',
'string': 'Partners',
'type': 'many2many',
'comodel': 'test_new_api.partner',
'default': [(partners[8].id, partners[8].display_name)],
'value': [(partners[9].id, partners[9].display_name)],
}])
@users('test')
@mute_logger('odoo.addons.base.models.ir_rule', 'odoo.fields')
def test_properties_field_many2many_filtering(self):
# a user read a properties with a many2many and he doesn't have access to all records
tags = self.env['test_new_api.multi.tag'].create(
[{'name': f'Test Tag {i}'} for i in range(10)])
message = self.env['test_new_api.message'].create({
'name': 'Test Message',
'discussion': self.discussion_1.id,
'author': self.user.id,
'attributes': [{
'name': 'my_tags',
'type': 'many2many',
'comodel': 'test_new_api.multi.tag',
'value': tags.ids,
'definition_changed': True,
}],
})
self.env['ir.rule'].sudo().create({
'name': 'test_rule_tags',
'model_id': self.env['ir.model']._get('test_new_api.multi.tag').id,
'domain_force': [('name', 'not in', tags[5:].mapped('name'))],
'perm_read': True,
'perm_create': True,
'perm_write': True,
})
self.env.invalidate_all()
values = message.read(['attributes'])[0]['attributes'][0]['value']
self.assertEqual(values, [(tag.id, None if i >= 5 else tag.name) for i, tag in enumerate(tags.sudo())])
def test_properties_field_performance(self):
self.env.invalidate_all()
with self.assertQueryCount(5):
# read to put the partner name in cache
self.message_1.read(['attributes'])
with self.assertQueryCount(0, msg='Must read value from cache'):
self.message_1.attributes
expected = ['UPDATE "test_new_api_message" SET "attributes" = %s, "write_date" = %s, "write_uid" = %s WHERE id IN %s']
with self.assertQueryCount(1), self.assertQueries(expected):
self.message_1.attributes = [
{
"name": "discussion_color_code",
"type": "char",
"string": "Color Code",
"default": "blue",
"value": "red"
},
{
"name": "moderator_partner_id",
"type": "many2one",
"string": "Partner",
"comodel": "test_new_api.partner",
"value": None
},
]
self.message_1.flush_recordset()
last_message_id = self.env['test_new_api.message'].search([], order="id DESC", limit=1).id
# based on batch optimization, _read_format should not crash on non existing records
values = self.env['test_new_api.message'].browse((self.message_1.id, last_message_id + 1))._read_format(['attributes'])
self.assertEqual(len(values), 1)
self.assertEqual(values[0]['id'], self.message_1.id)
def test_properties_field_change_definition(self):
"""Test the behavior of the field when changing the definition."""
attributes_definition = self.discussion_1.attributes_definition
self.message_1.attributes = [
{
"name": "discussion_color_code",
"value": None,
},
{
"name": "moderator_partner_id",
"value": None,
}
]
self.env.invalidate_all()
self.assertFalse(self.message_1.attributes[0]['value'])
# add a property on the definition record
attributes_definition += [{'name': 'state', 'string': 'State', 'type': 'char'}]
self.discussion_1.attributes_definition = attributes_definition
self.message_1.attributes = [{'name': 'state', 'value': 'ready'}]
self.env.invalidate_all()
self.assertEqual(self.message_1.attributes[2]['value'], 'ready')
# remove a property from the definition
# the properties on the child should remain, until we write on it
# when reading, the removed property must be filtered
self.discussion_1.attributes_definition = attributes_definition[:-1] # remove the state field
self.assertFalse(self.message_1.attributes[0]['value'])
value = self._get_sql_properties(self.message_1)
self.assertEqual(value.get('state'), 'ready', msg='The field should be in database')
self.message_1.attributes = [{'name': 'name', 'value': 'Test name'}]
value = self._get_sql_properties(self.message_1)
self.assertFalse(
value.get('state'),
msg='After updating an other property, the value must be cleaned')
# check that we can only set a allowed list of properties type
with self.assertRaises(ValueError):
self.discussion_1.attributes_definition = [{'name': 'state', 'type': 'wrong_type'}]
# check the property ID unicity
with self.assertRaises(ValueError):
self.discussion_1.attributes_definition = [
{'name': 'state', 'type': 'char'},
{'name': 'state', 'type': 'datetime'},
]
@mute_logger('odoo.fields')
def test_properties_field_onchange(self):
"""If we change the definition record, the onchange of the properties field must be triggered."""
message_form = Form(self.env['test_new_api.message'])
with self.assertQueryCount(8):
message_form.discussion = self.discussion_1
message_form.author = self.user
self.assertEqual(
message_form.attributes,
[{
'name': 'discussion_color_code',
'string': 'Color Code',
'type': 'char',
'default': 'blue',
'value': 'blue',
}, {
'name': 'moderator_partner_id',
'string': 'Partner',
'type': 'many2one',
'comodel': 'test_new_api.partner',
'value': False,
}],
msg='Should take the new definition when changing the definition record',
)
# change the discussion field
message_form.discussion = self.discussion_2
properties = message_form.attributes
self.assertEqual(len(properties), 1)
self.assertEqual(
properties[0]['name'],
'state',
msg='Should take the values of the new definition record',
)
with self.assertQueryCount(6):
message = message_form.save()
self.assertEqual(
message.attributes[0]['value'],
'draft',
msg='Should take the default value',
)
# check cached value
cached_value = self.env.cache.get(message, message._fields['attributes'])
self.assertEqual(cached_value, {'state': 'draft'})
# change the definition record, change the definition and add default values
self.assertEqual(message.discussion, self.discussion_2)
with self.assertQueryCount(4):
message.discussion = self.discussion_1
self.assertEqual(
self.discussion_1.attributes_definition,
[{
'name': 'discussion_color_code',
'type': 'char',
'string': 'Color Code',
'default': 'blue',
}, {
'name': 'moderator_partner_id',
'type': 'many2one',
'string': 'Partner',
'comodel': 'test_new_api.partner',
}],
)
self.assertEqual(
message.attributes,
[{
'name': 'discussion_color_code',
'type': 'char',
'string': 'Color Code',
'default': 'blue',
'value': 'blue',
}, {
'name': 'moderator_partner_id',
'type': 'many2one',
'string': 'Partner',
'comodel': 'test_new_api.partner',
'value': False,
}],
)
self.discussion_1.attributes_definition = False
self.discussion_2.attributes_definition = [{
'name': 'test',
'type': 'char',
'default': 'Default',
}]
# change the message discussion to remove the properties
# discussion 1 -> discussion 2
message.discussion = self.discussion_2
message.attributes = [{'name': 'test', 'value': 'Test'}]
onchange_values = message.onchange(
values={
'discussion': self.discussion_1.id,
'attributes': [{
'name': 'test',
'type': 'char',
'default': 'Default',
'value': 'Test',
}],
},
field_name=['discussion'],
field_onchange={'attributes': '1'},
)
self.assertTrue(
'attributes' in onchange_values['value'],
msg='Should have detected the definition record change')
self.assertEqual(
onchange_values['value']['attributes'], [],
msg='Should have reset the properties definition')
# change the message discussion to add new properties
# discussion 2 -> discussion 1
message.discussion = self.discussion_1
onchange_values = message.onchange(
values={
'discussion': self.discussion_2.id,
'attributes': [],
},
field_name=['discussion'],
field_onchange={'attributes': '1'},
)
self.assertTrue(
'attributes' in onchange_values['value'],
msg='Should have detected the definition record change')
self.assertEqual(
onchange_values['value']['attributes'],
[{'name': 'test', 'type': 'char', 'default': 'Default', 'value': 'Default'}],
msg='Should have reset the properties definition to the discussion 1 definition')
# change the definition record and the definition at the same time
message_form = Form(message)
message_form.discussion = self.discussion_2
message_form.attributes = [{
'name': 'new_property',
'type': 'char',
'value': 'test value',
'definition_changed': True,
}]
message = message_form.save()
self.assertEqual(
self.discussion_2.attributes_definition,
[{'name': 'new_property', 'type': 'char'}])
self.assertEqual(
message.attributes,
[{'name': 'new_property', 'type': 'char', 'value': 'test value'}])
# re-write the same parent again and check that value are not reset
message.discussion = message.discussion
self.assertEqual(
message.attributes,
[{'name': 'new_property', 'type': 'char', 'value': 'test value'}])
# trigger a other onchange after setting the properties
# and check that it does not impact the properties
message.discussion.attributes_definition = []
message_form = Form(message)
message.attributes = [{
'name': 'new_property',
'type': 'char',
'value': 'test value',
'definition_changed': True,
}]
message_form.body = "a" * 42
message = message_form.save()
self.assertEqual(
message.attributes,
[{'name': 'new_property', 'type': 'char', 'value': 'test value'}])
@mute_logger('odoo.fields')
def test_properties_field_definition_update(self):
"""Test the definition update from the child."""
self.discussion_1.attributes_definition = []
self.message_1.attributes = [
{
'name': 'my_many2one',
'string': 'Partner',
'comodel': 'test_new_api.partner',
'type': 'many2one',
# send the value like the web client does
'default': [self.partner.id, 'Bob'],
'value': [self.partner_2.id, "Test"],
}, {
'name': 'my_many2many',
'string': 'Partner',
'comodel': 'test_new_api.partner',
'type': 'many2many',
# send the value like the web client does
'default': [[self.partner.id, 'Bob'], [self.partner_2.id, 'Test']],
'value': [[self.partner_2.id, "Test"]],
'definition_changed': True,
},
]
self.env.invalidate_all()
sql_definition = self._get_sql_definition(self.discussion_1)
expected_definition = [
{
'name': 'my_many2one',
'string': 'Partner',
'comodel': 'test_new_api.partner',
'type': 'many2one',
'default': self.partner.id,
}, {
'name': 'my_many2many',
'string': 'Partner',
'comodel': 'test_new_api.partner',
'type': 'many2many',
'default': [self.partner.id, self.partner_2.id],
},
]
self.assertEqual(sql_definition, expected_definition)
sql_properties = self._get_sql_properties(self.message_1)
expected_properties = {
'my_many2one': self.partner_2.id,
'my_many2many': [self.partner_2.id],
}
self.assertEqual(expected_properties, sql_properties)
@mute_logger('odoo.fields')
@users('test')
def test_properties_field_security(self):
"""Check the access right related to the Properties fields."""
def _mocked_check_access_rights(records, operation, raise_exception=True):
if records.env.su: # called with SUDO
return True
if raise_exception:
raise AccessError('')
return False
message = self.message_1.with_user(self.test_user)
# a user read a properties with a many2one to a record he doesn't have access to
tag = self.env['test_new_api.multi.tag'].create({'name': 'Test Tag'})
message.attributes = [{
'name': 'test',
'type': 'many2one',
'comodel': 'test_new_api.multi.tag',
'value': [tag.id, 'Tag'],
'definition_changed': True,
}]
values = message.read(['attributes'])[0]['attributes'][0]
self.assertEqual(values['value'], (tag.id, 'Test Tag'))
self.env.invalidate_all()
with patch('odoo.addons.test_new_api.models.test_new_api.MultiTag.check_access_rights', _mocked_check_access_rights):
values = message.read(['attributes'])[0]['attributes'][0]
self.assertEqual(values['value'], (tag.id, None))
# a user read a properties with a many2one to a record
# but doesn't have access to its parent
self.env.invalidate_all()
with patch('odoo.addons.test_new_api.models.test_new_api.Discussion.check_access_rights', _mocked_check_access_rights):
values = message.read(['attributes'])[0]['attributes'][0]
self.assertEqual(values['value'], (tag.id, 'Test Tag'))
def _get_sql_properties(self, message):
self.env.flush_all()
self.env.cr.execute(
"""
SELECT attributes
FROM test_new_api_message
WHERE id = %s
""", (message.id, ),
)
value = self.env.cr.fetchone()
self.assertTrue(value and value[0])
return value[0]
def _get_sql_definition(self, discussion):
self.env.flush_all()
self.env.cr.execute(
"""
SELECT attributes_definition
FROM test_new_api_discussion
WHERE id = %s
""", (discussion.id, ),
)
value = self.env.cr.fetchone()
self.assertTrue(value and value[0])
return value[0]
class PropertiesSearchCase(PropertiesCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.messages = cls.message_1 | cls.message_2 | cls.message_3
cls.env['test_new_api.message'].search([('id', 'not in', cls.messages.ids)]).unlink()
@mute_logger('odoo.fields')
def test_properties_field_search_boolean(self):
# search on boolean
self.message_1.attributes = [{
'name': 'myboolean',
'type': 'boolean',
'value': True,
'definition_changed': True,
}]
self.message_2.attributes = {'myboolean': False}
messages = self.env['test_new_api.message'].search([('attributes.myboolean', '=', True)])
self.assertEqual(messages, self.message_1)
messages = self.env['test_new_api.message'].search([('attributes.myboolean', '!=', False)])
self.assertEqual(messages, self.message_1)
messages = self.env['test_new_api.message'].search([('attributes.myboolean', '=', False)])
# message 2 has a falsy boolean properties
# message 3 doesn't have the properties (key in dict doesn't exist)
self.assertEqual(messages, self.message_2 | self.message_3)
messages = self.env['test_new_api.message'].search([('attributes.myboolean', '!=', True)])
self.assertEqual(messages, self.message_2 | self.message_3)
@mute_logger('odoo.fields')
def test_properties_field_search_char(self):
# search on text properties
self.message_1.attributes = [{
'name': 'mychar',
'type': 'char',
'value': 'Test',
'definition_changed': True,
}]
self.message_2.attributes = {'mychar': 'TeSt'}
messages = self.env['test_new_api.message'].search([('attributes.mychar', '=', 'Test')])
self.assertEqual(messages, self.message_1, "Should be able to search on a properties field")
messages = self.env['test_new_api.message'].search([('attributes.mychar', '=', '"Test"')])
self.assertFalse(messages)
messages = self.env['test_new_api.message'].search([('attributes.mychar', 'ilike', 'test')])
self.assertEqual(messages, self.message_1 | self.message_2)
messages = self.env['test_new_api.message'].search([('attributes.mychar', 'not ilike', 'test')])
self.assertFalse(messages)
messages = self.env['test_new_api.message'].search([('attributes.mychar', 'ilike', '"test"')])
self.assertFalse(messages)
for forbidden_char in '! ()"\'.':
searches = (
f'mychar{forbidden_char}',
f'my{forbidden_char}char',
f'{forbidden_char}mychar',
)
for search in searches:
with self.assertRaises(ValueError), self.assertQueryCount(0):
self.env['test_new_api.message'].search([(f'attributes.{search}', '=', 'Test')])
# search falsy properties
self.message_3.discussion = self.message_2.discussion
self.message_3.attributes = [{'name': 'mychar', 'value': False}]
self.assertEqual(self._get_sql_properties(self.message_3), {'mychar': False})
messages = self.env['test_new_api.message'].search([('attributes.mychar', '=', False)])
self.assertEqual(messages, self.message_3)
# search falsy properties when the key doesn't exist in the dict
# message 2 properties is False, message 3 properties doesn't exist in database
self.message_2.attributes = [{'name': 'mychar', 'value': False}]
self.env.cr.execute(
"UPDATE test_new_api_message SET attributes = '{}' WHERE id = %s",
[self.message_3.id],
)
messages = self.env['test_new_api.message'].search([('attributes.mychar', '=', False)])
self.assertEqual(messages, self.message_2 | self.message_3)
messages = self.env['test_new_api.message'].search([('attributes.mychar', '!=', False)])
self.assertEqual(messages, self.message_1)
# message 1 property contain a string but is not falsy so it's not returned
messages = self.env['test_new_api.message'].search([('attributes.mychar', '!=', True)])
self.assertEqual(messages, self.message_2 | self.message_3)
messages = self.env['test_new_api.message'].search([('attributes.mychar', '=', True)])
self.assertEqual(messages, self.message_1)
# message 3 is now null instead of being an empty dict
self.env.cr.execute(
"UPDATE test_new_api_message SET attributes = NULL WHERE id = %s",
[self.message_3.id],
)
messages = self.env['test_new_api.message'].search([('attributes.mychar', '=', False)])
self.assertEqual(messages, self.message_2 | self.message_3)
messages = self.env['test_new_api.message'].search([('attributes.mychar', '!=', False)])
self.assertEqual(messages, self.message_1)
@mute_logger('odoo.fields')
def test_properties_field_search_float(self):
# search on float
self.message_1.attributes = [{
'name': 'myfloat',
'type': 'float',
'value': 3.14,
'definition_changed': True,
}]
self.message_2.attributes = {'myfloat': 5.55}
messages = self.env['test_new_api.message'].search([('attributes.myfloat', '>', 4.4)])
self.assertEqual(messages, self.message_2)
messages = self.env['test_new_api.message'].search([('attributes.myfloat', '<', 4.4)])
self.assertEqual(messages, self.message_1)
messages = self.env['test_new_api.message'].search([('attributes.myfloat', '>', 1.1)])
self.assertEqual(messages, self.message_1 | self.message_2)
messages = self.env['test_new_api.message'].search([('attributes.myfloat', '<=', 1.1)])
self.assertFalse(messages)
messages = self.env['test_new_api.message'].search([('attributes.myfloat', '=', 3.14)])
self.assertEqual(messages, self.message_1)
@mute_logger('odoo.fields')
def test_properties_field_search_integer(self):
# search on integer
self.messages.discussion = self.discussion_1
self.message_1.attributes = [{
'name': 'myint',
'type': 'integer',
'value': 33,
'definition_changed': True,
}]
self.message_2.attributes = {'myint': 111}
self.message_3.attributes = {'myint': -2}
messages = self.env['test_new_api.message'].search([('attributes.myint', '>', 4)])
self.assertEqual(messages, self.message_1 | self.message_2)
messages = self.env['test_new_api.message'].search([('attributes.myint', '<', 4)])
self.assertEqual(messages, self.message_3)
messages = self.env['test_new_api.message'].search([('attributes.myint', '=', 111)])
self.assertEqual(messages, self.message_2)
# search on the JSONified value (operator "->>")
messages = self.env['test_new_api.message'].search([('attributes.myint', 'ilike', '1')])
self.assertEqual(messages, self.message_2)
messages = self.env['test_new_api.message'].search([('attributes.myint', 'not ilike', '1')])
self.assertEqual(messages, self.message_1 | self.message_3)
@mute_logger('odoo.fields')
def test_properties_field_search_many2many(self):
self.messages.discussion = self.discussion_1
partners = self.env['res.partner'].create([{'name': 'A'}, {'name': 'B'}, {'name': 'C'}])
self.message_1.attributes = [{
'name': 'mymany2many',
'type': 'many2many',
'comodel': 'res.partner',
'value': partners.ids,
'definition_changed': True,
}]
self.message_2.attributes = {'mymany2many': [partners[1].id]}
self.message_3.attributes = {'mymany2many': [partners[2].id]}
messages = self.env['test_new_api.message'].search(
[('attributes.mymany2many', 'in', partners[0].id)])
self.assertEqual(messages, self.message_1)
messages = self.env['test_new_api.message'].search(
[('attributes.mymany2many', 'in', partners[1].id)])
self.assertEqual(messages, self.message_1 | self.message_2)
messages = self.env['test_new_api.message'].search(
[('attributes.mymany2many', 'in', partners[2].id)])
self.assertEqual(messages, self.message_1 | self.message_3)
messages = self.env['test_new_api.message'].search(
[('attributes.mymany2many', 'not in', partners[0].id)])
self.assertEqual(messages, self.message_2 | self.message_3)
# IN operator (not supported on many2many and return weird results)
messages = self.env['test_new_api.message'].search(
[('attributes.mymany2many', 'in', [partners[0].id, partners[1].id])])
self.assertEqual(messages, self.message_2) # should be self.message_1 | self.message_2
@mute_logger('odoo.fields')
def test_properties_field_search_many2one(self):
# many2one are just like integer
self.messages.discussion = self.discussion_1
self.message_1.attributes = [{
'name': 'mypartner',
'type': 'integer',
'value': self.partner.id,
'definition_changed': True,
}]
self.message_2.attributes = {'mypartner': self.partner_2.id}
self.message_3.attributes = {'mypartner': False}
messages = self.env['test_new_api.message'].search(
[('attributes.mypartner', 'in', [self.partner.id, self.partner_2.id])])
self.assertEqual(messages, self.message_1 | self.message_2)
messages = self.env['test_new_api.message'].search(
[('attributes.mypartner', 'not in', [self.partner.id, self.partner_2.id])])
self.assertEqual(messages, self.message_3)
messages = self.env['test_new_api.message'].search(
[('attributes.mypartner', 'ilike', self.partner.display_name)])
self.assertFalse(messages, "The ilike on relational properties is not supported")
@mute_logger('odoo.fields')
def test_properties_field_search_tags(self):
self.messages.discussion = self.discussion_1
self.message_1.attributes = [{
'name': 'mytags',
'type': 'tags',
'value': ['a', 'b'],
'tags': [['a', 'A', 1], ['b', 'B', 2], ['aa', 'AA', 3]],
'definition_changed': True,
}]
self.message_2.attributes = {'mytags': ['b']}
self.message_3.attributes = {'mytags': ['aa']}
messages = self.env['test_new_api.message'].search([('attributes.mytags', 'in', 'a')])
self.assertEqual(messages, self.message_1)
# the search is done on the JSONified value (operator "->>")
messages = self.env['test_new_api.message'].search([('attributes.mytags', 'ilike', 'a')])
self.assertEqual(messages, self.message_1 | self.message_3)
messages = self.env['test_new_api.message'].search([('attributes.mytags', 'not ilike', 'a')])
self.assertEqual(messages, self.message_2)
messages = self.env['test_new_api.message'].search([('attributes.mytags', 'in', 'b')])
self.assertEqual(messages, self.message_1 | self.message_2)
messages = self.env['test_new_api.message'].search([('attributes.mytags', 'in', 'aa')])
self.assertEqual(messages, self.message_3)
messages = self.env['test_new_api.message'].search([('attributes.mytags', 'not in', 'b')])
self.assertEqual(messages, self.message_3)
# the search is done on the JSONified value (operator "->>")
messages = self.env['test_new_api.message'].search([('attributes.mytags', 'ilike', '["aa"]')])
self.assertEqual(messages, self.message_3)
# IN operator on array
messages = self.env['test_new_api.message'].search([('attributes.mytags', 'in', [])])
self.assertFalse(messages)
messages = self.env['test_new_api.message'].search([('attributes.mytags', 'not in', [])])
self.assertEqual(messages, self.message_1 | self.message_2 | self.message_3)
messages = self.env['test_new_api.message'].search([('attributes.mytags', 'in', ['a', 'b'])])
self.assertEqual(messages, self.message_1 | self.message_2)
messages = self.env['test_new_api.message'].search([('attributes.mytags', 'in', ['b', 'a'])])
self.assertEqual(messages, self.message_1 | self.message_2)
messages = self.env['test_new_api.message'].search([('attributes.mytags', 'in', ['aa'])])
self.assertEqual(messages, self.message_3)
messages = self.env['test_new_api.message'].search([('attributes.mytags', 'in', ['aa', 'b'])])
self.assertEqual(messages, self.message_3 | self.message_2)
messages = self.env['test_new_api.message'].search([('attributes.mytags', 'not in', ['a', 'b'])])
self.assertEqual(messages, self.message_3)
@mute_logger('odoo.fields')
def test_properties_field_search_unaccent(self):
if not self.registry.has_unaccent:
# To enable unaccent feature:
# CREATE EXTENSION unaccent;
raise unittest.SkipTest("unaccent not enabled")
Model = self.env['test_new_api.message']
(self.message_1 | self.message_2).discussion = self.discussion_1
# search on text properties
self.message_1.attributes = [{
'name': 'mychar',
'type': 'char',
'value': 'Hélène',
'definition_changed': True,
}]
self.message_2.attributes = {'mychar': 'Helene'}
result = Model.search([('attributes.mychar', 'ilike', 'Helene')])
self.assertEqual(self.message_1 | self.message_2, result)
result = Model.search([('attributes.mychar', 'ilike', 'hélène')])
self.assertEqual(self.message_1 | self.message_2, result)
result = Model.search([('attributes.mychar', 'not ilike', 'Helene')])
self.assertNotIn(self.message_1, result)
self.assertNotIn(self.message_2, result)
result = Model.search([('attributes.mychar', 'not ilike', 'hélène')])
self.assertNotIn(self.message_1, result)
self.assertNotIn(self.message_2, result)
@mute_logger('odoo.fields')
def test_properties_field_search_orderby_string(self):
"""Test that we can order record by properties string values."""
(self.message_1 | self.message_2 | self.message_3).discussion = self.discussion_1
self.message_1.attributes = [{
'name': 'mychar',
'type': 'char',
'value': 'BB',
'definition_changed': True,
}]
self.message_2.attributes = {'mychar': 'AA'}
self.message_3.attributes = {'mychar': 'CC'}
self.env.flush_all()
result = self.env['test_new_api.message'].search(
domain=[['attributes.mychar', '!=', False]],
order='attributes.mychar ASC')
self.assertEqual(result[0], self.message_2)
self.assertEqual(result[1], self.message_1)
self.assertEqual(result[2], self.message_3)
result = self.env['test_new_api.message'].search(
domain=[['attributes.mychar', '!=', False]],
order='attributes.mychar DESC')
self.assertEqual(result[0], self.message_3)
self.assertEqual(result[1], self.message_1)
self.assertEqual(result[2], self.message_2)
@mute_logger('odoo.fields')
def test_properties_field_search_orderby_integer(self):
"""Test that we can order record by properties integer values."""
(self.message_1 | self.message_2 | self.message_3).discussion = self.discussion_1
self.message_1.attributes = [{
'name': 'myinteger',
'type': 'integer',
'value': 22,
'definition_changed': True,
}]
self.message_2.attributes = {'myinteger': 111}
self.message_3.attributes = {'myinteger': 33}
self.env.flush_all()
result = self.env['test_new_api.message'].search(
domain=[['attributes.myinteger', '!=', False]],
order='attributes.myinteger ASC')
self.assertEqual(result[0], self.message_1)
self.assertEqual(result[1], self.message_3)
self.assertEqual(result[2], self.message_2)
result = self.env['test_new_api.message'].search(
domain=[['attributes.myinteger', '!=', False]],
order='attributes.myinteger DESC')
self.assertEqual(result[0], self.message_2)
self.assertEqual(result[1], self.message_3)
self.assertEqual(result[2], self.message_1)
@mute_logger('odoo.fields')
def test_properties_field_search_orderby_injection(self):
"""Check the restriction on the property name."""
self.message_1.attributes = [{
'name': 'myinteger',
'type': 'integer',
'value': 22,
'definition_changed': True,
}]
for c in '! ()"\'.':
orders = (
f'attributes.myinteger{c} ASC',
f'attributes.my{c}integer ASC',
f'attribut{c}es.myinteger ASC',
)
if c == ' ':
# allow multiple spaces after the property name
orders = orders[1:]
for order in orders:
with self.assertRaises(UserError), self.assertQueryCount(0):
self.env['test_new_api.message'].search(domain=[], order=order)