Files
odoo_source/addons/mail/models/ir_model.py
T
Alvaro Fuentes 6f6fb10511 [FIX] mail: allow to create a manual blacklist model
Issue 1: before this patch it was impossible to create a manual model
marked as "Is blacklist". The reason is that a blacklist model
implicitly need an `email` field, but such field is impossible to add in
a manual model: the field must start with `x_`.

Solution: append `x_` to the implicit email field. Note, in principle
the user gets an error if `x_email` is not present. Solved by adding the
field when creating the custom model. Ideally we should show some hint
in the interface to make it more user friendly. That is out of the scope
of this patch.

Issue 2: when we have a manual model that is mail blacklist it's
impossible to create its model class. We get an error because the MRO is
not correct. The reason is that we are adding `mail.thread.blacklist`
_after_ `mail.thread` in the `_inherit` list. That list is used to
generated the `__bases__` of the model class[1]. According to Python's
MRO rules[2], since `mail.thread.blacklist` appears after `mail.thread`
as parents of the custom model class this order _must_ be respected. But
`mail.thread` must appear _before_ `mail.thread.blacklist` because the
latter inherits from the former. This is a contradiction and the MRO
algorithm cannot succeed. To put it in a simple example:
```py
class A: pass
class B(A): pass
 # This fails:
 # class C(A, B): pass
 # The right order is:
class C(B, A): pass
 # Equivalent to:
class D(B): pass
 # C and D have the same MRO linearization excluding themselves
assert D.mro()[1:] == C.mro()[1:]
```
Example traceback:
```
Traceback (most recent call last):
  File "/home/odoo/src/odoo/14.0/odoo/service/server.py", line 1201, in preload_registries
    registry = Registry.new(dbname, update_module=update_module)
  File "/home/odoo/src/odoo/14.0/odoo/modules/registry.py", line 89, in new
    odoo.modules.load_modules(registry._db, force_demo, status, update_module)
  File "/home/odoo/src/odoo/14.0/odoo/modules/loading.py", line 464, in load_modules
    registry.setup_models(cr)
  File "/home/odoo/src/odoo/14.0/odoo/modules/registry.py", line 263, in setup_models
    env['ir.model']._add_manual_models()
  File "/home/odoo/src/odoo/14.0/odoo/addons/base/models/ir_model.py", line 430, in _add_manual_models
    Model = model_class._build_model(self.pool, cr)
  File "/home/odoo/src/odoo/14.0/odoo/models.py", line 585, in _build_model
    ModelClass.__bases__ = tuple(bases)
TypeError: Cannot create a consistent method resolution
order (MRO) for bases BaseModel, mail.thread, mail.thread.blacklist, base
```

Solution: check if a model inherits from `mail.thread.blacklist` first.
There is no need to add `mail.thread` if inheriting
`mail.thread.blacklist` because the inheritance is already implicit.

This issue was observed during upgrades. We convert custom models and
fields into manual to allow upgrading without custom code. This causes
issues because the MRO error appears when a custom model inherits mail
blacklist.

[1]: https://github.com/odoo/odoo/blob/02f820fb0eaddbb3a4269a0967184c8aaf52c363/odoo/models.py#L585
[2]: https://www.python.org/download/releases/2.3/mro/

closes odoo/odoo#120977

X-original-commit: 8848bb57ff7f086d801178c582d7f6371eeb8479
Signed-off-by: Christophe Simonis <chs@odoo.com>
Signed-off-by: Alvaro Fuentes Suarez (afu) <afu@odoo.com>
2023-05-10 21:18:03 +02:00

119 lines
5.2 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class IrModel(models.Model):
_inherit = 'ir.model'
_order = 'is_mail_thread DESC, name ASC'
is_mail_thread = fields.Boolean(
string="Has Mail Thread", default=False,
)
is_mail_activity = fields.Boolean(
string="Has Mail Activity", default=False,
)
is_mail_blacklist = fields.Boolean(
string="Has Mail Blacklist", default=False,
)
def unlink(self):
if not self:
return True
# Delete followers, messages and attachments for models that will be unlinked.
models = tuple(self.mapped('model'))
model_ids = tuple(self.ids)
query = "DELETE FROM mail_activity WHERE res_model_id IN %s"
self.env.cr.execute(query, [model_ids])
query = "DELETE FROM mail_activity_type WHERE res_model IN %s"
self.env.cr.execute(query, [models])
query = "DELETE FROM mail_followers WHERE res_model IN %s"
self.env.cr.execute(query, [models])
query = "DELETE FROM mail_message WHERE model in %s"
self.env.cr.execute(query, [models])
# Get files attached solely by the models
query = """
SELECT DISTINCT store_fname
FROM ir_attachment
WHERE res_model IN %s
EXCEPT
SELECT store_fname
FROM ir_attachment
WHERE res_model not IN %s;
"""
self.env.cr.execute(query, [models, models])
fnames = self.env.cr.fetchall()
query = """DELETE FROM ir_attachment WHERE res_model in %s"""
self.env.cr.execute(query, [models])
for (fname,) in fnames:
self.env['ir.attachment']._file_delete(fname)
return super(IrModel, self).unlink()
def write(self, vals):
if self and ('is_mail_thread' in vals or 'is_mail_activity' in vals or 'is_mail_blacklist' in vals):
if any(rec.state != 'manual' for rec in self):
raise UserError(_('Only custom models can be modified.'))
if 'is_mail_thread' in vals and any(rec.is_mail_thread > vals['is_mail_thread'] for rec in self):
raise UserError(_('Field "Mail Thread" cannot be changed to "False".'))
if 'is_mail_activity' in vals and any(rec.is_mail_activity > vals['is_mail_activity'] for rec in self):
raise UserError(_('Field "Mail Activity" cannot be changed to "False".'))
if 'is_mail_blacklist' in vals and any(rec.is_mail_blacklist > vals['is_mail_blacklist'] for rec in self):
raise UserError(_('Field "Mail Blacklist" cannot be changed to "False".'))
res = super(IrModel, self).write(vals)
self.env.flush_all()
# setup models; this reloads custom models in registry
self.pool.setup_models(self._cr)
# update database schema of models
models = self.pool.descendants(self.mapped('model'), '_inherits')
self.pool.init_models(self._cr, models, dict(self._context, update_custom_fields=True))
else:
res = super(IrModel, self).write(vals)
return res
def _reflect_model_params(self, model):
vals = super(IrModel, self)._reflect_model_params(model)
vals['is_mail_thread'] = issubclass(type(model), self.pool['mail.thread'])
vals['is_mail_activity'] = issubclass(type(model), self.pool['mail.activity.mixin'])
vals['is_mail_blacklist'] = issubclass(type(model), self.pool['mail.thread.blacklist'])
return vals
@api.model
def _instanciate(self, model_data):
model_class = super(IrModel, self)._instanciate(model_data)
if model_data.get('is_mail_blacklist') and model_class._name != 'mail.thread.blacklist':
parents = model_class._inherit or []
parents = [parents] if isinstance(parents, str) else parents
model_class._inherit = parents + ['mail.thread.blacklist']
if model_class._custom:
model_class._primary_email = 'x_email'
elif model_data.get('is_mail_thread') and model_class._name != 'mail.thread':
parents = model_class._inherit or []
parents = [parents] if isinstance(parents, str) else parents
model_class._inherit = parents + ['mail.thread']
if model_data.get('is_mail_activity') and model_class._name != 'mail.activity.mixin':
parents = model_class._inherit or []
parents = [parents] if isinstance(parents, str) else parents
model_class._inherit = parents + ['mail.activity.mixin']
return model_class
def _get_model_definitions(self, model_names_to_fetch):
fields_by_model_names = super()._get_model_definitions(model_names_to_fetch)
for model_name, field_by_fname in fields_by_model_names.items():
model = self.env[model_name]
tracked_field_names = model._track_get_fields() if 'mail.thread' in model._inherit else []
for fname, field in field_by_fname.items():
if fname in tracked_field_names:
field['tracking'] = True
return fields_by_model_names