import random from email.utils import parseaddr from markupsafe import Markup, escape import odoo.tools from odoo import fields, models, tools, api, Command from .. import github class CIText(fields.Char): type = 'char' column_type = ('citext', 'citext') column_cast_from = ('varchar', 'text') class Partner(models.Model): _name = 'res.partner' _inherit = ['res.partner', 'mail.thread'] email = fields.Char(index=True) github_login = CIText() delegate_reviewer = fields.Many2many('runbot_merge.pull_requests') formatted_email = fields.Char(string="commit email", compute='_rfc5322_formatted') review_rights = fields.One2many('res.partner.review', 'partner_id') override_rights = fields.Many2many('res.partner.override') override_sensitive = fields.Boolean(compute="_compute_sensitive_overrides") def _auto_init(self): res = super(Partner, self)._auto_init() tools.create_unique_index( self._cr, 'runbot_merge_unique_gh_login', self._table, ['github_login']) return res @api.depends('name', 'email', 'github_login') def _rfc5322_formatted(self): for partner in self: if partner.email: email = parseaddr(partner.email)[1] elif partner.github_login: email = '%s@users.noreply.github.com' % partner.github_login else: email = '' partner.formatted_email = '%s <%s>' % (partner.name, email) def fetch_github_email(self): # this requires a token in order to fetch the email field, otherwise # it's just not returned, select a random project to fetch gh = github.GH(random.choice(self.env['runbot_merge.project'].search([])).github_token, None) for p in self.filtered(lambda p: p.github_login and p.email is False): p.email = gh.user(p.github_login)['email'] or False return False @api.depends("override_rights.context") def _compute_sensitive_overrides(self): for p in self: p.override_sensitive = any(o.context == 'ci/security' for o in p.override_rights) def write(self, vals): created = [] updated = {} deleted = set() for cmd, id, values in vals.get('review_rights', []): if cmd == Command.DELETE: deleted.add(id) elif cmd == Command.CREATE: # 'repository_id': 3, 'review': True, 'self_review': False created.append(values) elif cmd == Command.UPDATE: updated[id] = values # could also be LINK for records which are not touched but we don't care new_rights = None if r := vals.get('override_rights'): # only handle reset (for now?) even though technically e.g. 0 works # the web client doesn't seem to use it (?) if r[0][0] == 6: new_rights = self.env['res.partner.override'].browse(r[0][2]) Repo = self.env['runbot_merge.repository'].browse for p in self: msgs = [] if ds := p.review_rights.filtered(lambda r: r.id in deleted): msgs.append("removed review rights on {}\n".format( ', '.join(ds.mapped('repository_id.name')) )) if us := p.review_rights.filtered(lambda r: r.id in updated): msgs.extend( "updated review rights on {}: {}\n".format( u.repository_id.name, ', '.join( f'allowed {f}' if v else f'forbid {f}' for f in ['review', 'self_review'] if (v := updated[u.id].get(f)) is not None ) ) for u in us ) msgs.extend( 'added review rights on {}: {}\n'.format( Repo(c['repository_id']).name, ', '.join(filter(c.get, ['review', 'self_review'])), ) for c in created ) if new_rights is not None: for r in p.override_rights - new_rights: msgs.append(f"removed override rights for {r.context!r} on {r.repository_id.name}") for r in new_rights - p.override_rights: msgs.append(f"added override rights for {r.context!r} on {r.repository_id.name}") if msgs: p._message_log(body=Markup('').format(Markup().join( map(Markup('
  • {}
  • ').format, reversed(msgs)) ))) return super().write(vals) class PartnerMerge(models.TransientModel): _inherit = 'base.partner.merge.automatic.wizard' @api.model def _update_values(self, src_partners, dst_partner): # sift down through src partners, removing all github_login and keeping # the last one new_login = None for p in src_partners: new_login = p.github_login or new_login if new_login: src_partners.write({'github_login': False}) if new_login and not dst_partner.github_login: dst_partner.github_login = new_login super()._update_values(src_partners, dst_partner) class ReviewRights(models.Model): _name = 'res.partner.review' _description = "mapping of review rights between partners and repos" partner_id = fields.Many2one('res.partner', required=True, ondelete='cascade') repository_id = fields.Many2one('runbot_merge.repository', required=True) review = fields.Boolean(default=False) self_review = fields.Boolean(default=False) def _auto_init(self): res = super()._auto_init() tools.create_unique_index(self._cr, 'runbot_merge_review_m2m', self._table, ['partner_id', 'repository_id']) return res @api.depends('repository_id.name', 'review', 'self_review') def _compute_display_name(self): for r in self: r.display_name = '%s: %s' % (r.repository_id.name, ', '.join(filter(None, [ r.review and "reviewer", r.self_review and "self-reviewer" ]))) @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): return self.search((args or []) + [('repository_id.name', operator, name)], limit=limit).name_get() class OverrideRights(models.Model): _name = 'res.partner.override' _description = 'lints which the partner can override' partner_ids = fields.Many2many('res.partner') repository_id = fields.Many2one('runbot_merge.repository') context = fields.Char(required=True) def init(self): super().init() tools.create_unique_index( self.env.cr, 'res_partner_override_unique', self._table, ['context', 'coalesce(repository_id, 0)'] ) @api.model_create_multi def create(self, vals_list): for partner, contexts in odoo.tools.groupby(( (partner_id, vals['context'], vals['repository_id']) for vals in vals_list # partner_ids is of the form [Command.set(ids) for partner_id in vals.get('partner_ids', [(None, None, [])])[0][2] ), lambda p: p[0]): partner = self.env['res.partner'].browse(partner) for _, context, repository in contexts: repository = self.env['runbot_merge.repository'].browse(repository) partner._message_log(body=f"added override rights for {context!r} on {repository.name}") return super().create(vals_list) def write(self, vals): new = None if pids := vals.get('partner_ids'): new = self.env['res.partner'].browse(pids[0][2]) if new is not None: for o in self: added = new - o.partner_ids removed = o.partner_ids - new for p in added: p._message_log(body=f"added override rights for {o.context!r} on {o.repository_id.name}") for r in removed: r._message_log(body=f"removed override rights for {o.context!r} on {o.repository_id.name}") return super().write(vals) def unlink(self): for o in self: for p in o.partner_ids: p._message_log(body=f"removed override rights for {o.context!r} on {o.repository_id.name}") return super().unlink() @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): return self.search((args or []) + [ '|', ('context', operator, name), ('repository_id.name', operator, name) ], limit=limit).name_get() @api.depends('repository_id.name', 'context') def _compute_display_name(self): for r in self: if r.repository_id: r.display_name = f'{r.repository_id.name}: {r.context}' else: r.display_name = r.context