summaryrefslogtreecommitdiffstats
path: root/bitbake
diff options
context:
space:
mode:
authorElliot Smith <elliot.smith@intel.com>2016-06-29 15:41:56 +0100
committerRichard Purdie <richard.purdie@linuxfoundation.org>2016-08-11 00:09:26 +0100
commit952ffb3e1f4a00793e0c9c49bc0c8fb8729424c4 (patch)
tree604083d9477c9d3d2f51c714fe3b74ac8196f26c /bitbake
parentc471740f5ba023dccc992438c75f1534950d26af (diff)
downloadpoky-952ffb3e1f4a00793e0c9c49bc0c8fb8729424c4.tar.gz
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>
Diffstat (limited to 'bitbake')
-rw-r--r--bitbake/LICENSE2
-rw-r--r--bitbake/lib/toaster/bldcontrol/migrations/0005_reorder_buildrequest_states.py19
-rw-r--r--bitbake/lib/toaster/bldcontrol/models.py12
-rw-r--r--bitbake/lib/toaster/orm/models.py44
-rw-r--r--bitbake/lib/toaster/tests/browser/test_all_builds_page.py4
-rw-r--r--bitbake/lib/toaster/toastergui/api.py105
-rw-r--r--bitbake/lib/toaster/toastergui/static/css/default.css1
-rw-r--r--bitbake/lib/toaster/toastergui/static/js/jsrender.min.js4
-rw-r--r--bitbake/lib/toaster/toastergui/static/js/libtoaster.js16
-rw-r--r--bitbake/lib/toaster/toastergui/static/js/mrbsection.js178
-rw-r--r--bitbake/lib/toaster/toastergui/templates/base.html6
-rw-r--r--bitbake/lib/toaster/toastergui/templates/buildrequestdetails.html64
-rw-r--r--bitbake/lib/toaster/toastergui/templates/mrb_section.html308
-rw-r--r--bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html2
-rw-r--r--bitbake/lib/toaster/toastergui/templatetags/projecttags.py8
-rw-r--r--bitbake/lib/toaster/toastergui/urls.py3
16 files changed, 473 insertions, 303 deletions
diff --git a/bitbake/LICENSE b/bitbake/LICENSE
index 7a0c272a4a..4a09b0f626 100644
--- a/bitbake/LICENSE
+++ b/bitbake/LICENSE
@@ -10,4 +10,6 @@ Foundation and individual contributors.
10 10
11* 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. 11* 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.
12 12
13* jsrender is redistributed under the MIT license.
14
13* QUnit is redistributed under the MIT license. 15* QUnit is redistributed under the MIT license.
diff --git a/bitbake/lib/toaster/bldcontrol/migrations/0005_reorder_buildrequest_states.py b/bitbake/lib/toaster/bldcontrol/migrations/0005_reorder_buildrequest_states.py
new file mode 100644
index 0000000000..4bb9517768
--- /dev/null
+++ b/bitbake/lib/toaster/bldcontrol/migrations/0005_reorder_buildrequest_states.py
@@ -0,0 +1,19 @@
1# -*- coding: utf-8 -*-
2from __future__ import unicode_literals
3
4from django.db import migrations, models
5
6
7class Migration(migrations.Migration):
8
9 dependencies = [
10 ('bldcontrol', '0004_auto_20160523_1446'),
11 ]
12
13 operations = [
14 migrations.AlterField(
15 model_name='buildrequest',
16 name='state',
17 field=models.IntegerField(choices=[(0, 'created'), (1, 'queued'), (2, 'in progress'), (3, 'failed'), (4, 'deleted'), (5, 'cancelling'), (6, 'completed'), (7, 'archive')], default=0),
18 ),
19 ]
diff --git a/bitbake/lib/toaster/bldcontrol/models.py b/bitbake/lib/toaster/bldcontrol/models.py
index f06c562a38..f055480686 100644
--- a/bitbake/lib/toaster/bldcontrol/models.py
+++ b/bitbake/lib/toaster/bldcontrol/models.py
@@ -63,20 +63,20 @@ class BuildRequest(models.Model):
63 REQ_CREATED = 0 63 REQ_CREATED = 0
64 REQ_QUEUED = 1 64 REQ_QUEUED = 1
65 REQ_INPROGRESS = 2 65 REQ_INPROGRESS = 2
66 REQ_COMPLETED = 3 66 REQ_FAILED = 3
67 REQ_FAILED = 4 67 REQ_DELETED = 4
68 REQ_DELETED = 5 68 REQ_CANCELLING = 5
69 REQ_CANCELLING = 6 69 REQ_COMPLETED = 6
70 REQ_ARCHIVE = 7 70 REQ_ARCHIVE = 7
71 71
72 REQUEST_STATE = ( 72 REQUEST_STATE = (
73 (REQ_CREATED, "created"), 73 (REQ_CREATED, "created"),
74 (REQ_QUEUED, "queued"), 74 (REQ_QUEUED, "queued"),
75 (REQ_INPROGRESS, "in progress"), 75 (REQ_INPROGRESS, "in progress"),
76 (REQ_COMPLETED, "completed"),
77 (REQ_FAILED, "failed"), 76 (REQ_FAILED, "failed"),
78 (REQ_DELETED, "deleted"), 77 (REQ_DELETED, "deleted"),
79 (REQ_CANCELLING, "cancelling"), 78 (REQ_CANCELLING, "cancelling"),
79 (REQ_COMPLETED, "completed"),
80 (REQ_ARCHIVE, "archive"), 80 (REQ_ARCHIVE, "archive"),
81 ) 81 )
82 82
@@ -91,7 +91,7 @@ class BuildRequest(models.Model):
91 91
92 def __init__(self, *args, **kwargs): 92 def __init__(self, *args, **kwargs):
93 super(BuildRequest, self).__init__(*args, **kwargs) 93 super(BuildRequest, self).__init__(*args, **kwargs)
94 # Save the old state incase it's about to be modified 94 # Save the old state in case it's about to be modified
95 self.old_state = self.state 95 self.old_state = self.state
96 96
97 def save(self, *args, **kwargs): 97 def save(self, *args, **kwargs):
diff --git a/bitbake/lib/toaster/orm/models.py b/bitbake/lib/toaster/orm/models.py
index caacc2a544..2df6d4910a 100644
--- a/bitbake/lib/toaster/orm/models.py
+++ b/bitbake/lib/toaster/orm/models.py
@@ -592,22 +592,42 @@ class Build(models.Model):
592 592
593 return target_labels 593 return target_labels
594 594
595 def get_current_status(self): 595 def get_buildrequest(self):
596 """ 596 buildrequest = None
597 get the status string from the build request if the build 597 if hasattr(self, 'buildrequest'):
598 has one, or the text for the build outcome if it doesn't 598 buildrequest = self.buildrequest
599 """ 599 return buildrequest
600 600
601 def is_queued(self):
601 from bldcontrol.models import BuildRequest 602 from bldcontrol.models import BuildRequest
603 buildrequest = self.get_buildrequest()
604 if buildrequest:
605 return buildrequest.state == BuildRequest.REQ_QUEUED
606 else:
607 return False
602 608
603 build_request = None 609 def is_cancelling(self):
604 if hasattr(self, 'buildrequest'): 610 from bldcontrol.models import BuildRequest
605 build_request = self.buildrequest 611 buildrequest = self.get_buildrequest()
612 if buildrequest:
613 return self.outcome == Build.IN_PROGRESS and \
614 buildrequest.state == BuildRequest.REQ_CANCELLING
615 else:
616 return False
606 617
607 if (build_request 618 def get_state(self):
608 and build_request.state != BuildRequest.REQ_INPROGRESS 619 """
609 and self.outcome == Build.IN_PROGRESS): 620 Get the state of the build; one of 'Succeeded', 'Failed', 'In Progress',
610 return self.buildrequest.get_state_display() 621 'Cancelled' (Build outcomes); or 'Queued', 'Cancelling' (states
622 dependent on the BuildRequest state).
623
624 This works around the fact that we have BuildRequest states as well
625 as Build states, but really we just want to know the state of the build.
626 """
627 if self.is_cancelling():
628 return 'Cancelling';
629 elif self.is_queued():
630 return 'Queued'
611 else: 631 else:
612 return self.get_outcome_text() 632 return self.get_outcome_text()
613 633
diff --git a/bitbake/lib/toaster/tests/browser/test_all_builds_page.py b/bitbake/lib/toaster/tests/browser/test_all_builds_page.py
index 5ea6532536..521a280de8 100644
--- a/bitbake/lib/toaster/tests/browser/test_all_builds_page.py
+++ b/bitbake/lib/toaster/tests/browser/test_all_builds_page.py
@@ -97,13 +97,13 @@ class TestAllBuildsPage(SeleniumTestCase):
97 self.get(url) 97 self.get(url)
98 98
99 # shouldn't see a rebuild button for command-line builds 99 # shouldn't see a rebuild button for command-line builds
100 selector = 'div[data-latest-build-result="%s"] a.run-again-btn' % default_build.id 100 selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % default_build.id
101 run_again_button = self.find_all(selector) 101 run_again_button = self.find_all(selector)
102 self.assertEqual(len(run_again_button), 0, 102 self.assertEqual(len(run_again_button), 0,
103 'should not see a rebuild button for cli builds') 103 'should not see a rebuild button for cli builds')
104 104
105 # should see a rebuild button for non-command-line builds 105 # should see a rebuild button for non-command-line builds
106 selector = 'div[data-latest-build-result="%s"] a.run-again-btn' % build1.id 106 selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % build1.id
107 run_again_button = self.find_all(selector) 107 run_again_button = self.find_all(selector)
108 self.assertEqual(len(run_again_button), 1, 108 self.assertEqual(len(run_again_button), 1,
109 'should see a rebuild button for non-cli builds') 109 'should see a rebuild button for non-cli builds')
diff --git a/bitbake/lib/toaster/toastergui/api.py b/bitbake/lib/toaster/toastergui/api.py
index 414afce1d0..aa3cbd83b2 100644
--- a/bitbake/lib/toaster/toastergui/api.py
+++ b/bitbake/lib/toaster/toastergui/api.py
@@ -27,7 +27,10 @@ from bldcontrol import bbcontroller
27from django.http import HttpResponse, JsonResponse 27from django.http import HttpResponse, JsonResponse
28from django.views.generic import View 28from django.views.generic import View
29from django.core.urlresolvers import reverse 29from django.core.urlresolvers import reverse
30 30from django.core import serializers
31from django.utils import timezone
32from django.template.defaultfilters import date
33from toastergui.templatetags.projecttags import json, sectohms, get_tasks
31 34
32def error_response(error): 35def error_response(error):
33 return JsonResponse({"error": error}) 36 return JsonResponse({"error": error})
@@ -208,3 +211,103 @@ class XhrLayer(View):
208 "error": "ok", 211 "error": "ok",
209 "redirect": reverse('project', args=(kwargs['pid'],)) 212 "redirect": reverse('project', args=(kwargs['pid'],))
210 }) 213 })
214
215class MostRecentBuildsView(View):
216 def _was_yesterday_or_earlier(self, completed_on):
217 now = timezone.now()
218 delta = now - completed_on
219
220 if delta.days >= 1:
221 return True
222
223 return False
224
225 def get(self, request, *args, **kwargs):
226 """
227 Returns a list of builds in JSON format.
228 """
229 mrb_type = 'all'
230 project = None
231
232 project_id = request.GET.get('project_id', None)
233 if project_id:
234 try:
235 mrb_type = 'project'
236 project = Project.objects.get(pk=project_id)
237 except:
238 # if project lookup fails, assume no project
239 pass
240
241 recent_build_objs = Build.get_recent(project)
242 recent_builds = []
243
244 # for timezone conversion
245 tz = timezone.get_current_timezone()
246
247 for build_obj in recent_build_objs:
248 dashboard_url = reverse('builddashboard', args=(build_obj.pk,))
249 buildtime_url = reverse('buildtime', args=(build_obj.pk,))
250 rebuild_url = \
251 reverse('xhr_buildrequest', args=(build_obj.project.pk,))
252 cancel_url = \
253 reverse('xhr_buildrequest', args=(build_obj.project.pk,))
254
255 build = {}
256 build['id'] = build_obj.pk
257 build['dashboard_url'] = dashboard_url
258
259 tasks_complete_percentage = 0
260 if build_obj.outcome in (Build.SUCCEEDED, Build.FAILED):
261 tasks_complete_percentage = 100
262 elif build_obj.outcome == Build.IN_PROGRESS:
263 tasks_complete_percentage = build_obj.completeper()
264 build['tasks_complete_percentage'] = tasks_complete_percentage
265
266 build['state'] = build_obj.get_state()
267
268 build['errors'] = build_obj.errors.count()
269 build['dashboard_errors_url'] = dashboard_url + '#errors'
270
271 build['warnings'] = build_obj.warnings.count()
272 build['dashboard_warnings_url'] = dashboard_url + '#warnings'
273
274 build['buildtime'] = sectohms(build_obj.timespent_seconds)
275 build['buildtime_url'] = buildtime_url
276
277 build['rebuild_url'] = rebuild_url
278 build['cancel_url'] = cancel_url
279
280 build['is_default_project_build'] = build_obj.project.is_default
281
282 build['build_targets_json'] = \
283 json(get_tasks(build_obj.target_set.all()))
284
285 # convert completed_on time to user's timezone
286 completed_on = timezone.localtime(build_obj.completed_on)
287
288 completed_on_template = '%H:%M'
289 if self._was_yesterday_or_earlier(completed_on):
290 completed_on_template = '%d/%m/%Y ' + completed_on_template
291 build['completed_on'] = completed_on.strftime(completed_on_template)
292
293 targets = []
294 target_objs = build_obj.get_sorted_target_list()
295 for target_obj in target_objs:
296 if target_obj.task:
297 targets.append(target_obj.target + ':' + target_obj.task)
298 else:
299 targets.append(target_obj.target)
300 build['targets'] = ' '.join(targets)
301
302 # abbreviated form of the full target list
303 abbreviated_targets = ''
304 num_targets = len(targets)
305 if num_targets > 0:
306 abbreviated_targets = targets[0]
307 if num_targets > 1:
308 abbreviated_targets += (' +%s' % (num_targets - 1))
309 build['targets_abbreviated'] = abbreviated_targets
310
311 recent_builds.append(build)
312
313 return JsonResponse(recent_builds, safe=False)
diff --git a/bitbake/lib/toaster/toastergui/static/css/default.css b/bitbake/lib/toaster/toastergui/static/css/default.css
index 0d3570a21f..3a0fbb82c8 100644
--- a/bitbake/lib/toaster/toastergui/static/css/default.css
+++ b/bitbake/lib/toaster/toastergui/static/css/default.css
@@ -45,6 +45,7 @@ img.logo { height: 30px; vertical-align: bottom; }
45.alert-link.build-warnings, 45.alert-link.build-warnings,
46.glyphicon-warning-sign.build-warnings { color: #8a6d3b; } 46.glyphicon-warning-sign.build-warnings { color: #8a6d3b; }
47.build-result .project-name { margin-top: -10px; margin-bottom: 5px; } 47.build-result .project-name { margin-top: -10px; margin-bottom: 5px; }
48.rebuild-btn, .cancel-build-btn { cursor: pointer; }
48 49
49/* Styles for the help information */ 50/* Styles for the help information */
50.get-help { color: #CCCCCC; } 51.get-help { color: #CCCCCC; }
diff --git a/bitbake/lib/toaster/toastergui/static/js/jsrender.min.js b/bitbake/lib/toaster/toastergui/static/js/jsrender.min.js
new file mode 100644
index 0000000000..87cac4eb35
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/static/js/jsrender.min.js
@@ -0,0 +1,4 @@
1/*! JsRender v0.9.78 (Beta): http://jsviews.com/#jsrender */
2/*! **VERSION FOR WEB** (For NODE.JS see http://jsviews.com/download/jsrender-node.js) */
3!function(e,t){var n=t.jQuery;"object"==typeof exports?module.exports=n?e(t,n):function(n){if(n&&!n.fn)throw"Provide jQuery or null";return e(t,n)}:"function"==typeof define&&define.amd?define(function(){return e(t)}):e(t,!1)}(function(e,t){"use strict";function n(e,t){return function(){var n,r=this,i=r.base;return r.base=e,n=t.apply(r,arguments),r.base=i,n}}function r(e,t){return te(t)&&(t=n(e?e._d?e:n(s,e):s,t),t._d=1),t}function i(e,t){for(var n in t.props)Re.test(n)&&(e[n]=r(e[n],t.props[n]))}function o(e){return e}function s(){return""}function a(e){try{throw console.log("JsRender dbg breakpoint: "+e),"dbg breakpoint"}catch(t){}return this.base?this.baseApply(arguments):e}function d(e){this.name=(t.link?"JsViews":"JsRender")+" Error",this.message=e||this.name}function u(e,t){for(var n in t)e[n]=t[n];return e}function l(e,t,n){return e?(de.delimiters=[e,t,ve=n?n.charAt(0):ve],pe=e.charAt(0),ce=e.charAt(1),fe=t.charAt(0),ge=t.charAt(1),e="\\"+pe+"(\\"+ve+")?\\"+ce,t="\\"+fe+"\\"+ge,G="(?:(\\w+(?=[\\/\\s\\"+fe+"]))|(\\w+)?(:)|(>)|(\\*))\\s*((?:[^\\"+fe+"]|\\"+fe+"(?!\\"+ge+"))*?)",ae.rTag="(?:"+G+")",G=new RegExp("(?:"+e+G+"(\\/)?|\\"+pe+"(\\"+ve+")?\\"+ce+"(?:(?:\\/(\\w+))\\s*|!--[\\s\\S]*?--))"+t,"g"),W=new RegExp("<.*>|([^\\\\]|^)[{}]|"+e+".*"+t),le):de.delimiters}function p(e,t){t||e===!0||(t=e,e=void 0);var n,r,i,o,s=this,a=!t||"root"===t;if(e){if(o=t&&s.type===t&&s,!o)if(n=s.views,s._.useKey){for(r in n)if(o=t?n[r].get(e,t):n[r])break}else for(r=0,i=n.length;!o&&i>r;r++)o=t?n[r].get(e,t):n[r]}else if(a)for(;s.parent;)o=s,s=s.parent;else for(;s&&!o;)o=s.type===t?s:void 0,s=s.parent;return o}function c(){var e=this.get("item");return e?e.index:void 0}function f(){return this.index}function g(t){var n,r=this,i=r.linkCtx,o=(r.ctx||{})[t];return void 0===o&&i&&i.ctx&&(o=i.ctx[t]),void 0===o&&(o=oe[t]),o&&te(o)&&!o._wrp&&(n=function(){return o.apply(this&&this!==e?this:r,arguments)},n._wrp=r,u(n,o)),n||o}function v(e){return e&&(e.fn?e:this.getRsc("templates",e)||re(e))}function h(e,t,n,r){var o,s,a="number"==typeof n&&t.tmpl.bnds[n-1],d=t.linkCtx;return void 0!==r?n=r={props:{},args:[r]}:a&&(n=a(t.data,t,ae)),s=n.args[0],(e||a)&&(o=d&&d.tag,o||(o=u(new ae._tg,{_:{inline:!d,bnd:a,unlinked:!0},tagName:":",cvt:e,flow:!0,tagCtx:n}),d&&(d.tag=o,o.linkCtx=d),n.ctx=L(n.ctx,(d?d.view:t).ctx)),o._er=r&&s,i(o,n),n.view=t,o.ctx=n.ctx||o.ctx||{},n.ctx=void 0,s=o.cvtArgs("true"!==e&&e)[0],s=a&&t._.onRender?t._.onRender(s,t,o):s),void 0!=s?s:""}function m(e){var t=this,n=t.tagCtx,r=n.view,i=n.args;return e=e||t.convert,e=e&&(""+e===e?r.getRsc("converters",e)||S("Unknown converter: '"+e+"'"):e),i=i.length||n.index?e?i.slice():i:[r.data],e&&(e.depends&&(t.depends=ae.getDeps(t.depends,t,e.depends,e)),i[0]=e.apply(t,i)),i}function w(e,t){for(var n,r,i=this;void 0===n&&i;)r=i.tmpl&&i.tmpl[e],n=r&&r[t],i=i.parent;return n||Y[e][t]}function x(e,t,n,r,o,s){t=t||X;var a,d,u,l,p,c,f,g,v,h,m,w,x,b,_,y,k,j,C,A="",T=t.linkCtx||0,V=t.ctx,R=n||t.tmpl,M="number"==typeof r&&t.tmpl.bnds[r-1];for("tag"===e._is?(a=e,e=a.tagName,r=a.tagCtxs,u=a.template):(d=t.getRsc("tags",e)||S("Unknown tag: {{"+e+"}} "),u=d.template),void 0!==s?(A+=s,r=s=[{props:{},args:[]}]):M&&(r=M(t.data,t,ae)),g=r.length,f=0;g>f;f++)h=r[f],(!T||!T.tag||f&&!T.tag._.inline||a._er)&&((w=R.tmpls&&h.tmpl)&&(w=h.content=R.tmpls[w-1]),h.index=f,h.tmpl=w,h.render=N,h.view=t,h.ctx=L(h.ctx,V)),(n=h.props.tmpl)&&(h.tmpl=t.getTmpl(n)),a||(a=new d._ctr,x=!!a.init,a.parent=c=V&&V.tag,a.tagCtxs=r,C=a.dataMap,T&&(a._.inline=!1,T.tag=a,a.linkCtx=T),(a._.bnd=M||T.fn)?a._.arrVws={}:a.dataBoundOnly&&S("{^{"+e+"}} tag must be data-bound")),r=a.tagCtxs,C=a.dataMap,h.tag=a,C&&r&&(h.map=r[f].map),a.flow||(m=h.ctx=h.ctx||{},l=a.parents=m.parentTags=V&&L(m.parentTags,V.parentTags)||{},c&&(l[c.tagName]=c),l[a.tagName]=m.tag=a);if(!(a._er=s)){for(i(a,r[0]),a.rendering={},f=0;g>f;f++)h=a.tagCtx=r[f],k=h.props,y=a.cvtArgs(),(b=k.dataMap||C)&&(y.length||k.dataMap)&&(_=h.map,_&&_.src===y[0]&&!o||(_&&_.src&&_.unmap(),_=h.map=b.map(y[0],k,void 0,!a._.bnd)),y=[_.tgt]),a.ctx=h.ctx,f||(x&&(j=a.template,a.init(h,T,a.ctx),x=void 0),T&&(T.attr=a.attr=T.attr||a.attr),p=a.attr,a._.noVws=p&&p!==Ee),v=void 0,a.render&&(v=a.render.apply(a,y)),y.length||(y=[t]),void 0===v&&(v=h.render(y[0],!0)||(o?void 0:"")),A=A?A+(v||""):v;a.rendering=void 0}return a.tagCtx=r[0],a.ctx=a.tagCtx.ctx,a._.noVws&&a._.inline&&(A="text"===p?ie.html(A):""),M&&t._.onRender?t._.onRender(A,t,a):A}function b(e,t,n,r,i,o,s,a){var d,u,l,p=this,f="array"===t;p.content=a,p.views=f?[]:{},p.parent=n,p.type=t||"top",p.data=r,p.tmpl=i,l=p._={key:0,useKey:f?0:1,id:""+$e++,onRender:s,bnds:{}},p.linked=!!s,n?(d=n.views,u=n._,u.useKey?(d[l.key="_"+u.useKey++]=p,p.index=Ue,p.getIndex=c):d.length===(l.key=p.index=o)?d.push(p):d.splice(o,0,p),p.ctx=e||n.ctx):p.ctx=e}function _(e){var t,n,r,i,o,s,a;for(t in Oe)if(o=Oe[t],(s=o.compile)&&(n=e[t+"s"]))for(r in n)i=n[r]=s(r,n[r],e,0),i._is=t,i&&(a=ae.onStore[t])&&a(r,i,s)}function y(e,t,n){function i(){var t=this;t._={inline:!0,unlinked:!0},t.tagName=e}var o,s,a,d=new ae._tg;if(te(t)?t={depends:t.depends,render:t}:""+t===t&&(t={template:t}),s=t.baseTag){t.flow=!!t.flow,t.baseTag=s=""+s===s?n&&n.tags[s]||se[s]:s,d=u(d,s);for(a in t)d[a]=r(s[a],t[a])}else d=u(d,t);return void 0!==(o=d.template)&&(d.template=""+o===o?re[o]||re(o):o),d.init!==!1&&((i.prototype=d).constructor=d._ctr=i),n&&(d._parentTmpl=n),d}function k(e){return this.base.apply(this,e)}function j(e,n,r,i){function o(n){var o,a;if(""+n===n||n.nodeType>0&&(s=n)){if(!s)if(/^\.\/[^\\:*?"<>]*$/.test(n))(a=re[e=e||n])?n=a:s=document.getElementById(n);else if(t.fn&&!W.test(n))try{s=t(document).find(n)[0]}catch(d){}s&&(i?n=s.innerHTML:(o=s.getAttribute(Se),o?o!==Ie?(n=re[o],delete re[o]):t.fn&&(n=t.data(s)[Ie]):(e=e||(t.fn?Ie:n),n=j(e,s.innerHTML,r,i)),n.tmplName=e=e||o,e!==Ie&&(re[e]=n),s.setAttribute(Se,e),t.fn&&t.data(s,Ie,n))),s=void 0}else n.fn||(n=void 0);return n}var s,a,d=n=n||"";return 0===i&&(i=void 0,d=o(d)),i=i||(n.markup?n:{}),i.tmplName=e,r&&(i._parentTmpl=r),!d&&n.markup&&(d=o(n.markup))&&d.fn&&(d=d.markup),void 0!==d?(d.fn||n.fn?d.fn&&(a=d):(n=V(d,i),U(d.replace(ke,"\\$&"),n)),a||(_(i),a=u(function(){return n.render.apply(n,arguments)},n)),e&&!r&&e!==Ie&&(qe[e]=a),a):void 0}function C(e,n){return t.isFunction(e)?e.call(n):e}function A(e){var t,n=[],r=e.length;for(t=0;r>t;t++)n.push(e[t].unmap());return n}function T(e,n){function r(e){l.apply(this,e)}function i(){return new r(arguments)}function o(e,t){var n,r,i,o,s,a=c.length;for(n=0;a>n;n++)o=c[n],r=void 0,o+""!==o&&(r=o,o=r.getter),void 0===(s=e[o])&&r&&void 0!==(i=r.defaultVal)&&(s=C(i,e)),t(s,r&&p[r.type],o)}function s(n){n=n+""===n?JSON.parse(n):n;var r,i,s,u=n,l=[];if(t.isArray(n)){for(n=n||[],i=n.length,r=0;i>r;r++)l.push(this.map(n[r]));return l._is=e,l.unmap=d,l.merge=a,l}if(n){o(n,function(e,t){t&&(e=t.map(e)),l.push(e)}),u=this.apply(this,l);for(s in n)s===ee||b[s]||(u[s]=n[s])}return u}function a(e){e=e+""===e?JSON.parse(e):e;var n,r,s,a,d,u,l,p,c,f,v=this;if(t.isArray(v)){for(p={},f=[],s=e.length,a=v.length,n=0;s>n;n++){for(c=e[n],l=!1,r=0;a>r&&!l;r++)p[r]||(u=v[r],g&&(p[r]=l=g+""===g?c[g]&&(b[g]?u[g]():u[g])===c[g]:g(u,c)));l?(u.merge(c),f.push(u)):f.push(i.map(c))}return void(x?x(v).refresh(f,!0):v.splice.apply(v,[0,v.length].concat(f)))}o(e,function(e,t,n){t?v[n]().merge(e):v[n](e)});for(d in e)d===ee||b[d]||(v[d]=e[d])}function d(){var e,n,r,i,o,s,a=this;if(t.isArray(a))return A(a);for(e={},i=c.length,r=0;i>r;r++)n=c[r],o=void 0,n+""!==n&&(o=n,n=o.getter),s=a[n](),e[n]=o&&s&&p[o.type]?t.isArray(s)?A(s):s.unmap():s;for(n in a)"_is"===n||b[n]||n===ee||"_"===n.charAt(0)&&b[n.slice(1)]||t.isFunction(a[n])||(e[n]=a[n]);return e}var u,l,p=this,c=n.getters,f=n.extend,g=n.id,v=t.extend({_is:e||"unnamed",unmap:d,merge:a},f),h="",m="",w=c?c.length:0,x=t.observable,b={};for(r.prototype=v,u=0;w>u;u++)!function(e){e=e.getter||e,b[e]=u+1;var t="_"+e;h+=(h?",":"")+e,m+="this."+t+" = "+e+";\n",v[e]=v[e]||function(n){return arguments.length?void(x?x(this).setProperty(e,n):this[t]=n):this[t]},x&&(v[e].set=v[e].set||function(e){this[t]=e})}(c[u]);return l=new Function(h,m.slice(0,-1)),l.prototype=v,v.constructor=l,i.map=s,i.getters=c,i.extend=f,i.id=g,i}function V(e,n){var r,i=ue._wm||{},o=u({tmpls:[],links:{},bnds:[],_is:"template",render:N},n);return o.markup=e,n.htmlTag||(r=Ae.exec(e),o.htmlTag=r?r[1].toLowerCase():""),r=i[o.htmlTag],r&&r!==i.div&&(o.markup=t.trim(o.markup)),o}function R(e,t){function n(i,o,s){var a,d,u,l;if(i&&typeof i===Fe&&!i.nodeType&&!i.markup&&!i.getTgt&&!("viewModel"===e&&i.getters||i.extend)){for(u in i)n(u,i[u],o);return o||Y}return void 0===o&&(o=i,i=void 0),i&&""+i!==i&&(s=o,o=i,i=void 0),l=s?"viewModel"===e?s:s[r]=s[r]||{}:n,d=t.compile,null===o?i&&delete l[i]:(o=d?d.call(l,i,o,s,0):o,i&&(l[i]=o)),d&&o&&(o._is=e),o&&(a=ae.onStore[e])&&a(i,o,d),o}var r=e+"s";Y[r]=n}function M(e){le[e]=function(t){return arguments.length?(de[e]=t,le):de[e]}}function $(e){function t(t,n){this.tgt=e.getTgt(t,n)}return te(e)&&(e={getTgt:e}),e.baseMap&&(e=u(u({},e.baseMap),e)),e.map=function(e,n){return new t(e,n)},e}function N(e,t,n,r,i,o){var s,a,d,u,l,p,c,f,g=r,v="";if(t===!0?(n=t,t=void 0):typeof t!==Fe&&(t=void 0),(d=this.tag)?(l=this,g=g||l.view,u=g.getTmpl(d.template||l.tmpl),arguments.length||(e=g)):u=this,u){if(!g&&e&&"view"===e._is&&(g=e),g&&e===g&&(e=g.data),p=!g,me=me||p,g||((t=t||{}).root=e),!me||ue.useViews||u.useViews||g&&g!==X)v=E(u,e,t,n,g,i,o,d);else{if(g?(c=g.data,f=g.index,g.index=Ue):(g=X,g.data=e,g.ctx=t),ne(e)&&!n)for(s=0,a=e.length;a>s;s++)g.index=s,g.data=e[s],v+=u.fn(e[s],g,ae);else g.data=e,v+=u.fn(e,g,ae);g.data=c,g.index=f}p&&(me=void 0)}return v}function E(e,t,n,r,i,o,s,a){function d(e){_=u({},n),_[x]=e}var l,p,c,f,g,v,h,m,w,x,_,y,k="";if(a&&(w=a.tagName,y=a.tagCtx,n=n?L(n,a.ctx):a.ctx,e===i.content?h=e!==i.ctx._wrp?i.ctx._wrp:void 0:e!==y.content?e===a.template?(h=y.tmpl,n._wrp=y.content):h=y.content||i.content:h=i.content,y.props.link===!1&&(n=n||{},n.link=!1),(x=y.props.itemVar)&&("~"!==x.charAt(0)&&I("Use itemVar='~myItem'"),x=x.slice(1))),i&&(s=s||i._.onRender,n=L(n,i.ctx)),o===!0&&(v=!0,o=0),s&&(n&&n.link===!1||a&&a._.noVws)&&(s=void 0),m=s,s===!0&&(m=void 0,s=i._.onRender),n=e.helpers?L(e.helpers,n):n,_=n,ne(t)&&!r)for(c=v?i:void 0!==o&&i||new b(n,"array",i,t,e,o,s),i&&i._.useKey&&(c._.bnd=!a||a._.bnd&&a),x&&(c.it=x),x=c.it,l=0,p=t.length;p>l;l++)x&&d(t[l]),f=new b(_,"item",c,t[l],e,(o||0)+l,s,h),g=e.fn(t[l],f,ae),k+=c._.onRender?c._.onRender(g,f):g;else x&&d(t),c=v?i:new b(_,w||"data",i,t,e,o,s,h),a&&!a.flow&&(c.tag=a),k+=e.fn(t,c,ae);return m?m(k,c):k}function F(e,t,n){var r=void 0!==n?te(n)?n.call(t.data,e,t):n||"":"{Error: "+e.message+"}";return de.onError&&void 0!==(n=de.onError.call(t.data,e,n&&r,t))&&(r=n),t&&!t.linkCtx?ie.html(r):r}function S(e){throw new ae.Err(e)}function I(e){S("Syntax error\n"+e)}function U(e,t,n,r,i){function o(t){t-=v,t&&m.push(e.substr(v,t).replace(_e,"\\n"))}function s(t,n){t&&(t+="}}",I((n?"{{"+n+"}} block has {{/"+t+" without {{"+t:"Unmatched or missing {{/"+t)+", in template:\n"+e))}function a(a,d,u,c,g,x,b,_,y,k,j,C){(b&&d||y&&!u||_&&":"===_.slice(-1)||k)&&I(a),x&&(g=":",c=Ee),y=y||n&&!i;var A=(d||n)&&[[]],T="",V="",R="",M="",$="",N="",E="",F="",S=!y&&!g;u=u||(_=_||"#data",g),o(C),v=C+a.length,b?f&&m.push(["*","\n"+_.replace(/^:/,"ret+= ").replace(ye,"$1")+";\n"]):u?("else"===u&&(Ce.test(_)&&I('for "{{else if expr}}" use "{{else expr}}"'),A=w[7]&&[[]],w[8]=e.substring(w[8],C),w=h.pop(),m=w[2],S=!0),_&&O(_.replace(_e," "),A,t).replace(je,function(e,t,n,r,i,o,s,a){return r="'"+i+"':",s?(V+=o+",",M+="'"+a+"',"):n?(R+=r+o+",",N+=r+"'"+a+"',"):t?E+=o:("trigger"===i&&(F+=o),T+=r+o+",",$+=r+"'"+a+"',",p=p||Re.test(i)),""}).slice(0,-1),A&&A[0]&&A.pop(),l=[u,c||!!r||p||"",S&&[],J(M||(":"===u?"'#data',":""),$,N),J(V||(":"===u?"data,":""),T,R),E,F,A||0],m.push(l),S&&(h.push(w),w=l,w[8]=v)):j&&(s(j!==w[0]&&"else"!==w[0]&&j,w[0]),w[8]=e.substring(w[8],C),w=h.pop()),s(!w&&j),m=w[2]}var d,u,l,p,c,f=de.allowCode||t&&t.allowCode||le.allowCode===!0,g=[],v=0,h=[],m=g,w=[,,g];if(f&&(t.allowCode=f),n&&(void 0!==r&&(e=e.slice(0,-r.length-2)+ge),e=pe+e+ge),s(h[0]&&h[0][2].pop()[0]),e.replace(G,a),o(e.length),(v=g[g.length-1])&&s(""+v!==v&&+v[8]===v[8]&&v[0]),n){for(u=B(g,e,n),c=[],d=g.length;d--;)c.unshift(g[d][7]);q(u,c)}else u=B(g,t);return u}function q(e,t){var n,r,i=0,o=t.length;for(e.deps=[];o>i;i++){r=t[i];for(n in r)"_jsvto"!==n&&r[n].length&&(e.deps=e.deps.concat(r[n]))}e.paths=r}function J(e,t,n){return[e.slice(0,-1),t.slice(0,-1),n.slice(0,-1)]}function K(e,t){return"\n "+(t?t+":{":"")+"args:["+e[0]+"]"+(e[1]||!t?",\n props:{"+e[1]+"}":"")+(e[2]?",\n ctx:{"+e[2]+"}":"")}function O(e,t,n){function r(r,m,w,x,b,_,y,k,j,C,A,T,V,R,M,$,N,E,F,S){function q(e,n,r,s,a,d,p,c){var f="."===r;if(r&&(b=b.slice(n.length),/^\.?constructor$/.test(c||b)&&I(e),f||(e=(s?'view.hlp("'+s+'")':a?"view":"data")+(c?(d?"."+d:s?"":a?"":"."+r)+(p||""):(c=s?"":a?d||"":r,"")),e+=c?"."+c:"",e=n+("view.data"===e.slice(0,9)?e.slice(5):e)),u)){if(O="linkTo"===i?o=t._jsvto=t._jsvto||[]:l.bd,B=f&&O[O.length-1]){if(B._jsv){for(;B.sb;)B=B.sb;B.bnd&&(b="^"+b.slice(1)),B.sb=b,B.bnd=B.bnd||"^"===b.charAt(0)}}else O.push(b);h[g]=F+(f?1:0)}return e}x=u&&x,x&&!k&&(b=x+b),_=_||"",w=w||m||T,b=b||j,C=C||N||"";var J,K,O,B,L,Q=")";if("["===C&&(C="[j._sq(",Q=")]"),!y||d||a){if(u&&$&&!d&&!a&&(!i||s||o)&&(J=h[g-1],S.length-1>F-(J||0))){if(J=S.slice(J,F+r.length),K!==!0)if(O=o||p[g-1].bd,B=O[O.length-1],B&&B.prm){for(;B.sb&&B.sb.prm;)B=B.sb;L=B.sb={path:B.sb,bnd:B.bnd}}else O.push(L={path:O.pop()});$=ce+":"+J+" onerror=''"+fe,K=f[$],K||(f[$]=!0,f[$]=K=U($,n,!0)),K!==!0&&L&&(L._jsv=K,L.prm=l.bd,L.bnd=L.bnd||L.path&&L.path.indexOf("^")>=0)}return d?(d=!V,d?r:T+'"'):a?(a=!R,a?r:T+'"'):(w?(h[g]=F++,l=p[++g]={bd:[]},w):"")+(E?g?"":(c=S.slice(c,F),(i?(i=s=o=!1,"\b"):"\b,")+c+(c=F+r.length,u&&t.push(l.bd=[]),"\b")):k?(g&&I(e),u&&t.pop(),i=b,s=x,c=F+r.length,x&&(u=l.bd=t[i]=[]),b+":"):b?b.split("^").join(".").replace(xe,q)+(C?(l=p[++g]={bd:[]},v[g]=Q,C):_):_?_:M?(M=v[g]||M,v[g]=!1,l=p[--g],M+(C?(l=p[++g],v[g]=Q,C):"")):A?(v[g]||I(e),","):m?"":(d=V,a=R,'"'))}I(e)}var i,o,s,a,d,u=t&&t[0],l={bd:u},p={0:l},c=0,f=n?n.links:u&&(u.links=u.links||{}),g=0,v={},h={},m=(e+(n?" ":"")).replace(be,r);return!g&&m||I(e)}function B(e,t,n){var r,i,o,s,a,d,u,l,p,c,f,g,v,h,m,w,x,b,_,y,k,j,C,A,T,R,M,$,N,E,F=0,S=ue.useViews||t.useViews||t.tags||t.templates||t.helpers||t.converters,U="",J={},O=e.length;for(""+t===t?(b=n?'data-link="'+t.replace(_e," ").slice(1,-1)+'"':t,t=0):(b=t.tmplName||"unnamed",t.allowCode&&(J.allowCode=!0),t.debug&&(J.debug=!0),f=t.bnds,x=t.tmpls),r=0;O>r;r++)if(i=e[r],""+i===i)U+='\n+"'+i+'"';else if(o=i[0],"*"===o)U+=";\n"+i[1]+"\nret=ret";else{if(s=i[1],k=!n&&i[2],a=K(i[3],"params")+"},"+K(v=i[4]),$=i[5],E=i[6],j=i[8]&&i[8].replace(ye,"$1"),(T="else"===o)?g&&g.push(i[7]):(F=0,f&&(g=i[7])&&(g=[g],F=f.push(1))),S=S||v[1]||v[2]||g||/view.(?!index)/.test(v[0]),(R=":"===o)?s&&(o=s===Ee?">":s+o):(k&&(_=V(j,J),_.tmplName=b+"/"+o,_.useViews=_.useViews||S,B(k,_),S=_.useViews,x.push(_)),T||(y=o,S=S||o&&(!se[o]||!se[o].flow),A=U,U=""),C=e[r+1],C=C&&"else"===C[0]),N=$?";\ntry{\nret+=":"\n+",h="",m="",R&&(g||E||s&&s!==Ee)){if(M=new Function("data,view,j,u"," // "+b+" "+F+" "+o+"\nreturn {"+a+"};"),M._er=$,M._tag=o,n)return M;q(M,g),w='c("'+s+'",view,',c=!0,h=w+F+",",m=")"}if(U+=R?(n?($?"try{\n":"")+"return ":N)+(c?(c=void 0,S=p=!0,w+(g?(f[F-1]=M,F):"{"+a+"}")+")"):">"===o?(u=!0,"h("+v[0]+")"):(l=!0,"((v="+v[0]+")!=null?v:"+(n?"null)":'"")'))):(d=!0,"\n{view:view,tmpl:"+(k?x.length:"0")+","+a+"},"),y&&!C){if(U="["+U.slice(0,-1)+"]",w='t("'+y+'",view,this,',n||g){if(U=new Function("data,view,j,u"," // "+b+" "+F+" "+y+"\nreturn "+U+";"),U._er=$,U._tag=y,g&&q(f[F-1]=U,g),n)return U;h=w+F+",undefined,",m=")"}U=A+N+w+(F||U)+")",g=0,y=0}$&&(S=!0,U+=";\n}catch(e){ret"+(n?"urn ":"+=")+h+"j._err(e,view,"+$+")"+m+";}"+(n?"":"ret=ret"))}U="// "+b+"\nvar v"+(d?",t=j._tag":"")+(p?",c=j._cnvt":"")+(u?",h=j._html":"")+(n?";\n":',ret=""\n')+(J.debug?"debugger;":"")+U+(n?"\n":";\nreturn ret;"),de.debugMode!==!1&&(U="try {\n"+U+"\n}catch(e){\nreturn j._err(e, view);\n}");try{U=new Function("data,view,j,u",U)}catch(L){I("Compiled template code:\n\n"+U+'\n: "'+L.message+'"')}return t&&(t.fn=U,t.useViews=!!S),U}function L(e,t){return e&&e!==t?t?u(u({},t),e):e:t&&u({},t)}function Q(e){return Ne[e]||(Ne[e]="&#"+e.charCodeAt(0)+";")}function H(e){var t,n,r=[];if(typeof e===Fe)for(t in e)n=e[t],t===ee||te(n)||r.push({key:t,prop:n});return r}function P(e,n,r){var i=this.jquery&&(this[0]||S('Unknown template: "'+this.selector+'"')),o=i.getAttribute(Se);return N.call(o?t.data(i)[Ie]:re(i),e,n,r)}function D(e){return void 0!=e?Ve.test(e)&&(""+e).replace(Me,Q)||e:""}var Z=t===!1;t=t&&t.fn?t:e.jQuery;var z,G,W,X,Y,ee,te,ne,re,ie,oe,se,ae,de,ue,le,pe,ce,fe,ge,ve,he,me,we="v0.9.78",xe=/^(!*?)(?:null|true|false|\d[\d.]*|([\w$]+|\.|~([\w$]+)|#(view|([\w$]+))?)([\w$.^]*?)(?:[.[^]([\w$]+)\]?)?)$/g,be=/(\()(?=\s*\()|(?:([([])\s*)?(?:(\^?)(!*?[#~]?[\w$.^]+)?\s*((\+\+|--)|\+|-|&&|\|\||===|!==|==|!=|<=|>=|[<>%*:?\/]|(=))\s*|(!*?[#~]?[\w$.^]+)([([])?)|(,\s*)|(\(?)\\?(?:(')|("))|(?:\s*(([)\]])(?=\s*[.^]|\s*$|[^([])|[)\]])([([]?))|(\s+)/g,_e=/[ \t]*(\r\n|\n|\r)/g,ye=/\\(['"])/g,ke=/['"\\]/g,je=/(?:\x08|^)(onerror:)?(?:(~?)(([\w$_\.]+):)?([^\x08]+))\x08(,)?([^\x08]+)/gi,Ce=/^if\s/,Ae=/<(\w+)[>\s]/,Te=/[\x00`><"'&=]/g,Ve=/[\x00`><\"'&=]/,Re=/^on[A-Z]|^convert(Back)?$/,Me=Te,$e=0,Ne={"&":"&amp;","<":"&lt;",">":"&gt;","\x00":"&#0;","'":"&#39;",'"':"&#34;","`":"&#96;","=":"&#61;"},Ee="html",Fe="object",Se="data-jsv-tmpl",Ie="jsvTmpl",Ue="For #index in nested block use #getIndex().",qe={},Je=e.jsrender,Ke=Je&&t&&!t.render,Oe={template:{compile:j},tag:{compile:y},viewModel:{compile:T},helper:{},converter:{}};if(Y={jsviews:we,sub:{View:b,Err:d,tmplFn:U,parse:O,extend:u,extendCtx:L,syntaxErr:I,onStore:{},addSetting:M,settings:{allowCode:!1},advSet:s,_ths:i,_tg:function(){},_cnvt:h,_tag:x,_er:S,_err:F,_html:D,_sq:function(e){return"constructor"===e&&I(""),e}},settings:{delimiters:l,advanced:function(e){return e?(u(ue,e),ae.advSet(),le):ue}},map:$},(d.prototype=new Error).constructor=d,c.depends=function(){return[this.get("item"),"index"]},f.depends="index",b.prototype={get:p,getIndex:f,getRsc:w,getTmpl:v,hlp:g,_is:"view"},ae=Y.sub,le=Y.settings,!(Je||t&&t.render)){for(z in Oe)R(z,Oe[z]);ie=Y.converters,oe=Y.helpers,se=Y.tags,ae._tg.prototype={baseApply:k,cvtArgs:m},X=ae.topView=new b,t?(t.fn.render=P,ee=t.expando,t.observable&&(u(ae,t.views.sub),Y.map=t.views.map)):(t={},Z&&(e.jsrender=t),t.renderFile=t.__express=t.compile=function(){throw"Node.js: use npm jsrender, or jsrender-node.js"},t.isFunction=function(e){return"function"==typeof e},t.isArray=Array.isArray||function(e){return"[object Array]"==={}.toString.call(e)},ae._jq=function(e){e!==t&&(u(e,t),t=e,t.fn.render=P,delete t.jsrender,ee=t.expando)},t.jsrender=we),de=ae.settings,de.allowCode=!1,te=t.isFunction,ne=t.isArray,t.render=qe,t.views=Y,t.templates=re=Y.templates;for(he in de)M(he);(le.debugMode=function(e){return void 0===e?de.debugMode:(de.debugMode=e,de.onError=e+""===e?new Function("","return '"+e+"';"):te(e)?e:void 0,le)})(!1),ue=de.advanced={useViews:!1,_jsv:!1},se({"if":{render:function(e){var t=this,n=t.tagCtx,r=t.rendering.done||!e&&(arguments.length||!n.index)?"":(t.rendering.done=!0,t.selected=n.index,n.render(n.view,!0));return r},flow:!0},"for":{render:function(e){var t,n=!arguments.length,r=this,i=r.tagCtx,o="",s=0;return r.rendering.done||(t=n?i.view.data:e,void 0!==t&&(o+=i.render(t,n),s+=ne(t)?t.length:1),(r.rendering.done=s)&&(r.selected=i.index)),o},flow:!0},props:{baseTag:"for",dataMap:$(H),flow:!0},include:{flow:!0},"*":{render:o,flow:!0},":*":{render:o,flow:!0},dbg:oe.dbg=ie.dbg=a}),ie({html:D,attr:D,url:function(e){return void 0!=e?encodeURI(""+e):null===e?e:""}})}return de=ae.settings,le.delimiters("{{","}}","^"),Ke&&Je.views.sub._jq(t),t||Je},window);
4//# sourceMappingURL=jsrender.min.js.map
diff --git a/bitbake/lib/toaster/toastergui/static/js/libtoaster.js b/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
index eafe70ddee..a61b10e972 100644
--- a/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
+++ b/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
@@ -148,6 +148,21 @@ var libtoaster = (function () {
148 }); 148 });
149 } 149 }
150 150
151 function _getMostRecentBuilds(url, onsuccess, onfail) {
152 $.ajax({
153 url: url,
154 type: 'GET',
155 data : {format: 'json'},
156 headers: {'X-CSRFToken': $.cookie('csrftoken')},
157 success: function (data) {
158 onsuccess ? onsuccess(data) : console.log(data);
159 },
160 error: function (data) {
161 onfail ? onfail(data) : console.error(data);
162 }
163 });
164 }
165
151 /* Get a project's configuration info */ 166 /* Get a project's configuration info */
152 function _getProjectInfo(url, onsuccess, onfail){ 167 function _getProjectInfo(url, onsuccess, onfail){
153 $.ajax({ 168 $.ajax({
@@ -426,6 +441,7 @@ var libtoaster = (function () {
426 reload_params : reload_params, 441 reload_params : reload_params,
427 startABuild : _startABuild, 442 startABuild : _startABuild,
428 cancelABuild : _cancelABuild, 443 cancelABuild : _cancelABuild,
444 getMostRecentBuilds: _getMostRecentBuilds,
429 makeTypeahead : _makeTypeahead, 445 makeTypeahead : _makeTypeahead,
430 getProjectInfo: _getProjectInfo, 446 getProjectInfo: _getProjectInfo,
431 getLayerDepsForProject : _getLayerDepsForProject, 447 getLayerDepsForProject : _getLayerDepsForProject,
diff --git a/bitbake/lib/toaster/toastergui/static/js/mrbsection.js b/bitbake/lib/toaster/toastergui/static/js/mrbsection.js
index 9a76ee6407..d8c3bf7750 100644
--- a/bitbake/lib/toaster/toastergui/static/js/mrbsection.js
+++ b/bitbake/lib/toaster/toastergui/static/js/mrbsection.js
@@ -1,33 +1,19 @@
1 1
2function mrbSectionInit(ctx){ 2function mrbSectionInit(ctx){
3 3 $('#latest-builds').on('click', '.cancel-build-btn', function(e){
4 var projectBuilds; 4 e.stopImmediatePropagation();
5
6 if (ctx.mrbType === 'project')
7 projectBuilds = true;
8
9 $(".cancel-build-btn").click(function(e){
10 e.preventDefault(); 5 e.preventDefault();
11 6
12 var url = $(this).data('request-url'); 7 var url = $(this).data('request-url');
13 var buildReqIds = $(this).data('buildrequest-id'); 8 var buildReqIds = $(this).data('buildrequest-id');
14 var banner = $(this).parents(".alert"); 9
15 10 libtoaster.cancelABuild(url, buildReqIds, function () {
16 banner.find(".progress-info").fadeOut().promise().done(function(){ 11 window.location.reload();
17 $("#cancelling-msg-" + buildReqIds).show(); 12 }, null);
18 console.log("cancel build");
19 libtoaster.cancelABuild(url, buildReqIds, function(){
20 if (projectBuilds == false){
21 /* the all builds page is not 'self updating' like thei
22 * project Builds
23 */
24 window.location.reload();
25 }
26 }, null);
27 });
28 }); 13 });
29 14
30 $(".run-again-btn").click(function(e){ 15 $('#latest-builds').on('click', '.rebuild-btn', function(e){
16 e.stopImmediatePropagation();
31 e.preventDefault(); 17 e.preventDefault();
32 18
33 var url = $(this).data('request-url'); 19 var url = $(this).data('request-url');
@@ -38,58 +24,110 @@ function mrbSectionInit(ctx){
38 }, null); 24 }, null);
39 }); 25 });
40 26
27 // cached version of buildData, so we can determine whether a build has
28 // changed since it was last fetched, and update the DOM appropriately
29 var buildData = {};
41 30
42 var progressTimer; 31 // returns the cached version of this build, or {} is there isn't a cached one
43 32 function getCached(build) {
44 if (projectBuilds === true){ 33 return buildData[build.id] || {};
45 progressTimer = window.setInterval(function() { 34 }
46 libtoaster.getProjectInfo(libtoaster.ctx.projectPageUrl, 35
47 function(prjInfo){ 36 // returns true if a build's state changed to "Succeeded" or "Failed"
48 /* These two are needed because a build can be 100% and still 37 // from some other value
49 * in progress due to the fact that the % done is updated at the 38 function buildFinished(build) {
50 * start of a task so it can be doing the last task at 100% 39 var cached = getCached(build);
51 */ 40 return cached.state &&
52 var inProgress = 0; 41 cached.state !== build.state &&
53 var allPercentDone = 0; 42 (build.state == 'Succeeded' || build.state == 'Failed' ||
54 if (prjInfo.builds.length === 0) 43 build.state == 'Cancelled');
55 return 44 }
56
57 for (var i in prjInfo.builds){
58 var build = prjInfo.builds[i];
59
60 if (build.outcomeText === "In Progress" ||
61 $(".progress .bar").length > 0){
62 /* Update the build progress */
63 var percentDone;
64
65 if (build.outcomeText !== "In Progress"){
66 /* We have to ignore the value when it's Succeeded because it
67 * goes back to 0
68 */
69 percentDone = 100;
70 } else {
71 percentDone = build.percentDone;
72 inProgress++;
73 }
74
75 $("#build-pc-done-" + build.id).text(percentDone);
76 $("#build-pc-done-title-" + build.id).attr("title", percentDone);
77 $("#build-pc-done-bar-" + build.id).css("width",
78 String(percentDone) + "%");
79
80 allPercentDone += percentDone;
81 }
82 }
83 45
84 if (allPercentDone === (100 * prjInfo.builds.length) && !inProgress) 46 // returns true if the state changed
47 function stateChanged(build) {
48 var cached = getCached(build);
49 return (cached.state !== build.state);
50 }
51
52 // returns true if the complete_percentage changed
53 function progressChanged(build) {
54 var cached = getCached(build);
55 return (cached.tasks_complete_percentage !== build.tasks_complete_percentage);
56 }
57
58 function refreshMostRecentBuilds(){
59 libtoaster.getMostRecentBuilds(
60 libtoaster.ctx.mostRecentBuildsUrl,
61
62 // success callback
63 function (data) {
64 var build;
65 var tmpl;
66 var container;
67 var selector;
68 var colourClass;
69 var elements;
70
71 // classes on the parent which signify the build state and affect
72 // the colour of the container for the build
73 var buildStateClasses = 'alert-info alert-success alert-danger';
74
75 for (var i = 0; i < data.length; i++) {
76 build = data[i];
77
78 if (buildFinished(build)) {
79 // a build finished: reload the whole page so that the build
80 // shows up in the builds table
85 window.location.reload(); 81 window.location.reload();
82 }
83 else if (stateChanged(build)) {
84 // update the whole template
85 tmpl = $.templates("#build-template");
86
87 html = tmpl.render(build);
88
89 selector = '[data-latest-build-result="' + build.id + '"] ' +
90 '[data-role="build-status-container"]';
91 container = $(selector);
92
93 container.html(html);
94
95 // style the outermost container for this build to reflect
96 // the new build state (red, green, blue);
97 // NB class set here should be in buildStateClasses
98 colourClass = 'alert-info';
99 if (build.state == 'Succeeded') {
100 colourClass = 'alert-success';
101 }
102 else if (build.state == 'Failed') {
103 colourClass = 'alert-danger';
104 }
86 105
87 /* Our progress bar is not still showing so shutdown the polling. */ 106 elements = $('[data-latest-build-result="' + build.id + '"]');
88 if ($(".progress .bar").length === 0) 107 elements.removeClass(buildStateClasses);
89 window.clearInterval(progressTimer); 108 elements.addClass(colourClass);
109 }
110 else if (progressChanged(build)) {
111 // update the progress text
112 selector = '#build-pc-done-' + build.id;
113 $(selector).html(build.tasks_complete_percentage);
114
115 // update the progress bar
116 selector = '#build-pc-done-bar-' + build.id;
117 $(selector).width(build.tasks_complete_percentage + '%');
118 }
90 119
91 }); 120 buildData[build.id] = build;
92 }, 1500); 121 }
122 },
123
124 // fail callback
125 function (data) {
126 console.error(data);
127 }
128 );
93 } 129 }
94}
95 130
131 window.setInterval(refreshMostRecentBuilds, 1000);
132 refreshMostRecentBuilds();
133}
diff --git a/bitbake/lib/toaster/toastergui/templates/base.html b/bitbake/lib/toaster/toastergui/templates/base.html
index 8a9f690309..58491eba81 100644
--- a/bitbake/lib/toaster/toastergui/templates/base.html
+++ b/bitbake/lib/toaster/toastergui/templates/base.html
@@ -22,6 +22,8 @@
22 </script> 22 </script>
23 <script src="{% static 'js/typeahead.jquery.js' %}"> 23 <script src="{% static 'js/typeahead.jquery.js' %}">
24 </script> 24 </script>
25 <script src="{% static 'js/jsrender.min.js' %}">
26 </script>
25 <script src="{% static 'js/prettify.js' %}"> 27 <script src="{% static 'js/prettify.js' %}">
26 </script> 28 </script>
27 <script src="{% static 'js/libtoaster.js' %}"> 29 <script src="{% static 'js/libtoaster.js' %}">
@@ -32,6 +34,8 @@
32 </script> 34 </script>
33 {% endif %} 35 {% endif %}
34 <script> 36 <script>
37 $.views.settings.delimiters("<%", "%>");
38
35 libtoaster.ctx = { 39 libtoaster.ctx = {
36 jsUrl : "{% static 'js/' %}", 40 jsUrl : "{% static 'js/' %}",
37 htmlUrl : "{% static 'html/' %}", 41 htmlUrl : "{% static 'html/' %}",
@@ -48,7 +52,9 @@
48 xhrCustomRecipeUrl : "{% url 'xhr_customrecipe' %}", 52 xhrCustomRecipeUrl : "{% url 'xhr_customrecipe' %}",
49 projectId : {{project.id}}, 53 projectId : {{project.id}},
50 xhrBuildRequestUrl: "{% url 'xhr_buildrequest' project.id %}", 54 xhrBuildRequestUrl: "{% url 'xhr_buildrequest' project.id %}",
55 mostRecentBuildsUrl: "{% url 'most_recent_builds' %}?project_id={{project.id}}",
51 {% else %} 56 {% else %}
57 mostRecentBuildsUrl: "{% url 'most_recent_builds' %}",
52 projectId : undefined, 58 projectId : undefined,
53 projectPageUrl : undefined, 59 projectPageUrl : undefined,
54 projectName : undefined, 60 projectName : undefined,
diff --git a/bitbake/lib/toaster/toastergui/templates/buildrequestdetails.html b/bitbake/lib/toaster/toastergui/templates/buildrequestdetails.html
deleted file mode 100644
index c782074448..0000000000
--- a/bitbake/lib/toaster/toastergui/templates/buildrequestdetails.html
+++ /dev/null
@@ -1,64 +0,0 @@
1{% extends "baseprojectpage.html" %}
2
3{% load static %}
4{% load projecttags %}
5{% load humanize %}
6
7
8{% block projectinfomain %}
9 <!-- begin content -->
10
11 <div class="row">
12
13 <!-- end left sidebar container -->
14 <!-- Begin right container -->
15 <div class="col-md-10">
16 <div class="page-header">
17 <h1>
18 <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>
19
20 </h1>
21 </div>
22 <div class="alert alert-error">
23 <p class="lead">
24 <strong>Failed</strong>
25 on {{ buildrequest.updated|date:'d/m/y H:i' }}
26 with
27
28 <i class="icon-minus-sign error" style="margin-left:6px;"></i>
29 <strong><a class="error accordion-toggle toggle-errors" href="#errors">
30 {{buildrequest.brerror_set.all.count}} error{{buildrequest.brerror_set.all.count|pluralize}}
31 </a></strong>
32 <span class="pull-right">Build time: {{buildrequest.get_duration|sectohms}}</span>
33 </p>
34 </div>
35
36 <div class="accordion" id="errors">
37 <div class="accordion-group">
38 <div class="accordion-heading">
39 <a class="accordion-toggle error toggle-errors">
40 <h2>
41 <i class="icon-minus-sign"></i>
42 {{buildrequest.brerror_set.all.count}} error{{buildrequest.brerror_set.all.count|pluralize}}
43 </h2>
44 </a>
45 </div>
46 <div class="accordion-body collapse in" id="collapse-errors">
47 <div class="accordion-inner">
48 <div class="col-md-10">
49 {% for error in buildrequest.brerror_set.all %}
50 <div class="alert alert-error">
51 ERROR: <div class="air well"><pre>{{error.errmsg}}</pre></div>
52 </div>
53 {% endfor %}
54 </div>
55 </div>
56 </div>
57
58 </div>
59 </div>
60 </div>
61 </div> <!-- end of row -->
62
63
64{%endblock%}
diff --git a/bitbake/lib/toaster/toastergui/templates/mrb_section.html b/bitbake/lib/toaster/toastergui/templates/mrb_section.html
index b164269a13..302b4b0da4 100644
--- a/bitbake/lib/toaster/toastergui/templates/mrb_section.html
+++ b/bitbake/lib/toaster/toastergui/templates/mrb_section.html
@@ -1,26 +1,9 @@
1{% load static %} 1{% load static %}
2{% load projecttags %}
3{% load project_url_tag %}
4{% load humanize %} 2{% load humanize %}
3{% load project_url_tag %}
5<script src="{% static 'js/mrbsection.js' %}"></script> 4<script src="{% static 'js/mrbsection.js' %}"></script>
6 5
7<script>
8 $(document).ready(function () {
9 var ctx = {
10 mrbType : "{{mrb_type}}",
11 }
12
13 try {
14 mrbSectionInit(ctx);
15 } catch (e) {
16 document.write("Sorry, An error has occurred loading this page");
17 console.warn(e);
18 }
19 });
20</script>
21
22{% if mru %} 6{% if mru %}
23
24 {% if mrb_type == 'project' %} 7 {% if mrb_type == 'project' %}
25 <h2> 8 <h2>
26 Latest project builds 9 Latest project builds
@@ -38,6 +21,7 @@
38 <div id="latest-builds"> 21 <div id="latest-builds">
39 {% for build in mru %} 22 {% for build in mru %}
40 <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 %}"> 23 <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 %}">
24 <!-- project title -->
41 {% if mrb_type != 'project' %} 25 {% if mrb_type != 'project' %}
42 <div class="row project-name"> 26 <div class="row project-name">
43 <div class="col-md-12"> 27 <div class="col-md-12">
@@ -48,134 +32,180 @@
48 </div> 32 </div>
49 {% endif %} 33 {% endif %}
50 34
51 <div class="row"> 35 <div class="row" data-role="build-status-container">
52 <div class="col-md-3"> 36 <div class="col-md-12">
53 {% if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %} 37 Loading...
54 <a href="{% url 'builddashboard' build.pk %}" class="alert-link">
55 {% endif %}
56
57 {% if build.target_set.all.count > 0 %}
58 <span data-toggle="tooltip"
59 {% if build.target_set.all.count > 1 %}
60 {{build.get_sorted_target_list.0.target}}
61 title="Recipes:
62 {% for target in build.get_sorted_target_list %}
63 {% if target.task %}
64 {{target.target}}:{{target.task}}
65 {% else %}
66 {{target.target}}
67 {% endif %}
68 {% endfor %}"
69 {% endif %}
70 >
71 {% if build.target_set.all.0.task %}
72 {{build.get_sorted_target_list.0.target}}:{{build.target_set.all.0.task}}
73 {% else %}
74 {{build.get_sorted_target_list.0.target}}
75 {% endif %}
76
77 {% if build.target_set.all.count > 1 %}
78 (+{{build.target_set.all.count|add:"-1"}})
79 {% endif %}
80 </span>
81 {% endif %}
82 {% if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %}
83 </a>
84 {% endif %}
85 </div> 38 </div>
39 </div>
40 </div>
41 {% endfor %}
42 </div>
43{% endif %}
44
45<!-- build main template -->
46<script id="build-template" type="text/x-jsrender">
47 <div class="col-md-3">
48 <!-- only show link for completed builds -->
49 <%if state == 'Succeeded' || state == 'Failed'%>
50 <a class="alert-link" href="<%:dashboard_url%>">
51 <span data-toggle="tooltip" data-role="targets-text" title="Recipes: <%:targets%>">
52 <%:targets_abbreviated%>
53 </span>
54 </a>
55 <%else%>
56 <span data-toggle="tooltip" data-role="targets-text" title="Recipes: <%:targets%>">
57 <%:targets_abbreviated%>
58 </span>
59 <%/if%>
60 </div>
86 61
87 {% if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %} 62 <%if state == 'Queued'%>
88 <div class="col-md-2"> 63 <%include tmpl='#queued-build-template'/%>
89 {% if build.completed_on|format_build_date %} 64 <%else state == 'Succeeded' || state == 'Failed'%>
90 {{build.completed_on|date:'d/m/y H:i'}} 65 <%include tmpl='#succeeded-or-failed-build-template'/%>
91 {% else %} 66 <%else state == 'Cancelling'%>
92 {{ build.completed_on|date:'H:i' }} 67 <%include tmpl='#cancelling-build-template'/%>
93 {% endif %} 68 <%else state == 'In Progress'%>
94 </div> 69 <%include tmpl='#in-progress-build-template'/%>
95 {% endif %} 70 <%else state == 'Cancelled'%>
96 71 <%include tmpl='#cancelled-build-template'/%>
97 {% if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %} 72 <%/if%>
98 <div class="col-md-2"> 73</script>
99 {% if build.errors.count %}
100 <span class="glyphicon glyphicon-minus-sign"></span>
101 <a href="{%url 'builddashboard' build.pk%}#errors" class="alert-link">
102 {{build.errors.count}} error{{build.errors.count|pluralize}}
103 </a>
104 {% endif %}
105 </div>
106 74
107 <div class="col-md-2"> 75<!-- queued build -->
108 {% if build.warnings.count %} 76<script id="queued-build-template" type="text/x-jsrender">
109 <span class="glyphicon glyphicon-warning-sign build-warnings"></span> 77 <div class="col-md-5">
110 <a href="{%url 'builddashboard' build.pk%}#warnings" class="alert-link build-warnings"> 78 Build queued
111 {{build.warnings.count}} warning{{build.warnings.count|pluralize}} 79 </div>
112 </a>
113 {% endif %}
114 </div>
115 80
116 <div class="col-md-3"> 81 <div class="col-md-4">
117 Build time: <a class="alert-link" href="{% url 'buildtime' build.pk %}">{{ build.timespent_seconds|sectohms }} 82 <%if is_default_project_build%>
118 </a> 83 <!-- no cancel icon -->
119 84 <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>
120 {% if build.project.is_default %} 85 <%else%>
121 <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 %}" 86 <!-- cancel button -->
122 title="Builds in this project cannot be started from Toaster: they are started from the command line"> 87 <span class="cancel-build-btn pull-right alert-link"
123 </span> 88 data-buildrequest-id="<%:id%>" data-request-url="<%:cancel_url%>">
124 {% else %} 89 <span class="glyphicon glyphicon-remove-circle"></span>
125 <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" 90 Cancel
126 data-request-url="{% url 'xhr_buildrequest' build.project.pk %}" 91 </span>
127 data-target='{{build.target_set.all|get_tasks|json}}'> 92 <%/if%>
128 <span class="glyphicon glyphicon-repeat"></span> 93 </div>
129 Rebuild 94</script>
130 </a>
131 {% endif %}
132 </div>
133 {% endif %}
134 95
135 {% if build.outcome == build.IN_PROGRESS %} 96<!-- in progress build -->
136 <div class="col-md-4" style="display:none" id="cancelling-msg-{{build.buildrequest.pk}}"> 97<script id="in-progress-build-template" type="text/x-jsrender">
137 Cancelling the build ... 98 <!-- progress bar and task completion percentage -->
138 </div> 99 <div data-role="build-status" class="col-md-4 col-md-offset-1 progress-info">
100 <!-- progress bar -->
101 <div class="progress" id="build-pc-done-title-<%:id%>">
102 <div id="build-pc-done-bar-<%:id%>"
103 style="width: <%:tasks_complete_percentage%>%;"
104 class="progress-bar">
105 </div>
106 </div>
107 </div>
139 108
140 <div class="col-md-4 col-md-offset-1 progress-info"> 109 <div class="col-md-4 progress-info">
141 <div class="progress" id="build-pc-done-title-{{build.pk}}"> 110 <!-- task completion percentage -->
142 <div id="build-pc-done-bar-{{build.pk}}" style="width: {{build.completeper}}%;" class="progress-bar"> 111 <span id="build-pc-done-<%:id%>"><%:tasks_complete_percentage%></span>% of
143 </div> 112 tasks complete
144 </div> 113 <%if is_default_project_build%>
145 </div> 114 <!-- no cancel icon -->
115 <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>
116 <%else%>
117 <!-- cancel button -->
118 <span class="cancel-build-btn pull-right alert-link"
119 data-buildrequest-id="<%:id%>" data-request-url="<%:cancel_url%>">
120 <span class="glyphicon glyphicon-remove-circle"></span>
121 Cancel
122 </span>
123 <%/if%>
124 </div>
125</script>
146 126
147 <div class="col-md-4 progress-info"> 127<!-- cancelling build -->
148 <span id="build-pc-done-{{build.pk}}">{{build.completeper}}</span>% of tasks complete 128<script id="cancelling-build-template" type="text/x-jsrender">
149 {# No build cancel for command line builds project #} 129 <div class="col-md-9">
150 {% if build.project.is_default %} 130 Cancelling the build ...
151 <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> 131 </div>
152 {% else %} 132</script>
153 <a href="#" class="cancel-build-btn pull-right alert-link"
154 data-buildrequest-id={{build.buildrequest.pk}}
155 data-request-url="{% url 'xhr_buildrequest' build.project.pk %}">
156 <span class="glyphicon glyphicon-remove-circle"></span>
157 Cancel
158 </a>
159 {% endif %}
160 </div>
161 {% endif %} {# end if in progress #}
162 133
163 {% if build.outcome == build.CANCELLED %} 134<!-- succeeded or failed build -->
164 <div class="col-md-6"> 135<script id="succeeded-or-failed-build-template" type="text/x-jsrender">
165 Build cancelled 136 <!-- completed_on -->
166 </div> 137 <div class="col-md-2">
138 <%:completed_on%>
139 </div>
167 140
168 <div class="col-md-3"> 141 <!-- errors -->
169 <a href="#" class="info pull-right run-again-btn alert-link" 142 <div class="col-md-2">
170 data-request-url="{% url 'xhr_buildrequest' build.project.pk %}" 143 <%if errors%>
171 data-target='{{build.target_set.all|get_tasks|json}}'> 144 <span class="glyphicon glyphicon-minus-sign"></span>
172 <span class="glyphicon glyphicon-repeat"></span> 145 <a href="<%:dashboard_errors_url%>" class="alert-link">
173 Rebuild 146 <%:errors%> error<%:errors_pluralize%>
174 </a> 147 </a>
175 </div> 148 <%/if%>
176 {% endif %} 149 </div>
177 </div> 150
178 </div> 151 <!-- warnings -->
179 {% endfor %} 152 <div class="col-md-2">
153 <%if warnings%>
154 <span class="glyphicon glyphicon-minus-sign"></span>
155 <a href="<%:dashboard_warnings_url%>" class="alert-link">
156 <%:warnings%> warning<%:warnings_pluralize%>
157 </a>
158 <%/if%>
159 </div>
160
161 <!-- build time -->
162 <div class="col-md-3">
163 Build time: <a class="alert-link" href="<%:buildtime_url%>"><%:buildtime%></a>
164
165 <%if is_default_project_build%>
166 <!-- info icon -->
167 <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%>"
168 title="Builds in this project cannot be started from Toaster: they are started from the command line">
169 </span>
170 <%else%>
171 <!-- rebuild button -->
172 <span class="rebuild-btn alert-link <%if state == 'Success'%>success<%else state == 'Failed'%>danger<%else%>info<%/if%> pull-right"
173 data-request-url="<%:rebuild_url%>" data-target='<%:build_targets_json%>'>
174 <span class="glyphicon glyphicon-repeat"></span>
175 Rebuild
176 </span>
177 <%/if%>
178 </div>
179</script>
180
181<!-- cancelled build -->
182<script id="cancelled-build-template" type="text/x-jsrender">
183 <!-- build cancelled message -->
184 <div class="col-md-6">
185 Build cancelled
180 </div> 186 </div>
181{% endif %} \ No newline at end of file 187
188 <!-- rebuild button -->
189 <div class="col-md-3">
190 <span class="info pull-right rebuild-btn alert-link"
191 data-request-url="<%:rebuild_url%>" data-target='<%:build_targets_json%>'>
192 <span class="glyphicon glyphicon-repeat"></span>
193 Rebuild
194 </span>
195 </div>
196</script>
197
198<script>
199 $(document).ready(function () {
200 var ctx = {
201 mrbType : "{{mrb_type}}",
202 }
203
204 try {
205 mrbSectionInit(ctx);
206 } catch (e) {
207 document.write("Sorry, An error has occurred loading this page");
208 console.warn(e);
209 }
210 });
211</script>
diff --git a/bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html b/bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html
index 1fe76a7a24..a5fed2dd41 100644
--- a/bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html
+++ b/bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html
@@ -24,7 +24,7 @@
24 24
25 <h2 class="top-air" data-role="page-title"></h2> 25 <h2 class="top-air" data-role="page-title"></h2>
26 26
27 {% if not build_in_progress_none_completed %} 27 {% if not build_in_progress_none_completed %}
28 {% url 'projectbuilds' project.id as xhr_table_url %} 28 {% url 'projectbuilds' project.id as xhr_table_url %}
29 {% include 'toastertable.html' %} 29 {% include 'toastertable.html' %}
30 {% endif %} 30 {% endif %}
diff --git a/bitbake/lib/toaster/toastergui/templatetags/projecttags.py b/bitbake/lib/toaster/toastergui/templatetags/projecttags.py
index dc75f229bb..b170a16165 100644
--- a/bitbake/lib/toaster/toastergui/templatetags/projecttags.py
+++ b/bitbake/lib/toaster/toastergui/templatetags/projecttags.py
@@ -271,14 +271,6 @@ def get_dict_value(dictionary, key):
271 return '' 271 return ''
272 272
273@register.filter 273@register.filter
274def format_build_date(completed_on):
275 now = timezone.now()
276 delta = now - completed_on
277
278 if delta.days >= 1:
279 return True
280
281@register.filter
282def is_shaid(text): 274def is_shaid(text):
283 """ return True if text length is 40 characters and all hex-digits 275 """ return True if text length is 40 characters and all hex-digits
284 """ 276 """
diff --git a/bitbake/lib/toaster/toastergui/urls.py b/bitbake/lib/toaster/toastergui/urls.py
index 1c0ccbb2e1..9892d2ab93 100644
--- a/bitbake/lib/toaster/toastergui/urls.py
+++ b/bitbake/lib/toaster/toastergui/urls.py
@@ -214,6 +214,9 @@ urlpatterns = patterns('toastergui.views',
214 api.XhrBuildRequest.as_view(), 214 api.XhrBuildRequest.as_view(),
215 name='xhr_buildrequest'), 215 name='xhr_buildrequest'),
216 216
217 url(r'^mostrecentbuilds$', api.MostRecentBuildsView.as_view(),
218 name='most_recent_builds'),
219
217 # default redirection 220 # default redirection
218 url(r'^$', RedirectView.as_view(url='landing', permanent=True)), 221 url(r'^$', RedirectView.as_view(url='landing', permanent=True)),
219) 222)