diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index 90f11c63257..81185de0e4b 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -25,6 +25,7 @@ 'views/mail_mail_views.xml', 'views/mail_followers_views.xml', 'views/mail_moderation_views.xml', + 'views/mail_channel_partner_views.xml', 'views/mail_channel_views.xml', 'views/mail_shortcode_views.xml', 'views/mail_activity_views.xml', diff --git a/addons/mail/models/__init__.py b/addons/mail/models/__init__.py index 38b80e6dc12..03c7e597fa2 100644 --- a/addons/mail/models/__init__.py +++ b/addons/mail/models/__init__.py @@ -15,6 +15,8 @@ from . import mail_thread from . import mail_thread_blacklist from . import mail_thread_cc from . import mail_blacklist +from . import mail_moderation +from . import mail_channel_partner from . import mail_channel from . import mail_template from . import mail_shortcode diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 9f2b2fb97a0..db1b39de91e 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -10,68 +10,11 @@ from odoo import _, api, fields, models, modules, tools, Command from odoo.exceptions import UserError, ValidationError from odoo.osv import expression from odoo.tools import ormcache, formataddr -from odoo.exceptions import AccessError -from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG MODERATION_FIELDS = ['moderation', 'moderator_ids', 'moderation_ids', 'moderation_notify', 'moderation_notify_msg', 'moderation_guidelines', 'moderation_guidelines_msg'] _logger = logging.getLogger(__name__) -class ChannelPartner(models.Model): - _name = 'mail.channel.partner' - _description = 'Listeners of a Channel' - _table = 'mail_channel_partner' - _rec_name = 'partner_id' - - custom_channel_name = fields.Char('Custom channel name') - partner_id = fields.Many2one('res.partner', string='Recipient', ondelete='cascade') - partner_email = fields.Char('Email', related='partner_id.email', readonly=False) - channel_id = fields.Many2one('mail.channel', string='Channel', ondelete='cascade') - fetched_message_id = fields.Many2one('mail.message', string='Last Fetched') - seen_message_id = fields.Many2one('mail.message', string='Last Seen') - fold_state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Conversation Fold State', default='open') - is_minimized = fields.Boolean("Conversation is minimized") - is_pinned = fields.Boolean("Is pinned on the interface", default=True) - - @api.model_create_multi - def create(self, vals_list): - """Similar access rule as the access rule of the mail channel. - - It can not be implemented in XML, because when the record will be created, the - partner will be added in the channel and the security rule will always authorize - the creation. - """ - if not self.env.is_admin(): - for vals in vals_list: - if 'channel_id' in vals and not self.env.is_admin(): - channel_id = self.env['mail.channel'].browse(vals['channel_id']) - if not channel_id._can_invite(vals.get('partner_id')): - raise AccessError(_('This user can not be added in this channel')) - return super(ChannelPartner, self).create(vals_list) - - def write(self, vals): - if not self.env.is_admin(): - if {'channel_id', 'partner_id', 'partner_email'} & set(vals): - raise AccessError(_('You can not write on this field')) - return super(ChannelPartner, self).write(vals) - - -class Moderation(models.Model): - _name = 'mail.moderation' - _description = 'Channel black/white list' - - email = fields.Char(string="Email", index=True, required=True) - status = fields.Selection([ - ('allow', 'Always Allow'), - ('ban', 'Permanent Ban')], - string="Status", required=True) - channel_id = fields.Many2one('mail.channel', string="Channel", index=True, required=True) - - _sql_constraints = [ - ('channel_email_uniq', 'unique (email,channel_id)', 'The email address must be unique per channel !') - ] - - class Channel(models.Model): """ A mail.channel is a discussion group that may behave like a listener on documents. """ @@ -94,39 +37,39 @@ class Channel(models.Model): 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.") channel_type = fields.Selection([ ('chat', 'Chat Discussion'), ('channel', 'Channel')], - 'Channel Type', default='channel') + string='Channel Type', default='channel') is_chat = fields.Boolean(string='Is a chat', compute='_compute_is_chat') description = fields.Text('Description') - uuid = fields.Char('UUID', size=50, index=True, default=lambda self: str(uuid4()), copy=False) email_send = fields.Boolean('Send messages by email', default=False) - # multi users channel - # depends=['...'] is for `test_mail/tests/common.py`, class Moderation, `setUpClass` + image_128 = fields.Image("Image", max_width=128, max_height=128, default=_get_default_image) + # members (depends=['...'] is for `test_mail/tests/common.py`, class Moderation, `setUpClass`) channel_last_seen_partner_ids = fields.One2many('mail.channel.partner', 'channel_id', string='Last Seen', depends=['channel_partner_ids']) channel_partner_ids = fields.Many2many('res.partner', 'mail_channel_partner', 'channel_id', 'partner_id', string='Listeners', depends=['channel_last_seen_partner_ids']) channel_message_ids = fields.Many2many('mail.message', 'mail_message_mail_channel_rel') is_member = fields.Boolean('Is a member', compute='_compute_is_member') - # access - public = fields.Selection([ - ('public', 'Everyone'), - ('private', 'Invited people only'), - ('groups', 'Selected group of users')], - 'Privacy', required=True, default='groups', - help='This group is visible by non members. Invisible groups can add members through the invite button.') - group_public_id = fields.Many2one('res.groups', string='Authorized Group', - default=lambda self: self.env.ref('base.group_user')) + is_subscribed = fields.Boolean( + 'Is Subscribed', compute='_compute_is_subscribed') group_ids = fields.Many2many( 'res.groups', string='Auto Subscription', help="Members of those groups will automatically added as followers. " "Note that they will be able to manage their subscription manually " "if necessary.") - image_128 = fields.Image("Image", max_width=128, max_height=128, default=_get_default_image) - is_subscribed = fields.Boolean( - 'Is Subscribed', compute='_compute_is_subscribed') + # access + uuid = fields.Char('UUID', size=50, index=True, default=lambda self: str(uuid4()), copy=False) + public = fields.Selection([ + ('public', 'Everyone'), + ('private', 'Invited people only'), + ('groups', 'Selected group of users')], string='Privacy', + required=True, default='groups', + help='This group is visible by non members. Invisible groups can add members through the invite button.') + group_public_id = fields.Many2one('res.groups', string='Authorized Group', + default=lambda self: self.env.ref('base.group_user')) # moderation moderation = fields.Boolean(string='Moderate this channel') moderator_ids = fields.Many2many('res.users', 'mail_channel_moderator_rel', string='Moderators') @@ -142,6 +85,22 @@ class Channel(models.Model): moderation_guidelines = fields.Boolean(string="Send guidelines to new subscribers", help="Newcomers on this moderated channel will automatically receive the guidelines.") moderation_guidelines_msg = fields.Text(string="Guidelines") + # COMPUTE / INVERSE + + @api.depends('channel_type') + def _compute_is_chat(self): + for record in self: + record.is_chat = record.channel_type == 'chat' + + def _compute_is_member(self): + memberships = self.env['mail.channel.partner'].sudo().search([ + ('channel_id', 'in', self.ids), + ('partner_id', '=', self.env.user.partner_id.id), + ]) + membership_ids = memberships.mapped('channel_id') + for record in self: + record.is_member = record in membership_ids + @api.depends('channel_partner_ids') def _compute_is_subscribed(self): for channel in self: @@ -159,6 +118,8 @@ class Channel(models.Model): for channel in self: channel.moderation_count = data.get(channel.id, 0) + # CONSTRAINTS + @api.constrains('moderator_ids') def _check_moderator_email(self): if any(not moderator.email for channel in self for moderator in channel.moderator_ids): @@ -184,19 +145,7 @@ class Channel(models.Model): if any(not channel.moderator_ids for channel in self if channel.moderation): raise ValidationError(_('Moderated channels must have moderators.')) - def _compute_is_member(self): - memberships = self.env['mail.channel.partner'].sudo().search([ - ('channel_id', 'in', self.ids), - ('partner_id', '=', self.env.user.partner_id.id), - ]) - membership_ids = memberships.mapped('channel_id') - for record in self: - record.is_member = record in membership_ids - - @api.depends('channel_type') - def _compute_is_chat(self): - for record in self: - record.is_chat = record.channel_type == 'chat' + # ONCHANGE @api.onchange('public') def _onchange_public(self): @@ -226,6 +175,10 @@ class Channel(models.Model): else: self.moderator_ids |= self.env.user + # ------------------------------------------------------------ + # CRUD + # ------------------------------------------------------------ + @api.model_create_multi def create(self, vals_list): defaults = self.default_get(['image_128']) @@ -305,12 +258,14 @@ class Channel(models.Model): return result - def _alias_get_creation_values(self): - values = super(Channel, self)._alias_get_creation_values() - values['alias_model_id'] = self.env['ir.model']._get('mail.channel').id - if self.id: - values['alias_force_thread_id'] = self.id - return values + def init(self): + self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_channel_partner_seen_message_id_idx',)) + if not self._cr.fetchone(): + self._cr.execute('CREATE INDEX mail_channel_partner_seen_message_id_idx ON mail_channel_partner (channel_id,partner_id,seen_message_id)') + + # ------------------------------------------------------------ + # MEMBERS MANAGEMENT + # ------------------------------------------------------------ def _subscribe_users(self): to_create = [] @@ -371,6 +326,85 @@ class Channel(models.Model): ]).unlink() self.invalidate_cache(fnames=['channel_partner_ids', 'channel_last_seen_partner_ids']) + def channel_invite(self, partner_ids): + """ Add the given partner_ids to the current channels and broadcast the channel header to them. + :param partner_ids : list of partner id to add + """ + partners = self.env['res.partner'].browse(partner_ids) + self._invite_check_access(partners) + + # add the partner + for channel in self: + partners_to_add = partners - channel.channel_partner_ids + channel.write({'channel_last_seen_partner_ids': [Command.create({'partner_id': partner_id}) for partner_id in partners_to_add.ids]}) + for partner in partners_to_add: + if partner.id != self.env.user.partner_id.id: + notification = _('
%(author)s invited %(new_partner)s to #%(channel_name)s
') % { + 'author': self.env.user.display_name, + 'new_partner': partner.display_name, + 'channel_id': channel.id, + 'channel_name': channel.name, + } + else: + notification = _('
joined #%s
') % (channel.id, channel.name,) + self.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment", author_id=partner.id, notify_by_email=False) + + # broadcast the channel header to the added partner + self._broadcast(partner_ids) + + def _invite_check_access(self, partners): + """ Check invited partners could match channel access """ + failed = [] + if any(channel.public == 'groups' for channel in self): + for channel in self.filtered(lambda c: c.public == 'groups'): + invalid_partners = [partner for partner in partners if channel.group_public_id not in partner.mapped('user_ids.groups_id')] + failed += [(channel, partner) for partner in invalid_partners] + + if failed: + raise UserError( + _('Following invites are invalid as user groups do not match: %s') % + ', '.join('%s (channel %s)' % (partner.name, channel.name) for channel, partner in failed) + ) + + def _can_invite(self, partner_id): + """Return True if the current user can invite the partner to the channel. + + * public: ok; + * private: must be member; + * group: both current user and target must have group; + + :return boolean: whether inviting is ok""" + partner = self.env['res.partner'].browse(partner_id) + + for channel in self.sudo(): + if channel.public == 'private' and not channel.is_member: + return False + if channel.public == 'groups': + if not partner.user_ids or channel.group_public_id not in partner.user_ids.groups_id: + return False + if channel.group_public_id not in self.env.user.groups_id: + return False + return True + + # ------------------------------------------------------------ + # MAILING + # ------------------------------------------------------------ + + def _alias_get_creation_values(self): + values = super(Channel, self)._alias_get_creation_values() + values['alias_model_id'] = self.env['ir.model']._get('mail.channel').id + if self.id: + values['alias_force_thread_id'] = self.id + return values + + def _alias_get_error_message(self, message, message_dict, alias): + if alias.alias_contact == 'followers' and self.ids: + author = self.env['res.partner'].browse(message_dict.get('author_id', False)) + if not author or author not in self.channel_partner_ids: + return _('restricted to channel members') + return False + return super(Channel, self)._alias_get_error_message(message, message_dict, alias) + def _notify_get_groups(self, msg_vals=None): """ All recipients of a message on a channel are considered as partners. This means they will receive a minimal email, without a link to access @@ -397,13 +431,6 @@ class Channel(models.Model): headers['X-Forge-To'] = list_to return headers - def _message_receive_bounce(self, email, partner): - """ Override bounce management to unsubscribe bouncing addresses """ - for p in partner: - if p.message_bounce >= self.MAX_BOUNCE_LIMIT: - self._action_unfollow(p) - return super(Channel, self)._message_receive_bounce(email, partner) - def _notify_email_recipient_values(self, recipient_ids): # Excluded Blacklisted whitelist = self.env['res.partner'].sudo().browse(recipient_ids).filtered(lambda p: not p.is_blacklisted) @@ -415,32 +442,19 @@ class Channel(models.Model): } return super(Channel, self)._notify_email_recipient_values(whitelist.ids) - def _extract_moderation_values(self, message_type, **kwargs): - """ This method is used to compute moderation status before the creation - of a message. For this operation the message's author email address is required. - This address is returned with status for other computations. """ - moderation_status = 'accepted' - email = '' - if self.moderation and message_type in ['email', 'comment']: - author_id = kwargs.get('author_id') - if author_id and isinstance(author_id, int): - email = self.env['res.partner'].browse([author_id]).email - elif author_id: - email = author_id.email - elif kwargs.get('email_from'): - email = tools.email_split(kwargs['email_from'])[0] - else: - email = self.env.user.email - if email in self.mapped('moderator_ids.email'): - return moderation_status, email - status = self.env['mail.moderation'].sudo().search([('email', '=', email), ('channel_id', 'in', self.ids)]).mapped('status') - if status and status[0] == 'allow': - moderation_status = 'accepted' - elif status and status[0] == 'ban': - moderation_status = 'rejected' - else: - moderation_status = 'pending_moderation' - return moderation_status, email + def _notify_thread(self, message, msg_vals=False, **kwargs): + # When posting a message on a mail channel, manage moderation and postpone notify users + if not msg_vals or msg_vals.get('moderation_status') != 'pending_moderation': + super(Channel, self)._notify_thread(message, msg_vals=msg_vals, **kwargs) + else: + message._notify_pending_by_chat() + + def _message_receive_bounce(self, email, partner): + """ Override bounce management to unsubscribe bouncing addresses """ + for p in partner: + if p.message_bounce >= self.MAX_BOUNCE_LIMIT: + self._action_unfollow(p) + return super(Channel, self)._message_receive_bounce(email, partner) @api.returns('mail.message', lambda value: value.id) def message_post(self, *, message_type='notification', **kwargs): @@ -473,22 +487,36 @@ class Channel(models.Model): self._set_last_seen_message(message) return super()._message_post_after_hook(message=message, msg_vals=msg_vals) - def _alias_get_error_message(self, message, message_dict, alias): - if alias.alias_contact == 'followers' and self.ids: - author = self.env['res.partner'].browse(message_dict.get('author_id', False)) - if not author or author not in self.channel_partner_ids: - return _('restricted to channel members') - return False - return super(Channel, self)._alias_get_error_message(message, message_dict, alias) + def _extract_moderation_values(self, message_type, **kwargs): + """ This method is used to compute moderation status before the creation + of a message. For this operation the message's author email address is required. + This address is returned with status for other computations. """ + moderation_status = 'accepted' + email = '' + if self.moderation and message_type in ['email', 'comment']: + author_id = kwargs.get('author_id') + if author_id and isinstance(author_id, int): + email = self.env['res.partner'].browse([author_id]).email + elif author_id: + email = author_id.email + elif kwargs.get('email_from'): + email = tools.email_split(kwargs['email_from'])[0] + else: + email = self.env.user.email + if email in self.mapped('moderator_ids.email'): + return moderation_status, email + status = self.env['mail.moderation'].sudo().search([('email', '=', email), ('channel_id', 'in', self.ids)]).mapped('status') + if status and status[0] == 'allow': + moderation_status = 'accepted' + elif status and status[0] == 'ban': + moderation_status = 'rejected' + else: + moderation_status = 'pending_moderation' + return moderation_status, email - def init(self): - self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_channel_partner_seen_message_id_idx',)) - if not self._cr.fetchone(): - self._cr.execute('CREATE INDEX mail_channel_partner_seen_message_id_idx ON mail_channel_partner (channel_id,partner_id,seen_message_id)') - - # -------------------------------------------------- - # Moderation - # -------------------------------------------------- + # ------------------------------------------------------------ + # MODERATION + # ------------------------------------------------------------ def send_guidelines(self): """ Send guidelines to all channel members. """ @@ -540,14 +568,9 @@ class Channel(models.Model): cmds += [Command.create({'email': email, 'status': status}) for email in not_moderated] return self.write({'moderation_ids': cmds}) - #------------------------------------------------------ - # Instant Messaging API - #------------------------------------------------------ - # A channel header should be broadcasted: - # - when adding user to channel (only to the new added partners) - # - when folding/minimizing a channel (only to the user making the action) - # A message should be broadcasted: - # - when a message is posted on a channel (to the channel, using _notify() method) + # ------------------------------------------------------------ + # BROADCAST + # ------------------------------------------------------------ # Anonymous method def _broadcast(self, partner_ids): @@ -573,13 +596,6 @@ class Channel(models.Model): notifications.append([(self._cr.dbname, 'res.partner', partner.id), channel_info]) return notifications - def _notify_thread(self, message, msg_vals=False, **kwargs): - # When posting a message on a mail channel, manage moderation and postpone notify users - if not msg_vals or msg_vals.get('moderation_status') != 'pending_moderation': - super(Channel, self)._notify_thread(message, msg_vals=msg_vals, **kwargs) - else: - message._notify_pending_by_chat() - def _channel_message_notifications(self, message, message_format=False): """ Generate the bus notifications for the given message :param message : the mail.message to sent @@ -594,6 +610,16 @@ class Channel(models.Model): notifications.append([channel.uuid, dict(message_format)]) return notifications + # ------------------------------------------------------------ + # INSTANT MESSAGING API + # ------------------------------------------------------------ + # A channel header should be broadcasted: + # - when adding user to channel (only to the new added partners) + # - when folding/minimizing a channel (only to the user making the action) + # A message should be broadcasted: + # - when a message is posted on a channel (to the channel, using _notify() method) + # ------------------------------------------------------------ + @api.model def partner_info(self, all_partners, direct_partners): """ @@ -893,66 +919,6 @@ class Channel(models.Model): } self.env['bus.bus'].sendmany([[(self._cr.dbname, 'mail.channel', channel.id), data]]) - def channel_invite(self, partner_ids): - """ Add the given partner_ids to the current channels and broadcast the channel header to them. - :param partner_ids : list of partner id to add - """ - partners = self.env['res.partner'].browse(partner_ids) - self._invite_check_access(partners) - - # add the partner - for channel in self: - partners_to_add = partners - channel.channel_partner_ids - channel.write({'channel_last_seen_partner_ids': [Command.create({'partner_id': partner_id}) for partner_id in partners_to_add.ids]}) - for partner in partners_to_add: - if partner.id != self.env.user.partner_id.id: - notification = _('
%(author)s invited %(new_partner)s to #%(channel_name)s
') % { - 'author': self.env.user.display_name, - 'new_partner': partner.display_name, - 'channel_id': channel.id, - 'channel_name': channel.name, - } - else: - notification = _('
joined #%s
') % (channel.id, channel.name,) - self.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment", author_id=partner.id, notify_by_email=False) - - # broadcast the channel header to the added partner - self._broadcast(partner_ids) - - def _invite_check_access(self, partners): - """ Check invited partners could match channel access """ - failed = [] - if any(channel.public == 'groups' for channel in self): - for channel in self.filtered(lambda c: c.public == 'groups'): - invalid_partners = [partner for partner in partners if channel.group_public_id not in partner.mapped('user_ids.groups_id')] - failed += [(channel, partner) for partner in invalid_partners] - - if failed: - raise UserError( - _('Following invites are invalid as user groups do not match: %s') % - ', '.join('%s (channel %s)' % (partner.name, channel.name) for channel, partner in failed) - ) - - def _can_invite(self, partner_id): - """Return True if the current user can invite the partner to the channel. - - * public: ok; - * private: must be member; - * group: both current user and target must have group; - - :return boolean: whether inviting is ok""" - partner = self.env['res.partner'].browse(partner_id) - - for channel in self.sudo(): - if channel.public == 'private' and not channel.is_member: - return False - if channel.public == 'groups': - if not partner.user_ids or channel.group_public_id not in partner.user_ids.groups_id: - return False - if channel.group_public_id not in self.env.user.groups_id: - return False - return True - @api.model def channel_set_custom_name(self, channel_id, name=False): domain = [('partner_id', '=', self.env.user.partner_id.id), ('channel_id.id', '=', channel_id)] @@ -977,9 +943,10 @@ class Channel(models.Model): notifications.append([channel.uuid, data]) # notify frontend users self.env['bus.bus'].sendmany(notifications) - #------------------------------------------------------ - # Instant Messaging View Specific (Slack Client Action) - #------------------------------------------------------ + # ------------------------------------------------------------ + # IM VIEW SPECIFIC (Slack Client Action) + # ------------------------------------------------------------ + @api.model def channel_fetch_slot(self): """ Return the channels of the user grouped by 'slot' (channel, direct_message or private_group), and @@ -1106,9 +1073,10 @@ class Channel(models.Model): """, (tuple(self.ids),)) return self.env.cr.dictfetchall() - #------------------------------------------------------ - # Commands - #------------------------------------------------------ + # ------------------------------------------------------------ + # COMMANDS + # ------------------------------------------------------------ + @api.model @ormcache() def get_mention_commands(self): diff --git a/addons/mail/models/mail_channel_partner.py b/addons/mail/models/mail_channel_partner.py new file mode 100644 index 00000000000..aec60edf971 --- /dev/null +++ b/addons/mail/models/mail_channel_partner.py @@ -0,0 +1,44 @@ +# -*- 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 AccessError + + +class ChannelPartner(models.Model): + _name = 'mail.channel.partner' + _description = 'Listeners of a Channel' + _table = 'mail_channel_partner' + _rec_name = 'partner_id' + + custom_channel_name = fields.Char('Custom channel name') + partner_id = fields.Many2one('res.partner', string='Recipient', ondelete='cascade') + partner_email = fields.Char('Email', related='partner_id.email', readonly=False) + channel_id = fields.Many2one('mail.channel', string='Channel', ondelete='cascade') + fetched_message_id = fields.Many2one('mail.message', string='Last Fetched') + seen_message_id = fields.Many2one('mail.message', string='Last Seen') + fold_state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Conversation Fold State', default='open') + is_minimized = fields.Boolean("Conversation is minimized") + is_pinned = fields.Boolean("Is pinned on the interface", default=True) + + @api.model_create_multi + def create(self, vals_list): + """Similar access rule as the access rule of the mail channel. + + It can not be implemented in XML, because when the record will be created, the + partner will be added in the channel and the security rule will always authorize + the creation. + """ + if not self.env.is_admin(): + for vals in vals_list: + if 'channel_id' in vals: + channel_id = self.env['mail.channel'].browse(vals['channel_id']) + if not channel_id._can_invite(vals.get('partner_id')): + raise AccessError(_('This user can not be added in this channel')) + return super(ChannelPartner, self).create(vals_list) + + def write(self, vals): + if not self.env.is_admin(): + if {'channel_id', 'partner_id', 'partner_email'} & set(vals): + raise AccessError(_('You can not write on this field')) + return super(ChannelPartner, self).write(vals) diff --git a/addons/mail/models/mail_moderation.py b/addons/mail/models/mail_moderation.py new file mode 100644 index 00000000000..7e4982a946a --- /dev/null +++ b/addons/mail/models/mail_moderation.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class Moderation(models.Model): + _name = 'mail.moderation' + _description = 'Channel black/white list' + + email = fields.Char(string="Email", index=True, required=True) + status = fields.Selection([ + ('allow', 'Always Allow'), + ('ban', 'Permanent Ban')], + string="Status", required=True) + channel_id = fields.Many2one('mail.channel', string="Channel", index=True, required=True) + + _sql_constraints = [ + ('channel_email_uniq', 'unique (email,channel_id)', 'The email address must be unique per channel !') + ] diff --git a/addons/mail/views/mail_channel_partner_views.xml b/addons/mail/views/mail_channel_partner_views.xml new file mode 100644 index 00000000000..b89ec9c61b3 --- /dev/null +++ b/addons/mail/views/mail_channel_partner_views.xml @@ -0,0 +1,46 @@ + + + + + mail.channel.partner.tree + mail.channel.partner + 10 + + + + + + + + + + + mail.channel.partner.form + mail.channel.partner + +
+ + + + + + + +
+
+
+ + + Channels/Partner + mail.channel.partner + tree,form + + + + +
diff --git a/addons/mail/views/mail_channel_views.xml b/addons/mail/views/mail_channel_views.xml index 78853166aca..16d279803c1 100644 --- a/addons/mail/views/mail_channel_views.xml +++ b/addons/mail/views/mail_channel_views.xml @@ -1,50 +1,6 @@ - - - - mail.channel.partner.tree - mail.channel.partner - 10 - - - - - - - - - - - mail.channel.partner.form - mail.channel.partner - -
- - - - - - - -
-
-
- - - Channels/Partner - mail.channel.partner - tree,form - - - - mail.channel.kanban