bitbake: toastergui: convert project builds page to ToasterTable

Use the all builds ToasterTable as the basis for the project builds
ToasterTable.

[YOCTO #8738]

(Bitbake rev: 87bcfb740dd2d9944e35a2a1f71cbf8ff3b266e9)

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
Signed-off-by: Ed Bartosh <ed.bartosh@linux.intel.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
This commit is contained in:
Elliot Smith
2016-01-15 13:00:55 +02:00
committed by Richard Purdie
parent 33b011c158
commit eaae82a19a
9 changed files with 239 additions and 49 deletions

View File

@@ -7,7 +7,10 @@ function projectTopBarInit(ctx) {
var projectName = $("#project-name"); var projectName = $("#project-name");
var projectNameFormToggle = $("#project-change-form-toggle"); var projectNameFormToggle = $("#project-change-form-toggle");
var projectNameChangeCancel = $("#project-name-change-cancel"); var projectNameChangeCancel = $("#project-name-change-cancel");
// this doesn't exist for command-line builds
var newBuildTargetInput = $("#build-input"); var newBuildTargetInput = $("#build-input");
var newBuildTargetBuildBtn = $("#build-button"); var newBuildTargetBuildBtn = $("#build-button");
var selectedTarget; var selectedTarget;
@@ -42,6 +45,12 @@ function projectTopBarInit(ctx) {
$(this).parent().removeClass('active'); $(this).parent().removeClass('active');
}); });
if (!newBuildTargetInput.length) {
return;
}
/* the following only applies for non-command-line projects */
/* Recipe build input functionality */ /* Recipe build input functionality */
if (ctx.numProjectLayers > 0 && ctx.machine){ if (ctx.numProjectLayers > 0 && ctx.machine){
newBuildTargetInput.removeAttr("disabled"); newBuildTargetInput.removeAttr("disabled");

View File

@@ -33,6 +33,10 @@ function tableInit(ctx){
loadData(tableParams); loadData(tableParams);
// clicking on this set of elements removes the search
var clearSearchElements = $('.remove-search-btn-'+ctx.tableName +
', .show-all-'+ctx.tableName);
function loadData(tableParams){ function loadData(tableParams){
$.ajax({ $.ajax({
type: "GET", type: "GET",
@@ -62,9 +66,9 @@ function tableInit(ctx){
paginationBtns.html(""); paginationBtns.html("");
if (tableParams.search) if (tableParams.search)
$('.remove-search-btn-'+ctx.tableName).show(); clearSearchElements.show();
else else
$('.remove-search-btn-'+ctx.tableName).hide(); clearSearchElements.hide();
$('.table-count-' + ctx.tableName).text(tableData.total); $('.table-count-' + ctx.tableName).text(tableData.total);
tableTotal = tableData.total; tableTotal = tableData.total;
@@ -230,9 +234,8 @@ function tableInit(ctx){
} else { } else {
/* Not orderable */ /* Not orderable */
header.addClass("muted");
header.css("font-weight", "normal"); header.css("font-weight", "normal");
header.append(col.title+' '); header.append('<span class="muted">' + col.title + '</span> ');
} }
/* Setup the filter button */ /* Setup the filter button */
@@ -665,7 +668,7 @@ function tableInit(ctx){
loadData(tableParams); loadData(tableParams);
}); });
$('.remove-search-btn-'+ctx.tableName).click(function(e){ clearSearchElements.click(function(e){
e.preventDefault(); e.preventDefault();
tableParams.page = 1; tableParams.page = 1;

View File

@@ -23,9 +23,11 @@ from toastergui.widgets import ToasterTable
from toastergui.querysetfilter import QuerysetFilter from toastergui.querysetfilter import QuerysetFilter
from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project
from orm.models import CustomImageRecipe, Package, Build, LogMessage, Task from orm.models import CustomImageRecipe, Package, Build, LogMessage, Task
from orm.models import ProjectTarget
from django.db.models import Q, Max, Count from django.db.models import Q, Max, Count
from django.conf.urls import url from django.conf.urls import url
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse, resolve
from django.http import HttpResponse
from django.views.generic import TemplateView from django.views.generic import TemplateView
import itertools import itertools
@@ -775,7 +777,7 @@ class ProjectsTable(ToasterTable):
''' '''
errors_template = ''' errors_template = '''
{% if data.get_number_of_builds > 0 %} {% if data.get_number_of_builds > 0 and data.get_last_errors > 0 %}
<a class="errors.count error" <a class="errors.count error"
href="{% url "builddashboard" data.get_last_build_id %}#errors"> href="{% url "builddashboard" data.get_last_build_id %}#errors">
{{data.get_last_errors}} error{{data.get_last_errors | pluralize}} {{data.get_last_errors}} error{{data.get_last_errors | pluralize}}
@@ -784,7 +786,7 @@ class ProjectsTable(ToasterTable):
''' '''
warnings_template = ''' warnings_template = '''
{% if data.get_number_of_builds > 0 %} {% if data.get_number_of_builds > 0 and data.get_last_warnings > 0 %}
<a class="warnings.count warning" <a class="warnings.count warning"
href="{% url "builddashboard" data.get_last_build_id %}#warnings"> href="{% url "builddashboard" data.get_last_build_id %}#warnings">
{{data.get_last_warnings}} warning{{data.get_last_warnings | pluralize}} {{data.get_last_warnings}} warning{{data.get_last_warnings | pluralize}}
@@ -886,30 +888,45 @@ class BuildsTable(ToasterTable):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(BuildsTable, self).__init__(*args, **kwargs) super(BuildsTable, self).__init__(*args, **kwargs)
self.default_orderby = '-completed_on' self.default_orderby = '-completed_on'
self.title = 'All builds'
self.static_context_extra['Build'] = Build self.static_context_extra['Build'] = Build
self.static_context_extra['Task'] = Task self.static_context_extra['Task'] = Task
# attributes that are overridden in subclasses
# title for the page
self.title = ''
# 'project' or 'all'; determines how the mrb (most recent builds)
# section is displayed
self.mrb_type = ''
def get_builds(self):
"""
overridden in ProjectBuildsTable to return builds for a
single project
"""
return Build.objects.all()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(BuildsTable, self).get_context_data(**kwargs) context = super(BuildsTable, self).get_context_data(**kwargs)
# for the latest builds section # for the latest builds section
queryset = Build.objects.all() builds = self.get_builds()
finished_criteria = Q(outcome=Build.SUCCEEDED) | Q(outcome=Build.FAILED) finished_criteria = Q(outcome=Build.SUCCEEDED) | Q(outcome=Build.FAILED)
latest_builds = itertools.chain( latest_builds = itertools.chain(
queryset.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"), builds.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"),
queryset.filter(finished_criteria).order_by("-completed_on")[:3] builds.filter(finished_criteria).order_by("-completed_on")[:3]
) )
context['mru'] = list(latest_builds) context['mru'] = list(latest_builds)
context['mrb_type'] = 'all' context['mrb_type'] = self.mrb_type
return context return context
def setup_queryset(self, *args, **kwargs): def setup_queryset(self, *args, **kwargs):
queryset = Build.objects.all() queryset = self.get_builds()
# don't include in progress builds # don't include in progress builds
queryset = queryset.exclude(outcome=Build.IN_PROGRESS) queryset = queryset.exclude(outcome=Build.IN_PROGRESS)
@@ -949,7 +966,8 @@ class BuildsTable(ToasterTable):
{% if data.cooker_log_path %} {% if data.cooker_log_path %}
&nbsp; &nbsp;
<a href="{% url "build_artifact" data.id "cookerlog" data.id %}"> <a href="{% url "build_artifact" data.id "cookerlog" data.id %}">
<i class="icon-download-alt" title="Download build log"></i> <i class="icon-download-alt get-help"
data-original-title="Download build log"></i>
</a> </a>
{% endif %} {% endif %}
''' '''
@@ -1031,19 +1049,6 @@ class BuildsTable(ToasterTable):
{% endif %} {% endif %}
''' '''
project_template = '''
{% load project_url_tag %}
<a href="{% project_url data.project %}">
{{data.project.name}}
</a>
{% if data.project.is_default %}
<i class="icon-question-sign get-help hover-help" title=""
data-original-title="This project shows information about
the builds you start from the command line while Toaster is
running" style="visibility: hidden;"></i>
{% endif %}
'''
self.add_column(title='Outcome', self.add_column(title='Outcome',
help_text='Final state of the build (successful \ help_text='Final state of the build (successful \
or failed)', or failed)',
@@ -1098,16 +1103,16 @@ class BuildsTable(ToasterTable):
help_text='The number of errors encountered during \ help_text='The number of errors encountered during \
the build (if any)', the build (if any)',
hideable=True, hideable=True,
orderable=False, orderable=True,
static_data_name='errors', static_data_name='errors_no',
static_data_template=errors_template) static_data_template=errors_template)
self.add_column(title='Warnings', self.add_column(title='Warnings',
help_text='The number of warnings encountered during \ help_text='The number of warnings encountered during \
the build (if any)', the build (if any)',
hideable=True, hideable=True,
orderable=False, orderable=True,
static_data_name='warnings', static_data_name='warnings_no',
static_data_template=warnings_template) static_data_template=warnings_template)
self.add_column(title='Time', self.add_column(title='Time',
@@ -1125,12 +1130,6 @@ class BuildsTable(ToasterTable):
static_data_name='image_files', static_data_name='image_files',
static_data_template=image_files_template) static_data_template=image_files_template)
self.add_column(title='Project',
hideable=True,
orderable=False,
static_data_name='project-name',
static_data_template=project_template)
def setup_filters(self, *args, **kwargs): def setup_filters(self, *args, **kwargs):
# outcomes # outcomes
outcome_filter = TableFilter( outcome_filter = TableFilter(
@@ -1239,3 +1238,122 @@ class BuildsTable(ToasterTable):
failed_tasks_filter.add_action(with_failed_tasks_action) failed_tasks_filter.add_action(with_failed_tasks_action)
failed_tasks_filter.add_action(without_failed_tasks_action) failed_tasks_filter.add_action(without_failed_tasks_action)
self.add_filter(failed_tasks_filter) self.add_filter(failed_tasks_filter)
def post(self, request, *args, **kwargs):
""" Process HTTP POSTs which make build requests """
project = Project.objects.get(pk=kwargs['pid'])
if 'buildCancel' in request.POST:
for i in request.POST['buildCancel'].strip().split(" "):
try:
br = BuildRequest.objects.select_for_update().get(project = project, pk = i, state__lte = BuildRequest.REQ_QUEUED)
br.state = BuildRequest.REQ_DELETED
br.save()
except BuildRequest.DoesNotExist:
pass
if 'buildDelete' in request.POST:
for i in request.POST['buildDelete'].strip().split(" "):
try:
BuildRequest.objects.select_for_update().get(project = project, pk = i, state__lte = BuildRequest.REQ_DELETED).delete()
except BuildRequest.DoesNotExist:
pass
if 'targets' in request.POST:
ProjectTarget.objects.filter(project = project).delete()
s = str(request.POST['targets'])
for t in s.translate(None, ";%|\"").split(" "):
if ":" in t:
target, task = t.split(":")
else:
target = t
task = ""
ProjectTarget.objects.create(project = project,
target = target,
task = task)
project.schedule_build()
# redirect back to builds page so any new builds in progress etc.
# are visible
response = HttpResponse()
response.status_code = 302
response['Location'] = request.build_absolute_uri()
return response
class AllBuildsTable(BuildsTable):
""" Builds page for all builds """
def __init__(self, *args, **kwargs):
super(AllBuildsTable, self).__init__(*args, **kwargs)
self.title = 'All builds'
self.mrb_type = 'all'
def setup_columns(self, *args, **kwargs):
"""
All builds page shows a column for the project
"""
super(AllBuildsTable, self).setup_columns(*args, **kwargs)
project_template = '''
{% load project_url_tag %}
<a href="{% project_url data.project %}">
{{data.project.name}}
</a>
{% if data.project.is_default %}
<i class="icon-question-sign get-help hover-help" title=""
data-original-title="This project shows information about
the builds you start from the command line while Toaster is
running" style="visibility: hidden;"></i>
{% endif %}
'''
self.add_column(title='Project',
hideable=True,
orderable=True,
static_data_name='project',
static_data_template=project_template)
class ProjectBuildsTable(BuildsTable):
"""
Builds page for a single project; a BuildsTable, with the queryset
filtered by project
"""
def __init__(self, *args, **kwargs):
super(ProjectBuildsTable, self).__init__(*args, **kwargs)
self.title = 'All project builds'
self.mrb_type = 'project'
# set from the querystring
self.project_id = None
def setup_queryset(self, *args, **kwargs):
"""
NOTE: self.project_id must be set before calling super(),
as it's used in setup_queryset()
"""
self.project_id = kwargs['pid']
super(ProjectBuildsTable, self).setup_queryset(*args, **kwargs)
project = Project.objects.get(pk=self.project_id)
self.queryset = self.queryset.filter(project=project)
def get_context_data(self, **kwargs):
"""
NOTE: self.project_id must be set before calling super(),
as it's used in get_context_data()
"""
self.project_id = kwargs['pid']
context = super(ProjectBuildsTable, self).get_context_data(**kwargs)
context['project'] = Project.objects.get(pk=self.project_id)
return context
def get_builds(self):
""" override: only return builds for the relevant project """
project = Project.objects.get(pk=self.project_id)
return Build.objects.filter(project=project)

View File

@@ -1,4 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load projecttags %} {% load projecttags %}
{% load humanize %} {% load humanize %}

View File

@@ -6,7 +6,7 @@
{%if mru and mru.count > 0%} {%if mru and mru.count > 0%}
{%if mrb_type == 'project' %} {%if mrb_type == 'project' %}
<h2> <h2 class="page-header">
Latest project builds Latest project builds
{% if project.is_default %} {% if project.is_default %}

View File

@@ -0,0 +1,56 @@
{% extends 'base.html' %}
{% load static %}
{% block extraheadcontent %}
<link rel="stylesheet" href="{% static 'css/jquery-ui.min.css' %}" type='text/css'>
<link rel="stylesheet" href="{% static 'css/jquery-ui.structure.min.css' %}" type='text/css'>
<link rel="stylesheet" href="{% static 'css/jquery-ui.theme.min.css' %}" type='text/css'>
<script src="{% static 'js/jquery-ui.min.js' %}">
</script>
{% endblock %}
{% block title %} {{title}} - {{project.name}} - Toaster {% endblock %}
{% block pagecontent %}
{% include "projecttopbar.html" %}
<div class="row-fluid">
{% with mru=mru mrb_type=mrb_type %}
{% include 'mrb_section.html' %}
{% endwith %}
<h2 class="page-header top-air" data-role="page-title"></h2>
{% url 'projectbuilds' project.id as xhr_table_url %}
{% include 'toastertable.html' %}
</div>
<script>
$(document).ready(function () {
// title
var tableElt = $("#{{table_name}}");
var titleElt = $("[data-role='page-title']");
tableElt.on("table-done", function (e, total, tableParams) {
var title = "All project builds";
if (tableParams.search || tableParams.filter) {
if (total === 0) {
title = "No project builds found";
}
else if (total > 0) {
title = total + " project build" + (total > 1 ? 's' : '') + " found";
}
}
titleElt.text(title);
});
// highlight builds tab
$("#topbar-builds-tab").addClass("active")
});
</script>
{% endblock %}

View File

@@ -28,7 +28,7 @@ urlpatterns = patterns('toastergui.views',
url(r'^landing/$', 'landing', name='landing'), url(r'^landing/$', 'landing', name='landing'),
url(r'^builds/$', url(r'^builds/$',
tables.BuildsTable.as_view(template_name="builds-toastertable.html"), tables.AllBuildsTable.as_view(template_name="builds-toastertable.html"),
name='all-builds'), name='all-builds'),
# build info navigation # build info navigation
@@ -83,7 +83,9 @@ urlpatterns = patterns('toastergui.views',
url(r'^project/(?P<pid>\d+)/$', 'project', name='project'), url(r'^project/(?P<pid>\d+)/$', 'project', name='project'),
url(r'^project/(?P<pid>\d+)/configuration$', 'projectconf', name='projectconf'), url(r'^project/(?P<pid>\d+)/configuration$', 'projectconf', name='projectconf'),
url(r'^project/(?P<pid>\d+)/builds/$', 'projectbuilds', name='projectbuilds'), url(r'^project/(?P<pid>\d+)/builds/$',
tables.ProjectBuildsTable.as_view(template_name="projectbuilds-toastertable.html"),
name='projectbuilds'),
# the import layer is a project-specific functionality; # the import layer is a project-specific functionality;
url(r'^project/(?P<pid>\d+)/importlayer$', 'importlayer', name='importlayer'), url(r'^project/(?P<pid>\d+)/importlayer$', 'importlayer', name='importlayer'),

View File

@@ -91,6 +91,7 @@ def landing(request):
return render(request, 'landing.html', context) return render(request, 'landing.html', context)
"""
# returns a list for most recent builds; # returns a list for most recent builds;
def _get_latest_builds(prj=None): def _get_latest_builds(prj=None):
queryset = Build.objects.all() queryset = Build.objects.all()
@@ -101,8 +102,9 @@ def _get_latest_builds(prj=None):
return list(itertools.chain( return list(itertools.chain(
queryset.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"), queryset.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"),
queryset.filter(outcome__lt=Build.IN_PROGRESS).order_by("-started_on")[:3] )) queryset.filter(outcome__lt=Build.IN_PROGRESS).order_by("-started_on")[:3] ))
"""
"""
# a JSON-able dict of recent builds; for use in the Project page, xhr_ updates, and other places, as needed # a JSON-able dict of recent builds; for use in the Project page, xhr_ updates, and other places, as needed
def _project_recent_build_list(prj): def _project_recent_build_list(prj):
data = [] data = []
@@ -131,8 +133,7 @@ def _project_recent_build_list(prj):
data.append(d) data.append(d)
return data return data
"""
def objtojson(obj): def objtojson(obj):
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
@@ -1915,6 +1916,7 @@ if True:
''' The exception raised on invalid POST requests ''' ''' The exception raised on invalid POST requests '''
pass pass
"""
# helper function, to be used on "all builds" and "project builds" pages # helper function, to be used on "all builds" and "project builds" pages
def _build_list_helper(request, queryset_all, redirect_page, pid=None): def _build_list_helper(request, queryset_all, redirect_page, pid=None):
default_orderby = 'completed_on:-' default_orderby = 'completed_on:-'
@@ -2119,6 +2121,7 @@ if True:
# merge daterange values # merge daterange values
context.update(context_date) context.update(context_date)
return context, pagesize, orderby return context, pagesize, orderby
"""
@@ -2256,7 +2259,7 @@ if True:
"completedbuilds": Build.objects.exclude(outcome = Build.IN_PROGRESS).filter(project_id = pid), "completedbuilds": Build.objects.exclude(outcome = Build.IN_PROGRESS).filter(project_id = pid),
"prj" : {"name": prj.name, }, "prj" : {"name": prj.name, },
"buildrequests" : prj.build_set.filter(outcome=Build.IN_PROGRESS), "buildrequests" : prj.build_set.filter(outcome=Build.IN_PROGRESS),
"builds" : _project_recent_build_list(prj), #"builds" : _project_recent_build_list(prj),
"layers" : map(lambda x: { "layers" : map(lambda x: {
"id": x.layercommit.pk, "id": x.layercommit.pk,
"orderid": x.pk, "orderid": x.pk,
@@ -2827,10 +2830,8 @@ if True:
# will set the GET parameters and redirect back to the # will set the GET parameters and redirect back to the
# all-builds or projectbuilds page as appropriate; # all-builds or projectbuilds page as appropriate;
# TODO don't use exceptions to control program flow # TODO don't use exceptions to control program flow
@_template_renderer('projectbuilds.html') """
def projectbuilds(request, pid): def projectbuilds(request, pid):
prj = Project.objects.get(id = pid)
if request.method == "POST": if request.method == "POST":
# process any build request # process any build request
@@ -2880,6 +2881,7 @@ if True:
context['mru'] = _get_latest_builds(prj) context['mru'] = _get_latest_builds(prj)
return context return context
"""
def _file_name_for_artifact(b, artifact_type, artifact_id): def _file_name_for_artifact(b, artifact_type, artifact_id):

View File

@@ -61,7 +61,6 @@ class ToasterTable(TemplateView):
self.total_count = 0 self.total_count = 0
self.static_context_extra = {} self.static_context_extra = {}
self.filter_actions = {}
self.empty_state = "Sorry - no data found" self.empty_state = "Sorry - no data found"
self.default_orderby = "" self.default_orderby = ""