bitbake: toaster: move most recent builds templating to client

The most recent builds area of the all builds and project builds
table needs to update as a build progresses. It also needs
additional functionality to show other states (e.g. recipe parsing,
queued) which again needs to update on the client side.

Rather than add to the existing mix of server-side templating
with client-side DOM updating, translate all of the server-side
templates to client-side ones (jsrender), and add logic which
updates the most recent builds area as the state of a build changes.

Add a JSON API for mostrecentbuilds, which returns the state of
all "recent" builds. Fetch this via Ajax from the build dashboard
(rather than fetching the ad hoc API as in the previous version).

Then, as new states for builds are fetched via Ajax, determine
whether the build state has changed completely, or whether the progress
has just updated. If the state completely changed, re-render the
template on the client side for that build. If only the progress
changed, just update the progress bar. (NB this fixes the
task progress bar so it works for the project builds and all builds
pages.)

In cases where the builds table needs to update as the result of
a build finishing, reload the whole page.

This work highlighted a variety of other issues, such as
build requests not being able to change state as necessary. This
was one part of the cause of the "cancelling build..." state
being fragile and disappearing entirely when the page refreshed.
The cancelling state now persists between page reloads, as the
logic for determining whether a build is cancelling is now on
the Build object itself.

Note that jsrender is redistributed as part of Toaster, so
a note was added to LICENSE to that effect.

[YOCTO #9631]

(Bitbake rev: c868ea036aa34b387a72ec5116a66b2cd863995b)

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
This commit is contained in:
Elliot Smith 2016-06-29 15:41:56 +01:00 committed by Richard Purdie
parent c471740f5b
commit 952ffb3e1f
16 changed files with 473 additions and 303 deletions

View File

@ -10,4 +10,6 @@ Foundation and individual contributors.
* Twitter typeahead.js redistributed under the MIT license. Note that the JS source has one small modification, so the full unminified file is currently included to make it obvious where this is.
* jsrender is redistributed under the MIT license.
* QUnit is redistributed under the MIT license.

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bldcontrol', '0004_auto_20160523_1446'),
]
operations = [
migrations.AlterField(
model_name='buildrequest',
name='state',
field=models.IntegerField(choices=[(0, 'created'), (1, 'queued'), (2, 'in progress'), (3, 'failed'), (4, 'deleted'), (5, 'cancelling'), (6, 'completed'), (7, 'archive')], default=0),
),
]

View File

@ -63,20 +63,20 @@ class BuildRequest(models.Model):
REQ_CREATED = 0
REQ_QUEUED = 1
REQ_INPROGRESS = 2
REQ_COMPLETED = 3
REQ_FAILED = 4
REQ_DELETED = 5
REQ_CANCELLING = 6
REQ_FAILED = 3
REQ_DELETED = 4
REQ_CANCELLING = 5
REQ_COMPLETED = 6
REQ_ARCHIVE = 7
REQUEST_STATE = (
(REQ_CREATED, "created"),
(REQ_QUEUED, "queued"),
(REQ_INPROGRESS, "in progress"),
(REQ_COMPLETED, "completed"),
(REQ_FAILED, "failed"),
(REQ_DELETED, "deleted"),
(REQ_CANCELLING, "cancelling"),
(REQ_COMPLETED, "completed"),
(REQ_ARCHIVE, "archive"),
)
@ -91,7 +91,7 @@ class BuildRequest(models.Model):
def __init__(self, *args, **kwargs):
super(BuildRequest, self).__init__(*args, **kwargs)
# Save the old state incase it's about to be modified
# Save the old state in case it's about to be modified
self.old_state = self.state
def save(self, *args, **kwargs):

View File

@ -592,22 +592,42 @@ class Build(models.Model):
return target_labels
def get_current_status(self):
"""
get the status string from the build request if the build
has one, or the text for the build outcome if it doesn't
"""
from bldcontrol.models import BuildRequest
build_request = None
def get_buildrequest(self):
buildrequest = None
if hasattr(self, 'buildrequest'):
build_request = self.buildrequest
buildrequest = self.buildrequest
return buildrequest
if (build_request
and build_request.state != BuildRequest.REQ_INPROGRESS
and self.outcome == Build.IN_PROGRESS):
return self.buildrequest.get_state_display()
def is_queued(self):
from bldcontrol.models import BuildRequest
buildrequest = self.get_buildrequest()
if buildrequest:
return buildrequest.state == BuildRequest.REQ_QUEUED
else:
return False
def is_cancelling(self):
from bldcontrol.models import BuildRequest
buildrequest = self.get_buildrequest()
if buildrequest:
return self.outcome == Build.IN_PROGRESS and \
buildrequest.state == BuildRequest.REQ_CANCELLING
else:
return False
def get_state(self):
"""
Get the state of the build; one of 'Succeeded', 'Failed', 'In Progress',
'Cancelled' (Build outcomes); or 'Queued', 'Cancelling' (states
dependent on the BuildRequest state).
This works around the fact that we have BuildRequest states as well
as Build states, but really we just want to know the state of the build.
"""
if self.is_cancelling():
return 'Cancelling';
elif self.is_queued():
return 'Queued'
else:
return self.get_outcome_text()

View File

@ -97,13 +97,13 @@ class TestAllBuildsPage(SeleniumTestCase):
self.get(url)
# shouldn't see a rebuild button for command-line builds
selector = 'div[data-latest-build-result="%s"] a.run-again-btn' % default_build.id
selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % default_build.id
run_again_button = self.find_all(selector)
self.assertEqual(len(run_again_button), 0,
'should not see a rebuild button for cli builds')
# should see a rebuild button for non-command-line builds
selector = 'div[data-latest-build-result="%s"] a.run-again-btn' % build1.id
selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % build1.id
run_again_button = self.find_all(selector)
self.assertEqual(len(run_again_button), 1,
'should see a rebuild button for non-cli builds')

View File

@ -27,7 +27,10 @@ from bldcontrol import bbcontroller
from django.http import HttpResponse, JsonResponse
from django.views.generic import View
from django.core.urlresolvers import reverse
from django.core import serializers
from django.utils import timezone
from django.template.defaultfilters import date
from toastergui.templatetags.projecttags import json, sectohms, get_tasks
def error_response(error):
return JsonResponse({"error": error})
@ -208,3 +211,103 @@ class XhrLayer(View):
"error": "ok",
"redirect": reverse('project', args=(kwargs['pid'],))
})
class MostRecentBuildsView(View):
def _was_yesterday_or_earlier(self, completed_on):
now = timezone.now()
delta = now - completed_on
if delta.days >= 1:
return True
return False
def get(self, request, *args, **kwargs):
"""
Returns a list of builds in JSON format.
"""
mrb_type = 'all'
project = None
project_id = request.GET.get('project_id', None)
if project_id:
try:
mrb_type = 'project'
project = Project.objects.get(pk=project_id)
except:
# if project lookup fails, assume no project
pass
recent_build_objs = Build.get_recent(project)
recent_builds = []
# for timezone conversion
tz = timezone.get_current_timezone()
for build_obj in recent_build_objs:
dashboard_url = reverse('builddashboard', args=(build_obj.pk,))
buildtime_url = reverse('buildtime', args=(build_obj.pk,))
rebuild_url = \
reverse('xhr_buildrequest', args=(build_obj.project.pk,))
cancel_url = \
reverse('xhr_buildrequest', args=(build_obj.project.pk,))
build = {}
build['id'] = build_obj.pk
build['dashboard_url'] = dashboard_url
tasks_complete_percentage = 0
if build_obj.outcome in (Build.SUCCEEDED, Build.FAILED):
tasks_complete_percentage = 100
elif build_obj.outcome == Build.IN_PROGRESS:
tasks_complete_percentage = build_obj.completeper()
build['tasks_complete_percentage'] = tasks_complete_percentage
build['state'] = build_obj.get_state()
build['errors'] = build_obj.errors.count()
build['dashboard_errors_url'] = dashboard_url + '#errors'
build['warnings'] = build_obj.warnings.count()
build['dashboard_warnings_url'] = dashboard_url + '#warnings'
build['buildtime'] = sectohms(build_obj.timespent_seconds)
build['buildtime_url'] = buildtime_url
build['rebuild_url'] = rebuild_url
build['cancel_url'] = cancel_url
build['is_default_project_build'] = build_obj.project.is_default
build['build_targets_json'] = \
json(get_tasks(build_obj.target_set.all()))
# convert completed_on time to user's timezone
completed_on = timezone.localtime(build_obj.completed_on)
completed_on_template = '%H:%M'
if self._was_yesterday_or_earlier(completed_on):
completed_on_template = '%d/%m/%Y ' + completed_on_template
build['completed_on'] = completed_on.strftime(completed_on_template)
targets = []
target_objs = build_obj.get_sorted_target_list()
for target_obj in target_objs:
if target_obj.task:
targets.append(target_obj.target + ':' + target_obj.task)
else:
targets.append(target_obj.target)
build['targets'] = ' '.join(targets)
# abbreviated form of the full target list
abbreviated_targets = ''
num_targets = len(targets)
if num_targets > 0:
abbreviated_targets = targets[0]
if num_targets > 1:
abbreviated_targets += (' +%s' % (num_targets - 1))
build['targets_abbreviated'] = abbreviated_targets
recent_builds.append(build)
return JsonResponse(recent_builds, safe=False)

View File

@ -45,6 +45,7 @@ img.logo { height: 30px; vertical-align: bottom; }
.alert-link.build-warnings,
.glyphicon-warning-sign.build-warnings { color: #8a6d3b; }
.build-result .project-name { margin-top: -10px; margin-bottom: 5px; }
.rebuild-btn, .cancel-build-btn { cursor: pointer; }
/* Styles for the help information */
.get-help { color: #CCCCCC; }

File diff suppressed because one or more lines are too long

View File

@ -148,6 +148,21 @@ var libtoaster = (function () {
});
}
function _getMostRecentBuilds(url, onsuccess, onfail) {
$.ajax({
url: url,
type: 'GET',
data : {format: 'json'},
headers: {'X-CSRFToken': $.cookie('csrftoken')},
success: function (data) {
onsuccess ? onsuccess(data) : console.log(data);
},
error: function (data) {
onfail ? onfail(data) : console.error(data);
}
});
}
/* Get a project's configuration info */
function _getProjectInfo(url, onsuccess, onfail){
$.ajax({
@ -426,6 +441,7 @@ var libtoaster = (function () {
reload_params : reload_params,
startABuild : _startABuild,
cancelABuild : _cancelABuild,
getMostRecentBuilds: _getMostRecentBuilds,
makeTypeahead : _makeTypeahead,
getProjectInfo: _getProjectInfo,
getLayerDepsForProject : _getLayerDepsForProject,

View File

@ -1,33 +1,19 @@
function mrbSectionInit(ctx){
var projectBuilds;
if (ctx.mrbType === 'project')
projectBuilds = true;
$(".cancel-build-btn").click(function(e){
$('#latest-builds').on('click', '.cancel-build-btn', function(e){
e.stopImmediatePropagation();
e.preventDefault();
var url = $(this).data('request-url');
var buildReqIds = $(this).data('buildrequest-id');
var banner = $(this).parents(".alert");
banner.find(".progress-info").fadeOut().promise().done(function(){
$("#cancelling-msg-" + buildReqIds).show();
console.log("cancel build");
libtoaster.cancelABuild(url, buildReqIds, function(){
if (projectBuilds == false){
/* the all builds page is not 'self updating' like thei
* project Builds
*/
window.location.reload();
}
}, null);
});
libtoaster.cancelABuild(url, buildReqIds, function () {
window.location.reload();
}, null);
});
$(".run-again-btn").click(function(e){
$('#latest-builds').on('click', '.rebuild-btn', function(e){
e.stopImmediatePropagation();
e.preventDefault();
var url = $(this).data('request-url');
@ -38,58 +24,110 @@ function mrbSectionInit(ctx){
}, null);
});
// cached version of buildData, so we can determine whether a build has
// changed since it was last fetched, and update the DOM appropriately
var buildData = {};
var progressTimer;
// returns the cached version of this build, or {} is there isn't a cached one
function getCached(build) {
return buildData[build.id] || {};
}
if (projectBuilds === true){
progressTimer = window.setInterval(function() {
libtoaster.getProjectInfo(libtoaster.ctx.projectPageUrl,
function(prjInfo){
/* These two are needed because a build can be 100% and still
* in progress due to the fact that the % done is updated at the
* start of a task so it can be doing the last task at 100%
*/
var inProgress = 0;
var allPercentDone = 0;
if (prjInfo.builds.length === 0)
return
// returns true if a build's state changed to "Succeeded" or "Failed"
// from some other value
function buildFinished(build) {
var cached = getCached(build);
return cached.state &&
cached.state !== build.state &&
(build.state == 'Succeeded' || build.state == 'Failed' ||
build.state == 'Cancelled');
}
for (var i in prjInfo.builds){
var build = prjInfo.builds[i];
// returns true if the state changed
function stateChanged(build) {
var cached = getCached(build);
return (cached.state !== build.state);
}
if (build.outcomeText === "In Progress" ||
$(".progress .bar").length > 0){
/* Update the build progress */
var percentDone;
// returns true if the complete_percentage changed
function progressChanged(build) {
var cached = getCached(build);
return (cached.tasks_complete_percentage !== build.tasks_complete_percentage);
}
if (build.outcomeText !== "In Progress"){
/* We have to ignore the value when it's Succeeded because it
* goes back to 0
*/
percentDone = 100;
} else {
percentDone = build.percentDone;
inProgress++;
}
function refreshMostRecentBuilds(){
libtoaster.getMostRecentBuilds(
libtoaster.ctx.mostRecentBuildsUrl,
$("#build-pc-done-" + build.id).text(percentDone);
$("#build-pc-done-title-" + build.id).attr("title", percentDone);
$("#build-pc-done-bar-" + build.id).css("width",
String(percentDone) + "%");
// success callback
function (data) {
var build;
var tmpl;
var container;
var selector;
var colourClass;
var elements;
allPercentDone += percentDone;
// classes on the parent which signify the build state and affect
// the colour of the container for the build
var buildStateClasses = 'alert-info alert-success alert-danger';
for (var i = 0; i < data.length; i++) {
build = data[i];
if (buildFinished(build)) {
// a build finished: reload the whole page so that the build
// shows up in the builds table
window.location.reload();
}
else if (stateChanged(build)) {
// update the whole template
tmpl = $.templates("#build-template");
html = tmpl.render(build);
selector = '[data-latest-build-result="' + build.id + '"] ' +
'[data-role="build-status-container"]';
container = $(selector);
container.html(html);
// style the outermost container for this build to reflect
// the new build state (red, green, blue);
// NB class set here should be in buildStateClasses
colourClass = 'alert-info';
if (build.state == 'Succeeded') {
colourClass = 'alert-success';
}
else if (build.state == 'Failed') {
colourClass = 'alert-danger';
}
elements = $('[data-latest-build-result="' + build.id + '"]');
elements.removeClass(buildStateClasses);
elements.addClass(colourClass);
}
else if (progressChanged(build)) {
// update the progress text
selector = '#build-pc-done-' + build.id;
$(selector).html(build.tasks_complete_percentage);
// update the progress bar
selector = '#build-pc-done-bar-' + build.id;
$(selector).width(build.tasks_complete_percentage + '%');
}
if (allPercentDone === (100 * prjInfo.builds.length) && !inProgress)
window.location.reload();
buildData[build.id] = build;
}
},
/* Our progress bar is not still showing so shutdown the polling. */
if ($(".progress .bar").length === 0)
window.clearInterval(progressTimer);
});
}, 1500);
// fail callback
function (data) {
console.error(data);
}
);
}
}
window.setInterval(refreshMostRecentBuilds, 1000);
refreshMostRecentBuilds();
}

View File

@ -22,6 +22,8 @@
</script>
<script src="{% static 'js/typeahead.jquery.js' %}">
</script>
<script src="{% static 'js/jsrender.min.js' %}">
</script>
<script src="{% static 'js/prettify.js' %}">
</script>
<script src="{% static 'js/libtoaster.js' %}">
@ -32,6 +34,8 @@
</script>
{% endif %}
<script>
$.views.settings.delimiters("<%", "%>");
libtoaster.ctx = {
jsUrl : "{% static 'js/' %}",
htmlUrl : "{% static 'html/' %}",
@ -48,7 +52,9 @@
xhrCustomRecipeUrl : "{% url 'xhr_customrecipe' %}",
projectId : {{project.id}},
xhrBuildRequestUrl: "{% url 'xhr_buildrequest' project.id %}",
mostRecentBuildsUrl: "{% url 'most_recent_builds' %}?project_id={{project.id}}",
{% else %}
mostRecentBuildsUrl: "{% url 'most_recent_builds' %}",
projectId : undefined,
projectPageUrl : undefined,
projectName : undefined,

View File

@ -1,64 +0,0 @@
{% extends "baseprojectpage.html" %}
{% load static %}
{% load projecttags %}
{% load humanize %}
{% block projectinfomain %}
<!-- begin content -->
<div class="row">
<!-- end left sidebar container -->
<!-- Begin right container -->
<div class="col-md-10">
<div class="page-header">
<h1>
<span data-toggle="tooltip" {%if buildrequest.brtarget_set.all.count > 1%}title="Targets: {%for target in buildrequest.brtarget_set.all%}{{target.target}} {%endfor%}"{%endif%}>{{buildrequest.brtarget_set.all.0.target}} {%if buildrequest.brtarget_set.all.count > 1%}(+ {{buildrequest.brtarget_set.all.count|add:"-1"}}){%endif%} {{buildrequest.get_machine}} </span>
</h1>
</div>
<div class="alert alert-error">
<p class="lead">
<strong>Failed</strong>
on {{ buildrequest.updated|date:'d/m/y H:i' }}
with
<i class="icon-minus-sign error" style="margin-left:6px;"></i>
<strong><a class="error accordion-toggle toggle-errors" href="#errors">
{{buildrequest.brerror_set.all.count}} error{{buildrequest.brerror_set.all.count|pluralize}}
</a></strong>
<span class="pull-right">Build time: {{buildrequest.get_duration|sectohms}}</span>
</p>
</div>
<div class="accordion" id="errors">
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle error toggle-errors">
<h2>
<i class="icon-minus-sign"></i>
{{buildrequest.brerror_set.all.count}} error{{buildrequest.brerror_set.all.count|pluralize}}
</h2>
</a>
</div>
<div class="accordion-body collapse in" id="collapse-errors">
<div class="accordion-inner">
<div class="col-md-10">
{% for error in buildrequest.brerror_set.all %}
<div class="alert alert-error">
ERROR: <div class="air well"><pre>{{error.errmsg}}</pre></div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
</div> <!-- end of row -->
{%endblock%}

View File

@ -1,26 +1,9 @@
{% load static %}
{% load projecttags %}
{% load project_url_tag %}
{% load humanize %}
{% load project_url_tag %}
<script src="{% static 'js/mrbsection.js' %}"></script>
<script>
$(document).ready(function () {
var ctx = {
mrbType : "{{mrb_type}}",
}
try {
mrbSectionInit(ctx);
} catch (e) {
document.write("Sorry, An error has occurred loading this page");
console.warn(e);
}
});
</script>
{% if mru %}
{% if mrb_type == 'project' %}
<h2>
Latest project builds
@ -38,6 +21,7 @@
<div id="latest-builds">
{% for build in mru %}
<div data-latest-build-result="{{build.id}}" class="alert build-result {% if build.outcome == build.SUCCEEDED %}alert-success{% elif build.outcome == build.FAILED %}alert-danger{% else %}alert-info{% endif %}">
<!-- project title -->
{% if mrb_type != 'project' %}
<div class="row project-name">
<div class="col-md-12">
@ -48,134 +32,180 @@
</div>
{% endif %}
<div class="row">
<div class="col-md-3">
{% if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %}
<a href="{% url 'builddashboard' build.pk %}" class="alert-link">
{% endif %}
{% if build.target_set.all.count > 0 %}
<span data-toggle="tooltip"
{% if build.target_set.all.count > 1 %}
{{build.get_sorted_target_list.0.target}}
title="Recipes:
{% for target in build.get_sorted_target_list %}
{% if target.task %}
{{target.target}}:{{target.task}}
{% else %}
{{target.target}}
{% endif %}
{% endfor %}"
{% endif %}
>
{% if build.target_set.all.0.task %}
{{build.get_sorted_target_list.0.target}}:{{build.target_set.all.0.task}}
{% else %}
{{build.get_sorted_target_list.0.target}}
{% endif %}
{% if build.target_set.all.count > 1 %}
(+{{build.target_set.all.count|add:"-1"}})
{% endif %}
</span>
{% endif %}
{% if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %}
</a>
{% endif %}
<div class="row" data-role="build-status-container">
<div class="col-md-12">
Loading...
</div>
{% if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %}
<div class="col-md-2">
{% if build.completed_on|format_build_date %}
{{build.completed_on|date:'d/m/y H:i'}}
{% else %}
{{ build.completed_on|date:'H:i' }}
{% endif %}
</div>
{% endif %}
{% if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %}
<div class="col-md-2">
{% if build.errors.count %}
<span class="glyphicon glyphicon-minus-sign"></span>
<a href="{%url 'builddashboard' build.pk%}#errors" class="alert-link">
{{build.errors.count}} error{{build.errors.count|pluralize}}
</a>
{% endif %}
</div>
<div class="col-md-2">
{% if build.warnings.count %}
<span class="glyphicon glyphicon-warning-sign build-warnings"></span>
<a href="{%url 'builddashboard' build.pk%}#warnings" class="alert-link build-warnings">
{{build.warnings.count}} warning{{build.warnings.count|pluralize}}
</a>
{% endif %}
</div>
<div class="col-md-3">
Build time: <a class="alert-link" href="{% url 'buildtime' build.pk %}">{{ build.timespent_seconds|sectohms }}
</a>
{% if build.project.is_default %}
<span class="pull-right glyphicon glyphicon-question-sign get-help {% if build.outcome == build.SUCCEEDED %}get-help-green{% elif build.outcome == build.FAILED %}get-help-red{% else %}get-help-blue{% endif %}"
title="Builds in this project cannot be started from Toaster: they are started from the command line">
</span>
{% else %}
<a href="#" class="run-again-btn alert-link {% if build.outcome == build.SUCCEEDED %}success{% elif build.outcome == build.FAILED %}danger{% else %}info{% endif %} pull-right"
data-request-url="{% url 'xhr_buildrequest' build.project.pk %}"
data-target='{{build.target_set.all|get_tasks|json}}'>
<span class="glyphicon glyphicon-repeat"></span>
Rebuild
</a>
{% endif %}
</div>
{% endif %}
{% if build.outcome == build.IN_PROGRESS %}
<div class="col-md-4" style="display:none" id="cancelling-msg-{{build.buildrequest.pk}}">
Cancelling the build ...
</div>
<div class="col-md-4 col-md-offset-1 progress-info">
<div class="progress" id="build-pc-done-title-{{build.pk}}">
<div id="build-pc-done-bar-{{build.pk}}" style="width: {{build.completeper}}%;" class="progress-bar">
</div>
</div>
</div>
<div class="col-md-4 progress-info">
<span id="build-pc-done-{{build.pk}}">{{build.completeper}}</span>% of tasks complete
{# No build cancel for command line builds project #}
{% if build.project.is_default %}
<span class="glyphicon glyphicon-question-sign get-help get-help-blue pull-right" title="Builds in this project cannot be cancelled from Toaster: they can only be cancelled from the command line"></span>
{% else %}
<a href="#" class="cancel-build-btn pull-right alert-link"
data-buildrequest-id={{build.buildrequest.pk}}
data-request-url="{% url 'xhr_buildrequest' build.project.pk %}">
<span class="glyphicon glyphicon-remove-circle"></span>
Cancel
</a>
{% endif %}
</div>
{% endif %} {# end if in progress #}
{% if build.outcome == build.CANCELLED %}
<div class="col-md-6">
Build cancelled
</div>
<div class="col-md-3">
<a href="#" class="info pull-right run-again-btn alert-link"
data-request-url="{% url 'xhr_buildrequest' build.project.pk %}"
data-target='{{build.target_set.all|get_tasks|json}}'>
<span class="glyphicon glyphicon-repeat"></span>
Rebuild
</a>
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endif %}
<!-- build main template -->
<script id="build-template" type="text/x-jsrender">
<div class="col-md-3">
<!-- only show link for completed builds -->
<%if state == 'Succeeded' || state == 'Failed'%>
<a class="alert-link" href="<%:dashboard_url%>">
<span data-toggle="tooltip" data-role="targets-text" title="Recipes: <%:targets%>">
<%:targets_abbreviated%>
</span>
</a>
<%else%>
<span data-toggle="tooltip" data-role="targets-text" title="Recipes: <%:targets%>">
<%:targets_abbreviated%>
</span>
<%/if%>
</div>
<%if state == 'Queued'%>
<%include tmpl='#queued-build-template'/%>
<%else state == 'Succeeded' || state == 'Failed'%>
<%include tmpl='#succeeded-or-failed-build-template'/%>
<%else state == 'Cancelling'%>
<%include tmpl='#cancelling-build-template'/%>
<%else state == 'In Progress'%>
<%include tmpl='#in-progress-build-template'/%>
<%else state == 'Cancelled'%>
<%include tmpl='#cancelled-build-template'/%>
<%/if%>
</script>
<!-- queued build -->
<script id="queued-build-template" type="text/x-jsrender">
<div class="col-md-5">
Build queued
</div>
<div class="col-md-4">
<%if is_default_project_build%>
<!-- no cancel icon -->
<span class="glyphicon glyphicon-question-sign get-help get-help-blue pull-right" title="Builds in this project cannot be cancelled from Toaster: they can only be cancelled from the command line"></span>
<%else%>
<!-- cancel button -->
<span class="cancel-build-btn pull-right alert-link"
data-buildrequest-id="<%:id%>" data-request-url="<%:cancel_url%>">
<span class="glyphicon glyphicon-remove-circle"></span>
Cancel
</span>
<%/if%>
</div>
</script>
<!-- in progress build -->
<script id="in-progress-build-template" type="text/x-jsrender">
<!-- progress bar and task completion percentage -->
<div data-role="build-status" class="col-md-4 col-md-offset-1 progress-info">
<!-- progress bar -->
<div class="progress" id="build-pc-done-title-<%:id%>">
<div id="build-pc-done-bar-<%:id%>"
style="width: <%:tasks_complete_percentage%>%;"
class="progress-bar">
</div>
</div>
</div>
<div class="col-md-4 progress-info">
<!-- task completion percentage -->
<span id="build-pc-done-<%:id%>"><%:tasks_complete_percentage%></span>% of
tasks complete
<%if is_default_project_build%>
<!-- no cancel icon -->
<span class="glyphicon glyphicon-question-sign get-help get-help-blue pull-right" title="Builds in this project cannot be cancelled from Toaster: they can only be cancelled from the command line"></span>
<%else%>
<!-- cancel button -->
<span class="cancel-build-btn pull-right alert-link"
data-buildrequest-id="<%:id%>" data-request-url="<%:cancel_url%>">
<span class="glyphicon glyphicon-remove-circle"></span>
Cancel
</span>
<%/if%>
</div>
</script>
<!-- cancelling build -->
<script id="cancelling-build-template" type="text/x-jsrender">
<div class="col-md-9">
Cancelling the build ...
</div>
</script>
<!-- succeeded or failed build -->
<script id="succeeded-or-failed-build-template" type="text/x-jsrender">
<!-- completed_on -->
<div class="col-md-2">
<%:completed_on%>
</div>
<!-- errors -->
<div class="col-md-2">
<%if errors%>
<span class="glyphicon glyphicon-minus-sign"></span>
<a href="<%:dashboard_errors_url%>" class="alert-link">
<%:errors%> error<%:errors_pluralize%>
</a>
<%/if%>
</div>
<!-- warnings -->
<div class="col-md-2">
<%if warnings%>
<span class="glyphicon glyphicon-minus-sign"></span>
<a href="<%:dashboard_warnings_url%>" class="alert-link">
<%:warnings%> warning<%:warnings_pluralize%>
</a>
<%/if%>
</div>
<!-- build time -->
<div class="col-md-3">
Build time: <a class="alert-link" href="<%:buildtime_url%>"><%:buildtime%></a>
<%if is_default_project_build%>
<!-- info icon -->
<span class="pull-right glyphicon glyphicon-question-sign get-help <%if state == 'Success'%>get-help-green<%else state == 'Failed'%>get-help-red<%else%>get-help-blue<%/if%>"
title="Builds in this project cannot be started from Toaster: they are started from the command line">
</span>
<%else%>
<!-- rebuild button -->
<span class="rebuild-btn alert-link <%if state == 'Success'%>success<%else state == 'Failed'%>danger<%else%>info<%/if%> pull-right"
data-request-url="<%:rebuild_url%>" data-target='<%:build_targets_json%>'>
<span class="glyphicon glyphicon-repeat"></span>
Rebuild
</span>
<%/if%>
</div>
</script>
<!-- cancelled build -->
<script id="cancelled-build-template" type="text/x-jsrender">
<!-- build cancelled message -->
<div class="col-md-6">
Build cancelled
</div>
<!-- rebuild button -->
<div class="col-md-3">
<span class="info pull-right rebuild-btn alert-link"
data-request-url="<%:rebuild_url%>" data-target='<%:build_targets_json%>'>
<span class="glyphicon glyphicon-repeat"></span>
Rebuild
</span>
</div>
</script>
<script>
$(document).ready(function () {
var ctx = {
mrbType : "{{mrb_type}}",
}
try {
mrbSectionInit(ctx);
} catch (e) {
document.write("Sorry, An error has occurred loading this page");
console.warn(e);
}
});
</script>

View File

@ -24,7 +24,7 @@
<h2 class="top-air" data-role="page-title"></h2>
{% if not build_in_progress_none_completed %}
{% if not build_in_progress_none_completed %}
{% url 'projectbuilds' project.id as xhr_table_url %}
{% include 'toastertable.html' %}
{% endif %}

View File

@ -270,14 +270,6 @@ def get_dict_value(dictionary, key):
except (KeyError, IndexError):
return ''
@register.filter
def format_build_date(completed_on):
now = timezone.now()
delta = now - completed_on
if delta.days >= 1:
return True
@register.filter
def is_shaid(text):
""" return True if text length is 40 characters and all hex-digits

View File

@ -214,6 +214,9 @@ urlpatterns = patterns('toastergui.views',
api.XhrBuildRequest.as_view(),
name='xhr_buildrequest'),
url(r'^mostrecentbuilds$', api.MostRecentBuildsView.as_view(),
name='most_recent_builds'),
# default redirection
url(r'^$', RedirectView.as_view(url='landing', permanent=True)),
)