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
32 changes: 20 additions & 12 deletions runbot/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,22 +63,30 @@

'assets': {
'web.assets_backend': [
'runbot/static/src/libs/diff_match_patch/diff_match_patch.js',
'runbot/static/libs/diff_match_patch/diff_match_patch.js',
'runbot/static/src/js/views/**/*',
'runbot/static/src/js/fields/*',
'runbot/static/src/diff_match_patch_module.js',
'runbot/static/src/fields/*',
],
'runbot.assets_frontend': [
'/web/static/lib/bootstrap/dist/css/bootstrap.css',
'/web/static/src/libs/fontawesome/css/font-awesome.css',
'/runbot/static/src/css/runbot.css',
('include', 'web.assets_frontend_minimal'), # Pray the gods this stays named correctly

'/web/static/lib/jquery/jquery.js',
'/web/static/lib/popper/popper.js',
#'/web/static/lib/bootstrap/js/dist/util.js',
'/web/static/lib/bootstrap/js/dist/dropdown.js',
'/web/static/lib/bootstrap/js/dist/collapse.js',
'/runbot/static/src/js/runbot.js',
],
'runbot/static/libs/bootstrap/css/bootstrap.css',
'runbot/static/libs/fontawesome/css/font-awesome.css',
'runbot/static/src/css/runbot.css',
'runbot/static/libs/jquery/jquery.js',
'runbot/static/libs/popper/popper.js',
'runbot/static/libs/bootstrap/js/bootstrap.bundle.js',

'runbot/static/libs/owl.js',
'runbot/static/src/owl_module.js',

'runbot/static/src/vendored/**/*', # Vendored files coming from odoo modules

'runbot/static/src/frontend/root.js',
'runbot/static/src/frontend/runbot.js',
'runbot/static/src/frontend/**/*',
]
},
'post_load': 'runbot_post_load',
}
1 change: 1 addition & 0 deletions runbot/controllers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
from . import frontend
from . import hook
from . import badge
from . import public_api
18 changes: 18 additions & 0 deletions runbot/controllers/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@
_logger = logging.getLogger(__name__)


def supports_owl_frontend(method):
""" Marks a route as working with frontend client. """
@functools.wraps(method)
def _wrapped(*args, **kwargs):
if request.httprequest.cookies.get('use_owl_client', '0') == '1':
return request.render('runbot.frontend_spa', {
'projects': request.env['runbot.project'].search([('hidden', '=', False)]),
'categories': request.env['runbot.category'].search([]),
'default_category': request.env['ir.model.data']._xmlid_to_res_id('runbot.default_category'),
'session_info': request.env['ir.http'].session_info(),
'error_count': request.env['runbot.build.error'].search_count([]),
'error_assigned_count': request.env['runbot.build.error'].search_count([('responsible', '=', request.env.user.id)]),
'error_team_count': request.env['runbot.build.error'].search_count([('responsible', '=', False), ('team_id', 'in', request.env.user.runbot_team_ids.ids)]),
})
return method(*args, **kwargs)
return _wrapped

def route(routes, **kw):
def decorator(f):
@o_route(routes, **kw)
Expand Down Expand Up @@ -107,6 +124,7 @@ def submit(self, more=False, redirect='/', keep_search=False, category=False, fi
'/runbot',
'/runbot/<model("runbot.project"):project>',
'/runbot/<model("runbot.project"):project>/search/<search>'], website=True, auth='public', type='http')
@supports_owl_frontend
def bundles(self, project=None, search='', projects=False, refresh=False, for_next_freeze=False, limit=40, has_pr=None, **kwargs):
search = search if len(search) < 60 else search[:60]
env = request.env
Expand Down
89 changes: 89 additions & 0 deletions runbot/controllers/public_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import json

from werkzeug.exceptions import BadRequest, Forbidden

from odoo.exceptions import AccessError
from odoo.http import Controller, request, route
from odoo.tools import mute_logger

from odoo.addons.runbot.models.public_model_mixin import PublicModelMixin


class PublicApi(Controller):

@mute_logger('odoo.addons.base.models.ir_model') # We don't care about logging acl errors
def _get_model(self, model: str) -> PublicModelMixin:
"""
Returns the model from a model string.

Raises the appropriate exception if:
- The model does not exist
- The model is not a public model
- The current user can not read the model
"""
pool = request.env.registry
try:
Model = pool[model]
except KeyError:
raise BadRequest('Unknown model')
if not issubclass(Model, pool['runbot.public.model.mixin']):
raise BadRequest('Unknown model')
Model = request.env[model]
Model.check_access('read')
if not Model._api_request_allow_direct_access():
raise Forbidden('This model does not allow direct access')
return Model

@route('/runbot/api/models', auth='public', methods=['GET'], readonly=True)
def models(self):
models = []
for model in request.env.keys():
try:
models.append(self._get_model(model))
except (BadRequest, AccessError, Forbidden):
pass
return request.make_json_response(
[Model._name for Model in models]
)

@route('/runbot/api/<model>/read', auth='public', methods=['POST'], readonly=True, csrf=False)
def read(self, *, model: str):
Model = self._get_model(model)
required_keys = Model._api_request_required_keys()
allowed_keys = Model._api_request_allowed_keys()
try:
data = request.get_json_data()
except json.JSONDecodeError:
raise BadRequest('Invalid payload, missing or malformed json')
if not isinstance(data, dict):
raise BadRequest('Invalid payload, should be a dict.')
if (missing_keys := required_keys - set(data.keys())):
raise BadRequest(f'Invalid payload, missing keys: {", ".join(missing_keys)}')
if (unknown_keys := set(data.keys()) - allowed_keys):
raise BadRequest(f'Invalid payload, unknown keys: {", ".join(unknown_keys)}')
if 'context' in data:
Model = Model.with_context(**data['context'])
if Model._api_request_requires_project():
if not isinstance(data['project_id'], int):
raise BadRequest('Invalid project_id, should be an int')
# This is an additional layer of protection for project_id
project = request.env['runbot.project'].browse(data['project_id']).exists()
if not project:
raise BadRequest('Unknown project_id')
project.check_access('read')
Model = Model.with_context(project_id=project.id)
return request.make_json_response(Model._api_request_read(data))

@route('/runbot/api/<model>/spec', auth='public', methods=['GET'], readonly=True)
def spec(self, *, model: str):
Model = self._get_model(model)
required_keys = Model._api_request_required_keys()
allowed_keys = Model._api_request_allowed_keys()
return request.make_json_response({
'requires_project': Model._api_request_requires_project(),
'default_page_size': Model._api_request_default_limit(),
'max_page_size': Model._api_request_max_limit(),
'required_keys': list(Model._api_request_required_keys()),
'allowed_keys': list(allowed_keys - required_keys),
'specification': self._get_model(model)._api_public_specification(),
})
2 changes: 2 additions & 0 deletions runbot/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-

from . import public_model_mixin

from . import batch
from . import branch
from . import build
Expand Down
32 changes: 21 additions & 11 deletions runbot/models/batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@
class Batch(models.Model):
_name = 'runbot.batch'
_description = "Bundle batch"
_inherit = ['runbot.public.model.mixin']

last_update = fields.Datetime('Last ref update')
last_update = fields.Datetime('Last ref update', public=True)
bundle_id = fields.Many2one('runbot.bundle', required=True, index=True, ondelete='cascade')
commit_link_ids = fields.Many2many('runbot.commit.link')
commit_link_ids = fields.Many2many('runbot.commit.link', public=True)
commit_ids = fields.Many2many('runbot.commit', compute='_compute_commit_ids')
slot_ids = fields.One2many('runbot.batch.slot', 'batch_id')
slot_ids = fields.One2many('runbot.batch.slot', 'batch_id', public=True)
all_build_ids = fields.Many2many('runbot.build', compute='_compute_all_build_ids', help="Recursive builds")
state = fields.Selection([('preparing', 'Preparing'), ('ready', 'Ready'), ('done', 'Done'), ('skipped', 'Skipped')])
state = fields.Selection([('preparing', 'Preparing'), ('ready', 'Ready'), ('done', 'Done'), ('skipped', 'Skipped')], public=True)
hidden = fields.Boolean('Hidden', default=False)
age = fields.Integer(compute='_compute_age', string='Build age')
age = fields.Integer(compute='_compute_age', string='Build age', public=True)
category_id = fields.Many2one('runbot.category', index=True, default=lambda self: self.env.ref('runbot.default_category', raise_if_not_found=False))
log_ids = fields.One2many('runbot.batch.log', 'batch_id')
has_warning = fields.Boolean("Has warning")
Expand All @@ -34,6 +35,10 @@ class Batch(models.Model):
column2='referenced_batch_id',
)

@api.model
def _api_project_id_field_path(self):
return 'bundle_id.project_id'

@api.depends('slot_ids.build_id')
def _compute_all_build_ids(self):
all_builds = self.env['runbot.build'].search([('id', 'child_of', self.slot_ids.build_id.ids)])
Expand Down Expand Up @@ -522,20 +527,25 @@ class BatchSlot(models.Model):
_name = 'runbot.batch.slot'
_description = 'Link between a bundle batch and a build'
_order = 'trigger_id,id'
_inherit = ['runbot.public.model.mixin']

batch_id = fields.Many2one('runbot.batch', index=True)
trigger_id = fields.Many2one('runbot.trigger', index=True)
build_id = fields.Many2one('runbot.build', index=True)
all_build_ids = fields.Many2many('runbot.build', compute='_compute_all_build_ids')
batch_id = fields.Many2one('runbot.batch', index=True, public=True)
trigger_id = fields.Many2one('runbot.trigger', index=True, public=True)
build_id = fields.Many2one('runbot.build', index=True, public=True)
all_build_ids = fields.Many2many('runbot.build', compute='_compute_all_build_ids', public=True)
params_id = fields.Many2one('runbot.build.params', index=True, required=True)
link_type = fields.Selection([('created', 'Build created'), ('matched', 'Existing build matched'), ('rebuild', 'Rebuild')], required=True) # rebuild type?
active = fields.Boolean('Attached', default=True)
link_type = fields.Selection([('created', 'Build created'), ('matched', 'Existing build matched'), ('rebuild', 'Rebuild')], required=True, public=True) # rebuild type?
active = fields.Boolean('Attached', default=True, public=True)
skipped = fields.Boolean('Skipped', default=False)
# rebuild, what to do: since build can be in multiple batch:
# - replace for all batch?
# - only available on batch and replace for batch only?
# - create a new bundle batch will new linked build?

@api.model
def _api_request_allow_direct_access(self):
return False

@api.depends('build_id')
def _compute_all_build_ids(self):
all_builds = self.env['runbot.build'].search([('id', 'child_of', self.build_id.ids)])
Expand Down
13 changes: 9 additions & 4 deletions runbot/models/branch.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ class Branch(models.Model):
_description = "Branch"
_order = 'name'
_rec_name = 'dname'
_inherit = ['runbot.public.model.mixin']

_sql_constraints = [('branch_repo_uniq', 'unique (name,remote_id)', 'The branch must be unique per repository !')]

name = fields.Char('Name', required=True)
name = fields.Char('Name', required=True, public=True)
remote_id = fields.Many2one('runbot.remote', 'Remote', required=True, ondelete='cascade', index=True)

head = fields.Many2one('runbot.commit', 'Head Commit', index=True)
Expand All @@ -25,7 +26,7 @@ class Branch(models.Model):
reference_name = fields.Char(compute='_compute_reference_name', string='Bundle name', store=True)
bundle_id = fields.Many2one('runbot.bundle', 'Bundle', ondelete='cascade', index=True)

is_pr = fields.Boolean('IS a pr', required=True)
is_pr = fields.Boolean('IS a pr', required=True, public=True)
pr_title = fields.Char('Pr Title')
pr_body = fields.Char('Pr Body')
pr_author = fields.Char('Pr Author')
Expand All @@ -37,12 +38,16 @@ class Branch(models.Model):

reflog_ids = fields.One2many('runbot.ref.log', 'branch_id')

branch_url = fields.Char(compute='_compute_branch_url', string='Branch url', readonly=True)
dname = fields.Char('Display name', compute='_compute_dname', search='_search_dname')
branch_url = fields.Char(compute='_compute_branch_url', string='Branch url', readonly=True, public=True)
dname = fields.Char('Display name', compute='_compute_dname', search='_search_dname', public=True)

alive = fields.Boolean('Alive', default=True)
draft = fields.Boolean('Draft', store=True)

@api.model
def _api_project_id_field_path(self):
return 'bundle_id.project_id'

@api.depends('name', 'remote_id.short_name')
def _compute_dname(self):
for branch in self:
Expand Down
Loading