Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions runbot/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
'templates/batches_by_date.xml',
'templates/commit_link_details.xml',
'templates/bundles_by_tag.xml',
'templates/team_reviews.xml',
'templates/versions.xml',
'templates/tree_hash.xml',

Expand Down
45 changes: 45 additions & 0 deletions runbot/controllers/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,51 @@ def team_dashboards(self, team=None, hide_empty=False, **kwargs):
}
return request.render('runbot.team', qctx)

@route(['/runbot/team_reviews/<int:team_id>'], type='http', auth='user', website=True, sitemap=False)
def team_reviews(self, team_id=None, **kwargs):
team = request.env['runbot.team'].browse(team_id).exists()
if not team:
raise NotFound()

reviews_by_pr = defaultdict(lambda: defaultdict(lambda: request.env['runbot.team.review']))
reviews = request.env['runbot.team.review'].search([
('team_id', '=', team.id),
('branch_id.alive', '=', True),
])
for review in reviews:
reviews_by_pr[review.branch_id][review.filename] |= review

sorted_reviews_by_pr = [
(pr, sorted(reviews_by_file.items(), key=lambda file_reviews: (file_reviews[1][0].reviewed, file_reviews[0] or '')))
for pr, reviews_by_file in reviews_by_pr.items()
]

context = {
'team': team,
'reviews_by_pr': sorted_reviews_by_pr,
'nb_files': len(reviews),
'nb_files_reviewed': len(reviews.filtered(lambda rec: rec.reviewed)),
'is_team_member': team in request.env.user.runbot_team_ids,
}
return request.render('runbot.team_reviews', context)

@route(['/runbot/team_reviews/reviewed/<int:review_id>'], type='http', auth='user', methods=['POST'], csrf=False, sitemap=False)
def team_review_reviewed(self, review_id=None, **kwargs):
review = request.env['runbot.team.review'].browse(review_id).exists()
if not review:
raise NotFound()
if review.team_id not in request.env.user.runbot_team_ids:
raise Forbidden('Only members of the team can check this file as reviewed')

reviews = request.env['runbot.team.review'].search([
('team_id', '=', review.team_id.id),
('branch_id', '=', review.branch_id.id),
('filename', '=', review.filename),
('reviewed', '=', False),
])
reviews.write({'reviewed': True, 'reviewer_id': request.env.user.id})
return str(len(reviews))

@route(['/runbot/dashboards/<model("runbot.dashboard"):dashboard>'], type='http', auth='user', website=True, sitemap=False)
def dashboards(self, dashboard=None, hide_empty=False, **kwargs):
qctx = {
Expand Down
15 changes: 15 additions & 0 deletions runbot/models/build_config_codeowner.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@ def _reviewer_per_file(self, files, regexes, ownerships, repo, build):
reviewer_per_file[file] = file_reviewers
return reviewer_per_file

def _create_team_review_links(self, build, pr, new_reviewers, reviewer_per_file):
teams = self.env['runbot.team'].search([('github_team', 'in', list(new_reviewers))])

vals_list = []
for team in teams:
for file in sorted(file for file, file_reviewers in reviewer_per_file.items() if team.github_team in file_reviewers):
vals_list.append({
'team_id': team.id,
'branch_id': pr.id,
'build_id': build.id,
'filename': file,
})
return self.env['runbot.team.review'].create(vals_list)

def _run_codeowner(self, build):
bundle = build.params_id.create_batch_id.bundle_id
if bundle.is_base:
Expand Down Expand Up @@ -155,5 +169,6 @@ def _run_codeowner(self, build):
response = pr.remote_id._github('/repos/:owner/:repo/pulls/%s/requested_reviewers' % pr.name, {"team_reviewers": list(new_reviewers)}, ignore_errors=False)
pr._update_branch_infos(response)
pr['reviewers'] = ','.join(sorted(reviewers))
self._create_team_review_links(build, pr, new_reviewers, reviewer_per_file)
else:
build._log('', 'All reviewers are already on pull request [%s](%s)', pr.dname, pr.branch_url, log_type='markdown')
14 changes: 14 additions & 0 deletions runbot/models/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class RunbotTeam(models.Model):
)
module_ownership_ids = fields.One2many('runbot.module.ownership', 'team_id')
codeowner_ids = fields.One2many('runbot.codeowner', 'team_id')
review_link_ids = fields.One2many('runbot.team.review', 'team_id', string='Team Review Links')
trigger_ids = fields.Many2many('runbot.trigger', string='Followed triggers')
upgrade_exception_ids = fields.One2many('runbot.upgrade.exception', 'team_id', string='Team Upgrade Exceptions')
github_team = fields.Char('Github team', tracking=True)
Expand Down Expand Up @@ -114,6 +115,19 @@ def _fetch_members(self):
team.github_logins = ','.join(sorted(members))


class TeamReviewLink(models.Model):
_name = 'runbot.team.review'
_description = 'Team Pull Request Extended Relation'
_order = 'branch_id desc'

team_id = fields.Many2one('runbot.team', 'Team', required=True, index=True)
branch_id = fields.Many2one('runbot.branch', domain=[('is_pr', '=', True), ('alive', '=', True)], required=True, string='Pull Request')
build_id = fields.Many2one('runbot.build', string='Triggering Build')
filename = fields.Char(string='Modified triggering file')
reviewed = fields.Boolean('Reviewed', default=False)
reviewer_id = fields.Many2one('res.users', 'Reviewed by', domain=[('share', '=', False), ('active', '=', True)])


class Module(models.Model):
_name = 'runbot.module'
_description = 'Modules'
Expand Down
4 changes: 4 additions & 0 deletions runbot/security/ir.model.access.csv
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ access_runbot_team_admin,runbot_team_admin,runbot.model_runbot_team,runbot.group
access_runbot_team_team_manager,runbot_team_team_manager,runbot.model_runbot_team,runbot.group_runbot_team_manager,1,1,1,1
access_runbot_team_user,runbot_team_user,runbot.model_runbot_team,group_user,1,0,0,0

access_runbot_team_review_admin,runbot_team_review_admin,runbot.model_runbot_team_review,runbot.group_runbot_admin,1,1,1,1
access_runbot_team_review_team_manager,runbot_team_review_team_manager,runbot.model_runbot_team_review,runbot.group_runbot_team_manager,1,1,1,1
access_runbot_team_review_user,runbot_team_review_user,runbot.model_runbot_team_review,group_user,1,0,0,0

access_runbot_error_bulk_wizard_admin,access_runbot_error_bulk_wizard_admin,runbot.model_runbot_error_bulk_wizard,runbot.group_runbot_admin,1,1,1,1
access_runbot_error_bulk_wizard_manager,access_runbot_error_bulk_wizard_manager,runbot.model_runbot_error_bulk_wizard,runbot.group_runbot_error_manager,1,1,1,1

Expand Down
3 changes: 3 additions & 0 deletions runbot/templates/build_error.xml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@
t-attf-href="/odoo/team/{{team.id}}" target="_blank" title="View in Backend">
<i class="fa fa-list"/>
</a>
<a t-attf-href="/runbot/team_reviews/{{team.id}}" class="btn btn-sm btn-default align-middle" title="Files to review">
<i class="fa fa-eye"/> Reviews
</a>
</h2>
</div>
<div t-if="team.dashboard_id">
Expand Down
74 changes: 74 additions & 0 deletions runbot/templates/team_reviews.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<template id="runbot.team_reviews">
<t t-call='runbot.layout'>
<div class="container-fluid">
</div>
<table class="table table-condensed table-group table-filter caption-top text-body-secondary">
<caption>
<div class="row align-items-center">
<h2 class="col-12 col-md-auto">
Reviews for team <t t-out="team.name.capitalize()"/>
<span t-attf-class="badge fs-6 align-middle text-bg-{{'success' if nb_files_reviewed == nb_files else 'primary'}}"
title="Reviewed files">
<t t-out="nb_files_reviewed"/> / <t t-out="nb_files"/>
</span>
<button type="button" class="btn btn-sm btn-outline-primary align-middle" data-toggle="table-group-collapse">Collapse all</button>
</h2>
</div>
</caption>
<colgroup>
<col class="w-0"/>
<col class="w-75"/>
<col/>
</colgroup>
<t t-foreach="reviews_by_pr" t-as="pr_reviews">
<t t-set="pr" t-value="pr_reviews[0]"/>
<t t-set="file_reviews" t-value="pr_reviews[1]"/>
<tbody class="table-group-divider">
<tr class="table-active">
<th colspan="3">
<a t-att-href="pr.branch_url" title="View on github" target="_blank"><t t-out="pr.pr_title"/><t t-out="pr.name"/></a>
<t t-set="nb_pr_files" t-value="len(file_reviews)"/>
<t t-set="nb_pr_files_reviewed" t-value="len([file_review for file_review in file_reviews if file_review[1][0].reviewed])"/>
<span t-attf-class="badge text-bg-{{'success' if nb_pr_files_reviewed == nb_pr_files else 'primary'}}"
title="Reviewed files">
<t t-out="nb_pr_files_reviewed"/> / <t t-out="nb_pr_files"/>
</span>
</th>
</tr>
<t t-foreach="file_reviews" t-as="file_review">
<t t-set="filename" t-value="file_review[0]"/>
<t t-set="reviews" t-value="file_review[1]"/>
<t t-set="review" t-value="reviews[0]"/>
<tr>
<td>
<span t-if="review.reviewed" class="btn btn-sm btn-default fa fa-check-square-o disabled"
title="Already reviewed"/>
<a t-elif="is_team_member" t-attf-href="/runbot/team_reviews/reviewed/{{review.id}}"
data-runbot="review" role="button"
class="btn btn-sm btn-default fa fa-square-o"
title="Mark as reviewed"/>
</td>
<td>
<code t-out="filename" t-attf-class="{{'text-body-secondary' if review.reviewed else ''}}"/>
<span t-if="review.reviewer_id" class="text-body-secondary">
(<t t-out="review.reviewer_id.name"/>)
</span>
</td>
<td class="text-nowrap">
<a t-if="review.build_id" t-att-href="review.build_id.build_url"
t-attf-title="View triggering build ({{review.build_id.id}})" target="_blank">
<i class="fa fa-external-link"/>
</a>
</td>
</tr>
</t>
</tbody>
</t>
</table>
</t>
</template>
</data>
</odoo>