odoo18/addons_extensions/knowledge/models/knowledge_article_member.py

98 lines
4.2 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, tools, _
from odoo.exceptions import AccessError, ValidationError
class ArticleMember(models.Model):
_name = 'knowledge.article.member'
_description = 'Article Member'
_rec_name = 'partner_id'
article_id = fields.Many2one(
'knowledge.article', 'Article',
ondelete='cascade', required=True)
partner_id = fields.Many2one(
'res.partner', 'Partner',
index=True, ondelete='cascade', required=True)
permission = fields.Selection(
[('write', 'Can edit'),
('read', 'Can read'),
('none', 'No access')],
required=True, default='read')
article_permission = fields.Selection(
related='article_id.inherited_permission',
readonly=True, store=True)
_sql_constraints = [
('unique_article_partner',
'unique(article_id, partner_id)',
'You already added this partner on this article.')
]
@api.constrains('article_permission', 'permission')
def _check_is_writable(self, on_unlink=False):
""" Articles must always have at least one writer. This constraint is done
on member level, in coordination to the constraint on article model (see
``_check_is_writable`` on ``knowledge.article``).
Since this constraint only triggers if we have at least one member another
validation is done on article model. The article_permission related field
has been added and stored to force triggering this constraint when
article.permission is modified.
Note: computation is done in Py instead of using optimized SQL queries
because value are not yet in DB at this point.
:param bool on_unlink: when called on unlink we must remove the members
in self (the ones that will be deleted) to check if one of the remaining
members has write access.
"""
if self.env.context.get('knowledge_member_skip_writable_check'):
return
articles_to_check = self.article_id.filtered(lambda a: a.inherited_permission != 'write')
if not articles_to_check:
return
if on_unlink:
deleted_members_by_article = dict.fromkeys(articles_to_check.ids, self.env['knowledge.article.member'])
for member in self.filtered(lambda member: member.article_id in articles_to_check):
deleted_members_by_article[member.article_id.id] |= member
for article in articles_to_check:
# Check on permission on members
members_to_check = article.article_member_ids
if on_unlink:
members_to_check -= deleted_members_by_article[article.id]
if any(m.permission == 'write' for m in members_to_check):
continue
members_to_exclude = deleted_members_by_article[article.id] if on_unlink else False
if not article._has_write_member(members_to_exclude=members_to_exclude):
raise ValidationError(
_("Article '%s' should always have a writer: inherit write permission, or have a member with write access",
article.display_name)
)
def write(self, vals):
""" Whatever rights, avoid any attempt at privilege escalation. """
if ('article_id' in vals or 'partner_id' in vals) and not self.env.is_admin():
raise AccessError(_("Can not update the article or partner of a member."))
return super().write(vals)
@api.ondelete(at_uninstall=False)
def _unlink_except_no_writer(self):
""" When removing a member, the constraint is not triggered.
We need to check manually on article with no write permission that we do not remove the last write member """
self._check_is_writable(on_unlink=True)
def _get_invitation_hash(self):
""" We use a method instead of a field in order to reduce DB space."""
self.ensure_one()
return tools.hmac(self.env(su=True),
'knowledge-article-invite',
f'{self.id}-{self.create_date}-{self.partner_id.id}-{self.article_id.id}'
)