runbot/runbot/controllers/frontend.py
Christophe Monniez eaf393fa1f [IMP] runbot: add icons for reverse dependency builds
When a reverse dependency is in testing, pending or deathrow state, there is no
icon in the depending build box.
This commit adds icons for testing, pending and deathrow states.
Also, icons are now displayed in the repository name order.

closes: #19
2018-05-28 09:40:01 +02:00

280 lines
12 KiB
Python

# -*- coding: utf-8 -*-
import operator
import werkzeug
from collections import OrderedDict
from odoo import http
from odoo.addons.http_routing.models.ir_http import slug
from odoo.addons.website.controllers.main import QueryURL
from odoo.http import request
from ..common import uniq_list, flatten, s2human
class Runbot(http.Controller):
def build_info(self, build):
real_build = build.duplicate_id if build.state == 'duplicate' else build
return {
'id': build.id,
'name': build.name,
'state': real_build.state,
'result': real_build.result,
'guess_result': real_build.guess_result,
'subject': build.subject,
'author': build.author,
'committer': build.committer,
'dest': build.dest,
'real_dest': real_build.dest,
'job_age': s2human(real_build.job_age),
'job_time': s2human(real_build.job_time),
'job': real_build.job,
'domain': real_build.domain,
'host': real_build.host,
'port': real_build.port,
'server_match': real_build.server_match,
'duplicate_of': build.duplicate_id if build.state == 'duplicate' else False,
'coverage': build.branch_id.coverage,
'revdep_build_ids': sorted(build.revdep_build_ids, key=lambda x: x.repo_id.name),
}
@http.route(['/runbot', '/runbot/repo/<model("runbot.repo"):repo>'], website=True, auth='public', type='http')
def repo(self, repo=None, search='', limit='100', refresh='', **kwargs):
branch_obj = request.env['runbot.branch']
build_obj = request.env['runbot.build']
repo_obj = request.env['runbot.repo']
repo_ids = repo_obj.search([])
repos = repo_obj.browse(repo_ids)
if not repo and repos:
repo = repos[0].id
context = {
'repos': repos.ids,
'repo': repo,
'host_stats': [],
'pending_total': build_obj.search_count([('state', '=', 'pending')]),
'limit': limit,
'search': search,
'refresh': refresh,
}
build_ids = []
if repo:
filters = {key: kwargs.get(key, '1') for key in ['pending', 'testing', 'running', 'done', 'deathrow']}
domain = [('repo_id', '=', repo.id)]
domain += [('state', '!=', key) for key, value in iter(filters.items()) if value == '0']
if search:
domain += ['|', '|', ('dest', 'ilike', search), ('subject', 'ilike', search), ('branch_id.branch_name', 'ilike', search)]
build_ids = build_obj.search(domain, limit=int(limit))
branch_ids, build_by_branch_ids = [], {}
if build_ids:
branch_query = """
SELECT br.id FROM runbot_branch br INNER JOIN runbot_build bu ON br.id=bu.branch_id WHERE bu.id in %s
ORDER BY bu.sequence DESC
"""
sticky_dom = [('repo_id', '=', repo.id), ('sticky', '=', True)]
sticky_branch_ids = [] if search else branch_obj.search(sticky_dom).ids
request._cr.execute(branch_query, (tuple(build_ids.ids),))
branch_ids = uniq_list(sticky_branch_ids + [br[0] for br in request._cr.fetchall()])
build_query = """
SELECT
branch_id,
max(case when br_bu.row = 1 then br_bu.build_id end),
max(case when br_bu.row = 2 then br_bu.build_id end),
max(case when br_bu.row = 3 then br_bu.build_id end),
max(case when br_bu.row = 4 then br_bu.build_id end)
FROM (
SELECT
br.id AS branch_id,
bu.id AS build_id,
row_number() OVER (PARTITION BY branch_id) AS row
FROM
runbot_branch br INNER JOIN runbot_build bu ON br.id=bu.branch_id
WHERE
br.id in %s
GROUP BY br.id, bu.id
ORDER BY br.id, bu.id DESC
) AS br_bu
WHERE
row <= 4
GROUP BY br_bu.branch_id;
"""
request._cr.execute(build_query, (tuple(branch_ids),))
build_by_branch_ids = {
rec[0]: [r for r in rec[1:] if r is not None] for rec in request._cr.fetchall()
}
branches = branch_obj.browse(branch_ids)
build_ids = flatten(build_by_branch_ids.values())
build_dict = {build.id: build for build in build_obj.browse(build_ids)}
def branch_info(branch):
return {
'branch': branch,
'builds': [self.build_info(build_dict[build_id]) for build_id in build_by_branch_ids.get(branch.id) or []]
}
context.update({
'branches': [branch_info(b) for b in branches],
'testing': build_obj.search_count([('repo_id', '=', repo.id), ('state', '=', 'testing')]),
'running': build_obj.search_count([('repo_id', '=', repo.id), ('state', '=', 'running')]),
'pending': build_obj.search_count([('repo_id', '=', repo.id), ('state', '=', 'pending')]),
'qu': QueryURL('/runbot/repo/' + slug(repo), search=search, limit=limit, refresh=refresh, **filters),
'filters': filters,
})
# consider host gone if no build in last 100
build_threshold = max(build_ids or [0]) - 100
for result in build_obj.read_group([('id', '>', build_threshold)], ['host'], ['host']):
if result['host']:
context['host_stats'].append({
'host': result['host'],
'testing': build_obj.search_count([('state', '=', 'testing'), ('host', '=', result['host'])]),
'running': build_obj.search_count([('state', '=', 'running'), ('host', '=', result['host'])]),
})
return http.request.render('runbot.repo', context)
@http.route(['/runbot/build/<int:build_id>/kill'], type='http', auth="user", methods=['POST'], csrf=False)
def build_ask_kill(self, build_id, search=None, **post):
build = request.env['runbot.build'].sudo().browse(build_id)
build._ask_kill()
return werkzeug.utils.redirect('/runbot/repo/%s' % build.repo_id.id + ('?search=%s' % search if search else ''))
@http.route(['/runbot/build/<int:build_id>/force'], type='http', auth="public", methods=['POST'], csrf=False)
def build_force(self, build_id, search=None, **post):
build = request.env['runbot.build'].sudo().browse(build_id)
build._force()
return werkzeug.utils.redirect('/runbot/repo/%s' % build.repo_id.id + ('?search=%s' % search if search else ''))
@http.route(['/runbot/build/<int:build_id>'], type='http', auth="public", website=True)
def build(self, build_id, search=None, **post):
"""Events/Logs"""
Build = request.env['runbot.build']
Logging = request.env['ir.logging']
build = Build.browse([build_id])[0]
if not build.exists():
return request.not_found()
real_build = build.duplicate_id if build.state == 'duplicate' else build
# other builds
build_ids = Build.search([('branch_id', '=', build.branch_id.id)])
other_builds = Build.browse(build_ids)
domain = [('build_id', '=', real_build.id)]
log_type = request.params.get('type', '')
if log_type:
domain.append(('type', '=', log_type))
level = request.params.get('level', '')
if level:
domain.append(('level', '=', level.upper()))
if search:
domain.append(('message', 'ilike', search))
logging_ids = Logging.sudo().search(domain)
context = {
'repo': build.repo_id,
'build': self.build_info(build),
'br': {'branch': build.branch_id},
'logs': Logging.sudo().browse(logging_ids).ids,
'other_builds': other_builds.ids
}
return request.render("runbot.build", context)
@http.route(['/runbot/b/<branch_name>', '/runbot/<model("runbot.repo"):repo>/<branch_name>'], type='http', auth="public", website=True)
def fast_launch(self, branch_name=False, repo=False, **post):
"""Connect to the running Odoo instance"""
Build = request.env['runbot.build']
domain = [('branch_id.branch_name', '=', branch_name)]
if repo:
domain.extend([('branch_id.repo_id', '=', repo.id)])
order = "sequence desc"
else:
order = 'repo_id ASC, sequence DESC'
# Take the 10 lasts builds to find at least 1 running... Else no luck
builds = Build.search(domain, order=order, limit=10)
if builds:
last_build = False
for build in builds:
if build.state == 'running' or (build.state == 'duplicate' and build.duplicate_id.state == 'running'):
last_build = build if build.state == 'running' else build.duplicate_id
break
if not last_build:
# Find the last build regardless the state to propose a rebuild
last_build = builds[0]
if last_build.state != 'running':
url = "/runbot/build/%s?ask_rebuild=1" % last_build.id
else:
url = build.branch_id._get_branch_quickconnect_url(last_build.domain, last_build.dest)[build.branch_id.id]
else:
return request.not_found()
return werkzeug.utils.redirect(url)
@http.route(['/runbot/dashboard'], type='http', auth="public", website=True)
def dashboard(self, refresh=None):
cr = request.cr
RB = request.env['runbot.build']
repos = request.env['runbot.repo'].search([]) # respect record rules
cr.execute("""SELECT bu.id
FROM runbot_branch br
JOIN LATERAL (SELECT *
FROM runbot_build bu
WHERE bu.branch_id = br.id
ORDER BY id DESC
LIMIT 3
) bu ON (true)
JOIN runbot_repo r ON (r.id = br.repo_id)
WHERE br.sticky
AND br.repo_id in %s
ORDER BY r.sequence, r.name, br.branch_name, bu.id DESC
""", [tuple(repos._ids)])
builds = RB.browse(map(operator.itemgetter(0), cr.fetchall()))
count = RB.search_count
qctx = {
'refresh': refresh,
'host_stats': [],
'pending_total': count([('state', '=', 'pending')]),
}
repos_values = qctx['repo_dict'] = OrderedDict()
for build in builds:
repo = build.repo_id
branch = build.branch_id
r = repos_values.setdefault(repo.id, {'branches': OrderedDict()})
if 'name' not in r:
r.update({
'name': repo.name,
'base': repo.base,
'testing': count([('repo_id', '=', repo.id), ('state', '=', 'testing')]),
'running': count([('repo_id', '=', repo.id), ('state', '=', 'running')]),
'pending': count([('repo_id', '=', repo.id), ('state', '=', 'pending')]),
})
b = r['branches'].setdefault(branch.id, {'name': branch.branch_name, 'builds': list()})
b['builds'].append(self.build_info(build))
# consider host gone if no build in last 100
build_threshold = max(builds.ids or [0]) - 100
for result in RB.read_group([('id', '>', build_threshold)], ['host'], ['host']):
if result['host']:
qctx['host_stats'].append({
'host': result['host'],
'testing': count([('state', '=', 'testing'), ('host', '=', result['host'])]),
'running': count([('state', '=', 'running'), ('host', '=', result['host'])]),
})
return request.render("runbot.sticky-dashboard", qctx)